# Decision log Choices made while implementing the specs, recorded so they can be challenged later. Each entry states what the specs left open, what was chosen and why. Format: `YYYY-MM-DD` · area · decision. --- ## 2026-09-13 · Rules · Turn passes *past* a defender who took GAMEPLAY.md says "after drawing cards move is passed to next player in clockwise order", but does not say what happens when the defender took the cards. **Decision.** The defender who took is skipped: the next attacker is the player after the defender. In 1v1 this means the same player attacks again — the standard rule, and the one that makes taking a real cost. ## 2026-09-13 · Rules · Interpretation of the add-card limits GAMEPLAY.md gives three limits on attacking cards. The second one ("there should not be more attacking cards than card amount in defending player's hand") is ambiguous about *when* it is measured. **Decision.** A card may be added only if all three hold at the moment of adding: 1. `table.length < 6` (`< 5` on the very first turn of the game); 2. `unbeatenCount < defenderHandSize` — where `unbeatenCount` counts attack cards with no defence on them, and `defenderHandSize` is the defender's hand *at that moment*. Once the defender has declared a take, their hand no longer shrinks, so this freezes naturally; 3. its rank matches the rank of some card already on the table, attack or defence alike. The first attacking card of a turn is exempt from (3) — there is nothing on the table to match. ## 2026-09-13 · Rules · Attack order is serialised In a real game attackers may throw cards in at any moment, concurrently with the defender thinking. Modelling genuine concurrency buys nothing for 1v1 against a bot and makes the state machine much harder to test. **Decision.** The core is a strict turn-based state machine with an explicit "who may act now" set. Attackers are polled for additions only when there are no unbeaten cards on the table, or after a take has been declared. This is behaviourally identical for 1v1 and extends cleanly to N players. ## 2026-09-13 · Architecture · Rules core is engine-independent **Decision.** Everything under `assets/scripts/core/` is plain TypeScript with no `cc` import, no `Component`, no scene access, and no wall-clock time. The state machine is a pure reducer `apply(state, action) -> { state, events }` over a seeded RNG. **Why.** It makes the entire rule set verifiable with `npm test` in a second, on any machine, with no editor running — which is the only practical way to build this unattended. It also leaves the door open for server-authoritative multiplayer later without a rewrite. ## 2026-09-13 · Architecture · Bots consume a restricted `PlayerView` **Decision.** A bot never receives `GameState`. It receives a `PlayerView`: its own hand, the table, the trump card, counts (deck size, each opponent's hand size) and the discard count. **Why.** It makes cheating structurally impossible rather than a matter of discipline, and a `PlayerView` is exactly the payload a future network protocol would send to a client. ## 2026-09-13 · Architecture · Savegame sections are self-registering ARCHITECTURE.md asks for a save composed of independent classes, keyed by class name. **Decision.** Sections implement `ISaveSection { readonly key; save(); load(data) }` and register with `SaveManager`. The key is an explicit string constant, *not* `constructor.name`, because minification in release builds renames classes and would silently orphan every existing save. ## 2026-09-13 · Presentation · 1080×1920 design resolution, fit-width UI.md requires portrait, 4:3 to 21:9. ART.md fixes the art budget at 1080 px wide. **Decision.** Design resolution 1080×1920 with `fitWidth = true`, `fitHeight = false`, so the width is always exactly 1080 design units and taller screens simply reveal more vertical space. Layout is anchored with widgets to top/bottom, the play area centred, and the safe-area inset respected on the top and bottom bars. ## 2026-09-13 · Art · Vector deck from `local/`, licence recorded `local/Playing Cards` holds the Chris Aguilar "Vector Playing Cards" set (also mirrored on Wikimedia Commons), which is **LGPL-3.0**. **Decision.** Use the 36 faces needed for a durak deck, rendered from the SVG sources so the downscale to the 1080-wide budget stays sharp. Attribution and licence text are recorded in `assets/resources/cards/ATTRIBUTION.md`. The card back, table surface and UI chrome are not in that set and are authored for this project. ## 2026-09-13 · Presentation · The screen renders state, it does not replay events The rules emit an event stream, which invites an animation queue that replays it. **Decision.** `GameScreen.render(state)` lays the whole table out from the current state and tweens every card node to its new place. A card node stays bound to its `Card` for as long as that card is visible. **Why.** "The six of clubs moved from my hand to the table" then needs no special case — it is the same node arriving at a new position. An event-driven queue has to choreograph every transition by hand and desynchronises the moment one case is missed. The event stream is still there for sound and for one-off flourishes. ## 2026-09-13 · Presentation · Card art lives in the `resources` bundle The first attempt gave `CardLibrary` a `SpriteAtlas` reference to the auto-atlas (`cards.pac`) and asked it for frames by name. The build came out with no card textures at all. **Decision.** The 36 faces and the back live in `assets/resources/cards` and are loaded with `resources.loadDir`. `Bootstrap` preloads the folder in the loader scene, so the lobby never waits. **Why.** A `.pac` is a build-time packing instruction, not a runtime asset: it packs whatever sprite frames the build already pulled in, and it cannot be loaded and queried by name. Nothing referenced the individual frames, so nothing was packed. The `.pac` still earns its keep — all 37 sprites end up in one 2048² texture, so the whole table costs one draw call. ## 2026-09-13 · Presentation · Button clicks are wired in code **Decision.** Buttons are exposed as `@property(Button)` and subscribed to in `onLoad` rather than through the scene's `clickEvents` list. **Why.** The wiring stays visible in the source, and renaming a handler becomes a compile error instead of a button that silently stops working. (The editor tool server also cannot write array-typed properties, which is how `clickEvents` is stored — so this was the only option available to automation anyway.) ## 2026-09-13 · Presentation · Interface text is English, collected in one file **Decision.** Every player-visible string is in `assets/scripts/ui/common/Strings.ts`, in English. **Why.** The specs, the code and the docs are all in English, and localisation is out of scope for iteration 1. Keeping the strings together means switching language — or adding one — is a change to that one file rather than a hunt through scenes and components. Worth revisiting before release, given the game is *durak*. ## 2026-09-13 · Presentation · One tap is the whole interaction **Decision.** Tapping a card plays it. When defending, the attack being answered is worked out from the card rather than selected separately. The logic lives in `core/rules/intent.ts`, not in the component. **Why.** Select-then-target doubles the taps for no gain, because the serialised rules leave only one unanswered attack at a time. Putting it in the core means the one piece of interface logic that can silently reject a legal move is covered by headless tests, including a property test that it never proposes an action the rules would refuse.