DroidFish: Time control information is now stored in pgn header data.

This commit is contained in:
Peter Osterlund 2013-04-14 13:33:36 +00:00
parent da625e085d
commit 26ac3b6ab5
7 changed files with 281 additions and 37 deletions

View File

@ -53,6 +53,7 @@ import org.petero.droidfish.gamelogic.Position;
import org.petero.droidfish.gamelogic.TextIO;
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 com.larvalabs.svgandroid.SVG;
@ -166,6 +167,7 @@ public class DroidFish extends Activity implements GUIInterface {
private int maxNumArrows;
private GameMode gameMode;
private boolean mPonderMode;
private TimeControlData tcData = new TimeControlData();
private int mEngineThreads;
private String playerName;
private boolean boardFlipped;
@ -405,18 +407,21 @@ public class DroidFish extends Activity implements GUIInterface {
ctrl = new DroidChessController(this, gameTextListener, pgnOptions);
egtbForceReload = true;
readPrefs();
ctrl.newGame(gameMode);
ctrl.newGame(gameMode, tcData);
{
byte[] data = null;
int version = 1;
if (savedInstanceState != null) {
data = savedInstanceState.getByteArray("gameState");
version = savedInstanceState.getInt("gameStateVersion", version);
} else {
String dataStr = settings.getString("gameState", null);
version = settings.getInt("gameStateVersion", version);
if (dataStr != null)
data = strToByteArr(dataStr);
}
if (data != null)
ctrl.fromByteArray(data);
ctrl.fromByteArray(data, version);
}
ctrl.setGuiPaused(true);
ctrl.setGuiPaused(false);
@ -813,6 +818,7 @@ public class DroidFish extends Activity implements GUIInterface {
if (ctrl != null) {
byte[] data = ctrl.toByteArray();
outState.putByteArray("gameState", data);
outState.putInt("gameStateVersion", 2);
}
}
@ -835,6 +841,7 @@ public class DroidFish extends Activity implements GUIInterface {
Editor editor = settings.edit();
String dataStr = byteArrToString(data);
editor.putString("gameState", dataStr);
editor.putInt("gameStateVersion", 2);
editor.commit();
}
lastVisibleMillis = System.currentTimeMillis();
@ -892,7 +899,7 @@ public class DroidFish extends Activity implements GUIInterface {
int timeControl = getIntSetting("timeControl", 120000);
int movesPerSession = getIntSetting("movesPerSession", 60);
int timeIncrement = getIntSetting("timeIncrement", 0);
ctrl.setTimeLimit(timeControl, movesPerSession, timeIncrement);
tcData.setTimeControl(timeControl, movesPerSession, timeIncrement);
updateTimeControlTitle();
boardGestures = settings.getBoolean("boardGestures", true);
@ -1654,7 +1661,7 @@ public class DroidFish extends Activity implements GUIInterface {
gameMode = new GameMode(gameModeType);
}
// savePGNToFile(".autosave.pgn", true);
ctrl.newGame(gameMode);
ctrl.newGame(gameMode, tcData);
ctrl.startGame();
setBoardFlip(true);
updateEngineTitle();

View File

@ -34,7 +34,6 @@ import org.petero.droidfish.engine.DroidComputerPlayer.SearchRequest;
import org.petero.droidfish.engine.DroidComputerPlayer.SearchType;
import org.petero.droidfish.gamelogic.Game.GameState;
import org.petero.droidfish.gamelogic.GameTree.Node;
import org.petero.droidfish.gamelogic.TimeControlData.TimeControlField;
/**
* The glue between the chess engine and the GUI.
@ -55,8 +54,6 @@ public class DroidChessController {
private int strength = 1000;
private int numPV = 1;
private TimeControlData tcData;
private SearchListener listener;
private boolean guiPaused = false;
@ -71,13 +68,12 @@ public class DroidChessController {
this.gameTextListener = gameTextListener;
gameMode = new GameMode(GameMode.TWO_PLAYERS);
pgnOptions = options;
tcData = new TimeControlData();
listener = new SearchListener();
searchId = 0;
}
/** Start a new game. */
public final synchronized void newGame(GameMode gameMode) {
public final synchronized void newGame(GameMode gameMode, TimeControlData tcData) {
boolean updateGui = abortSearch();
if (updateGui)
updateGUI();
@ -103,23 +99,11 @@ public class DroidChessController {
updateGameMode();
}
/** Set time control parameters. */
public final synchronized void setTimeLimit(int time, int moves, int inc) {
tcData.setTimeControl(time, moves, inc);
if (game != null)
game.timeController.setTimeControl(tcData);
}
/** @return Array containing time control, moves per session and time increment. */
public final int[] getTimeLimit() {
if (game != null)
return game.timeController.getTimeLimit(game.currPos().whiteMove);
int[] ret = new int[3];
TimeControlField tc = tcData.getTC(true).get(0);
ret[0] = (int)tc.timeControl;
ret[1] = tc.movesPerSession;
ret[2] = (int)tc.increment;
return ret;
return new int[]{5*60*1000, 60, 0};
}
/** The chess clocks are stopped when the GUI is paused. */
@ -214,8 +198,8 @@ public class DroidChessController {
}
/** De-serialize from byte array. */
public final synchronized void fromByteArray(byte[] data) {
game.fromByteArray(data);
public final synchronized void fromByteArray(byte[] data, int version) {
game.fromByteArray(data, version);
game.tree.translateMoves();
}
@ -236,7 +220,7 @@ public class DroidChessController {
/** Parse a string as FEN or PGN data. */
public final synchronized void setFENOrPGN(String fenPgn) throws ChessParseError {
Game newGame = new Game(gameTextListener, tcData);
Game newGame = new Game(gameTextListener, game.timeController.tcData);
try {
Position pos = TextIO.readFEN(fenPgn);
newGame.setPos(pos);

View File

@ -40,16 +40,16 @@ public class Game {
public Game(PgnToken.PgnTokenReceiver gameTextListener, TimeControlData tcData) {
this.gameTextListener = gameTextListener;
tree = new GameTree(gameTextListener);
timeController = new TimeControl();
timeController.setTimeControl(tcData);
gamePaused = false;
newGame();
tree.setTimeControlData(tcData);
}
/** De-serialize from byte array. */
final void fromByteArray(byte[] data) {
tree.fromByteArray(data);
final void fromByteArray(byte[] data, int version) {
tree.fromByteArray(data, version);
updateTimeControl(true);
}
@ -83,8 +83,12 @@ public class Game {
final boolean readPGN(String pgn, PGNOptions options) throws ChessParseError {
boolean ret = tree.readPGN(pgn, options);
if (ret)
updateTimeControl(false);
if (ret) {
TimeControlData tcData = tree.getTimeControlData();
if (tcData != null)
timeController.setTimeControl(tcData);
updateTimeControl(tcData != null);
}
return ret;
}

View File

@ -28,11 +28,13 @@ import java.util.Calendar;
import java.util.Collections;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import org.petero.droidfish.PGNOptions;
import org.petero.droidfish.gamelogic.Game.GameState;
import org.petero.droidfish.gamelogic.TimeControlData.TimeControlField;
public class GameTree {
// Data from the seven tag roster (STR) part of the PGN standard
@ -40,7 +42,7 @@ public class GameTree {
// Result is the last tag pair in the STR, but it is computed on demand from the game tree.
Position startPos;
private String timeControl;
private String timeControl, whiteTimeControl, blackTimeControl;
// Non-standard tags
static private final class TagPair {
@ -88,6 +90,8 @@ public class GameTree {
black = "?";
startPos = pos;
timeControl = "?";
whiteTimeControl = "?";
blackTimeControl = "?";
tagPairs = new ArrayList<TagPair>();
rootNode = new Node();
currentNode = rootNode;
@ -308,6 +312,10 @@ public class GameTree {
}
if (!timeControl.equals("?"))
addTagPair(out, "TimeControl", timeControl);
if (!whiteTimeControl.equals("?"))
addTagPair(out, "WhiteTimeControl", whiteTimeControl);
if (!blackTimeControl.equals("?"))
addTagPair(out, "BlackTimeControl", blackTimeControl);
// Write other non-standard tag pairs
for (int i = 0; i < tagPairs.size(); i++)
@ -559,6 +567,10 @@ public class GameTree {
result = val;
} else if (name.equals("TimeControl")) {
timeControl = val;
} else if (name.equals("WhiteTimeControl")) {
whiteTimeControl = val;
} else if (name.equals("BlackTimeControl")) {
blackTimeControl = val;
} else {
this.tagPairs.add(tagPairs.get(i));
}
@ -614,6 +626,8 @@ public class GameTree {
dos.writeUTF(black);
dos.writeUTF(TextIO.toFEN(startPos));
dos.writeUTF(timeControl);
dos.writeUTF(whiteTimeControl);
dos.writeUTF(blackTimeControl);
int nTags = tagPairs.size();
dos.writeInt(nTags);
for (int i = 0; i < nTags; i++) {
@ -637,7 +651,7 @@ public class GameTree {
}
/** De-serialize from byte array. */
public final void fromByteArray(byte[] data) {
public final void fromByteArray(byte[] data, int version) {
try {
ByteArrayInputStream bais = new ByteArrayInputStream(data);
DataInputStream dis = new DataInputStream(bais);
@ -650,6 +664,13 @@ public class GameTree {
startPos = TextIO.readFEN(dis.readUTF());
currentPos = new Position(startPos);
timeControl = dis.readUTF();
if (version >= 2) {
whiteTimeControl = dis.readUTF();
blackTimeControl = dis.readUTF();
} else {
whiteTimeControl = "?";
blackTimeControl = "?";
}
int nTags = dis.readInt();
tagPairs.clear();
for (int i = 0; i < nTags; i++) {
@ -1516,9 +1537,118 @@ public class GameTree {
headers.put("Round", round);
headers.put("White", white);
headers.put("Black", black);
if (!timeControl.equals("?"))
headers.put("TimeControl", timeControl);
if (!whiteTimeControl.equals("?"))
headers.put("WhiteTimeControl", whiteTimeControl);
if (!blackTimeControl.equals("?"))
headers.put("BlackTimeControl", blackTimeControl);
for (int i = 0; i < tagPairs.size(); i++) {
TagPair tp = tagPairs.get(i);
headers.put(tp.tagName, tp.tagValue);
}
}
private ArrayList<TimeControlField> stringToTCFields(String tcStr) {
String[] fields = tcStr.split(":");
int nf = fields.length;
ArrayList<TimeControlField> ret = new ArrayList<TimeControlField>(nf);
for (int i = 0; i < nf; i++) {
String f = fields[i].trim();
if (f.equals("?") || f.equals("-") || f.contains("*")) {
// Not supported
} else {
try {
int moves = 0;
int time = 0;
int inc = 0;
int idx = f.indexOf('/');
if (idx > 0)
moves = Integer.parseInt(f.substring(0, idx).trim());
if (idx >= 0)
f = f.substring(idx+1);
idx = f.indexOf('+');
if (idx >= 0) {
if (idx > 0)
time = (int)(Double.parseDouble(f.substring(0, idx).trim())*1e3);
if (idx >= 0)
f = f.substring(idx+1);
inc = (int)(Double.parseDouble(f.trim())*1e3);
} else {
time = (int)(Double.parseDouble(f.trim())*1e3);
}
ret.add(new TimeControlField(time, moves, inc));
} catch (NumberFormatException ex) {
// Invalid syntax, ignore
}
}
}
return ret;
}
private String tcFieldsToString(ArrayList<TimeControlField> tcFields) {
StringBuilder sb = new StringBuilder();
int nf = tcFields.size();
for (int i = 0; i < nf; i++) {
if (i > 0)
sb.append(':');
TimeControlField t = tcFields.get(i);
if (t.movesPerSession > 0) {
sb.append(t.movesPerSession);
sb.append('/');
}
sb.append(t.timeControl / 1000);
int ms = (int)t.timeControl % 1000;
if (ms > 0) {
sb.append('.');
sb.append(String.format(Locale.US, "%03d", ms));
}
if (t.increment > 0) {
sb.append('+');
sb.append(t.increment / 1000);
ms = (int)t.increment % 1000;
if (ms > 0) {
sb.append('.');
sb.append(String.format(Locale.US, "%03d", ms));
}
}
}
return sb.toString();
}
/** Get time control data, or null if not present. */
public TimeControlData getTimeControlData() {
if (!whiteTimeControl.equals("?") && !blackTimeControl.equals("?")) {
ArrayList<TimeControlField> tcW = stringToTCFields(whiteTimeControl);
ArrayList<TimeControlField> tcB = stringToTCFields(blackTimeControl);
if (!tcW.isEmpty() && !tcB.isEmpty()) {
TimeControlData tcData = new TimeControlData();
tcData.tcW = tcW;
tcData.tcB = tcB;
return tcData;
}
}
if (!timeControl.equals("?")) {
ArrayList<TimeControlField> tc = stringToTCFields(timeControl);
if (!tc.isEmpty()) {
TimeControlData tcData = new TimeControlData();
tcData.tcW = tc;
tcData.tcB = tc;
return tcData;
}
}
return null;
}
public void setTimeControlData(TimeControlData tcData) {
if (tcData.isSymmetric()) {
timeControl = tcFieldsToString(tcData.tcW);
whiteTimeControl = "?";
blackTimeControl = "?";
} else {
whiteTimeControl = tcFieldsToString(tcData.tcW);
blackTimeControl = tcFieldsToString(tcData.tcB);
timeControl = "?";
}
}
}

View File

@ -24,7 +24,7 @@ import org.petero.droidfish.gamelogic.TimeControlData.TimeControlField;
/** Keep track of time control information for both players. */
public class TimeControl {
private TimeControlData tcData;
TimeControlData tcData;
private long whiteBaseTime; // Current remaining time, or remaining time when clock started
private long blackBaseTime; // Current remaining time, or remaining time when clock started

View File

@ -4,9 +4,9 @@ import java.util.ArrayList;
public final class TimeControlData {
public static final class TimeControlField {
long timeControl;
long timeControl; // Time in milliseconds
int movesPerSession;
long increment;
long increment; // Increment in milliseconds
public TimeControlField(long time, int moves, long inc) {
timeControl = time;
@ -17,13 +17,15 @@ public final class TimeControlData {
ArrayList<TimeControlField> tcW, tcB;
TimeControlData() {
/** Constructor. Set a default time control. */
public TimeControlData() {
tcW = new ArrayList<TimeControlField>();
tcW.add(new TimeControlField(5*60*1000, 60, 0));
tcB = new ArrayList<TimeControlField>();
tcB.add(new TimeControlField(5*60*1000, 60, 0));
}
/** Set a single time control for both white and black. */
public final void setTimeControl(long time, int moves, long inc) {
tcW = new ArrayList<TimeControlField>();
tcW.add(new TimeControlField(time, moves, inc));
@ -35,4 +37,32 @@ public final class TimeControlData {
public ArrayList<TimeControlField> getTC(boolean whiteMove) {
return whiteMove ? tcW : tcB;
}
/** Return true if white and black time controls are equal. */
public boolean isSymmetric() {
return arrayEquals(tcW, tcB);
}
@Override
public boolean equals(Object o) {
if (!(o instanceof TimeControlData))
return false;
TimeControlData tc2 = (TimeControlData)o;
return arrayEquals(tcW, tc2.tcW) && arrayEquals(tcB, tc2.tcB);
}
private static boolean arrayEquals(ArrayList<TimeControlField> a1,
ArrayList<TimeControlField> a2) {
if (a1.size() != a2.size())
return false;
for (int i = 0; i < a1.size(); i++) {
TimeControlField f1 = a1.get(i);
TimeControlField f2 = a2.get(i);
if ((f1.timeControl != f2.timeControl) ||
(f1.movesPerSession != f2.movesPerSession) ||
(f1.increment != f2.increment))
return false;
}
return true;
}
}

View File

@ -21,6 +21,8 @@ package org.petero.droidfish.gamelogic;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import junit.framework.TestCase;
@ -28,6 +30,7 @@ import org.petero.droidfish.PGNOptions;
import org.petero.droidfish.gamelogic.Game.GameState;
import org.petero.droidfish.gamelogic.GameTree.Node;
import org.petero.droidfish.gamelogic.GameTree.PgnScanner;
import org.petero.droidfish.gamelogic.TimeControlData.TimeControlField;
public class GameTreeTest extends TestCase {
@ -96,7 +99,7 @@ public class GameTreeTest extends TestCase {
byte[] serialState = gt.toByteArray();
gt = new GameTree(null);
gt.fromByteArray(serialState);
gt.fromByteArray(serialState, 2);
assertEquals(expectedPos, gt.currentPos);
gt.goBack();
@ -694,4 +697,90 @@ public class GameTreeTest extends TestCase {
gt.goNode(ne4);
assertEquals("e4* e5 Nf3 Nc6 Bb5 a6", getMoveListAsString(gt));
}
public final void testTimeControl() throws ChessParseError {
GameTree gt = new GameTree(null);
PGNOptions options = new PGNOptions();
TimeControlData tcData = new TimeControlData();
tcData.setTimeControl(180*1000, 35, 0);
gt.setTimeControlData(tcData);
TimeControlData tcData2 = gt.getTimeControlData();
assertTrue(tcData2.isSymmetric());
assertTrue(tcData.equals(tcData2));
Map<String,String> headers = new TreeMap<String,String>();
gt.getHeaders(headers);
assertEquals("35/180", headers.get("TimeControl"));
assertEquals(null, headers.get("WhiteTimeControl"));
assertEquals(null, headers.get("BlackTimeControl"));
String pgn = gt.toPGN(options);
boolean res = gt.readPGN(pgn, options);
assertTrue(res);
tcData2 = gt.getTimeControlData();
assertTrue(tcData2.isSymmetric());
assertEquals(tcData, tcData2);
headers = new TreeMap<String,String>();
gt.getHeaders(headers);
assertEquals("35/180", headers.get("TimeControl"));
assertEquals(null, headers.get("WhiteTimeControl"));
assertEquals(null, headers.get("BlackTimeControl"));
tcData = new TimeControlData();
tcData.tcW.clear();
tcData.tcW.add(new TimeControlField(15*60*1000,40,0));
tcData.tcW.add(new TimeControlField(5*60*1000+345,20,0));
tcData.tcW.add(new TimeControlField(0,10,1000));
tcData.tcW.add(new TimeControlField(0,0,5000));
tcData.tcB.clear();
tcData.tcB.add(new TimeControlField(60*1000,20,3004));
tcData.tcB.add(new TimeControlField(30*1000,0,0));
gt.setTimeControlData(tcData);
headers = new TreeMap<String,String>();
gt.getHeaders(headers);
assertEquals(null, headers.get("TimeControl"));
assertEquals("40/900:20/300.345:10/0+1:0+5", headers.get("WhiteTimeControl"));
assertEquals("20/60+3.004:30", headers.get("BlackTimeControl"));
tcData2 = gt.getTimeControlData();
assertTrue(!tcData2.isSymmetric());
assertEquals(tcData, tcData2);
pgn = gt.toPGN(options);
res = gt.readPGN(pgn, options);
assertTrue(res);
tcData2 = gt.getTimeControlData();
assertTrue(!tcData2.isSymmetric());
assertEquals(tcData, tcData2);
headers = new TreeMap<String,String>();
gt.getHeaders(headers);
assertEquals(null, headers.get("TimeControl"));
assertEquals("40/900:20/300.345:10/0+1:0+5", headers.get("WhiteTimeControl"));
assertEquals("20/60+3.004:30", headers.get("BlackTimeControl"));
tcData = new TimeControlData();
tcData.setTimeControl(2*60*1000, 0, 12000);
gt.setTimeControlData(tcData);
headers = new TreeMap<String,String>();
gt.getHeaders(headers);
assertEquals("120+12", headers.get("TimeControl"));
assertEquals(null, headers.get("WhiteTimeControl"));
assertEquals(null, headers.get("BlackTimeControl"));
// Test pgn data with extra white space
res = gt.readPGN("[TimeControl \" 40 / 5400 + 60 : 3.14 + 2.718 \"]", options);
assertTrue(res);
tcData = gt.getTimeControlData();
assertTrue(tcData.isSymmetric());
assertEquals(2, tcData.tcW.size());
TimeControlField tf = tcData.tcW.get(0);
assertEquals(40, tf.movesPerSession);
assertEquals(5400*1000, tf.timeControl);
assertEquals(60*1000, tf.increment);
tf = tcData.tcW.get(1);
assertEquals(0, tf.movesPerSession);
assertEquals(3140, tf.timeControl);
assertEquals(2718, tf.increment);
}
}