@@ -10,7 +10,6 @@ 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
13
15
14
const CONNECT_FOUR_COLUMN_COUNT = 7 ;
16
15
const CONNECT_FOUR_ROW_COUNT = 6 ;
@@ -228,14 +227,13 @@ export class ConnectFourGame {
228
227
left_pointer_y ++ ;
229
228
}
230
229
while (
231
- right_pointer_x + 1 < CONNECT_FOUR_COLUMN_COUNT - 1 &&
230
+ right_pointer_x + 1 < CONNECT_FOUR_COLUMN_COUNT && // used to be an extra -1, fixed bug
232
231
right_pointer_y - 1 >= 0 &&
233
232
state . columns [ right_pointer_x + 1 ] . tokens [ right_pointer_y - 1 ] == sign
234
233
) {
235
234
right_pointer_x ++ ;
236
235
right_pointer_y -- ;
237
236
}
238
-
239
237
if ( right_pointer_x - left_pointer_x + 1 >= 4 ) {
240
238
return true ;
241
239
}
@@ -253,6 +251,8 @@ export class ConnectFourGame {
253
251
// newly placed token
254
252
const horizontalIndex = columnIndex ;
255
253
const verticalIndex = state . columns [ columnIndex ] . fill - 1 ;
254
+ //console.log(horizontalIndex);
255
+ //console.log(verticalIndex);
256
256
const column = state . columns [ columnIndex ] ;
257
257
const sign : ConnectFourGameSign = state . columns [ columnIndex ] . tokens [ verticalIndex ] ;
258
258
@@ -265,11 +265,11 @@ export class ConnectFourGame {
265
265
state . winType = ConnectFourWinType . Horizontal ;
266
266
state . status = this . determineWinner ( sign ) ;
267
267
} else if ( this . checkForDiagonalLBRTWin ( state , horizontalIndex , verticalIndex , sign ) ) {
268
- // Check for diagonal win (left top right bottom )
268
+ // Check for diagonal win (left bottom right top )
269
269
state . winType = ConnectFourWinType . DiagonalLBRT ;
270
270
state . status = this . determineWinner ( sign ) ;
271
271
} else if ( this . checkForDiagonalLTRBWin ( state , horizontalIndex , verticalIndex , sign ) ) {
272
- // Check for diagonal win (left bottom right top )
272
+ // Check for diagonal win (left top right bottom )
273
273
state . winType = ConnectFourWinType . DiagonalLTRB ;
274
274
state . status = this . determineWinner ( sign ) ;
275
275
} else if ( state . columns . every ( ( column ) => column . fill === 6 ) ) {
@@ -318,13 +318,13 @@ export class ConnectFourGame {
318
318
return (
319
319
'**' +
320
320
getStateAsString ( this . state ) +
321
- `\n${ this . state . player1Username } has won with a ${ this . parseWin ( this . state ) } connect 4!**`
321
+ `\n<@ ${ this . state . player1Id } > has won with a ${ this . parseWin ( this . state ) } connect 4!**`
322
322
) ;
323
323
case ConnectFourGameStatus . Player2Win :
324
324
return (
325
325
'**' +
326
326
getStateAsString ( this . state ) +
327
- `\n${ this . state . player2Username } has won with a ${ this . parseWin ( this . state ) } connect 4**`
327
+ `\n${ this . state . player2Username } has won with a ${ this . parseWin ( this . state ) } connect 4! **`
328
328
) ;
329
329
case ConnectFourGameStatus . Draw :
330
330
return `The match ended in a draw!` ;
@@ -341,7 +341,7 @@ export class ConnectFourGame {
341
341
. setTitle ( 'Connect4' )
342
342
. setDescription (
343
343
`
344
- ${ this . state . player1Username } vs. ${ this . state . player2Username }
344
+ <@ ${ this . state . player1Id } > vs. ${ this . state . player2Username }
345
345
` ,
346
346
)
347
347
. addFields ( [
@@ -350,7 +350,7 @@ ${this.state.player1Username} vs. ${this.state.player2Username}
350
350
value : `
351
351
${ this . getStatusAsString ( ) }
352
352
353
- ${ this . state . player1Username } : ${ getEmojiFromSign ( this . state . player1Sign ) }
353
+ <@ ${ this . state . player1Id } > : ${ getEmojiFromSign ( this . state . player1Sign ) }
354
354
${ this . state . player2Username } : ${ getEmojiFromSign ( this . state . player2Sign ) }
355
355
` ,
356
356
} ,
@@ -373,13 +373,13 @@ ${this.state.player2Username}: ${getEmojiFromSign(this.state.player2Sign)}
373
373
. setCustomId ( `connect4-4-${ this . id } ` )
374
374
. setLabel ( '4' )
375
375
. setStyle ( ButtonStyle . Secondary ) ,
376
+ ) ;
377
+
378
+ const row2 = new ActionRowBuilder < ButtonBuilder > ( ) . addComponents (
376
379
new ButtonBuilder ( )
377
380
. setCustomId ( `connect4-5-${ this . id } ` )
378
381
. setLabel ( '5' )
379
382
. setStyle ( ButtonStyle . Secondary ) ,
380
- ) ;
381
-
382
- const row2 = new ActionRowBuilder < ButtonBuilder > ( ) . addComponents (
383
383
new ButtonBuilder ( )
384
384
. setCustomId ( `connect4-6-${ this . id } ` )
385
385
. setLabel ( '6' )
@@ -395,6 +395,195 @@ ${this.state.player2Username}: ${getEmojiFromSign(this.state.player2Sign)}
395
395
components : this . state . status === ConnectFourGameStatus . Pending ? [ row1 , row2 ] : [ ] ,
396
396
} ;
397
397
}
398
+
399
+ // ConnectFourGameState
400
+ // Waiting = 0,
401
+ // Pending = 1,
402
+ // Draw = 2,
403
+ // Player1Win = 3,
404
+ // Player2Win = 4,
405
+ // Player1TimeOut = 5,
406
+ // Player2TimeOut = 6,
407
+ // Unknown = 7,
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 ) {
415
+ state . columns [ columnNumber ] . tokens [ fill ] = ConnectFourGameSign . Player2 ;
416
+ } else {
417
+ state . columns [ columnNumber ] . tokens [ fill ] = ConnectFourGameSign . Player1 ;
418
+ }
419
+ state . columns [ columnNumber ] . fill = state . columns [ columnNumber ] . fill + 1 ;
420
+ return state ;
421
+ } ;
422
+
423
+ // returns number of possible wins remaining
424
+ private possibleWins = (
425
+ state : ConnectFourGameState ,
426
+ opponentSign : ConnectFourGameSign ,
427
+ ) : number => {
428
+ let possibleWins = 0 ;
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 (
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
+ ) {
438
+ possibleWins = possibleWins + 1 ;
439
+ }
440
+ }
441
+ }
442
+ // check horizonal
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
+ ) {
451
+ possibleWins = possibleWins + 1 ;
452
+ }
453
+ }
454
+ }
455
+ // check diagonal up
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
+ ) {
464
+ possibleWins = possibleWins + 1 ;
465
+ }
466
+ }
467
+ }
468
+
469
+ //check diagonal down
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
+ ) {
478
+ possibleWins = possibleWins + 1 ;
479
+ }
480
+ }
481
+ }
482
+ return possibleWins ;
483
+ } ;
484
+
485
+ // takes a ConnectFourGameState and evaluates it according to heuristic function
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
489
+ return codeyPoints - opponentPoints ;
490
+ } ;
491
+
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 ) {
500
+ return 0 ;
501
+ }
502
+ if ( state . status === ConnectFourGameStatus . Player1Win ) {
503
+ return - Infinity ;
504
+ }
505
+ if ( state . status === ConnectFourGameStatus . Player2Win ) {
506
+ return Infinity ;
507
+ }
508
+ if ( depth === 0 ) {
509
+ return this . evaluate ( state ) ; //heuristic function to evaluate state of game
510
+ }
511
+
512
+ // if it is Codeybot's turn, we want to find move that maximizes score
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 ) ) ; // create a deep copy
519
+ this . updateState ( newState , column_choice , turn ) ;
520
+ this . setStatus ( newState , column_choice ) ; // setStatus assumes newState has already been updated with chip
521
+ value = Math . max ( value , this . miniMax ( newState , depth - 1 , ConnectFourGameSign . Player1 ) ) ;
522
+ }
523
+ }
524
+ return value ;
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 ) {
529
+ // if selected column is not already full, recurse down the branch
530
+ if ( state . columns [ column_choice ] . fill < CONNECT_FOUR_ROW_COUNT ) {
531
+ const newState : ConnectFourGameState = JSON . parse ( JSON . stringify ( state ) ) ;
532
+ this . updateState ( newState , column_choice , turn ) ;
533
+ this . setStatus ( newState , column_choice ) ;
534
+ value = Math . min ( value , this . miniMax ( newState , depth - 1 , ConnectFourGameSign . Player2 ) ) ;
535
+ }
536
+ }
537
+ return value ;
538
+ }
539
+ } ;
540
+
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
554
+ this . updateState ( newState , i , ConnectFourGameSign . Player2 ) ;
555
+ this . setStatus ( newState , i ) ;
556
+ // need to check if there is more than one possible guaranteed win
557
+ // in that case, if there exists an immediate win, we want the bot to choose that column
558
+ if ( newState . status === ConnectFourGameStatus . Player2Win ) {
559
+ return i ;
560
+ }
561
+ column_scores [ i ] = this . miniMax ( newState , 4 , ConnectFourGameSign . Player1 ) ;
562
+ }
563
+ }
564
+ let value = - Infinity ;
565
+ let best_column = - 1 ;
566
+ for ( let i = 0 ; i < 7 ; i ++ ) {
567
+ if ( column_scores [ i ] > value ) {
568
+ value = column_scores [ i ] ;
569
+ best_column = i ;
570
+ }
571
+ }
572
+ // if best_column = -1, then that means all posible moves lead to certain loss
573
+ if ( best_column === - 1 ) {
574
+ for ( let i = 0 ; i < 7 ; i ++ ) {
575
+ if ( state . columns [ i ] . fill < CONNECT_FOUR_ROW_COUNT ) {
576
+ return i ;
577
+ }
578
+ }
579
+ }
580
+ return best_column ;
581
+ } ;
582
+
583
+ // takes in ConnectFourGameState, returns best column for Codeybot to play in
584
+ public getBestMove = ( state : ConnectFourGameState ) : number => {
585
+ return this . findBestColumn ( state ) ;
586
+ } ;
398
587
}
399
588
400
589
export enum ConnectFourGameStatus {
@@ -467,10 +656,6 @@ export const getStateAsString = (state: ConnectFourGameState): string => {
467
656
return result ;
468
657
} ;
469
658
470
- export const getCodeyConnectFourSign = ( ) : ConnectFourGameSign => {
471
- return getRandomIntFrom1 ( 7 ) ;
472
- } ;
473
-
474
659
export const updateColumn = ( column : ConnectFourColumn , sign : ConnectFourGameSign ) : boolean => {
475
660
const fill : number = column . fill ;
476
661
if ( fill < CONNECT_FOUR_ROW_COUNT ) {
0 commit comments