]> git.cworth.org Git - lmno.games/commitdiff
anaagrams: Implement a game-ending modal
authorCarl Worth <cworth@cworth.org>
Sun, 8 Mar 2026 22:34:49 +0000 (18:34 -0400)
committerCarl Worth <cworth@cworth.org>
Sun, 8 Mar 2026 22:34:49 +0000 (18:34 -0400)
Once the bag is empty and all players have gone idle, (nobody has even
attempted to claim a word in some time), the server will report that
the game is ready to end. The client then displays a modal with a
countdown giving the user the chance to say whether they are still
looking for words. If anyone is still looking, the server should start
its idle counter over. If not, then the server should declare the game
over.

anagrams/anagrams.jsx

index 929f57dd4b10367c160044e9c9c03d8773ff3363..8056fe213c5385b988900e99779a53520e072471 100644 (file)
@@ -128,6 +128,9 @@ class Game extends React.Component {
       my_vote: null,
       votes_cast: 0,
       voters_total: 0,
+      /* Game ending countdown */
+      game_ending: false,
+      game_ending_seconds: 0,
       /* Game over */
       game_over: false,
       final_scores: null,
@@ -150,6 +153,7 @@ class Game extends React.Component {
   componentWillUnmount() {
     document.removeEventListener("keydown", this._onKeyDown);
     if (this._debug_interval) clearInterval(this._debug_interval);
+    this._clear_game_ending();
   }
 
   _check_state() {
@@ -422,12 +426,46 @@ class Game extends React.Component {
   }
 
   receive_game_over(data) {
+    this._clear_game_ending();
     this.setState({
       game_over: true,
       final_scores: data.scores
     });
   }
 
+  receive_game_ending(data) {
+    this._clear_game_ending();
+    const seconds = Math.ceil(data.countdown_ms / 1000);
+    this.setState({ game_ending: true, game_ending_seconds: seconds });
+    this._game_ending_interval = setInterval(() => {
+      this.setState(prev => {
+        const next = prev.game_ending_seconds - 1;
+        if (next <= 0) {
+          clearInterval(this._game_ending_interval);
+          this._game_ending_interval = null;
+          return { game_ending_seconds: 0 };
+        }
+        return { game_ending_seconds: next };
+      });
+    }, 1000);
+  }
+
+  receive_game_continued() {
+    this._clear_game_ending();
+    this.setState({ game_ending: false, game_ending_seconds: 0 });
+  }
+
+  _clear_game_ending() {
+    if (this._game_ending_interval) {
+      clearInterval(this._game_ending_interval);
+      this._game_ending_interval = null;
+    }
+  }
+
+  async still_looking() {
+    await fetch_post_json("still-looking");
+  }
+
   /*****************************************************
    * Actions                                           *
    *****************************************************/
@@ -981,6 +1019,18 @@ class Game extends React.Component {
           voters_total={state.voters_total}
           onVote={(accept) => this.vote(accept)}
         />
+      ) : null,
+
+      state.game_ending ? (
+        <div key="ending" className="vote-overlay">
+          <div className="vote-modal">
+            <h3>Are you still playing?</h3>
+            <p>Game ends in {state.game_ending_seconds} seconds</p>
+            <button onClick={() => this.still_looking()}>
+              Still looking
+            </button>
+          </div>
+        </div>
       ) : null
     ];
   }
@@ -1551,6 +1601,14 @@ events.addEventListener("done-update", event => {
   /* Could show progress, but game-over handles the end. */
 });
 
+events.addEventListener("game-ending", event => {
+  window.game.receive_game_ending(JSON.parse(event.data));
+});
+
+events.addEventListener("game-continued", event => {
+  window.game.receive_game_continued();
+});
+
 events.addEventListener("game-over", event => {
   window.game.receive_game_over(JSON.parse(event.data));
 });