From 5619d6e14a4268996730af6e1e868f0a84979661 Mon Sep 17 00:00:00 2001 From: Carl Worth Date: Sun, 8 Mar 2026 09:13:38 -0400 Subject: [PATCH] anagrams: Add a bunch of keybindings to make things keyboard friendly The most significant is that just typing a word starts a claim implicitly. But we also add "space" for drawing a new letter from the bag, and Y/N for voting. --- anagrams/anagrams.jsx | 55 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/anagrams/anagrams.jsx b/anagrams/anagrams.jsx index 52036c0..0d2397d 100644 --- a/anagrams/anagrams.jsx +++ b/anagrams/anagrams.jsx @@ -258,6 +258,18 @@ class Game extends React.Component { 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_letter(tile); + } + } } receive_claim_end(data) { @@ -510,15 +522,36 @@ class Game extends React.Component { *****************************************************/ on_key_down(e) { - if (!this.state.claim_active) return; - /* Ignore if typing in an input field. */ if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") return; + if (!this.state.joined || this.state.game_over) return; const key = e.key.toUpperCase(); - if (key === "BACKSPACE" || key === "DELETE") { - /* Return the last letter from the rack. */ + /* Vote: Y to accept, N to reject. */ + if (this.state.vote_pending && !this.state.my_vote) { + if (key === "Y") { e.preventDefault(); this.vote(true); return; } + if (key === "N") { e.preventDefault(); this.vote(false); return; } + } + + /* Spacebar: deal a letter from the bag. */ + if (e.key === " " && !this.state.claim_player) { + e.preventDefault(); + if (this.state.bag_remaining > 0) { + this.deal_letter(); + } + return; + } + + /* Escape: cancel claim. */ + if (key === "ESCAPE" && this.state.claim_active) { + e.preventDefault(); + this.cancel_claim(); + return; + } + + /* Backspace/Delete: return last letter during claim. */ + if ((key === "BACKSPACE" || key === "DELETE") && this.state.claim_active) { e.preventDefault(); const rack = this.state.claim_rack; if (rack.length > 0) { @@ -527,7 +560,8 @@ class Game extends React.Component { return; } - if (key === "ENTER") { + /* Enter: submit word during claim. */ + if (key === "ENTER" && this.state.claim_active) { e.preventDefault(); if (this.state.claim_rack.length >= 4) { this.submit_word(); @@ -535,13 +569,18 @@ class Game extends React.Component { return; } - if (key === "ESCAPE") { + /* Everything below requires a letter key. */ + if (!/^[A-Z]$/.test(key)) return; + + /* If not claiming yet, start a claim and queue the letter. */ + if (!this.state.claim_active) { + if (this.state.claiming || this.state.claim_player) return; e.preventDefault(); - this.cancel_claim(); + this._pending_key = key; + this.start_claim(); return; } - if (!/^[A-Z]$/.test(key)) return; e.preventDefault(); /* Find a matching letter in the center and claim it. */ -- 2.45.2