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