parent
945d4cea64
commit
d823eab61f
@ -0,0 +1,67 @@ |
|||||||
|
import t from "tap"; |
||||||
|
import { ExpectedOutcome, FinalOutcome, Player } from "./datatypes.ts"; |
||||||
|
import { createOpponent } from "./opponent.ts"; |
||||||
|
import { Board } from "./board.ts"; |
||||||
|
|
||||||
|
void t.test("createOpponent", async (t) => { |
||||||
|
const outcomesByBoard: Record<string, ExpectedOutcome> = { |
||||||
|
"X_|__|__": { finalOutcome: FinalOutcome.WinO, movesLeft: 10 }, |
||||||
|
"XO|__|__": { finalOutcome: FinalOutcome.Draw, movesLeft: 9 }, |
||||||
|
"X_|O_|__": { finalOutcome: FinalOutcome.WinO, movesLeft: 20 }, |
||||||
|
"X_|_O|__": { finalOutcome: FinalOutcome.WinX, movesLeft: 9 }, |
||||||
|
"X_|__|O_": { finalOutcome: FinalOutcome.WinO, movesLeft: 10 }, |
||||||
|
"X_|__|_O": { finalOutcome: FinalOutcome.WinO, movesLeft: 9 }, |
||||||
|
"XX|__|__": { finalOutcome: FinalOutcome.WinO, movesLeft: 9 }, |
||||||
|
"X_|X_|__": { finalOutcome: FinalOutcome.WinO, movesLeft: 20 }, |
||||||
|
"X_|_X|__": { finalOutcome: FinalOutcome.WinX, movesLeft: 9 }, |
||||||
|
"X_|__|X_": { finalOutcome: FinalOutcome.WinO, movesLeft: 10 }, |
||||||
|
"X_|__|_X": { finalOutcome: FinalOutcome.WinX, movesLeft: 9 }, |
||||||
|
"XO|XO|XO": { finalOutcome: FinalOutcome.WinX, movesLeft: 0 }, |
||||||
|
|
||||||
|
// Incorrect scenarios
|
||||||
|
// movesLeft is not zero, but there is no information about next boards
|
||||||
|
"XX|XX|X_": { finalOutcome: FinalOutcome.WinO, movesLeft: 10 }, |
||||||
|
// movesLeft is not zero, and information about next boards
|
||||||
|
// is inconsistent with information about current board
|
||||||
|
"OO|OO|__": { finalOutcome: FinalOutcome.WinO, movesLeft: 10 }, |
||||||
|
"OO|OO|O_": { finalOutcome: FinalOutcome.WinO, movesLeft: 0 }, |
||||||
|
"OO|OO|_O": { finalOutcome: FinalOutcome.WinX, movesLeft: 9 }, |
||||||
|
}; |
||||||
|
const opponent = createOpponent(new Map(Object.entries(outcomesByBoard))); |
||||||
|
|
||||||
|
const checkNextMove = ( |
||||||
|
currentBoardSerialized: string, |
||||||
|
currentPlayer: Player, |
||||||
|
expectedNextBoardSerialized: string, |
||||||
|
) => { |
||||||
|
t.equal( |
||||||
|
opponent |
||||||
|
.getNextMove( |
||||||
|
Board.fromSerialized(currentBoardSerialized), |
||||||
|
currentPlayer, |
||||||
|
) |
||||||
|
.serialize(), |
||||||
|
expectedNextBoardSerialized, |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
checkNextMove("X_|__|__", Player.O, "X_|__|_O"); |
||||||
|
checkNextMove("X_|__|__", Player.X, "XX|__|__"); |
||||||
|
|
||||||
|
t.throws( |
||||||
|
() => opponent.getNextMove(Board.fromSerialized("XO|XO|XO"), Player.X), |
||||||
|
{ message: "There are no possible moves left: XO|XO|XO" }, |
||||||
|
); |
||||||
|
t.throws( |
||||||
|
() => opponent.getNextMove(Board.fromSerialized("XX|XX|XX"), Player.X), |
||||||
|
{ message: "Board is not solved: XX|XX|XX" }, |
||||||
|
); |
||||||
|
t.throws( |
||||||
|
() => opponent.getNextMove(Board.fromSerialized("XX|XX|X_"), Player.X), |
||||||
|
{ message: "Next board is not solved: XX|XX|XX" }, |
||||||
|
); |
||||||
|
t.throws( |
||||||
|
() => opponent.getNextMove(Board.fromSerialized("OO|OO|__"), Player.O), |
||||||
|
{ message: "Cannot find appropriate next move: OO|OO|__" }, |
||||||
|
); |
||||||
|
}); |
@ -0,0 +1,52 @@ |
|||||||
|
import { |
||||||
|
BoardType, |
||||||
|
ExpectedOutcome, |
||||||
|
Player, |
||||||
|
SquareState, |
||||||
|
getOccupiedStateByPlayer, |
||||||
|
} from "./datatypes.ts"; |
||||||
|
|
||||||
|
export const createOpponent = ( |
||||||
|
outcomesByBoard: Map<string, ExpectedOutcome>, |
||||||
|
) => { |
||||||
|
const getNextMove = (board: BoardType, currentPlayer: Player) => { |
||||||
|
const currentExpectedOutcome = outcomesByBoard.get(board.serialize()); |
||||||
|
if (!currentExpectedOutcome) { |
||||||
|
throw new Error(`Board is not solved: ${board.serialize()}`); |
||||||
|
} |
||||||
|
|
||||||
|
if (!currentExpectedOutcome.movesLeft) { |
||||||
|
throw new Error(`There are no possible moves left: ${board.serialize()}`); |
||||||
|
} |
||||||
|
|
||||||
|
const occupiedState = getOccupiedStateByPlayer(currentPlayer); |
||||||
|
for (let row = 0; board.hasRow(row); row++) { |
||||||
|
for (let column = 0; board.hasSquare(row, column); column++) { |
||||||
|
if (board.get(row, column) === SquareState.Unoccupied) { |
||||||
|
const nextBoard = board.with(row, column, occupiedState); |
||||||
|
const nextExpectedOutcome = outcomesByBoard.get( |
||||||
|
nextBoard.serialize(), |
||||||
|
); |
||||||
|
if (!nextExpectedOutcome) { |
||||||
|
throw new Error( |
||||||
|
`Next board is not solved: ${nextBoard.serialize()}`, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
if ( |
||||||
|
nextExpectedOutcome.finalOutcome === |
||||||
|
currentExpectedOutcome.finalOutcome && |
||||||
|
nextExpectedOutcome.movesLeft === |
||||||
|
currentExpectedOutcome.movesLeft - 1 |
||||||
|
) { |
||||||
|
return nextBoard; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
throw new Error(`Cannot find appropriate next move: ${board.serialize()}`); |
||||||
|
}; |
||||||
|
|
||||||
|
return { getNextMove }; |
||||||
|
}; |
Loading…
Reference in new issue