From 249cf0aa0aec693d8c4293ebcf486ae3ff03d48f Mon Sep 17 00:00:00 2001 From: dykstrom Date: Fri, 21 Dec 2018 19:31:48 +0100 Subject: [PATCH] Checking draw by repetition. Improved time management. --- .../se/dykstrom/ronja/common/model/Game.java | 21 +++------ .../dykstrom/ronja/common/model/Position.java | 20 ++++++++ .../ronja/engine/core/AlphaBetaFinder.java | 2 +- .../dykstrom/ronja/engine/time/TimeUtils.java | 18 ++++++-- .../ui/command/AbstractMoveCommand.java | 12 +++-- .../ronja/engine/ui/command/GoCommand.java | 2 +- .../ronja/engine/ui/command/HintCommand.java | 2 +- .../engine/ui/command/PlayOtherCommand.java | 2 +- .../engine/ui/command/UserMoveCommand.java | 4 +- .../ronja/engine/utils/PositionUtils.java | 30 ++++++++---- .../ronja/engine/time/TimeUtilsTest.java | 14 ++++-- .../ronja/engine/utils/PositionUtilsTest.java | 46 ++++++++++--------- 12 files changed, 109 insertions(+), 64 deletions(-) diff --git a/src/main/java/se/dykstrom/ronja/common/model/Game.java b/src/main/java/se/dykstrom/ronja/common/model/Game.java index 96fc9ad..75e7fa7 100644 --- a/src/main/java/se/dykstrom/ronja/common/model/Game.java +++ b/src/main/java/se/dykstrom/ronja/common/model/Game.java @@ -55,11 +55,17 @@ public class Game { /** Index to keep track of the number of stored moves. */ private int moveIndex; + /** All historic positions in this game. */ + public final Position[] positions = new Position[MAX_MOVES]; + + /** Index to keep track of the number of stored positions. */ + public int positionIndex; + /** The name of the opponent as set by the "name" command.*/ private String opponent; /** A reference to the opening book used in this game. */ - private OpeningBook book; + private final OpeningBook book; /** The game result, or {@code null} if the game has not yet ended. */ private String result; @@ -82,12 +88,6 @@ public class Game { /** Remaining time and moves for the engine. */ private TimeData timeData; - /** All historic positions in this game. */ - private final Position[] positions = new Position[MAX_MOVES]; - - /** Index to keep track of the number of stored positions. */ - private int positionIndex; - // ------------------------------------------------------------------------ /** @@ -211,13 +211,6 @@ public String getOpponent() { return opponent; } - /** - * Returns the list of historical positions. - */ - public Position[] getPositions() { - return positions; - } - /** * Sets the current position and the start position of the game to the given position. * Also resets the lists of historical positions and moves. diff --git a/src/main/java/se/dykstrom/ronja/common/model/Position.java b/src/main/java/se/dykstrom/ronja/common/model/Position.java index 7bab0b2..9ca84cb 100644 --- a/src/main/java/se/dykstrom/ronja/common/model/Position.java +++ b/src/main/java/se/dykstrom/ronja/common/model/Position.java @@ -726,6 +726,26 @@ public boolean equals(Object obj) { } } + /** + * Returns {@code true} if this position is equal to the given position if the full move number + * and half move clock are ignored. + * + * @param other The other position to compare with. + * @return True if the positions are equal. + */ + public boolean equalTo(Position other) { + return ((flags == other.flags) && + (enPassantSquare == other.enPassantSquare) && + (white == other.white) && + (black == other.black) && + (bishop == other.bishop) && + (king == other.king) && + (knight == other.knight) && + (pawn == other.pawn) && + (queen == other.queen) && + (rook == other.rook)); + } + @Override public String toString() { StringBuilder[] ranks = new StringBuilder[8]; diff --git a/src/main/java/se/dykstrom/ronja/engine/core/AlphaBetaFinder.java b/src/main/java/se/dykstrom/ronja/engine/core/AlphaBetaFinder.java index 2e2c3b2..9712c84 100644 --- a/src/main/java/se/dykstrom/ronja/engine/core/AlphaBetaFinder.java +++ b/src/main/java/se/dykstrom/ronja/engine/core/AlphaBetaFinder.java @@ -202,7 +202,7 @@ int alphaBeta(Position position, int lastMove, int depth, int alpha, int beta) { if (DEBUG) TLOG.finest(leave(position, depth) + ", score = " + Evaluator.CHECK_MATE_VALUE); return Evaluator.CHECK_MATE_VALUE; } - if (PositionUtils.isDraw(position)) { + if (PositionUtils.isDraw(position, game)) { if (DEBUG) TLOG.finest(leave(position, depth) + ", score = " + Evaluator.DRAW_VALUE); return Evaluator.DRAW_VALUE; } diff --git a/src/main/java/se/dykstrom/ronja/engine/time/TimeUtils.java b/src/main/java/se/dykstrom/ronja/engine/time/TimeUtils.java index 7a96708..650e2bd 100644 --- a/src/main/java/se/dykstrom/ronja/engine/time/TimeUtils.java +++ b/src/main/java/se/dykstrom/ronja/engine/time/TimeUtils.java @@ -127,13 +127,21 @@ private static long getMinutesAsMillis(Matcher matcher) { public static long calculateTimeForNextMove(TimeControl timeControl, TimeData timeData) { if (timeControl.getType() == TimeControlType.SECONDS_PER_MOVE) { // Use all available time minus a safety margin - return timeData.getRemainingTime() - 50; + // The safety margin is 10% of the time up to 500 ms + long margin = Math.min(timeData.getRemainingTime() / 10, 500); + return timeData.getRemainingTime() - margin; } else if (timeControl.getType() == TimeControlType.CLASSIC) { - // Divide remaining time evenly between remaining moves - return timeData.getRemainingTime() / timeData.getNumberOfMoves(); + // Divide the remaining time between remaining moves, + // but allocate more time to moves early in the game + double partOfMovesLeft = 1.0 * timeData.getNumberOfMoves() / timeControl.getNumberOfMoves(); + double factor = 0.6 * partOfMovesLeft + 0.7; + long evenlyDividedTime = timeData.getRemainingTime() / timeData.getNumberOfMoves(); + return (long) (evenlyDividedTime * factor); } else { // TimeControlType.INCREMENTAL - // Always assume there are 20 moves left - return timeData.getRemainingTime() / 20; + // Remove increment that was added after last move to get remaining base time + long baseTime = timeData.getRemainingTime() - timeControl.getIncrement(); + // Use a certain part of the base time, and add increment to that + return baseTime / 20 + timeControl.getIncrement(); } } diff --git a/src/main/java/se/dykstrom/ronja/engine/ui/command/AbstractMoveCommand.java b/src/main/java/se/dykstrom/ronja/engine/ui/command/AbstractMoveCommand.java index 57d60a0..bb76b37 100644 --- a/src/main/java/se/dykstrom/ronja/engine/ui/command/AbstractMoveCommand.java +++ b/src/main/java/se/dykstrom/ronja/engine/ui/command/AbstractMoveCommand.java @@ -74,7 +74,7 @@ protected void move() { response.write("move " + CanParser.format(move)); // Check game status after move (get new position) - if (PositionUtils.isGameOver(game.getPosition())) { + if (PositionUtils.isGameOver(game.getPosition(), game)) { notifyUserGameOverOk(); } @@ -117,8 +117,8 @@ void notifyUserGameOverOk() { } else { result = "1-0 {White mates}"; } - } else if (PositionUtils.isDraw(position)) { - result = "1/2-1/2 {" + PositionUtils.getDrawType(position) + "}"; + } else if (PositionUtils.isDraw(position, game)) { + result = "1/2-1/2 {" + PositionUtils.getDrawType(position, game) + "}"; } else { result = "?"; } @@ -133,8 +133,10 @@ void notifyUserGameOverError(String command) { Position position = game.getPosition(); if (PositionUtils.isCheckMate(position)) { response.write("Error (checkmate): " + command); - } else if (PositionUtils.isDraw(position)) { - response.write("Error (draw): " + command); + } else { + if (PositionUtils.isDraw(position, game)) { + response.write("Error (draw): " + command); + } } } } diff --git a/src/main/java/se/dykstrom/ronja/engine/ui/command/GoCommand.java b/src/main/java/se/dykstrom/ronja/engine/ui/command/GoCommand.java index bcfd50a..a1efcaf 100644 --- a/src/main/java/se/dykstrom/ronja/engine/ui/command/GoCommand.java +++ b/src/main/java/se/dykstrom/ronja/engine/ui/command/GoCommand.java @@ -36,7 +36,7 @@ public GoCommand(String args, Response response, Game game) { @Override public void execute() { - if (PositionUtils.isGameOver(game.getPosition())) { + if (PositionUtils.isGameOver(game.getPosition(), game)) { notifyUserGameOverError(NAME); } else { game.setForceMode(false); diff --git a/src/main/java/se/dykstrom/ronja/engine/ui/command/HintCommand.java b/src/main/java/se/dykstrom/ronja/engine/ui/command/HintCommand.java index 01e6b88..53579f2 100644 --- a/src/main/java/se/dykstrom/ronja/engine/ui/command/HintCommand.java +++ b/src/main/java/se/dykstrom/ronja/engine/ui/command/HintCommand.java @@ -43,7 +43,7 @@ public HintCommand(String args, Response response, Game game) { public void execute() { OpeningBook book = game.getBook(); Position position = game.getPosition(); - if (!PositionUtils.isGameOver(position)) { + if (!PositionUtils.isGameOver(position, game)) { int move = book.findBestMove(position); if (move == 0) { var finder = new AlphaBetaFinder(game); diff --git a/src/main/java/se/dykstrom/ronja/engine/ui/command/PlayOtherCommand.java b/src/main/java/se/dykstrom/ronja/engine/ui/command/PlayOtherCommand.java index b533288..59f1984 100644 --- a/src/main/java/se/dykstrom/ronja/engine/ui/command/PlayOtherCommand.java +++ b/src/main/java/se/dykstrom/ronja/engine/ui/command/PlayOtherCommand.java @@ -37,7 +37,7 @@ public PlayOtherCommand(String args, Response response, Game game) { @Override public void execute() { - if (PositionUtils.isGameOver(game.getPosition())) { + if (PositionUtils.isGameOver(game.getPosition(), game)) { notifyUserGameOverError(NAME); } else { game.setForceMode(false); diff --git a/src/main/java/se/dykstrom/ronja/engine/ui/command/UserMoveCommand.java b/src/main/java/se/dykstrom/ronja/engine/ui/command/UserMoveCommand.java index 064f426..8ff3f00 100644 --- a/src/main/java/se/dykstrom/ronja/engine/ui/command/UserMoveCommand.java +++ b/src/main/java/se/dykstrom/ronja/engine/ui/command/UserMoveCommand.java @@ -44,7 +44,7 @@ public UserMoveCommand(String move, Response response, Game game) throws Invalid public void execute() { Position position = game.getPosition(); - if (PositionUtils.isGameOver(position)) { + if (PositionUtils.isGameOver(position, game)) { notifyUserGameOverError(NAME); } else { try { @@ -61,7 +61,7 @@ public void execute() { } // If game is over notify user, otherwise make engine's move (in the new position) - if (PositionUtils.isGameOver(game.getPosition())) { + if (PositionUtils.isGameOver(game.getPosition(), game)) { notifyUserGameOverOk(); } else { move(); diff --git a/src/main/java/se/dykstrom/ronja/engine/utils/PositionUtils.java b/src/main/java/se/dykstrom/ronja/engine/utils/PositionUtils.java index d0ca776..ba0f347 100644 --- a/src/main/java/se/dykstrom/ronja/engine/utils/PositionUtils.java +++ b/src/main/java/se/dykstrom/ronja/engine/utils/PositionUtils.java @@ -19,6 +19,7 @@ import se.dykstrom.ronja.common.model.Board; import se.dykstrom.ronja.common.model.Color; +import se.dykstrom.ronja.common.model.Game; import se.dykstrom.ronja.common.model.Position; import se.dykstrom.ronja.engine.core.FullMoveGenerator; @@ -44,8 +45,8 @@ public static boolean isLegal(Position position) { /** * Returns {@code true} if the game is over, that is, if the given position is draw or checkmate. */ - public static boolean isGameOver(Position position) { - return isCheckMate(position) || isDraw(position); + public static boolean isGameOver(Position position, Game game) { + return isCheckMate(position) || isDraw(position, game); } /** @@ -74,15 +75,15 @@ public static boolean isCheckMate(Position position) { /** * Returns {@code true} if the given position is a draw. */ - public static boolean isDraw(Position position) { - return getDrawType(position) != null; + public static boolean isDraw(Position position, Game game) { + return getDrawType(position, game) != null; } /** * Returns a string describing the type of draw in the given position, or {@code null} * if the given position is not a draw at all. */ - public static String getDrawType(Position position) { + public static String getDrawType(Position position, Game game) { if (isDrawByFiftyMoveRule(position)) { return "Fifty move rule"; } @@ -91,7 +92,7 @@ public static String getDrawType(Position position) { return "Insufficient mating material"; } - if (isDrawByThreefoldRepetition(position)) { + if (isDrawByThreefoldRepetition(position, game)) { return "Threefold repetition"; } @@ -126,9 +127,22 @@ private static boolean isDrawByStalemate(Position position) { /** * Returns {@code true} if the given position is a draw by threefold repetition of position. + * + * @param position The position to check. + * @param game A reference to the current game that contains all positions that have occurred so far. + * @return True if a draw was found. */ - private static boolean isDrawByThreefoldRepetition(Position position) { - return false; + private static boolean isDrawByThreefoldRepetition(Position position, Game game) { + int count = 1; + int index = game.positionIndex - 2; + while (index >= 0 && count < 3) { + if (position.equalTo(game.positions[index])) { + count++; + } + index--; + } + + return count >= 3; } /** diff --git a/src/test/java/se/dykstrom/ronja/engine/time/TimeUtilsTest.java b/src/test/java/se/dykstrom/ronja/engine/time/TimeUtilsTest.java index 5958e4a..b510829 100644 --- a/src/test/java/se/dykstrom/ronja/engine/time/TimeUtilsTest.java +++ b/src/test/java/se/dykstrom/ronja/engine/time/TimeUtilsTest.java @@ -22,6 +22,7 @@ import java.text.ParseException; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static se.dykstrom.ronja.engine.time.TimeControlType.*; import static se.dykstrom.ronja.engine.time.TimeUtils.*; @@ -40,6 +41,7 @@ public class TimeUtilsTest { private static final TimeControl TC_40_2_30_0 = new TimeControl(40, 150 * 1000, 0, CLASSIC); private static final TimeControl TC_40_25_0 = new TimeControl(40, 25 * 60 * 1000, 0, CLASSIC); + private static final TimeControl TC_0_0_3 = new TimeControl(0, 0, 3 * 1000, SECONDS_PER_MOVE); private static final TimeControl TC_0_0_30 = new TimeControl(0, 0, 30 * 1000, SECONDS_PER_MOVE); @Test @@ -87,18 +89,20 @@ public void testParseStText_TwoArguments() throws Exception { @Test public void testCalculateTimeForNextMoveClassic() { - assertEquals(7500, calculateTimeForNextMove(TC_40_5_0, TimeData.from(TC_40_5_0))); - assertEquals(9000, calculateTimeForNextMove(TC_10_1_30_0, TimeData.from(TC_10_1_30_0))); + assertTrue(calculateTimeForNextMove(TC_40_5_0, TimeData.from(TC_40_5_0)) > 7500); + assertTrue(calculateTimeForNextMove(TC_10_1_30_0, TimeData.from(TC_10_1_30_0)) > 9000); } @Test public void testCalculateTimeForNextMoveIncremental() { - assertEquals(90000, calculateTimeForNextMove(TC_0_30_5, TimeData.from(TC_0_30_5))); - assertEquals(50, calculateTimeForNextMove(TC_0_30_5, TimeData.from(TC_0_30_5).withRemainingTime(1000))); + TimeData timeData = TimeData.from(TC_0_30_5); + assertEquals(95000, calculateTimeForNextMove(TC_0_30_5, timeData.withRemainingTime(30 * 60 * 1000 + 5 * 1000))); + assertEquals(5050, calculateTimeForNextMove(TC_0_30_5, timeData.withRemainingTime(1000 + 5 * 1000))); } @Test public void testCalculateTimeForNextMoveSecondsPerMove() { - assertEquals(29950, calculateTimeForNextMove(TC_0_0_30, TimeData.from(TC_0_0_30))); + assertEquals(2700, calculateTimeForNextMove(TC_0_0_3, TimeData.from(TC_0_0_3))); + assertEquals(29500, calculateTimeForNextMove(TC_0_0_30, TimeData.from(TC_0_0_30))); } } diff --git a/src/test/java/se/dykstrom/ronja/engine/utils/PositionUtilsTest.java b/src/test/java/se/dykstrom/ronja/engine/utils/PositionUtilsTest.java index 10fba96..d575d5c 100644 --- a/src/test/java/se/dykstrom/ronja/engine/utils/PositionUtilsTest.java +++ b/src/test/java/se/dykstrom/ronja/engine/utils/PositionUtilsTest.java @@ -34,25 +34,29 @@ */ public class PositionUtilsTest extends AbstractTestCase { + private final Game game = new Game(OpeningBook.DEFAULT); + @Test public void testIsGameOver() throws Exception { - assertFalse(PositionUtils.isGameOver(FenParser.parse(FEN_START))); - assertFalse(PositionUtils.isGameOver(FenParser.parse(FEN_END_GAME_0))); - assertFalse(PositionUtils.isGameOver(FenParser.parse(FEN_DRAW_1_1))); - assertFalse(PositionUtils.isGameOver(FenParser.parse(FEN_CHECKMATE_1_2))); - assertTrue(PositionUtils.isGameOver(FenParser.parse(FEN_DRAW_1_2))); - assertTrue(PositionUtils.isGameOver(FenParser.parse(FEN_CHECKMATE_1_3))); - assertTrue(PositionUtils.isGameOver(FenParser.parse(FEN_SCHOLARS_MATE))); - assertTrue(PositionUtils.isGameOver(FenParser.parse(FEN_ONE_BISHOP))); + assertFalse(PositionUtils.isGameOver(FenParser.parse(FEN_START), game)); + assertFalse(PositionUtils.isGameOver(FenParser.parse(FEN_END_GAME_0), game)); + assertFalse(PositionUtils.isGameOver(FenParser.parse(FEN_DRAW_1_1), game)); + assertFalse(PositionUtils.isGameOver(FenParser.parse(FEN_CHECKMATE_1_2), game)); + + assertTrue(PositionUtils.isGameOver(FenParser.parse(FEN_DRAW_1_2), game)); + assertTrue(PositionUtils.isGameOver(FenParser.parse(FEN_CHECKMATE_1_3), game)); + assertTrue(PositionUtils.isGameOver(FenParser.parse(FEN_SCHOLARS_MATE), game)); + assertTrue(PositionUtils.isGameOver(FenParser.parse(FEN_ONE_BISHOP), game)); } @Test public void testIsDraw() throws Exception { - assertFalse(PositionUtils.isDraw(FenParser.parse(FEN_START))); - assertFalse(PositionUtils.isDraw(FenParser.parse(FEN_CHECKMATE_1_3))); - assertFalse(PositionUtils.isDraw(FenParser.parse(FEN_DRAW_1_1))); - assertTrue(PositionUtils.isDraw(FenParser.parse(FEN_DRAW_1_2))); - assertTrue(PositionUtils.isDraw(FenParser.parse(FEN_ONE_BISHOP))); + assertFalse(PositionUtils.isDraw(FenParser.parse(FEN_START), game)); + assertFalse(PositionUtils.isDraw(FenParser.parse(FEN_CHECKMATE_1_3), game)); + assertFalse(PositionUtils.isDraw(FenParser.parse(FEN_DRAW_1_1), game)); + + assertTrue(PositionUtils.isDraw(FenParser.parse(FEN_DRAW_1_2), game)); + assertTrue(PositionUtils.isDraw(FenParser.parse(FEN_ONE_BISHOP), game)); } @Test @@ -63,24 +67,24 @@ public void testIsDrawByThreefoldRepetition() { // Make moves to repeat position game.makeMove(MOVE_G1F3); - assertNull(PositionUtils.getDrawType(position)); + assertNull(PositionUtils.getDrawType(game.getPosition(), game)); game.makeMove(MOVE_G8F6); - assertNull(PositionUtils.getDrawType(position)); + assertNull(PositionUtils.getDrawType(game.getPosition(), game)); game.makeMove(MOVE_F3G1); - assertNull(PositionUtils.getDrawType(position)); + assertNull(PositionUtils.getDrawType(game.getPosition(), game)); game.makeMove(MOVE_F6G8); - assertNull(PositionUtils.getDrawType(position)); + assertNull(PositionUtils.getDrawType(game.getPosition(), game)); game.makeMove(MOVE_G1F3); - assertNull(PositionUtils.getDrawType(position)); + assertNull(PositionUtils.getDrawType(game.getPosition(), game)); game.makeMove(MOVE_G8F6); - assertNull(PositionUtils.getDrawType(position)); + assertNull(PositionUtils.getDrawType(game.getPosition(), game)); game.makeMove(MOVE_F3G1); - assertNull(PositionUtils.getDrawType(position)); + assertNull(PositionUtils.getDrawType(game.getPosition(), game)); game.makeMove(MOVE_F6G8); // Check for draw - assertEquals("Threefold repetition", PositionUtils.getDrawType(position)); + assertEquals("Threefold repetition", PositionUtils.getDrawType(position, game)); } @Test