From dfe2069c1dd4eb14d44a57d35d76afdc47d1598e Mon Sep 17 00:00:00 2001 From: Morgana Date: Sat, 3 Oct 2026 10:25:51 -0500 Subject: Initial captcha code --- captcha.py | 193 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 captcha.py (limited to 'captcha.py') diff --git a/captcha.py b/captcha.py new file mode 100644 index 0000000..e5fe28c --- /dev/null +++ b/captcha.py @@ -0,0 +1,193 @@ +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] + + +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 "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")) -- cgit v1.3.1