Closing the position no longer wins the game on the spot. The player handed
a syllable that starts nothing keeps the turn they were given and loses it to
the clock, the way they lose any turn they cannot answer — the win used to
land before they had seen the board at all. A turn lost to a position nobody
could answer is reported as the dead end it was rather than as time spent
thinking.
The bot is the exception, and stays one: it has no clock to spend, so the
room settles its dead end the moment the search comes back empty. NoMove
does that without dressing it up as a resignation the bot never chose.
GameOver now carries a few of the words the position still had, filled for
the losing player only — the winner was not the one who was stuck. An empty
list on a loss is the other half of the message: nothing could have been
played, so the panel says so instead of listing nothing.
Three things a player asked for after playing:
The field now arrives holding the syllable the next word has to start with,
so only the half that is still in question gets typed. It is written once per
turn and never over an existing draft, which keeps the field uncontrolled and
leaves Telex and VNI composition alone.
The chain lists the newest word first. The move that decides what to play
next is the last one, and it belongs where the eye lands rather than at the
end of a list that has to be scrolled.
A finished game can be downloaded as a plain-text transcript: the chain in
playing order, who played each word, and the final score. The export keeps
playing order because a chain read on its own only makes sense from the
opening word forward — newest-first is a reading aid for the live board, not
the shape of the game.
Create a room and share a six-character code or an invite link; join by either.
The waiting room shows the code until an opponent arrives. During a game the
opponent's disconnection raises a banner with the grace countdown, and their
return clears it. When the game ends, the rematch prompt appears only while an
offer is actually open, so there is never a button that can only produce an
error.
The board is now one component both modes render, which is what makes online
play the same screen as the bot game rather than a second implementation of it.
Room codes are normalized by stripping separators and reading the code out of a
pasted invite link, and nothing else. Filtering out every character outside the
alphabet would turn any sentence into six plausible letters and send the player
to a room that was never theirs.
The socket now opens when the player asks for a room, not on arrival: the
handshake carries the nickname once, and a socket opened before they typed it
introduced them under whatever was stored beforehand. A tab that already holds
a session reconnects immediately instead, so refreshing mid-game returns to the
game. A resume the server cannot honour is dropped quietly rather than opening
the lobby with an error the player did nothing to cause.
The client also notices a socket that fails without closing. Nothing did
before, so the page kept showing a live connection and a running countdown over
a socket nothing could reach. A tick that fired late is ignored, because a
backgrounded tab has its timers throttled and would otherwise close a healthy
connection every time the player switched away.
The board, the home screen and the game-over panel, in Vietnamese, served by
the Go binary as a static single-page app.
The store is a reducer over ServerMessage and computes nothing. Validity, turn
order, scores and the result are read from the wire, which is what lets one
screen serve the bot now and online play later. Every Vietnamese string lives in
one module, including the map from RejectReason to a message, so the server can
send UI keys instead of prose.
The word field is uncontrolled. A Telex or VNI input method composes a diacritic
across several keystrokes, and writing the value back on each one cancels the
composition. It is read on submit and cleared only there.
The countdown is drawn against the server's clock, estimated from the ping round
trip, and settles 300ms early so the ring never claims more time than the server
allows.
The screen owns the socket and the game while it is mounted, and gives the
current game up on the way out. Asking for a game is stored intent rather than a
condition inferred from the board being empty: clearing the board for a rematch
is that same condition, so the inference sent a second StartBotGame and the
server built two rooms that then destroyed each other.
A test greps the built bundle for dictionary words and holds a size budget, so
the wordlist cannot reach the browser unnoticed. Another reads the error codes
out of the Go transport and fails when one has no Vietnamese message.