Add local bot practice mode

This commit is contained in:
2026-05-08 13:22:53 -04:00
parent c26a833eda
commit 4631f9b7c5
3 changed files with 182 additions and 15 deletions

142
game.js
View File

@@ -16,6 +16,8 @@ const boardElement = document.querySelector("#board");
const messageElement = document.querySelector("#message");
const turnBadge = document.querySelector("#turnBadge");
const resetButton = document.querySelector("#resetButton");
const practiceButton = document.querySelector("#practiceButton");
const onlineButton = document.querySelector("#onlineButton");
const xCapturesElement = document.querySelector("#xCaptures");
const oCapturesElement = document.querySelector("#oCaptures");
const moveList = document.querySelector("#moveList");
@@ -30,13 +32,16 @@ let winningCells;
let socket;
let localPlayer = "";
let roomId = "";
let multiplayerState = "connecting";
let gameMode = "practice";
let multiplayerState = "local";
let botTimer = 0;
function createBoard() {
return Array.from({ length: size }, () => Array(size).fill(empty));
}
function setupGame() {
clearBotTimer();
board = createBoard();
currentPlayer = "X";
captures = { X: 0, O: 0 };
@@ -48,9 +53,14 @@ function setupGame() {
}
function connectToServer() {
gameMode = "online";
closeSocket();
localPlayer = "";
roomId = "";
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
socket = new WebSocket(`${protocol}//${window.location.host}/ws`);
setMultiplayerState("connecting", "Connecting to matchmaker...");
updateModeButtons();
socket.addEventListener("open", () => {
setMultiplayerState("waiting", "Connected. Waiting for another player...");
@@ -61,19 +71,50 @@ function connectToServer() {
handleServerMessage(payload);
});
socket.addEventListener("close", () => {
socket.addEventListener("close", (event) => {
if (event.currentTarget !== socket) {
return;
}
if (gameMode !== "online") {
return;
}
localPlayer = "";
roomId = "";
setMultiplayerState("error", "Connection closed. Refresh to reconnect.");
render();
});
socket.addEventListener("error", () => {
socket.addEventListener("error", (event) => {
if (event.currentTarget !== socket) {
return;
}
setMultiplayerState("error", "Could not reach the multiplayer server.");
render();
});
}
function startPracticeMode() {
gameMode = "practice";
closeSocket();
localPlayer = "X";
roomId = "";
multiplayerState = "local";
setupGame();
setMultiplayerState("local", "BOT practice. You are X. Random O replies after each move.");
updateModeButtons();
}
function closeSocket() {
if (socket && socket.readyState <= WebSocket.OPEN) {
socket.close();
}
socket = null;
}
function handleServerMessage(payload) {
if (payload.type === "waiting") {
localPlayer = "";
@@ -154,7 +195,7 @@ function render() {
turnBadge.className = `turn-badge player-${currentPlayer.toLowerCase()}`;
xCapturesElement.textContent = captures.X;
oCapturesElement.textContent = captures.O;
resetButton.disabled = multiplayerState !== "paired";
resetButton.disabled = gameMode === "online" && multiplayerState !== "paired";
}
function turnLabel() {
@@ -174,6 +215,10 @@ function turnLabel() {
return "Offline";
}
if (multiplayerState === "local" && currentPlayer !== localPlayer) {
return "Bot turn";
}
if (currentPlayer === localPlayer) {
return `Your turn (${localPlayer})`;
}
@@ -182,7 +227,7 @@ function turnLabel() {
}
function canPlayCell(row, col) {
return multiplayerState === "paired" &&
return (multiplayerState === "paired" || multiplayerState === "local") &&
!gameOver &&
localPlayer === currentPlayer &&
board[row][col] === empty;
@@ -204,7 +249,15 @@ function playLocalMove(row, col) {
return;
}
sendToServer({ type: "move", roomId, row, col, player: localPlayer });
if (gameMode === "online") {
sendToServer({ type: "move", roomId, row, col, player: localPlayer });
return;
}
if (!gameOver) {
setMultiplayerState("local", "Bot thinking...");
botTimer = window.setTimeout(playBotMove, 360);
}
}
function applyMove(row, col, player, source) {
@@ -223,22 +276,22 @@ function applyMove(row, col, player, source) {
if (win.length > 0) {
winningCells = win;
gameOver = true;
setMultiplayerState("paired", `${player} wins with four in a row.`);
setMultiplayerState(activeState(), `${player} wins with four in a row.`);
render();
return true;
}
if (isDraw()) {
gameOver = true;
setMultiplayerState("paired", "Draw. The board is full.");
setMultiplayerState(activeState(), "Draw. The board is full.");
render();
return true;
}
currentPlayer = opponentOf(player);
const captureMessage = captured.length > 0 ? `${player} captured ${captured.length}. ` : "";
const turnMessage = currentPlayer === localPlayer ? "Your turn." : `Waiting for ${currentPlayer}.`;
setMultiplayerState("paired", `${captureMessage}${turnMessage}`);
const turnMessage = currentPlayer === localPlayer ? "Your turn." : waitingMessage();
setMultiplayerState(activeState(), `${captureMessage}${turnMessage}`);
render();
flashCapturedCells(captured);
@@ -249,6 +302,14 @@ function applyMove(row, col, player, source) {
return true;
}
function activeState() {
return gameMode === "practice" ? "local" : "paired";
}
function waitingMessage() {
return gameMode === "practice" ? "Bot turn." : `Waiting for ${currentPlayer}.`;
}
function captureFrom(row, col, player) {
const opponent = opponentOf(player);
const captured = [];
@@ -340,6 +401,46 @@ function setMultiplayerState(state, text) {
messageElement.textContent = text;
}
function playBotMove() {
if (gameMode !== "practice" || gameOver || currentPlayer !== "O") {
return;
}
const moves = legalMoves();
if (moves.length === 0) {
return;
}
const move = moves[Math.floor(Math.random() * moves.length)];
applyMove(move.row, move.col, "O", "bot");
}
function legalMoves() {
const moves = [];
for (let row = 0; row < size; row += 1) {
for (let col = 0; col < size; col += 1) {
if (board[row][col] === empty) {
moves.push({ row, col });
}
}
}
return moves;
}
function clearBotTimer() {
if (botTimer) {
window.clearTimeout(botTimer);
botTimer = 0;
}
}
function updateModeButtons() {
practiceButton.classList.toggle("active", gameMode === "practice");
onlineButton.classList.toggle("active", gameMode === "online");
}
function sendToServer(payload) {
if (!socket || socket.readyState !== WebSocket.OPEN) {
setMultiplayerState("error", "Connection lost. Refresh to reconnect.");
@@ -363,14 +464,29 @@ function flashCapturedCells(captured) {
}
resetButton.addEventListener("click", () => {
if (multiplayerState !== "paired") {
if (gameMode === "online" && multiplayerState !== "paired") {
return;
}
setupGame();
if (gameMode === "practice") {
setMultiplayerState("local", "BOT practice. You are X. Random O replies after each move.");
return;
}
setMultiplayerState("paired", `Game reset. You are ${localPlayer}. ${currentPlayer} plays first.`);
sendToServer({ type: "reset", roomId });
});
setupGame();
connectToServer();
practiceButton.addEventListener("click", startPracticeMode);
onlineButton.addEventListener("click", () => {
gameMode = "online";
localPlayer = "";
roomId = "";
multiplayerState = "connecting";
setupGame();
connectToServer();
});
startPracticeMode();