DroidFish: Better handling of transpositions in ECO classification.

This commit is contained in:
Peter Osterlund 2016-11-15 20:54:25 +01:00
parent 4a9bcb8a21
commit 3292bec55c
8 changed files with 178 additions and 100 deletions

View File

@ -1954,7 +1954,7 @@ public class DroidFish extends Activity
private String thinkingStr2 = "";
private String bookInfoStr = "";
private String ecoInfoStr = "";
private boolean ecoInTree = false;
private int distToEcoTree = 0;
private String variantStr = "";
private ArrayList<ArrayList<Move>> pvMoves = new ArrayList<ArrayList<Move>>();
private ArrayList<Move> bookMoves = null;
@ -1966,7 +1966,7 @@ public class DroidFish extends Activity
thinkingStr2 = ti.statStr;
bookInfoStr = ti.bookInfo;
ecoInfoStr = ti.eco;
ecoInTree = ti.ecoInTree;
distToEcoTree = ti.distToEcoTree;
pvMoves = ti.pvMoves;
bookMoves = ti.bookMoves;
updateThinkingInfo();
@ -1995,7 +1995,9 @@ public class DroidFish extends Activity
}
thinking.setText(s, TextView.BufferType.SPANNABLE);
}
if ((mEcoHints == ECO_HINTS_ALWAYS || (mEcoHints == ECO_HINTS_AUTO && ecoInTree)) &&
int maxDistToEcoTree = 10;
if ((mEcoHints == ECO_HINTS_ALWAYS ||
(mEcoHints == ECO_HINTS_AUTO && distToEcoTree <= maxDistToEcoTree)) &&
!ecoInfoStr.isEmpty()) {
String s = thinkingEmpty ? "" : "<br>";
s += ecoInfoStr;

View File

@ -60,7 +60,7 @@ public interface GUIInterface {
public ArrayList<ArrayList<Move>> pvMoves;
public ArrayList<Move> bookMoves;
public String eco;
public boolean ecoInTree;
public int distToEcoTree;
}
/** Update the computer thinking information. */

View File

@ -49,55 +49,103 @@ public class EcoDb {
return instance;
}
/** Get ECO classification for a given tree node. */
public Pair<String,Boolean> getEco(GameTree gt, GameTree.Node node) {
ArrayList<GameTree.Node> gtNodePath = new ArrayList<GameTree.Node>();
/** Get ECO classification for a given tree node. Also returns distance in plies to "ECO tree". */
public Pair<String,Integer> getEco(GameTree gt) {
ArrayList<Integer> treePath = new ArrayList<Integer>(); // Path to restore gt to original node
ArrayList<Pair<GameTree.Node,Boolean>> toCache = new ArrayList<Pair<GameTree.Node,Boolean>>();
int nodeIdx = -1;
boolean inEcoTree = true;
while (node != null) {
int distToEcoTree = 0;
// Find matching node furtherest from root in the ECO tree
boolean checkForDup = true;
while (true) {
GameTree.Node node = gt.currentNode;
CacheEntry e = findNode(node);
if (e != null) {
nodeIdx = e.nodeIdx;
inEcoTree = e.inEcoTree;
distToEcoTree = e.distToEcoTree;
checkForDup = false;
break;
}
if (node == gt.rootNode) {
Short idx = posHashToNodeIdx.get(gt.startPos.zobristHash());
if (idx != null) {
Short idx = posHashToNodeIdx.get(gt.currentPos.zobristHash());
boolean inEcoTree = idx != null;
toCache.add(new Pair<GameTree.Node,Boolean>(node, inEcoTree));
if (idx != null) {
Node ecoNode = readNode(idx);
if (ecoNode.nameIdx != -1) {
nodeIdx = idx;
break;
}
}
gtNodePath.add(node);
node = node.getParent();
if (node == gt.rootNode)
break;
treePath.add(node.getChildNo());
gt.goBack();
}
if (nodeIdx != -1) {
Node ecoNode = readNode(nodeIdx);
for (int i = gtNodePath.size() - 1; i >= 0; i--) {
GameTree.Node gtNode = gtNodePath.get(i);
int m = gtNode.move.getCompressedMove();
int child = inEcoTree ? ecoNode.firstChild : -1;
while (child != -1) {
Node cNode = readNode(child);
if (cNode.move == m)
break;
child = cNode.nextSibling;
// Handle duplicates in ECO tree (same position reachable from more than one path)
if (nodeIdx != -1 && checkForDup && gt.startPos.zobristHash() == startPosHash) {
ArrayList<Short> dups = posHashToNodeIdx2.get(gt.currentPos.zobristHash());
if (dups != null) {
while (gt.currentNode != gt.rootNode) {
treePath.add(gt.currentNode.getChildNo());
gt.goBack();
}
int currEcoNode = 0;
boolean foundDup = false;
while (!treePath.isEmpty()) {
gt.goForward(treePath.get(treePath.size() - 1));
treePath.remove(treePath.size() - 1);
int m = gt.currentNode.move.getCompressedMove();
Node ecoNode = readNode(currEcoNode);
boolean foundChild = false;
int child = ecoNode.firstChild;
while (child != -1) {
ecoNode = readNode(child);
if (ecoNode.move == m) {
foundChild = true;
break;
}
child = ecoNode.nextSibling;
}
if (!foundChild)
break;
currEcoNode = child;
for (Short dup : dups) {
if (dup == currEcoNode) {
nodeIdx = currEcoNode;
foundDup = true;
break;
}
}
if (foundDup)
break;
}
if (child != -1) {
nodeIdx = child;
ecoNode = readNode(nodeIdx);
} else
inEcoTree = false;
cacheNode(gtNode, nodeIdx, inEcoTree);
}
}
for (int i = treePath.size() - 1; i >= 0; i--)
gt.goForward(treePath.get(i));
for (int i = toCache.size() - 1; i >= 0; i--) {
Pair<GameTree.Node,Boolean> p = toCache.get(i);
distToEcoTree++;
if (p.second)
distToEcoTree = 0;
cacheNode(p.first, nodeIdx, distToEcoTree);
}
if (nodeIdx != -1) {
Node n = readNode(nodeIdx);
if (n.nameIdx >= 0)
return new Pair<String, Boolean>(ecoNames[n.nameIdx], inEcoTree);
return new Pair<String, Integer>(ecoNames[n.nameIdx], distToEcoTree);
}
return new Pair<String, Boolean>("", false);
return new Pair<String, Integer>("", 0);
}
@ -111,13 +159,15 @@ public class EcoDb {
private byte[] nodesBuffer;
private String[] ecoNames;
private HashMap<Long, Short> posHashToNodeIdx;
private HashMap<Long, ArrayList<Short>> posHashToNodeIdx2; // Handles collisions
private final long startPosHash; // Zobrist hash for standard starting position
private static class CacheEntry {
final int nodeIdx;
final boolean inEcoTree;
CacheEntry(int n, boolean i) {
final int distToEcoTree;
CacheEntry(int n, int d) {
nodeIdx = n;
inEcoTree = i;
distToEcoTree = d;
}
}
private WeakLRUCache<GameTree.Node, CacheEntry> gtNodeToIdx;
@ -128,13 +178,14 @@ public class EcoDb {
}
/** Store GameTree.Node to Node index in cache. */
private void cacheNode(GameTree.Node node, int nodeIdx, boolean inTree) {
gtNodeToIdx.put(node, new CacheEntry(nodeIdx, inTree));
private void cacheNode(GameTree.Node node, int nodeIdx, int distToEcoTree) {
gtNodeToIdx.put(node, new CacheEntry(nodeIdx, distToEcoTree));
}
/** Constructor. */
private EcoDb(Context context) {
posHashToNodeIdx = new HashMap<Long, Short>();
posHashToNodeIdx2 = new HashMap<Long, ArrayList<Short>>();
gtNodeToIdx = new WeakLRUCache<GameTree.Node, CacheEntry>(50);
try {
ByteArrayOutputStream bufStream = new ByteArrayOutputStream();
@ -174,19 +225,32 @@ public class EcoDb {
throw new RuntimeException("Can't read ECO database");
}
try {
Position pos = TextIO.readFEN(TextIO.startPosFEN);
startPosHash = pos.zobristHash();
if (nodesBuffer.length > 0) {
Position pos = TextIO.readFEN(TextIO.startPosFEN);
populateCache(pos, 0);
}
} catch (ChessParseError e) {
throw new RuntimeException("Internal error");
}
}
/** Initialize popHashToNodeIdx. */
/** Initialize posHashToNodeIdx. */
private void populateCache(Position pos, int nodeIdx) {
if (posHashToNodeIdx.get(pos.zobristHash()) == null)
posHashToNodeIdx.put(pos.zobristHash(), (short)nodeIdx);
Node node = readNode(nodeIdx);
long hash = pos.zobristHash();
if (posHashToNodeIdx.get(hash) == null) {
posHashToNodeIdx.put(hash, (short)nodeIdx);
} else if (node.nameIdx != -1) {
ArrayList<Short> lst = null;
if (posHashToNodeIdx2.get(hash) == null) {
lst = new ArrayList<Short>();
posHashToNodeIdx2.put(hash, lst);
} else {
lst = posHashToNodeIdx2.get(hash);
}
lst.add((short)nodeIdx);
}
int child = node.firstChild;
UndoInfo ui = new UndoInfo();
while (child != -1) {

View File

@ -78,22 +78,10 @@ public class EcoBuilder {
gotMoves |= !isHeader;
}
readGame(pgn.toString());
setNameIndices(0);
writeDataFile(ecoDatFile);
}
/** For all tree nodes, if nameIndex not already set,
* set it from parent node nameIndex. */
private void setNameIndices(int nodeIdx) {
Node n = nodes.get(nodeIdx);
for (Node c : n.children) {
if (c.nameIdx == -1)
c.nameIdx = n.nameIdx;
setNameIndices(c.index);
}
}
/** Read and process one game. */
private void readGame(String pgn) throws Throwable {
if (pgn.isEmpty())
@ -142,7 +130,7 @@ public class EcoBuilder {
Node node = new Node();
node.index = nodes.size();
node.move = m;
node.nameIdx = parent.nameIdx;
node.nameIdx = -1;
node.parent = parent;
nodes.add(node);
parent.children.add(node);

View File

@ -684,7 +684,7 @@ public class DroidChessController {
private String bookInfo = "";
private ArrayList<Move> bookMoves = null;
private String eco = ""; // ECO classification
private boolean ecoInTree = false; // True if current position is inside the EcoDB tree
private int distToEcoTree = 0; // Number of plies since game was in the "ECO tree".
private Move ponderMove = null;
private ArrayList<PvInfo> pvInfoV = new ArrayList<PvInfo>();
@ -698,7 +698,7 @@ public class DroidChessController {
bookInfo = "";
bookMoves = null;
eco = "";
ecoInTree = false;
distToEcoTree = 0;
setSearchInfo(id);
}
@ -789,7 +789,7 @@ public class DroidChessController {
ti.statStr = statStr;
ti.bookInfo = bookInfo;
ti.eco = eco;
ti.ecoInTree = ecoInTree;
ti.distToEcoTree = distToEcoTree;
ti.pvMoves = pvMoves;
ti.bookMoves = bookMoves;
latestThinkingInfo = ti;
@ -863,11 +863,11 @@ public class DroidChessController {
@Override
public void notifyBookInfo(int id, String bookInfo, ArrayList<Move> moveList,
String eco, boolean ecoInTree) {
String eco, int distToEcoTree) {
this.bookInfo = bookInfo;
bookMoves = moveList;
this.eco = eco;
this.ecoInTree = ecoInTree;
this.distToEcoTree = distToEcoTree;
setSearchInfo(id);
}
@ -931,11 +931,10 @@ public class DroidChessController {
private final void updateBookHints() {
if (game != null) {
Pair<String, ArrayList<Move>> bi = computerPlayer.getBookHints(game.currPos(), localPt());
Pair<String, Boolean> ecoData =
EcoDb.getInstance(gui.getContext()).getEco(game.tree, game.tree.currentNode);
Pair<String, Integer> ecoData =
EcoDb.getInstance(gui.getContext()).getEco(game.tree);
String eco = ecoData.first;
boolean ecoInTree = ecoData.second;
listener.notifyBookInfo(searchId, bi.first, bi.second, eco, ecoInTree);
listener.notifyBookInfo(searchId, bi.first, bi.second, eco, ecoData.second);
}
}
@ -975,11 +974,10 @@ public class DroidChessController {
computerPlayer.queueAnalyzeRequest(sr);
} else if (computersTurn || ponder) {
listener.clearSearchInfo(searchId);
Pair<String, Boolean> ecoData =
EcoDb.getInstance(gui.getContext()).getEco(game.tree, game.tree.currentNode);
Pair<String, Integer> ecoData =
EcoDb.getInstance(gui.getContext()).getEco(game.tree);
String eco = ecoData.first;
boolean ecoInTree = ecoData.second;
listener.notifyBookInfo(searchId, "", null, eco, ecoInTree);
listener.notifyBookInfo(searchId, "", null, eco, ecoData.second);
final Pair<Position, ArrayList<Move>> ph = game.getUCIHistory();
Position currPos = new Position(game.currPos());
long now = System.currentTimeMillis();

View File

@ -51,7 +51,7 @@ public class GameTree {
public Node rootNode;
public Node currentNode;
Position currentPos; // Cached value. Computable from "currentNode".
public Position currentPos; // Cached value. Computable from "currentNode".
private final PgnToken.PgnTokenReceiver gameStateListener;
@ -1086,21 +1086,22 @@ public class GameTree {
ArrayList<Integer> ret = new ArrayList<Integer>(64);
Node node = this;
while (node.parent != null) {
Node p = node.parent;
int childNo = -1;
for (int i = 0; i < p.children.size(); i++)
if (p.children.get(i) == node) {
childNo = i;
break;
}
if (childNo == -1) throw new RuntimeException();
ret.add(childNo);
ret.add(node.getChildNo());
node = node.parent;
}
Collections.reverse(ret);
return ret;
}
/** Return this node's position in the parent node child list. */
public final int getChildNo() {
Node p = parent;
for (int i = 0; i < p.children.size(); i++)
if (p.children.get(i) == this)
return i;
throw new RuntimeException();
}
static final void writeToStream(DataOutputStream dos, Node node) throws IOException {
while (true) {
dos.writeUTF(node.moveStr);

View File

@ -72,7 +72,7 @@ public interface SearchListener {
/** Report opening book information. */
public void notifyBookInfo(int id, String bookInfo, ArrayList<Move> moveList,
String eco, boolean ecoInTree);
String eco, int distToEcoTree);
/** Report move (or command, such as "resign") played by the engine. */
public void notifySearchResult(int id, String cmd, Move ponder);

View File

@ -36,27 +36,27 @@ public class EcoTest extends AndroidTestCase {
{
String pgn = "e4 e5 Nf3 Nc6 Bb5 a6 Ba4 Nf6 O-O Be7 Re1";
GameTree gt = readPGN(pgn);
String eco = ecoDb.getEco(gt, gt.currentNode).first;
String eco = ecoDb.getEco(gt).first;
assertEquals("", eco);
gt.goForward(0);
eco = ecoDb.getEco(gt, gt.currentNode).first;
eco = ecoDb.getEco(gt).first;
assertEquals("B00: King's pawn opening", eco);
gt.goForward(0);
eco = ecoDb.getEco(gt, gt.currentNode).first;
eco = ecoDb.getEco(gt).first;
assertEquals("C20: King's pawn game", eco);
gt.goForward(0);
eco = ecoDb.getEco(gt, gt.currentNode).first;
eco = ecoDb.getEco(gt).first;
assertEquals("C40: King's knight opening", eco);
gt.goForward(0);
eco = ecoDb.getEco(gt, gt.currentNode).first;
eco = ecoDb.getEco(gt).first;
assertEquals("C44: King's pawn game", eco);
gt.goForward(0);
eco = ecoDb.getEco(gt, gt.currentNode).first;
eco = ecoDb.getEco(gt).first;
assertEquals("C60: Ruy Lopez (Spanish opening)", eco);
}
{
@ -65,60 +65,85 @@ public class EcoTest extends AndroidTestCase {
game.processString("e5");
game.processString("Nf3");
game.processString("Nf6");
String eco = ecoDb.getEco(game.tree, game.tree.currentNode).first;
String eco = ecoDb.getEco(game.tree).first;
assertEquals("C42: Petrov's defence", eco);
game.processString("Nxe5");
eco = ecoDb.getEco(game.tree, game.tree.currentNode).first;
eco = ecoDb.getEco(game.tree).first;
assertEquals("C42: Petrov's defence", eco);
game.processString("d6");
eco = ecoDb.getEco(game.tree, game.tree.currentNode).first;
eco = ecoDb.getEco(game.tree).first;
assertEquals("C42: Petrov's defence", eco);
game.processString("Nxf7");
eco = ecoDb.getEco(game.tree, game.tree.currentNode).first;
eco = ecoDb.getEco(game.tree).first;
assertEquals("C42: Petrov, Cochrane gambit", eco);
game.undoMove();
eco = ecoDb.getEco(game.tree, game.tree.currentNode).first;
eco = ecoDb.getEco(game.tree).first;
assertEquals("C42: Petrov's defence", eco);
game.processString("Nf3");
game.processString("Nxe4");
game.processString("d4");
eco = ecoDb.getEco(game.tree, game.tree.currentNode).first;
eco = ecoDb.getEco(game.tree).first;
assertEquals("C42: Petrov, classical attack", eco);
}
{
Game game = new Game(null, new TimeControlData());
game.processString("e4");
game.processString("c5");
String eco = ecoDb.getEco(game.tree, game.tree.currentNode).first;
String eco = ecoDb.getEco(game.tree).first;
assertEquals("B20: Sicilian defence", eco);
game.processString("h3");
eco = ecoDb.getEco(game.tree, game.tree.currentNode).first;
eco = ecoDb.getEco(game.tree).first;
assertEquals("B20: Sicilian defence", eco);
game.processString("Nc6");
eco = ecoDb.getEco(game.tree, game.tree.currentNode).first;
eco = ecoDb.getEco(game.tree).first;
assertEquals("B20: Sicilian defence", eco);
game.processString("g3");
eco = ecoDb.getEco(game.tree, game.tree.currentNode).first;
eco = ecoDb.getEco(game.tree).first;
assertEquals("B20: Sicilian defence", eco);
}
{
Game game = new Game(null, new TimeControlData());
for (String m : new String[]{"d4", "d5", "c4", "c6", "Nf3", "Nf6", "Nc3", "g6"})
game.processString(m);
String eco = ecoDb.getEco(game.tree).first;
assertEquals("D15: QGD Slav, Schlechter variation", eco);
assertEquals(0, ecoDb.getEco(game.tree).second.intValue());
game.processString("a4");
assertEquals("D15: QGD Slav, Schlechter variation", eco);
assertEquals(1, ecoDb.getEco(game.tree).second.intValue());
}
{
Game game = new Game(null, new TimeControlData());
for (String m : new String[]{"d4", "Nf6", "c4", "g6", "Nc3", "d5", "Nf3", "c6"})
game.processString(m);
String eco = ecoDb.getEco(game.tree).first;
assertEquals("D90: Gruenfeld, Schlechter variation", eco);
assertEquals(0, ecoDb.getEco(game.tree).second.intValue());
game.processString("h4");
assertEquals("D90: Gruenfeld, Schlechter variation", eco);
assertEquals(1, ecoDb.getEco(game.tree).second.intValue());
game.processString("h5");
assertEquals("D90: Gruenfeld, Schlechter variation", eco);
assertEquals(2, ecoDb.getEco(game.tree).second.intValue());
}
}
public void testEcoFromFEN() throws Throwable {
EcoDb ecoDb = EcoDb.getInstance(getContext());
GameTree gt = gtFromFEN("rnbqkbnr/ppp2ppp/8/3p4/3P4/8/PPP2PPP/RNBQKBNR w KQkq - 0 4");
String eco = ecoDb.getEco(gt, gt.currentNode).first;
GameTree gt = gtFromFEN("rnbqkbnr/ppp2ppp/4p3/3P4/3P4/8/PPP2PPP/RNBQKBNR b KQkq - 0 3");
String eco = ecoDb.getEco(gt).first;
assertEquals("C01: French, exchange variation", eco);
gt = gtFromFEN("rnbqk1nr/ppppppbp/6p1/8/3PP3/8/PPP2PPP/RNBQKBNR w KQkq - 1 3");
eco = ecoDb.getEco(gt, gt.currentNode).first;
eco = ecoDb.getEco(gt).first;
assertEquals("B06: Robatsch (modern) defence", eco);
}