Skip to content

Commit b7b21b9

Browse files
committed
Thumbnails: Only select from games with p25 number of moves made, then select a random move between p25 and p75 for the thumbnail.
1 parent e116858 commit b7b21b9

File tree

2 files changed

+75
-27
lines changed

2 files changed

+75
-27
lines changed

utils/summarize.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ interface GeoStats {
7979
n: number;
8080
}
8181

82-
type StatSummary = {
82+
export type StatSummary = {
8383
numGames: number;
8484
numPlayers: number;
8585
oldestRec?: string;

utils/thumbnails.ts

Lines changed: 74 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
import { S3Client, GetObjectCommand, ListObjectsV2Command, PutObjectCommand, type _Object } from "@aws-sdk/client-s3";
44
import { Handler } from "aws-lambda";
55
import { 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';
77
import { gunzipSync, strFromU8 } from "fflate";
88
import { load as loadIon } from "ion-js";
99
import { ReservoirSampler } from "../lib/ReservoirSampler";
10+
import { type StatSummary } from "./summarize";
1011
import i18n from 'i18next';
1112
// import enGames from "@abstractplay/gameslib/locales/en/apgames.json";
1213
import enBack from "../locales/en/apback.json";
@@ -15,6 +16,7 @@ const REGION = "us-east-1";
1516
const s3 = new S3Client({region: REGION});
1617
const DUMP_BUCKET = "abstractplay-db-dump";
1718
const REC_BUCKET = "thumbnails.abstractplay.com";
19+
const STATS_BUCKET = "records.abstractplay.com";
1820
const cloudfront = new CloudFrontClient({region: REGION});
1921

2022
type 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+
5058
export 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

Comments
 (0)