Skip to content

Commit

Permalink
implement minimax
Browse files Browse the repository at this point in the history
  • Loading branch information
CortezSMz committed Apr 7, 2022
1 parent 68fbcb9 commit 20face1
Show file tree
Hide file tree
Showing 3 changed files with 192 additions and 50 deletions.
47 changes: 34 additions & 13 deletions src/engine/Board.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ export interface Disc {
export interface GridSlot {
x: number;
z: number;
disc?: Disc;
row: number;
col: number;
disc: Disc | null;
}

export class Board {
Expand All @@ -31,31 +33,50 @@ export class Board {
const offsetX = 0.0847;
const offsetY = 0.0679;

for (let i = 0; i < 6; i++) {
Board[i] = [];
for (let j = 0; j < 7; j++) {
Board[i].push({
x: j * discDiameter - offsetX,
z: i * discDiameter - offsetY,
for (let row = 0; row < 6; row++) {
Board[row] = [];
for (let col = 0; col < 7; col++) {
Board[row].push({
x: col * discDiameter - offsetX,
z: row * discDiameter - offsetY,
row,
col,
disc: null,
});
}
}
return Board;
}

public isValidLocation(x: number, disc: Disc) {
for (let i = this.grid.length - 1; i >= 0; i--) {
const col = this.grid[this.grid.length - 1].findIndex((c) => c.x === x);
public getColFromXCoord(x: number) {
return this.grid[this.grid.length - 1].findIndex((c) => c.x === x);
}

if (!this.grid[i][col].disc) {
this.grid[i][col].disc = disc;
return this.grid[i][col];
public isValidLocation(board: GridSlot[][], col: number) {
for (let i = board.length - 1; i >= 0; i--) {
if (!board[i][col].disc) {
return {
...board[i][col],
col,
row: i,
};
}
}

return null;
}

public allValidLocations(board: GridSlot[][]) {
const validMoves: GridSlot[] = [];

for (let i = 0; i < board[0].length; i++) {
const isValid = this.isValidLocation(board, i);
if (isValid) validMoves.push(isValid);
}

return validMoves;
}

public getDiscById(id: number): Disc | undefined {
return this.discs.find((disc) => disc.id === id);
}
Expand Down
90 changes: 53 additions & 37 deletions src/engine/GameManager.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { Board } from "@/engine/Board";
import { Disc } from "./Board";
import { Minimax } from "@/engine/Minimax";
import { Board, Disc, GridSlot } from "@/engine/Board";

export default class GameManager {
public board: Board;

public minimax: Minimax;

public dropping: boolean;

public state: {
Expand All @@ -15,6 +17,8 @@ export default class GameManager {
constructor() {
this.board = new Board();

this.minimax = new Minimax(this);

if (this.board.discs.length === 0) this.spawnNext(-1, "YELLOW");

this.dropping = false;
Expand All @@ -31,20 +35,24 @@ export default class GameManager {

if (!currentDisc) return;

const isValid = this.board.isValidLocation(x, currentDisc);
const isValid = this.board.isValidLocation(
this.board.grid,
this.board.getColFromXCoord(x)
);

if (isValid) {
this.dropping = true;

if (!currentDisc) return;
this.board.grid[isValid.row][isValid.col].disc = currentDisc;

currentDisc.x = isValid.x;
currentDisc.z = isValid.z;
currentDisc.dropped = true;
}
}

spawnNext(id: number, color: "RED" | "YELLOW") {
const isFinished = this.check(this.board);
const isFinished = this.check(this.board.grid);

if (isFinished.result)
return (this.state = {
Expand All @@ -60,23 +68,31 @@ export default class GameManager {
z: -0.14,
});

if (color === "RED") {
const best = this.minimax.getBestMove();

setTimeout(() => {
this.drop(best.move.x);
}, 500);
}

setTimeout(() => {
this.dropping = false;
}, 250);
}

check(board: Board): {
check(board: GridSlot[][]): {
result: "RED" | "YELLOW" | "TIE" | null;
discs: Disc[];
} {
const rows = board.grid.length;
const cols = board.grid[0].length;
const rows = board.length;
const cols = board[0].length;

let empty = 0;

for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const disc = board.grid[row][col].disc;
const disc = board[row][col].disc;

if (!disc) continue;

Expand All @@ -85,18 +101,18 @@ export default class GameManager {
col < cols - 3 &&
this.fourConnected(
disc,
board.grid[row][col + 1].disc,
board.grid[row][col + 2].disc,
board.grid[row][col + 3].disc
board[row][col + 1].disc,
board[row][col + 2].disc,
board[row][col + 3].disc
)
) {
return {
result: disc.color,
discs: [
disc,
board.grid[row][col + 1].disc!,
board.grid[row][col + 2].disc!,
board.grid[row][col + 3].disc!,
board[row][col + 1].disc!,
board[row][col + 2].disc!,
board[row][col + 3].disc!,
],
};
}
Expand All @@ -106,18 +122,18 @@ export default class GameManager {
row < rows - 3 &&
this.fourConnected(
disc,
board.grid[row + 1][col].disc,
board.grid[row + 2][col].disc,
board.grid[row + 3][col].disc
board[row + 1][col].disc,
board[row + 2][col].disc,
board[row + 3][col].disc
)
) {
return {
result: disc.color,
discs: [
disc,
board.grid[row + 1][col].disc!,
board.grid[row + 2][col].disc!,
board.grid[row + 3][col].disc!,
board[row + 1][col].disc!,
board[row + 2][col].disc!,
board[row + 3][col].disc!,
],
};
}
Expand All @@ -128,18 +144,18 @@ export default class GameManager {
col < cols - 3 &&
this.fourConnected(
disc,
board.grid[row + 1][col + 1].disc,
board.grid[row + 2][col + 2].disc,
board.grid[row + 3][col + 3].disc
board[row + 1][col + 1].disc,
board[row + 2][col + 2].disc,
board[row + 3][col + 3].disc
)
) {
return {
result: disc.color,
discs: [
disc,
board.grid[row + 1][col + 1].disc!,
board.grid[row + 2][col + 2].disc!,
board.grid[row + 3][col + 3].disc!,
board[row + 1][col + 1].disc!,
board[row + 2][col + 2].disc!,
board[row + 3][col + 3].disc!,
],
};
}
Expand All @@ -150,18 +166,18 @@ export default class GameManager {
col > 2 &&
this.fourConnected(
disc,
board.grid[row + 1][col - 1].disc,
board.grid[row + 2][col - 2].disc,
board.grid[row + 3][col - 3].disc
board[row + 1][col - 1].disc,
board[row + 2][col - 2].disc,
board[row + 3][col - 3].disc
)
) {
return {
result: disc.color,
discs: [
disc,
board.grid[row + 1][col - 1].disc!,
board.grid[row + 2][col - 2].disc!,
board.grid[row + 3][col - 3].disc!,
board[row + 1][col - 1].disc!,
board[row + 2][col - 2].disc!,
board[row + 3][col - 3].disc!,
],
};
}
Expand All @@ -177,10 +193,10 @@ export default class GameManager {
}

fourConnected(
a: Disc | undefined,
b: Disc | undefined,
c: Disc | undefined,
d: Disc | undefined
a: Disc | null,
b: Disc | null,
c: Disc | null,
d: Disc | null
) {
return (
!!a &&
Expand Down
105 changes: 105 additions & 0 deletions src/engine/Minimax.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import GameManager from "@/engine/GameManager";
import { GridSlot } from "@/engine/Board";

export class Minimax {
private manager: GameManager;

public depth: number;

constructor(manager: GameManager) {
this.manager = manager;

this.depth = 5;
}

public getBestMove() {
const { moves, bestScore } = this.getMoves();

const bestMoves = moves.filter((m) => m.score === bestScore);

return bestMoves[(bestMoves.length * Math.random()) | 0];
}

private getMoves(depth: number = this.depth) {
const board = [...this.manager.board.grid];
let bestScore = -Infinity;
const moves: { score: number; move: GridSlot }[] = [];

const validMoves = this.manager.board.allValidLocations(board);

for (const { row, col, x, z } of validMoves) {
board[row][col].disc = {
dropped: false,
color: "YELLOW",
id: -1,
x,
z,
};

const score = this.minimax(board, depth, false);

board[row][col].disc = null;

moves.push({ score, move: { row, col, x, z, disc: null } });

if (score > bestScore) {
bestScore = score;
}
}

return { moves, bestScore };
}

private minimax(
board: GridSlot[][],
depth: number,
playing: boolean
): number {
const res = this.manager.check(board);
if (res.result != null || depth === 0) {
if (res.result === "RED") return -1;
else if (res.result === "YELLOW") return 1;
return 0;
}

if (playing) {
let bestScore = -Infinity;
const validMoves = this.manager.board.allValidLocations(board);
for (const { row, col, x, z } of validMoves) {
board[row][col].disc = {
dropped: false,
color: "YELLOW",
id: -1,
x,
z,
};
const score = this.minimax(board, depth - 1, false);
board[row][col].disc = null;
if (score > bestScore) {
bestScore = score;
}
}
return bestScore;
} else if (!playing) {
let bestScore = Infinity;
const validMoves = this.manager.board.allValidLocations(board);
for (const { row, col, x, z } of validMoves) {
board[row][col].disc = {
dropped: false,
color: "RED",
id: -1,
x,
z,
};
const score = this.minimax(board, depth - 1, true);
board[row][col].disc = null;
if (score < bestScore) {
bestScore = score;
}
}
return bestScore;
}

return 0;
}
}

0 comments on commit 20face1

Please sign in to comment.