Both game and snake packages had identical PlayerID types and the snake
package imported game.GenerateID. Now both use player.ID and
player.GenerateID from the shared player package.
Wrap free persistence functions in instance methods for cleaner call
sites (gi.save() instead of saveGame(gi.queries, gi.game)). Methods
log errors via zerolog before returning them.
- Replace Create+Get+Update with UpsertGame/UpsertSnakeGame queries
- Extract free functions (saveGame, loadGame, etc.) from duplicated
receiver methods on Store and Instance types
- Remove duplicate generateID from snake package, reuse game.GenerateID
- Throttle snake game DB writes to every 2s instead of every tick
- Fix double-lock in c4game chat handler
- Update all code for sqlc pointer types (*string instead of sql.NullString)
- Add brotli compression (level 5) to long-lived SSE event streams
(HandleGameEvents, HandleSnakeEvents) to reduce wire payload
- Fix all errcheck violations with nolint annotations for best-effort calls
- Fix goimports: separate stdlib, third-party, and local import groups
- Fix staticcheck: add package comments, use tagged switch
- Zero lint issues remaining
Inline persistence logic directly into game stores and handlers:
- game/persist.go: DB mapping methods on GameStore and GameInstance
- snake/persist.go: DB mapping methods on SnakeStore and SnakeGameInstance
- Chat persistence inlined into c4game handlers
- Delete db/persister.go (GamePersister, SnakePersister, ChatPersister)
- Stores now take *repository.Queries directly instead of Persister interface
v.PubSub() was captured at startup before v.Start() initialized NATS,
so both stores held nil and notify() silently no-oped. Replace the
PubSub interface with a callback that evaluates v.PubSub() lazily at
call time.
Use via's embedded NATS server to notify players of state changes
instead of a 100ms polling ticker. Each player subscribes to
"game.<id>" on page load; via auto-cleans subscriptions on disconnect,
eliminating the need for manual player tracking and RegisterSync.
When a game finishes (win or draw), players see a "Play again" button.
Clicking it creates a new game and the opponent sees a "Join Rematch"
link to join the same game.
- Add Delete method to GameStore and Persister interface
- Add delete button to game list on home page
- Verify user owns game before allowing deletion
- Use status constants instead of magic numbers
- Remove unused variable in persister
- User registration/login with bcrypt password hashing
- SQLite database with goose migrations and sqlc-generated queries
- Games and players persisted to database, resumable after restart
- Guest play still supported alongside authenticated users
- Auth UI components (login/register forms, auth header, guest banner)
Invitees no longer need to enter a nickname - they automatically
join with a random name like "Swift Tiger" or "Happy Falcon".
Game creators still enter their nickname manually.
- Enter key now triggers createGame action on home page
- Remove redundant setNickname action from home page
- Remove unused code: join channel, Leave(), Stop() methods
- Consolidate ID generation into game.GenerateID()
- Remove unused CreatedAt field from Game struct
Real-time two-player Connect 4 using Via framework with:
- Game creation and invite links
- SSE-based live updates for both players
- Win detection with animated highlighting
- Session-based nickname persistence