]> git.cworth.org Git - lmno.games/commitdiff
Fix race condition in handling of stuck button
authorCarl Worth <cworth@cworth.org>
Fri, 6 Mar 2026 13:14:16 +0000 (08:14 -0500)
committerCarl Worth <cworth@cworth.org>
Fri, 6 Mar 2026 16:29:48 +0000 (11:29 -0500)
Which was causing the button to stay stuck (ha!). The new logic
lets the SEE event be the single source of truth.

While fixing this, fix the styling of the button to be more clear,
specifically making it appear as a button saying "I'm stuck" where
it was previously blank which made its use non-obvious.

letterrip/letterrip.css
letterrip/letterrip.jsx

index 561ede8122912c83699235b193064d5140f7f298..27498cef488cf1b97d3224016789631d8e0ab409 100644 (file)
 
 .controls button {
   padding: 0.4em 1em;
-  border: 2px solid #555;
   border-radius: 4px;
-  background: transparent;
   cursor: pointer;
   font-size: 1em;
 }
 
-.controls button:hover {
-  background: #eee;
+.controls button.stuck-btn {
+  background: #f0f0f0;
+  border: 2px solid #999;
+  color: #333;
+}
+
+.controls button.stuck-btn:hover {
+  background: #e0e0e0;
 }
 
 .controls button.stuck-active {
   color: white;
 }
 
-.controls button.complete-btn {
-  background: #27ae60;
-  border-color: #27ae60;
-  color: white;
-}
-
-.controls button.complete-btn:disabled {
-  background: #95a5a6;
-  border-color: #95a5a6;
-  cursor: default;
-}
-
 .controls button.join-btn {
   background: #3498db;
-  border-color: #3498db;
+  border: 2px solid #3498db;
   color: white;
   font-size: 1.1em;
   padding: 0.5em 1.5em;
 }
 
+/* "Letter Rip" button appears inside the rack when all tiles are
+   placed and all words are valid. */
+.letter-rip-btn {
+  padding: 0.5em 1.5em;
+  background: #27ae60;
+  border: 2px solid #27ae60;
+  border-radius: 6px;
+  color: white;
+  font-size: 1.2em;
+  font-weight: bold;
+  cursor: pointer;
+  margin: auto;
+}
+
 /* Tile styling */
 .tile {
   display: inline-flex;
index 17f5cfd78dc5ab09efec0f52a5939baea53da245..da78d90da13c4e67aa674a83234df1c51728bab1 100644 (file)
@@ -241,10 +241,10 @@ function GameInfo(props) {
 }
 
 function PlayerList(props) {
-  const { player_info, other_players, all_stuck } = props;
+  const { player_info, other_players, stuck_players } = props;
   if (!player_info.id) return null;
 
-  const stuck_set = new Set(all_stuck);
+  const stuck_set = new Set(stuck_players);
 
   const render_player = (name, key) => (
     <span key={key}>
@@ -317,7 +317,7 @@ class Game extends React.Component {
       grid: {},
       rack: [],
       stuck: false,
-      all_stuck: [],
+      stuck_players: [],
       bag_remaining: null,
       game_over: false,
       winner: null,
@@ -375,7 +375,11 @@ class Game extends React.Component {
   }
 
   receive_stuck(data) {
-    this.setState({ all_stuck: data.stuck });
+    const my_name = this.state.player_info.name;
+    this.setState({
+      stuck_players: data.stuck,
+      stuck: data.stuck.indexOf(my_name) >= 0
+    });
   }
 
   receive_game_over(data) {
@@ -401,11 +405,7 @@ class Game extends React.Component {
   }
 
   async toggle_stuck() {
-    const response = await fetch_post_json("stuck");
-    if (response.ok) {
-      const data = await response.json();
-      this.setState({ stuck: data.stuck });
-    }
+    await fetch_post_json("stuck");
   }
 
   async declare_complete() {
@@ -577,7 +577,7 @@ class Game extends React.Component {
       <PlayerList key="pl"
         player_info={state.player_info}
         other_players={state.other_players}
-        all_stuck={state.all_stuck} />,
+        stuck_players={state.stuck_players} />,
 
       state.game_over ? (
         <div key="go" className="game-over-banner">
@@ -596,24 +596,19 @@ class Game extends React.Component {
       state.joined && !state.game_over ? (
         <div key="ctrl" className="controls">
           <button
-            className={state.stuck ? "stuck-active" : ""}
+            className={"stuck-btn" + (state.stuck ? " stuck-active" : "")}
             onClick={() => this.toggle_stuck()}>
             {state.stuck ? "Stuck!" : "I'm Stuck"}
           </button>
-          <button className="complete-btn"
-                  disabled={!can_complete}
-                  onClick={() => this.declare_complete()}>
-            Done!
-          </button>
           {state.bag_remaining !== null ? (
-            <span className="bag-count">{state.bag_remaining} tiles remaining in bag</span>
+            <span className="bag-count">{state.bag_remaining} tiles in bag</span>
           ) : null}
         </div>
       ) : null,
 
       state.joined ? this.render_board(analysis, invalid_cells, unconnected_cells) : null,
 
-      state.joined ? this.render_rack() : null,
+      state.joined ? this.render_rack(can_complete) : null,
 
       state.blank_pending ? (
         <BlankTileModal
@@ -690,7 +685,7 @@ class Game extends React.Component {
     );
   }
 
-  render_rack() {
+  render_rack(can_complete) {
     const { rack, tiles, rack_drag_over } = this.state;
     let className = "tile-rack";
     if (rack_drag_over) className += " drag-over";
@@ -719,7 +714,12 @@ class Game extends React.Component {
                   onClick={null} />
           );
         })}
-        {rack.length === 0 ? <span style={{color:"#999",padding:"8px"}}>Drag tiles here</span> : null}
+        {can_complete ? (
+          <button className="letter-rip-btn"
+                  onClick={() => this.declare_complete()}>
+            Letter Rip
+          </button>
+        ) : null}
       </div>
     );
   }