Обе сцены собраны через 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>
7.6 KiB
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:
table.length < 6(< 5on the very first turn of the game);unbeatenCount < defenderHandSize— whereunbeatenCountcounts attack cards with no defence on them, anddefenderHandSizeis the defender's hand at that moment. Once the defender has declared a take, their hand no longer shrinks, so this freezes naturally;- 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.