]> git.cworth.org Git - lmno-server/commitdiff
Implement better support for a queue of claimants
authorCarl Worth <cworth@cworth.org>
Mon, 9 Mar 2026 15:34:02 +0000 (08:34 -0700)
committerCarl Worth <cworth@cworth.org>
Mon, 9 Mar 2026 15:34:02 +0000 (08:34 -0700)
And when sending an event to the clients to end one claim, indicate in
that event the next claimant so that the client can transition from
one to the next cleanly without a tranient "no claim in progress"
state.

anagrams.js

index db8d23a8c92e5b0153089b912713d22430ed04e1..51326b060b5a4e2bca7bd3fb27d0f8e9e6d4b012 100644 (file)
@@ -144,7 +144,15 @@ class Anagrams extends Game {
 
     /* Don't allow if already in queue. */
     if (this.state.claim_queue.includes(session_id)) {
-      response.json({ queued: true, active: session_id === this.active_claimer_session() });
+      const pos = this.state.claim_queue.indexOf(session_id);
+      const active_session = this.active_claimer_session();
+      const active_player = this.players_by_session[active_session];
+      response.json({
+        queued: true,
+        active: session_id === active_session,
+        position: pos,
+        active_player: active_player ? active_player.name : null
+      });
       return;
     }
 
@@ -152,33 +160,41 @@ class Anagrams extends Game {
 
     /* If this is the only person in queue, activate them. */
     if (this.state.claim_queue.length === 1) {
-      this._activate_claimer();
-    } else {
-      /* Let them know they're queued. */
-      const player = this.players_by_session[session_id];
-      if (player) {
-        player.send(`event: claim-queued\ndata: ${JSON.stringify({
-          position: this.state.claim_queue.indexOf(session_id)
-        })}\n\n`);
-      }
+      this._activate_claimer(true);
     }
 
-    response.json({ queued: true, active: session_id === this.active_claimer_session() });
+    const pos = this.state.claim_queue.indexOf(session_id);
+    const active_session = this.active_claimer_session();
+    const active_player = this.players_by_session[active_session];
+    response.json({
+      queued: true,
+      active: session_id === active_session,
+      position: pos,
+      active_player: active_player ? active_player.name : null
+    });
   }
 
-  _activate_claimer() {
+  /* Set up the active claimer's state and timers. If broadcast is
+   * true, send a standalone claim-start event (used for the first
+   * claim when no prior claim is ending). Returns the claim-start
+   * data for callers that merge it into a claim-end event. */
+  _activate_claimer(broadcast) {
     const session_id = this.active_claimer_session();
-    if (!session_id) return;
+    if (!session_id) return null;
 
     this.state.claimed_letters = [];
     this.state.claimed_words = [];
 
     const player = this.players_by_session[session_id];
-    this.broadcast_event_object("claim-start", {
+    const data = {
       player_name: player ? player.name : "Unknown",
       timeout_ms: CLAIM_TIMEOUT_MS,
       warning_ms: CLAIM_WARNING_MS
-    });
+    };
+
+    if (broadcast) {
+      this.broadcast_event_object("claim-start", data);
+    }
 
     /* Start claim timeout. */
     this._claim_warning_timer = setTimeout(() => {
@@ -190,6 +206,8 @@ class Anagrams extends Game {
     this._claim_timer = setTimeout(() => {
       this._cancel_claim("timeout");
     }, CLAIM_TIMEOUT_MS);
+
+    return data;
   }
 
   /* Take a letter from the center. */
@@ -352,19 +370,23 @@ class Anagrams extends Game {
 
     const session_id = this.state.claim_queue.shift();
     const player = this.players_by_session[session_id];
+
+    /* Activate next claimer (if any) and merge into claim-end
+     * as a single atomic event so clients never see a gap. */
+    let next = null;
+    if (this.state.claim_queue.length > 0) {
+      next = this._activate_claimer(false);
+    }
+
     this.broadcast_event_object("claim-end", {
       player_name: player ? player.name : "Unknown",
-      reason
+      reason,
+      next
     });
 
     /* Broadcast authoritative state so clients resync. */
     this.broadcast_event_object("center",  this.state.center);
     this._broadcast_player_state();
-
-    /* Activate next claimer if any. */
-    if (this.state.claim_queue.length > 0) {
-      this._activate_claimer();
-    }
   }
 
   _clear_claim_timers() {
@@ -521,21 +543,22 @@ class Anagrams extends Game {
       score
     });
 
-    /* End the claim so clients exit claim mode. */
+    /* Activate next claimer (if any) and merge into claim-end. */
+    let next = null;
+    if (this.state.claim_queue.length > 0) {
+      next = this._activate_claimer(false);
+    }
+
     this.broadcast_event_object("claim-end", {
       player_name: player ? player.name : "Unknown",
-      reason: "accepted"
+      reason: "accepted",
+      next
     });
 
     /* Broadcast authoritative state so clients resync. */
     this.broadcast_event_object("center", this.state.center);
     this._broadcast_player_state();
 
-    /* Activate next claimer if any. */
-    if (this.state.claim_queue.length > 0) {
-      this._activate_claimer();
-    }
-
     /* Replenish center if needed. */
     this._auto_deal();
   }
@@ -858,7 +881,7 @@ class Anagrams extends Game {
     if (this.state.vote_pending) {
       this._vote_timer = setTimeout(() => this._resolve_vote(), VOTE_TIMEOUT_MS);
     } else if (this.state.claim_queue.length > 0) {
-      this._activate_claimer();
+      this._activate_claimer(true);
     }
   }
 }