Commit Graph
4 Commits
Author SHA1 Message Date
tiennm99 d481a093ec feat(online): make a room a lobby with an owner and a ready-up
A room used to be a wrapper around one game: joining started it, and the
room died with it unless both players accepted a rematch inside thirty
seconds. It is now a lobby that outlives its games.

Whoever created the room owns it and the other seat is the guest. The guest
readies and the owner starts; the owner has no readiness of their own,
because starting is the same statement. A finished game returns both to the
lobby, where the next one is agreed exactly as the last was — the readiness
that started a game is spent with it.

A guest takes their readiness back before leaving, which is deliberate
friction: a player the owner is waiting on should have to say so before
walking away. The owner can free the seat of a guest who is not ready, and
not of one who is — readiness is a commitment, not an inconvenience. An
owner who leaves hands the room to whoever is left, unreadied, because they
are the one who starts now.

Something has to bound a room that outlives its games: the last player out
closes it, as does ten minutes in a lobby nobody started a game in. A
dropped connection is still not a player leaving — the seat is held for the
reconnect window in the lobby as well as mid-game, so a refresh no longer
costs somebody their room, and a resume lands in the lobby it left.

The rematch handshake is retired, and RoomCreated and RoomJoined go with it.
All three described part of what RoomState now describes in full, and three
messages for one lobby is three ways for a client to hold a view of it the
server never had. One snapshot per recipient, broadcast from the one place
that knows an input is finished, so no handler can forget to send it.
2026-09-07 16:32:14 +07:00
tiennm99 ef638e52fd feat(game): leave a dead end to the clock and show what was missed
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.
2026-09-07 15:51:22 +07:00
tiennm99 e5b03d041d fix(wsapi): set cache headers on the served frontend
Assets under _app/immutable carry a content hash in the name, so a changed file
is a changed URL and the old one can be cached forever. The shell cannot: it
names those hashed assets, so a copy cached across a deploy points at files that
no longer exist and the app loads into a blank page with nothing in the log.
2026-09-05 12:55:09 +07:00
tiennm99 5935f5c879 feat(wsapi): add websocket server with rooms, turn timers, and bot play
One goroutine owns each room and its engine. The room goroutine starts before
anyone is seated and seating is itself a message, so reading run() is a complete
proof of the concurrency contract rather than a convention to uphold. The bot
searches a frozen copy of the board instead of the live engine.

Reads carry no deadline; liveness is ping-based, because a read timeout cannot
distinguish a healthy player idling in the lobby from a dead socket.

The hub no longer binds a joiner to a seat before the room decides whether to
seat them. Anyone holding a room code could previously resign or play on a
seated player's behalf, and the room code is the only credential online 1v1 has.

cmd/noitu-server serves the API and, when NOITU_WEB_DIR is set, the built
frontend, with unknown paths falling back to index.html for client routes. All
configuration is environment-only and every variable has a working default.
2026-09-05 12:07:57 +07:00