From b286e9a7f90a0814e9ff87d09dc1e97ef7b3531f Mon Sep 17 00:00:00 2001 From: RyanLauQF Date: Mon, 29 Nov 2021 21:25:18 +0800 Subject: [PATCH] Improved Move Ordering Quiescence search now also accesses transposition table and has the hash move prioritised. Silent moves with no scores assigned are placed at a lower priority and ordered based on PSQT change Fixed a UCI bug where knight promotion was processed wrongly (i.e. e1e2k instead of e1e2n). --- BLANKChess/src/engine/MoveOrdering.java | 135 +++++++++++++++++++++--- BLANKChess/src/engine/Search.java | 25 ++++- BLANKChess/src/main/UCI.java | 4 +- 3 files changed, 144 insertions(+), 20 deletions(-) diff --git a/BLANKChess/src/engine/MoveOrdering.java b/BLANKChess/src/engine/MoveOrdering.java index 687a41f..3e234e9 100644 --- a/BLANKChess/src/engine/MoveOrdering.java +++ b/BLANKChess/src/engine/MoveOrdering.java @@ -10,6 +10,18 @@ public class MoveOrdering { private static final int PAWN_INDEX = 5; private static final int NONE_INDEX = 6; + // Move ordering scores + private static final int PV_MOVE_SCORE = 30000; + private static final int HASH_MOVE_SCORE = 20000; + private static final int CAPTURE_BONUS = 10000; + private static final int QUEEN_PROMOTION_BONUS = 9000; + private static final int FIRST_KILLER = 8000; + private static final int SECOND_KILLER = 7000; + private static final int CASTLING_BONUS = 3000; + private static final int KNIGHT_PROMOTION_BONUS = 1000; + private static final int UNINTERESTING_PROMOTION = 300; + private static final int SILENT_MOVE_PENALTY = -1000; + private static final int[][] MVV_LVA_SCORES = { {0, 0, 0, 0, 0, 0, 0}, // victim K, attacker K, Q, R, B, N, P, None {50, 51, 52, 53, 54, 55, 0}, // victim Q, attacker K, Q, R, B, N, P, None @@ -20,17 +32,17 @@ public class MoveOrdering { {0, 0, 0, 0, 0, 0, 0}, // victim None, attacker K, Q, R, B, N, P, None }; - public static ArrayList orderMoves(ArrayList moves, Search searcher, int searchPly) { + public static ArrayList orderMoves(ArrayList moves, Search searcher, int searchPly, short ttMove) { moves.sort((move1, move2) -> { - int moveScore1 = getMoveScore(move1, searcher, searchPly); - int moveScore2 = getMoveScore(move2, searcher, searchPly); + int moveScore1 = getMoveScore(move1, searcher, searchPly, ttMove); + int moveScore2 = getMoveScore(move2, searcher, searchPly, ttMove); return Integer.compare(moveScore2, moveScore1); }); return moves; } - private static int getMoveScore(Short move, Search searcher, int ply){ + private static int getMoveScore(Short move, Search searcher, int ply, short ttMove){ Board board = searcher.board; // evaluate the move scores @@ -38,6 +50,7 @@ private static int getMoveScore(Short move, Search searcher, int ply){ int start = MoveGenerator.getStart(move); int end = MoveGenerator.getEnd(move); + // PV Move if(searcher.pvMoveScoring){ if (searcher.PVMoves[0][ply] == move) { @@ -45,14 +58,24 @@ private static int getMoveScore(Short move, Search searcher, int ply){ searcher.pvMoveScoring = false; // give PV move the highest score to search it first - return 20000; + return PV_MOVE_SCORE; } } + // Hash Move + if(move == ttMove){ + return HASH_MOVE_SCORE; + } + + if(MoveGenerator.isCastling(move)){ + return CASTLING_BONUS; + } + Piece startPiece = board.getTile(start).getPiece(); + // Captures sorted by MVV-LVA if(MoveGenerator.isCapture(move)){ - // Sort by Most-Valuable Victim / Least-Valuable Aggressor + // Most-Valuable Victim / Least-Valuable Aggressor if (MoveGenerator.getMoveType(move) == 4) { // normal capture score += MVV_LVA(board.getTile(end).getPiece(), startPiece); @@ -61,16 +84,16 @@ private static int getMoveScore(Short move, Search searcher, int ply){ // enpassant capture score += MVV_LVA_SCORES[PAWN_INDEX][PAWN_INDEX]; // pawn (victim) - pawn (attacker) capture } - score += 10000; // prioritise captures + score += CAPTURE_BONUS; // prioritise captures } // quiet moves positions else{ // killer move if(move == searcher.killerMoves[0][ply]){ - score += 8000; + score += FIRST_KILLER; } else if(move == searcher.killerMoves[1][ply]){ - score += 7000; + score += SECOND_KILLER; } else{ // history move score @@ -78,18 +101,102 @@ else if(move == searcher.killerMoves[1][ply]){ } } + // Promotion bonus score (Queen promotion is prioritised to search) + if(MoveGenerator.isPromotion(move)){ + int moveType = MoveGenerator.getMoveType(move); + if(moveType == 8 || moveType == 12){ + // knight promotion + score += KNIGHT_PROMOTION_BONUS; + } + else if(moveType == 11 || moveType == 15){ + // queen promotion + score += QUEEN_PROMOTION_BONUS; + } + else{ + score += UNINTERESTING_PROMOTION; // rook and bishop not as useful as Queen / Knight promotion (knight discovered checks) + } + } + + // silent move + // score using change of Mid-game PSQT values + if(score == 0){ + int startPos = (startPiece.isWhite()) ? start : EvalUtilities.blackFlippedPosition[start]; + int endPos = (startPiece.isWhite()) ? end : EvalUtilities.blackFlippedPosition[end]; + + if(startPiece.isPawn()){ + score += EvalUtilities.pawnMidGamePST[endPos] - EvalUtilities.pawnMidGamePST[startPos]; + } + else if(startPiece.isBishop()){ + score += EvalUtilities.bishopMidGamePST[endPos] - EvalUtilities.bishopMidGamePST[startPos]; + } + else if(startPiece.isKnight()){ + score += EvalUtilities.knightMidGamePST[endPos] - EvalUtilities.knightMidGamePST[startPos]; + } + else if(startPiece.isRook()){ + score += EvalUtilities.rookMidGamePST[endPos] - EvalUtilities.rookMidGamePST[startPos]; + } + else if(startPiece.isQueen()){ + score += EvalUtilities.queenMidGamePST[endPos] - EvalUtilities.queenMidGamePST[startPos]; + } + else if(startPiece.isKing()){ + score += EvalUtilities.kingMidGamePST[endPos] - EvalUtilities.kingMidGamePST[startPos]; + } + + score += SILENT_MOVE_PENALTY; + } + + return score; + } + + public static ArrayList orderQuiescence(ArrayList moves, Search searcher, short bestMove) { + moves.sort((move1, move2) -> { + int moveScore1 = getQuiescenceScore(move1, searcher, bestMove); + int moveScore2 = getQuiescenceScore(move2, searcher, bestMove); + + return Integer.compare(moveScore2, moveScore1); + }); + return moves; + } + + private static int getQuiescenceScore(Short move, Search searcher, short bestMove){ + Board board = searcher.board; + + // evaluate the move scores + int score = 0; + int start = MoveGenerator.getStart(move); + int end = MoveGenerator.getEnd(move); + + if(move == bestMove){ + return HASH_MOVE_SCORE; + } + + Piece startPiece = board.getTile(start).getPiece(); + + if(MoveGenerator.isCapture(move)){ + // Sort by Most-Valuable Victim / Least-Valuable Aggressor + if (MoveGenerator.getMoveType(move) == 4) { + // normal capture + score += MVV_LVA(board.getTile(end).getPiece(), startPiece); + } + else{ + // enpassant capture + score += MVV_LVA_SCORES[PAWN_INDEX][PAWN_INDEX]; // pawn (victim) - pawn (attacker) capture + } + score += CAPTURE_BONUS; // prioritise captures + } + if(MoveGenerator.isPromotion(move)){ int moveType = MoveGenerator.getMoveType(move); if(moveType == 8 || moveType == 12){ // knight promotion - score += Knight.KNIGHT_MG_VALUE; + score += KNIGHT_PROMOTION_BONUS; } else if(moveType == 11 || moveType == 15){ // queen promotion - score += Queen.QUEEN_MG_VALUE; + score += QUEEN_PROMOTION_BONUS; } else{ - score += 300; // rook and bishop not as useful as Queen / Knight promotion (knight discovered checks) + score += UNINTERESTING_PROMOTION; // rook and bishop not as useful as Queen / Knight promotion (knight discovered checks) } } @@ -133,10 +240,10 @@ public static void main(String[] args) throws IOException { Search searcher = new Search(board, new TranspositionTable()); searcher.depthSearch(8); - ArrayList allMoves = orderMoves(board.getAllLegalMoves(), searcher, 1); + ArrayList allMoves = orderMoves(board.getAllLegalMoves(), searcher, 1, (short) 0); for(Short moves : allMoves){ System.out.print(FENUtilities.convertIndexToRankAndFile(MoveGenerator.getStart(moves)) + "-" + FENUtilities.convertIndexToRankAndFile(MoveGenerator.getEnd(moves)) + " "); - System.out.println("Score: " + getMoveScore(moves, searcher, 1) + " "); + System.out.println("Score: " + getMoveScore(moves, searcher, 1, (short) 0) + " "); } } } diff --git a/BLANKChess/src/engine/Search.java b/BLANKChess/src/engine/Search.java index c16cb0b..c76c567 100644 --- a/BLANKChess/src/engine/Search.java +++ b/BLANKChess/src/engine/Search.java @@ -358,6 +358,7 @@ public int negamax(int depth, int searchPly, int alpha, int beta){ // Probe transposition table if current position has already been evaluated before long zobrist = board.getZobristHash(); + short prevBestMove = -1; if(searchPly != 0 && !isPV && TT.containsKey(zobrist)){ TranspositionTable.TTEntry entry = TT.getEntry(zobrist); @@ -387,6 +388,7 @@ else if(entry.entry_TYPE == TranspositionTable.UPPERBOUND_TYPE){ cutOffCount++; return entryScore; } + prevBestMove = entry.bestMove; } } @@ -495,7 +497,7 @@ else if(entry.entry_TYPE == TranspositionTable.UPPERBOUND_TYPE){ // set to check for fail-low node byte moveFlag = TranspositionTable.UPPERBOUND_TYPE; - for (Short encodedMove : MoveOrdering.orderMoves(encodedMoves, this, searchPly)) { + for (Short encodedMove : MoveOrdering.orderMoves(encodedMoves, this, searchPly, prevBestMove)) { moveCount++; Move move = new Move(board, encodedMove); move.makeMove(); @@ -598,6 +600,8 @@ else if(entry.entry_TYPE == TranspositionTable.UPPERBOUND_TYPE){ * i.e. Prevents the AI from blundering a piece due to search being cut at a certain depth causing it to not "see" opponent attacks */ private int quiescenceSearch(int alpha, int beta){ + //info depth 11 seldepth 26 score cp -86 nodes 1386950 nps 124916 ttCut 28247 time 11103 pv e2a6 e6d5 c3d5 f6d5 e4d5 e7e5 e1f1 e8g8 a6b7 e5b2 a1d1 h3g2 f1g2 + // every 32767 (in binary: 0b111111111111111) nodes, check for UCI commands if((nodeCount & 32767) == 0){ listen(); @@ -626,14 +630,19 @@ private int quiescenceSearch(int alpha, int beta){ } nodeCount++; + short prevBestMove = -1; + if(TT.containsKey(board.getZobristHash())){ + TranspositionTable.TTEntry entry = TT.getEntry(board.getZobristHash()); + prevBestMove = entry.bestMove; + } + if(alpha < stand_pat){ alpha = stand_pat; } ArrayList captureMoves = board.getAllCaptures(); - for (Short encodedMove : MoveOrdering.orderMoves(captureMoves, this, ply)) { - + for (Short encodedMove : MoveOrdering.orderQuiescence(captureMoves, this, prevBestMove)) { Move move = new Move(board, encodedMove); ply++; @@ -651,10 +660,17 @@ private int quiescenceSearch(int alpha, int beta){ if(searchedScore > alpha){ alpha = searchedScore; + stand_pat = searchedScore; // cut-off has occurred if(searchedScore >= beta) { + TT.recordEntry(board.getZobristHash(), encodedMove, (byte) 0, beta, TranspositionTable.LOWERBOUND_TYPE); return beta; } + TT.recordEntry(board.getZobristHash(), encodedMove, (byte) 0, alpha, TranspositionTable.UPPERBOUND_TYPE); + } + else if(searchedScore > stand_pat){ + stand_pat = searchedScore; + TT.recordEntry(board.getZobristHash(), encodedMove, (byte) 0, stand_pat, TranspositionTable.UPPERBOUND_TYPE); } } return alpha; @@ -775,7 +791,8 @@ public static void main(String[] args) throws IOException { //board.init("r1bq1rk1/2p1bppp/p1np1n2/1p2p3/3PP3/1B3N2/PPP2PPP/RNBQR1K1 w - - 0 9"); //board.init("8/7k/5Q2/8/8/8/8/1K6 b - - 0 1"); Search search = new Search(board, new TranspositionTable()); - search.depthSearch(11); + search.depthSearch(13); + search.depthSearch(13); System.exit(0); /* diff --git a/BLANKChess/src/main/UCI.java b/BLANKChess/src/main/UCI.java index 2b3f515..ec1e567 100644 --- a/BLANKChess/src/main/UCI.java +++ b/BLANKChess/src/main/UCI.java @@ -474,7 +474,7 @@ else if (Piece.getRow(end) == 0 || Piece.getRow(end) == 7) { moveType = 14; } else if (promotionType == 'b') { moveType = 13; - } else if (promotionType == 'k') { + } else if (promotionType == 'n') { moveType = 12; } } else { @@ -485,7 +485,7 @@ else if (Piece.getRow(end) == 0 || Piece.getRow(end) == 7) { moveType = 10; } else if (promotionType == 'b') { moveType = 9; - } else if (promotionType == 'k') { + } else if (promotionType == 'n') { moveType = 8; } }