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 */
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: [],
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) {
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 *
*****************************************************/
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
+ });
}
}
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;
/* 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;
}
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 (
<div key="ctrl" className="controls">
<button className={is_queued ? "queued" : ""}
- disabled={!can_claim && !is_queued}
+ disabled={!can_claim && !can_queue}
onClick={() => this.start_claim()}>
- {is_queued ? "Queued..." :
- state.claim_player ? state.claim_player + " is claiming" :
- "Claim a Word"}
+ {button_label}
</button>
<span className={"bag" + (state.bag_remaining === 0 || !!state.claim_player ? " disabled" : "")}