/* DroidFish - An Android chess program. Copyright (C) 2011 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 com.if3games.chessonline.gamelogic; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; import com.if3games.chessonline.PGNOptions; import com.if3games.chessonline.gamelogic.GameTree.Node; /** * * @author petero */ public class Game { boolean pendingDrawOffer; GameTree tree; TimeControl timeController; private boolean gamePaused; /** If true, add new moves as mainline moves. */ private AddMoveBehavior addMoveBehavior; PgnToken.PgnTokenReceiver gameTextListener; public Game(PgnToken.PgnTokenReceiver gameTextListener, TimeControlData tcData) { this.gameTextListener = gameTextListener; timeController = new TimeControl(); timeController.setTimeControl(tcData); gamePaused = false; newGame(); tree.setTimeControlData(tcData); } /** De-serialize from input stream. */ final void readFromStream(DataInputStream dis, int version) throws IOException, ChessParseError { tree.readFromStream(dis, version); if (version >= 3) timeController.readFromStream(dis, version); updateTimeControl(true); } /** Serialize to output stream. */ final synchronized void writeToStream(DataOutputStream dos) throws IOException { tree.writeToStream(dos); timeController.writeToStream(dos); } public final void setGamePaused(boolean gamePaused) { if (gamePaused != this.gamePaused) { this.gamePaused = gamePaused; updateTimeControl(false); } } /** Controls behavior when a new move is added to the game.*/ public static enum AddMoveBehavior { /** Add the new move first in the list of variations. */ ADD_FIRST, /** Add the new move last in the list of variations. */ ADD_LAST, /** Remove all variations not matching the new move. */ REPLACE }; /** Set whether new moves are entered as mainline moves or variations. */ public final void setAddFirst(AddMoveBehavior amb) { addMoveBehavior = amb; } /** Sets start position and discards the whole game tree. */ final void setPos(Position pos) { tree.setStartPos(new Position(pos)); updateTimeControl(false); } final boolean readPGN(String pgn, PGNOptions options) throws ChessParseError { boolean ret = tree.readPGN(pgn, options); if (ret) { TimeControlData tcData = tree.getTimeControlData(); if (tcData != null) timeController.setTimeControl(tcData); updateTimeControl(tcData != null); } return ret; } final Position currPos() { return tree.currentPos; } final Position prevPos() { Move m = tree.currentNode.move; if (m != null) { tree.goBack(); Position ret = new Position(currPos()); tree.goForward(-1); return ret; } else { return currPos(); } } public final Move getNextMove() { if (canRedoMove()) { tree.goForward(-1); Move ret = tree.currentNode.move; tree.goBack(); return ret; } else { return null; } } /** * Update the game state according to move/command string from a player. * @param str The move or command to process. * @return True if str was understood, false otherwise. */ public final boolean processString(String str) { if (getGameState() != GameState.ALIVE) return false; if (str.startsWith("draw ")) { String drawCmd = str.substring(str.indexOf(" ") + 1); handleDrawCmd(drawCmd); return true; } else if (str.equals("resign")) { addToGameTree(new Move(0, 0, 0), "resign"); return true; } else if(str.equals("gms_accept")) { addToGameTree(new Move(0, 0, 0), "draw accept"); return true; } Move m = TextIO.UCIstringToMove(str); if (m != null) if (!TextIO.isValid(currPos(), m)) m = null; if (m == null) m = TextIO.stringToMove(currPos(), str); if (m == null) return false; addToGameTree(m, pendingDrawOffer ? "draw offer" : ""); return true; } private final void addToGameTree(Move m, String playerAction) { if (m.equals(new Move(0, 0, 0))) { // Don't create more than one game-ending move at a node List<Move> varMoves = tree.variations(); for (int i = varMoves.size() - 1; i >= 0; i--) { if (varMoves.get(i).equals(m)) { tree.deleteVariation(i); } } } boolean movePresent = false; int varNo; { ArrayList<Move> varMoves = tree.variations(); int nVars = varMoves.size(); if (addMoveBehavior == AddMoveBehavior.REPLACE) { boolean modified = false; for (int i = nVars-1; i >= 0; i--) { if (!m.equals(varMoves.get(i))) { tree.deleteVariation(i); modified = true; } } if (modified) { varMoves = tree.variations(); nVars = varMoves.size(); } } for (varNo = 0; varNo < nVars; varNo++) { if (varMoves.get(varNo).equals(m)) { movePresent = true; break; } } } if (!movePresent) { String moveStr = TextIO.moveToUCIString(m); varNo = tree.addMove(moveStr, playerAction, 0, "", ""); } int newPos = 0; if (addMoveBehavior == AddMoveBehavior.ADD_LAST) newPos = varNo; tree.reorderVariation(varNo, newPos); tree.goForward(newPos); int remaining = timeController.moveMade(System.currentTimeMillis(), !gamePaused); tree.setRemainingTime(remaining); updateTimeControl(true); pendingDrawOffer = false; } private final void updateTimeControl(boolean discardElapsed) { Position currPos = currPos(); int move = currPos.fullMoveCounter; boolean wtm = currPos.whiteMove; if (discardElapsed || (move != timeController.currentMove) || (wtm != timeController.whiteToMove)) { int whiteBaseTime = tree.getRemainingTime(true, timeController.getInitialTime(true)); int blackBaseTime = tree.getRemainingTime(false, timeController.getInitialTime(false)); timeController.setCurrentMove(move, wtm, whiteBaseTime, blackBaseTime); } long now = System.currentTimeMillis(); boolean stopTimer = gamePaused || (getGameState() != GameState.ALIVE); if (!stopTimer) { try { if (TextIO.readFEN(TextIO.startPosFEN).equals(currPos)) stopTimer = true; } catch (ChessParseError e) { } } if (stopTimer) { timeController.stopTimer(now); } else { timeController.startTimer(now); } } public final String getDrawInfo(boolean localized) { return tree.getGameStateInfo(localized); } /** * Get the last played move, or null if no moves played yet. */ public final Move getLastMove() { return tree.currentNode.move; } /** Return true if there is a move to redo. */ public final boolean canRedoMove() { int nVar = tree.variations().size(); return nVar > 0; } /** Get number of variations in current game position. */ public final int numVariations() { if (tree.currentNode == tree.rootNode) return 1; tree.goBack(); int nChildren = tree.variations().size(); tree.goForward(-1); return nChildren; } /** Get current variation in current position. */ public final int currVariation() { if (tree.currentNode == tree.rootNode) return 0; tree.goBack(); int defChild = tree.currentNode.defaultChild; tree.goForward(-1); return defChild; } /** Go to a new variation in the game tree. */ public final void changeVariation(int delta) { if (tree.currentNode == tree.rootNode) return; tree.goBack(); int defChild = tree.currentNode.defaultChild; int nChildren = tree.variations().size(); int newChild = defChild + delta; newChild = Math.max(newChild, 0); newChild = Math.min(newChild, nChildren - 1); tree.goForward(newChild); pendingDrawOffer = false; updateTimeControl(true); } /** Move current variation up/down in the game tree. */ public final void moveVariation(int delta) { if (tree.currentNode == tree.rootNode) return; tree.goBack(); int varNo = tree.currentNode.defaultChild; int nChildren = tree.variations().size(); int newPos = varNo + delta; newPos = Math.max(newPos, 0); newPos = Math.min(newPos, nChildren - 1); tree.reorderVariation(varNo, newPos); tree.goForward(newPos); pendingDrawOffer = false; updateTimeControl(true); } /** Delete whole game sub-tree rooted at current position. */ public final void removeSubTree() { if (getLastMove() != null) { tree.goBack(); int defChild = tree.currentNode.defaultChild; tree.deleteVariation(defChild); } else { while (canRedoMove()) tree.deleteVariation(0); } pendingDrawOffer = false; updateTimeControl(true); } public static enum GameState { ALIVE, WHITE_MATE, // White mates BLACK_MATE, // Black mates WHITE_STALEMATE, // White is stalemated BLACK_STALEMATE, // Black is stalemated DRAW_REP, // Draw by 3-fold repetition DRAW_50, // Draw by 50 move rule DRAW_NO_MATE, // Draw by impossibility of check mate DRAW_AGREE, // Draw by agreement RESIGN_WHITE, // White resigns RESIGN_BLACK // Black resigns } /** * Get the current state (draw, mate, ongoing, etc) of the game. */ public final GameState getGameState() { return tree.getGameState(); } /** * Check if a draw offer is available. * @return True if the current player has the option to accept a draw offer. */ public final boolean haveDrawOffer() { return tree.currentNode.playerAction.equals("draw offer"); } public final void undoMove() { Move m = tree.currentNode.move; if (m != null) { tree.goBack(); pendingDrawOffer = false; updateTimeControl(true); } } public final void redoMove() { if (canRedoMove()) { tree.goForward(-1); pendingDrawOffer = false; updateTimeControl(true); } } /** Go to given node in game tree. * @return True if current node changed, false otherwise. */ public final boolean goNode(Node node) { if (!tree.goNode(node)) return false; pendingDrawOffer = false; updateTimeControl(true); return true; } public final void newGame() { tree = new GameTree(gameTextListener); timeController.reset(); pendingDrawOffer = false; updateTimeControl(true); } /** * Return the last zeroing position and a list of moves * to go from that position to the current position. */ public final Pair<Position, ArrayList<Move>> getUCIHistory() { Pair<List<Node>, Integer> ml = tree.getMoveList(); List<Node> moveList = ml.first; Position pos = new Position(tree.startPos); ArrayList<Move> mList = new ArrayList<Move>(); Position currPos = new Position(pos); UndoInfo ui = new UndoInfo(); int nMoves = ml.second; for (int i = 0; i < nMoves; i++) { Node n = moveList.get(i); mList.add(n.move); currPos.makeMove(n.move, ui); if (currPos.halfMoveClock == 0) { pos = new Position(currPos); mList.clear(); } } return new Pair<Position, ArrayList<Move>>(pos, mList); } private final void handleDrawCmd(String drawCmd) { Position pos = tree.currentPos; if (drawCmd.startsWith("rep") || drawCmd.startsWith("50")) { boolean rep = drawCmd.startsWith("rep"); Move m = null; String ms = null; int firstSpace = drawCmd.indexOf(" "); if (firstSpace >= 0) { ms = drawCmd.substring(firstSpace + 1); if (ms.length() > 0) { m = TextIO.stringToMove(pos, ms); } } boolean valid; if (rep) { valid = false; UndoInfo ui = new UndoInfo(); int repetitions = 0; Position posToCompare = new Position(tree.currentPos); if (m != null) { posToCompare.makeMove(m, ui); repetitions = 1; } Pair<List<Node>, Integer> ml = tree.getMoveList(); List<Node> moveList = ml.first; Position tmpPos = new Position(tree.startPos); if (tmpPos.drawRuleEquals(posToCompare)) repetitions++; int nMoves = ml.second; for (int i = 0; i < nMoves; i++) { Node n = moveList.get(i); tmpPos.makeMove(n.move, ui); TextIO.fixupEPSquare(tmpPos); if (tmpPos.drawRuleEquals(posToCompare)) repetitions++; } if (repetitions >= 3) valid = true; } else { Position tmpPos = new Position(pos); if (m != null) { UndoInfo ui = new UndoInfo(); tmpPos.makeMove(m, ui); } valid = tmpPos.halfMoveClock >= 100; } if (valid) { String playerAction = rep ? "draw rep" : "draw 50"; if (m != null) playerAction += " " + TextIO.moveToString(pos, m, false, false); addToGameTree(new Move(0, 0, 0), playerAction); } else { pendingDrawOffer = true; if (m != null) { processString(ms); } } } else if (drawCmd.startsWith("offer ")) { pendingDrawOffer = true; String ms = drawCmd.substring(drawCmd.indexOf(" ") + 1); if (TextIO.stringToMove(pos, ms) != null) { processString(ms); } } else if (drawCmd.equals("accept")) { if (haveDrawOffer()) addToGameTree(new Move(0, 0, 0), "draw accept"); } } }