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>
This commit is contained in:
gamer147
2026-06-06 21:48:50 -04:00
parent 51419d15cd
commit 0d7136787a
9 changed files with 261 additions and 194 deletions

View File

@@ -88,49 +88,6 @@ public class BattleSessionStateTests
Assert.That(a, Is.Not.EqualTo(b));
}
[Test]
public void RecordSpellboostFrom_accumulates_add_ops_and_routes_by_isSelf()
{
var state = new BattleSessionState(masterSeed: 1);
var caster = new StubParticipant(7, Ctx(900L));
var oppo = new StubParticipant(6, Ctx(901L));
// Two spell-plays each grant +1 to the caster's hand card idx 3 (the classic spellboost ramp).
var grant = new List<object?> { new Dictionary<string, object?>
{ ["alter"] = new Dictionary<string, object?>
{ ["idx"] = new List<object?> { 3L }, ["isSelf"] = 1L, ["type"] = "add", ["spellboost"] = "a1" } } };
state.RecordSpellboostFrom(caster, oppo, grant);
state.RecordSpellboostFrom(caster, oppo, grant);
Assert.That(state.GetSpellboostMap(caster)[3], Is.EqualTo(2), "two +1 grants accumulate");
Assert.That(state.GetSpellboostMap(oppo).ContainsKey(3), Is.False, "isSelf:1 routes to the caster only");
}
[Test]
public void Boosted_card_carries_real_spellboost_into_its_knownList()
{
// End-to-end regression for the 2026-06-05 desync: a spellboosted card whose relayed knownList
// shipped spellboost:0 made the opponent compute full cost and silently reject the play. After the
// grant is recorded, BuildPlayedCard must emit the accumulated count, not 0.
var state = new BattleSessionState(masterSeed: 1);
var caster = new StubParticipant(7, Ctx(101311010L, 999L, 998L)); // idx 1..3 deck-seeded
var grant = new List<object?> { new Dictionary<string, object?>
{ ["alter"] = new Dictionary<string, object?>
{ ["idx"] = new List<object?> { 3L }, ["isSelf"] = 1L, ["type"] = "add", ["spellboost"] = "a1" } } };
state.RecordSpellboostFrom(caster, new StubParticipant(6, Ctx(901L)), grant);
// idx 3 is now played hand->board; its knownList must reflect the +1.
var play = new List<object?> { new Dictionary<string, object?>
{ ["move"] = new Dictionary<string, object?>
{ ["idx"] = new List<object?> { 3L }, ["isSelf"] = 1L, ["from"] = 10L, ["to"] = 20L } } };
var entry = KnownListBuilder.BuildPlayedCard(
state.GetOrSeedDeckMap(caster), playIdx: 3, orderList: play, state.GetSpellboostMap(caster));
Assert.That(entry, Is.Not.Null);
Assert.That(entry!.Spellboost, Is.EqualTo(1));
}
private static long[] DistinctDeck() =>
Enumerable.Range(1, 30).Select(i => 200_000_000L + i).ToArray();
}