Skip to content

Instantly share code, notes, and snippets.

@pringshia
Last active July 22, 2018 00:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pringshia/8482ae82d5988744651a947b31ff6402 to your computer and use it in GitHub Desktop.
Save pringshia/8482ae82d5988744651a947b31ff6402 to your computer and use it in GitHub Desktop.
Tic Tac Toe Game
const readline = require("readline");
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const initialGameState = {
board: [[0, 0, 0], [0, 0, 0], [0, 0, 0]],
currentPlayer: "X"
};
// Table of Code Contents:
// 1. Utils
// 2. Game methods
// - updateBoard(gameState)(coords) => returns a new gameState with a move at the updated coords
// - validate(gameState) => returns helper methods for validating and transforming user input
// - display(gameState) => prints out the board to console
// - analyze(gameState) => determine if there is a winner
// - checkWinner(board) => has anyone won the game? returns 'X', 'O', 'None' for ties, or null
// - playGameTurn(gameState) => main game loop that is run recursively
const utils = {
deepClone: obj => JSON.parse(JSON.stringify(obj)),
clearTerminal: () => {
const blank = "\n".repeat(process.stdout.rows);
console.log(blank);
readline.cursorTo(process.stdout, 0, 0);
readline.clearScreenDown(process.stdout);
},
map: {
dataToString: data => (data === 10 ? "X" : data === 1 ? "O" : " "),
stringToData: string => (string === "X" ? 10 : string === "O" ? 1 : 0),
rowCodeToIndex: rowCode => rowCode.toUpperCase().charCodeAt(0) - 65,
colCodeToIndex: colCode => {
const parsedCode = parseInt(colCode, 10);
if (isNaN(parsedCode)) {
throw new Error("Invalid move. There is no column " + colCode);
}
return parsedCode - 1;
},
indexToRowCode: index => String.fromCharCode(65 + index),
indexToColCode: colCode => colCode + 1 + ""
}
};
const updateBoard = state => ([x, y]) => {
const newBoard = utils.deepClone(state.board);
newBoard[x][y] = utils.map.stringToData(state.currentPlayer);
return {
board: newBoard,
currentPlayer: state.currentPlayer === "X" ? "O" : "X"
};
};
const validate = gameState => ({
move: string => {
let [x, y] = string.split("");
if (x === undefined)
throw new Error(
"No row specified. Try again. (Example: 'A1' for row A column 1)"
);
if (y === undefined)
throw new Error(
"No column specified. Try again. (Example: 'A1' for row A column 1)"
);
const mappedX = utils.map.rowCodeToIndex(x);
if (mappedX >= gameState.board.length || mappedX < 0)
throw new Error("Invalid move. There is no row: " + x);
const mappedY = utils.map.colCodeToIndex(y);
if (mappedY >= gameState.board.length || mappedY < 0)
throw new Error("Invalid move. There is no column: " + y);
if (gameState.board[mappedX][mappedY] !== 0)
throw new Error("Invalid move. Someone already played there.");
return [utils.map.rowCodeToIndex(x), utils.map.colCodeToIndex(y)];
}
});
const display = gameState => {
console.log(
" " +
gameState.board[0]
.map((_, idx) => utils.map.indexToColCode(idx))
.join(" ") +
"\r\n"
);
gameState.board.forEach((row, rNum) => {
process.stdout.write(utils.map.indexToRowCode(rNum) + " ");
row.forEach((col, cNum) => {
process.stdout.write(utils.map.dataToString(col));
if (cNum < row.length - 1) {
process.stdout.write("|");
}
});
process.stdout.write("\r\n");
if (rNum < gameState.board.length - 1) {
process.stdout.write(" -+-+-\r\n");
}
});
};
const analyze = gameState => {
return {
hasWinner: checkWinner(gameState.board)
};
};
// returns "X", "O", "None" if tie, or null if no winner yet
const checkWinner = board => {
const doAdd = (a, b) => a + b;
const sumsToWin = sum => {
return sum === 10 * board.length
? "X"
: sum === 1 * board.length
? "O"
: null;
};
let winner = null;
// check all rows
board.forEach(row => {
const sum = row.reduce(doAdd, 0);
winner = sumsToWin(sum) || winner;
});
// console.log("checked all rows", winner);
if (winner) return winner;
// check all cols
board[0].map((_, idx) => {
const sum = board.map(row => row[idx]).reduce(doAdd, 0);
winner = sumsToWin(sum) || winner;
});
// console.log("checked all cols", winner);
if (winner) return winner;
// check tlbr diagonal
const tlbrSum = board.map((_, idx) => board[idx][idx]).reduce(doAdd, 0);
winner = sumsToWin(tlbrSum) || winner;
// console.log("checked tlbr diag", winner);
if (winner) return winner;
// check bltr diagonal
const bltrSum = board
.map((_, idx) => board[board.length - 1 - idx][idx])
.reduce(doAdd, 0);
winner = sumsToWin(bltrSum) || winner;
// console.log("checked bltr diag", winner);
if (winner) return winner;
if (!winner) {
let isFull = true;
board.forEach(row => {
row.forEach(col => {
if (col === 0) isFull = false;
});
});
if (isFull) winner = "None";
}
return winner;
};
const playGameTurn = (gameState, flashMessage = null) => {
utils.clearTerminal();
display(gameState);
const gameAnalysis = analyze(gameState);
if (flashMessage) {
console.log("\r\n" + flashMessage + "\r\n");
}
if (gameAnalysis.hasWinner) {
console.log("\r\nWinner: " + gameAnalysis.hasWinner);
rl.close();
return;
}
console.log();
rl.question(
`[TURN: ${
gameState.currentPlayer
}] Where would you like to move (e.g. B2)? `,
answer => {
try {
const requestMove = validate(gameState).move(answer);
gameState = updateBoard(gameState)(requestMove);
playGameTurn(gameState);
} catch (e) {
playGameTurn(gameState, e);
}
}
);
};
playGameTurn(initialGameState);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment