@@ -187,6 +187,23 @@ function report(seed) {
187187 . map ( ( [ k , v ] ) => `${ k } ${ v } ` )
188188 . join ( ', ' ) ;
189189 } ;
190+ const chests = map . chests ?? [ ] ;
191+ say ( `\nCHESTS (${ chests . length } )` ) ;
192+ for ( const c of chests ) {
193+ const p = uvOf ( c ) ;
194+ let road = Infinity ;
195+ for ( const r of map . roads ) road = Math . min ( road , polyDist ( p , roadUV . get ( r . id ) ) ) ;
196+ const site = map . sites . find ( ( s ) => s . id === c . siteId ) ;
197+ const paid = site ? c . grantIds . filter ( ( id ) => site . buildings . some ( ( b ) => b . id === id ) ) : [ ] ;
198+ say (
199+ ` ${ c . id } ${ c . reward . padEnd ( 8 ) } u,v ${ f1 ( p [ 0 ] ) . padStart ( 6 ) } ,${ f1 ( p [ 1 ] ) . padStart ( 6 ) } ` +
200+ `${ c . biome . padEnd ( 6 ) } ${ ( '“' + c . where + '”' ) . padEnd ( 18 ) } ` +
201+ `road ${ f1 ( road ) . padStart ( 5 ) } river ${ f1 ( polyDist ( p , river ) ) . padStart ( 5 ) } ` +
202+ `${ c . found ? 'FOUND' : 'buried' } by ${ c . siteId } ${ site ? '' : ' (trimmed)' } ` +
203+ `pays ${ paid . length } /${ c . grantIds . length } ${ c . grantIds . length ? ' [' + c . grantIds . join ( ' ' ) + ']' : '' } `
204+ ) ;
205+ }
206+
190207 say ( `\nforest character: ${ woodCharacter ( seed ) } ` ) ;
191208 say ( `tree kinds: ${ hist ( map . trees , ( t ) => t . kind ) } ` ) ;
192209 say ( `scatter kinds: ${ hist ( map . scatter , ( p ) => p . kind ) } ` ) ;
@@ -456,6 +473,78 @@ function checks(seed, map, scale = 1) {
456473 ) ;
457474 }
458475
476+ /* (t) buried treasure: nought to two, in deep cover, well off every road,
477+ and paying out only where the day's roster could carry the reward.
478+ Chests are TERRAIN — the scale-identity half of that is in subsetChecks. */
479+ {
480+ const chests = map . chests ?? [ ] ;
481+ const bad = [ ] ;
482+ const ids = new Set ( ) ;
483+ let minRoad = Infinity ;
484+ let minRiver = Infinity ;
485+ let minSite = Infinity ;
486+ for ( const c of chests ) {
487+ if ( ids . has ( c . id ) ) bad . push ( `duplicate id ${ c . id } ` ) ;
488+ ids . add ( c . id ) ;
489+ const p = uvOf ( c ) ;
490+ const chunk = map . chunks . find (
491+ ( k ) => p [ 0 ] >= k . u0 && p [ 0 ] < k . u1 && p [ 1 ] >= k . v0 && p [ 1 ] < k . v1
492+ ) ;
493+ if ( ! chunk ) bad . push ( `${ c . id } sits outside every chunk` ) ;
494+ else if ( chunk . biome !== 'forest' && chunk . biome !== 'moor' )
495+ bad . push ( `${ c . id } buried in ${ chunk . biome } ` ) ;
496+ else if ( chunk . biome !== c . biome ) bad . push ( `${ c . id } says ${ c . biome } , ground says ${ chunk . biome } ` ) ;
497+ if ( ! c . where ) bad . push ( `${ c . id } has nowhere to be` ) ;
498+ for ( const r of map . roads ) minRoad = Math . min ( minRoad , polyDist ( p , roadUV . get ( r . id ) ) ) ;
499+ minRiver = Math . min ( minRiver , polyDist ( p , river ) ) ;
500+ for ( const s of map . sites ) minSite = Math . min ( minSite , d2 ( p , uvOf ( s ) ) - s . radius ) ;
501+ for ( const o of chests ) if ( o !== c && d2 ( p , uvOf ( o ) ) < 12 - 1e-9 ) bad . push ( `${ c . id } crowds ${ o . id } ` ) ;
502+
503+ if ( ! ( c . siteIndex >= 1 ) ) bad . push ( `${ c . id } names s0 as its finder` ) ;
504+ if ( c . siteId !== `s${ c . siteIndex } ` ) bad . push ( `${ c . id } siteId/siteIndex disagree` ) ;
505+ if ( ! [ 'coin' , 'charter' , 'trinket' ] . includes ( c . reward ) ) bad . push ( `${ c . id } reward ${ c . reward } ` ) ;
506+ if ( ! c . found && c . grantIds . length ) bad . push ( `${ c . id } was never found but pays out` ) ;
507+ if ( c . reward === 'trinket' && c . grantIds . length ) bad . push ( `${ c . id } is a trinket that pays out` ) ;
508+ if ( c . reward === 'charter' && c . found && c . grantIds . length !== 1 )
509+ bad . push ( `${ c . id } charter grants ${ c . grantIds . length } ` ) ;
510+ for ( const id of c . grantIds )
511+ if ( ! id . startsWith ( `${ c . siteId } -` ) ) bad . push ( `${ c . id } grant ${ id } is not ${ c . siteId } 's` ) ;
512+
513+ const site = map . sites . find ( ( s ) => s . id === c . siteId ) ;
514+ if ( site && c . grantIds . length ) {
515+ const here = c . grantIds . filter ( ( id ) => site . buildings . some ( ( b ) => b . id === id ) ) ;
516+ // All of it, or (only where the pace control trimmed the town's roster
517+ // back past the grant) none of it. Never half a windfall.
518+ if ( here . length && here . length !== c . grantIds . length )
519+ bad . push ( `${ c . id } pays out ${ here . length } /${ c . grantIds . length } at ${ scale } x` ) ;
520+ if ( scale >= 1 && here . length !== c . grantIds . length )
521+ bad . push ( `${ c . id } reward trimmed at ${ scale } x — a normal day must always pay` ) ;
522+ if ( c . reward === 'charter' && here . length && ! site . buildings . some ( ( b ) => b . role === 'gildhall' ) )
523+ bad . push ( `${ c . id } charter paid out with no gildhall in ${ site . id } ` ) ;
524+ }
525+ }
526+ // A gildhall exists for exactly one reason.
527+ for ( const s of map . sites )
528+ for ( const b of s . buildings )
529+ if (
530+ b . role === 'gildhall' &&
531+ ! chests . some ( ( c ) => c . found && c . reward === 'charter' && c . grantIds . includes ( b . id ) )
532+ )
533+ bad . push ( `${ b . id } is a gildhall nobody found a charter for` ) ;
534+
535+ check (
536+ 't chests buried off the road' ,
537+ chests . length <= 2 &&
538+ bad . length === 0 &&
539+ ( ! chests . length || ( minRoad >= 2 && minRiver >= 3 && minSite >= 3 ) ) ,
540+ `${ chests . length } chest(s)` +
541+ ( chests . length
542+ ? `, nearest road ${ f1 ( minRoad ) } , river ${ f1 ( minRiver ) } , town rim ${ f1 ( minSite ) } `
543+ : '' ) +
544+ ( bad . length ? ` — ${ bad . join ( '; ' ) } ` : '' )
545+ ) ;
546+ }
547+
459548 /* (s) no two towns wear the same accent */
460549 {
461550 const acc = map . sites . map ( ( s ) => s . accent ) ;
@@ -490,7 +579,11 @@ function subsetChecks(seed) {
490579 const same = ( a , b ) => JSON . stringify ( a ) === JSON . stringify ( b ) ;
491580
492581 // Terrain is scale-invariant, full stop.
493- for ( const field of [ 'river' , 'riverWidth' , 'chunks' , 'trees' , 'scatter' , 'bounds' , 'content' , 'valleyName' ] ) {
582+ // `chests` is in here on purpose: a chest is terrain. Where it is, what is in
583+ // it, who finds it and whether the day's digging turns it up at all are all a
584+ // pure function of the seed. Only the REWARD is allowed to notice the pace,
585+ // and it does that through the ordinary site/building prefix below.
586+ for ( const field of [ 'river' , 'riverWidth' , 'chunks' , 'trees' , 'scatter' , 'chests' , 'bounds' , 'content' , 'valleyName' ] ) {
494587 const ok = maps . every ( ( m ) => same ( m [ field ] , maps [ 0 ] [ field ] ) ) ;
495588 check ( `terrain: ${ field } identical` , ok ) ;
496589 }
@@ -576,6 +669,64 @@ function subsetChecks(seed) {
576669 return pass ;
577670}
578671
672+ /* ------------------------------ chest rates ------------------------------ */
673+
674+ /**
675+ * How often a day buries treasure, and how often the day's digging turns any of
676+ * it up. The number that matters is the last one: a discovery on roughly one in
677+ * three to one in two of the days that have a chest to find. Rarer than that and
678+ * nobody ever sees the feature; commoner and it stops being luck.
679+ */
680+ function chestReport ( n ) {
681+ let chestDays = 0 ;
682+ let findDays = 0 ;
683+ let total = 0 ;
684+ let found = 0 ;
685+ const nHist = { } ;
686+ const tier = { } ;
687+ const paid = { } ;
688+ const where = { } ;
689+ for ( let s = 1 ; s <= n ; s ++ ) {
690+ const map = generateMap ( s ) ;
691+ const chests = map . chests ?? [ ] ;
692+ nHist [ chests . length ] = ( nHist [ chests . length ] ?? 0 ) + 1 ;
693+ if ( chests . length ) chestDays ++ ;
694+ let f = 0 ;
695+ for ( const c of chests ) {
696+ total ++ ;
697+ tier [ c . reward ] = ( tier [ c . reward ] ?? 0 ) + 1 ;
698+ where [ c . where ] = ( where [ c . where ] ?? 0 ) + 1 ;
699+ if ( ! c . found ) continue ;
700+ found ++ ;
701+ f ++ ;
702+ const site = map . sites . find ( ( x ) => x . id === c . siteId ) ;
703+ const key =
704+ c . reward === 'trinket'
705+ ? 'trinket (nothing owed)'
706+ : site && c . grantIds . every ( ( id ) => site . buildings . some ( ( b ) => b . id === id ) )
707+ ? `${ c . reward } paid`
708+ : `${ c . reward } TRIMMED` ;
709+ paid [ key ] = ( paid [ key ] ?? 0 ) + 1 ;
710+ }
711+ if ( f ) findDays ++ ;
712+ }
713+ const pct = ( a , b ) => ( b ? `${ ( ( a / b ) * 100 ) . toFixed ( 1 ) } %` : '--' ) ;
714+ const hist = ( o ) =>
715+ Object . entries ( o )
716+ . sort ( ( a , b ) => b [ 1 ] - a [ 1 ] )
717+ . map ( ( [ k , v ] ) => `${ k } ${ v } ` )
718+ . join ( ', ' ) ;
719+ console . log ( `\nBURIED TREASURE over ${ n } seeds` ) ;
720+ console . log ( ` chests per day: ${ hist ( nHist ) } (${ chestDays } days with a chest, ${ total } chests)` ) ;
721+ console . log ( ` rewards buried: ${ hist ( tier ) } ` ) ;
722+ console . log ( ` ground: ${ hist ( where ) } ` ) ;
723+ console . log ( ` found at 1x: ${ found } /${ total } chests (${ pct ( found , total ) } ), ${ hist ( paid ) } ` ) ;
724+ console . log (
725+ ` => a discovery on ${ findDays } /${ chestDays } chest-days (${ pct ( findDays , chestDays ) } ), ` +
726+ `${ pct ( findDays , n ) } of all days`
727+ ) ;
728+ }
729+
579730/* ---------------------------------- main --------------------------------- */
580731
581732const argv = process . argv . slice ( 2 ) ;
@@ -618,6 +769,7 @@ if (argv[0] === '--sweep') {
618769 }
619770 console . log ( `sweep ${ Math . min ( n , 60 ) } seeds subset stability: ${ subFails . length ? `FAIL ${ subFails . join ( ',' ) } ` : 'ALL PASS' } ` ) ;
620771 if ( subFails . length ) allPass = false ;
772+ chestReport ( n ) ;
621773} else {
622774 const seeds = argv . length ? argv . map ( Number ) : [ 1 , 42 , 20260802 ] ;
623775 for ( const s of seeds ) {
0 commit comments