Last active
July 22, 2018 00:52
-
-
Save pringshia/8482ae82d5988744651a947b31ff6402 to your computer and use it in GitHub Desktop.
Tic Tac Toe Game
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
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