Commit Graph
3 Commits
Author SHA1 Message Date
tiennm99 48b3b3c8ac feat(proto): add protobuf wire contract and cross-language codegen
proto/noitu/v1/game.proto is the single source of truth for every WebSocket
message. buf generates Go types into server/gen and JavaScript types into
web/src/lib/proto; both trees are committed so building needs no codegen
toolchain.

The Go suite emits binary fixtures into proto/testdata and the JavaScript suite
decodes the same bytes, so the two generated clients are checked against one
artifact rather than against each other's assumptions. CI lints the schema,
rejects breaking changes against main, and fails when the committed generated
trees drift from the schema.

game.NumRejectReasons and game.NumEndReasons let the mapping tests prove every
engine reason has a wire value without guessing where the enum ends.
2026-09-05 12:06:58 +07:00
tiennm99 e8b76cc643 feat(dictionary): add read-only store over the game wordlist
Load the whole dictionary into maps at Open and close the database before
Open returns. The plan called for per-lookup SQLite, but a round-trip
benchmarked at 55us against 8.9ns for a map hit, and the hard bot in a
later phase explores hundreds of candidates inside a 150ms budget. The
in-memory form is also simpler: no connection pool, no prepared
statements, no tail latency. Costs ~70ms and ~7.8MB at startup.

Resolve returns the canonical word, never the spelling the player typed.
Canonicalization moves either end: about half the aliases differ in the
last syllable and more than a third in the first, so "sy hai" resolves to
"si hai". FirstSyllable and LastSyllable report the canonical's ends, and
the engine must chain on those or it will reject legal moves.

WordsStartingWith yields an iterator rather than the backing slice. A
caller could otherwise sort, shuffle or append into dictionary state:
verified that a write landed in the store, that most buckets have spare
capacity for append to scribble into, and that concurrent callers race.

Open validates what it loaded against the builder's recorded word count,
cross-checks every out-degree against the words actually indexed, and
rejects orphan aliases. A truncated database otherwise opens cleanly and
the server starts, rejects every word, and fails every room creation.

RandomOpeningWord picks from a pre-sorted slice by binary search instead
of rebuilding a filtered copy per call, cutting room creation from 374us
and 720KB to 18ns and no allocation.

Escape the database path when building the URI: SQLite reads # as a
fragment delimiter, so an unescaped path opens a different file and
reports a misleading schema error.

The store does not log. A library writing to the global logger fights
structured logging later, and the caller has WordCount, AliasCount and
License to state the CC BY-SA attribution itself.
2026-09-04 16:52:39 +07:00
tiennm99 582ba27354 docs: add noi tu web game plan and dictionary research
Research the Vietnamese noi tu word-chain game and plan a 7-phase web
implementation: SvelteKit frontend, Go backend, WebSocket transport with
Protobuf framing, and a server-authoritative dictionary over SQLite.

The server validates every move so the browser never holds the wordlist,
which keeps player-vs-player cheat-resistant and lets the bot and PvP
modes share one rule implementation.

Records the validated decisions: words of two or more syllables linking
on first and last syllable, a 20s turn limit, user-typed nicknames, and
Docker deployment behind a reverse proxy.
2026-09-04 16:25:29 +07:00