diff --git a/DroidFish/src/org/petero/droidfish/DroidFish.java b/DroidFish/src/org/petero/droidfish/DroidFish.java index c5f543d..308d0df 100644 --- a/DroidFish/src/org/petero/droidfish/DroidFish.java +++ b/DroidFish/src/org/petero/droidfish/DroidFish.java @@ -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(); diff --git a/DroidFish/src/org/petero/droidfish/gamelogic/DroidChessController.java b/DroidFish/src/org/petero/droidfish/gamelogic/DroidChessController.java index 0202ff4..eab3197 100644 --- a/DroidFish/src/org/petero/droidfish/gamelogic/DroidChessController.java +++ b/DroidFish/src/org/petero/droidfish/gamelogic/DroidChessController.java @@ -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); diff --git a/DroidFish/src/org/petero/droidfish/gamelogic/Game.java b/DroidFish/src/org/petero/droidfish/gamelogic/Game.java index e115fdd..5656403 100644 --- a/DroidFish/src/org/petero/droidfish/gamelogic/Game.java +++ b/DroidFish/src/org/petero/droidfish/gamelogic/Game.java @@ -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; } diff --git a/DroidFish/src/org/petero/droidfish/gamelogic/GameTree.java b/DroidFish/src/org/petero/droidfish/gamelogic/GameTree.java index aed0b51..e0e6034 100644 --- a/DroidFish/src/org/petero/droidfish/gamelogic/GameTree.java +++ b/DroidFish/src/org/petero/droidfish/gamelogic/GameTree.java @@ -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(); 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 stringToTCFields(String tcStr) { + String[] fields = tcStr.split(":"); + int nf = fields.length; + ArrayList ret = new ArrayList(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 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 tcW = stringToTCFields(whiteTimeControl); + ArrayList tcB = stringToTCFields(blackTimeControl); + if (!tcW.isEmpty() && !tcB.isEmpty()) { + TimeControlData tcData = new TimeControlData(); + tcData.tcW = tcW; + tcData.tcB = tcB; + return tcData; + } + } + if (!timeControl.equals("?")) { + ArrayList 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 = "?"; + } + } } diff --git a/DroidFish/src/org/petero/droidfish/gamelogic/TimeControl.java b/DroidFish/src/org/petero/droidfish/gamelogic/TimeControl.java index 1204b28..0cf82a6 100644 --- a/DroidFish/src/org/petero/droidfish/gamelogic/TimeControl.java +++ b/DroidFish/src/org/petero/droidfish/gamelogic/TimeControl.java @@ -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 diff --git a/DroidFish/src/org/petero/droidfish/gamelogic/TimeControlData.java b/DroidFish/src/org/petero/droidfish/gamelogic/TimeControlData.java index f579dea..2efcb56 100644 --- a/DroidFish/src/org/petero/droidfish/gamelogic/TimeControlData.java +++ b/DroidFish/src/org/petero/droidfish/gamelogic/TimeControlData.java @@ -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 tcW, tcB; - TimeControlData() { + /** Constructor. Set a default time control. */ + public TimeControlData() { tcW = new ArrayList(); tcW.add(new TimeControlField(5*60*1000, 60, 0)); tcB = new ArrayList(); 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(); tcW.add(new TimeControlField(time, moves, inc)); @@ -35,4 +37,32 @@ public final class TimeControlData { public ArrayList 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 a1, + ArrayList 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; + } } diff --git a/DroidFishTest/src/org/petero/droidfish/gamelogic/GameTreeTest.java b/DroidFishTest/src/org/petero/droidfish/gamelogic/GameTreeTest.java index 27d920e..f3bff74 100644 --- a/DroidFishTest/src/org/petero/droidfish/gamelogic/GameTreeTest.java +++ b/DroidFishTest/src/org/petero/droidfish/gamelogic/GameTreeTest.java @@ -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 headers = new TreeMap(); + 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(); + 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(); + 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(); + 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(); + 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); + } }