From 3292bec55c849c15c252c7831e93723335214f13 Mon Sep 17 00:00:00 2001 From: Peter Osterlund Date: Tue, 15 Nov 2016 20:54:25 +0100 Subject: [PATCH] DroidFish: Better handling of transpositions in ECO classification. --- .../src/org/petero/droidfish/DroidFish.java | 8 +- .../org/petero/droidfish/GUIInterface.java | 2 +- .../src/org/petero/droidfish/book/EcoDb.java | 142 +++++++++++++----- .../droidfish/buildtools/EcoBuilder.java | 14 +- .../gamelogic/DroidChessController.java | 24 ++- .../petero/droidfish/gamelogic/GameTree.java | 21 +-- .../droidfish/gamelogic/SearchListener.java | 2 +- .../org/petero/droidfish/book/EcoTest.java | 65 +++++--- 8 files changed, 178 insertions(+), 100 deletions(-) diff --git a/DroidFish/src/org/petero/droidfish/DroidFish.java b/DroidFish/src/org/petero/droidfish/DroidFish.java index d0b52cd..2a0297e 100644 --- a/DroidFish/src/org/petero/droidfish/DroidFish.java +++ b/DroidFish/src/org/petero/droidfish/DroidFish.java @@ -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> pvMoves = new ArrayList>(); private ArrayList 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 ? "" : "
"; s += ecoInfoStr; diff --git a/DroidFish/src/org/petero/droidfish/GUIInterface.java b/DroidFish/src/org/petero/droidfish/GUIInterface.java index 15c9354..c21f6f0 100644 --- a/DroidFish/src/org/petero/droidfish/GUIInterface.java +++ b/DroidFish/src/org/petero/droidfish/GUIInterface.java @@ -60,7 +60,7 @@ public interface GUIInterface { public ArrayList> pvMoves; public ArrayList bookMoves; public String eco; - public boolean ecoInTree; + public int distToEcoTree; } /** Update the computer thinking information. */ diff --git a/DroidFish/src/org/petero/droidfish/book/EcoDb.java b/DroidFish/src/org/petero/droidfish/book/EcoDb.java index b2d0893..88cf28e 100644 --- a/DroidFish/src/org/petero/droidfish/book/EcoDb.java +++ b/DroidFish/src/org/petero/droidfish/book/EcoDb.java @@ -49,55 +49,103 @@ public class EcoDb { return instance; } - /** Get ECO classification for a given tree node. */ - public Pair getEco(GameTree gt, GameTree.Node node) { - ArrayList gtNodePath = new ArrayList(); + /** Get ECO classification for a given tree node. Also returns distance in plies to "ECO tree". */ + public Pair getEco(GameTree gt) { + ArrayList treePath = new ArrayList(); // Path to restore gt to original node + ArrayList> toCache = new ArrayList>(); + 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(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 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 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(ecoNames[n.nameIdx], inEcoTree); + return new Pair(ecoNames[n.nameIdx], distToEcoTree); } - return new Pair("", false); + return new Pair("", 0); } @@ -111,13 +159,15 @@ public class EcoDb { private byte[] nodesBuffer; private String[] ecoNames; private HashMap posHashToNodeIdx; + private HashMap> 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 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(); + posHashToNodeIdx2 = new HashMap>(); gtNodeToIdx = new WeakLRUCache(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 lst = null; + if (posHashToNodeIdx2.get(hash) == null) { + lst = new ArrayList(); + posHashToNodeIdx2.put(hash, lst); + } else { + lst = posHashToNodeIdx2.get(hash); + } + lst.add((short)nodeIdx); + } int child = node.firstChild; UndoInfo ui = new UndoInfo(); while (child != -1) { diff --git a/DroidFish/src/org/petero/droidfish/buildtools/EcoBuilder.java b/DroidFish/src/org/petero/droidfish/buildtools/EcoBuilder.java index e6ce0a7..75ed272 100644 --- a/DroidFish/src/org/petero/droidfish/buildtools/EcoBuilder.java +++ b/DroidFish/src/org/petero/droidfish/buildtools/EcoBuilder.java @@ -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); diff --git a/DroidFish/src/org/petero/droidfish/gamelogic/DroidChessController.java b/DroidFish/src/org/petero/droidfish/gamelogic/DroidChessController.java index c7085ef..da8b341 100644 --- a/DroidFish/src/org/petero/droidfish/gamelogic/DroidChessController.java +++ b/DroidFish/src/org/petero/droidfish/gamelogic/DroidChessController.java @@ -684,7 +684,7 @@ public class DroidChessController { private String bookInfo = ""; private ArrayList 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 pvInfoV = new ArrayList(); @@ -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 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> bi = computerPlayer.getBookHints(game.currPos(), localPt()); - Pair ecoData = - EcoDb.getInstance(gui.getContext()).getEco(game.tree, game.tree.currentNode); + Pair 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 ecoData = - EcoDb.getInstance(gui.getContext()).getEco(game.tree, game.tree.currentNode); + Pair 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> ph = game.getUCIHistory(); Position currPos = new Position(game.currPos()); long now = System.currentTimeMillis(); diff --git a/DroidFish/src/org/petero/droidfish/gamelogic/GameTree.java b/DroidFish/src/org/petero/droidfish/gamelogic/GameTree.java index fe02551..25527d9 100644 --- a/DroidFish/src/org/petero/droidfish/gamelogic/GameTree.java +++ b/DroidFish/src/org/petero/droidfish/gamelogic/GameTree.java @@ -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 ret = new ArrayList(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); diff --git a/DroidFish/src/org/petero/droidfish/gamelogic/SearchListener.java b/DroidFish/src/org/petero/droidfish/gamelogic/SearchListener.java index a3e5ed4..1f50390 100644 --- a/DroidFish/src/org/petero/droidfish/gamelogic/SearchListener.java +++ b/DroidFish/src/org/petero/droidfish/gamelogic/SearchListener.java @@ -72,7 +72,7 @@ public interface SearchListener { /** Report opening book information. */ public void notifyBookInfo(int id, String bookInfo, ArrayList 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); diff --git a/DroidFishTest/src/org/petero/droidfish/book/EcoTest.java b/DroidFishTest/src/org/petero/droidfish/book/EcoTest.java index c7c990e..25aeda0 100644 --- a/DroidFishTest/src/org/petero/droidfish/book/EcoTest.java +++ b/DroidFishTest/src/org/petero/droidfish/book/EcoTest.java @@ -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); }