Skip to content

Commit

Permalink
Checking draw by repetition. Improved time management.
Browse files Browse the repository at this point in the history
  • Loading branch information
dykstrom committed Dec 21, 2018
1 parent e4457e3 commit 249cf0a
Show file tree
Hide file tree
Showing 12 changed files with 109 additions and 64 deletions.
21 changes: 7 additions & 14 deletions src/main/java/se/dykstrom/ronja/common/model/Game.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

// ------------------------------------------------------------------------

/**
Expand Down Expand Up @@ -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.
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/se/dykstrom/ronja/common/model/Position.java
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
18 changes: 13 additions & 5 deletions src/main/java/se/dykstrom/ronja/engine/time/TimeUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down Expand Up @@ -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 = "?";
}
Expand All @@ -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);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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();
Expand Down
30 changes: 22 additions & 8 deletions src/main/java/se/dykstrom/ronja/engine/utils/PositionUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
}

/**
Expand Down Expand Up @@ -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";
}
Expand All @@ -91,7 +92,7 @@ public static String getDrawType(Position position) {
return "Insufficient mating material";
}

if (isDrawByThreefoldRepetition(position)) {
if (isDrawByThreefoldRepetition(position, game)) {
return "Threefold repetition";
}

Expand Down Expand Up @@ -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;
}

/**
Expand Down
14 changes: 9 additions & 5 deletions src/test/java/se/dykstrom/ronja/engine/time/TimeUtilsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;

Expand All @@ -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
Expand Down Expand Up @@ -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)));
}
}
46 changes: 25 additions & 21 deletions src/test/java/se/dykstrom/ronja/engine/utils/PositionUtilsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down

0 comments on commit 249cf0a

Please sign in to comment.