Created
April 17, 2020 21:29
-
-
Save olavoasantos/5953fc92dcc8344a0e4f81b4bed856d9 to your computer and use it in GitHub Desktop.
Tic Tac Toe implementation
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
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); | |
} | |
} |
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
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(); | |
} | |
} |
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
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; | |
} | |
} |
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
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; | |
} | |
} |
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
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); | |
} | |
} |
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
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