fix(battlenode): PlayedCardTribe degrades to 0 not empty; clan/tribe builder tests (M-HC-4e review)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-06-07 00:23:07 -04:00
parent 693fba5003
commit d3508d7bd4
3 changed files with 38 additions and 6 deletions

View File

@@ -61,7 +61,7 @@ internal sealed class PlayActionsHandler : IFrameHandler
// value would miss it). Prod always emits both on every knownList entry: clan as the int ClanType
// ordinal, tribe as the comma-joined int TribeType string ("0" for none). Same senderSeat mapping.
int playedClan = ctx.Engine.PlayedCardClan(senderSeat, playIdx, fallback: 0);
string playedTribe = ctx.Engine.PlayedCardTribe(senderSeat, playIdx);
string playedTribe = ctx.Engine.PlayedCardTribe(senderSeat, playIdx, fallback: "0");
var played = KnownListBuilder.BuildPlayedCard(
deckMap, playIdx, orderList, cost: playedCost, spellboost: playedSpellboost,

View File

@@ -302,13 +302,18 @@ internal sealed class SessionBattleEngine
/// <see cref="BattleCardBase.Tribe"/>, whose getter folds in any skill-applied tribe CHANGE/ADD over
/// <c>BaseParameter.Tribe</c> (and drops ALL when the resolved list has ≥2 entries) — so the wire carries
/// the LIVE tribe, the faithful value over the static card-master one.
/// <para>Same post-resolution zone search as <see cref="PlayedCardCost"/>; no engine / no card → "" (an
/// engine that isn't owned this session emits no card, so the caller's BuildPlayedCard never fires).</para></summary>
public string PlayedCardTribe(bool playerSeat, int idx)
/// <para>Same post-resolution zone search + degrade-to-<paramref name="fallback"/> contract as
/// <see cref="PlayedCardClan"/>: no engine / no card → <paramref name="fallback"/> (default <c>"0"</c>, the
/// prod no-tribe form — NEVER empty, which is wire-illegal: prod always sends tribe as a non-empty string,
/// the client reads it via <c>item.Value.ToString()</c> at NetworkBattleReceiver.cs:2382). The degrade is
/// LIVE, not dead: a second concurrent battle that loses the single-active-engine gate has <c>_mgr is null</c>
/// yet still emits a knownList entry (KnownListBuilder.BuildPlayedCard gates on the deck map, not engine
/// ownership), so this path must hand back a legal wire value.</para></summary>
public string PlayedCardTribe(bool playerSeat, int idx, string fallback = "0")
{
if (_mgr is null) return string.Empty;
if (_mgr is null) return fallback;
var card = FindByIndex(Seat(playerSeat), idx);
if (card is null) return string.Empty;
if (card is null) return fallback;
var tribe = card.Tribe;
// Prod's no-tribe form is the single "0" (TribeType.ALL == 0), never an empty string; an empty list
// (defensive) renders the same "0".

View File

@@ -125,6 +125,33 @@ public class KnownListBuilderTests
Assert.That(KnownListBuilder.BuildPlayedCard(deckMap, 3, OrderListMove(3, 10, 20))!.Spellboost, Is.EqualTo(0));
}
[Test]
public void BuildPlayedCard_emits_clan_tribe_passed_by_caller()
{
// M-HC-4e: the handler reads the engine-resolved clan/tribe
// (SessionBattleEngine.PlayedCardClan / PlayedCardTribe) and passes them in; BuildPlayedCard lands
// them on the entry verbatim. (A wrong clan/tribe yields a different field — non-vacuity.)
var deckMap = new Dictionary<int, long> { [3] = 101314020L };
var entry = KnownListBuilder.BuildPlayedCard(
deckMap, playIdx: 3, orderList: OrderListMove(3, 10, 20), cost: 3, spellboost: 2, clan: 8, tribe: "7,16");
Assert.That(entry, Is.Not.Null);
Assert.That(entry!.Clan, Is.EqualTo(8));
Assert.That(entry.Tribe, Is.EqualTo("7,16"));
}
[Test]
public void BuildPlayedCard_defaults_clan_to_zero_and_tribe_to_string_zero_when_caller_passes_none()
{
// A play whose engine read degraded (single-active-engine gate: _mgr null → the accessor fallback)
// emits clan 0 (ClanType.ALL ordinal) and tribe "0" (the prod no-tribe form, NEVER empty —
// empty is wire-illegal). The param defaults match the accessor fallbacks.
var deckMap = new Dictionary<int, long> { [3] = 101311010L };
var entry = KnownListBuilder.BuildPlayedCard(deckMap, 3, OrderListMove(3, 10, 20));
Assert.That(entry, Is.Not.Null);
Assert.That(entry!.Clan, Is.EqualTo(0));
Assert.That(entry.Tribe, Is.EqualTo("0"));
}
[Test]
public void RenameTargets_passes_isSelf_through_verbatim()
{