Add support for ABK opening books

Move probabilities are not exactly the same as in the Arena Chess GUI
because it is unknown how the computation in Arena works.
This commit is contained in:
Peter Osterlund 2020-05-17 20:28:07 +02:00
parent 03375fc479
commit 06375cbf1b
17 changed files with 396 additions and 37 deletions

View File

@ -23,7 +23,7 @@ import java.util.ArrayList;
import junit.framework.TestCase;
import org.petero.droidfish.book.DroidBook;
import org.petero.droidfish.book.IOpeningBook.BookPosInput;
import org.petero.droidfish.gamelogic.ChessParseError;
import org.petero.droidfish.gamelogic.Move;
import org.petero.droidfish.gamelogic.MoveGen;
@ -38,19 +38,21 @@ public class BookTest extends TestCase {
public void testGetBookMove() throws ChessParseError {
Position pos = TextIO.readFEN(TextIO.startPosFEN);
DroidBook book = DroidBook.getInstance();
Move move = book.getBookMove(pos);
BookPosInput posInput = new BookPosInput(pos, null, null);
Move move = book.getBookMove(posInput);
checkValid(pos, move);
// Test "out of book" condition
pos.setCastleMask(0);
move = book.getBookMove(pos);
move = book.getBookMove(posInput);
assertEquals(null, move);
}
public void testGetAllBookMoves() throws ChessParseError {
Position pos = TextIO.readFEN(TextIO.startPosFEN);
DroidBook book = DroidBook.getInstance();
ArrayList<Move> moves = book.getAllBookMoves(pos, false).second;
BookPosInput posInput = new BookPosInput(pos, null, null);
ArrayList<Move> moves = book.getAllBookMoves(posInput, false).second;
assertTrue(moves.size() > 1);
for (Move m : moves) {
checkValid(pos, m);

View File

@ -2500,7 +2500,7 @@ public class DroidFish extends Activity
if (dotIdx < 0)
return false;
String ext = filename.substring(dotIdx+1);
return ("ctg".equals(ext) || "bin".equals(ext));
return ("ctg".equals(ext) || "bin".equals(ext) || "abk".equals(ext));
});
final int numFiles = fileNames.length;
final String[] items = new String[numFiles + 3];

View File

@ -0,0 +1,294 @@
/*
DroidFish - An Android chess program.
Copyright (C) 2020 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 java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import org.petero.droidfish.book.DroidBook.BookEntry;
import org.petero.droidfish.gamelogic.ChessParseError;
import org.petero.droidfish.gamelogic.Move;
import org.petero.droidfish.gamelogic.Piece;
import org.petero.droidfish.gamelogic.Position;
import org.petero.droidfish.gamelogic.TextIO;
/** Handle Arena Chess GUI opening books. */
class AbkBook implements IOpeningBook {
private File abkFile; // The ".abk" file
private Position startPos;
/** Constructor. */
public AbkBook() {
try {
startPos = TextIO.readFEN(TextIO.startPosFEN);
} catch (ChessParseError ex) {
throw new RuntimeException(ex);
}
}
static boolean canHandle(BookOptions options) {
String filename = options.filename;
return filename.endsWith(".abk");
}
@Override
public boolean enabled() {
return abkFile.canRead();
}
@Override
public void setOptions(BookOptions options) {
abkFile = new File(options.filename);
}
private static class MoveData {
Move move;
double weightPrio;
double weightNGames;
double weightScore;
}
@Override
public ArrayList<BookEntry> getBookEntries(BookPosInput posInput) {
if (!startPos.equals(posInput.getPrevPos()))
return null;
try (RandomAccessFile abkF = new RandomAccessFile(abkFile, "r")) {
ArrayList<Move> gameMoves = posInput.getMoves();
BookSettings bs = new BookSettings(abkF);
if (gameMoves.size() >= bs.maxPly)
return null;
AbkBookEntry ent = new AbkBookEntry();
int entNo = 900;
for (Move m : gameMoves) {
int iter = 0;
while (true) {
if (entNo < 0)
return null;
ent.read(abkF, entNo);
if (ent.getMove().equals(m) && ent.isValid()) {
entNo = ent.nextMove;
break;
}
entNo = ent.nextSibling;
iter++;
if (iter > 255)
return null; // Corrupt book
}
}
if (entNo < 0)
return null;
boolean wtm = (gameMoves.size() % 2) == 0;
ArrayList<MoveData> moves = new ArrayList<>();
while (entNo >= 0) {
ent.read(abkF, entNo);
MoveData md = new MoveData();
md.move = ent.getMove();
int nWon = wtm ? ent.nWon : ent.nLost;
int nLost = wtm ? ent.nLost : ent.nWon;
int nDraw = ent.nGames - nWon - nLost;
md.weightPrio = scaleWeight(ent.priority, bs.prioImportance);
md.weightNGames = scaleWeight(ent.nGames, bs.nGamesImportance);
double score = (nWon + nDraw * 0.5) / ent.nGames;
md.weightScore = scaleWeight(score, bs.scoreImportance);
if (ent.isValid() &&
(!bs.skipPrio0Moves || ent.priority > 0) &&
(ent.nGames >= bs.minGames) &&
(nWon >= bs.minWonGames) &&
(score * 100 >= (wtm ? bs.minWinPercentWhite : bs.minWinPercentBlack))) {
moves.add(md);
}
if (moves.size() > 255)
return null; // Corrupt book
entNo = ent.nextSibling;
}
double sumWeightPrio = 0;
double sumWeightNGames = 0;
double sumWeightScore = 0;
for (MoveData md : moves) {
sumWeightPrio += md.weightPrio;
sumWeightNGames += md.weightNGames;
sumWeightScore += md.weightScore;
}
ArrayList<BookEntry> ret = new ArrayList<>();
boolean hasNonZeroWeight = false;
for (MoveData md : moves) {
BookEntry be = new BookEntry(md.move);
double wP = sumWeightPrio > 0 ? md.weightPrio / sumWeightPrio : 0.0;
double wN = sumWeightNGames > 0 ? md.weightNGames / sumWeightNGames : 0.0;
double wS = sumWeightScore > 0 ? md.weightScore / sumWeightScore : 0.0;
double a = 0.624;
double w = wP * Math.exp(a * bs.prioImportance) +
wN * Math.exp(a * bs.nGamesImportance) +
wS * Math.exp(a * bs.scoreImportance) * 1.4;
hasNonZeroWeight |= w > 0;
be.weight = (float)w;
ret.add(be);
}
if (!hasNonZeroWeight)
for (BookEntry be : ret)
be.weight = 1;
return ret;
} catch (IOException e) {
return null;
}
}
private static class AbkBookEntry {
private byte[] data = new byte[28];
private byte from; // From square, 0 = a1, 7 = h1, 8 = a2, 63 = h8
private byte to; // To square
private byte promotion; // 0 = none, +-1 = rook, +-2 = knight, +-3 = bishop, +-4 = queen
byte priority; // 0 = bad, >0 better, 9 best
int nGames; // Number of times games in which move was played
int nWon; // Number of won games for white
int nLost; // Number of lost games for white
int flags; // Value is 0x01000000 if move has been deleted
int nextMove; // First following move (by opposite color)
int nextSibling; // Next alternative move (by same color)
AbkBookEntry() {
}
void read(RandomAccessFile f, long entNo) throws IOException {
f.seek(entNo * 28);
f.readFully(data);
from = data[0];
to = data[1];
promotion = data[2];
priority = data[3];
nGames = extractInt(4);
nWon = extractInt(8);
nLost = extractInt(12);
flags = extractInt(16);
nextMove = extractInt(20);
nextSibling = extractInt(24);
}
Move getMove() {
int prom;
switch (promotion) {
case 0: prom = Piece.EMPTY; break;
case -1: prom = Piece.WROOK; break;
case -2: prom = Piece.WKNIGHT; break;
case -3: prom = Piece.WBISHOP; break;
case -4: prom = Piece.WQUEEN; break;
case 1: prom = Piece.BROOK; break;
case 2: prom = Piece.BKNIGHT; break;
case 3: prom = Piece.BBISHOP; break;
case 4: prom = Piece.BQUEEN; break;
default: prom = -1; break;
}
return new Move(from, to, prom);
}
boolean isValid() {
return flags != 0x01000000;
}
private int extractInt(int offs) {
return AbkBook.extractInt(data, offs);
}
}
/** Convert 4 bytes starting at "offs" in buf[] to an integer. */
private static int extractInt(byte[] buf, int offs) {
int ret = 0;
for (int i = 3; i >= 0; i--) {
int b = buf[offs + i];
if (b < 0) b += 256;
ret = (ret << 8) + b;
}
return ret;
}
private static class BookSettings {
private byte[] buf = new byte[256];
int minGames;
int minWonGames;
int minWinPercentWhite; // 0 - 100
int minWinPercentBlack; // 0 - 100
int prioImportance; // 0 - 15
int nGamesImportance; // 0 - 15
int scoreImportance; // 0 - 15
int maxPly;
boolean skipPrio0Moves = false; // Not stored in abk file
public BookSettings(RandomAccessFile abkF) throws IOException {
abkF.seek(0);
abkF.readFully(buf);
minGames = getInt(0xde, Integer.MAX_VALUE);
minWonGames = getInt(0xe2, Integer.MAX_VALUE);
minWinPercentWhite = getInt(0xe6, 100);
minWinPercentBlack = getInt(0xea, 100);
prioImportance = getInt(0xee, 15);
nGamesImportance = getInt(0xf2, 15);
scoreImportance = getInt(0xf6, 15);
maxPly = getInt(0xfa, 9999);
if (prioImportance == 0 && nGamesImportance == 0 && scoreImportance == 0) {
minGames = 0;
minWonGames = 0;
minWinPercentWhite = 0;
minWinPercentBlack = 0;
}
}
private int getInt(int offs, int maxVal) {
int val = extractInt(buf, offs);
return Math.min(Math.max(val, 0), maxVal);
}
}
private static double scaleWeight(double w, int importance) {
double e;
switch (importance) {
case 0:
return 0;
case 1:
e = 0.66;
break;
case 2:
e = 0.86;
break;
default:
e = 1 + ((double)importance - 3) / 6;
break;
}
return Math.pow(w, e);
}
}

View File

@ -62,7 +62,8 @@ class CtgBook implements IOpeningBook {
}
@Override
public ArrayList<BookEntry> getBookEntries(Position pos) {
public ArrayList<BookEntry> getBookEntries(BookPosInput posInput) {
Position pos = posInput.getCurrPos();
try (RandomAccessFile ctgF = new RandomAccessFile(ctgFile, "r");
RandomAccessFile ctbF = new RandomAccessFile(ctbFile, "r");
RandomAccessFile ctoF = new RandomAccessFile(ctoFile, "r")) {

View File

@ -28,6 +28,7 @@ import java.util.List;
import java.util.Random;
import org.petero.droidfish.Util;
import org.petero.droidfish.book.IOpeningBook.BookPosInput;
import org.petero.droidfish.gamelogic.Move;
import org.petero.droidfish.gamelogic.MoveGen;
import org.petero.droidfish.gamelogic.Position;
@ -64,7 +65,6 @@ public final class DroidBook {
}
private DroidBook() {
rndGen.setSeed(System.currentTimeMillis());
}
/** Set opening book options. */
@ -74,6 +74,8 @@ public final class DroidBook {
externalBook = new CtgBook();
else if (PolyglotBook.canHandle(options))
externalBook = new PolyglotBook();
else if (AbkBook.canHandle(options))
externalBook = new AbkBook();
else
externalBook = new NullBook();
externalBook.setOptions(options);
@ -83,10 +85,11 @@ public final class DroidBook {
}
/** Return a random book move for a position, or null if out of book. */
public final synchronized Move getBookMove(Position pos) {
public final synchronized Move getBookMove(BookPosInput posInput) {
Position pos = posInput.getCurrPos();
if ((options != null) && (pos.fullMoveCounter > options.maxLength))
return null;
List<BookEntry> bookMoves = getBook().getBookEntries(pos);
List<BookEntry> bookMoves = getBook().getBookEntries(posInput);
if (bookMoves == null || bookMoves.isEmpty())
return null;
@ -116,11 +119,12 @@ public final class DroidBook {
}
/** Return all book moves, both as a formatted string and as a list of moves. */
public final synchronized Pair<String,ArrayList<Move>> getAllBookMoves(Position pos,
public final synchronized Pair<String,ArrayList<Move>> getAllBookMoves(BookPosInput posInput,
boolean localized) {
Position pos = posInput.getCurrPos();
StringBuilder ret = new StringBuilder();
ArrayList<Move> bookMoveList = new ArrayList<>();
ArrayList<BookEntry> bookMoves = getBook().getBookEntries(pos);
ArrayList<BookEntry> bookMoves = getBook().getBookEntries(posInput);
// Check legality
if (bookMoves != null) {

View File

@ -43,7 +43,8 @@ public class EcoBook implements IOpeningBook {
}
@Override
public ArrayList<BookEntry> getBookEntries(Position pos) {
public ArrayList<BookEntry> getBookEntries(BookPosInput posInput) {
Position pos = posInput.getCurrPos();
ArrayList<Move> moves = EcoDb.getInstance().getMoves(pos);
ArrayList<BookEntry> entries = new ArrayList<>();
for (int i = 0; i < moves.size(); i++) {

View File

@ -18,18 +18,61 @@
package org.petero.droidfish.book;
import android.util.Pair;
import java.util.ArrayList;
import org.petero.droidfish.book.DroidBook.BookEntry;
import org.petero.droidfish.gamelogic.Game;
import org.petero.droidfish.gamelogic.Move;
import org.petero.droidfish.gamelogic.Position;
interface IOpeningBook {
public interface IOpeningBook {
/** Return true if book is currently enabled. */
boolean enabled();
/** Set book options, including filename. */
void setOptions(BookOptions options);
/** Information required to query an opening book. */
class BookPosInput {
private final Position currPos;
private Game game;
private Position prevPos;
private ArrayList<Move> moves;
public BookPosInput(Position currPos, Position prevPos, ArrayList<Move> moves) {
this.currPos = currPos;
this.prevPos = prevPos;
this.moves = moves;
}
public BookPosInput(Game game) {
currPos = game.currPos();
this.game = game;
}
public Position getCurrPos() {
return currPos;
}
public Position getPrevPos() {
lazyInit();
return prevPos;
}
public ArrayList<Move> getMoves() {
lazyInit();
return moves;
}
private void lazyInit() {
if (prevPos == null) {
Pair<Position, ArrayList<Move>> ph = game.getUCIHistory();
prevPos = ph.first;
moves = ph.second;
}
}
}
/** Get all book entries for a position. */
ArrayList<BookEntry> getBookEntries(Position pos);
ArrayList<BookEntry> getBookEntries(BookPosInput posInput);
}

View File

@ -52,7 +52,8 @@ final class InternalBook implements IOpeningBook {
}
@Override
public ArrayList<BookEntry> getBookEntries(Position pos) {
public ArrayList<BookEntry> getBookEntries(BookPosInput posInput) {
Position pos = posInput.getCurrPos();
initInternalBook();
ArrayList<BookEntry> ents = bookMap.get(pos.zobristHash());
if (ents == null)

View File

@ -37,7 +37,7 @@ class NoBook implements IOpeningBook {
}
@Override
public ArrayList<BookEntry> getBookEntries(Position pos) {
public ArrayList<BookEntry> getBookEntries(BookPosInput posInput) {
return null;
}
}

View File

@ -35,7 +35,7 @@ class NullBook implements IOpeningBook {
}
@Override
public ArrayList<BookEntry> getBookEntries(Position pos) {
public ArrayList<BookEntry> getBookEntries(BookPosInput posInput) {
return null;
}
}

View File

@ -370,7 +370,8 @@ class PolyglotBook implements IOpeningBook {
}
@Override
public final ArrayList<BookEntry> getBookEntries(Position pos) {
public final ArrayList<BookEntry> getBookEntries(BookPosInput posInput) {
Position pos = posInput.getCurrPos();
try (RandomAccessFile f = new RandomAccessFile(bookFile, "r")) {
long numEntries = f.length() / 16;
long key = getHashKey(pos);

View File

@ -28,6 +28,7 @@ import java.util.TreeMap;
import org.petero.droidfish.EngineOptions;
import org.petero.droidfish.book.BookOptions;
import org.petero.droidfish.book.DroidBook;
import org.petero.droidfish.book.IOpeningBook.BookPosInput;
import org.petero.droidfish.gamelogic.Move;
import org.petero.droidfish.gamelogic.MoveGen;
import org.petero.droidfish.gamelogic.Position;
@ -95,7 +96,7 @@ public class DroidComputerPlayer {
int searchId; // Unique identifier for this search request
long startTime; // System time (milliseconds) when search request was created
Position prevPos; // Position at last irreversible move
Position prevPos; // Position at last null move
ArrayList<Move> mList; // Moves after prevPos, including ponderMove
Position currPos; // currPos = prevPos + mList - ponderMove
boolean drawOffer; // True if other side made draw offer
@ -361,8 +362,9 @@ public class DroidComputerPlayer {
}
/** Return all book moves, both as a formatted string and as a list of moves. */
public final Pair<String, ArrayList<Move>> getBookHints(Position pos, boolean localized) {
return book.getAllBookMoves(pos, localized);
public final Pair<String, ArrayList<Move>> getBookHints(BookPosInput posInput,
boolean localized) {
return book.getAllBookMoves(posInput, localized);
}
/** Get engine reported name. */
@ -454,7 +456,8 @@ public class DroidComputerPlayer {
if (sr.ponderMove == null) {
// If we have a book move, play it.
Move bookMove = book.getBookMove(sr.currPos);
BookPosInput posInput = new BookPosInput(sr.currPos, sr.prevPos, sr.mList);
Move bookMove = book.getBookMove(posInput);
if (bookMove != null) {
if (canClaimDraw(sr.currPos, posHashList, posHashListSize, bookMove).isEmpty()) {
listener.notifySearchResult(sr.searchId,

View File

@ -39,6 +39,7 @@ import org.petero.droidfish.PGNOptions;
import org.petero.droidfish.Util;
import org.petero.droidfish.book.BookOptions;
import org.petero.droidfish.book.EcoDb;
import org.petero.droidfish.book.IOpeningBook.BookPosInput;
import org.petero.droidfish.engine.DroidComputerPlayer;
import org.petero.droidfish.engine.UCIOptions;
import org.petero.droidfish.engine.DroidComputerPlayer.EloData;
@ -955,7 +956,8 @@ public class DroidChessController {
private void updateBookHints() {
if (game != null) {
Pair<String, ArrayList<Move>> bi = computerPlayer.getBookHints(game.currPos(), localPt());
BookPosInput posInput = new BookPosInput(game);
Pair<String, ArrayList<Move>> bi = computerPlayer.getBookHints(posInput, localPt());
EcoDb.Result ecoData = EcoDb.getInstance().getEco(game.tree);
String eco = ecoData.getName();
listener.notifyBookInfo(searchId, bi.first, bi.second, eco, ecoData.distToEcoTree);

View File

@ -112,7 +112,7 @@ public class Game {
return ret;
}
final Position currPos() {
public final Position currPos() {
return tree.currentPos;
}

View File

@ -389,7 +389,7 @@ If you are running on battery power, it is recommended that you change settings
<string name="prefs_bookTournamentMode_summary">Ignore moves marked as not for tournament play</string>
<string name="prefs_bookRandom_title">Book Randomization</string>
<string name="prefs_bookFile_title">Book Filename</string>
<string name="prefs_bookFile_summary">Polyglot or CTG book file in DroidFish directory on SD Card</string>
<string name="prefs_bookFile_summary">Polyglot, ABK or CTG book file in DroidFish directory on SD Card</string>
<string name="prefs_pgnSettings_title">PGN Settings</string>
<string name="prefs_pgnSettings_summary">Settings for import and export of portable game notation (PGN) data</string>
<string name="prefs_pgn_viewer">PGN viewer</string>

View File

@ -560,16 +560,19 @@ restored.
## Installing additional opening books
To use *polyglot* or *CTG* book files:
To use *polyglot*, *CTG* or *ABK* book files:
1. Copy one or more polyglot book files to the `DroidFish/book` directory on the
external storage. Polyglot books must have the file extension `.bin`.
**Note!** The Android file system is case sensitive, so the extension must be
`.bin`, not `.Bin` or `.BIN`.
1. Copy one or more opening book files to the `DroidFish/book` directory on the
external storage.
1. Copy one or more CTG book files to the `DroidFish/book` directory. A CTG
book consists of three files with file extensions `.ctg`, `.ctb` and
`.cto`. You must copy all three files.
1. Polyglot books must have the file extension `.bin`.
**Note!** The Android file system may be case sensitive, in which case the
extension must be `.bin`, not `.Bin` or `.BIN`.
1. A *CTG* book consists of three files with file extensions `.ctg`, `.ctb`
and `.cto`. You must copy all three files.
1. an *ABK* book must have the file extension `.abk`.
1. Go to *Left drawer menu* -> *Select opening book*.
@ -862,11 +865,11 @@ You can change aspects of the opening book from *Left drawer menu* -> *Settings*
* *Prefer main lines*: When enabled, moves that are marked as main line moves in
the book are given a higher weight so they will be played more often by the
chess engine.
**Note!** This option only has an effect for CTG opening books.
**Note!** This option only has an effect for *CTG* opening books.
* *Tournament mode*: When enabled, only book moves that are marked for
tournament play are played by the chess engine.
**Note!** This option only has an effect for CTG opening books.
**Note!** This option only has an effect for *CTG* opening books.
* *Book randomization*: Controls how often different book moves are played by
the engine. The default is 50% which means that the statistics from the
@ -882,6 +885,10 @@ You can change aspects of the opening book from *Left drawer menu* -> *Settings*
very big opening book stored somewhere on the device but it would be
impractical to copy it to the `DroidFish/book` directory.
**Note!** The move percentages calculated by *DroidFish* for CTG books are
**Note!** The move percentages calculated by *DroidFish* for *CTG* books are
unlikely to agree with percentages calculated by other chess programs that can
use CTG books.
use *CTG* books.
**Note!** The move percentages calculated by *DroidFish* for *ABK* books are not
always equal to percentages shown in the Arena Chess GUI, because the algorithm
used by Arena to compute the percentages is unknown.

Binary file not shown.