Files
fool-cardgame/docs/TECHNICAL.md
T
ftcandClaude Opus 5 4f3a26dc45 M5-M7: сцены, лобби, экран игры и цикл матча
Обе сцены собраны через Code Mode, не руками.

- loader: поднимает модули, прогревает арт карт, уходит в lobby
- lobby: обе UI-ветки в одной сцене (по ARCHITECTURE.md), матч — это
  смена видимого корня, а не загрузка сцены
- GameScreen рисует текущее состояние и доводит узлы твинами, а не
  проигрывает поток событий: узел карты живёт вместе со своей картой,
  поэтому "шестёрка ушла из руки на стол" не требует отдельного случая
- один тап = ход целиком; логика вынесена в core/rules/intent.ts
  и покрыта тестами, в том числе проверкой, что она никогда не предложит
  ход, который правила отвергнут
- арт карт переехал в assets/resources: .pac — это инструкция упаковки
  на сборке, а не рантайм-ассет, и без ссылок на кадры в сборку
  не попадало ничего. Теперь все 37 спрайтов в одной текстуре 2048x2048
- клики кнопок подписываются в коде, а не через clickEvents сцены
- строки интерфейса собраны в одном файле

Сборка web-mobile проходит чисто, все ссылки в сцене проверены.
Вживую не запускалось: браузер в этом окружении не поднимает
главный цикл движка. Подробности — в docs/TECHNICAL.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 11:30:28 +03:00

245 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Technical structure
How this project is laid out, what each layer is allowed to do, and how to work
on it. Companion to [ARCHITECTURE.md](ARCHITECTURE.md), which states the *intent*;
this file states the *implementation*.
- Engine: **Cocos Creator 3.8.8**, TypeScript.
- Targets: Web (mobile browser), Android, iOS. Portrait only.
- Editor automation: the `cocos-code-mode` extension — see
[CODE_MODE_SETUP.md](CODE_MODE_SETUP.md).
## Layering
Four layers, with dependencies pointing strictly downward. A layer may import
from the layers below it and never from the ones above.
```
ui/ Cocos components: scenes, prefabs, views, input, animation
↓
app/ bootstrap, service locator, scene transitions
↓
modules/ engine-aware services: savegame, (later) audio, analytics
↓
core/ pure rules and bots — no `cc` import anywhere
```
The rule, enforced by `npm run check:core-purity`: **nothing under
`assets/scripts/core/` may import from `cc`**, and under
`assets/scripts/modules/` only the adapters explicitly listed in that script
may. Everything else is plain TypeScript, runnable under Node, which is what
makes the rules and the save format testable without the engine.
## Folder map
```
assets/
scenes/
loader.scene module init and art preload, then → lobby
lobby.scene Canvas > Background, Safe > LobbyRoot | GameRoot
prefabs/
card.prefab one playing card, face or back
resources/
cards/ 36 faces + back + cards.pac, loaded by path
art/
ui/ table surface, nine-slice panels and buttons
scripts/
core/ PURE TS — no engine imports
cards/ Suit, Rank, Card, Deck
rules/ GameState, actions, events, the reducer
bot/ PlayerView projection, Bot contract, BasicBot
match/ Match (stateful shell), runMatch (headless driver)
util/ seeded RNG, shuffle
modules/
save/ SaveManager, ISaveSection, StatsSection,
IStorage + MemoryStorage, CocosStorage (engine)
app/ Bootstrap, ServiceLocator, SceneRouter
ui/
MainFlow.ts owns both screens and moves between them
lobby/ LobbyScreen: statistics and the New Game button
game/ GameScreen, CardView, CardLibrary
common/ Theme (layout, palette, timings), Strings
tests/
core/ vitest specs against assets/scripts/core
support/ position fixtures and the state invariant checker
tools/ measuring tools, not part of the suite
art-src/cards/ vector sources for the card faces (LGPL, kept)
docs/ specs, this file, plan, decisions
local/ scratch art storage; contents get consumed
scripts/ build and check tools run through npm
extensions/cocos-code-mode/ editor tool server
```
## The rules core
A deterministic state machine. The only entry point is a pure reducer:
```ts
apply(state: GameState, action: Action): { state: GameState; events: GameEvent[] }
```
- `GameState` is plain data: hands, deck, trump card, table slots, discard,
phase, whose turn it is, and the RNG state. It is serialisable, which makes
every test a fixture and every bug reproducible from a seed.
- `Action` is what a participant *wants* to do — `Attack`, `Defend`, `Take`,
`Pass`. Legality is decided by the core, never by the caller; `legalActions()`
exists so the UI can grey out impossible taps, not as the source of truth.
- `GameEvent` is what *happened* — `CardDealt`, `CardPlayed`, `CardBeaten`,
`TableTaken`, `TableDiscarded`, `TurnPassed`, `PlayerOut`, `GameOver`. The view
layer animates the event stream; it never inspects `GameState` directly for
anything it is not allowed to see.
- Randomness comes from a seeded PRNG carried inside the state. Same seed plus
same action sequence always reproduces the same match.
## Bots
A bot is `decide(view: PlayerView) => Action`. `PlayerView` is a projection of
`GameState` containing only legitimately visible information — own hand, table,
trump, opponents' hand *sizes*, deck and discard counts — plus the list of legal
actions. `BasicBot` implements the policy in [GAMEPLAY.md](GAMEPLAY.md).
`Match` wraps the reducer with the current state and an event subscription; the
game screen and the headless `runMatch` driver both hold one. As a reference
point, 5000 `BasicBot` self-play matches split roughly 43/43/14 between the two
seats and draws, with the opening attacker losing about 37% of the time —
`npm run stats:bot` reprints these.
## Savegame
Per [ARCHITECTURE.md](ARCHITECTURE.md): `SaveManager` keeps a registry of
`ISaveSection` implementations. `save()` enumerates them and builds
`{ [section.key]: section.save() }`; `load()` parses and dispatches each payload
back to the matching section. A missing or unparsable section is skipped with a
warning rather than failing the whole load, so a bad release cannot brick a save.
Keys are explicit string constants, not class names — release builds minify.
The document written is `{ version, sections: { <key>: <payload> } }`. Storage
sits behind `IStorage`, so all of the save logic is testable headlessly;
`CocosStorage` is the only file in the module that imports the engine, and it
falls back to an in-memory store when the device refuses to persist (private
browsing, a full quota) rather than taking the game down.
## The lobby scene
Both screens live in one scene, per ARCHITECTURE.md, so starting a match is a
change of visible root rather than a scene load:
```
Canvas (1080×1920, Canvas + MainFlow)
Camera orthographic, UI_2D only, clears to table green
Background table_bg, widget-stretched
Safe cc.SafeArea — everything tappable is inside it
LobbyRoot LobbyScreen: title, statistics panel, New Game
GameRoot GameScreen + CardLibrary (inactive until a match starts)
Deck / OpponentLabel / Message / CardLayer / ActionBar / QuitButton
ResultOverlay dimmer, result panel, Back
```
`GameScreen` renders from the current `GameState` rather than replaying the
event stream — see docs/DECISIONS.md. Card nodes are pooled and stay bound to
their `Card` while it is visible, so movement is a tween to a new position.
Buttons are `@property(Button)` references subscribed in `onLoad`; the scene's
`clickEvents` lists are deliberately empty.
## Conventions
- TypeScript, 4-space indent, `PascalCase` for types, `camelCase` for members,
`_private` for backing fields (matching Cocos's own style).
- One exported class per file, file named after it.
- Cocos components live only in `ui/`; they are thin and hold no rules logic.
- No `any` in `core/`. `strict` is off in the Cocos tsconfig for engine-side
convenience, but the test harness type-checks `core/` in strict mode.
## Workflows
### Tests
```bash
npm test
```
Runs vitest over `tests/core` against `assets/scripts/core`. No editor required.
The soak specs play several thousand complete matches and re-check every state
invariant after every single action, so a rule regression surfaces at the move
that caused it.
```bash
npm run typecheck # strict tsc over core/ and the tests
npm run check:core-purity # fails if core/ ever imports the engine
npm run stats:bot # BasicBot self-play distribution, not a test
```
### Driving the editor
Cocos Creator must be running with this project open. See
[CODE_MODE_SETUP.md](CODE_MODE_SETUP.md); the port changes on every editor
restart, so always read `~/.utcp_config.json`.
Scripts and other text assets are written to disk directly and picked up by the
editor's asset watcher. Scenes and prefabs are authored through the tools —
never by hand-editing the `.scene`/`.prefab` JSON.
### Art
```bash
npm run build:art
```
Regenerates every sprite in `assets/art` from source. Card faces are rasterised
from the vectors in `art-src/cards`; the card back, table and UI chrome are
authored as SVG inside [`scripts/build-art.mjs`](../scripts/build-art.mjs) and
rasterised from there, so a colour or radius is changed in one readable place
rather than in a binary.
Fixed numbers the layout depends on:
| | |
|---|---|
| Card sprite | 220 × 320 px |
| Nine-slice border | 40 px on 120 × 120 tiles |
| Table background | 540 × 960, drawn at 2× |
Card sprite frames have trimming switched off, because layout maths assumes
every card rect is exactly 220 × 320. Card faces are named `<rank>_of_<suit>`,
matching `Card.assetName` — that string is the only contract between the rules
core and the art.
Licence: the faces are LGPL-3.0 and attribution is mandatory. See
[`assets/resources/cards/ATTRIBUTION.md`](../assets/resources/cards/ATTRIBUTION.md), which
also explains what would have to change to drop the dependency.
### Builds
A headless build, which is what CI would run:
```bash
"C:/ProgramData/cocos/editors/Creator/3.8.8/CocosCreator.exe" --project "C:/Work/fool-cardgame" --build "platform=web-mobile;debug=true;startScene=<loader.scene uuid>"
```
It writes to `build/web-mobile` in about 40 seconds. Two caveats: it starts a
second editor instance, so it competes with a running editor for
`temp/logs/project.log` and can fail with a duplicate-class error — close the
editor, or build from its Build panel. And `startScene` must be passed, or the
build has no launch scene and stops on the splash.
Mobile: ASTC textures per [ART.md](ART.md), PNG for web.
### What is and is not verified
The rules, the bot and the save format are covered by `npm test` and are
verified. The presentation layer is verified only as far as this environment
allows: the project builds clean, every component reference in the scene is
populated, and all 37 card sprites pack into one 2048² texture.
It has **not** been seen running. The browser available here falls back to a
software renderer that cannot start the Cocos main loop (`totalFrames` stays at
0), so no screenshot of the actual game was possible. To check it by eye, open
the project in Cocos Creator and press Preview, or serve `build/web-mobile` over
HTTP — it is a static folder and needs no server features:
```bash
npx http-server build/web-mobile -p 8123 -c-1
```