/* CuckooChess - A java 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 chess; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * * @author petero */ public class Game { protected List<Move> moveList = null; protected List<UndoInfo> uiInfoList = null; List<Boolean> drawOfferList = null; protected int currentMove; boolean pendingDrawOffer; GameState drawState; String drawStateMoveStr; // Move required to claim DRAW_REP or DRAW_50 GameState resignState; public Position pos = null; protected Player whitePlayer; protected Player blackPlayer; public Game(Player whitePlayer, Player blackPlayer) { this.whitePlayer = whitePlayer; this.blackPlayer = blackPlayer; handleCommand("new"); } /** * 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 boolean processString(String str) { if (handleCommand(str)) { return true; } if (getGameState() != GameState.ALIVE) { return false; } Move m = TextIO.stringToMove(pos, str); if (m == null) { return false; } UndoInfo ui = new UndoInfo(); pos.makeMove(m, ui); TextIO.fixupEPSquare(pos); while (currentMove < moveList.size()) { moveList.remove(currentMove); uiInfoList.remove(currentMove); drawOfferList.remove(currentMove); } moveList.add(m); uiInfoList.add(ui); drawOfferList.add(pendingDrawOffer); pendingDrawOffer = false; currentMove++; return true; } public final String getGameStateString() { switch (getGameState()) { case ALIVE: return ""; case WHITE_MATE: return "Game over, white mates!"; case BLACK_MATE: return "Game over, black mates!"; case WHITE_STALEMATE: case BLACK_STALEMATE: return "Game over, draw by stalemate!"; case DRAW_REP: { String ret = "Game over, draw by repetition!"; if ((drawStateMoveStr != null) && (drawStateMoveStr.length() > 0)) { ret = ret + " [" + drawStateMoveStr + "]"; } return ret; } case DRAW_50: { String ret = "Game over, draw by 50 move rule!"; if ((drawStateMoveStr != null) && (drawStateMoveStr.length() > 0)) { ret = ret + " [" + drawStateMoveStr + "]"; } return ret; } case DRAW_NO_MATE: return "Game over, draw by impossibility of mate!"; case DRAW_AGREE: return "Game over, draw by agreement!"; case RESIGN_WHITE: return "Game over, white resigns!"; case RESIGN_BLACK: return "Game over, black resigns!"; default: throw new RuntimeException(); } } /** * Get the last played move, or null if no moves played yet. */ public Move getLastMove() { Move m = null; if (currentMove > 0) { m = moveList.get(currentMove - 1); } return m; } public 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 of the game. */ public GameState getGameState() { MoveGen.MoveList moves = new MoveGen().pseudoLegalMoves(pos); MoveGen.removeIllegal(pos, moves); if (moves.size == 0) { if (MoveGen.inCheck(pos)) { return pos.whiteMove ? GameState.BLACK_MATE : GameState.WHITE_MATE; } else { return pos.whiteMove ? GameState.WHITE_STALEMATE : GameState.BLACK_STALEMATE; } } if (insufficientMaterial()) { return GameState.DRAW_NO_MATE; } if (resignState != GameState.ALIVE) { return resignState; } return drawState; } /** * Check if a draw offer is available. * @return True if the current player has the option to accept a draw offer. */ public boolean haveDrawOffer() { if (currentMove > 0) { return drawOfferList.get(currentMove - 1); } else { return false; } } /** * Handle a special command. * @param moveStr The command to handle * @return True if command handled, false otherwise. */ protected boolean handleCommand(String moveStr) { if (moveStr.equals("new")) { moveList = new ArrayList<Move>(); uiInfoList = new ArrayList<UndoInfo>(); drawOfferList = new ArrayList<Boolean>(); currentMove = 0; pendingDrawOffer = false; drawState = GameState.ALIVE; resignState = GameState.ALIVE; try { pos = TextIO.readFEN(TextIO.startPosFEN); } catch (ChessParseError ex) { throw new RuntimeException(); } whitePlayer.clearTT(); blackPlayer.clearTT(); activateHumanPlayer(); return true; } else if (moveStr.equals("undo")) { if (currentMove > 0) { pos.unMakeMove(moveList.get(currentMove - 1), uiInfoList.get(currentMove - 1)); currentMove--; pendingDrawOffer = false; drawState = GameState.ALIVE; resignState = GameState.ALIVE; return handleCommand("swap"); } else { System.out.println("Nothing to undo"); } return true; } else if (moveStr.equals("redo")) { if (currentMove < moveList.size()) { pos.makeMove(moveList.get(currentMove), uiInfoList.get(currentMove)); currentMove++; pendingDrawOffer = false; return handleCommand("swap"); } else { System.out.println("Nothing to redo"); } return true; } else if (moveStr.equals("swap") || moveStr.equals("go")) { Player tmp = whitePlayer; whitePlayer = blackPlayer; blackPlayer = tmp; return true; } else if (moveStr.equals("list")) { listMoves(); return true; } else if (moveStr.startsWith("setpos ")) { String fen = moveStr.substring(moveStr.indexOf(" ") + 1); Position newPos = null; try { newPos = TextIO.readFEN(fen); } catch (ChessParseError ex) { System.out.printf("Invalid FEN: %s (%s)%n", fen, ex.getMessage()); } if (newPos != null) { handleCommand("new"); pos = newPos; activateHumanPlayer(); } return true; } else if (moveStr.equals("getpos")) { String fen = TextIO.toFEN(pos); System.out.println(fen); return true; } else if (moveStr.startsWith("draw ")) { if (getGameState() == GameState.ALIVE) { String drawCmd = moveStr.substring(moveStr.indexOf(" ") + 1); return handleDrawCmd(drawCmd); } else { return true; } } else if (moveStr.equals("resign")) { if (getGameState()== GameState.ALIVE) { resignState = pos.whiteMove ? GameState.RESIGN_WHITE : GameState.RESIGN_BLACK; return true; } else { return true; } } else if (moveStr.startsWith("book")) { String bookCmd = moveStr.substring(moveStr.indexOf(" ") + 1); return handleBookCmd(bookCmd); } else if (moveStr.startsWith("time")) { try { String timeStr = moveStr.substring(moveStr.indexOf(" ") + 1); int timeLimit = Integer.parseInt(timeStr); whitePlayer.timeLimit(timeLimit, timeLimit, false); blackPlayer.timeLimit(timeLimit, timeLimit, false); return true; } catch (NumberFormatException nfe) { System.out.printf("Number format exception: %s\n", nfe.getMessage()); return false; } } else if (moveStr.startsWith("perft ")) { try { String depthStr = moveStr.substring(moveStr.indexOf(" ") + 1); int depth = Integer.parseInt(depthStr); MoveGen moveGen = new MoveGen(); long t0 = System.currentTimeMillis(); long nodes = perfT(moveGen, pos, depth); long t1 = System.currentTimeMillis(); System.out.printf("perft(%d) = %d, t=%.3fs\n", depth, nodes, (t1 - t0)*1e-3); } catch (NumberFormatException nfe) { System.out.printf("Number format exception: %s\n", nfe.getMessage()); return false; } return true; } else { return false; } } /** Swap players around if needed to make the human player in control of the next move. */ protected void activateHumanPlayer() { if (!(pos.whiteMove ? whitePlayer : blackPlayer).isHumanPlayer()) { Player tmp = whitePlayer; whitePlayer = blackPlayer; blackPlayer = tmp; } } public List<String> getPosHistory() { List<String> ret = new ArrayList<String>(); Position pos = new Position(this.pos); for (int i = currentMove; i > 0; i--) { pos.unMakeMove(moveList.get(i - 1), uiInfoList.get(i - 1)); } ret.add(TextIO.toFEN(pos)); // Store initial FEN StringBuilder moves = new StringBuilder(); for (int i = 0; i < moveList.size(); i++) { Move move = moveList.get(i); String strMove = TextIO.moveToString(pos, move, false); moves.append(String.format(" %s", strMove)); UndoInfo ui = new UndoInfo(); pos.makeMove(move, ui); } ret.add(moves.toString()); // Store move list string int numUndo = moveList.size() - currentMove; ret.add(((Integer)numUndo).toString()); return ret; } /** * Print a list of all moves. */ private void listMoves() { String movesStr = getMoveListString(false); System.out.printf("%s", movesStr); } final public String getMoveListString(boolean compressed) { StringBuilder ret = new StringBuilder(); // Undo all moves in move history. Position pos = new Position(this.pos); for (int i = currentMove; i > 0; i--) { pos.unMakeMove(moveList.get(i - 1), uiInfoList.get(i - 1)); } // Print all moves String whiteMove = ""; String blackMove = ""; for (int i = 0; i < currentMove; i++) { Move move = moveList.get(i); String strMove = TextIO.moveToString(pos, move, false); if (drawOfferList.get(i)) { strMove += " (d)"; } if (pos.whiteMove) { whiteMove = strMove; } else { blackMove = strMove; if (whiteMove.length() == 0) { whiteMove = "..."; } if (compressed) { ret.append(String.format("%d. %s %s ", pos.fullMoveCounter, whiteMove, blackMove)); } else { ret.append(String.format("%3d. %-10s %-10s%n", pos.fullMoveCounter, whiteMove, blackMove)); } whiteMove = ""; blackMove = ""; } UndoInfo ui = new UndoInfo(); pos.makeMove(move, ui); } if ((whiteMove.length() > 0) || (blackMove.length() > 0)) { if (whiteMove.length() == 0) { whiteMove = "..."; } if (compressed) { ret.append(String.format("%d. %s %s ", pos.fullMoveCounter, whiteMove, blackMove)); } else { ret.append(String.format("%3d. %-8s %-8s%n", pos.fullMoveCounter, whiteMove, blackMove)); } } String gameResult = getPGNResultString(); if (!gameResult.equals("*")) { if (compressed) { ret.append(gameResult); } else { ret.append(String.format("%s%n", gameResult)); } } return ret.toString(); } public final String getPGNResultString() { String gameResult = "*"; switch (getGameState()) { case ALIVE: break; case WHITE_MATE: case RESIGN_BLACK: gameResult = "1-0"; break; case BLACK_MATE: case RESIGN_WHITE: gameResult = "0-1"; break; case WHITE_STALEMATE: case BLACK_STALEMATE: case DRAW_REP: case DRAW_50: case DRAW_NO_MATE: case DRAW_AGREE: gameResult = "1/2-1/2"; break; } return gameResult; } /** Return a list of previous positions in this game, back to the last "zeroing" move. */ public ArrayList<Position> getHistory() { ArrayList<Position> posList = new ArrayList<Position>(); Position pos = new Position(this.pos); for (int i = currentMove; i > 0; i--) { if (pos.halfMoveClock == 0) break; pos.unMakeMove(moveList.get(i- 1), uiInfoList.get(i- 1)); posList.add(new Position(pos)); } Collections.reverse(posList); return posList; } private boolean handleDrawCmd(String drawCmd) { if (drawCmd.startsWith("rep") || drawCmd.startsWith("50")) { boolean rep = drawCmd.startsWith("rep"); Move m = null; String ms = drawCmd.substring(drawCmd.indexOf(" ") + 1); if (ms.length() > 0) { m = TextIO.stringToMove(pos, ms); } boolean valid; if (rep) { valid = false; List<Position> oldPositions = new ArrayList<Position>(); if (m != null) { UndoInfo ui = new UndoInfo(); Position tmpPos = new Position(pos); tmpPos.makeMove(m, ui); oldPositions.add(tmpPos); } oldPositions.add(pos); Position tmpPos = pos; for (int i = currentMove - 1; i >= 0; i--) { tmpPos = new Position(tmpPos); tmpPos.unMakeMove(moveList.get(i), uiInfoList.get(i)); oldPositions.add(tmpPos); } int repetitions = 0; Position firstPos = oldPositions.get(0); for (Position p : oldPositions) { if (p.drawRuleEquals(firstPos)) 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) { drawState = rep ? GameState.DRAW_REP : GameState.DRAW_50; drawStateMoveStr = null; if (m != null) { drawStateMoveStr = TextIO.moveToString(pos, m, false); } } else { pendingDrawOffer = true; if (m != null) { processString(ms); } } return true; } else if (drawCmd.startsWith("offer ")) { pendingDrawOffer = true; String ms = drawCmd.substring(drawCmd.indexOf(" ") + 1); if (TextIO.stringToMove(pos, ms) != null) { processString(ms); } return true; } else if (drawCmd.equals("accept")) { if (haveDrawOffer()) { drawState = GameState.DRAW_AGREE; } return true; } else { return false; } } private boolean handleBookCmd(String bookCmd) { if (bookCmd.equals("off")) { whitePlayer.useBook(false); blackPlayer.useBook(false); return true; } else if (bookCmd.equals("on")) { whitePlayer.useBook(true); whitePlayer.useBook(true); return true; } return false; } private boolean insufficientMaterial() { if (pos.pieceTypeBB[Piece.WQUEEN] != 0) return false; if (pos.pieceTypeBB[Piece.WROOK] != 0) return false; if (pos.pieceTypeBB[Piece.WPAWN] != 0) return false; if (pos.pieceTypeBB[Piece.BQUEEN] != 0) return false; if (pos.pieceTypeBB[Piece.BROOK] != 0) return false; if (pos.pieceTypeBB[Piece.BPAWN] != 0) return false; int wb = Long.bitCount(pos.pieceTypeBB[Piece.WBISHOP]); int wn = Long.bitCount(pos.pieceTypeBB[Piece.WKNIGHT]); int bb = Long.bitCount(pos.pieceTypeBB[Piece.BBISHOP]); int bn = Long.bitCount(pos.pieceTypeBB[Piece.BKNIGHT]); if (wb + wn + bb + bn <= 1) { return true; // King + bishop/knight vs king is draw } if (wn + bn == 0) { // Only bishops. If they are all on the same color, the position is a draw. long bMask = pos.pieceTypeBB[Piece.WBISHOP] | pos.pieceTypeBB[Piece.BBISHOP]; if (((bMask & BitBoard.maskDarkSq) == 0) || ((bMask & BitBoard.maskLightSq) == 0)) return true; } return false; } final static long perfT(MoveGen moveGen, Position pos, int depth) { if (depth == 0) return 1; long nodes = 0; MoveGen.MoveList moves = moveGen.pseudoLegalMoves(pos); MoveGen.removeIllegal(pos, moves); if (depth == 1) { int ret = moves.size; moveGen.returnMoveList(moves); return ret; } UndoInfo ui = new UndoInfo(); for (int mi = 0; mi < moves.size; mi++) { Move m = moves.m[mi]; pos.makeMove(m, ui); nodes += perfT(moveGen, pos, depth - 1); pos.unMakeMove(m, ui); } moveGen.returnMoveList(moves); return nodes; } }