@@ -11,10 +11,15 @@ import { openDB } from '../db';
11
11
import { CodeyUserError } from '../../codeyUserError' ;
12
12
import { getEmojiByName } from '../emojis' ;
13
13
import { getRandomIntFrom1 } from '../../utils/num' ;
14
+ import { isNull } from 'lodash' ;
14
15
15
16
const CONNECT_FOUR_COLUMN_COUNT = 7 ;
16
17
const CONNECT_FOUR_ROW_COUNT = 6 ;
17
18
19
+ const TWO_IN_A_ROW_WEIGHT = 1 ;
20
+ const THREE_IN_A_ROW_WEIGHT = 3 ;
21
+
22
+
18
23
class ConnectFourGameTracker {
19
24
// Key = id, Value = game
20
25
games : Map < number , ConnectFourGame > ;
@@ -247,6 +252,7 @@ export class ConnectFourGame {
247
252
state : ConnectFourGameState ,
248
253
columnIndex : number ,
249
254
) : Promise < ConnectFourGameStatus > {
255
+
250
256
// Instead of exhaustively checking every combination of tokens we can simply use the fact that
251
257
// as of this point the user hasn't won yet, so we just need to check if the token that was just placed
252
258
// is part of a winning combination
@@ -395,6 +401,152 @@ ${this.state.player2Username}: ${getEmojiFromSign(this.state.player2Sign)}
395
401
components : this . state . status === ConnectFourGameStatus . Pending ? [ row1 , row2 ] : [ ] ,
396
402
} ;
397
403
}
404
+
405
+ // ConnectFourGameState
406
+ // Waiting = 0,
407
+ // Pending = 1,
408
+ // Draw = 2,
409
+ // Player1Win = 3,
410
+ // Player2Win = 4,
411
+ // Player1TimeOut = 5,
412
+ // Player2TimeOut = 6,
413
+ // Unknown = 7,
414
+ private updateState = ( state : ConnectFourGameState , columnNumber : number , turn : number ) : ConnectFourGameState => {
415
+ const fill : number = state . columns [ columnNumber ] . fill ;
416
+ if ( turn === 1 ) { //means its Codeybot's turn
417
+ state . columns [ columnNumber ] . tokens [ fill ] = 3 ;
418
+ } else {
419
+ state . columns [ columnNumber ] . tokens [ fill ] = 2 ;
420
+ }
421
+ state . columns [ columnNumber ] . fill = state . columns [ columnNumber ] . fill + 1 ;
422
+ return state ;
423
+ }
424
+
425
+ // returns number of possible wins remaining
426
+ private possibleWins = ( state : ConnectFourGameState , opponentSign : number ) : number => {
427
+ let possibleWins : number = 0 ;
428
+ // check vertical
429
+ for ( let i = 0 ; i < CONNECT_FOUR_COLUMN_COUNT ; i ++ ) {
430
+ for ( let j = 3 ; j < CONNECT_FOUR_ROW_COUNT ; j ++ ) {
431
+ if ( state . columns [ i ] . tokens [ j ] !== opponentSign && state . columns [ i ] . tokens [ j - 1 ] !== opponentSign && state . columns [ i ] . tokens [ j - 2 ] !== opponentSign && state . columns [ i ] . tokens [ j - 3 ] !== opponentSign ) {
432
+ possibleWins = possibleWins + 1 ;
433
+ }
434
+ }
435
+ }
436
+ // check horizonal
437
+ for ( let j = 0 ; j < CONNECT_FOUR_ROW_COUNT ; j ++ ) {
438
+ for ( let i = 3 ; i < CONNECT_FOUR_COLUMN_COUNT ; i ++ ) {
439
+ if ( state . columns [ i ] . tokens [ j ] !== opponentSign && state . columns [ i - 1 ] . tokens [ j ] !== opponentSign && state . columns [ i - 2 ] . tokens [ j ] !== opponentSign && state . columns [ i - 3 ] . tokens [ j ] !== opponentSign ) {
440
+ possibleWins = possibleWins + 1 ;
441
+ }
442
+ }
443
+ }
444
+ // check diagonal up
445
+ for ( let i = 0 ; i <= 3 ; i ++ ) {
446
+ for ( let j = 0 ; j <= 2 ; j ++ ) {
447
+ if ( state . columns [ i ] . tokens [ j ] !== opponentSign && state . columns [ i + 1 ] . tokens [ j + 1 ] !== opponentSign && state . columns [ i + 2 ] . tokens [ j + 2 ] !== opponentSign && state . columns [ i + 3 ] . tokens [ j + 3 ] !== opponentSign ) {
448
+ possibleWins = possibleWins + 1 ;
449
+ }
450
+ }
451
+ }
452
+
453
+ //check diagonal down
454
+ for ( let i = 3 ; i < CONNECT_FOUR_COLUMN_COUNT ; i ++ ) {
455
+ for ( let j = 0 ; j <= 2 ; j ++ ) {
456
+ if ( state . columns [ i ] . tokens [ j ] !== opponentSign && state . columns [ i - 1 ] . tokens [ j + 1 ] !== opponentSign && state . columns [ i - 2 ] . tokens [ j + 2 ] !== opponentSign && state . columns [ i - 3 ] . tokens [ j + 3 ] !== opponentSign ) {
457
+ possibleWins = possibleWins + 1 ;
458
+ }
459
+ }
460
+ }
461
+ return possibleWins ;
462
+ }
463
+
464
+ // takes a ConnectFourGameState and evaluates it according to heuristic function
465
+ private evaluate = ( state : ConnectFourGameState ) : number => {
466
+ let codeyPoints : number = this . possibleWins ( state , 2 ) ; // 3 represents Codeybot sign
467
+ let opponentPoints : number = this . possibleWins ( state , 3 ) ; // 2 represents player1 sign
468
+ return codeyPoints - opponentPoints ;
469
+ }
470
+
471
+ // from perspective of Codeybot, +infinity means Codeybot win, -infinity means Player1 wins
472
+ // turn = 1, means it's Codeybot's turn, turn = -1 means it's opponent's turn
473
+ // returns the best possible score that can be achieved, given that Player1 plays optimally
474
+ private miniMax = ( state : ConnectFourGameState , depth : number , turn : number ) : number => {
475
+ if ( state . status === 2 ) { // means draw
476
+ return 0 ;
477
+ }
478
+ if ( state . status === 3 ) { // means Player1 wins
479
+ return - Infinity ;
480
+ }
481
+ if ( state . status === 4 ) { // means Player 2 wins
482
+ return Infinity ;
483
+ }
484
+ if ( depth === 0 ) {
485
+ return this . evaluate ( state ) ; //heuristic function to evaluate state of game
486
+ }
487
+
488
+ // if it is Codeybot's turn, we want to find move that maximizes score
489
+ if ( turn === 1 ) {
490
+ let value : number = - Infinity ;
491
+ const column_choices : number [ ] = [ 0 , 1 , 2 , 3 , 4 , 5 , 6 ] ;
492
+ for ( const column_choice of column_choices ) {
493
+ if ( state . columns [ column_choice ] . fill < CONNECT_FOUR_ROW_COUNT ) {
494
+ let newState : ConnectFourGameState = JSON . parse ( JSON . stringify ( state ) ) ;
495
+ this . updateState ( newState , column_choice , turn ) ;
496
+ this . setStatus ( newState , column_choice ) ;
497
+ value = Math . max ( value , this . miniMax ( newState , depth - 1 , turn * - 1 ) ) ;
498
+ }
499
+ }
500
+ return value ;
501
+ } else { // (turn = -1) it is Player 1's turn, so we want to find minimum score (this assumes Player 1 plays optimally)
502
+ let value : number = Infinity ;
503
+ const column_choices : number [ ] = [ 0 , 1 , 2 , 3 , 4 , 5 , 6 ] ;
504
+ for ( const column_choice of column_choices ) {
505
+ // if selected column is not already full, recurse down the branch
506
+ if ( state . columns [ column_choice ] . fill < CONNECT_FOUR_ROW_COUNT ) {
507
+ let newState : ConnectFourGameState = JSON . parse ( JSON . stringify ( state ) ) ;
508
+ this . updateState ( newState , column_choice , turn ) ;
509
+ this . setStatus ( newState , column_choice ) ;
510
+ value = Math . min ( value , this . miniMax ( newState , depth - 1 , turn * - 1 ) ) ;
511
+ }
512
+ }
513
+ return value ;
514
+ }
515
+ }
516
+
517
+ private findBestColumn = ( state : ConnectFourGameState ) : number => {
518
+ let column_scores : number [ ] = [ - Infinity , - Infinity , - Infinity , - Infinity , - Infinity , - Infinity , - Infinity ] ;
519
+ for ( let i = 0 ; i < 7 ; i ++ ) {
520
+ if ( state . columns [ i ] . fill < CONNECT_FOUR_ROW_COUNT ) {
521
+ let newState : ConnectFourGameState = JSON . parse ( JSON . stringify ( state ) ) ; // make a deep copy of game state
522
+ this . updateState ( newState , i , 1 ) ;
523
+ this . setStatus ( newState , i ) ;
524
+ column_scores [ i ] = this . miniMax ( newState , 4 , - 1 ) ;
525
+ }
526
+ }
527
+ let value = - Infinity ;
528
+ let best_column = - 1 ;
529
+ for ( let i = 0 ; i < 7 ; i ++ ) {
530
+ if ( column_scores [ i ] > value ) {
531
+ value = column_scores [ i ] ;
532
+ best_column = i ;
533
+ }
534
+ }
535
+ // if best_column = -1, then that means all posible moves lead to certain loss
536
+ if ( best_column === - 1 ) {
537
+ for ( let i = 0 ; i < 7 ; i ++ ) {
538
+ if ( state . columns [ i ] . fill < CONNECT_FOUR_ROW_COUNT ) {
539
+ return i ;
540
+ }
541
+ }
542
+ }
543
+ return best_column ;
544
+ }
545
+
546
+ // takes in ConnectFourGameState, returns best column for Codeybot to play in
547
+ public getBestMove = ( state : ConnectFourGameState ) : number => {
548
+ return this . findBestColumn ( state ) ;
549
+ } ;
398
550
}
399
551
400
552
export enum ConnectFourGameStatus {
@@ -467,9 +619,9 @@ export const getStateAsString = (state: ConnectFourGameState): string => {
467
619
return result ;
468
620
} ;
469
621
470
- export const getCodeyConnectFourSign = ( ) : ConnectFourGameSign => {
471
- return getRandomIntFrom1 ( 7 ) ;
472
- } ;
622
+ // export const getCodeyConnectFourSign = (): ConnectFourGameSign => {
623
+ // return getRandomIntFrom1(7);
624
+ // };
473
625
474
626
export const updateColumn = ( column : ConnectFourColumn , sign : ConnectFourGameSign ) : boolean => {
475
627
const fill : number = column . fill ;
0 commit comments