Commit Graph

55 Commits

Author SHA1 Message Date
gamer147
693fba5003 feat(battlenode): emit engine-resolved clan/tribe on knownList entries (M-HC-4e)
Prod always emits clan (int ClanType) + tribe (comma-joined int TribeType
string, "0" for none) on every knownList entry (battle-traffic_tk2_regular
.ndjson). Source both off the resolved engine (SessionBattleEngine.PlayedCardClan/
PlayedCardTribe -> BattleCardBase.Clan/Tribe), so skill-applied clan/tribe
changes ride the wire rather than the static card-master value. Thread through
KnownListBuilder.BuildPlayedCard + PlayActionsHandler; add clan/tribe to the
KnownCardEntry DTO (always present, non-null). Node-side only; no engine edits,
drift clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 00:11:28 -04:00
gamer147
0d7136787a refactor(battlenode): retire spellboost bookkeeping, engine owns cost+spellboost (M-HC-3)
The headless engine accumulates spell-charge for real on the receive path
(each spell play runs the played card's own AddSpellChargeCount) and resolves
the discounted cost by construction, so the wire-derived spellboost-count
bookkeeping is redundant. Engine-source the knownList spellboost COUNT too
(prod-faithful) via a new SessionBattleEngine.PlayedCardSpellboost, using the
same persist-post-play zone search as PlayedCardCost (SpellChargeCount survives
PlayCard; only ctor/ReturnCard zero it).

- Delete IdxToSpellboost/SpellboostMap/GetSpellboostMap/RecordSpellboostFrom
  (BattleSessionState) and MineAlterSpellboosts (KnownListBuilder); token/choice/
  copy identity maps are untouched.
- BuildPlayedCard takes an engine-sourced spellboost int (drops spellboostMap).
- Seed BattleLogManager fusion lists headless (the per-frame filter cleanup
  NREs on null EnemyFusionCard when a fanfare card registers a CalledCreateFilter)
  so real spell-charge grantor plays resolve.
- Add committed real-charge regression tests (no SeedHandCardSpellboostCost seam):
  one grantor play accumulates +1 on the reducer -> cost 5->4, count 1, persisting
  post-play; handler emits cost 4 + spellboost 1 engine-sourced.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 21:48:50 -04:00
gamer147
51419d15cd feat(battlenode): emit engine-resolved cost on every knownList entry (M-HC-3)
The opponent-facing PlayActions knownList now carries the engine-RESOLVED
play-time cost (KnownCardEntry.cost), sourced from the headless shadow engine's
PlayedCost on the just-resolved card. This closes the spellboost cost-desync BY
CONSTRUCTION: the engine already knows the true discounted cost (spellboost +
board modifiers folded in), so no bookkeeping is needed.

- DTO: add non-nullable cost to KnownCardEntry (prod emits cost 45/45).
- SessionBattleEngine.PlayedCardCost(seat, idx, fallback): finds the resolved
  card by engine Index across in-play/cemetery/hand zones and returns PlayedCost
  (captured by PlayCard at resolution == discounted Cost), degrading to fallback
  when the engine is not owned/ready.
- PlayActionsHandler sources the played card's cost from ctx.Engine (ShadowIngest
  already resolved the play before the handler runs). Spellboost-map plumbing
  stays for now; Task 6 (M-HC-3b) retires it.
- Validation: engine-read test (charge-seeded reducer 101314020: base 5, cost
  5/1/0 at charge 0/4/5) + handler-emit test asserting knownList[0].cost == 1
  (discounted, not base 5) with non-vacuity. Board-dependent (when_evolve_other)
  case deferred to M-HC-4 (evolve not yet headless); cost is read off the resolved
  engine so board modifiers are captured by construction once their ops resolve.
- Harness: promote alt vanilla follower id (101211120) to AltVanillaFollowerId.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 21:18:29 -04:00
gamer147
e982300c6d feat(battlenode): inject SessionBattleEngine into BattleSession in pure shadow (Phase 2 N1 exit)
The engine is constructed per session, seated once from the master seed + both
shuffled decks (F-N-5), and fed each frame via ShadowIngest — all inside a
try/catch in ComputeFrames so a shadow failure can never break live dispatch
(ND1/ND6). Routes still come from the existing handlers: wire output is
byte-for-byte unchanged. FrameDispatchContext gains the Engine ref for N2+.

csproj: PrivateAssets=compile on the engine ref so its global-namespace type
surface (MessagePackSerializer, UserConfig, UserCard, ChallengeConfig, ...) does
not leak transitively into SVSim.EmulatedEntrypoint (which references BattleNode)
and collide with that project's own types; the runtime DLL still flows.

All 238 BattleNode unit tests pass; EmulatedEntrypoint builds clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 15:35:35 -04:00
gamer147
13f902ce58 fix(battlenode): emit real spellboost count in played-card knownList
The node hardcoded knownList.spellboost=0 on every played card. Prod sends
the true accumulated count, which the client reads straight into the card's
cost model; with 0 the opponent computes the card at full price and silently
rejects the play in OperateReceiveChecker.IsPlayCard (PP-over -> ConductError
-> NullOperationCollection -> no render/echo), desyncing the board.

Mine spellboost-count changes from the sender''s orderList alter ops
(MineAlterSpellboosts: a/s/h ops), accumulate per-side idx->count in
BattleSessionState (RecordSpellboostFrom), and surface the current count on
the played card via BuildPlayedCard. Recorded from the authoritative
PlayActions only (never the Echo) and folded in AFTER the played card is
built, since a card''s cost is fixed as it leaves hand and a play that grants
spellboost targets the rest of the hand.

Also adds a [sio-in-body] full-body inbound log to RealParticipant to capture
both clients'' re-simulated responses for PvP RNG verification.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 13:51:40 -04:00
gamer147
2d32051cc0 refactor(battlenode): key dispatch on OpponentIsAckOnly, drop per-frame BattleType switch
Behavior-identical; 231 BattleNode tests green with ZERO test changes.

The 10 handler arms no longer switch on BattleType:
- 4 Bot arms gate on the new FrameDispatchContext.OpponentIsAckOnly
  (Other is not IHasHandshakePhase) — the participant property the audit asked for.
- 6 relay arms drop the Type == Pvp guard; it was redundant with BothSidesAfterReady()
  (only a two-real-player session has both handshake phases). Its doc now records that.
- FrameDispatchContext.Type removed (+ the Type = Type in BuildContext). BattleSession.Type
  stays for the session-level drop cascade.

Zero test churn because the stubs already encode the split: FakeRealParticipant/ProbeParticipant
implement IHasHandshakePhase, the bot stub FakeParticipant doesn't, and NewBotSession uses it as
the opponent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 08:20:56 -04:00
gamer147
9ff8948903 docs(battlenode): document four latent low-tier hygiene hazards
Comment-only; behavior-preserving; 231 BattleNode tests green.

- OutboundSequencer._archive: name the unbounded-per-match growth + ack-prune point.
- NodeCrypto.BuildAes: SECURITY remarks on key-derived IV reuse + base64 entropy loss;
  warn against caching the session key.
- MatchContext/BattlePlayer: FOOTGUN notes on reference-based record equality over the deck list.
- RecordTokensFrom: TRUST note on isSelf/idx overwrite; name the idx>deckCount guard for
  untrusted peers (not added — trusted-LAN today).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 08:11:13 -04:00
gamer147
9b8a7f1e37 refactor(battlenode): name sender-only vs both-sides handshake checks (§D)
Behavior-preserving; 231 BattleNode tests green.

FrameDispatchContext.BothAfterReady() -> BothSidesAfterReady() (7 call sites). The
4 inline `SenderPhase == AfterReady` checks in TurnEndHandler/TurnEndFinalHandler now
read a new SenderIsAfterReady property. Both carry cross-referencing docs so the
Bot-arm (sender-only) vs PvP-arm (both-sides) distinction is explicit at the type.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 07:49:27 -04:00
gamer147
7e167b1cef refactor(battlenode): centralize inbound wire-key literals in WireKeys (§C)
Behavior-preserving; 231 BattleNode tests green (capture-conformance suite drives
real prod frames, so a wrong constant would fail).

New Sessions/Dispatch/WireKeys.cs holds the 28 inbound-body read keys (orderList /
keyAction / targetList / uList field names). KnownListBuilder, PlayActionsHandler,
EchoHandler, and BattleFrames.ExtractIdxList now read through it instead of repeated
inline strings, so a parse-side typo ("isSelf" vs "IsSelf") can no longer silently
degrade token resolution. Outbound [JsonPropertyName] attributes left as-is (already
single-source per DTO).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 07:30:02 -04:00
gamer147
3e8901eec3 refactor(battlenode): split BattleSessionPhase into HandshakePhase + SessionLifecycle
Behavior-preserving; 231 BattleNode tests green.

One enum conflated two axes. Split:
- HandshakePhase (per participant): AwaitingInitNetwork..AfterReady. On
  IHasHandshakePhase.Phase, FrameDispatchContext.SenderPhase, the handler gates.
- SessionLifecycle (per battle): Active | Terminal. On the renamed
  BattleSessionState.Lifecycle (was SessionPhase, defaulting to a handshake value)
  and BattleSession.Lifecycle (was Phase). Reads are only != Terminal, so the
  Active default is behavior-identical.

OpponentTurn was dead (never assigned) -> dropped. BattleSessionPhase deleted; the
two axes can no longer be cross-assigned.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 07:21:59 -04:00
gamer147
7d4da69f22 refactor(battlenode): low-churn §B/§D/§E/§F quality cleanups
Behavior-preserving; 231 BattleNode tests green.

- §D: MsgEnvelope.Try -> RetryAttempt (drops keyword-escape; wire key stays "try");
  SocketIoFrame.AckResponse arg -> pubSeqEcho.
- §B: Gungnir.EmitInterval -> BattleNodeOptions.AliveEmitInterval (unused literal
  moved to its config home); deck-idx 4L -> InitialHand.Length + 1.
- §E: shared Wire.WireJsonOptions.CamelCase replaces the duplicated camelCase
  JsonSerializerOptions in EngineIoHandshake and MsgEnvelope.
- §F: do-NOT-consistency-fix polarity notes on TurnEndFinalHandler (From wins)
  and RetireKillHandler (From loses).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 23:06:44 -04:00
gamer147
e70f32db79 refactor(battlenode): close §A boolean-blindness items (MinedToken, Stock, KeyActionType)
Behavior-preserving; 231 BattleNode tests green.

- MinedToken record struct replaces the transpose-prone (int Idx, long CardId,
  CardOwner IsSelf) tuple returned by KnownListBuilder.Mine*. Positional deconstruct
  keeps the Record*From call sites unchanged.
- enum Stock { Normal, Bypass } replaces the negative `bool noStock` on
  IBattleParticipant.PushAsync and DispatchRoute, threaded through both participants,
  BattleSession, and all handler construction sites.
- enum KeyActionType mirrors the client's SendKeyActionDataManager.KeyActionType;
  the StripKeyActionForOpponent guard compares named values, KeyActionEntry.Type is
  the enum (wire-identical via JsonNumberEnumConverter).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 22:53:32 -04:00
gamer147
a3e445cf2f refactor(battle-node): replace int IsSelf with CardOwner enum on mined-token tuples
MineAddOps/MineChoicePicks/MineCopyTokens return types and all
extraction casts changed from int to CardOwner. The 4 routing
comparisons in BattleSessionState now read isSelf == CardOwner.Self
instead of isSelf == 1.

No wire or behavioral change — CardOwner was already in use on the
wire-facing side (OppoTargetEntry, UnapprovedCardEntry); this extends
it to the internal mining path so the bare-int transpose risk is gone.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-04 22:25:01 -04:00
gamer147
24180d5b4b refactor(battle-node): de-magic wire flags and scattered constants
Quality pass from the 2026-06-04 BattleNode review (audit in the outer
repo). All changes are behavior-preserving — identical wire bytes,
verified by the full 1008-test suite staying green.

- Name scattered magic numbers: crypto key/IV lengths, outbound-sequencer
  base, WS receive buffer / EIO ping / SID length, polite-close timeout,
  upgrade-credential keys, battle-id digit math, deterministic-turn spin.
- resultCode = 1 -> (int)ReceiveNodeResultCode.Success across body records.
- Pong "3" -> EngineIoPacketType.Pong; remove dead NoOpBotParticipant.Touch
  (replace with #pragma warning disable CS0067).
- Wire-flag enums, serialized as numbers via JsonNumberEnumConverter:
  turnState -> TurnState{First,Second}, isSelf -> CardOwner{Opponent,Self},
  open -> ChoiceVisibility{Hidden,Open}.
- isOfficial / isInvoke -> bool / bool? via new NumericBoolJsonConverter
  (reads/writes 0/1; TDD'd). Scoped to the BattleNode wire boundary only;
  MatchContext and the HTTP/AI-start path stay int (AI-start uses -1 as a
  sentinel, so it is not boolean).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 20:46:09 -04:00
gamer147
3f5d97cb2f feat(battle-node): derive Matched.seed + Ready.idxChangeSeed from master seed
InitBattle now emits Stable(master) as the shared effect seed and the master-
shuffled deck as selfDeck; Swap emits each recipient's per-side IdxChange seed.
BattleSession exposes + logs the master seed per battle for future replay.
Updated lifecycle/dispatch/integration tests (deck assertions now permutation-
based since selfDeck is shuffled).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 18:20:51 -04:00
gamer147
6f7fcfe28e feat(battle-node): per-battle master seed + node-side deck shuffle
GetOrSeedDeckMap now seeds from a Fisher-Yates shuffle of the deck keyed by the
per-battle MasterSeed, so the reveal map and the wire selfDeck share one
shuffled order. Updated the existing build-order test to the shuffle semantics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 18:14:14 -04:00
gamer147
75f3d8ea5b revert(battle-node): remove real-spin logic (CountHiddenDraws + per-frame spin)
Two-sided capture (data_dumps/captures/battle_test/rng, 2026-06-04) showed the
receiver already reproduces uList-relayed deck fetches (Hoverboard) and turn
draws on its own shared stream, so the emitted spin=1 double-cranked and desynced
the clients by 1. Residual spin is ~0 for the current card pool. Reverts 63cb324
and 617714e; back to the prior correct spin:0 behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 16:07:08 -04:00
gamer147
617714ebea feat(battle-node): emit real spin per-frame on forwarded PlayActions
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 15:13:47 -04:00
gamer147
63cb3248b4 feat(battle-node): CountHiddenDraws — hidden shared-RNG draw tally for real spin
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 15:13:47 -04:00
gamer147
a0aa58cfbe feat(battle-node): relay uList on PvP PlayActions
Forwards the sender's deck-sourced summons/fetches to the opponent
(closes the spin-independent slice of direct-to-field summons). uList
coexists with the synthesized knownList in the same frame.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:18:20 -04:00
gamer147
c0309061fa feat(battle-node): UnapprovedCardEntry + RelayUList pure transform
Verbatim uList relay shape + transform (deck-sourced summons/fetches),
mirroring RenameTargets. Not yet wired into the handler.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:17:10 -04:00
gamer147
b6edfbcf15 feat(battle-node): reveal copy tokens on play via baseIdx resolution
PlayActionsHandler + EchoHandler now call RecordCopyTokensFrom (ordered
after plain/choice mining) to resolve a copy add's baseIdx against the
side's live idx->cardId map and record copyIdx->cardId. A copy played in a
later (or same) frame synthesizes a knownList instead of degrading.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 10:11:34 -04:00
gamer147
f9c7e6124b feat(battle-node): resolve copy-token cardIds from baseIdx (pure)
KnownListBuilder.MineCopyTokens resolves a copy add's baseIdx against the
actor's own idx->cardId map (self/other by isSelf), yielding (idx,cardId,
isSelf). Skips concrete/choice adds, string (private-group) baseIdx, and
unknown sources (degrade). Third token-reveal slice.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 10:09:36 -04:00
gamer147
5c3835f4fd feat(battle-node): reveal choice/Discover tokens to opponent
Choice/Discover-into-hand fanfares add a candidates-only token to hand; the
chosen cardId rides keyAction.selectCard on the generating play, not the
orderList add op. Record idx->chosenCardId at generation (candidate-membership
join) so the later play reveals the real identity via the existing
BuildPlayedCard path; forward {type,cardId} to the opponent and strip
selectCard for hidden (open:0) picks (pass through for open:1, provisional).

- KnownListBuilder.MineChoicePicks + StripKeyActionForOpponent (pure)
- BattleSessionState.RecordChoicePicksFrom (reuses IdxToCardId, no new state)
- PlayActionsBroadcastBody.keyAction + KeyActionEntry/SelectCardEntry
- PlayActionsHandler wires both; EchoHandler unchanged (picks ride the send)

Tests (TDD red->green): 8 KnownListBuilder + 2 dispatch + 2 conformance
(shape-locked to tk2_regular L151 generation / L193 reveal). Full suite 976/0.

Spec: docs/superpowers/specs/2026-06-04-battle-node-choice-token-reveal-design.md

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 08:53:48 -04:00
gamer147
62251482e4 feat(battle-node): cross-side gift + Echo-frame token mining
Close the two generated-token gaps that desynced PvP live test #3 (the
Forestcraft Fairy), both sourced from the 2026-06-03 decomp-validation table.

- MineAddOps now returns (idx, cardId, isSelf) and no longer drops isSelf:0.
  isSelf is the sender's perspective tag on CardObj.IsPlayer (RegisterToken.cs:22)
  and a card has one CardObj.Index, so an isSelf:0 add is the opponent's card.
- New shared BattleSessionState.RecordTokensFrom routes isSelf:1 -> sender,
  isSelf:0 -> opponent (the gift lives in the recipient's map, consulted when
  they play it). PlayActionsHandler delegates to it.
- EchoHandler now mines via the same helper but still returns no routes. An
  Echo's orderList carries the same add-op shape as a send (MakeEchoData ->
  MakeCommonSendAndEchoCardData), so MineAddOps applies verbatim; mining != relaying.

Choice/copy/private-group adds stay skipped (no concrete cardId). Full solution
963/963 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 07:59:46 -04:00
gamer147
d8b5ef950d feat(battle-node): reveal generated tokens on play via remembered identity
PlayActionsHandler mines add ops into BattleSessionState.RecordToken each
frame; a token played in a later frame now synthesizes a knownList from the
remembered cardId instead of degrading. Bullet-3 audit F1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 23:36:44 -04:00
gamer147
b6af8bfb7d feat(battle-node): mine generated-token cardIds from orderList add ops
KnownListBuilder.MineAddOps extracts (idx,cardId) from isSelf:1 add ops,
skipping cross-side gifts and choice tokens. Bullet-3 audit F1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 23:30:47 -04:00
gamer147
ac78e809cd refactor(battle-node): clear residual scripted-bot prose from comments/docs
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 20:52:41 -04:00
gamer147
ba18790156 refactor(battle-node): rename ScriptedLifecycle->ServerBattleFrames, ScriptedProfiles->BattleFrameDefaults
Pure rename. These hold the shared server-authored frame builders used by every
battle mode's handshake/mulligan dispatch — the 'Scripted' name was a historical
accident that hid the PvP/Bot crossover. No behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 20:36:32 -04:00
gamer147
8085119439 refactor(battle-node): tidy residue after scripted dispatch-arm removal
Remove the now-unused SVSim.BattleNode.Lifecycle using from
FrameDispatchContext (it was only needed for ScriptedLifecycle inside
the deleted IsScriptedBot helper) and reword the SenderPhase doc comment
so it no longer references the removed dispatch-test scripted-bot stub.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 20:06:25 -04:00
gamer147
ca9ad5db8f refactor(battle-node): remove scripted-bot test-stub arms from dispatch handlers
The IsScriptedBot(ctx.From) forwards in JudgeHandler/TurnStartHandler/TurnEndHandler
and the 'if Type==Scripted' raw-forward only ever fired for ScriptedBotParticipant
emissions; NoOpBot (Bot mode) never emits, so they are dead. Routing is now purely
PvP-vs-Bot. Drops the IsScriptedBot helper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 20:00:57 -04:00
gamer147
e98bd10dbe fix(battle-node): reflect PvP Judge back to its sender (turn handover)
Live two-client run (data_dumps/captures/battle_test) exposed a turn-handover
stall: ending a turn on client A made BOTH clients show A's turn again; the
opponent never got a turn. Root cause: JudgeHandler routed the {spin:0} Judge to
ctx.Other. The client rule is 'receive opponent TurnEnd -> SendJudge', so the
PASSIVE player (the one taking over the turn) is the Judge sender, and 'receive
Judge -> ControlTurnStartPlayer' starts the RECEIVER's turn. Routing to ctx.Other
delivered the Judge to the player who had just ended their turn, restarting it in
a closed loop while the taker-over sat on 'Opponent's Turn'.

Fix: the PvP Judge {spin} reflects back to ctx.From (the sender / turn taker-over),
matching the Bot arm's existing 'Judge to sender only' handover. The sender then
emits TurnStart, which relays to the opponent as {spin}. Updated the dispatch unit
test and the PvpHandshakeAndGameplay integration test to the real handover order
(passive sends Judge -> receives it back -> sends TurnStart -> opponent sees it).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 18:45:17 -04:00
gamer147
c360d639f2 refactor(battle-node): address final-review minor notes (comments + test backfill)
- PlayActionsHandler doc: drop the phantom 'with a debug log' (handlers are
  stateless singletons with no logger); say token plays degrade silently.
- KnownListBuilder.ExtractMoveTo doc: note first-match-wins semantics and the
  send-side==recv-side 'to' assumption pending recv-capture confirmation.
- KnownListBuilderTests: add multi-move first-match coverage and the
  in-deck-but-no-matching-move null branch for BuildPlayedCard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 18:26:07 -04:00
gamer147
f9c671c089 feat(battle-node): TurnEndActionsHandler emits empty body to opponent in PvP
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 18:11:41 -04:00
gamer147
58994a53c9 feat(battle-node): JudgeHandler emits {spin:0} to opponent in PvP
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 18:09:44 -04:00
gamer147
3c8a00c928 feat(battle-node): TurnEndHandler emits {turnState:0} to opponent only in PvP
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 18:07:44 -04:00
gamer147
6e85a6b2db feat(battle-node): TurnStartHandler emits {spin:0} to opponent in PvP
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 18:05:15 -04:00
gamer147
6b580c622d feat(battle-node): EchoHandler consumes Echo instead of relaying
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 18:03:19 -04:00
gamer147
506d286529 feat(battle-node): PlayActionsHandler synthesizes knownList (vanilla deck-card slice)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 17:59:54 -04:00
gamer147
030d3b8057 feat(battle-node): KnownListBuilder pure transforms (knownList synth, target rename)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 17:56:12 -04:00
gamer147
b295fd8f09 feat(battle-node): per-side idx->cardId map on BattleSessionState
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 17:53:32 -04:00
gamer147
503c382646 refactor(battle-node): extract ForwardWhenBothReadyHandler; share handler instances via BuildHandlers 2026-06-03 14:33:26 -04:00
gamer147
db2f711894 refactor(battle-node): extract JudgeHandler
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 14:30:40 -04:00
gamer147
aacd7b56ad refactor(battle-node): extract TurnStartHandler
Unions the two legacy TurnStart arms (IsRealForwardableFromScripted case 11 +
BothAfterReady case 12) into TurnStartHandler. Both arms produce (Other, Env, false)
with no extra guards or state mutations — union is behavior-equivalent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 14:27:17 -04:00
gamer147
c03fb3c139 refactor(battle-node): extract RetireKillHandler
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 14:24:35 -04:00
gamer147
d35818360f refactor(battle-node): extract TurnEndFinalHandler 2026-06-03 14:21:54 -04:00
gamer147
538099ff4b refactor(battle-node): extract TurnEndHandler 2026-06-03 14:20:25 -04:00
gamer147
477faf3df3 refactor(battle-node): extract SwapHandler (mulligan barrier) 2026-06-03 14:13:26 -04:00
gamer147
3e2931b085 refactor(battle-node): extract LoadedHandler
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 14:10:33 -04:00
gamer147
e5ec8a0de1 refactor(battle-node): extract InitBattleHandler 2026-06-03 14:07:49 -04:00