Skip to content

Instantly share code, notes, and snippets.

@isidentical
Created March 18, 2020 08:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save isidentical/bd113f813b047c9f1be0e8b5682b41a0 to your computer and use it in GitHub Desktop.
Save isidentical/bd113f813b047c9f1be0e8b5682b41a0 to your computer and use it in GitHub Desktop.
import random
import shutil
from collections import UserList, deque
from dataclasses import dataclass, field
from enum import Enum, auto
from typing import Dict, Tuple
Blocks = Enum("Blocks", "AIR GOLD COAL LAPIS EMERALD")
BOOST_AMOUNT = 0.1
MARGIN = 4
MOVEMENT = {"w": (0, -1), "s": (0, 1), "a": (-1, 0), "d": (1, 0)}
@dataclass
class Probability:
probability: float
minimum_height: int
cluster_size_range: Tuple[int, int]
@dataclass
class ProbabilityBoost:
block: Blocks
boost: float
PROBABILITIES = {
Blocks.COAL: Probability(0.50, 1, (5, 10)),
Blocks.GOLD: Probability(0.1, 1, (2, 5)),
Blocks.LAPIS: Probability(0.25, 1, (3, 7)),
Blocks.EMERALD: Probability(0.05, 5, (1, 1)),
}
COLOR_CODES = {
Blocks.AIR: "7;37;40",
Blocks.GOLD: "7;33;40",
Blocks.LAPIS: "7;34;40",
Blocks.COAL: "7;30;40",
Blocks.EMERALD: "7;32;40",
}
def color_of(item):
if isinstance(item, Blocks):
return COLOR_CODES[item], " "
elif hasattr(item, "color"):
return getattr(item, "color"), type(item).__name__[0]
def clear():
print(chr(27) + "[2J")
@dataclass
class Player:
x_pos: int
y_pos: int
color: str
inventory: Dict[Blocks, int] = field(
default_factory=lambda: dict.fromkeys(Blocks.__members__.values(), 0)
)
def inventory_preview(self):
return " --- ".join(
f"{item.name}:{value}"
for item, value in self.inventory.items()
if value > 0
if item is not Blocks.AIR
)
class Terrain(UserList):
def __init__(self, x, y):
self.x_max = x
self.y_max = y
self.data = [[Blocks.AIR] * x for _ in range(y)]
self._fill_terrain()
def preview(self):
buffer = ""
for row in self.data:
for column in row:
color, value = color_of(column)
buffer += f"\x1b[{color}m"
buffer += value
buffer += "\x1b[0m"
buffer += "\n"
return buffer
def calculate_cluster_size(self, block_type, x_level, y_level):
def get_neighbors(x_level, y_level):
neighbors = []
if x_level != 0:
neighbors.append((x_level - 1, y_level))
if x_level < self.x_max - 1:
neighbors.append((x_level + 1, y_level))
if y_level != 0:
neighbors.append((x_level, y_level - 1))
if y_level < self.y_max - 1:
neighbors.append((x_level, y_level + 1))
for x_level, y_level in neighbors:
yield ((x_level, y_level), self.data[y_level][x_level])
cluster_size = 0
seen_coords = set((x_level, y_level))
queue = [((x_level, y_level), block_type)]
while queue:
found_coords, found_type = queue.pop()
if found_type is block_type:
cluster_size += 1
else:
continue
for neighbor_coords, neighbor_type in get_neighbors(*found_coords):
if neighbor_type is not block_type:
continue
if neighbor_coords in seen_coords:
continue
seen_coords.add(neighbor_coords)
queue.append((neighbor_coords, neighbor_type))
return cluster_size
def generate_random_block(self, y_level, probability_boost=None):
seed = random.uniform(0, 1)
last_probability = 0
for block, probability in PROBABILITIES.items():
block_probability = probability.probability
if (
probability_boost is not None
and probability_boost.block is block
):
block_probability *= probability_boost.boost
if y_level <= probability.minimum_height:
continue
if (
last_probability
<= seed
<= last_probability + block_probability
):
return (block, probability)
else:
last_probability += block_probability
else:
return (Blocks.AIR, None)
def _fill_terrain(self):
probability_boost = None
def find_block_for_pos(x_level, y_level):
nonlocal probability_boost
block, probability = self.generate_random_block(
y_level, probability_boost=probability_boost
)
probability_boost = None
if probability is not None:
cluster_size = self.calculate_cluster_size(
block, x_level, y_level
)
missing_cluster_size = (
probability.cluster_size_range[0] ** 2 - cluster_size
)
if cluster_size > probability.cluster_size_range[1] ** 2:
return find_block_for_pos(x_level, y_level)
if missing_cluster_size < 0:
probability_boost = ProbabilityBoost(
block, missing_cluster_size * BOOST_AMOUNT
)
return block
for y_level, row in enumerate(self.data):
for x_level in range(len(row)):
row[x_level] = find_block_for_pos(x_level, y_level)
class MineMore:
def __init__(self):
x_max, y_max = shutil.get_terminal_size((80, 20))
y_max -= MARGIN
self.map = Terrain(x_max, y_max)
self.player = Player(0, 0, "5;37;41")
self.draw_player()
def start(self):
action = errors = None
while action != "q":
clear()
print(self.map.preview())
print(self.player.inventory_preview())
if errors:
print(errors)
action = input(">>> ")
if action == "":
continue
if action in "wasd":
errors = self.check_and_move(*MOVEMENT[action])
def check_and_move(self, x_amount, y_amount):
if (
self.player.x_pos + x_amount >= 0
and self.player.x_pos + x_amount < self.map.x_max
and self.player.y_pos + y_amount >= 0
and self.player.y_pos + y_amount < self.map.y_max
):
old_value = self.move_player(x_amount, y_amount)
self.player.inventory[old_value] += 1
else:
return "Can't move to that position"
def move_player(self, x_amount, y_amount):
self.map[self.player.y_pos][self.player.x_pos] = Blocks.AIR
self.player.x_pos += x_amount
self.player.y_pos += y_amount
return self.draw_player()
def draw_player(self):
old_value = self.map[self.player.y_pos][self.player.x_pos]
self.map[self.player.y_pos][self.player.x_pos] = self.player
return old_value
if __name__ == "__main__":
game = MineMore()
game.start()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment