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 -[![Read the Docs](https://img.shields.io/readthedocs/socha-python-client?label=Docs)](https://socha-python-client.readthedocs.io/en/) +[![Read the Docs](https://img.shields.io/readthedocs/socha-python-client?label=Docs)](https://socha-python-client.readthedocs.io/de/latest/) [![PyPI](https://img.shields.io/pypi/v/socha?label=PyPi)](https://pypi.org/project/socha/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/socha?label=Python)](https://pypi.org/project/socha/) [![Discord](https://img.shields.io/discord/233577109363097601?color=blue&label=Discord)](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