@@ -176,12 +176,53 @@ export interface MainStreetState {
176176 finalScore : number ;
177177 /** The seed string used for this game. */
178178 seed : string ;
179+ /** Numeric seed derived from the seed string (used for restore). */
180+ numericSeed : number ;
181+ /** Number of RNG draws consumed so far (used for deterministic restore). */
182+ rngCalls : number ;
179183 /** The RNG function for this game (seeded, deterministic). */
180184 rng : ( ) => number ;
181185 /** Chronological log of game activities for the UI activity log panel. */
182186 activityLog : LogEntry [ ] ;
183187}
184188
189+ export interface MainStreetSerializedState {
190+ config : GameConfig ;
191+ turn : number ;
192+ phase : DayPhase ;
193+ streetGrid : ( BusinessCard | null ) [ ] ;
194+ market : MarketState ;
195+ resourceBank : ResourceBank ;
196+ decks : {
197+ business : BusinessCard [ ] ;
198+ event : EventCard [ ] ;
199+ upgrade : UpgradeCard [ ] ;
200+ } ;
201+ challengesCompleted : string [ ] ;
202+ activeChallenges : {
203+ challengeId : string ;
204+ completed : boolean ;
205+ } [ ] ;
206+ heldEvent : EventCard | null ;
207+ incidentQueue : EventCard [ ] ;
208+ gameResult : GameResult ;
209+ endReason : EndReason ;
210+ finalScore : number ;
211+ seed : string ;
212+ numericSeed : number ;
213+ rngCalls : number ;
214+ activityLog : LogEntry [ ] ;
215+ }
216+
217+ export interface MainStreetCampaignProgress {
218+ unlockedTiers : string [ ] ;
219+ persistentReputation : number ;
220+ highestScore : number ;
221+ totalRuns : number ;
222+ totalWins : number ;
223+ lastUpdatedAt : string ;
224+ }
225+
185226// ── Setup Options ───────────────────────────────────────────
186227
187228/** Options for setting up a new Main Street game. */
@@ -246,7 +287,16 @@ function fillMarketSlots<T>(deck: T[], count: number): T[] {
246287export function setupMainStreetGame ( options : MainStreetSetupOptions = { } ) : MainStreetState {
247288 const seed = options . seed ?? generateSeedString ( ) ;
248289 const numericSeed = seedToNumber ( seed ) ;
249- const rng = createSeededRng ( numericSeed ) ;
290+ const baseRng = createSeededRng ( numericSeed ) ;
291+ let rngCalls = 0 ;
292+ let state ! : MainStreetState ;
293+ const rng = ( ) : number => {
294+ rngCalls += 1 ;
295+ if ( state ) {
296+ state . rngCalls = rngCalls ;
297+ }
298+ return baseRng ( ) ;
299+ } ;
250300
251301 // Resolve difficulty preset into runtime config
252302 const config = getPreset ( options . difficulty ) ;
@@ -288,7 +338,7 @@ export function setupMainStreetGame(options: MainStreetSetupOptions = {}): MainS
288338 }
289339
290340 // Build initial state -- use config values instead of hard-coded constants
291- const state : MainStreetState = {
341+ state = {
292342 config,
293343 turn : 1 ,
294344 phase : 'DayStart' ,
@@ -311,6 +361,8 @@ export function setupMainStreetGame(options: MainStreetSetupOptions = {}): MainS
311361 endReason : null ,
312362 finalScore : 0 ,
313363 seed,
364+ numericSeed,
365+ rngCalls,
314366 rng,
315367 activityLog : [ ] ,
316368 } ;
@@ -324,3 +376,83 @@ export function setupMainStreetGame(options: MainStreetSetupOptions = {}): MainS
324376
325377 return state ;
326378}
379+
380+ /**
381+ * Serializes Main Street runtime state into a JSON-safe checkpoint shape.
382+ */
383+ export function serializeMainStreetState ( state : MainStreetState ) : MainStreetSerializedState {
384+ return {
385+ config : structuredClone ( state . config ) ,
386+ turn : state . turn ,
387+ phase : state . phase ,
388+ streetGrid : structuredClone ( state . streetGrid ) ,
389+ market : structuredClone ( state . market ) ,
390+ resourceBank : structuredClone ( state . resourceBank ) ,
391+ decks : structuredClone ( state . decks ) ,
392+ challengesCompleted : [ ...state . challengesCompleted ] ,
393+ activeChallenges : state . activeChallenges . map ( ( ac ) => ( {
394+ challengeId : ac . challenge . id ,
395+ completed : ac . completed ,
396+ } ) ) ,
397+ heldEvent : structuredClone ( state . heldEvent ) ,
398+ incidentQueue : structuredClone ( state . incidentQueue ) ,
399+ gameResult : state . gameResult ,
400+ endReason : state . endReason ,
401+ finalScore : state . finalScore ,
402+ seed : state . seed ,
403+ numericSeed : state . numericSeed ,
404+ rngCalls : state . rngCalls ,
405+ activityLog : structuredClone ( state . activityLog ) ,
406+ } ;
407+ }
408+
409+ /**
410+ * Rehydrates runtime state from a serialized checkpoint.
411+ */
412+ export function deserializeMainStreetState ( saved : MainStreetSerializedState ) : MainStreetState {
413+ const baseRng = createSeededRng ( saved . numericSeed ) ;
414+ for ( let i = 0 ; i < saved . rngCalls ; i ++ ) {
415+ baseRng ( ) ;
416+ }
417+
418+ let rngCalls = saved . rngCalls ;
419+ let state ! : MainStreetState ;
420+ const rng = ( ) : number => {
421+ rngCalls += 1 ;
422+ state . rngCalls = rngCalls ;
423+ return baseRng ( ) ;
424+ } ;
425+
426+ state = {
427+ config : structuredClone ( saved . config ) ,
428+ turn : saved . turn ,
429+ phase : saved . phase ,
430+ streetGrid : structuredClone ( saved . streetGrid ) ,
431+ market : structuredClone ( saved . market ) ,
432+ resourceBank : structuredClone ( saved . resourceBank ) ,
433+ decks : structuredClone ( saved . decks ) ,
434+ challengesCompleted : [ ...saved . challengesCompleted ] ,
435+ activeChallenges : saved . activeChallenges . map ( ( ac ) => {
436+ const challenge = CHALLENGE_TEMPLATES . find ( ( tpl ) => tpl . id === ac . challengeId ) ;
437+ if ( ! challenge ) {
438+ throw new Error ( `Unknown challenge id in save: ${ ac . challengeId } ` ) ;
439+ }
440+ return {
441+ challenge,
442+ completed : ac . completed ,
443+ } ;
444+ } ) ,
445+ heldEvent : structuredClone ( saved . heldEvent ) ,
446+ incidentQueue : structuredClone ( saved . incidentQueue ) ,
447+ gameResult : saved . gameResult ,
448+ endReason : saved . endReason ,
449+ finalScore : saved . finalScore ,
450+ seed : saved . seed ,
451+ numericSeed : saved . numericSeed ,
452+ rngCalls : saved . rngCalls ,
453+ rng,
454+ activityLog : structuredClone ( saved . activityLog ) ,
455+ } ;
456+
457+ return state ;
458+ }
0 commit comments