-
-
Save andrew-t-james/cba38ad5df78bd2a1a5c847457b1804b to your computer and use it in GitHub Desktop.
Tic Tac Toe in the command line with NodeJS. Run with: node playtictactoe.js
This file contains hidden or 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
module.exports = class TicTacToe { | |
constructor() { | |
this.board = [ | |
["", "", ""], | |
["", "", ""], | |
["", "", ""] | |
]; | |
this.currentPlayer = "X"; | |
this.lastMove; // keep track of most recent move, for hasWon() check | |
} | |
// Alternate players X and O | |
switchTurn() { | |
this.currentPlayer = this.currentPlayer === "X" ? "O" : "X"; | |
} | |
// Given a move object {row: 1, col: 0}, return true if that cell is open | |
isCellFree(move) { | |
return this.board[move.row][move.col] === ""; | |
} | |
// Given a move object {row: 1, col: 0}, set that location on the board to contain the current player's symbol | |
playerMove(move) { | |
this.board[move.row][move.col] = this.currentPlayer; | |
} | |
// Given the most recent move, check if that makes a win! | |
hasWon(move) { | |
// Concatenate current move's row and column and the 2 diagonals | |
let possibleWins = [ | |
this.board[move.row][0] + this.board[move.row][1] + this.board[move.row][2], | |
this.board[0][move.col] + this.board[1][move.col] + this.board[2][move.col], | |
this.board[0][0] + this.board[1][1] + this.board[2][2], | |
this.board[2][0] + this.board[1][1] + this.board[0][2] | |
]; | |
return possibleWins.some( (str) => str === "XXX" || str === "OOO"); | |
} | |
isBoardFull() { | |
// Return false as soon as a cell with "" value is found; otherwise, return true | |
for (let row = 0; row < this.board.length; row++) { | |
for (let col = 0; col < this.board[row].length; col++) { | |
if (this.board[row][col] === "") return false | |
} | |
} | |
return true; | |
} | |
// Display board in the console (for testing) | |
log() { | |
this.board.forEach( (row, index) => { | |
console.log(row); | |
}); | |
} | |
}; |
This file contains hidden or 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
const TicTacToe = require('./gamelogic.js'); | |
test("Game initialized", () => { | |
const game = new TicTacToe(); | |
console.log("Initial board: "); | |
game.log(); | |
console.log("Initial player: " + game.currentPlayer); | |
return game.board && game.currentPlayer === "X"; | |
}); | |
test("switchTurn alternates X and O", () => { | |
const game = new TicTacToe(); | |
console.log("Initial player: " + game.currentPlayer); | |
game.switchTurn(); | |
console.log("Next player: " + game.currentPlayer); | |
if (game.currentPlayer !== "O") return false; | |
game.switchTurn(); | |
console.log("Next player: " + game.currentPlayer); | |
return game.currentPlayer === "X"; | |
}); | |
test("playerMove updates board at {move} with currentPlayer", () => { | |
const game = new TicTacToe(); | |
console.log("Initial board: "); | |
game.log(); | |
console.log("Initial player: " + game.currentPlayer); | |
let move = {row: 1, col: 0}; | |
console.log("Move: ", move); | |
game.playerMove(move); | |
game.log(); | |
if (game.board[1][0] !== "X") return false; | |
game.switchTurn(); | |
console.log("Next player: " + game.currentPlayer); | |
move = {row: 2, col: 2}; | |
console.log("Move: ", move); | |
game.playerMove(move); | |
game.log(); | |
return game.board[2][2] === "O"; | |
}); | |
test("top row of Xs win", () => { | |
const game = new TicTacToe(); | |
console.log("Initial board: "); | |
game.log(); | |
console.log("Initial player: " + game.currentPlayer); | |
let hasWon = false; | |
let moves = [ | |
{row: 0, col: 0}, | |
{row: 0, col: 1}, | |
{row: 0, col: 2} | |
]; | |
moves.forEach( (move) => { | |
game.playerMove(move); | |
game.log(); | |
hasWon = game.hasWon(move); | |
console.log("hasWon: " + hasWon); | |
}); | |
return hasWon; // should be true! | |
}); | |
test("middle col of Os win", () => { | |
const game = new TicTacToe(); | |
console.log("Initial board: "); | |
game.log(); | |
game.switchTurn(); | |
console.log("Switch turn to player: " + game.currentPlayer); | |
let hasWon = false; | |
let moves = [ | |
{row: 0, col: 1}, | |
{row: 1, col: 1}, | |
{row: 2, col: 1} | |
]; | |
moves.forEach( (move) => { | |
game.playerMove(move); | |
game.log(); | |
hasWon = game.hasWon(move); | |
console.log("hasWon: " + hasWon); | |
}); | |
return hasWon; // should be true! | |
}); | |
test("diagonal 1 of Xs win", () => { | |
const game = new TicTacToe(); | |
console.log("Initial board: "); | |
game.log(); | |
console.log("Initial player: " + game.currentPlayer); | |
let hasWon = false; | |
let moves = [ | |
{row: 0, col: 0}, | |
{row: 1, col: 1}, | |
{row: 2, col: 2} | |
]; | |
moves.forEach( (move) => { | |
game.playerMove(move); | |
game.log(); | |
hasWon = game.hasWon(move); | |
console.log("hasWon: " + hasWon); | |
}); | |
return hasWon; // should be true! | |
}); | |
test("diagonal 2 of Os win", () => { | |
const game = new TicTacToe(); | |
game.log(); | |
game.switchTurn(); | |
console.log("Switch turn to player: " + game.currentPlayer); | |
let hasWon = false; | |
let moves = [ | |
{row: 0, col: 2}, | |
{row: 1, col: 1}, | |
{row: 2, col: 0} | |
]; | |
moves.forEach( (move) => { | |
game.playerMove(move); | |
game.log(); | |
hasWon = game.hasWon(move); | |
console.log("hasWon: " + hasWon); | |
}); | |
return hasWon; // should be true! | |
}); | |
test("stalemate: hasWon is false, isBoardFull is true", () => { | |
const game = new TicTacToe(); | |
let hasWon = false; | |
let isBoardFull = false; | |
// Moves alternate between players: | |
let moves = [ | |
{row: 0, col: 0}, | |
{row: 1, col: 1}, | |
{row: 1, col: 0}, | |
{row: 2, col: 0}, | |
{row: 0, col: 2}, | |
{row: 0, col: 1}, | |
{row: 2, col: 1}, | |
{row: 1, col: 2}, | |
{row: 2, col: 2} | |
]; | |
moves.forEach( (move) => { | |
game.playerMove(move); | |
// console.log("Move: ", move); | |
// game.log(); | |
hasWon = game.hasWon(move); | |
isBoardFull = game.isBoardFull(); | |
// console.log("hasWon: " + hasWon); | |
// console.log("isBoardFull: " + isBoardFull); | |
if (hasWon || isBoardFull) return false; // expect no win, board not full! | |
game.switchTurn(); // this time, switch turns after each move! | |
}); | |
console.log("hasWon: " + hasWon); | |
console.log("isBoardFull: " + isBoardFull); | |
game.log(); | |
return !hasWon && isBoardFull; // hasWon should be false, isBoardFull should be true! | |
}); | |
// 5 sec testing library: | |
function test(description, testFunction) { | |
console.log(description); | |
console.log("---------------------------------"); | |
if (!testFunction()) { | |
console.log("*********** FAIL! ***********"); | |
} | |
console.log("======================================\n"); | |
} |
This file contains hidden or 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
// MESSAGES TO DISPLAY IN THE CONSOLE | |
exports.CELL_FULL = "Oops, that cell on the board is already taken! Please enter a valid move (Example: 0, 1): "; | |
exports.INVALID_MOVE = "Sorry, didn't understand that move! Please enter a row and column from 0 to 2, separated by a comma. (Example: 2,1): "; | |
exports.getNextMovePrompt = (currentPlayer) => `Player ${currentPlayer}, please enter your move: `; | |
exports.START_MSG = ` | |
~ * ~ * ~ * ~ * Let's play Tic Tac Toe! * ~ * ~ * ~ * ~ | |
This is a two-player game, so bring a friend and let's begin! | |
------------------------------------------------- | |
INSTRUCTIONS: | |
- Choose your move by typing a row and column (0, 1 or 2) separated by a comma. | |
Example: 0,2 or 1,1 | |
- Press the ENTER key to make your move. | |
(If you don't know how to play Tic Tac Toe, Google "Tic Tac Toe rules"!) | |
------------------------------------------------- | |
Player X starts! | |
Player X, please enter your first move:`; | |
exports.getWinMsg = (currentPlayer) => ` | |
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
\tPlayer ${currentPlayer} wins!!!!! ᕕ( ᐛ )ᕗ | |
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
That's game over. GG! Run 'node playtictactoe.js' to play again. | |
`; | |
exports.STALEMATE_MSG = ` | |
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
\tIt's a stalemate! Nobody won! ¯\\_(ツ)_/¯ | |
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
That's game over. GG! Run 'node playtictactoe.js' to play again. | |
`; |
This file contains hidden or 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
const TicTacToe = require('./gamelogic.js'); | |
const messages = require('./messages.js'); | |
const game = new TicTacToe(); | |
process.stdin.setEncoding('utf8'); | |
process.stdin.on('error', (error) => { | |
console.log('error: ' + error); | |
}); | |
// Handle each chunk of user input | |
// NOTE: Pretty sure there are edge cases here I'm not handling! | |
// TODO: Should probably use a module like readline or similar | |
process.stdin.on('data', updateGame); | |
// START THE GAME! Print initial instructions: | |
console.log(messages.START_MSG); | |
function updateGame(data) { | |
let input = data.toString().trim().toUpperCase(); | |
// Quit game early if requested: | |
if (input === "QUIT") { | |
process.exit(); | |
} | |
// Parse and validate the user's move | |
let move = parseMove(input); | |
if (move === false) { | |
console.log(messages.INVALID_MOVE); | |
return; | |
} | |
if (!game.isCellFree(move)) { | |
console.log(messages.CELL_FULL); | |
return; | |
} | |
// Update game board with valid move and display the board | |
game.playerMove(move); | |
game.log(); | |
// If somebody won, show msg and quit! | |
if (game.hasWon(move)) { | |
console.log(messages.getWinMsg(game.currentPlayer)); | |
process.exit(); | |
return; | |
} | |
// Stalemate if board is full but nobody won. | |
// Show msg and quit! | |
if (game.isBoardFull()) { | |
console.log(messages.STALEMATE_MSG); | |
process.exit(); | |
return; | |
} | |
// If game is still continuing, switch turns and prompt for next move: | |
game.switchTurn(); | |
console.log(messages.getNextMovePrompt(game.currentPlayer)); | |
} | |
// Given a string like "1,2" validate and return {row:1, col:2} | |
function parseMove(input) { | |
// Input must be a number from 0 to 2 | |
let inputArray = input.split(","); | |
let row = validateCoordinate(inputArray[0]); | |
let col = validateCoordinate(inputArray[1]); | |
if (row === false || col === false) return false; | |
return {row: row, col: col}; | |
} | |
// Row/col coordinates must be a number from 0 to 2 | |
function validateCoordinate(num) { | |
let coordinate = parseInt(num, 10); | |
if (isNaN(coordinate) || coordinate < 0 || coordinate > 2) { | |
return false; | |
} | |
return coordinate; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment