From 9f087ff6eb3bdfc73cbd3747cc294521c34b9a6e Mon Sep 17 00:00:00 2001 From: Denis Lebedev <829783+delebedev@users.noreply.github.com> Date: Sun, 22 Mar 2026 21:24:07 +0000 Subject: [PATCH 1/2] chore: ignore .claude/worktrees/ Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 6dfa1362..0f70be38 100644 --- a/.gitignore +++ b/.gitignore @@ -68,6 +68,7 @@ app/main/proto/messages.proto !.claude/ .claude/CLAUDE.md +.claude/worktrees/ # Contains creds deploy/services-proxy.conf From 87a7abdb60d811f334d4f238676a729d52473b9c Mon Sep 17 00:00:00 2001 From: Denis Lebedev <829783+delebedev@users.noreply.github.com> Date: Sun, 22 Mar 2026 21:52:58 +0000 Subject: [PATCH 2/2] fix(conformance): mill ZoneTransfer annotations emit affectorId (#176) Add sourceForgeCardId to CardMilled event (same pattern as CardSurveiled/CardDestroyed). Source resolved from stack's resolving spell at zone-change time. Wire through affectorSourceFromEvents. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/main/kotlin/leyline/game/AnnotationBuilder.kt | 1 + matchdoor/src/main/kotlin/leyline/game/GameEvent.kt | 1 + .../main/kotlin/leyline/game/GameEventCollector.kt | 6 ++++-- .../kotlin/leyline/game/CategoryFromEventsTest.kt | 11 +++++++++-- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/matchdoor/src/main/kotlin/leyline/game/AnnotationBuilder.kt b/matchdoor/src/main/kotlin/leyline/game/AnnotationBuilder.kt index a5e9a49b..4465ab1f 100644 --- a/matchdoor/src/main/kotlin/leyline/game/AnnotationBuilder.kt +++ b/matchdoor/src/main/kotlin/leyline/game/AnnotationBuilder.kt @@ -124,6 +124,7 @@ object AnnotationBuilder { fun affectorSourceFromEvents(forgeCardId: Int, events: List): Int? { for (ev in events) { when { + ev is GameEvent.CardMilled && ev.forgeCardId == forgeCardId -> return ev.sourceForgeCardId ev is GameEvent.CardSurveiled && ev.forgeCardId == forgeCardId -> return ev.sourceForgeCardId ev is GameEvent.CardDestroyed && ev.forgeCardId == forgeCardId -> return ev.sourceForgeCardId } diff --git a/matchdoor/src/main/kotlin/leyline/game/GameEvent.kt b/matchdoor/src/main/kotlin/leyline/game/GameEvent.kt index 36655bd6..9a0a867b 100644 --- a/matchdoor/src/main/kotlin/leyline/game/GameEvent.kt +++ b/matchdoor/src/main/kotlin/leyline/game/GameEvent.kt @@ -186,6 +186,7 @@ sealed interface GameEvent { data class CardMilled( val forgeCardId: Int, val seatId: Int, + val sourceForgeCardId: Int? = null, ) : GameEvent /** A card was moved Library→Hand via a search effect (tutor, ChangeZone). diff --git a/matchdoor/src/main/kotlin/leyline/game/GameEventCollector.kt b/matchdoor/src/main/kotlin/leyline/game/GameEventCollector.kt index 3443774b..cb2b2e0d 100644 --- a/matchdoor/src/main/kotlin/leyline/game/GameEventCollector.kt +++ b/matchdoor/src/main/kotlin/leyline/game/GameEventCollector.kt @@ -117,8 +117,10 @@ class GameEventCollector(private val bridge: GameBridge) : IGameEventVisitor.Bas } from == ZoneType.Hand && to == ZoneType.Graveyard -> GameEvent.CardDiscarded(card.id, seat) - from == ZoneType.Library && to == ZoneType.Graveyard -> - GameEvent.CardMilled(card.id, seat) + from == ZoneType.Library && to == ZoneType.Graveyard -> { + val sourceId = bridge.getGame()?.stack?.peek()?.spellAbility?.hostCard?.id + GameEvent.CardMilled(card.id, seat, sourceId) + } from == ZoneType.Library && to == ZoneType.Hand && isSearchedToHand(card.id) -> GameEvent.CardSearchedToHand(card.id) else -> GameEvent.ZoneChanged(card.id, Zone.fromForge(from), Zone.fromForge(to)) diff --git a/matchdoor/src/test/kotlin/leyline/game/CategoryFromEventsTest.kt b/matchdoor/src/test/kotlin/leyline/game/CategoryFromEventsTest.kt index c122ee52..22557566 100644 --- a/matchdoor/src/test/kotlin/leyline/game/CategoryFromEventsTest.kt +++ b/matchdoor/src/test/kotlin/leyline/game/CategoryFromEventsTest.kt @@ -449,9 +449,16 @@ class CategoryFromEventsTest : AnnotationBuilder.affectorSourceFromEvents(55, events) shouldBe 42 } - test("affectorSourceReturnsNullForNonSurveil") { + test("affectorSourceReturnsMillSourceCard") { val events = listOf( - GameEvent.CardMilled(forgeCardId = 55, seatId = 1), + GameEvent.CardMilled(forgeCardId = 55, seatId = 1, sourceForgeCardId = 42), + ) + AnnotationBuilder.affectorSourceFromEvents(55, events) shouldBe 42 + } + + test("affectorSourceReturnsNullWhenMillHasNoSource") { + val events = listOf( + GameEvent.CardMilled(forgeCardId = 55, seatId = 1, sourceForgeCardId = null), ) AnnotationBuilder.affectorSourceFromEvents(55, events).shouldBeNull() }