Skip to content

Instantly share code, notes, and snippets.

@Joshix-1
Last active July 8, 2022 21:23
Show Gist options
  • Save Joshix-1/bc37a3f0a6b5c53e70e8002e7103c77c to your computer and use it in GitHub Desktop.
Save Joshix-1/bc37a3f0a6b5c53e70e8002e7103c77c to your computer and use it in GitHub Desktop.
Just a fun thing I made
#!/usr/bin/env python3
import random
import re
import sys
from collections.abc import Iterator
from enum import IntFlag
from string import ascii_letters, digits
class Direction(IntFlag):
NONE = 0
TOP = 1
RIGHT = 2
BOTTOM = 4
LEFT = 8
SINGLE_DIRECTIONS = {Direction.TOP, Direction.RIGHT, Direction.BOTTOM, Direction.LEFT}
def get_opposite_direction(direction) -> Direction:
if direction == direction.TOP:
return direction.BOTTOM
if direction == direction.RIGHT:
return direction.LEFT
if direction == direction.BOTTOM:
return direction.TOP
if direction == direction.LEFT:
return direction.RIGHT
return Direction.NONE
Row = list[None | Direction]
Grid = list[Row]
CHARACTERS = { # uncomment lines to allow more chars
# Direction.NONE: " ",
# Direction.TOP: "╵",
# Direction.RIGHT: "╶",
# Direction.BOTTOM: "╷",
# Direction.LEFT: "╴",
Direction.TOP | Direction.RIGHT: "└",
# Direction.TOP | Direction.BOTTOM: "│",
Direction.TOP | Direction.LEFT: "┘",
Direction.RIGHT | Direction.BOTTOM: "┌",
# Direction.RIGHT | Direction.LEFT: "─",
Direction.BOTTOM | Direction.LEFT: "┐",
Direction.TOP | Direction.RIGHT | Direction.BOTTOM: "├",
Direction.TOP | Direction.RIGHT | Direction.LEFT: "┴",
Direction.TOP | Direction.BOTTOM | Direction.LEFT: "┤",
Direction.RIGHT | Direction.BOTTOM | Direction.LEFT: "┬",
# Direction.TOP | Direction.RIGHT | Direction.BOTTOM | Direction.LEFT: "┼",
}
def generate_neighbors(
x: int, y: int, width: int, height: int
) -> dict[Direction, tuple[int, int]]:
neighbors: dict[Direction, tuple[int, int]] = {}
if x > 0:
neighbors[Direction.LEFT] = (x - 1, y)
if x < width - 1:
neighbors[Direction.RIGHT] = (x + 1, y)
if y > 0:
neighbors[Direction.TOP] = (x, y - 1)
if y < height - 1:
neighbors[Direction.BOTTOM] = (x, y + 1)
return neighbors
def coord_iterator(
width: int, height: int, grid: Grid, border: bool
) -> Iterator[tuple[int, int]]:
def entropy(_x: int, _y: int) -> int:
valid_items = get_valid_items(grid, width, height, _x, _y, border=border)
return len(valid_items) if valid_items else 0
coords: dict[tuple[int, int], int] = {}
for x in range(width):
for y in range(height):
coords[(x, y)] = entropy(x, y)
current: tuple[int, int]
while coords:
sorted_ = sorted((ent, key) for key, ent in coords.items() if ent)
if not sorted_:
return
current = sorted_.pop(0)[1]
del coords[current]
yield current
for neighbor in generate_neighbors(
*current, width=width, height=height
).values():
if neighbor in coords:
coords[neighbor] = entropy(*neighbor)
def get_valid_items(
grid: Grid, width: int, height: int, x: int, y: int, border: bool
) -> tuple[Direction, ...] | None:
connected = set()
unconnected = set()
neighbors = generate_neighbors(x, y, width, height)
for dir_, (nx, ny) in neighbors.items():
if (neighbor := grid[ny][nx]) is None:
continue
if get_opposite_direction(dir_) in neighbor:
connected.add(dir_)
else:
unconnected.add(dir_)
if border and len(neighbors) < len(SINGLE_DIRECTIONS):
unconnected |= SINGLE_DIRECTIONS ^ set(neighbors.keys())
if not connected and not unconnected:
return tuple(CHARACTERS.keys())
possible = [
key
for key in CHARACTERS
if (
(not unconnected or not any(spam in key for spam in unconnected))
and (not connected or all(eggs in key for eggs in connected))
)
]
if not possible:
return None # bad
return tuple(possible)
def row_as_str(row: Row) -> str:
return "".join(("X" if dir_ is None else CHARACTERS[dir_]) for dir_ in row)
def grid_as_str(grid: Grid) -> str:
return "\n".join(row_as_str(row) for row in grid)
def fix_missing(grid: Grid, width: int, height: int, border: bool) -> bool:
change_counter = 0
while any(None in row for row in grid):
for y, row in enumerate(grid):
while None in row:
if change_counter > width * height:
return False
x = row.index(None)
grid[y][x] = random.choice(tuple(CHARACTERS.keys()))
neighbors = generate_neighbors(x, y, width, height)
for nx, ny in neighbors.values():
grid[ny][nx] = random.choice(
get_valid_items(grid, width, height, nx, ny, border) or (None,)
)
change_field(grid, width, height, x, y, border)
change_counter += 1
return True
def change_field(
grid: Grid, width: int, height: int, x: int, y: int, border: bool
) -> bool:
valid_items = get_valid_items(grid, width, height, x, y, border)
if valid_items:
grid[y][x] = random.choice(valid_items)
return bool(valid_items)
def create_grid(width: int, height: int, border: bool) -> Grid:
grid: Grid = [[None] * width for _ in range(height)]
for x, y in coord_iterator(width, height, grid, border):
valid_items = get_valid_items(grid, width, height, x, y, border)
if valid_items:
grid[y][x] = random.choice(valid_items)
fix_missing(grid, width, height, border) # sometimes necessary
return grid
def main() -> int | str:
width, height = 20, 5
border, print_command = False, False
args = sys.argv[1:]
if "--border" in args:
args.remove("--border")
border = True
if "--print-command" in args:
args.remove("--print-command")
print_command = True
seed: None | str = None
if args:
if "--help" in args or "-h" in args or "?" == args[0]:
print(
f"Usage: {sys.argv[0]} WIDTHxHEIGHT? --border? --print-command? seed..."
)
return 0
if re.fullmatch(r"^\d+x\d+$", args[0]):
width, height = [max(3, int(spam)) for spam in args[0].split("x")]
seed = " ".join(args[1:])
else:
seed = " ".join(args)
if seed:
if print_command:
print(" ".join(sys.argv))
else:
seed = "".join([random.choice(ascii_letters + digits) for _ in range(12)])
if print_command:
print(" ".join(sys.argv + [seed]))
random.seed(seed)
grid = create_grid(width, height, border)
print(grid_as_str(grid))
return 0
if __name__ == "__main__":
sys.exit(main())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment