Files
fool-cardgame/docs/TECHNICAL.md
T
ftcandClaude Opus 5 51117a698c M3: модуль сохранения
По ARCHITECTURE.md: реестр независимых секций, JSON с ключом на секцию.
Ключи — явные строковые константы, а не имена классов: релизная сборка
минифицирует имена и молча потеряла бы все сохранения.

- ISaveSection, SaveManager, IStorage + MemoryStorage
- CocosStorage: единственный файл модуля, знающий про движок;
  при отказе хранилища уходит в память, а не роняет игру
- StatsSection: сыграно/побед/поражений/ничьих, текущая и лучшая серия
- терпимая загрузка: битый JSON, чужая версия, сломанная секция
  или отвалившееся хранилище не ломают остальное
- проверка чистоты расширена на modules/ со списком адаптеров

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 10:45:52 +03:00

6.7 KiB

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, 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.

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, then → lobby
    lobby.scene               main menu; hosts the match UI
  prefabs/
    card.prefab               one playing card, face or back
    ...
  art/
    cards/                    36 faces + back, atlased
    ui/                       panels, buttons, table surface
  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/
      lobby/                  lobby screen controller
      game/                   match screen: hand, table, deck, animations
      common/                 reusable widgets, safe-area root
tests/
  core/                       vitest specs against assets/scripts/core
  support/                    position fixtures and the state invariant checker
  tools/                      measuring tools, not part of the suite
docs/                         specs, this file, plan, decisions
local/                        scratch art storage; contents get consumed
extensions/cocos-code-mode/   editor tool server

The rules core

A deterministic state machine. The only entry point is a pure reducer:

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.

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: 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.

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

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.

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; 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.

Builds

Web: web-mobile template. Mobile: ASTC textures per ART.md, PNG for web.