From 7fe2800b907ceba5335c953383b12679a1b0932b Mon Sep 17 00:00:00 2001 From: Carl Worth Date: Sun, 8 Mar 2026 18:33:12 -0400 Subject: [PATCH] anagrams: Implement an idle counter to detect a game-end condition The idle counter doesn't start until the bag is empty. After that, any client post resets the counter. Once the counter expires the servers sends a game-ending event to all clients. If they don't reply that the user is still looking, then the game will be ended. --- anagrams.js | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/anagrams.js b/anagrams.js index f84437c..7a6a250 100644 --- a/anagrams.js +++ b/anagrams.js @@ -9,6 +9,8 @@ const VOTE_TIMEOUT_MS = 15000; /* 15 seconds to vote */ const REVEAL_COUNTDOWN_MS = 1500; /* 1.5 seconds to reveal a tile */ const MIN_CENTER_TILES = 7; /* auto-deal to maintain this many */ const MIN_WORD_LENGTH = 4; +const IDLE_TIMEOUT_MS = 120000; /* 2 minutes of inactivity before warning */ +const IDLE_COUNTDOWN_MS = 10000; /* 10 seconds to respond to warning */ class Anagrams extends Game { constructor(id) { @@ -39,6 +41,8 @@ class Anagrams extends Game { this._vote_timer = null; this._revealing = false; this._reveal_timer = null; + this._idle_timer = null; + this._idle_countdown_timer = null; } /***************************************************** @@ -698,12 +702,63 @@ class Anagrams extends Game { _finish_game() { this.state.finished = true; + this._clear_idle_timers(); this.broadcast_event_object("game-over", { scores: this._compute_all_scores() }); } + /***************************************************** + * Idle timeout (bag empty only) * + *****************************************************/ + + _reset_idle_timer() { + if (!this.state.started || this.state.finished) return; + if (this.state.bag.length > 0) return; + + this._clear_idle_timers(); + this._idle_timer = setTimeout(() => { + this._idle_timer = null; + this._start_idle_countdown(); + }, IDLE_TIMEOUT_MS); + } + + _start_idle_countdown() { + this.broadcast_event_object("game-ending", { + countdown_ms: IDLE_COUNTDOWN_MS + }); + + this._idle_countdown_timer = setTimeout(() => { + this._idle_countdown_timer = null; + this._finish_game(); + }, IDLE_COUNTDOWN_MS); + } + + handle_still_looking(request, response) { + const session_id = request.session.id; + if (!this.state.player_words[session_id]) { + response.sendStatus(400); + return; + } + + this._clear_idle_timers(); + this.broadcast_event_object("game-continued", {}); + this._reset_idle_timer(); + response.json({ ok: true }); + } + + _clear_idle_timers() { + if (this._idle_timer) { + clearTimeout(this._idle_timer); + this._idle_timer = null; + } + if (this._idle_countdown_timer) { + clearTimeout(this._idle_countdown_timer); + this._idle_countdown_timer = null; + } + } + _all_player_words() { const result = {}; for (const sid of Object.keys(this.state.player_words)) { @@ -780,6 +835,13 @@ class Anagrams extends Game { })}\n\n`); } + /* Send game-ending countdown if one is active. */ + if (this._idle_countdown_timer) { + response.write(`event: game-ending\ndata: ${JSON.stringify({ + countdown_ms: IDLE_COUNTDOWN_MS + })}\n\n`); + } + /* Send game-over if finished. */ if (this.state.finished) { response.write(`event: game-over\ndata: ${JSON.stringify({ @@ -792,6 +854,12 @@ class Anagrams extends Game { Anagrams.router = express.Router(); const router = Anagrams.router; +/* Reset idle timer on any player action. */ +router.post('*', (request, response, next) => { + if (request.game) request.game._reset_idle_timer(); + next(); +}); + router.post('/start', (request, response) => { request.game.handle_start(request, response); }); @@ -836,6 +904,10 @@ router.post('/done', (request, response) => { request.game.handle_done(request, response); }); +router.post('/still-looking', (request, response) => { + request.game.handle_still_looking(request, response); +}); + Anagrams.meta = { name: "Anagrams", identifier: "anagrams", -- 2.45.2