DroidFish: Implemented DTZ/WDL tablebase hints in the GUI when only syzygy tablebases are available.

This commit is contained in:
Peter Osterlund
2014-10-12 00:21:10 +00:00
parent 0545004d3a
commit aa95d57e7b
33 changed files with 5312 additions and 97 deletions

View File

@@ -26,6 +26,7 @@ import org.petero.droidfish.gamelogic.Move;
import org.petero.droidfish.gamelogic.Piece;
import org.petero.droidfish.gamelogic.Position;
import org.petero.droidfish.gamelogic.UndoInfo;
import org.petero.droidfish.gtb.ProbeResult;
import android.content.Context;
import android.graphics.Canvas;
@@ -57,22 +58,17 @@ public abstract class ChessBoard extends View {
List<Move> moveHints;
/** Decoration for a square. Currently the only possible decoration is a number. */
/** Decoration for a square. Currently the only possible decoration is a tablebase probe result. */
public final static class SquareDecoration implements Comparable<SquareDecoration> {
int sq;
int number;
public SquareDecoration(int sq, int number) {
ProbeResult tbData;
public SquareDecoration(int sq, ProbeResult tbData) {
this.sq = sq;
this.number = number;
this.tbData = tbData;
}
@Override
public int compareTo(SquareDecoration another) {
int M0 = 100000;
int n = number;
int s1 = (n > 0) ? M0 - n : ((n == 0) ? 0 : -M0-n);
n = another.number;
int s2 = (n > 0) ? M0 - n : ((n == 0) ? 0 : -M0-n);
return s2 - s1;
return tbData.compareTo(another.tbData);
}
}
private ArrayList<SquareDecoration> decorations;
@@ -681,20 +677,42 @@ public abstract class ChessBoard extends View {
int xCrd = getXCrd(Position.getX(sq));
int yCrd = getYCrd(Position.getY(sq));
int num = sd.number;
String s;
if (num > 0)
s = "+" + String.valueOf(num);
else if (num < 0)
s = String.valueOf(num);
else
s = "0";
Rect bounds = new Rect();
decorationPaint.getTextBounds(s, 0, s.length(), bounds);
xCrd += (sqSize - (bounds.left + bounds.right)) / 2;
yCrd += (sqSize - (bounds.top + bounds.bottom)) / 2;
canvas.drawText(s, xCrd, yCrd, decorationPaint);
String s = null;
int wdl = sd.tbData.wdl;
int num = (sd.tbData.score + 1) / 2;
switch (sd.tbData.type) {
case DTM:
if (wdl > 0)
s = "+" + String.valueOf(num);
else if (wdl < 0)
s = "-" + String.valueOf(num);
else
s = "0";
break;
case DTZ:
if (wdl > 0)
s = "W" + String.valueOf(num);
else if (wdl < 0)
s = "L" + String.valueOf(num);
else
s = "0";
break;
case WDL:
if (wdl > 0)
s = "W";
else if (wdl < 0)
s = "L";
else
s = "0";
break;
}
if (s != null) {
Rect bounds = new Rect();
decorationPaint.getTextBounds(s, 0, s.length(), bounds);
xCrd += (sqSize - (bounds.left + bounds.right)) / 2;
yCrd += (sqSize - (bounds.top + bounds.bottom)) / 2;
canvas.drawText(s, xCrd, yCrd, decorationPaint);
}
}
}

View File

@@ -58,6 +58,7 @@ import org.petero.droidfish.gamelogic.PgnToken;
import org.petero.droidfish.gamelogic.GameTree.Node;
import org.petero.droidfish.gamelogic.TimeControlData;
import org.petero.droidfish.gtb.Probe;
import org.petero.droidfish.gtb.ProbeResult;
import com.kalab.chess.enginesupport.ChessEngine;
import com.kalab.chess.enginesupport.ChessEngineResolver;
@@ -147,7 +148,6 @@ public class DroidFish extends Activity implements GUIInterface {
// FIXME!!! Computer clock should stop if phone turned off (computer stops thinking if unplugged)
// FIXME!!! Add support for "no time control" and "hour-glass time control" as defined by the PGN standard
// FIXME!!! Online play on FICS
// FIXME!!! Add chess960 support
// FIXME!!! Implement "hint" feature
@@ -1234,7 +1234,8 @@ public class DroidFish extends Activity implements GUIInterface {
private final void setEngineOptions(boolean restart) {
computeNetEngineID();
ctrl.setEngineOptions(new EngineOptions(engineOptions), restart);
Probe.getInstance().setPath(engineOptions.gtbPath, egtbForceReload);
Probe.getInstance().setPath(engineOptions.gtbPath, engineOptions.rtbPath,
egtbForceReload);
egtbForceReload = false;
}
@@ -1259,14 +1260,14 @@ public class DroidFish extends Activity implements GUIInterface {
}
Probe gtbProbe = Probe.getInstance();
ArrayList<Pair<Integer, Integer>> x = gtbProbe.movePieceProbe(cb.pos, sq);
ArrayList<Pair<Integer,ProbeResult>> x = gtbProbe.movePieceProbe(cb.pos, sq);
if (x == null) {
cb.setSquareDecorations(null);
return;
}
ArrayList<SquareDecoration> sd = new ArrayList<SquareDecoration>();
for (Pair<Integer,Integer> p : x)
for (Pair<Integer,ProbeResult> p : x)
sd.add(new SquareDecoration(p.first, p.second));
cb.setSquareDecorations(sd);
}

View File

@@ -35,6 +35,7 @@ import org.petero.droidfish.gamelogic.Piece;
import org.petero.droidfish.gamelogic.Position;
import org.petero.droidfish.gamelogic.TextIO;
import org.petero.droidfish.gtb.Probe;
import org.petero.droidfish.gtb.ProbeResult;
import android.annotation.SuppressLint;
import android.app.Activity;
@@ -256,14 +257,14 @@ public class EditBoard extends Activity {
}
Probe gtbProbe = Probe.getInstance();
ArrayList<Pair<Integer, Integer>> x = gtbProbe.relocatePieceProbe(cb.pos, sq);
ArrayList<Pair<Integer,ProbeResult>> x = gtbProbe.relocatePieceProbe(cb.pos, sq);
if (x == null) {
cb.setSquareDecorations(null);
return;
}
ArrayList<SquareDecoration> sd = new ArrayList<SquareDecoration>();
for (Pair<Integer,Integer> p : x)
for (Pair<Integer,ProbeResult> p : x)
sd.add(new SquareDecoration(p.first, p.second));
cb.setSquareDecorations(sd);
}

View File

@@ -214,15 +214,21 @@ public class Position {
return whiteMove ? wKingSq : bKingSq;
}
/**
* Count number of pieces of a certain type.
*/
/** Count number of pieces of a certain type. */
public final int nPieces(int pType) {
int ret = 0;
for (int sq = 0; sq < 64; sq++) {
for (int sq = 0; sq < 64; sq++)
if (squares[sq] == pType)
ret++;
}
return ret;
}
/** Count total number of pieces. */
public final int nPieces() {
int ret = 0;
for (int sq = 0; sq < 64; sq++)
if (squares[sq] != Piece.EMPTY)
ret++;
return ret;
}

View File

@@ -27,53 +27,56 @@ import org.petero.droidfish.gamelogic.Piece;
import org.petero.droidfish.gamelogic.Position;
import org.petero.droidfish.gamelogic.UndoInfo;
/** Interface between Position class and GTB probing code. */
/** Interface between Position class and GTB/RTB probing code. */
public class Probe {
private final GtbProbe gtb;
private final RtbProbe rtb;
private final int whiteSquares[];
private final int blackSquares[];
private final byte whitePieces[];
private final byte blackPieces[];
private static final Probe INSTANCE = new Probe();
private static final Probe instance = new Probe();
/** Get singleton instance. */
public static Probe getInstance() {
return INSTANCE;
return instance;
}
/** Constructor. */
private Probe() {
gtb = new GtbProbe();
rtb = new RtbProbe();
whiteSquares = new int[65];
blackSquares = new int[65];
whitePieces = new byte[65];
blackPieces = new byte[65];
}
public void setPath(String tbPath, boolean forceReload) {
gtb.setPath(tbPath, forceReload);
public void setPath(String gtbPath, String rtbPath, boolean forceReload) {
gtb.setPath(gtbPath, forceReload);
rtb.setPath(rtbPath, forceReload);
}
public static final class ProbeResult {
private static final class GtbProbeResult {
public final static int DRAW = 0;
public final static int WMATE = 1;
public final static int BMATE = 2;
public final static int UNKNOWN = 3;
public int result;
public int movesToMate; // Full moves to mate, or 0 if DRAW or UNKNOWN.
public int pliesToMate; // Plies to mate, or 0 if DRAW or UNKNOWN.
}
/**
* Probe table bases.
* Probe GTB tablebases.
* @param pos The position to probe.
* @param result Two element array. Set to [tbinfo, plies].
* @return True if success.
*/
public final ProbeResult probeHard(Position pos) {
ProbeResult ret = probeHardRaw(pos);
if (ret.result == ProbeResult.DRAW && pos.getEpSquare() != -1) {
private final GtbProbeResult gtbProbe(Position pos) {
GtbProbeResult ret = gtbProbeRaw(pos);
if (ret.result == GtbProbeResult.DRAW && pos.getEpSquare() != -1) {
ArrayList<Move> moveList = MoveGen.instance.legalMoves(pos);
int pawn = pos.whiteMove ? Piece.WPAWN : Piece.BPAWN;
int maxMate = -1;
@@ -82,29 +85,29 @@ public class Probe {
if ((move.to != pos.getEpSquare()) || (pos.getPiece(move.from) != pawn))
return ret;
pos.makeMove(move, ui);
ProbeResult ret2 = probeHard(pos);
GtbProbeResult ret2 = gtbProbe(pos);
pos.unMakeMove(move, ui);
switch (ret2.result) {
case ProbeResult.DRAW:
case GtbProbeResult.DRAW:
break;
case ProbeResult.WMATE:
case ProbeResult.BMATE:
maxMate = Math.max(maxMate, ret2.movesToMate);
case GtbProbeResult.WMATE:
case GtbProbeResult.BMATE:
maxMate = Math.max(maxMate, ret2.pliesToMate);
break;
case ProbeResult.UNKNOWN:
ret.result = ProbeResult.UNKNOWN;
case GtbProbeResult.UNKNOWN:
ret.result = GtbProbeResult.UNKNOWN;
return ret;
}
}
if (maxMate != -1) {
ret.result = pos.whiteMove ? ProbeResult.BMATE : ProbeResult.WMATE;
ret.movesToMate = maxMate;
ret.result = pos.whiteMove ? GtbProbeResult.BMATE : GtbProbeResult.WMATE;
ret.pliesToMate = maxMate;
}
}
return ret;
}
private final ProbeResult probeHardRaw(Position pos) {
private final GtbProbeResult gtbProbeRaw(Position pos) {
int castleMask = 0;
if (pos.a1Castle()) castleMask |= GtbProbe.A1_CASTLE;
if (pos.h1Castle()) castleMask |= GtbProbe.H1_CASTLE;
@@ -183,33 +186,81 @@ public class Probe {
whiteSquares, blackSquares, whitePieces, blackPieces,
result);
}
ProbeResult ret = new ProbeResult();
GtbProbeResult ret = new GtbProbeResult();
if (res) {
switch (result[0]) {
case GtbProbe.DRAW:
ret.result = ProbeResult.DRAW;
ret.movesToMate = 0;
ret.result = GtbProbeResult.DRAW;
ret.pliesToMate = 0;
break;
case GtbProbe.WMATE:
ret.result = ProbeResult.WMATE;
ret.movesToMate = (result[1] + 1) / 2;
ret.result = GtbProbeResult.WMATE;
ret.pliesToMate = result[1];
break;
case GtbProbe.BMATE:
ret.result = ProbeResult.BMATE;
ret.movesToMate = (result[1] + 1) / 2;
ret.result = GtbProbeResult.BMATE;
ret.pliesToMate = result[1];
break;
default:
ret.result = ProbeResult.UNKNOWN;
ret.movesToMate = 0;
ret.result = GtbProbeResult.UNKNOWN;
ret.pliesToMate = 0;
break;
}
} else {
ret.result = ProbeResult.UNKNOWN;
ret.movesToMate = 0;
ret.result = GtbProbeResult.UNKNOWN;
ret.pliesToMate = 0;
}
return ret;
}
private final ProbeResult rtbProbe(Position pos) {
if (pos.nPieces() > 6)
return new ProbeResult(ProbeResult.Type.NONE, 0, 0);
rtb.initIfNeeded();
byte[] squares = new byte[64];
for (int sq = 0; sq < 64; sq++)
squares[sq] = (byte)pos.getPiece(sq);
int[] result = new int[2];
rtb.probe(squares, pos.whiteMove, pos.getEpSquare(), pos.getCastleMask(),
pos.halfMoveClock, pos.fullMoveCounter, result);
int wdl = 0;
if (result[1] != RtbProbe.NOINFO) {
int score = result[1];
if (score > 0) {
wdl = 1;
} else if (score < 0) {
wdl = -1;
score = -score;
}
return new ProbeResult(ProbeResult.Type.DTZ, wdl, score);
} else if (result[0] != RtbProbe.NOINFO) {
return new ProbeResult(ProbeResult.Type.WDL, result[0], 0);
} else {
return new ProbeResult(ProbeResult.Type.NONE, 0, 0);
}
}
final ProbeResult probe(Position pos) {
GtbProbeResult gtbRes = gtbProbe(pos);
if (gtbRes.result != GtbProbeResult.UNKNOWN) {
int wdl = 0;
int score = 0;
if (gtbRes.result == GtbProbeResult.WMATE) {
wdl = 1;
score = gtbRes.pliesToMate;
} else if (gtbRes.result == GtbProbeResult.BMATE) {
wdl = -1;
score = gtbRes.pliesToMate;
}
if (!pos.whiteMove)
wdl = -wdl;
return new ProbeResult(ProbeResult.Type.DTM, wdl, score);
}
return rtbProbe(pos);
}
/** Return a list of all moves in moveList that are not known to be non-optimal.
* Returns null if no legal move could be excluded. */
public final ArrayList<Move> removeNonOptimal(Position pos, ArrayList<Move> moveList) {
@@ -220,16 +271,16 @@ public class Probe {
UndoInfo ui = new UndoInfo();
for (Move m : moveList) {
pos.makeMove(m, ui);
ProbeResult res = probeHard(pos);
GtbProbeResult res = gtbProbe(pos);
pos.unMakeMove(m, ui);
if (res.result == ProbeResult.UNKNOWN) {
if (res.result == GtbProbeResult.UNKNOWN) {
unknownMoves.add(m);
} else {
int wScore;
if (res.result == ProbeResult.WMATE)
wScore = MATE0 - res.movesToMate;
else if (res.result == ProbeResult.BMATE)
wScore = -(MATE0 - res.movesToMate);
if (res.result == GtbProbeResult.WMATE)
wScore = MATE0 - res.pliesToMate;
else if (res.result == GtbProbeResult.BMATE)
wScore = -(MATE0 - res.pliesToMate);
else
wScore = 0;
int score = pos.whiteMove ? wScore : -wScore;
@@ -251,11 +302,11 @@ public class Probe {
/** For a given position and from square, return EGTB information
* about all legal destination squares. Return null if no information available. */
public final ArrayList<Pair<Integer,Integer>> movePieceProbe(Position pos, int fromSq) {
public final ArrayList<Pair<Integer,ProbeResult>> movePieceProbe(Position pos, int fromSq) {
int p = pos.getPiece(fromSq);
if ((p == Piece.EMPTY) || (pos.whiteMove != Piece.isWhite(p)))
return null;
ArrayList<Pair<Integer,Integer>> ret = new ArrayList<Pair<Integer,Integer>>();
ArrayList<Pair<Integer,ProbeResult>> ret = new ArrayList<Pair<Integer,ProbeResult>>();
ArrayList<Move> moveList = new MoveGen().legalMoves(pos);
UndoInfo ui = new UndoInfo();
@@ -263,17 +314,18 @@ public class Probe {
if (m.from != fromSq)
continue;
pos.makeMove(m, ui);
ProbeResult res = probeHard(pos);
boolean isZeroing = pos.halfMoveClock == 0;
ProbeResult res = probe(pos);
pos.unMakeMove(m, ui);
if (res.result == ProbeResult.UNKNOWN)
if (res.type == ProbeResult.Type.NONE)
continue;
int score = 0;
if (res.result == ProbeResult.WMATE) {
score = pos.whiteMove ? res.movesToMate + 1 : -res.movesToMate;
} else if (res.result == ProbeResult.BMATE) {
score = pos.whiteMove ? -res.movesToMate : res.movesToMate + 1;
res.wdl = -res.wdl;
if (isZeroing && (res.type == ProbeResult.Type.DTZ)) {
res.score = 1;
} else if (res.type != ProbeResult.Type.WDL) {
res.score++;
}
ret.add(new Pair<Integer,Integer>(m.to, score));
ret.add(new Pair<Integer,ProbeResult>(m.to, res));
}
return ret;
}
@@ -281,12 +333,12 @@ public class Probe {
/** For a given position and from square, return EGTB information
* about all legal alternative positions for the piece on from square.
* Return null if no information is available. */
public final ArrayList<Pair<Integer, Integer>> relocatePieceProbe(Position pos, int fromSq) {
public final ArrayList<Pair<Integer,ProbeResult>> relocatePieceProbe(Position pos, int fromSq) {
int p = pos.getPiece(fromSq);
if (p == Piece.EMPTY)
return null;
boolean isPawn = (Piece.makeWhite(p) == Piece.WPAWN);
ArrayList<Pair<Integer,Integer>> ret = new ArrayList<Pair<Integer,Integer>>();
ArrayList<Pair<Integer,ProbeResult>> ret = new ArrayList<Pair<Integer,ProbeResult>>();
for (int sq = 0; sq < 64; sq++) {
if ((sq != fromSq) && (pos.getPiece(sq) != Piece.EMPTY))
continue;
@@ -294,18 +346,14 @@ public class Probe {
continue;
pos.setPiece(fromSq, Piece.EMPTY);
pos.setPiece(sq, p);
ProbeResult res = probeHard(pos);
ProbeResult res = probe(pos);
pos.setPiece(sq, Piece.EMPTY);
pos.setPiece(fromSq, p);
if (res.result == ProbeResult.UNKNOWN)
if (res.type == ProbeResult.Type.NONE)
continue;
int score = 0;
if (res.result == ProbeResult.WMATE) {
score = res.movesToMate;
} else if (res.result == ProbeResult.BMATE) {
score = -res.movesToMate;
}
ret.add(new Pair<Integer,Integer>(sq, score));
if (!pos.whiteMove)
res.wdl = -res.wdl;
ret.add(new Pair<Integer,ProbeResult>(sq, res));
}
return ret;
}

View File

@@ -0,0 +1,96 @@
package org.petero.droidfish.gtb;
/** Tablebase probe result. */
public final class ProbeResult implements Comparable<ProbeResult> {
public static enum Type {
DTM, // score is distance (full moves) to mate, or 0
DTZ, // score is distance (full moves) to zeroing move, or 0
WDL, // score is +-1 or 0
NONE, // No info available, score is 0
}
public Type type;
public int wdl; // +1 if if side to move wins, 0 for draw, -1 for loss
public int score; // Distance to win in plies. Always >= 0.
// Note! Zero if side to move is checkmated.
ProbeResult(Type type, int wdl, int score) {
this.type = type;
this.wdl = wdl;
this.score = score;
}
/**
* Return > 0 if other is "better" than this.
* A win is better than a draw, which is better than a loss.
* A DTM win is better than a DTZ win, which is better than a WDL win.
* A WDL loss is better than a DTZ loss, which is better than a DTM loss.
*/
@Override
public final int compareTo(ProbeResult other) {
final Type type1 = this.type;
final Type type2 = other.type;
final boolean none1 = type1 == Type.NONE;
final boolean none2 = type2 == Type.NONE;
if (none1 != none2)
return none2 ? -1 : 1;
if (none1)
return 0;
final int wdl1 = this.wdl;
final int wdl2 = other.wdl;
final boolean win1 = wdl1 > 0;
final boolean win2 = wdl2 > 0;
if (win1 != win2)
return win2 ? 1 : -1;
final boolean draw1 = wdl1 == 0;
final boolean draw2 = wdl2 == 0;
if (draw1 != draw2)
return draw2 ? 1 : -1;
final int score1 = this.score;
final int score2 = other.score;
if (win1) {
final boolean dtm1 = type1 == Type.DTM;
final boolean dtm2 = type2 == Type.DTM;
if (dtm1 != dtm2)
return dtm2 ? 1 : -1;
if (dtm1)
return -compareScore(wdl1, score1, wdl2, score2);
final boolean dtz1 = type1 == Type.DTZ;
final boolean dtz2 = type2 == Type.DTZ;
if (dtz1 != dtz2)
return dtz2 ? 1 : -1;
return -compareScore(wdl1, score1, wdl2, score2);
} else if (draw1) {
return 0;
} else {
final boolean wdlType1 = type1 == Type.WDL;
final boolean wdlType2 = type2 == Type.WDL;
if (wdlType1 != wdlType2)
return wdlType2 ? 1 : -1;
if (wdlType1)
return -compareScore(wdl1, score1, wdl2, score2);
final boolean dtzType1 = type1 == Type.DTZ;
final boolean dtzType2 = type2 == Type.DTZ;
if (dtzType1 != dtzType2)
return dtzType2 ? 1 : -1;
return -compareScore(wdl1, score1, wdl2, score2);
}
}
/** Return f((wdl1,score1)) - f((wdl2,score2)), where f(x) modifies
* the score so that larger values are better. */
final static int compareScore(int wdl1, int score1,
int wdl2, int score2) {
final int M = 1000;
if (wdl1 > 0)
score1 = M - score1;
else if (wdl1 < 0)
score1 = -M + score1;
if (wdl2 > 0)
score2 = M - score2;
else if (wdl2 < 0)
score2 = -M + score2;
return score1 - score2;
}
}

View File

@@ -0,0 +1,95 @@
/*
DroidFish - An Android chess program.
Copyright (C) 2011-2014 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 org.petero.droidfish.gtb;
import java.util.concurrent.ConcurrentLinkedQueue;
import org.petero.droidfish.engine.EngineUtil;
/** */
public class RtbProbe {
static {
System.loadLibrary("rtb");
}
private String currTbPath = "";
private ConcurrentLinkedQueue<String> tbPathQueue = new ConcurrentLinkedQueue<String>();
RtbProbe() {
}
public final void setPath(String tbPath, boolean forceReload) {
if (forceReload || !tbPathQueue.isEmpty() || !currTbPath.equals(tbPath)) {
tbPathQueue.add(tbPath);
Thread t = new Thread(new Runnable() {
@Override
public void run() {
// Sleep 0.4s to increase probability that engine
// is initialized before TB.
try { Thread.sleep(400); } catch (InterruptedException e) { }
initIfNeeded();
}
});
t.setPriority(Thread.MIN_PRIORITY);
t.start();
}
}
public final synchronized void initIfNeeded() {
String path = tbPathQueue.poll();
while (!tbPathQueue.isEmpty())
path = tbPathQueue.poll();
if (path != null) {
currTbPath = path;
synchronized (EngineUtil.nativeLock) {
init(currTbPath);
}
}
}
public final static int NOINFO = 1000;
/**
* Probe table bases.
* @param squares Array of length 64, see Position class.
* @param wtm True if white to move.
* @param epSq En passant square, see Position class.
* @param castleMask Castle mask, see Position class.
* @param halfMoveClock half move clock, see Position class.
* @param fullMoveCounter Full move counter, see Position class.
* @param result Two element array. Set to [wdlScore, dtzScore].
* The wdl score is one of: 0: Draw
* 1: win for side to move
* -1: loss for side to move
* NOINFO: No info available
* The dtz score is one of: 0: Draw
* x>0: Win in x plies
* x<0: Loss in -x plies
* NOINFO: No info available
* @return True if success.
*/
public final native void probe(byte[] squares,
boolean wtm,
int epSq, int castleMask,
int halfMoveClock,
int fullMoveCounter,
int[] result);
private final native static boolean init(String tbPath);
}