From: Carl Worth Date: Mon, 9 Mar 2026 15:36:20 +0000 (-0700) Subject: Allow queueing up a claim while another claim is in progress X-Git-Url: https://git.cworth.org/git?a=commitdiff_plain;h=8259648db550cafce8defe3751182536d1b1d495;p=lmno.games Allow queueing up a claim while another claim is in progress Prior to this commit, when another player is claiming, the "Claim a word" button would be deactivated. Now, instead, we allow this button to be active, and it puts the player's claim request onto a queue so that they will get their turn eventually. --- diff --git a/anagrams/anagrams.jsx b/anagrams/anagrams.jsx index 9ff1c4b..0127c6c 100644 --- a/anagrams/anagrams.jsx +++ b/anagrams/anagrams.jsx @@ -179,6 +179,7 @@ class Game extends React.Component { claiming: false, claim_active: false, /* true if I'm the active claimer */ claim_player: null, /* name of the current claimer */ + claim_queue_position: null, /* my position in queue, null if not queued */ claim_rack: [], /* { letter, source, tile?, word_id?, idx? } */ claimed_words: [], /* word objects I've stolen */ claim_center_tiles: [], /* center tiles I've taken */ @@ -368,43 +369,46 @@ class Game extends React.Component { receive_claim_start(data) { const my_session = this.state.player_info.id; const is_me = data.player_name === this.state.player_info.name; - this.setState({ + this.setState(prev => ({ claim_player: data.player_name, claim_active: is_me, - claiming: is_me, - claim_rack: is_me ? [] : this.state.claim_rack, - claimed_words: is_me ? [] : this.state.claimed_words, - claim_center_tiles: is_me ? [] : this.state.claim_center_tiles, + claiming: is_me || prev.claim_queue_position != null, + claim_queue_position: is_me ? null : prev.claim_queue_position, + claim_rack: is_me ? [] : prev.claim_rack, + claimed_words: is_me ? [] : prev.claimed_words, + claim_center_tiles: is_me ? [] : prev.claim_center_tiles, other_claim_letters: is_me ? [] : (data.claimed_letters || []), other_claim_words: is_me ? [] : (data.claimed_words || []), claim_error: null, claim_warning: false, claim_remaining_ms: data.timeout_ms - }); + })); if (is_me && data.timeout_ms > 0) { this._start_claim_timer(data.timeout_ms, data.warning_ms); } - /* If a letter was queued from a keyboard-initiated claim, take it. */ - if (is_me && this._pending_key) { - const pending = this._pending_key; - this._pending_key = null; - const tile = this.state.center.find( - t => t.letter === pending && !this.state.revealing[t.id] - ); - if (tile) { - this.take_center_letter(tile); - } + if (is_me && this._pending_keys) { + this._replay_pending_keys(); } } receive_claim_end(data) { - /* Center and player-words are resynced by separate server events. */ - this.setState({ - claim_player: null, - claim_active: false, - claiming: false, + /* Center and player-words are resynced by separate server events. + * If data.next is present, the next claimer is already active + * (merged into this event to avoid a gap). */ + const next = data.next; + const next_is_me = next + && next.player_name === this.state.player_info.name; + + this.setState(prev => ({ + claim_player: next ? next.player_name : null, + claim_active: !!next_is_me, + claiming: next_is_me || prev.claim_queue_position > 1, + claim_queue_position: prev.claim_queue_position != null + ? (prev.claim_queue_position <= 1 ? null + : prev.claim_queue_position - 1) + : null, claim_rack: [], claimed_words: [], claim_center_tiles: [], @@ -412,9 +416,23 @@ class Game extends React.Component { other_claim_words: [], claim_error: null, claim_warning: false, - claim_remaining_ms: 0 - }); + claim_remaining_ms: next_is_me ? next.timeout_ms : 0 + })); this._stop_claim_timer(); + + if (next_is_me) { + this._start_claim_timer(next.timeout_ms, next.warning_ms); + + /* We were queued, so the center may have changed. Check if + * all buffered letters are still available. If so, replay + * them all; if not, discard the buffer entirely rather than + * replaying a partial/garbled word. */ + if (this._pending_keys) { + this._replay_pending_keys(); + } + } else if (!next) { + this._pending_keys = null; + } } receive_letter_claimed(data) { @@ -533,6 +551,24 @@ class Game extends React.Component { await fetch_post_json("still-looking"); } + /* Replay buffered keystrokes from before claim was active. + * Each letter is taken if available, skipped if not. The user + * sees what they typed minus any missing letters and can work + * out what happened (typo, or letters taken by another player). */ + _replay_pending_keys() { + const keys = this._pending_keys; + this._pending_keys = null; + + for (const k of keys) { + const tile = this.state.center.find( + t => t.letter === k && !this.state.revealing[t.id] + ); + if (tile) { + this.take_center_letter(tile); + } + } + } + /***************************************************** * Actions * *****************************************************/ @@ -553,7 +589,10 @@ class Game extends React.Component { const response = await fetch_post_json("claim"); if (response.ok) { const data = await response.json(); - this.setState({ claiming: data.queued }); + this.setState({ + claiming: data.queued, + claim_queue_position: data.active ? null : data.position + }); } } @@ -840,7 +879,7 @@ class Game extends React.Component { if (this.state.claim_rack.length >= 4) { this.submit_word(); } - } else if (!this.state.claiming && !this.state.claim_player) { + } else if (!this.state.claiming) { this.start_claim(); } return; @@ -849,12 +888,19 @@ class Game extends React.Component { /* Everything below requires a letter key. */ if (!/^[A-Z]$/.test(key)) return; - /* If not claiming yet, start a claim and queue the letter. */ + /* If not claiming yet, start a claim and buffer typed letters. + * When the claim activates, all buffered letters are replayed. + * If we end up queued behind someone, only the first letter is + * kept (the center will likely change before our turn). */ if (!this.state.claim_active) { - if (this.state.claiming || this.state.claim_player) return; e.preventDefault(); - this._pending_key = key; - this.start_claim(); + if (!this._pending_keys) { + if (this.state.claiming) return; + this._pending_keys = [key]; + this.start_claim(); + } else { + this._pending_keys.push(key); + } return; } @@ -1132,18 +1178,31 @@ class Game extends React.Component { render_controls() { const state = this.state; + const is_queued = state.claim_queue_position != null; + const can_queue = state.claim_player && !state.claim_active + && !is_queued && !state.game_over; const can_claim = !state.claiming && !state.claim_player && !state.game_over; - const is_queued = state.claiming && !state.claim_active; + + let button_label; + if (is_queued) { + const pos = state.claim_queue_position; + button_label = (pos <= 1 ? "You're next" : "You're #" + pos) + + " after " + state.claim_player; + } else if (can_queue) { + button_label = "Claim after " + state.claim_player; + } else if (state.claim_player && !state.claim_active) { + button_label = state.claim_player + " is claiming"; + } else { + button_label = "Claim a Word"; + } return (