Skip to content

Instantly share code, notes, and snippets.

@hcharley
Last active December 2, 2023 06:00
Show Gist options
  • Save hcharley/50abf2f988ff2c678db63fc728b23476 to your computer and use it in GitHub Desktop.
Save hcharley/50abf2f988ff2c678db63fc728b23476 to your computer and use it in GitHub Desktop.
Tic Tac Toe coding exercise

Exercise

// Tic Tac Toe

// Implement a simulated Tic-tac-toe game that is played between two players on a 3 x 3 grid.

// You may assume the following rules:

// There are two players that play against each other X and O.

// X and O should play randomly, always making valid moves. X will always go first.
// After X and O play a move, the board state should be printed out like so.

// |X| | |
// | | | |
// | | | |

// |X| |O|
// | | | |
// | | | |

// If the game ends, the simulation should stop and print out the result.

// Possible results are: X Wins!, O Wins!, Draw

V1

V1 accomplishes the exercise, but lacks the tests and goodies that V2 has.

V2

V2 shines with tests, different board sizes, a strategic mode to supplement the random mode, stats, game history, player switching depending on who won, and logging.

/**
* ###############################################################
* Tic Tac Toe Game code
* ###############################################################
*/
enum Player {
X = "X",
O = "O",
}
enum TicTacToeGameMode {
Random = "random",
Strategic = "strategic",
}
type NullablePlayer = Player | null;
type BoardRow = NullablePlayer[];
type BoardCellGroup = NullablePlayer[];
type BoardCellGroupType = "row" | "column" | "diagonal";
type Result = "X Wins!" | "O Wins!" | "Draw";
class Config {
readonly minBoardSize = 2;
readonly maxBoardSize = 10;
roundsOfGamesToRun = 5;
// Board size is square: boardSize x boardSize
boardSize = 3;
// Choose between random and strategic modes for the computer to operate with:
defaultGameMode: TicTacToeGameMode = TicTacToeGameMode.Random;
// X is always first, expect during state aware games where the loser of the last game goes first (if Config.loserPlaysFirst is true):
firstPlayer: Player = Player.X;
// For state aware games:
lastPlayerToWin?: Player;
loserPlaysFirst: boolean | null = true;
// For strategic mode:
oneInXOddOfMissingWinningMove = 10;
}
const CONFIG = new Config();
interface IGameRecord {
firstPlayer: Player;
mode: TicTacToeGameMode;
boardSize: number;
winner: null | Player;
board: BoardRow[];
winningCellGroupName: null | string;
winningCellGroupType: null | BoardCellGroupType;
gameWinningMistakesMade: number;
}
class GameRecords extends Array<IGameRecord> {
getLastGameRecord = () => {
const lastGameRecord = [...this].reverse()[0];
return lastGameRecord;
};
getLastGameRecordWon = () => {
const lastGameRecord = [
...this.filter((gameRecord) => gameRecord.winner !== null),
].reverse()[0];
return lastGameRecord;
};
getLastWinner = () => {
const lastGameRecord = this.getLastGameRecordWon();
if (!lastGameRecord) {
return null;
}
return lastGameRecord.winner;
};
getScores = () => {
return {
[Player.X]: this.filter((game) => game.winner === Player.X).length,
[Player.O]: this.filter((game) => game.winner === Player.O).length,
draws: this.filter((game) => game.winner === null).length,
};
};
getStats = () => {
const gamesWithoutWinners = this.filter((game) => !game.winner);
const gamesWithWinners = this.filter((game) => game.winner);
const strategicGamesWithWinners = gamesWithWinners.filter(
(game) => game.mode === TicTacToeGameMode.Strategic,
);
const randomGamesWithWinners = gamesWithWinners.filter(
(game) => game.mode === TicTacToeGameMode.Random,
);
const wonByFirstPlayers = (games: IGameRecord[]) =>
games.filter((game) => game.firstPlayer === game.winner);
const wonBySecondPlayers = (games: IGameRecord[]) =>
games.filter((game) => game.firstPlayer !== game.winner);
const stats = {
games: this.length,
gamesWithMistakesMade: this.reduce(
(acc, game) => acc + (game.gameWinningMistakesMade ? 1 : 0),
0,
),
gamesWithMistakesMadeThatDrawed: this.reduce(
(acc, game) =>
acc + (game.gameWinningMistakesMade && !game.winner ? 1 : 0),
0,
),
gamesWithMistakesMadeThatWereWon: this.reduce(
(acc, game) =>
acc + (game.gameWinningMistakesMade && !!game.winner ? 1 : 0),
0,
),
gameWinningMistakesMade: this.reduce(
(acc, game) => acc + game.gameWinningMistakesMade,
0,
),
gamesInStrategicMode: this.filter(
(game) => game.mode === TicTacToeGameMode.Strategic,
).length,
drawsInStrategicMode: gamesWithoutWinners.filter(
(game) => game.mode === TicTacToeGameMode.Strategic,
).length,
gamesInRandomMode: this.filter(
(game) => game.mode === TicTacToeGameMode.Random,
).length,
drawsInRandomMode: gamesWithoutWinners.filter(
(game) => game.mode === TicTacToeGameMode.Random,
).length,
firstPlayer: wonByFirstPlayers(gamesWithWinners).length,
secondPlayer: wonBySecondPlayers(gamesWithWinners).length,
firstPlayerInStrategicMode: wonByFirstPlayers(strategicGamesWithWinners)
.length,
secondPlayerInStrategicMode: wonBySecondPlayers(strategicGamesWithWinners)
.length,
firstPlayerInRandomMode: wonByFirstPlayers(randomGamesWithWinners).length,
secondPlayerInRandomMode: wonBySecondPlayers(randomGamesWithWinners)
.length,
};
return {
...stats,
draws: gamesWithoutWinners.length,
firstPlayerPercent: stats.firstPlayer / gamesWithWinners.length,
secondPlayerPercent: stats.secondPlayer / gamesWithWinners.length,
firstPlayerInStrategicPercent:
stats.firstPlayerInStrategicMode / strategicGamesWithWinners.length,
secondPlayerInStrategicPercent:
stats.secondPlayerInStrategicMode / strategicGamesWithWinners.length,
firstPlayerInRandomPercent:
stats.firstPlayerInRandomMode / randomGamesWithWinners.length,
secondPlayerInRandomPercent:
stats.secondPlayerInRandomMode / randomGamesWithWinners.length,
};
};
}
const GAME_RECORDS = new GameRecords();
const addGameRecord = (gameRecord: IGameRecord) =>
GAME_RECORDS.push(gameRecord);
const shuffleArray = (array: Array<unknown>) => {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
};
const formatPercent = (value: number) => {
return `${(value * 100).toFixed(2).replace(/\.0+$/, "")}%`;
};
const createBoardRowColumns = (boardSize: number): BoardRow => {
return Array.from({
length: boardSize,
}).map(() => null);
};
const createBoardRows = (boardSize: number): BoardRow[] => {
return Array.from({ length: boardSize }).map(
(): BoardRow => createBoardRowColumns(boardSize),
);
};
const hasPlayerWonCellGroup = (player: Player, cellGroup: BoardCellGroup) => {
return cellGroup.every((cell) => cell === player);
};
const hasPlayerWonAnyCellGroup = (
player: Player,
cellGroups: BoardCellGroup[],
) => {
return cellGroups.some((cellGroup) =>
hasPlayerWonCellGroup(player, cellGroup),
);
};
const formatCellGroupOutput = (cellGroup: BoardCellGroup) => {
return `|${cellGroup.map((cell) => cell ?? " ").join("|")}|`;
};
interface ITicTacToeBoardOptions {
boardSize?: number;
rows?: BoardRow[];
}
class TicTacToeBoard extends Array<BoardRow> {
constructor(private options: ITicTacToeBoardOptions) {
super(
...(options.rows
? options.rows
: createBoardRows(options.boardSize ?? CONFIG.boardSize)),
);
if (options.rows && options.rows.length !== this.boardSize) {
throw new Error(
`Mismatch in provided rows (${options.rows.length}) for board of size ${this.boardSize}`,
);
}
const invalidRowIndex = this.findIndex(
(row) => row.length !== this.boardSize,
);
if (invalidRowIndex >= 0) {
const invalidRow = this[invalidRowIndex];
throw new Error(
`Mismatch in provided columns (${invalidRow.length}) in row #${invalidRowIndex} (zero-indexed) for board of size ${this.boardSize}`,
);
}
if (this.boardSize > CONFIG.maxBoardSize) {
throw new Error(
`A maximum board size of ${CONFIG.maxBoardSize} is supported, but the game was started with ${options.boardSize}.`,
);
}
if (this.boardSize < CONFIG.minBoardSize) {
throw new Error(
`A minimum board size of ${CONFIG.minBoardSize} is supported, but the game was started with ${options.boardSize}.`,
);
}
}
readonly boardSize = this.options.boardSize ?? CONFIG.boardSize;
get numberOfMovesMade() {
let moves = 0;
for (const row of this) {
moves += row.filter((player) => player !== null).length;
}
return moves;
}
get numberOfAvailableMovesLeft() {
return this.reduce<number>((acc, row) => {
return acc + row.filter((player) => player === null).length;
}, 0);
}
doesPlayerHaveAWinningRow(player: Player) {
return hasPlayerWonAnyCellGroup(player, this);
}
getColumn(columnIndex: number): BoardCellGroup {
return this.reduce((acc, row) => {
acc.push(row[columnIndex]);
return acc;
}, []);
}
doesPlayerHaveAWinningColumn(player: Player) {
for (let columnIndex = 0; columnIndex < this.boardSize; columnIndex++) {
const column = this.getColumn(columnIndex);
if (hasPlayerWonCellGroup(player, column)) {
return true;
}
}
return false;
}
doesPlayerHaveAWinningDiagonal(player: Player) {
const cellGroups = this.getDiagonals();
return hasPlayerWonAnyCellGroup(player, Object.values(cellGroups));
}
doesPlayerHaveAWinningCellGroup(player: Player) {
if (this.doesPlayerHaveAWinningRow(player)) {
return true;
}
if (this.doesPlayerHaveAWinningColumn(player)) {
return true;
}
if (this.doesPlayerHaveAWinningDiagonal(player)) {
return true;
}
return false;
}
getDiagonals(): {
topLeftToBottomRight: BoardCellGroup;
topRightToBottomLeft: BoardCellGroup;
} {
const topLeftToBottomRight: BoardCellGroup = [];
const topRightToBottomLeft: BoardCellGroup = [];
for (let index = 0; index < this.boardSize; index++) {
topLeftToBottomRight.push(this[index][index]);
topRightToBottomLeft.push(this[this.boardSize - 1 - index][index]);
}
return {
topLeftToBottomRight,
topRightToBottomLeft,
};
}
get asConsoleOutput(): string {
const output = [...this]
.map((row) => {
return formatCellGroupOutput(row);
})
.join("\n");
return output;
}
getColumns() {
return Array.from({
length: this.boardSize,
}).map((_, index) => this.getColumn(index));
}
getRows() {
return [...this];
}
getCellGroups(): Record<string, BoardCellGroup> {
const diagonals = this.getDiagonals();
return {
...this.getColumns().reduce((acc, cellGroup, index) => {
return {
...acc,
[`Column #${index}`]: cellGroup,
};
}, {}),
...this.getRows().reduce((acc, cellGroup, index) => {
return {
...acc,
[`Row #${index}`]: cellGroup,
};
}, {}),
"Top left to bottom right diagonal": diagonals.topLeftToBottomRight,
"Top right to bottom left diagonal": diagonals.topRightToBottomLeft,
};
}
getWinningCellGroup(
player: Player,
): null | {
name: string;
type: BoardCellGroupType;
cellGroup: BoardCellGroup;
} {
const winningCellGroup = Object.entries(this.getCellGroups()).find(
([name, cellGroup]) => {
return hasPlayerWonCellGroup(player, cellGroup);
},
);
if (winningCellGroup) {
const type = this.doesPlayerHaveAWinningColumn(player)
? "column"
: this.doesPlayerHaveAWinningRow(player)
? "row"
: this.doesPlayerHaveAWinningDiagonal(player)
? "diagonal"
: null;
if (!type) {
throw new Error(
`Unable to find winning cell group type for ${winningCellGroup[0]}`,
);
}
return {
name: winningCellGroup[0],
cellGroup: winningCellGroup[1],
type,
};
}
return null;
}
}
const getNextPlayer = (player: Player) => {
switch (player) {
case Player.X:
return Player.O;
case Player.O:
return Player.X;
default:
throw new Error(
`"${player}" is not a valid player. Choices are: ${Object.values(
Player,
).join(", ")}`,
);
}
};
const getRandomNumber = (max: number) => {
return Math.floor(Math.random() * max);
};
const getDefaultFirstPlayer = () => {
const lastPlayerToWin = GAME_RECORDS.getLastWinner();
if (!lastPlayerToWin) {
return CONFIG.firstPlayer;
}
if (CONFIG.loserPlaysFirst) {
return getNextPlayer(lastPlayerToWin);
}
return lastPlayerToWin;
};
interface ITicTacToeGameOptions extends ITicTacToeBoardOptions {
firstPlayer?: Player;
enableState?: boolean;
mode?: TicTacToeGameMode;
printEveryMove?: boolean;
}
class TicTacToeGame {
constructor(
private options: ITicTacToeGameOptions = {
boardSize: CONFIG.boardSize,
firstPlayer: CONFIG.firstPlayer,
mode: CONFIG.defaultGameMode,
printEveryMove: false,
},
private board: TicTacToeBoard = new TicTacToeBoard(options),
private currentPlayer: Player = options.firstPlayer ?? options.enableState
? getDefaultFirstPlayer()
: CONFIG.firstPlayer,
) {
if (!this.options.firstPlayer) {
this.options.firstPlayer = this.currentPlayer ?? CONFIG.firstPlayer;
}
if (!this.options.boardSize) {
this.options.boardSize = CONFIG.boardSize;
}
if (!this.options.mode) {
this.options.mode = CONFIG.defaultGameMode;
}
if (this.boardSize !== board.boardSize) {
throw new Error(
`The Tic Tac Toe game was started with a board size of ${options.boardSize}, but a custom board was used that has a size of ${board.boardSize}. Please ensure the game and the board are of the same size.`,
);
}
}
get firstPlayer() {
return this.options.firstPlayer ?? CONFIG.firstPlayer;
}
get mode() {
return this.options.mode ?? CONFIG.defaultGameMode;
}
get boardSize() {
return this.options.boardSize ?? CONFIG.boardSize;
}
public getCellGroups(): Record<string, BoardCellGroup> {
return this.board.getCellGroups();
}
private switchPlayer() {
this.setPlayer(getNextPlayer(this.currentPlayer));
}
public setPlayer(player: Player) {
this.currentPlayer = player;
}
private printBoard() {
console.log(this.asConsoleOutput);
}
private isValidMove(row: number, column: number) {
return this.board[row][column] === null;
}
private updateBoardWithMove(row: number, column: number, player: Player) {
if (!this.isValidMove(row, column)) {
throw new Error(
`Board move of ${player} to row, column location of ${row}:${column} is invalid as ${this.board[row][column]} is already there.`,
);
}
return (this.board[row][column] = player);
}
private undoBoardMove(row: number, column: number) {
return (this.board[row][column] = null);
}
private findWinningMove(player: Player): [number, number] | null {
for (let row = 0; row < this.boardSize; row++) {
for (let column = 0; column < this.boardSize; column++) {
if (this.isValidMove(row, column)) {
this.updateBoardWithMove(row, column, player);
const isWinning = this.board.doesPlayerHaveAWinningCellGroup(player);
this.undoBoardMove(row, column);
if (isWinning) {
return [row, column];
}
}
}
}
return null;
}
private getStrategicMoveSuggestions(): [number, number][] {
const moves: [number, number][] = [];
const isBoardSizeEven = this.boardSize % 2 === 1;
// Center
if (isBoardSizeEven) {
const center = Math.floor(this.boardSize / 2);
moves.push([center, center]);
}
// Corners
if (isBoardSizeEven) {
moves.push(
[0, 0],
[0, this.boardSize - 1],
[this.boardSize - 1, 0],
[this.boardSize - 1, this.boardSize - 1],
);
}
// Remaining cells
const remainingCells: [number, number][] = [];
for (let i = 0; i < this.boardSize; i++) {
for (let j = 0; j < this.boardSize; j++) {
if (!remainingCells.some(([x, y]) => x === i && y === j)) {
remainingCells.push([i, j]);
}
}
}
shuffleArray(remainingCells);
moves.push(...remainingCells);
return moves;
}
public gameWinningMistakesMade = {
[Player.X]: 0,
[Player.O]: 0,
};
private makeStrategicMove(): void {
// Introduce a 1 in X possibility that the computer overlooks the winning move:
const willTheComputerMakeAMistake =
getRandomNumber(CONFIG.oneInXOddOfMissingWinningMove) === 1;
// Check for a winning move
let move = this.findWinningMove(this.currentPlayer);
if (move) {
if (willTheComputerMakeAMistake) {
this.gameWinningMistakesMade[this.currentPlayer]++;
} else {
this.updateBoardWithMove(move[0], move[1], this.currentPlayer);
}
return;
}
// Check if opponent is about to win and block
const opponent = getNextPlayer(this.currentPlayer);
move = this.findWinningMove(opponent);
if (move) {
if (willTheComputerMakeAMistake) {
this.gameWinningMistakesMade[this.currentPlayer]++;
} else {
this.updateBoardWithMove(move[0], move[1], this.currentPlayer);
}
return;
}
// Pick center, corners, then sides
for (const [row, column] of this.getStrategicMoveSuggestions()) {
if (this.isValidMove(row, column)) {
this.updateBoardWithMove(row, column, this.currentPlayer);
return;
}
}
}
private makeRandomMove() {
let row: number;
let column: number;
// Continually generate a random number until there is a valid move
do {
(row = getRandomNumber(this.boardSize)),
(column = getRandomNumber(this.boardSize));
} while (!this.isValidMove(row, column));
// Now that a valid move has been found, update the board
this.updateBoardWithMove(row, column, this.currentPlayer);
}
private makeMove() {
switch (this.mode) {
case TicTacToeGameMode.Random:
return this.makeRandomMove();
case TicTacToeGameMode.Strategic:
return this.makeStrategicMove();
default:
throw new Error(
`"${
this.mode
}" is not a valid game mode. Choices are: ${Object.values(
TicTacToeGameMode,
).join(", ")}`,
);
}
}
public getGameResult(): Result | null {
// Winning states are:
// - If a row is fully owned by a player
// - If a column is fully owned by a Player
// - If a diagonal is fully owned by a player
if (this.board.doesPlayerHaveAWinningCellGroup(this.currentPlayer)) {
return `${this.currentPlayer} Wins!` as const;
}
// Draw state if no available moves:
if (this.board.numberOfAvailableMovesLeft === 0) {
return "Draw";
}
return null;
}
public get asConsoleOutput() {
return this.board.asConsoleOutput;
}
public playGame() {
console.log(" ");
console.log(
`${this.boardSize}x${this.boardSize} game started by ${this.currentPlayer} in ${this.mode} mode.`,
);
if (this.options.enableState) {
const lastWinner = GAME_RECORDS.getLastWinner();
console.log(
lastWinner
? ` ${lastWinner} won the last winning game.`
: GAME_RECORDS.getLastGameRecord()
? " No games have been won yet."
: " This is the first game on the record.",
);
}
while (true) {
this.makeMove();
const result = this.getGameResult();
if (this.options.printEveryMove || result) {
console.log(" ");
this.printBoard();
}
// Winner winner, chicken dinner:
if (result !== null) {
console.log(" ");
console.log(result);
if (this.options.enableState) {
if (result !== "Draw") {
CONFIG.lastPlayerToWin = this.currentPlayer;
}
const winningCellGroup =
result === "Draw"
? null
: this.board.getWinningCellGroup(this.currentPlayer);
if (!winningCellGroup && result !== "Draw") {
throw new Error(`Cannot find the winning cell group for the game.`);
}
addGameRecord({
board: [...this.board],
boardSize: this.boardSize,
mode: this.mode,
firstPlayer: this.firstPlayer,
winner: result === "Draw" ? null : this.currentPlayer,
winningCellGroupName: winningCellGroup
? winningCellGroup.name
: null,
winningCellGroupType: winningCellGroup
? winningCellGroup.type
: null,
gameWinningMistakesMade:
this.gameWinningMistakesMade[Player.O] +
this.gameWinningMistakesMade[Player.X],
});
}
break;
}
// Switch the player
this.switchPlayer();
}
}
}
/**
* Run the game 10 times, for X number of rounds at different board sizes:
* - in the default random mode
* - in strategic mode
*/
for (const i of Array.from({ length: CONFIG.roundsOfGamesToRun })) {
for (const mode of [TicTacToeGameMode.Random, TicTacToeGameMode.Strategic]) {
new TicTacToeGame({
enableState: true,
mode,
}).playGame();
new TicTacToeGame({
enableState: true,
mode,
}).playGame();
new TicTacToeGame({
enableState: true,
mode,
}).playGame();
new TicTacToeGame({
enableState: true,
mode,
}).playGame();
new TicTacToeGame({
enableState: true,
mode,
}).playGame();
new TicTacToeGame({
enableState: true,
boardSize: 2,
mode,
}).playGame();
new TicTacToeGame({
boardSize: 2,
enableState: true,
mode,
}).playGame();
new TicTacToeGame({
boardSize: 4,
enableState: true,
mode,
}).playGame();
new TicTacToeGame({
boardSize: 5,
enableState: true,
mode,
}).playGame();
new TicTacToeGame({
boardSize: 6,
enableState: true,
mode,
}).playGame();
}
}
// Run the standard example last to show the exercise working:
new TicTacToeGame({
enableState: false,
printEveryMove: true,
}).playGame();
const scores = GAME_RECORDS.getScores();
console.log(
`\nScores:\n Draws: ${scores.draws}\n X: ${scores[Player.X]}\n O: ${
scores[Player.O]
}`,
);
const stats = GAME_RECORDS.getStats();
console.log(`\nStats:
Games: ${stats.games}
Draws: ${stats.draws} (${formatPercent(stats.draws / stats.games)} of games)
First player wins: ${stats.firstPlayer} (${formatPercent(
stats.firstPlayerPercent,
)} of wins)
Second player wins: ${stats.secondPlayer} (${formatPercent(
stats.secondPlayerPercent,
)} of wins)
Games played in strategic mode: ${stats.gamesInStrategicMode}
Draws in strategic mode: ${stats.drawsInStrategicMode} (${formatPercent(
stats.drawsInStrategicMode / stats.gamesInStrategicMode,
)} of games)
First player wins in strategic mode: ${
stats.firstPlayerInStrategicMode
} (${formatPercent(stats.firstPlayerInStrategicPercent)} of wins)
Second player wins in strategic mode: ${
stats.secondPlayerInStrategicMode
} (${formatPercent(stats.secondPlayerInStrategicPercent)} of wins)
Important mistakes made: ${stats.gameWinningMistakesMade} in ${
stats.gamesWithMistakesMade
} game${stats.gamesWithMistakesMade !== 1 ? "s" : ""} (${formatPercent(
stats.gamesWithMistakesMade / stats.gamesInStrategicMode,
)} of strategic games had mistakes. The odds of making a mistake at any point during a game are ${formatPercent(
1 / CONFIG.oneInXOddOfMissingWinningMove,
)} or 1 in ${CONFIG.oneInXOddOfMissingWinningMove}. Mistakes led to ${
stats.gamesWithMistakesMadeThatWereWon
} win${stats.gamesWithMistakesMadeThatWereWon !== 1 ? "s" : ""} and ${
stats.gamesWithMistakesMadeThatDrawed
} draw${stats.gamesWithMistakesMadeThatDrawed !== 1 ? "s" : ""}.)
Games played in random mode: ${stats.gamesInRandomMode}
Draws in random mode: ${stats.drawsInRandomMode} (${formatPercent(
stats.drawsInRandomMode / stats.gamesInRandomMode,
)} of games)
First player wins in random mode: ${
stats.firstPlayerInRandomMode
} (${formatPercent(stats.firstPlayerInRandomPercent)} of wins)
Second player wins in random mode: ${
stats.secondPlayerInRandomMode
} (${formatPercent(stats.secondPlayerInRandomPercent)}) of wins`);
/**
* ###############################################################
* Testing
* ###############################################################
*/
interface ITestCaseContext {
assertCellGroup: (name: string, expectedCells: NullablePlayer[]) => void;
}
interface ITestCase {
name: string;
boardSize?: number;
board: TicTacToeBoard;
player?: Player;
// One or two of these must be provided for the test case to run:
result?: string | null;
test?: (game: TicTacToeGame, context: ITestCaseContext) => void;
onGameFailToStart?: (cause: Error | unknown) => void;
}
const TEST_CASES: ITestCase[] = [
{
name: "X wins middle column",
board: new TicTacToeBoard({
rows: [
[null, Player.X, null],
[null, Player.X, null],
[null, Player.X, null],
],
}),
result: "X Wins!",
test(game) {
assert.equal(game.asConsoleOutput, "| |X| |\n| |X| |\n| |X| |");
},
},
{
name: "X wins right column",
board: new TicTacToeBoard({
rows: [
[null, null, Player.X],
[null, null, Player.X],
[null, null, Player.X],
],
}),
result: "X Wins!",
},
{
name: "X wins left column",
board: new TicTacToeBoard({
rows: [
[Player.X, null, null],
[Player.X, null, null],
[Player.X, null, null],
],
}),
result: "X Wins!",
},
{
name: "X wins left column, but there is no result because it is not X's turn",
board: new TicTacToeBoard({
rows: [
[Player.X, null, null],
[Player.X, null, null],
[Player.X, null, null],
],
}),
result: null,
player: Player.O,
},
{
name: "X wins diagonal top left to bottom right",
board: new TicTacToeBoard({
rows: [
[Player.X, null, null],
[null, Player.X, null],
[null, null, Player.X],
],
}),
result: "X Wins!",
},
{
name: "X wins diagonal top right to bottom left",
board: new TicTacToeBoard({
rows: [
[null, null, Player.X],
[null, Player.X, null],
[Player.X, null, null],
],
}),
result: "X Wins!",
},
{
name: "O wins middle column",
board: new TicTacToeBoard({
rows: [
[null, Player.O, null],
[null, Player.O, null],
[null, Player.O, null],
],
}),
result: "O Wins!",
player: Player.O,
},
{
name: "O wins right column",
board: new TicTacToeBoard({
rows: [
[null, null, Player.O],
[null, null, Player.O],
[null, null, Player.O],
],
}),
result: "O Wins!",
player: Player.O,
},
{
name: "O wins left column",
board: new TicTacToeBoard({
rows: [
[Player.O, null, null],
[Player.O, null, null],
[Player.O, null, null],
],
}),
result: "O Wins!",
player: Player.O,
},
{
name: "O wins diagonal top left to bottom right",
board: new TicTacToeBoard({
rows: [
[Player.O, null, null],
[null, Player.O, null],
[null, null, Player.O],
],
}),
result: "O Wins!",
player: Player.O,
},
{
name: "O wins diagonal top right to bottom left",
board: new TicTacToeBoard({
rows: [
[null, null, Player.O],
[null, Player.O, null],
[Player.O, null, null],
],
}),
result: "O Wins!",
player: Player.O,
test(game, { assertCellGroup }) {
assertCellGroup("Column #0", [null, null, Player.O]);
assertCellGroup("Column #1", [null, Player.O, null]);
assertCellGroup("Column #2", [Player.O, null, null]);
assertCellGroup("Row #0", [null, null, Player.O]);
assertCellGroup("Row #1", [null, Player.O, null]);
assertCellGroup("Row #2", [Player.O, null, null]);
assertCellGroup("Top left to bottom right diagonal", [
null,
Player.O,
null,
]);
assertCellGroup("Top right to bottom left diagonal", [
Player.O,
Player.O,
Player.O,
]);
assert.equal(game.getGameResult(), "O Wins!");
assert.equal(game.asConsoleOutput, "| | |O|\n| |O| |\n|O| | |");
},
},
{
name: "O wins diagonal top right to bottom left, but there is no result because it is not O's turn",
board: new TicTacToeBoard({
rows: [
[null, null, Player.O],
[null, Player.O, null],
[Player.O, null, null],
],
}),
result: null,
player: Player.X,
},
{
name: "No result yet for player X (1)",
board: new TicTacToeBoard({
rows: [
[Player.X, null, Player.O],
[null, Player.O, null],
[Player.X, null, null],
],
}),
result: null,
player: Player.X,
},
{
name: "No result yet for player X (2)",
board: new TicTacToeBoard({
rows: [
[Player.O, Player.X, Player.O],
[Player.X, Player.O, null],
[Player.X, null, Player.X],
],
}),
result: null,
player: Player.X,
},
{
name: "No result yet for player X (3)",
board: new TicTacToeBoard({
rows: [
[null, null, null],
[null, null, null],
[null, null, null],
],
}),
result: null,
player: Player.X,
},
{
name: "No result yet for player O (1)",
board: new TicTacToeBoard({
rows: [
[Player.X, null, Player.O],
[null, Player.O, null],
[Player.X, null, null],
],
}),
result: null,
player: Player.O,
},
{
name: "No result yet for player O (2)",
board: new TicTacToeBoard({
rows: [
[Player.X, Player.O, Player.X],
[null, Player.X, Player.O],
[Player.X, Player.O, Player.O],
],
}),
result: null,
player: Player.O,
},
{
name: "No result yet for player O (3)",
board: new TicTacToeBoard({
rows: [
[null, null, null],
[null, null, null],
[null, null, null],
],
}),
result: null,
player: Player.O,
},
{
name: "There is a draw (1)",
board: new TicTacToeBoard({
rows: [
[Player.O, Player.X, Player.O],
[Player.X, Player.O, Player.X],
[Player.X, Player.O, Player.X],
],
}),
result: "Draw",
player: Player.X,
},
{
name: "There is a draw (2)",
board: new TicTacToeBoard({
rows: [
[Player.O, Player.X, Player.O],
[Player.X, Player.O, Player.X],
[Player.X, Player.O, Player.X],
],
}),
result: "Draw",
player: Player.O,
},
{
name: "X wins",
board: new TicTacToeBoard({
rows: [
[Player.X, Player.X, Player.O],
[Player.X, Player.O, Player.X],
[Player.X, Player.O, Player.X],
],
}),
result: "X Wins!",
player: Player.X,
},
{
name: "O wins",
board: new TicTacToeBoard({
rows: [
[Player.O, Player.X, Player.O],
[Player.X, Player.O, Player.X],
[Player.X, Player.O, Player.O],
],
}),
result: "O Wins!",
player: Player.O,
},
{
name: "Throws error if there a board size mismatch",
boardSize: 5,
board: new TicTacToeBoard({
rows: [
[Player.O, Player.X, Player.O],
[Player.X, Player.O, Player.X],
[Player.X, Player.O, Player.O],
],
}),
result: "O Wins!",
player: Player.O,
onGameFailToStart(cause) {
assert.equal(
cause instanceof Error ? cause.message : `${cause}`,
"The Tic Tac Toe game was started with a board size of 5, but a custom board was used that has a size of 3. Please ensure the game and the board are of the same size.",
);
},
},
{
name: "2x2 has right output",
boardSize: 2,
board: new TicTacToeBoard({
boardSize: 2,
rows: [
[Player.O, Player.X],
[Player.X, null],
],
}),
player: Player.O,
test(game) {
assert.equal(game.asConsoleOutput, "|O|X|\n|X| |");
},
},
{
name: "4x4 has right output",
boardSize: 4,
board: new TicTacToeBoard({
boardSize: 4,
rows: [
[Player.X, null, Player.X, Player.X],
[Player.O, Player.X, Player.X, null],
[Player.O, Player.O, Player.X, Player.X],
[Player.O, Player.X, Player.O, Player.X],
],
}),
player: Player.X,
result: "X Wins!",
test(game, { assertCellGroup }) {
assertCellGroup("Column #0", [Player.X, Player.O, Player.O, Player.O]);
assertCellGroup("Column #1", [null, Player.X, Player.O, Player.X]);
assertCellGroup("Column #2", [Player.X, Player.X, Player.X, Player.O]);
assertCellGroup("Column #3", [Player.X, null, Player.X, Player.X]);
assertCellGroup("Row #0", [Player.X, null, Player.X, Player.X]);
assertCellGroup("Row #1", [Player.O, Player.X, Player.X, null]);
assertCellGroup("Row #3", [Player.O, Player.X, Player.O, Player.X]);
assertCellGroup("Top left to bottom right diagonal", [
Player.X,
Player.X,
Player.X,
Player.X,
]);
assertCellGroup("Top right to bottom left diagonal", [
Player.O,
Player.O,
Player.X,
Player.X,
]);
assert.equal(
game.asConsoleOutput,
"|X| |X|X|\n|O|X|X| |\n|O|O|X|X|\n|O|X|O|X|",
);
},
},
];
console.debug(" ");
console.debug("Running tests...");
let testCaseIndex = 0;
for (const testCase of TEST_CASES) {
testCaseIndex++;
let testRan = false;
let game: TicTacToeGame | undefined;
const testCaseId = `Test case ${testCaseIndex}/${TEST_CASES.length} "${testCase.name}"`;
try {
game = new TicTacToeGame(
{
boardSize: testCase.boardSize ?? CONFIG.boardSize,
},
testCase.board,
);
if ("player" in testCase && testCase.player) {
game.setPlayer(testCase.player);
}
} catch (cause) {
if (
"onGameFailToStart" in testCase &&
typeof testCase.onGameFailToStart === "function"
) {
testRan = true;
try {
testCase.onGameFailToStart(cause);
} catch (cause) {
console.error(`[${testCaseId}] failed to start game`);
throw cause;
}
} else {
console.error(`[${testCaseId}] failed to start game`);
throw cause;
}
}
if (!testRan) {
if (!game) {
throw new Error(`No game started for ${testCaseId}`);
}
// Custom test runner function provided:
if ("test" in testCase && typeof testCase.test === "function") {
testRan = true;
const cellGroups = game.getCellGroups();
const assertCellGroup = (
name: string,
expectedCells: NullablePlayer[],
) => {
const cells = cellGroups[name];
if (!cells) {
throw new Error(`Cannot find cell group for ${name}`);
}
assert.deepEqual(
cells,
expectedCells,
`${name} should have ${formatCellGroupOutput(
expectedCells,
)} but instead has ${formatCellGroupOutput(cells)}`,
);
};
try {
testCase.test(game, { assertCellGroup });
} catch (cause) {
console.error(`[${testCaseId}] failed`);
throw cause;
}
}
// Standard test of comparing the game result:
if (
"result" in testCase &&
(typeof testCase.result === "string" || testCase.result === null)
) {
testRan = true;
const result = game.getGameResult();
assert.strictEqual(
result,
testCase.result,
`[${testCaseId}] "${
testCase.result
}" was expected, but instead the result of the game is "${result}". Game:\n${
game.asConsoleOutput
}\n\nCell groups:\n${JSON.stringify(game.getCellGroups(), null, 2)}`,
);
}
// Test case is invalid, because either a test() function or result string should be provided:
if (!testRan) {
console.error(
`[${testCaseId}] is invalid because it requires a test() function or result string.`,
);
throw new Error(
`${testCaseId} is invalid: ${JSON.stringify(testCase, null, 2)}`,
);
}
}
console.debug(`[${testCaseId}] passed`);
}
console.debug("Tests finished.");
Version 5.0.3
Everyone can modify this shell in real time.
It's a sandbox that works exactly like a native shell.
>
H. Charley Bodkin ran 1389 lines of TypeScript (finished in 2.95s):
3x3 game started by X in random mode.
This is the first game on the record.
|X|X|O|
| | |O|
|X| |O|
O Wins!
3x3 game started by X in random mode.
O won the last winning game.
|O|X|O|
|X|X|O|
|O|X|X|
X Wins!
3x3 game started by O in random mode.
X won the last winning game.
| |O|O|
|X|X|X|
|O|O|X|
X Wins!
3x3 game started by O in random mode.
X won the last winning game.
| |X|O|
| |O| |
|O| |X|
O Wins!
3x3 game started by X in random mode.
O won the last winning game.
|X|X|O|
|O|X|O|
| |X| |
X Wins!
2x2 game started by O in random mode.
X won the last winning game.
|X| |
|O|O|
O Wins!
2x2 game started by X in random mode.
O won the last winning game.
|X|X|
|O| |
X Wins!
4x4 game started by O in random mode.
X won the last winning game.
| |X|X|O|
|X|X|X| |
|O|O|O|O|
|X|O| |O|
O Wins!
5x5 game started by X in random mode.
O won the last winning game.
|X|X|X|X|X|
|O|O|O|O| |
|O|X|X|X| |
| |X|X|O|O|
|O|X| |O|O|
X Wins!
6x6 game started by O in random mode.
X won the last winning game.
|X|O|X|X|O|O|
|O|X|O|X|O|X|
|X|O|X|O|X|O|
|X|O|X|O|O|O|
|X|O|O|X|O|O|
|O|X|X|X|X|X|
Draw
3x3 game started by O in strategic mode.
X won the last winning game.
|X|X|O|
|O|O|X|
|X|O|O|
Draw
3x3 game started by O in strategic mode.
X won the last winning game.
|X|O|O|
|O|O|X|
|X|X|O|
Draw
3x3 game started by O in strategic mode.
X won the last winning game.
|X|X|O|
|O|O|X|
|X|O|O|
Draw
3x3 game started by O in strategic mode.
X won the last winning game.
|X|X|O|
|O|O|X|
|X|O|O|
Draw
3x3 game started by O in strategic mode.
X won the last winning game.
|X|X|O|
|O|O|X|
|X|O|O|
Draw
2x2 game started by O in strategic mode.
X won the last winning game.
|O|O|
| | |
O Wins!
2x2 game started by X in strategic mode.
O won the last winning game.
|O|X|
| |X|
X Wins!
4x4 game started by O in strategic mode.
X won the last winning game.
|O|X| | |
|X|O|X|X|
|X|O|O|O|
| | |O|O|
O Wins!
5x5 game started by X in strategic mode.
O won the last winning game.
|O|O|O|X|X|
|O|X|X|X|O|
|O|O|X|O|X|
|X|O|X|X|X|
|O|O|X|O|X|
Draw
6x6 game started by X in strategic mode.
O won the last winning game.
|X|X|X|O|X|X|
|O|O|X|X|O|O|
|O|X|X|O|O|O|
|O| |O|O|O|X|
|O|X| |O|X|O|
|X|X|X|X|X|X|
X Wins!
3x3 game started by O in random mode.
X won the last winning game.
|X|X|X|
|O| |O|
|X|O|O|
X Wins!
3x3 game started by O in random mode.
X won the last winning game.
|X|O|X|
|O|O|X|
|O| |X|
X Wins!
3x3 game started by O in random mode.
X won the last winning game.
|X|O|X|
|O|O|X|
|X|O|O|
O Wins!
3x3 game started by X in random mode.
O won the last winning game.
|X|X|O|
|O|X|X|
|X|O|O|
Draw
3x3 game started by X in random mode.
O won the last winning game.
|O|X|X|
|O|O|X|
|O|X| |
O Wins!
2x2 game started by X in random mode.
O won the last winning game.
|X| |
|X|O|
X Wins!
2x2 game started by O in random mode.
X won the last winning game.
|O| |
|O|X|
O Wins!
4x4 game started by X in random mode.
O won the last winning game.
|X|O|X|X|
|O|X|O|O|
|X|O|X|O|
|X|X|O|O|
Draw
5x5 game started by X in random mode.
O won the last winning game.
|O|X|X|O|X|
|X|O|O|X|X|
| |O|X| |O|
|O|X|X|X|X|
|X|O|O|O|O|
X Wins!
6x6 game started by O in random mode.
X won the last winning game.
|O|O|O|O|O|X|
|X|O|O|X|X|O|
|O|X|X|O|X|X|
|X|X|O|X|O|X|
|X|O|O|X|X|O|
|X|O|X|X|O|O|
Draw
3x3 game started by O in strategic mode.
X won the last winning game.
|X| |O|
|X|O| |
|X| | |
X Wins!
3x3 game started by O in strategic mode.
X won the last winning game.
|X|O|O|
|O|O|X|
|X|X|O|
Draw
3x3 game started by O in strategic mode.
X won the last winning game.
|X|X|O|
|O|O|X|
|X|O|O|
Draw
3x3 game started by O in strategic mode.
X won the last winning game.
|X|O|O|
|O|O|X|
|X|X|O|
Draw
3x3 game started by O in strategic mode.
X won the last winning game.
|X|X|O|
|O|O|X|
|X|O|O|
Draw
2x2 game started by O in strategic mode.
X won the last winning game.
|O|X|
|O| |
O Wins!
2x2 game started by X in strategic mode.
O won the last winning game.
|O|X|
|X| |
X Wins!
4x4 game started by O in strategic mode.
X won the last winning game.
|O|O|X|O|
|X|X|O|O|
|X|X|O|X|
|X|O|X|O|
Draw
5x5 game started by O in strategic mode.
X won the last winning game.
|X|X|O|X|O|
|X|O|X|O|O|
|O|X|O|X|O|
|O|O|X|X|X|
|X|O|X|O|O|
Draw
6x6 game started by O in strategic mode.
X won the last winning game.
|O|O|O|O|X|O|
|O|O|X|X|O|X|
|X|X|X|O|X|X|
|O|O|X|O|X|X|
|O|X|X|O|X|O|
|X|O|X|X|O|O|
Draw
3x3 game started by O in random mode.
X won the last winning game.
|O|X|O|
| |X|O|
|O|X|X|
X Wins!
3x3 game started by O in random mode.
X won the last winning game.
|X|X|X|
| |O|O|
| | |O|
X Wins!
3x3 game started by O in random mode.
X won the last winning game.
|O|X|X|
|O|O| |
| |X|O|
O Wins!
3x3 game started by X in random mode.
O won the last winning game.
|X|X|X|
|O|O|X|
|O|O|X|
X Wins!
3x3 game started by O in random mode.
X won the last winning game.
|O|O|X|
| |O|O|
|X|X|X|
X Wins!
2x2 game started by O in random mode.
X won the last winning game.
|O|O|
|X| |
O Wins!
2x2 game started by X in random mode.
O won the last winning game.
|X|X|
| |O|
X Wins!
4x4 game started by O in random mode.
X won the last winning game.
|X| |X|O|
|X| |O|O|
| |X|X|O|
| | |O|O|
O Wins!
5x5 game started by X in random mode.
O won the last winning game.
|X|X|O|X| |
|X| |X| |O|
|X|O|O|X|X|
|X|O|X| | |
|O|O|O|O|O|
O Wins!
6x6 game started by X in random mode.
O won the last winning game.
|X|O|X|O|O|X|
|X|X|X|X|O|O|
|O|O|O|O|X|O|
|X|O|O|X|O|X|
|O|X|X|X|O|O|
|X|X|O|X|O|X|
Draw
3x3 game started by X in strategic mode.
O won the last winning game.
|O|X|X|
|X|X|O|
|O|O|X|
Draw
3x3 game started by X in strategic mode.
O won the last winning game.
|O|X|X|
|X|X|O|
|O|O|X|
Draw
3x3 game started by X in strategic mode.
O won the last winning game.
|O|O|X|
|X|X|O|
|O|X|X|
Draw
3x3 game started by X in strategic mode.
O won the last winning game.
|O|X|X|
|X|X|O|
|O|O|X|
Draw
3x3 game started by X in strategic mode.
O won the last winning game.
|O|O|X|
|X|X|O|
|O|X|X|
Draw
2x2 game started by X in strategic mode.
O won the last winning game.
|O|X|
| |X|
X Wins!
2x2 game started by O in strategic mode.
X won the last winning game.
|O|X|
|O| |
O Wins!
4x4 game started by X in strategic mode.
O won the last winning game.
|X|O|O|X|
|O|X|O|X|
|O|X|X|X|
|O|O|X|O|
Draw
5x5 game started by X in strategic mode.
O won the last winning game.
|O|O|O|X|X|
|O|X|X|X|O|
|X|X|X|O|O|
|X|X|O|O|X|
|O|X|O|O|X|
Draw
6x6 game started by X in strategic mode.
O won the last winning game.
|O|X|O|O|O|X|
|O|X|X|X|X|O|
|O|O|X|O|O|O|
|O|O|X|O|X|O|
|X|O|X|X|X|O|
|O|X|X|X|X|X|
Draw
3x3 game started by X in random mode.
O won the last winning game.
|O|X|O|
|X|O|O|
|X|X|X|
X Wins!
3x3 game started by O in random mode.
X won the last winning game.
|X| |O|
|O|X|O|
|X|O|X|
X Wins!
3x3 game started by O in random mode.
X won the last winning game.
|O|X|X|
|O|X|O|
|X|O| |
X Wins!
3x3 game started by O in random mode.
X won the last winning game.
|O|O|X|
|X|O|X|
|O| |X|
X Wins!
3x3 game started by O in random mode.
X won the last winning game.
|X|O|X|
|O|X|O|
|O|X|O|
Draw
2x2 game started by O in random mode.
X won the last winning game.
|O|O|
| |X|
O Wins!
2x2 game started by X in random mode.
O won the last winning game.
|X| |
|X|O|
X Wins!
4x4 game started by O in random mode.
X won the last winning game.
|O|O|X|X|
|O|X|X|O|
|X|X|X|O|
|O|O|X|O|
X Wins!
5x5 game started by O in random mode.
X won the last winning game.
|X|O|X|X|O|
|O|X|O|O|X|
|X|O|X|O|O|
|O|O|O|O|X|
|X|X|X|X|O|
Draw
6x6 game started by O in random mode.
X won the last winning game.
|X|X|X| |O|O|
|O|O|O|X|O|X|
|X| | | |O|X|
|X| |X|X|O| |
|O| | |X|O| |
|O| |O| |O|X|
O Wins!
3x3 game started by X in strategic mode.
O won the last winning game.
|O|O|X|
|X|X|O|
|O|X|X|
Draw
3x3 game started by X in strategic mode.
O won the last winning game.
|O| |X|
|X|X|X|
|O| | |
X Wins!
3x3 game started by O in strategic mode.
X won the last winning game.
|X| |O|
| |O| |
|O| | |
O Wins!
3x3 game started by X in strategic mode.
O won the last winning game.
|O|O|X|
|X|X|O|
|O|X|X|
Draw
3x3 game started by X in strategic mode.
O won the last winning game.
|O|O|X|
|X|X|O|
|O|X|X|
Draw
2x2 game started by X in strategic mode.
O won the last winning game.
|O|X|
| |X|
X Wins!
2x2 game started by O in strategic mode.
X won the last winning game.
|X|O|
| |O|
O Wins!
4x4 game started by X in strategic mode.
O won the last winning game.
|O|X|O|O|
|O|X|O|X|
|O|O|X|X|
|X|X|O|X|
Draw
5x5 game started by X in strategic mode.
O won the last winning game.
|O|O|O|X|X|
|X|X|O|X|O|
|X|O|X|O|X|
|O|X|O|X|X|
|O|O|O|X|X|
Draw
6x6 game started by X in strategic mode.
O won the last winning game.
|O|X|X|X|X|O|
|X|X|O|O|X|O|
|O|X|X|O|X|X|
|X|X|O|O|O|O|
|O|X|X|X|O|O|
|O|O|O|X|O|X|
Draw
3x3 game started by X in random mode.
O won the last winning game.
| |X|O|
|X|O|X|
|O|O|X|
O Wins!
3x3 game started by X in random mode.
O won the last winning game.
|X|O|X|
|O|X|O|
|O|X|X|
X Wins!
3x3 game started by O in random mode.
X won the last winning game.
| | |X|
|X|O|X|
|O|O|O|
O Wins!
3x3 game started by X in random mode.
O won the last winning game.
|O| |X|
|O|X| |
|O|X| |
O Wins!
3x3 game started by X in random mode.
O won the last winning game.
| |X| |
|O|X|O|
|X|X|O|
X Wins!
2x2 game started by O in random mode.
X won the last winning game.
|O| |
|X|O|
O Wins!
2x2 game started by X in random mode.
O won the last winning game.
| |X|
|O|X|
X Wins!
4x4 game started by O in random mode.
X won the last winning game.
|O|X|O|O|
|O|X|X|X|
|X|X|O|O|
|X|O|O|X|
Draw
5x5 game started by O in random mode.
X won the last winning game.
|X|O|X|O|X|
|O|X|O|X|X|
|O|O|O|X|O|
|X|X|O|O|X|
|O|X|X|O|O|
Draw
6x6 game started by O in random mode.
X won the last winning game.
|X|X|X|O|O|O|
|X| | |X|O|O|
| |X|O|O|O|X|
| |O|O|X|O|X|
|X| |X|X|O|X|
|O| |O| |O|X|
O Wins!
3x3 game started by X in strategic mode.
O won the last winning game.
|O|O|X|
|X|X|O|
|O|X|X|
Draw
3x3 game started by X in strategic mode.
O won the last winning game.
|O|O|X|
|X|X|O|
|O|X|X|
Draw
3x3 game started by X in strategic mode.
O won the last winning game.
|O|O|X|
|X|X|O|
|O|X|X|
Draw
3x3 game started by X in strategic mode.
O won the last winning game.
|O|X|X|
|X|X|O|
|O|O|X|
Draw
3x3 game started by X in strategic mode.
O won the last winning game.
|O| |X|
| |X| |
|X| | |
X Wins!
2x2 game started by O in strategic mode.
X won the last winning game.
|X|O|
|O| |
O Wins!
2x2 game started by X in strategic mode.
O won the last winning game.
|O|X|
|X| |
X Wins!
4x4 game started by O in strategic mode.
X won the last winning game.
|X|O|X|O|
|O|O|X|O|
|X|X|O|O|
|X|X|O|X|
Draw
5x5 game started by O in strategic mode.
X won the last winning game.
|X|O|O|X|O|
|O|O|X|O|O|
|X|X|O|X|O|
|O|O|X|X|X|
|X|X|O|X|O|
Draw
6x6 game started by O in strategic mode.
X won the last winning game.
|X|O|O|X|O|X|
|O|X|X|X|O|O|
|X|O|X|O|O|X|
|O|O|X|X|X|O|
|O|X|X|O|O|O|
|O|X|X|X|O|X|
Draw
3x3 game started by X in random mode.
| | | |
|X| | |
| | | |
| | | |
|X| | |
|O| | |
| | |X|
|X| | |
|O| | |
| | |X|
|X| | |
|O| |O|
| |X|X|
|X| | |
|O| |O|
| |X|X|
|X|O| |
|O| |O|
|X|X|X|
|X|O| |
|O| |O|
X Wins!
Scores:
Draws: 43
X: 32
O: 25
Stats:
Games: 100
Draws: 43 (43% of games)
First player wins: 41 (71.93% of wins)
Second player wins: 16 (28.07% of wins)
Games played in strategic mode: 50
Draws in strategic mode: 34 (68% of games)
First player wins in strategic mode: 15 (93.75% of wins)
Second player wins in strategic mode: 1 (6.25% of wins)
Important mistakes made: 11 in 9 games (18% of strategic games had mistakes. The odds of making a mistake at any point during a game are 10% or 1 in 10. Mistakes led to 7 wins and 2 draws.)
Games played in random mode: 50
Draws in random mode: 9 (18% of games)
First player wins in random mode: 26 (63.41% of wins)
Second player wins in random mode: 15 (36.59%) of wins
Running tests...
[Test case 1/25 "X wins middle column"] passed
[Test case 2/25 "X wins right column"] passed
[Test case 3/25 "X wins left column"] passed
[Test case 4/25 "X wins left column, but there is no result because it is not X's turn"] passed
[Test case 5/25 "X wins diagonal top left to bottom right"] passed
[Test case 6/25 "X wins diagonal top right to bottom left"] passed
[Test case 7/25 "O wins middle column"] passed
[Test case 8/25 "O wins right column"] passed
[Test case 9/25 "O wins left column"] passed
[Test case 10/25 "O wins diagonal top left to bottom right"] passed
[Test case 11/25 "O wins diagonal top right to bottom left"] passed
[Test case 12/25 "O wins diagonal top right to bottom left, but there is no result because it is not O's turn"] passed
[Test case 13/25 "No result yet for player X (1)"] passed
[Test case 14/25 "No result yet for player X (2)"] passed
[Test case 15/25 "No result yet for player X (3)"] passed
[Test case 16/25 "No result yet for player O (1)"] passed
[Test case 17/25 "No result yet for player O (2)"] passed
[Test case 18/25 "No result yet for player O (3)"] passed
[Test case 19/25 "There is a draw (1)"] passed
[Test case 20/25 "There is a draw (2)"] passed
[Test case 21/25 "X wins"] passed
[Test case 22/25 "O wins"] passed
[Test case 23/25 "Throws error if there a board size mismatch"] passed
[Test case 24/25 "2x2 has right output"] passed
[Test case 25/25 "4x4 has right output"] passed
Tests finished.
>
type Player = 'X' | 'O';
type NullablePlayer = Player | null;
type BoardRow = [NullablePlayer, NullablePlayer, NullablePlayer];
type Board = [BoardRow, BoardRow, BoardRow];
type Result = 'X Wins!' | 'O Wins!' | 'Draw';
const createBoard = (): Board => {
return [
[null, null, null],
[null, null, null],
[null, null, null],
]
}
const getNextPlayer = (player: Player) => {
switch (player) {
case 'X':
return 'O';
case 'O':
return 'X';
}
}
const getRandomNumber = (max = 3) => {
return Math.floor(Math.random() * max);
}
class TicTacToe {
private board: Board = createBoard();
private currentPlayer: Player = 'X';
private getNumberOfMovesMade() {
let numberOfMovesMade = 0;
for (const row of this.board) {
numberOfMovesMade += row.filter(player => player !== null).length;
}
return numberOfMovesMade;
}
constructor() {
}
private switchPlayer() {
this.currentPlayer = getNextPlayer(this.currentPlayer);
}
private getBoardOutput() {
const output = this.board.map(row => {
return ['|', row.map(player => player ?? ' ').join('|'), '|'].join('');
}).join('\n');
return output;
}
private printBoard() {
console.log(this.getBoardOutput());
}
private isValidMove(row: number, column: number) {
return this.board[row][column] === null;
}
private updateBoardWithMove(row: number, column: number, player: Player) {
return this.board[row][column] = player;
}
private makeRandomMove() {
let row: number;
let column: number;
// Continually generate a random number until there is a valid move
do {
row = getRandomNumber(this.board.length),
column = getRandomNumber(this.board[0].length)
} while (!this.isValidMove(row, column));
// Now that a valid move has been found, update the board
this.updateBoardWithMove(row, column, this.currentPlayer);
}
private getGameResult(): Result | null {
// Winning states are:
// - If a row is fully owned by a player
// - If a column is fully owned by a Player
// - If a diagonal is fully owned by a player
const getWinningGameResult = () => {
switch (this.currentPlayer) {
case 'X':
return 'X Wins!';
case 'O':
return 'O Wins!';
default:
return 'Draw';
}
}
for (const row of this.board) {
const hasWonBecauseOfRow = row.filter(player =>
player === this.currentPlayer
).length === 3;
if (hasWonBecauseOfRow) {
return getWinningGameResult();
}
}
for (let column = 0; column < 3; column++) {
const playsInColumn = [this.board[0][column], this.board[1][column], this.board[2][column]];
const hasWonBecauseOfColumn = playsInColumn.filter(player =>
player === this.currentPlayer
).length === 3;
if (hasWonBecauseOfColumn) {
return getWinningGameResult();
}
}
const topRightCell = this.board[0][2];
const middleCell = this.board[1][1];
const bottomLeftCell = this.board[2][0];
const topLeftCell = this.board[0][0];
const bottomRightCell = this.board[2][2];
const hasWonBecauseOfDiagonalTopLeftToBottomRight = topLeftCell === this.currentPlayer && middleCell === this.currentPlayer && bottomRightCell === this.currentPlayer;
if (hasWonBecauseOfDiagonalTopLeftToBottomRight) {
return getWinningGameResult();
}
const hasWonBecauseOfDiagonalTopRightToBottomLeft = topRightCell === this.currentPlayer && middleCell === this.currentPlayer && bottomLeftCell === this.currentPlayer;
if (hasWonBecauseOfDiagonalTopRightToBottomLeft) {
return getWinningGameResult();
}
// Draw state if no available moves:
const availableMovesLeft = this.board.reduce<number>((acc, row) => {
return acc + row.filter(player => player === null).length;
}, 0);
if (availableMovesLeft === 0) {
return 'Draw';
}
return null;
}
public playGame() {
while (true) {
console.log(' ');
this.makeRandomMove();
this.printBoard();;
const result = this.getGameResult()
// Winner winner, chicken dinner:
if (result !== null) {
console.log(result);
break;
}
// Switch the player (TODO: Might move to the main game progression loop)
this.switchPlayer();
}
}
}
for (let column = 0; column < 3; column++) {
new TicTacToe().playGame();
}
Version 5.0.3
Everyone can modify this shell in real time.
It's a sandbox that works exactly like a native shell.
H. Charley Bodkin ran 190 lines of TypeScript (finished in 2.23s):
| | | |
| | |X|
| | | |
| | | |
| |O|X|
| | | |
| | |X|
| |O|X|
| | | |
| | |X|
|O|O|X|
| | | |
| | |X|
|O|O|X|
| | |X|
X Wins!
| | | |
| | | |
| | |X|
| | | |
| | |O|
| | |X|
| | | |
|X| |O|
| | |X|
| |O| |
|X| |O|
| | |X|
| |O| |
|X| |O|
| |X|X|
| |O| |
|X| |O|
|O|X|X|
|X|O| |
|X| |O|
|O|X|X|
|X|O| |
|X|O|O|
|O|X|X|
|X|O|X|
|X|O|O|
|O|X|X|
Draw
| | | |
| | | |
| |X| |
|O| | |
| | | |
| |X| |
|O| | |
| | | |
| |X|X|
|O| | |
| | | |
|O|X|X|
|O| | |
| |X| |
|O|X|X|
|O|O| |
| |X| |
|O|X|X|
|O|O| |
|X|X| |
|O|X|X|
|O|O|O|
|X|X| |
|O|X|X|
O Wins!
>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment