pigeon-magnet-solver/pigeon_magnet_solver/board.py

119 lines
2.9 KiB
Python

from dataclasses import dataclass
from typing import Self
from .helpers import rotate_r
@dataclass(kw_only=True, slots=True)
class Board:
rows: list[str]
buttons: tuple[int, ...]
@classmethod
@property
def default_puzzle(cls) -> Self:
return cls(
rows=["011", "011", "100", "x1x"],
buttons=(3, 4, 5, 7),
)
@property
def height(self) -> int:
return len(self.rows)
@property
def width(self) -> int:
if not self.rows:
return 0
else:
return len(self.rows[0])
def __post_init__(self) -> None:
# check char set
chars = {c for c in "".join(self.rows)}
assert chars.issubset({"0", "1", "x"}), "Invalid char set!"
# check defined width
assert all(
len(row) == self.width
for row in self.rows
), "Inconsistent width!"
# check buttons
assert all(
button in range(self.height * self.width)
for button in self.buttons
), "Invalid buttons!"
def __str__(self) -> str:
return "\n".join(self.rows) \
.replace("0", "⚪️") \
.replace("1", "🟢️") \
.replace("x", "⚫️")
@property
def columns(self) -> tuple[str, ...]:
return tuple(
"".join(row[x] for row in self.rows)
for x in range(self.width)
)
@property
def binary_repr(self) -> tuple[int, ...]:
return tuple(
int(column.replace("x", "0")[::-1], base=2)
for column in self.columns
)
@property
def solution_class(self) -> tuple[int, ...]:
return tuple(
column.count("1")
for column in self.columns
)
def __getitem__(self, key: tuple[int, int]) -> str:
row, col = key
if row not in range(self.height) or col not in range(self.width):
return "x"
return self.rows[row][col]
def __setitem__(self, key: tuple[int, int], value: str):
row, col = key
if row not in range(self.height) or col not in range(self.width):
return
old_row = self.rows[row]
self.rows[row] = old_row[:col] + value + old_row[col+1:]
def click(self, index: int) -> Self | None:
# check is clickable
row, col = index // self.width, index % self.width
if self.rows[row][col] != "1":
# button is not clickable
return None
# get neighbor positions
nb_rc = [
(row - 1, col),
(row, col + 1),
(row + 1, col),
(row, col - 1),
]
# rotate neighbors
nb_rot = rotate_r([self[rc] for rc in nb_rc])
# create new state
result = self.__class__(
rows=self.rows.copy(),
buttons=self.buttons,
)
for rc, val in zip(nb_rc, nb_rot):
result[rc] = val
return result