Skip to content

Commit

Permalink
Merge pull request #10 from dykstrom/feature/draw-by-repetition
Browse files Browse the repository at this point in the history
Feature/draw by repetition
  • Loading branch information
dykstrom authored Dec 25, 2018
2 parents c5cd2ac + 60ed285 commit 02222c5
Show file tree
Hide file tree
Showing 25 changed files with 306 additions and 269 deletions.
75 changes: 47 additions & 28 deletions src/main/java/se/dykstrom/ronja/common/model/Game.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

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

Expand All @@ -49,13 +50,22 @@ public class Game {
private Color engineColor;

/** All moves made in this game. */
private List<Integer> 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;
Expand Down Expand Up @@ -95,7 +105,6 @@ public void reset() {
setForceMode(false);
setPosition(Position.START);
setEngineColor(Color.BLACK);
setMoves(new ArrayList<>());
setOpponent(null);
setResult("*");
setStartTime(LocalDateTime.now());
Expand All @@ -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];
}

/**
Expand Down Expand Up @@ -162,17 +181,18 @@ public Color getEngineColor() {
}

/**
* Sets the list of moves.
* Sets the array of historical moves.
*/
public void setMoves(List<Integer> 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<Integer> getMoves() {
return moves;
public int[] getMoves() {
return Arrays.copyOf(moves, moveIndex);
}

/**
Expand All @@ -192,14 +212,20 @@ 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.
*/
public void setPosition(Position position) {
this.position = position;
this.startPosition = position;
this.startMoveNumber = position.getFullMoveNumber();

positions[0] = position;
positionIndex = 1;

moveIndex = 0;
}

/**
Expand Down Expand Up @@ -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.
*/
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
4 changes: 1 addition & 3 deletions src/main/java/se/dykstrom/ronja/common/parser/PgnParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -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).
*
Expand Down Expand Up @@ -95,7 +93,7 @@ private static String getMoves(Game game) {
StringBuilder line = new StringBuilder();

int moveNumber = game.getStartMoveNumber();
Iterator<String> iterator = SanParser.format(game.getStartPosition(), toArray(game.getMoves())).iterator();
Iterator<String> 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()));
Expand Down
37 changes: 0 additions & 37 deletions src/main/java/se/dykstrom/ronja/common/utils/ArrayUtils.java

This file was deleted.

46 changes: 26 additions & 20 deletions src/main/java/se/dykstrom/ronja/engine/core/AlphaBetaFinder.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand All @@ -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;
}
Expand All @@ -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;
}

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.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();
}
}

Expand Down
Loading

0 comments on commit 02222c5

Please sign in to comment.