mirror of
https://github.com/peterosterlund2/droidfish.git
synced 2025-04-16 09:02:45 +02:00
436 lines
15 KiB
Java
436 lines
15 KiB
Java
/*
|
|
CuckooChess - A java chess program.
|
|
Copyright (C) 2011 Peter Österlund, peterosterlund2@gmail.com
|
|
|
|
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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
package uci;
|
|
|
|
import chess.Book;
|
|
import chess.ComputerPlayer;
|
|
import chess.Move;
|
|
import chess.MoveGen;
|
|
import chess.Parameters;
|
|
import chess.Piece;
|
|
import chess.Position;
|
|
import chess.Search;
|
|
import chess.TextIO;
|
|
import chess.TranspositionTable;
|
|
import chess.Parameters.CheckParam;
|
|
import chess.Parameters.ComboParam;
|
|
import chess.Parameters.ParamBase;
|
|
import chess.Parameters.SpinParam;
|
|
import chess.Parameters.StringParam;
|
|
import chess.TranspositionTable.TTEntry;
|
|
import chess.UndoInfo;
|
|
import java.io.PrintStream;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.List;
|
|
import java.util.Random;
|
|
|
|
/**
|
|
* Control the search thread.
|
|
* @author petero
|
|
*/
|
|
public class EngineControl {
|
|
PrintStream os;
|
|
|
|
Thread engineThread;
|
|
private final Object threadMutex;
|
|
Search sc;
|
|
TranspositionTable tt;
|
|
MoveGen moveGen;
|
|
|
|
Position pos;
|
|
long[] posHashList;
|
|
int posHashListSize;
|
|
boolean ponder; // True if currently doing pondering
|
|
boolean onePossibleMove;
|
|
boolean infinite;
|
|
|
|
int minTimeLimit;
|
|
int maxTimeLimit;
|
|
int maxDepth;
|
|
int maxNodes;
|
|
List<Move> searchMoves;
|
|
|
|
// Options
|
|
int hashSizeMB = 16;
|
|
boolean ownBook = false;
|
|
boolean analyseMode = false;
|
|
boolean ponderMode = true;
|
|
|
|
// Reduced strength variables
|
|
int strength = 1000;
|
|
long randomSeed = 0;
|
|
|
|
/**
|
|
* This class is responsible for sending "info" strings during search.
|
|
*/
|
|
private static class SearchListener implements Search.Listener {
|
|
PrintStream os;
|
|
|
|
SearchListener(PrintStream os) {
|
|
this.os = os;
|
|
}
|
|
|
|
public void notifyDepth(int depth) {
|
|
os.printf("info depth %d%n", depth);
|
|
}
|
|
|
|
public void notifyCurrMove(Move m, int moveNr) {
|
|
os.printf("info currmove %s currmovenumber %d%n", moveToString(m), moveNr);
|
|
}
|
|
|
|
public void notifyPV(int depth, int score, int time, long nodes, int nps, boolean isMate,
|
|
boolean upperBound, boolean lowerBound, ArrayList<Move> pv) {
|
|
StringBuilder pvBuf = new StringBuilder();
|
|
for (Move m : pv) {
|
|
pvBuf.append(" ");
|
|
pvBuf.append(moveToString(m));
|
|
}
|
|
String bound = "";
|
|
if (upperBound) {
|
|
bound = " upperbound";
|
|
} else if (lowerBound) {
|
|
bound = " lowerbound";
|
|
}
|
|
os.printf("info depth %d score %s %d%s time %d nodes %d nps %d pv%s%n",
|
|
depth, isMate ? "mate" : "cp", score, bound, time, nodes, nps, pvBuf.toString());
|
|
}
|
|
|
|
public void notifyStats(long nodes, int nps, int time) {
|
|
os.printf("info nodes %d nps %d time %d%n", nodes, nps, time);
|
|
}
|
|
}
|
|
|
|
public EngineControl(PrintStream os) {
|
|
this.os = os;
|
|
threadMutex = new Object();
|
|
setupTT();
|
|
moveGen = new MoveGen();
|
|
}
|
|
|
|
final public void startSearch(Position pos, ArrayList<Move> moves, SearchParams sPar) {
|
|
setupPosition(new Position(pos), moves);
|
|
computeTimeLimit(sPar);
|
|
ponder = false;
|
|
infinite = (maxTimeLimit < 0) && (maxDepth < 0) && (maxNodes < 0);
|
|
startThread(minTimeLimit, maxTimeLimit, maxDepth, maxNodes);
|
|
searchMoves = sPar.searchMoves;
|
|
}
|
|
|
|
final public void startPonder(Position pos, List<Move> moves, SearchParams sPar) {
|
|
setupPosition(new Position(pos), moves);
|
|
computeTimeLimit(sPar);
|
|
ponder = true;
|
|
infinite = false;
|
|
startThread(-1, -1, -1, -1);
|
|
}
|
|
|
|
final public void ponderHit() {
|
|
Search mySearch;
|
|
synchronized (threadMutex) {
|
|
mySearch = sc;
|
|
}
|
|
if (mySearch != null) {
|
|
if (onePossibleMove) {
|
|
if (minTimeLimit > 1) minTimeLimit = 1;
|
|
if (maxTimeLimit > 1) maxTimeLimit = 1;
|
|
}
|
|
mySearch.timeLimit(minTimeLimit, maxTimeLimit);
|
|
}
|
|
infinite = (maxTimeLimit < 0) && (maxDepth < 0) && (maxNodes < 0);
|
|
ponder = false;
|
|
}
|
|
|
|
final public void stopSearch() {
|
|
stopThread();
|
|
}
|
|
|
|
final public void newGame() {
|
|
randomSeed = new Random().nextLong();
|
|
tt.clear();
|
|
}
|
|
|
|
/**
|
|
* Compute thinking time for current search.
|
|
*/
|
|
final public void computeTimeLimit(SearchParams sPar) {
|
|
minTimeLimit = -1;
|
|
maxTimeLimit = -1;
|
|
maxDepth = -1;
|
|
maxNodes = -1;
|
|
if (sPar.infinite) {
|
|
minTimeLimit = -1;
|
|
maxTimeLimit = -1;
|
|
maxDepth = -1;
|
|
} else if (sPar.depth > 0) {
|
|
maxDepth = sPar.depth;
|
|
} else if (sPar.mate > 0) {
|
|
maxDepth = sPar.mate * 2 - 1;
|
|
} else if (sPar.moveTime > 0) {
|
|
minTimeLimit = maxTimeLimit = sPar.moveTime;
|
|
} else if (sPar.nodes > 0) {
|
|
maxNodes = sPar.nodes;
|
|
} else {
|
|
int moves = sPar.movesToGo;
|
|
if (moves == 0) {
|
|
moves = 999;
|
|
}
|
|
moves = Math.min(moves, 45); // Assume 45 more moves until end of game
|
|
if (ponderMode) {
|
|
final double ponderHitRate = 0.35;
|
|
moves = (int)Math.ceil(moves * (1 - ponderHitRate));
|
|
}
|
|
boolean white = pos.whiteMove;
|
|
int time = white ? sPar.wTime : sPar.bTime;
|
|
int inc = white ? sPar.wInc : sPar.bInc;
|
|
final int margin = Math.min(1000, time * 9 / 10);
|
|
int timeLimit = (time + inc * (moves - 1) - margin) / moves;
|
|
minTimeLimit = (int)(timeLimit * 0.85);
|
|
maxTimeLimit = (int)(minTimeLimit * (Math.max(2.5, Math.min(4.0, moves / 2))));
|
|
|
|
// Leave at least 1s on the clock, but can't use negative time
|
|
minTimeLimit = clamp(minTimeLimit, 1, time - margin);
|
|
maxTimeLimit = clamp(maxTimeLimit, 1, time - margin);
|
|
}
|
|
}
|
|
|
|
private static final int clamp(int val, int min, int max) {
|
|
if (val < min) {
|
|
return min;
|
|
} else if (val > max) {
|
|
return max;
|
|
} else {
|
|
return val;
|
|
}
|
|
}
|
|
|
|
final private void startThread(final int minTimeLimit, final int maxTimeLimit,
|
|
int maxDepth, final int maxNodes) {
|
|
synchronized (threadMutex) {} // Must not start new search until old search is finished
|
|
sc = new Search(pos, posHashList, posHashListSize, tt);
|
|
sc.timeLimit(minTimeLimit, maxTimeLimit);
|
|
sc.setListener(new SearchListener(os));
|
|
sc.setStrength(strength, randomSeed);
|
|
MoveGen.MoveList moves = moveGen.pseudoLegalMoves(pos);
|
|
MoveGen.removeIllegal(pos, moves);
|
|
if ((searchMoves != null) && (searchMoves.size() > 0)) {
|
|
Arrays.asList(moves.m).retainAll(searchMoves);
|
|
}
|
|
final MoveGen.MoveList srchMoves = moves;
|
|
onePossibleMove = false;
|
|
if ((srchMoves.size < 2) && !infinite) {
|
|
onePossibleMove = true;
|
|
if (!ponder) {
|
|
if ((maxDepth < 0) || (maxDepth > 2)) maxDepth = 2;
|
|
}
|
|
}
|
|
tt.nextGeneration();
|
|
final int srchmaxDepth = maxDepth;
|
|
engineThread = new Thread(new Runnable() {
|
|
public void run() {
|
|
Move m = null;
|
|
if (ownBook && !analyseMode) {
|
|
Book book = new Book(false);
|
|
m = book.getBookMove(pos);
|
|
}
|
|
if (m == null) {
|
|
m = sc.iterativeDeepening(srchMoves, srchmaxDepth, maxNodes, false);
|
|
}
|
|
while (ponder || infinite) {
|
|
// We should not respond until told to do so. Just wait until
|
|
// we are allowed to respond.
|
|
try {
|
|
Thread.sleep(10);
|
|
} catch (InterruptedException ex) {
|
|
break;
|
|
}
|
|
}
|
|
Move ponderMove = getPonderMove(pos, m);
|
|
synchronized (threadMutex) {
|
|
if (ponderMove != null) {
|
|
os.printf("bestmove %s ponder %s%n", moveToString(m), moveToString(ponderMove));
|
|
} else {
|
|
os.printf("bestmove %s%n", moveToString(m));
|
|
}
|
|
engineThread = null;
|
|
sc = null;
|
|
}
|
|
}
|
|
});
|
|
engineThread.start();
|
|
}
|
|
|
|
private final void stopThread() {
|
|
Thread myThread;
|
|
Search mySearch;
|
|
synchronized (threadMutex) {
|
|
myThread = engineThread;
|
|
mySearch = sc;
|
|
}
|
|
if (myThread != null) {
|
|
mySearch.timeLimit(0, 0);
|
|
infinite = false;
|
|
ponder = false;
|
|
try {
|
|
myThread.join();
|
|
} catch (InterruptedException ex) {
|
|
throw new RuntimeException();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
private final void setupTT() {
|
|
int nEntries = hashSizeMB > 0 ? hashSizeMB * (1 << 20) / 24 : 1024;
|
|
int logSize = (int) Math.floor(Math.log(nEntries) / Math.log(2));
|
|
tt = new TranspositionTable(logSize);
|
|
}
|
|
|
|
private final void setupPosition(Position pos, List<Move> moves) {
|
|
UndoInfo ui = new UndoInfo();
|
|
posHashList = new long[200 + moves.size()];
|
|
posHashListSize = 0;
|
|
for (Move m : moves) {
|
|
posHashList[posHashListSize++] = pos.zobristHash();
|
|
pos.makeMove(m, ui);
|
|
}
|
|
this.pos = pos;
|
|
}
|
|
|
|
/**
|
|
* Try to find a move to ponder from the transposition table.
|
|
*/
|
|
private final Move getPonderMove(Position pos, Move m) {
|
|
if (m == null) return null;
|
|
Move ret = null;
|
|
UndoInfo ui = new UndoInfo();
|
|
pos.makeMove(m, ui);
|
|
TTEntry ent = tt.probe(pos.historyHash());
|
|
if (ent.type != TTEntry.T_EMPTY) {
|
|
ret = new Move(0, 0, 0);
|
|
ent.getMove(ret);
|
|
MoveGen.MoveList moves = moveGen.pseudoLegalMoves(pos);
|
|
MoveGen.removeIllegal(pos, moves);
|
|
boolean contains = false;
|
|
for (int mi = 0; mi < moves.size; mi++)
|
|
if (moves.m[mi].equals(ret)) {
|
|
contains = true;
|
|
break;
|
|
}
|
|
if (!contains)
|
|
ret = null;
|
|
}
|
|
pos.unMakeMove(m, ui);
|
|
return ret;
|
|
}
|
|
|
|
private static final String moveToString(Move m) {
|
|
if (m == null)
|
|
return "0000";
|
|
String ret = TextIO.squareToString(m.from);
|
|
ret += TextIO.squareToString(m.to);
|
|
switch (m.promoteTo) {
|
|
case Piece.WQUEEN:
|
|
case Piece.BQUEEN:
|
|
ret += "q";
|
|
break;
|
|
case Piece.WROOK:
|
|
case Piece.BROOK:
|
|
ret += "r";
|
|
break;
|
|
case Piece.WBISHOP:
|
|
case Piece.BBISHOP:
|
|
ret += "b";
|
|
break;
|
|
case Piece.WKNIGHT:
|
|
case Piece.BKNIGHT:
|
|
ret += "n";
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
static void printOptions(PrintStream os) {
|
|
os.printf("option name Hash type spin default 16 min 1 max 2048%n");
|
|
os.printf("option name OwnBook type check default false%n");
|
|
os.printf("option name Ponder type check default true%n");
|
|
os.printf("option name UCI_AnalyseMode type check default false%n");
|
|
os.printf("option name UCI_EngineAbout type string default %s by Peter Osterlund, see http://web.comhem.se/petero2home/javachess/index.html%n",
|
|
ComputerPlayer.engineName);
|
|
os.printf("option name Strength type spin default 1000 min 0 max 1000\n");
|
|
|
|
for (String pName : Parameters.instance().getParamNames()) {
|
|
ParamBase p = Parameters.instance().getParam(pName);
|
|
switch (p.type) {
|
|
case CHECK: {
|
|
CheckParam cp = (CheckParam)p;
|
|
os.printf("optionn name %s type check default %s\n",
|
|
p.name, cp.defaultValue?"true":"false");
|
|
break;
|
|
}
|
|
case SPIN: {
|
|
SpinParam sp = (SpinParam)p;
|
|
os.printf("option name %s type spin default %d min %d max %d\n",
|
|
p.name, sp.defaultValue, sp.minValue, sp.maxValue);
|
|
break;
|
|
}
|
|
case COMBO: {
|
|
ComboParam cp = (ComboParam)p;
|
|
os.printf("option name %s type combo default %s ", cp.name, cp.defaultValue);
|
|
for (String s : cp.allowedValues)
|
|
os.printf(" var %s", s);
|
|
os.printf("\n");
|
|
break;
|
|
}
|
|
case BUTTON:
|
|
os.printf("option name %s type button\n", p.name);
|
|
break;
|
|
case STRING: {
|
|
StringParam sp = (StringParam)p;
|
|
os.printf("option name %s type string default %s\n",
|
|
p.name, sp.defaultValue);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
final void setOption(String optionName, String optionValue) {
|
|
try {
|
|
if (optionName.equals("hash")) {
|
|
hashSizeMB = Integer.parseInt(optionValue);
|
|
setupTT();
|
|
} else if (optionName.equals("ownbook")) {
|
|
ownBook = Boolean.parseBoolean(optionValue);
|
|
} else if (optionName.equals("ponder")) {
|
|
ponderMode = Boolean.parseBoolean(optionValue);
|
|
} else if (optionName.equals("uci_analysemode")) {
|
|
analyseMode = Boolean.parseBoolean(optionValue);
|
|
} else if (optionName.equals("strength")) {
|
|
strength = Integer.parseInt(optionValue);
|
|
} else {
|
|
Parameters.instance().set(optionName, optionValue);
|
|
}
|
|
} catch (NumberFormatException nfe) {
|
|
}
|
|
}
|
|
}
|