@game-hub/game-stpetersburg
v0.1.1
Published
Saint Petersburg (1st edition) as a Game Hub game package — a standalone repository built against the published @game-hub/kernel and @game-hub/ui-kit, extracted from the platform monorepo on the out-of-repo pattern the Labyrinth pilot proved. Four subpath
Downloads
375
Readme
@game-hub/game-stpetersburg
Saint Petersburg (1st edition — Michael Tummelhofer, 2004; the 2009 Rio Grande printing) as a Game Hub game package: a 2–4 player card-buying engine builder played over ~7–10 rounds of four phases — hire workers for rubles, raise buildings for points, court aristocrats for both, and use trading cards to upgrade what you already own. Your money and your hand are secret; most points wins.
It is a standalone repository that depends on @game-hub/kernel and @game-hub/ui-kit from the public
npm registry, like any third-party consumer would. It was extracted from the platform monorepo
(container's packages/games/stpetersburg) onto the out-of-repo pattern the Labyrinth pilot proved
(Track D): four subpath exports (./engine, ./module, ./client, ./bot) behind game-agnostic hosts,
shipped as a compiled dist/ package a host installs like any dependency.
Why this game is interesting to the platform
Saint Petersburg is the hub's first game with real hidden information. The three games before it barely used the redaction seam; this one lives on it:
- A player's rubles and hand are secret (rulebook pg. 2, "may never tell").
viewForredacts both: an opponent sees your hand count and nothing else, andrubles: null. Your own seat sees everything. - The deck order is a real secret. The four draw stacks are projected as counts, never contents — a view can never leak what will be dealt next.
- The first bot on the platform that decides from a genuinely incomplete view.
./botreads only its own seat's knowledge (the redactedStPetersburgView), so cheating is structurally impossible — there is no hidden field for it to peek at.
Redaction lives in the engine (src/engine/view.ts), under the 100% gate — what a player may see
is as much a rule as what they may do.
Status
Feature-complete (SP0–SP9). The full game plays: the four-phase spine and round loop, the hidden
hand, trading-card displacement, the six special cards (Pub, Observatory, Warehouse, and the rest), final
scoring, an illustrated board (the "Malachite & Gilt" art), and an AI opponent that decides from the
redacted view. 161 engine + bot tests; engine coverage 100%, bot coverage ≥ 90%. The slice history, the
rules digest and every ruling live in ROADMAP.md.
The ./module is routeless — no server-only actions and no per-turn dice. Every special card is a
rule, so it lives in the engine: the Pub and Observatory interludes are engine-level turn locks
(pendingPubBuy / pendingDraw), and the Observatory draw is a pure engine action (the stack top is
deterministic, shuffled once at setup). So this package needs no fastify dependency.
Layout
src/
engine/ the pure rules core — no I/O, no Date, no Math.random. 100% coverage gate.
core/ constants, domain types, errors (rulebook-sourced values)
actions/ one file per move: buy, addToHand, playFromHand, pass, pubBuy, observatory + applyAction
internal/ shared helpers (deck, phase, scoring, displacement, specials, the kernel record()/seating)
view.ts viewFor — the redaction (opponent rubles + hands, and the draw-stack contents)
tests/ one file per concern
module/ the backend seam — the routeless GameModule: createGame wiring, parseAction, the error map
client/ the UI seam — the GameClient + the board
Board.tsx the game screen; CardRow / PlayerPanel / Results / Status the panels
art/ the "Malachite & Gilt" card art (CardFace, Salon, icons)
bot/ the AI (SP9) — decide / policy / self-play / bench. 90% coverage gate.Running it
Requires Node 22 (.nvmrc) and pnpm (the version is pinned in packageManager).
pnpm install # resolves @game-hub/* from the public registry
pnpm test # vitest + the coverage gates (engine 100%; bot 90%)
pnpm test:watch
pnpm typecheck # strict TS across all four subpaths
pnpm lint # ESLint 9 flat config — real hazards, not a second typecheck
pnpm format:check # Prettier (hand-wrap Markdown; *.md is Prettier-ignored)
pnpm build # tsc → dist/ (JS + .d.ts + inline-source maps), what publishConfig points at
pnpm pack:smoke # pack, install outside this repo, play a game under plain node, typecheck a consumer⚠️ Relative imports in the shipped sources carry an explicit .js extension ('../engine/index.js')
— tsc emits them verbatim and Node ESM resolves neither extensions nor directories, so extensionless
ones would produce a tarball that throws on a host's first import while every command above stayed green.
pack:smoke is the only check that runs against the built dist/, and it is what catches that. Test
files under src/**/tests/ are excluded from the build and keep the extensionless style.
CI (.github/workflows/ci.yml) runs exactly those, in that order, on a runner with no access to the
platform monorepo — it installs @game-hub/* from the registry with --frozen-lockfile.
Using this package in a host, before it is published: pnpm pack here, then depend on the tarball —
the same vendored-tarball loop the hub uses for the Labyrinth pilot.
Rules, and what's original
The rulebook PDF is not in this repository — it is copyrighted, so it stays local (gitignored) and
the code cites page numbers instead (see reference_materials/README.md).
Mechanics and card names aren't copyrightable; the illustrations are, so every asset in src/client/art
is drawn fresh in the house style and nothing is traced.
Licence
BSD-3-Clause (see LICENSE). Not affiliated with or endorsed by the rights holders of
Saint Petersburg.
