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