diff --git a/src/chess.nim b/src/chess.nim index b8523f0..71681db 100644 --- a/src/chess.nim +++ b/src/chess.nim @@ -24,8 +24,8 @@ type castleRights: CastleRights Move* = object ## `Move` stores all important information for a move. - start: int - dest: int + start*: int + dest*: int color: Color prom: int PieceAmount = tuple @@ -136,7 +136,7 @@ let "e": 3, "f": 2, "g": 1, - "h": 0 + "h": 0, }.newTable ## \ # `FileChar` maps the files of the chessboard to numbers for better # conversion. diff --git a/src/engine.nim b/src/engine.nim new file mode 100644 index 0000000..72ffec7 --- /dev/null +++ b/src/engine.nim @@ -0,0 +1,111 @@ +import sequtils +import ./chess + +type + MoveTree* = object + ## `Movetree` is a visualization for possible moves. + game*: Game + evaluation: float + children*: seq[Movetree] + +const + PawnVal = 1 ## `PawnVal` is the engines value for a pawn. + KnightVal = 3 ## `KnightVal` is the engines value for a knight. + BishopVal = 3 ## `BishopVal` is the engines value for a bishop. + RookVal = 5 ## `RookVal` is the engines value for a rook. + QueenVal = 9 ## `QueenVal` is the engines value for a queen. + CheckmateVal = 1000 ## `CheckmateVal` is the engines value for a checkmate. + DrawVal = 0 ## `DrawVal` is the engines value for a draw. + HiVal = 1000000 ## `HiVal` is the highest possible value (used in minimax). + LoVal = -HiVal ## `LoVal` is the lowest possible value (used in minimax). + +proc pieceEval*(game: Game): int = + ## Returns the evaluation of existing pieces on the `board` + var evaluation = DrawVal + for square in game.board: + case square: + of WPawn: + evaluation += ord(Color.White) * PawnVal + of WKnight: + evaluation += ord(Color.White) * KnightVal + of WBishop: + evaluation += ord(Color.White) * BishopVal + of WRook: + evaluation += ord(Color.White) * RookVal + of WQueen: + evaluation += ord(Color.White) * QueenVal + of BPawn: + evaluation += ord(Color.Black) * PawnVal + of BKnight: + evaluation += ord(Color.Black) * KnightVal + of BBishop: + evaluation += ord(Color.Black) * BishopVal + of BRook: + evaluation += ord(Color.Black) * RookVal + of BQueen: + evaluation += ord(Color.Black) * QueenVal + else: + continue + return evaluation + +proc evaluate(game: Game): int = + ## Returns a complete evaluation of a `game` with player `toMove` about to make + ## a move. + var evaluation = game.pieceEval() + return evaluation + +proc spanMoveTree*(game: Game, depth: int): MoveTree = + ## Create and return a Movetree of a given `game` with a given maximum `depth`. + var mTree: MoveTree + mTree.game = game + if depth != 1 and not game.isCheckmate(game.toMove) and not game.isStalemate(game.toMove): + let possibleMoves = game.genLegalMoves(game.toMove) + for move in possibleMoves: + var tmpGame = game + tmpGame.checkedMove(move) + mTree.children.add(spanMoveTree(tmpGame, depth-1)) + return mTree + +proc minimax*(mTree: MoveTree): int = + ## Return the value of the root node of a given `MoveTree` + if mTree.children == []: + return evaluate(mTree.game) + var bestVal: int + var tmpVal: int + if mTree.game.toMove == Color.White: + bestVal = LoVal + for child in mTree.children: + tmpVal = minimax(child) + bestVal = max(bestVal, tmpVal) + else: + bestVal = HiVal + for child in mTree.children: + tmpVal = minimax(child) + bestVal = min(bestVal, tmpVal) + return bestVal + +proc bestMove*(game: Game, depth: int): Move = + ## Generate a MoveTree of a `game` with a given `depth`, run Minimax and return + ## the best evaluated move. + var moves = game.genLegalMoves(game.toMove) + var bestMove: Move + var bestEval: int + if game.toMove == Color.White: + bestEval = LoVal + else: + bestEval = HiVal + for move in moves: + var tmpGame = game + tmpGame.checkedMove(move) + var tmpEval = tmpGame.evaluate() + if game.toMove == Color.White: + if tmpEval > bestEval: + bestEval = tmpEval + bestMove = move + else: + if tmpEval < bestEval: + bestEval = tmpEval + bestMove = move + return bestMove + + diff --git a/src/engineTest.nim b/src/engineTest.nim new file mode 100644 index 0000000..252c8d5 --- /dev/null +++ b/src/engineTest.nim @@ -0,0 +1,75 @@ +import einheit +import algorithm + +import ./chess +import ./engine + +testSuite GameTest of TestSuite: + + var + game: Game + + method setup() = + self.game = initGame() + + method testPieceEval() = + self.game = initGame([ + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, WKing, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, BKing, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0 + ], Color.Black) + var pieceEvaluation = self.game.pieceEval() + self.check(pieceEvaluation == 0) + + method testSpanMoveTree() = + self.game = initGame([ + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, WKing, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, BKing, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0 + ], Color.Black) + var mTree = self.game.spanMoveTree(2) + self.check(mTree.children == []) + + method testManualMiniMaxEval() = + self.game = initGame([ + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, WKing, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, WPawn, BKing, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0 + ], Color.Black) + var mTree = self.game.spanMoveTree(2) + var evaluation = mTree.minimax() + self.check(evaluation == 0) + + method testBestMove() = + var testGame = initGame([ + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, WKing, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, WPawn, BKing, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0 + ], Color.Black) + var testBestMove = testGame.bestMove(2) + self.check(indToField(testBestMove.start) == "c7") + self.check(indToField(testBestMove.dest) == "d7") + + +when isMainModule: + einheit.runTests()