from typing import Self from hashlib import md5 ADJ = [(x, y) for x in range(-1, 2) for y in range(-1, 2) if x != 0 or y != 0] ORTH = [dir for dir in ADJ if dir[0] == 0 or dir[1] == 0] VALID_LABEL = set("abcdefghijklmnopqrstuv.") VALID_LOCK = set("0123.") VALID_ANSWER = set("0123") def value(grid, x: int, y: int) -> int | None: side_length = int(len(grid) ** 0.5) if not 0 <= x < side_length: return None if not 0 <= y < side_length: return None return grid[y * side_length + x] def adj(grid, x: int, y: int, orth=False, center=False) -> list[int]: base = [value(grid, x + dx, y + dy) for dx, dy in (ORTH if orth else ADJ)] if center: base.append(value(grid, x, y)) return [v for v in base if v is not None] def across(grid, x: int, y: int) -> bool: side_length = int(len(grid) ** 0.5) cell = value(grid, x, y) for nx in range(0, side_length): if value(grid, nx, y) != cell: break else: return True for ny in range(0, side_length): if value(grid, x, ny) != cell: break else: return True return False class Shape: def __init__(self, grid, x: int, y: int) -> None: self.value = value(grid, x, y) proc = [(x, y)] seen = set(proc) while proc: x, y = proc.pop() for dx, dy in ORTH: possible = (x + dx, y + dy) if value(grid, possible[0], possible[1]) == self.value: if possible not in seen: seen.add(possible) proc.append(possible) self.size = len(seen) left = min([pt[0] for pt in seen]) right = max([pt[0] for pt in seen]) top = min([pt[1] for pt in seen]) bottom = max([pt[1] for pt in seen]) output = [] for y in range(top, bottom + 1): row = [] for x in range(left, right + 1): row.append("X" if (x, y) in seen else ".") output.append("".join(row)) self.shape = "\n".join(output) self.points = seen class Captcha: def __init__(self, size: int) -> None: tile_count = size**2 self.size = size self.label: list[str | None] = [None] * tile_count self.lock: list[int | None] = [None] * tile_count self.reload = False def __members(self) -> tuple[int, list[str | None], list[int | None]]: return (self.size, self.label, self.lock) def __eq__(self, other) -> bool: if not isinstance(other, Captcha): return False return self.__members() == other.__members() def __hash__(self) -> int: return hash(self.__members()) @classmethod def from_string( cls, size: int, label: str, lock: str, reload: bool = False ) -> Self: result = cls(size) result.label = [char if char != "." else None for char in label] result.lock = [int(char) if char != "." else None for char in lock] result.reload = reload result.recalc_id() return result def make_label(self) -> str: return "".join(char if char else "." for char in self.label) def make_lock(self) -> str: return "".join(str(char) if char is not None else "." for char in self.lock) def recalc_id(self) -> None: self.id = md5( (self.make_label() + self.make_lock()).encode("utf-8") ).hexdigest() def validate(self, answer_string: str) -> bool: # Ensure answer is of right length answer = [int(char) for char in answer_string] if len(answer) != len(self.label): return False # Ensure lock is correct for ans, lk in zip(answer, self.lock): if lk is not None: if ans != lk: return False # Ensure label is correct color_match = None shape_match_m = None shape_match_n = None for x in range(0, self.size): for y in range(0, self.size): match value(self.label, x, y): case "a": if Shape(answer, x, y).size != 1: return False case "b": if Shape(answer, x, y).size != 2: return False case "c": if Shape(answer, x, y).size != 3: return False case "d": if Shape(answer, x, y).size != 4: return False case "e": near = adj(answer, x, y, True, False) if len(near) != len(set(near)): return False case "l": if not across(answer, x, y): return False case "m": shape = Shape(answer, x, y) if shape_match_m: if shape_match_m != shape.shape: return False else: shape_match_m = shape.shape if shape_match_m == shape_match_n: return False case "n": shape = Shape(answer, x, y) if shape_match_n: if shape_match_n != shape.shape: return False else: shape_match_n = shape.shape if shape_match_m == shape_match_n: return False case "r": color = value(answer, x, y) if color_match: if color_match != color: return False else: color_match = color case "v": if len(set(adj(answer, x, y, True, True))) != 1: return False case _: pass return True @classmethod def safe_validate( cls, size: str, label: str, lock: str, answer: str ) -> Captcha | None: try: int_size = int(size) except ValueError: return None tile_count = int_size**2 if ( len(label) != tile_count or len(lock) != tile_count or len(answer) != tile_count or any(char not in VALID_LABEL for char in label) or any(char not in VALID_LOCK for char in lock) or any(char not in VALID_ANSWER for char in answer) ): return None captcha = Captcha.from_string(int_size, label, lock) if captcha.validate(answer): return captcha return None if __name__ == "__main__": test = Captcha.from_string( 5, "m...cena....m...ebneb.clm", "........................." ) print(test.validate("0111001010203101231212212"))