Problem Statement
Design a command-line-based, 3x3 grid Tic Tac Toe game that can be played by 2-players.
Requirements
Board
3x3 grid board with the following naming convention for squares of the grid
A1 A2 A3
B1 B2 B3
C1 C2 C3
Players
The game has 2 players. Every player has a name and a character. The character is by default, “X” and “O” for players 1 and 2, but players can pick their own characters. Both players must not have the same character.
Game
The game starts with Player 1 inserting their character into one of the boxes
Players get alternative turns and insert their characters on boxes
On every turn, take command line input of which box to place character
After every turn show state of game
After every turn, one of 4 conditions can happen
The player specifies the wrong box (already has been used);
In this case, the same player will try againPlacing the character results in game-winning condition (specified below);
in this case, end the game and declare a winnerThe player specifies the correct box, but doesn’t win game;
Give a turn to another player and continue the gameThe board is full (this was the last entry), and no winner;
Declare game draw and end game
Winning conditions:
Same character in all boxes of:any row
Any column
Any diagonal
Data Format
Initialise Game & Players
Enter Player 1 Name:
> John
Enter Player 1 Character (X):
> ❎
Enter Player 2 Name:
> Jane
Enter Player 2 Character (O):
> 🛑
Note: If a player doesn’t enter character, default to X and O
Gameplay
Board:
_ _ _
_ _ _
_ _ _
Player 1: Enter box:
> A1
Board:
❎ _ _
_ _ _
_ _ _
Player 2: Enter box:
> A2
Board:
❎ 🛑 _
_ _ _
_ _ _
Player 2: Enter box:
>
Minimum Requirements
Make sure that you have a working and demonstrable code
Make sure that the code is functionally correct
Code should be modular and readable
Separation of concern should be addressed
Please do not write everything in a single file (if not coding in C/C++)
Code should easily accommodate new requirements and minimal changes
There should be a main method from where the code could be easily testable
Write unit tests, wherever applicable
No need to create a GUI
Optional Requirements
Please do these only if you’ve time left. You can write your code such that these can be accommodated without changing your code much.
Keep the code extensible to change the size of the grid.
Keep the code extensible to allow different types of pieces.
Keep the code extensible to allow more than 2 players/piece types.
Identifying classes, objects and relationships
A Board maintains a 2D array of strings, it has getter/setters and other utility functions relating to the board
A Player has a name and a character he will play the game with, we have a place for a player builder as the player needs to be built piecewise by sequential user inputs ie pb=new Player.Builder(); player:Player=pb.setName().setCharacter().b..
A Game has a Player and a Board, since the board too needs to be initiated piecewise we need the builder design pattern
The main.ts file uses the game builder to build both players(builder) and the board(new) and then builds the game in the process
User Flow
The Game. Builder initialises both players with their names and characters and the new board
Until the game doesn't draw or somebody wins, a prompt is shown informing which player has the active turn along with the state of the board, once the user specifies the position on the board, a move is made using the game.play()
play uses markerboard from the board instance to mark character move on the board
After each successful move, we call a function checkWinner with that active player as the context
Code
// main.ts
import { Game } from "./entities/Game";
import * as rl from 'readline-sync'
const gameBuilder = new Game.Builder()
const player1Name = rl.question("Enter Player 1 name: ")
const player1Char = rl.question("Enter Player 1 character (X): ", { defaultInput: "X" })
gameBuilder.addPlayer1(player1Name, player1Char)
const player2Name = rl.question("Enter Player 2 name: ")
const player2Char = rl.question("Enter Player 2 character (O): : ", { defaultInput: "O" })
gameBuilder.addPlayer2(player2Name, player2Char)
const game = gameBuilder.build()
while (game.state == "STARTED") {
console.log(game.nextTurnPrompt())
const box = rl.question("Enter Box: ")
game.play(box)
}
// Modes/Game.ts
import { Player } from "./Player";
import { Board } from "./Board";
type GameState = "STARTED" | "END_WINNER" | "END_DRAW"
export class Game {
p1: Player
p2: Player
board: Board
turn = 0
state: GameState = "STARTED"
private constructor(p1: Player, p2: Player, board: Board) {
this.p1 = p1;
this.p2 = p2;
this.board = board;
}
private checkWinner(player: Player): boolean {
const c = player.character
const winningLine = `${c}${c}${c}`;// assuming a 3x3 board
for (let row of ["A", "B", "C"]) {
if (this.board.getRowAsString(row) == winningLine)
return true
}
for (let col of [0,1,2]) {
if (this.board.getColAsString(col) == winningLine)
return true
}
for (let diag of [0,1]) {
if (this.board.getDiagAsString(diag) == winningLine)
return true
}
return false
}
nextTurnPrompt(): string {
const player = this.turn % 2 == 0 ? this.p1 : this.p2;
return '\n' + this.board.getBoardForDisplay()
+ '\n'
+ `Turn: ${this.turn + 1} | Player : ${player.name} (${player.character})`
}
play(box: string) {
const player = this.turn % 2 == 0 ? this.p1 : this.p2;
const success = this.board.markBoard(box, player.character)
if (success) {
if (this.checkWinner(player)) {
this.state = "END_WINNER"
console.log(`Game Over! ${player.name} has won!`)
return
}
this.turn++;
}
if (this.turn == 9) {
this.state = "END_DRAW"
console.log("Game ended in DRAW")
}
}
static Builder = class GameBuilder {
p1!: Player
p2!: Player
addPlayer1(name: string, character: string = "X"): GameBuilder {
this.p1 = new Player.Builder()
.setName(name)
.setCharacter(character)
.build()
return this;
}
addPlayer2(name: string, character: string = "O"): GameBuilder {
this.p2 = new Player.Builder()
.setName(name)
.setCharacter(character)
.build()
return this;
}
build(): Game {
if (!this.p1) {
throw new Error("You need to create player 1 before building game")
}
if (!this.p2) {
throw new Error("You need to create player 2 before building game")
}
return new Game(this.p1, this.p2, new Board())
}
}
}
// Models/Player.ts
export class Player {
name: string;
character: string;
static Builder = class PlayerBuilder {
private name!: string;
private character!: string;
setName(value: string): PlayerBuilder {
this.name = value;
return this;
}
setCharacter(value: string): PlayerBuilder {
if (value == "_") {
throw new Error("Underscore '_' is not a valid player character")
}
this.character = value;
return this;
}
build(): Player {
return new Player(this.name, this.character)
}
}
private constructor(name: string, character: string) {
this.name = name;// private constructor to enforce creation by builder
this.character = character;
}
}
// Main/Board.ts
class InvalidBoxNameError extends Error {
message = 'Invalid box identifier';
}
const BOARD_DEFAULT_SIZE = 3
const ROWS = "ABCDEFGHIJ"
export class Board {
size!: number
grid!: Array<Array<string>>
constructor(size: number = BOARD_DEFAULT_SIZE) {
this.size = size
this.grid = []
for (let i = 0; i < size; i++)
this.grid.push(new Array(size).fill("_"));
}
getBoardForDisplay(): string {
const displayRows = []
for (let row of this.grid) {
displayRows.push(row.join('\t'))
}
return displayRows.join('\n')
}
markBoard(box: string, character: string): boolean {
// box: A1 or B2 like that
if (box.length != 2) {
throw new InvalidBoxNameError()
}
const row = ROWS.indexOf(box.charAt(0))
const col = Number(box.charAt(1)) - 1
if (row < 0 || row >= this.size || col < 0 || col >= this.size) {
throw new InvalidBoxNameError()
}
if (this.grid[row][col] != "_") {
return false;//[row][col] is not empty
}
this.grid[row][col] = character
return true
}
getRowAsString(rowName: string): string {
const row = ROWS.indexOf(rowName)
if (row == -1 || row >= this.size) {
throw new Error(`row number is invalid`)
}
return this.grid[row].join("")
}
getColAsString(col: number): string {
if (col < 0 || col > this.size) {
throw new Error("col has to be between 0 and 2")
}
const colVals = []
for (let i = 0; i < this.size; i++) {
colVals.push(this.grid[i][col])
}
return colVals.join("")
}
getDiagAsString(diagNo: number): string {
const diagVals = []
if (diagNo == 0) {
for (let i = 0; i < this.size; i++) {
diagVals.push(this.grid[i][i])
}
return diagVals.join("")
} else if (diagNo == 1) {
for (let i = 0; i < this.size; i++) {
diagVals.push(this.grid[i][this.size - 1 - i])
}
return diagVals.join("")
} else {
throw new Error("Invalid diagonal")
}
}
}