// Game of Fanorona // David Eppstein, UC Irvine, 11 Jun 1997 // // Position representation and alpha-beta search public class Board { static final boolean PVS = true; // Flag to enable or disable principal variation search. In this modification to // alpha-beta, once we have one line with a reasonable evaluation, we search the other // lines with a zero-width window, and then search again (with a normal window) // any lines that don't fail low. static final boolean IID = true; static final int IID_PLY = 25; static final int IID_LIMIT = 55; // Internal iterated deepening: if we have no bestMove, and depth is at least // the limit, try a recursive search to depth-ply to get a good first move. static final int MIN_HASH_DEPTH = 15; // Avoid hashing beneath this depth, not worth the time static final int PLY = 10; // unit for fractional search extensions static final int FORCED_MOVE_EXTENSION = 5; // forced move is interesting but not odd static final int FORCED_CAPTURE_EXTENSION = 10; // forced capture might be sac, b v careful static final int ENDGAME_CAPTURE_EXTENSION = 5; // endgame capture interesting but not odd static final int FORCED_ENDGAME_CAPTURE = 10; // forced in endgame static final int MULTIPLE_CAPTURE_EXTENSION = 7; // extend multiple-capture sequences static final int EARLY_PASS_EXTENSION = 10; // extend when pass looks better than capture static final int WON_POSITION = 10000; // multiplied by no pieces on board at win static final int DECREMENTABLE = 5000; // big enough to use PLY_DECREMENT static final int PLY_DECREMENT = 1; // make far-away win look worse than nearer static Hash hash = new Hash(32768); // make a hash table for the search static EndgameDatabase endgameDatabase = null; // don't make db except application version public Board previousPosition; public long myPieces, opponentPieces; public long alreadyVisited; // moves already part of capture sequence volatile static int sequenceNumber = 0; // check value for early termination of search // Search results public int evaluation; // numerical value of current position public long bestMove; // move giving that numerical value public Board principalVariation; // the Board resulting from that move public Board child; // the Board we're currently searching public MoveGenerator moveGenerator; // the Move Generator used by this board public boolean forced; // only one move exists -- used to determine extensions public boolean hasPrincipalVariation; // does principalVariation point to anything useful? // type of evaluation -- upper bound on true value, lower bound, or exact // for communication between search and hash table static public final int EVAL_UPPER_BOUND = 0; static public final int EVAL_LOWER_BOUND = 1; static public final int EVAL_EXACT = 2; // search statistics public static long nodeCount; // for callers of alphabeta to collect stats public static long leafCount; public static final boolean COLLECT_EXTRA_STATISTICS = false; public static long endgameEvalCount; public static long boardConsCount; public static long moveGenConsCount; public static long pvChangeCount; // bunch o' quick booleans public final boolean whiteToMove() { // is white the player on move? return (myPieces & Bits.IS_WHITE) != 0; } public final boolean midCapture() { // was last move a capture? return myPieces < 0; } public final boolean wasPass() { // was last move a pass? (opponent didn't move) return (myPieces ^ opponentPieces) < 0; } public final boolean wasShuffle() { // opponent moved but didn't capture? return opponentPieces >= 0; } public final boolean gameOver() { // has game finished? return (myPieces & Bits.ON_BOARD) == 0 || (opponentPieces & Bits.ON_BOARD) == 0 || !(new MoveGenerator(this)).hasMoreElements(); } public final boolean whiteWins() { // if so, is player on move the winner? return !(whiteToMove() ^ ((opponentPieces & Bits.ON_BOARD) == 0)); } // make initial board position public Board() { previousPosition = null; myPieces = Bits.INITIAL_BOT | Bits.IS_WHITE; opponentPieces = Bits.INITIAL_TOP; alreadyVisited = 0; } public Board(boolean blackAtTop, boolean whiteGoesFirst) { previousPosition = null; boolean ImOnTop = (blackAtTop ^ whiteGoesFirst); if (ImOnTop) { myPieces = Bits.INITIAL_TOP; opponentPieces = Bits.INITIAL_BOT; } else { myPieces = Bits.INITIAL_BOT; opponentPieces = Bits.INITIAL_TOP; } if (whiteGoesFirst) myPieces |= Bits.IS_WHITE; else opponentPieces |= Bits.IS_WHITE; alreadyVisited = 0; } // Make board for position after move // Arg is bitboard of changed piece positions public Board(Board prev, long move) { previousPosition = prev; long captures = prev.opponentPieces & move; if (captures != 0) { // capturing anything? opponentPieces = (prev.opponentPieces ^ captures) | Bits.CAPTURED; move ^= captures; alreadyVisited = prev.alreadyVisited | move; myPieces = (prev.myPieces ^ move) | Bits.CAPTURED; } else { opponentPieces = prev.myPieces ^ move; myPieces = prev.opponentPieces &~ Bits.CAPTURED; alreadyVisited = 0; } } // Create a child position for the given move. // The logic here should match Board(Board,long). public final void setChild(long move) { if (child == null) { child = new Board(this,move); if (COLLECT_EXTRA_STATISTICS) boardConsCount++; } else { long captures = opponentPieces & move; if (captures != 0) { // capturing anything? child.opponentPieces = (opponentPieces ^ captures) | Bits.CAPTURED; move ^= captures; child.alreadyVisited = alreadyVisited | move; child.myPieces = (myPieces ^ move) | Bits.CAPTURED; } else { child.opponentPieces = myPieces ^ move; child.myPieces = opponentPieces &~ Bits.CAPTURED; child.alreadyVisited = 0; } } child.bestMove = -1L; } // set up to generate moves available from this position public final void setMoveGenerator() { if (moveGenerator == null) { moveGenerator = new MoveGenerator(this); if (COLLECT_EXTRA_STATISTICS) moveGenConsCount++; } else moveGenerator.reset(this); } // Set up a move as the principal variation of a position // assumes that it is already in child // // This routine is key to our strategy of avoiding too many memory allocations. // During the search, the set of Board objects we create has a comb-like structure, // in which the objects in a linked list formed by child pointers each have dangling // off them another linked list formed by principalVariation pointers; the size of // this comb is quadratic in the search depth (therefore small compared to the whole // search tree). This routine maintains this comb-like structure, while changing // the principal variation of a node to a new line found while searching the child, // by switching the positions of the node's child and principalVariation pointers. // public final void setPrincipalVariation() { if (COLLECT_EXTRA_STATISTICS) pvChangeCount++; if (principalVariation == null) { principalVariation = child; child = principalVariation.child; if (child != null) child.previousPosition = this; } else { Board temp = child; child = principalVariation; principalVariation = temp; child.child = principalVariation.child; if (child.child != null) child.child.previousPosition = child; } principalVariation.child = null; hasPrincipalVariation = true; } // search; sets principalVariation and evaluation // search gets aborted whenever sequenceNumber != Board.sequenceNumber public final void alphaBeta(int depth, int alpha, int beta, int sequenceNumber) { nodeCount++; hasPrincipalVariation = false; int hashKey = -1; if (myPieces >= 0) { // if (!midCapture()) // Time for a leaf evaluation? if (depth <= 0 && Eval.evaluate(this, alpha, beta, depth)) { leafCount++; return; } // See if we've already hashed the results of searching this position hashKey = hash.hashKey(this); if (hash.getHash(this, hashKey, alpha, beta, depth)) { if (bestMove >= 0 && evaluation >= alpha && evaluation <= beta) { setChild(bestMove); child.hasPrincipalVariation = false; setPrincipalVariation(); } return; } } // Check if aborting (after leaf eval and hashing to speed up non-aborted search) if (sequenceNumber != Board.sequenceNumber) return; // POSITION IS NOT TERMINAL AND NOT HASHED. WE HAVE TO DO A RECURSIVE SEARCH... // Find first move to be searched long move; boolean moveGeneratorIsSet = false; if (bestMove >= 0) move = bestMove; // history heuristic else if (IID && depth >= IID_LIMIT) { // internal iterated deepening alphaBeta(depth - IID_PLY, alpha, beta, sequenceNumber); move = bestMove; } else { setMoveGenerator(); // make move generator now we know we need one moveGeneratorIsSet = true; move = moveGenerator.nextElement(); forced = !(moveGenerator.hasMoreElements()); } long firstMove = move; // Compute extensions int newDepth = depth - PLY; int captureExtension = 0; if (forced) { newDepth += FORCED_MOVE_EXTENSION; if (opponentPieces >= 0) captureExtension = FORCED_ENDGAME_CAPTURE - FORCED_MOVE_EXTENSION; else captureExtension = FORCED_CAPTURE_EXTENSION - FORCED_MOVE_EXTENSION; } else if (opponentPieces >= 0) // if (wasShuffle()) captureExtension = ENDGAME_CAPTURE_EXTENSION; else if (myPieces < 0) // if (midCapture()) captureExtension = MULTIPLE_CAPTURE_EXTENSION; // Set up alpha-beta parameters evaluation = -Integer.MAX_VALUE; int evalType = EVAL_UPPER_BOUND; // assume u.b. until we reach eval > alpha int pvsBeta = beta; // MAIN ALPHA-BETA LOOP while (move >= 0) { setChild(move); // Call alpha beta recursively to evaluate child position int moveEval; if (child.myPieces >= 0) { // if (!child.midCapture()) child.alphaBeta(newDepth, -pvsBeta, -alpha, sequenceNumber); moveEval = -child.evaluation; if (moveEval >= pvsBeta && moveEval < beta) { child.alphaBeta(newDepth, -beta, -alpha, sequenceNumber); moveEval = -child.evaluation; } if (moveEval > evaluation && move == 0 && !forced) { // we think a pass is the best move? try again a little deeper, // it looks suspiciously like a horizon effect delaying tactic child.alphaBeta(newDepth + EARLY_PASS_EXTENSION, -beta, -alpha, sequenceNumber); moveEval = -child.evaluation; } } else if ((child.opponentPieces & Bits.ON_BOARD) != 0) { // disable pvs for capture nodes, it only makes sense for when turn ends child.alphaBeta(newDepth + captureExtension, alpha, beta, sequenceNumber); moveEval = child.evaluation; } else { // all pieces captured, fail high but hash exact eval evaluation = Bits.count(myPieces & Bits.ON_BOARD) * WON_POSITION; evalType = EVAL_EXACT; bestMove = move; child.hasPrincipalVariation = false; setPrincipalVariation(); break; } // Test if move is best so far, and if so how it compares to alpha and beta if (moveEval > evaluation) { bestMove = move; evaluation = moveEval; if (moveEval > alpha) { alpha = moveEval; if (moveEval >= beta) { evalType = EVAL_LOWER_BOUND; break; // fail high, prune search by breaking from loop } evalType = EVAL_EXACT; setPrincipalVariation(); } } // Prepare for next iteration by getting next move if (forced) break; if (!moveGeneratorIsSet) { setMoveGenerator(); moveGeneratorIsSet = true; } move = moveGenerator.nextElement(); if (move == firstMove) move = moveGenerator.nextElement(); // Apply principal variation search: make artificially narrow window // for searches after the first, then widen when necessary. // But this gets screwed up by PLY_DECREMENT so don't use it in that case. if (PVS && alpha < DECREMENTABLE && -alpha > -DECREMENTABLE) pvsBeta = alpha+1; } // here after the main loop. adjust eval; hash the position for the next time we see it. if (sequenceNumber != Board.sequenceNumber) return; // don't hash trash if (evaluation > DECREMENTABLE) evaluation -= PLY_DECREMENT; else if (evaluation < -DECREMENTABLE) evaluation += PLY_DECREMENT; if (hashKey >= 0) hash.setHash(this, hashKey, evalType, depth); } }