Skip to content

Instantly share code, notes, and snippets.

@olavoasantos
Created April 17, 2020 21:29
Show Gist options
  • Save olavoasantos/5953fc92dcc8344a0e4f81b4bed856d9 to your computer and use it in GitHub Desktop.
Save olavoasantos/5953fc92dcc8344a0e4f81b4bed856d9 to your computer and use it in GitHub Desktop.
Tic Tac Toe implementation
export class Board {
public size: number;
protected board: (null | 'o' | 'x')[];
constructor(size: number = 3) {
this.size = size;
this.board = Array(size * size).fill(null);
}
public peak() {
return this.board.slice();
}
public set(position: number, mark: 'o' | 'x') {
this.board[position] = mark;
}
public has(position: number) {
return this.board[position] != null;
}
public get(position: number) {
return this.board[position];
}
public reset() {
this.board = Array(9).fill(null);
}
}
import { Board } from './Board';
import { Judge } from './Judge';
export class Game {
protected board: Board;
protected judge: Judge;
protected lastMark?: 'o' | 'x';
constructor(board?: Board) {
this.board = board ?? new Board();
this.judge = new Judge(this.board);
}
public peak() {
return this.board.peak();
}
public restart() {
this.board.reset();
}
public getEmptySlots() {
return this.board.peak().reduce((slots: number[], pos, index) => {
if (pos === null) slots.push(index);
return slots;
}, []);
}
public hasEmptySlots() {
return this.board.peak().includes(null);
}
public has(position: number) {
return this.board.has(position);
}
public isMyTurn(mark: 'o' | 'x') {
return !this.lastMark || this.lastMark !== mark;
}
public set(position: number, mark: 'o' | 'x') {
if (!this.isMyTurn(mark)) {
if (process.env.NODE_ENV !== 'test')
console.error(
`Please alternate turns! It's ${
mark === 'o' ? 'x' : 'o'
}'s turn now.`,
);
} else if (this.has(position)) {
if (process.env.NODE_ENV !== 'test')
console.error(
`Can't mark the same position twice! Please choose another position`,
);
} else if (!this.hasWinner()) {
this.board.set(position, mark);
this.lastMark = mark;
}
}
public hasWinner() {
return this.judge.check();
}
public winner() {
return this.judge.getWinner();
}
}
import { Opponent } from './Opponent';
export class GullibleOpponent extends Opponent {
protected positions: number[] = [];
blockPositions(positions: number[]) {
this.positions = positions.slice();
}
choosePosition() {
const random = () => Math.floor(Math.random() * 8);
let position = random();
while (this.game.has(position) || this.positions.includes(position)) {
position = random();
}
return position;
}
}
import { Board } from './Board';
export class Judge {
protected board: Board;
protected size: number;
constructor(board: Board) {
this.board = board;
this.size = board.size;
}
public line(n: number) {
const start = (n % this.size) * this.size;
const factor = 1;
return Array.from(
{ length: this.size },
(_, index) => start + index * factor,
);
}
public column(n: number) {
const start = n % this.size;
const factor = this.size;
return Array.from(
{ length: this.size },
(_, index) => start + index * factor,
);
}
public diagonal(n: number) {
const start = (n % 2) * (this.size - 1);
const factor = start === 0 ? this.size + 1 : this.size - 1;
return Array.from(
{ length: this.size },
(_, index) => start + index * factor,
);
}
public verify(positions: number[]) {
const mark = this.board.get(positions.shift() as number);
return positions.every(
(position) =>
this.board.has(position) && mark === this.board.get(position),
);
}
public check() {
for (let n = 0; n < this.size; n += 1) {
const line = this.line(n);
if (this.verify(line)) {
return true;
}
}
for (let n = 0; n < this.size; n += 1) {
const column = this.column(n);
if (this.verify(column)) {
return true;
}
}
for (let n = 0; n < 2; n += 1) {
const diagonal = this.diagonal(n);
if (this.verify(diagonal)) {
return true;
}
}
return false;
}
public getWinner() {
for (let n = 0; n < this.size; n += 1) {
const line = this.line(n);
if (this.verify(line)) {
return this.board.get(line[0]);
}
}
for (let n = 0; n < this.size; n += 1) {
const column = this.column(n);
if (this.verify(column)) {
return this.board.get(column[0]);
}
}
for (let n = 0; n < 2; n += 1) {
const diagonal = this.diagonal(n);
if (this.verify(diagonal)) {
return this.board.get(diagonal[0]);
}
}
return undefined;
}
}
import { Game } from './Game';
export class Opponent {
protected game: Game;
protected mark: 'o' | 'x';
constructor(game: Game, mark: 'o' | 'x') {
this.game = game;
this.mark = mark;
}
choosePosition() {
const positions = this.game.getEmptySlots();
return positions[Math.floor(Math.random() * (positions.length + 1))];
}
play() {
const position = this.choosePosition();
this.game.set(position, this.mark);
}
}
import { Board } from './Board';
import { Judge } from './Judge';
import { Game } from './Game';
import { GullibleOpponent } from './GullibleOpponent';
describe('tic tac toe winner', () => {
describe('Board tests', () => {
it('should return the whole board', () => {
const board = new Board();
expect(board.peak()).toMatchObject([
null,
null,
null,
null,
null,
null,
null,
null,
null,
]);
});
it('should set a given mark in a given position', () => {
const board = new Board();
board.set(0, 'o');
expect(board.peak()[0]).toBe('o');
});
it('should get a given mark in a given position', () => {
const board = new Board();
board.set(0, 'o');
expect(board.get(0)).toBe('o');
});
it('should check if a given position has a mark', () => {
const board = new Board();
board.set(0, 'o');
expect(board.has(0)).toBeTruthy();
expect(board.has(1)).toBeFalsy();
});
it('should reset the board', () => {
const board = new Board();
board.set(0, 'o');
board.reset();
expect(board.has(0)).toBeFalsy();
});
});
describe('Judge tests', () => {
it('should get the correct line positions', () => {
const judge = new Judge(new Board());
expect(judge.line(0)).toMatchObject([0, 1, 2]);
expect(judge.line(1)).toMatchObject([3, 4, 5]);
expect(judge.line(2)).toMatchObject([6, 7, 8]);
});
it('should get the correct column positions', () => {
const judge = new Judge(new Board());
expect(judge.column(0)).toMatchObject([0, 3, 6]);
expect(judge.column(1)).toMatchObject([1, 4, 7]);
expect(judge.column(2)).toMatchObject([2, 5, 8]);
});
it('should get the correct diagonal positions', () => {
const judge = new Judge(new Board());
expect(judge.diagonal(0)).toMatchObject([0, 4, 8]);
expect(judge.diagonal(1)).toMatchObject([2, 4, 6]);
});
it('should verify if a line does not have a winner if empty', () => {
const board = new Board();
const judge = new Judge(board);
expect(judge.verify(judge.line(0))).toBeFalsy();
});
it('should verify if a line does not have a winner if incomplete', () => {
const board = new Board();
const judge = new Judge(board);
[1, 2].forEach((pos) => board.set(pos, 'x'));
expect(judge.verify(judge.line(0))).toBeFalsy();
});
it('should verify if a line does not have a winner if all symbols do not match', () => {
const board = new Board();
const judge = new Judge(board);
board.set(0, 'o');
[1, 2].forEach((pos) => board.set(pos, 'x'));
expect(judge.verify(judge.line(0))).toBeFalsy();
});
it('should verify if a line has a winner', () => {
const board = new Board();
const judge = new Judge(board);
[0, 1, 2].forEach((pos) => board.set(pos, 'o'));
expect(judge.verify(judge.line(0))).toBeTruthy();
});
it('should verify if a column does not have a winner if empty', () => {
const board = new Board();
const judge = new Judge(board);
expect(judge.verify(judge.column(0))).toBeFalsy();
});
it('should verify if a column does not have a winner if incomplete', () => {
const board = new Board();
const judge = new Judge(board);
[0, 3].forEach((pos) => board.set(pos, 'x'));
expect(judge.verify(judge.column(0))).toBeFalsy();
});
it('should verify if a column does not have a winner if all symbols do not match', () => {
const board = new Board();
const judge = new Judge(board);
board.set(6, 'o');
[0, 3].forEach((pos) => board.set(pos, 'x'));
expect(judge.verify(judge.column(0))).toBeFalsy();
});
it('should verify if a column has a winner', () => {
const board = new Board();
const judge = new Judge(board);
[0, 3, 6].forEach((pos) => board.set(pos, 'o'));
expect(judge.verify(judge.column(0))).toBeTruthy();
});
it('should verify if a diagonal does not have a winner if empty', () => {
const board = new Board();
const judge = new Judge(board);
expect(judge.verify(judge.diagonal(0))).toBeFalsy();
});
it('should verify if a diagonal does not have a winner if incomplete', () => {
const board = new Board();
const judge = new Judge(board);
[0, 4].forEach((pos) => board.set(pos, 'x'));
expect(judge.verify(judge.diagonal(0))).toBeFalsy();
});
it('should verify if a diagonal does not have a winner if all symbols do not match', () => {
const board = new Board();
const judge = new Judge(board);
board.set(8, 'o');
[0, 4].forEach((pos) => board.set(pos, 'x'));
expect(judge.verify(judge.diagonal(0))).toBeFalsy();
});
it('should verify if a diagonal has a winner', () => {
const board = new Board();
const judge = new Judge(board);
[0, 4, 8].forEach((pos) => board.set(pos, 'o'));
expect(judge.verify(judge.diagonal(0))).toBeTruthy();
});
it('should check return true if a board has a winner', () => {
const board = new Board();
const judge = new Judge(board);
[0, 4, 8].forEach((pos) => board.set(pos, 'o'));
expect(judge.check()).toBeTruthy();
});
it('should check return false if a board has no winners', () => {
const board = new Board();
const judge = new Judge(board);
[0, 8].forEach((pos) => board.set(pos, 'o'));
expect(judge.check()).toBeFalsy();
});
it('should check return the winning mark if a board has a winner', () => {
const board = new Board();
const judge = new Judge(board);
[0, 4, 8].forEach((pos) => board.set(pos, 'o'));
expect(judge.getWinner()).toBe('o');
});
it('should check return undefined if a board has no winners', () => {
const board = new Board();
const judge = new Judge(board);
[0, 8].forEach((pos) => board.set(pos, 'o'));
expect(judge.getWinner()).toBeUndefined();
});
});
describe('Game tests', () => {
it('should peak the board', () => {
const game = new Game();
expect(game.peak()).toMatchObject([
null,
null,
null,
null,
null,
null,
null,
null,
null,
]);
});
it('should accept a board', () => {
const board = new Board();
const game = new Game(board);
board.set(0, 'o');
expect(game.peak()).toMatchObject([
'o',
null,
null,
null,
null,
null,
null,
null,
null,
]);
});
it('should restart a game', () => {
const board = new Board();
const game = new Game(board);
board.set(0, 'o');
game.restart();
expect(game.peak()).toMatchObject([
null,
null,
null,
null,
null,
null,
null,
null,
null,
]);
});
it('should return all empty slots from the board', () => {
const game = new Game();
expect(game.getEmptySlots()).toMatchObject([0, 1, 2, 3, 4, 5, 6, 7, 8]);
});
it('should return true if there are still empty slots on the board', () => {
const game = new Game();
expect(game.hasEmptySlots()).toBeTruthy();
});
it('should return false if there are no empty slots on the board', () => {
const board = new Board();
const game = new Game(board);
game
.getEmptySlots()
.forEach((pos) => board.set(pos, pos % 2 ? 'o' : 'x'));
expect(game.hasEmptySlots()).toBeFalsy();
});
it('should set a given position on the board', () => {
const board = new Board();
const game = new Game(board);
game.set(0, 'o');
expect(board.has(0)).toBeTruthy();
});
it('should return true a given position is filled on the board', () => {
const board = new Board();
const game = new Game(board);
game.set(0, 'o');
expect(game.has(0)).toBeTruthy();
expect(game.has(1)).toBeFalsy();
});
it('should check if a given mark matches the last turn', () => {
const game = new Game();
game.set(0, 'o');
expect(game.isMyTurn('x')).toBeTruthy();
expect(game.isMyTurn('o')).toBeFalsy();
});
it('should not allow the same player to play twice in a row', () => {
const board = new Board();
const game = new Game(board);
game.set(0, 'o');
game.set(1, 'o');
expect(board.get(0)).toBe('o');
expect(board.get(1)).toBeNull();
});
it('should not allow a mark to be set twice in the same place', () => {
const board = new Board();
const game = new Game(board);
game.set(0, 'o');
game.set(0, 'x');
expect(board.get(0)).toBe('o');
});
it('should not allow a mark to be set if a board has a winner', () => {
const board = new Board();
const game = new Game(board);
[0, 4, 8].forEach((pos) => board.set(pos, 'o'));
game.set(2, 'x');
expect(board.get(2)).toBeNull();
});
it('should return true if the game has a winner', () => {
const board = new Board();
const game = new Game(board);
[0, 4, 8].forEach((pos) => board.set(pos, 'o'));
expect(game.hasWinner()).toBeTruthy();
});
it('should return false if the game has no winners', () => {
const board = new Board();
const game = new Game(board);
expect(game.hasWinner()).toBeFalsy();
});
it('should return the winner mark if the game has a winner', () => {
const board = new Board();
const game = new Game(board);
[0, 4, 8].forEach((pos) => board.set(pos, 'o'));
expect(game.winner()).toBe('o');
});
it('should return undefined if the game has no winners', () => {
const board = new Board();
const game = new Game(board);
expect(game.winner()).toBeFalsy();
});
});
const possibilities = {
lines: [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
],
columns: [
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
],
diagonals: [
[0, 4, 8],
[2, 4, 6],
],
};
describe('Game simulation tests', () => {
it('it should check lines', () => {
possibilities.lines.forEach((line) => {
const game = new Game();
const opponent = new GullibleOpponent(game, 'x');
opponent.blockPositions(line);
line.forEach((pos) => {
game.set(pos, 'o');
opponent.play();
});
expect(game.hasWinner()).toBeTruthy();
expect(game.winner()).toBe('o');
});
});
it('it should check columns', () => {
possibilities.columns.forEach((column) => {
const game = new Game();
const opponent = new GullibleOpponent(game, 'x');
opponent.blockPositions(column);
column.forEach((pos) => {
game.set(pos, 'o');
opponent.play();
});
expect(game.hasWinner()).toBeTruthy();
expect(game.winner()).toBe('o');
});
});
it('it should check diagonals', () => {
possibilities.diagonals.forEach((diagonal) => {
const game = new Game();
const opponent = new GullibleOpponent(game, 'x');
opponent.blockPositions(diagonal);
diagonal.forEach((pos) => {
game.set(pos, 'o');
opponent.play();
});
expect(game.hasWinner()).toBeTruthy();
expect(game.winner()).toBe('o');
});
});
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment