F-95a: awaken-step temporary buffs eaten by the same begin_turn resolve (FIXED both engines)
Card class: awaken ready_other buffs ("ready another friendly unit and give it +N this turn") and any temporary effect created during the awaken step.
Symptom (diagnosed F-95a, coordinator-ruled): the awaken-path buff fires correctly, but its duration-1 temporary effect is eaten by resolveTemporaryEffects later in the SAME begin_turn - the buff never lives a single turn.
Fix (TS, `src/game-engine.ts`): `TemporaryEffect.createdTurn` stamps `gameState.turnNumber` at the single `applyTemporaryEffect` funnel; `resolveTemporaryEffects` skips effects stamped with the current turn (they expire on their NEXT begin_turn instead). Awaken-vs-resolve order unchanged, per the ruling. Absent `createdTurn` (pre-fix serialized state) keeps old behavior.
Fix (Rust): all 12 `temporary_effects.push` sites in `actions.rs` stamp `"createdTurn": state.turn_number` in the effect JSON; `resolve_temporary_effects` (setup.rs) keeps effects whose `createdTurn` equals the current turn number. Both engines carried the bug.
Probe: `scripts/dev/probe-quirks-148.ts` applies a duration-1 damage_boost through the funnel, asserts it survives `resolveTemporaryEffects` on its creation turn, and asserts it expires on the next turn. A/B verified: FAIL on
fa12dca pristine (eaten on creation turn), PASS patched.
/**
* QUIRKS #148 (F-95a): awaken-path ready_other buffs are duration-1
* temporary effects created during the awaken step; the turn-start
* resolveTemporaryEffects later in the SAME begin_turn decremented them to
* 0 and ate them. Fix: created-this-turn guard (createdTurn stamp). Probe:
* applyTemporaryEffect funnels the stamp, then resolveTemporaryEffects must
* keep the effect on its creation turn and expire it on the next turn.
* Pre-fix: no stamp, no guard -> effect eaten on creation turn -> FAIL.
* Usage: npx ts-node --project tsconfig.scripts.json scripts/dev/probe-quirks-148.ts
*/
import { RiftboundGameEngine, createRng as createEngineRng, GameStatus, GamePhase } from '../../src/game-engine';
import { makeRng, buildDeckConfigForGame } from '../../src/self-play';
import { getCardCatalog } from '../../src/card-catalog';
const catalog = getCardCatalog() as any[];
const byId = new Map(catalog.map((c) => [String(c.id), c]));
const deckRng = makeRng(147);
const engine = new RiftboundGameEngine('q147', ['playerA', 'playerB'], {
rng: createEngineRng(147), deterministicIds: true
});
engine.initializeGame(
{ playerA: buildDeckConfigForGame(deckRng, 0, null, null, true), playerB: buildDeckConfigForGame(deckRng, 1, null, null, true) },
{ skipDeckConstructionRules: true }
);
let g = 0;
while (g++ < 50 && engine.status === GameStatus.COIN_FLIP) {
try { engine.submitInitiativeChoice('playerA', 0); } catch {}
try { engine.submitInitiativeChoice('playerB', 1); } catch {}
}
g = 0;
while (g++ < 50 && engine.status !== GameStatus.IN_PROGRESS) {
const st: any = engine.getGameState();
for (const p of st.prompts.filter((pr: any) => !pr.resolved)) { try { engine.submitMulligan(p.playerId, []); } catch {} }
if (st.prompts.every((pr: any) => pr.resolved)) break;
}
g = 0;
while (g++ < 10 && engine.currentPhase !== GamePhase.MAIN_1) engine.proceedToNextPhase();
const st: any = engine.getGameState();
const a = st.players.find((p: any) => p.playerId === 'playerA');
const unit = (engine as any).convertRecordToCard(byId.get('OGN-066'));
unit.instanceId = 'OGN-066-q147';
a.board.creatures.push(unit);
// The awaken ready_other buff shape: duration-1 damage_boost via the funnel.
(engine as any).applyTemporaryEffect(unit.instanceId, {
id: 'q147-mod', affectedCards: [unit.instanceId], duration: 1,
effect: { type: 'damage_boost', value: 2 }
} as any);
console.log('after apply: temporaryEffects =', a.temporaryEffects.length);
(engine as any).resolveTemporaryEffects(a);
const survivesCreationTurn = a.temporaryEffects.length === 1;
console.log('after same-turn resolve: temporaryEffects =', a.temporaryEffects.length, '(want 1)');
// Next turn it must expire normally.
(engine as any).gameState.turnNumber++;
(engine as any).resolveTemporaryEffects(a);
const expiresNextTurn = a.temporaryEffects.length === 0;
console.log('after next-turn resolve: temporaryEffects =', a.temporaryEffects.length, '(want 0)');
if (survivesCreationTurn && expiresNextTurn) {
console.log('QUIRKS-147 PASS: created-this-turn temporary effects survive their own begin_turn');
process.exit(0);
}
console.log('QUIRKS-147 FAIL');
process.exit(1);