Created
January 15, 2024 19:32
-
-
Save iamwilhelm/3ff3fbd6b570f63c000f0211453b6c3f to your computer and use it in GitHub Desktop.
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
// tic tac toe | |
import * as readline from "readline"; | |
enum Turn { | |
Player = "player", | |
Computer = "computer", | |
} | |
type Piece = string; | |
type Board = Piece[][]; | |
type GameState = { | |
turn: Turn; | |
board: Board; | |
}; | |
type GameInput = { | |
piece: Piece; | |
position: [number, number] | null; // row, col | |
}; | |
function initializeBoard(): Board { | |
return [ | |
[" ", " ", " "], | |
[" ", " ", " "], | |
[" ", " ", " "], | |
]; | |
} | |
function winner(gameState: GameState): Piece | null { | |
// check for winner rows | |
for (const piece of ["O", "X"]) { | |
for (const row of gameState.board) { | |
if (row.every((col) => col === piece)) return piece; | |
} | |
} | |
// check for col rows | |
for (const piece of ["O", "X"]) { | |
for (const col of [0, 1, 2]) { | |
if ( | |
gameState.board[0][col] === piece && | |
gameState.board[1][col] === piece && | |
gameState.board[2][col] === piece | |
) | |
return piece; | |
} | |
} | |
// check for diagonals | |
for (const piece of ["O", "X"]) { | |
if ( | |
gameState.board[0][0] === piece && | |
gameState.board[1][1] === piece && | |
gameState.board[2][2] === piece | |
) | |
return piece; | |
if ( | |
gameState.board[0][2] === piece && | |
gameState.board[1][1] === piece && | |
gameState.board[2][0] === piece | |
) | |
return piece; | |
} | |
return null; | |
} | |
function isTie(gameState: GameState): boolean { | |
return gameState.board.every((row) => { | |
return row.every((col) => { | |
return col !== " "; | |
}); | |
}); | |
} | |
function isValidInput(gameState: GameState, input: GameInput): boolean { | |
if (input.position === null) return false; | |
if (input.position[0] < 0 || input.position[0] > 2) return false; | |
if (input.position[1] < 0 || input.position[1] > 2) return false; | |
if (gameState.board[input.position[0]][input.position[1]] !== " ") | |
return false; | |
return true; | |
} | |
const rl = readline.createInterface({ | |
input: process.stdin, | |
output: process.stdout, | |
}); | |
rl.on("SIGINT", () => { | |
console.log("quitting..."); | |
rl.close(); | |
process.exit(); | |
}); | |
async function getPlayerInput(gameState: GameState): Promise<GameInput> { | |
let input: GameInput = { | |
piece: "O", | |
position: null, | |
}; | |
return new Promise<GameInput>(async (resolve) => { | |
// keep asking until we get a valid move | |
while (!isValidInput(gameState, input)) { | |
input = await new Promise<GameInput>((resolve) => { | |
rl.question("Please enter your move: ", (answer) => { | |
const gameInput = { | |
piece: "O", | |
position: answer.split(",").map(Number) as [number, number], | |
}; | |
resolve(gameInput); | |
}); | |
}); | |
} | |
resolve(input); | |
}); | |
} | |
function getAiInput(gameState: GameState): GameInput { | |
const validMoves = gameState.board | |
.flatMap((row, row_idx) => { | |
return row.map((col, col_idx) => { | |
return col === " " ? [row_idx, col_idx] : null; | |
}); | |
}) | |
.filter((move) => move !== null); | |
const randIdx = Math.floor(Math.random() * validMoves.length); | |
return { | |
piece: "X", | |
position: validMoves[randIdx], | |
}; | |
} | |
function updateGameState(gameState: GameState, input: GameInput): GameState { | |
if (input.position === null) return gameState; | |
gameState.board[input.position[0]][input.position[1]] = input.piece; | |
return gameState; | |
} | |
function render(gameState: GameState) { | |
console.log(" 0 1 2"); | |
console.log("0 ", gameState.board[0].join(" | ")); | |
console.log(" ---------"); | |
console.log("1 ", gameState.board[1].join(" | ")); | |
console.log(" ---------"); | |
console.log("2 ", gameState.board[2].join(" | ")); | |
} | |
async function gameloop() { | |
// setup | |
let gameState = { | |
turn: Turn.Player, | |
board: initializeBoard(), | |
}; | |
render(gameState); | |
while (winner(gameState) === null && !isTie(gameState)) { | |
// get player or ai input based on turn | |
let input; | |
if (gameState.turn === Turn.Player) { | |
input = await getPlayerInput(gameState); | |
console.log(`Player move (${input.piece}) at ${input.position}`); | |
gameState.turn = Turn.Computer; | |
} else { | |
input = getAiInput(gameState); | |
console.log(`Computer move (${input.piece}) at ${input.position}`); | |
gameState.turn = Turn.Player; | |
} | |
// change game state | |
gameState = updateGameState(gameState, input); | |
// render game state | |
render(gameState); | |
} | |
if (winner(gameState)) { | |
console.log("Winner is: ", winner(gameState), "!"); | |
} else { | |
console.log("Tie!"); | |
} | |
} | |
await gameloop(); | |
console.log("game ended"); | |
process.exit(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment