]> git.cworth.org Git - lmno-server/commitdiff
anagrams: Implement an idle counter to detect a game-end condition
authorCarl Worth <cworth@cworth.org>
Sun, 8 Mar 2026 22:33:12 +0000 (18:33 -0400)
committerCarl Worth <cworth@cworth.org>
Sun, 8 Mar 2026 22:33:12 +0000 (18:33 -0400)
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

index f84437c9e28ae9495a195f1996cfbde681a29e4d..7a6a250354ef2dd0fac9a035f4c61c08b3d9d163 100644 (file)
@@ -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",