/* DroidFish - An Android chess program. Copyright (C) 2016 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.book; import android.annotation.SuppressLint; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.WeakHashMap; import org.petero.droidfish.DroidFishApp; import org.petero.droidfish.gamelogic.ChessParseError; import org.petero.droidfish.gamelogic.GameTree; import org.petero.droidfish.gamelogic.Move; import org.petero.droidfish.gamelogic.Pair; import org.petero.droidfish.gamelogic.Position; import org.petero.droidfish.gamelogic.TextIO; import org.petero.droidfish.gamelogic.UndoInfo; /** ECO code database. */ @SuppressLint("UseSparseArrays") public class EcoDb { private static EcoDb instance; /** Get singleton instance. */ public static EcoDb getInstance() { if (instance == null) { instance = new EcoDb(); } return instance; } public static class Result { public final String eco; // The ECO code public final String opn; // The opening name, or null public final String var; // The variation name, or null public final int distToEcoTree; Result(String eco, String opn, String var, int d) { this.eco = eco; this.opn = opn; this.var = var; distToEcoTree = d; } /** Return string formatted as "eco: opn, var". */ public String getName() { String s = eco; if (!opn.isEmpty()) { s = s + ": " + opn; if (!var.isEmpty()) s = s + ", " + var; } return s; } } /** Get ECO classification for a given tree node. Also returns distance in plies to "ECO tree". */ public Result 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; 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; distToEcoTree = e.distToEcoTree; checkForDup = false; break; } 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.ecoIdx != -1) { nodeIdx = idx; break; } } if (node == gt.rootNode) break; treePath.add(node.getChildNo()); gt.goBack(); } // 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), false); 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; } } } for (int i = treePath.size() - 1; i >= 0; i--) gt.goForward(treePath.get(i), false); 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); String eco = "", opn = "", var = ""; if (n.ecoIdx >= 0) { eco = strPool[n.ecoIdx]; if (n.opnIdx >= 0) { opn = strPool[n.opnIdx]; if (n.varIdx >= 0) var = strPool[n.varIdx]; } return new Result(eco, opn, var, distToEcoTree); } } return new Result("", "", "", 0); } /** Get all moves in the ECO tree from a given position. */ public ArrayList<Move> getMoves(Position pos) { ArrayList<Move> moves = new ArrayList<Move>(); long hash = pos.zobristHash(); Short idx = posHashToNodeIdx.get(hash); if (idx != null) { Node node = readNode(idx); int child = node.firstChild; while (child != -1) { node = readNode(child); moves.add(Move.fromCompressed(node.move)); child = node.nextSibling; } ArrayList<Short> lst = posHashToNodeIdx2.get(hash); if (lst != null) { for (Short idx2 : lst) { node = readNode(idx2); child = node.firstChild; while (child != -1) { node = readNode(child); Move m = Move.fromCompressed(node.move); if (!moves.contains(m)) moves.add(m); child = node.nextSibling; } } } } return moves; } private static class Node { int move; // Move (compressed) leading to the position corresponding to this node int ecoIdx; // Index in string array, or -1 int opnIdx; // Index in string array, or -1 int varIdx; // Index in string array, or -1 int firstChild; int nextSibling; } private byte[] nodesBuffer; private String[] strPool; 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 int distToEcoTree; CacheEntry(int n, int d) { nodeIdx = n; distToEcoTree = d; } } private WeakLRUCache<GameTree.Node, CacheEntry> gtNodeToIdx; /** Return cached Node index corresponding to a GameTree.Node, or -1 if not found. */ private CacheEntry findNode(GameTree.Node node) { return gtNodeToIdx.get(node); } /** Store GameTree.Node to Node index in cache. */ private void cacheNode(GameTree.Node node, int nodeIdx, int distToEcoTree) { gtNodeToIdx.put(node, new CacheEntry(nodeIdx, distToEcoTree)); } /** Constructor. */ private EcoDb() { posHashToNodeIdx = new HashMap<Long, Short>(); posHashToNodeIdx2 = new HashMap<Long, ArrayList<Short>>(); gtNodeToIdx = new WeakLRUCache<GameTree.Node, CacheEntry>(50); try { ByteArrayOutputStream bufStream = new ByteArrayOutputStream(); InputStream inStream = DroidFishApp.getContext().getAssets().open("eco.dat"); if (inStream == null) throw new IOException("Can't read ECO database"); byte[] buf = new byte[1024]; while (true) { int len = inStream.read(buf); if (len <= 0) break; bufStream.write(buf, 0, len); } inStream.close(); bufStream.flush(); buf = bufStream.toByteArray(); int nNodes = 0; while (true) { Node n = readNode(nNodes, buf); if (n.move == 0xffff) break; nNodes++; } nodesBuffer = new byte[nNodes * 12]; System.arraycopy(buf, 0, nodesBuffer, 0, nNodes * 12); ArrayList<String> names = new ArrayList<String>(); int idx = (nNodes + 1) * 12; int start = idx; for (int i = idx; i < buf.length; i++) { if (buf[i] == 0) { names.add(new String(buf, start, i - start, "UTF-8")); start = i + 1; } } strPool = names.toArray(new String[names.size()]); } catch (IOException ex) { throw new RuntimeException("Can't read ECO database"); } try { Position pos = TextIO.readFEN(TextIO.startPosFEN); startPosHash = pos.zobristHash(); if (nodesBuffer.length > 0) { populateCache(pos, 0); } } catch (ChessParseError e) { throw new RuntimeException("Internal error"); } } /** Initialize posHashToNodeIdx. */ private void populateCache(Position pos, int nodeIdx) { Node node = readNode(nodeIdx); long hash = pos.zobristHash(); if (posHashToNodeIdx.get(hash) == null) { posHashToNodeIdx.put(hash, (short)nodeIdx); } else if (node.ecoIdx != -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) { node = readNode(child); Move m = Move.fromCompressed(node.move); pos.makeMove(m, ui); populateCache(pos, child); pos.unMakeMove(m, ui); child = node.nextSibling; } } private Node readNode(int index) { return readNode(index, nodesBuffer); } private static Node readNode(int index, byte[] buf) { Node n = new Node(); int o = index * 12; n.move = getU16(buf, o); n.ecoIdx = getS16(buf, o + 2); n.opnIdx = getS16(buf, o + 4); n.varIdx = getS16(buf, o + 6); n.firstChild = getS16(buf, o + 8); n.nextSibling = getS16(buf, o + 10); return n; } private static int getU16(byte[] buf, int offs) { int b0 = buf[offs] & 255; int b1 = buf[offs + 1] & 255; return (b0 << 8) + b1; } private static int getS16(byte[] buf, int offs) { int ret = getU16(buf, offs); if (ret >= 0x8000) ret -= 0x10000; return ret; } /** A Cache where the keys are weak references and the cache automatically * shrinks when it becomes too large, using approximate LRU ordering. * This cache is not designed to store null values. */ private static class WeakLRUCache<K, V> { private WeakHashMap<K, V> mapNew; // Most recently used entries private WeakHashMap<K, V> mapOld; // Older entries private int maxSize; public WeakLRUCache(int maxSize) { mapNew = new WeakHashMap<K, V>(); mapOld = new WeakHashMap<K, V>(); this.maxSize = maxSize; } /** Insert a value in the map, replacing any old value with the same key. */ public void put(K key, V val) { if (mapNew.containsKey(key)) { mapNew.put(key, val); } else { if (mapOld.containsKey(key)) mapOld.remove(key); insertNew(key, val); } } /** Returns the value corresponding to key, or null if not found. */ public V get(K key) { V val = mapNew.get(key); if (val != null) return val; val = mapOld.get(key); if (val != null) { mapOld.remove(key); insertNew(key, val); } return val; } private void insertNew(K key, V val) { if (mapNew.size() >= maxSize) { WeakHashMap<K, V> tmp = mapNew; mapNew = mapOld; mapOld = tmp; mapNew.clear(); } mapNew.put(key, val); } } }