diff --git a/.gitignore b/.gitignore index cb5876d..559cd48 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,6 @@ public/as-api.wasm.map # PGN files **/pgn/* + +# FEN files +**/fen/* diff --git a/README.md b/README.md index a3e730a..f8eec64 100644 --- a/README.md +++ b/README.md @@ -55,3 +55,4 @@ This project is licensed under the GNU General Public License - see the [LICENSE ### Attributions * Images for the chess pieces come from [Wikimedia Commons](https://commons.wikimedia.org/wiki/Category:SVG_chess_pieces) * The opening book was generated from a selection of chess games from the [FICS Games Database](https://www.ficsgames.org) +* A set of 725000 [test positions](https://bitbucket.org/zurichess/tuner/downloads/) collected by the author of Zurichess was used to tune all evaluation parameters diff --git a/artifacts/release_notes.md b/artifacts/release_notes.md index b33731b..1f3c7a2 100644 --- a/artifacts/release_notes.md +++ b/artifacts/release_notes.md @@ -1,15 +1,14 @@ -This release contains many small improvements, but the largest ELO gain came from the new mobility evaluation :cartwheeling: +This release focussed on tuning the evaluation function :balance_scale: > The Web App version of **Wasabi Chess** can be played [**here**](https://mhonert.github.io/chess). ## Changes -- Add mobility evaluation -- UCI: output mate distance as score when a mate was found -- Evaluation: provide bonus for pawns covering pawns and knights -- Adjust piece and move ordering values -- Slightly increase LMR threshold -- Simplify and tune futility margin calculation -- Directly jump to quiescence search for futile positions at depth 1 +- New tuning tool +- Tuned all evaluation parameters +- Replaced simple mobility score calculation with mobility score table +- Replaced simple king safety calculation with king threat score table +- Improved game phase calculation for tapered eval +- Added endgame piece square tables ## Installation - Download and unpack the archive for your platform (Linux or Windows 8/10) diff --git a/assembly/__tests__/board.spec.ts b/assembly/__tests__/board.spec.ts index 03a1bee..ddda18f 100644 --- a/assembly/__tests__/board.spec.ts +++ b/assembly/__tests__/board.spec.ts @@ -605,7 +605,7 @@ describe('King threats', () => { const board: Board = fromFEN("2kr4/ppp5/2n5/5p2/1Pb1P3/P4P2/1BP1RKrq/R7 w - - 0 28") board.getMaterialScore() - expect(board.getMaterialScore() - board.getScore()).toBeGreaterThan(90, "King threat penalty is not high enough") + expect(board.getMaterialScore() - board.getScore()).toBeGreaterThan(70, "King threat penalty is not high enough") }); }); diff --git a/assembly/__tests__/engine.spec.ts b/assembly/__tests__/engine.spec.ts index 3badfc1..62326e7 100644 --- a/assembly/__tests__/engine.spec.ts +++ b/assembly/__tests__/engine.spec.ts @@ -198,9 +198,9 @@ describe('Finds moves', () => { ]); board.increaseHalfMoveCount(); - board.performEncodedMove(findBestMoveIncrementally(board, BLACK, 11, 0)); - board.performEncodedMove(findBestMoveIncrementally(board, WHITE, 3, 0)); - board.performEncodedMove(findBestMoveIncrementally(board, BLACK, 5, 0)); + board.performEncodedMove(findBestMoveIncrementally(board, BLACK, 13, 0)); + board.performEncodedMove(findBestMoveIncrementally(board, WHITE, 5, 0)); + board.performEncodedMove(findBestMoveIncrementally(board, BLACK, 9, 0)); board.performEncodedMove(findBestMoveIncrementally(board, WHITE, 3, 0)); board.performEncodedMove(findBestMoveIncrementally(board, BLACK, 1, 0)); diff --git a/assembly/bitboard.ts b/assembly/bitboard.ts index 9168f06..ea2008b 100644 --- a/assembly/bitboard.ts +++ b/assembly/bitboard.ts @@ -31,7 +31,7 @@ export const BLACK_QUEEN_SIDE_CASTLING_BIT_PATTERN: u64 = 0b00000000_00000000_00 export const LIGHT_COLORED_FIELD_PATTERN: u64 = 0b01010101_01010101_01010101_01010101_01010101_01010101_01010101_01010101; export const DARK_COLORED_FIELD_PATTERN: u64 = 0b10101010_10101010_10101010_10101010_10101010_10101010_10101010_10101010; -const KING_DANGER_ZONE_SIZE: i32 = 3; +export const KING_DANGER_ZONE_SIZE: i32 = 2; function isBorder(boardPos: i32): bool { if (boardPos < 21 || boardPos > 98) { diff --git a/assembly/board.ts b/assembly/board.ts index afc1326..5736749 100644 --- a/assembly/board.ts +++ b/assembly/board.ts @@ -73,30 +73,33 @@ const MAX_GAME_HALFMOVES = 5898 * 2; export const EN_PASSANT_BIT = 1 << 31; // Evaluation constants -export const DOUBLED_PAWN_PENALTY: i32 = 6; +export const DOUBLED_PAWN_PENALTY: i32 = 17; -const PASSED_PAWN_BONUS: i32 = 25; +const PASSED_PAWN_BONUS: i32 = 22; -const KING_SHIELD_BONUS: i32 = 21; +const KING_SHIELD_BONUS: i32 = 20; const PAWNLESS_DRAW_SCORE_LOW_THRESHOLD = 100; const PAWNLESS_DRAW_SCORE_HIGH_THRESHOLD = 400; const PAWNLESS_DRAW_CLOCK_THRESHOLD = 64; const CASTLING_BONUS: i32 = 28; -const LOST_QUEENSIDE_CASTLING_PENALTY: i32 = 18; -const LOST_KINGSIDE_CASTLING_PENALTY: i32 = 21; +const LOST_QUEENSIDE_CASTLING_PENALTY: i32 = 24; +const LOST_KINGSIDE_CASTLING_PENALTY: i32 = 39; -const KING_DANGER_THRESHOLD: i32 = 1; -const KING_DANGER_PIECE_PENALTY: i32 = 21; +export const KING_DANGER_PIECE_PENALTY: StaticArray = StaticArray.fromArray([ 0, -3, -3, 8, 25, 52, 95, 168, 258, 320, 1200, 1200, 1200, 1200, 1200, 1200 ]); -const PAWN_COVER_BONUS: i32 = 14; +const PAWN_COVER_BONUS: i32 = 12; -const KNIGHT_MOB_BONUS: i32 = 5; -const BISHOP_MOB_BONUS: i32 = 5; -const ROOK_MOB_BONUS: i32 = 5; -const QUEEN_MOB_BONUS: i32 = 5; +export const KNIGHT_MOB_BONUS: StaticArray = StaticArray.fromArray([ -9, 3, 10, 11, 20, 22, 29, 29, 55 ]); +export const BISHOP_MOB_BONUS: StaticArray = StaticArray.fromArray([ -2, 6, 14, 19, 22, 24, 29, 30, 35, 32, 49, 105, 73, 56 ]); +export const ROOK_MOB_BONUS: StaticArray = StaticArray.fromArray([ -13, -10, -6, -1, 2, 9, 14, 23, 29, 36, 59, 64, 52, 62, 57 ]); +export const QUEEN_MOB_BONUS: StaticArray = StaticArray.fromArray([ -2, -6, 0, 3, 11, 13, 15, 17, 20, 28, 28, 35, 51, 42, 50, 62, 99, 105, 102, 159, 100, 122, 131, 131, 115, 64, 75, 61 ]); +export const EG_KNIGHT_MOB_BONUS: StaticArray = StaticArray.fromArray([ -65, -27, -2, 9, 13, 23, 20, 25, 4 ]); +export const EG_BISHOP_MOB_BONUS: StaticArray = StaticArray.fromArray([ -46, -16, 1, 9, 16, 24, 27, 25, 30, 29, 20, 11, 35, 22 ]); +export const EG_ROOK_MOB_BONUS: StaticArray = StaticArray.fromArray([ -72, -31, -8, 2, 15, 25, 28, 31, 35, 37, 36, 41, 50, 48, 45 ]); +export const EG_QUEEN_MOB_BONUS: StaticArray = StaticArray.fromArray([ -77, -7, -18, 11, 4, 35, 42, 61, 70, 66, 85, 87, 85, 100, 108, 109, 98, 86, 109, 95, 123, 121, 118, 129, 127, 128, 159, 123 ]); const BASE_PIECE_PHASE_VALUE: i32 = 2; const PAWN_PHASE_VALUE: i32 = -1; // relative to the base piece value @@ -269,12 +272,179 @@ export class Board { const whitePieces = this.getAllPieceBitBoard(WHITE); const blackPieces = this.getAllPieceBitBoard(BLACK); - // Interpolate between opening/mid-game score and end game score for a smooth eval score transition - const pawnCount: i32 = i32(popcnt(whitePawns | blackPawns)); - const piecesExceptKingCount: i32 = i32(popcnt((whitePieces | blackPieces))) - 2; // -2 for two kings + // Mobility evaluation + const emptyBoard = ~whitePieces & ~blackPieces; + const emptyOrBlackPiece = emptyBoard | blackPieces; + + const blackPawnAttacks = blackLeftPawnAttacks(blackPawns) | blackRightPawnAttacks(blackPawns); + let whiteSafeTargets = emptyOrBlackPiece & ~blackPawnAttacks; + + let whiteKingThreat: i32 = 0; + let blackKingThreat: i32 = 0; + + const blackKingDangerZone = unchecked(KING_DANGER_ZONE_PATTERNS[this.blackKingIndex]); + const whiteKingDangerZone = unchecked(KING_DANGER_ZONE_PATTERNS[this.whiteKingIndex]); + + // Knights + let whiteKnightAttacks: u64 = 0; + let whiteKnights = this.getBitBoard(KNIGHT); + { + let knights = whiteKnights; + while (knights != 0) { + const pos: i32 = i32(ctz(knights)); + knights ^= 1 << pos; // unset bit + + const possibleMoves = unchecked(KNIGHT_PATTERNS[pos]); + whiteKnightAttacks |= possibleMoves; + const moveCount = i32(popcnt(possibleMoves & whiteSafeTargets)); + score += unchecked(KNIGHT_MOB_BONUS[moveCount]); + egScore += unchecked(EG_KNIGHT_MOB_BONUS[moveCount]); + + if (possibleMoves & blackKingDangerZone) { + blackKingThreat++; + } + } + } + + const emptyOrWhitePiece = emptyBoard | whitePieces; + const whitePawnAttacks = whiteLeftPawnAttacks(whitePawns) | whiteRightPawnAttacks(whitePawns); + let blackSafeTargets = emptyOrWhitePiece & ~whitePawnAttacks; + + let blackKnightAttacks: u64 = 0; + let blackKnights = this.getBitBoard(-KNIGHT); + { + let knights = blackKnights; + while (knights != 0) { + const pos: i32 = i32(ctz(knights)); + knights ^= 1 << pos; // unset bit + + const possibleMoves = unchecked(KNIGHT_PATTERNS[pos]); + blackKnightAttacks |= possibleMoves; + const moveCount = i32(popcnt(possibleMoves & blackSafeTargets)); + score -= unchecked(KNIGHT_MOB_BONUS[moveCount]); + egScore -= unchecked(EG_KNIGHT_MOB_BONUS[moveCount]); + + if (possibleMoves & whiteKingDangerZone) { + whiteKingThreat++; + } + } + } + + whiteSafeTargets &= ~blackKnightAttacks; + blackSafeTargets &= ~whiteKnightAttacks; + + // Bishops + const occupied = ~emptyBoard; + + let whiteBishops = this.getBitBoard(BISHOP); + let whiteBishopAttacks: u64 = 0; + while (whiteBishops != 0) { + const pos: i32 = i32(ctz(whiteBishops)); + whiteBishops ^= 1 << pos; // unset bit + const possibleMoves = diagonalAttacks(occupied, pos) | antiDiagonalAttacks(occupied, pos); + whiteBishopAttacks |= possibleMoves; + const moveCount = i32(popcnt(possibleMoves & whiteSafeTargets)); + score += unchecked(BISHOP_MOB_BONUS[moveCount]); + egScore += unchecked(EG_BISHOP_MOB_BONUS[moveCount]); + + if (possibleMoves & blackKingDangerZone) { + blackKingThreat++; + } + } + + let blackBishops = this.getBitBoard(-BISHOP); + let blackBishopAttacks: u64 = 0; + while (blackBishops != 0) { + const pos: i32 = i32(ctz(blackBishops)); + blackBishops ^= 1 << pos; // unset bit + const possibleMoves = diagonalAttacks(occupied, pos) | antiDiagonalAttacks(occupied, pos); + blackBishopAttacks |= possibleMoves; + const moveCount = i32(popcnt(possibleMoves & blackSafeTargets)); + score -= unchecked(BISHOP_MOB_BONUS[moveCount]); + egScore -= unchecked(EG_BISHOP_MOB_BONUS[moveCount]); + + if (possibleMoves & whiteKingDangerZone) { + whiteKingThreat++; + } + } + + whiteSafeTargets &= ~blackBishopAttacks; + blackSafeTargets &= ~whiteBishopAttacks; + // Rooks + let whiteRooks = this.getBitBoard(ROOK); + let whiteRookAttacks: u64 = 0; + while (whiteRooks != 0) { + const pos: i32 = i32(ctz(whiteRooks)); + whiteRooks ^= 1 << pos; // unset bit + const possibleMoves = horizontalAttacks(occupied, pos) | verticalAttacks(occupied, pos); + whiteRookAttacks |= possibleMoves; + const moveCount = i32(popcnt(possibleMoves & whiteSafeTargets)); + score += unchecked(ROOK_MOB_BONUS[moveCount]); + egScore += unchecked(EG_ROOK_MOB_BONUS[moveCount]); + + if (possibleMoves & blackKingDangerZone) { + blackKingThreat++; + } + } + + let blackRooks = this.getBitBoard(-ROOK); + let blackRookAttacks: u64 = 0; + while (blackRooks != 0) { + const pos: i32 = i32(ctz(blackRooks)); + blackRooks ^= 1 << pos; // unset bit + const possibleMoves = horizontalAttacks(occupied, pos) | verticalAttacks(occupied, pos); + blackRookAttacks |= possibleMoves; + const moveCount = i32(popcnt(possibleMoves & blackSafeTargets)); + score -= unchecked(ROOK_MOB_BONUS[moveCount]); + egScore -= unchecked(EG_ROOK_MOB_BONUS[moveCount]); + + if (possibleMoves & whiteKingDangerZone) { + whiteKingThreat++; + } + } + + whiteSafeTargets &= ~blackRookAttacks; + blackSafeTargets &= ~whiteRookAttacks; + + // Queens const whiteQueens = this.getBitBoard(QUEEN); + { + let queens = whiteQueens; + while (queens != 0) { + const pos: i32 = i32(ctz(queens)); + queens ^= 1 << pos; // unset bit + const possibleMoves = horizontalAttacks(occupied, pos) | verticalAttacks(occupied, pos) | diagonalAttacks(occupied, pos) | antiDiagonalAttacks(occupied, pos); + const moveCount = i32(popcnt(possibleMoves & whiteSafeTargets)); + score += unchecked(QUEEN_MOB_BONUS[moveCount]); + egScore += unchecked(EG_QUEEN_MOB_BONUS[moveCount]); + + if (possibleMoves & blackKingDangerZone) { + blackKingThreat++; + } + } + } + const blackQueens = this.getBitBoard(-QUEEN); + { + let queens = blackQueens; + while (queens != 0) { + const pos: i32 = i32(ctz(queens)); + queens ^= 1 << pos; // unset bit + const possibleMoves = horizontalAttacks(occupied, pos) | verticalAttacks(occupied, pos) | diagonalAttacks(occupied, pos) | antiDiagonalAttacks(occupied, pos); + const moveCount = i32(popcnt(possibleMoves & blackSafeTargets)); + score -= unchecked(QUEEN_MOB_BONUS[moveCount]); + egScore -= unchecked(EG_QUEEN_MOB_BONUS[moveCount]); + + if (possibleMoves & whiteKingDangerZone) { + whiteKingThreat++; + } + } + } + + // Interpolate between opening/mid-game score and end game score for a smooth eval score transition + const pawnCount: i32 = i32(popcnt(whitePawns | blackPawns)); + const piecesExceptKingCount: i32 = i32(popcnt((whitePieces | blackPieces))) - 2; // -2 for two kings const queenPhaseScore: i32 = (whiteQueens > 0 ? QUEEN_PHASE_VALUE : 0) + (blackQueens > 0 ? QUEEN_PHASE_VALUE : 0); @@ -286,25 +456,32 @@ export class Board { // Perform evaluations which apply to all game phases // Pawn cover bonus - const whitePawnAttacks = whiteLeftPawnAttacks(whitePawns) | whiteRightPawnAttacks(whitePawns); - let whiteKnights = this.getBitBoard(KNIGHT); const whitePawnsAndKnights = whitePawns | whiteKnights; interpolatedScore += i32(popcnt(whitePawnsAndKnights & whitePawnAttacks)) * PAWN_COVER_BONUS; - const blackPawnAttacks = blackLeftPawnAttacks(blackPawns) | blackRightPawnAttacks(blackPawns); - let blackKnights = this.getBitBoard(-KNIGHT); const blackPawnsAndKnights = blackPawns | blackKnights; interpolatedScore -= i32(popcnt(blackPawnsAndKnights & blackPawnAttacks)) * PAWN_COVER_BONUS; + blackKingThreat += i32(popcnt(whitePawnAttacks & blackKingDangerZone)) / 2; + whiteKingThreat += i32(popcnt(blackPawnAttacks & whiteKingDangerZone)) / 2; + // Doubled pawn penalty interpolatedScore -= this.calcDoubledPawnPenalty(whitePawns); interpolatedScore += this.calcDoubledPawnPenalty(blackPawns); - interpolatedScore += this.mobilityScore(whitePawnAttacks, blackPawnAttacks, whitePieces, blackPieces); + // King threat (uses king threat values from mobility evaluation) + if (whiteQueens & blackKingDangerZone) { + blackKingThreat += 3; + } + interpolatedScore += unchecked(KING_DANGER_PIECE_PENALTY[blackKingThreat]); + if (blackQueens & whiteKingDangerZone) { + whiteKingThreat += 3; + } + interpolatedScore -= unchecked(KING_DANGER_PIECE_PENALTY[whiteKingThreat]); // Passed white pawns bonus - let pawns = whitePawns + let pawns = whitePawns; while (pawns != 0) { const pos: i32 = i32(ctz(pawns)); pawns ^= 1 << pos; // unset bit @@ -349,26 +526,6 @@ export class Board { } } - // King threat - { - const whiteKingDangerZone = unchecked(KING_DANGER_ZONE_PATTERNS[this.whiteKingIndex]); - const opponentPiecesInKingDangerZone = i32(popcnt((blackPieces & ~blackPawns) & whiteKingDangerZone)); - if (opponentPiecesInKingDangerZone >= KING_DANGER_THRESHOLD) { - const queensInKingDangerZone = i32(popcnt(blackQueens & whiteKingDangerZone)); - const dangerScore = KING_DANGER_PIECE_PENALTY << (opponentPiecesInKingDangerZone + queensInKingDangerZone - KING_DANGER_THRESHOLD); - interpolatedScore -= min(dangerScore, 500); - } - } - { - const blackKingDangerZone = unchecked(KING_DANGER_ZONE_PATTERNS[this.blackKingIndex]); - const opponentPiecesInKingDangerZone = i32(popcnt((whitePieces & ~whitePawns) & blackKingDangerZone)); - if (opponentPiecesInKingDangerZone >= KING_DANGER_THRESHOLD) { - const queensInKingDangerZone = i32(popcnt(whiteQueens & blackKingDangerZone)); - const dangerScore = KING_DANGER_PIECE_PENALTY << (opponentPiecesInKingDangerZone + queensInKingDangerZone - KING_DANGER_THRESHOLD); - interpolatedScore += min(dangerScore, 500); - } - } - // Adjust score for positions, which are very likely to end in a draw if ((blackPawns == 0 && interpolatedScore < -PAWNLESS_DRAW_SCORE_LOW_THRESHOLD && interpolatedScore > -PAWNLESS_DRAW_SCORE_HIGH_THRESHOLD) || (whitePawns == 0 && interpolatedScore > PAWNLESS_DRAW_SCORE_LOW_THRESHOLD && interpolatedScore < PAWNLESS_DRAW_SCORE_HIGH_THRESHOLD)) { @@ -383,122 +540,6 @@ export class Board { return interpolatedScore; } - @inline - mobilityScore(whitePawnAttacks: u64, blackPawnAttacks: u64, whitePieces: u64, blackPieces: u64): i32 { - const emptyBoard = ~whitePieces & ~blackPieces; - const emptyOrBlackPiece = emptyBoard | blackPieces; - - let whiteSafeTargets = emptyOrBlackPiece & ~blackPawnAttacks; - - let score: i32 = 0; - - // Knights - let whiteKnights = this.getBitBoard(KNIGHT); - let whiteKnightAttacks: u64 = 0; - while (whiteKnights != 0) { - const pos: i32 = i32(ctz(whiteKnights)); - whiteKnights ^= 1 << pos; // unset bit - - const possibleMoves = unchecked(KNIGHT_PATTERNS[pos]); - whiteKnightAttacks |= possibleMoves; - const moveCount = i32(popcnt(possibleMoves & whiteSafeTargets)); - score += moveCount * KNIGHT_MOB_BONUS; - } - - const emptyOrWhitePiece = emptyBoard | whitePieces; - let blackSafeTargets = emptyOrWhitePiece & ~whitePawnAttacks; - - let blackKnights = this.getBitBoard(-KNIGHT); - let blackKnightAttacks: u64 = 0; - while (blackKnights != 0) { - const pos: i32 = i32(ctz(blackKnights)); - blackKnights ^= 1 << pos; // unset bit - - const possibleMoves = unchecked(KNIGHT_PATTERNS[pos]); - blackKnightAttacks |= possibleMoves; - const moveCount = i32(popcnt(possibleMoves & blackSafeTargets)); - score -= moveCount * KNIGHT_MOB_BONUS; - } - - whiteSafeTargets &= ~blackKnightAttacks; - blackSafeTargets &= ~whiteKnightAttacks; - - // Bishops - const occupied = ~emptyBoard; - - let whiteBishops = this.getBitBoard(BISHOP); - let whiteBishopAttacks: u64 = 0; - while (whiteBishops != 0) { - const pos: i32 = i32(ctz(whiteBishops)); - whiteBishops ^= 1 << pos; // unset bit - const possibleMoves = diagonalAttacks(occupied, pos) | antiDiagonalAttacks(occupied, pos); - whiteBishopAttacks |= possibleMoves; - const moveCount = i32(popcnt(possibleMoves & whiteSafeTargets)); - score += moveCount * BISHOP_MOB_BONUS; - } - - let blackBishops = this.getBitBoard(-BISHOP); - let blackBishopAttacks: u64 = 0; - while (blackBishops != 0) { - const pos: i32 = i32(ctz(blackBishops)); - blackBishops ^= 1 << pos; // unset bit - const possibleMoves = diagonalAttacks(occupied, pos) | antiDiagonalAttacks(occupied, pos); - blackBishopAttacks |= possibleMoves; - const moveCount = i32(popcnt(possibleMoves & blackSafeTargets)); - score -= moveCount * BISHOP_MOB_BONUS; - } - - whiteSafeTargets &= ~blackBishopAttacks; - blackSafeTargets &= ~whiteBishopAttacks; - - // Rooks - let whiteRooks = this.getBitBoard(ROOK); - let whiteRookAttacks: u64 = 0; - while (whiteRooks != 0) { - const pos: i32 = i32(ctz(whiteRooks)); - whiteRooks ^= 1 << pos; // unset bit - const possibleMoves = horizontalAttacks(occupied, pos) | verticalAttacks(occupied, pos); - whiteRookAttacks |= possibleMoves; - const moveCount = i32(popcnt(possibleMoves & whiteSafeTargets)); - score += moveCount * ROOK_MOB_BONUS; - } - - let blackRooks = this.getBitBoard(-ROOK); - let blackRookAttacks: u64 = 0; - while (blackRooks != 0) { - const pos: i32 = i32(ctz(blackRooks)); - blackRooks ^= 1 << pos; // unset bit - const possibleMoves = horizontalAttacks(occupied, pos) | verticalAttacks(occupied, pos); - blackRookAttacks |= possibleMoves; - const moveCount = i32(popcnt(possibleMoves & blackSafeTargets)); - score -= moveCount * ROOK_MOB_BONUS; - } - - whiteSafeTargets &= ~blackRookAttacks; - blackSafeTargets &= ~whiteRookAttacks; - - // Queens - let whiteQueens = this.getBitBoard(QUEEN); - while (whiteQueens != 0) { - const pos: i32 = i32(ctz(whiteQueens)); - whiteQueens ^= 1 << pos; // unset bit - const possibleMoves = horizontalAttacks(occupied, pos) | verticalAttacks(occupied, pos) | diagonalAttacks(occupied, pos) | antiDiagonalAttacks(occupied, pos); - const moveCount = i32(popcnt(possibleMoves & whiteSafeTargets)); - score += moveCount * QUEEN_MOB_BONUS; - } - - let blackQueens = this.getBitBoard(-QUEEN); - while (blackQueens != 0) { - const pos: i32 = i32(ctz(blackQueens)); - blackQueens ^= 1 << pos; // unset bit - const possibleMoves = horizontalAttacks(occupied, pos) | verticalAttacks(occupied, pos) | diagonalAttacks(occupied, pos) | antiDiagonalAttacks(occupied, pos); - const moveCount = i32(popcnt(possibleMoves & blackSafeTargets)); - score -= moveCount * QUEEN_MOB_BONUS; - } - - return score; - } - @inline calcDoubledPawnPenalty(pawns: u64): i32 { const doubled = (pawns & rotr(pawns, 8)) @@ -1286,107 +1327,161 @@ function calculateEnPassantBitMask(bit: i32, index: i32, array: Array): i32 } export const PAWN_POSITION_SCORES: StaticArray = StaticArray.fromArray([ - 0, 0, 0, 0, 0, 0, 0, 0, - 10, 10, 10, 10, 10, 10, 10, 10, - 6, 6, 7, 8, 8, 7, 6, 6, - 2, 2, 3, 5, 5, 3, 2, 2, - 0, 0, 0, 4, 4, 0, 0, 0, - 1, -1, -2, 0, 0, -2, -1, 1, - 1, 2, 2, -4, -4, 2, 2, 1, - 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 98, 94, 33, 98, 70, 123, + 50, 2, -6, -13, 23, 22, 69, + 87, 33, -9, -19, 13, 3, 29, + 31, 23, 17, -17, -22, -9, -3, + 18, 24, 10, 9, -28, -25, -10, + -12, -22, -10, -17, 24, -24, -24, + 0, -12, -19, -15, 18, 34, -18, + 0, 0, 0, 0, 0, 0, 0, 0 ]); -const KNIGHT_POSITION_SCORES: StaticArray = StaticArray.fromArray([ - -10, -8, -6, -6, -6, -6, -8,-10, - -8, -4, 0, 0, 0, 0, -4, -8, - -6, 0, 2, 3, 3, 2, 0, -6, - -6, 1, 3, 4, 4, 3, 1, -6, - -6, 0, 3, 4, 4, 3, 0, -6, - -6, 1, 2, 3, 3, 2, 1, -6, - -8, -4, 0, 1, 1, 0, -4, -8, - -10, -8, -6, -6, -6, -6, -8,-10, +export const EG_PAWN_POSITION_SCORES: StaticArray = StaticArray.fromArray([ + 0, 0, 0, 0, 0, 0, 0, 0, + 134, 127, 110, 68, 92, 66, 120, 145, + 81, 92, 56, 28, 6, 15, 60, 64, + 26, 7, -6, -30, -30, -15, 3, 7, + 5, 3, -16, -35, -32, -23, -8, -9, + -1, 3, -9, 0, -1, 0, -14, -13, + 14, 11, 10, 12, 24, 3, 2, -12, + 0, 0, 0, 0, 0, 0, 0, 0 ]); -const BISHOP_POSITION_SCORES: StaticArray = StaticArray.fromArray([ - -4, -2, -2, -2, -2, -2, -2, -4, - -2, 0, 0, 0, 0, 0, 0, -2, - -2, 0, 1, 2, 2, 1, 0, -2, - -2, 1, 1, 2, 2, 1, 1, -2, - -2, 0, 2, 2, 2, 2, 0, -2, - -2, 2, 2, 2, 2, 2, 2, -2, - -2, 1, 0, 0, 0, 0, 1, -2, - -4, -2, -2, -2, -2, -2, -2, -4 +export const KNIGHT_POSITION_SCORES: StaticArray = StaticArray.fromArray([ + -178, -109, -69, -41, 99, -101, -14, -118, + -116, -57, 89, 31, -4, 60, 9, -40, + -67, 31, 10, 31, 101, 121, 46, 42, + -21, 7, 1, 48, 22, 65, 14, 31, + -5, 7, 7, 12, 24, 17, 24, -2, + -28, -9, 8, 11, 27, 12, 24, -23, + -10, -38, 4, 22, 18, 33, 3, 8, + -98, -4, -41, -24, 12, -3, -7, 5 ]); -const ROOK_POSITION_SCORES: StaticArray = StaticArray.fromArray([ - 0, 0, 0, 0, 0, 0, 0, 0, - 1, 2, 2, 2, 2, 2, 2, 1, - -1, 0, 0, 0, 0, 0, 0, -1, - -1, 0, 0, 0, 0, 0, 0, -1, - -1, 0, 0, 0, 0, 0, 0, -1, - -1, 0, 0, 0, 0, 0, 0, -1, - -1, 0, 0, 0, 0, 0, 0, -1, - 0, 0, 0, 1, 1, 0, 0, 0 +export const EG_KNIGHT_POSITION_SCORES: StaticArray = StaticArray.fromArray([ + 17, 14, 35, 2, -27, 8, -42, -44, + 38, 32, -27, 16, 15, -10, 1, -10, + 18, 0, 29, 28, -9, -6, -1, -16, + 30, 29, 42, 36, 47, 27, 34, 2, + 8, 11, 38, 35, 33, 34, 26, 16, + 16, 15, 10, 32, 21, 15, -3, 26, + -20, 16, 8, 2, 26, -6, 3, -17, + 28, -9, 22, 38, 14, 15, 9, -35 ]); -const QUEEN_POSITION_SCORES: StaticArray = StaticArray.fromArray([ - -4, -2, -2, -1, -1, -2, -2, -4, - -2, 0, 0, 0, 0, 0, 0, -2, - -2, 0, 1, 1, 1, 1, 0, -2, - -1, 0, 1, 1, 1, 1, 0, -1, - 0, 0, 1, 1, 1, 1, 0, -1, - -2, 1, 1, 1, 1, 1, 0, -2, - -2, 0, 1, 0, 0, 0, 0, -2, - -4, -2, -2, -1, -1, -2, -2, -4 +export const BISHOP_POSITION_SCORES: StaticArray = StaticArray.fromArray([ + -54, -14, -99, -66, -13, -24, -34, -13, + -58, 1, -26, -30, 44, 78, 27, -68, + -33, 13, 46, 28, 37, 57, 28, -2, + -23, 17, 26, 63, 39, 40, 19, 1, + 4, 28, 17, 40, 61, 15, 19, 18, + 7, 38, 29, 27, 23, 53, 29, 10, + 35, 31, 39, 21, 33, 44, 51, 16, + -13, 20, 13, -3, 18, 11, -10, -12 ]); -const KING_POSITION_SCORES: StaticArray = StaticArray.fromArray([ - -5, -5, -5, -5, -5, -5, -5, -5, - -5, -5, -5, -5, -5, -5, -5, -5, - -5, -5, -5, -5, -5, -5, -5, -5, - -5, -5, -5, -5, -5, -5, -5, -5, - -5, -5, -5, -5, -5, -5, -5, -5, - -5, -5, -5, -5, -5, -5, -5, -5, - 2, 2, 0, 0, 0, 0, 2, 2, - 3, 2, 2, 0, 0, 2, 2, 3 +export const EG_BISHOP_POSITION_SCORES: StaticArray = StaticArray.fromArray([ + -12, -46, -17, -25, -29, -36, -32, -33, + -12, -30, -21, -38, -46, -57, -40, -18, + -10, -33, -44, -38, -41, -34, -31, -18, + -13, -29, -26, -35, -28, -25, -34, -23, + -39, -36, -16, -22, -45, -26, -42, -34, + -35, -39, -28, -27, -16, -43, -36, -30, + -57, -43, -47, -35, -30, -48, -39, -55, + -44, -36, -29, -19, -33, -31, -36, -36 ]); -const KING_ENDGAME_POSITION_SCORES: StaticArray = StaticArray.fromArray([ - -10, -8, -6, -4, -4, -6, -8, -10, - -6, -4, -2, 0, 0, -2, -4, -6, - -6, -2, 4, 6, 6, 4, -2, -6, - -6, -2, 6, 8, 8, 6, -2, -6, - -6, -2, 6, 8, 8, 6, -2, -6, - -6, -2, 4, 6, 6, 4, -2, -6, - -6, -6, 0, 0, 0, 0, -6, -6, - -10, -6, -6, -6, -6, -6, -6, -10 +export const ROOK_POSITION_SCORES: StaticArray = StaticArray.fromArray([ + 23, 20, 31, 72, 69, 8, 5, 5, + 18, 40, 86, 92, 82, 97, 53, 47, + -15, 39, 42, 43, 23, 73, 85, 22, + -13, -7, 21, 30, 16, 34, 14, 1, + -33, -15, 6, 15, 18, 1, 46, -5, + -43, -11, -4, -11, 1, 2, 17, -11, + -39, -9, -7, 7, 7, 10, 13, -46, + -21, -1, 18, 18, 18, 3, -1, -8 +]); + +export const EG_ROOK_POSITION_SCORES: StaticArray = StaticArray.fromArray([ + 18, 18, 14, 3, 9, 17, 18, 15, + 18, 13, 1, -2, -6, -2, 8, 8, + 21, 11, 7, 11, 9, 0, 0, 5, + 16, 16, 20, 14, 15, 19, 8, 12, + 21, 17, 17, 8, 6, 6, -9, 2, + 13, 11, 4, 8, 6, 3, -2, -8, + 8, 4, 8, 6, 2, 1, -5, 8, + 5, 5, 2, 5, 4, 15, 1, -26 +]); + +export const QUEEN_POSITION_SCORES: StaticArray = StaticArray.fromArray([ + -25, 3, 47, 48, 62, 44, 40, 38, + -31, -49, -3, 20, -23, 53, 7, 19, + 8, -2, 16, -3, 12, 40, 1, 23, + -39, -33, -33, -27, -21, 7, -24, -9, + 0, -32, -10, -16, -3, 3, -5, 4, + -11, 10, -10, 0, -5, 2, 16, 16, + -25, -9, 22, 13, 25, 34, 14, 32, + 4, 2, 11, 28, 2, -18, -23, -52 +]); + +export const EG_QUEEN_POSITION_SCORES: StaticArray = StaticArray.fromArray([ + 44, 77, 50, 44, 56, 51, 58, 88, + 45, 75, 60, 73, 73, 36, 81, 81, + 21, 52, 35, 98, 86, 64, 89, 83, + 84, 89, 80, 96, 124, 91, 147, 132, + 32, 89, 68, 101, 78, 84, 123, 104, + 58, 26, 67, 53, 65, 78, 86, 85, + 40, 37, 18, 24, 25, 14, 10, 11, + 29, 18, 20, 7, 51, 38, 58, 34 +]); + +export const KING_POSITION_SCORES: StaticArray = StaticArray.fromArray([ + -6, 293, 247, 145, 35, -28, 41, 55, + 159, 114, 203, 245, 153, 108, -49, 16, + 69, 223, 180, 232, 238, 227, 225, 16, + 46, 120, 136, 155, 180, 102, 54, -47, + -119, 75, 66, 56, 78, 38, 5, -83, + -19, -3, 11, -21, -19, 8, -10, -52, + -2, 9, -31, -97, -76, -40, 2, 12, + -27, 20, 8, -69, -54, -55, 36, 30 + ] +); + +export const EG_KING_POSITION_SCORES: StaticArray = StaticArray.fromArray([ + -87, -79, -54, -42, -11, 33, 14, -13, + -28, 6, -9, -17, 4, 33, 43, 13, + 10, -4, 8, -11, -9, 30, 30, 16, + -10, 11, 18, 17, 11, 35, 36, 25, + 11, -7, 28, 33, 33, 38, 24, 14, + -12, 9, 23, 42, 47, 33, 23, 9, + -31, -10, 24, 47, 48, 26, 3, -23, + -66, -47, -23, 3, -7, 5, -47, -77 ]); const WHITE_POSITION_SCORES: StaticArray = new StaticArray(64 * 7); const BLACK_POSITION_SCORES: StaticArray = new StaticArray(64 * 7); -export const POSITION_SCORE_MULTIPLIERS: StaticArray = StaticArray.fromArray([0, 5, 3, 6, 3, 3, 6]); - export function calculatePieceSquareTables(): void { combineScores(WHITE_POSITION_SCORES, WHITE, [PAWN_POSITION_SCORES, KNIGHT_POSITION_SCORES, BISHOP_POSITION_SCORES, ROOK_POSITION_SCORES, QUEEN_POSITION_SCORES, KING_POSITION_SCORES], - [PAWN_POSITION_SCORES, KNIGHT_POSITION_SCORES, BISHOP_POSITION_SCORES, ROOK_POSITION_SCORES, QUEEN_POSITION_SCORES, KING_ENDGAME_POSITION_SCORES]); + [EG_PAWN_POSITION_SCORES, EG_KNIGHT_POSITION_SCORES, EG_BISHOP_POSITION_SCORES, EG_ROOK_POSITION_SCORES, EG_QUEEN_POSITION_SCORES, EG_KING_POSITION_SCORES]); combineScores(BLACK_POSITION_SCORES, BLACK, [mirrored(PAWN_POSITION_SCORES), mirrored(KNIGHT_POSITION_SCORES), mirrored(BISHOP_POSITION_SCORES), mirrored(ROOK_POSITION_SCORES), mirrored(QUEEN_POSITION_SCORES), mirrored(KING_POSITION_SCORES)], - [mirrored(PAWN_POSITION_SCORES), mirrored(KNIGHT_POSITION_SCORES), mirrored(BISHOP_POSITION_SCORES), mirrored(ROOK_POSITION_SCORES), mirrored(QUEEN_POSITION_SCORES), mirrored(KING_ENDGAME_POSITION_SCORES)]); + [mirrored(EG_PAWN_POSITION_SCORES), mirrored(EG_KNIGHT_POSITION_SCORES), mirrored(EG_BISHOP_POSITION_SCORES), mirrored(EG_ROOK_POSITION_SCORES), mirrored(EG_QUEEN_POSITION_SCORES), mirrored(EG_KING_POSITION_SCORES)]); } function combineScores(result: StaticArray, color: i32, midgameScores: StaticArray[], endgameScores: StaticArray[]): StaticArray { let index = 64; for (let pieceId = PAWN; pieceId <= KING; pieceId++) { - const multiplier = unchecked(POSITION_SCORE_MULTIPLIERS[pieceId]) * color; const pieceValue = i16(unchecked(PIECE_VALUES[pieceId]) * color); const egPieceValue = i16(unchecked(EG_PIECE_VALUES[pieceId]) * color); for (let pos = 0; pos < 64; pos++) { - const posScore = pieceValue + i16(unchecked(midgameScores[pieceId - 1][pos]) * multiplier); - const egPosScore = egPieceValue + i16(unchecked(endgameScores[pieceId - 1][pos]) * multiplier); + const posScore = pieceValue + i16(unchecked(midgameScores[pieceId - 1][pos]) * color); + const egPosScore = egPieceValue + i16(unchecked(endgameScores[pieceId - 1][pos]) * color); unchecked(result[index++] = packScores(posScore, egPosScore)); } } diff --git a/assembly/pieces.ts b/assembly/pieces.ts index 3eea644..55651ef 100644 --- a/assembly/pieces.ts +++ b/assembly/pieces.ts @@ -23,17 +23,17 @@ export const ROOK: i32 = 4; export const QUEEN: i32 = 5; export const KING: i32 = 6; -const KING_VALUE = 1200; -export let QUEEN_VALUE = 950; -export let EG_QUEEN_VALUE = 1023; -export let ROOK_VALUE = 492; -export let EG_ROOK_VALUE = 569; -export let BISHOP_VALUE = 351; -export let EG_BISHOP_VALUE = 354; -export let KNIGHT_VALUE = 325; -export let EG_KNIGHT_VALUE = 283; -export let PAWN_VALUE = 83; -export let EG_PAWN_VALUE = 105; +const KING_VALUE = 1500; +export let EG_QUEEN_VALUE = 991; +export let QUEEN_VALUE = 1376; +export let EG_ROOK_VALUE = 568; +export let ROOK_VALUE = 659; +export let EG_BISHOP_VALUE = 335; +export let BISHOP_VALUE = 489; +export let EG_KNIGHT_VALUE = 267; +export let KNIGHT_VALUE = 456; +export let EG_PAWN_VALUE = 107; +export let PAWN_VALUE = 102; export const PIECE_VALUES: StaticArray = StaticArray.fromArray([0, PAWN_VALUE, KNIGHT_VALUE, BISHOP_VALUE, ROOK_VALUE, QUEEN_VALUE, KING_VALUE]); export const EG_PIECE_VALUES: StaticArray = StaticArray.fromArray([0, EG_PAWN_VALUE, EG_KNIGHT_VALUE, EG_BISHOP_VALUE, EG_ROOK_VALUE, EG_QUEEN_VALUE, KING_VALUE]); diff --git a/assembly/uci.ts b/assembly/uci.ts index 71b7e95..59a834b 100644 --- a/assembly/uci.ts +++ b/assembly/uci.ts @@ -19,17 +19,32 @@ /// /// -import { POSITION_SCORE_MULTIPLIERS } from './board'; +import { EG_QUEEN_MOB_BONUS } from './board'; +import { EG_ROOK_MOB_BONUS } from './board'; +import { EG_BISHOP_MOB_BONUS } from './board'; +import { EG_KNIGHT_MOB_BONUS } from './board'; +import { EG_QUEEN_POSITION_SCORES } from './board'; +import { QUEEN_POSITION_SCORES } from './board'; +import { EG_ROOK_POSITION_SCORES } from './board'; +import { ROOK_POSITION_SCORES } from './board'; +import { EG_BISHOP_POSITION_SCORES } from './board'; +import { BISHOP_POSITION_SCORES } from './board'; +import { EG_KNIGHT_POSITION_SCORES } from './board'; +import { KNIGHT_POSITION_SCORES } from './board'; +import { EG_PAWN_POSITION_SCORES } from './board'; +import { EG_KING_POSITION_SCORES } from './board'; +import { KING_POSITION_SCORES } from './board'; +import { PAWN_POSITION_SCORES } from './board'; +import { QUEEN_MOB_BONUS } from './board'; +import { ROOK_MOB_BONUS } from './board'; +import { BISHOP_MOB_BONUS } from './board'; +import { KNIGHT_MOB_BONUS } from './board'; +import { KING_DANGER_PIECE_PENALTY } from './board'; import EngineControl from './engine'; import { TIMEEXT_MULTIPLIER } from './engine'; +import { fromFEN } from './fen'; import { clock, stdio } from './io'; import { STARTPOS } from './fen'; -import { KING } from './pieces'; -import { QUEEN } from './pieces'; -import { ROOK } from './pieces'; -import { BISHOP } from './pieces'; -import { KNIGHT } from './pieces'; -import { PAWN } from './pieces'; import { UCIMove } from './uci-move-notation'; import { DEFAULT_SIZE_MB, MAX_HASH_SIZE_MB, TRANSPOSITION_MAX_DEPTH } from './transposition-table'; import { calculatePieceSquareTables, WHITE } from './board'; @@ -118,6 +133,10 @@ export function _start(): void { isRunning = false; break; + } else if (command == "eval" && (i + 1) < tokens.length) { + evalPositions(tokens.slice(i + 1)); + break; + } else if (command == "stop") { // No op break; @@ -264,6 +283,16 @@ function perft(depthStr: string): void { } } +function setArrayOptionValue(name: string, key: string, optionValue: string, targetArray: StaticArray): void { + const index = I32.parseInt(name.substring(key.length)); + if (index < 0 || index > targetArray.length) { + stdio.writeError("Index outside target array: " + key) + return; + } + const value = I32.parseInt(optionValue); + targetArray[index] = value; +} + function setOption(params: Array): void { if (params.length < 4) { stdio.writeLine("Missing parameters for setoption"); @@ -295,15 +324,116 @@ function setOption(params: Array): void { if (useBook) { randomizeOpeningBookMoves(); } + // *** Engine/Search Parameters + // } else if (name.toLowerCase() == "futilitymarginmultiplier") { + // FUTILITY_MARGIN_MULTIPLIER = I32.parseInt(params[3]); + // + // } else if (name.toLowerCase() == "qsseethreshold") { + // QS_SEE_THRESHOLD = I32.parseInt(params[3]); + // + // } else if (name.toLowerCase() == "qsprunemargin") { + // QS_PRUNE_MARGIN = I32.parseInt(params[3]) * 25; + // + // } else if (name.toLowerCase() == "razormargin") { + // RAZOR_MARGIN = I32.parseInt(params[3]); + + // *** Eval Parameters + } else if (name.toLowerCase().startsWith("kingdangerpenalty")) { + setArrayOptionValue(name, "kingdangerpenalty", params[3], KING_DANGER_PIECE_PENALTY); + + } else if (name.toLowerCase().startsWith("knightmobbonus")) { + setArrayOptionValue(name, "knightmobbonus", params[3], KNIGHT_MOB_BONUS); + + } else if (name.toLowerCase().startsWith("bishopmobbonus")) { + setArrayOptionValue(name, "bishopmobbonus", params[3], BISHOP_MOB_BONUS); + + } else if (name.toLowerCase().startsWith("rookmobbonus")) { + setArrayOptionValue(name, "rookmobbonus", params[3], ROOK_MOB_BONUS); + + } else if (name.toLowerCase().startsWith("queenmobbonus")) { + setArrayOptionValue(name, "queenmobbonus", params[3], QUEEN_MOB_BONUS); + + } else if (name.toLowerCase().startsWith("egknightmobbonus")) { + setArrayOptionValue(name, "egknightmobbonus", params[3], EG_KNIGHT_MOB_BONUS); + + } else if (name.toLowerCase().startsWith("egbishopmobbonus")) { + setArrayOptionValue(name, "egbishopmobbonus", params[3], EG_BISHOP_MOB_BONUS); + + } else if (name.toLowerCase().startsWith("egrookmobbonus")) { + setArrayOptionValue(name, "egrookmobbonus", params[3], EG_ROOK_MOB_BONUS); + + } else if (name.toLowerCase().startsWith("egqueenmobbonus")) { + setArrayOptionValue(name, "egqueenmobbonus", params[3], EG_QUEEN_MOB_BONUS); + + } else if (name.toLowerCase().startsWith("pawnpst")) { + setArrayOptionValue(name, "pawnpst", params[3], PAWN_POSITION_SCORES); + pieceValuesChanged = true; + + } else if (name.toLowerCase().startsWith("egpawnpst")) { + setArrayOptionValue(name, "egpawnpst", params[3], EG_PAWN_POSITION_SCORES); + pieceValuesChanged = true; + + } else if (name.toLowerCase().startsWith("knightpst")) { + setArrayOptionValue(name, "knightpst", params[3], KNIGHT_POSITION_SCORES); + pieceValuesChanged = true; + + } else if (name.toLowerCase().startsWith("egknightpst")) { + setArrayOptionValue(name, "egknightpst", params[3], EG_KNIGHT_POSITION_SCORES); + pieceValuesChanged = true; + + } else if (name.toLowerCase().startsWith("bishoppst")) { + setArrayOptionValue(name, "bishoppst", params[3], BISHOP_POSITION_SCORES); + pieceValuesChanged = true; - } else if (name.toLowerCase() == "kingmultiplier") { - unchecked(POSITION_SCORE_MULTIPLIERS[KING] = I32.parseInt(params[3])); + } else if (name.toLowerCase().startsWith("egbishoppst")) { + setArrayOptionValue(name, "egbishoppst", params[3], EG_BISHOP_POSITION_SCORES); pieceValuesChanged = true; - } else if (name.toLowerCase() == "queenmultiplier") { - unchecked(POSITION_SCORE_MULTIPLIERS[QUEEN] = I32.parseInt(params[3])); + } else if (name.toLowerCase().startsWith("rookpst")) { + setArrayOptionValue(name, "rookpst", params[3], ROOK_POSITION_SCORES); pieceValuesChanged = true; + } else if (name.toLowerCase().startsWith("egrookpst")) { + setArrayOptionValue(name, "egrookpst", params[3], EG_ROOK_POSITION_SCORES); + pieceValuesChanged = true; + + } else if (name.toLowerCase().startsWith("queenpst")) { + setArrayOptionValue(name, "queenpst", params[3], QUEEN_POSITION_SCORES); + pieceValuesChanged = true; + + } else if (name.toLowerCase().startsWith("egqueenpst")) { + setArrayOptionValue(name, "egqueenpst", params[3], EG_QUEEN_POSITION_SCORES); + pieceValuesChanged = true; + + } else if (name.toLowerCase().startsWith("kingpst")) { + setArrayOptionValue(name, "kingpst", params[3], KING_POSITION_SCORES); + pieceValuesChanged = true; + + } else if (name.toLowerCase().startsWith("egkingpst")) { + setArrayOptionValue(name, "egkingpst", params[3], EG_KING_POSITION_SCORES); + pieceValuesChanged = true; + + // } else if (name.toLowerCase() == "doubledpawnpenalty") { + // DOUBLED_PAWN_PENALTY = I32.parseInt(params[3]); + // + // } else if (name.toLowerCase() == "passedpawnbonus") { + // PASSED_PAWN_BONUS = I32.parseInt(params[3]); + // + // } else if (name.toLowerCase() == "kingshieldbonus") { + // KING_SHIELD_BONUS = I32.parseInt(params[3]); + // + // } else if (name.toLowerCase() == "castlingbonus") { + // CASTLING_BONUS = I32.parseInt(params[3]); + // + // } else if (name.toLowerCase() == "lostkingsidecastlingpenalty") { + // LOST_KINGSIDE_CASTLING_PENALTY = I32.parseInt(params[3]); + // + // } else if (name.toLowerCase() == "lostqueensidecastlingpenalty") { + // LOST_QUEENSIDE_CASTLING_PENALTY = I32.parseInt(params[3]); + // + // } else if (name.toLowerCase() == "pawncoverbonus") { + // PAWN_COVER_BONUS = I32.parseInt(params[3]); + } else if (name.toLowerCase() == "queenvalue") { QUEEN_VALUE = I32.parseInt(params[3]); pieceValuesChanged = true; @@ -312,10 +442,6 @@ function setOption(params: Array): void { EG_QUEEN_VALUE = I32.parseInt(params[3]); pieceValuesChanged = true; - } else if (name.toLowerCase() == "rookmultiplier") { - unchecked(POSITION_SCORE_MULTIPLIERS[ROOK] = I32.parseInt(params[3])); - pieceValuesChanged = true; - } else if (name.toLowerCase() == "rookvalue") { ROOK_VALUE = I32.parseInt(params[3]); pieceValuesChanged = true; @@ -324,10 +450,6 @@ function setOption(params: Array): void { EG_ROOK_VALUE = I32.parseInt(params[3]); pieceValuesChanged = true; - } else if (name.toLowerCase() == "bishopmultiplier") { - unchecked(POSITION_SCORE_MULTIPLIERS[BISHOP] = I32.parseInt(params[3])); - pieceValuesChanged = true; - } else if (name.toLowerCase() == "bishopvalue") { BISHOP_VALUE = I32.parseInt(params[3]); pieceValuesChanged = true; @@ -336,10 +458,6 @@ function setOption(params: Array): void { EG_BISHOP_VALUE = I32.parseInt(params[3]); pieceValuesChanged = true; - } else if (name.toLowerCase() == "knightmultiplier") { - unchecked(POSITION_SCORE_MULTIPLIERS[KNIGHT] = I32.parseInt(params[3])); - pieceValuesChanged = true; - } else if (name.toLowerCase() == "knightvalue") { KNIGHT_VALUE = I32.parseInt(params[3]); pieceValuesChanged = true; @@ -348,10 +466,6 @@ function setOption(params: Array): void { EG_KNIGHT_VALUE = I32.parseInt(params[3]); pieceValuesChanged = true; - } else if (name.toLowerCase() == "pawnmultiplier") { - unchecked(POSITION_SCORE_MULTIPLIERS[PAWN] = I32.parseInt(params[3])); - pieceValuesChanged = true; - } else if (name.toLowerCase() == "pawnvalue") { PAWN_VALUE = I32.parseInt(params[3]); pieceValuesChanged = true; @@ -376,6 +490,22 @@ function test(): void { stdio.writeLine("debug Self test completed"); } +function evalPositions(tokens: Array): void { + const fens = tokens.join(" ").split(";") + + let scores = "scores "; + for (let i = 0; i < fens.length; i++) { + const fen = fens[i]; + const score = fromFEN(fen).getScore(); + if (i > 0) { + scores += ';' + } + scores += score.toString(); + } + + stdio.writeLine(scores); +} + // Helper functions function tokenize(str: string): Array { const parts = str.split(' ', 65536); diff --git a/tools/tuning/.gitignore b/tools/tuning/.gitignore new file mode 100644 index 0000000..02ecc26 --- /dev/null +++ b/tools/tuning/.gitignore @@ -0,0 +1,2 @@ +config.yml +tuning_result.yml diff --git a/tools/tuning/config_example.yml b/tools/tuning/config_example.yml new file mode 100644 index 0000000..c71ebdd --- /dev/null +++ b/tools/tuning/config_example.yml @@ -0,0 +1,31 @@ +engine: + cmd: /absolutePath/to/chess-engine + +options: + debug_log: false + test_positions_file: fen/quiet.fen + concurrency: 8 + +tuning: + - name: QueenValue + value: 950 + + - name: RookValue + value: 500 + + - name: BishopValue + value: 350 + + - name: KnightValue + value: 350 + + - name: PawnValue + value: 100 + + # Options with list values will be send to the engine as: + # set option KnightMobBonus0 value 0 + # set option KnightMobBonus1 value 0 + # set option KnightMobBonus2 value 0 + # ... + - name: KnightMobBonus + value: [ 0, 0, 0, 0, 0, 0, 0, 0, 0 ] diff --git a/tools/tuning/tune.py b/tools/tuning/tune.py new file mode 100644 index 0000000..7bcb332 --- /dev/null +++ b/tools/tuning/tune.py @@ -0,0 +1,376 @@ +# A free and open source chess game using AssemblyScript and React +# Copyright (C) 2020 mhonert (https://github.com/mhonert) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from dataclasses import dataclass +import logging as log +import yaml +import subprocess +from concurrent.futures import ThreadPoolExecutor, as_completed +from time import time +import sys +import os +from typing import List, Dict +import os.path +import copy + + +# Uses "Texel's Tuning Method" for tuning evaluation parameters +# see https://www.chessprogramming.org/Texel%27s_Tuning_Method for a detailed description of the method + + +# Scaling factor (calculated for Wasabi Chess engine) +K = 1.342224 + + +@dataclass +class TestPosition: + fen: str + result: float + score: int = 0 + + +@dataclass +class TuningOption: + name: str + value: int + is_part: bool = False + orig_name: str = "" + steps: int = 16 # 4 ^ n (e.g. 1/4/16/64/...) + direction: int = 1 + improvements: int = 0 + iterations: int = 0 + remaining_skips: int = 0 # Skip this option for 'remaining_skips' iterations + skip_count: int = 0 # How many times this option has already been skipped + + +# Read test positions in format: FEN result +# result may be "1-0" for a white win, "0-1" for a black win or "1/2" for a draw +def read_fens(fen_file) -> List[TestPosition]: + test_positions = [] + with open(fen_file, 'r') as file: + + # Sample line: + # rnbqkb1r/1p2ppp1/p2p1n2/2pP3p/4P3/5N2/PPP1QPPP/RNB1KB1R w KQkq - 0 1 1-0 + for line in file: + fen = line[:-5] + result_str = line[-4:].strip() + result = 1 if result_str == "1-0" else 0 if result_str == "0-1" else 0.5 + test_positions.append(TestPosition(fen, result)) + + return test_positions + + +class Engine: + def __init__(self, engine_cmd): + self.process = subprocess.Popen([engine_cmd], bufsize=1, stdin=subprocess.PIPE, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, universal_newlines=True) + + def stop(self): + log.debug("Stopping engine instance") + self.process.communicate("quit\n", timeout=2) + + self.process.kill() + self.process.communicate() + + def send_command(self, cmd): + log.debug(">>> " + cmd) + self.process.stdin.write(cmd + "\n") + + def wait_for_command(self, cmd): + for line in self.process.stdout: + line = line.rstrip() + log.debug("<<< " + line) + if cmd in line: + return line + + +def run_engine(engine: Engine, tuning_options: List[TuningOption], test_positions: List[TestPosition]): + results = [] + + try: + engine.send_command("uci") + engine.wait_for_command("uciok") + + for option in tuning_options: + engine.send_command("setoption name {} value {}".format(option.name, option.value)) + + engine.send_command("isready") + engine.wait_for_command("readyok") + + for chunk in make_chunks(test_positions, 100): + fens = "eval " + is_first = True + for pos in chunk: + if not is_first: + fens += ";" + fens += pos.fen + is_first = False + + engine.send_command(fens) + + result = engine.wait_for_command("scores") + + scores = [int(score) for score in result[len("scores "):].split(";")] + assert len(scores) == len(chunk) + + for i in range(len(scores)): + chunk[i].score = scores[i] + + except subprocess.TimeoutExpired as error: + engine.stop() + raise error + + except Exception as error: + log.error(str(error)) + + return results + + +# Split list of test positions into "batch_count" batches +def make_batches(positions: List[TestPosition], batch_count: int) -> List[List[TestPosition]]: + max_length = len(positions) + batch_size = max(1, max_length // batch_count) + return make_chunks(positions, batch_size) + + +# Split list of test positions into chunks of size "chunk_size" +def make_chunks(positions: List[TestPosition], chunk_size: int) -> List[List[TestPosition]]: + max_length = len(positions) + for i in range(0, max_length, chunk_size): + yield positions[i:min(i + chunk_size, max_length)] + + +def get_config(cfg: Dict, key: str, msg: str): + value = cfg.get(key) + if value is None: + sys.exit(msg) + return value + + +@dataclass +class Config: + engine_cmd: str + debug_log: bool + test_positions_file: str + concurrent_workers: int + tuning_optins: List[TuningOption] + + def __init__(self, config_file: str): + log.info("Reading configuration ...") + + cfg_stream = open(config_file, "r") + + cfg = yaml.safe_load(cfg_stream) + engine_cfg = get_config(cfg, "engine", "Missing 'engine' configuration") + + self.engine_cmd = get_config(engine_cfg, "cmd", "Missing 'engine > cmd' configuration") + + options = get_config(cfg, "options", "Missing 'options' configuration") + + self.debug_log = bool(options.get("debug_log", False)) + + self.test_positions_file = get_config(options, "test_positions_file", + "Missing 'options.test_positions_file' configuration") + + self.concurrent_workers = int(options.get("concurrency", 1)) + if self.concurrent_workers <= 0: + sys.exit("Invalid value for 'options > concurrency': " + options.get("concurrency")) + log.info("- use %i concurrent engine processes", self.concurrent_workers) + + if self.concurrent_workers >= os.cpu_count(): + log.warning("Configured 'options > concurrency' to be >= the number of logical CPU cores") + log.info("It is recommended to set concurrency to the number of physical CPU cores - 1") + + tuning_cfg = cfg.get('tuning') + self.tuning_options = [] + if tuning_cfg is not None: + for t in tuning_cfg: + value = t["value"] + if type(value) is list: + for index, v in enumerate(value): + option = TuningOption(t["name"] + str(index), int(v), True, t["name"]) + self.tuning_options.append(option) + + else: + option = TuningOption(t["name"], int(value)) + self.tuning_options.append(option) + + cfg_stream.close() + + +def run_pass(config: Config, k: float, engines: List[Engine], test_positions: List[TestPosition]) -> float: + futures = [] + + log.debug("Starting pass") + + with ThreadPoolExecutor(max_workers=config.concurrent_workers) as executor: + worker_id = 1 + for batch in make_batches(test_positions, config.concurrent_workers): + engine = engines[worker_id - 1] + futures.append(executor.submit(run_engine, engine, worker_id, config.tuning_options, batch)) + worker_id += 1 + + for future in as_completed(futures): + if future.cancelled(): + sys.exit("Worker was cancelled - possible engine bug? try enabling the debug_log output and re-run the 'tunomat'") + + log.debug("Pass completed") + + e = calc_avg_error(k, test_positions) + + return e + + +def calc_avg_error(k: float, positions: List[TestPosition]) -> float: + errors = .0 + for pos in positions: + win_probability = 1.0 / (1.0 + 10.0 ** (-pos.score * k / 400.0)) + error = pos.result - win_probability + error *= error + errors += error + return errors / float(len(positions)) + + +def write_options(options: List[TuningOption]): + results = [] + result_by_name = {} + for option in options: + if option.is_part: + if option.orig_name in result_by_name: + result_by_name[option.orig_name]["value"].append(option.value) + else: + result = {"name": option.orig_name, "value": [option.value]} + results.append(result) + result_by_name[option.orig_name] = result + + else: + results.append({"name": option.name, "value": option.value}) + + with open("tuning_result.yml", "w") as file: + yaml.dump(results, file, sort_keys=True, indent=4) + + +def main(): + log.basicConfig(stream=sys.stdout, level=log.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + + config = Config("config.yml") + if config.debug_log: + log.getLogger().setLevel(log.DEBUG) + + log.info("Reading test positions ...") + + test_positions = read_fens(config.test_positions_file) + log.info("Read %i test positions", len(test_positions)) + + # Start multiple engines + engines = [] + for i in range(config.concurrent_workers + 1): + engine = Engine(config.engine_cmd) + engines.append(engine) + + try: + + best_err = run_pass(config, K, engines, test_positions) + init_err = best_err + log.info("Starting err: %f", init_err) + best_options = copy.deepcopy(config.tuning_options) + + tick = time() + + retry_postponed = False + improved = True + while improved: + improved = False + config.tuning_options = best_options + write_options(best_options) + for i in range(len(config.tuning_options)): + option = config.tuning_options[i] + option.iterations += 1 + best_options[i].iterations = option.iterations + if option.remaining_skips > 0: + option.remaining_skips -= 1 + best_options[i].remaining_skips = option.remaining_skips + continue + + prev_value = option.value + option.value = prev_value + option.steps * option.direction + new_err = run_pass(config, K, engines, test_positions) + log.info("Try %s = %d [step %d] => %f", option.name, option.value, option.steps * option.direction, new_err - best_err) + if new_err < best_err: + best_err = new_err + option.improvements += 1 + option.skip_count = 0 + best_options = copy.deepcopy(config.tuning_options) + log.info("Improvement: %f", best_err) + improved = True + else: + option.value = prev_value + option.steps * -option.direction + new_err = run_pass(config, K, engines, test_positions) + log.info("Try %s = %d [step %d] => %f", option.name, option.value, option.steps * -option.direction, new_err - best_err) + if new_err < best_err: + best_err = new_err + option.direction = -option.direction + option.improvements += 1 + option.skip_count = 0 + best_options = copy.deepcopy(config.tuning_options) + log.info("Improvement: %f", best_err) + improved = True + else: + option.value = prev_value + if option.steps > 1: + # No improvement at the current 'step' level => decrease step level + option.steps >>= 2 + + if option.iterations > 1 and option.improvements == 0: + option.steps = 1 + + best_options[i].steps = option.steps + + else: + # No improvement at smallest 'step' level => skip this option for a couple iterations + option.skip_count += 1 + option.remaining_skips += 8 * option.skip_count + best_options[i].remaining_skips = option.remaining_skips + best_options[i].skip_count = option.skip_count + + if not improved and not retry_postponed: + log.info("No improvement => check postponed options") + retry_postponed = True + any_postponed = False + for option in best_options: + option.steps = 1 + if option.remaining_skips > 0: + option.remaining_skips = 0 + any_postponed = True + + improved = any_postponed + else: + retry_postponed = False + + log.info("Avg. error before tuning: %f", init_err) + log.info("Avg. error after tuning : %f", best_err) + log.info("Tuning duration : %.2fs", time() - tick) + + write_options(best_options) + + finally: + for engine in engines: + engine.stop() + + +# Main +if __name__ == "__main__": + main()