/** * txt-game — number guessing game * Single-file Node.js HTTP server using only stdlib. * Listens on port 3000, serves HTML form UI, maintains per-session state via cookie. */ import { createServer } from "node:http"; import { randomUUID } from "node:crypto"; const PORT = 3000; const MAX_ATTEMPTS = 7; /** @type {Map} */ const sessions = new Map(); function newGame() { return { target: Math.floor(Math.random() * 100) + 1, attempts: 0, won: false, lost: false, history: [], }; } function getSession(cookies) { const match = (cookies || "").match(/sid=([a-f0-9-]{36})/); if (match) { const id = match[1]; if (sessions.has(id)) return { id, session: sessions.get(id) }; } const id = randomUUID(); const session = newGame(); sessions.set(id, session); return { id, session }; } function hint(guess, target) { if (guess < target) return "Too low ↑"; if (guess > target) return "Too high ↓"; return "Correct!"; } function clue(guess, target) { const diff = Math.abs(guess - target); if (diff === 0) return "🎯 Spot on!"; if (diff <= 5) return "🔥 Very hot"; if (diff <= 15) return "♨️ Warm"; if (diff <= 30) return "🌡️ Cool"; return "🧊 Cold"; } function renderPage(session, message, guess) { const attemptsLeft = MAX_ATTEMPTS - session.attempts; const historyRows = session.history .map( (g, i) => `${i + 1}${g}${hint(g, session.won && i === session.history.length - 1 ? g : session.target)}${clue(g, session.target)}` ) .join(""); const historySection = session.history.length > 0 ? `${historyRows}
#GuessHintTemperature
` : ""; const playAgainBtn = `
`; let formSection = ""; if (!session.won && !session.lost) { formSection = `
`; } let statusSection = ""; if (session.won) { statusSection = `

🎉 You won! The number was ${session.target}. You used ${session.attempts} attempt${session.attempts === 1 ? "" : "s"}.

${playAgainBtn}`; } else if (session.lost) { statusSection = `

💀 Game over! The number was ${session.target}.

${playAgainBtn}`; } else { statusSection = `

Attempts left: ${attemptsLeft}

`; } const messageHtml = message ? `

${message}

` : ""; return ` Number Guessing Game

🔢 Number Guessing Game

I'm thinking of a number between 1 and 100. You have ${MAX_ATTEMPTS} attempts.

${statusSection} ${messageHtml} ${formSection} ${historySection}
`; } function parseBody(req) { return new Promise((resolve) => { let body = ""; req.on("data", (chunk) => (body += chunk)); req.on("end", () => { const params = new URLSearchParams(body); resolve(Object.fromEntries(params.entries())); }); }); } function setCookie(res, id) { res.setHeader( "Set-Cookie", `sid=${id}; Path=/; HttpOnly; SameSite=Strict; Max-Age=86400` ); } const server = createServer(async (req, res) => { // Forward x-forwarded-host so URLs resolve correctly behind Traefik const fwdHost = req.headers["x-forwarded-host"]; if (fwdHost) req.headers.host = fwdHost; const { id, session } = getSession(req.headers["cookie"]); const url = req.url.split("?")[0]; if (req.method === "GET" && url === "/") { setCookie(res, id); res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); res.end(renderPage(session, "", null)); return; } if (req.method === "POST" && url === "/guess") { const body = await parseBody(req); const guess = parseInt(body.guess, 10); let message = ""; if (session.won || session.lost) { // Ignore guesses after game ends } else if (!Number.isInteger(guess) || guess < 1 || guess > 100) { message = "Please enter a number between 1 and 100."; } else { session.attempts++; session.history.push(guess); if (guess === session.target) { session.won = true; // Fix last history row hint (show "Correct!") } else if (session.attempts >= MAX_ATTEMPTS) { session.lost = true; message = `${hint(guess, session.target)} — ${clue(guess, session.target)}`; } else { message = `${hint(guess, session.target)} — ${clue(guess, session.target)}`; } } setCookie(res, id); res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); res.end(renderPage(session, message, guess)); return; } if (req.method === "POST" && url === "/reset") { const fresh = newGame(); sessions.set(id, fresh); setCookie(res, id); res.writeHead(303, { Location: "/" }); res.end(); return; } res.writeHead(404, { "Content-Type": "text/plain" }); res.end("Not found"); }); server.listen(PORT, () => { console.log(`txt-game listening on port ${PORT}`); });