-
Notifications
You must be signed in to change notification settings - Fork 0
Implement AI optimizations: Alpha-Beta Pruning, Bitboards, and Transposition Table #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2bfdeff
27a3d31
5176c67
49df547
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,14 +5,168 @@ const HUMAN_WIN_SCORE = -4; | |||||||||||||||||||||||||||||||||||||||||||||||
| const COMPUTER_WIN_SCORE = 4; | ||||||||||||||||||||||||||||||||||||||||||||||||
| const NO_WIN_SCORE = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Transposition Table constants | ||||||||||||||||||||||||||||||||||||||||||||||||
| const TT_EXACT = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||
| const TT_LOWERBOUND = 1; | ||||||||||||||||||||||||||||||||||||||||||||||||
| const TT_UPPERBOUND = 2; | ||||||||||||||||||||||||||||||||||||||||||||||||
| const MAX_TT_SIZE = 1000000; // Max entries in transposition table | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Bitboard constants | ||||||||||||||||||||||||||||||||||||||||||||||||
| const BOARD_HEIGHT = TOTAL_ROWS + 1; // Extra row for overflow detection | ||||||||||||||||||||||||||||||||||||||||||||||||
| const BOARD_WIDTH = TOTAL_COLUMNS; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Initialize Zobrist hashing table (random 64-bit values for each position and player) | ||||||||||||||||||||||||||||||||||||||||||||||||
| const zobristTable = []; | ||||||||||||||||||||||||||||||||||||||||||||||||
| function initZobrist() { | ||||||||||||||||||||||||||||||||||||||||||||||||
| zobristTable.length = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Use a simple seeded random number generator for better distribution | ||||||||||||||||||||||||||||||||||||||||||||||||
| let seed = 12345n; | ||||||||||||||||||||||||||||||||||||||||||||||||
| const next = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| seed = (seed * 48271n) % 2147483647n; | ||||||||||||||||||||||||||||||||||||||||||||||||
| return seed; | ||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| for (let col = 0; col < TOTAL_COLUMNS; col++) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| zobristTable[col] = []; | ||||||||||||||||||||||||||||||||||||||||||||||||
| for (let row = 0; row < TOTAL_ROWS; row++) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| zobristTable[col][row] = []; | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Generate pseudo-random 64-bit values for each player | ||||||||||||||||||||||||||||||||||||||||||||||||
| zobristTable[col][row][1] = (next() << 32n) | next(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| zobristTable[col][row][2] = (next() << 32n) | next(); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+23
to
+35
|
||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| initZobrist(); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Transposition table | ||||||||||||||||||||||||||||||||||||||||||||||||
| let transpositionTable = new Map(); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Bitboard utility functions | ||||||||||||||||||||||||||||||||||||||||||||||||
| function createBitboard() { | ||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||
| player1: 0n, // Bitboard for player 1 | ||||||||||||||||||||||||||||||||||||||||||||||||
| player2: 0n, // Bitboard for player 2 | ||||||||||||||||||||||||||||||||||||||||||||||||
| heights: Array(TOTAL_COLUMNS).fill(0), // Height of each column | ||||||||||||||||||||||||||||||||||||||||||||||||
| hash: 0n // Zobrist hash | ||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| function copyBitboard(bb) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||
| player1: bb.player1, | ||||||||||||||||||||||||||||||||||||||||||||||||
| player2: bb.player2, | ||||||||||||||||||||||||||||||||||||||||||||||||
| heights: bb.heights.slice(), | ||||||||||||||||||||||||||||||||||||||||||||||||
| hash: bb.hash | ||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Convert column and row to bit position | ||||||||||||||||||||||||||||||||||||||||||||||||
| function positionToBit(col, row) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| return BigInt(col * BOARD_HEIGHT + row); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Make a move on bitboard | ||||||||||||||||||||||||||||||||||||||||||||||||
| function bitboardMakeMove(bb, player, col) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const row = bb.heights[col]; | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (row >= TOTAL_ROWS) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| return null; // Column full | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const pos = positionToBit(col, row); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const mask = 1n << pos; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| if (player === 1) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| bb.player1 |= mask; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||
| bb.player2 |= mask; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Update Zobrist hash | ||||||||||||||||||||||||||||||||||||||||||||||||
| bb.hash ^= zobristTable[col][row][player]; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| bb.heights[col]++; | ||||||||||||||||||||||||||||||||||||||||||||||||
| return { col, row }; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Check if a bitboard position has 4 in a row | ||||||||||||||||||||||||||||||||||||||||||||||||
| function bitboardCheckWin(bitboard) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Horizontal check | ||||||||||||||||||||||||||||||||||||||||||||||||
| let m = bitboard & (bitboard >> BigInt(BOARD_HEIGHT)); | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (m & (m >> BigInt(BOARD_HEIGHT * 2))) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Vertical check | ||||||||||||||||||||||||||||||||||||||||||||||||
| m = bitboard & (bitboard >> 1n); | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (m & (m >> 2n)) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Diagonal / check | ||||||||||||||||||||||||||||||||||||||||||||||||
| m = bitboard & (bitboard >> BigInt(BOARD_HEIGHT - 1)); | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (m & (m >> BigInt((BOARD_HEIGHT - 1) * 2))) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Diagonal \ check | ||||||||||||||||||||||||||||||||||||||||||||||||
| m = bitboard & (bitboard >> BigInt(BOARD_HEIGHT + 1)); | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (m & (m >> BigInt((BOARD_HEIGHT + 1) * 2))) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Find winning chips for highlighting | ||||||||||||||||||||||||||||||||||||||||||||||||
| function findWinningChips(bitboard, lastCol, lastRow) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const directions = [ | ||||||||||||||||||||||||||||||||||||||||||||||||
| { dc: 0, dr: 1 }, // Vertical | ||||||||||||||||||||||||||||||||||||||||||||||||
| { dc: 1, dr: 0 }, // Horizontal | ||||||||||||||||||||||||||||||||||||||||||||||||
| { dc: 1, dr: 1 }, // Diagonal / | ||||||||||||||||||||||||||||||||||||||||||||||||
| { dc: 1, dr: -1 } // Diagonal \ | ||||||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| for (const dir of directions) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const chips = []; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Check in both directions from last move | ||||||||||||||||||||||||||||||||||||||||||||||||
| for (let step = -3; step <= 3; step++) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const c = lastCol + step * dir.dc; | ||||||||||||||||||||||||||||||||||||||||||||||||
| const r = lastRow + step * dir.dr; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| if (c >= 0 && c < TOTAL_COLUMNS && r >= 0 && r < TOTAL_ROWS) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const pos = positionToBit(c, r); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const mask = 1n << pos; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| if (bitboard & mask) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| chips.push({ col: c, row: r }); | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (chips.length === 4) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| return chips; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||
| chips.length = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (step === 0) break; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||
| chips.length = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (step === 0) break; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // game state object | ||||||||||||||||||||||||||||||||||||||||||||||||
| const GameState = function (cloneGameState) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.board = Array.from({ length: TOTAL_COLUMNS }, () => []); | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.bitboard = createBitboard(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.score = NO_WIN_SCORE; | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.winningChips = undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| if (cloneGameState) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.board = cloneGameState.board.map(col => col.slice()); | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.bitboard = copyBitboard(cloneGameState.bitboard); | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.score = cloneGameState.score; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
| this.score = cloneGameState.score; | |
| this.score = cloneGameState.score; | |
| this.winningChips = Array.isArray(cloneGameState.winningChips) | |
| ? cloneGameState.winningChips.map(chip => ({ ...chip })) | |
| : cloneGameState.winningChips; |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The declaration of coords is initialized to undefined on line 175, but then immediately overwritten on line 181 by the result of bitboardMakeMove. This initialization is redundant since coords will always be assigned a new value within the if block. Consider declaring coords only when needed for clarity.
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The makeMove function updates both the legacy array-based board and the new bitboard representation. However, there's a subtle inconsistency: the row variable is calculated from the array-based board (line 176), but then used to make a move on the bitboard (line 181). This works currently because both representations start empty and are kept in sync, but if there were ever a desynchronization, this could lead to incorrect behavior. It would be more robust to use the bitboard's heights array directly for determining the row, or to verify that both representations agree.
| const row = this.board[col].length; | |
| if (row < TOTAL_ROWS) { | |
| this.board[col][row] = player; | |
| // Also make move on bitboard | |
| coords = bitboardMakeMove(this.bitboard, player, col); | |
| this.setScore(player, col, row); | |
| // Use bitboard's heights to determine row after move | |
| if (this.board[col].length < TOTAL_ROWS) { | |
| const row = this.board[col].length; | |
| this.board[col][row] = player; | |
| // Make move on bitboard and get the actual row from bitboard | |
| coords = bitboardMakeMove(this.bitboard, player, col); | |
| // Use the row from bitboard's heights (coords.row) | |
| if (coords && typeof coords.row === "number") { | |
| this.setScore(player, col, coords.row); | |
| } else { | |
| // Fallback: use row from array-based board (should not happen) | |
| this.setScore(player, col, row); | |
| } |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a potential issue with the logic when no valid moves are available (all columns full). If all columns are full and no scoreSet occurs, node.score will retain its previous value (or be undefined for a new node), and bestMove will remain -1. This could cause issues when the function returns -1 as the best move. While this is a rare edge case (game should be detected as full before this happens), it would be more robust to handle this case explicitly or ensure scoreSet is always true when at least one move is possible.
| // If no valid moves were found, set node.score and bestMove explicitly | |
| if (!scoreSet) { | |
| node.score = NO_WIN_SCORE; | |
| bestMove = -1; | |
| } |
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a subtle issue with storing transposition table entries when alpha-beta pruning occurs. When the search is cut off (break at line 407), the node.score may not represent the true minimax value of the position—it's just a bound. However, the flag determination logic at lines 416-422 correctly handles this by comparing against the original alpha and beta values. The issue is that even with correct flag types, storing a pruned position might lead to suboptimal move ordering since bestMove might be the move that caused the cutoff rather than the actual best move. This is generally acceptable in practice but worth documenting.
Copilot
AI
Dec 10, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The variable declaration uses 'let' but 'col' is already declared in the function scope at line 353. This shadows the outer 'col' variable, which could lead to confusion. While this is valid JavaScript, it would be clearer to use a different variable name for the loop in lines 436-440, such as 'i' or 'candidateCol', to avoid shadowing and improve code readability.
| for (col = 0; col < TOTAL_COLUMNS; col++) { | |
| if (childNodes[col] !== undefined && childNodes[col].score === node.score) { | |
| candidates.push(col); | |
| for (let candidateCol = 0; candidateCol < TOTAL_COLUMNS; candidateCol++) { | |
| if (childNodes[candidateCol] !== undefined && childNodes[candidateCol].score === node.score) { | |
| candidates.push(candidateCol); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The BOARD_WIDTH constant is defined but never used in the code. Since TOTAL_COLUMNS is used consistently throughout the codebase for the same purpose, BOARD_WIDTH appears to be redundant. Consider removing this unused constant to reduce code clutter.