@@ -15,6 +15,19 @@ const MAX_TT_SIZE = 1000000; // Max entries in transposition table
1515const BOARD_HEIGHT = TOTAL_ROWS + 1 ; // Extra row for overflow detection
1616const BOARD_WIDTH = TOTAL_COLUMNS ;
1717
18+ // Opening book - prioritize center column
19+ const OPENING_BOOK = {
20+ '' : 3 , // First move - always play center column
21+ } ;
22+ const MAX_OPENING_MOVES = 2 ; // Only use opening book for first 2 moves
23+
24+ // Column ordering for move ordering (center columns first for better alpha-beta pruning)
25+ const COLUMN_ORDER = [ 3 , 2 , 4 , 1 , 5 , 0 , 6 ] ;
26+
27+ // Position evaluation weights
28+ const CENTER_COLUMN_WEIGHT = 3 ;
29+ const CENTER_ADJACENT_WEIGHT = 2 ;
30+
1831// Initialize Zobrist hashing table (random 64-bit values for each position and player)
1932const zobristTable = [ ] ;
2033function initZobrist ( ) {
@@ -239,6 +252,53 @@ GameState.prototype.getPlayerForChipAt = function(col, row) {
239252 return player ;
240253}
241254
255+ // Evaluate position heuristically for non-terminal positions
256+ GameState . prototype . evaluatePosition = function ( player ) {
257+ let score = 0 ;
258+
259+ // Center control - pieces in center columns are more valuable
260+ for ( let row = 0 ; row < this . bitboard . heights [ 3 ] ; row ++ ) {
261+ if ( this . board [ 3 ] [ row ] === player ) {
262+ score += CENTER_COLUMN_WEIGHT ;
263+ } else if ( this . board [ 3 ] [ row ] !== undefined ) {
264+ score -= CENTER_COLUMN_WEIGHT ;
265+ }
266+ }
267+
268+ // Adjacent to center also valuable
269+ for ( let col of [ 2 , 4 ] ) {
270+ for ( let row = 0 ; row < this . bitboard . heights [ col ] ; row ++ ) {
271+ if ( this . board [ col ] [ row ] === player ) {
272+ score += CENTER_ADJACENT_WEIGHT ;
273+ } else if ( this . board [ col ] [ row ] !== undefined ) {
274+ score -= CENTER_ADJACENT_WEIGHT ;
275+ }
276+ }
277+ }
278+
279+ // Normalize score to be within minimax range
280+ return score * 0.1 ;
281+ }
282+
283+ // Get a simple board state hash for opening book lookup
284+ function getBoardStateKey ( gameState ) {
285+ let key = '' ;
286+ let moveCount = 0 ;
287+ for ( let col = 0 ; col < TOTAL_COLUMNS ; col ++ ) {
288+ moveCount += gameState . board [ col ] . length ;
289+ }
290+
291+ // Only use opening book for first few moves
292+ if ( moveCount > MAX_OPENING_MOVES ) return null ;
293+
294+ for ( let col = 0 ; col < TOTAL_COLUMNS ; col ++ ) {
295+ for ( let row = 0 ; row < gameState . board [ col ] . length ; row ++ ) {
296+ key += col + '' + gameState . board [ col ] [ row ] ;
297+ }
298+ }
299+ return key ;
300+ }
301+
242302// listen for messages from the main thread
243303self . addEventListener ( 'message' , function ( e ) {
244304 switch ( e . data . messageType ) {
@@ -285,27 +345,40 @@ function makeComputerMove(maxDepth) {
285345 let isWinImminent = false ;
286346 let isLossImminent = false ;
287347
288- for ( let depth = 0 ; depth <= maxDepth ; depth ++ ) {
289- const origin = new GameState ( currentGameState ) ;
290- const isTopLevel = ( depth === maxDepth ) ;
291-
292- // Alpha-beta search with initial bounds
293- const tentativeCol = think ( origin , 2 , depth , isTopLevel , - Infinity , Infinity ) ;
294-
295- if ( origin . score === HUMAN_WIN_SCORE ) {
296- // AI realizes it can lose, thinks all moves suck now, keep move picked at previous depth
297- // this solves the "apathy" problem
298- isLossImminent = true ;
299- break ;
300- } else if ( origin . score === COMPUTER_WIN_SCORE ) {
301- // AI knows how to win, no need to think deeper, use this move
302- // this solves the "cocky" problem
303- col = tentativeCol ;
304- isWinImminent = true ;
305- break ;
306- } else {
307- // go with this move, for now at least
308- col = tentativeCol ;
348+ // Check opening book first
349+ const boardKey = getBoardStateKey ( currentGameState ) ;
350+ if ( boardKey !== null && boardKey in OPENING_BOOK ) {
351+ const openingCol = OPENING_BOOK [ boardKey ] ;
352+ // Verify move is valid
353+ if ( currentGameState . bitboard . heights [ openingCol ] < TOTAL_ROWS ) {
354+ col = openingCol ;
355+ }
356+ }
357+
358+ if ( col === undefined ) {
359+ // Use iterative deepening with fixed high depth
360+ for ( let depth = 0 ; depth <= maxDepth ; depth ++ ) {
361+ const origin = new GameState ( currentGameState ) ;
362+ const isTopLevel = ( depth === maxDepth ) ;
363+
364+ // Alpha-beta search with initial bounds
365+ const tentativeCol = think ( origin , 2 , depth , isTopLevel , - Infinity , Infinity ) ;
366+
367+ if ( origin . score === HUMAN_WIN_SCORE ) {
368+ // AI realizes it can lose, thinks all moves suck now, keep move picked at previous depth
369+ // this solves the "apathy" problem
370+ isLossImminent = true ;
371+ break ;
372+ } else if ( origin . score === COMPUTER_WIN_SCORE ) {
373+ // AI knows how to win, no need to think deeper, use this move
374+ // this solves the "cocky" problem
375+ col = tentativeCol ;
376+ isWinImminent = true ;
377+ break ;
378+ } else {
379+ // go with this move, for now at least
380+ col = tentativeCol ;
381+ }
309382 }
310383 }
311384
@@ -350,13 +423,14 @@ function think(node, player, recursionsRemaining, isTopLevel, alpha, beta) {
350423 }
351424 }
352425
353- let col ;
354426 let scoreSet = false ;
355427 const childNodes = [ ] ;
356428 let bestMove = - 1 ;
357429
358- // consider each column as a potential move
359- for ( col = 0 ; col < TOTAL_COLUMNS ; col ++ ) {
430+ // Use column ordering for better alpha-beta pruning (center columns first)
431+ for ( let colIdx = 0 ; colIdx < COLUMN_ORDER . length ; colIdx ++ ) {
432+ const col = COLUMN_ORDER [ colIdx ] ;
433+
360434 if ( isTopLevel ) {
361435 self . postMessage ( {
362436 messageType : 'progress' ,
@@ -376,6 +450,10 @@ function think(node, player, recursionsRemaining, isTopLevel, alpha, beta) {
376450 // no game stopping win and there are still recursions to make, think deeper
377451 const nextPlayer = ( player === 1 ) ? 2 : 1 ;
378452 think ( childNode , nextPlayer , recursionsRemaining - 1 , false , alpha , beta ) ;
453+ } else if ( ! childNode . isWin ( ) && recursionsRemaining === 0 ) {
454+ // At leaf node, apply heuristic evaluation
455+ const heuristicScore = childNode . evaluatePosition ( 2 ) ; // Evaluate for computer
456+ childNode . score = heuristicScore ;
379457 }
380458
381459 if ( ! scoreSet ) {
@@ -433,7 +511,7 @@ function think(node, player, recursionsRemaining, isTopLevel, alpha, beta) {
433511 // For non-top level, just return the best move (may have been pruned)
434512 if ( isTopLevel ) {
435513 const candidates = [ ] ;
436- for ( col = 0 ; col < TOTAL_COLUMNS ; col ++ ) {
514+ for ( let col = 0 ; col < TOTAL_COLUMNS ; col ++ ) {
437515 if ( childNodes [ col ] !== undefined && childNodes [ col ] . score === node . score ) {
438516 candidates . push ( col ) ;
439517 }
0 commit comments