From 9095cf8eee848c8165b9f0eb53461deeaeda0ee3 Mon Sep 17 00:00:00 2001 From: TiynGER Date: Sat, 1 May 2021 01:58:26 +0200 Subject: [PATCH] engine: engine now correctly uses the negamax algorithm NegaMax is a simplified version of the MiniMax algorithm that doesn't need different subroutines for the different colors. The testcases were selected and extended. Evaluation now has values for draws and wins. --- README.md | 2 +- src/engine.nim | 64 +++++++++++++++++++++------------------------- src/engineTest.nim | 57 ++++++++++++++++++++++++++++++++--------- 3 files changed, 75 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index ce4f1f3..8b88bb7 100644 --- a/README.md +++ b/README.md @@ -38,4 +38,4 @@ Due to easier off the board checking a ### Engine The engine uses a simple implementation of the -[Minimax](https://www.chessprogramming.org/Minimax)-algorithm. +[NegaMax](https://www.chessprogramming.org/NegaMax)-algorithm. diff --git a/src/engine.nim b/src/engine.nim index dc9ac43..b89021a 100644 --- a/src/engine.nim +++ b/src/engine.nim @@ -13,10 +13,9 @@ const 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. + 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). + LoVal = -1000000 ## `LoVal` is a value always lower than any evaluation. proc pieceEval*(game: Game): int = ## Returns the evaluation of existing pieces on the `board` @@ -50,14 +49,25 @@ proc pieceEval*(game: Game): int = proc evaluate(game: Game): int = ## Returns a complete evaluation of a `game` with player `toMove` about to make ## a move. - var evaluation = game.pieceEval() + var evaluation: int + if game.isCheckmate(game.toMove): + evaluation = ord(game.toMove) * CheckmateVal + if game.isStalemate(game.toMove): + evaluation = DrawVal + else: + evaluation = game.pieceEval() + if game.isDrawClaimable(): + if game.toMove == Color.White: + evaluation = max(DrawVal, evaluation) + else: + evaluation = min(DrawVal, evaluation) 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): + if depth != 0 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 @@ -65,46 +75,30 @@ proc spanMoveTree*(game: Game, depth: int): MoveTree = mTree.children.add(spanMoveTree(tmpGame, depth-1)) return mTree -proc minimax*(mTree: MoveTree): int = +proc negaMax*(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 mTree.game.evaluate() + var bestVal = LoVal + for child in mTree.children: + var tmpVal = -negaMax(child) + bestVal = max(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 + ## Generate a MoveTree of a `game` with a given `depth`, run negaMax 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 + bestEval = LoVal 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 + var tmpMTree = tmpGame.spanMoveTree(depth) + var tmpEval = -tmpMTree.negaMax() + echo("move:", moveToNotation(move),"; eval:", tmpEval) + if tmpEval > bestEval: + bestEval = tmpEval + bestMove = move return bestMove - - diff --git a/src/engineTest.nim b/src/engineTest.nim index 252c8d5..bc2d3c5 100644 --- a/src/engineTest.nim +++ b/src/engineTest.nim @@ -1,5 +1,4 @@ import einheit -import algorithm import ./chess import ./engine @@ -12,7 +11,7 @@ testSuite GameTest of TestSuite: method setup() = self.game = initGame() - method testPieceEval() = + method testPieceEvalStalemate() = self.game = initGame([ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, WKing, 0, 0, 0, 0, @@ -37,10 +36,10 @@ testSuite GameTest of TestSuite: 0, 0, 0, 0, 0, BKing, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], Color.Black) - var mTree = self.game.spanMoveTree(2) + var mTree = self.game.spanMoveTree(1) self.check(mTree.children == []) - method testManualMiniMaxEval() = + method testBestMoveProm() = self.game = initGame([ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, WKing, 0, 0, 0, 0, @@ -48,15 +47,18 @@ testSuite GameTest of TestSuite: 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, WPawn, 0, 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) + ], Color.White) + var testBestMove = self.game.bestMove(2) + self.check(testBestMove.start != 0) + self.check(indToField(testBestMove.start) == "e7") + self.check(indToField(testBestMove.dest) == "e8") + - method testBestMove() = - var testGame = initGame([ + + method testBestMoveStopProm() = + 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, @@ -66,10 +68,41 @@ testSuite GameTest of TestSuite: 0, 0, 0, 0, WPawn, BKing, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], Color.Black) - var testBestMove = testGame.bestMove(2) + var testBestMove = self.game.bestMove(2) + self.check(testBestMove.start != 0) self.check(indToField(testBestMove.start) == "c7") self.check(indToField(testBestMove.dest) == "d7") + method testBestMoveTacticBlack() = + self.game = initGame([ + 0, 0, 0, 0, 0, 0, 0, 0, + 0, WRook, 0, WKing, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, WPawn, 0, 0, 0, 0, 0, + 0, BPawn, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, BRook, 0, 0, 0, BKing, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0 + ], Color.Black) + var testBestMove = self.game.bestMove(2) + self.check(testBestMove.start != 0) + self.check(indToField(testBestMove.start) != "g5" or indToField(testBestMove.dest) != "f4") + + method testBestMoveTacticWhite() = + self.game = initGame([ + 0, 0, 0, 0, 0, 0, 0, 0, + 0, WRook, 0, WKing, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, WPawn, 0, 0, 0, 0, 0, 0, + 0, 0, BPawn, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, BRook, 0, 0, 0, BKing, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0 + ], Color.White) + var testBestMove = self.game.bestMove(2) + self.check(testBestMove.start != 0) + self.check(indToField(testBestMove.start) != "g4" or indToField(testBestMove.dest) != "f5") + when isMainModule: einheit.runTests()