-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
271 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters