From 9f5b9c83e140fa834421fb998e14ea67ccb87f72 Mon Sep 17 00:00:00 2001 From: sigmasternchen Date: Sun, 27 Oct 2024 12:36:10 +0100 Subject: [PATCH] feat: Add deserialization for JS move notation --- src/Game/Move.php | 19 ++++++++++++++-- src/Game/Piece.php | 13 +++++++++++ src/Game/PieceType.php | 12 ++++++++++ src/Game/Position.php | 12 ++++++++++ src/Game/Side.php | 8 +++++++ tests/Game/MoveTest.php | 50 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 112 insertions(+), 2 deletions(-) diff --git a/src/Game/Move.php b/src/Game/Move.php index 3b99717..f852afe 100644 --- a/src/Game/Move.php +++ b/src/Game/Move.php @@ -165,12 +165,27 @@ class Move { return join(",", [ $this->piece->toJS(), $this->target, - $this->captures ?? "", + $this->captures?->toJS() ?? "", $this->promoteTo?->getShort() ?? "", - $this->castleWith ?? "" + $this->castleWith?->toJS() ?? "" ]); } + static public function fromJS(string $jsStr): Move { + $components = explode(",", $jsStr); + if (count($components) != 5) { + throw new \InvalidArgumentException("unable to deserialize move string: " . $jsStr); + } + + return new Move( + Piece::fromJS($components[0]), + Position::fromString($components[1]), + $components[2] != "" ? Piece::fromJS($components[2]) : null, + $components[3] != "" ? PieceType::fromShort($components[3]) : null, + $components[4] != "" ? Piece::fromJS($components[4]) : null, + ); + } + public function __toString(): string { return $this->getLong(); } diff --git a/src/Game/Piece.php b/src/Game/Piece.php index 95dcd96..69be006 100644 --- a/src/Game/Piece.php +++ b/src/Game/Piece.php @@ -55,6 +55,19 @@ abstract class Piece { ]); } + public static function fromJS(string $jsStr): Piece { + $components = explode("-", $jsStr); + if (count($components) != 3) { + throw new \InvalidArgumentException("unable to deserialize piece string: " . $jsStr); + } + + return self::ofType( + PieceType::fromShort($components[1]), + Position::fromString($components[2]), + Side::fromShort($components[0]), + ); + } + private static function getClassForType(PieceType $type): string { switch ($type) { case PieceType::PAWN: diff --git a/src/Game/PieceType.php b/src/Game/PieceType.php index c449b2c..a6ffbbc 100644 --- a/src/Game/PieceType.php +++ b/src/Game/PieceType.php @@ -25,6 +25,18 @@ enum PieceType { }; } + public static function fromShort(string $short): PieceType { + return match (strtoupper($short)) { + "" => self::PAWN, + "B" => self::BISHOP, + "N" => self::KNIGHT, + "R" => self::ROOK, + "Q" => self::QUEEN, + "K" => self::KING, + default => throw new \InvalidArgumentException("unknown piece type: " . $short) + }; + } + public function getLong(): string { return match ($this) { self::PAWN => "Pawn", diff --git a/src/Game/Position.php b/src/Game/Position.php index 791f66e..70444dc 100644 --- a/src/Game/Position.php +++ b/src/Game/Position.php @@ -33,6 +33,18 @@ class Position { return $this->getFileString() . $this->getRankString(); } + public static function fromString(string $str): Position { + $str = strtolower($str); + $file = ord($str[0]) - ord("a"); + $rank = ord($str[1]) - ord("1"); + + $position = new Position($file, $rank); + if (!$position->isValid()) { + throw new \InvalidArgumentException("position is not valid: " . $str); + } + return $position; + } + public function equals(Position $position): bool { return $this->file == $position->file && $this->rank == $position->rank; } diff --git a/src/Game/Side.php b/src/Game/Side.php index 3b85036..bd255eb 100644 --- a/src/Game/Side.php +++ b/src/Game/Side.php @@ -21,4 +21,12 @@ enum Side { return "b"; } } + + public static function fromShort(string $short): Side { + return match ($short) { + "w" => self::WHITE, + "b" => self::BLACK, + default => throw new \InvalidArgumentException("unknown side: " . $short), + }; + } } \ No newline at end of file diff --git a/tests/Game/MoveTest.php b/tests/Game/MoveTest.php index 679235d..1a5d2f3 100644 --- a/tests/Game/MoveTest.php +++ b/tests/Game/MoveTest.php @@ -97,4 +97,54 @@ final class MoveTest extends TestCase { $this->assertEquals("Nc2xd4++", $subject->getShort($game)); } + public function testToJS_capturesAndPromotes() { + $move = new Move( + new Pawn(new Position(1, 6), Side::WHITE), + new Position(2, 7), + new Queen(new Position(2, 7), Side::BLACK), + PieceType::QUEEN, + ); + + $this->assertEquals("w--b7,c8,b-Q-c8,Q,", $move->toJS()); + } + + public function testToJS_minimal() { + $move = new Move( + new Queen(new Position(1, 6), Side::WHITE), + new Position(3, 4) + ); + + $this->assertEquals("w-Q-b7,d5,,,", $move->toJS()); + } + + public function testToJS_castle() { + $move = new Move( + new King(new Position(4, 0), Side::WHITE), + new Position(2, 0), + null, + null, + new Rook(new Position(0, 0), Side::WHITE) + ); + + $this->assertEquals("w-K-e1,c1,,,w-R-a1", $move->toJS()); + } + + public function testFromJS_capturesAndPromotes() { + $move = Move::fromJS("w--b7,c8,b-Q-c8,Q,"); + + $this->assertEquals("b7xc8Q", $move->getLong()); + } + + public function testFromJS_minimal() { + $move = Move::fromJS("w-Q-b7,d5,,,"); + + $this->assertEquals("Qb7d5", $move->getLong()); + } + + public function testFromJS_castle() { + $move = Move::fromJS("w-K-e1,c1,,,w-R-a1"); + + $this->assertEquals("O-O-O", $move->getLong()); + } + } \ No newline at end of file