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:
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user