Last active
September 7, 2023 22:06
-
-
Save RodEsp/b350d82731dc77afff2a4d14800841ad to your computer and use it in GitHub Desktop.
Interactive tic-tac-toe in a terminal
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 clearScreen = () => { | |
process.stdout.cursorTo(0, 0); | |
process.stdout.clearScreenDown(); | |
}; | |
clearScreen(); | |
console.log('Welcome to TicTacToe!'); | |
console.log('Please select your opponent:'); | |
console.log(' 1. Human'); | |
console.log(' 2. Computer'); | |
process.stdin.setRawMode(true); | |
process.stdin.setEncoding('utf8'); | |
process.stdin.on('data', function (key) { | |
if (key === '1' || key === '2') { | |
opponent = key === '1' ? 'human' : 'computer'; | |
process.stdin.removeAllListeners('data'); | |
process.stdin.on('data', function (key) { | |
switch (key) { | |
case '\u001B\u005B\u0041': // Arrow up | |
moveCursor('up'); | |
break; | |
case '\u001B\u005B\u0042': // Arrow down | |
moveCursor('down'); | |
break; | |
case '\u001B\u005B\u0043': // Arrow right | |
moveCursor('right'); | |
break; | |
case '\u001B\u005B\u0044': // Arrow left | |
moveCursor('left'); | |
break; | |
case '\u000D': // Carrige Return | |
if (squareIsEmpty()) { | |
updateBoardState(); | |
checkForWinner(); | |
switchPlayer(); | |
renderBoard(); | |
} else { | |
renderBoard('That square is already taken. Try again.'); | |
} | |
break; | |
case '\u0003': // Ctrl + C | |
clearScreen(); | |
process.exit(); | |
break; | |
default: | |
break; | |
} | |
}); | |
renderBoard(); | |
} | |
}); | |
let opponent = undefined; | |
let winner = undefined; | |
let currentPlayer = { | |
name: 'Player 1', | |
mark: 'X', | |
}; | |
const cursorBoardPosition = { | |
row: 0, | |
column: 0, | |
}; | |
const boardState = [ | |
[' ', ' ', ' '], | |
[' ', ' ', ' '], | |
[' ', ' ', ' '], | |
]; | |
const renderBoard = (message) => { | |
clearScreen(); | |
console.log(boardState[0].join('|')); | |
console.log(`—————`); | |
console.log(boardState[1].join('|')); | |
console.log(`—————`); | |
console.log(boardState[2].join('|')); | |
if (!winner) { | |
console.log(`\n${message ? `${message}\n` : ''}${currentPlayer.name}'s turn.`); | |
console.log('\nUse the arrow keys to move the cursor and press enter to place your mark.'); | |
process.stdout.cursorTo(cursorBoardPosition.column * 2, cursorBoardPosition.row * 2); | |
} else { | |
if (winner === 'tie') { | |
console.log(`\nIt's a tie!`); | |
} else { | |
console.log(`\n${winner} wins!`); | |
} | |
process.exit(); | |
} | |
}; | |
const moveCursor = (direction) => { | |
switch (direction) { | |
case 'up': | |
if (cursorBoardPosition.row > 0) { | |
cursorBoardPosition.row -= 1; | |
process.stdout.moveCursor(0, -2); | |
} | |
break; | |
case 'right': | |
if (cursorBoardPosition.column < 2) { | |
cursorBoardPosition.column += 1; | |
process.stdout.moveCursor(2, 0); | |
} | |
break; | |
case 'down': | |
if (cursorBoardPosition.row < 2) { | |
cursorBoardPosition.row += 1; | |
process.stdout.moveCursor(0, 2); | |
} | |
break; | |
case 'left': | |
if (cursorBoardPosition.column > 0) { | |
cursorBoardPosition.column -= 1; | |
process.stdout.moveCursor(-2, 0); | |
} | |
break; | |
} | |
}; | |
const updateBoardState = () => { | |
boardState[cursorBoardPosition.row][cursorBoardPosition.column] = currentPlayer.mark; | |
}; | |
const squareIsEmpty = () => { | |
return boardState[cursorBoardPosition.row][cursorBoardPosition.column] === ' '; | |
}; | |
const checkForWinner = () => { | |
const mark = currentPlayer.mark; | |
const rowsWin = () => { | |
return boardState[0].every((square) => square === mark) || boardState[1].every((square) => square === mark) || boardState[2].every((square) => square === mark); | |
}; | |
const columnsWin = () => { | |
return ( | |
(boardState[0][0] === mark && boardState[1][0] === mark && boardState[2][0] === mark) || | |
(boardState[0][1] === mark && boardState[1][1] === mark && boardState[2][1] === mark) || | |
(boardState[0][2] === mark && boardState[1][2] === mark && boardState[2][2] === mark) | |
); | |
}; | |
const diagonalsWin = () => { | |
return (boardState[0][0] === mark && boardState[1][1] === mark && boardState[2][2] === mark) || (boardState[0][2] === mark && boardState[1][1] === mark && boardState[2][0] === mark); | |
}; | |
if (rowsWin() || columnsWin() || diagonalsWin()) { | |
winner = currentPlayer.name; | |
} else if (boardState.every((row) => row.every((square) => square !== ' '))) { // Check for tie | |
winner = 'tie'; | |
} | |
}; | |
const switchPlayer = () => { | |
if (currentPlayer.name === 'Player 1') { | |
if (opponent === 'human') { | |
currentPlayer = { name: 'Player 2', mark: 'O' }; | |
} else if (opponent === 'computer') { | |
currentPlayer = { name: 'TicBot', mark: 'O' }; | |
// Bot player will make a move with a small delay to make it seem like it's thinking | |
setTimeout(() => { | |
while (squareIsEmpty() === false) { | |
cursorBoardPosition.row = Math.floor(Math.random() * 3); | |
cursorBoardPosition.column = Math.floor(Math.random() * 3); | |
} | |
updateBoardState(); | |
checkForWinner(); | |
switchPlayer(); | |
renderBoard(); | |
}, 300) | |
} | |
} else { | |
currentPlayer = { name: 'Player 1', mark: 'X' }; | |
} | |
}; | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment