Skip to content

Commit

Permalink
impl ai seeking win avoiding loss
Browse files Browse the repository at this point in the history
  • Loading branch information
soryy708 committed Aug 19, 2023
1 parent ce02298 commit ad689d6
Show file tree
Hide file tree
Showing 5 changed files with 271 additions and 2 deletions.
195 changes: 195 additions & 0 deletions src/front/game/ai/evaluator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import { AbstractBoard, BoardPosition } from '../board/abstract-board';
import { GamePiece } from '../piece/interface';
import { Evaluator } from './evaluator';

class Opponent implements GamePiece {
bootstrap() {
// nop
}

destroy() {
// nop
}

isSameTypeAs(other: GamePiece): boolean {
return other instanceof Opponent;
}
}

class Identity implements GamePiece {
bootstrap() {
// nop
}

destroy() {
// nop
}

isSameTypeAs(other: GamePiece): boolean {
return other instanceof Identity;
}
}

describe('Evaluator', () => {
describe('When given an empty board', () => {
it('Should return the same value for all plays', () => {
const board = new AbstractBoard();
const evaluator = new Evaluator();
let prevValue = evaluator.evaluate(
board,
{ x: 0, y: 0 },
new Identity(),
new Opponent(),
);
for (let y = 0; y < 4; ++y) {
for (let x = 0; x < 4; ++x) {
const value = evaluator.evaluate(
board,
{
x: x as BoardPosition,
y: y as BoardPosition,
},
new Identity(),
new Opponent(),
);
expect(value).toEqual(prevValue);
prevValue = value;
}
}
});
});

describe('When given a board where about to lose', () => {
describe('When loss is vertical', () => {
it('Should give highest value to preventing loss', () => {
const board = new AbstractBoard();
board.stack({ x: 0, y: 0 }, new Opponent());
board.stack({ x: 0, y: 0 }, new Opponent());
board.stack({ x: 0, y: 0 }, new Opponent());
const evaluator = new Evaluator();

const maxValue = evaluator.evaluate(
board,
{ x: 0, y: 0 },
new Identity(),
new Opponent(),
);
for (let y = 0; y < 4; ++y) {
for (let x = 0; x < 4; ++x) {
if (x === 0 && y === 0) {
continue;
}
const value = evaluator.evaluate(
board,
{
x: x as BoardPosition,
y: y as BoardPosition,
},
new Identity(),
new Opponent(),
);
expect(value).toBeLessThan(maxValue);
}
}
});
});

describe('When loss is horizontal', () => {
it('Should give highest value to preventing loss', () => {
const board = new AbstractBoard();
board.stack({ x: 0, y: 0 }, new Opponent());
board.stack({ x: 1, y: 0 }, new Opponent());
board.stack({ x: 2, y: 0 }, new Opponent());
const evaluator = new Evaluator();

const maxValue = evaluator.evaluate(
board,
{ x: 3, y: 0 },
new Identity(),
new Opponent(),
);
for (let y = 0; y < 4; ++y) {
for (let x = 0; x < 4; ++x) {
if (x === 3 && y === 0) {
continue;
}
const value = evaluator.evaluate(
board,
{
x: x as BoardPosition,
y: y as BoardPosition,
},
new Identity(),
new Opponent(),
);
expect(value).toBeLessThan(maxValue);
}
}
});
});
});

describe('When given a board where about to win', () => {
it('Should give highest value to winning', () => {
const board = new AbstractBoard();
board.stack({ x: 0, y: 0 }, new Identity());
board.stack({ x: 1, y: 0 }, new Identity());
board.stack({ x: 2, y: 0 }, new Identity());
const evaluator = new Evaluator();

const maxValue = evaluator.evaluate(
board,
{ x: 3, y: 0 },
new Identity(),
new Opponent(),
);
for (let y = 0; y < 4; ++y) {
for (let x = 0; x < 4; ++x) {
if (x === 3 && y === 0) {
continue;
}
const value = evaluator.evaluate(
board,
{
x: x as BoardPosition,
y: y as BoardPosition,
},
new Identity(),
new Opponent(),
);
expect(value).toBeLessThan(maxValue);
}
}
});
});

describe('When given choice between winning or preventing loss', () => {
it('Should prefer to win', () => {
const board = new AbstractBoard();
board.stack({ x: 0, y: 0 }, new Identity());
board.stack({ x: 1, y: 0 }, new Identity());
board.stack({ x: 2, y: 0 }, new Identity());
const winningMove = { x: 3, y: 0 } as const;
board.stack({ x: 0, y: 1 }, new Opponent());
board.stack({ x: 1, y: 1 }, new Opponent());
board.stack({ x: 2, y: 1 }, new Opponent());
const lossPreventingMove = { x: 3, y: 1 } as const;
const evaluator = new Evaluator();

const winningMoveValue = evaluator.evaluate(
board,
winningMove,
new Identity(),
new Opponent(),
);
const lossPreventingMoveValue = evaluator.evaluate(
board,
lossPreventingMove,
new Identity(),
new Opponent(),
);

expect(winningMoveValue).toBeGreaterThan(lossPreventingMoveValue);
});
});
});
44 changes: 44 additions & 0 deletions src/front/game/ai/evaluator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { AbstractBoard, BoardPosition } from '../board/abstract-board';
import { GamePiece } from '../piece/interface';
import { WinChecker } from '../win';

export class Evaluator {
public evaluate(
board: AbstractBoard,
move: { x: BoardPosition; y: BoardPosition },
identity: GamePiece,
opponent: GamePiece,
): number {
if (this.weWillWin(board, move, identity)) {
return 1;
}
if (this.weWillLose(board, move, opponent)) {
return 0.5;
}
return 0;
}

private weWillLose(
board: AbstractBoard,
move: { x: BoardPosition; y: BoardPosition },
opponent: GamePiece,
): boolean {
const boardAfterOpponentMoves = board.clone();
boardAfterOpponentMoves.stack(move, opponent);
const winChecker = new WinChecker(boardAfterOpponentMoves);
const winner = winChecker.getWinner();
return winner && winner.isSameTypeAs(opponent);
}

private weWillWin(
board: AbstractBoard,
move: { x: BoardPosition; y: BoardPosition },
identity: GamePiece,
): boolean {
const boardAfterWeMove = board.clone();
boardAfterWeMove.stack(move, identity);
const winChecker = new WinChecker(boardAfterWeMove);
const winner = winChecker.getWinner();
return winner && winner.isSameTypeAs(identity);
}
}
21 changes: 19 additions & 2 deletions src/front/game/ai/solver.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,30 @@
import { BoardPosition } from '../board/abstract-board';
import { Board } from '../board/board';
import { GamePiece } from '../piece/interface';
import { Evaluator } from './evaluator';

export class AiSolver {
private evaluator = new Evaluator();

constructor(private board: Board) {}

solveNextTurn(_piece: GamePiece): { x: BoardPosition; y: BoardPosition } {
solveNextTurn(
identity: GamePiece,
opponent: GamePiece,
): { x: BoardPosition; y: BoardPosition } {
const allValid = this.getAllValidPlays(this.board);
return allValid[Math.floor(Math.random() * allValid.length)];
const playValues = allValid.map((play) =>
this.evaluator.evaluate(this.board, play, identity, opponent),
);
const maxValue = playValues.reduce(
(prev, cur) => (cur >= prev ? cur : prev),
-Infinity,
);
if (maxValue === 0) {
return allValid[Math.floor(Math.random() * allValid.length)];
}
const maxValueIndex = playValues.findIndex((v) => v === maxValue);
return allValid[maxValueIndex];
}

private getAllValidPlays(
Expand Down
12 changes: 12 additions & 0 deletions src/front/game/board/abstract-board.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,18 @@ export class AbstractBoard {
return height;
}

public clone(): AbstractBoard {
const newBoard = new AbstractBoard();
for (let x = 0; x < 4; ++x) {
for (let y = 0; y < 4; ++y) {
for (let z = 0; z < 4; ++z) {
newBoard.state[x][y][z] = this.state[x][y][z];
}
}
}
return newBoard;
}

private setCell(
position: { x: BoardPosition; y: BoardPosition; z: BoardPosition },
content: BoardCell,
Expand Down
1 change: 1 addition & 0 deletions src/front/game/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ export class Game {
const sleepPromise = sleep(500);
const position = this.ai.solveNextTurn(
new Nought({ x: 0, y: 0, z: 0 }),
new Cross({ x: 0, y: 0, z: 0 }),
);
sleepPromise.then(() => {
const piece = this.buildCurrentPlayerPiece({
Expand Down

0 comments on commit ad689d6

Please sign in to comment.