Skip to content

Commit bd33a5c

Browse files
committed
Basic Codey Connect4 AI created
1 parent 1491363 commit bd33a5c

File tree

2 files changed

+163
-9
lines changed

2 files changed

+163
-9
lines changed

src/components/games/connectFour.ts

Lines changed: 155 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,15 @@ import { openDB } from '../db';
1111
import { CodeyUserError } from '../../codeyUserError';
1212
import { getEmojiByName } from '../emojis';
1313
import { getRandomIntFrom1 } from '../../utils/num';
14+
import { isNull } from 'lodash';
1415

1516
const CONNECT_FOUR_COLUMN_COUNT = 7;
1617
const CONNECT_FOUR_ROW_COUNT = 6;
1718

19+
const TWO_IN_A_ROW_WEIGHT = 1;
20+
const THREE_IN_A_ROW_WEIGHT = 3;
21+
22+
1823
class ConnectFourGameTracker {
1924
// Key = id, Value = game
2025
games: Map<number, ConnectFourGame>;
@@ -247,6 +252,7 @@ export class ConnectFourGame {
247252
state: ConnectFourGameState,
248253
columnIndex: number,
249254
): Promise<ConnectFourGameStatus> {
255+
250256
// Instead of exhaustively checking every combination of tokens we can simply use the fact that
251257
// as of this point the user hasn't won yet, so we just need to check if the token that was just placed
252258
// is part of a winning combination
@@ -395,6 +401,152 @@ ${this.state.player2Username}: ${getEmojiFromSign(this.state.player2Sign)}
395401
components: this.state.status === ConnectFourGameStatus.Pending ? [row1, row2] : [],
396402
};
397403
}
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+
};
398550
}
399551

400552
export enum ConnectFourGameStatus {
@@ -467,9 +619,9 @@ export const getStateAsString = (state: ConnectFourGameState): string => {
467619
return result;
468620
};
469621

470-
export const getCodeyConnectFourSign = (): ConnectFourGameSign => {
471-
return getRandomIntFrom1(7);
472-
};
622+
// export const getCodeyConnectFourSign = (): ConnectFourGameSign => {
623+
// return getRandomIntFrom1(7);
624+
// };
473625

474626
export const updateColumn = (column: ConnectFourColumn, sign: ConnectFourGameSign): boolean => {
475627
const fill: number = column.fill;

src/interaction-handlers/games/connectFour.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { Option } from '@sapphire/result';
33
import { ButtonInteraction } from 'discord.js';
44
import { getEmojiByName } from '../../components/emojis';
55
import {
6-
getCodeyConnectFourSign,
76
ConnectFourGameStatus,
87
ConnectFourGameSign,
98
connectFourGameTracker,
@@ -62,16 +61,19 @@ export class ConnectFourHandler extends InteractionHandler {
6261
const status = await game.setStatus(game.state, result.sign - 1);
6362
if (status == ConnectFourGameStatus.Pending) {
6463
if (!game.state.player2Id) {
65-
let codeySign = getCodeyConnectFourSign();
66-
while (!updateColumn(game.state.columns[codeySign - 1], game.state.player2Sign)) {
67-
codeySign = getCodeyConnectFourSign();
68-
}
69-
game.setStatus(game.state, codeySign - 1);
64+
let bestMove = game.getBestMove(game.state);
65+
// while (!updateColumn(game.state.columns[codeySign - 1], game.state.player2Sign)) {
66+
// codeySign = getCodeyConnectFourSign(game.state);
67+
// }
68+
updateColumn(game.state.columns[bestMove], game.state.player2Sign);
69+
game.setStatus(game.state, bestMove);
70+
7071
}
7172
}
7273
}
7374
updateMessageEmbed(game.gameMessage, game.getGameResponse());
7475
});
76+
7577
connectFourGameTracker.endGame(result.gameId);
7678
}
7779
}

0 commit comments

Comments
 (0)