198 Commits

Author SHA1 Message Date
gamer147
11c98bf67b feat(battle-node): BattleSeeds — stable per-battle seed derivation
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 18:13:06 -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
e9493e24c4 refactor(battle-node): drop BattleType.Scripted and the scripted-only builders
Removes the Scripted enum value, the bot's client-shaped emissions (BuildClient*),
the canned opponent turn (BuildOpponent*), and OpponentTurnStartSpin. The shared
server-frame builders (Matched/BattleStart/Deal/Swap/Ready + ComputeHandAfterSwap)
and OpponentJudgeSpin (Bot mode) stay.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 20:27:57 -04:00
gamer147
f21ab7a38c refactor(battle-node): remove ScriptedBotParticipant and dev-affordance wiring
Deletes the scripted opponent and every entry point that created a
BattleType.Scripted session (the ?scripted=1 query opt-in, the
SoloDefaultsToScripted toggle, the resolver short-circuit, the WS handler case,
the bridge validation arm). Real two-client PvP and the Bot matchmaking-timeout
fallback are untouched. ResolveAsync drops its scriptedOptIn parameter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 20:15:48 -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
486f72f4a0 feat(battle-node): typed PlayActionsBroadcastBody + KnownCardEntry/OppoTargetEntry
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 17:51:02 -04:00
gamer147
268b864e28 refactor(battle-node): delete legacy ComputeFrames switch; dispatch is now lookup-or-drop 2026-06-03 14:48:33 -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
gamer147
7c36933c06 refactor(battle-node): extract InitNetworkHandler 2026-06-03 14:04:58 -04:00
gamer147
73d2c4e1b8 refactor(battle-node): add frame-handler contract, context, and empty registry shim 2026-06-03 14:03:11 -04:00
gamer147
4f89463f9c refactor(battle-node): extract frame factories into BattleFrames 2026-06-03 13:56:41 -04:00
gamer147
85c43a9a72 refactor(battle-node): move session phase + post-swap hands into BattleSessionState 2026-06-03 13:47:35 -04:00
gamer147
95554cee04 refactor(battle-node): name ComputeFrames routes as DispatchRoute 2026-06-03 13:43:39 -04:00
gamer147
2d31037648 fix(battle-node): type-agnostic mulligan barrier withholds Ready until both swap
Ready was sent per-side immediately carrying the placeholder opponent hand, so
one client cleared mulligan before the other. The barrier now releases Ready to
every IHasHandshakePhase participant only once all have swapped, each carrying
the opponent's real post-mulligan hand. No Type check — NoOp (Bot/AINetwork)
isn't a phase impl, so that mode still releases immediately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 10:52:33 -04:00
gamer147
8052ed60ec refactor(battle-node): scripted bot drives the handshake as a real participant
Implements IHasHandshakePhase and emits client-shaped InitNetwork/InitBattle/
Loaded/Swap (reacting to the session's pushes) instead of being a passive
TurnEnd-only fixture the session narrates around. This is what lets the
type-agnostic mulligan barrier (next task) work in Scripted mode.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 10:51:08 -04:00
gamer147
a533e9d89d feat(battle-node): client-shaped handshake builders for the scripted bot
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 10:49:38 -04:00
gamer147
633c29b44f feat(battle-node): BuildReady overload carrying the opponent's hand
Adds BuildReady(selfHand, oppoHand) for the mulligan barrier; the single-arg
overload keeps the InitialHand placeholder for non-interactive opponents.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 10:48:43 -04:00
gamer147
ae11fe0957 fix(battle-node): assign turnState per side instead of hardcoding 0
Both PvP clients received turnState:0 ('both go first'). BuildBattleStart
now takes turnState; the Loaded arm assigns 0 to A, 1 to B — no Type check,
correct in Scripted (real player = A = first) and PvP (first arriver first).

Updated three existing BuildBattleStart callers in the test suite to pass
turnState:0 (the param is now required).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 10:47:56 -04:00
gamer147
c551d7b05e refactor(battle-node): drop dead BattleResult.{Lose,Win,Consistency} members
No dispatch arm has emitted these since the Retire/Kill rewrite to RetireWin=105
/ RetireLose=106. Remove them and the docstring paragraph that explained them.

Test fallout: delete BattleFinishBody_LoseAndConsistency_SerializeAsZeroAndTwo
(its only purpose was locking the dead wire values), and re-point
BattleFinishBody_SerializesResultAndResultCode_AsNumericWireValues at the live
LifeWin=101 so it still guards the JsonNumberEnumConverter numeric-wire behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 21:17:30 -04:00