Carl Worth [Sun, 8 Mar 2026 13:32:04 +0000 (09:32 -0400)]
anagrams: Bring claim area into our color scheme
By preferring --accent-color and existing button styling wherever
possible, and add a new --accent-color-muted. Avoid bright red for
actions like cancelling the claim or returning a word, using subtle
gray instead.
Carl Worth [Sun, 8 Mar 2026 13:20:25 +0000 (09:20 -0400)]
Add support for dragging tiles while claiming
This can be either to drag tiles to the rack (to claim them) or to
just rearrange the tiles (as can already be done outside of
claiming). A 5-pixel threshold of movement will trigger a drag
operation. Movement less than that will be considered a click and will
claim the letter.
Carl Worth [Sun, 8 Mar 2026 13:13:38 +0000 (09:13 -0400)]
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.
Carl Worth [Sun, 8 Mar 2026 12:31:38 +0000 (08:31 -0400)]
Make the "Host a new game" buttons primary on each page
Displaying it first, (before the form for joining an existing game).
Also drop the submit button from the join form, (relying on the user
simply entering a game ID instead).
This makes the primary flow smoother: Choose a game, then click "Host"
without the user being confronted with a form and having to decide
which of two buttons to click.
Carl Worth [Sun, 8 Mar 2026 12:21:49 +0000 (08:21 -0400)]
Simplify tile dealing from the bag
No longer require multiple people to request a letter. Instead, any person
can draw a new letter from the bag as long as no letter reveal is currently
in progress.
Carl Worth [Sun, 8 Mar 2026 12:04:14 +0000 (08:04 -0400)]
Fix tile dealing animation
It was previously showing only a "3" instead of "3", "2", "1", and it
was fading the whole tile instead of just the number. Both issues are
fixed in this commit.
1. Center tiles can be dragged to rearrange (client-side only,
mouse and touch) to help visualize possible words.
2. Keyboard input during claiming: type letters to grab from
center, Backspace to return last, Enter to submit, Escape
to cancel.
3. Countdown shows 3, 2, 1 on the tile with opacity fade
animation each second.
4. Fix cancelled claims not returning tiles to center (let
server SSE events handle state cleanup).
5. Fix UI staying in claim mode after successful word submission
(server-side fix: broadcast claim-end after acceptance).
Also remove game_steal references (deferred for later).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Carl Worth [Sun, 8 Mar 2026 03:40:25 +0000 (22:40 -0500)]
Add Anagrams client-side UI
React game component with center letter pool (random tile
positions), claim mode with rack for arranging words, player
word areas with steal support, voting modal for non-dictionary
words, and game-over scoreboard. Also adds lobby page and
listing on main index.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Carl Worth [Sat, 7 Mar 2026 23:05:55 +0000 (18:05 -0500)]
letterrip: Fix blank-letter to work even if letter selection is cancelled
If a user is moving a blank tile off of their rack onto the board,
then they are required to choose a letter for the blank. In this
situation, cancelling letter selection cause the blank tile to be
returned to the rack.
In contrast, when moving a blank tile from one space on the board to
the other, the blank will already have a selected letter, and it's
reasonable for that selection to be unchanged. So in this case,
cancelling the letter selection should let the movement work, and
should not send the letter back to the rack.
This commit fixes that scenario by performing the movement prior to
bringing up the selection modal. That way, if the selection modal is
cancelled, there's nothing else to do and the user gets the desired
behavior (the tile moved, unchanged).
Carl Worth [Sat, 7 Mar 2026 16:40:11 +0000 (11:40 -0500)]
Add a form to join an existing game (by 4-letter code) from each game page
We already had the ability to host a new game at these pages, but a
user may land here on want to join an existing game. So give them that
form to be able to do that. (Saves them having to know that they could
go to / to do that, and saves them the work of doing that.)
Carl Worth [Sat, 7 Mar 2026 14:36:10 +0000 (09:36 -0500)]
letterrip: Allow the grid to grow beyond the default page width
It's silly to make the user scroll back and forth when the grid gets
wider than the default page. So set the board's container to use a
maximum width that is based on the viewport, rather than just being a
percentage of the parent. For this to work, we also have to drop the
parent-relative 100% maximum width from .board-and-rack.
Carl Worth [Sat, 7 Mar 2026 13:53:12 +0000 (08:53 -0500)]
letterrip: Don't bring up modal for selecting blank tile when first clicking it
Instead, just select the tile on first click, and then bring up the
selection modoal on the second click, (either when selecting the
destination cell for movement or deselecting it).
This is consistent behavior with what the user will have first seen
when moving the blank from the rack to the board, and now means that
the user can use click-to-select/click-to-move to move a blank tile
from one board square to another, (which was impossible prior to this
commit).
Carl Worth [Sat, 7 Mar 2026 13:10:22 +0000 (08:10 -0500)]
letterrip: Remove all word validation from the client
Now that the server has a dictionary, it performs all validation of
valid tile placements. This allows for a smaller client, (no need
to download a word list anymore), and ensures that word validation
is performed consistently in a single place on the server side.
Carl Worth [Fri, 6 Mar 2026 22:06:06 +0000 (17:06 -0500)]
Rework touch drag operation to render dragging tile as a React component
We encountered a bug where a touch drag operation was interrupted, by
some operation, leaving a ghost tile left around half-way through the
drag operation. One option to fix this would be to implement a
tounchCancel handler to clean things up.
But an alternate approach, in this commit, is to render the tile being
dragged as a React component. This way, everything is rendered
uniformly as React components, and we have less dependence on precise
browser semantics with respect to touchCanel, etc.
Carl Worth [Fri, 6 Mar 2026 21:44:58 +0000 (16:44 -0500)]
Several fixes for styling of the "blank modal" selector
This is the modal dialog that pops up when the user clicks on a
blank tile to select what letter it represents. The changes in
this commit are:
1. Use the same appearance (referencing the same CSS class) for
the tiles in this modal as for tiles in other contexts, (this
replaces the previous custom styling which was white text on
a light background and hard to read).
2. Restructure the modal so that it is centered over the grid
rather than centered over the viewport (also constrain the
rack to not be wider than the grid).
3. Fix a bug that resulted from the user cancelling a selection of a
blank tile, (it was "returned" to the rack before being properly
removed from the rack, resulting in two occurences of the same tile
on the rack). See the reassign_true logic in this commit for the
relevant fix.
Carl Worth [Fri, 6 Mar 2026 14:30:06 +0000 (09:30 -0500)]
Don't grow the grid if not needed, (shift tiles instead)
At least, shifting of the tiles is what the user experiences. Within
the code, the concept is simpler: We don't have a grid size at all
anymore, but simply compute the grid bounds that we want whenever
things change. The bounds are made to just fit the tiles (plus one
extra row/column on each side) and never smaller than the original
size chosen to fill the screen.
Carl Worth [Fri, 6 Mar 2026 14:00:37 +0000 (09:00 -0500)]
Add a "touch drag" interface to the game
This sits alongside the existing HTML5 DND interface.
The new touch interface is _much_ more friendly on a phone than the
previous HTMl DND interface, (which required long pressing before
it would do anything).
Carl Worth [Fri, 6 Mar 2026 13:34:51 +0000 (08:34 -0500)]
Leave the "Some words are not valid" text up whenever it is relevant
Prior to this commit, this message was disappearing when the last tile
was removed from the rack, but there's no reason for that. The message
depends only on the state of the board, not whether there are tiles
on the rack or not.
Carl Worth [Fri, 6 Mar 2026 13:14:16 +0000 (08:14 -0500)]
Fix race condition in handling of stuck button
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.
Carl Worth [Fri, 6 Mar 2026 12:58:57 +0000 (07:58 -0500)]
Calm down the dynamic grid resizing
There was some annoying jitter as rows were being constantly
added/removed so that there were always 3 empty rows/columns around
placed tiles. Instead, in this commit, the grid starts initially at a
size of 10x10 and then only grows (by one tile) when a tile is placed
at an extreme edge.
Carl Worth [Fri, 6 Mar 2026 12:50:02 +0000 (07:50 -0500)]
Add logging for any non-alphabetic tile that might show up
I saw a bug where a tile was diplayed with a value of '1' which was
unexpected. If this happens again, this logging should help us
determine what happened.
Carl Worth [Tue, 7 Jul 2020 06:26:43 +0000 (23:26 -0700)]
Score each mini glyph and render the winner for each
At this point the game is almost totally complete. All that is missing
is the final-score indication of the majority winner and the
super-glyph winner.
Carl Worth [Tue, 7 Jul 2020 06:10:53 +0000 (23:10 -0700)]
Prep for stashing final scores against each mini grid
By turning each mini grid into an object rather than just an array of
squares. This new object now has a place to lodge scores and a final
winner for each mini grid, (though we aren't populating those yet).
This commit is intended to have no visible change on the game.
Carl Worth [Tue, 7 Jul 2020 05:58:12 +0000 (22:58 -0700)]
Add support for detecting glyph shapes
And color the background of each cell that is part fo a glyph in a
player-specific color.
Note: I'm not at all convinced that this is a good way to style
things, but the actual functionality is here now, and I'll be happy to
improve the specific styling in the future.
Carl Worth [Tue, 7 Jul 2020 02:08:32 +0000 (19:08 -0700)]
Scribe: Prep game state for detection of glyphs
There's not any detection of glyphs here yet, there's simply a new
slot in each of the squares: Where we used to just have a textual
ymbol for each occupied square, we now have an object:
{
symbol: '+' or 'o',
glyph: true or false
}
And the glyph Boolean is used to add a class to the HTML square
element as well.
Carl Worth [Sun, 5 Jul 2020 18:31:54 +0000 (11:31 -0700)]
Stop ignoring .gitattributes
We're implementing a cleaner approach for this where the nogit script will provide its own .gitattributes data as needed, (and the lmno-todo repository won't need to).
Carl Worth [Sat, 4 Jul 2020 14:53:32 +0000 (07:53 -0700)]
empathy: Don't force active:true when adding state for a new player
This fixes the bug where reloading the interface would cause inactive,
grayed-out player names to appear solid instead of grayed out.
What I had been thinking when writing this code is that this function
would be called only when a new player joins the game, (and by
definition, a newly joined player must be active). But this function
can also be called when a player reloads their interface, and at that
point player-join events are received for all players whether active
or not.
Meanwhile, the server is now explicitly providing the active bit for
players, so we get the right information without doing anything.
Carl Worth [Tue, 30 Jun 2020 00:09:31 +0000 (17:09 -0700)]
Display (grayed out) inactive players with non-zero scores
This issue came up with playtesting last night: We had a player who
finished in the top three overall, but dropped out for the last couple
of rounds. But we still wanted to see their score in the final
results. So, here we keep track of active and inactive players and
display the inactive ones (if they have non-zero score) with 50%
opacity.
Carl Worth [Tue, 30 Jun 2020 00:05:17 +0000 (17:05 -0700)]
Only consider active players when counting players needed for prompt voting
Now that the server is sending us inactive players, we don't want to
consider them when computing how many votes we need to go forward with
a category.
Carl Worth [Mon, 29 Jun 2020 22:19:07 +0000 (15:19 -0700)]
Add the KUDOS SLAM and GREAT MINDS achievements
At this point, I've implemented all of the achievements we originally
thought of, (except for TWINNER which is going to take a bit more code
to find cases where it applies).
Carl Worth [Mon, 29 Jun 2020 21:56:16 +0000 (14:56 -0700)]
Fix category voting to select among only those with the most votes
Previously, the code was selecting randomly among all categories with
at least a quorum of votes. With this commit, the code now does what
was intended, by selecting randomly only among the categories that are
tied for the most votes.
Carl Worth [Mon, 29 Jun 2020 21:54:31 +0000 (14:54 -0700)]
Change category voting to require fewer votes when there are more categories
Now, at the beginning, it's no longer enough for there to be a
majority of votes on a single prompt. Instead, the number of votes
required is the number of players minus the number of available
prompts.
Carl Worth [Mon, 29 Jun 2020 21:28:35 +0000 (14:28 -0700)]
Display kudos (as stars) in the per-round scores
Now that the server gives us this information, let's display it,
(otherwise, the players could be very confused about why some ties
were apparently broken in the sorting).
Carl Worth [Mon, 29 Jun 2020 17:49:22 +0000 (10:49 -0700)]
Add a QUIRKSTER achievement
This one recognizes the dubious achievement of submitting an entire
docket of answers with no matches with anyone else at all. Thanks to
Andrew for naming it.
Carl Worth [Mon, 29 Jun 2020 15:55:29 +0000 (08:55 -0700)]
Rename "label" to "achievement"
The word "label" was far too generic to capture the stlying needs for
this class. Thanks to Andrew for pointing out that this feature is
really what games today call an "achievement".
in our game last night, which ended up scribbling over the rows above
and below in the judging view. This fix chops things off vertically,
(which doesn't solve the case if someone is _legitimately_ trying to
get such text to appear—we can address that case when it appears).
Carl Worth [Mon, 29 Jun 2020 15:32:51 +0000 (08:32 -0700)]
Reword the Move On button when we're not waiting for anyone specific
This case arises when there are additional players still connected to
the game, but that haven't even started typing any answers.
In this case, the view looks like this:
Submission received
The following players have submitted their answers: <list>
In this case, wording of "Move On Without Their Answers" suggests the
exact opposite, (as if "their" is referring to the submitted players
rather than those not listed on this page). The new wording in this
case, "Move On Without Anyone Else" makes it more clear what the
behavior is.
Carl Worth [Mon, 29 Jun 2020 15:29:51 +0000 (08:29 -0700)]
Define the move_on button after the still_waiting component
This is just code movement here with no intended logical change. The
motivation here is so that the "Move On" button can be worded
differently depending on who we're waiting for.
Carl Worth [Sun, 28 Jun 2020 23:25:29 +0000 (16:25 -0700)]
empathy: Add the ability to star one favorite item
The client isn't currently saving information about which words the
player themself submitted, so it isn't yet preventing the user from
sending kudos to an item they submitted themself.
Carl Worth [Sun, 28 Jun 2020 22:55:46 +0000 (15:55 -0700)]
empathy: Pre-group words according to common patterns
Specifically, the following will be considered identical and
pre-grouped:
<word> = <word>s
<word> = a <word>
<word> = an <word>
<word> = the <word>
Unlike the server-side canonicalization of case differences, which are
not visible, the above matching words are all still made available to
the player and simply pre-grouped. So, the player can decide whether
to leave them grouped or to separate them back out.
Carl Worth [Sun, 28 Jun 2020 21:44:47 +0000 (14:44 -0700)]
Drop extra PromptOption state for not showing a specific prompt
It was fun for me to learn about how to use the useState hook,
but this only code path was only necessary for when I was not
planning to distribute negative votes through the server to
other clients.
I ended up distributing that state after all, which has the benefit
that hidden prompts stay hidden when reloading (and from one round to
the next). [One potential downside is that hidden prompts are
permanently hidden---they can't be made to appear again. Oh well!]
And now that the server is telling us about the prompts we've already
voted against, and we're already not dispalying those, we don't need
the additional state here to hide things on the client side before we
receive that notification from the server.
Carl Worth [Sun, 28 Jun 2020 16:08:06 +0000 (09:08 -0700)]
Add a containing div element to the "still waiting" component chunks
And drop the use of a list.
The list was annoying because it was triggering the React warning:
Each child in a list should have a unique "key" prop
And in this case, it seemed really silly to have to name/distinguish
the <p> element from the following <ul> element.
And this new <div> does give us a logical place to treat this chunk of
the document as a single element if needed, so this is the logically
correct thing to do.
Carl Worth [Sun, 28 Jun 2020 15:21:41 +0000 (08:21 -0700)]
Eliminate lead-in text when there is nothing following
I was often annoyed to see the text "Still waiting for the following
players:" when it was followed by no players at all.
It would have been really awkward to try to achieve the result of this
commit withing the final return statement. It's funny how often I
forget that JSX doesn't require all content to be contained in a
single return statement. I often find myself migrating to an approach
that can be seen in this commit, and I seem to prefer it:
* Conditions are captured in JavaScript code, (and not within JSX)
* Various React componenents are each captured in separate JSX chunks
* The final return statement is often just listing previously
constructed components
Carl Worth [Sun, 28 Jun 2020 15:17:58 +0000 (08:17 -0700)]
empathy: Add some missing whitespace
JSX is sometimes annoyingly aggressive about eliminating whitespace
between textual content and component content. I'm not sure if this
solution is the best approach, but it works at least.
Carl Worth [Sun, 28 Jun 2020 00:34:06 +0000 (17:34 -0700)]
Stop abusing the JavaScript back-tick syntax within JSX
JSX already lets us implicitly combine adjacent string literals
together so it's simpler to not use the JavaScript back-tick syntax
but instead simply pop back to JSX when we need some literal string
content.
Carl Worth [Sat, 27 Jun 2020 23:32:05 +0000 (16:32 -0700)]
Implement voting for the "New Game" button
Making this phase advance much like the other phases, and dependent on
a majority of the players rather than just letting one player stomp
all over things.
Beyond this, we still do want to allow players to inspect the state of
past rounds, but the UI for that that will have to come later.
Carl Worth [Sat, 27 Jun 2020 18:07:49 +0000 (11:07 -0700)]
empathy: Track the player-exit event
Now that the server is correctly noticing when a client drops it
connection, and notifying clients when a player has dropped every
connection, we now listen for that and drop players from our list
when they have left the game.
Carl Worth [Sun, 21 Jun 2020 16:19:53 +0000 (09:19 -0700)]
Fix square to not be active if occupied
Thanks to Scott who noticed yesterday he could tap on an occupied
square and get an error message including "Square is already
occupied". It's good the server catches that case and sends an
error. And it's good the client presentes that cleanly.
But the client shouldn't ever even let the user submit a move that the
client already know is not legal.
In this commit we appropriately set a square to not be active if it is
already occupied. This fixes the case described above, and further
makes it so that hovering over an occupied square won't even present
the user with a cursor suggesting the square is "clickable".
Carl Worth [Sun, 21 Jun 2020 16:16:29 +0000 (09:16 -0700)]
Rename local variable from "active" to "grid_active"
There is an "active" property up and down our component stack, (at the
board level, whether it's the current players turn; further at the
mini-grid level, whether it's a legal mini-grid; further still at the
square level, whether the square is unoccupied). It's clear enough to
uses a props name of "active" at each level.
But when using a local variable to compute the child's "active" prop
while in a current component with its own "active" prop, this can get
confusing.
So, at the board level, use a local variable of "grid_active" to
compute the "active" prop for the child grid.
Carl Worth [Sat, 20 Jun 2020 18:01:33 +0000 (11:01 -0700)]
Move definition of Scribe glyphs to an array of objects
And loop over this with map() to generate a list of React elements.
This is instead of the previous list of literal React elements we had
before.
This doesn't have any immediate impact, but makes the glyph data
available if other code wants to access it in the future, (such as
code that is scoring a mini grid).