def _enumMoves_pawn(board, color, fromPosn): pawnStartRank = ChessBoard.PAWN_STARTING_RANKS[color] pawnDirection = [-1, 1][color == ChessBoard.WHITE] oneAhead = fromPosn.getTranslatedBy(pawnDirection, 0) twoAhead = fromPosn.getTranslatedBy(pawnDirection * 2, 0) moves = [] if board[oneAhead] is None: moves.append(oneAhead) if (fromPosn.rankN == pawnStartRank and board[oneAhead] is None and board[twoAhead] is None): moves.append(twoAhead) otherColor = ChessBoard.getOtherColor(color) otherPawnStartRank = ChessBoard.PAWN_STARTING_RANKS[otherColor] for fileDelta in [-1, 1]: toPosn = fromPosn.getTranslatedBy(pawnDirection, fileDelta) if __enumMoves_isOnBoard(toPosn): enemyP = (board[toPosn] is not None and board[toPosn].color == otherColor) enpP = (board[toPosn] is None and board.flag_enpassant[otherColor][toPosn.fileN] and twoAhead.rankN == otherPawnStartRank) if enemyP or enpP: moves.append(toPosn) return moves
def heuristicPcsUnderAttack(color, board): """Return the value of the given color's pieces that are under attack @param color: The color of the pieces to test - one of maverick.data.ChessBoard.WHITE or maverick.data.ChessBoard.BLACK @param board: a ChessBoard object @return: A number representing the value of the given color's pieces that are under attack, weighted by piece value""" otherColor = ChessBoard.getOtherColor(color) # Get posns the enemy can move to enemyMoveDstPosns = [m[1] for m in enumMoves(board, otherColor)] # Record which friendly pieces are under attack # A piece is under attack if its posn (pcPsn) is in myPieces # TODO: use filter and lambda piecesUnderAttack = [pcPsn for pcPsn in board.getPiecesOfColor(color) if pcPsn in enemyMoveDstPosns] # Sum weighted values of under-attack pieces weightedTotal = 0 for piecePosn in piecesUnderAttack: piece = board[piecePosn] # Check if there is a value for this piece in the mappings if piece.pieceType in PIECE_VALUES: weightedTotal += PIECE_VALUES[piece.pieceType] # Compress return value into range [-1..1] return 1 - 2 * (weightedTotal / MAX_TOTAL_PIECE_VALUE)
def _getBoard2(): _w = ChessBoard.WHITE _b = ChessBoard.BLACK return ChessBoard(startLayout=[[ ChessPiece(_w, ChessBoard.ROOK), None, None, None, ChessPiece(_w, "K"), None, None, ChessPiece(_w, "R") ], [ ChessPiece(_w, "P"), ChessPiece(_w, "P"), ChessPiece(_w, "P"), None, None, ChessPiece(_w, "P"), ChessPiece(_w, "P"), ChessPiece(_w, "P") ], [ None, None, None, ChessPiece(_w, "B"), None, None, None, None ], [ None, None, None, ChessPiece(_w, "P"), ChessPiece(_w, "N"), None, None, None ], [ None, None, None, None, ChessPiece(_w, "N"), None, None, ChessPiece(_w, "Q") ], [ None, ChessPiece(_b, "P"), None, None, ChessPiece(_b, "P"), ChessPiece(_b, "B"), None, None ], [ ChessPiece(_b, "P"), ChessPiece(_b, "B"), ChessPiece(_b, "P"), ChessPiece(_b, "P"), ChessPiece(_b, "Q"), None, ChessPiece(_b, "P"), ChessPiece(_b, "P") ], [ ChessPiece(_b, "R"), ChessPiece(_b, "N"), None, None, None, ChessPiece(_b, "R"), ChessPiece(_b, "K"), None ]], startEnpassantFlags={ _b: [False] * 8, _w: [False] * 8 }, startCanCastleFlags={ _b: (False, False), _w: (True, False) })
def getBoard8(): return ChessBoard(startLayout=[[ None, None, ChessPiece(_w, "B"), ChessPiece(_w, "Q"), ChessPiece(_w, "K"), ChessPiece(_w, "R"), None, None ], [ ChessPiece(_w, "P"), None, ChessPiece(_w, "P"), None, None, ChessPiece(_w, "P"), None, None ], [ None, ChessPiece(_w, "N"), None, None, ChessPiece(_b, "Q"), ChessPiece(_w, "N"), None, ChessPiece(_w, "P") ], [ None, ChessPiece(_w, "P"), None, None, ChessPiece(_w, "R"), ChessPiece(_b, "B"), ChessPiece(_w, "P"), None ], [ None, None, None, ChessPiece(_b, "P"), ChessPiece(_w, "P"), None, None, None ], [ ChessPiece(_b, "P"), None, ChessPiece(_b, "N"), None, ChessPiece(_b, "P"), ChessPiece(_b, "P"), ChessPiece(_b, "N"), None ], [ None, ChessPiece(_b, "P"), None, None, ChessPiece(_b, "K"), None, None, ChessPiece(_b, "P") ], [ ChessPiece(_b, "R"), None, ChessPiece(_b, "B"), None, None, ChessPiece(_b, "R"), None, None ]], startEnpassantFlags={ _b: [False] * 8, _w: [False] * 8 }, startCanCastleFlags={ _b: (False, False), _w: (False, False) })
def getBoard5(): return ChessBoard(startLayout=[[ None, None, None, None, None, ChessPiece("W", "R"), ChessPiece("W", "N"), ChessPiece("W", "R") ], [ None, None, None, ChessPiece("W", "K"), None, None, ChessPiece("W", "P"), None ], [ None, ChessPiece("B", "P"), ChessPiece("W", "P"), ChessPiece("W", "P"), None, ChessPiece("W", "P"), None, None ], [ ChessPiece("W", "P"), None, None, ChessPiece("B", "Q"), None, None, ChessPiece("B", "N"), ChessPiece("W", "P") ], [ ChessPiece("B", "P"), ChessPiece("W", "N"), None, None, None, ChessPiece("B", "P"), None, None ], [ None, ChessPiece("W", "Q"), None, None, None, ChessPiece("W", "B"), ChessPiece("B", "P"), None ], [ None, ChessPiece("B", "P"), None, None, ChessPiece("B", "P"), ChessPiece("B", "K"), None, ChessPiece("B", "P") ], [ ChessPiece("B", "R"), ChessPiece("B", "N"), ChessPiece("B", "B"), None, None, None, None, ChessPiece("B", "R") ]], startEnpassantFlags={ 'B': [False] * 8, 'W': [False] * 8 }, startCanCastleFlags={ 'B': [False, False], 'W': [False, False] })
def _request_getState(self, playerID, gameID): """Return the current state of the game The state contains information about the playerIDs of the black and white players, whose turn it is, the current board state, and the game history. @param playerID: the integer of the playerID of the player on which _request_getState is being called @param gameID: the integer gameID of an in-progress game""" response = self._makeRequest("GET_STATE", playerID=playerID, gameID=gameID) ## TODO (James): check constants to validate received data # Construct board object from serialized data # The serialized board layout rawLayout = response["board"]["layout"] # The deserialized board layout layout = MaverickClient.__request_getState_deserializeLayout(rawLayout) # The serialized history rawHst = response["history"] # Deserialize the history histList = MaverickClient.__request_getState_deserializeHistory(rawHst) curEnPassantFlags = response["board"]["enPassantFlags"] curCastleFlags = response["board"]["canCastleFlags"] curBoardObj = ChessBoard(startLayout=layout, startEnpassantFlags=curEnPassantFlags, startCanCastleFlags=curCastleFlags) # Build up return dictionary stateDict = {} stateDict["youAreColor"] = response["youAreColor"] stateDict["isWhitesTurn"] = response["isWhitesTurn"] stateDict["board"] = curBoardObj stateDict["history"] = histList return stateDict
def _boardSearch(self, board, color, depth, alpha, beta, isMaxNode, stopSrchTime): """Performs a board via alpha-beta pruning/quiescence search NOTE: Not guaranteed to stop promptly at stopSrchTime - may take some time to terminate. Leave a time buffer. Selectively explores past the final depth if many pieces have been captured recently, by calling quiescent search @param board: The starting board state to evaluate @param color: The color of the player to generate a move for @param depth: The number of plies forward that should be explored @param alpha: Nodes with a likability below this will be ignored @param beta: Nodes with a likability above this will be ignored @param isMaxNode: Is this a beta node? (Is this node seeking to maximize the value of child nodes?) @param stopSrchTime: Time at which the search should begin to terminate @param nodesVisited: The number of nodes already searched @return: A tuple with the following elements: 1. None, or a move of the form (fromChessPosn, toChessPosn) representing the next move that should be made by the given player 2. The likability of this move's path in the tree, as followed by the search and as determined by the likability of the leaf node terminating the path 3. The number of nodes visited in the search Implementation based on information found here: http://bit.ly/t1dHKA""" ## TODO (James): Check timeout less than once per iteration ## TODO (James): Make logging conditional - temporarily disabled ## TODO (James): Set this to True before handing in! # Whether to limit based on wall clock time or number of nodes seen USE_WALL_CLOCK = False # Number of nodes to visit (for when wall clock time is not used) NUM_NODES_TO_VISIT = 1200 #logStrF = "Performing minimax search to depth {0}.".format(depth) #QLAI._logger.debug(logStrF) # Note that we've visited a node nodesVisited = 1 otherColor = ChessBoard.getOtherColor(color) # Check if we are at a leaf node if (depth == 0): (a, b) = self._quiescentSearch(board, color, alpha, beta, isMaxNode) return (a, b, 1) # Check if we should otherwise terminate elif (time() > stopSrchTime or board.isKingCheckmated(color) or board.isKingCheckmated(otherColor)): return (None, evaluateBoardLikability(color, board, self.heuristicWgts), nodesVisited) else: moveChoices = enumMoves(board, color) #logStrF = "Considering {0} poss. moves".format(len(moveChoices)) #QLAI._logger.debug(logStrF) # Check whether seeking to find minimum or maximum value if isMaxNode: newMin = alpha newMoveChoice = None for move in moveChoices: # Rather than calling getPlyResult, use THIS board. Much # faster. REMEMBER TO UNDO THIS HYPOTHETICAL MOVE # Save the old flag sets so they can be restored boardMoveUndoDict = board.getPlyResult(move[0], move[1]) # Find the next move for this node, and how likable the # enemy will consider this child node (_, nodeEnemyLikability, nVisit) = self._boardSearch(board, otherColor, depth - 1, newMin, beta, not isMaxNode, stopSrchTime) # RESTORE THE OLD BOARD STATE - VERY IMPORTANT board.undoPlyResult(boardMoveUndoDict) # Note how many more nodes we've visited nodesVisited += nVisit # Make note of the least likable branches that it still # makes sense to pursue, given how likable this one is if nodeEnemyLikability > newMin: newMin = nodeEnemyLikability newMoveChoice = move # Don't search outside of the target range elif nodeEnemyLikability > beta: #QLAI._logger.debug("Pruning because new value > beta") return (move, beta, nodesVisited) # Check to see if we've evaluated the max number of nodes if ((not USE_WALL_CLOCK) and (nodesVisited > NUM_NODES_TO_VISIT)): return (newMoveChoice, newMin, nodesVisited) return (newMoveChoice, newMin, nodesVisited) else: newMax = beta newMoveChoice = None for move in moveChoices: # Rather than calling getPlyResult, use THIS board. Much # faster. REMEMBER TO UNDO THIS HYPOTHETICAL MOVE # Save the old flag sets so they can be restored boardMoveUndoDict = board.getPlyResult(move[0], move[1]) # Find how likable the enemy will consider this child node (_, nodeEnemyLikability, nVisit) = self._boardSearch(board, otherColor, depth - 1, alpha, newMax, not isMaxNode, stopSrchTime) # RESTORE THE OLD BOARD STATE - VERY IMPORTANT: board.undoPlyResult(boardMoveUndoDict) # Note how many more nodes we've visited nodesVisited += nVisit # Make note of the most likable branches that it still # makes sense to pursue, given how likable this one is if nodeEnemyLikability < newMax: newMax = nodeEnemyLikability newMoveChoice = move # Don't bother searching outside of our target range elif nodeEnemyLikability < alpha: #QLAI._logger.debug("pruning because new val < alpha") return (move, alpha, nodesVisited) # Check to see if we've evaluated the max number of nodes if ((not USE_WALL_CLOCK) and (nodesVisited > NUM_NODES_TO_VISIT)): return (newMoveChoice, newMin, nodesVisited) return (newMoveChoice, newMax, nodesVisited)
def _quiescentSearch(self, board, color, alpha, beta, isMaxNode): """Perform a quiescent search on the given board, examining captures Enumerates captures, and evaluates them to see if they alter results @param board: The starting board state to evaluate @param color: The color of the player to generate a move for @param alpha: Nodes with a likability below this will be ignored @param beta: Nodes with a likability above this will be ignored @param isMaxNode: Is this a beta node? (Is this node seeking to maximize the value of child nodes?) @return: A tuple with the following elements: 1. None, or a move of the form (fromChessPosn, toChessPosn) representing the next move that should be made by the given player 2. The likability of this move's path in the tree, as followed by the search and as determined by the likability of the leaf node terminating the path No timeout is allowed. This shouldn't take long, anyway. If it does, then we're doing good things. Note: this was influenced by information here: http://bit.ly/VYlJVC """ otherColor = ChessBoard.getOtherColor(color) # Note the appeal of this board, with no captures standPatVal = evaluateBoardLikability(color, board, self.heuristicWgts) # Build up a list of capture moves moveChoices = enumMoves(board, color) moveFilterFunct = lambda m: ( (board[m[1]] is not None) and (board[m[1]].color == otherColor)) captureMoves = filter(moveFilterFunct, moveChoices) # Determine whether captures are a good or a bad thing if isMaxNode: # Check whether it is even worth proceeding with evaluation if (standPatVal > beta): return (None, beta) elif (alpha < standPatVal): alpha = standPatVal # Evaluate all captures for capMv in captureMoves: boardMoveUndoDict = board.getPlyResult(capMv[0], capMv[1]) moveResultScore = evaluateBoardLikability( otherColor, board, self.heuristicWgts) board.undoPlyResult(boardMoveUndoDict) # Don't bother searching outside of target range if (moveResultScore > beta): return (None, beta) # Check whether we've found something superior to our best elif (moveResultScore > alpha): alpha = moveResultScore # All captures for this node have been evaluated - return best return (None, alpha) else: # Check whether it is even worth proceeding with evaluation if (standPatVal < alpha): return (None, alpha) elif (beta > standPatVal): beta = standPatVal # Evaluate all captures for capMv in captureMoves: boardMoveUndoDict = board.getPlyResult(capMv[0], capMv[1]) moveResultScore = evaluateBoardLikability( otherColor, board, self.heuristicWgts) board.undoPlyResult(boardMoveUndoDict) # Don't bother searching outside of target range if (moveResultScore > alpha): return (None, alpha) # Check whether we've found something superior to our best elif (moveResultScore < beta): beta = moveResultScore # All captures for this node have been evaluated - return best return (None, beta)
def getBoardWD4(): board = ChessBoard() # TODO (mattsh): Is this what we want to do? board.getPlyResult(ChessPosn(1, 3), ChessPosn(3, 3)) return board
def evaluateBoardLikability(color, board, weightDict): """Return a number in [-1,1] based on board's likability to color @param color: One of maverick.data.ChessBoard.WHITE or maverick.data.ChessBoard.BLACK @param board: A ChessBoard Object @param weightDict: A dictionary describing the weights to use for the various heuristics, of form {'pieceValWeight': pieceValWeight, 'inCheckWeight': inCheckWeight, 'pcsUnderAttackWeight': pcsUnderAttackWeight, 'emptySpaceCvgWeight': emptySpaceCoverageWeight, 'piecesCoveredWeight': piecesCoveredWeight} @return: a number in [-1,1] indicating the likability of the given board state for the given color, where -1 is least likable and 1 is most likable. The calculated value has the following properties: - Lower numbers indicate greater likelihood of a bad outcome (loss) - Higher numbers indicate greater likelihood of a good outcome (win) - -1 means guaranteed loss - 0 means neither player is favored; can mean a state of draw - +1 means guaranteed win""" # Pairing of heuristics with their weights pieceValueWeight = weightDict['pieceValWeight'] inCheckWeight = weightDict['inCheckWeight'] piecesUnderAttackWeight = weightDict['pcsUnderAttackWeight'] emptySpaceCoverageWeight = weightDict['emptySpaceCvgWeight'] piecesCoveredWeight = weightDict['piecesCoveredWeight'] # TODO (mattsh) # Determine opposing player color otherColor = ChessBoard.getOtherColor(color) # Check to see if either player is checkmated and return appropriately if board.isKingCheckmated(otherColor): return 1 elif board.isKingCheckmated(color): return -1 else: # Data structure of 'opinions' from heuristics # Format: ListOf[("Name", weight, value)] opinions = [] ## TODO (James): Clean up the code below a bit - there has to be # a cleaner way to do this # Add piece value opinion pieceValueFriend = heuristicPieceValue(otherColor, board) pieceValueFoe = heuristicPieceValue(color, board) pieceValueRes = combineHeuristicValues(pieceValueFriend, pieceValueFoe) opinions.append(("PieceValue", pieceValueWeight, pieceValueRes)) # Add in check opinion inCheckFriend = heuristicInCheck(color, board) inCheckFoe = heuristicInCheck(otherColor, board) inCheckRes = combineHeuristicValues(inCheckFriend, inCheckFoe) opinions.append(("InCheck", inCheckWeight, inCheckRes)) # Add pieces under attack opinion pcsUnderAtkFriend = heuristicPcsUnderAttack(color, board) pcsUnderAtkFoe = heuristicPcsUnderAttack(otherColor, board) pcsUnderAtkRes = combineHeuristicValues(pcsUnderAtkFriend, pcsUnderAtkFoe) opinions.append(("PiecesUnderAttack", piecesUnderAttackWeight, pcsUnderAtkRes)) # Add empty space coverage opinion emptySpcsCvdFriend = heuristicEmptySpaceCvrg(color, board) emptySpcsCvdFoe = heuristicEmptySpaceCvrg(otherColor, board) emptySpcsCvdRes = combineHeuristicValues(emptySpcsCvdFriend, emptySpcsCvdFoe) opinions.append(("EmptySpaceCoverage", emptySpaceCoverageWeight, emptySpcsCvdRes)) ## TODO (James): Re-enable this when it is more efficient # Add pieces covered opinion # pcsCoveredFriend = heuristicPiecesCovered(color, board) # pcsCoveredFoe = heuristicPiecesCovered(otherColor, board) # pcsCoveredRes = combineHeuristicValues(pcsCoveredFriend, # pcsCoveredFoe) # opinions.append(("PiecesCovered", piecesCoveredWeight, # pcsCoveredRes)) # Return the weighted average return sum([weight * value for (_, weight, value) in opinions]) / \ sum([weight for (_, weight, _) in opinions])
def heuristicEmptySpaceCvrg(color, board): """Return a value representing the number of empty squares controlled @param color: one of maverick.data.ChessBoard.WHITE or maverick.data.ChessBoard.BLACK @param board: a ChessBoard object @return: a value representing the number of empty squares that the given color can attack on the given board, with weight for center squares""" # The value of regular and center squares ## TODO (James): research and tweak these. # See http://tinyurl.com/cpjqnw4l centerSquareValue = 2 squareValue = 1 # Build up a list of all piece locations as tuples pieceLocations = [] otherColor = ChessBoard.getOtherColor(color) # TODO (mattsh): Not sure, do we want to add both ours and theirs? # Find friendly piece locations and add to pieceLocations for piecePosn in board.getPiecesOfColor(color): pieceLocations.append(piecePosn) # Find enemy piece locations and add to pieceLocations for enemyPiecePosn in board.getPiecesOfColor(otherColor): pieceLocations.append(enemyPiecePosn) # Build list of empty squares emptyLocations = [] # Check each location to see if it is occupied for r in range(0, ChessBoard.BOARD_LAYOUT_SIZE): for f in range(0, ChessBoard.BOARD_LAYOUT_SIZE): testPosn = ChessPosn(r, f) if testPosn not in pieceLocations: emptyLocations.append(testPosn) # Build list of possible friendly piece moves friendlyMoves = enumMoves(board, color) friendlyMoveDestPosns = map(lambda x: x[1], friendlyMoves) # Find possible moves to empty squares and build up return value # Accumulator for return value weightedReturn = 0 for dest in emptyLocations: # Check if a move can be made to that Posn if dest in friendlyMoveDestPosns: #Check if it is a center square if __heuristicEmptySpaceCoverage_isCenterSquare(dest): weightedReturn += centerSquareValue else: weightedReturn += squareValue # Calculate total weight of empty squares on board totalEmptyPosnWeight = 0 for posn in emptyLocations: if __heuristicEmptySpaceCoverage_isCenterSquare(posn): totalEmptyPosnWeight += centerSquareValue else: totalEmptyPosnWeight += squareValue # Compress return value into range [-1..1] return -1 + weightedReturn / totalEmptyPosnWeight * 2
def _quiescentSearch(self, board, color, alpha, beta, isMaxNode): """Perform a quiescent search on the given board, examining captures Enumerates captures, and evaluates them to see if they alter results @param board: The starting board state to evaluate @param color: The color of the player to generate a move for @param alpha: Nodes with a likability below this will be ignored @param beta: Nodes with a likability above this will be ignored @param isMaxNode: Is this a beta node? (Is this node seeking to maximize the value of child nodes?) @return: A tuple with the following elements: 1. None, or a move of the form (fromChessPosn, toChessPosn) representing the next move that should be made by the given player 2. The likability of this move's path in the tree, as followed by the search and as determined by the likability of the leaf node terminating the path No timeout is allowed. This shouldn't take long, anyway. If it does, then we're doing good things. Note: this was influenced by information here: http://bit.ly/VYlJVC """ otherColor = ChessBoard.getOtherColor(color) # Note the appeal of this board, with no captures standPatVal = evaluateBoardLikability(color, board, self.heuristicWgts) # Build up a list of capture moves moveChoices = enumMoves(board, color) moveFilterFunct = lambda m: ((board[m[1]] is not None) and (board[m[1]].color == otherColor)) captureMoves = filter(moveFilterFunct, moveChoices) # Determine whether captures are a good or a bad thing if isMaxNode: # Check whether it is even worth proceeding with evaluation if (standPatVal > beta): return (None, beta) elif (alpha < standPatVal): alpha = standPatVal # Evaluate all captures for capMv in captureMoves: boardMoveUndoDict = board.getPlyResult(capMv[0], capMv[1]) moveResultScore = evaluateBoardLikability(otherColor, board, self.heuristicWgts) board.undoPlyResult(boardMoveUndoDict) # Don't bother searching outside of target range if (moveResultScore > beta): return (None, beta) # Check whether we've found something superior to our best elif (moveResultScore > alpha): alpha = moveResultScore # All captures for this node have been evaluated - return best return (None, alpha) else: # Check whether it is even worth proceeding with evaluation if (standPatVal < alpha): return (None, alpha) elif (beta > standPatVal): beta = standPatVal # Evaluate all captures for capMv in captureMoves: boardMoveUndoDict = board.getPlyResult(capMv[0], capMv[1]) moveResultScore = evaluateBoardLikability(otherColor, board, self.heuristicWgts) board.undoPlyResult(boardMoveUndoDict) # Don't bother searching outside of target range if (moveResultScore > alpha): return (None, alpha) # Check whether we've found something superior to our best elif (moveResultScore < beta): beta = moveResultScore # All captures for this node have been evaluated - return best return (None, beta)
def getBoardNew(): return ChessBoard()