From c87d932cf448d2d23a8191c34b8f89870670b0ab Mon Sep 17 00:00:00 2001 From: BlazingTwist <39350649+BlazingTwist@users.noreply.github.com> Date: Sun, 28 Jan 2024 22:07:34 +0100 Subject: [PATCH 1/5] create test to reproduce issue --- src/test/java/jchess/RegressionTests.java | 56 +++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/test/java/jchess/RegressionTests.java diff --git a/src/test/java/jchess/RegressionTests.java b/src/test/java/jchess/RegressionTests.java new file mode 100644 index 0000000..ef3a582 --- /dev/null +++ b/src/test/java/jchess/RegressionTests.java @@ -0,0 +1,56 @@ +package jchess; + +import dx.schema.types.PieceType; +import jchess.common.IChessGame; +import jchess.common.events.BoardClickedEvent; +import jchess.common.moveset.NormalMove; +import jchess.ecs.Entity; +import jchess.gamemode.PieceStore; +import jchess.gamemode.hex3p.Hex3PlayerGame; +import jchess.gamemode.hex3p.Hex3pPieceLayouts; +import jchess.gamemode.hex3p.Hex3pPieces; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class RegressionTests { + private static Entity getTileAtPosition(IChessGame game, int x, int y) { + return game.getEntityManager().getEntities().stream() + .filter(entity -> entity.tile != null + && entity.tile.position.x == x + && entity.tile.position.y == y) + .findFirst().orElse(null); + } + + /** + * Simulates an error-condition caused by incorrect MoveSimulator#revert logic, as described in Issue #27 + */ + @Test + public void test_issue27_kingCheckException() { + Hex3PlayerGame game = new Hex3PlayerGame(new PieceStore(Hex3pPieces.values()), Hex3pPieceLayouts.Standard); + game.start(); + + Entity whiteRook = getTileAtPosition(game, 24, 16); + Assertions.assertTrue(whiteRook.piece != null + && whiteRook.piece.identifier.pieceType() == PieceType.ROOK + && whiteRook.piece.identifier.ownerId() == 0, + "Expected to find white rook at position 24,16"); + + // basic setup: + // white rook checks black king. + // white rook can move to un-check king. + Entity distance1 = getTileAtPosition(game, 26, 4); + Entity distance2 = getTileAtPosition(game, 25, 5); + distance1.piece = null; + NormalMove.getMove(game, whiteRook, distance2).onClick().run(); + + Entity blackKingTile = getTileAtPosition(game, 27, 3); + + Assertions.assertNotNull(blackKingTile.tile); + Assertions.assertNotNull(distance2.tile); + + Assertions.assertTrue(blackKingTile.isAttacked(), "Black King should be attacked by White Rook"); + Assertions.assertTrue(blackKingTile.tile.attackingPieces.contains(distance2), "Black King should be attacked by White Rook"); + + Assertions.assertDoesNotThrow(() -> game.getEventManager().getEvent(BoardClickedEvent.class).fire(distance2.tile.position)); + } +} From b5ca294d554cf4d1b290b2e7b58952f2b12c9809 Mon Sep 17 00:00:00 2001 From: BlazingTwist <39350649+BlazingTwist@users.noreply.github.com> Date: Sun, 28 Jan 2024 22:08:03 +0100 Subject: [PATCH 2/5] create StateManagement system --- .../jchess/common/state/IRevertibleState.java | 7 +++++ .../jchess/common/state/StateManager.java | 25 ++++++++++++++++ .../jchess/common/state/impl/ArrayState.java | 29 ++++++++++++++++++ .../common/state/impl/BooleanState.java | 30 +++++++++++++++++++ 4 files changed, 91 insertions(+) create mode 100644 src/main/java/jchess/common/state/IRevertibleState.java create mode 100644 src/main/java/jchess/common/state/StateManager.java create mode 100644 src/main/java/jchess/common/state/impl/ArrayState.java create mode 100644 src/main/java/jchess/common/state/impl/BooleanState.java diff --git a/src/main/java/jchess/common/state/IRevertibleState.java b/src/main/java/jchess/common/state/IRevertibleState.java new file mode 100644 index 0000000..d674b9a --- /dev/null +++ b/src/main/java/jchess/common/state/IRevertibleState.java @@ -0,0 +1,7 @@ +package jchess.common.state; + +public interface IRevertibleState { + void saveState(); + + void revertState(); +} diff --git a/src/main/java/jchess/common/state/StateManager.java b/src/main/java/jchess/common/state/StateManager.java new file mode 100644 index 0000000..182786b --- /dev/null +++ b/src/main/java/jchess/common/state/StateManager.java @@ -0,0 +1,25 @@ +package jchess.common.state; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class StateManager { + private final List stateList = new ArrayList<>(); + + public void registerState(IRevertibleState... states) { + Collections.addAll(stateList, states); + } + + public void saveState() { + for (IRevertibleState state : stateList) { + state.saveState(); + } + } + + public void revertState() { + for (IRevertibleState state : stateList) { + state.revertState(); + } + } +} diff --git a/src/main/java/jchess/common/state/impl/ArrayState.java b/src/main/java/jchess/common/state/impl/ArrayState.java new file mode 100644 index 0000000..ea0e890 --- /dev/null +++ b/src/main/java/jchess/common/state/impl/ArrayState.java @@ -0,0 +1,29 @@ +package jchess.common.state.impl; + +import jchess.common.state.IRevertibleState; + +import java.util.function.Function; + +public class ArrayState implements IRevertibleState { + private final T[] current; + private final T[] saved; + + public ArrayState(int size, Function arrayConstructor) { + current = arrayConstructor.apply(size); + saved = arrayConstructor.apply(size); + } + + public T[] getCurrent() { + return current; + } + + @Override + public void saveState() { + System.arraycopy(current, 0, saved, 0, current.length); + } + + @Override + public void revertState() { + System.arraycopy(saved, 0, current, 0, saved.length); + } +} diff --git a/src/main/java/jchess/common/state/impl/BooleanState.java b/src/main/java/jchess/common/state/impl/BooleanState.java new file mode 100644 index 0000000..4578851 --- /dev/null +++ b/src/main/java/jchess/common/state/impl/BooleanState.java @@ -0,0 +1,30 @@ +package jchess.common.state.impl; + +import jchess.common.state.IRevertibleState; + +public class BooleanState implements IRevertibleState { + private boolean current; + private boolean saved; + + public BooleanState(boolean initValue) { + this.current = initValue; + } + + public boolean getValue() { + return current; + } + + public void setValue(boolean current) { + this.current = current; + } + + @Override + public void saveState() { + saved = current; + } + + @Override + public void revertState() { + current = saved; + } +} From afd166268517abfc42bd7673f2ae6eba1fd9b99c Mon Sep 17 00:00:00 2001 From: BlazingTwist <39350649+BlazingTwist@users.noreply.github.com> Date: Sun, 28 Jan 2024 22:09:12 +0100 Subject: [PATCH 3/5] update SpecialMoves to utilize state management, coincidentally fix #33 --- .../jchess/common/moveset/MoveIntention.java | 16 ++ .../jchess/common/moveset/NormalMove.java | 25 ++- .../common/moveset/special/Castling.java | 162 +++++++----------- .../common/moveset/special/EnPassant.java | 47 ++--- .../common/moveset/special/PawnPromotion.java | 15 +- .../common/moveset/special/RangedAttack.java | 13 +- .../common/moveset/special/ShapeShifting.java | 36 ++-- .../moveset/special/SpecialFirstMove.java | 37 ++-- .../jchess/gamemode/hex3p/Hex3pPieces.java | 6 +- .../gamemode/square2p/Square2pPieces.java | 6 +- 10 files changed, 174 insertions(+), 189 deletions(-) diff --git a/src/main/java/jchess/common/moveset/MoveIntention.java b/src/main/java/jchess/common/moveset/MoveIntention.java index 7260af6..c85e6a2 100644 --- a/src/main/java/jchess/common/moveset/MoveIntention.java +++ b/src/main/java/jchess/common/moveset/MoveIntention.java @@ -1,11 +1,27 @@ package jchess.common.moveset; +import jchess.common.IChessGame; import jchess.ecs.Entity; public record MoveIntention(Entity displayTile, Runnable onClick, IMoveSimulator moveSimulator) { + /** + * This is a utility Method for constructing a MoveIntention in the specific use-case where the moveSimulator matches the actual move. + */ + public static MoveIntention fromMoveSimulator(IChessGame game, Entity displayTile, IMoveSimulator moveSimulator) { + return new MoveIntention( + displayTile, + () -> { + moveSimulator.simulate(); + game.endTurn(); + }, + moveSimulator + ); + } + public interface IMoveSimulator { void simulate(); + void revert(); } } diff --git a/src/main/java/jchess/common/moveset/NormalMove.java b/src/main/java/jchess/common/moveset/NormalMove.java index 5f580fc..39c0d5a 100644 --- a/src/main/java/jchess/common/moveset/NormalMove.java +++ b/src/main/java/jchess/common/moveset/NormalMove.java @@ -6,21 +6,30 @@ public class NormalMove { public static MoveIntention getMove(IChessGame game, Entity fromTile, Entity toTile) { - return new MoveIntention( - toTile, - () -> game.movePiece(fromTile, toTile, NormalMove.class), - new NormalMoveSimulator(toTile.piece, fromTile, toTile) - ); + NormalMoveSimulator moveSimulator = new NormalMoveSimulator(game, fromTile, toTile, NormalMove.class); + return MoveIntention.fromMoveSimulator(game, toTile, moveSimulator); } - private record NormalMoveSimulator( - PieceComponent capturedPiece, Entity fromTile, Entity toTile - ) implements MoveIntention.IMoveSimulator { + public static class NormalMoveSimulator implements MoveIntention.IMoveSimulator { + private final IChessGame game; + private final Entity fromTile; + private final Entity toTile; + private final Class moveType; + private final PieceComponent capturedPiece; + + public NormalMoveSimulator(IChessGame game, Entity fromTile, Entity toTile, Class moveType) { + this.game = game; + this.fromTile = fromTile; + this.toTile = toTile; + this.moveType = moveType; + this.capturedPiece = toTile.piece; + } @Override public void simulate() { toTile.piece = fromTile.piece; fromTile.piece = null; + game.notifyPieceMove(fromTile, toTile, moveType); } @Override diff --git a/src/main/java/jchess/common/moveset/special/Castling.java b/src/main/java/jchess/common/moveset/special/Castling.java index 5bb2122..34472c9 100644 --- a/src/main/java/jchess/common/moveset/special/Castling.java +++ b/src/main/java/jchess/common/moveset/special/Castling.java @@ -7,10 +7,9 @@ import jchess.common.events.PieceMoveEvent; import jchess.common.moveset.ISpecialRule; import jchess.common.moveset.MoveIntention; +import jchess.common.state.impl.BooleanState; import jchess.ecs.Entity; import jchess.el.CompiledTileExpression; -import jchess.el.v2.ExpressionCompiler; -import jchess.el.v2.TileExpression; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,44 +17,54 @@ import java.util.List; import java.util.stream.Stream; +import static jchess.el.v2.TileExpression.filter; +import static jchess.el.v2.TileExpression.neighbor; +import static jchess.el.v2.TileExpression.repeat; + public class Castling implements ISpecialRule { private static final Logger logger = LoggerFactory.getLogger(Castling.class); private final IChessGame game; - private final CompiledTileExpression kingMoveLeft; - private final CompiledTileExpression kingMoveRight; + private final CompiledTileExpression kingMove; private final PieceIdentifier kingId; private final PieceType rookTypeId; - private final int rightRookDirection; - private final int leftRookDirection; - private final CompiledTileExpression stepLeftExpression; - private final CompiledTileExpression stepRightExpression; + private final int kingToRookDir; + private final CompiledTileExpression stepToRookExpression; + private final CompiledTileExpression stepToKingExpression; + private final BooleanState kingMoved; + private final BooleanState rookMoved; - private PieceIdentifier leftRookId; - private PieceIdentifier rightRookId; - private boolean kingMoved = false; - private boolean leftRookMoved = false; - private boolean rightRookMoved = false; + private Entity rookTile; + private PieceIdentifier rookId; public Castling( - IChessGame game, PieceIdentifier kingId, PieceType rookTypeId, int rightRookDirection, int leftRookDirection, - ExpressionCompiler kingMoveLeft, ExpressionCompiler kingMoveRight + IChessGame game, PieceIdentifier kingId, PieceType rookTypeId, int kingToRookDir, int numStepsKing ) { this.game = game; - this.kingMoveLeft = kingMoveLeft.toV1(kingId); - this.kingMoveRight = kingMoveRight.toV1(kingId); + this.kingMove = repeat(filter(neighbor(kingToRookDir), Castling::kingStepFilter), numStepsKing, numStepsKing, true).toV1(kingId); this.kingId = kingId; this.rookTypeId = rookTypeId; - this.rightRookDirection = rightRookDirection; - this.leftRookDirection = leftRookDirection; - this.stepRightExpression = TileExpression.neighbor(rightRookDirection).toV1(kingId); - this.stepLeftExpression = TileExpression.neighbor(leftRookDirection).toV1(kingId); + this.kingToRookDir = kingToRookDir; + this.stepToRookExpression = neighbor(kingToRookDir).toV1(kingId); + this.stepToKingExpression = neighbor(kingToRookDir + 180).toV1(kingId); + + kingMoved = new BooleanState(false); + rookMoved = new BooleanState(false); + + game.getStateManager().registerState(kingMoved, rookMoved); - game.getEventManager().getEvent(BoardInitializedEvent.class).addListener(_void -> lookupRooks()); + game.getEventManager().getEvent(BoardInitializedEvent.class).addListener(_void -> lookupRook()); game.getEventManager().getEvent(PieceMoveEvent.class).addListener(this::onPieceMove); } - private void lookupRooks() { + private static boolean kingStepFilter(Entity moveTo) { + // Castling requires that: + // - the tiles the king moves over are empty + // - the tiles the king moves over are not attacked + return moveTo.piece == null && !moveTo.isAttacked(); + } + + private void lookupRook() { Entity kingEntity = game.getEntityManager().getEntities().stream() .filter(entity -> entity.piece != null && entity.piece.identifier == kingId) .findFirst().orElse(null); @@ -70,26 +79,20 @@ private void lookupRooks() { return; } - List rightRooks = findRooks(kingEntity, rightRookDirection); - List leftRooks = findRooks(kingEntity, leftRookDirection); - if (rightRooks.size() != 1) { - logger.error("Expected exactly 1 rook to the right of king, but found {}", rightRooks.size()); - return; - } - if (leftRooks.size() != 1) { - logger.error("Expected exactly 1 rook to the left of king, but found {}", leftRooks.size()); + List rooks = findRooks(kingEntity, kingToRookDir); + if (rooks.size() != 1) { + logger.error("Expected exactly 1 rook in direction {} from king, but found {}", kingToRookDir, rooks.size()); return; } - //noinspection DataFlowIssue - rightRookId = rightRooks.get(0).piece.identifier; - //noinspection DataFlowIssue - leftRookId = leftRooks.get(0).piece.identifier; + rookTile = rooks.get(0); + assert rookTile.piece != null; + rookId = rookTile.piece.identifier; } private List findRooks(Entity fromTile, int direction) { assert fromTile.piece != null; - return TileExpression.repeat(TileExpression.neighbor(direction), 1, -1, true) + return repeat(neighbor(direction), 1, -1, true) .toV1(fromTile.piece.identifier) .findTiles(fromTile) .filter(tile -> tile.piece != null && tile.piece.identifier.pieceType() == rookTypeId) @@ -97,97 +100,63 @@ private List findRooks(Entity fromTile, int direction) { } private void onPieceMove(PieceMoveEvent.PieceMove move) { - if (leftRookId == null || rightRookId == null) { - logger.error("Castling observed PieceMove, but Rooks were not identified yet! LeftRookId: {} | RightRookId: {}", leftRookId, rightRookId); + if (rookId == null) { + logger.error("Castling observed PieceMove, but Rook was not identified yet! RookId is null."); } assert move.toTile().piece != null; // move always contains to moved piece in 'toTile' PieceIdentifier movedPiece = move.toTile().piece.identifier; if (movedPiece == this.kingId) { - kingMoved = true; - } else if (movedPiece == leftRookId) { - leftRookMoved = true; - } else if (movedPiece == rightRookId) { - rightRookMoved = true; + kingMoved.setValue(true); + } else if (movedPiece == rookId) { + rookMoved.setValue(true); } } - private MoveIntention getLeftCastle(Entity king) { - return getCastleMove(king, kingMoveLeft, leftRookId, leftRookMoved, stepRightExpression, stepLeftExpression); - } - - private MoveIntention getRightCastle(Entity king) { - return getCastleMove(king, kingMoveRight, rightRookId, rightRookMoved, stepLeftExpression, stepRightExpression); - } - - private MoveIntention getCastleMove( - Entity king, CompiledTileExpression kingMove, - PieceIdentifier rookId, boolean rookHasMoved, - CompiledTileExpression stepRookToKing, CompiledTileExpression stepKingToRook - ) { - assert king.piece != null; - if (rookHasMoved) { - return null; - } - - Entity kingMoveTile = checkKingMoveTile(king, kingMove); + private MoveIntention getCastleMove(Entity king) { + Entity kingMoveTile = kingMove.findTiles(king).findFirst().orElse(null); if (kingMoveTile == null) { // tile not empty, or king would be attacked on the path return null; } - - Entity rookMoveTile = stepRookToKing.findTiles(kingMoveTile).findFirst().orElse(null); - Entity rookTile = kingMoveTile; - while (true) { - rookTile = stepKingToRook.findTiles(rookTile).findFirst().orElse(null); - if (rookTile == null) { - logger.error("Exceeded board bounds while searching for rook. Did rook already move?"); - return null; - } - if (rookTile.piece != null) { - if (rookTile.piece.identifier == rookId) { - return getCastleMove(king, kingMoveTile, rookTile, rookMoveTile); - } else { - return null; - } - } + if (!checkTilesToRookEmpty(kingMoveTile, rookTile)) { + return null; // all tiles between king and rook must be empty } + + Entity rookMoveTile = stepToKingExpression.findTiles(kingMoveTile).findFirst().orElse(null); + return getCastleMove(king, kingMoveTile, rookTile, rookMoveTile); } - private Entity checkKingMoveTile(Entity king, CompiledTileExpression kingMove) { - assert king.piece != null; - // TODO erja - checkDetection is not correct. - return kingMove.findTiles(king) - .filter(tile -> tile.piece == null && !tile.isAttacked()) - .findFirst().orElse(null); + private boolean checkTilesToRookEmpty(Entity currentTile, Entity rookTile) { + if (currentTile == null) { + logger.error("Exceeded board bounds while searching for rookTile. Is Castle direction correct?"); + return false; + } + if (currentTile == rookTile) return true; + if (currentTile.piece != null) return false; + return checkTilesToRookEmpty(stepToRookExpression.findTiles(currentTile).findFirst().orElse(null), rookTile); } private MoveIntention getCastleMove(Entity kingStart, Entity kingEnd, Entity rookStart, Entity rookEnd) { - return new MoveIntention(kingEnd, () -> { - rookEnd.piece = rookStart.piece; - rookStart.piece = null; - game.movePiece(kingStart, kingEnd, Castling.class); - }, new CastlingSimulator(rookStart, rookEnd, kingStart, kingEnd)); + CastlingSimulator simulator = new CastlingSimulator(game, rookStart, rookEnd, kingStart, kingEnd); + return MoveIntention.fromMoveSimulator(game, kingEnd, simulator); } @Override public Stream getSpecialMoves(Entity king, Stream baseMoves) { - if (kingMoved || king.isAttacked()) { + if (kingMoved.getValue() || king.isAttacked() || rookMoved.getValue()) { return baseMoves; } List moves = new ArrayList<>(); - MoveIntention leftCastle = getLeftCastle(king); - if (leftCastle != null) moves.add(leftCastle); - - MoveIntention rightCastle = getRightCastle(king); - if (rightCastle != null) moves.add(rightCastle); + MoveIntention castleMove = getCastleMove(king); + if (castleMove != null) moves.add(castleMove); return Stream.concat(baseMoves, moves.stream()); } private record CastlingSimulator( - Entity rookStartTile, Entity rookEndTile, Entity kingStartTile, Entity kingEndTile + IChessGame game, Entity rookStartTile, Entity rookEndTile, Entity kingStartTile, Entity kingEndTile ) implements MoveIntention.IMoveSimulator { @Override @@ -196,6 +165,7 @@ public void simulate() { rookStartTile.piece = null; kingEndTile.piece = kingStartTile.piece; kingStartTile.piece = null; + game.notifyPieceMove(kingStartTile, kingEndTile, Castling.class); } @Override diff --git a/src/main/java/jchess/common/moveset/special/EnPassant.java b/src/main/java/jchess/common/moveset/special/EnPassant.java index ed28e0a..c056dbd 100644 --- a/src/main/java/jchess/common/moveset/special/EnPassant.java +++ b/src/main/java/jchess/common/moveset/special/EnPassant.java @@ -5,16 +5,17 @@ import jchess.common.components.PieceComponent; import jchess.common.components.PieceIdentifier; import jchess.common.events.PieceMoveEvent; +import jchess.common.events.PieceMoveEvent.PieceMove; import jchess.common.moveset.ISpecialRule; import jchess.common.moveset.MoveIntention; +import jchess.common.state.impl.ArrayState; import jchess.ecs.Entity; import jchess.el.CompiledTileExpression; import jchess.el.v2.TileExpression; import java.util.ArrayList; -import java.util.HashMap; +import java.util.Arrays; import java.util.List; -import java.util.Map; import java.util.stream.Stream; public class EnPassant implements ISpecialRule { @@ -23,7 +24,7 @@ public class EnPassant implements ISpecialRule { private final PieceType pawnTypeId; private final int[] pawnDoubleMoveDirections; private final CompiledTileExpression captureTiles; - private final Map doubleMovesByPlayer = new HashMap<>(); + private final PieceMove[] doubleMovesByPlayer; public EnPassant(IChessGame game, PieceIdentifier thisPawnId, PieceType pawnTypeId, int[] pawnDoubleMoveDirections, int[] pawnCaptureDirections) { this.game = game; @@ -32,34 +33,36 @@ public EnPassant(IChessGame game, PieceIdentifier thisPawnId, PieceType pawnType this.pawnDoubleMoveDirections = pawnDoubleMoveDirections; this.captureTiles = TileExpression.neighbor(pawnCaptureDirections).toV1(thisPawnId); + ArrayState movesState = new ArrayState<>(game.getNumPlayers(), PieceMove[]::new); + game.getStateManager().registerState(movesState); + doubleMovesByPlayer = movesState.getCurrent(); + game.getEventManager().getEvent(PieceMoveEvent.class).addListener(this::onPieceMove); } - private void onPieceMove(PieceMoveEvent.PieceMove move) { + private void onPieceMove(PieceMove move) { assert move.toTile().piece != null; // move always contains moved piece in toTile PieceComponent movedPiece = move.toTile().piece; if (movedPiece.identifier.ownerId() == thisPawnId.ownerId()) { // player made a move -> the window for an EnPassant move has passed - doubleMovesByPlayer.clear(); + Arrays.fill(doubleMovesByPlayer, null); return; } if (movedPiece.identifier.pieceType() == pawnTypeId && move.moveType() == SpecialFirstMove.class) { // opponent has made a move that can be attacked with EnPassant - doubleMovesByPlayer.put(movedPiece.identifier.ownerId(), move); + doubleMovesByPlayer[movedPiece.identifier.ownerId()] = move; } } @Override public Stream getSpecialMoves(Entity thisPawn, Stream baseMoves) { - if (doubleMovesByPlayer.isEmpty()) { - return baseMoves; - } - List result = new ArrayList<>(); - for (Map.Entry doubleMove : doubleMovesByPlayer.entrySet()) { - int doubleMovePlayer = doubleMove.getKey(); - PieceMoveEvent.PieceMove moveInfo = doubleMove.getValue(); + for (int doubleMovePlayer = 0; doubleMovePlayer < doubleMovesByPlayer.length; doubleMovePlayer++) { + PieceMove moveInfo = doubleMovesByPlayer[doubleMovePlayer]; + if (moveInfo == null) { + continue; + } if (moveInfo.toTile().piece == null) { // Might happen if another player took the moved pawn by EnPassant @@ -75,10 +78,10 @@ public Stream getSpecialMoves(Entity thisPawn, Stream { - doubleMove.toTile().piece = null; - game.movePiece(thisPawnFromTile, thisPawnToTile, EnPassant.class); - }, new EnPassantSimulator(doubleMove.toTile().piece, doubleMove.toTile(), thisPawnFromTile, thisPawnToTile)); + private MoveIntention getEnPassantMove(PieceMove doubleMove, Entity thisPawnFromTile, Entity thisPawnToTile) { + EnPassantSimulator simulator = new EnPassantSimulator( + game, + doubleMove.toTile().piece, doubleMove.toTile(), + thisPawnFromTile, thisPawnToTile + ); + return MoveIntention.fromMoveSimulator(game, thisPawnToTile, simulator); } private record EnPassantSimulator( + IChessGame game, PieceComponent capturedPiece, Entity capturedPieceTile, Entity moveFromTile, Entity moveToTile ) implements MoveIntention.IMoveSimulator { @@ -125,6 +131,7 @@ public void simulate() { capturedPieceTile.piece = null; moveToTile.piece = moveFromTile.piece; moveFromTile.piece = null; + game.notifyPieceMove(moveFromTile, moveToTile, EnPassant.class); } @Override diff --git a/src/main/java/jchess/common/moveset/special/PawnPromotion.java b/src/main/java/jchess/common/moveset/special/PawnPromotion.java index 0e446f9..8f434f1 100644 --- a/src/main/java/jchess/common/moveset/special/PawnPromotion.java +++ b/src/main/java/jchess/common/moveset/special/PawnPromotion.java @@ -29,11 +29,16 @@ public PawnPromotion(IChessGame game, Predicate isPromotionTile, Piece.. game.getEventManager().getEvent(PieceOfferSelectedEvent.class).addListener(selection -> { if (currentAwaitingPromotion != null) { - Entity promotedPiece = currentAwaitingPromotion.moveFrom(); - assert promotedPiece.piece != null; - int owner = promotedPiece.piece.identifier.ownerId(); - game.createPiece(promotedPiece, selection.getPieceTypeId(), owner); - game.movePiece(promotedPiece, currentAwaitingPromotion.moveTo(), PawnPromotion.class); + Entity moveFrom = currentAwaitingPromotion.moveFrom(); + assert moveFrom.piece != null; + int owner = moveFrom.piece.identifier.ownerId(); + game.createPiece(moveFrom, selection.getPieceTypeId(), owner); + + Entity moveTo = currentAwaitingPromotion.moveTo(); + moveTo.piece = moveFrom.piece; + moveFrom.piece = null; + game.notifyPieceMove(moveFrom, moveTo, PawnPromotion.class); + game.endTurn(); currentAwaitingPromotion = null; game.getEventManager().getEvent(RenderEvent.class).fire(null); diff --git a/src/main/java/jchess/common/moveset/special/RangedAttack.java b/src/main/java/jchess/common/moveset/special/RangedAttack.java index 68bfc2d..71031d3 100644 --- a/src/main/java/jchess/common/moveset/special/RangedAttack.java +++ b/src/main/java/jchess/common/moveset/special/RangedAttack.java @@ -31,23 +31,18 @@ public Stream getSpecialMoves(Entity thisRangedPiece, Stream { - targetTile.piece = null; - game.movePieceStationary(thisRangedPiece, RangedAttack.class); - }, - new RangedAttackSimulator(targetTile.piece, targetTile) - ); + RangedAttackSimulator simulator = new RangedAttackSimulator(game, thisRangedPiece, targetTile.piece, targetTile); + return MoveIntention.fromMoveSimulator(game, targetTile, simulator); } private record RangedAttackSimulator( - PieceComponent attackedPiece, Entity attackedPieceTile + IChessGame game, Entity stationaryTile, PieceComponent attackedPiece, Entity attackedPieceTile ) implements MoveIntention.IMoveSimulator { @Override public void simulate() { attackedPieceTile.piece = null; + game.notifyPieceMove(stationaryTile, stationaryTile, RangedAttack.class); } @Override diff --git a/src/main/java/jchess/common/moveset/special/ShapeShifting.java b/src/main/java/jchess/common/moveset/special/ShapeShifting.java index 7b8286f..d8d8408 100644 --- a/src/main/java/jchess/common/moveset/special/ShapeShifting.java +++ b/src/main/java/jchess/common/moveset/special/ShapeShifting.java @@ -40,30 +40,30 @@ public Stream getSpecialMoves(Entity movingPiece, Stream move.tile != null - && move.piece != null - && move.piece.identifier.ownerId() != movingPiece.piece.identifier.ownerId() - && shiftablePieceTypes.contains(move.piece.identifier.pieceType())) - .map(move -> new MoveIntention( - move, - () -> { - int owner = movingPiece.piece.identifier.ownerId(); - game.createPiece(movingPiece, move.piece.identifier.pieceType(), owner); - game.movePieceStationary(movingPiece, ShapeShifting.class); - }, - new ShapeShiftingSimulator(movingPiece, move.piece.identifier.pieceType(), shiftingPieceId.pieceType(), game) - ))); + .filter(target -> target.tile != null + && target.piece != null + && target.piece.identifier.ownerId() != movingPiece.piece.identifier.ownerId() + && shiftablePieceTypes.contains(target.piece.identifier.pieceType())) + .map(target -> { + ShapeShiftingSimulator simulator = new ShapeShiftingSimulator( + game, movingPiece, target.piece.identifier.pieceType(), shiftingPieceId.pieceType() + ); + return MoveIntention.fromMoveSimulator(game, target, simulator); + })); } - private record ShapeShiftingSimulator(Entity shiftingPieceTile, - PieceType attackedPieceType, - PieceType originalPieceType, - IChessGame game) implements MoveIntention.IMoveSimulator { + private record ShapeShiftingSimulator( + IChessGame game, + Entity shiftingPieceTile, + PieceType attackedPieceType, + PieceType originalPieceType + ) implements MoveIntention.IMoveSimulator { @Override public void simulate() { assert shiftingPieceTile.piece != null; int owner = shiftingPieceTile.piece.identifier.ownerId(); game.createPiece(shiftingPieceTile, attackedPieceType, owner); + game.notifyPieceMove(shiftingPieceTile, shiftingPieceTile, ShapeShifting.class); } @Override @@ -73,4 +73,4 @@ public void revert() { game.createPiece(shiftingPieceTile, originalPieceType, owner); } } -} \ No newline at end of file +} diff --git a/src/main/java/jchess/common/moveset/special/SpecialFirstMove.java b/src/main/java/jchess/common/moveset/special/SpecialFirstMove.java index 5277e07..19a2b1f 100644 --- a/src/main/java/jchess/common/moveset/special/SpecialFirstMove.java +++ b/src/main/java/jchess/common/moveset/special/SpecialFirstMove.java @@ -1,11 +1,12 @@ package jchess.common.moveset.special; import jchess.common.IChessGame; -import jchess.common.components.PieceComponent; import jchess.common.components.PieceIdentifier; import jchess.common.events.PieceMoveEvent; import jchess.common.moveset.ISpecialRule; import jchess.common.moveset.MoveIntention; +import jchess.common.moveset.NormalMove.NormalMoveSimulator; +import jchess.common.state.impl.BooleanState; import jchess.ecs.Entity; import jchess.el.CompiledTileExpression; import jchess.el.v2.ExpressionCompiler; @@ -16,52 +17,38 @@ public class SpecialFirstMove implements ISpecialRule { private final IChessGame game; private final PieceIdentifier pieceIdentifier; private final CompiledTileExpression compiledFirstMove; - private boolean hasMoved = false; + private final BooleanState hasMoved; public SpecialFirstMove(IChessGame game, PieceIdentifier pieceIdentifier, ExpressionCompiler firstMove) { this.game = game; this.pieceIdentifier = pieceIdentifier; this.compiledFirstMove = firstMove.toV1(pieceIdentifier); + this.hasMoved = new BooleanState(false); + + game.getStateManager().registerState(hasMoved); game.getEventManager().getEvent(PieceMoveEvent.class).addListener(move -> { assert move.toTile().piece != null; // move always contains moved piece in toTile if (move.toTile().piece.identifier == this.pieceIdentifier) { - hasMoved = true; + hasMoved.setValue(true); } }); } @Override public Stream getSpecialMoves(Entity movingPiece, Stream baseMoves) { - if (hasMoved) { + if (hasMoved.getValue()) { return baseMoves; } return Stream.concat( baseMoves, compiledFirstMove.findTiles(movingPiece) - .map(move -> new MoveIntention( - move, - () -> game.movePiece(movingPiece, move, SpecialFirstMove.class), - new SpecialFirstMoveSimulator(move.piece, movingPiece, move) - )) + .map(toTile -> { + NormalMoveSimulator simulator = new NormalMoveSimulator(game, movingPiece, toTile, SpecialFirstMove.class); + return MoveIntention.fromMoveSimulator(game, toTile, simulator); + }) ); } - private record SpecialFirstMoveSimulator( - PieceComponent capturedPiece, Entity fromTile, Entity toTile - ) implements MoveIntention.IMoveSimulator { - - @Override - public void simulate() { - toTile.piece = fromTile.piece; - fromTile.piece = null; - } - - @Override - public void revert() { - fromTile.piece = toTile.piece; - toTile.piece = capturedPiece; - } - } } diff --git a/src/main/java/jchess/gamemode/hex3p/Hex3pPieces.java b/src/main/java/jchess/gamemode/hex3p/Hex3pPieces.java index cd3c2b9..91296e1 100644 --- a/src/main/java/jchess/gamemode/hex3p/Hex3pPieces.java +++ b/src/main/java/jchess/gamemode/hex3p/Hex3pPieces.java @@ -41,10 +41,8 @@ public enum Hex3pPieces implements PieceStore.IPieceDefinitionProvider { King(PieceType.KING, new PieceStore.PieceDefinition( "K", rotations(regex("0", false), 12), - (game, kingIdentifier) -> new Castling( - game, kingIdentifier, Rook.pieceType, 90, 270, - regex("270.270.270", true), regex("90.90", true) - ) + (game, kingIdentifier) -> new Castling(game, kingIdentifier, Rook.pieceType, 90, 2), + (game, kingIdentifier) -> new Castling(game, kingIdentifier, Rook.pieceType, 270, 3) )), Pawn(PieceType.PAWN, new PieceStore.PieceDefinition( "", diff --git a/src/main/java/jchess/gamemode/square2p/Square2pPieces.java b/src/main/java/jchess/gamemode/square2p/Square2pPieces.java index 2eadf3d..f6b5dc4 100644 --- a/src/main/java/jchess/gamemode/square2p/Square2pPieces.java +++ b/src/main/java/jchess/gamemode/square2p/Square2pPieces.java @@ -38,10 +38,8 @@ public enum Square2pPieces implements PieceStore.IPieceDefinitionProvider { King(PieceType.KING, new PieceStore.PieceDefinition( "K", rotations(regex("0", false), 8), - (game, kingIdentifier) -> new Castling( - game, kingIdentifier, Rook.pieceType, 90, 270, - regex("270.270", true), regex("90.90", true) - ) + (game, kingIdentifier) -> new Castling(game, kingIdentifier, Rook.pieceType, 90, 2), + (game, kingIdentifier) -> new Castling(game, kingIdentifier, Rook.pieceType, 270, 2) )), Pawn(PieceType.PAWN, new PieceStore.PieceDefinition( "", From 0a3846a7cd7911c9ded56abf86d183bad872868e Mon Sep 17 00:00:00 2001 From: BlazingTwist <39350649+BlazingTwist@users.noreply.github.com> Date: Sun, 28 Jan 2024 22:10:50 +0100 Subject: [PATCH 4/5] updateAttackInfo uses StateManagement --- .../java/jchess/common/BaseChessGame.java | 51 +++++++++++-------- src/main/java/jchess/common/IChessGame.java | 9 +++- .../common/components/PieceComponent.java | 15 ++++-- .../common/components/TileComponent.java | 2 +- .../common/events/ComputeAttackInfoEvent.java | 6 --- src/main/java/jchess/ecs/Entity.java | 5 +- 6 files changed, 52 insertions(+), 36 deletions(-) delete mode 100644 src/main/java/jchess/common/events/ComputeAttackInfoEvent.java diff --git a/src/main/java/jchess/common/BaseChessGame.java b/src/main/java/jchess/common/BaseChessGame.java index 0f007d7..45094b9 100644 --- a/src/main/java/jchess/common/BaseChessGame.java +++ b/src/main/java/jchess/common/BaseChessGame.java @@ -6,13 +6,14 @@ import jchess.common.components.TileComponent; import jchess.common.events.BoardClickedEvent; import jchess.common.events.BoardInitializedEvent; -import jchess.common.events.ComputeAttackInfoEvent; import jchess.common.events.GameOverEvent; import jchess.common.events.OfferPieceSelectionEvent; import jchess.common.events.PieceMoveEvent; import jchess.common.events.PieceOfferSelectedEvent; import jchess.common.events.RenderEvent; import jchess.common.moveset.MoveIntention; +import jchess.common.state.IRevertibleState; +import jchess.common.state.StateManager; import jchess.ecs.EcsEventManager; import jchess.ecs.Entity; import jchess.ecs.EntityManager; @@ -27,6 +28,7 @@ public abstract class BaseChessGame implements IChessGame { private static final Logger logger = LoggerFactory.getLogger(BaseChessGame.class); protected final EntityManager entityManager; protected final EcsEventManager eventManager; + protected final StateManager stateManager; protected final int numPlayers; protected final PieceStore pieceStore; protected final IPieceLayoutProvider pieceLayout; @@ -36,6 +38,7 @@ public abstract class BaseChessGame implements IChessGame { public BaseChessGame(int numPlayers, PieceStore pieceStore, IPieceLayoutProvider pieceLayout) { this.entityManager = new EntityManager(); this.eventManager = new EcsEventManager(); + this.stateManager = new StateManager(); this.numPlayers = numPlayers; this.pieceStore = pieceStore; this.pieceLayout = pieceLayout; @@ -45,16 +48,25 @@ public BaseChessGame(int numPlayers, PieceStore pieceStore, IPieceLayoutProvider eventManager.registerEvent(new BoardInitializedEvent()); eventManager.registerEvent(new PieceMoveEvent()); - ComputeAttackInfoEvent computeAttackInfoEvent = new ComputeAttackInfoEvent(); - eventManager.registerEvent(computeAttackInfoEvent); - computeAttackInfoEvent.addListener(_void -> TileComponent.updateAttackInfo(this)); - BoardClickedEvent boardClickedEvent = new BoardClickedEvent(); eventManager.registerEvent(boardClickedEvent); boardClickedEvent.addListener(point -> onBoardClicked(point.x, point.y)); eventManager.registerEvent(new OfferPieceSelectionEvent()); eventManager.registerEvent(new PieceOfferSelectedEvent()); + + stateManager.registerState(new IRevertibleState() { + @Override + public void saveState() { + // attackInfo is temporary data, no need to save it + } + + @Override + public void revertState() { + // on revert, "simply" recompute the attackInfo + TileComponent.updateAttackInfo(BaseChessGame.this); + } + }); } protected abstract void generateBoard(); @@ -83,7 +95,7 @@ protected void onBoardClicked(int x, int y) { // show the tiles this piece can move to boolean isActivePiece = clickedEntity.piece.identifier.ownerId() == activePlayerId; - clickedEntity.findValidMoves(true).forEach(move -> createMoveToMarker(move, isActivePiece)); + clickedEntity.findValidMoves(this, true).forEach(move -> createMoveToMarker(move, isActivePiece)); createSelectionMarker(clickedEntity); long endTime = System.currentTimeMillis(); @@ -154,7 +166,7 @@ protected void checkGameOver() { // Game is over if the current player is unable to make any moves. boolean gameOver = entityManager.getEntities().stream() .filter(entity -> entity.piece != null && entity.piece.identifier.ownerId() == activePlayerId) - .allMatch(entity -> entity.findValidMoves(true).findAny().isEmpty()); + .allMatch(entity -> entity.findValidMoves(this, true).findAny().isEmpty()); if (gameOver) { logger.info("Game Over! Losing Player: {}", activePlayerId); @@ -219,30 +231,29 @@ public EcsEventManager getEventManager() { return eventManager; } + @Override + public StateManager getStateManager() { + return stateManager; + } + @Override public int getActivePlayerId() { return activePlayerId; } @Override - public void movePiece(Entity fromTile, Entity toTile, Class moveType) { - toTile.piece = fromTile.piece; - fromTile.piece = null; + public int getNumPlayers() { + return numPlayers; + } + @Override + public void notifyPieceMove(Entity fromTile, Entity toTile, Class moveType) { eventManager.getEvent(PieceMoveEvent.class).fire(new PieceMoveEvent.PieceMove(fromTile, toTile, moveType)); - eventManager.getEvent(ComputeAttackInfoEvent.class).fire(null); - - // end turn - activePlayerId = (activePlayerId + 1) % numPlayers; - checkGameOver(); + TileComponent.updateAttackInfo(this); } @Override - public void movePieceStationary(Entity tile, Class moveType) { - eventManager.getEvent(PieceMoveEvent.class).fire(new PieceMoveEvent.PieceMove(tile, tile, moveType)); - eventManager.getEvent(ComputeAttackInfoEvent.class).fire(null); - - // end turn + public void endTurn() { activePlayerId = (activePlayerId + 1) % numPlayers; checkGameOver(); } diff --git a/src/main/java/jchess/common/IChessGame.java b/src/main/java/jchess/common/IChessGame.java index 4cd7158..213247c 100644 --- a/src/main/java/jchess/common/IChessGame.java +++ b/src/main/java/jchess/common/IChessGame.java @@ -1,6 +1,7 @@ package jchess.common; import dx.schema.types.PieceType; +import jchess.common.state.StateManager; import jchess.ecs.EcsEventManager; import jchess.ecs.Entity; import jchess.ecs.EntityManager; @@ -10,15 +11,19 @@ public interface IChessGame { EcsEventManager getEventManager(); + StateManager getStateManager(); + int getActivePlayerId(); + int getNumPlayers(); + void start(); void createPiece(Entity targetTile, PieceType pieceType, int ownerId); - void movePiece(Entity fromTile, Entity toTile, Class moveType); + void notifyPieceMove(Entity fromTile, Entity toTile, Class moveType); - void movePieceStationary(Entity Tile, Class moveType); + void endTurn(); dx.schema.types.Entity applyPerspective(dx.schema.types.Entity tile, int playerIndex); } diff --git a/src/main/java/jchess/common/components/PieceComponent.java b/src/main/java/jchess/common/components/PieceComponent.java index c976042..b5c20f5 100644 --- a/src/main/java/jchess/common/components/PieceComponent.java +++ b/src/main/java/jchess/common/components/PieceComponent.java @@ -6,6 +6,7 @@ import jchess.common.moveset.ISpecialRuleProvider; import jchess.common.moveset.MoveIntention; import jchess.common.moveset.NormalMove; +import jchess.common.state.StateManager; import jchess.ecs.Entity; import jchess.el.CompiledTileExpression; import jchess.el.v2.ExpressionCompiler; @@ -42,14 +43,14 @@ public void addSpecialMoves(ISpecialRuleProvider... specialRuleProviders) { ); } - public Stream findValidMoves(Entity thisTile, boolean verifyKingSafe) { + public Stream findValidMoves(IChessGame game, Entity thisTile, boolean verifyKingSafe) { if (thisTile.tile == null) return Stream.empty(); Stream moves = Stream.empty(); if (baseMoveSet != null) { moves = Stream.concat( moves, - baseMoveSet.findTiles(thisTile).map(toTile -> NormalMove.getMove(game, thisTile, toTile)) + baseMoveSet.findTiles(thisTile).map(toTile -> NormalMove.getMove(this.game, thisTile, toTile)) ); } for (ISpecialRule specialRule : specialMoveSet) { @@ -57,15 +58,18 @@ public Stream findValidMoves(Entity thisTile, boolean verifyKingS } if (verifyKingSafe) { - moves = verifyKingSafe(moves); + moves = verifyKingSafe(game, moves); } return moves; } - private Stream verifyKingSafe(Stream allMoves) { + private Stream verifyKingSafe(IChessGame game, Stream allMoves) { int ownPlayerId = identifier.ownerId(); + StateManager stateManager = game.getStateManager(); + stateManager.saveState(); + return allMoves.filter(move -> { MoveIntention.IMoveSimulator simulator = move.moveSimulator(); simulator.simulate(); @@ -79,11 +83,12 @@ private Stream verifyKingSafe(Stream allMoves) { boolean kingInCheckAfterMove = game.getEntityManager().getEntities().parallelStream() .filter(entity -> entity.piece != null && entity.piece.identifier.ownerId() != ownPlayerId) .anyMatch(entity -> entity - .findValidMoves(false) + .findValidMoves(game, false) .anyMatch(moveTo -> moveTo.displayTile() == ownKing) ); simulator.revert(); + stateManager.revertState(); return !kingInCheckAfterMove; }); diff --git a/src/main/java/jchess/common/components/TileComponent.java b/src/main/java/jchess/common/components/TileComponent.java index b55f253..e1db1ee 100644 --- a/src/main/java/jchess/common/components/TileComponent.java +++ b/src/main/java/jchess/common/components/TileComponent.java @@ -18,7 +18,7 @@ public static void updateAttackInfo(IChessGame game) { game.getEntityManager().getEntities().parallelStream() .filter(entity -> entity.tile != null && entity.piece != null) .forEach(entity -> entity - .findValidMoves(false) + .findValidMoves(game, false) .forEach(move -> { assert move.displayTile().tile != null; move.displayTile().tile.attackingPieces.add(entity); diff --git a/src/main/java/jchess/common/events/ComputeAttackInfoEvent.java b/src/main/java/jchess/common/events/ComputeAttackInfoEvent.java deleted file mode 100644 index b6f7330..0000000 --- a/src/main/java/jchess/common/events/ComputeAttackInfoEvent.java +++ /dev/null @@ -1,6 +0,0 @@ -package jchess.common.events; - -import jchess.ecs.EcsEvent; - -public class ComputeAttackInfoEvent extends EcsEvent { -} diff --git a/src/main/java/jchess/ecs/Entity.java b/src/main/java/jchess/ecs/Entity.java index 96a22a4..a86415f 100644 --- a/src/main/java/jchess/ecs/Entity.java +++ b/src/main/java/jchess/ecs/Entity.java @@ -1,5 +1,6 @@ package jchess.ecs; +import jchess.common.IChessGame; import jchess.common.components.MarkerComponent; import jchess.common.components.PieceComponent; import jchess.common.components.TileComponent; @@ -18,8 +19,8 @@ public class Entity { @Nullable public MarkerComponent marker; - public Stream findValidMoves(boolean verifyKingSafe) { - return piece == null ? Stream.empty() : piece.findValidMoves(this, verifyKingSafe); + public Stream findValidMoves(IChessGame game, boolean verifyKingSafe) { + return piece == null ? Stream.empty() : piece.findValidMoves(game, this, verifyKingSafe); } public boolean isAttacked() { From 20786570eff68437ff37cea0378d9d5d492ff824 Mon Sep 17 00:00:00 2001 From: BlazingTwist <39350649+BlazingTwist@users.noreply.github.com> Date: Sun, 28 Jan 2024 22:11:00 +0100 Subject: [PATCH 5/5] update tests --- src/test/java/jchess/gamemode/hex3p/PieceMoveRulesTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/jchess/gamemode/hex3p/PieceMoveRulesTest.java b/src/test/java/jchess/gamemode/hex3p/PieceMoveRulesTest.java index 1f5ec14..b1679dd 100644 --- a/src/test/java/jchess/gamemode/hex3p/PieceMoveRulesTest.java +++ b/src/test/java/jchess/gamemode/hex3p/PieceMoveRulesTest.java @@ -132,7 +132,7 @@ private void moveTestNoOtherPieces(Hex3pPieces pieceType, int tileToTest, int[] PieceIdentifier identifier = new PieceIdentifier(pieceType.getPieceType(), pieceDefinition.shortName(), 0, 0); PieceComponent piece = new PieceComponent(null, identifier, pieceDefinition.baseMoves()); - List moves = piece.findValidMoves(testField[tileToTest], false).map(MoveIntention::displayTile).toList(); + List moves = piece.findValidMoves(null, testField[tileToTest], false).map(MoveIntention::displayTile).toList(); List expectedMoves = Arrays.stream(expectedTiles).mapToObj(expectedTile -> testField[expectedTile]).toList(); Assertions.assertEquals(new HashSet<>(expectedMoves), new HashSet<>(moves), "Set of possible moves does not match the expected ones. piece: '" + pieceType + "'"); }