119 lines
2.9 KiB
Python
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
|