This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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