From 0f9bfbd77590838da7707a1aa144f0f62db80e1e Mon Sep 17 00:00:00 2001 From: YoEnte Date: Tue, 1 Sep 2026 08:14:13 +0200 Subject: [PATCH 1/4] first full blokus implementation --- .gitignore | 5 +- Cargo.toml | 3 +- README.md | 10 +- logic.py | 15 +- pyproject.toml | 2 +- python/socha/_socha.pyi | 1107 ++++++++--------- python/socha/api/networking/utils.py | 272 ++-- .../api/networking/xml_protocol_interface.py | 26 +- python/socha/api/protocol/protocol.py | 284 ++++- src/lib.rs | 52 +- src/plugin2026.rs | 8 - src/plugin2027.rs | 11 + src/plugin2027/board.rs | 117 ++ src/plugin2027/color.rs | 70 ++ src/plugin2027/errors.rs | 69 + src/plugin2027/field.rs | 42 + src/plugin2027/field_content.rs | 50 + src/plugin2027/game_state.rs | 178 +++ src/plugin2027/move.rs | 51 + src/plugin2027/piece.rs | 110 ++ src/plugin2027/piece_shape.rs | 163 +++ src/plugin2027/rotation.rs | 49 + src/plugin2027/utils.rs | 7 + src/plugin2027/utils/constants.rs | 13 + src/plugin2027/utils/coordinate.rs | 74 ++ src/plugin2027/utils/coordinate_helper.rs | 73 ++ src/plugin2027/utils/direction.rs | 107 ++ src/plugin2027/utils/game_rule_logic.rs | 366 ++++++ src/plugin2027/utils/team.rs | 32 + src/plugin2027/utils/vector.rs | 66 + 30 files changed, 2614 insertions(+), 818 deletions(-) delete mode 100644 src/plugin2026.rs create mode 100644 src/plugin2027.rs create mode 100644 src/plugin2027/board.rs create mode 100644 src/plugin2027/color.rs create mode 100644 src/plugin2027/errors.rs create mode 100644 src/plugin2027/field.rs create mode 100644 src/plugin2027/field_content.rs create mode 100644 src/plugin2027/game_state.rs create mode 100644 src/plugin2027/move.rs create mode 100644 src/plugin2027/piece.rs create mode 100644 src/plugin2027/piece_shape.rs create mode 100644 src/plugin2027/rotation.rs create mode 100644 src/plugin2027/utils.rs create mode 100644 src/plugin2027/utils/constants.rs create mode 100644 src/plugin2027/utils/coordinate.rs create mode 100644 src/plugin2027/utils/coordinate_helper.rs create mode 100644 src/plugin2027/utils/direction.rs create mode 100644 src/plugin2027/utils/game_rule_logic.rs create mode 100644 src/plugin2027/utils/team.rs create mode 100644 src/plugin2027/utils/vector.rs diff --git a/.gitignore b/.gitignore index 311c305..31cd58b 100644 --- a/.gitignore +++ b/.gitignore @@ -71,4 +71,7 @@ docs/_build/ .vscode/ # Pyenv -.python-version \ No newline at end of file +.python-version + +# custom venv names +venv*/ \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index f94922b..e7104e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "_socha" -version = "4.3.9" +version = "5.0.0" edition = "2021" [lib] @@ -12,6 +12,7 @@ pyo3 = { version = "0.21.2" } pyo3-log = "0.10.0" log = "0.4.20" itertools = "0.13.0" +rand = "0.10.2" [features] extension-module = ["pyo3/extension-module"] diff --git a/README.md b/README.md index 711c0ee..c7c9802 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

Software-Challenge Logo

-# Python-Client für die Software-Challenge Germany 2025 +# Python-Client für die Software-Challenge Germany 2027 [![Read the Docs](https://img.shields.io/readthedocs/socha-python-client?label=Docs)](https://socha-python-client.readthedocs.io/de/latest/) [![PyPI](https://img.shields.io/pypi/v/socha?label=PyPi)](https://pypi.org/project/socha/) @@ -12,11 +12,11 @@ Dieses Repository enthält das Python-Paket für die [Software-Challenge Germany](https://www.software-challenge.de), einem Programmierwettbewerb für Schülerinnen und Schüler. Dabei muss eine künstliche Intelligenz entwickelt werden, die in einem jährlich wechselnden Spiel gegen andere Gegner antritt. -> In diesem Jahr ist es das Spiel **[Piranhas](https://docs.software-challenge.de/spiele/26_piranhas/)**. +> In diesem Jahr ist es das Spiel **[Blokus](https://docs.software-challenge.de/spiele/27_blokus/regeln)**. ## Inhaltsverzeichnis -- [Python-Client für die Software-Challenge Germany 2025](#python-client-für-die-software-challenge-germany-2025) +- [Python-Client für die Software-Challenge Germany 2027](#python-client-für-die-software-challenge-germany-2027) - [Inhaltsverzeichnis](#inhaltsverzeichnis) - [Installation](#installation) - [Global](#global) @@ -118,6 +118,10 @@ if __name__ == "__main__": Starter(Logik()) ``` +**ACHTUNG** Die aktuelle ReadtheDocs Dokumentation ist nicht aktuell und leider Fehlerhaft. Die wichtigen Startschritte findetet ihr wie gehabt in dieser ReadMe. + +Eine Liste an Klassen und Methoden findet sich hier [`_socha.pyi`](https://github.com/software-challenge/player_python/blob/master/python/socha/_socha.pyi). + > Ein komplettes Beispiel ist in dieser [`logic.py`](https://github.com/maxblan/socha-python-client/blob/master/logic.py) zu finden. ### Startargumente diff --git a/logic.py b/logic.py index aff38b0..307a69c 100644 --- a/logic.py +++ b/logic.py @@ -6,15 +6,20 @@ import time from typing import Optional, Tuple from socha import ( + Move, + GameState, + Color, + Piece, + PieceShape, + Rotation, Coordinate, Vector, + GameRuleLogic, + Board, Direction, - FieldType, TeamEnum, - Board, - Move, - GameState, - RulesEngine, + Field, + FieldContent, ) from socha.api.networking.game_client import IClientHandler from socha.starter import Starter diff --git a/pyproject.toml b/pyproject.toml index 8b5ff5d..3234d2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "socha" -version = "4.3.9" +version = "5.0.0" authors = [ { name = "yoente", email = "stu250140@mail.uni-kiel.de" }, { name = "maxblan", email = "stu222782@mail.uni-kiel.de" }, diff --git a/python/socha/_socha.pyi b/python/socha/_socha.pyi index b105b30..7c5ac26 100644 --- a/python/socha/_socha.pyi +++ b/python/socha/_socha.pyi @@ -1,838 +1,753 @@ from enum import Enum -from typing import List, Optional +from typing import Dict, List, Optional, Set, Tuple -class Coordinate: +class Vector: """ - Eine 2 dimensionale Koordinate auf einem Spielfeld. + Ein 2 dimensionaler Vektor. Attributes: - x (int): Der x-Wert. - y (int): Der y-Wert. + delta_x (int): Die Entfernung in x-Richtung. + delta_y (int): Die Entfernung in y-Richtung. """ - x: int - y: int + delta_x: int + delta_y: int - def __init__(self, x: int, y: int) -> None: ... + def __init__(self, delta_x: int, delta_y: int) -> None: ... def __str__(self) -> str: ... def __repr__(self) -> str: ... - def __eq__(self, other: Coordinate) -> bool: - """ - Unterstützt den Vergleichsoperator ==, um die Werte mit denen eines - weiteren Objektes zuvergleichen. + def __eq__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... - Args: - other: (Coordinate): Die andere Koordinate. - - Returns: - bool: Das Ergebnis des Vergleichs. - """ + def deepcopy(self) -> Vector: + """Kopiert das Objekt.""" ... - def __ne__(self, other: Coordinate) -> bool: - """ - Unterstützt den Vergleichsoperator !=, um die Werte mit denen eines - weiteren Objektes zuvergleichen. - - Args: - other: (Coordinate): Die andere Koordinate. - Returns: - bool: Das Ergebnis des Vergleichs. - """ + def add_vector(self, other: Vector) -> Vector: + """Addiert einen anderen Vector (nicht mutierend).""" ... - def deepcopy(self) -> Coordinate: - """ - Kopiert das Objekt rekursiv. - """ + def add_vector_mut(self, other: Vector) -> None: + """Addiert einen anderen Vector (mutierend).""" ... - def add_vector(self, vector: Vector) -> Coordinate: - """ - Addiert einen Vector auf die Werte dieser Koordinate (**nicht mutierend**). - - Args: - vector (Vector): Der Vektor. - - Returns: - Coordinate: Ein neues Koordinatenobjekt mit den berechneten Werten. - """ + def scale(self, scalar: int) -> Vector: + """Skaliert diesen Vektor (nicht mutierend).""" ... - def add_vector_mut(self, vector: Vector) -> None: - """ - Addiert einen Vector auf die Werte dieser Koordinate (**mutierend**). - - Args: - vector (Vector): Der Vektor. - """ + def scale_mut(self, scalar: int) -> None: + """Skaliert diesen Vektor (mutierend).""" ... - def get_difference(self, other: Coordinate) -> Vector: - """ - Berechnet die Differenz zwischen zwei Koordinaten Punkten als Vektor. - - Args: - other (Coordinate): Die andere Koordinate - - Returns: - Vector: Der Vektor zwischen den Punkten - """ + def get_length(self) -> Optional[float]: + """Berechnet die Länge dieses Vektors.""" ... -class Vector: + +class Coordinate: """ - Ein 2 dimensionaler Vektor. + Eine 2 dimensionale Koordinate auf einem Spielfeld. Attributes: - delta_x (int): Die Entfernung in x-Richtung. - delta_y (int): Die Entfernung in y-Richtung. + x (int): Der x-Wert. + y (int): Der y-Wert. """ - delta_x: int - delta_y: int + x: int + y: int - def __init__(self, delta_x: int, delta_y: int) -> None: ... + def __init__(self, x: int, y: int) -> None: ... def __str__(self) -> str: ... def __repr__(self) -> str: ... - def __eq__(self, other: Vector) -> bool: - """ - Unterstützt den Vergleichsoperator ==, um die Werte mit denen eines - weiteren Objektes zuvergleichen. - - Args: - other: (Vector): Die andere Koordinate. + def __eq__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... - Returns: - bool: Das Ergebnis des Vergleichs. - """ + def deepcopy(self) -> Coordinate: + """Kopiert das Objekt.""" ... - def __ne__(self, other: Vector) -> bool: - """ - Unterstützt den Vergleichsoperator !=, um die Werte mit denen eines - weiteren Objektes zuvergleichen. - Args: - other: (Vector): Die andere Koordinate. + def add_vector(self, vector: Vector) -> Coordinate: + """Addiert einen Vector auf diese Koordinate (nicht mutierend).""" + ... - Returns: - bool: Das Ergebnis des Vergleichs. - """ + def add_vector_mut(self, vector: Vector) -> None: + """Addiert einen Vector auf diese Koordinate (mutierend).""" ... - - def deepcopy(self) -> GameState: - """ - Kopiert das Objekt rekursiv. - """ + + def get_difference(self, other: Coordinate) -> Vector: + """Berechnet die Differenz zwischen zwei Koordinaten als Vektor.""" ... - def add_vector(self, other: Vector) -> Vector: - """ - Addiert einen anderen Vector auf die Werte dieses Vektors (**nicht mutierend**). + def neighbors(self) -> List[Coordinate]: + """Gibt die vier benachbarten Feldkoordinaten zurück.""" + ... - Args: - other (Vector): Der andere Vektor. + def diagonal_neighbors(self) -> List[Coordinate]: + """Gibt die vier angrenzenden Ecken der Feldkoordinaten zurück.""" + ... - Returns: - Vector: Ein neues Vektorobjekt mit den berechneten Werten. - """ + def as_vector(self) -> Vector: + """Coordinate als Vektor Objekt""" ... - def add_vector_mut(self, other: Vector) -> None: - """ - Addiert einen anderen Vector auf die Werte dieses Vektors (**mutierend**). - Args: - other (Vector): Der andere Vektor. - """ - ... +class Direction(Enum): + """Eine Darstellung für eine normierte Richtung.""" - def scale(self, scalar: int) -> Vector: - """ - Skaliert diesen Vektor um ein gegebenes Skalar (**nicht mutierend**). + Up = 0 + UpRight = 1 + Right = 2 + DownRight = 3 + Down = 4 + DownLeft = 5 + Left = 6 + UpLeft = 7 - Args: - scalar (int): Das Skalar. + def __str__(self) -> str: ... + def __repr__(self) -> str: ... + def __eq__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... + def deepcopy(self) -> Direction: ... - Returns: - Vector: Ein neues Vektorobjekt mit den berechneten Werten. - """ + @staticmethod + def from_vector(vector: Vector) -> Optional[Direction]: + """Wandelt einen Vektor in eine der 8 Richtungen um.""" ... - def scale_mut(self, scalar: int) -> None: - """ - Skaliert diesen Vektor um ein gegebenes Skalar (**mutierend**). + @staticmethod + def all_directions() -> List[Direction]: + """Gibt eine Liste aller 8 Richtungen zurück.""" + ... - Args: - scalar (int): Das Skalar. - """ + @staticmethod + def cardinals() -> List[Direction]: + """Gibt die vier nicht-diagonalen Richtungen zurück (Up, Right, Down, Left).""" ... - def get_length(self) -> float: - """ - Berechnet die Länge dieses Vektors. + @staticmethod + def diagonals() -> List[Direction]: + """Gibt die vier diagonalen Richtungen zurück.""" + ... - Returns: - float: Die Länge des Vektors (in 32 bit Präzision). - """ + def to_vector(self) -> Vector: + """Wandelt die Richtung in den entsprechenden Vektor um.""" ... -class Direction(Enum): - """ - Eine Darstellung für eine normierte Richtung.
- Kann in einen Vektor konvertiert werden. - """ - - Up: int = 0 - """ - Richtung nach oben, entspricht Vektor(0, 1). - """ - UpRight: int = 1 - """ - Richtung nach oben-rechts, entspricht Vektor(1, 1). - """ - Right: int = 2 - """ - Richtung nach rechts, entspricht Vektor(1, 0). - """ - DownRight: int = 3 - """ - Richtung nach unten-rechts, entspricht Vektor(1, -1). - """ - Down: int = 4 - """ - Richtung nach unten, entspricht Vektor(0, -1). - """ - DownLeft: int = 5 - """ - Richtung nach unten-links, entspricht Vektor(-1, -1). - """ - Left: int = 6 - """ - Richtung nach links, entspricht Vektor(-1, 0). - """ - UpLeft: int = 7 - """ - Richtung nach oben-links, entspricht Vektor(-1, 1). - """ + def to_mirrored(self) -> Direction: + """Spiegelt die gegebene Richtung.""" + ... + + +class TeamEnum(Enum): + """Eine Darstellung für die beiden Teams.""" + + One = 0 + Two = 1 def __str__(self) -> str: ... def __repr__(self) -> str: ... - def __eq__(self, other: Direction) -> bool: - """ - Unterstützt den Vergleichsoperator ==, um die Werte mit denen eines - weiteren Objektes zuvergleichen. + def __eq__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... - Args: - other: (Direction): Die andere Koordinate. - - Returns: - bool: Das Ergebnis des Vergleichs. - """ + def opponent(self) -> TeamEnum: + """Gibt den Gegner dieses Teams zurück.""" ... - def __ne__(self, other: Direction) -> bool: - """ - Unterstützt den Vergleichsoperator !=, um die Werte mit denen eines - weiteren Objektes zuvergleichen. - Args: - other: (Direction): Die andere Koordinate. - Returns: - bool: Das Ergebnis des Vergleichs. - """ - ... +class Color(Enum): + """Die Farbe eines Spielsteins / Teams.""" - @staticmethod - def from_vector(vector: Vector) -> Optional[Direction]: - """ - Wandelt einen Vektor in eine der 8 Richtungen um, insofern der Vektor exakt der Richtung entspricht. + BLUE = 0 + YELLOW = 1 + RED = 2 + GREEN = 3 - Args: - vector (Vector): Der Vektor, der konvertiert werden soll. + def __str__(self) -> str: ... + def __repr__(self) -> str: ... + def __eq__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... + def __hash__(self) -> int: ... - Returns: - Optional[Direction]: Die Richtung oder None, wenn der Vektor nicht direkt übersetzt werden kann. - """ - ... - - @staticmethod - def all_directions() -> List[Direction]: - """ - Gibt eine Liste aller 8 Richtungen aus.
- Der erste Wert ist oben und alle weiteren folgen im Uhrzeigersinn. - - Returns: - List[Direction]: Die Liste der Richtungen. - """ + def next(self) -> Color: + """Gibt die nächste Farbe in der Zugreihenfolge zurück.""" ... - def to_vector(self) -> Vector: - """ - Wandelt die Richtung in den entsprechenden Vektor um. + def team(self) -> TeamEnum: + """Gibt das Team zurück, zu dem diese Farbe gehört.""" + ... - Returns: - Vector: Der Richtungsvektor. - """ + def to_field_content(self) -> FieldContent: + """Wandelt die Farbe in den entsprechenden Feldinhalt um.""" ... - def to_mirrored(self) -> Direction: - """ - Spiegelt die gegebene Richtung.
- Beispiel: Up -> Down. + def name(self) -> str: - Returns: - Direction: Die neue Richtung. - """ ... -class FieldType(Enum): - """ - Stellt alle verfügbaren Feldtypen dar. - """ +class FieldContent(Enum): + """Der Inhalt eines Feldes: eine Farbe oder leer.""" - OneS: int = 0 - """ - Der kleine Fisch von Spieler 1. - """ - OneM: int = 1 - """ - Der mittlere Fisch von Spieler 1. - """ - OneL: int = 2 - """ - Der große Fisch von Spieler 1. - """ - TwoS: int = 3 - """ - Der kleine Fisch von Spieler 2. - """ - TwoM: int = 4 - """ - Der mittlere Fisch von Spieler 2. - """ - TwoL: int = 5 - """ - Der große Fisch von Spieler 2. - """ - Squid: int = 6 - """ - Der Kraken. - """ - Empty: int = 7 - """ - Alle anderen unbesetzten Felder. - """ + BLUE = 0 + YELLOW = 1 + RED = 2 + GREEN = 3 + EMPTY = 4 def __str__(self) -> str: ... def __repr__(self) -> str: ... - def __eq__(self, other: FieldType) -> bool: - """ - Unterstützt den Vergleichsoperator ==, um die Werte mit denen eines - weiteren Objektes zuvergleichen. + def __eq__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... - Args: - other: (FieldType): Die andere Koordinate. + def to_team_color(self) -> Optional[Color]: + """Wandelt den Feldinhalt in die entsprechende Farbe um, oder None, wenn leer.""" + ... - Returns: - bool: Das Ergebnis des Vergleichs. - """ + def is_empty(self) -> bool: + """Gibt zurück, ob das Feld leer ist.""" ... - def __ne__(self, other: FieldType) -> bool: - """ - Unterstützt den Vergleichsoperator !=, um die Werte mit denen eines - weiteren Objektes zuvergleichen. - Args: - other: (FieldType): Die andere Koordinate. - Returns: - bool: Das Ergebnis des Vergleichs. - """ - ... +class Field: + """Ein einzelnes Feld auf dem Spielbrett.""" - def get_value(self) -> int: - """ - Gibt die entsprechende Wertigkeit eines Feldes zurück.
- Für Fische je nachdem 1-3, für die anderen Felder 0. - - Returns: - int: Der Wert des Feldes. - """ - ... + coordinate: Coordinate + content: FieldContent - def get_team(self) -> Optional[TeamEnum]: - """ - Gibt für ein Fischfeld aus, zu welchem Team dieser Fisch gehört. + def __init__(self, coordinate: Coordinate, color: Color) -> None: ... + def __str__(self) -> str: ... + def __repr__(self) -> str: ... + def __eq__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... - Returns: - Optional[TeamEnum]: Das Team, zudem der Fisch gehört, oder None, wenn das Feld kein Fisch ist. - """ + def deepcopy(self) -> Field: + """Kopiert das Objekt.""" ... - @staticmethod - def all_field_types() -> List[FieldType]: - """ - Gibt eine Liste aller Feldtypen aus.
- - Returns: - List[Direction]: Die Liste der Richtungen. - """ + def is_empty(self) -> bool: + """Gibt zurück, ob das Feld leer ist.""" ... -class TeamEnum(Enum): - """ - Eine Darstellung für die beiden Teams - """ - One = 0 - """ - Team 1 - """ - Two = 1 +class Board: """ - Team 2 + Das Spielbrett. + + Attributes: + map (List[List[Field]]): Die 2-dimensionale Liste der Felder. """ + map: List[List[Field]] + + def __init__(self, map: Optional[List[List[Field]]]) -> None: ... def __str__(self) -> str: ... def __repr__(self) -> str: ... - def __eq__(self, other: TeamEnum) -> bool: - """ - Unterstützt den Vergleichsoperator ==, um die Werte mit denen eines - weiteren Objektes zuvergleichen. + def __eq__(self, other: object) -> bool: ... - Args: - other: (TeamEnum): Die andere Koordinate. + def get(self, position: Coordinate) -> Field: + """ + Gibt das Feld an der gegebenen Koordinate zurück. - Returns: - bool: Das Ergebnis des Vergleichs. + Raises: + IndexError: Wenn die Koordinate außerhalb des Spielfelds liegt. """ ... - def __ne__(self, other: TeamEnum) -> bool: - """ - Unterstützt den Vergleichsoperator !=, um die Werte mit denen eines - weiteren Objektes zuvergleichen. - Args: - other: (TeamEnum): Die andere Koordinate. + def set_content(self, position: Coordinate, content: FieldContent) -> None: + """Setzt den Inhalt eines Feldes.""" + ... - Returns: - bool: Das Ergebnis des Vergleichs. - """ + def get_content(self, position: Coordinate) -> Optional[FieldContent]: + """Gibt den Inhalt eines Feldes zurück, oder None, wenn außerhalb des Feldes.""" ... - def get_fish_types(self) -> List[FieldType]: - """ - Gibt eine Liste aller Fischtypen des Teams aus. + def is_empty(self) -> bool: + """Prüft, ob alle Felder leer sind.""" + ... - Returns: - List[FieldType]: Die Liste der Feldtypen. - """ + def is_obstructed(self, position: Coordinate) -> bool: + """Prüft, ob auf dieser Position bereits eine Spielerfarbe liegt.""" ... - def opponent(self) -> TeamEnum: - """ - Gibt den Gegner dieses Teams an. + def get_team(self, position: Coordinate) -> Optional[Color]: + """Gibt das Team zurück, das auf dem Feld liegt, oder None.""" + ... - Return: - TeamEnum: Das Gegnerteam. - """ + def pretty_string(self) -> str: + """Gibt eine lesbare String-Darstellung des Spielfelds zurück.""" ... -class Board: - """ - Ein Spielbrett, das die Felder des Spiels enthält. + def compare(self, other: Board) -> List[Field]: + """Vergleicht dieses Board mit einem anderen und gibt die unterschiedlichen Felder zurück.""" + ... - Das Feld unten-links hat Koordinate (0, 0) und das Feld oben-rechts ist an Position (9, 9).
- Gleichzeitig bedeutet das, dass map[0] auch die unterste Zeile des Spielfeldes ist. + @staticmethod + def random_fields() -> List[List[Field]]: + """Erstellt ein leeres Spielfeld.""" + ... - Attributes: - map (List[List[Field]]): Die 2 dimensionale Liste der Felder, die das Spielbrett darstellen.
- """ + @staticmethod + def contains(position: Coordinate) -> bool: + """Prüft, ob die Koordinate innerhalb der Grenzen des Spielfelds liegt.""" + ... - map: List[List[FieldType]] - def __init__(self, map: List[List[FieldType]]) -> None: ... +class Rotation(Enum): + """Beschreibt, wie weit eine PieceShape gedreht werden soll.""" + + NONE = 0 + RIGHT = 1 + MIRROR = 2 + LEFT = 3 + def __str__(self) -> str: ... def __repr__(self) -> str: ... - - def __eq__(self, other: TeamEnum) -> bool: - """ - Unterstützt den Vergleichsoperator ==, um die Werte mit denen eines - weiteren Objektes zuvergleichen. + def __eq__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... - Args: - other: (TeamEnum): Die andere Koordinate. - - Returns: - bool: Das Ergebnis des Vergleichs. - """ + def value(self) -> int: + """Gibt den numerischen Wert (Anzahl Vierteldrehungen) zurück.""" ... - def __ne__(self, other: TeamEnum) -> bool: - """ - Unterstützt den Vergleichsoperator !=, um die Werte mit denen eines - weiteren Objektes zuvergleichen. - - Args: - other: (TeamEnum): Die andere Koordinate. - Returns: - bool: Das Ergebnis des Vergleichs. - """ - ... - - def deepcopy(self) -> GameState: - """ - Kopiert das Objekt rekursiv. - """ + def rotate(self, other: Rotation) -> Rotation: + """Summiert beide Rotationen auf.""" ... - def get_field(self, position: Coordinate) -> Optional[FieldType]: - """ - Gibt das Feld an der gegebenen Koordinate zurück. + @staticmethod + def all() -> List[Rotation]: + """Gibt alle vier Rotationen zurück.""" + ... + + def name(self) -> str: + ... + + +class PieceShape(Enum): + """Eine Enumeration aller 21 verschiedenen Formen.""" + + Mono = 0 + Domino = 1 + TrioL = 2 + TrioI = 3 + TetroO = 4 + TetroT = 5 + TetroI = 6 + TetroL = 7 + TetroZ = 8 + PentoL = 9 + PentoT = 10 + PentoV = 11 + PentoS = 12 + PentoZ = 13 + PentoI = 14 + PentoP = 15 + PentoW = 16 + PentoU = 17 + PentoR = 18 + PentoX = 19 + PentoY = 20 - Args: - position (Coordinate): Die Position des Feldes, das abgerufen werden soll. + def __str__(self) -> str: ... + def __repr__(self) -> str: ... + def __eq__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... - Returns: - Field: Das Feld an der gegebenen Koordinate, oder None, wenn außerhalb des gültigen Bereichs. - """ + @staticmethod + def all() -> List[PieceShape]: + """Gibt alle 21 Formen in Reihenfolge zurück.""" ... - def get_fields_by_type(self, field: FieldType) -> List[Coordinate]: - """ - Gibt eine Liste aller Koordinaten zurück, auf dem der angegebene Feld-Typ zu finden ist. + @staticmethod + def from_index(index: int) -> Optional[PieceShape]: + """Gibt die Form anhand ihres Index zurück, oder None.""" + ... - Args: - field (FieldType): Der Feld-Typ, nachdem gesucht werden soll. + def coordinates(self) -> Set[Coordinate]: + """Die normalisierten Koordinaten der Grundform.""" + ... - Returns: - List[Coordinate]: Die Liste der Koordinaten. - """ + def dimension(self) -> Vector: + """Das kleinstmögliche Rechteck, das die Form umfasst.""" ... - def get_fields_in_direction(self, position: Coordinate, direction: Direction) -> List[FieldType]: - """ - Gibt eine Liste aller Feld-Typen zurück, die in einer Richtung liegen.
- Dabei wird als Ausgangspunkt eine Koordinate genommen und dazu die Richtung, in die aufgelistet werden soll. + def as_vectors(self) -> Set[Vector]: + """Die Form als Menge von Vektoren relativ zu (0,0).""" + ... - **Achtung**: Das Feld der Ausgangskoordinate wird *nicht* beachtet und ausgegeben.
- Wenn die Startkoordinate nicht im Spielfeld liegt, wird eine leere Liste zurückgegeben. + def size(self) -> int: + """Die Anzahl der Felder, die diese Form belegt.""" + ... - Args: - position (Coordinate): Die Ausgangskoordinate. - direction (Direction): Die Richtung. + def variants(self) -> List[Tuple[Set[Coordinate], Rotation, bool]]: + """ + Alle eindeutigen Varianten der Form (Rotation + Spiegelung), ohne Duplikate. Returns: - List[FieldType]: Die Liste der Felder. + Eine Liste von (Koordinatenmenge, Rotation, ist_gespiegelt)-Tupeln. """ + ... - def get_fields_on_line(self, position: Coordinate, direction: Direction) -> List[FieldType]: - """ - Gibt eine Liste aller Feld-Typen zurück, die auf einer Gerade liegen.
- Die Gerade wird aufgespannt durch eine Koordinate und einen Richtungsvektor. + def transform(self, rotation: Rotation, should_flip: bool) -> Set[Coordinate]: + """Transformiert die Form entsprechend Rotation und Spiegelung.""" + ... - Das Ergebnis wird in Richtung des "Vektorpfeils" abgelesen.
- Wenn die Startkoordinate nicht im Spielfeld liegt, wird eine leere Liste zurückgegeben. + def name(self) -> str: + ... - Args: - position (Coordinate): Die Startkoordinate für die Gerade. - direction (Direction): Der aufspannende Richtungsvektor. +class Piece: + """Ein Spielstein mit Farbe, Position und Transformation.""" - Returns: - List[FieldType]: Die Liste der Felder. - """ - ... + color: Color + kind: PieceShape + rotation: Rotation + is_flipped: bool + position: Coordinate - def get_fish_on_line(self, position: Coordinate, direction: Direction) -> List[FieldType]: - """ - Funktioniert ähnlich wie *Board.get_fields_on_line()*, - gibt aber nur die Feldtypen, die Fische sind, auf einer Geraden als Liste aus. + def __init__( + self, + color: Color, + kind: PieceShape, + rotation: Rotation, + is_flipped: bool, + position: Coordinate, + ) -> None: ... + def __str__(self) -> str: ... + def __repr__(self) -> str: ... + def __eq__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... + def __hash__(self) -> int: ... - Args: - position (Coordinate): Die Startkoordinate für die Gerade. - direction (Direction): Der aufspannende Richtungsvektor. + def shape(self) -> Set[Coordinate]: + """Die normalisierte Form des Steins (gedreht/gespiegelt, nicht verschoben).""" + ... - Returns: - List[FieldType]: Die Liste der Fisch-Felder. - """ + def coordinates(self) -> Set[Coordinate]: + """Die tatsächlichen Koordinaten, die der Stein auf dem Feld einnimmt.""" ... -class Move: - """ - Repräsentiert einen Zug im Spiel. + def transform(self, rotation: Rotation, is_flipped: bool) -> Piece: + """Dreht/spiegelt den Stein, Position bleibt gleich.""" + ... - Attribute: - start (Coordinate): Die Koordinate, von wo aus ein Fisch bewegt werden soll. - direction (Direction): Die Richtung, in die der Fisch schwimmt. - """ - start: Coordinate - direction: Direction +class Move: + """Repräsentiert einen Zug im Spiel: entweder ein SetMove oder ein SkipMove.""" - def __init__(self, start: Coordinate, direction: Direction) -> None: ... def __str__(self) -> str: ... def __repr__(self) -> str: ... - - def __eq__(self, other: Move) -> bool: - """ - Unterstützt den Vergleichsoperator ==, um die Werte mit denen eines - weiteren Objektes zuvergleichen. - - Args: - other: (Move): Die andere Koordinate. + def __eq__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... - Returns: - bool: Das Ergebnis des Vergleichs. - """ + @staticmethod + def set_move(piece: Piece) -> Move: + """Erstellt einen Zug, der den gegebenen Stein platziert.""" ... - def __ne__(self, other: Move) -> bool: - """ - Unterstützt den Vergleichsoperator !=, um die Werte mit denen eines - weiteren Objektes zuvergleichen. - Args: - other: (Move): Die andere Koordinate. + @staticmethod + def skip_move(color: Color) -> Move: + """Erstellt einen Zug, der die aktuelle Runde für die gegebene Farbe aussetzt.""" + ... - Returns: - bool: Das Ergebnis des Vergleichs. - """ + def get_color(self) -> Color: + """Die Farbe, die diesen Zug getätigt hat.""" ... - - def deepcopy(self) -> GameState: - """ - Kopiert das Objekt rekursiv. - """ + + def as_piece(self) -> Optional[Piece]: + """Gibt das verwendete Piece aus, wenn vorhanden""" ... + class GameState: """ - Repräsentiert einen Spielstand. + Repräsentiert den aktuellen Spielstand. Attribute: - board (Board): Das Spielbrett. - turn (int): Die aktuelle Runde. - last_move (Optional[Move]): Der zuletzt ausgeführte Zug. + turn (int): Die Anzahl der bereits getätigten Züge. + last_move (Optional[Move]): Der zuletzt gespielte Zug. + board (Board): Das aktuelle Spielfeld. + start_piece (PieceShape): Der Spielstein, der im ersten Zug gesetzt werden muss. + last_move_mono (Dict[Color, bool]): Ob das Monomino zuletzt für jede Farbe gelegt wurde. """ - board: Board turn: int last_move: Optional[Move] - - def __init__(self, board: Board, turn: int, last_move: Optional[Move]) -> None: ... + board: Board + start_piece: PieceShape + last_move_mono: Dict[Color, bool] + + def __init__( + self, + turn: int = 0, + last_move: Optional[Move] = None, + board: Optional[Board] = None, + start_piece: PieceShape = PieceShape.Mono, + last_move_mono: Optional[Dict[Color, bool]] = None, + ) -> None: ... def __str__(self) -> str: ... def __repr__(self) -> str: ... - def __eq__(self, other: GameState) -> bool: - """ - Unterstützt den Vergleichsoperator ==, um die Werte mit denen eines - weiteren Objektes zuvergleichen. + def __eq__(self, other: object) -> bool: ... - Args: - other: (GameState): Die andere Koordinate. + def round(self) -> int: + """Die aktuelle Rundenzahl.""" + ... - Returns: - bool: Das Ergebnis des Vergleichs. - """ + def undeployed_piece_shapes(self, color: Color) -> List[PieceShape]: + """Gibt die noch nicht gesetzten Formen der gegebenen Farbe zurück.""" ... - def __ne__(self, other: GameState) -> bool: - """ - Unterstützt den Vergleichsoperator !=, um die Werte mit denen eines - weiteren Objektes zuvergleichen. - Args: - other: (GameState): Die andere Koordinate. + def remove_undeployed_piece(self, color: Color, shape: PieceShape) -> bool: + """Entfernt eine Form aus der Liste der noch nicht gesetzten Steine.""" + ... - Returns: - bool: Das Ergebnis des Vergleichs. - """ + def current_color(self) -> Color: + """Die Farbe, die aktuell am Zug ist.""" ... - def deepcopy(self) -> GameState: - """ - Kopiert das Objekt rekursiv. - """ + def has_valid_colors(self) -> bool: + """Gibt zurück, ob noch Farben im Spiel sind.""" ... - def set_board_field(self, position: Coordinate, field: FieldType) -> None: - """ - Ändert ein Feld auf dem Spielfeld an einer Koordinate. + def is_valid_color(self, color: Color) -> bool: + """Prüft, ob die gegebene Farbe noch im Spiel ist.""" + ... - Args: - position (Coordinate): Die Position des Feldes, das geändert werden soll. - field (FieldType): Das Feld, was dort platziert werden soll. - """ + def remove_active_color(self) -> bool: + """Entfernt die aktuell aktive Farbe aus dem Spiel und rückt vor.""" ... - def possible_moves_for(self, start: Coordinate) -> List[Move]: - """ - Berechnet alle Züge, die aus der aktuellen Spielposition für den Fisch an der Koordinate möglich sind. - - Args: - start (Coordinate): Die Position des gewählten Fisch. + def advance(self, turns: int = 1) -> bool: + """Geht zum Zug der nächsten gültigen Farbe über.""" + ... - Returns: - List[Move]: Die Liste der Züge. - """ + def is_over(self) -> bool: + """Gibt zurück, ob das Spiel vorbei ist.""" ... def possible_moves(self) -> List[Move]: """ - Berechnet alle Züge, die aus der aktuellen Spielposition für den aktuellen Spieler möglich sind. - - Returns: - List[Move]: Die Liste der Züge. + Berechnet alle sinnvollen Züge der aktuellen Farbe. + Enthält einen SkipMove nur, wenn kein anderer Zug möglich ist. """ ... - def perform_move(self, move: Move) -> GameState: + def get_points_for_color(self, color: Color) -> int: + """Berechnet die Punkteanzahl für die gegebene Farbe.""" + ... + + def get_points_for_team(self, team: TeamEnum) -> int: + """Berechnet die Punkteanzahl für das gegebene Team (Summe der Farben des Teams).""" + ... + + def win_condition(self) -> Optional[TeamEnum]: """ - Führt den gegebenen Zug auf dem Spielstand aus, insofern dieser ausführbar ist (**nicht mutierend**). - Dabei wird *kein* Zug an den Spielserver übermittelt. + Gibt das Gewinnerteam zurück, oder None bei einem Unentschieden. + """ + ... - Args: - move_ (Move): Der zuverwendene Zug. - Returns: - Gamestate: Der neue Spielstand. +class GameRuleLogic: + """Eine Sammlung an statischen Methoden, die die Spielregeln logisch umsetzen.""" - Raises: - PiranhasError: Wenn der Zug nicht valide ist. - """ + @staticmethod + def get_points_from_undeployed(undeployed: List[PieceShape], mono_last: bool = False) -> int: + """Berechnet den Punktestand anhand der gegebenen, nicht gelegten Formen.""" ... - def perform_move_mut(self, move: Move) -> None: + @staticmethod + def perform_move(game_state: GameState, move: Move) -> None: """ - Führt den gegebenen Zug auf dem Spielstand aus, insofern dieser ausführbar ist (**mutierend**). - Dabei wird *kein* Zug an den Spielserver übermittelt. - - Args: - move_ (Move): Der zuverwendene Zug. + Führt den Zug im GameState aus (mutierend). Raises: - PiranhasError: Wenn der Zug nicht valide ist. + Eine der Blokus-Move-Mistake-Exceptions, wenn der Zug nicht valide ist. """ ... -class RulesEngine: - """ - Stellt Methoden, die zur Überprüfung der Spielregeln dienen. - """ - @staticmethod - def move_distance(board: Board, move_: Move) -> int: + def validate_move_color(game_state: GameState, move: Move) -> None: """ - Gibt die Länge / Anzahl der Felder von einem Zug auf dem Spielfeld zurück. - - Args: - board (Board): Das Spielfeld, auf dem die Länge berechnet werden soll. - move_ (Move): Der zuverwendene Zug. + Prüft, ob die Farbe des Zuges der aktiven Farbe entspricht. - Returns: - int: Die Länge. + Raises: + WrongColor: Wenn die Farbe nicht am Zug ist. """ ... @staticmethod - def target_position(board: Board, move_: Move) -> Coordinate: + def validate_set_move(game_state: GameState, piece: Piece) -> None: """ - Gibt die Koordinate zurück, auf der ein Fisch landen würde, wenn man den Zug ausführt. - - Es wird nicht berücksichtigt, ob diese Koordinate im Spielfeld ist. - - Args: - board (Board): Das Spielfeld, auf dem der Zug berechnet werden soll. - move_ (Move): Der zuverwendene Zug. + Prüft, ob der gegebene Stein gesetzt werden könnte. - Returns: - Coordinate: Die Koordinate. + Raises: + Eine der Blokus-Move-Mistake-Exceptions, wenn der Zug nicht valide ist. """ ... @staticmethod - def is_in_bounds(coordinate: Coordinate) -> bool: - """ - Gibt einen Wahrheitswert zurück, ob eine Position in dem (Standard-) Spielfeld (10x10) liegt. + def perform_set_move(game_state: GameState, piece: Piece) -> None: + """Platziert den gegebenen Stein auf dem Spielfeld (mutierend, intern genutzt).""" + ... - Args: - coordinate (Coordinate): Die Position + @staticmethod + def validate_shape(game_state: GameState, shape: PieceShape, color: Color) -> None: + """ + Prüft, ob die Form im ersten Zug/den nachfolgenden Zügen erlaubt ist. - Returns: - bool: Ob die Koordinate im Feld ist. + Raises: + WrongShape: Im ersten Zug, falls die falsche Form gewählt wurde. + DuplicateShape: In folgenden Zügen, falls die Form bereits gesetzt wurde. """ ... @staticmethod - def can_execute_move(board: Board, move_: Move) -> None: + def is_valid_set_move(game_state: GameState, piece: Piece) -> bool: + """Gibt zurück, ob der SetMove zulässig ist, ohne eine Exception zu werfen.""" + ... + + @staticmethod + def validate_set_move_on_board(board: Board, piece: Piece) -> None: """ - Prüft, ob ein Zug auf dem Board nach den Regeln durchgeführt werden könnte.
- Dabei ist nicht relevant, welcher Spieler gerade tatsächlich dran wäre. + Prüft, ob der Stein auf dem Board platziert werden kann (Grenzen, Überlappung, Farbregeln). - Gibt keinen Wert zurück, sondern wirft eine Fehlermeldung, falls der Zug nicht valide ist. + Raises: + OutOfBounds: Wenn der Stein nicht vollständig auf das Spielfeld passt. + Obstructed: Wenn der Stein eine andere Farbe überlagern würde. + TouchesSameColor: Wenn der Stein ein Feld gleicher Farbe berührt. + """ + ... - Args: - board (Board): Das Spielfeld. - move_ (Move): Der Zug, der geprüft werden soll. + @staticmethod + def validate_skip_move(game_state: GameState) -> None: + """ + Prüft, ob die aktuelle Farbe den Zug überspringen kann. Raises: - PiranhasError: Wenn der Zug nicht valide ist. + SkipFirstTurn: Wenn im ersten Zug übersprungen werden soll. """ ... - @staticmethod - def get_team_on_turn(turn: int) -> TeamEnum: - """ - Berechnet anhand der Zugzahl, welcher Spieler dran sein müsste.
- Es wird nicht beachtet, ob die Zahl kleiner 0 oder größer 59 ist. + @staticmethod + def perform_skip_move(game_state: GameState) -> None: + """Führt einen Skip-Zug aus (validiert, mutiert aber sonst nichts).""" + ... - Args: - turn (int): Die Zugzahl. + @staticmethod + def borders_on_color(board: Board, field: Field) -> bool: + """Prüft, ob das Feld an ein Feld gleicher Farbe angrenzt (Kante).""" + ... - Returns: - TeamEnum: Das Team, was dran ist. - """ + @staticmethod + def corners_on_color(board: Board, field: Field) -> bool: + """Prüft, ob das Feld an die Ecke eines Feldes gleicher Farbe angrenzt.""" ... @staticmethod - def swarm_from(board: Board, position: Coordinate) -> List[Coordinate]: - """ - Berechnet auf einem Spielbrett von einer Startposition aus, welche Fische - von dort aus in einem Schwarm zusammenhängen. - Dabei werden nur Fische beachtet, die im selben Team sind, wie der auf dem Startfeld. - Gibt eine Liste an Koordinaten zurück, die leer ist, wenn die Startposition - außerhalb des Spielbrettes ist, oder kein Fisch als Start angegeben ist. + def is_on_border(position: Coordinate) -> bool: + """Prüft, ob die Position am Rand des Spielfelds liegt.""" + ... + + @staticmethod + def is_first_move(game_state: GameState) -> bool: + """Gibt zurück, ob sich der GameState noch in der ersten Runde befindet.""" + ... - Args: - board (Board): Das Spielbrett. - position (Coordinate): Die Startkoordinate + @staticmethod + def get_random_start_pentomino() -> PieceShape: + """Gibt ein zufälliges Pentomino zurück (Startstein).""" + ... - Returns: - List[Coordinate]: Die Liste an zusammenhängenden Fischen. + @staticmethod + def remove_invalid_colors(game_state: GameState) -> None: + """Entfernt rekursiv alle Farben, die keine Steine mehr platzieren können (mutierend).""" + ... - """ + @staticmethod + def get_all_possible_moves(game_state: GameState) -> List[Piece]: + """Gibt eine Liste aller möglichen SetMoves zurück (inkl. möglicher Startzüge).""" ... @staticmethod - def swarms_of_team(board: Board, team: TeamEnum) -> List[List[Coordinate]]: + def get_filtered_possible_moves(game_state: GameState) -> List[Piece]: + """ + Gibt eine gefilterte Liste möglicher SetMoves zurück: + Startzüge, dann 5 Runden nur Pentominos, danach alle. """ - Berechnet auf einem Spielbrett alle Schwärme, die ein Team gerade gebildet hat. - Gibt eine 2-Dimensionale Liste zurück, wobei jede Sub-Liste ein einzelner Schwarm ist. + ... - Args: - board (Board): Das Spielbrett. - team (TeamEnum): Das gewählte Team. + @staticmethod + def get_possible_start_moves(game_state: GameState, filter: bool = False) -> List[Piece]: + """Gibt alle möglichen SetMoves für den ersten Zug zurück.""" + ... - Returns: - List[List[Coordinate]]: Die Liste an Schwärmen. + @staticmethod + def get_possible_moves(game_state: GameState) -> List[Piece]: + """Gibt alle möglichen SetMoves (ohne Startzug) zurück.""" + ... - """ + @staticmethod + def get_pentomino_moves(game_state: GameState) -> List[Piece]: + """Gibt nur die möglichen SetMoves mit Pentominos zurück.""" ... -class PluginConstants: - """ - Hält globale Konstanten. - """ + @staticmethod + def get_possible_moves_for_shape( + game_state: GameState, shape: PieceShape, valid_fields: Set[Coordinate] + ) -> List[Piece]: + """Gibt alle möglichen SetMoves für eine bestimmte Form zurück.""" + ... + + @staticmethod + def get_valid_fields(board: Board, color: Color) -> Set[Coordinate]: + """Gibt alle Koordinaten zurück, auf die die gegebene Farbe einen Stein platzieren könnte.""" + ... - BOARD_WIDTH: int - BOARD_HEIGHT: int + @staticmethod + def get_colored_fields(board: Board, color: Color) -> Set[Coordinate]: + """Gibt alle Koordinaten mit der gegebenen Farbe auf dem Board zurück.""" + ... + +class Constants: + """Hält globale Konstanten.""" + + BOARD_LENGTH: int ROUND_LIMIT: int + TOTAL_PIECE_SHAPES: int + COLORS: int + VALIDATE_MOVE: bool + + +class WrongColor(Exception): + """Die Farbe des Zuges ist nicht an der Reihe.""" + ... + +class NotOnBorder(Exception): + """Der erste Zug muss an den Rand gesetzt werden.""" + ... + +class NoSharedCorner(Exception): + """Alle Teile müssen ein vorheriges Teil gleicher Farbe über mindestens eine Ecke berühren.""" + ... + +class WrongShape(Exception): + """Der erste Zug muss den festgelegten Spielstein setzen.""" + ... + +class SkipFirstTurn(Exception): + """Der erste Zug muss einen Stein setzen.""" + ... + +class DuplicateShape(Exception): + """Der gewählte Stein wurde bereits gesetzt.""" + ... + +class OutOfBounds(Exception): + """Der Spielstein passt nicht vollständig auf das Spielfeld.""" + ... + +class Obstructed(Exception): + """Der Spielstein würde eine andere Farbe überlagern.""" + ... + +class TouchesSameColor(Exception): + """Der Spielstein berührt ein Feld gleicher Farbe.""" + ... \ No newline at end of file diff --git a/python/socha/api/networking/utils.py b/python/socha/api/networking/utils.py index 46f87dd..f37a814 100644 --- a/python/socha/api/networking/utils.py +++ b/python/socha/api/networking/utils.py @@ -1,121 +1,197 @@ -import re -from typing import List +from typing import Optional + from socha import _socha from socha.api.protocol.protocol import ( - Coordinate, Board, + Data, + LastMove, + LastMoveMono, + Piece, + Position, Room, State, - Data, ) +# SCREAMING_SNAKE_CASE (Server) <-> CamelCase-Variantenname (Rust/_socha) +_SHAPE_NAME_MAP = { + "MONO": "Mono", + "DOMINO": "Domino", + "TRIO_L": "TrioL", + "TRIO_I": "TrioI", + "TETRO_O": "TetroO", + "TETRO_T": "TetroT", + "TETRO_I": "TetroI", + "TETRO_L": "TetroL", + "TETRO_Z": "TetroZ", + "PENTO_L": "PentoL", + "PENTO_T": "PentoT", + "PENTO_V": "PentoV", + "PENTO_S": "PentoS", + "PENTO_Z": "PentoZ", + "PENTO_I": "PentoI", + "PENTO_P": "PentoP", + "PENTO_W": "PentoW", + "PENTO_U": "PentoU", + "PENTO_R": "PentoR", + "PENTO_X": "PentoX", + "PENTO_Y": "PentoY", +} +_SHAPE_NAME_MAP_REVERSE = {v: k for k, v in _SHAPE_NAME_MAP.items()} + + +def map_shape(name: str) -> _socha.PieceShape: + """ + Wandelt einen Server-Formnamen (z.B. 'PENTO_Y') in die entsprechende + _socha.PieceShape-Variante (z.B. PieceShape.PentoY) um. + """ + return getattr(_socha.PieceShape, _SHAPE_NAME_MAP[name]) + + +def map_shape_to_string(shape: _socha.PieceShape) -> str: + """ + Wandelt eine _socha.PieceShape-Variante zurück in den Server-Formnamen. + """ + return _SHAPE_NAME_MAP_REVERSE[shape.name()] + + +def map_color(name: str) -> _socha.Color: + """ + Color-Varianten matchen 1:1 zwischen Server und _socha (beide UPPERCASE). + """ + return getattr(_socha.Color, name) + + +def map_rotation(name: str) -> _socha.Rotation: + """ + Rotation-Varianten matchen 1:1 zwischen Server und _socha (beide UPPERCASE). + """ + return getattr(_socha.Rotation, name) + + +def map_piece(piece: Piece) -> _socha.Piece: + """ + Konvertiert ein protokoll Piece-Objekt in ein _socha.Piece-Objekt. + """ + return _socha.Piece( + color=map_color(piece.color), + kind=map_shape(piece.kind), + rotation=map_rotation(piece.rotation), + is_flipped=piece.is_flipped, + position=_socha.Coordinate(piece.position.x, piece.position.y), + ) + + +def map_piece_to_protocol(piece: _socha.Piece) -> Piece: + """ + Konvertiert ein _socha.Piece-Objekt zurück in ein protokoll Piece-Objekt, + zum Versenden an den Server. + """ + return Piece( + color=piece.color.name(), + kind=map_shape_to_string(piece.kind), + rotation=piece.rotation.name(), + is_flipped=piece.is_flipped, + position=Position(x=piece.position.x, y=piece.position.y), + ) + + +def map_last_move(protocol_last_move: Optional[LastMove]) -> Optional[_socha.Move]: + """ + Konvertiert das lastMove-Element eines State in ein _socha.Move-Objekt. + Kann entweder ein SetMove (piece gesetzt) oder ein SkipMove (color gesetzt) sein. + """ + if protocol_last_move is None: + return None + if protocol_last_move.piece is not None: + return _socha.Move.set_move(map_piece(protocol_last_move.piece)) + if protocol_last_move.color is not None: + return _socha.Move.skip_move(map_color(protocol_last_move.color)) + return None + + +def map_last_move_mono(last_move_mono: Optional[LastMoveMono]) -> dict: + """ + Konvertiert das lastMoveMono-Element (XStream-Standard-Map-Serialisierung) + in ein Python-Dict[Color, bool]. + + UNBESTÄTIGT: In allen bisherigen Aufzeichnungen war dieses Element leer, + daher basiert die Struktur auf einer fundierten Annahme über XStreams + Standardverhalten für unkonvertierte HashMaps. + """ + if last_move_mono is None: + return {} + result = {} + for entry in last_move_mono.entry: + if entry.color is not None: + result[map_color(entry.color)] = bool(entry.value) + return result + + def map_board(protocol_board: Board) -> _socha.Board: """ - Converts a protocol Board to a usable game board for using in the logic. - :param protocol_board: A Board object in protocol format - :type protocol_board: Board - :return: A Board object in the format used by the game logic - :rtype: _socha.Board - """ - - board_map: List[List[_socha.FieldType]] = [] - - for row in protocol_board.rows: - board_map.append([]) - for field in row.field_value: - if field == 'EMPTY': - board_map[-1].append(_socha.FieldType.Empty) - elif field == 'ONE_S': - board_map[-1].append(_socha.FieldType.OneS) - elif field == 'ONE_M': - board_map[-1].append(_socha.FieldType.OneM) - elif field == 'ONE_L': - board_map[-1].append(_socha.FieldType.OneL) - elif field == 'TWO_S': - board_map[-1].append(_socha.FieldType.TwoS) - elif field == 'TWO_M': - board_map[-1].append(_socha.FieldType.TwoM) - elif field == 'TWO_L': - board_map[-1].append(_socha.FieldType.TwoL) - elif field == 'SQUID': - board_map[-1].append(_socha.FieldType.Squid) - else: - raise ValueError(f'Unknown field type: {field}') - + Baut ein volles Board auf. Der Server sendet nur belegte Felder, + daher wird zunächst ein leeres Board erzeugt und dann überschrieben. + """ + board_map = _socha.Board.random_fields() # bereits vollständig EMPTY + + for f in protocol_board.field_value: + color = map_color(f.content) + coord = _socha.Coordinate(f.x, f.y) + board_map[f.y][f.x] = _socha.Field(coord, color) + return _socha.Board(map=board_map) -def map_string_to_direction(direction: str) -> _socha.Direction: - direction = re.sub(r'[^A-Za-z0-9_]', '', direction) - - if direction == 'UP': - return _socha.Direction.Up - elif direction == 'UP_RIGHT': - return _socha.Direction.UpRight - elif direction == 'RIGHT': - return _socha.Direction.Right - elif direction == 'DOWN_RIGHT': - return _socha.Direction.DownRight - elif direction == 'DOWN': - return _socha.Direction.Down - elif direction == 'DOWN_LEFT': - return _socha.Direction.DownLeft - elif direction == 'LEFT': - return _socha.Direction.Left - elif direction == 'UP_LEFT': - return _socha.Direction.UpLeft - else: - raise ValueError(f'Unknown direction: {direction}') - -def map_direction_to_string(direction: _socha.Direction): - - if direction == _socha.Direction.Up: - return 'UP' - elif direction == _socha.Direction.UpRight: - return 'UP_RIGHT' - elif direction == _socha.Direction.Right: - return 'RIGHT' - elif direction == _socha.Direction.DownRight: - return 'DOWN_RIGHT' - elif direction == _socha.Direction.Down: - return 'DOWN' - elif direction == _socha.Direction.DownLeft: - return 'DOWN_LEFT' - elif direction == _socha.Direction.Left: - return 'LEFT' - elif direction == _socha.Direction.UpLeft: - return 'UP_LEFT' - else: - raise ValueError(f'Unknown direction: {direction}') -def handle_move(move_response: _socha.Move) -> Data: +def handle_move(move: _socha.Move) -> Data: + """ + Konvertiert einen _socha.Move (SetMove oder SkipMove) in das ausgehende + Data-Paket, das an den Server gesendet wird. - return Data( - class_value='move', - from_=Coordinate(move_response.start.x, move_response.start.y), # invert y coordinate for server compatibility - direction=map_direction_to_string(move_response.direction), + UNBESTÄTIGT für SkipMove: Es wurde in keiner Aufzeichnung tatsächlich + ein SkipMove beobachtet. Die Struktur (color als Kindelement) basiert + auf der Kotlin-Deklaration von SkipMove.color (kein @XStreamAsAttribute). + """ + piece = move.as_piece() + if piece is not None: + return Data( + class_value="sc.plugin2027.SetMove", + piece=map_piece_to_protocol(piece), ) + else: + return Data( + class_value="sc.plugin2027.SkipMove", + skip_color=move.get_color().name(), + ) + def message_to_state(message: Room) -> _socha.GameState: """ - Constructs a GameState from the provided message, ensuring to reflect the - current state based on the ships' positions, teams, and other attributes. - - Args: - message: The input message containing the current game state. - second_last_move: the last_move object from the last game state before this game state - - Returns: - GameState: The constructed game state from the message. + Konstruiert einen vollständigen GameState aus der vom Server empfangenen + Nachricht, inklusive aller noch nicht gesetzten Formen pro Farbe und + der aktuell gültigen Farben. """ - state: State = message.data.class_binding - # extract last move of current gameState - state_last_move = state.last_move.class_binding if state.last_move and state.last_move.class_binding else None - return _socha.GameState( - board=map_board(state.board), turn=state.turn, - last_move=state_last_move, - ) + last_move=map_last_move(state.last_move), + board=map_board(state.board), + start_piece=map_shape(state.start_piece), + last_move_mono=map_last_move_mono(state.last_move_mono), + blue_shapes=[map_shape(s) for s in state.blue_shapes.shape] + if state.blue_shapes + else [], + yellow_shapes=[map_shape(s) for s in state.yellow_shapes.shape] + if state.yellow_shapes + else [], + red_shapes=[map_shape(s) for s in state.red_shapes.shape] + if state.red_shapes + else [], + green_shapes=[map_shape(s) for s in state.green_shapes.shape] + if state.green_shapes + else [], + valid_colors=[map_color(c) for c in state.valid_colors.color] + if state.valid_colors + else [], + ) \ No newline at end of file diff --git a/python/socha/api/networking/xml_protocol_interface.py b/python/socha/api/networking/xml_protocol_interface.py index c8507fa..e043772 100644 --- a/python/socha/api/networking/xml_protocol_interface.py +++ b/python/socha/api/networking/xml_protocol_interface.py @@ -6,7 +6,6 @@ import logging from typing import Any, Callable, Iterator -from socha.api.networking.utils import map_string_to_direction from socha import _socha from socha.api.networking.network_socket import NetworkSocket from socha.api.protocol.protocol import ( @@ -24,10 +23,10 @@ from xsdata.formats.dataclass.serializers import XmlSerializer from xsdata.formats.dataclass.serializers.config import SerializerConfig -from socha.api.protocol.protocol import Data, LastMove +from socha.api.protocol.protocol import Data -def map_object(data: Data , params: dict): +def map_object(data: Data, params: dict): try: params.pop('class_binding') @@ -58,31 +57,16 @@ def map_object(data: Data , params: dict): ) return data(class_binding=error_object, **params) else: - logging.warn('Unknown class value: %s', params.get('class_value')) + logging.warning('Unknown class value: %s', params.get('class_value')) return data(**params) -def map_last_move(last_move: LastMove, params: dict): - try: - params.pop('class_binding') - except KeyError: - ... - - move_object = _socha.Move( - start=_socha.Coordinate(x=params.get('from_').x, y=params.get('from_').y), - direction=map_string_to_direction(params.get('direction')), - ) - return last_move(class_binding=move_object, **params) - def custom_class_factory(clazz, params: dict): # print("TEST01: ", clazz, params) if clazz.__name__ == 'Data': return map_object(clazz, params) - if clazz.__name__ == 'LastMove': - test = map_last_move(clazz, params) - return test return clazz(**params) @@ -143,7 +127,7 @@ def _receive(self): # Return None if the server returns an empty response if not receiving: return None - + # weird replacing of unicode chars, that are not working in xml rn unicodes = [b"\xe5", b"\xf6", b"\xfc", b"\xdf"] replaces = [b"ae", b"oe", b"ue", b"ss"] @@ -213,4 +197,4 @@ def _serialize_object(self, object_class: object) -> bytes: :param object_class: The ProtocolPacket child to serialize. :return: The serialized byte stream. """ - return self.serializer.render(object_class).encode('utf-8') + return self.serializer.render(object_class).encode('utf-8') \ No newline at end of file diff --git a/python/socha/api/protocol/protocol.py b/python/socha/api/protocol/protocol.py index f4d9d0c..675a0da 100644 --- a/python/socha/api/protocol/protocol.py +++ b/python/socha/api/protocol/protocol.py @@ -17,97 +17,175 @@ @dataclass -class Row: +class Position: + """ + Eine Position auf dem Spielbrett, verschachtelt innerhalb eines Piece-Elements. + """ + class Meta: - name = 'row' + name = 'position' - field_value: List[str] = field( - default_factory=list, - metadata={ - 'name': 'field', - 'type': 'Element', - 'min_occurs': 1, - }, + x: Optional[int] = field( + default=None, + metadata={'type': 'Attribute'}, + ) + y: Optional[int] = field( + default=None, + metadata={'type': 'Attribute'}, ) @dataclass -class Board: +class Piece: + """ + Ein Spielstein mit Farbe, Form, Rotation, Spiegelung und Position. + """ + class Meta: - name = 'board' + name = 'piece' - rows: List[Row] = field( - default_factory=list, + color: Optional[str] = field( + default=None, + metadata={'type': 'Attribute'}, + ) + kind: Optional[str] = field( + default=None, + metadata={'type': 'Attribute'}, + ) + rotation: Optional[str] = field( + default=None, + metadata={'type': 'Attribute'}, + ) + is_flipped: Optional[bool] = field( + default=None, metadata={ - 'name': 'row', - 'type': 'Element', - 'min_occurs': 1, + 'name': 'isFlipped', + 'type': 'Attribute', }, ) + position: Optional[Position] = field( + default=None, + metadata={'type': 'Element'}, + ) @dataclass -class Coordinate: - +class LastMove: class Meta: - name = 'from' + name = 'lastMove' - x: Optional[int] = field( + class_binding: Optional[object] = field(default=None) + class_value: Optional[str] = field( default=None, metadata={ + 'name': 'class', 'type': 'Attribute', + 'required': True, }, ) - y: Optional[int] = field( + piece: Optional[Piece] = field( default=None, - metadata={ - 'type': 'Attribute', - }, + metadata={'type': 'Element'}, + ) + # Für SkipMove: SkipMove.color hat kein @XStreamAsAttribute in Kotlin, + # wird daher als Kindelement serialisiert (nicht als Attribut). + color: Optional[str] = field( + default=None, + metadata={'type': 'Element'}, ) @dataclass -class LastMove: +class Field: class Meta: - name = 'lastMove' - - class_binding: Optional[object] = field(default=None) - from_: Optional[Coordinate] = field( + name = 'field' + + x: Optional[int] = field( default=None, - metadata={ - 'name': 'from', - 'type': 'Element', - }, + metadata={'type': 'Attribute'}, + ) + y: Optional[int] = field( + default=None, + metadata={'type': 'Attribute'}, ) - direction: Optional[str] = field( + content: Optional[str] = field( default=None, + metadata={'type': 'Attribute'}, + ) + + +@dataclass +class Board: + """ + Das Spielbrett. Enthält nur belegte Felder; + leere Felder werden vom Server nicht mitgeschickt. + """ + + class Meta: + name = 'board' + + field_value: List[Field] = field( + default_factory=list, metadata={ + 'name': 'field', 'type': 'Element', }, ) @dataclass -class Player: +class ShapeList: + """ + Wiederverwendet für blueShapes / yellowShapes / redShapes / greenShapes. + Enthält die noch nicht gesetzten Formen einer Farbe. + """ + + shape: List[str] = field( + default_factory=list, + metadata={'type': 'Element'}, + ) + + +@dataclass +class ColorList: class Meta: - name = 'player' + name = 'validColors' - name: Optional[str] = field( + color: List[str] = field( + default_factory=list, + metadata={'type': 'Element'}, + ) + + +@dataclass +class LastMoveMonoEntry: + class Meta: + name = 'entry' + + color: Optional[str] = field( default=None, - metadata={ - 'type': 'Attribute', - 'required': True, - }, + metadata={'type': 'Element'}, ) - team: Optional[str] = field( + value: Optional[bool] = field( default=None, metadata={ - 'type': 'Attribute', - 'required': True, + 'name': 'boolean', + 'type': 'Element', }, ) +@dataclass +class LastMoveMono: + class Meta: + name = 'lastMoveMono' + + entry: List[LastMoveMonoEntry] = field( + default_factory=list, + metadata={'type': 'Element'}, + ) + + @dataclass class State(ObservableRoomMessage): class Meta: @@ -136,10 +214,18 @@ class Meta: 'required': True, }, ) - board: Optional[Board] = field( + start_piece: Optional[str] = field( default=None, metadata={ - 'type': 'Element', + 'name': 'startPiece', + 'type': 'Attribute', + 'required': True, + }, + ) + round: Optional[int] = field( + default=None, + metadata={ + 'type': 'Attribute', 'required': True, }, ) @@ -150,6 +236,76 @@ class Meta: 'type': 'Element', }, ) + board: Optional[Board] = field( + default=None, + metadata={ + 'type': 'Element', + 'required': True, + }, + ) + last_move_mono: Optional[LastMoveMono] = field( + default=None, + metadata={ + 'name': 'lastMoveMono', + 'type': 'Element', + }, + ) + blue_shapes: Optional[ShapeList] = field( + default=None, + metadata={ + 'name': 'blueShapes', + 'type': 'Element', + }, + ) + yellow_shapes: Optional[ShapeList] = field( + default=None, + metadata={ + 'name': 'yellowShapes', + 'type': 'Element', + }, + ) + red_shapes: Optional[ShapeList] = field( + default=None, + metadata={ + 'name': 'redShapes', + 'type': 'Element', + }, + ) + green_shapes: Optional[ShapeList] = field( + default=None, + metadata={ + 'name': 'greenShapes', + 'type': 'Element', + }, + ) + valid_colors: Optional[ColorList] = field( + default=None, + metadata={ + 'name': 'validColors', + 'type': 'Element', + }, + ) + + +@dataclass +class Player: + class Meta: + name = 'player' + + name: Optional[str] = field( + default=None, + metadata={ + 'type': 'Attribute', + 'required': True, + }, + ) + team: Optional[str] = field( + default=None, + metadata={ + 'type': 'Attribute', + 'required': True, + }, + ) @dataclass @@ -645,6 +801,12 @@ class OriginalMessage: """ The original message that was sent by the client. Is sent by the server if an error occurs. + + UNBESTÄTIGT für Blokus: Struktur übernommen aus dem alten Piranhas-Format + mit from_/direction. Da wir keine echte error-Nachricht aufgezeichnet haben, + ist unklar, ob der Server hier stattdessen ein - oder -Element + verwendet, analog zu Data. Passe ggf. an, sobald eine echte error-Nachricht + für einen Blokus-Move vorliegt. """ class Meta: @@ -658,18 +820,13 @@ class Meta: 'required': True, }, ) - from_: Optional[Coordinate] = field( + piece: Optional[Piece] = field( default=None, - metadata={ - 'name': 'from', - 'type': 'Element', - }, + metadata={'type': 'Element'}, ) - direction: Optional[str] = field( + color: Optional[str] = field( default=None, - metadata={ - 'type': 'Element', - }, + metadata={'type': 'Element'}, ) @@ -728,22 +885,25 @@ class Meta: 'type': 'Element', }, ) + # Nur für welcomeMessage: color="ONE"/"TWO" (TeamEnum), als Attribut. color: Optional[str] = field( default=None, metadata={ 'type': 'Attribute', }, ) - from_: Optional[Coordinate] = field( + # Für ausgehenden SetMove. + piece: Optional[Piece] = field( default=None, - metadata={ - 'name': 'from', - 'type': 'Element', - }, + metadata={'type': 'Element'}, ) - direction: Optional[str] = field( + # Für ausgehenden SkipMove: eigenes Feld, da 'color' oben schon als + # Attribut für welcomeMessage belegt ist. SkipMove.color ist ein + # Kindelement (kein @XStreamAsAttribute in Kotlin). + skip_color: Optional[str] = field( default=None, metadata={ + 'name': 'color', 'type': 'Element', }, ) @@ -888,4 +1048,4 @@ class Meta: metadata={ 'type': 'Element', }, - ) + ) \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index c746fe7..5486d31 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,37 +1,45 @@ use pyo3::*; use types::PyModule; -pub mod plugin2026; - -use crate::plugin2026::utils::vector::Vector; -use crate::plugin2026::utils::direction::Direction; -use crate::plugin2026::utils::coordinate::Coordinate; -use crate::plugin2026::utils::constants::PluginConstants; -use crate::plugin2026::utils::team::TeamEnum; - -use crate::plugin2026::game_state::GameState; -use crate::plugin2026::board::Board; -use crate::plugin2026::field_type::FieldType; -use crate::plugin2026::r#move::Move; - -use crate::plugin2026::rules_engine::RulesEngine; +pub mod plugin2027; + +use crate::plugin2027::utils::constants::Constants; +use crate::plugin2027::utils::coordinate::Coordinate; +use crate::plugin2027::utils::direction::Direction; +use crate::plugin2027::utils::game_rule_logic::GameRuleLogic; +use crate::plugin2027::utils::team::TeamEnum; +use crate::plugin2027::utils::vector::Vector; + +use crate::plugin2027::board::Board; +use crate::plugin2027::color::Color; +use crate::plugin2027::field_content::FieldContent; +use crate::plugin2027::field::Field; +use crate::plugin2027::game_state::GameState; +use crate::plugin2027::r#move::Move; +use crate::plugin2027::piece_shape::PieceShape; +use crate::plugin2027::piece::Piece; +use crate::plugin2027::rotation::Rotation; #[pymodule] fn _socha(m: &Bound<'_, PyModule>) -> PyResult<()> { pyo3_log::init(); - - m.add_class::()?; - m.add_class::()?; + + m.add_class::()?; m.add_class::()?; - m.add_class::()?; + m.add_class::()?; + m.add_class::()?; m.add_class::()?; + m.add_class::()?; - m.add_class::()?; m.add_class::()?; - m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; m.add_class::()?; - - m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; Ok(()) } diff --git a/src/plugin2026.rs b/src/plugin2026.rs deleted file mode 100644 index 9e5fc23..0000000 --- a/src/plugin2026.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub mod rules_engine; -pub mod game_state; -pub mod field_type; -pub mod board; -pub mod r#move; -pub mod utils; -pub mod test; -pub mod errors; \ No newline at end of file diff --git a/src/plugin2027.rs b/src/plugin2027.rs new file mode 100644 index 0000000..543f5f1 --- /dev/null +++ b/src/plugin2027.rs @@ -0,0 +1,11 @@ +pub mod game_state; +pub mod field; +pub mod field_content; +pub mod color; +pub mod piece; +pub mod piece_shape; +pub mod board; +pub mod r#move; +pub mod errors; +pub mod rotation; +pub mod utils; \ No newline at end of file diff --git a/src/plugin2027/board.rs b/src/plugin2027/board.rs new file mode 100644 index 0000000..e7a2ae8 --- /dev/null +++ b/src/plugin2027/board.rs @@ -0,0 +1,117 @@ +use pyo3::*; + +use crate::plugin2027::{ + color::Color, field::Field, field_content::FieldContent, + utils::{constants::Constants, coordinate::Coordinate}, +}; + +#[pyclass] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Board { + #[pyo3(get, set)] + pub map: Vec>, +} + +#[pymethods] +impl Board { + #[new] + pub fn new(map: Option>>) -> Self { + Self { + map: map.unwrap_or_else(Self::random_fields), + } + } + + /// Zugriff auf ein Feld über Koordinaten (entspricht Kotlins `this[position]`). + pub fn get(&self, position: Coordinate) -> PyResult { + self.map + .get(position.y as usize) + .and_then(|row| row.get(position.x as usize)) + .cloned() + .ok_or_else(|| pyo3::exceptions::PyIndexError::new_err("Coordinate out of bounds")) + } + + /// Setzt den Inhalt eines Feldes über Koordinaten. + pub fn set_content(&mut self, position: Coordinate, content: FieldContent) { + if let Some(row) = self.map.get_mut(position.y as usize) { + if let Some(field) = row.get_mut(position.x as usize) { + field.content = content; + } + } + } + + pub fn get_content(&self, position: Coordinate) -> Option { + self.map + .get(position.y as usize) + .and_then(|row| row.get(position.x as usize)) + .map(|field| field.content) + } + + pub fn is_empty(&self) -> bool { + self.map + .iter() + .all(|row| row.iter().all(|f| f.content == FieldContent::EMPTY)) + } + + pub fn is_obstructed(&self, position: Coordinate) -> bool { + self.get_content(position) + .is_some_and(|c| c != FieldContent::EMPTY) + } + + pub fn get_team(&self, position: Coordinate) -> Option { + self.get_content(position).and_then(|c| c.to_team_color()) + } + + pub fn pretty_string(&self) -> String { + self.map + .iter() + .map(|row| { + row.iter() + .map(|f| f.content.to_string()) + .collect::>() + .join(" ") + }) + .collect::>() + .join("\n") + } + + /// Vergleicht zwei Boards und gibt die Felder zurück, die sich unterscheiden (Werte von `other`). + pub fn compare(&self, other: &Board) -> Vec { + let mut different = Vec::new(); + for y in 0..self.map.len() { + for x in 0..self.map[y].len() { + let field = &self.map[y][x]; + let other_field = &other.map[y][x]; + if field != other_field { + different.push(other_field.clone()); + } + } + } + different + } + + #[staticmethod] + pub fn random_fields() -> Vec> { + (0..Constants::BOARD_LENGTH) + .map(|y| { + (0..Constants::BOARD_LENGTH) + .map(|x| Field { + coordinate: Coordinate::new(x as isize, y as isize), + content: FieldContent::EMPTY, + }) + .collect() + }) + .collect() + } + + #[staticmethod] + pub fn contains(position: Coordinate) -> bool { + let len = Constants::BOARD_LENGTH as isize; + position.x >= 0 && position.x < len && position.y >= 0 && position.y < len + } +} + +impl std::fmt::Display for Board { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Board {}", self.pretty_string()) + } +} \ No newline at end of file diff --git a/src/plugin2027/color.rs b/src/plugin2027/color.rs new file mode 100644 index 0000000..47d4257 --- /dev/null +++ b/src/plugin2027/color.rs @@ -0,0 +1,70 @@ + +use pyo3::*; + +use crate::plugin2027::{ + utils::team::TeamEnum, + field_content::FieldContent +}; + +#[pyclass] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Color { + BLUE, + YELLOW, + RED, + GREEN +} + +#[pymethods] +impl Color { + fn __str__(&self) -> String {self.to_string()} + fn __repr__(&self) -> String {format!("{:?}", self)} + fn __eq__(&self, other: &Color) -> bool {self == other} + fn __ne__(&self, other: &Color) -> bool {self != other} + fn __hash__(&self) -> u64 { + use std::hash::{Hash, Hasher}; + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + self.hash(&mut hasher); + hasher.finish() + } + + pub fn next(&self) -> Color { + match self { + Color::BLUE => Color::YELLOW, + Color::YELLOW => Color::RED, + Color::RED => Color::GREEN, + Color::GREEN => Color::BLUE + } + } + + pub fn team(&self) -> TeamEnum { + match self { + Color::BLUE | Color::RED => TeamEnum::One, + Color::YELLOW | Color::GREEN => TeamEnum::Two, + } + } + + pub fn to_field_content(&self) -> FieldContent { + match self { + Color::BLUE => FieldContent::BLUE, + Color::YELLOW => FieldContent::YELLOW, + Color::RED => FieldContent::RED, + Color::GREEN => FieldContent::GREEN + } + } + + pub fn name(&self) -> String { + format!("{:?}", self) + } +} + +impl std::fmt::Display for Color { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Color::BLUE => write!(f, "Color Blue"), + Color::YELLOW => write!(f, "Color Yellow"), + Color::RED => write!(f, "Color Red"), + Color::GREEN => write!(f, "Color green"), + } + } +} \ No newline at end of file diff --git a/src/plugin2027/errors.rs b/src/plugin2027/errors.rs new file mode 100644 index 0000000..4e906a5 --- /dev/null +++ b/src/plugin2027/errors.rs @@ -0,0 +1,69 @@ +use pyo3::prelude::*; +use pyo3::exceptions::PyException; +use pyo3::create_exception; + +create_exception!(_socha, WrongColor, PyException); +create_exception!(_socha, NotOnBorder, PyException); +create_exception!(_socha, NoSharedCorner, PyException); +create_exception!(_socha, WrongShape, PyException); +create_exception!(_socha, SkipFirstTurn, PyException); +create_exception!(_socha, DuplicateShape, PyException); +create_exception!(_socha, OutOfBounds, PyException); +create_exception!(_socha, Obstructed, PyException); +create_exception!(_socha, TouchesSameColor, PyException); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum BlokusMoveMistake { + WrongColor, + NotOnBorder, + NoSharedCorner, + WrongShape, + SkipFirstTurn, + DuplicateShape, + OutOfBounds, + Obstructed, + TouchesSameColor, +} + +impl BlokusMoveMistake { + pub fn message(&self) -> &'static str { + match self { + BlokusMoveMistake::WrongColor => "Die Farbe des Zuges ist nicht an der Reihe", + BlokusMoveMistake::NotOnBorder => "Der erste Zug muss an den Rand gesetzt werden", + BlokusMoveMistake::NoSharedCorner => "Alle Teile müssen ein vorheriges Teil gleicher Farbe über mindestens eine Ecke berühren", + BlokusMoveMistake::WrongShape => "Der erste Zug muss den festgelegten Spielstein setzen", + BlokusMoveMistake::SkipFirstTurn => "Der erste Zug muss einen Stein setzen", + BlokusMoveMistake::DuplicateShape => "Der gewählte Stein wurde bereits gesetzt", + BlokusMoveMistake::OutOfBounds => "Der Spielstein passt nicht vollständig auf das Spielfeld", + BlokusMoveMistake::Obstructed => "Der Spielstein würde eine andere Farbe überlagern", + BlokusMoveMistake::TouchesSameColor => "Der Spielstein berührt ein Feld gleicher Farbe", + } + } + + pub fn to_py_err(&self) -> PyErr { + let msg = self.message(); + match self { + BlokusMoveMistake::WrongColor => WrongColor::new_err(msg), + BlokusMoveMistake::NotOnBorder => NotOnBorder::new_err(msg), + BlokusMoveMistake::NoSharedCorner => NoSharedCorner::new_err(msg), + BlokusMoveMistake::WrongShape => WrongShape::new_err(msg), + BlokusMoveMistake::SkipFirstTurn => SkipFirstTurn::new_err(msg), + BlokusMoveMistake::DuplicateShape => DuplicateShape::new_err(msg), + BlokusMoveMistake::OutOfBounds => OutOfBounds::new_err(msg), + BlokusMoveMistake::Obstructed => Obstructed::new_err(msg), + BlokusMoveMistake::TouchesSameColor => TouchesSameColor::new_err(msg), + } + } +} + +impl std::fmt::Display for BlokusMoveMistake { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.message()) + } +} + +impl From for PyErr { + fn from(mistake: BlokusMoveMistake) -> PyErr { + mistake.to_py_err() + } +} \ No newline at end of file diff --git a/src/plugin2027/field.rs b/src/plugin2027/field.rs new file mode 100644 index 0000000..c591aa2 --- /dev/null +++ b/src/plugin2027/field.rs @@ -0,0 +1,42 @@ + +use pyo3::*; + +use crate::plugin2027::{ + color::Color, field_content::FieldContent, utils::coordinate::Coordinate +}; + +#[pyclass] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Field { + #[pyo3(get, set)] + pub coordinate: Coordinate, + #[pyo3(get, set)] + pub content: FieldContent +} + +#[pymethods] +impl Field { + #[new] + pub fn new(coordinate: Coordinate, color: Color) -> Self { + Self { + coordinate, + content: color.to_field_content(), + } + } + + fn __str__(&self) -> String {self.to_string()} + fn __repr__(&self) -> String {format!("{:?}", self)} + fn __eq__(&self, other: &Field) -> bool {self == other} + fn __ne__(&self, other: &Field) -> bool {self != other} + fn deepcopy(&self) -> Field {self.clone()} + + pub fn is_empty(&self) -> bool { + self.content == FieldContent::EMPTY + } +} + +impl std::fmt::Display for Field { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Field at {} filled with {}", self.coordinate, self.content) + } +} diff --git a/src/plugin2027/field_content.rs b/src/plugin2027/field_content.rs new file mode 100644 index 0000000..25e10db --- /dev/null +++ b/src/plugin2027/field_content.rs @@ -0,0 +1,50 @@ + +use pyo3::*; + +use crate::plugin2027::{ + color::Color, +}; + +#[pyclass] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum FieldContent { + BLUE, + YELLOW, + RED, + GREEN, + EMPTY +} + +#[pymethods] +impl FieldContent { + fn __str__(&self) -> String {self.to_string()} + fn __repr__(&self) -> String {format!("{:?}", self)} + fn __eq__(&self, other: &FieldContent) -> bool {self == other} + fn __ne__(&self, other: &FieldContent) -> bool {self != other} + + pub fn to_team_color(&self) -> Option { + match self { + FieldContent::BLUE => Some(Color::BLUE), + FieldContent::YELLOW => Some(Color::YELLOW), + FieldContent::RED => Some(Color::RED), + FieldContent::GREEN => Some(Color::GREEN), + FieldContent::EMPTY => None + } + } + + pub fn is_empty(&self) -> bool { + *self == FieldContent::EMPTY + } +} + +impl std::fmt::Display for FieldContent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + FieldContent::BLUE => write!(f, "B"), + FieldContent::YELLOW => write!(f, "Y"), + FieldContent::RED => write!(f, "R"), + FieldContent::GREEN => write!(f, "G"), + FieldContent::EMPTY => write!(f, "-"), + } + } +} \ No newline at end of file diff --git a/src/plugin2027/game_state.rs b/src/plugin2027/game_state.rs new file mode 100644 index 0000000..e9450b2 --- /dev/null +++ b/src/plugin2027/game_state.rs @@ -0,0 +1,178 @@ +use pyo3::prelude::*; +use std::collections::{HashMap, HashSet}; + +use crate::plugin2027::{ + board::Board, color::Color, r#move::Move, piece_shape::PieceShape, + utils::{constants::Constants, game_rule_logic::GameRuleLogic, team::TeamEnum}, +}; + +#[pyclass] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GameState { + #[pyo3(get, set)] + pub turn: usize, + #[pyo3(get, set)] + pub last_move: Option, + #[pyo3(get, set)] + pub board: Board, + #[pyo3(get, set)] + pub start_piece: PieceShape, + #[pyo3(get, set)] + pub last_move_mono: HashMap, + blue_shapes: HashSet, + yellow_shapes: HashSet, + red_shapes: HashSet, + green_shapes: HashSet, + valid_colors: Vec, + round: usize, +} + +#[pymethods] +impl GameState { + #[new] + #[pyo3(signature = (turn=0, last_move=None, board=None, start_piece=PieceShape::Mono, last_move_mono=None, blue_shapes=None, yellow_shapes=None, red_shapes=None, green_shapes=None, valid_colors=None))] + #[allow(clippy::too_many_arguments)] + pub fn new( + turn: usize, + last_move: Option, + board: Option, + start_piece: PieceShape, + last_move_mono: Option>, + blue_shapes: Option>, + yellow_shapes: Option>, + red_shapes: Option>, + green_shapes: Option>, + valid_colors: Option>, + ) -> Self { + Self { + turn, + last_move, + board: board.unwrap_or_else(|| Board::new(None)), + start_piece, + last_move_mono: last_move_mono.unwrap_or_default(), + blue_shapes: blue_shapes.map(|v| v.into_iter().collect()).unwrap_or_else(|| PieceShape::all().into_iter().collect()), + yellow_shapes: yellow_shapes.map(|v| v.into_iter().collect()).unwrap_or_else(|| PieceShape::all().into_iter().collect()), + red_shapes: red_shapes.map(|v| v.into_iter().collect()).unwrap_or_else(|| PieceShape::all().into_iter().collect()), + green_shapes: green_shapes.map(|v| v.into_iter().collect()).unwrap_or_else(|| PieceShape::all().into_iter().collect()), + valid_colors: valid_colors.unwrap_or_else(|| vec![Color::BLUE, Color::YELLOW, Color::RED, Color::GREEN]), + round: GameRuleLogic::round_from_turn(turn), + } + } + + fn round_from_turn(&self, turn: usize) -> usize { + 1 + turn / Constants::COLORS + } + + pub fn round(&self) -> usize { + self.round + } + + pub fn undeployed_piece_shapes(&self, color: Color) -> Vec { + self.shapes_for(color).iter().copied().collect() + } + + pub fn remove_undeployed_piece(&mut self, color: Color, shape: PieceShape) -> bool { + self.shapes_for_mut(color).remove(&shape) + } + + pub fn current_color(&self) -> Color { + let ordered = [Color::BLUE, Color::YELLOW, Color::RED, Color::GREEN]; + ordered[self.turn % Constants::COLORS] + } + + pub fn has_valid_colors(&self) -> bool { + !self.valid_colors.is_empty() + } + + pub fn is_valid_color(&self, color: Color) -> bool { + self.valid_colors.contains(&color) + } + + pub fn remove_active_color(&mut self) -> bool { + let color = self.current_color(); + self.valid_colors.retain(|c| *c != color); + self.advance(1) + } + + pub fn advance(&mut self, turns: usize) -> bool { + if !self.has_valid_colors() { + return false; + } + self.turn += turns; + while !self.is_valid_color(self.current_color()) { + self.turn += 1; + } + self.round = self.round_from_turn(self.turn); + true + } + + pub fn is_over(&self) -> bool { + !self.has_valid_colors() || self.round >= Constants::ROUND_LIMIT + } + + pub fn possible_moves(&self) -> Vec { + let pieces = GameRuleLogic::get_filtered_possible_moves(self); + if pieces.is_empty() { + vec![Move::skip_move(self.current_color())] + } else { + pieces.into_iter().map(Move::set_move).collect() + } + } + + pub fn get_points_for_color(&self, color: Color) -> usize { + let pieces = self.undeployed_piece_shapes(color); + let last_mono = *self.last_move_mono.get(&color).unwrap_or(&false); + GameRuleLogic::get_points_from_undeployed(pieces, last_mono) + } + + pub fn get_points_for_team(&self, team: TeamEnum) -> usize { + [Color::BLUE, Color::YELLOW, Color::RED, Color::GREEN] + .iter() + .filter(|c| c.team() == team) + .map(|c| self.get_points_for_color(*c)) + .sum() + } + + pub fn win_condition(&self) -> Option { + let one = self.get_points_for_team(TeamEnum::One); + let two = self.get_points_for_team(TeamEnum::Two); + if one > two { + Some(TeamEnum::One) + } else if two > one { + Some(TeamEnum::Two) + } else { + None // Unentschieden + } + } +} + +impl GameState { + fn shapes_for(&self, color: Color) -> &HashSet { + match color { + Color::BLUE => &self.blue_shapes, + Color::YELLOW => &self.yellow_shapes, + Color::RED => &self.red_shapes, + Color::GREEN => &self.green_shapes, + } + } + + fn shapes_for_mut(&mut self, color: Color) -> &mut HashSet { + match color { + Color::BLUE => &mut self.blue_shapes, + Color::YELLOW => &mut self.yellow_shapes, + Color::RED => &mut self.red_shapes, + Color::GREEN => &mut self.green_shapes, + } + } +} + +impl std::fmt::Display for GameState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "GameState(turn={}, currentColor={})", + self.turn, + self.current_color() + ) + } +} \ No newline at end of file diff --git a/src/plugin2027/move.rs b/src/plugin2027/move.rs new file mode 100644 index 0000000..12901c4 --- /dev/null +++ b/src/plugin2027/move.rs @@ -0,0 +1,51 @@ +use pyo3::prelude::*; + +use crate::plugin2027::{color::Color, piece::Piece}; + +#[pyclass] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Move { + SetMove { piece: Piece }, + SkipMove { color: Color }, +} + +#[pymethods] +impl Move { + #[staticmethod] + pub fn set_move(piece: Piece) -> Move { + Move::SetMove { piece } + } + + #[staticmethod] + pub fn skip_move(color: Color) -> Move { + Move::SkipMove { color } + } + + pub fn get_color(&self) -> Color { + match self { + Move::SetMove { piece } => piece.color, + Move::SkipMove { color } => *color, + } + } + + pub fn as_piece(&self) -> Option { + match self { + Move::SetMove { piece } => Some(piece.clone()), + Move::SkipMove { .. } => None, + } + } + + fn __str__(&self) -> String {self.to_string()} + fn __repr__(&self) -> String {format!("{:?}", self)} + fn __eq__(&self, other: &Move) -> bool {self == other} + fn __ne__(&self, other: &Move) -> bool {self != other} +} + +impl std::fmt::Display for Move { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Move::SetMove { piece } => write!(f, "Setze {}", piece), + Move::SkipMove { color } => write!(f, "{} setzt aus", color), + } + } +} \ No newline at end of file diff --git a/src/plugin2027/piece.rs b/src/plugin2027/piece.rs new file mode 100644 index 0000000..5bdc76a --- /dev/null +++ b/src/plugin2027/piece.rs @@ -0,0 +1,110 @@ +use std::collections::HashSet; +use pyo3::*; + +use crate::plugin2027::{ + color::Color, piece_shape::PieceShape, rotation::Rotation, + utils::{coordinate::Coordinate, coordinate_helper::CoordinateSetExt}, +}; + +#[pyclass] +#[derive(Debug, Clone)] +pub struct Piece { + #[pyo3(get, set)] + pub color: Color, + #[pyo3(get, set)] + pub kind: PieceShape, + #[pyo3(get, set)] + pub rotation: Rotation, + #[pyo3(get, set)] + pub is_flipped: bool, + #[pyo3(get, set)] + pub position: Coordinate, +} + +#[pymethods] +impl Piece { + #[new] + pub fn new( + color: Color, + kind: PieceShape, + rotation: Rotation, + is_flipped: bool, + position: Coordinate, + ) -> Self { + Self { color, kind, rotation, is_flipped, position } + } + + /// Die normalisierte Form des Steins (gedreht/gespiegelt, aber nicht verschoben). + pub fn shape(&self) -> HashSet { + self.kind + .coordinates() + .flip(self.is_flipped) + .rotate(self.rotation) + } + + /// Die tatsächlichen Koordinaten, die der Stein am Ende auf dem Feld einnimmt. + pub fn coordinates(&self) -> HashSet { + self.shape() + .iter() + .map(|c| self.position.add_vector(&c.as_vector())) + .collect() + } + + /// Dreht und spiegelt den Stein entsprechend den gegebenen Parametern, Position bleibt gleich. + pub fn transform(&self, rotation: Rotation, is_flipped: bool) -> Piece { + Piece::new(self.color, self.kind, rotation, is_flipped, self.position) + } + + fn __str__(&self) -> String { + self.to_string() + } + fn __repr__(&self) -> String { + format!("{:?}", self) + } + fn __eq__(&self, other: &Piece) -> bool { + self == other + } + fn __ne__(&self, other: &Piece) -> bool { + self != other + } + fn __hash__(&self) -> u64 { + use std::hash::{Hash, Hasher}; + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + self.hash(&mut hasher); + hasher.finish() + } +} + +/// Gleichheit basiert nur auf Farbe und den tatsächlichen Koordinaten (wie im Original). +impl PartialEq for Piece { + fn eq(&self, other: &Self) -> bool { + self.color == other.color && self.coordinates() == other.coordinates() + } +} +impl Eq for Piece {} + +impl std::hash::Hash for Piece { + fn hash(&self, state: &mut H) { + self.color.hash(state); + let mut coords: Vec = self.coordinates().into_iter().collect(); + // sortieren, damit die Reihenfolge das Hash-Ergebnis nicht beeinflusst + coords.sort_by_key(|c| (c.x, c.y)); + coords.hash(state); + } +} + +impl std::fmt::Display for Piece { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let rotation_part = if self.rotation != Rotation::NONE { + format!(", {}", self.rotation) + } else { + String::new() + }; + let flipped_part = if self.is_flipped { ", gespiegelt" } else { "" }; + write!( + f, + "{}({}{}{})[{},{}]", + self.kind, self.color, rotation_part, flipped_part, self.position.x, self.position.y + ) + } +} \ No newline at end of file diff --git a/src/plugin2027/piece_shape.rs b/src/plugin2027/piece_shape.rs new file mode 100644 index 0000000..15e0c9d --- /dev/null +++ b/src/plugin2027/piece_shape.rs @@ -0,0 +1,163 @@ +use pyo3::prelude::*; +use std::collections::{HashSet}; + +use crate::plugin2027::{ + utils::{coordinate::Coordinate, vector::Vector, coordinate_helper::CoordinateSetExt}, + rotation::Rotation, +}; + +#[pyclass] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum PieceShape { + Mono, + Domino, + TrioL, + TrioI, + TetroO, + TetroT, + TetroI, + TetroL, + TetroZ, + PentoL, + PentoT, + PentoV, + PentoS, + PentoZ, + PentoI, + PentoP, + PentoW, + PentoU, + PentoR, + PentoX, + PentoY, +} + +#[pymethods] +impl PieceShape { + /// Alle 21 Formen in Reihenfolge (entspricht dem Enum-Index in Kotlin). + #[staticmethod] + pub fn all() -> Vec { + vec![ + PieceShape::Mono, PieceShape::Domino, PieceShape::TrioL, PieceShape::TrioI, + PieceShape::TetroO, PieceShape::TetroT, PieceShape::TetroI, PieceShape::TetroL, + PieceShape::TetroZ, PieceShape::PentoL, PieceShape::PentoT, PieceShape::PentoV, + PieceShape::PentoS, PieceShape::PentoZ, PieceShape::PentoI, PieceShape::PentoP, + PieceShape::PentoW, PieceShape::PentoU, PieceShape::PentoR, PieceShape::PentoX, + PieceShape::PentoY, + ] + } + + /// Gibt die Form anhand ihres Index zurück (analog zu Kotlins `shapes`-Map). + #[staticmethod] + pub fn from_index(index: usize) -> Option { + PieceShape::all().get(index).copied() + } + + /// Die normalisierten (an (0,0) ausgerichteten) Koordinaten der Grundform. + pub fn coordinates(&self) -> HashSet { + let coords: &[(isize, isize)] = match self { + PieceShape::Mono => &[(0, 0)], + PieceShape::Domino => &[(0, 0), (1, 0)], + PieceShape::TrioL => &[(0, 0), (0, 1), (1, 1)], + PieceShape::TrioI => &[(0, 0), (0, 1), (0, 2)], + PieceShape::TetroO => &[(0, 0), (1, 0), (0, 1), (1, 1)], + PieceShape::TetroT => &[(0, 0), (1, 0), (2, 0), (1, 1)], + PieceShape::TetroI => &[(0, 0), (0, 1), (0, 2), (0, 3)], + PieceShape::TetroL => &[(0, 0), (0, 1), (0, 2), (1, 2)], + PieceShape::TetroZ => &[(0, 0), (1, 0), (1, 1), (2, 1)], + PieceShape::PentoL => &[(0, 0), (0, 1), (0, 2), (0, 3), (1, 3)], + PieceShape::PentoT => &[(0, 0), (1, 0), (2, 0), (1, 1), (1, 2)], + PieceShape::PentoV => &[(0, 0), (0, 1), (0, 2), (1, 2), (2, 2)], + PieceShape::PentoS => &[(1, 0), (2, 0), (3, 0), (0, 1), (1, 1)], + PieceShape::PentoZ => &[(0, 0), (1, 0), (1, 1), (1, 2), (2, 2)], + PieceShape::PentoI => &[(0, 0), (0, 1), (0, 2), (0, 3), (0, 4)], + PieceShape::PentoP => &[(0, 0), (1, 0), (0, 1), (1, 1), (0, 2)], + PieceShape::PentoW => &[(0, 0), (0, 1), (1, 1), (1, 2), (2, 2)], + PieceShape::PentoU => &[(0, 0), (0, 1), (1, 1), (2, 1), (2, 0)], + PieceShape::PentoR => &[(0, 1), (1, 1), (1, 2), (2, 1), (2, 0)], + PieceShape::PentoX => &[(1, 0), (0, 1), (1, 1), (2, 1), (1, 2)], + PieceShape::PentoY => &[(0, 1), (1, 0), (1, 1), (1, 2), (1, 3)], + }; + coords + .iter() + .map(|&(x, y)| Coordinate::new(x, y)) + .collect::>() + .align() + } + + /// Das kleinstmögliche Rechteck, das die Form umfasst. + pub fn dimension(&self) -> Vector { + self.coordinates().area() + } + + /// Die Form als Menge von Vektoren relativ zu (0,0). + pub fn as_vectors(&self) -> HashSet { + self.coordinates() + .iter() + .map(|c| c.get_difference(&Coordinate::new(0, 0))) + .collect() + } + + /// Die Anzahl der Felder, die diese Form belegt. + pub fn size(&self) -> usize { + self.coordinates().len() + } + + /// Alle eindeutigen Varianten der Form (Rotation + Spiegelung), ohne Duplikate. + /// Gibt eine Liste von (Koordinatenmenge, Rotation, isFlipped) zurück. + pub fn variants(&self) -> Vec<(HashSet, Rotation, bool)> { + let base = self.coordinates(); + let mut seen: Vec> = Vec::new(); + let mut result = Vec::new(); + + for rotation in Rotation::all() { + for flip in [false, true] { + let shape = base.rotate(rotation).flip(flip); + if !seen.contains(&shape) { + seen.push(shape.clone()); + result.push((shape, rotation, flip)); + } + } + } + result + } + + /// Transformiert die Form entsprechend Rotation und Spiegelung. + /// Entspricht Kotlins `transform`/`get`-Operator. + pub fn transform(&self, rotation: Rotation, should_flip: bool) -> HashSet { + self.coordinates().rotate(rotation).flip(should_flip) + } + + pub fn name(&self) -> String { + format!("{:?}", self) + } +} + +impl std::fmt::Display for PieceShape { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let name = match self { + PieceShape::Mono => "Mono", + PieceShape::Domino => "Domino", + PieceShape::TrioL => "Trio-L", + PieceShape::TrioI => "Trio-I", + PieceShape::TetroO => "Tetro-O", + PieceShape::TetroT => "Tetro-T", + PieceShape::TetroI => "Tetro-I", + PieceShape::TetroL => "Tetro-L", + PieceShape::TetroZ => "Tetro-Z", + PieceShape::PentoL => "Pento-L", + PieceShape::PentoT => "Pento-T", + PieceShape::PentoV => "Pento-V", + PieceShape::PentoS => "Pento-S", + PieceShape::PentoZ => "Pento-Z", + PieceShape::PentoI => "Pento-I", + PieceShape::PentoP => "Pento-P", + PieceShape::PentoW => "Pento-W", + PieceShape::PentoU => "Pento-U", + PieceShape::PentoR => "Pento-R", + PieceShape::PentoX => "Pento-X", + PieceShape::PentoY => "Pento-Y", + }; + write!(f, "{}", name) + } +} \ No newline at end of file diff --git a/src/plugin2027/rotation.rs b/src/plugin2027/rotation.rs new file mode 100644 index 0000000..fdd10e3 --- /dev/null +++ b/src/plugin2027/rotation.rs @@ -0,0 +1,49 @@ + +use pyo3::*; + +#[pyclass] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Rotation { + NONE, + RIGHT, + MIRROR, // 180 rotation actually + LEFT +} + +#[pymethods] +impl Rotation { + fn __str__(&self) -> String {self.to_string()} + fn __repr__(&self) -> String {format!("{:?}", self)} + fn __eq__(&self, other: &Rotation) -> bool {self == other} + fn __ne__(&self, other: &Rotation) -> bool {self != other} + + pub fn value(&self) -> usize { + match self { + Rotation::NONE => 0, + Rotation::RIGHT => 1, + Rotation::MIRROR => 2, + Rotation::LEFT => 3 + } + } + + pub fn rotate(&self, other: &Rotation) -> Rotation { + let variants = [Rotation::NONE, Rotation::RIGHT, Rotation::MIRROR, Rotation::LEFT]; + let sum = self.value() + other.value(); + variants[sum % variants.len()] + } + + #[staticmethod] + pub fn all() -> Vec { + vec![Rotation::NONE, Rotation::RIGHT, Rotation::MIRROR, Rotation::LEFT] + } + + pub fn name(&self) -> String { + format!("{:?}", self) + } +} + +impl std::fmt::Display for Rotation { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "rotated by {}°", {self.value() * 90}) + } +} \ No newline at end of file diff --git a/src/plugin2027/utils.rs b/src/plugin2027/utils.rs new file mode 100644 index 0000000..cc5ffed --- /dev/null +++ b/src/plugin2027/utils.rs @@ -0,0 +1,7 @@ +pub mod constants; +pub mod coordinate; +pub mod vector; +pub mod direction; +pub mod game_rule_logic; +pub mod team; +pub mod coordinate_helper; \ No newline at end of file diff --git a/src/plugin2027/utils/constants.rs b/src/plugin2027/utils/constants.rs new file mode 100644 index 0000000..8171dd9 --- /dev/null +++ b/src/plugin2027/utils/constants.rs @@ -0,0 +1,13 @@ +use pyo3::*; + +#[pyclass] +pub struct Constants; + +#[pymethods] +impl Constants { + pub const BOARD_LENGTH: usize = 20; + pub const ROUND_LIMIT: usize = 25; + pub const TOTAL_PIECE_SHAPES: usize = 21; + pub const COLORS: usize = 4; + pub const VALIDATE_MOVE: bool = true; +} diff --git a/src/plugin2027/utils/coordinate.rs b/src/plugin2027/utils/coordinate.rs new file mode 100644 index 0000000..122af3f --- /dev/null +++ b/src/plugin2027/utils/coordinate.rs @@ -0,0 +1,74 @@ +use pyo3::*; + +use crate::plugin2027::{ + utils::vector::Vector, utils::direction::Direction +}; + +#[pyclass] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Coordinate { + #[pyo3(get, set)] + pub x: isize, + #[pyo3(get, set)] + pub y: isize, +} + +#[pymethods] +impl Coordinate { + #[new] + pub fn new(x: isize, y: isize) -> Self { + Self { + x, y + } + } + + fn __str__(&self) -> String {self.to_string()} + fn __repr__(&self) -> String {format!("{:?}", self)} + fn __eq__(&self, other: &Coordinate) -> bool {self == other} + fn __ne__(&self, other: &Coordinate) -> bool {self != other} + fn deepcopy(&self) -> Coordinate {*self} + + pub fn add_vector(&self, vector: &Vector) -> Coordinate { + Coordinate { + x: self.x + vector.delta_x, + y: self.y + vector.delta_y + } + } + + pub fn add_vector_mut(&mut self, vector: &Vector) { + + self.x += vector.delta_x; + self.y += vector.delta_y; + } + + pub fn get_difference(&self, other: &Coordinate) -> Vector { + Vector { + delta_x: other.x - self.x, + delta_y: other.y - self.y + } + } + + pub fn neighbors(&self) -> Vec { + Direction::cardinals() + .iter() + .map(|d| self.add_vector(&d.to_vector())) + .collect() + } + + pub fn diagonal_neighbors(&self) -> Vec { + Direction::diagonals() + .iter() + .map(|d| self.add_vector(&d.to_vector())) + .collect() + } + + pub fn as_vector(&self) -> Vector { + Vector::new(self.x, self.y) + } +} + +impl std::fmt::Display for Coordinate { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "({}, {})", self.x, self.y) + } +} \ No newline at end of file diff --git a/src/plugin2027/utils/coordinate_helper.rs b/src/plugin2027/utils/coordinate_helper.rs new file mode 100644 index 0000000..777f39e --- /dev/null +++ b/src/plugin2027/utils/coordinate_helper.rs @@ -0,0 +1,73 @@ +use std::collections::HashSet; + +use crate::plugin2027::{ + utils::{coordinate::Coordinate, vector::Vector, constants::Constants}, + rotation::Rotation, +}; + +pub trait CoordinateSetExt { + fn rotate(&self, rotation: Rotation) -> HashSet; + fn flip(&self, should_flip: bool) -> HashSet; + fn mirror(&self) -> HashSet; + fn turn_right(&self) -> HashSet; + fn turn_left(&self) -> HashSet; + fn align(&self) -> HashSet; + fn area(&self) -> Vector; +} + +impl CoordinateSetExt for HashSet { + fn rotate(&self, rotation: Rotation) -> HashSet { + match rotation { + Rotation::NONE => self.clone(), + Rotation::RIGHT => self.turn_right().align(), + Rotation::MIRROR => self.mirror().align(), + Rotation::LEFT => self.turn_left().align(), + } + } + + fn flip(&self, should_flip: bool) -> HashSet { + if !should_flip { + self.clone() + } else { + self.iter() + .map(|c| Coordinate::new(-c.x, c.y)) + .collect::>() + .align() + } + } + + fn mirror(&self) -> HashSet { + self.iter().map(|c| Coordinate::new(-c.x, -c.y)).collect() + } + + fn turn_right(&self) -> HashSet { + self.iter().map(|c| Coordinate::new(-c.y, c.x)).collect() + } + + fn turn_left(&self) -> HashSet { + self.iter().map(|c| Coordinate::new(c.y, -c.x)).collect() + } + + fn align(&self) -> HashSet { + let board_length = Constants::BOARD_LENGTH as isize; + let mut min_x = board_length; + let mut min_y = board_length; + for c in self.iter() { + min_x = min_x.min(c.x); + min_y = min_y.min(c.y); + } + self.iter() + .map(|c| Coordinate::new(c.x - min_x, c.y - min_y)) + .collect() + } + + fn area(&self) -> Vector { + let mut dx = 0; + let mut dy = 0; + for c in self.iter() { + dx = dx.max(c.x); + dy = dy.max(c.y); + } + Vector::new(dx, dy) + } +} \ No newline at end of file diff --git a/src/plugin2027/utils/direction.rs b/src/plugin2027/utils/direction.rs new file mode 100644 index 0000000..3f0f685 --- /dev/null +++ b/src/plugin2027/utils/direction.rs @@ -0,0 +1,107 @@ +use pyo3::*; + +use crate::plugin2027::{ + utils::vector::Vector +}; + +#[pyclass] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Direction { + Up, + UpRight, + Right, + DownRight, + Down, + DownLeft, + Left, + UpLeft +} + +#[pymethods] +impl Direction { + fn __str__(&self) -> String {self.to_string()} + fn __repr__(&self) -> String {format!("{:?}", self)} + fn __eq__(&self, other: &Direction) -> bool {self == other} + fn __ne__(&self, other: &Direction) -> bool {self != other} + fn deepcopy(&self) -> Direction {*self} + + #[staticmethod] + pub fn from_vector(vector: &Vector) -> Option { + match (vector.delta_x, vector.delta_y) { + (0, 1) => Some(Direction::Up), + (1, 1) => Some(Direction::UpRight), + (1, 0) => Some(Direction::Right), + (1, -1) => Some(Direction::DownRight), + (0, -1) => Some(Direction::Down), + (-1, -1) => Some(Direction::DownLeft), + (-1, 0) => Some(Direction::Left), + (-1, 1) => Some(Direction::UpLeft), + _ => None, + } + } + + #[staticmethod] + pub fn all_directions() -> Vec { + vec![ + Direction::Up, + Direction::UpRight, + Direction::Right, + Direction::DownRight, + Direction::Down, + Direction::DownLeft, + Direction::Left, + Direction::UpLeft + ] + } + + pub fn to_vector(&self) -> Vector { + match self { + Direction::Up => Vector { delta_x: 0, delta_y: 1 }, + Direction::UpRight => Vector { delta_x: 1, delta_y: 1 }, + Direction::Right => Vector { delta_x: 1, delta_y: 0 }, + Direction::DownRight => Vector { delta_x: 1, delta_y: -1 }, + Direction::Down => Vector { delta_x: 0, delta_y: -1 }, + Direction::DownLeft => Vector { delta_x: -1, delta_y: -1 }, + Direction::Left => Vector { delta_x: -1, delta_y: 0 }, + Direction::UpLeft => Vector { delta_x: -1, delta_y: 1 }, + } + } + + pub fn to_mirrored(&self) -> Direction { + match self { + Direction::Up => Direction::Down, + Direction::UpRight => Direction::DownLeft, + Direction::Right => Direction::Left, + Direction::DownRight => Direction::UpLeft, + Direction::Down => Direction::Up, + Direction::DownLeft => Direction::UpRight, + Direction::Left => Direction::Right, + Direction::UpLeft => Direction::DownRight, + } + } + + #[staticmethod] + pub fn cardinals() -> Vec { + vec![Direction::Up, Direction::Right, Direction::Down, Direction::Left] + } + + #[staticmethod] + pub fn diagonals() -> Vec { + vec![Direction::UpRight, Direction::DownRight, Direction::DownLeft, Direction::UpLeft] + } +} + +impl std::fmt::Display for Direction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Direction::Up => write!(f, "(Up ↑)"), + Direction::UpRight => write!(f, "(UpRight ↗)"), + Direction::Right => write!(f, "(Right →)"), + Direction::DownRight => write!(f, "(DownRight ↘)"), + Direction::Down => write!(f, "(Down ↓)"), + Direction::DownLeft => write!(f, "(DownLeft ↙)"), + Direction::Left => write!(f, "(Left ←)"), + Direction::UpLeft => write!(f, "(UpLeft ↖)") + } + } +} diff --git a/src/plugin2027/utils/game_rule_logic.rs b/src/plugin2027/utils/game_rule_logic.rs new file mode 100644 index 0000000..2f4678f --- /dev/null +++ b/src/plugin2027/utils/game_rule_logic.rs @@ -0,0 +1,366 @@ +use pyo3::prelude::*; +use std::collections::HashSet; + +use crate::plugin2027::{ + board::Board, color::Color, r#move::Move, piece::Piece, piece_shape::PieceShape, + field::Field, game_state::GameState, + utils::{constants::Constants, coordinate::Coordinate, coordinate_helper::CoordinateSetExt}, + errors::BlokusMoveMistake, +}; + +#[allow(clippy::identity_op)] +pub const SUM_MAX_SQUARES: usize = 1 * 1 + 1 * 2 + 2 * 3 + 5 * 4 + 12 * 5; // = 89 + +#[pyclass] +pub struct GameRuleLogic; + +#[pymethods] +impl GameRuleLogic { + + #[staticmethod] + pub fn round_from_turn(turn: usize) -> usize { + 1 + turn / Constants::COLORS + } + + #[staticmethod] + pub fn get_points_from_undeployed(undeployed: Vec, mono_last: bool) -> usize { + if undeployed.is_empty() { + SUM_MAX_SQUARES + 15 + if mono_last { 5 } else { 0 } + } else { + SUM_MAX_SQUARES + - undeployed + .iter() + .map(|shape| shape.coordinates().len()) + .sum::() + } + } + + #[staticmethod] + pub fn perform_move(game_state: &mut GameState, mv: &Move) -> PyResult<()> { + if Constants::VALIDATE_MOVE { + Self::validate_move_color(game_state, mv)?; + } + match mv { + Move::SkipMove { .. } => Self::perform_skip_move(game_state)?, + Move::SetMove { piece } => Self::perform_set_move(game_state, piece)?, + } + game_state.advance(1); + game_state.last_move = Some(mv.clone()); + Ok(()) + } + + #[staticmethod] + pub fn validate_move_color(game_state: &GameState, mv: &Move) -> PyResult<()> { + if mv.get_color() != game_state.current_color() { + return Err(BlokusMoveMistake::WrongColor.to_py_err()); + } + Ok(()) + } + + #[staticmethod] + pub fn validate_set_move(game_state: &GameState, piece: &Piece) -> PyResult<()> { + Self::validate_move_color(game_state, &Move::SetMove { piece: piece.clone() })?; + Self::validate_shape(game_state, piece.kind, piece.color)?; + Self::validate_set_move_on_board(&game_state.board, piece)?; + + if Self::is_first_move(game_state) { + if !piece.coordinates().iter().any(|c| Self::is_on_border(*c)) { + return Err(BlokusMoveMistake::NotOnBorder.to_py_err()); + } + } else { + let touches_corner = piece.coordinates().iter().any(|c| { + Self::corners_on_color(&game_state.board, &Field { coordinate: *c, content: piece.color.to_field_content() }) + }); + if !touches_corner { + return Err(BlokusMoveMistake::NoSharedCorner.to_py_err()); + } + } + Ok(()) + } + + #[staticmethod] + pub fn perform_set_move(game_state: &mut GameState, piece: &Piece) -> PyResult<()> { + if Constants::VALIDATE_MOVE { + Self::validate_set_move(game_state, piece)?; + } + Self::perform_set_move_on_board(&mut game_state.board, piece); + game_state.remove_undeployed_piece(piece.color, piece.kind); + + if game_state.undeployed_piece_shapes(piece.color).is_empty() { + game_state + .last_move_mono + .insert(piece.color, piece.kind == PieceShape::Mono); + } + Ok(()) + } + + #[staticmethod] + pub fn validate_shape(game_state: &GameState, shape: PieceShape, color: Color) -> PyResult<()> { + if Self::is_first_move(game_state) { + if shape != game_state.start_piece { + return Err(BlokusMoveMistake::WrongShape.to_py_err()); + } + } else if !game_state.undeployed_piece_shapes(color).contains(&shape) { + return Err(BlokusMoveMistake::DuplicateShape.to_py_err()); + } + Ok(()) + } + + #[staticmethod] + pub fn is_valid_set_move(game_state: &GameState, piece: &Piece) -> bool { + Self::validate_set_move(game_state, piece).is_ok() + } + + #[staticmethod] + pub fn validate_set_move_on_board(board: &Board, piece: &Piece) -> PyResult<()> { + for coord in &piece.coordinates() { + if !Board::contains(*coord) { + return Err(BlokusMoveMistake::OutOfBounds.to_py_err()); + } + if board.is_obstructed(*coord) { + return Err(BlokusMoveMistake::Obstructed.to_py_err()); + } + let field = Field { coordinate: *coord, content: piece.color.to_field_content() }; + if Self::borders_on_color(board, &field) { + return Err(BlokusMoveMistake::TouchesSameColor.to_py_err()); + } + } + Ok(()) + } + + #[staticmethod] + pub fn validate_skip_move(game_state: &GameState) -> PyResult<()> { + if Self::is_first_move(game_state) { + return Err(BlokusMoveMistake::SkipFirstTurn.to_py_err()); + } + Ok(()) + } + + #[staticmethod] + pub fn perform_skip_move(game_state: &GameState) -> PyResult<()> { + Self::validate_skip_move(game_state) + } + + #[staticmethod] + pub fn borders_on_color(board: &Board, field: &Field) -> bool { + field + .coordinate + .neighbors() + .iter() + .any(|n| match board.get_content(*n) { + Some(content) => content == field.content && !field.content.is_empty(), + None => false, + }) + } + + #[staticmethod] + pub fn corners_on_color(board: &Board, field: &Field) -> bool { + field + .coordinate + .diagonal_neighbors() + .iter() + .any(|n| match board.get_content(*n) { + Some(content) => content == field.content && !field.content.is_empty(), + None => false, + }) + } + + #[staticmethod] + pub fn is_on_border(position: Coordinate) -> bool { + let max = Constants::BOARD_LENGTH as isize - 1; + position.x == 0 || position.x == max || position.y == 0 || position.y == max + } + + #[staticmethod] + pub fn is_first_move(game_state: &GameState) -> bool { + game_state.undeployed_piece_shapes(game_state.current_color()).len() == Constants::TOTAL_PIECE_SHAPES + } + + #[staticmethod] + pub fn get_random_start_pentomino() -> PieceShape { + use rand::seq::IndexedRandom; + let pentominoes: Vec = PieceShape::all() + .into_iter() + .filter(|shape| shape.coordinates().len() == 5) + .collect(); + *pentominoes.choose(&mut rand::rng()).unwrap() + } + + #[staticmethod] + pub fn remove_invalid_colors(game_state: &mut GameState) { + if !game_state.has_valid_colors() { + return; + } + let no_valid_move = Self::get_all_possible_moves(game_state) + .iter() + .all(|piece| !Self::is_valid_set_move(game_state, piece)); + + if no_valid_move { + game_state.remove_active_color(); + Self::remove_invalid_colors(game_state); + } + } + + #[staticmethod] + pub fn get_all_possible_moves(game_state: &GameState) -> Vec { + if Self::is_first_move(game_state) { + Self::get_possible_start_moves(game_state, false) + } else { + Self::get_possible_moves(game_state) + } + } + + #[staticmethod] + pub fn get_filtered_possible_moves(game_state: &GameState) -> Vec { + if Self::is_first_move(game_state) { + Self::get_possible_start_moves(game_state, true) + } else if game_state.round() <= 5 { + Self::get_pentomino_moves(game_state) + } else { + Self::get_possible_moves(game_state) + } + } + + #[staticmethod] + pub fn get_possible_start_moves(game_state: &GameState, filter: bool) -> Vec { + let mut moves = Vec::new(); + let kind = game_state.start_piece; + let color = game_state.current_color(); + + for (shape, rotation, is_flipped) in kind.variants() { + let area = shape.area(); + let mut border_coords: Vec = Vec::new(); + + let top_left_half = !filter || color == Color::BLUE || color == Color::YELLOW; + let bottom_right_half = !filter || color == Color::RED || color == Color::GREEN; + + let board_len = Constants::BOARD_LENGTH as isize; + + if top_left_half { + for x in 0..(board_len - area.delta_x - 1) { + border_coords.push(Coordinate::new(x, 0)); + } + for y in 1..(board_len - area.delta_y) { + border_coords.push(Coordinate::new(0, y)); + } + } + if bottom_right_half { + for y in 0..(board_len - area.delta_y - 1) { + border_coords.push(Coordinate::new(board_len - area.delta_x - 1, y)); + } + for x in 1..(board_len - area.delta_x) { + border_coords.push(Coordinate::new(x, board_len - area.delta_y - 1)); + } + } + + for position in border_coords { + let piece = Piece::new(color, kind, rotation, is_flipped, position); + if Self::is_valid_set_move(game_state, &piece) { + moves.push(piece); + } + } + } + moves + } + + #[staticmethod] + pub fn get_possible_moves(game_state: &GameState) -> Vec { + let color = game_state.current_color(); + let valid_fields = Self::get_valid_fields(&game_state.board, color); + let mut moves = Vec::new(); + for shape in game_state.undeployed_piece_shapes(color) { + moves.extend(Self::get_possible_moves_for_shape(game_state, shape, valid_fields.clone())); + } + moves + } + + #[staticmethod] + pub fn get_pentomino_moves(game_state: &GameState) -> Vec { + let color = game_state.current_color(); + let valid_fields = Self::get_valid_fields(&game_state.board, color); + let mut moves = Vec::new(); + for shape in game_state.undeployed_piece_shapes(color) { + if shape.coordinates().len() == 5 { + moves.extend(Self::get_possible_moves_for_shape(game_state, shape, valid_fields.clone())); + } + } + moves + } + + #[staticmethod] + pub fn get_possible_moves_for_shape( + game_state: &GameState, + shape: PieceShape, + valid_fields: HashSet, + ) -> Vec { + let mut moves: HashSet = HashSet::new(); + + if Self::is_first_move(game_state) { + if shape == game_state.start_piece { + return Self::get_possible_start_moves(game_state, false); + } else { + return Vec::new(); + } + } + + let color = game_state.current_color(); + for field in &valid_fields { + for (_variant_shape, rotation, is_flipped) in shape.variants() { + let area = shape.transform(rotation, is_flipped).area(); + for x in (field.x - area.delta_x)..=field.x { + for y in (field.y - area.delta_y)..=field.y { + let position = Coordinate::new(x, y); + let piece = Piece::new(color, shape, rotation, is_flipped, position); + if Self::is_valid_set_move(game_state, &piece) { + moves.insert(piece); + } + } + } + } + } + moves.into_iter().collect() + } + + #[staticmethod] + pub fn get_valid_fields(board: &Board, color: Color) -> HashSet { + let colored_fields = Self::get_colored_fields(board, color); + colored_fields + .iter() + .flat_map(|c| c.diagonal_neighbors()) + .filter(|corner| { + Board::contains(*corner) + && board.get_content(*corner).is_some_and(|c| c.is_empty()) + && corner + .neighbors() + .iter() + .all(|n| { + !Board::contains(*n) + || board.get_content(*n) != Some(color.to_field_content()) + }) + }) + .collect() + } + + #[staticmethod] + pub fn get_colored_fields(board: &Board, color: Color) -> HashSet { + let board_len = Constants::BOARD_LENGTH as isize; + let mut colored = HashSet::new(); + for x in 0..board_len { + for y in 0..board_len { + let pos = Coordinate::new(x, y); + if board.get_content(pos) == Some(color.to_field_content()) { + colored.insert(pos); + } + } + } + colored + } +} + +impl GameRuleLogic { + fn perform_set_move_on_board(board: &mut Board, piece: &Piece) { + for coord in &piece.coordinates() { + board.set_content(*coord, piece.color.to_field_content()); + } + } +} \ No newline at end of file diff --git a/src/plugin2027/utils/team.rs b/src/plugin2027/utils/team.rs new file mode 100644 index 0000000..d091b38 --- /dev/null +++ b/src/plugin2027/utils/team.rs @@ -0,0 +1,32 @@ +use pyo3::*; + +#[pyclass] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum TeamEnum { + One, + Two, +} + +#[pymethods] +impl TeamEnum { + fn __str__(&self) -> String {self.to_string()} + fn __repr__(&self) -> String {format!("{:?}", self)} + fn __eq__(&self, other: &TeamEnum) -> bool {self == other} + fn __ne__(&self, other: &TeamEnum) -> bool {self != other} + + pub fn opponent(&self) -> TeamEnum { + match self { + TeamEnum::One => TeamEnum::Two, + TeamEnum::Two => TeamEnum::One + } + } +} + +impl std::fmt::Display for TeamEnum { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::One => write!(f, "Team One"), + Self::Two => write!(f, "Team Two") + } + } +} \ No newline at end of file diff --git a/src/plugin2027/utils/vector.rs b/src/plugin2027/utils/vector.rs new file mode 100644 index 0000000..8b0b4af --- /dev/null +++ b/src/plugin2027/utils/vector.rs @@ -0,0 +1,66 @@ +use pyo3::*; + +#[pyclass] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Vector { + #[pyo3(get, set)] + pub delta_x: isize, + #[pyo3(get, set)] + pub delta_y: isize, +} + +#[pymethods] +impl Vector { + #[new] + pub fn new(delta_x: isize, delta_y: isize) -> Self { + Self { + delta_x, delta_y + } + } + + fn __str__(&self) -> String {self.to_string()} + fn __repr__(&self) -> String {format!("{:?}", self)} + fn __eq__(&self, other: &Vector) -> bool {self == other} + fn __ne__(&self, other: &Vector) -> bool {self != other} + fn deepcopy(&self) -> Vector {*self} + + pub fn add_vector(&self, other: &Vector) -> Vector { + Vector { + delta_x: self.delta_x + other.delta_x, + delta_y: self.delta_y + other.delta_y + } + } + + pub fn add_vector_mut(&mut self, other: &Vector) { + self.delta_x += other.delta_x; + self.delta_y += other.delta_y; + } + + pub fn scale(&self, scalar: isize) -> Vector { + Vector { + delta_x: self.delta_x * scalar, + delta_y: self.delta_y * scalar + } + } + + pub fn scale_mut(&mut self, scalar: isize) { + self.delta_x *= scalar; + self.delta_y *= scalar; + } + + pub fn get_length(&self) -> Option { + let squared_length = self.delta_x * self.delta_x + self.delta_y * self.delta_y; + + if squared_length < 0 { + None // Return None for negative numbers + } else { + Some((squared_length as f32).sqrt()) // Convert to f32 then compute sqrt + } + } +} + +impl std::fmt::Display for Vector { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "vec({}, {})", self.delta_x, self.delta_y) + } +} \ No newline at end of file From bfe19b03513e3c5d9b542e1b78b604ab8e144d49 Mon Sep 17 00:00:00 2001 From: YoEnte Date: Tue, 1 Sep 2026 08:51:59 +0200 Subject: [PATCH 2/4] fix linter --- python/socha/_socha.pyi | 212 ++++++-------------------- python/socha/api/protocol/protocol.py | 201 ++++++++++++------------ 2 files changed, 146 insertions(+), 267 deletions(-) diff --git a/python/socha/_socha.pyi b/python/socha/_socha.pyi index 7c5ac26..dbb0b25 100644 --- a/python/socha/_socha.pyi +++ b/python/socha/_socha.pyi @@ -1,5 +1,4 @@ from enum import Enum -from typing import Dict, List, Optional, Set, Tuple class Vector: """ @@ -14,34 +13,26 @@ class Vector: delta_y: int def __init__(self, delta_x: int, delta_y: int) -> None: ... - def __str__(self) -> str: ... - def __repr__(self) -> str: ... def __eq__(self, other: object) -> bool: ... def __ne__(self, other: object) -> bool: ... def deepcopy(self) -> Vector: """Kopiert das Objekt.""" - ... def add_vector(self, other: Vector) -> Vector: """Addiert einen anderen Vector (nicht mutierend).""" - ... def add_vector_mut(self, other: Vector) -> None: """Addiert einen anderen Vector (mutierend).""" - ... def scale(self, scalar: int) -> Vector: """Skaliert diesen Vektor (nicht mutierend).""" - ... def scale_mut(self, scalar: int) -> None: """Skaliert diesen Vektor (mutierend).""" - ... - def get_length(self) -> Optional[float]: + def get_length(self) -> float | None: """Berechnet die Länge dieses Vektors.""" - ... class Coordinate: @@ -57,38 +48,29 @@ class Coordinate: y: int def __init__(self, x: int, y: int) -> None: ... - def __str__(self) -> str: ... - def __repr__(self) -> str: ... def __eq__(self, other: object) -> bool: ... def __ne__(self, other: object) -> bool: ... def deepcopy(self) -> Coordinate: """Kopiert das Objekt.""" - ... def add_vector(self, vector: Vector) -> Coordinate: """Addiert einen Vector auf diese Koordinate (nicht mutierend).""" - ... def add_vector_mut(self, vector: Vector) -> None: """Addiert einen Vector auf diese Koordinate (mutierend).""" - ... def get_difference(self, other: Coordinate) -> Vector: """Berechnet die Differenz zwischen zwei Koordinaten als Vektor.""" - ... - def neighbors(self) -> List[Coordinate]: + def neighbors(self) -> list[Coordinate]: """Gibt die vier benachbarten Feldkoordinaten zurück.""" - ... - def diagonal_neighbors(self) -> List[Coordinate]: + def diagonal_neighbors(self) -> list[Coordinate]: """Gibt die vier angrenzenden Ecken der Feldkoordinaten zurück.""" - ... def as_vector(self) -> Vector: """Coordinate als Vektor Objekt""" - ... class Direction(Enum): @@ -103,39 +85,31 @@ class Direction(Enum): Left = 6 UpLeft = 7 - def __str__(self) -> str: ... - def __repr__(self) -> str: ... def __eq__(self, other: object) -> bool: ... def __ne__(self, other: object) -> bool: ... def deepcopy(self) -> Direction: ... @staticmethod - def from_vector(vector: Vector) -> Optional[Direction]: + def from_vector(vector: Vector) -> Direction | None: """Wandelt einen Vektor in eine der 8 Richtungen um.""" - ... @staticmethod - def all_directions() -> List[Direction]: + def all_directions() -> list[Direction]: """Gibt eine Liste aller 8 Richtungen zurück.""" - ... @staticmethod - def cardinals() -> List[Direction]: + def cardinals() -> list[Direction]: """Gibt die vier nicht-diagonalen Richtungen zurück (Up, Right, Down, Left).""" - ... @staticmethod - def diagonals() -> List[Direction]: + def diagonals() -> list[Direction]: """Gibt die vier diagonalen Richtungen zurück.""" - ... def to_vector(self) -> Vector: """Wandelt die Richtung in den entsprechenden Vektor um.""" - ... def to_mirrored(self) -> Direction: """Spiegelt die gegebene Richtung.""" - ... class TeamEnum(Enum): @@ -144,14 +118,11 @@ class TeamEnum(Enum): One = 0 Two = 1 - def __str__(self) -> str: ... - def __repr__(self) -> str: ... def __eq__(self, other: object) -> bool: ... def __ne__(self, other: object) -> bool: ... def opponent(self) -> TeamEnum: """Gibt den Gegner dieses Teams zurück.""" - ... class Color(Enum): @@ -162,28 +133,23 @@ class Color(Enum): RED = 2 GREEN = 3 - def __str__(self) -> str: ... - def __repr__(self) -> str: ... def __eq__(self, other: object) -> bool: ... def __ne__(self, other: object) -> bool: ... def __hash__(self) -> int: ... def next(self) -> Color: """Gibt die nächste Farbe in der Zugreihenfolge zurück.""" - ... def team(self) -> TeamEnum: """Gibt das Team zurück, zu dem diese Farbe gehört.""" - ... def to_field_content(self) -> FieldContent: """Wandelt die Farbe in den entsprechenden Feldinhalt um.""" - ... def name(self) -> str: - ... + class FieldContent(Enum): """Der Inhalt eines Feldes: eine Farbe oder leer.""" @@ -193,18 +159,14 @@ class FieldContent(Enum): GREEN = 3 EMPTY = 4 - def __str__(self) -> str: ... - def __repr__(self) -> str: ... def __eq__(self, other: object) -> bool: ... def __ne__(self, other: object) -> bool: ... - def to_team_color(self) -> Optional[Color]: + def to_team_color(self) -> Color | None: """Wandelt den Feldinhalt in die entsprechende Farbe um, oder None, wenn leer.""" - ... def is_empty(self) -> bool: """Gibt zurück, ob das Feld leer ist.""" - ... class Field: @@ -214,18 +176,14 @@ class Field: content: FieldContent def __init__(self, coordinate: Coordinate, color: Color) -> None: ... - def __str__(self) -> str: ... - def __repr__(self) -> str: ... def __eq__(self, other: object) -> bool: ... def __ne__(self, other: object) -> bool: ... def deepcopy(self) -> Field: """Kopiert das Objekt.""" - ... def is_empty(self) -> bool: """Gibt zurück, ob das Feld leer ist.""" - ... class Board: @@ -233,14 +191,12 @@ class Board: Das Spielbrett. Attributes: - map (List[List[Field]]): Die 2-dimensionale Liste der Felder. + map (list[list[Field]]): Die 2-dimensionale Liste der Felder. """ - map: List[List[Field]] + map: list[list[Field]] - def __init__(self, map: Optional[List[List[Field]]]) -> None: ... - def __str__(self) -> str: ... - def __repr__(self) -> str: ... + def __init__(self, map: list[list[Field]] | None) -> None: ... def __eq__(self, other: object) -> bool: ... def get(self, position: Coordinate) -> Field: @@ -250,45 +206,35 @@ class Board: Raises: IndexError: Wenn die Koordinate außerhalb des Spielfelds liegt. """ - ... def set_content(self, position: Coordinate, content: FieldContent) -> None: """Setzt den Inhalt eines Feldes.""" - ... - def get_content(self, position: Coordinate) -> Optional[FieldContent]: + def get_content(self, position: Coordinate) -> FieldContent | None: """Gibt den Inhalt eines Feldes zurück, oder None, wenn außerhalb des Feldes.""" - ... def is_empty(self) -> bool: """Prüft, ob alle Felder leer sind.""" - ... def is_obstructed(self, position: Coordinate) -> bool: """Prüft, ob auf dieser Position bereits eine Spielerfarbe liegt.""" - ... - def get_team(self, position: Coordinate) -> Optional[Color]: + def get_team(self, position: Coordinate) -> Color | None: """Gibt das Team zurück, das auf dem Feld liegt, oder None.""" - ... def pretty_string(self) -> str: """Gibt eine lesbare String-Darstellung des Spielfelds zurück.""" - ... - def compare(self, other: Board) -> List[Field]: + def compare(self, other: Board) -> list[Field]: """Vergleicht dieses Board mit einem anderen und gibt die unterschiedlichen Felder zurück.""" - ... @staticmethod - def random_fields() -> List[List[Field]]: + def random_fields() -> list[list[Field]]: """Erstellt ein leeres Spielfeld.""" - ... @staticmethod def contains(position: Coordinate) -> bool: """Prüft, ob die Koordinate innerhalb der Grenzen des Spielfelds liegt.""" - ... class Rotation(Enum): @@ -299,28 +245,22 @@ class Rotation(Enum): MIRROR = 2 LEFT = 3 - def __str__(self) -> str: ... - def __repr__(self) -> str: ... def __eq__(self, other: object) -> bool: ... def __ne__(self, other: object) -> bool: ... def value(self) -> int: """Gibt den numerischen Wert (Anzahl Vierteldrehungen) zurück.""" - ... def rotate(self, other: Rotation) -> Rotation: """Summiert beide Rotationen auf.""" - ... @staticmethod - def all() -> List[Rotation]: + def all() -> list[Rotation]: """Gibt alle vier Rotationen zurück.""" - ... def name(self) -> str: ... - class PieceShape(Enum): """Eine Enumeration aller 21 verschiedenen Formen.""" @@ -346,49 +286,39 @@ class PieceShape(Enum): PentoX = 19 PentoY = 20 - def __str__(self) -> str: ... - def __repr__(self) -> str: ... def __eq__(self, other: object) -> bool: ... def __ne__(self, other: object) -> bool: ... @staticmethod - def all() -> List[PieceShape]: + def all() -> list[PieceShape]: """Gibt alle 21 Formen in Reihenfolge zurück.""" - ... @staticmethod - def from_index(index: int) -> Optional[PieceShape]: + def from_index(index: int) -> PieceShape | None: """Gibt die Form anhand ihres Index zurück, oder None.""" - ... - def coordinates(self) -> Set[Coordinate]: + def coordinates(self) -> set[Coordinate]: """Die normalisierten Koordinaten der Grundform.""" - ... def dimension(self) -> Vector: """Das kleinstmögliche Rechteck, das die Form umfasst.""" - ... - def as_vectors(self) -> Set[Vector]: + def as_vectors(self) -> set[Vector]: """Die Form als Menge von Vektoren relativ zu (0,0).""" - ... def size(self) -> int: """Die Anzahl der Felder, die diese Form belegt.""" - ... - def variants(self) -> List[Tuple[Set[Coordinate], Rotation, bool]]: + def variants(self) -> list[tuple[set[Coordinate], Rotation, bool]]: """ Alle eindeutigen Varianten der Form (Rotation + Spiegelung), ohne Duplikate. Returns: Eine Liste von (Koordinatenmenge, Rotation, ist_gespiegelt)-Tupeln. """ - ... - def transform(self, rotation: Rotation, should_flip: bool) -> Set[Coordinate]: + def transform(self, rotation: Rotation, should_flip: bool) -> set[Coordinate]: """Transformiert die Form entsprechend Rotation und Spiegelung.""" - ... def name(self) -> str: ... @@ -410,50 +340,39 @@ class Piece: is_flipped: bool, position: Coordinate, ) -> None: ... - def __str__(self) -> str: ... - def __repr__(self) -> str: ... def __eq__(self, other: object) -> bool: ... def __ne__(self, other: object) -> bool: ... def __hash__(self) -> int: ... - def shape(self) -> Set[Coordinate]: + def shape(self) -> set[Coordinate]: """Die normalisierte Form des Steins (gedreht/gespiegelt, nicht verschoben).""" - ... - def coordinates(self) -> Set[Coordinate]: + def coordinates(self) -> set[Coordinate]: """Die tatsächlichen Koordinaten, die der Stein auf dem Feld einnimmt.""" - ... def transform(self, rotation: Rotation, is_flipped: bool) -> Piece: """Dreht/spiegelt den Stein, Position bleibt gleich.""" - ... class Move: """Repräsentiert einen Zug im Spiel: entweder ein SetMove oder ein SkipMove.""" - def __str__(self) -> str: ... - def __repr__(self) -> str: ... def __eq__(self, other: object) -> bool: ... def __ne__(self, other: object) -> bool: ... @staticmethod def set_move(piece: Piece) -> Move: """Erstellt einen Zug, der den gegebenen Stein platziert.""" - ... @staticmethod def skip_move(color: Color) -> Move: """Erstellt einen Zug, der die aktuelle Runde für die gegebene Farbe aussetzt.""" - ... def get_color(self) -> Color: """Die Farbe, die diesen Zug getätigt hat.""" - ... - def as_piece(self) -> Optional[Piece]: + def as_piece(self) -> Piece | None: """Gibt das verwendete Piece aus, wenn vorhanden""" - ... class GameState: @@ -462,95 +381,79 @@ class GameState: Attribute: turn (int): Die Anzahl der bereits getätigten Züge. - last_move (Optional[Move]): Der zuletzt gespielte Zug. + last_move (Move | None): Der zuletzt gespielte Zug. board (Board): Das aktuelle Spielfeld. start_piece (PieceShape): Der Spielstein, der im ersten Zug gesetzt werden muss. - last_move_mono (Dict[Color, bool]): Ob das Monomino zuletzt für jede Farbe gelegt wurde. + last_move_mono (dict[Color, bool]): Ob das Monomino zuletzt für jede Farbe gelegt wurde. """ turn: int - last_move: Optional[Move] + last_move: Move | None board: Board start_piece: PieceShape - last_move_mono: Dict[Color, bool] + last_move_mono: dict[Color, bool] def __init__( self, turn: int = 0, - last_move: Optional[Move] = None, - board: Optional[Board] = None, + last_move: Move | None = None, + board: Board | None = None, start_piece: PieceShape = PieceShape.Mono, - last_move_mono: Optional[Dict[Color, bool]] = None, + last_move_mono: dict[Color, bool] | None = None, ) -> None: ... - def __str__(self) -> str: ... - def __repr__(self) -> str: ... def __eq__(self, other: object) -> bool: ... def round(self) -> int: """Die aktuelle Rundenzahl.""" - ... - def undeployed_piece_shapes(self, color: Color) -> List[PieceShape]: + def undeployed_piece_shapes(self, color: Color) -> list[PieceShape]: """Gibt die noch nicht gesetzten Formen der gegebenen Farbe zurück.""" - ... def remove_undeployed_piece(self, color: Color, shape: PieceShape) -> bool: """Entfernt eine Form aus der Liste der noch nicht gesetzten Steine.""" - ... def current_color(self) -> Color: """Die Farbe, die aktuell am Zug ist.""" - ... def has_valid_colors(self) -> bool: """Gibt zurück, ob noch Farben im Spiel sind.""" - ... def is_valid_color(self, color: Color) -> bool: """Prüft, ob die gegebene Farbe noch im Spiel ist.""" - ... def remove_active_color(self) -> bool: """Entfernt die aktuell aktive Farbe aus dem Spiel und rückt vor.""" - ... def advance(self, turns: int = 1) -> bool: """Geht zum Zug der nächsten gültigen Farbe über.""" - ... def is_over(self) -> bool: """Gibt zurück, ob das Spiel vorbei ist.""" - ... - def possible_moves(self) -> List[Move]: + def possible_moves(self) -> list[Move]: """ Berechnet alle sinnvollen Züge der aktuellen Farbe. Enthält einen SkipMove nur, wenn kein anderer Zug möglich ist. """ - ... def get_points_for_color(self, color: Color) -> int: """Berechnet die Punkteanzahl für die gegebene Farbe.""" - ... def get_points_for_team(self, team: TeamEnum) -> int: """Berechnet die Punkteanzahl für das gegebene Team (Summe der Farben des Teams).""" - ... - def win_condition(self) -> Optional[TeamEnum]: + def win_condition(self) -> TeamEnum | None: """ Gibt das Gewinnerteam zurück, oder None bei einem Unentschieden. """ - ... class GameRuleLogic: """Eine Sammlung an statischen Methoden, die die Spielregeln logisch umsetzen.""" @staticmethod - def get_points_from_undeployed(undeployed: List[PieceShape], mono_last: bool = False) -> int: + def get_points_from_undeployed(undeployed: list[PieceShape], mono_last: bool = False) -> int: """Berechnet den Punktestand anhand der gegebenen, nicht gelegten Formen.""" - ... @staticmethod def perform_move(game_state: GameState, move: Move) -> None: @@ -560,7 +463,6 @@ class GameRuleLogic: Raises: Eine der Blokus-Move-Mistake-Exceptions, wenn der Zug nicht valide ist. """ - ... @staticmethod def validate_move_color(game_state: GameState, move: Move) -> None: @@ -570,7 +472,6 @@ class GameRuleLogic: Raises: WrongColor: Wenn die Farbe nicht am Zug ist. """ - ... @staticmethod def validate_set_move(game_state: GameState, piece: Piece) -> None: @@ -580,12 +481,10 @@ class GameRuleLogic: Raises: Eine der Blokus-Move-Mistake-Exceptions, wenn der Zug nicht valide ist. """ - ... @staticmethod def perform_set_move(game_state: GameState, piece: Piece) -> None: """Platziert den gegebenen Stein auf dem Spielfeld (mutierend, intern genutzt).""" - ... @staticmethod def validate_shape(game_state: GameState, shape: PieceShape, color: Color) -> None: @@ -596,12 +495,10 @@ class GameRuleLogic: WrongShape: Im ersten Zug, falls die falsche Form gewählt wurde. DuplicateShape: In folgenden Zügen, falls die Form bereits gesetzt wurde. """ - ... @staticmethod def is_valid_set_move(game_state: GameState, piece: Piece) -> bool: """Gibt zurück, ob der SetMove zulässig ist, ohne eine Exception zu werfen.""" - ... @staticmethod def validate_set_move_on_board(board: Board, piece: Piece) -> None: @@ -613,7 +510,6 @@ class GameRuleLogic: Obstructed: Wenn der Stein eine andere Farbe überlagern würde. TouchesSameColor: Wenn der Stein ein Feld gleicher Farbe berührt. """ - ... @staticmethod def validate_skip_move(game_state: GameState) -> None: @@ -623,87 +519,71 @@ class GameRuleLogic: Raises: SkipFirstTurn: Wenn im ersten Zug übersprungen werden soll. """ - ... @staticmethod def perform_skip_move(game_state: GameState) -> None: """Führt einen Skip-Zug aus (validiert, mutiert aber sonst nichts).""" - ... @staticmethod def borders_on_color(board: Board, field: Field) -> bool: """Prüft, ob das Feld an ein Feld gleicher Farbe angrenzt (Kante).""" - ... @staticmethod def corners_on_color(board: Board, field: Field) -> bool: """Prüft, ob das Feld an die Ecke eines Feldes gleicher Farbe angrenzt.""" - ... @staticmethod def is_on_border(position: Coordinate) -> bool: """Prüft, ob die Position am Rand des Spielfelds liegt.""" - ... @staticmethod def is_first_move(game_state: GameState) -> bool: """Gibt zurück, ob sich der GameState noch in der ersten Runde befindet.""" - ... @staticmethod def get_random_start_pentomino() -> PieceShape: """Gibt ein zufälliges Pentomino zurück (Startstein).""" - ... @staticmethod def remove_invalid_colors(game_state: GameState) -> None: """Entfernt rekursiv alle Farben, die keine Steine mehr platzieren können (mutierend).""" - ... @staticmethod - def get_all_possible_moves(game_state: GameState) -> List[Piece]: + def get_all_possible_moves(game_state: GameState) -> list[Piece]: """Gibt eine Liste aller möglichen SetMoves zurück (inkl. möglicher Startzüge).""" - ... @staticmethod - def get_filtered_possible_moves(game_state: GameState) -> List[Piece]: + def get_filtered_possible_moves(game_state: GameState) -> list[Piece]: """ Gibt eine gefilterte Liste möglicher SetMoves zurück: Startzüge, dann 5 Runden nur Pentominos, danach alle. """ - ... @staticmethod - def get_possible_start_moves(game_state: GameState, filter: bool = False) -> List[Piece]: + def get_possible_start_moves(game_state: GameState, filter: bool = False) -> list[Piece]: """Gibt alle möglichen SetMoves für den ersten Zug zurück.""" - ... @staticmethod - def get_possible_moves(game_state: GameState) -> List[Piece]: + def get_possible_moves(game_state: GameState) -> list[Piece]: """Gibt alle möglichen SetMoves (ohne Startzug) zurück.""" - ... @staticmethod - def get_pentomino_moves(game_state: GameState) -> List[Piece]: + def get_pentomino_moves(game_state: GameState) -> list[Piece]: """Gibt nur die möglichen SetMoves mit Pentominos zurück.""" - ... @staticmethod def get_possible_moves_for_shape( - game_state: GameState, shape: PieceShape, valid_fields: Set[Coordinate] - ) -> List[Piece]: + game_state: GameState, shape: PieceShape, valid_fields: set[Coordinate] + ) -> list[Piece]: """Gibt alle möglichen SetMoves für eine bestimmte Form zurück.""" - ... @staticmethod - def get_valid_fields(board: Board, color: Color) -> Set[Coordinate]: + def get_valid_fields(board: Board, color: Color) -> set[Coordinate]: """Gibt alle Koordinaten zurück, auf die die gegebene Farbe einen Stein platzieren könnte.""" - ... @staticmethod - def get_colored_fields(board: Board, color: Color) -> Set[Coordinate]: + def get_colored_fields(board: Board, color: Color) -> set[Coordinate]: """Gibt alle Koordinaten mit der gegebenen Farbe auf dem Board zurück.""" - ... class Constants: diff --git a/python/socha/api/protocol/protocol.py b/python/socha/api/protocol/protocol.py index 675a0da..3f08d96 100644 --- a/python/socha/api/protocol/protocol.py +++ b/python/socha/api/protocol/protocol.py @@ -1,5 +1,4 @@ from dataclasses import dataclass, field -from typing import List, Optional from socha._socha import TeamEnum @@ -25,11 +24,11 @@ class Position: class Meta: name = 'position' - x: Optional[int] = field( + x: int | None = field( default=None, metadata={'type': 'Attribute'}, ) - y: Optional[int] = field( + y: int | None = field( default=None, metadata={'type': 'Attribute'}, ) @@ -44,26 +43,26 @@ class Piece: class Meta: name = 'piece' - color: Optional[str] = field( + color: str | None = field( default=None, metadata={'type': 'Attribute'}, ) - kind: Optional[str] = field( + kind: str | None = field( default=None, metadata={'type': 'Attribute'}, ) - rotation: Optional[str] = field( + rotation: str | None = field( default=None, metadata={'type': 'Attribute'}, ) - is_flipped: Optional[bool] = field( + is_flipped: bool | None = field( default=None, metadata={ 'name': 'isFlipped', 'type': 'Attribute', }, ) - position: Optional[Position] = field( + position: Position | None = field( default=None, metadata={'type': 'Element'}, ) @@ -74,8 +73,8 @@ class LastMove: class Meta: name = 'lastMove' - class_binding: Optional[object] = field(default=None) - class_value: Optional[str] = field( + class_binding: object | None = field(default=None) + class_value: str | None = field( default=None, metadata={ 'name': 'class', @@ -83,13 +82,13 @@ class Meta: 'required': True, }, ) - piece: Optional[Piece] = field( + piece: Piece | None = field( default=None, metadata={'type': 'Element'}, ) # Für SkipMove: SkipMove.color hat kein @XStreamAsAttribute in Kotlin, # wird daher als Kindelement serialisiert (nicht als Attribut). - color: Optional[str] = field( + color: str | None = field( default=None, metadata={'type': 'Element'}, ) @@ -100,15 +99,15 @@ class Field: class Meta: name = 'field' - x: Optional[int] = field( + x: int | None = field( default=None, metadata={'type': 'Attribute'}, ) - y: Optional[int] = field( + y: int | None = field( default=None, metadata={'type': 'Attribute'}, ) - content: Optional[str] = field( + content: str | None = field( default=None, metadata={'type': 'Attribute'}, ) @@ -124,7 +123,7 @@ class Board: class Meta: name = 'board' - field_value: List[Field] = field( + field_value: list[Field] = field( default_factory=list, metadata={ 'name': 'field', @@ -134,24 +133,24 @@ class Meta: @dataclass -class ShapeList: +class Shapelist: """ Wiederverwendet für blueShapes / yellowShapes / redShapes / greenShapes. Enthält die noch nicht gesetzten Formen einer Farbe. """ - shape: List[str] = field( + shape: list[str] = field( default_factory=list, metadata={'type': 'Element'}, ) @dataclass -class ColorList: +class Colorlist: class Meta: name = 'validColors' - color: List[str] = field( + color: list[str] = field( default_factory=list, metadata={'type': 'Element'}, ) @@ -162,11 +161,11 @@ class LastMoveMonoEntry: class Meta: name = 'entry' - color: Optional[str] = field( + color: str | None = field( default=None, metadata={'type': 'Element'}, ) - value: Optional[bool] = field( + value: bool | None = field( default=None, metadata={ 'name': 'boolean', @@ -180,7 +179,7 @@ class LastMoveMono: class Meta: name = 'lastMoveMono' - entry: List[LastMoveMonoEntry] = field( + entry: list[LastMoveMonoEntry] = field( default_factory=list, metadata={'type': 'Element'}, ) @@ -191,7 +190,7 @@ class State(ObservableRoomMessage): class Meta: name = 'state' - class_value: Optional[str] = field( + class_value: str | None = field( default=None, metadata={ 'name': 'class', @@ -199,7 +198,7 @@ class Meta: 'required': True, }, ) - start_team: Optional[str] = field( + start_team: str | None = field( default=None, metadata={ 'name': 'startTeam', @@ -207,14 +206,14 @@ class Meta: 'required': True, }, ) - turn: Optional[int] = field( + turn: int | None = field( default=None, metadata={ 'type': 'Attribute', 'required': True, }, ) - start_piece: Optional[str] = field( + start_piece: str | None = field( default=None, metadata={ 'name': 'startPiece', @@ -222,63 +221,63 @@ class Meta: 'required': True, }, ) - round: Optional[int] = field( + round: int | None = field( default=None, metadata={ 'type': 'Attribute', 'required': True, }, ) - last_move: Optional[LastMove] = field( + last_move: LastMove | None = field( default=None, metadata={ 'name': 'lastMove', 'type': 'Element', }, ) - board: Optional[Board] = field( + board: Board | None = field( default=None, metadata={ 'type': 'Element', 'required': True, }, ) - last_move_mono: Optional[LastMoveMono] = field( + last_move_mono: LastMoveMono | None = field( default=None, metadata={ 'name': 'lastMoveMono', 'type': 'Element', }, ) - blue_shapes: Optional[ShapeList] = field( + blue_shapes: Shapelist | None = field( default=None, metadata={ 'name': 'blueShapes', 'type': 'Element', }, ) - yellow_shapes: Optional[ShapeList] = field( + yellow_shapes: Shapelist | None = field( default=None, metadata={ 'name': 'yellowShapes', 'type': 'Element', }, ) - red_shapes: Optional[ShapeList] = field( + red_shapes: Shapelist | None = field( default=None, metadata={ 'name': 'redShapes', 'type': 'Element', }, ) - green_shapes: Optional[ShapeList] = field( + green_shapes: Shapelist | None = field( default=None, metadata={ 'name': 'greenShapes', 'type': 'Element', }, ) - valid_colors: Optional[ColorList] = field( + valid_colors: Colorlist | None = field( default=None, metadata={ 'name': 'validColors', @@ -292,14 +291,14 @@ class Player: class Meta: name = 'player' - name: Optional[str] = field( + name: str | None = field( default=None, metadata={ 'type': 'Attribute', 'required': True, }, ) - team: Optional[str] = field( + team: str | None = field( default=None, metadata={ 'type': 'Attribute', @@ -313,14 +312,14 @@ class OriginalRequest(ProtocolPacket): class Meta: name = 'originalRequest' - class_value: Optional[str] = field( + class_value: str | None = field( default=None, metadata={ 'name': 'class', 'type': 'Attribute', }, ) - reservation_code: Optional[str] = field( + reservation_code: str | None = field( default=None, metadata={ 'name': 'reservationCode', @@ -334,13 +333,13 @@ class Errorpacket(ProtocolPacket): class Meta: name = 'errorpacket' - message: Optional[str] = field( + message: str | None = field( default=None, metadata={ 'type': 'Attribute', }, ) - original_request: Optional[OriginalRequest] = field( + original_request: OriginalRequest | None = field( default=None, metadata={ 'name': 'originalRequest', @@ -358,7 +357,7 @@ class Left(ProtocolPacket): class Meta: name = 'left' - room_id: Optional[str] = field( + room_id: str | None = field( default=None, metadata={ 'name': 'roomId', @@ -397,7 +396,7 @@ class Authenticate(AdminLobbyRequest): class Meta: name = 'authenticate' - password: Optional[str] = field( + password: str | None = field( default=None, metadata={ 'type': 'Attribute', @@ -414,7 +413,7 @@ class Cancel(AdminLobbyRequest): class Meta: name = 'cancel' - room_id: Optional[str] = field( + room_id: str | None = field( default=None, metadata={ 'name': 'roomId', @@ -432,14 +431,14 @@ class JoinedGameRoom(ObservableRoomMessage): class Meta: name = 'joinedGameRoom' - room_id: Optional[str] = field( + room_id: str | None = field( default=None, metadata={ 'name': 'roomId', 'type': 'Attribute', }, ) - player_count: Optional[int] = field( + player_count: int | None = field( default=None, metadata={ 'name': 'playerCount', @@ -457,7 +456,7 @@ class Observe(AdminLobbyRequest): class Meta: name = 'observe' - room_id: Optional[str] = field( + room_id: str | None = field( default=None, metadata={ 'name': 'roomId', @@ -475,14 +474,14 @@ class Pause(AdminLobbyRequest): class Meta: name = 'pause' - room_id: Optional[str] = field( + room_id: str | None = field( default=None, metadata={ 'name': 'roomId', 'type': 'Attribute', }, ) - pause: Optional[bool] = field( + pause: bool | None = field( default=None, metadata={ 'type': 'Attribute', @@ -499,21 +498,21 @@ class Slot(RoomOrchestrationMessage): class Meta: name = 'slot' - display_name: Optional[str] = field( + display_name: str | None = field( default=None, metadata={ 'name': 'displayName', 'type': 'Attribute', }, ) - can_timeout: Optional[bool] = field( + can_timeout: bool | None = field( default=None, metadata={ 'name': 'canTimeout', 'type': 'Attribute', }, ) - reserved: Optional[bool] = field( + reserved: bool | None = field( default=None, metadata={ 'type': 'Attribute', @@ -532,7 +531,7 @@ class Step(RoomOrchestrationMessage): class Meta: name = 'step' - room_id: Optional[str] = field( + room_id: str | None = field( default=None, metadata={ 'name': 'roomId', @@ -551,20 +550,20 @@ class Prepare(RoomOrchestrationMessage): class Meta: name = 'prepare' - game_type: Optional[str] = field( + game_type: str | None = field( default=None, metadata={ 'name': 'gameType', 'type': 'Attribute', }, ) - pause: Optional[bool] = field( + pause: bool | None = field( default=None, metadata={ 'type': 'Attribute', }, ) - slot: List[Slot] = field( + slot: list[Slot] = field( default_factory=list, metadata={ 'type': 'Element', @@ -593,7 +592,7 @@ class JoinPrepared(LobbyRequest): class Meta: name = 'joinPrepared' - reservation_code: Optional[str] = field( + reservation_code: str | None = field( default=None, metadata={ 'name': 'reservationCode', @@ -611,7 +610,7 @@ class JoinRoom(LobbyRequest): class Meta: name = 'joinRoom' - room_id: Optional[str] = field( + room_id: str | None = field( default=None, metadata={ 'name': 'roomId', @@ -629,19 +628,19 @@ class Fragment: class Meta: name = 'fragment' - name: Optional[str] = field( + name: str | None = field( default=None, metadata={ 'type': 'Attribute', }, ) - aggregation: Optional[str] = field( + aggregation: str | None = field( default=None, metadata={ 'type': 'Element', }, ) - relevant_for_ranking: Optional[bool] = field( + relevant_for_ranking: bool | None = field( default=None, metadata={ 'name': 'relevantForRanking', @@ -659,7 +658,7 @@ class Joined(ResponsePacket): class Meta: name = 'joined' - room_id: Optional[str] = field( + room_id: str | None = field( default=None, metadata={ 'name': 'roomId', @@ -677,7 +676,7 @@ class Score: class Meta: name = 'score' - part: List[int] = field( + part: list[int] = field( default_factory=list, metadata={ 'type': 'Element', @@ -691,21 +690,21 @@ class Winner: class Meta: name = 'winner' - team: Optional[str] = field( + team: str | None = field( default=None, metadata={ 'type': 'Attribute', 'required': True, }, ) - regular: Optional[bool] = field( + regular: bool | None = field( default=None, metadata={ 'type': 'Attribute', 'required': True, }, ) - reason: Optional[str] = field( + reason: str | None = field( default=None, metadata={ 'type': 'Attribute', @@ -725,7 +724,7 @@ class Definition: class Meta: name = 'definition' - fragment: List[Fragment] = field( + fragment: list[Fragment] = field( default_factory=list, metadata={ 'type': 'Element', @@ -743,13 +742,13 @@ class Entry: class Meta: name = 'entry' - player: Optional[Player] = field( + player: Player | None = field( default=None, metadata={ 'type': 'Element', }, ) - score: Optional[Score] = field( + score: Score | None = field( default=None, metadata={ 'type': 'Element', @@ -766,7 +765,7 @@ class Scores: class Meta: name = 'scores' - entry: List[Entry] = field( + entry: list[Entry] = field( default_factory=list, metadata={ 'type': 'Element', @@ -812,7 +811,7 @@ class OriginalMessage: class Meta: name = 'originalMessage' - class_value: Optional[str] = field( + class_value: str | None = field( default=None, metadata={ 'name': 'class', @@ -820,11 +819,11 @@ class Meta: 'required': True, }, ) - piece: Optional[Piece] = field( + piece: Piece | None = field( default=None, metadata={'type': 'Element'}, ) - color: Optional[str] = field( + color: str | None = field( default=None, metadata={'type': 'Element'}, ) @@ -845,7 +844,7 @@ class Data: class Meta: name = 'data' - class_value: Optional[str] = field( + class_value: str | None = field( default=None, metadata={ 'name': 'class', @@ -853,54 +852,54 @@ class Meta: 'required': True, }, ) - class_binding: Optional[object] = field(default=None) - definition: Optional[Definition] = field( + class_binding: object | None = field(default=None) + definition: Definition | None = field( default=None, metadata={ 'type': 'Element', }, ) - original_message: Optional[OriginalMessage] = field( + original_message: OriginalMessage | None = field( default=None, metadata={ 'name': 'originalMessage', 'type': 'Element', }, ) - scores: Optional[Scores] = field( + scores: Scores | None = field( default=None, metadata={ 'type': 'Element', }, ) - winner: Optional[Winner] = field( + winner: Winner | None = field( default=None, metadata={ 'type': 'Element', }, ) - state: Optional[State] = field( + state: State | None = field( default=None, metadata={ 'type': 'Element', }, ) # Nur für welcomeMessage: color="ONE"/"TWO" (TeamEnum), als Attribut. - color: Optional[str] = field( + color: str | None = field( default=None, metadata={ 'type': 'Attribute', }, ) # Für ausgehenden SetMove. - piece: Optional[Piece] = field( + piece: Piece | None = field( default=None, metadata={'type': 'Element'}, ) # Für ausgehenden SkipMove: eigenes Feld, da 'color' oben schon als # Attribut für welcomeMessage belegt ist. SkipMove.color ist ein # Kindelement (kein @XStreamAsAttribute in Kotlin). - skip_color: Optional[str] = field( + skip_color: str | None = field( default=None, metadata={ 'name': 'color', @@ -914,7 +913,7 @@ class Room(ProtocolPacket): class Meta: name = 'room' - room_id: Optional[str] = field( + room_id: str | None = field( default=None, metadata={ 'name': 'roomId', @@ -922,7 +921,7 @@ class Meta: 'required': True, }, ) - data: Optional[Data] = field( + data: Data | None = field( default=None, metadata={ 'type': 'Element', @@ -936,7 +935,7 @@ class Observed(RoomOrchestrationMessage): class Meta: name = 'observed' - room_id: Optional[str] = field( + room_id: str | None = field( default=None, metadata={ 'name': 'roomId', @@ -950,14 +949,14 @@ class Prepared(RoomOrchestrationMessage): class Meta: name = 'prepared' - room_id: Optional[str] = field( + room_id: str | None = field( default=None, metadata={ 'name': 'roomId', 'type': 'Attribute', }, ) - reservation: List[str] = field( + reservation: list[str] = field( default_factory=list, metadata={ 'type': 'Element', @@ -976,74 +975,74 @@ class Protocol: class Meta: name = 'protocol' - authenticate: Optional[Authenticate] = field( + authenticate: Authenticate | None = field( default=None, metadata={ 'type': 'Element', }, ) - joined_game_room: Optional[JoinedGameRoom] = field( + joined_game_room: JoinedGameRoom | None = field( default=None, metadata={ 'name': 'joinedGameRoom', 'type': 'Element', }, ) - prepare: Optional[Prepare] = field( + prepare: Prepare | None = field( default=None, metadata={ 'type': 'Element', }, ) - observe: Optional[Observe] = field( + observe: Observe | None = field( default=None, metadata={ 'type': 'Element', }, ) - pause: Optional[Pause] = field( + pause: Pause | None = field( default=None, metadata={ 'type': 'Element', }, ) - step: Optional[Step] = field( + step: Step | None = field( default=None, metadata={ 'type': 'Element', }, ) - cancel: Optional[Cancel] = field( + cancel: Cancel | None = field( default=None, metadata={ 'type': 'Element', }, ) - join: Optional[Join] = field( + join: Join | None = field( default=None, metadata={ 'type': 'Element', }, ) - joined: Optional[Joined] = field( + joined: Joined | None = field( default=None, metadata={ 'type': 'Element', }, ) - room: List[Room] = field( + room: list[Room] = field( default_factory=list, metadata={ 'type': 'Element', }, ) - prepared: Optional[Prepared] = field( + prepared: Prepared | None = field( default=None, metadata={ 'type': 'Element', }, ) - observed: Optional[Observed] = field( + observed: Observed | None = field( default=None, metadata={ 'type': 'Element', From 938ae0bbf2d05f511b7ae5a95d48b8dd5b702448 Mon Sep 17 00:00:00 2001 From: YoEnte Date: Wed, 2 Sep 2026 08:49:45 +0200 Subject: [PATCH 3/4] fix lints 2 --- docs/conf.py | 3 +- python/socha/_socha.pyi | 9 -- python/socha/api/networking/game_client.py | 93 ++++++++++--------- python/socha/api/networking/network_socket.py | 11 ++- python/socha/api/networking/utils.py | 7 +- .../api/networking/xml_protocol_interface.py | 39 ++++---- python/socha/api/protocol/protocol.py | 28 ++++-- python/socha/api/protocol/room_message.py | 8 +- python/socha/starter.py | 46 ++++----- python/socha/utils/package_builder.py | 55 +++++------ 10 files changed, 150 insertions(+), 149 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 95e19b0..affba55 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,6 @@ import os import sys + sys.path.insert(0, os.path.abspath('../')) # -- Project information ----------------------------------------------------- @@ -34,4 +35,4 @@ todo_include_todos = True -autoclass_content = 'both' +autoclass_content = 'both' \ No newline at end of file diff --git a/python/socha/_socha.pyi b/python/socha/_socha.pyi index dbb0b25..b819d50 100644 --- a/python/socha/_socha.pyi +++ b/python/socha/_socha.pyi @@ -598,36 +598,27 @@ class Constants: class WrongColor(Exception): """Die Farbe des Zuges ist nicht an der Reihe.""" - ... class NotOnBorder(Exception): """Der erste Zug muss an den Rand gesetzt werden.""" - ... class NoSharedCorner(Exception): """Alle Teile müssen ein vorheriges Teil gleicher Farbe über mindestens eine Ecke berühren.""" - ... class WrongShape(Exception): """Der erste Zug muss den festgelegten Spielstein setzen.""" - ... class SkipFirstTurn(Exception): """Der erste Zug muss einen Stein setzen.""" - ... class DuplicateShape(Exception): """Der gewählte Stein wurde bereits gesetzt.""" - ... class OutOfBounds(Exception): """Der Spielstein passt nicht vollständig auf das Spielfeld.""" - ... class Obstructed(Exception): """Der Spielstein würde eine andere Farbe überlagern.""" - ... class TouchesSameColor(Exception): """Der Spielstein berührt ein Feld gleicher Farbe.""" - ... \ No newline at end of file diff --git a/python/socha/api/networking/game_client.py b/python/socha/api/networking/game_client.py index b1d0ea1..f052465 100644 --- a/python/socha/api/networking/game_client.py +++ b/python/socha/api/networking/game_client.py @@ -7,15 +7,16 @@ import sys import threading import time -from typing import List, Union +from typing import ClassVar from socha._socha import GameState, Move - +from socha.api.networking.utils import handle_move, message_to_state from socha.api.networking.xml_protocol_interface import XMLProtocolInterface from socha.api.protocol.protocol import ( Authenticate, Cancel, Error, + Errorpacket, Join, Joined, JoinPrepared, @@ -33,14 +34,16 @@ State, Step, ) - -from socha.api.networking.utils import handle_move, message_to_state -from socha.api.protocol.protocol import Errorpacket from socha.api.protocol.protocol_packet import ProtocolPacket +logger = logging.getLogger(__name__) + +# custom "VERBOSE" level, between DEBUG and INFO, used for chatty protocol logs +VERBOSE = 15 + class IClientHandler: - history: List[List[Union[GameState, Error, Result]]] = [] + history: ClassVar[list[list[GameState | Error | Result]]] = [] def calculate_move(self) -> Move: """ @@ -118,7 +121,7 @@ def on_create_game(self, game_client: 'GameClient') -> None: """ def on_prepared( - self, game_client: 'GameClient', room_id: str, reservations: List[str] + self, game_client: 'GameClient', room_id: str, reservations: list[str] ) -> None: """ This method will be called if the client is in admin mode and the client has created a game. @@ -170,46 +173,46 @@ def __init__( self.headless = headless def join_game(self): - logging.info('Joining game') + logger.info('Joining game') self.send(Join()) def join_game_room(self, room_id: str): - logging.info(f"Joining game room '{room_id}'") + logger.info(f"Joining game room '{room_id}'") self.send(JoinRoom(room_id=room_id)) def join_game_with_reservation(self, reservation: str): - logging.info(f"Joining game with reservation '{reservation}'") + logger.info(f"Joining game with reservation '{reservation}'") self.send(JoinPrepared(reservation_code=reservation)) def authenticate(self, password: str): - logging.info(f"Authenticating with password '{password}'") + logger.info(f"Authenticating with password '{password}'") self.send(Authenticate(password=password)) def create_game(self, player_1: Slot, player_2: Slot, game_type: str, pause: bool): - logging.info( + logger.info( f"Creating game with {player_1}, {player_2} and game type '{game_type}'" ) self.send(Prepare(game_type=game_type, pause=pause, slot=[player_1, player_2])) def observe(self, room_id: str): - logging.info(f"Observing game room '{room_id}'") + logger.info(f"Observing game room '{room_id}'") self.send(Observe(room_id=room_id)) def cancel(self, room_id: str): - logging.info(f"Cancelling game room '{room_id}'") + logger.info(f"Cancelling game room '{room_id}'") self.send(Cancel(room_id=room_id)) def step(self, room_id: str): - logging.info(f"Stepping game room '{room_id}'") + logger.info(f"Stepping game room '{room_id}'") self.send(Step(room_id=room_id)) def pause(self, room_id: str, pause: bool): - logging.info(f"Set pause of game room '{room_id}' to '{pause}'") + logger.info(f"Set pause of game room '{room_id}' to '{pause}'") self.send(Pause(room_id=room_id, pause=pause)) def send_message_to_room(self, room_id: str, message): - logging.log(15, f"Sending message to room '{room_id}'") - logging.debug(f"Message is '{message}'") + logger.log(VERBOSE, f"Sending message to room '{room_id}'") + logger.debug(f"Message is '{message}'") self.send(Room(room_id=room_id, data=message)) def _on_object(self, message): @@ -224,18 +227,18 @@ def _on_object(self, message): """ if isinstance(message, Errorpacket): - logging.error(f'An error occurred while handling the request: {message}') + logger.error(f'An error occurred while handling the request: {message}') self._game_handler.on_error(str(message)) self.stop() elif isinstance(message, Joined): - logging.log(15, f"Game joined received with room id '{message.room_id}'") + logger.log(VERBOSE, f"Game joined received with room id '{message.room_id}'") self._game_handler.on_game_joined(room_id=message.room_id) elif isinstance(message, Left): - logging.log(15, f"Game left received with room id '{message.room_id}'") + logger.log(VERBOSE, f"Game left received with room id '{message.room_id}'") self._game_handler.on_game_left() elif isinstance(message, Prepared): - logging.log( - 15, f"Game prepared received with reservation '{message.reservation}'" + logger.log( + VERBOSE, f"Game prepared received with reservation '{message.reservation}'" ) self._game_handler.on_prepared( game_client=self, @@ -243,27 +246,27 @@ def _on_object(self, message): reservations=message.reservation, ) elif isinstance(message, Observed): - logging.log(15, f"Game observing received with room id '{message.room_id}'") + logger.log(VERBOSE, f"Game observing received with room id '{message.room_id}'") self._game_handler.on_observed(game_client=self, room_id=message.room_id) elif isinstance(message, Room) and not self.headless: room_id = message.room_id if isinstance(message.data.class_binding, MoveRequest): - logging.log(15, f"Move request received for room id '{room_id}'") + logger.log(VERBOSE, f"Move request received for room id '{room_id}'") self._on_move_request(room_id) elif isinstance(message.data.class_binding, State): - logging.log(15, f"State received for room id '{room_id}'") + logger.log(VERBOSE, f"State received for room id '{room_id}'") self._on_state(message) elif isinstance(message.data.class_binding, Result): - logging.info(f"Result received for room id '{room_id}'") - logging.info(f"Result was '{message.data.class_binding}'") + logger.info(f"Result received for room id '{room_id}'") + logger.info(f"Result was '{message.data.class_binding}'") self._game_handler.history[-1].append(message.data.class_binding) self._game_handler.on_game_over(message.data.class_binding) else: - logging.log(15, f"Room message received for room id '{room_id}'") + logger.log(VERBOSE, f"Room message received for room id '{room_id}'") self._game_handler.on_room_message(message.data.class_binding) else: room_id = message.room_id - logging.log(15, f"Room message received for room id '{room_id}'") + logger.log(VERBOSE, f"Room message received for room id '{room_id}'") self._game_handler.on_room_message(message) def _on_move_request(self, room_id): @@ -271,12 +274,12 @@ def _on_move_request(self, room_id): move_response = self._game_handler.calculate_move() if move_response: response = handle_move(move_response) - logging.info( + logger.info( f'Sent {move_response} after {round(time.time() - start_time, ndigits=3)} seconds.' ) self.send_message_to_room(room_id, response) else: - logging.error(f'{move_response} is not a valid move.') + logger.error(f'{move_response} is not a valid move.') def _on_state(self, message): _state = message_to_state(message) @@ -313,30 +316,30 @@ def _handle_left(self): self.first_time = True self.network_interface.close() if self.survive: - logging.info( + logger.info( 'The server left. Client is in survive mode and keeps running.\n' 'Please shutdown the client manually.' ) self._game_handler.while_disconnected(player_client=self) if self.auto_reconnect: - logging.info('The server left. Client tries to reconnect to the server.') + logger.info('The server left. Client tries to reconnect to the server.') for _ in range(3): - logging.info('Try to establish a connection with the server...') + logger.info('Try to establish a connection with the server...') try: self.connect() if self.network_interface.connected: - logging.info('Reconnected to server.') + logger.info('Reconnected to server.') break - except Exception as e: - logging.exception(e) - logging.info( + except Exception: + logger.exception('Failed to reconnect to the server.') + logger.info( "The client couldn't reconnect due to a previous error." ) self.stop() time.sleep(1) self.join() return - logging.info('The server left.') + logger.info('The server left.') self.stop() def _client_loop(self): @@ -351,7 +354,7 @@ def _client_loop(self): if not response: continue elif isinstance(response, ProtocolPacket): - logging.debug(f'Received new object: {response}') + logger.debug(f'Received new object: {response}') if while_waiting: while_waiting.join(timeout=0.0) if isinstance(response, Left): @@ -365,19 +368,19 @@ def _client_loop(self): while_waiting.start() gc.collect() elif self.running: - logging.error(f'Received a object of unknown class: {response}') + logger.error(f'Received a object of unknown class: {response}') raise NotImplementedError('Received object of unknown class.') else: self._game_handler.while_disconnected(player_client=self) - logging.info('Done.') + logger.info('Done.') sys.exit(0) def stop(self): """ Disconnects from the server and stops the client loop. """ - logging.info('Shutting down...') + logger.info('Shutting down...') if self.network_interface.connected: self.disconnect() - self.running = False + self.running = False \ No newline at end of file diff --git a/python/socha/api/networking/network_socket.py b/python/socha/api/networking/network_socket.py index 526bcd6..48f1afc 100644 --- a/python/socha/api/networking/network_socket.py +++ b/python/socha/api/networking/network_socket.py @@ -1,7 +1,8 @@ import logging import re import socket -from typing import Union + +logger = logging.getLogger(__name__) class NetworkSocket: @@ -52,7 +53,7 @@ def send(self, data: bytes): """ self.socket.sendall(data) - def receive(self) -> Union[bytes, None]: + def receive(self) -> bytes | None: """ Attempts to receive data from the server. The received data is processed using a regular expression to extract complete messages. If a complete message is found, it is returned as bytes and removed from the buffer. @@ -66,17 +67,17 @@ def receive(self) -> Union[bytes, None]: while True: try: chunk = self.socket.recv(16129) - except socket.timeout: + except TimeoutError: chunk = b"" except ConnectionResetError: self.close() return None if chunk: - logging.debug(f"Received message: {chunk}") + logger.debug(f"Received message: {chunk}") self.buffer += chunk if regex.search(self.buffer): receive = regex.search(self.buffer).group() self.buffer = self.buffer.replace(receive, b"") return receive else: - return None + return None \ No newline at end of file diff --git a/python/socha/api/networking/utils.py b/python/socha/api/networking/utils.py index f37a814..681cce6 100644 --- a/python/socha/api/networking/utils.py +++ b/python/socha/api/networking/utils.py @@ -1,5 +1,3 @@ -from typing import Optional - from socha import _socha from socha.api.protocol.protocol import ( Board, @@ -12,7 +10,6 @@ State, ) - # SCREAMING_SNAKE_CASE (Server) <-> CamelCase-Variantenname (Rust/_socha) _SHAPE_NAME_MAP = { "MONO": "Mono", @@ -96,7 +93,7 @@ def map_piece_to_protocol(piece: _socha.Piece) -> Piece: ) -def map_last_move(protocol_last_move: Optional[LastMove]) -> Optional[_socha.Move]: +def map_last_move(protocol_last_move: LastMove | None) -> _socha.Move | None: """ Konvertiert das lastMove-Element eines State in ein _socha.Move-Objekt. Kann entweder ein SetMove (piece gesetzt) oder ein SkipMove (color gesetzt) sein. @@ -110,7 +107,7 @@ def map_last_move(protocol_last_move: Optional[LastMove]) -> Optional[_socha.Mov return None -def map_last_move_mono(last_move_mono: Optional[LastMoveMono]) -> dict: +def map_last_move_mono(last_move_mono: LastMoveMono | None) -> dict: """ Konvertiert das lastMoveMono-Element (XStream-Standard-Map-Serialisierung) in ein Python-Dict[Color, bool]. diff --git a/python/socha/api/networking/xml_protocol_interface.py b/python/socha/api/networking/xml_protocol_interface.py index e043772..dbdf6a1 100644 --- a/python/socha/api/networking/xml_protocol_interface.py +++ b/python/socha/api/networking/xml_protocol_interface.py @@ -4,26 +4,29 @@ import contextlib import logging -from typing import Any, Callable, Iterator +from collections.abc import Callable, Iterator +from typing import Any + +from xsdata.formats.dataclass.context import XmlContext +from xsdata.formats.dataclass.parsers import XmlParser +from xsdata.formats.dataclass.parsers.config import ParserConfig +from xsdata.formats.dataclass.parsers.handlers import XmlEventHandler +from xsdata.formats.dataclass.serializers import XmlSerializer +from xsdata.formats.dataclass.serializers.config import SerializerConfig from socha import _socha from socha.api.networking.network_socket import NetworkSocket from socha.api.protocol.protocol import ( Close, + Data, Error, MoveRequest, Result, WelcomeMessage, ) from socha.api.protocol.protocol_packet import ProtocolPacket -from xsdata.formats.dataclass.context import XmlContext -from xsdata.formats.dataclass.parsers import XmlParser -from xsdata.formats.dataclass.parsers.config import ParserConfig -from xsdata.formats.dataclass.parsers.handlers import XmlEventHandler -from xsdata.formats.dataclass.serializers import XmlSerializer -from xsdata.formats.dataclass.serializers.config import SerializerConfig -from socha.api.protocol.protocol import Data +logger = logging.getLogger(__name__) def map_object(data: Data, params: dict): @@ -57,21 +60,19 @@ def map_object(data: Data, params: dict): ) return data(class_binding=error_object, **params) else: - logging.warning('Unknown class value: %s', params.get('class_value')) + logger.warning('Unknown class value: %s', params.get('class_value')) return data(**params) def custom_class_factory(clazz, params: dict): - # print("TEST01: ", clazz, params) - if clazz.__name__ == 'Data': return map_object(clazz, params) return clazz(**params) -PROTOCOL_PREFIX = ''.encode('utf-8') +PROTOCOL_PREFIX = b'' class XMLProtocolInterface: @@ -137,13 +138,11 @@ def _receive(self): cls = self._deserialize_object(receiving) return cls except OSError: - logging.error('An OSError occurred while receiving data from the server.') + logger.error('An OSError occurred while receiving data from the server.') self.running = False raise - except Exception as e: - logging.error( - 'An error occurred while receiving data from the server: %s', e - ) + except Exception: + logger.exception('An error occurred while receiving data from the server.') self.running = False raise @@ -165,11 +164,11 @@ def send(self, obj: ProtocolPacket) -> None: try: self.network_interface.send(shipment) - except Exception as e: - logging.exception('Error sending shipment to server: %s', e) + except Exception: + logger.exception('Error sending shipment to server.') raise else: - logging.debug('Sent shipment to server: %s', shipment) + logger.debug('Sent shipment to server: %s', shipment) self.first_time = False @contextlib.contextmanager diff --git a/python/socha/api/protocol/protocol.py b/python/socha/api/protocol/protocol.py index 3f08d96..0d57b7b 100644 --- a/python/socha/api/protocol/protocol.py +++ b/python/socha/api/protocol/protocol.py @@ -1,7 +1,6 @@ from dataclasses import dataclass, field from socha._socha import TeamEnum - from socha.api.protocol.protocol_packet import ( AdminLobbyRequest, LobbyRequest, @@ -133,7 +132,7 @@ class Meta: @dataclass -class Shapelist: +class ShapeList: """ Wiederverwendet für blueShapes / yellowShapes / redShapes / greenShapes. Enthält die noch nicht gesetzten Formen einer Farbe. @@ -146,7 +145,7 @@ class Shapelist: @dataclass -class Colorlist: +class ColorList: class Meta: name = 'validColors' @@ -158,12 +157,23 @@ class Meta: @dataclass class LastMoveMonoEntry: + """ + Ein Eintrag der lastMoveMono-HashMap, wie von XStreams + Standard-Map-Serialisierung erzeugt (kein eigener Converter registriert). + + UNBESTÄTIGT: Struktur beruht auf XStream-Standardverhalten, + da lastMoveMono in allen Aufzeichnungen bisher leer war. + """ + class Meta: name = 'entry' color: str | None = field( default=None, - metadata={'type': 'Element'}, + metadata={ + 'name': 'sc.plugin2027.Color', + 'type': 'Element', + }, ) value: bool | None = field( default=None, @@ -249,35 +259,35 @@ class Meta: 'type': 'Element', }, ) - blue_shapes: Shapelist | None = field( + blue_shapes: ShapeList | None = field( default=None, metadata={ 'name': 'blueShapes', 'type': 'Element', }, ) - yellow_shapes: Shapelist | None = field( + yellow_shapes: ShapeList | None = field( default=None, metadata={ 'name': 'yellowShapes', 'type': 'Element', }, ) - red_shapes: Shapelist | None = field( + red_shapes: ShapeList | None = field( default=None, metadata={ 'name': 'redShapes', 'type': 'Element', }, ) - green_shapes: Shapelist | None = field( + green_shapes: ShapeList | None = field( default=None, metadata={ 'name': 'greenShapes', 'type': 'Element', }, ) - valid_colors: Colorlist | None = field( + valid_colors: ColorList | None = field( default=None, metadata={ 'name': 'validColors', diff --git a/python/socha/api/protocol/room_message.py b/python/socha/api/protocol/room_message.py index e24cd3a..254ea40 100644 --- a/python/socha/api/protocol/room_message.py +++ b/python/socha/api/protocol/room_message.py @@ -6,20 +6,14 @@ class RoomMessage(ProtocolPacket): For all communication within a GameRoom. """ - ... - class RoomOrchestrationMessage(RoomMessage): """ A RoomMessage that does not concern the progress of the game. """ - ... - class ObservableRoomMessage(RoomMessage): """ A RoomMessage that can be received by observers. - """ - - ... + """ \ No newline at end of file diff --git a/python/socha/starter.py b/python/socha/starter.py index b722f26..c9df012 100644 --- a/python/socha/starter.py +++ b/python/socha/starter.py @@ -6,12 +6,16 @@ import datetime import json import logging +import sys import urllib.request import pkg_resources + from socha.api.networking.game_client import GameClient, IClientHandler from socha.utils.package_builder import SochaPackageBuilder +logger = logging.getLogger(__name__) + class Starter: """ @@ -25,17 +29,17 @@ def __init__( logic: IClientHandler, host: str = "localhost", port: int = 13050, - reservation: str = None, - room_id: str = None, - password: str = None, + reservation: str | None = None, + room_id: str | None = None, + password: str | None = None, survive: bool = False, auto_reconnect: bool = False, headless: bool = False, log: bool = False, verbose: bool = False, build: bool = False, - directory: str = None, - architecture: str = None, + directory: str | None = None, + architecture: str | None = None, log_level: int = logging.INFO, python_version: str = '3.10', ): @@ -69,26 +73,26 @@ def __init__( self.check_socha_version() - self.directory: str = args.directory or directory - self.architecture: str = args.architecture or architecture + self.directory: str | None = args.directory or directory + self.architecture: str | None = args.architecture or architecture self.build: str = args.build or build self.python_version: str = args.python_version or python_version if self.build: builder = SochaPackageBuilder(self.directory, self.architecture, self.python_version) builder.build_package() - exit(0) + sys.exit(0) self.host: str = args.host or host self.port: int = args.port or port - self.reservation: str = args.reservation or reservation - self.room_id: str = args.room or room_id + self.reservation: str | None = args.reservation or reservation + self.room_id: str | None = args.room or room_id if self.room_id and self.reservation: - logging.warning( + logger.warning( "The room ID is not taken into account because a reservation is available." ) - self.password: str = args.password or password + self.password: str | None = args.password or password if self.password and (self.reservation or self.room_id): - logging.warning( + logger.warning( "The password is not taken into account because a reservation or Room ID is available." ) self.survive: bool = args.survive or survive @@ -118,7 +122,7 @@ def _setup_debugger(self, verbose: bool, log_level: int): level: int = log_level if self.write_log: - now = datetime.datetime.now().strftime("%Y%m%d%H%M%S") + now = datetime.datetime.now(tz=datetime.timezone.utc).strftime("%Y%m%d%H%M%S") logging.basicConfig( filename=f"log{now}", level=level, @@ -129,8 +133,8 @@ def _setup_debugger(self, verbose: bool, log_level: int): logging.basicConfig( level=level, format="%(asctime)s: %(levelname)s - %(message)s" ) - logging.info("Starting...") - logging.info( + logger.info("Starting...") + logger.info( "We would greatly appreciate it if you could share any issues " "or feature requests you may have regarding socha by either creating " "an issue on our GitHub repository or contributing to the project." @@ -149,18 +153,18 @@ def check_socha_version(): json_data = json.loads(response.read()) latest_version = json_data["info"]["version"] if installed_version != latest_version: - logging.warning( + logger.warning( f"A newer version ({latest_version}) of {package_name} is available. You have version " f"{installed_version}." ) else: - logging.info( + logger.info( f"You're running the latest version of {package_name} ({latest_version})" ) except pkg_resources.DistributionNotFound: - logging.error(f"{package_name} is not installed.") + logger.error(f"{package_name} is not installed.") except urllib.error.URLError as e: - logging.warning( + logger.warning( f"Could not check the latest version of {package_name} due to {type(e).__name__}: {e}" ) @@ -249,4 +253,4 @@ def _handle_start_args(): help="Specifies the build python version (e.g.: 3.10 - this is standard).", ) - return parser.parse_args() + return parser.parse_args() \ No newline at end of file diff --git a/python/socha/utils/package_builder.py b/python/socha/utils/package_builder.py index af2d0d1..e5109dd 100644 --- a/python/socha/utils/package_builder.py +++ b/python/socha/utils/package_builder.py @@ -11,6 +11,8 @@ import types import zipfile +logger = logging.getLogger(__name__) + class SochaPackageBuilder: @@ -29,16 +31,16 @@ def _download_dependencies(self): req_file = os.path.join(current_dir, "requirements.txt") try: - with open(req_file, "r") as f: + with open(req_file) as f: requirements = f.read().splitlines() - except Exception as e: - logging.error(f"Error reading requirements file: {str(e)}") - logging.info( + except OSError as e: + logger.error(f"Error reading requirements file: {e!s}") + logger.info( "Please create a 'requirements.txt' in the same folder as your logic." ) sys.exit(1) - logging.info(f"Downloading the following packages: {requirements}") + logger.info(f"Downloading the following packages: {requirements}") # Download all dependencies to the dependencies directory try: @@ -59,7 +61,7 @@ def _download_dependencies(self): + requirements ) except subprocess.CalledProcessError as e: - logging.error(f"Error downloading dependencies: {str(e)}") + logger.error(f"Error downloading dependencies: {e!s}") sys.exit(1) @staticmethod @@ -72,10 +74,10 @@ def _create_build_directory(): def _create_directory_structure(self): try: if not os.path.exists(f"{self.build_dir}/{self.package_name}"): - logging.info(f"Creating directory {self.package_name}") + logger.info(f"Creating directory {self.package_name}") os.mkdir(f"{self.build_dir}/{self.package_name}") - logging.info(f"Creating directory {self.dependencies_dir}") + logger.info(f"Creating directory {self.dependencies_dir}") if not os.path.exists( f"{self.build_dir}/{self.package_name}/{self.dependencies_dir}" ): @@ -83,20 +85,20 @@ def _create_directory_structure(self): f"{self.build_dir}/{self.package_name}/{self.dependencies_dir}" ) - logging.info(f"Creating directory {self.packages_dir}") + logger.info(f"Creating directory {self.packages_dir}") if not os.path.exists( f"{self.build_dir}/{self.package_name}/{self.packages_dir}" ): os.mkdir(f"{self.build_dir}/{self.package_name}/{self.packages_dir}") - logging.info(f"Creating directory {self.cache_dir}") + logger.info(f"Creating directory {self.cache_dir}") if not os.path.exists( f"{self.build_dir}/{self.package_name}/{self.cache_dir}" ): os.mkdir(f"{self.build_dir}/{self.package_name}/{self.cache_dir}") - except OSError as e: - logging.error(f"Error creating directory: {e}") + except OSError: + logger.exception("Error creating directory.") sys.exit(1) @staticmethod @@ -112,12 +114,12 @@ def _get_modules(): # and are in the same directory or a subdirectory main_modules = set(sys.modules) - set(globals()) if main_module: - main_modules |= set( + main_modules |= { obj.__name__ for obj in gc.get_objects() if isinstance(obj, types.ModuleType) and obj.__name__.startswith(main_module.__name__) - ) + } main_modules = { name for name in main_modules @@ -145,7 +147,7 @@ def _copy_modules(self): Recursively searches for the given python file in the current working directory and its subdirectories, and copies all python files with their directory structure to the target_folder. """ - logging.info(f"Copying python files to {self.package_name}") + logger.info(f"Copying python files to {self.package_name}") source_folder = os.getcwd() main_modules = self._get_modules() for root, dirs, files in os.walk(source_folder): @@ -162,18 +164,18 @@ def _copy_modules(self): if file in sys.argv[0]: os.makedirs(os.path.dirname(target_file_path), exist_ok=True) shutil.copy2(source_file_path, target_file_path) - logging.info( + logger.info( f"Copying {source_file_path} to {target_file_path}" ) if source_file_path in main_modules: os.makedirs(os.path.dirname(target_file_path), exist_ok=True) shutil.copy2(source_file_path, target_file_path) - logging.info( + logger.info( f"Copying {source_file_path} to {target_file_path}" ) def _create_shell_script(self): - logging.info(f"Creating shell script {self.package_name}/start.sh") + logger.info(f"Creating shell script {self.package_name}/start.sh") with open( f"{self.build_dir}/{self.package_name}/start.sh", "w", newline="\n" ) as f: @@ -200,10 +202,9 @@ def _create_shell_script(self): ) # Add all downloaded packages to the pip install command - for package in os.listdir( + f.writelines(f"./{self.package_name}/{self.dependencies_dir}/{package} " for package in os.listdir( f"{self.build_dir}/{self.package_name}/{self.dependencies_dir}" - ): - f.write(f"./{self.package_name}/{self.dependencies_dir}/{package} ") + )) f.write("\n\n") f.write( @@ -214,7 +215,7 @@ def _create_shell_script(self): ) def _zipdir(self): - logging.info(f"Zipping directory {self.package_name}") + logger.info(f"Zipping directory {self.package_name}") try: zipf = zipfile.ZipFile( f"{self.build_dir}/{self.package_name}.zip", "w", zipfile.ZIP_DEFLATED @@ -229,13 +230,13 @@ def _zipdir(self): arc_name = os.path.relpath(dir_path, self.build_dir) zipf.write(dir_path, arcname=arc_name) zipf.close() - logging.info(f"{self.package_name}.zip successfully created!") - except Exception as e: - logging.error(f"Error creating {self.package_name}.zip: {str(e)}") + logger.info(f"{self.package_name}.zip successfully created!") + except (OSError, zipfile.BadZipFile) as e: + logger.error(f"Error creating {self.package_name}.zip: {e!s}") sys.exit(1) def build_package(self): - logging.info("Building package...") + logger.info("Building package...") # Create the directory structure self._create_directory_structure() @@ -253,4 +254,4 @@ def build_package(self): self._zipdir() # Log a success message - logging.info(f"{self.package_name} package successfully built!") + logger.info(f"{self.package_name} package successfully built!") \ No newline at end of file From ddb4388966587e6c4f481497753bad8b74f9cce7 Mon Sep 17 00:00:00 2001 From: YoEnte Date: Wed, 2 Sep 2026 17:27:07 +0200 Subject: [PATCH 4/4] back to Optional and ignore linter --- python/socha/api/protocol/protocol.py | 179 +++++++++++++------------- 1 file changed, 91 insertions(+), 88 deletions(-) diff --git a/python/socha/api/protocol/protocol.py b/python/socha/api/protocol/protocol.py index 0d57b7b..da15c8c 100644 --- a/python/socha/api/protocol/protocol.py +++ b/python/socha/api/protocol/protocol.py @@ -1,4 +1,7 @@ +# ruff: noqa: UP045 + from dataclasses import dataclass, field +from typing import Optional from socha._socha import TeamEnum from socha.api.protocol.protocol_packet import ( @@ -23,11 +26,11 @@ class Position: class Meta: name = 'position' - x: int | None = field( + x: Optional[int] = field( default=None, metadata={'type': 'Attribute'}, ) - y: int | None = field( + y: Optional[int] = field( default=None, metadata={'type': 'Attribute'}, ) @@ -42,26 +45,26 @@ class Piece: class Meta: name = 'piece' - color: str | None = field( + color: Optional[str] = field( default=None, metadata={'type': 'Attribute'}, ) - kind: str | None = field( + kind: Optional[str] = field( default=None, metadata={'type': 'Attribute'}, ) - rotation: str | None = field( + rotation: Optional[str] = field( default=None, metadata={'type': 'Attribute'}, ) - is_flipped: bool | None = field( + is_flipped: Optional[bool] = field( default=None, metadata={ 'name': 'isFlipped', 'type': 'Attribute', }, ) - position: Position | None = field( + position: Optional[Position] = field( default=None, metadata={'type': 'Element'}, ) @@ -72,8 +75,8 @@ class LastMove: class Meta: name = 'lastMove' - class_binding: object | None = field(default=None) - class_value: str | None = field( + class_binding: Optional[object] = field(default=None) + class_value: Optional[str] = field( default=None, metadata={ 'name': 'class', @@ -81,13 +84,13 @@ class Meta: 'required': True, }, ) - piece: Piece | None = field( + piece: Optional[Piece] = field( default=None, metadata={'type': 'Element'}, ) # Für SkipMove: SkipMove.color hat kein @XStreamAsAttribute in Kotlin, # wird daher als Kindelement serialisiert (nicht als Attribut). - color: str | None = field( + color: Optional[str] = field( default=None, metadata={'type': 'Element'}, ) @@ -98,15 +101,15 @@ class Field: class Meta: name = 'field' - x: int | None = field( + x: Optional[int] = field( default=None, metadata={'type': 'Attribute'}, ) - y: int | None = field( + y: Optional[int] = field( default=None, metadata={'type': 'Attribute'}, ) - content: str | None = field( + content: Optional[str] = field( default=None, metadata={'type': 'Attribute'}, ) @@ -168,14 +171,14 @@ class LastMoveMonoEntry: class Meta: name = 'entry' - color: str | None = field( + color: Optional[str] = field( default=None, metadata={ 'name': 'sc.plugin2027.Color', 'type': 'Element', }, ) - value: bool | None = field( + value: Optional[bool] = field( default=None, metadata={ 'name': 'boolean', @@ -200,7 +203,7 @@ class State(ObservableRoomMessage): class Meta: name = 'state' - class_value: str | None = field( + class_value: Optional[str] = field( default=None, metadata={ 'name': 'class', @@ -208,7 +211,7 @@ class Meta: 'required': True, }, ) - start_team: str | None = field( + start_team: Optional[str] = field( default=None, metadata={ 'name': 'startTeam', @@ -216,14 +219,14 @@ class Meta: 'required': True, }, ) - turn: int | None = field( + turn: Optional[int] = field( default=None, metadata={ 'type': 'Attribute', 'required': True, }, ) - start_piece: str | None = field( + start_piece: Optional[str] = field( default=None, metadata={ 'name': 'startPiece', @@ -231,63 +234,63 @@ class Meta: 'required': True, }, ) - round: int | None = field( + round: Optional[int] = field( default=None, metadata={ 'type': 'Attribute', 'required': True, }, ) - last_move: LastMove | None = field( + last_move: Optional[LastMove] = field( default=None, metadata={ 'name': 'lastMove', 'type': 'Element', }, ) - board: Board | None = field( + board: Optional[Board] = field( default=None, metadata={ 'type': 'Element', 'required': True, }, ) - last_move_mono: LastMoveMono | None = field( + last_move_mono: Optional[LastMoveMono] = field( default=None, metadata={ 'name': 'lastMoveMono', 'type': 'Element', }, ) - blue_shapes: ShapeList | None = field( + blue_shapes: Optional[ShapeList] = field( default=None, metadata={ 'name': 'blueShapes', 'type': 'Element', }, ) - yellow_shapes: ShapeList | None = field( + yellow_shapes: Optional[ShapeList] = field( default=None, metadata={ 'name': 'yellowShapes', 'type': 'Element', }, ) - red_shapes: ShapeList | None = field( + red_shapes: Optional[ShapeList] = field( default=None, metadata={ 'name': 'redShapes', 'type': 'Element', }, ) - green_shapes: ShapeList | None = field( + green_shapes: Optional[ShapeList] = field( default=None, metadata={ 'name': 'greenShapes', 'type': 'Element', }, ) - valid_colors: ColorList | None = field( + valid_colors: Optional[ColorList] = field( default=None, metadata={ 'name': 'validColors', @@ -301,14 +304,14 @@ class Player: class Meta: name = 'player' - name: str | None = field( + name: Optional[str] = field( default=None, metadata={ 'type': 'Attribute', 'required': True, }, ) - team: str | None = field( + team: Optional[str] = field( default=None, metadata={ 'type': 'Attribute', @@ -322,14 +325,14 @@ class OriginalRequest(ProtocolPacket): class Meta: name = 'originalRequest' - class_value: str | None = field( + class_value: Optional[str] = field( default=None, metadata={ 'name': 'class', 'type': 'Attribute', }, ) - reservation_code: str | None = field( + reservation_code: Optional[str] = field( default=None, metadata={ 'name': 'reservationCode', @@ -343,13 +346,13 @@ class Errorpacket(ProtocolPacket): class Meta: name = 'errorpacket' - message: str | None = field( + message: Optional[str] = field( default=None, metadata={ 'type': 'Attribute', }, ) - original_request: OriginalRequest | None = field( + original_request: Optional[OriginalRequest] = field( default=None, metadata={ 'name': 'originalRequest', @@ -367,7 +370,7 @@ class Left(ProtocolPacket): class Meta: name = 'left' - room_id: str | None = field( + room_id: Optional[str] = field( default=None, metadata={ 'name': 'roomId', @@ -406,7 +409,7 @@ class Authenticate(AdminLobbyRequest): class Meta: name = 'authenticate' - password: str | None = field( + password: Optional[str] = field( default=None, metadata={ 'type': 'Attribute', @@ -423,7 +426,7 @@ class Cancel(AdminLobbyRequest): class Meta: name = 'cancel' - room_id: str | None = field( + room_id: Optional[str] = field( default=None, metadata={ 'name': 'roomId', @@ -441,14 +444,14 @@ class JoinedGameRoom(ObservableRoomMessage): class Meta: name = 'joinedGameRoom' - room_id: str | None = field( + room_id: Optional[str] = field( default=None, metadata={ 'name': 'roomId', 'type': 'Attribute', }, ) - player_count: int | None = field( + player_count: Optional[int] = field( default=None, metadata={ 'name': 'playerCount', @@ -466,7 +469,7 @@ class Observe(AdminLobbyRequest): class Meta: name = 'observe' - room_id: str | None = field( + room_id: Optional[str] = field( default=None, metadata={ 'name': 'roomId', @@ -484,14 +487,14 @@ class Pause(AdminLobbyRequest): class Meta: name = 'pause' - room_id: str | None = field( + room_id: Optional[str] = field( default=None, metadata={ 'name': 'roomId', 'type': 'Attribute', }, ) - pause: bool | None = field( + pause: Optional[bool] = field( default=None, metadata={ 'type': 'Attribute', @@ -508,21 +511,21 @@ class Slot(RoomOrchestrationMessage): class Meta: name = 'slot' - display_name: str | None = field( + display_name: Optional[str] = field( default=None, metadata={ 'name': 'displayName', 'type': 'Attribute', }, ) - can_timeout: bool | None = field( + can_timeout: Optional[bool] = field( default=None, metadata={ 'name': 'canTimeout', 'type': 'Attribute', }, ) - reserved: bool | None = field( + reserved: Optional[bool] = field( default=None, metadata={ 'type': 'Attribute', @@ -541,7 +544,7 @@ class Step(RoomOrchestrationMessage): class Meta: name = 'step' - room_id: str | None = field( + room_id: Optional[str] = field( default=None, metadata={ 'name': 'roomId', @@ -560,14 +563,14 @@ class Prepare(RoomOrchestrationMessage): class Meta: name = 'prepare' - game_type: str | None = field( + game_type: Optional[str] = field( default=None, metadata={ 'name': 'gameType', 'type': 'Attribute', }, ) - pause: bool | None = field( + pause: Optional[bool] = field( default=None, metadata={ 'type': 'Attribute', @@ -602,7 +605,7 @@ class JoinPrepared(LobbyRequest): class Meta: name = 'joinPrepared' - reservation_code: str | None = field( + reservation_code: Optional[str] = field( default=None, metadata={ 'name': 'reservationCode', @@ -620,7 +623,7 @@ class JoinRoom(LobbyRequest): class Meta: name = 'joinRoom' - room_id: str | None = field( + room_id: Optional[str] = field( default=None, metadata={ 'name': 'roomId', @@ -638,19 +641,19 @@ class Fragment: class Meta: name = 'fragment' - name: str | None = field( + name: Optional[str] = field( default=None, metadata={ 'type': 'Attribute', }, ) - aggregation: str | None = field( + aggregation: Optional[str] = field( default=None, metadata={ 'type': 'Element', }, ) - relevant_for_ranking: bool | None = field( + relevant_for_ranking: Optional[bool] = field( default=None, metadata={ 'name': 'relevantForRanking', @@ -668,7 +671,7 @@ class Joined(ResponsePacket): class Meta: name = 'joined' - room_id: str | None = field( + room_id: Optional[str] = field( default=None, metadata={ 'name': 'roomId', @@ -700,21 +703,21 @@ class Winner: class Meta: name = 'winner' - team: str | None = field( + team: Optional[str] = field( default=None, metadata={ 'type': 'Attribute', 'required': True, }, ) - regular: bool | None = field( + regular: Optional[bool] = field( default=None, metadata={ 'type': 'Attribute', 'required': True, }, ) - reason: str | None = field( + reason: Optional[str] = field( default=None, metadata={ 'type': 'Attribute', @@ -752,13 +755,13 @@ class Entry: class Meta: name = 'entry' - player: Player | None = field( + player: Optional[Player] = field( default=None, metadata={ 'type': 'Element', }, ) - score: Score | None = field( + score: Optional[Score] = field( default=None, metadata={ 'type': 'Element', @@ -821,7 +824,7 @@ class OriginalMessage: class Meta: name = 'originalMessage' - class_value: str | None = field( + class_value: Optional[str] = field( default=None, metadata={ 'name': 'class', @@ -829,11 +832,11 @@ class Meta: 'required': True, }, ) - piece: Piece | None = field( + piece: Optional[Piece] = field( default=None, metadata={'type': 'Element'}, ) - color: str | None = field( + color: Optional[str] = field( default=None, metadata={'type': 'Element'}, ) @@ -854,7 +857,7 @@ class Data: class Meta: name = 'data' - class_value: str | None = field( + class_value: Optional[str] = field( default=None, metadata={ 'name': 'class', @@ -862,54 +865,54 @@ class Meta: 'required': True, }, ) - class_binding: object | None = field(default=None) - definition: Definition | None = field( + class_binding: Optional[object] = field(default=None) + definition: Optional[Definition] = field( default=None, metadata={ 'type': 'Element', }, ) - original_message: OriginalMessage | None = field( + original_message: Optional[OriginalMessage] = field( default=None, metadata={ 'name': 'originalMessage', 'type': 'Element', }, ) - scores: Scores | None = field( + scores: Optional[Scores] = field( default=None, metadata={ 'type': 'Element', }, ) - winner: Winner | None = field( + winner: Optional[Winner] = field( default=None, metadata={ 'type': 'Element', }, ) - state: State | None = field( + state: Optional[State] = field( default=None, metadata={ 'type': 'Element', }, ) # Nur für welcomeMessage: color="ONE"/"TWO" (TeamEnum), als Attribut. - color: str | None = field( + color: Optional[str] = field( default=None, metadata={ 'type': 'Attribute', }, ) # Für ausgehenden SetMove. - piece: Piece | None = field( + piece: Optional[Piece] = field( default=None, metadata={'type': 'Element'}, ) # Für ausgehenden SkipMove: eigenes Feld, da 'color' oben schon als # Attribut für welcomeMessage belegt ist. SkipMove.color ist ein # Kindelement (kein @XStreamAsAttribute in Kotlin). - skip_color: str | None = field( + skip_color: Optional[str] = field( default=None, metadata={ 'name': 'color', @@ -923,7 +926,7 @@ class Room(ProtocolPacket): class Meta: name = 'room' - room_id: str | None = field( + room_id: Optional[str] = field( default=None, metadata={ 'name': 'roomId', @@ -931,7 +934,7 @@ class Meta: 'required': True, }, ) - data: Data | None = field( + data: Optional[Data] = field( default=None, metadata={ 'type': 'Element', @@ -945,7 +948,7 @@ class Observed(RoomOrchestrationMessage): class Meta: name = 'observed' - room_id: str | None = field( + room_id: Optional[str] = field( default=None, metadata={ 'name': 'roomId', @@ -959,7 +962,7 @@ class Prepared(RoomOrchestrationMessage): class Meta: name = 'prepared' - room_id: str | None = field( + room_id: Optional[str] = field( default=None, metadata={ 'name': 'roomId', @@ -985,56 +988,56 @@ class Protocol: class Meta: name = 'protocol' - authenticate: Authenticate | None = field( + authenticate: Optional[Authenticate] = field( default=None, metadata={ 'type': 'Element', }, ) - joined_game_room: JoinedGameRoom | None = field( + joined_game_room: Optional[JoinedGameRoom] = field( default=None, metadata={ 'name': 'joinedGameRoom', 'type': 'Element', }, ) - prepare: Prepare | None = field( + prepare: Optional[Prepare] = field( default=None, metadata={ 'type': 'Element', }, ) - observe: Observe | None = field( + observe: Optional[Observe] = field( default=None, metadata={ 'type': 'Element', }, ) - pause: Pause | None = field( + pause: Optional[Pause] = field( default=None, metadata={ 'type': 'Element', }, ) - step: Step | None = field( + step: Optional[Step] = field( default=None, metadata={ 'type': 'Element', }, ) - cancel: Cancel | None = field( + cancel: Optional[Cancel] = field( default=None, metadata={ 'type': 'Element', }, ) - join: Join | None = field( + join: Optional[Join] = field( default=None, metadata={ 'type': 'Element', }, ) - joined: Joined | None = field( + joined: Optional[Joined] = field( default=None, metadata={ 'type': 'Element', @@ -1046,13 +1049,13 @@ class Meta: 'type': 'Element', }, ) - prepared: Prepared | None = field( + prepared: Optional[Prepared] = field( default=None, metadata={ 'type': 'Element', }, ) - observed: Observed | None = field( + observed: Optional[Observed] = field( default=None, metadata={ 'type': 'Element',