Skip to content

Instantly share code, notes, and snippets.

@LearningNerd
Last active April 6, 2023 19:36
Show Gist options
  • Save LearningNerd/0f9bd58032d43e0458744cc03cdbb618 to your computer and use it in GitHub Desktop.
Save LearningNerd/0f9bd58032d43e0458744cc03cdbb618 to your computer and use it in GitHub Desktop.
Tic Tac Toe in the command line with NodeJS. Run with: node playtictactoe.js
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);
});
}
};
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");
}
// 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.
`;
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