33import { S3Client , GetObjectCommand , ListObjectsV2Command , PutObjectCommand , type _Object } from "@aws-sdk/client-s3" ;
44import { Handler } from "aws-lambda" ;
55import { CloudFrontClient , CreateInvalidationCommand , type CreateInvalidationCommandInput } from "@aws-sdk/client-cloudfront" ;
6- import { GameFactory , addResource , gameinfo } from '@abstractplay/gameslib' ;
6+ import { GameFactory , addResource , gameinfo , type APGamesInformation } from '@abstractplay/gameslib' ;
77import { gunzipSync , strFromU8 } from "fflate" ;
88import { load as loadIon } from "ion-js" ;
99import { ReservoirSampler } from "../lib/ReservoirSampler" ;
10+ import { type StatSummary } from "./summarize" ;
1011import i18n from 'i18next' ;
1112// import enGames from "@abstractplay/gameslib/locales/en/apgames.json";
1213import enBack from "../locales/en/apback.json" ;
@@ -15,6 +16,7 @@ const REGION = "us-east-1";
1516const s3 = new S3Client ( { region : REGION } ) ;
1617const DUMP_BUCKET = "abstractplay-db-dump" ;
1718const REC_BUCKET = "thumbnails.abstractplay.com" ;
19+ const STATS_BUCKET = "records.abstractplay.com" ;
1820const cloudfront = new CloudFrontClient ( { region : REGION } ) ;
1921
2022type BasicRec = {
@@ -47,6 +49,12 @@ type SamplerEntry = {
4749 completed : ReservoirSampler < GameRec > ;
4850}
4951
52+ const randomInt = ( max : number , min = 1 ) : number => {
53+ min = Math . ceil ( min ) ;
54+ max = Math . floor ( max ) ;
55+ return Math . floor ( Math . random ( ) * ( max - min + 1 ) ) + min ;
56+ }
57+
5058export const handler : Handler = async ( event : any , context ?: any ) => {
5159 await ( i18n
5260 . init ( {
@@ -66,6 +74,39 @@ export const handler: Handler = async (event: any, context?: any) => {
6674 throw new Error ( `i18n is not initialized where it should be!` ) ;
6775 }
6876 addResource ( "en" ) ;
77+
78+ // get list of production metas
79+ const gameInfoProd = ( [ ...gameinfo . values ( ) ] as APGamesInformation [ ] ) . filter ( rec => ! rec . flags . includes ( "experimental" ) ) ;
80+
81+ // load summary stats
82+ const cmd = new GetObjectCommand ( {
83+ Bucket : STATS_BUCKET ,
84+ Key : "_summary.json"
85+ } ) ;
86+ const response = await s3 . send ( cmd ) ;
87+ const chunks : Uint8Array [ ] = [ ] ;
88+ for await ( const chunk of response . Body as any ) {
89+ chunks . push ( chunk as Uint8Array ) ;
90+ }
91+ const fileContent = Buffer . concat ( chunks ) . toString ( "utf-8" ) ;
92+ const parsed = JSON . parse ( fileContent ) as StatSummary ;
93+ const stats = parsed . metaStats ;
94+
95+ // determine minimum and maximum move numbers
96+ const MIN = 5 ;
97+ const MAX = 1000 ;
98+ const meta2min = new Map < string , number > ( ) ;
99+ const meta2max = new Map < string , number > ( ) ;
100+ gameInfoProd . forEach ( rec => {
101+ if ( rec . name in stats ) {
102+ const len = stats [ rec . name ] . lenMedian ;
103+ meta2min . set ( rec . uid , len * 0.25 ) ;
104+ meta2max . set ( rec . uid , len * 0.75 ) ;
105+ } else {
106+ console . log ( `Could not find meta stats for "${ rec . uid } ".` ) ;
107+ }
108+ } ) ;
109+
69110 // scan bucket for data folder
70111 const command = new ListObjectsV2Command ( {
71112 Bucket : DUMP_BUCKET ,
@@ -136,24 +177,32 @@ export const handler: Handler = async (event: any, context?: any) => {
136177 const rec = json . Item ;
137178 if ( rec . pk === "GAME" ) {
138179 const [ meta , cbit , ] = rec . sk . split ( "#" ) ;
139- if ( samplerMap . has ( meta ) ) {
140- const sampler = samplerMap . get ( meta ) ! ;
141- if ( cbit === "1" ) {
142- sampler . completed . add ( rec as GameRec ) ;
143- } else {
144- sampler . active . add ( rec as GameRec ) ;
145- }
146- } else {
147- const sampler : SamplerEntry = {
148- completed : new ReservoirSampler < GameRec > ( ) ,
149- active : new ReservoirSampler < GameRec > ( ) ,
150- } ;
151- if ( cbit === "1" ) {
152- sampler . completed . add ( rec as GameRec ) ;
180+ const g = GameFactory ( meta , rec . state ) ;
181+ if ( g === undefined ) {
182+ throw new Error ( `Error instantiating the following game record:\n ${ rec } ` ) ;
183+ }
184+ const numMoves = g . stack . length ;
185+ const min = meta2min . get ( meta ) || MIN ;
186+ if ( numMoves >= min ) {
187+ if ( samplerMap . has ( meta ) ) {
188+ const sampler = samplerMap . get ( meta ) ! ;
189+ if ( cbit === "1" ) {
190+ sampler . completed . add ( rec as GameRec ) ;
191+ } else {
192+ sampler . active . add ( rec as GameRec ) ;
193+ }
153194 } else {
154- sampler . active . add ( rec as GameRec ) ;
195+ const sampler : SamplerEntry = {
196+ completed : new ReservoirSampler < GameRec > ( ) ,
197+ active : new ReservoirSampler < GameRec > ( ) ,
198+ } ;
199+ if ( cbit === "1" ) {
200+ sampler . completed . add ( rec as GameRec ) ;
201+ } else {
202+ sampler . active . add ( rec as GameRec ) ;
203+ }
204+ samplerMap . set ( meta , sampler ) ;
155205 }
156- samplerMap . set ( meta , sampler ) ;
157206 }
158207 }
159208 }
@@ -177,6 +226,7 @@ export const handler: Handler = async (event: any, context?: any) => {
177226 // We now have a list of random records for each game. For each one:
178227 // - Instantiate
179228 // - Serialize it with the `strip` option to strip out hidden information
229+ // - Select a random move between p25 and p75
180230 // - Render and store the JSON
181231 const allRecs = new Map < string , string > ( ) ;
182232 for ( const [ meta , entry ] of samplerMap . entries ( ) ) {
@@ -201,20 +251,18 @@ export const handler: Handler = async (event: any, context?: any) => {
201251 if ( g === undefined ) {
202252 throw new Error ( `Error instantiating the following game record AFTER STRIPPING:\n${ rec } ` ) ;
203253 }
254+ const min = meta2min . get ( meta ) || MIN ;
255+ const max = meta2max . get ( meta ) || MAX ;
256+ const random = randomInt ( max , min ) ;
257+ const realmove = Math . min ( random , g . stack . length ) ;
258+ g . load ( realmove ) ;
204259 const json = g . render ( { } ) ;
205260 allRecs . set ( meta , JSON . stringify ( json ) ) ;
206261 }
207262 console . log ( `Generated ${ allRecs . size } thumbnails` ) ;
208263
209- // get list of production metas
210- const metasProd = [ ...gameinfo . keys ( ) ] . sort ( ( a , b ) => {
211- const na = gameinfo . get ( a ) . name ;
212- const nb = gameinfo . get ( b ) . name ;
213- if ( na < nb ) return - 1 ;
214- else if ( na > nb ) return 1 ;
215- return 0 ;
216- } )
217- . filter ( id => ! gameinfo . get ( id ) . flags . includes ( "experimental" ) ) ;
264+ // look for games with no thumbnails
265+ const metasProd = gameInfoProd . map ( rec => rec . uid ) ;
218266 const keys = [ ...allRecs . keys ( ) ] . filter ( id => ! metasProd . includes ( id ) ) ;
219267 if ( keys . length > 0 ) {
220268 console . log ( `${ keys . length } production games do not have active or completed game records, and so no thumbnail was generated: ${ JSON . stringify ( keys ) } ` ) ;
0 commit comments