diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index 09bb7c0..f64ec7e 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -28,4 +28,4 @@ Verstöße gegen diesen Verhaltenskodex können durch die Projektbetreuer überp
## 6. Kontakt
-Bei Fragen oder zur Meldung von Verstößen bitte eine Nachricht an [@maxblan](https://github.com/maxblan) senden.
+Bei Fragen oder zur Meldung von Verstößen bitte eine Nachricht an [@yoente](https://github.com/yoente) oder [@maxblan](https://github.com/maxblan) senden.
diff --git a/Cargo.toml b/Cargo.toml
index 6410e14..53e7094 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "_socha"
-version = "3.7.2"
+version = "4.0.0"
edition = "2021"
[lib]
diff --git a/README.md b/README.md
index e010f78..df64397 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
# Python-Client für die Software-Challenge Germany 2025
-[](https://socha-python-client.readthedocs.io/en/)
+[](https://socha-python-client.readthedocs.io/de/latest/)
[](https://pypi.org/project/socha/)
[](https://pypi.org/project/socha/)
[](https://discord.gg/ARZamDptG5)
@@ -16,13 +16,15 @@ Dieses Repository enthält das Python-Paket für die [Software-Challenge Germany
## Inhaltsverzeichnis
-- [Installation](#installation)
- - [Global](#global)
- - [Virtuelle Umgebung](#virtuelle-umgebung)
-- [Erste Schritte](#erste-schritte)
- - [Startargumente](#startargumente)
-- [Vorbereitung des Spielers für den Wettbewerb](#vorbereitung-des-spielers-für-den-wettbewerb)
-- [Lokale Entwicklung](#lokale-entwicklung)
+- [Python-Client für die Software-Challenge Germany 2025](#python-client-für-die-software-challenge-germany-2025)
+ - [Inhaltsverzeichnis](#inhaltsverzeichnis)
+ - [Installation](#installation)
+ - [Global](#global)
+ - [Virtuelle Umgebung](#virtuelle-umgebung)
+ - [Erste Schritte](#erste-schritte)
+ - [Startargumente](#startargumente)
+ - [Vorbereitung des Spielers für den Wettbewerb](#vorbereitung-des-spielers-für-den-wettbewerb)
+ - [Lokale Entwicklung](#lokale-entwicklung)
## Installation
diff --git a/docs/conf.py b/docs/conf.py
index 0196354..95e19b0 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -6,7 +6,7 @@
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = 'python_player'
-copyright = '2024, Software-Challenge Germany'
+copyright = '2025, Software-Challenge Germany'
author = 'Software-Challenge Germany'
# -- General configuration ---------------------------------------------------
diff --git a/docs/index.rst b/docs/index.rst
index ea65fc6..ac95571 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -2,7 +2,7 @@
-Python Client for the Software-Challenge Germany 2024
+Python Client for the Software-Challenge Germany 2026
=====================================================
|Read the Docs| |PyPI| |PyPI - Python Version| |Discord| |Documentation|
@@ -15,7 +15,7 @@ competition for students. The students have to develop an artificial
intelligence that plays and competes against other opponents in an
annually changing game.
- This year it is the game `Hase und Igel <>`__.
+ This year it is the game `Piranhas <>`__.
Table of Contents
-----------------
@@ -201,6 +201,9 @@ start arguments.
+------------------------+--------------------------------------------+
| **-a, --architecture** | The architecture of the package. |
+------------------------+--------------------------------------------+
+| **--python-version** | Specifies the build python version |
+| | (e.g.: '3.10' - this is standard). |
++------------------------+--------------------------------------------+
Preparing Your Player for the Competition
-----------------------------------------
diff --git a/pyproject.toml b/pyproject.toml
index 169c099..a8c8ac9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,12 +1,15 @@
[build-system]
-requires = ["maturin==1.6.0"]
+requires = ["maturin==1.8.1"]
build-backend = "maturin"
[project]
name = "socha"
-version = "3.7.2"
-authors = [{ name = "maxblan", email = "stu222782@mail.uni-kiel.de" }]
-description = "Dieses Paket ist für die Software-Challenge Germany 2025, bei der in dieser Saison das Spiel 'Hase und Igel' im Mittelpunkt steht."
+version = "4.0.0"
+authors = [
+ { name = "yoente", email = "stu250140@mail.uni-kiel.de" },
+ { name = "maxblan", email = "stu222782@mail.uni-kiel.de" },
+]
+description = "Dieses Paket ist für die Software-Challenge Germany 2026, bei der in dieser Saison das Spiel 'Piranhas' im Mittelpunkt steht."
readme = "README.md"
requires-python = ">=3.10"
dependencies = ["xsdata==22.9"]
@@ -28,9 +31,9 @@ classifiers = [
[project.urls]
homepage = "https://software-challenge.de/"
-repository = "https://github.com/FalconsSky/socha-python-client"
-documentation = "https://software-challenge-python-client.readthedocs.io/en/latest/"
-bug-tracker = "https://github.com/FalconsSky/Software-Challenge-Python-Client/issues"
+repository = "https://github.com/software-challenge/player_python"
+documentation = "https://socha-python-client.readthedocs.io/de/latest/"
+bug-tracker = "https://github.com/software-challenge/player_python/issues"
[tool.maturin]
python-source = "python"
diff --git a/python/socha/_socha.pyi b/python/socha/_socha.pyi
index 953efea..e339fe3 100644
--- a/python/socha/_socha.pyi
+++ b/python/socha/_socha.pyi
@@ -1,350 +1,397 @@
from enum import Enum
from typing import List, Optional
-class Card(Enum):
+class Coordinate:
"""
- Eine Karte, die im Spiel verwendet werden kann.
+ Eine 2 dimensionale Koordinate auf einem Spielfeld.
Attributes:
- FallBack (int): Die Karte, die den Spieler zurückfallen lässt.
- HurryAhead (int): Die Karte, die den Spieler vorrücken lässt.
- EatSalad (int): Die Karte, die den Spieler Salat essen lässt.
- SwapCarrots (int): Die Karte, die die Karotten der Spieler tauscht.
+ x (int): Der x-Wert.
+ y (int): Der y-Wert.
"""
- FallBack: int = 0
- HurryAhead: int = 1
- EatSalad: int = 2
- SwapCarrots: int = 3
+ x: int
+ y: int
- def __init__(self) -> None: ...
- def moves(self) -> bool:
+ def __init__(self, x: int, y: int) -> None: ...
+ def __str__(self) -> str: ...
+ def __repr__(self) -> str: ...
+
+ def add_vector(self, vector: Vector) -> Coordinate:
"""
- Gibt an, ob die Karte den Spieler bewegt.
+ Addiert einen Vector auf die Werte dieser Koordinate (**nicht mutierend**).
Args:
- None
+ vector (Vector): Der Vektor.
Returns:
- bool: True, wenn die Karte den Spieler bewegt, False sonst.
+ Coordinate: Ein neues Koordinatenobjekt mit den berechneten Werten.
"""
...
- def perform(self, state: GameState, remaining_cards: List[Card]) -> None:
+ def add_vector_mut(self, vector: Vector) -> None:
"""
- Führt die Karte aus.\n
- Diese Methode **kann** den GameState **mutieren**, unabhängig vom Erfolg des Zuges.
+ Addiert einen Vector auf die Werte dieser Koordinate (**mutierend**).
Args:
- state (GameState): Der aktuelle Spielzustand.
- remaining_cards (List[Card]): Die verbleibenden Karten.
+ vector (Vector): Der Vektor.
+ """
+ ...
- Raises:
- HUIError: Ein Fehler, wenn die Karte nicht ausgeführt werden kann.
+ def get_difference(self, other: Coordinate) -> Vector:
+ """
+ Berechnet die Differenz zwischen zwei Koordinaten Punkten als Vektor.
+
+ Args:
+ other (Coordinate): Die andere Koordinate
+
+ Returns:
+ Vector: Der Vektor zwischen den Punkten
"""
...
-class Advance:
+class Vector:
"""
- Eine Klasse, die einen Vorwärtszug im Spiel darstellt.
+ Ein 2 dimensionaler Vektor.
Attributes:
- distance (int): Die Anzahl der Felder, die der Spieler vorrücken soll.
- cards (List[Card]): Die Karten, die während des Vorgangs gespielt werden.
+ delta_x (int): Die Entfernung in x-Richtung.
+ delta_y (int): Die Entfernung in y-Richtung.
"""
- distance: int
- cards: List[Card]
+ delta_x: int
+ delta_y: int
+
+ def __init__(self, delta_x: int, delta_y: int) -> None: ...
+ def __str__(self) -> str: ...
+ def __repr__(self) -> str: ...
- def __init__(self, distance: int, cards: List[Card]): ...
- def perform(self, state: GameState) -> None:
+ def add_vector(self, other: Vector) -> Vector:
"""
- Führt den Vorwärtszug aus.\n
- Diese Methode **kann** den GameState **mutieren**, unabhängig vom Erfolg des Zuges.
+ Addiert einen anderen Vector auf die Werte dieses Vektors (**nicht mutierend**).
Args:
- state (GameState): Der aktuelle Zustand des Spiels.
+ other (Vector): Der andere Vektor.
- Raises:
- HUIError: Wenn der Vorwärtszug nicht erfolgreich ausgeführt werden kann.
+ Returns:
+ Vector: Ein neues Vektorobjekt mit den berechneten Werten.
"""
...
-class EatSalad:
- def __init__(self) -> None: ...
- def perform(self, state: GameState) -> None:
+ def add_vector_mut(self, other: Vector) -> None:
"""
- Führt die Salat-Fressen Aktion aus.\n
- Diese Methode **kann** den GameState **mutieren**, unabhängig vom Erfolg des Zuges.
+ Addiert einen anderen Vector auf die Werte dieses Vektors (**mutierend**).
Args:
- state (GameState): Der aktuelle Zustand des Spiels.
-
- Raises:
- HUIError: Wenn der Salat nicht erfolgreich gegessen werden kann.
+ other (Vector): Der andere Vektor.
"""
...
-class ExchangeCarrots:
- amount: int
-
- def __init__(self, amount: int) -> None: ...
- def perform(self, state: GameState) -> None:
+ def scale(self, scalar: int) -> Vector:
"""
- Führt die Karotten-Tausch-Aktion aus.\n
- Diese Methode **kann** den GameState **mutieren**, unabhängig vom Erfolg des Zuges.
+ Skaliert diesen Vektor um ein gegebenes Skalar (**nicht mutierend**).
Args:
- state (GameState): Der aktuelle Zustand des Spiels.
+ scalar (int): Das Skalar.
- Raises:
- HUIError: Wenn die Karotten nicht erfolgreich getauscht werden können.
+ Returns:
+ Vector: Ein neues Vektorobjekt mit den berechneten Werten.
"""
...
-class FallBack:
- def __init__(self) -> None: ...
- def perform(self, state: GameState) -> None: ...
+ def scale_mut(self, scalar: int) -> None:
+ """
+ Skaliert diesen Vektor um ein gegebenes Skalar (**mutierend**).
-class Field(Enum):
- Position1: int = 0
- """
- Zahlfeld
- """
- Position2: int = 1
+ Args:
+ scalar (int): Das Skalar.
+ """
+ ...
+
+ def get_length(self) -> float:
+ """
+ Berechnet die Länge dieses Vektors.
+
+ Returns:
+ float: Die Länge des Vektors (in 32 bit Präzision).
+ """
+ ...
+
+class Direction(Enum):
"""
- Flaggenfeld
+ Eine Darstellung für eine normierte Richtung.
+ Kann in einen Vektor konvertiert werden.
"""
- Hedgehog: int = 2
+
+ Up: int = 0
"""
- Igelfeld: Hierauf kann nur rückwärts gezogen werden.
+ Richtung nach oben, entspricht Vektor(0, 1).
"""
- Salad: int = 3
+ UpRight: int = 1
"""
- Salatfeld: Beim Betreten wird im nächsten Zug ein Salat gegessen.
+ Richtung nach oben-rechts, entspricht Vektor(1, 1).
"""
- Carrots: int = 4
+ Right: int = 2
"""
- Karottenfeld: Hier dürfen Karotten getauscht werden.
+ Richtung nach rechts, entspricht Vektor(1, 0).
"""
- Hare: int = 5
+ DownRight: int = 3
"""
- Hasenfeld: Hier wird sofort eine Hasenkarte gespielt.
+ Richtung nach unten-rechts, entspricht Vektor(1, -1).
"""
- Market: int = 6
+ Down: int = 4
"""
- Marktfeld: Hier wird eine Hasenkarte gekauft (Variation).
+ Richtung nach unten, entspricht Vektor(0, -1).
"""
- Goal: int = 7
+ DownLeft: int = 5
"""
- Zielfeld
+ Richtung nach unten-links, entspricht Vektor(-1, -1).
"""
- Start: int = 8
+ Left: int = 6
"""
- Startfeld
+ Richtung nach links, entspricht Vektor(-1, 0).
"""
-
-class Board:
+ UpLeft: int = 7
"""
- Ein Spielbrett, das die Felder des Spiels enthält.
-
- Attributes:
- track (List[Field]): Die Liste der Felder, die das Spielbrett darstellen
+ Richtung nach oben-links, entspricht Vektor(-1, 1).
"""
- track: list[Field]
+ def __str__(self) -> str: ...
+ def __repr__(self) -> str: ...
- def __init__(self, track: list[Field]) -> None: ...
- def get_field(self, index: int) -> Optional[Field]:
+ @staticmethod
+ def from_vector(vector: Vector) -> Optional[Direction]:
"""
- Gibt das Feld am angegebenen Index zurück.
+ Wandelt einen Vektor in eine der 8 Richtungen um, insofern der Vektor exakt der Richtung entspricht.
Args:
- index (int): Der Index des Feldes, das abgerufen werden soll.
+ vector (Vector): Der Vektor, der konvertiert werden soll.
Returns:
- Field: Das Feld am angegebenen Index, oder None, wenn außerhalb des gültigen Bereichs.
+ Optional[Direction]: Die Richtung oder None, wenn der Vektor nicht direkt übersetzt werden kann.
"""
...
- def find_field(self, field: Field, start: int, end: int) -> Optional[int]:
+
+ @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.
"""
- Findet den ersten Index des angegebenen Feldes innerhalb des angegebenen Bereichs.
+ ...
- Args:
- field (Field): Das Feld, nach dem gesucht werden soll.
- start (int): Der Startindex des Bereichs, in dem gesucht werden soll.
- end (int): Der Endindex des Bereichs, in dem gesucht werden soll.
+ def to_vector(self) -> Vector:
+ """
+ Wandelt die Richtung in den entsprechenden Vektor um.
Returns:
- int: Der Index des Feldes, wenn gefunden, oder None, wenn nicht gefunden.
+ Vector: Der Richtungsvektor.
"""
...
- def get_previous_field(self, field: Field, index: int) -> Optional[int]:
+
+ def to_mirrored(self) -> Direction:
"""
- Findet die vorherige Vorkommen des angegebenen Feldes vor dem angegebenen Index.
+ Spiegelt die gegebene Richtung.
+ Beispiel: Up -> Down.
- Args:
- field (Field): Das Feld, nach dem gesucht werden soll.
- index (int): Der Index, von dem aus gesucht werden soll.
+ Returns:
+ Direction: Die neue Richtung.
+ """
+ ...
+
+class FieldType(Enum):
+ """
+ Stellt alle verfügbaren Feldtypen dar.
+ """
+
+ 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.
+ """
+
+ def __str__(self) -> str: ...
+ def __repr__(self) -> str: ...
+ 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 Index des vorherigen Vorkommens des Feldes, oder None, wenn nicht gefunden.
+ int: Der Wert des Feldes.
"""
...
- def get_next_field(self, field: Field, index: int) -> Optional[int]:
+
+ def get_team(self) -> Optional[TeamEnum]:
"""
- Findet das nächste Vorkommen des angegebenen Feldes nach dem angegebenen Index.
+ Gibt für ein Fischfeld aus, zu welchem Team dieser Fisch gehört.
- Args:
- field (Field): Das Feld, nach dem gesucht werden soll.
- index (int): Der Index, von dem aus gesucht werden soll.
+ Returns:
+ Optional[TeamEnum]: Das Team, zudem der Fisch gehört, oder None, wenn das Feld kein Fisch ist.
+ """
+ ...
+ @staticmethod
+ def all_field_types() -> List[FieldType]:
+ """
+ Gibt eine Liste aller Feldtypen aus.
+
Returns:
- int: Der Index des nächsten Vorkommens des Feldes, oder None, wenn nicht gefunden.
+ List[Direction]: Die Liste der Richtungen.
"""
...
class TeamEnum(Enum):
- One: int = 0
+ """
+ Eine Darstellung für die beiden Teams
+ """
+
+ One = 0
"""
Team 1
"""
- Two: int = 1
+ Two = 1
"""
Team 2
"""
+ def __str__(self) -> str: ...
def __repr__(self) -> str: ...
-class Hare:
- """
- Repräsentiert einen Hasen im Spiel.
-
- Attribute:
- team (TeamEnum): Das Team, dem der Hase angehört.
- position (int): Die aktuelle Position des Hasen auf dem Brett.
- salads (int): Die Anzahl der Salate, die der Hase hat.
- carrots (int): Die Anzahl der Karotten, die der Hase hat.
- last_move (Optional[Move]): Der letzte Zug, den der Hase gemacht hat.
- cards (List[Card]): Die Karten, die der Hase hat.
- """
-
- team: TeamEnum
- position: int
- salads: int
- carrots: int
- last_move: Optional[Move]
- cards: List[Card]
-
- def __init__(
- self,
- team: TeamEnum,
- cards: Optional[List[Card]] = None,
- carrots: Optional[int] = None,
- salads: Optional[int] = None,
- last_move: Optional[Move] = None,
- position: Optional[int] = None,
- ) -> None: ...
- def is_in_goal(self) -> bool:
+ def get_fish_types(self) -> List[FieldType]:
"""
- Überprüft, ob der Hase im Ziel ist.
+ Gibt eine Liste aller Fischtypen des Teams aus.
Returns:
- bool: True, wenn der Hase im Ziel ist, False sonst.
+ List[FieldType]: Die Liste der Feldtypen.
"""
...
- def can_enter_goal(self) -> bool:
+ def opponent(self) -> TeamEnum:
"""
- Überprüft, ob der Hase das Ziel betreten kann.
+ Gibt den Gegner dieses Teams an.
- Returns:
- bool: True, wenn der Hase das Ziel betreten kann, False sonst.
+ Return:
+ TeamEnum: Das Gegnerteam.
"""
...
- def advance_by(self, state: GameState, distance: int, cards: List[Card]) -> None:
- """
- Rückt den Hasen um eine bestimmte Entfernung vor.
+class Board:
+ """
+ Ein Spielbrett, das die Felder des Spiels enthält.
- Args:
- state (GameState): Der aktuelle Spielzustand.
- distance (int): Die Entfernung, um die der Hase vorrücken soll.
- cards (List[Card]): Die Karten, die während des Vorgangs gespielt werden.
+ 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.
- Raises:
- HUIError: Wenn der Hase nicht vorrücken kann.
- """
- ...
-
- def exchange_carrots(self, state: GameState, carrots: int) -> None:
- """
- Tauscht Karotten mit dem Hasen.
+ Attributes:
+ map (List[List[Field]]): Die 2 dimensionale Liste der Felder, die das Spielbrett darstellen.
+ """
- Args:
- state (GameState): Der aktuelle Spielzustand.
- carrots (int): Die Anzahl der Karotten, die getauscht werden sollen.
+ map: List[List[FieldType]]
- Raises:
- HUIError: Wenn die Karotten nicht getauscht werden können.
- """
- ...
+ def __init__(self, map: List[List[FieldType]]) -> None: ...
+ def __str__(self) -> str: ...
+ def __repr__(self) -> str: ...
- def consume_carrots(self, state: GameState, carrots: int) -> None:
+ def get_field(self, position: Coordinate) -> Optional[FieldType]:
"""
- Verbraucht Karotten vom Hasen.
+ Gibt das Feld an der gegebenen Koordinate zurück.
Args:
- state (GameState): Der aktuelle Spielzustand.
- carrots (int): Die Anzahl der Karotten, die verbraucht werden sollen.
+ position (Coordinate): Die Position des Feldes, das abgerufen werden soll.
- Raises:
- HUIError: Wenn die Karotten nicht verbraucht werden können.
+ Returns:
+ Field: Das Feld an der gegebenen Koordinate, oder None, wenn außerhalb des gültigen Bereichs.
"""
...
- def eat_salad(self, state: GameState) -> None:
+ def get_fields_by_type(self, field: FieldType) -> List[Coordinate]:
"""
- Lässt den Hasen einen Salat essen.
+ Gibt eine Liste aller Koordinaten zurück, auf dem der angegebene Feld-Typ zu finden ist.
Args:
- state (GameState): Der aktuelle Spielzustand.
+ field (FieldType): Der Feld-Typ, nachdem gesucht werden soll.
+
+ Returns:
+ List[Coordinate]: Die Liste der Koordinaten.
"""
...
- def get_fall_back(self, state: GameState) -> Optional[int]:
+ def get_fields_in_direction(self, position: Coordinate, direction: Direction) -> List[FieldType]:
"""
- Gibt den nächsten möglich Index zurück, an dem der Hase zurückfallen kann.
+ 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.
+
+ **Achtung**: Das Feld der Ausgangskoordinate wird *nicht* beachtet und ausgegeben.
+ Wenn die Startkoordinate nicht im Spielfeld liegt, wird eine leere Liste zurückgegeben.
Args:
- state (GameState): Der aktuelle Spielzustand.
+ position (Coordinate): Die Ausgangskoordinate.
+ direction (Direction): Die Richtung.
Returns:
- Optional[int]: Die Rückfallposition des Hasen, oder None, wenn nicht gefunden.
+ List[FieldType]: Die Liste der Felder.
"""
- ...
- def fall_back(self, state: GameState) -> None:
+ def get_fields_on_line(self, position: Coordinate, direction: Direction) -> List[FieldType]:
"""
- Lässt den Hasen zu einer vorherigen Position zurückfallen.
+ Gibt eine Liste aller Feld-Typen zurück, die auf einer Gerade liegen.
+ Die Gerade wird aufgespannt durch eine Koordinate und einen Richtungsvektor.
+
+ Das Ergebnis wird in Richtung des "Vektorpfeils" abgelesen.
+ Wenn die Startkoordinate nicht im Spielfeld liegt, wird eine leere Liste zurückgegeben.
Args:
- state (GameState): Der aktuelle Spielzustand.
+ position (Coordinate): Die Startkoordinate für die Gerade.
+ direction (Direction): Der aufspannende Richtungsvektor.
- Raises:
- HUIError: Wenn der Hase nicht zurückfallen kann.
+ Returns:
+ List[FieldType]: Die Liste der Felder.
"""
...
- def is_ahead(self, state: GameState) -> bool:
+ def get_fish_on_line(self, position: Coordinate, direction: Direction) -> List[FieldType]:
"""
- Überprüft, ob der Hase vor dem anderen Spieler ist.
+ Funktioniert ähnlich wie *Board.get_fields_on_line()*,
+ gibt aber nur die Feldtypen, die Fische sind, auf einer Geraden als Liste aus.
Args:
- state (GameState): Der aktuelle Spielzustand.
+ position (Coordinate): Die Startkoordinate für die Gerade.
+ direction (Direction): Der aufspannende Richtungsvektor.
Returns:
- bool: True, wenn der Hase vor dem anderen Spieler ist, False sonst.
+ List[FieldType]: Die Liste der Fisch-Felder.
"""
...
@@ -353,200 +400,127 @@ class Move:
Repräsentiert einen Zug im Spiel.
Attribute:
- action (Advance | EatSalad | ExchangeCarrots | FallBack): Die Aktion, die der Zug ausführt.
+ start (Coordinate): Die Koordinate, von wo aus ein Fisch bewegt werden soll.
+ direction (Direction): Die Richtung, in die der Fisch schwimmt.
"""
- action: Advance | EatSalad | ExchangeCarrots | FallBack
+ start: Coordinate
+ direction: Direction
- def __init__(
- self, action: Advance | EatSalad | ExchangeCarrots | FallBack
- ) -> None: ...
- def perform(self, state: GameState) -> None:
- """
- Führt den Zug aus.
-
- Args:
- state (GameState): Der aktuelle Spielzustand.
-
- Raises:
- HUIError: Wenn der Zug nicht ausgeführt werden kann.
- """
- ...
+ def __init__(self, start: Coordinate, direction: Direction) -> None: ...
+ def __str__(self) -> str: ...
def __repr__(self) -> str: ...
- def __eq__(self) -> bool: ...
-
class GameState:
"""
- Repräsentiert den aktuellen Zustand des Spiels.
+ Repräsentiert einen Spielstand.
Attribute:
board (Board): Das Spielbrett.
turn (int): Die aktuelle Runde.
+ last_move (Optional[Move]): Der zuletzt ausgeführte Zug.
"""
board: Board
turn: int
last_move: Optional[Move]
- def __init__(
- self, board: Board, turn: int, player_one: Hare, player_two: Hare, last_move: Optional[Move]
- ) -> None: ...
- def perform_move(self, move: Move) -> GameState:
- """
- Führt einen Zug aus und gibt den neuen Spielzustand zurück.
-
- Args:
- move (Move): Der Zug, der ausgeführt werden soll.
-
- Returns:
- GameState: Der neue Spielzustand.
-
- Raises:
- HUIError: Wenn der Zug nicht ausgeführt werden kann.
- """
- ...
-
- def clone_current_player(self) -> Hare:
- """
- Gibt eine Kopie des aktuellen Spielers zurück.
-
- Returns:
- Hare: Eine Kopie des aktuellen Spielers.
- """
- ...
-
- def clone_other_player(self) -> Hare:
- """
- Gibt eine Kopie des anderen Spielers zurück.
-
- Returns:
- Hare: Eine Kopie des anderen Spielers.
- """
- ...
-
- def update_player(self, player: Hare) -> None:
- """
- Aktualisiert den Spieler.
-
- Args:
- player (Hare): Der Spieler, der aktualisiert werden soll.
- """
- ...
-
- def is_over(self) -> bool:
- """
- Überprüft, ob das Spiel vorbei ist.
-
- Returns:
- bool: True, wenn das Spiel vorbei ist, False sonst.
- """
- ...
-
- def possible_moves_old(self) -> List[Move]:
- """
- Gibt eine Liste aller möglichen Züge zurück.
+ def __init__(self, board: Board, turn: int, last_move: Optional[Move]) -> None: ...
+ def __str__(self) -> str: ...
+ def __repr__(self) -> str: ...
- Returns:
- List[Move]: Eine Liste aller möglichen Züge.
- """
- ...
-
def possible_moves(self) -> List[Move]:
"""
- Gibt eine Liste aller möglichen Züge zurück.
+ Berechnet alle Züge, die aus der aktuellen Spielposition für den aktuellen Spieler möglich sind.
Returns:
- List[Move]: Eine Liste aller möglichen Züge.
+ List[Move]: Die Liste der Züge.
"""
...
class RulesEngine:
"""
- Dient zur Überprüfung der Spielregeln.
+ Stellt Methoden, die zur Überprüfung der Spielregeln dienen.
"""
@staticmethod
- def calculates_carrots(distance: int) -> int:
+ def move_distance(board: Board, move_: Move) -> int:
"""
- Berechnet die Anzahl der Karotten, die für einen Zug benötigt werden.
+ Gibt die Länge / Anzahl der Felder von einem Zug auf dem Spielfeld zurück.
Args:
- distance (int): Die Entfernung, die zurückgelegt werden soll.
+ board (Board): Das Spielfeld, auf dem die Länge berechnet werden soll.
+ move_ (Move): Der zuverwendene Zug.
Returns:
- int: Die Anzahl der Karotten, die benötigt werden.
+ int: Die Länge.
"""
...
+
@staticmethod
- def can_exchange_carrots(board: Board, player: Hare, count: int) -> None:
+ def target_position(board: Board, move_: Move) -> Coordinate:
"""
- Überprüft, ob ein Spieler Karotten tauschen kann.
+ Gibt die Koordinate zurück, auf der ein Fisch landen würde, wenn man den Zug ausführt.
+
+ Es wird nicht berücksichtigt, ob diese Koordinate im Spielfeld ist.
Args:
- board (Board): Das Spielbrett.
- player (Hare): Der Spieler, der Karotten tauschen möchte.
- count (int): Die Anzahl der Karotten, die getauscht werden sollen.
+ board (Board): Das Spielfeld, auf dem der Zug berechnet werden soll.
+ move_ (Move): Der zuverwendene Zug.
- Raises:
- HUIError: Wenn der Spieler nicht genug Karotten hat oder wenn das Feld nicht ein Karottenfeld ist.
+ Returns:
+ Coordinate: Die Koordinate.
"""
...
@staticmethod
- def can_eat_salad(board: Board, player: Hare) -> None:
+ def is_in_bounds(coordinate: Coordinate) -> bool:
"""
- Überprüft, ob ein Spieler einen Salat essen kann.
+ Gibt einen Wahrheitswert zurück, ob eine Position in dem (Standard-) Spielfeld (10x10) liegt.
Args:
- board (Board): Das Spielbrett.
- player (Hare): Der Spieler, der einen Salat essen möchte.
+ coordinate (Coordinate): Die Position
- Raises:
- HUIError: Wenn der Spieler keinen Salat hat oder wenn das Feld nicht ein Salatfeld ist.
+ Returns:
+ bool: Ob die Koordinate im Feld ist.
"""
...
@staticmethod
- def has_to_eat_salad(board: Board, player: Hare) -> None:
+ def can_execute_move(board: Board, move_: Move) -> None:
"""
- Überprüft, ob ein Spieler einen Salat essen muss.
+ 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.
- Args:
- board (Board): Das Spielbrett.
- player (Hare): Der Spieler, der einen Salat essen muss.
+ Gibt keinen Wert zurück, sondern wirft eine Fehlermeldung, falls der Zug nicht valide ist.
- Raises:
- Exception: Wenn der Spieler nicht genug Salate hat oder wenn das Feld nicht ein Salatfeld ist.
+ Args:
+ board (Board): Das Spielfeld.
+ move_ (Move): Der Zug, der geprüft werden soll.
"""
...
- @staticmethod
- def can_move_to(
- board: Board,
- new_position: int,
- player: Hare,
- other_player: Hare,
- cards: List[Card],
- ) -> None:
+ @staticmethod
+ def get_team_on_turn(turn: int) -> TeamEnum:
"""
- Überprüft, ob ein Spieler zu einem bestimmten Feld ziehen kann.
+ 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:
- board (Board): Das Spielbrett.
- new_position (int): Die neue Position, zu der der Spieler ziehen möchte.
- player (Hare): Der Spieler, der ziehen möchte.
- other_player (Hare): Der andere Spieler.
- cards (List[Card]): Die Karten, die der Spieler spielen möchte.
+ turn (int): Die Zugzahl
- Raises:
- Exception: Wenn das Feld besetzt ist oder wenn der Spieler nicht genug Karotten hat.
+ Returns:
+ TeamEnum: Das Team, was dran ist.
"""
...
class PluginConstants:
- NUM_FIELDS: int
- INITIAL_SALADS: int
- INITIAL_CARROTS: int
+ """
+ Hält globale Konstanten.
+ """
+
+ BOARD_WIDTH: int
+ BOARD_HEIGHT: int
+
ROUND_LIMIT: int
diff --git a/python/socha/_socha2025.pyi b/python/socha/_socha2025.pyi
new file mode 100644
index 0000000..953efea
--- /dev/null
+++ b/python/socha/_socha2025.pyi
@@ -0,0 +1,552 @@
+from enum import Enum
+from typing import List, Optional
+
+class Card(Enum):
+ """
+ Eine Karte, die im Spiel verwendet werden kann.
+
+ Attributes:
+ FallBack (int): Die Karte, die den Spieler zurückfallen lässt.
+ HurryAhead (int): Die Karte, die den Spieler vorrücken lässt.
+ EatSalad (int): Die Karte, die den Spieler Salat essen lässt.
+ SwapCarrots (int): Die Karte, die die Karotten der Spieler tauscht.
+ """
+
+ FallBack: int = 0
+ HurryAhead: int = 1
+ EatSalad: int = 2
+ SwapCarrots: int = 3
+
+ def __init__(self) -> None: ...
+ def moves(self) -> bool:
+ """
+ Gibt an, ob die Karte den Spieler bewegt.
+
+ Args:
+ None
+
+ Returns:
+ bool: True, wenn die Karte den Spieler bewegt, False sonst.
+ """
+ ...
+
+ def perform(self, state: GameState, remaining_cards: List[Card]) -> None:
+ """
+ Führt die Karte aus.\n
+ Diese Methode **kann** den GameState **mutieren**, unabhängig vom Erfolg des Zuges.
+
+ Args:
+ state (GameState): Der aktuelle Spielzustand.
+ remaining_cards (List[Card]): Die verbleibenden Karten.
+
+ Raises:
+ HUIError: Ein Fehler, wenn die Karte nicht ausgeführt werden kann.
+ """
+ ...
+
+class Advance:
+ """
+ Eine Klasse, die einen Vorwärtszug im Spiel darstellt.
+
+ Attributes:
+ distance (int): Die Anzahl der Felder, die der Spieler vorrücken soll.
+ cards (List[Card]): Die Karten, die während des Vorgangs gespielt werden.
+ """
+
+ distance: int
+ cards: List[Card]
+
+ def __init__(self, distance: int, cards: List[Card]): ...
+ def perform(self, state: GameState) -> None:
+ """
+ Führt den Vorwärtszug aus.\n
+ Diese Methode **kann** den GameState **mutieren**, unabhängig vom Erfolg des Zuges.
+
+ Args:
+ state (GameState): Der aktuelle Zustand des Spiels.
+
+ Raises:
+ HUIError: Wenn der Vorwärtszug nicht erfolgreich ausgeführt werden kann.
+ """
+ ...
+
+class EatSalad:
+ def __init__(self) -> None: ...
+ def perform(self, state: GameState) -> None:
+ """
+ Führt die Salat-Fressen Aktion aus.\n
+ Diese Methode **kann** den GameState **mutieren**, unabhängig vom Erfolg des Zuges.
+
+ Args:
+ state (GameState): Der aktuelle Zustand des Spiels.
+
+ Raises:
+ HUIError: Wenn der Salat nicht erfolgreich gegessen werden kann.
+ """
+ ...
+
+class ExchangeCarrots:
+ amount: int
+
+ def __init__(self, amount: int) -> None: ...
+ def perform(self, state: GameState) -> None:
+ """
+ Führt die Karotten-Tausch-Aktion aus.\n
+ Diese Methode **kann** den GameState **mutieren**, unabhängig vom Erfolg des Zuges.
+
+ Args:
+ state (GameState): Der aktuelle Zustand des Spiels.
+
+ Raises:
+ HUIError: Wenn die Karotten nicht erfolgreich getauscht werden können.
+ """
+ ...
+
+class FallBack:
+ def __init__(self) -> None: ...
+ def perform(self, state: GameState) -> None: ...
+
+class Field(Enum):
+ Position1: int = 0
+ """
+ Zahlfeld
+ """
+ Position2: int = 1
+ """
+ Flaggenfeld
+ """
+ Hedgehog: int = 2
+ """
+ Igelfeld: Hierauf kann nur rückwärts gezogen werden.
+ """
+ Salad: int = 3
+ """
+ Salatfeld: Beim Betreten wird im nächsten Zug ein Salat gegessen.
+ """
+ Carrots: int = 4
+ """
+ Karottenfeld: Hier dürfen Karotten getauscht werden.
+ """
+ Hare: int = 5
+ """
+ Hasenfeld: Hier wird sofort eine Hasenkarte gespielt.
+ """
+ Market: int = 6
+ """
+ Marktfeld: Hier wird eine Hasenkarte gekauft (Variation).
+ """
+ Goal: int = 7
+ """
+ Zielfeld
+ """
+ Start: int = 8
+ """
+ Startfeld
+ """
+
+class Board:
+ """
+ Ein Spielbrett, das die Felder des Spiels enthält.
+
+ Attributes:
+ track (List[Field]): Die Liste der Felder, die das Spielbrett darstellen
+ """
+
+ track: list[Field]
+
+ def __init__(self, track: list[Field]) -> None: ...
+ def get_field(self, index: int) -> Optional[Field]:
+ """
+ Gibt das Feld am angegebenen Index zurück.
+
+ Args:
+ index (int): Der Index des Feldes, das abgerufen werden soll.
+
+ Returns:
+ Field: Das Feld am angegebenen Index, oder None, wenn außerhalb des gültigen Bereichs.
+ """
+ ...
+ def find_field(self, field: Field, start: int, end: int) -> Optional[int]:
+ """
+ Findet den ersten Index des angegebenen Feldes innerhalb des angegebenen Bereichs.
+
+ Args:
+ field (Field): Das Feld, nach dem gesucht werden soll.
+ start (int): Der Startindex des Bereichs, in dem gesucht werden soll.
+ end (int): Der Endindex des Bereichs, in dem gesucht werden soll.
+
+ Returns:
+ int: Der Index des Feldes, wenn gefunden, oder None, wenn nicht gefunden.
+ """
+ ...
+ def get_previous_field(self, field: Field, index: int) -> Optional[int]:
+ """
+ Findet die vorherige Vorkommen des angegebenen Feldes vor dem angegebenen Index.
+
+ Args:
+ field (Field): Das Feld, nach dem gesucht werden soll.
+ index (int): Der Index, von dem aus gesucht werden soll.
+
+ Returns:
+ int: Der Index des vorherigen Vorkommens des Feldes, oder None, wenn nicht gefunden.
+ """
+ ...
+ def get_next_field(self, field: Field, index: int) -> Optional[int]:
+ """
+ Findet das nächste Vorkommen des angegebenen Feldes nach dem angegebenen Index.
+
+ Args:
+ field (Field): Das Feld, nach dem gesucht werden soll.
+ index (int): Der Index, von dem aus gesucht werden soll.
+
+ Returns:
+ int: Der Index des nächsten Vorkommens des Feldes, oder None, wenn nicht gefunden.
+ """
+ ...
+
+class TeamEnum(Enum):
+ One: int = 0
+ """
+ Team 1
+ """
+ Two: int = 1
+ """
+ Team 2
+ """
+
+ def __repr__(self) -> str: ...
+
+class Hare:
+ """
+ Repräsentiert einen Hasen im Spiel.
+
+ Attribute:
+ team (TeamEnum): Das Team, dem der Hase angehört.
+ position (int): Die aktuelle Position des Hasen auf dem Brett.
+ salads (int): Die Anzahl der Salate, die der Hase hat.
+ carrots (int): Die Anzahl der Karotten, die der Hase hat.
+ last_move (Optional[Move]): Der letzte Zug, den der Hase gemacht hat.
+ cards (List[Card]): Die Karten, die der Hase hat.
+ """
+
+ team: TeamEnum
+ position: int
+ salads: int
+ carrots: int
+ last_move: Optional[Move]
+ cards: List[Card]
+
+ def __init__(
+ self,
+ team: TeamEnum,
+ cards: Optional[List[Card]] = None,
+ carrots: Optional[int] = None,
+ salads: Optional[int] = None,
+ last_move: Optional[Move] = None,
+ position: Optional[int] = None,
+ ) -> None: ...
+ def is_in_goal(self) -> bool:
+ """
+ Überprüft, ob der Hase im Ziel ist.
+
+ Returns:
+ bool: True, wenn der Hase im Ziel ist, False sonst.
+ """
+ ...
+
+ def can_enter_goal(self) -> bool:
+ """
+ Überprüft, ob der Hase das Ziel betreten kann.
+
+ Returns:
+ bool: True, wenn der Hase das Ziel betreten kann, False sonst.
+ """
+ ...
+
+ def advance_by(self, state: GameState, distance: int, cards: List[Card]) -> None:
+ """
+ Rückt den Hasen um eine bestimmte Entfernung vor.
+
+ Args:
+ state (GameState): Der aktuelle Spielzustand.
+ distance (int): Die Entfernung, um die der Hase vorrücken soll.
+ cards (List[Card]): Die Karten, die während des Vorgangs gespielt werden.
+
+ Raises:
+ HUIError: Wenn der Hase nicht vorrücken kann.
+ """
+ ...
+
+ def exchange_carrots(self, state: GameState, carrots: int) -> None:
+ """
+ Tauscht Karotten mit dem Hasen.
+
+ Args:
+ state (GameState): Der aktuelle Spielzustand.
+ carrots (int): Die Anzahl der Karotten, die getauscht werden sollen.
+
+ Raises:
+ HUIError: Wenn die Karotten nicht getauscht werden können.
+ """
+ ...
+
+ def consume_carrots(self, state: GameState, carrots: int) -> None:
+ """
+ Verbraucht Karotten vom Hasen.
+
+ Args:
+ state (GameState): Der aktuelle Spielzustand.
+ carrots (int): Die Anzahl der Karotten, die verbraucht werden sollen.
+
+ Raises:
+ HUIError: Wenn die Karotten nicht verbraucht werden können.
+ """
+ ...
+
+ def eat_salad(self, state: GameState) -> None:
+ """
+ Lässt den Hasen einen Salat essen.
+
+ Args:
+ state (GameState): Der aktuelle Spielzustand.
+ """
+ ...
+
+ def get_fall_back(self, state: GameState) -> Optional[int]:
+ """
+ Gibt den nächsten möglich Index zurück, an dem der Hase zurückfallen kann.
+
+ Args:
+ state (GameState): Der aktuelle Spielzustand.
+
+ Returns:
+ Optional[int]: Die Rückfallposition des Hasen, oder None, wenn nicht gefunden.
+ """
+ ...
+
+ def fall_back(self, state: GameState) -> None:
+ """
+ Lässt den Hasen zu einer vorherigen Position zurückfallen.
+
+ Args:
+ state (GameState): Der aktuelle Spielzustand.
+
+ Raises:
+ HUIError: Wenn der Hase nicht zurückfallen kann.
+ """
+ ...
+
+ def is_ahead(self, state: GameState) -> bool:
+ """
+ Überprüft, ob der Hase vor dem anderen Spieler ist.
+
+ Args:
+ state (GameState): Der aktuelle Spielzustand.
+
+ Returns:
+ bool: True, wenn der Hase vor dem anderen Spieler ist, False sonst.
+ """
+ ...
+
+class Move:
+ """
+ Repräsentiert einen Zug im Spiel.
+
+ Attribute:
+ action (Advance | EatSalad | ExchangeCarrots | FallBack): Die Aktion, die der Zug ausführt.
+ """
+
+ action: Advance | EatSalad | ExchangeCarrots | FallBack
+
+ def __init__(
+ self, action: Advance | EatSalad | ExchangeCarrots | FallBack
+ ) -> None: ...
+ def perform(self, state: GameState) -> None:
+ """
+ Führt den Zug aus.
+
+ Args:
+ state (GameState): Der aktuelle Spielzustand.
+
+ Raises:
+ HUIError: Wenn der Zug nicht ausgeführt werden kann.
+ """
+ ...
+ def __repr__(self) -> str: ...
+
+ def __eq__(self) -> bool: ...
+
+class GameState:
+ """
+ Repräsentiert den aktuellen Zustand des Spiels.
+
+ Attribute:
+ board (Board): Das Spielbrett.
+ turn (int): Die aktuelle Runde.
+ """
+
+ board: Board
+ turn: int
+ last_move: Optional[Move]
+
+ def __init__(
+ self, board: Board, turn: int, player_one: Hare, player_two: Hare, last_move: Optional[Move]
+ ) -> None: ...
+ def perform_move(self, move: Move) -> GameState:
+ """
+ Führt einen Zug aus und gibt den neuen Spielzustand zurück.
+
+ Args:
+ move (Move): Der Zug, der ausgeführt werden soll.
+
+ Returns:
+ GameState: Der neue Spielzustand.
+
+ Raises:
+ HUIError: Wenn der Zug nicht ausgeführt werden kann.
+ """
+ ...
+
+ def clone_current_player(self) -> Hare:
+ """
+ Gibt eine Kopie des aktuellen Spielers zurück.
+
+ Returns:
+ Hare: Eine Kopie des aktuellen Spielers.
+ """
+ ...
+
+ def clone_other_player(self) -> Hare:
+ """
+ Gibt eine Kopie des anderen Spielers zurück.
+
+ Returns:
+ Hare: Eine Kopie des anderen Spielers.
+ """
+ ...
+
+ def update_player(self, player: Hare) -> None:
+ """
+ Aktualisiert den Spieler.
+
+ Args:
+ player (Hare): Der Spieler, der aktualisiert werden soll.
+ """
+ ...
+
+ def is_over(self) -> bool:
+ """
+ Überprüft, ob das Spiel vorbei ist.
+
+ Returns:
+ bool: True, wenn das Spiel vorbei ist, False sonst.
+ """
+ ...
+
+ def possible_moves_old(self) -> List[Move]:
+ """
+ Gibt eine Liste aller möglichen Züge zurück.
+
+ Returns:
+ List[Move]: Eine Liste aller möglichen Züge.
+ """
+ ...
+
+ def possible_moves(self) -> List[Move]:
+ """
+ Gibt eine Liste aller möglichen Züge zurück.
+
+ Returns:
+ List[Move]: Eine Liste aller möglichen Züge.
+ """
+ ...
+
+class RulesEngine:
+ """
+ Dient zur Überprüfung der Spielregeln.
+ """
+
+ @staticmethod
+ def calculates_carrots(distance: int) -> int:
+ """
+ Berechnet die Anzahl der Karotten, die für einen Zug benötigt werden.
+
+ Args:
+ distance (int): Die Entfernung, die zurückgelegt werden soll.
+
+ Returns:
+ int: Die Anzahl der Karotten, die benötigt werden.
+ """
+ ...
+
+ @staticmethod
+ def can_exchange_carrots(board: Board, player: Hare, count: int) -> None:
+ """
+ Überprüft, ob ein Spieler Karotten tauschen kann.
+
+ Args:
+ board (Board): Das Spielbrett.
+ player (Hare): Der Spieler, der Karotten tauschen möchte.
+ count (int): Die Anzahl der Karotten, die getauscht werden sollen.
+
+ Raises:
+ HUIError: Wenn der Spieler nicht genug Karotten hat oder wenn das Feld nicht ein Karottenfeld ist.
+ """
+ ...
+
+ @staticmethod
+ def can_eat_salad(board: Board, player: Hare) -> None:
+ """
+ Überprüft, ob ein Spieler einen Salat essen kann.
+
+ Args:
+ board (Board): Das Spielbrett.
+ player (Hare): Der Spieler, der einen Salat essen möchte.
+
+ Raises:
+ HUIError: Wenn der Spieler keinen Salat hat oder wenn das Feld nicht ein Salatfeld ist.
+ """
+ ...
+
+ @staticmethod
+ def has_to_eat_salad(board: Board, player: Hare) -> None:
+ """
+ Überprüft, ob ein Spieler einen Salat essen muss.
+
+ Args:
+ board (Board): Das Spielbrett.
+ player (Hare): Der Spieler, der einen Salat essen muss.
+
+ Raises:
+ Exception: Wenn der Spieler nicht genug Salate hat oder wenn das Feld nicht ein Salatfeld ist.
+ """
+ ...
+
+ @staticmethod
+ def can_move_to(
+ board: Board,
+ new_position: int,
+ player: Hare,
+ other_player: Hare,
+ cards: List[Card],
+ ) -> None:
+ """
+ Überprüft, ob ein Spieler zu einem bestimmten Feld ziehen kann.
+
+ Args:
+ board (Board): Das Spielbrett.
+ new_position (int): Die neue Position, zu der der Spieler ziehen möchte.
+ player (Hare): Der Spieler, der ziehen möchte.
+ other_player (Hare): Der andere Spieler.
+ cards (List[Card]): Die Karten, die der Spieler spielen möchte.
+
+ Raises:
+ Exception: Wenn das Feld besetzt ist oder wenn der Spieler nicht genug Karotten hat.
+ """
+ ...
+
+class PluginConstants:
+ NUM_FIELDS: int
+ INITIAL_SALADS: int
+ INITIAL_CARROTS: int
+ ROUND_LIMIT: int
diff --git a/python/socha/api/networking/game_client.py b/python/socha/api/networking/game_client.py
index f4b26d3..b1d0ea1 100644
--- a/python/socha/api/networking/game_client.py
+++ b/python/socha/api/networking/game_client.py
@@ -279,11 +279,7 @@ def _on_move_request(self, room_id):
logging.error(f'{move_response} is not a valid move.')
def _on_state(self, message):
- second_last_move = None # last move from last gamestate
- if len(self._game_handler.history[-1]) > 0 and isinstance(self._game_handler.history[-1][-1], GameState):
- second_last_move = self._game_handler.history[-1][-1].last_move
-
- _state = message_to_state(message, second_last_move)
+ _state = message_to_state(message)
self._game_handler.history[-1].append(_state)
self._game_handler.on_update(_state)
diff --git a/python/socha/api/networking/utils.py b/python/socha/api/networking/utils.py
index 2616cd2..46f87dd 100644
--- a/python/socha/api/networking/utils.py
+++ b/python/socha/api/networking/utils.py
@@ -2,9 +2,9 @@
from typing import List
from socha import _socha
from socha.api.protocol.protocol import (
+ Coordinate,
Board,
Room,
- Hare,
State,
Data,
)
@@ -16,83 +16,87 @@ def map_board(protocol_board: Board) -> _socha.Board:
: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: penguins.Board
+ :rtype: _socha.Board
"""
- track: List[_socha.Field] = []
-
- for field in protocol_board.field_value:
- if field == 'START':
- track.append(_socha.Field.Start)
- elif field == 'MARKET':
- track.append(_socha.Field.Market)
- elif field == 'HARE':
- track.append(_socha.Field.Hare)
- elif field == 'HEDGEHOG':
- track.append(_socha.Field.Hedgehog)
- elif field == 'CARROTS':
- track.append(_socha.Field.Carrots)
- elif field == 'POSITION_1':
- track.append(_socha.Field.Position1)
- elif field == 'POSITION_2':
- track.append(_socha.Field.Position2)
- elif field == 'SALAD':
- track.append(_socha.Field.Salad)
- elif field == 'GOAL':
- track.append(_socha.Field.Goal)
- else:
- raise ValueError(f'Unknown field type: {field}')
-
- return _socha.Board(track=track)
-
-
-def map_card_to_string(card: _socha.Card) -> str:
- if card == _socha.Card.EatSalad:
- return 'EAT_SALAD'
- elif card == _socha.Card.HurryAhead:
- return 'HURRY_AHEAD'
- elif card == _socha.Card.FallBack:
- return 'FALL_BACK'
- elif card == _socha.Card.SwapCarrots:
- return 'SWAP_CARROTS'
+
+ 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}')
+
+ 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 card type: {card}')
-
-
-def map_string_to_card(card: str) -> _socha.Card:
- card = re.sub(r'[^A-Za-z0-9_]', '', card)
-
- if card == 'EAT_SALAD':
- return _socha.Card.EatSalad
- elif card == 'HURRY_AHEAD':
- return _socha.Card.HurryAhead
- elif card == 'FALL_BACK':
- return _socha.Card.FallBack
- elif card == 'SWAP_CARROTS':
- return _socha.Card.SwapCarrots
+ 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 card type: {card}')
-
+ raise ValueError(f'Unknown direction: {direction}')
def handle_move(move_response: _socha.Move) -> Data:
- if isinstance(move_response.action, _socha.Advance):
- advance: _socha.Advance = move_response.action
- return Data(
- class_value='advance',
- distance=advance.distance,
- card=[map_card_to_string(card) for card in advance.cards],
- )
- elif isinstance(move_response.action, _socha.EatSalad):
- return Data(class_value='eatsalad')
- elif isinstance(move_response.action, _socha.ExchangeCarrots):
- exchangeCarrots: _socha.ExchangeCarrots = move_response.action
- return Data(class_value='exchangecarrots', amount=exchangeCarrots.amount)
- elif isinstance(move_response.action, _socha.FallBack):
- return Data(class_value='fallback')
- else:
- raise ValueError(f'Unknown move response action: {move_response.action}')
+ 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),
+ )
-def message_to_state(message: Room, second_last_move: _socha.Move) -> _socha.GameState:
+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.
@@ -105,42 +109,13 @@ def message_to_state(message: Room, second_last_move: _socha.Move) -> _socha.Gam
GameState: The constructed game state from the message.
"""
-
state: State = message.data.class_binding
# extract last move of current gameState
- state_last_move = _socha.Move(action=state.last_move.class_binding) if state.last_move and state.last_move.class_binding else None
-
- def create_hare(hare: Hare) -> _socha.Hare:
-
- players_last_move = None
-
- if state.turn % 2 == 0: # now is ONE at turn, last turn was None or TWO
- if hare.team == 'ONE':
- players_last_move = second_last_move
- else:
- players_last_move = state_last_move
- else:
- if hare.team == 'TWO':
- players_last_move = second_last_move
- else:
- players_last_move = state_last_move
-
- return _socha.Hare(
- cards=[map_string_to_card(card) for card in hare.cards.card]
- if hare.cards
- else [],
- carrots=hare.carrots,
- position=hare.position,
- last_move=players_last_move,
- salads=hare.salads,
- team=_socha.TeamEnum.One if hare.team == 'ONE' else _socha.TeamEnum.Two,
- )
+ 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),
- player_one=create_hare(state.hare[0]),
- player_two=create_hare(state.hare[1]),
turn=state.turn,
last_move=state_last_move,
)
diff --git a/python/socha/api/networking/xml_protocol_interface.py b/python/socha/api/networking/xml_protocol_interface.py
index cdccde5..c8507fa 100644
--- a/python/socha/api/networking/xml_protocol_interface.py
+++ b/python/socha/api/networking/xml_protocol_interface.py
@@ -1,12 +1,12 @@
"""
-Here are all incoming byte streams and all outgoing protocol objects handheld.
+Here are all incoming byte streams and all outgoing protocol objects handled.
"""
import contextlib
import logging
from typing import Any, Callable, Iterator
-from socha.api.networking.utils import map_string_to_card
+from socha.api.networking.utils import map_string_to_direction
from socha import _socha
from socha.api.networking.network_socket import NetworkSocket
from socha.api.protocol.protocol import (
@@ -24,10 +24,11 @@
from xsdata.formats.dataclass.serializers import XmlSerializer
from xsdata.formats.dataclass.serializers.config import SerializerConfig
-from socha.api.protocol.protocol import Data, LastAction, LastMove
+from socha.api.protocol.protocol import Data, LastMove
-def map_object(data: Data | LastAction | LastMove, params: dict):
+def map_object(data: Data , params: dict):
+
try:
params.pop('class_binding')
except KeyError:
@@ -56,32 +57,32 @@ def map_object(data: Data | LastAction | LastMove, params: dict):
originalMessage=params.get('original_message'),
)
return data(class_binding=error_object, **params)
- elif params.get('class_value') == 'advance':
- advance_object = _socha.Advance(
- distance=params.get('distance'),
- cards=[map_string_to_card(card) for card in params.get('card', [])],
- )
- return data(class_binding=advance_object, **params)
- elif params.get('class_value') == 'exchangecarrots':
- exchange_object = _socha.ExchangeCarrots(amount=params.get('amount'))
- return data(class_binding=exchange_object, **params)
- elif params.get('class_value') == 'fallback':
- back_object = _socha.FallBack()
- return data(class_binding=back_object, **params)
- elif params.get('class_value') == 'eatsalad':
- salad_object = _socha.EatSalad()
- return data(class_binding=salad_object, **params)
- elif params.get('class_value') == 'card': # work around for LastAction, because buggy and now prevents warning
- return data(**params)
else:
logging.warn('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):
- if clazz.__name__ == 'Data' or clazz.__name__ == 'LastAction' or clazz.__name__ == 'LastMove':
+ # 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)
@@ -102,11 +103,19 @@ def __init__(self, host: str, port: int):
context = XmlContext()
deserialize_config = ParserConfig(class_factory=custom_class_factory)
+
self.deserializer = XmlParser(
- handler=XmlEventHandler, context=context, config=deserialize_config
+ handler=XmlEventHandler,
+ context=context,
+ config=deserialize_config,
+ )
+
+ serialize_config = SerializerConfig(
+ pretty_print=True,
+ xml_declaration=False,
+ encoding='utf-8',
)
- serialize_config = SerializerConfig(pretty_print=True, xml_declaration=False)
self.serializer = XmlSerializer(config=serialize_config)
def connect(self):
@@ -134,6 +143,12 @@ 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"]
+ for i, t in enumerate(unicodes):
+ receiving = receiving.replace(t, replaces[i])
cls = self._deserialize_object(receiving)
return cls
diff --git a/python/socha/api/protocol/protocol.py b/python/socha/api/protocol/protocol.py
index ea002c3..f4d9d0c 100644
--- a/python/socha/api/protocol/protocol.py
+++ b/python/socha/api/protocol/protocol.py
@@ -17,9 +17,9 @@
@dataclass
-class Board:
+class Row:
class Meta:
- name = 'board'
+ name = 'row'
field_value: List[str] = field(
default_factory=list,
@@ -32,13 +32,14 @@ class Meta:
@dataclass
-class Cards:
+class Board:
class Meta:
- name = 'cards'
+ name = 'board'
- card: List[str] = field(
+ rows: List[Row] = field(
default_factory=list,
metadata={
+ 'name': 'row',
'type': 'Element',
'min_occurs': 1,
},
@@ -46,128 +47,58 @@ class Meta:
@dataclass
-class LastAction:
+class Coordinate:
+
class Meta:
- name = 'lastAction'
+ name = 'from'
- class_value: Optional[str] = field(
+ x: Optional[int] = field(
default=None,
metadata={
- 'name': 'class',
'type': 'Attribute',
- 'required': True,
},
)
- class_binding: Optional[object] = field(default=None)
- distance: Optional[int] = field(
+ y: Optional[int] = field(
default=None,
metadata={
'type': 'Attribute',
},
)
- card: Optional[List[str]] = field(
- default=None,
- metadata={
- 'type': 'Element',
- },
- )
- amount: Optional[int] = field(
- default=None,
- metadata={
- 'type': 'Attribute',
- },
- )
- value: str = field(default='')
@dataclass
class LastMove:
class Meta:
name = 'lastMove'
-
- class_value: Optional[str] = field(
- default=None,
- metadata={
- 'name': 'class',
- 'type': 'Attribute',
- 'required': True,
- },
- )
+
class_binding: Optional[object] = field(default=None)
- distance: Optional[int] = field(
- default=None,
- metadata={
- 'type': 'Attribute',
- },
- )
- card: Optional[List[str]] = field(
+ from_: Optional[Coordinate] = field(
default=None,
metadata={
+ 'name': 'from',
'type': 'Element',
},
)
- amount: Optional[int] = field(
+ direction: Optional[str] = field(
default=None,
metadata={
- 'type': 'Attribute',
+ 'type': 'Element',
},
)
@dataclass
-class Hare:
+class Player:
class Meta:
- name = 'hare'
+ name = 'player'
- team: Optional[str] = field(
- default=None,
- metadata={
- 'type': 'Attribute',
- 'required': True,
- },
- )
- position: Optional[int] = field(
- default=None,
- metadata={
- 'type': 'Attribute',
- 'required': True,
- },
- )
- salads: Optional[int] = field(
- default=None,
- metadata={
- 'type': 'Attribute',
- 'required': True,
- },
- )
- carrots: Optional[int] = field(
+ name: Optional[str] = field(
default=None,
metadata={
'type': 'Attribute',
'required': True,
},
)
- last_action: Optional[LastAction] = field(
- default=None,
- metadata={
- 'name': 'lastAction',
- 'type': 'Element',
- },
- )
- cards: Optional[Cards] = field(
- default=None,
- metadata={
- 'type': 'Element',
- 'required': True,
- },
- )
-
-
-@dataclass
-class Player:
- class Meta:
- name = 'player'
-
team: Optional[str] = field(
default=None,
metadata={
@@ -212,13 +143,6 @@ class Meta:
'required': True,
},
)
- hare: List[Hare] = field(
- default_factory=list,
- metadata={
- 'type': 'Element',
- 'min_occurs': 1,
- },
- )
last_move: Optional[LastMove] = field(
default=None,
metadata={
@@ -734,15 +658,15 @@ class Meta:
'required': True,
},
)
- distance: Optional[int] = field(
+ from_: Optional[Coordinate] = field(
default=None,
metadata={
+ 'name': 'from',
'type': 'Element',
- 'required': True,
},
)
- card: List[str] = field(
- default_factory=list,
+ direction: Optional[str] = field(
+ default=None,
metadata={
'type': 'Element',
},
@@ -798,34 +722,29 @@ class Meta:
'type': 'Element',
},
)
- distance: Optional[int] = field(
- default=None,
- metadata={
- 'type': 'Attribute',
- },
- )
- card: Optional[List[str]] = field(
+ state: Optional[State] = field(
default=None,
metadata={
'type': 'Element',
},
)
- state: Optional[State] = field(
+ color: Optional[str] = field(
default=None,
metadata={
- 'type': 'Element',
+ 'type': 'Attribute',
},
)
- amount: Optional[int] = field(
+ from_: Optional[Coordinate] = field(
default=None,
metadata={
- 'type': 'Attribute',
+ 'name': 'from',
+ 'type': 'Element',
},
)
- color: Optional[str] = field(
+ direction: Optional[str] = field(
default=None,
metadata={
- 'type': 'Attribute',
+ 'type': 'Element',
},
)
diff --git a/python/socha/starter.py b/python/socha/starter.py
index 673d1ec..b722f26 100644
--- a/python/socha/starter.py
+++ b/python/socha/starter.py
@@ -246,7 +246,7 @@ def _handle_start_args():
parser.add_argument(
"--python-version",
- help="Specifies the build python version (e.g.: 3.10 - this is standard]).",
+ help="Specifies the build python version (e.g.: 3.10 - this is standard).",
)
return parser.parse_args()
diff --git a/src/lib.rs b/src/lib.rs
index 2349e02..c746fe7 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,39 +1,35 @@
-use plugin::rules_engine::RulesEngine;
use pyo3::*;
use types::PyModule;
-pub mod plugin;
-
-use crate::plugin::action::advance::Advance;
-use crate::plugin::action::card::Card;
-use crate::plugin::action::eat_salad::EatSalad;
-use crate::plugin::action::exchange_carrots::ExchangeCarrots;
-use crate::plugin::action::fall_back::FallBack;
-use crate::plugin::board::Board;
-use crate::plugin::constants::PluginConstants;
-use crate::plugin::field::Field;
-use crate::plugin::game_state::GameState;
-use crate::plugin::hare::Hare;
-use crate::plugin::hare::TeamEnum;
-use crate::plugin::r#move::Move;
+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;
#[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::()?;
diff --git a/src/lib2025.rs b/src/lib2025.rs
new file mode 100644
index 0000000..b89f019
--- /dev/null
+++ b/src/lib2025.rs
@@ -0,0 +1,41 @@
+use plugin2025::rules_engine::RulesEngine;
+use pyo3::*;
+use types::PyModule;
+
+pub mod plugin2025;
+
+use crate::plugin2025::action::advance::Advance;
+use crate::plugin2025::action::card::Card;
+use crate::plugin2025::action::eat_salad::EatSalad;
+use crate::plugin2025::action::exchange_carrots::ExchangeCarrots;
+use crate::plugin2025::action::fall_back::FallBack;
+use crate::plugin2025::board::Board;
+use crate::plugin2025::constants::PluginConstants;
+use crate::plugin2025::field::Field;
+use crate::plugin2025::game_state::GameState;
+use crate::plugin2025::hare::Hare;
+use crate::plugin2025::hare::TeamEnum;
+use crate::plugin2025::r#move::Move;
+
+#[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::()?;
+
+ Ok(())
+}
diff --git a/src/plugin.rs b/src/plugin2025.rs
similarity index 100%
rename from src/plugin.rs
rename to src/plugin2025.rs
diff --git a/src/plugin/action.rs b/src/plugin2025/action.rs
similarity index 100%
rename from src/plugin/action.rs
rename to src/plugin2025/action.rs
diff --git a/src/plugin/action/advance.rs b/src/plugin2025/action/advance.rs
similarity index 97%
rename from src/plugin/action/advance.rs
rename to src/plugin2025/action/advance.rs
index 182f2fb..b39e33c 100644
--- a/src/plugin/action/advance.rs
+++ b/src/plugin2025/action/advance.rs
@@ -1,6 +1,6 @@
use pyo3::{pyclass, pymethods, PyErr};
-use crate::plugin::{errors::HUIError, field::Field, game_state::GameState, hare::Hare};
+use crate::plugin2025::{errors::HUIError, field::Field, game_state::GameState, hare::Hare};
use super::card::Card;
diff --git a/src/plugin/action/card.rs b/src/plugin2025/action/card.rs
similarity index 99%
rename from src/plugin/action/card.rs
rename to src/plugin2025/action/card.rs
index 32fad01..6cc5883 100644
--- a/src/plugin/action/card.rs
+++ b/src/plugin2025/action/card.rs
@@ -2,7 +2,7 @@ use std::mem::swap;
use pyo3::*;
-use crate::plugin::{
+use crate::plugin2025::{
constants::PluginConstants, errors::HUIError, field::Field, game_state::GameState, hare::Hare,
rules_engine::RulesEngine,
};
diff --git a/src/plugin/action/eat_salad.rs b/src/plugin2025/action/eat_salad.rs
similarity index 90%
rename from src/plugin/action/eat_salad.rs
rename to src/plugin2025/action/eat_salad.rs
index f15603b..cdb8088 100644
--- a/src/plugin/action/eat_salad.rs
+++ b/src/plugin2025/action/eat_salad.rs
@@ -1,6 +1,6 @@
use pyo3::*;
-use crate::plugin::{game_state::GameState, rules_engine::RulesEngine};
+use crate::plugin2025::{game_state::GameState, rules_engine::RulesEngine};
#[pyclass]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Hash, Default)]
diff --git a/src/plugin/action/exchange_carrots.rs b/src/plugin2025/action/exchange_carrots.rs
similarity index 94%
rename from src/plugin/action/exchange_carrots.rs
rename to src/plugin2025/action/exchange_carrots.rs
index 56c8348..025846b 100644
--- a/src/plugin/action/exchange_carrots.rs
+++ b/src/plugin2025/action/exchange_carrots.rs
@@ -1,6 +1,6 @@
use pyo3::*;
-use crate::plugin::game_state::GameState;
+use crate::plugin2025::game_state::GameState;
#[pyclass]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Hash)]
diff --git a/src/plugin/action/fall_back.rs b/src/plugin2025/action/fall_back.rs
similarity index 93%
rename from src/plugin/action/fall_back.rs
rename to src/plugin2025/action/fall_back.rs
index 7f2e35e..dbb8291 100644
--- a/src/plugin/action/fall_back.rs
+++ b/src/plugin2025/action/fall_back.rs
@@ -1,6 +1,6 @@
use pyo3::*;
-use crate::plugin::game_state::GameState;
+use crate::plugin2025::game_state::GameState;
#[pyclass]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Hash, Default)]
diff --git a/src/plugin/board.rs b/src/plugin2025/board.rs
similarity index 100%
rename from src/plugin/board.rs
rename to src/plugin2025/board.rs
diff --git a/src/plugin/constants.rs b/src/plugin2025/constants.rs
similarity index 100%
rename from src/plugin/constants.rs
rename to src/plugin2025/constants.rs
diff --git a/src/plugin/errors.rs b/src/plugin2025/errors.rs
similarity index 100%
rename from src/plugin/errors.rs
rename to src/plugin2025/errors.rs
diff --git a/src/plugin/field.rs b/src/plugin2025/field.rs
similarity index 100%
rename from src/plugin/field.rs
rename to src/plugin2025/field.rs
diff --git a/src/plugin/game_state.rs b/src/plugin2025/game_state.rs
similarity index 100%
rename from src/plugin/game_state.rs
rename to src/plugin2025/game_state.rs
diff --git a/src/plugin/hare.rs b/src/plugin2025/hare.rs
similarity index 100%
rename from src/plugin/hare.rs
rename to src/plugin2025/hare.rs
diff --git a/src/plugin/move.rs b/src/plugin2025/move.rs
similarity index 100%
rename from src/plugin/move.rs
rename to src/plugin2025/move.rs
diff --git a/src/plugin/rules_engine.rs b/src/plugin2025/rules_engine.rs
similarity index 100%
rename from src/plugin/rules_engine.rs
rename to src/plugin2025/rules_engine.rs
diff --git a/src/plugin/test.rs b/src/plugin2025/test.rs
similarity index 100%
rename from src/plugin/test.rs
rename to src/plugin2025/test.rs
diff --git a/src/plugin/test/advance_test.rs b/src/plugin2025/test/advance_test.rs
similarity index 99%
rename from src/plugin/test/advance_test.rs
rename to src/plugin2025/test/advance_test.rs
index 0f1f207..2c1e085 100644
--- a/src/plugin/test/advance_test.rs
+++ b/src/plugin2025/test/advance_test.rs
@@ -2,7 +2,7 @@
mod tests {
use pyo3::Python;
- use crate::plugin::{
+ use crate::plugin2025::{
action::{advance::Advance, card::Card},
board::Board,
field::Field,
diff --git a/src/plugin/test/board_test.rs b/src/plugin2025/test/board_test.rs
similarity index 97%
rename from src/plugin/test/board_test.rs
rename to src/plugin2025/test/board_test.rs
index f3e2c99..59aa44a 100644
--- a/src/plugin/test/board_test.rs
+++ b/src/plugin2025/test/board_test.rs
@@ -1,6 +1,6 @@
#[cfg(test)]
mod tests {
- use crate::plugin::{board::Board, field::Field};
+ use crate::plugin2025::{board::Board, field::Field};
#[test]
fn test_new_board() {
diff --git a/src/plugin/test/card_test.rs b/src/plugin2025/test/card_test.rs
similarity index 99%
rename from src/plugin/test/card_test.rs
rename to src/plugin2025/test/card_test.rs
index aee758e..c6daaa4 100644
--- a/src/plugin/test/card_test.rs
+++ b/src/plugin2025/test/card_test.rs
@@ -1,6 +1,6 @@
#[cfg(test)]
mod tests {
- use crate::plugin::{
+ use crate::plugin2025::{
action::{advance::Advance, card::Card, Action},
board::Board,
field::Field,
diff --git a/src/plugin/test/rules_test.rs b/src/plugin2025/test/rules_test.rs
similarity index 99%
rename from src/plugin/test/rules_test.rs
rename to src/plugin2025/test/rules_test.rs
index bd96f39..cfce5e6 100644
--- a/src/plugin/test/rules_test.rs
+++ b/src/plugin2025/test/rules_test.rs
@@ -1,6 +1,6 @@
#[cfg(test)]
mod tests {
- use crate::plugin::{
+ use crate::plugin2025::{
action::card::Card,
board::Board,
field::Field,
diff --git a/src/plugin/test/state_test.rs b/src/plugin2025/test/state_test.rs
similarity index 99%
rename from src/plugin/test/state_test.rs
rename to src/plugin2025/test/state_test.rs
index dbc39f8..edd7b69 100644
--- a/src/plugin/test/state_test.rs
+++ b/src/plugin2025/test/state_test.rs
@@ -2,7 +2,7 @@
mod tests {
use std::vec;
- use crate::plugin::{
+ use crate::plugin2025::{
action::{advance::Advance, card::Card, Action},
board::Board,
field::Field,
diff --git a/src/plugin2026.rs b/src/plugin2026.rs
new file mode 100644
index 0000000..9e5fc23
--- /dev/null
+++ b/src/plugin2026.rs
@@ -0,0 +1,8 @@
+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/plugin2026/board.rs b/src/plugin2026/board.rs
new file mode 100644
index 0000000..b5b924a
--- /dev/null
+++ b/src/plugin2026/board.rs
@@ -0,0 +1,119 @@
+use pyo3::*;
+
+use crate::plugin2026::{
+ field_type::FieldType,
+ utils::{
+ constants::PluginConstants,
+ coordinate::Coordinate,
+ direction::Direction
+ }
+};
+
+#[pyclass]
+#[derive(PartialEq, Eq, PartialOrd, Clone, Debug, Hash)]
+pub struct Board {
+ #[pyo3(get, set)]
+ pub map: Vec>,
+}
+
+#[pymethods]
+impl Board {
+ #[new]
+ pub fn new(map: Vec>) -> Self {
+ Self { map }
+ }
+
+ pub fn __str__(&self) -> String {self.to_string()}
+ pub fn __repr__(&self) -> String {format!("{:?}", self)}
+
+ pub fn get_field(&self, position: &Coordinate) -> Option {
+
+ let x = usize::try_from(position.x).ok()?;
+ let y = usize::try_from(position.y).ok()?;
+
+ self.map
+ .get(y)?
+ .get(x)
+ .cloned()
+ }
+
+ pub fn get_fields_by_type(&self, field: FieldType) -> Vec {
+
+ let mut positions: Vec = vec![];
+
+ for (y, row) in self.map.iter().enumerate() {
+ for (x, f) in row.iter().enumerate() {
+ if *f == field {
+ positions.push(Coordinate {x: x as isize, y: y as isize});
+ }
+ }
+ }
+
+ positions
+ }
+
+ pub fn get_fields_in_direction(&self, position: &Coordinate, direction: &Direction) -> Vec {
+
+ let mut fields: Vec = Vec::new();
+
+ for scalar in 1..PluginConstants::BOARD_WIDTH {
+ let new_pos = position.add_vector(&direction.to_vector().scale(scalar as isize));
+ if let Some(field) = self.get_field(&new_pos) {
+ fields.push(field);
+ } else {break}
+ }
+
+ fields
+ }
+
+ pub fn get_fields_on_line(&self, position: &Coordinate, direction: &Direction) -> Vec {
+
+ let mut fields: Vec = Vec::new();
+
+ if let Some(field) = self.get_field(position) {
+ fields.push(field);
+ } else {return fields}
+
+ let in_dir = self.get_fields_in_direction(position, direction);
+ let in_mirror = self.get_fields_in_direction(position, &direction.to_mirrored());
+
+ // merge vecs
+ for f in in_dir {
+ fields.push(f);
+ }
+
+ for f in in_mirror {
+ fields.insert(0, f);
+ }
+
+ fields
+ }
+
+ pub fn get_fish_on_line(&self, position: &Coordinate, direction: &Direction) -> Vec {
+
+ let fields_on_line = self.get_fields_on_line(position, direction);
+
+ let fish_only: Vec = fields_on_line.iter()
+ .filter(|&f| matches!(f,
+ FieldType::OneS | FieldType::OneM | FieldType::OneL |
+ FieldType::TwoS | FieldType::TwoM | FieldType::TwoL
+ ))
+ .cloned()
+ .collect();
+
+ fish_only
+ }
+}
+
+impl std::fmt::Display for Board {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ writeln!(f)?;
+ for row in self.map.iter().rev() {
+ for field in row {
+ write!(f, "{} ", *field)?;
+ }
+ writeln!(f)?;
+ }
+ Ok(())
+ }
+}
diff --git a/src/plugin2026/errors.rs b/src/plugin2026/errors.rs
new file mode 100644
index 0000000..5d09acd
--- /dev/null
+++ b/src/plugin2026/errors.rs
@@ -0,0 +1,3 @@
+use pyo3::{exceptions::PyException, *};
+
+create_exception!(_socha, PiranhasError, PyException);
\ No newline at end of file
diff --git a/src/plugin2026/field_type.rs b/src/plugin2026/field_type.rs
new file mode 100644
index 0000000..da496e7
--- /dev/null
+++ b/src/plugin2026/field_type.rs
@@ -0,0 +1,76 @@
+use pyo3::*;
+
+use crate::plugin2026::utils::team::TeamEnum;
+
+#[pyclass]
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Hash)]
+pub enum FieldType {
+ OneS,
+ OneM,
+ OneL,
+ TwoS,
+ TwoM,
+ TwoL,
+ Squid,
+ Empty
+}
+
+#[pymethods]
+impl FieldType {
+ pub fn __str__(&self) -> String {self.to_string()}
+ pub fn __repr__(&self) -> String {format!("{:?}", self)}
+
+ pub fn get_value(&self) -> usize {
+ match self {
+ FieldType::OneS => 1,
+ FieldType::OneM => 2,
+ FieldType::OneL => 3,
+ FieldType::TwoS => 1,
+ FieldType::TwoM => 2,
+ FieldType::TwoL => 3,
+ FieldType::Squid => 0,
+ FieldType::Empty => 0,
+ }
+ }
+
+ pub fn get_team(&self) -> Option {
+ match self {
+ FieldType::OneS => Some(TeamEnum::One),
+ FieldType::OneM => Some(TeamEnum::One),
+ FieldType::OneL => Some(TeamEnum::One),
+ FieldType::TwoS => Some(TeamEnum::Two),
+ FieldType::TwoM => Some(TeamEnum::Two),
+ FieldType::TwoL => Some(TeamEnum::Two),
+ _ => None
+ }
+ }
+
+ #[staticmethod]
+ pub fn all_field_types() -> Vec {
+ vec![
+ FieldType::OneS,
+ FieldType::OneM,
+ FieldType::OneL,
+ FieldType::TwoS,
+ FieldType::TwoM,
+ FieldType::TwoL,
+ FieldType::Squid,
+ FieldType::Empty
+ ]
+ }
+}
+
+impl std::fmt::Display for FieldType {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::OneS => write!(f, "O1"),
+ Self::OneM => write!(f, "O2"),
+ Self::OneL => write!(f, "O3"),
+ Self::TwoS => write!(f, "T1"),
+ Self::TwoM => write!(f, "T2"),
+ Self::TwoL => write!(f, "T3"),
+ Self::Squid => write!(f, "SQ"),
+ Self::Empty => write!(f, "--"),
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/plugin2026/game_state.rs b/src/plugin2026/game_state.rs
new file mode 100644
index 0000000..152e8b7
--- /dev/null
+++ b/src/plugin2026/game_state.rs
@@ -0,0 +1,69 @@
+use pyo3::*;
+
+use crate::plugin2026::{
+ board::Board,
+ r#move::Move,
+ rules_engine::RulesEngine,
+ utils::{
+ coordinate::Coordinate,
+ direction::Direction
+ }
+};
+
+#[pyclass]
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Hash)]
+pub struct GameState {
+ #[pyo3(get, set)]
+ pub board: Board,
+ #[pyo3(get, set)]
+ pub turn: usize,
+ #[pyo3(get, set)]
+ pub last_move: Option
+}
+
+#[pymethods]
+impl GameState {
+ #[new]
+ pub fn new(board: Board, turn: usize, last_move: Option) -> Self {
+ Self {
+ board,
+ turn,
+ last_move,
+ }
+ }
+
+ pub fn __str__(&self) -> String {self.to_string()}
+ pub fn __repr__(&self) -> String {format!("{:?}", self)}
+
+ pub fn possible_moves(&self) -> Vec {
+ let mut moves: Vec = Vec::new();
+ let mut fish: Vec = Vec::new();
+
+ for f in RulesEngine::get_team_on_turn(self.turn).get_fish_types() {
+ fish.append(&mut self.board.get_fields_by_type(f));
+ }
+
+ for f in fish {
+ for d in Direction::all_directions() {
+ moves.push(Move { start: f.clone(), direction: d });
+ }
+ }
+
+ moves
+ .into_iter()
+ .filter(|m| RulesEngine::can_execute_move(&self.board, m).is_ok())
+ .collect()
+ }
+}
+
+impl std::fmt::Display for GameState {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(
+ f,
+ "Zug: {}\nSpielbrett:{}Letzter Zug: {:?}",
+ self.turn,
+ self.board,
+ self.last_move
+ )
+ }
+}
\ No newline at end of file
diff --git a/src/plugin2026/move.rs b/src/plugin2026/move.rs
new file mode 100644
index 0000000..c8681c7
--- /dev/null
+++ b/src/plugin2026/move.rs
@@ -0,0 +1,38 @@
+use std::fmt::Debug;
+
+use pyo3::*;
+
+use crate::plugin2026::{
+ utils::{
+ coordinate::Coordinate,
+ direction::Direction
+ }
+};
+
+#[pyclass]
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Hash)]
+pub struct Move {
+ #[pyo3(get, set)]
+ pub start: Coordinate,
+ #[pyo3(get, set)]
+ pub direction: Direction
+}
+
+#[pymethods]
+impl Move {
+ #[new]
+ pub fn new(start: Coordinate, direction: Direction) -> Self {
+ Self {
+ start, direction
+ }
+ }
+
+ pub fn __str__(&self) -> String {self.to_string()}
+ pub fn __repr__(&self) -> String {format!("{:?}", self)}
+}
+
+impl std::fmt::Display for Move {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "Move von {} in Richtung {}", self.start, self.direction)
+ }
+}
diff --git a/src/plugin2026/rules_engine.rs b/src/plugin2026/rules_engine.rs
new file mode 100644
index 0000000..4726993
--- /dev/null
+++ b/src/plugin2026/rules_engine.rs
@@ -0,0 +1,100 @@
+use pyo3::*;
+
+use crate::plugin2026::{
+ board::Board, errors::PiranhasError, field_type::FieldType, r#move::Move, utils::{
+ constants::PluginConstants,
+ coordinate::Coordinate, team::TeamEnum
+ }
+};
+
+#[pyclass]
+pub struct RulesEngine;
+
+#[pymethods]
+impl RulesEngine {
+
+ #[staticmethod]
+ pub fn move_distance(board: &Board, move_: &Move) -> usize {
+ board.get_fish_on_line(&move_.start, &move_.direction).len()
+ }
+
+ #[staticmethod]
+ pub fn target_position(board: &Board, move_: &Move) -> Coordinate {
+ move_.start.clone().add_vector(&move_.direction.to_vector().scale(Self::move_distance(board, move_) as isize))
+ }
+
+ #[staticmethod]
+ pub fn is_in_bounds(coordinate: &Coordinate) -> bool {
+ coordinate.x >= 0 && coordinate.x < PluginConstants::BOARD_WIDTH as isize
+ && coordinate.y >= 0 && coordinate.y < PluginConstants::BOARD_HEIGHT as isize
+ }
+
+ #[staticmethod]
+ pub fn can_execute_move(board: &Board, move_: &Move) -> Result<(), PyErr> {
+
+ let target_pos = Self::target_position(board, move_);
+
+ Self::is_in_bounds(&move_.start)
+ .then_some(())
+ .ok_or_else(|| PiranhasError::new_err("Start position is out of bounds"))?;
+
+ Self::is_in_bounds(&target_pos)
+ .then_some(())
+ .ok_or_else(|| PiranhasError::new_err("Target position is out of bounds"))?;
+
+ let start_field = board.get_field(&move_.start)
+ .expect("Already validated in-bounds position");
+ let target_field = board.get_field(&target_pos)
+ .expect("Already validated in-bounds position");
+
+ let this_team = start_field
+ .get_team()
+ .ok_or_else(|| PiranhasError::new_err("Start position is not on fish field"))?;
+
+ let mut blocked_fields = this_team.get_fish_types();
+ blocked_fields.push(FieldType::Squid);
+
+ if blocked_fields.contains(&target_field) {
+ return Err(PiranhasError::new_err("Cannot swim onto field of own team or squid"));
+ }
+
+ let distance = Self::move_distance(board, move_);
+ let direction_fields = board.get_fields_in_direction(&move_.start, &move_.direction);
+ let path_fields: Vec<_> = direction_fields.iter().take(distance - 1).cloned().collect(); // not including start or target
+/*
+ println!("{}", move_);
+ println!("{}", move_.start);
+ println!("{}", move_.direction);
+ println!("{}", distance);
+ println!("{}", target_pos);
+ println!("{}", target_field);
+ for d in direction_fields {
+ print!("{} ", d);
+ }
+ println!();
+ for d in &path_fields {
+ print!("{} ", *d);
+ }
+ println!();*/
+
+ for d in path_fields {
+ let mut blocked_fields = this_team.get_fish_types();
+ blocked_fields.push(FieldType::Squid);
+
+ if this_team.opponent().get_fish_types().contains(&d) {
+ return Err(PiranhasError::new_err("Cannot swim over other team's fish"));
+ }
+ }
+
+ Ok(())
+ }
+
+ #[staticmethod]
+ pub fn get_team_on_turn(turn: usize) -> TeamEnum {
+ if turn % 2 == 0 {
+ TeamEnum::One
+ } else {
+ TeamEnum::Two
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/plugin2026/test/board_test.rs b/src/plugin2026/test/board_test.rs
new file mode 100644
index 0000000..82c789d
--- /dev/null
+++ b/src/plugin2026/test/board_test.rs
@@ -0,0 +1,43 @@
+#[cfg(test)]
+mod tests {
+ use crate::plugin2026::field_type::FieldType;
+ use crate::plugin2026::test::common::*;
+ use crate::plugin2026::utils::coordinate::Coordinate;
+ use crate::plugin2026::utils::direction::Direction;
+
+ #[test]
+ pub fn test01() {
+ let b = create_test_board();
+
+ println!("{}", b);
+
+ for variant in Direction::all_directions() {
+ let c = Coordinate {x: 2, y: 1};
+ let f = b.get_fields_in_direction(&c, &variant);
+
+ print!("[ ");
+ for field in &f {
+ print!("{} ", field);
+ }
+ print!("] {} {}", c, variant);
+ println!();
+ }
+
+ let mut sum = 0;
+
+ for fvarient in FieldType::all_field_types() {
+ let f = b.get_fields_by_type(fvarient);
+
+ print!("[ ");
+ for field in &f {
+ print!("{} ", field);
+ }
+ print!("]");
+ println!();
+
+ sum += f.len();
+ }
+
+ println!("{}", sum);
+ }
+}
diff --git a/src/plugin2026/test/common.rs b/src/plugin2026/test/common.rs
new file mode 100644
index 0000000..2864940
--- /dev/null
+++ b/src/plugin2026/test/common.rs
@@ -0,0 +1,40 @@
+use crate::plugin2026::{
+ board::Board,
+ field_type::FieldType,
+ utils::constants::PluginConstants
+};
+
+pub fn create_test_board() -> Board {
+ let example_map = [
+ ["--", "T2", "T1", "T3", "T3", "T2", "T2", "T1", "T1", "--"],
+ ["O1", "--", "--", "--", "--", "--", "--", "--", "--", "O1"],
+ ["O3", "--", "--", "SQ", "--", "--", "--", "--", "--", "O1"],
+ ["O1", "--", "--", "--", "--", "--", "--", "--", "--", "O2"],
+ ["O2", "--", "--", "--", "--", "--", "--", "--", "--", "O2"],
+ ["O1", "--", "--", "--", "--", "--", "--", "--", "--", "O3"],
+ ["O3", "--", "--", "--", "--", "--", "--", "--", "--", "O3"],
+ ["O1", "--", "--", "--", "--", "--", "SQ", "--", "--", "O1"],
+ ["O2", "--", "--", "--", "--", "--", "--", "--", "--", "O2"],
+ ["--", "T2", "T1", "T3", "T1", "T2", "T1", "T3", "T1", "--"],
+ ];
+
+ let mut new_map = vec![vec![FieldType::Empty; PluginConstants::BOARD_HEIGHT]; PluginConstants::BOARD_WIDTH];
+
+ for y in 0..PluginConstants::BOARD_HEIGHT {
+ for x in 0..PluginConstants::BOARD_WIDTH {
+ match example_map[9 - y][x] { // read in reverse y because board is read from bottom to top
+ "O1" => {new_map[y][x] = FieldType::OneS},
+ "O2" => {new_map[y][x] = FieldType::OneM},
+ "O3" => {new_map[y][x] = FieldType::OneL},
+ "T1" => {new_map[y][x] = FieldType::TwoS},
+ "T2" => {new_map[y][x] = FieldType::TwoM},
+ "T3" => {new_map[y][x] = FieldType::TwoL},
+ "SQ" => {new_map[y][x] = FieldType::Squid},
+ "--" => {}, // already FieldType::Empty
+ _ => {}
+ }
+ }
+ }
+
+ Board::new(new_map)
+}
\ No newline at end of file
diff --git a/src/plugin2026/test/coordinate_test.rs b/src/plugin2026/test/coordinate_test.rs
new file mode 100644
index 0000000..2556e30
--- /dev/null
+++ b/src/plugin2026/test/coordinate_test.rs
@@ -0,0 +1,30 @@
+#[cfg(test)]
+mod tests {
+ use crate::plugin2026::{
+ utils::coordinate::Coordinate,
+ utils::vector::Vector
+ };
+
+ #[test]
+ pub fn test_eq() {
+ let coord01 = Coordinate {x: 1, y: 2};
+ let coord02 = Coordinate {x: 1, y: 2};
+ let coord03 = Coordinate {x: 2, y: 1};
+ let coord04 = Coordinate {x: 1, y: 1};
+
+ assert_eq!(coord01, coord02);
+ assert_ne!(coord02, coord03);
+ assert_ne!(coord03, coord04);
+ }
+
+ #[test]
+ pub fn test_add_vector() {
+ let coord01 = Coordinate {x: 1, y: 2};
+ let coord02 = Coordinate {x: 1, y: 2};
+ let vec01 = Vector {delta_x: 2, delta_y: 3};
+ let vec02 = Vector {delta_x: -1, delta_y: 1};
+
+ println!("{}", coord01.add_vector(&vec01));
+ println!("{}", coord02.add_vector(&vec02));
+ }
+}
diff --git a/src/plugin2026/test/game_state_test.rs b/src/plugin2026/test/game_state_test.rs
new file mode 100644
index 0000000..fed1ec6
--- /dev/null
+++ b/src/plugin2026/test/game_state_test.rs
@@ -0,0 +1,4 @@
+#[cfg(test)]
+mod tests {
+
+}
diff --git a/src/plugin2026/test/mod.rs b/src/plugin2026/test/mod.rs
new file mode 100644
index 0000000..7e89469
--- /dev/null
+++ b/src/plugin2026/test/mod.rs
@@ -0,0 +1,10 @@
+#[cfg(test)]
+mod common;
+#[cfg(test)]
+mod board_test;
+#[cfg(test)]
+mod game_state_test;
+#[cfg(test)]
+mod coordinate_test;
+#[cfg(test)]
+mod rules_engine_test;
\ No newline at end of file
diff --git a/src/plugin2026/test/rules_engine_test.rs b/src/plugin2026/test/rules_engine_test.rs
new file mode 100644
index 0000000..0dd4e7a
--- /dev/null
+++ b/src/plugin2026/test/rules_engine_test.rs
@@ -0,0 +1,32 @@
+#[cfg(test)]
+mod tests {
+ use crate::plugin2026::r#move::Move;
+ use crate::plugin2026::rules_engine::RulesEngine;
+ use crate::plugin2026::test::common::*;
+ use crate::plugin2026::utils::coordinate::Coordinate;
+ use crate::plugin2026::utils::direction::Direction;
+
+ #[test]
+ pub fn test01() {
+ pyo3::prepare_freethreaded_python();
+
+ let b = create_test_board();
+
+ println!("{}", b);
+
+ for variant in Direction::all_directions() {
+ let c = Coordinate {x: 2, y: 0};
+ let move_ = Move {start: c, direction: variant};
+
+ println!("Move: {}", move_);
+
+ if let Err(e) = RulesEngine::can_execute_move(&b, &move_) {
+ println!("Error occurred: {:?}", e);
+ } else {
+ println!("Alles super");
+ }
+
+ println!();
+ }
+ }
+}
diff --git a/src/plugin2026/utils.rs b/src/plugin2026/utils.rs
new file mode 100644
index 0000000..b41dac7
--- /dev/null
+++ b/src/plugin2026/utils.rs
@@ -0,0 +1,5 @@
+pub mod vector;
+pub mod direction;
+pub mod constants;
+pub mod coordinate;
+pub mod team;
\ No newline at end of file
diff --git a/src/plugin2026/utils/constants.rs b/src/plugin2026/utils/constants.rs
new file mode 100644
index 0000000..88a09bf
--- /dev/null
+++ b/src/plugin2026/utils/constants.rs
@@ -0,0 +1,12 @@
+use pyo3::*;
+
+#[pyclass]
+pub struct PluginConstants;
+
+#[pymethods]
+impl PluginConstants {
+ pub const BOARD_WIDTH: usize = 10;
+ pub const BOARD_HEIGHT: usize = 10;
+
+ pub const ROUND_LIMIT: usize = 30;
+}
diff --git a/src/plugin2026/utils/coordinate.rs b/src/plugin2026/utils/coordinate.rs
new file mode 100644
index 0000000..4d0d39c
--- /dev/null
+++ b/src/plugin2026/utils/coordinate.rs
@@ -0,0 +1,50 @@
+use pyo3::*;
+
+use crate::plugin2026::{utils::vector::Vector};
+
+#[pyclass]
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, 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
+ }
+ }
+
+ pub fn __str__(&self) -> String {self.to_string()}
+ pub fn __repr__(&self) -> String {format!("{:?}", 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
+ }
+ }
+}
+
+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/plugin2026/utils/direction.rs b/src/plugin2026/utils/direction.rs
new file mode 100644
index 0000000..d60ba2a
--- /dev/null
+++ b/src/plugin2026/utils/direction.rs
@@ -0,0 +1,94 @@
+use pyo3::*;
+
+use crate::plugin2026::{
+ utils::vector::Vector
+};
+
+#[pyclass]
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Hash)]
+pub enum Direction {
+ Up,
+ UpRight,
+ Right,
+ DownRight,
+ Down,
+ DownLeft,
+ Left,
+ UpLeft
+}
+
+#[pymethods]
+impl Direction {
+ pub fn __str__(&self) -> String {self.to_string()}
+ pub fn __repr__(&self) -> String {format!("{:?}", 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,
+ }
+ }
+}
+
+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/plugin2026/utils/team.rs b/src/plugin2026/utils/team.rs
new file mode 100644
index 0000000..6ad5c18
--- /dev/null
+++ b/src/plugin2026/utils/team.rs
@@ -0,0 +1,39 @@
+use pyo3::*;
+
+use crate::plugin2026::field_type::FieldType;
+
+#[pyclass]
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Hash)]
+pub enum TeamEnum {
+ One,
+ Two,
+}
+
+#[pymethods]
+impl TeamEnum {
+ pub fn __str__(&self) -> String {self.to_string()}
+ pub fn __repr__(&self) -> String {format!("{:?}", self)}
+
+ pub fn get_fish_types(&self) -> Vec {
+ match self {
+ TeamEnum::One => vec![FieldType::OneS, FieldType::OneM, FieldType::OneL],
+ TeamEnum::Two => vec![FieldType::TwoS, FieldType::TwoM, FieldType::TwoL]
+ }
+ }
+
+ 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/plugin2026/utils/vector.rs b/src/plugin2026/utils/vector.rs
new file mode 100644
index 0000000..9fad462
--- /dev/null
+++ b/src/plugin2026/utils/vector.rs
@@ -0,0 +1,63 @@
+use pyo3::*;
+
+#[pyclass]
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, 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
+ }
+ }
+
+ pub fn __str__(&self) -> String {self.to_string()}
+ pub fn __repr__(&self) -> String {format!("{:?}", 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