feat(state): scope card evidence and policy memory by reported run identity

This commit is contained in:
0xrsydn 2026-09-22 12:45:46 +07:00
commit ba519a850e
9 changed files with 343 additions and 82 deletions

View file

@ -538,7 +538,7 @@ class CombatFacts:
f"block_available={self.max_block_available} survives_with_cards={self.survives_with_cards}",
f"potions={[p.get('name') for p in self.potions]}",
f"piles: draw={self.draw_pile_count} discard={self.discard_pile_count} exhaust={self.exhaust_pile_count}",
f"deck: {self.deck_counts}",
f"visible combat piles: {self.deck_counts}",
]
for e in self.enemies:
lines.append(
@ -620,22 +620,12 @@ def enemy_status_names(enemy: EnemyFact) -> list:
def deck_summary(player: dict) -> dict:
"""
Stable aggregates over ALL FOUR PILES, for macro decisions.
"""Aggregate visible combat piles, not the persistent run deck.
The reward, shop and map states do NOT expose the deck, so this travels with
the composition snapshot taken during combat. It is deliberately a
*supplement* to the name->count map, never a replacement: card identities
are the signal for synergy, redundancy and upgrades, and only counts can
carry them. These aggregates are the part that is arithmetic, so it is
computed here and never asked of the model.
Every field is computed over the whole deck and is stable across snapshots
of the same deck. There is deliberately no cost aggregate: the state does
not expose a card's BASE cost, and a temporary in-combat cost modifier
applies to the copy in hand. Measured over 600 captures, one Strike read
`cost: "0"` in hand while the same Strike read `cost: "1"` in the draw pile,
so an average cost would differ between two snapshots of an identical deck.
Generated cards, Status cards, temporary upgrades, and missing pile data
limit this evidence. These counts can change during combat. Cost is omitted
because temporary modifiers make displayed costs unsuitable for a base-cost
aggregate. The historical function name is retained for existing callers.
"""
names: list[str] = []
attacks = blocks = other = upgraded = 0
@ -668,22 +658,19 @@ def deck_summary(player: dict) -> dict:
def deck_context(deck: Any) -> Any:
"""
What a macro question receives as deck context: the full name->count map
(card identities, which carry the synergy and redundancy signal) plus the
stable aggregates. Tolerates a legacy flat name->count snapshot.
"""
"""Supply card evidence with its limits; unscoped legacy data is unknown."""
if not deck:
return "unknown"
if isinstance(deck, dict) and "counts" in deck:
counts = deck.get("counts") or {}
if not counts:
return "unknown"
return {"cards": counts, **({"summary": deck["summary"]}
if deck.get("summary") else {})}
if isinstance(deck, dict):
return {"cards": deck}
return deck
provenance = deck.get("provenance")
if not isinstance(provenance, dict) or not provenance.get("run_id"):
return "unknown"
return {"cards": counts, "provenance": provenance,
**({"summary": deck["summary"]} if deck.get("summary") else {})}
return "unknown"
# --------------------------------------------------------------------------