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 5a80bd6..75e7fa7 100644 --- a/src/main/java/se/dykstrom/ronja/common/model/Game.java +++ b/src/main/java/se/dykstrom/ronja/common/model/Game.java @@ -18,14 +18,12 @@ package se.dykstrom.ronja.common.model; import se.dykstrom.ronja.common.book.OpeningBook; -import se.dykstrom.ronja.common.parser.IllegalMoveException; import se.dykstrom.ronja.engine.time.TimeControl; import se.dykstrom.ronja.engine.time.TimeControlType; import se.dykstrom.ronja.engine.time.TimeData; import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; +import java.util.Arrays; import static se.dykstrom.ronja.engine.time.TimeControlType.CLASSIC; @@ -39,6 +37,9 @@ public class Game { /** Default time control is 40 moves in 2 minutes. */ private static final TimeControl TWO_MINUTES = new TimeControl(40, 2 * 60 * 1000, 0, CLASSIC); + /** The maximum number of moves in a game. */ + private static final int MAX_MOVES = 500; + /** True if force mode is on. */ private boolean force; @@ -49,13 +50,22 @@ public class Game { private Color engineColor; /** All moves made in this game. */ - private List moves; + private final int[] moves = new int[MAX_MOVES]; + + /** 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; @@ -95,7 +105,6 @@ public void reset() { setForceMode(false); setPosition(Position.START); setEngineColor(Color.BLACK); - setMoves(new ArrayList<>()); setOpponent(null); setResult("*"); setStartTime(LocalDateTime.now()); @@ -104,18 +113,28 @@ public void reset() { } /** - * Makes the given move, and updates game data accordingly. + * Makes the given move, updates game data, and returns the resulting position. */ - public void makeMove(int move) throws IllegalMoveException { - Position newPosition = position.withMove(move); + public Position makeMove(int move) { + moves[moveIndex++] = move; + + position = position.withMove(move); + positions[positionIndex++] = position; - // If the user is in check after his move - if (newPosition.isIllegalCheck()) { - throw new IllegalMoveException("in check after move"); + return position; + } + + /** + * Unmakes the last move that was made, and updates game data. + */ + public void unmakeMove() { + if (moveIndex == 0) { + throw new IllegalStateException("no moves to unmake"); } + moveIndex--; - position = newPosition; - moves.add(move); + positionIndex--; + position = positions[positionIndex - 1]; } /** @@ -162,17 +181,18 @@ public Color getEngineColor() { } /** - * Sets the list of moves. + * Sets the array of historical moves. */ - public void setMoves(List moves) { - this.moves = moves; + public void setMoves(int[] moves) { + System.arraycopy(moves, 0, this.moves, 0, moves.length); + moveIndex = moves.length; } /** - * Returns the list of moves made so far in this game. + * Returns the array of historical moves. */ - public List getMoves() { - return moves; + public int[] getMoves() { + return Arrays.copyOf(moves, moveIndex); } /** @@ -192,7 +212,8 @@ public String getOpponent() { } /** - * Sets the current position. Also sets the start position of the game to the given position. + * Sets the current position and the start position of the game to the given position. + * Also resets the lists of historical positions and moves. * * @param position The position to set. */ @@ -200,6 +221,11 @@ public void setPosition(Position position) { this.position = position; this.startPosition = position; this.startMoveNumber = position.getFullMoveNumber(); + + positions[0] = position; + positionIndex = 1; + + moveIndex = 0; } /** @@ -239,13 +265,6 @@ public boolean getForceMode() { return force; } - /** - * Sets the opening book. - */ - public void setBook(OpeningBook book) { - this.book = book; - } - /** * Returns a reference to the opening book used in this game. */ 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/common/parser/PgnParser.java b/src/main/java/se/dykstrom/ronja/common/parser/PgnParser.java index c14add1..3f9eee5 100644 --- a/src/main/java/se/dykstrom/ronja/common/parser/PgnParser.java +++ b/src/main/java/se/dykstrom/ronja/common/parser/PgnParser.java @@ -29,8 +29,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import static se.dykstrom.ronja.common.utils.ArrayUtils.toArray; - /** * A class that can parse and format files specified in Portable Game Notation (PGN). * @@ -95,7 +93,7 @@ private static String getMoves(Game game) { StringBuilder line = new StringBuilder(); int moveNumber = game.getStartMoveNumber(); - Iterator iterator = SanParser.format(game.getStartPosition(), toArray(game.getMoves())).iterator(); + Iterator iterator = SanParser.format(game.getStartPosition(), game.getMoves()).iterator(); // If the game was setup, and the first move was by black, we need some special formatting if (!game.getStartPosition().isWhiteMove() && iterator.hasNext()) { line.append(String.format("%d... %s ", moveNumber++, iterator.next())); diff --git a/src/main/java/se/dykstrom/ronja/common/utils/ArrayUtils.java b/src/main/java/se/dykstrom/ronja/common/utils/ArrayUtils.java deleted file mode 100644 index af31ee2..0000000 --- a/src/main/java/se/dykstrom/ronja/common/utils/ArrayUtils.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) 2017 Johan Dykstrom - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package se.dykstrom.ronja.common.utils; - -import java.util.List; - -/** - * Utility methods related to arrays. - * - * @author Johan Dykstrom - */ -public final class ArrayUtils { - - private ArrayUtils() { } - - /** - * Converts the given list of Integer objects to an array of ints. - */ - public static int[] toArray(List list) { - return list.stream().mapToInt(element -> element).toArray(); - } -} 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 a161336..5b60772 100644 --- a/src/main/java/se/dykstrom/ronja/engine/core/AlphaBetaFinder.java +++ b/src/main/java/se/dykstrom/ronja/engine/core/AlphaBetaFinder.java @@ -17,6 +17,7 @@ package se.dykstrom.ronja.engine.core; +import se.dykstrom.ronja.common.model.Game; import se.dykstrom.ronja.common.model.Position; import se.dykstrom.ronja.common.parser.SanParser; import se.dykstrom.ronja.engine.time.TimeUtils; @@ -58,6 +59,13 @@ public class AlphaBetaFinder extends AbstractFinder { /** Used to generate moves. */ private final FullMoveGenerator fullMoveGenerator = new FullMoveGenerator(); + /** The current game. */ + private final Game game; + + public AlphaBetaFinder(Game game) { + this.game = game; + } + @Override public int findBestMoveWithinTime(Position position, long maxTime) { TLOG.fine("Available time " + maxTime + " = " + formatTime(maxTime)); @@ -106,8 +114,7 @@ public int findBestMove(Position position, int maxDepth) { /** * Finds the best move in the given position, searching in the given list of - * moves. Searching is limited to the given max depth and the given max - * time. + * moves. Searching is limited to the given max depth and the given max time. */ private int findBestMove(Position position, int maxDepth, int numberOfMoves, long maxTime) { setMaxDepth(maxDepth); @@ -123,13 +130,17 @@ private int findBestMove(Position position, int maxDepth, int numberOfMoves, lon // Abort search if we realize we won't finish in time abortSearchIfOutOfTime(numberOfMoves, moveIndex, startTime, maxTime); - // Make the move int move = fullMoveGenerator.moves[0][moveIndex]; - Position next = position.withMove(move); + + // Make the move + Position next = game.makeMove(move); // Calculate the score for the move by searching deeper int score = -alphaBeta(next, move, 1, -beta, -alpha); + // Unmake the move again + game.unmakeMove(); + // No beta cut-off needed here // If this move is the best yet @@ -172,18 +183,11 @@ private void abortSearchIfOutOfTime(int numberOfMoves, int moveIndex, long start * {@code beta} are search results from already searched branches in the * tree. * - * @param position - * The position to calculate the score for. - * @param lastMove - * The last move made that led to this position. - * @param depth - * The current search depth. - * @param alpha - * The score of the best move found so far in any branch of the - * tree. - * @param beta - * The score of the best move for our opponent found so far in - * any branch of the tree. + * @param position The position to calculate the score for. + * @param lastMove The last move made that led to this position. + * @param depth The current search depth. + * @param alpha The score of the best move found so far in any branch of the tree. + * @param beta The score of the best move for our opponent found so far in any branch of the tree. */ int alphaBeta(Position position, int lastMove, int depth, int alpha, int beta) { if (DEBUG) TLOG.finest(enter(position, depth) + ", after " + lastMove + ", alpha = " + alpha + ", beta = " + beta); @@ -197,7 +201,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; } @@ -220,16 +224,18 @@ int alphaBeta(Position position, int lastMove, int depth, int alpha, int beta) { int move = fullMoveGenerator.moves[depth][moveIndex]; // Make the move - Position next = position.withMove(move); + Position next = game.makeMove(move); // Calculate the score for the move by searching deeper int score = -alphaBeta(next, move, depth + 1, -beta, -alpha); + // Unmake the move again + game.unmakeMove(); + // If the score is too good, we cut off the search tree here, // because the opponent will not select this branch if (score >= beta) { - if (DEBUG) - TLOG.finest(leave(position, depth) + ", score = " + beta + " (beta cut-off for score " + score + ")"); + if (DEBUG) TLOG.finest(leave(position, depth) + ", score = " + beta + " (beta cut-off for score " + score + ")"); return beta; } 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..5d7fa90 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.2 * partOfMovesLeft + 0.9; + 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 f3dc357..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 @@ -21,10 +21,8 @@ import se.dykstrom.ronja.common.model.Game; import se.dykstrom.ronja.common.model.Position; import se.dykstrom.ronja.common.parser.CanParser; -import se.dykstrom.ronja.common.parser.IllegalMoveException; import se.dykstrom.ronja.common.parser.SanParser; import se.dykstrom.ronja.engine.core.AlphaBetaFinder; -import se.dykstrom.ronja.engine.core.Finder; import se.dykstrom.ronja.engine.time.TimeUtils; import se.dykstrom.ronja.engine.ui.io.Response; import se.dykstrom.ronja.engine.utils.PositionUtils; @@ -40,8 +38,6 @@ public abstract class AbstractMoveCommand extends AbstractCommand { private static final Logger TLOG = Logger.getLogger(AbstractMoveCommand.class.getName()); - private static final Finder FINDER = new AlphaBetaFinder(); - AbstractMoveCommand(String args, Response response, Game game) { super(args, response, game); } @@ -63,25 +59,22 @@ protected void move() { // If no book move found, use Finder to find best move if (move == 0) { - long availableTime = TimeUtils.calculateTimeForNextMove(game.getTimeControl(), game.getTimeData()); - move = FINDER.findBestMoveWithinTime(position, availableTime); + var availableTime = TimeUtils.calculateTimeForNextMove(game.getTimeControl(), game.getTimeData()); + var finder = new AlphaBetaFinder(game); + move = finder.findBestMoveWithinTime(position, availableTime); TLOG.fine("Engine move: " + formatForLogging(move, position)); } else { TLOG.fine("Engine move: " + formatForLogging(move, position) + " (book)"); } // Make the move - try { - game.makeMove(move); - } catch (IllegalMoveException ime) { - TLOG.severe("Engine made an illegal move (" + CanParser.format(move) + "): " + ime.getMessage()); - } + game.makeMove(move); // Reply to XBoard 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(); } @@ -124,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 = "?"; } @@ -140,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 bd86bf6..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 @@ -22,7 +22,6 @@ import se.dykstrom.ronja.common.model.Position; import se.dykstrom.ronja.common.parser.SanParser; import se.dykstrom.ronja.engine.core.AlphaBetaFinder; -import se.dykstrom.ronja.engine.core.Finder; import se.dykstrom.ronja.engine.ui.io.Response; import se.dykstrom.ronja.engine.utils.PositionUtils; @@ -35,8 +34,6 @@ public class HintCommand extends AbstractCommand { public static final String NAME = "hint"; - private static final Finder FINDER = new AlphaBetaFinder(); - @SuppressWarnings("WeakerAccess") public HintCommand(String args, Response response, Game game) { super(args, response, game); @@ -46,10 +43,11 @@ 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) { - move = FINDER.findBestMove(position, 3); // Limit the search depth in this case + var finder = new AlphaBetaFinder(game); + move = finder.findBestMove(position, 3); // Limit the search depth in this case } response.write("Hint: " + SanParser.format(position, move)); } diff --git a/src/main/java/se/dykstrom/ronja/engine/ui/command/MovesCommand.java b/src/main/java/se/dykstrom/ronja/engine/ui/command/MovesCommand.java index dd7714c..740bd60 100644 --- a/src/main/java/se/dykstrom/ronja/engine/ui/command/MovesCommand.java +++ b/src/main/java/se/dykstrom/ronja/engine/ui/command/MovesCommand.java @@ -17,14 +17,13 @@ package se.dykstrom.ronja.engine.ui.command; -import static se.dykstrom.ronja.common.parser.SanParser.format; -import static se.dykstrom.ronja.common.utils.ArrayUtils.toArray; +import se.dykstrom.ronja.common.model.Game; +import se.dykstrom.ronja.engine.ui.io.Response; import java.util.Iterator; import java.util.List; -import se.dykstrom.ronja.common.model.Game; -import se.dykstrom.ronja.engine.ui.io.Response; +import static se.dykstrom.ronja.common.parser.SanParser.format; public class MovesCommand extends AbstractCommand { @@ -41,7 +40,7 @@ public void execute() { response.write(" White Black"); int moveNumber = game.getStartMoveNumber(); - List formattedMoves = format(game.getStartPosition(), toArray(game.getMoves())); + List formattedMoves = format(game.getStartPosition(), game.getMoves()); if (!game.getStartPosition().isWhiteMove() && !formattedMoves.isEmpty()) { formattedMoves.add(0, ""); } 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/SetBoardCommand.java b/src/main/java/se/dykstrom/ronja/engine/ui/command/SetBoardCommand.java index 7b29a7c..9ec1a2f 100644 --- a/src/main/java/se/dykstrom/ronja/engine/ui/command/SetBoardCommand.java +++ b/src/main/java/se/dykstrom/ronja/engine/ui/command/SetBoardCommand.java @@ -24,7 +24,6 @@ import se.dykstrom.ronja.engine.utils.PositionUtils; import java.text.ParseException; -import java.util.ArrayList; public class SetBoardCommand extends AbstractCommand { @@ -48,7 +47,6 @@ public void execute() { response.write("tellusererror Not in force mode"); } else { game.setPosition(position); - game.setMoves(new ArrayList<>()); } } catch (ParseException e) { response.write("tellusererror Illegal position"); 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 704b43b..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 @@ -17,8 +17,6 @@ package se.dykstrom.ronja.engine.ui.command; -import java.util.logging.Logger; - import se.dykstrom.ronja.common.model.Game; import se.dykstrom.ronja.common.model.Position; import se.dykstrom.ronja.common.parser.IllegalMoveException; @@ -26,6 +24,8 @@ import se.dykstrom.ronja.engine.ui.io.Response; import se.dykstrom.ronja.engine.utils.PositionUtils; +import java.util.logging.Logger; + public class UserMoveCommand extends AbstractMoveCommand { public static final String NAME = "usermove"; @@ -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 { @@ -54,8 +54,14 @@ public void execute() { // Make the user's move game.makeMove(move); + // If the user is in check after his move + if (game.getPosition().isIllegalCheck()) { + game.unmakeMove(); + throw new IllegalMoveException("in check after move"); + } + // 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 b71fd90..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,9 @@ public static String getDrawType(Position position) { return "Insufficient mating material"; } - // TODO: Three-fold repetition of position. + if (isDrawByThreefoldRepetition(position, game)) { + return "Threefold repetition"; + } if (isDrawByStalemate(position)) { return "Stalemate"; @@ -122,6 +125,26 @@ private static boolean isDrawByStalemate(Position position) { return true; } + /** + * 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, 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; + } + /** * Returns {@code true} if the given position is a draw by lack of mating material. */ diff --git a/src/main/scripts/history.html b/src/main/scripts/history.html index 55f0afa..4e69808 100644 --- a/src/main/scripts/history.html +++ b/src/main/scripts/history.html @@ -7,6 +7,13 @@

Version History

+

0.8.1 (2018-12-25)

+
    +
  • Implemented draw by threefold repetition.
  • +
  • Improved performance by replacing some lists with arrays.
  • +
  • Improved time management.
  • +
+

0.8.0 (2018-12-12)

  • Ported engine to Java 11.
  • diff --git a/src/test/java/se/dykstrom/ronja/common/model/PositionTest.java b/src/test/java/se/dykstrom/ronja/common/model/PositionTest.java index 191c110..24f3ea2 100644 --- a/src/test/java/se/dykstrom/ronja/common/model/PositionTest.java +++ b/src/test/java/se/dykstrom/ronja/common/model/PositionTest.java @@ -17,19 +17,15 @@ package se.dykstrom.ronja.common.model; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static se.dykstrom.ronja.common.model.Piece.BISHOP; -import static se.dykstrom.ronja.common.model.Piece.ROOK; - import org.junit.Test; - import se.dykstrom.ronja.common.parser.FenParser; import se.dykstrom.ronja.common.parser.MoveParser; import se.dykstrom.ronja.test.AbstractTestCase; +import static org.junit.Assert.*; +import static se.dykstrom.ronja.common.model.Piece.BISHOP; +import static se.dykstrom.ronja.common.model.Piece.ROOK; + /** * This class is for testing class {@code Position} using JUnit. * @@ -141,20 +137,20 @@ public void testEquals() throws Exception { assertEquals(p2, p1); p1 = p1.withMove(MoveParser.parse(e2e4, p1)); p2 = p2.withMove(MoveParser.parse(d2d4, p2)); - assertFalse(p1.equals(p2)); - assertFalse(p2.equals(p1)); + assertNotEquals(p1, p2); + assertNotEquals(p2, p1); p1 = p1.withMove(MoveParser.parse(e7e5, p1)); p2 = p2.withMove(MoveParser.parse(e7e5, p2)); - assertFalse(p1.equals(p2)); - assertFalse(p2.equals(p1)); + assertNotEquals(p1, p2); + assertNotEquals(p2, p1); p1 = p1.withMove(MoveParser.parse(d2d4, p1)); p2 = p2.withMove(MoveParser.parse(e2e4, p2)); // The positions look equal, but have different 'en passant' squares - assertFalse(p1.equals(p2)); - assertFalse(p2.equals(p1)); + assertNotEquals(p1, p2); + assertNotEquals(p2, p1); p1 = p1.withMove(MoveParser.parse(d7d5, p1)); p2 = p2.withMove(MoveParser.parse(d7d5, p2)); @@ -191,8 +187,8 @@ public void testEqualsCastlingRights() throws Exception { p2 = p2.withMove(MoveParser.parse(e2e4, p2)); p2 = p2.withMove(MoveParser.parse(e7e5, p2)); - assertFalse(p1.equals(p2)); - assertFalse(p2.equals(p1)); + assertNotEquals(p1, p2); + assertNotEquals(p2, p1); } @Test diff --git a/src/test/java/se/dykstrom/ronja/common/model/SquareTest.java b/src/test/java/se/dykstrom/ronja/common/model/SquareTest.java index 24a5c40..fee4d93 100644 --- a/src/test/java/se/dykstrom/ronja/common/model/SquareTest.java +++ b/src/test/java/se/dykstrom/ronja/common/model/SquareTest.java @@ -379,19 +379,6 @@ private void assertSquareIndices(String[] expectedNames, List actualInd assertArrayEquals(expected, actual); } - /** - * Asserts that the squares defined by the names in {@code expectedNames} are the same as - * the squares defined by the IDs in {@code actualIds}. - * - * @param expectedNames An array of the expected square names, e.g. ["a1", "e4"]. - * @param actualIds A list of the actual square IDs, e.g. [Square.A1, Square.E4]. - */ - private static void assertSquareIds(String[] expectedNames, List actualIds) { - long[] expected = Arrays.stream(expectedNames).mapToLong(Square::nameToId).sorted().toArray(); - long[] actual = actualIds.stream().mapToLong(n -> n).sorted().toArray(); - assertArrayEquals(expected, actual); - } - /** * Asserts that the squares defined by the names in {@code expectedNames} are the same as * the squares defined by the IDs in {@code actualIds}. diff --git a/src/test/java/se/dykstrom/ronja/common/parser/PgnParserTest.java b/src/test/java/se/dykstrom/ronja/common/parser/PgnParserTest.java index 221e942..91e2b90 100644 --- a/src/test/java/se/dykstrom/ronja/common/parser/PgnParserTest.java +++ b/src/test/java/se/dykstrom/ronja/common/parser/PgnParserTest.java @@ -51,7 +51,7 @@ private void setUpGame(String opponent, String result) throws IllegalMoveExcepti game.setEngineColor(Color.WHITE); game.setOpponent(opponent); game.setResult(result); - game.setMoves(toMoveList(MOVE_E4_C5_NF3)); + game.setMoves(parseMoves(MOVE_E4_C5_NF3)); } @Test diff --git a/src/test/java/se/dykstrom/ronja/common/utils/ArrayUtilsTest.java b/src/test/java/se/dykstrom/ronja/common/utils/ArrayUtilsTest.java deleted file mode 100644 index 91ef872..0000000 --- a/src/test/java/se/dykstrom/ronja/common/utils/ArrayUtilsTest.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2017 Johan Dykstrom - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package se.dykstrom.ronja.common.utils; - -import static org.junit.Assert.assertArrayEquals; - -import java.util.Arrays; -import java.util.Collections; - -import org.junit.Test; - -import se.dykstrom.ronja.test.AbstractTestCase; - -/** - * This class is for testing class {@code ArrayUtils} using JUnit. - * - * @author Johan Dykstrom - * @see ArrayUtils - */ -public class ArrayUtilsTest extends AbstractTestCase { - - @Test - public void shouldConvertEmptyList() { - assertArrayEquals(new int[0], ArrayUtils.toArray(Collections.emptyList())); - } - - @Test - public void shouldConvertList() { - assertArrayEquals(new int[] {1, 2, 3}, ArrayUtils.toArray(Arrays.asList(1, 2, 3))); - } -} diff --git a/src/test/java/se/dykstrom/ronja/engine/core/AlphaBetaFinderTest.java b/src/test/java/se/dykstrom/ronja/engine/core/AlphaBetaFinderTest.java index 550ee6a..c5d51ae 100644 --- a/src/test/java/se/dykstrom/ronja/engine/core/AlphaBetaFinderTest.java +++ b/src/test/java/se/dykstrom/ronja/engine/core/AlphaBetaFinderTest.java @@ -17,8 +17,9 @@ package se.dykstrom.ronja.engine.core; -import org.junit.Before; import org.junit.Test; +import se.dykstrom.ronja.common.book.OpeningBook; +import se.dykstrom.ronja.common.model.Game; import se.dykstrom.ronja.common.model.Move; import se.dykstrom.ronja.common.model.Square; import se.dykstrom.ronja.test.AbstractTestCase; @@ -40,13 +41,6 @@ public class AlphaBetaFinderTest extends AbstractTestCase { private static final int MAX_DEPTH = 3; - private final AlphaBetaFinder finder = new AlphaBetaFinder(); - - @Before - public void setUp() { - finder.setMaxDepth(MAX_DEPTH); - } - /** * Tests calling alphaBeta with depth = max depth. No moves are made, the given positions are just evaluated. */ @@ -214,6 +208,7 @@ public void testFindBestMoveWithinTime() throws Exception { * Calls findBestMoveWithinTime with the position specified by {@code fen} and the maximum search time. */ private int findBestMoveWithTime(String fen, int maxTime) throws ParseException { + AlphaBetaFinder finder = setupFinder(fen); return finder.findBestMoveWithinTime(parse(fen), maxTime); } @@ -221,6 +216,7 @@ private int findBestMoveWithTime(String fen, int maxTime) throws ParseException * Calls findBestMove with the position specified by {@code fen} and the maximum search depth. */ private int findBestMoveWithDepth(String fen) throws ParseException { + AlphaBetaFinder finder = setupFinder(fen); return finder.findBestMove(parse(fen), MAX_DEPTH); } @@ -228,6 +224,15 @@ private int findBestMoveWithDepth(String fen) throws ParseException { * Calls the alphaBeta method with the position specified by {@code fen} and the given depth. */ private int alphaBeta(String fen, int maxDepth) throws ParseException { + AlphaBetaFinder finder = setupFinder(fen); return finder.alphaBeta(parse(fen), 0, maxDepth, AlphaBetaFinder.ALPHA_START, AlphaBetaFinder.BETA_START); } + + private AlphaBetaFinder setupFinder(String fen) throws ParseException { + Game game = new Game(OpeningBook.DEFAULT); + game.setPosition(parse(fen)); + AlphaBetaFinder finder = new AlphaBetaFinder(game); + finder.setMaxDepth(MAX_DEPTH); + return finder; + } } diff --git a/src/test/java/se/dykstrom/ronja/engine/core/SlowFinderTest.java b/src/test/java/se/dykstrom/ronja/engine/core/SlowFinderTest.java index 5194919..b0089b3 100644 --- a/src/test/java/se/dykstrom/ronja/engine/core/SlowFinderTest.java +++ b/src/test/java/se/dykstrom/ronja/engine/core/SlowFinderTest.java @@ -19,10 +19,11 @@ import org.junit.Ignore; import org.junit.Test; +import se.dykstrom.ronja.common.book.OpeningBook; +import se.dykstrom.ronja.common.model.Game; import se.dykstrom.ronja.common.model.Move; import se.dykstrom.ronja.common.model.Piece; import se.dykstrom.ronja.common.model.Square; -import se.dykstrom.ronja.common.parser.FenParser; import se.dykstrom.ronja.test.AbstractTestCase; import java.text.ParseException; @@ -30,6 +31,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static se.dykstrom.ronja.common.model.Piece.*; +import static se.dykstrom.ronja.common.parser.FenParser.parse; /** * This class is for testing the different {@code Finder} classes using JUnit. @@ -40,8 +42,6 @@ @Ignore public class SlowFinderTest extends AbstractTestCase { - private final Finder finder = new AlphaBetaFinder(); - /** * Tests calling findBestMove with positions that result in mate in four moves. */ @@ -120,42 +120,51 @@ public void testFindBestMove_OpeningFive() throws Exception { */ @Test public void testFindBestMove_Profiler() throws Exception { - int waitTime = 15000; + var waitTime = 15000; System.out.println("Waiting " + (waitTime / 1000) + " seconds..."); Thread.sleep(waitTime); System.out.println("Starting test..."); - long start = System.currentTimeMillis(); + var start = System.currentTimeMillis(); assertNotEquals(0, findBestMoveWithDepth(FEN_MIDDLE_GAME_0, 6)); - System.out.println("Finished step 1"); + System.out.println("Finished step 0 after " + elapsedTime(start) + " seconds"); assertNotEquals(0, findBestMoveWithDepth(FEN_MIDDLE_GAME_1, 6)); - System.out.println("Finished step 2"); + System.out.println("Finished step 1 after " + elapsedTime(start) + " seconds"); assertNotEquals(0, findBestMoveWithDepth(FEN_MIDDLE_GAME_2, 6)); - System.out.println("Finished step 3"); - long stop = System.currentTimeMillis(); - System.out.println("Finished test after " + ((stop - start) / 1000.0) + " seconds"); + System.out.println("Finished step 2 after " + elapsedTime(start) + " seconds"); + System.out.println("Finished test after " + elapsedTime(start) + " seconds"); } /** * Calls findBestMove with the position specified by {@code fen} and the given depth. */ private int findBestMoveWithDepth(String fen, int maxDepth) throws ParseException { - return finder.findBestMove(FenParser.parse(fen), maxDepth); + var game = new Game(OpeningBook.DEFAULT); + game.setPosition(parse(fen)); + var finder = new AlphaBetaFinder(game); + return finder.findBestMove(parse(fen), maxDepth); } public static void main(String[] args) throws Exception { - SlowFinderTest test = new SlowFinderTest(); - int waitTime = 1000; + var test = new SlowFinderTest(); + var waitTime = 1000; System.out.println("Waiting " + (waitTime / 1000) + " seconds..."); Thread.sleep(waitTime); System.out.println("Starting test..."); - long start = System.currentTimeMillis(); + var start = System.currentTimeMillis(); System.out.println("Best move: " + test.findBestMoveWithDepth(FEN_MIDDLE_GAME_0, 6)); - System.out.println("Finished step 1"); - System.out.println("Best move: " + test.findBestMoveWithDepth(FEN_MIDDLE_GAME_1, 6)); - System.out.println("Finished step 2"); + System.out.println("Finished step 0 after " + elapsedTime(start) + " seconds"); + System.out.println("Best move: " + test.findBestMoveWithDepth(FEN_MIDDLE_GAME_1, 7)); + System.out.println("Finished step 1 after " + elapsedTime(start) + " seconds"); System.out.println("Best move: " + test.findBestMoveWithDepth(FEN_MIDDLE_GAME_2, 6)); - System.out.println("Finished step 3"); - long stop = System.currentTimeMillis(); - System.out.println("Finished test after " + ((stop - start) / 1000.0) + " seconds"); + System.out.println("Finished step 2 after " + elapsedTime(start) + " seconds"); + System.out.println("Best move: " + test.findBestMoveWithDepth(FEN_MIDDLE_GAME_3, 7)); + System.out.println("Finished step 3 after " + elapsedTime(start) + " seconds"); + System.out.println("Best move: " + test.findBestMoveWithDepth(FEN_MIDDLE_GAME_4, 7)); + System.out.println("Finished step 4 after " + elapsedTime(start) + " seconds"); + System.out.println("Finished test after " + elapsedTime(start) + " seconds"); + } + + private static double elapsedTime(long start) { + return (System.currentTimeMillis() - start) / 1000.0; } } 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/ui/command/CommandTest.java b/src/test/java/se/dykstrom/ronja/engine/ui/command/CommandTest.java index 9d5fe6d..96719b2 100644 --- a/src/test/java/se/dykstrom/ronja/engine/ui/command/CommandTest.java +++ b/src/test/java/se/dykstrom/ronja/engine/ui/command/CommandTest.java @@ -75,7 +75,7 @@ public void testBkCommand() { } @Test - public void testBkCommand_NoMoves() throws Exception { + public void testBkCommand_NoMoves() { game.makeMove(Move.create(Piece.PAWN, Square.A2, Square.A4)); ListResponse response = new ListResponse(); Command command = new BkCommand(null, response, game); 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 a2c9f98..d575d5c 100644 --- a/src/test/java/se/dykstrom/ronja/engine/utils/PositionUtilsTest.java +++ b/src/test/java/se/dykstrom/ronja/engine/utils/PositionUtilsTest.java @@ -18,11 +18,13 @@ package se.dykstrom.ronja.engine.utils; import org.junit.Test; +import se.dykstrom.ronja.common.book.OpeningBook; +import se.dykstrom.ronja.common.model.Game; +import se.dykstrom.ronja.common.model.Position; import se.dykstrom.ronja.common.parser.FenParser; import se.dykstrom.ronja.test.AbstractTestCase; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; /** * This class is for testing class {@code PositionUtils} using JUnit. @@ -32,25 +34,57 @@ */ 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 + public void testIsDrawByThreefoldRepetition() { + // Set up game + Game game = new Game(OpeningBook.DEFAULT); + Position position = game.getPosition(); + + // Make moves to repeat position + game.makeMove(MOVE_G1F3); + assertNull(PositionUtils.getDrawType(game.getPosition(), game)); + game.makeMove(MOVE_G8F6); + assertNull(PositionUtils.getDrawType(game.getPosition(), game)); + game.makeMove(MOVE_F3G1); + assertNull(PositionUtils.getDrawType(game.getPosition(), game)); + game.makeMove(MOVE_F6G8); + assertNull(PositionUtils.getDrawType(game.getPosition(), game)); + + game.makeMove(MOVE_G1F3); + assertNull(PositionUtils.getDrawType(game.getPosition(), game)); + game.makeMove(MOVE_G8F6); + assertNull(PositionUtils.getDrawType(game.getPosition(), game)); + game.makeMove(MOVE_F3G1); + assertNull(PositionUtils.getDrawType(game.getPosition(), game)); + game.makeMove(MOVE_F6G8); + + // Check for draw + assertEquals("Threefold repetition", PositionUtils.getDrawType(position, game)); } @Test diff --git a/src/test/java/se/dykstrom/ronja/test/AbstractTestCase.java b/src/test/java/se/dykstrom/ronja/test/AbstractTestCase.java index 2f34bb8..d3f66c3 100644 --- a/src/test/java/se/dykstrom/ronja/test/AbstractTestCase.java +++ b/src/test/java/se/dykstrom/ronja/test/AbstractTestCase.java @@ -23,7 +23,6 @@ import se.dykstrom.ronja.common.parser.MoveParser; import se.dykstrom.ronja.engine.core.FullMoveGenerator; -import java.util.ArrayList; import java.util.List; import static java.util.Arrays.asList; @@ -44,6 +43,9 @@ public abstract class AbstractTestCase { protected static final int MOVE_E7E6 = Move.create(Piece.PAWN, Square.E7, Square.E6); protected static final int MOVE_C7C5 = Move.create(Piece.PAWN, Square.C7, Square.C5); protected static final int MOVE_G1F3 = Move.create(Piece.KNIGHT, Square.G1, Square.F3); + protected static final int MOVE_F3G1 = Move.create(Piece.KNIGHT, Square.F3, Square.G1); + protected static final int MOVE_G8F6 = Move.create(Piece.KNIGHT, Square.G8, Square.F6); + protected static final int MOVE_F6G8 = Move.create(Piece.KNIGHT, Square.F6, Square.G8); protected static final int MOVE_E1G1 = Move.createCastling(Square.E1, Square.G1); protected static final int MOVE_E1C1 = Move.createCastling(Square.E1, Square.C1); protected static final int MOVE_E8G8 = Move.createCastling(Square.E8, Square.G8); @@ -118,10 +120,14 @@ public abstract class AbstractTestCase { // Middle-game positions protected static final String FEN_MIDDLE_GAME_0 = "r3kb1r/pbpnqppp/1p2pn2/3p2B1/2PP4/2N1PN2/PPQ2PPP/2KR1B1R b kq - 3 8"; - /* Danielsen-Gunnarsson, 2006 */ + // Danielsen-Gunnarsson, 2006 protected static final String FEN_MIDDLE_GAME_1 = "R4rk1/1bq2pbp/2n2np1/1pp1p3/2N1PP2/2P2NPP/1P4B1/2B1QRK1 b - - 0 16"; - /** treeless_druid-volhouder, 2015 */ + // treeless_druid-volhouder, 2015 protected static final String FEN_MIDDLE_GAME_2 = "rn3qk1/pp4pb/1b3p1p/3p4/4nP2/1P2PNP1/PB1P2BP/2RQ1RK1 b - - 0 1"; + // Anand-Carlsen, 2013 + protected static final String FEN_MIDDLE_GAME_3 = "r3r1k1/2pn1pp1/1b1pqn1p/1p2p3/4P1NB/2PPN2P/1P3PP1/R2QR1K1 w - - 2 21"; + // Carlsen-Anand, 2013 + protected static final String FEN_MIDDLE_GAME_4 = "3qn1k1/1p1r1pp1/p1rPp2p/P7/2PR1P2/1P1R3P/3QN1P1/6K1 b - - 2 32"; // End-game positions protected static final String FEN_END_GAME_0 = "5rn1/2pq1Bk1/3p1b1N/1p3P1Q/1P6/6P1/5P2/4R1K1 b - - 0 36"; @@ -223,20 +229,22 @@ protected static void assertGeneratedMoves(FullMoveGenerator moveGenerator, int. } /** - * Converts an array of moves in string format to a list of real moves. + * Parses an array of move strings and returns an array of actual moves. * This method assumes that the moves are made from the start position. * * @param moves An array move moves in string format to convert. - * @return The list of converted moves. + * @return The array of converted moves. * @throws IllegalMoveException If there was an illegal move. */ - protected static List toMoveList(String[] moves) throws IllegalMoveException { - Position position = Position.START; - List result = new ArrayList<>(moves.length); - for (String move : moves) { - result.add(MoveParser.parse(move, position)); - position = position.withMove(result.get(result.size() - 1)); + protected static int[] parseMoves(String[] moves) throws IllegalMoveException { + var result = new int[moves.length]; + var position = Position.START; + + for (int i = 0; i < moves.length; i++) { + result[i] = MoveParser.parse(moves[i], position); + position = position.withMove(result[i]); } + return result; } }