1414 */
1515
1616import Phaser from 'phaser' ;
17- import type { Suit } from '../../../src/card-system/Card' ;
17+ import type { Rank , Suit } from '../../../src/card-system/Card' ;
18+ import { createCard , RANKS } from '../../../src/card-system/Card' ;
1819import type { BeleagueredCastleState , BCMove } from '../BeleagueredCastleState' ;
1920import { FOUNDATION_COUNT , TABLEAU_COUNT , FOUNDATION_SUITS } from '../BeleagueredCastleState' ;
2021import {
@@ -33,7 +34,7 @@ import {
3334import type { Command } from '../../../src/core-engine/UndoRedoManager' ;
3435import { UndoRedoManager , CompoundCommand } from '../../../src/core-engine/UndoRedoManager' ;
3536import { BCTranscriptRecorder } from '../GameTranscript' ;
36- import type { BCGameTranscript } from '../GameTranscript' ;
37+ import type { BCGameTranscript , BoardSnapshot } from '../GameTranscript' ;
3738import {
3839 HelpPanel , HelpButton ,
3940 SettingsPanel , SettingsButton ,
@@ -207,6 +208,9 @@ export class BeleagueredCastleScene extends Phaser.Scene {
207208 private seed : number = Date . now ( ) ;
208209 private undoManager ! : UndoRedoManager ;
209210
211+ /** When true, the scene suppresses all input and sound for replay use. */
212+ private replayMode : boolean = false ;
213+
210214 // Whether the deal animation has finished (interactions blocked until then)
211215 private dealComplete : boolean = false ;
212216
@@ -305,6 +309,10 @@ export class BeleagueredCastleScene extends Phaser.Scene {
305309 const seedParam = params . get ( 'seed' ) ;
306310 this . seed = seedParam ? parseInt ( seedParam , 10 ) : Date . now ( ) ;
307311
312+ // Check for replay mode via URL parameter (?mode=replay)
313+ this . replayMode =
314+ params . get ( 'mode' ) === 'replay' ;
315+
308316 // Deal the game
309317 this . gameState = deal ( this . seed ) ;
310318 this . undoManager = new UndoRedoManager ( ) ;
@@ -334,60 +342,75 @@ export class BeleagueredCastleScene extends Phaser.Scene {
334342 this . createFoundationSlots ( ) ;
335343 this . createTableauDropZones ( ) ;
336344 this . createHUD ( ) ;
337- this . createHelpPanel ( ) ;
345+ if ( ! this . replayMode ) {
346+ this . createHelpPanel ( ) ;
347+ }
338348
339349 // Sound system: event emitter, bridge, sound manager, settings
340350 this . gameEvents = new GameEventEmitter ( ) ;
341351 this . eventBridge = new PhaserEventBridge ( this . gameEvents , this . events ) ;
352+ ( window as unknown as Record < string , unknown > ) . __GAME_EVENTS__ =
353+ this . gameEvents ;
354+
355+ if ( ! this . replayMode ) {
356+ const phaserSound = this . sound ;
357+ const player : SoundPlayer = {
358+ play : ( key : string ) => { phaserSound . play ( key ) ; } ,
359+ stop : ( key : string ) => { phaserSound . stopByKey ( key ) ; } ,
360+ setVolume : ( v : number ) => { phaserSound . volume = v ; } ,
361+ setMute : ( m : boolean ) => { phaserSound . mute = m ; } ,
362+ } ;
363+ this . soundManager = new SoundManager ( player ) ;
342364
343- const phaserSound = this . sound ;
344- const player : SoundPlayer = {
345- play : ( key : string ) => { phaserSound . play ( key ) ; } ,
346- stop : ( key : string ) => { phaserSound . stopByKey ( key ) ; } ,
347- setVolume : ( v : number ) => { phaserSound . volume = v ; } ,
348- setMute : ( m : boolean ) => { phaserSound . mute = m ; } ,
349- } ;
350- this . soundManager = new SoundManager ( player ) ;
351-
352- // Register all SFX keys
353- for ( const sfxKey of Object . values ( SFX_KEYS ) ) {
354- this . soundManager . register ( sfxKey ) ;
355- }
365+ // Register all SFX keys
366+ for ( const sfxKey of Object . values ( SFX_KEYS ) ) {
367+ this . soundManager . register ( sfxKey ) ;
368+ }
356369
357- // Declarative event-to-sound mapping
358- const mapping : EventSoundMapping = {
359- 'card-pickup' : SFX_KEYS . CARD_PICKUP ,
360- 'card-to-foundation' : SFX_KEYS . CARD_TO_FOUNDATION ,
361- 'card-to-tableau' : SFX_KEYS . CARD_TO_TABLEAU ,
362- 'card-snap-back' : SFX_KEYS . CARD_SNAP_BACK ,
363- 'deal-card' : SFX_KEYS . DEAL_CARD ,
364- 'game-ended' : SFX_KEYS . LOSS_SOUND , // default; win overrides with direct play
365- 'auto-complete-start' : SFX_KEYS . AUTO_COMPLETE_START ,
366- 'auto-complete-card' : SFX_KEYS . AUTO_COMPLETE_CARD ,
367- 'undo' : SFX_KEYS . UNDO ,
368- 'redo' : SFX_KEYS . REDO ,
369- 'card-selected' : SFX_KEYS . CARD_SELECT ,
370- 'card-deselected' : SFX_KEYS . CARD_DESELECT ,
371- 'ui-interaction' : SFX_KEYS . UI_CLICK ,
372- } ;
373- this . soundManager . connectToEvents ( this . gameEvents , mapping ) ;
374-
375- this . createSettingsPanel ( ) ;
370+ // Declarative event-to-sound mapping
371+ const mapping : EventSoundMapping = {
372+ 'card-pickup' : SFX_KEYS . CARD_PICKUP ,
373+ 'card-to-foundation' : SFX_KEYS . CARD_TO_FOUNDATION ,
374+ 'card-to-tableau' : SFX_KEYS . CARD_TO_TABLEAU ,
375+ 'card-snap-back' : SFX_KEYS . CARD_SNAP_BACK ,
376+ 'deal-card' : SFX_KEYS . DEAL_CARD ,
377+ 'game-ended' : SFX_KEYS . LOSS_SOUND , // default; win overrides with direct play
378+ 'auto-complete-start' : SFX_KEYS . AUTO_COMPLETE_START ,
379+ 'auto-complete-card' : SFX_KEYS . AUTO_COMPLETE_CARD ,
380+ 'undo' : SFX_KEYS . UNDO ,
381+ 'redo' : SFX_KEYS . REDO ,
382+ 'card-selected' : SFX_KEYS . CARD_SELECT ,
383+ 'card-deselected' : SFX_KEYS . CARD_DESELECT ,
384+ 'ui-interaction' : SFX_KEYS . UI_CLICK ,
385+ } ;
386+ this . soundManager . connectToEvents ( this . gameEvents , mapping ) ;
387+
388+ this . createSettingsPanel ( ) ;
389+ }
376390
377391 // Render foundations (aces already placed)
378392 this . refreshFoundations ( ) ;
379393
380- // Deal cards to tableau with animation
381- this . dealTableauAnimated ( ) ;
394+ if ( this . replayMode ) {
395+ // In replay mode: skip deal animation, skip input handlers,
396+ // mark deal as complete, and emit state-settled for the replay tool.
397+ this . dealComplete = true ;
398+ this . refreshTableau ( ) ;
399+ this . refreshHUD ( ) ;
400+ this . emitStateSettled ( ) ;
401+ } else {
402+ // Deal cards to tableau with animation
403+ this . dealTableauAnimated ( ) ;
382404
383- // Setup drag-and-drop event handlers
384- this . setupDragAndDrop ( ) ;
405+ // Setup drag-and-drop event handlers
406+ this . setupDragAndDrop ( ) ;
385407
386- // Setup click-to-move event handlers
387- this . setupClickToMove ( ) ;
408+ // Setup click-to-move event handlers
409+ this . setupClickToMove ( ) ;
388410
389- // Setup keyboard shortcuts
390- this . setupKeyboard ( ) ;
411+ // Setup keyboard shortcuts
412+ this . setupKeyboard ( ) ;
413+ }
391414 }
392415
393416 // ── UI creation ─────────────────────────────────────────
@@ -1701,6 +1724,75 @@ export class BeleagueredCastleScene extends Phaser.Scene {
17011724 return this . recorder ;
17021725 }
17031726
1727+ // ── Replay API ──────────────────────────────────────────
1728+
1729+ /**
1730+ * Inject an arbitrary board state from a transcript snapshot and
1731+ * refresh the visual display. Intended for use by the replay tool
1732+ * via `page.evaluate()`.
1733+ *
1734+ * Only operational in replay mode (?mode=replay). Throws if called
1735+ * outside of replay mode.
1736+ *
1737+ * After updating the internal state and refreshing all sprites,
1738+ * emits a `state-settled` event so the caller can synchronize
1739+ * screenshot capture.
1740+ *
1741+ * @param snapshot A BoardSnapshot containing foundations and tableau state.
1742+ */
1743+ loadBoardState ( snapshot : BoardSnapshot ) : void {
1744+ if ( ! this . replayMode ) {
1745+ throw new Error (
1746+ 'loadBoardState() is only available in replay mode (?mode=replay)' ,
1747+ ) ;
1748+ }
1749+
1750+ // Rebuild foundation piles from the snapshot
1751+ for ( let i = 0 ; i < FOUNDATION_COUNT ; i ++ ) {
1752+ const fs = snapshot . foundations [ i ] ;
1753+ this . gameState . foundations [ i ] . clear ( ) ;
1754+ // Push cards A through topRank for this foundation's suit
1755+ if ( fs . size > 0 && fs . topRank !== null ) {
1756+ const topIdx = RANKS . indexOf ( fs . topRank ) ;
1757+ for ( let ri = 0 ; ri <= topIdx ; ri ++ ) {
1758+ this . gameState . foundations [ i ] . push (
1759+ createCard ( RANKS [ ri ] , fs . suit , true ) ,
1760+ ) ;
1761+ }
1762+ }
1763+ }
1764+
1765+ // Rebuild tableau piles from the snapshot
1766+ for ( let col = 0 ; col < TABLEAU_COUNT ; col ++ ) {
1767+ this . gameState . tableau [ col ] . clear ( ) ;
1768+ const cs = snapshot . tableau [ col ] ;
1769+ for ( const cardSnap of cs . cards ) {
1770+ this . gameState . tableau [ col ] . push (
1771+ createCard ( cardSnap . rank as Rank , cardSnap . suit as Suit , true ) ,
1772+ ) ;
1773+ }
1774+ }
1775+
1776+ // Refresh all visual elements
1777+ this . refreshFoundations ( ) ;
1778+ this . refreshTableau ( ) ;
1779+ this . refreshHUD ( ) ;
1780+
1781+ // Signal that the board is visually stable and ready for screenshot
1782+ this . emitStateSettled ( ) ;
1783+ }
1784+
1785+ /**
1786+ * Emit state-settled when the board is visually stable and safe
1787+ * to screenshot. Called after state injection and display refresh.
1788+ */
1789+ private emitStateSettled ( ) : void {
1790+ this . gameEvents . emit ( 'state-settled' , {
1791+ turnNumber : this . gameState . moveCount ,
1792+ phase : ( this . gameEnded ? 'ended' : 'playing' ) as 'setup' | 'playing' | 'ended' ,
1793+ } ) ;
1794+ }
1795+
17041796 // ── Cleanup ─────────────────────────────────────────────
17051797
17061798 shutdown ( ) : void {
0 commit comments