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 @@

-# Python-Client für die Software-Challenge Germany 2025
+# Python-Client für die Software-Challenge Germany 2027
[](https://socha-python-client.readthedocs.io/de/latest/)
[](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/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/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..b819d50 100644
--- a/python/socha/_socha.pyi
+++ b/python/socha/_socha.pyi
@@ -1,5 +1,39 @@
from enum import Enum
-from typing import List, Optional
+
+class Vector:
+ """
+ Ein 2 dimensionaler Vektor.
+
+ Attributes:
+ delta_x (int): Die Entfernung in x-Richtung.
+ delta_y (int): Die Entfernung in y-Richtung.
+ """
+
+ delta_x: int
+ delta_y: int
+
+ def __init__(self, delta_x: int, delta_y: int) -> None: ...
+ 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) -> float | None:
+ """Berechnet die Länge dieses Vektors."""
+
class Coordinate:
"""
@@ -14,825 +48,577 @@ class Coordinate:
y: int
def __init__(self, x: int, 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.
-
- Args:
- other: (Coordinate): Die andere Koordinate.
-
- Returns:
- bool: Das Ergebnis des Vergleichs.
- """
- ...
- def __ne__(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) -> Coordinate:
- """
- Kopiert das Objekt rekursiv.
- """
- ...
+ def deepcopy(self) -> Coordinate:
+ """Kopiert das Objekt."""
def add_vector(self, vector: Vector) -> Coordinate:
- """
- Addiert einen Vector auf die Werte dieser Koordinate (**nicht mutierend**).
+ """Addiert einen Vector auf diese Koordinate (nicht mutierend)."""
- Args:
- vector (Vector): Der Vektor.
+ def add_vector_mut(self, vector: Vector) -> None:
+ """Addiert einen Vector auf diese Koordinate (mutierend)."""
- Returns:
- Coordinate: Ein neues Koordinatenobjekt mit den berechneten Werten.
- """
- ...
+ def get_difference(self, other: Coordinate) -> Vector:
+ """Berechnet die Differenz zwischen zwei Koordinaten als Vektor."""
- def add_vector_mut(self, vector: Vector) -> None:
- """
- Addiert einen Vector auf die Werte dieser Koordinate (**mutierend**).
+ def neighbors(self) -> list[Coordinate]:
+ """Gibt die vier benachbarten Feldkoordinaten zurück."""
- Args:
- vector (Vector): Der Vektor.
- """
- ...
+ def diagonal_neighbors(self) -> list[Coordinate]:
+ """Gibt die vier angrenzenden Ecken der Feldkoordinaten zurück."""
- def get_difference(self, other: Coordinate) -> Vector:
- """
- Berechnet die Differenz zwischen zwei Koordinaten Punkten als Vektor.
+ def as_vector(self) -> Vector:
+ """Coordinate als Vektor Objekt"""
- Args:
- other (Coordinate): Die andere Koordinate
- Returns:
- Vector: Der Vektor zwischen den Punkten
- """
- ...
+class Direction(Enum):
+ """Eine Darstellung für eine normierte Richtung."""
-class Vector:
- """
- Ein 2 dimensionaler Vektor.
+ Up = 0
+ UpRight = 1
+ Right = 2
+ DownRight = 3
+ Down = 4
+ DownLeft = 5
+ Left = 6
+ UpLeft = 7
- Attributes:
- delta_x (int): Die Entfernung in x-Richtung.
- delta_y (int): Die Entfernung in y-Richtung.
- """
+ def __eq__(self, other: object) -> bool: ...
+ def __ne__(self, other: object) -> bool: ...
+ def deepcopy(self) -> Direction: ...
- delta_x: int
- delta_y: int
+ @staticmethod
+ def from_vector(vector: Vector) -> Direction | None:
+ """Wandelt einen Vektor in eine der 8 Richtungen um."""
- def __init__(self, delta_x: int, delta_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.
+ @staticmethod
+ def all_directions() -> list[Direction]:
+ """Gibt eine Liste aller 8 Richtungen zurück."""
- Args:
- other: (Vector): Die andere Koordinate.
+ @staticmethod
+ def cardinals() -> list[Direction]:
+ """Gibt die vier nicht-diagonalen Richtungen zurück (Up, Right, Down, Left)."""
- Returns:
- bool: Das Ergebnis des Vergleichs.
- """
- ...
- def __ne__(self, other: Vector) -> bool:
- """
- Unterstützt den Vergleichsoperator !=, um die Werte mit denen eines
- weiteren Objektes zuvergleichen.
+ @staticmethod
+ def diagonals() -> list[Direction]:
+ """Gibt die vier diagonalen Richtungen zurück."""
- Args:
- other: (Vector): Die andere Koordinate.
+ def to_vector(self) -> Vector:
+ """Wandelt die Richtung in den entsprechenden Vektor um."""
- Returns:
- bool: Das Ergebnis des Vergleichs.
- """
- ...
-
- def deepcopy(self) -> GameState:
- """
- Kopiert das Objekt rekursiv.
- """
- ...
+ def to_mirrored(self) -> Direction:
+ """Spiegelt die gegebene Richtung."""
- def add_vector(self, other: Vector) -> Vector:
- """
- Addiert einen anderen Vector auf die Werte dieses Vektors (**nicht mutierend**).
- Args:
- other (Vector): Der andere Vektor.
+class TeamEnum(Enum):
+ """Eine Darstellung für die beiden Teams."""
- Returns:
- Vector: Ein neues Vektorobjekt mit den berechneten Werten.
- """
- ...
+ One = 0
+ Two = 1
- def add_vector_mut(self, other: Vector) -> None:
- """
- Addiert einen anderen Vector auf die Werte dieses Vektors (**mutierend**).
+ def __eq__(self, other: object) -> bool: ...
+ def __ne__(self, other: object) -> bool: ...
- Args:
- other (Vector): Der andere Vektor.
- """
- ...
+ def opponent(self) -> TeamEnum:
+ """Gibt den Gegner dieses Teams zurück."""
- def scale(self, scalar: int) -> Vector:
- """
- Skaliert diesen Vektor um ein gegebenes Skalar (**nicht mutierend**).
- Args:
- scalar (int): Das Skalar.
+class Color(Enum):
+ """Die Farbe eines Spielsteins / Teams."""
- Returns:
- Vector: Ein neues Vektorobjekt mit den berechneten Werten.
- """
- ...
+ BLUE = 0
+ YELLOW = 1
+ RED = 2
+ GREEN = 3
- def scale_mut(self, scalar: int) -> None:
- """
- Skaliert diesen Vektor um ein gegebenes Skalar (**mutierend**).
+ def __eq__(self, other: object) -> bool: ...
+ def __ne__(self, other: object) -> bool: ...
+ def __hash__(self) -> int: ...
- Args:
- scalar (int): Das Skalar.
- """
- ...
+ def next(self) -> Color:
+ """Gibt die nächste Farbe in der Zugreihenfolge zurück."""
- def get_length(self) -> float:
- """
- Berechnet die Länge dieses Vektors.
+ def team(self) -> TeamEnum:
+ """Gibt das Team zurück, zu dem diese Farbe gehört."""
- Returns:
- float: Die Länge des Vektors (in 32 bit Präzision).
- """
+ def to_field_content(self) -> FieldContent:
+ """Wandelt die Farbe in den entsprechenden Feldinhalt um."""
+
+ def name(self) -> str:
...
-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 __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.
+class FieldContent(Enum):
+ """Der Inhalt eines Feldes: eine Farbe oder leer."""
- Args:
- other: (Direction): Die andere Koordinate.
+ BLUE = 0
+ YELLOW = 1
+ RED = 2
+ GREEN = 3
+ EMPTY = 4
- Returns:
- bool: Das Ergebnis des Vergleichs.
- """
- ...
- def __ne__(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.
+ def to_team_color(self) -> Color | None:
+ """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."""
- @staticmethod
- def from_vector(vector: Vector) -> Optional[Direction]:
- """
- Wandelt einen Vektor in eine der 8 Richtungen um, insofern der Vektor exakt der Richtung entspricht.
- Args:
- vector (Vector): Der Vektor, der konvertiert werden soll.
+class Field:
+ """Ein einzelnes Feld auf dem Spielbrett."""
- 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.
- """
- ...
+ coordinate: Coordinate
+ content: FieldContent
- def to_vector(self) -> Vector:
- """
- Wandelt die Richtung in den entsprechenden Vektor um.
+ def __init__(self, coordinate: Coordinate, color: Color) -> None: ...
+ def __eq__(self, other: object) -> bool: ...
+ def __ne__(self, other: object) -> bool: ...
- Returns:
- Vector: Der Richtungsvektor.
- """
- ...
+ def deepcopy(self) -> Field:
+ """Kopiert das Objekt."""
- def to_mirrored(self) -> Direction:
- """
- Spiegelt die gegebene Richtung.
- Beispiel: Up -> Down.
+ def is_empty(self) -> bool:
+ """Gibt zurück, ob das Feld leer ist."""
- Returns:
- Direction: Die neue Richtung.
- """
- ...
-class FieldType(Enum):
- """
- Stellt alle verfügbaren Feldtypen dar.
+class Board:
"""
+ Das Spielbrett.
- 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.
+ Attributes:
+ map (list[list[Field]]): Die 2-dimensionale Liste der Felder.
"""
- 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.
+ map: list[list[Field]]
- Args:
- other: (FieldType): Die andere Koordinate.
+ def __init__(self, map: list[list[Field]] | None) -> None: ...
+ def __eq__(self, other: object) -> bool: ...
- Returns:
- bool: Das Ergebnis des Vergleichs.
+ def get(self, position: Coordinate) -> Field:
"""
- ...
- 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.
- """
- ...
+ Gibt das Feld an der gegebenen Koordinate zurück.
- 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.
+ Raises:
+ IndexError: Wenn die Koordinate außerhalb des Spielfelds liegt.
"""
- ...
- def get_team(self) -> Optional[TeamEnum]:
- """
- Gibt für ein Fischfeld aus, zu welchem Team dieser Fisch gehört.
+ def set_content(self, position: Coordinate, content: FieldContent) -> None:
+ """Setzt den Inhalt eines Feldes."""
- Returns:
- Optional[TeamEnum]: Das Team, zudem der Fisch gehört, oder None, wenn das Feld kein Fisch ist.
- """
- ...
+ def get_content(self, position: Coordinate) -> FieldContent | None:
+ """Gibt den Inhalt eines Feldes zurück, oder None, wenn außerhalb des Feldes."""
- @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:
+ """Prüft, ob alle Felder leer sind."""
-class TeamEnum(Enum):
- """
- Eine Darstellung für die beiden Teams
- """
+ def is_obstructed(self, position: Coordinate) -> bool:
+ """Prüft, ob auf dieser Position bereits eine Spielerfarbe liegt."""
- One = 0
- """
- Team 1
- """
- Two = 1
- """
- Team 2
- """
+ def get_team(self, position: Coordinate) -> Color | None:
+ """Gibt das Team zurück, das auf dem Feld liegt, oder 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 pretty_string(self) -> str:
+ """Gibt eine lesbare String-Darstellung des Spielfelds zurück."""
- Args:
- other: (TeamEnum): Die andere Koordinate.
+ def compare(self, other: Board) -> list[Field]:
+ """Vergleicht dieses Board mit einem anderen und gibt die unterschiedlichen Felder zurück."""
- Returns:
- bool: Das Ergebnis des Vergleichs.
- """
- ...
- def __ne__(self, other: TeamEnum) -> bool:
- """
- Unterstützt den Vergleichsoperator !=, um die Werte mit denen eines
- weiteren Objektes zuvergleichen.
+ @staticmethod
+ def random_fields() -> list[list[Field]]:
+ """Erstellt ein leeres Spielfeld."""
- Args:
- other: (TeamEnum): Die andere Koordinate.
+ @staticmethod
+ def contains(position: Coordinate) -> bool:
+ """Prüft, ob die Koordinate innerhalb der Grenzen des Spielfelds liegt."""
- Returns:
- bool: Das Ergebnis des Vergleichs.
- """
- ...
- def get_fish_types(self) -> List[FieldType]:
- """
- Gibt eine Liste aller Fischtypen des Teams aus.
+class Rotation(Enum):
+ """Beschreibt, wie weit eine PieceShape gedreht werden soll."""
- Returns:
- List[FieldType]: Die Liste der Feldtypen.
- """
- ...
+ NONE = 0
+ RIGHT = 1
+ MIRROR = 2
+ LEFT = 3
- def opponent(self) -> TeamEnum:
- """
- Gibt den Gegner dieses Teams an.
+ def __eq__(self, other: object) -> bool: ...
+ def __ne__(self, other: object) -> bool: ...
- Return:
- TeamEnum: Das Gegnerteam.
- """
- ...
+ def value(self) -> int:
+ """Gibt den numerischen Wert (Anzahl Vierteldrehungen) zurück."""
-class Board:
- """
- Ein Spielbrett, das die Felder des Spiels enthält.
+ def rotate(self, other: Rotation) -> Rotation:
+ """Summiert beide Rotationen auf."""
- 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 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
+
+ def __eq__(self, other: object) -> bool: ...
+ def __ne__(self, other: object) -> bool: ...
- Attributes:
- map (List[List[Field]]): Die 2 dimensionale Liste der Felder, die das Spielbrett darstellen.
- """
+ @staticmethod
+ def all() -> list[PieceShape]:
+ """Gibt alle 21 Formen in Reihenfolge zurück."""
- map: List[List[FieldType]]
+ @staticmethod
+ def from_index(index: int) -> PieceShape | None:
+ """Gibt die Form anhand ihres Index zurück, oder None."""
- def __init__(self, map: List[List[FieldType]]) -> 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 coordinates(self) -> set[Coordinate]:
+ """Die normalisierten Koordinaten der Grundform."""
- Args:
- other: (TeamEnum): Die andere Koordinate.
+ def dimension(self) -> Vector:
+ """Das kleinstmögliche Rechteck, das die Form umfasst."""
- Returns:
- bool: Das Ergebnis des Vergleichs.
- """
- ...
- def __ne__(self, other: TeamEnum) -> bool:
- """
- Unterstützt den Vergleichsoperator !=, um die Werte mit denen eines
- weiteren Objektes zuvergleichen.
+ def as_vectors(self) -> set[Vector]:
+ """Die Form als Menge von Vektoren relativ zu (0,0)."""
- Args:
- other: (TeamEnum): Die andere Koordinate.
+ def size(self) -> int:
+ """Die Anzahl der Felder, die diese Form belegt."""
- Returns:
- bool: Das Ergebnis des Vergleichs.
- """
- ...
-
- def deepcopy(self) -> GameState:
+ def variants(self) -> list[tuple[set[Coordinate], Rotation, bool]]:
"""
- Kopiert das Objekt rekursiv.
- """
- ...
+ Alle eindeutigen Varianten der Form (Rotation + Spiegelung), ohne Duplikate.
- def get_field(self, position: Coordinate) -> Optional[FieldType]:
+ Returns:
+ Eine Liste von (Koordinatenmenge, Rotation, ist_gespiegelt)-Tupeln.
"""
- Gibt das Feld an der gegebenen Koordinate zurück.
- Args:
- position (Coordinate): Die Position des Feldes, das abgerufen werden soll.
+ def transform(self, rotation: Rotation, should_flip: bool) -> set[Coordinate]:
+ """Transformiert die Form entsprechend Rotation und Spiegelung."""
- Returns:
- Field: Das Feld an der gegebenen Koordinate, oder None, wenn außerhalb des gültigen Bereichs.
- """
+ def name(self) -> str:
...
- 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.
+class Piece:
+ """Ein Spielstein mit Farbe, Position und Transformation."""
- Args:
- field (FieldType): Der Feld-Typ, nachdem gesucht werden soll.
+ color: Color
+ kind: PieceShape
+ rotation: Rotation
+ is_flipped: bool
+ position: Coordinate
- Returns:
- List[Coordinate]: Die Liste der Koordinaten.
- """
- ...
+ def __init__(
+ self,
+ color: Color,
+ kind: PieceShape,
+ rotation: Rotation,
+ is_flipped: bool,
+ position: Coordinate,
+ ) -> None: ...
+ def __eq__(self, other: object) -> bool: ...
+ def __ne__(self, other: object) -> bool: ...
+ def __hash__(self) -> int: ...
- 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 shape(self) -> set[Coordinate]:
+ """Die normalisierte Form des Steins (gedreht/gespiegelt, nicht verschoben)."""
- **Achtung**: Das Feld der Ausgangskoordinate wird *nicht* beachtet und ausgegeben.
- Wenn die Startkoordinate nicht im Spielfeld liegt, wird eine leere Liste zurückgegeben.
+ def coordinates(self) -> set[Coordinate]:
+ """Die tatsächlichen Koordinaten, die der Stein auf dem Feld einnimmt."""
- Args:
- position (Coordinate): Die Ausgangskoordinate.
- direction (Direction): Die Richtung.
+ def transform(self, rotation: Rotation, is_flipped: bool) -> Piece:
+ """Dreht/spiegelt den Stein, Position bleibt gleich."""
- Returns:
- List[FieldType]: Die Liste der Felder.
- """
- 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.
+class Move:
+ """Repräsentiert einen Zug im Spiel: entweder ein SetMove oder ein SkipMove."""
- Das Ergebnis wird in Richtung des "Vektorpfeils" abgelesen.
- Wenn die Startkoordinate nicht im Spielfeld liegt, wird eine leere Liste zurückgegeben.
+ def __eq__(self, other: object) -> bool: ...
+ def __ne__(self, other: object) -> bool: ...
- Args:
- position (Coordinate): Die Startkoordinate für die Gerade.
- direction (Direction): Der aufspannende Richtungsvektor.
+ @staticmethod
+ def set_move(piece: Piece) -> Move:
+ """Erstellt einen Zug, der den gegebenen Stein platziert."""
- Returns:
- List[FieldType]: Die Liste der Felder.
- """
- ...
+ @staticmethod
+ def skip_move(color: Color) -> Move:
+ """Erstellt einen Zug, der die aktuelle Runde für die gegebene Farbe aussetzt."""
- 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 get_color(self) -> Color:
+ """Die Farbe, die diesen Zug getätigt hat."""
- Args:
- position (Coordinate): Die Startkoordinate für die Gerade.
- direction (Direction): Der aufspannende Richtungsvektor.
+ def as_piece(self) -> Piece | None:
+ """Gibt das verwendete Piece aus, wenn vorhanden"""
- Returns:
- List[FieldType]: Die Liste der Fisch-Felder.
- """
- ...
-class Move:
+class GameState:
"""
- Repräsentiert einen Zug im Spiel.
+ Repräsentiert den aktuellen Spielstand.
Attribute:
- start (Coordinate): Die Koordinate, von wo aus ein Fisch bewegt werden soll.
- direction (Direction): Die Richtung, in die der Fisch schwimmt.
+ turn (int): Die Anzahl der bereits getätigten Züge.
+ 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.
"""
- start: Coordinate
- direction: Direction
+ turn: int
+ last_move: Move | None
+ board: Board
+ start_piece: PieceShape
+ last_move_mono: dict[Color, bool]
- 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.
+ def __init__(
+ self,
+ turn: int = 0,
+ last_move: Move | None = None,
+ board: Board | None = None,
+ start_piece: PieceShape = PieceShape.Mono,
+ last_move_mono: dict[Color, bool] | None = None,
+ ) -> None: ...
+ def __eq__(self, other: object) -> bool: ...
- Args:
- other: (Move): Die andere Koordinate.
+ def round(self) -> int:
+ """Die aktuelle Rundenzahl."""
- Returns:
- bool: Das Ergebnis des Vergleichs.
- """
- ...
- def __ne__(self, other: Move) -> bool:
- """
- Unterstützt den Vergleichsoperator !=, um die Werte mit denen eines
- weiteren Objektes zuvergleichen.
+ def undeployed_piece_shapes(self, color: Color) -> list[PieceShape]:
+ """Gibt die noch nicht gesetzten Formen der gegebenen Farbe zurück."""
- Args:
- other: (Move): 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 deepcopy(self) -> GameState:
- """
- Kopiert das Objekt rekursiv.
- """
- ...
+ def current_color(self) -> Color:
+ """Die Farbe, die aktuell am Zug ist."""
-class GameState:
- """
- Repräsentiert einen Spielstand.
+ def has_valid_colors(self) -> bool:
+ """Gibt zurück, ob noch Farben im Spiel sind."""
- Attribute:
- board (Board): Das Spielbrett.
- turn (int): Die aktuelle Runde.
- last_move (Optional[Move]): Der zuletzt ausgeführte Zug.
- """
+ def is_valid_color(self, color: Color) -> bool:
+ """Prüft, ob die gegebene Farbe noch im Spiel ist."""
- board: Board
- turn: int
- last_move: Optional[Move]
+ def remove_active_color(self) -> bool:
+ """Entfernt die aktuell aktive Farbe aus dem Spiel und rückt vor."""
- def __init__(self, board: Board, turn: int, last_move: Optional[Move]) -> 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 advance(self, turns: int = 1) -> bool:
+ """Geht zum Zug der nächsten gültigen Farbe über."""
- Args:
- other: (GameState): Die andere Koordinate.
+ def is_over(self) -> bool:
+ """Gibt zurück, ob das Spiel vorbei ist."""
- Returns:
- bool: Das Ergebnis des Vergleichs.
+ def possible_moves(self) -> list[Move]:
"""
- ...
- def __ne__(self, other: GameState) -> bool:
+ Berechnet alle sinnvollen Züge der aktuellen Farbe.
+ Enthält einen SkipMove nur, wenn kein anderer Zug möglich ist.
"""
- Unterstützt den Vergleichsoperator !=, um die Werte mit denen eines
- weiteren Objektes zuvergleichen.
- Args:
- other: (GameState): Die andere Koordinate.
+ def get_points_for_color(self, color: Color) -> int:
+ """Berechnet die Punkteanzahl für die gegebene Farbe."""
- Returns:
- bool: Das Ergebnis des Vergleichs.
- """
- ...
+ def get_points_for_team(self, team: TeamEnum) -> int:
+ """Berechnet die Punkteanzahl für das gegebene Team (Summe der Farben des Teams)."""
- def deepcopy(self) -> GameState:
+ def win_condition(self) -> TeamEnum | None:
"""
- Kopiert das Objekt rekursiv.
+ Gibt das Gewinnerteam zurück, oder None bei einem Unentschieden.
"""
- ...
- def set_board_field(self, position: Coordinate, field: FieldType) -> None:
- """
- Ändert ein Feld auf dem Spielfeld an einer Koordinate.
- Args:
- position (Coordinate): Die Position des Feldes, das geändert werden soll.
- field (FieldType): Das Feld, was dort platziert werden soll.
- """
- ...
+class GameRuleLogic:
+ """Eine Sammlung an statischen Methoden, die die Spielregeln logisch umsetzen."""
- 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.
+ @staticmethod
+ def get_points_from_undeployed(undeployed: list[PieceShape], mono_last: bool = False) -> int:
+ """Berechnet den Punktestand anhand der gegebenen, nicht gelegten Formen."""
- Returns:
- List[Move]: Die Liste der Züge.
+ @staticmethod
+ def perform_move(game_state: GameState, move: Move) -> None:
"""
- ...
+ Führt den Zug im GameState aus (mutierend).
- def possible_moves(self) -> List[Move]:
+ Raises:
+ Eine der Blokus-Move-Mistake-Exceptions, wenn der Zug nicht valide ist.
"""
- 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.
+ @staticmethod
+ def validate_move_color(game_state: GameState, move: Move) -> None:
"""
- ...
+ Prüft, ob die Farbe des Zuges der aktiven Farbe entspricht.
- def perform_move(self, move: Move) -> GameState:
+ Raises:
+ WrongColor: Wenn die Farbe nicht am Zug ist.
"""
- Führt den gegebenen Zug auf dem Spielstand aus, insofern dieser ausführbar ist (**nicht mutierend**).
- Dabei wird *kein* Zug an den Spielserver übermittelt.
-
- Args:
- move_ (Move): Der zuverwendene Zug.
- Returns:
- Gamestate: Der neue Spielstand.
+ @staticmethod
+ def validate_set_move(game_state: GameState, piece: Piece) -> None:
+ """
+ Prüft, ob der gegebene Stein gesetzt werden könnte.
Raises:
- PiranhasError: Wenn der Zug nicht valide ist.
+ Eine der Blokus-Move-Mistake-Exceptions, wenn der Zug nicht valide ist.
"""
- ...
- def perform_move_mut(self, 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.
+ @staticmethod
+ def perform_set_move(game_state: GameState, piece: Piece) -> None:
+ """Platziert den gegebenen Stein auf dem Spielfeld (mutierend, intern genutzt)."""
- Args:
- move_ (Move): Der zuverwendene Zug.
+ @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.
Raises:
- PiranhasError: Wenn der Zug nicht valide ist.
+ WrongShape: Im ersten Zug, falls die falsche Form gewählt wurde.
+ DuplicateShape: In folgenden Zügen, falls die Form bereits gesetzt wurde.
"""
- ...
-class RulesEngine:
- """
- Stellt Methoden, die zur Überprüfung der Spielregeln dienen.
- """
+ @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 move_distance(board: Board, move_: Move) -> int:
+ def validate_set_move_on_board(board: Board, piece: Piece) -> None:
"""
- Gibt die Länge / Anzahl der Felder von einem Zug auf dem Spielfeld zurück.
+ Prüft, ob der Stein auf dem Board platziert werden kann (Grenzen, Überlappung, Farbregeln).
- Args:
- board (Board): Das Spielfeld, auf dem die Länge berechnet werden soll.
- move_ (Move): Der zuverwendene Zug.
-
- Returns:
- int: Die Länge.
+ 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.
"""
- ...
@staticmethod
- def target_position(board: Board, move_: Move) -> Coordinate:
+ def validate_skip_move(game_state: GameState) -> None:
"""
- Gibt die Koordinate zurück, auf der ein Fisch landen würde, wenn man den Zug ausführt.
+ Prüft, ob die aktuelle Farbe den Zug überspringen kann.
- Es wird nicht berücksichtigt, ob diese Koordinate im Spielfeld ist.
+ Raises:
+ SkipFirstTurn: Wenn im ersten Zug übersprungen werden soll.
+ """
- Args:
- board (Board): Das Spielfeld, auf dem der Zug berechnet werden soll.
- move_ (Move): Der zuverwendene Zug.
+ @staticmethod
+ def perform_skip_move(game_state: GameState) -> None:
+ """Führt einen Skip-Zug aus (validiert, mutiert aber sonst nichts)."""
- Returns:
- Coordinate: Die Koordinate.
- """
- ...
+ @staticmethod
+ def borders_on_color(board: Board, field: Field) -> bool:
+ """Prüft, ob das Feld an ein Feld gleicher Farbe angrenzt (Kante)."""
@staticmethod
- def is_in_bounds(coordinate: Coordinate) -> bool:
- """
- Gibt einen Wahrheitswert zurück, ob eine Position in dem (Standard-) Spielfeld (10x10) liegt.
+ def corners_on_color(board: Board, field: Field) -> bool:
+ """Prüft, ob das Feld an die Ecke eines Feldes gleicher Farbe angrenzt."""
- Args:
- coordinate (Coordinate): Die Position
+ @staticmethod
+ def is_on_border(position: Coordinate) -> bool:
+ """Prüft, ob die Position am Rand des Spielfelds liegt."""
- Returns:
- bool: Ob die Koordinate im Feld ist.
- """
- ...
+ @staticmethod
+ def is_first_move(game_state: GameState) -> bool:
+ """Gibt zurück, ob sich der GameState noch in der ersten Runde befindet."""
@staticmethod
- def can_execute_move(board: Board, move_: Move) -> 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.
+ def get_random_start_pentomino() -> PieceShape:
+ """Gibt ein zufälliges Pentomino zurück (Startstein)."""
- Gibt keinen Wert zurück, sondern wirft eine Fehlermeldung, falls der Zug nicht valide ist.
+ @staticmethod
+ def remove_invalid_colors(game_state: GameState) -> None:
+ """Entfernt rekursiv alle Farben, die keine Steine mehr platzieren können (mutierend)."""
- Args:
- board (Board): Das Spielfeld.
- move_ (Move): Der Zug, der geprüft werden soll.
+ @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)."""
- Raises:
- PiranhasError: Wenn der Zug nicht valide ist.
+ @staticmethod
+ def get_filtered_possible_moves(game_state: GameState) -> list[Piece]:
"""
- ...
-
- @staticmethod
- def get_team_on_turn(turn: int) -> TeamEnum:
+ Gibt eine gefilterte Liste möglicher SetMoves zurück:
+ Startzüge, dann 5 Runden nur Pentominos, danach alle.
"""
- Berechnet anhand der Zugzahl, welcher Spieler dran sein müsste.
- Es wird nicht beachtet, ob die Zahl kleiner 0 oder größer 59 ist.
-
- Args:
- turn (int): Die Zugzahl.
- Returns:
- TeamEnum: Das Team, was dran ist.
- """
- ...
+ @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."""
@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 get_possible_moves(game_state: GameState) -> list[Piece]:
+ """Gibt alle möglichen SetMoves (ohne Startzug) zurück."""
- Args:
- board (Board): Das Spielbrett.
- position (Coordinate): Die Startkoordinate
+ @staticmethod
+ def get_pentomino_moves(game_state: GameState) -> list[Piece]:
+ """Gibt nur die möglichen SetMoves mit Pentominos zurück."""
- Returns:
- List[Coordinate]: Die Liste an zusammenhängenden Fischen.
+ @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."""
@staticmethod
- def swarms_of_team(board: Board, team: TeamEnum) -> List[List[Coordinate]]:
- """
- 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.
+ def get_colored_fields(board: Board, color: Color) -> set[Coordinate]:
+ """Gibt alle Koordinaten mit der gegebenen Farbe auf dem Board zurück."""
- Args:
- board (Board): Das Spielbrett.
- team (TeamEnum): Das gewählte Team.
- Returns:
- List[List[Coordinate]]: Die Liste an Schwärmen.
+class Constants:
+ """Hält globale Konstanten."""
- """
- ...
+ BOARD_LENGTH: int
+ ROUND_LIMIT: int
+ TOTAL_PIECE_SHAPES: int
+ COLORS: int
+ VALIDATE_MOVE: bool
-class PluginConstants:
- """
- Hält globale Konstanten.
- """
- BOARD_WIDTH: int
- BOARD_HEIGHT: int
+class WrongColor(Exception):
+ """Die Farbe des Zuges ist nicht an der Reihe."""
- ROUND_LIMIT: int
+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."""
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 46f87dd..681cce6 100644
--- a/python/socha/api/networking/utils.py
+++ b/python/socha/api/networking/utils.py
@@ -1,121 +1,194 @@
-import re
-from typing import List
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: 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.
+ """
+ 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: LastMoveMono | None) -> 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..dbdf6a1 100644
--- a/python/socha/api/networking/xml_protocol_interface.py
+++ b/python/socha/api/networking/xml_protocol_interface.py
@@ -4,30 +4,32 @@
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.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 (
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, LastMove
+logger = logging.getLogger(__name__)
-def map_object(data: Data , params: dict):
+def map_object(data: Data, params: dict):
try:
params.pop('class_binding')
@@ -58,36 +60,19 @@ 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'))
+ logger.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)
-PROTOCOL_PREFIX = ''.encode('utf-8')
+PROTOCOL_PREFIX = b''
class XMLProtocolInterface:
@@ -143,7 +128,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"]
@@ -153,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
@@ -181,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
@@ -213,4 +196,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..0d57b7b 100644
--- a/python/socha/api/protocol/protocol.py
+++ b/python/socha/api/protocol/protocol.py
@@ -1,8 +1,6 @@
from dataclasses import dataclass, field
-from typing import List, Optional
from socha._socha import TeamEnum
-
from socha.api.protocol.protocol_packet import (
AdminLobbyRequest,
LobbyRequest,
@@ -17,103 +15,192 @@
@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: int | None = field(
+ default=None,
+ metadata={'type': 'Attribute'},
+ )
+ y: int | None = 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: str | None = field(
+ default=None,
+ metadata={'type': 'Attribute'},
+ )
+ kind: str | None = field(
+ default=None,
+ metadata={'type': 'Attribute'},
+ )
+ rotation: str | None = field(
+ default=None,
+ metadata={'type': 'Attribute'},
+ )
+ is_flipped: bool | None = field(
+ default=None,
metadata={
- 'name': 'row',
- 'type': 'Element',
- 'min_occurs': 1,
+ 'name': 'isFlipped',
+ 'type': 'Attribute',
},
)
+ position: Position | None = field(
+ default=None,
+ metadata={'type': 'Element'},
+ )
@dataclass
-class Coordinate:
-
+class LastMove:
class Meta:
- name = 'from'
+ name = 'lastMove'
- x: Optional[int] = field(
+ class_binding: object | None = field(default=None)
+ class_value: str | None = field(
default=None,
metadata={
+ 'name': 'class',
'type': 'Attribute',
+ 'required': True,
},
)
- y: Optional[int] = field(
+ piece: Piece | None = 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: str | None = 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: int | None = field(
default=None,
- metadata={
- 'name': 'from',
- 'type': 'Element',
- },
+ metadata={'type': 'Attribute'},
)
- direction: Optional[str] = field(
+ y: int | None = field(
default=None,
+ metadata={'type': 'Attribute'},
+ )
+ content: str | None = 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'
+
+ color: list[str] = field(
+ default_factory=list,
+ metadata={'type': 'Element'},
+ )
+
- name: Optional[str] = field(
+@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': 'Attribute',
- 'required': True,
+ 'name': 'sc.plugin2027.Color',
+ 'type': 'Element',
},
)
- team: Optional[str] = field(
+ value: bool | None = 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:
name = 'state'
- class_value: Optional[str] = field(
+ class_value: str | None = field(
default=None,
metadata={
'name': 'class',
@@ -121,7 +208,7 @@ class Meta:
'required': True,
},
)
- start_team: Optional[str] = field(
+ start_team: str | None = field(
default=None,
metadata={
'name': 'startTeam',
@@ -129,27 +216,105 @@ class Meta:
'required': True,
},
)
- turn: Optional[int] = field(
+ turn: int | None = field(
default=None,
metadata={
'type': 'Attribute',
'required': True,
},
)
- board: Optional[Board] = field(
+ start_piece: str | None = field(
default=None,
metadata={
- 'type': 'Element',
+ 'name': 'startPiece',
+ 'type': 'Attribute',
'required': True,
},
)
- last_move: Optional[LastMove] = field(
+ round: int | None = field(
+ default=None,
+ metadata={
+ 'type': 'Attribute',
+ 'required': True,
+ },
+ )
+ last_move: LastMove | None = field(
default=None,
metadata={
'name': 'lastMove',
'type': 'Element',
},
)
+ board: Board | None = field(
+ default=None,
+ metadata={
+ 'type': 'Element',
+ 'required': True,
+ },
+ )
+ last_move_mono: LastMoveMono | None = field(
+ default=None,
+ metadata={
+ 'name': 'lastMoveMono',
+ 'type': 'Element',
+ },
+ )
+ blue_shapes: ShapeList | None = field(
+ default=None,
+ metadata={
+ 'name': 'blueShapes',
+ 'type': 'Element',
+ },
+ )
+ yellow_shapes: ShapeList | None = field(
+ default=None,
+ metadata={
+ 'name': 'yellowShapes',
+ 'type': 'Element',
+ },
+ )
+ red_shapes: ShapeList | None = field(
+ default=None,
+ metadata={
+ 'name': 'redShapes',
+ 'type': 'Element',
+ },
+ )
+ green_shapes: ShapeList | None = field(
+ default=None,
+ metadata={
+ 'name': 'greenShapes',
+ 'type': 'Element',
+ },
+ )
+ valid_colors: ColorList | None = field(
+ default=None,
+ metadata={
+ 'name': 'validColors',
+ 'type': 'Element',
+ },
+ )
+
+
+@dataclass
+class Player:
+ class Meta:
+ name = 'player'
+
+ name: str | None = field(
+ default=None,
+ metadata={
+ 'type': 'Attribute',
+ 'required': True,
+ },
+ )
+ team: str | None = field(
+ default=None,
+ metadata={
+ 'type': 'Attribute',
+ 'required': True,
+ },
+ )
@dataclass
@@ -157,14 +322,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',
@@ -178,13 +343,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',
@@ -202,7 +367,7 @@ class Left(ProtocolPacket):
class Meta:
name = 'left'
- room_id: Optional[str] = field(
+ room_id: str | None = field(
default=None,
metadata={
'name': 'roomId',
@@ -241,7 +406,7 @@ class Authenticate(AdminLobbyRequest):
class Meta:
name = 'authenticate'
- password: Optional[str] = field(
+ password: str | None = field(
default=None,
metadata={
'type': 'Attribute',
@@ -258,7 +423,7 @@ class Cancel(AdminLobbyRequest):
class Meta:
name = 'cancel'
- room_id: Optional[str] = field(
+ room_id: str | None = field(
default=None,
metadata={
'name': 'roomId',
@@ -276,14 +441,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',
@@ -301,7 +466,7 @@ class Observe(AdminLobbyRequest):
class Meta:
name = 'observe'
- room_id: Optional[str] = field(
+ room_id: str | None = field(
default=None,
metadata={
'name': 'roomId',
@@ -319,14 +484,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',
@@ -343,21 +508,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',
@@ -376,7 +541,7 @@ class Step(RoomOrchestrationMessage):
class Meta:
name = 'step'
- room_id: Optional[str] = field(
+ room_id: str | None = field(
default=None,
metadata={
'name': 'roomId',
@@ -395,20 +560,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',
@@ -437,7 +602,7 @@ class JoinPrepared(LobbyRequest):
class Meta:
name = 'joinPrepared'
- reservation_code: Optional[str] = field(
+ reservation_code: str | None = field(
default=None,
metadata={
'name': 'reservationCode',
@@ -455,7 +620,7 @@ class JoinRoom(LobbyRequest):
class Meta:
name = 'joinRoom'
- room_id: Optional[str] = field(
+ room_id: str | None = field(
default=None,
metadata={
'name': 'roomId',
@@ -473,19 +638,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',
@@ -503,7 +668,7 @@ class Joined(ResponsePacket):
class Meta:
name = 'joined'
- room_id: Optional[str] = field(
+ room_id: str | None = field(
default=None,
metadata={
'name': 'roomId',
@@ -521,7 +686,7 @@ class Score:
class Meta:
name = 'score'
- part: List[int] = field(
+ part: list[int] = field(
default_factory=list,
metadata={
'type': 'Element',
@@ -535,21 +700,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',
@@ -569,7 +734,7 @@ class Definition:
class Meta:
name = 'definition'
- fragment: List[Fragment] = field(
+ fragment: list[Fragment] = field(
default_factory=list,
metadata={
'type': 'Element',
@@ -587,13 +752,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',
@@ -610,7 +775,7 @@ class Scores:
class Meta:
name = 'scores'
- entry: List[Entry] = field(
+ entry: list[Entry] = field(
default_factory=list,
metadata={
'type': 'Element',
@@ -645,12 +810,18 @@ 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:
name = 'originalMessage'
- class_value: Optional[str] = field(
+ class_value: str | None = field(
default=None,
metadata={
'name': 'class',
@@ -658,18 +829,13 @@ class Meta:
'required': True,
},
)
- from_: Optional[Coordinate] = field(
+ piece: Piece | None = field(
default=None,
- metadata={
- 'name': 'from',
- 'type': 'Element',
- },
+ metadata={'type': 'Element'},
)
- direction: Optional[str] = field(
+ color: str | None = field(
default=None,
- metadata={
- 'type': 'Element',
- },
+ metadata={'type': 'Element'},
)
@@ -688,7 +854,7 @@ class Data:
class Meta:
name = 'data'
- class_value: Optional[str] = field(
+ class_value: str | None = field(
default=None,
metadata={
'name': 'class',
@@ -696,54 +862,57 @@ 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',
},
)
- color: Optional[str] = field(
+ # Nur für welcomeMessage: color="ONE"/"TWO" (TeamEnum), als Attribut.
+ color: str | None = field(
default=None,
metadata={
'type': 'Attribute',
},
)
- from_: Optional[Coordinate] = field(
+ # Für ausgehenden SetMove.
+ piece: Piece | None = 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: str | None = field(
default=None,
metadata={
+ 'name': 'color',
'type': 'Element',
},
)
@@ -754,7 +923,7 @@ class Room(ProtocolPacket):
class Meta:
name = 'room'
- room_id: Optional[str] = field(
+ room_id: str | None = field(
default=None,
metadata={
'name': 'roomId',
@@ -762,7 +931,7 @@ class Meta:
'required': True,
},
)
- data: Optional[Data] = field(
+ data: Data | None = field(
default=None,
metadata={
'type': 'Element',
@@ -776,7 +945,7 @@ class Observed(RoomOrchestrationMessage):
class Meta:
name = 'observed'
- room_id: Optional[str] = field(
+ room_id: str | None = field(
default=None,
metadata={
'name': 'roomId',
@@ -790,14 +959,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',
@@ -816,76 +985,76 @@ 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',
},
- )
+ )
\ No newline at end of file
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
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