SPB Git

spb/spinza

Public
4.6 KB · 125 lines typescript
1import fs from "node:fs";2import path from "node:path";3import { createHash } from "node:crypto";4import { createRequire } from "node:module";5import { GAMES, FEATURED_ORDER } from "@spinza/games";6import { validateDefinition, type CertificationReport, type GameDefinition } from "@spinza/game-core";7import { eq, sql } from "drizzle-orm";8import type { Db } from "./index";9import { gameStatistics, gameVersions, games } from "./schema";1011const require = createRequire(import.meta.url);1213/** Locate games/certifications on disk (works from source and from the deployed tree). */14export function certificationsDir(): string {15  const pkg = path.dirname(require.resolve("@spinza/games/package.json"));16  return path.join(pkg, "certifications");17}1819export function loadCertification(slug: string): CertificationReport | null {20  try {21    return JSON.parse(fs.readFileSync(path.join(certificationsDir(), `${slug}.json`), "utf8"));22  } catch {23    return null;24  }25}2627export function publicSummary(def: GameDefinition) {28  return {29    tagline: def.tagline,30    description: def.description,31    theme: def.theme,32    volatility: def.volatility,33    grid: def.grid,34    minBet: def.minBet,35    maxBet: def.maxBet,36    maxMultiplier: def.maxMultiplier,37    features: def.featureNames,38    tags: def.tags,39    palette: def.presentation.palette,40    presentation: def.presentation,41    isJackpot: !!(def.jackpot || def.holdRespin),42    rtp: def.rtp,43  };44}4546export function definitionHash(def: GameDefinition): string {47  return createHash("sha256").update(JSON.stringify(def)).digest("hex");48}4950export interface SyncReport {51  slug: string;52  version: string;53  lifecycle: string;54  certified: boolean;55}5657/**58 * Upsert every game of the library. Lifecycle rules:59 *  - a game with a PASS certification for its exact version becomes `published`60 *    (unless an admin has set it to `disabled`);61 *  - otherwise it stays `simulation` (visible only in admin).62 */63export async function syncGames(db: Db, log: (s: string) => void = () => {}): Promise<SyncReport[]> {64  const reports: SyncReport[] = [];65  let order = 0;66  for (const def of GAMES) {67    order++;68    const errors = validateDefinition(def).filter((i) => i.level === "error");69    const cert = loadCertification(def.slug);70    const certified = !!cert && cert.status === "PASS" && cert.version === def.version && errors.length === 0;71    const existing = await db.query.games.findFirst({ where: eq(games.slug, def.slug) });72    let lifecycle = certified ? "published" : "simulation";73    if (existing?.lifecycle === "disabled") lifecycle = "disabled";74    const featuredIdx = FEATURED_ORDER.indexOf(def.slug);75    const summary = { ...publicSummary(def), certification: cert ? { status: cert.status, spins: cert.spins, observedRtp: cert.observedRtp, hitRate: cert.hitRate, certifiedAt: cert.certifiedAt } : null };76    const isNew = existing ? existing.isNew && Date.now() - existing.createdAt.getTime() < 14 * 86400_000 : true;77    const [row] = await db78      .insert(games)79      .values({80        slug: def.slug,81        name: def.name,82        version: def.version,83        lifecycle,84        sortOrder: order,85        isFeatured: featuredIdx >= 0,86        isNew,87        summary,88        publishedAt: certified ? new Date() : null,89      })90      .onConflictDoUpdate({91        target: games.slug,92        set: {93          name: def.name,94          version: def.version,95          lifecycle,96          sortOrder: order,97          isFeatured: featuredIdx >= 0,98          isNew,99          summary,100          publishedAt: certified ? sql`coalesce(${games.publishedAt}, now())` : games.publishedAt,101          updatedAt: new Date(),102        },103      })104      .returning();105    await db106      .insert(gameVersions)107      .values({108        gameId: row.id,109        version: def.version,110        definition: def as unknown as Record<string, unknown>,111        definitionHash: definitionHash(def),112        certification: (cert as unknown as Record<string, unknown>) ?? null,113        status: certified ? "published" : "simulation",114      })115      .onConflictDoUpdate({116        target: [gameVersions.gameId, gameVersions.version],117        set: { definition: def as unknown as Record<string, unknown>, definitionHash: definitionHash(def), certification: (cert as unknown as Record<string, unknown>) ?? null, status: certified ? "published" : "simulation" },118      });119    await db.insert(gameStatistics).values({ gameId: row.id }).onConflictDoNothing();120    reports.push({ slug: def.slug, version: def.version, lifecycle, certified });121    log(`  ${certified ? "✓" : "·"} ${def.slug}@${def.version} → ${lifecycle}`);122  }123  return reports;124}125
Files · 3b4179a710ab1cd5f12ca7cfd253b81674f1521e
Menu

Theme