last version before 2.0

This commit is contained in:
MasterGordon 2024-09-21 12:12:39 +02:00
parent ce3eb836da
commit 62055855e5
25 changed files with 627 additions and 119 deletions

3
.gitignore vendored
View File

@ -6,12 +6,13 @@ yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
tsconfig.app.tsbuildinfo
tsconfig.*.tsbuildinfo
node_modules
dist
dist-ssr
*.local
temp_dbs
# Editor directories and files
.vscode/*

View File

@ -0,0 +1,37 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { BunSQLiteDatabase } from "drizzle-orm/bun-sqlite";
import type { z } from "zod";
interface RequestContext {
user?: string;
db: BunSQLiteDatabase;
}
export type Endpoint<TInput, TResponse> = {
validate: z.ZodType<TInput>;
handler: (input: TInput, context: RequestContext) => Promise<TResponse>;
};
export type Request<TEndpoint extends Endpoint<any, any>> = {
method: "POST";
url: string;
body: z.infer<TEndpoint["validate"]>;
};
export const createEndpoint = <TInput, TResponse>(
validate: z.ZodType<TInput>,
handler: (input: TInput, context: RequestContext) => Promise<TResponse>,
): Endpoint<TInput, TResponse> => {
return { validate, handler };
};
export type Controller<TEndpoints extends Record<string, Endpoint<any, any>>> =
TEndpoints;
export const createController = <
TEndpoints extends Record<string, Endpoint<any, any>>,
>(
endpoints: TEndpoints,
): Controller<TEndpoints> => {
return endpoints;
};

View File

@ -0,0 +1,45 @@
import { z } from "zod";
import { createController, createEndpoint } from "./controller";
import { getGame, upsertGameState } from "../repositories/gameRepository";
import {
serverGame,
serverToClientGame,
type ServerGame,
} from "../../shared/game";
import crypto from "crypto";
import { game } from "../entities/game";
import { UnauthorizedError } from "../errors/UnauthorizedError";
import { emit } from "../events";
export const gameController = createController({
getGameState: createEndpoint(z.string(), async (uuid, ctx) => {
const game = await getGame(ctx.db, uuid);
const parsed = JSON.parse(game.gameState);
const gameState = await serverGame.parseAsync(parsed);
if (game.finished) return gameState;
return serverToClientGame(gameState);
}),
createGame: createEndpoint(z.undefined(), async (_, { user, db }) => {
if (!user) throw new UnauthorizedError("Unauthorized");
const uuid = crypto.randomUUID() as string;
const newGame: ServerGame = game.createGame({
uuid,
user: user,
mines: 2,
width: 4,
height: 4,
});
upsertGameState(db, newGame);
emit({
type: "new",
user,
});
emit({
type: "updateStage",
game: uuid,
stage: newGame.stage,
started: newGame.started,
});
return newGame;
}),
});

53
backend/entities/game.ts Normal file
View File

@ -0,0 +1,53 @@
import type { ServerGame } from "../../shared/game";
interface CreateGameOptions {
uuid: string;
user: string;
width: number;
height: number;
mines: number;
}
export const game = {
createGame: (options: CreateGameOptions): ServerGame => {
const { uuid, user, width, height, mines } = options;
if (mines > width * height) {
throw new Error("Too many mines");
}
const minesArray = Array.from({ length: width }, () =>
new Array(height).fill(false),
);
const isRevealedArray = Array.from({ length: width }, () =>
new Array(height).fill(false),
);
const isFlaggedArray = Array.from({ length: width }, () =>
new Array(height).fill(false),
);
let remainingMines = mines;
while (remainingMines > 0) {
const x = Math.floor(Math.random() * width);
const y = Math.floor(Math.random() * height);
if (!minesArray[x][y]) {
minesArray[x][y] = true;
remainingMines--;
}
}
return {
uuid,
user,
finished: 0,
started: Date.now(),
width,
height,
mines: minesArray,
isRevealed: isRevealedArray,
isFlagged: isFlaggedArray,
stage: 1,
lastClick: [-1, -1],
minesCount: mines,
};
},
};

View File

@ -0,0 +1,6 @@
export class BadRequestError extends Error {
constructor(message: string) {
super(message);
this.name = "BadRequestError";
}
}

View File

@ -0,0 +1,6 @@
export class UnauthorizedError extends Error {
constructor(message: string) {
super(message);
this.name = "UnauthorizedError";
}
}

39
backend/events.ts Normal file
View File

@ -0,0 +1,39 @@
import type { ClientGame } from "../shared/game";
export type EventType = "new" | "finished" | "updateGame" | "updateStage";
type Events =
| {
type: "new";
user: string;
}
| {
type: "loss";
user: string;
stage: number;
}
| {
type: "updateGame";
game: string;
data: ClientGame;
}
| {
type: "updateStage";
game: string;
stage: number;
started: number;
};
const listeners = new Set<(event: Events) => void>();
export const on = (listener: (event: Events) => void) => {
listeners.add(listener);
};
export const off = (listener: (event: Events) => void) => {
listeners.delete(listener);
};
export const emit = (event: Events) => {
listeners.forEach((listener) => listener(event));
};

View File

@ -1,28 +1,12 @@
import type { ServerWebSocket } from "bun";
interface Scoreboard {
stage: number;
user: string;
}
const loadScoreboard = async (): Promise<Scoreboard[]> => {
try {
const scoreboardFile = Bun.file("./scoreboard.json");
const scoreboard = await scoreboardFile.json();
return scoreboard;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
return [];
}
};
const allowCors = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
};
const lastMessage = new WeakMap<ServerWebSocket<unknown>, number>();
const userName = new WeakMap<ServerWebSocket<unknown>, string>();
const server = Bun.serve({
async fetch(request: Request) {
if (request.method === "OPTIONS") {
@ -32,67 +16,23 @@ const server = Bun.serve({
if (request.url.endsWith("ws")) {
if (server.upgrade(request)) return new Response("ok");
}
if (new URL(request.url).pathname === "/submit") {
const body = await request.text();
const data = JSON.parse(body) as { stage: number; user: string };
const scoreboardFile = Bun.file("./scoreboard.json");
const scoreboard = await loadScoreboard();
const currentScore = scoreboard.find((s) => s.user === data.user);
if (currentScore) {
if (currentScore.stage < data.stage) {
currentScore.stage = data.stage;
Bun.write(scoreboardFile, JSON.stringify(scoreboard));
}
return new Response(JSON.stringify(currentScore), {
headers: {
"content-type": "application/json",
...allowCors,
},
});
}
scoreboard.push(data);
Bun.write(scoreboardFile, JSON.stringify(scoreboard));
return new Response(JSON.stringify(data), {
headers: {
"content-type": "application/json",
...allowCors,
},
});
}
const scoreboard = await loadScoreboard();
const sorted = scoreboard.sort((a, b) => b.stage - a.stage);
return new Response(JSON.stringify(sorted), {
headers: {
"content-type": "application/json",
...allowCors,
},
});
},
websocket: {
message: (ws, message) => {
if (typeof message !== "string") {
return;
}
const msg = JSON.parse(message);
const now = Date.now();
if (lastMessage.has(ws) && now - lastMessage.get(ws)! < 200) {
const user = userName.get(ws);
try {
const msg = JSON.parse(message);
console.log(msg);
} catch (e) {
console.error("Faulty request", message, e);
return;
}
lastMessage.set(ws, now);
if (msg.type === "loss") {
server.publish(
"minesweeper",
JSON.stringify({ type: "loss", user: msg.user, stage: msg.stage }),
);
} else if (msg.type === "new") {
server.publish(
"minesweeper",
JSON.stringify({ type: "new", user: msg.user }),
);
}
},
open: async (ws) => {
ws.subscribe("minesweeper");
ws.subscribe("minesweeper-global");
},
},
port: 8076,

View File

@ -0,0 +1,139 @@
import { describe, it, expect } from "bun:test";
import { getTestDb } from "../database/getTestDb";
import { Game } from "../schema";
import {
getCurrentGame,
getGame,
getGamesCount,
upsertGame,
} from "./gameRepository";
describe("GameRepository", () => {
it("should get game by uuid", async () => {
const db = getTestDb();
const started = Date.now();
await db.insert(Game).values({
uuid: "TestUuid",
user: "TestUser",
stage: 1,
gameState: "ANY",
finished: 1,
started,
});
const game = await getGame(db, "TestUuid");
expect(game).toEqual({
uuid: "TestUuid",
user: "TestUser",
stage: 1,
gameState: "ANY",
finished: 1,
started,
});
});
it("should return undefined if game does not exist", async () => {
const db = getTestDb();
const game = await getGame(db, "TestUuid");
expect(game).toBeUndefined();
});
it("should return latest game", async () => {
const db = getTestDb();
const started = Date.now();
await db.insert(Game).values({
uuid: "TestUuid",
user: "TestUser",
stage: 1,
gameState: "ANY",
finished: 1,
started,
});
await db.insert(Game).values({
uuid: "TestUuid2",
user: "TestUser",
stage: 2,
gameState: "ANY",
finished: 1,
started: started + 1,
});
const game = await getCurrentGame(db, "TestUser");
expect(game).toEqual({
uuid: "TestUuid2",
user: "TestUser",
stage: 2,
gameState: "ANY",
finished: 1,
started: started + 1,
});
});
it("should return game count", async () => {
const db = getTestDb();
const started = Date.now();
const uuids = ["TestUuid", "TestUuid2", "TestUuid3"];
for (const uuid of uuids) {
await db.insert(Game).values({
uuid,
user: "TestUser",
stage: 1,
gameState: "ANY",
finished: 1,
started,
});
}
const count = await getGamesCount(db, "TestUser");
expect(count).toEqual(3);
});
it("should insert game", async () => {
const db = getTestDb();
const started = Date.now();
await upsertGame(db, {
uuid: "TestUuid",
user: "TestUser",
stage: 1,
gameState: "ANY",
finished: 1,
started,
});
const game = await getGame(db, "TestUuid");
expect(game).toEqual({
uuid: "TestUuid",
user: "TestUser",
stage: 1,
gameState: "ANY",
finished: 1,
started,
});
});
it("should update game", async () => {
const db = getTestDb();
const started = Date.now();
await db.insert(Game).values({
uuid: "TestUuid",
user: "TestUser",
stage: 1,
gameState: "ANY",
finished: 1,
started,
});
await upsertGame(db, {
uuid: "TestUuid",
user: "TestUser",
stage: 2,
gameState: "ANY",
finished: 1,
started: started + 1,
});
const game = await getGame(db, "TestUuid");
expect(game).toEqual({
uuid: "TestUuid",
user: "TestUser",
stage: 2,
gameState: "ANY",
finished: 1,
started: started + 1,
});
});
});

View File

@ -0,0 +1,76 @@
import type { BunSQLiteDatabase } from "drizzle-orm/bun-sqlite";
import { Game, type GameType } from "../schema";
import { eq, sql, desc, and, not } from "drizzle-orm";
import type { ServerGame } from "../../shared/game";
export const getGame = async (db: BunSQLiteDatabase, uuid: string) => {
return (await db.select().from(Game).where(eq(Game.uuid, uuid)))[0];
};
export const getGames = async (db: BunSQLiteDatabase, user: string) => {
return await db
.select()
.from(Game)
.where(and(eq(Game.user, user), not(eq(Game.finished, 0))))
.orderBy(Game.started, sql`desc`);
};
export const getCurrentGame = async (db: BunSQLiteDatabase, user: string) => {
return (
await db
.select()
.from(Game)
.where(eq(Game.user, user))
.orderBy(desc(Game.started))
.limit(1)
)[0];
};
export const getGamesCount = async (db: BunSQLiteDatabase, user: string) => {
return (
await db
.select({ count: sql<number>`count(*)` })
.from(Game)
.where(eq(Game.user, user))
)[0].count;
};
export const upsertGame = async (db: BunSQLiteDatabase, game: GameType) => {
const { uuid, user, stage, gameState, finished, started } = game;
const games = await db.select().from(Game).where(eq(Game.uuid, uuid));
if (games.length > 0) {
await db
.update(Game)
.set({
stage,
gameState,
finished,
started,
})
.where(eq(Game.uuid, uuid));
} else {
await db.insert(Game).values({
uuid,
user,
stage,
gameState,
finished,
started,
});
}
};
export const upsertGameState = async (
db: BunSQLiteDatabase,
game: ServerGame,
) => {
const { uuid, user, stage, finished, started } = game;
upsertGame(db, {
uuid,
user,
stage,
gameState: JSON.stringify(game),
finished,
started,
});
};

View File

@ -1,10 +1,10 @@
import { describe, it, expect } from "bun:test";
import { getScoreBoard } from "./score.ts";
import { getScoreBoard } from "./scoreRepository.ts";
import { getTestDb } from "../database/getTestDb.ts";
import { Game, User } from "../schema.ts";
describe("Score", () => {
it("should return the score board", async () => {
describe("ScoreRepository", () => {
it("should return the scoreboard", async () => {
const db = getTestDb();
await db.insert(User).values({
name: "TestUser",
@ -16,7 +16,7 @@ describe("Score", () => {
stage: 1,
gameState: "ANY",
finished: 1,
timestamp: Date.now(),
started: Date.now(),
});
await db.insert(Game).values({
user: "TestUser",
@ -24,7 +24,7 @@ describe("Score", () => {
stage: 10,
gameState: "ANY",
finished: 1,
timestamp: Date.now(),
started: Date.now(),
});
await db.insert(Game).values({
user: "TestUser",
@ -32,7 +32,7 @@ describe("Score", () => {
stage: 20,
gameState: "ANY",
finished: 0,
timestamp: Date.now(),
started: Date.now(),
});
const result = await getScoreBoard(db);
expect(result).toEqual([{ stage: 10, user: "TestUser" }]);

View File

@ -1,4 +1,4 @@
import { eq, sql } from "drizzle-orm";
import { eq, sql, not } from "drizzle-orm";
import { Game } from "../schema";
import type { BunSQLiteDatabase } from "drizzle-orm/bun-sqlite";
@ -6,6 +6,6 @@ export const getScoreBoard = async (db: BunSQLiteDatabase) => {
return await db
.select({ stage: sql<number>`max(${Game.stage})`, user: Game.user })
.from(Game)
.where(eq(Game.finished, 1))
.where(not(eq(Game.finished, 0)))
.groupBy(Game.user);
};

View File

@ -1,8 +1,8 @@
import { describe, it, expect } from "bun:test";
import { getTestDb } from "../database/getTestDb";
import { getUser, loginUser, registerUser } from "./user";
import { getUser, loginUser, registerUser } from "./userRepository";
describe("User", () => {
describe("UserRepository", () => {
it("should register a user", async () => {
const db = getTestDb();
await registerUser(db, "TestUser", "test");

37
backend/router.ts Normal file
View File

@ -0,0 +1,37 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { Controller, Endpoint } from "./controller/controller";
import { gameController } from "./controller/gameController";
import { db } from "./database/db";
import { BadRequestError } from "./errors/BadRequestError";
const controllers = {
game: gameController,
} satisfies Record<string, Controller<any>>;
export const handleRequest = (message: unknown, sessionUser?: string) => {
const ctx = {
user: sessionUser,
db,
};
if (
!message ||
!(typeof message === "object") ||
!("type" in message) ||
!("payload" in message)
)
return;
const { type, payload } = message;
if (!(typeof type === "string")) return;
const [controllerName, action] = type.split(".");
if (!(controllerName in controllers)) return;
// @ts-expect-error controllers[controllerName] is a Controller
const endpoint = controllers[controllerName][action] as Endpoint<any, any>;
const input = endpoint.validate.safeParse(payload);
if (input.success) {
const result = endpoint.handler(input.data, ctx);
return result;
}
throw new BadRequestError(input.error.message);
};
export type Routes = typeof controllers;

View File

@ -13,7 +13,7 @@ export const Game = sqliteTable("games", {
gameState: text("gameState").notNull(),
stage: integer("stage").notNull(),
finished: integer("finished").notNull().default(0),
timestamp: integer("timestamp").notNull(),
started: integer("timestamp").notNull(),
});
export type UserType = Omit<typeof User.$inferSelect, "password"> & {

BIN
bun.lockb

Binary file not shown.

View File

@ -20,6 +20,7 @@
"react-dom": "^18.3.1",
"react-hot-toast": "^2.4.1",
"use-sound": "^4.0.3",
"zod": "^3.23.8",
"zustand": "^4.5.5"
},
"devDependencies": {

74
shared/game.test.ts Normal file
View File

@ -0,0 +1,74 @@
import { describe, it, expect } from "bun:test";
import { getValue, serverToClientGame } from "./game";
describe("Game", () => {
it("should get value", () => {
const mines = [
[false, false, true, true, true],
[true, false, true, false, true],
[false, false, true, true, true],
[false, false, false, false, false],
];
expect(getValue(mines, 0, 0)).toEqual(1);
expect(getValue(mines, 0, 1)).toEqual(3);
expect(getValue(mines, 3, 0)).toEqual(0);
expect(getValue(mines, 1, 3)).toEqual(8);
});
it("should convert server to client game", () => {
const serverGame = {
mines: [
[false, false, true, true, true],
[true, false, true, false, true],
[false, false, true, true, true],
[false, false, false, false, false],
],
minesCount: 4,
isRevealed: [
[false, false, true, false, true],
[true, false, true, false, true],
[false, false, true, false, true],
[false, false, false, false, false],
],
isFlagged: [
[false, false, true, false, true],
[true, false, true, false, true],
[false, false, true, false, true],
[true, false, false, false, false],
],
isGameOver: false,
started: 1679599200000,
finished: 0,
lastClick: [0, 0] satisfies [number, number],
uuid: "C270D7CD-AF97-42CE-A6C9-CB765102CA17",
width: 5,
height: 4,
};
expect(serverToClientGame(serverGame)).toEqual({
minesCount: 4,
isRevealed: [
[false, false, true, false, true],
[true, false, true, false, true],
[false, false, true, false, true],
[false, false, false, false, false],
],
isFlagged: [
[false, false, true, false, true],
[true, false, true, false, true],
[false, false, true, false, true],
[true, false, false, false, false],
],
values: [
[-1, -1, 2, -1, 2],
[0, -1, 4, -1, 4],
[-1, -1, 2, -1, 2],
[-1, -1, -1, -1, -1],
],
lastClick: [0, 0],
started: 1679599200000,
uuid: "C270D7CD-AF97-42CE-A6C9-CB765102CA17",
width: 5,
height: 4,
});
});
});

68
shared/game.ts Normal file
View File

@ -0,0 +1,68 @@
import { z } from "zod";
export const clientGame = z.object({
user: z.string(),
uuid: z.string(),
width: z.number(),
height: z.number(),
isRevealed: z.array(z.array(z.boolean())),
isFlagged: z.array(z.array(z.boolean())),
values: z.array(z.array(z.number())),
minesCount: z.number(),
lastClick: z.tuple([z.number(), z.number()]),
started: z.number(),
stage: z.number(),
});
export const serverGame = z.object({
user: z.string(),
uuid: z.string(),
width: z.number(),
height: z.number(),
isRevealed: z.array(z.array(z.boolean())),
isFlagged: z.array(z.array(z.boolean())),
mines: z.array(z.array(z.boolean())),
minesCount: z.number(),
lastClick: z.tuple([z.number(), z.number()]),
started: z.number(),
finished: z.number().default(0),
stage: z.number(),
});
export type ClientGame = z.infer<typeof clientGame>;
export type ServerGame = z.infer<typeof serverGame>;
export const getValue = (mines: boolean[][], x: number, y: number) => {
const neighbors = [
mines[x - 1]?.[y - 1],
mines[x]?.[y - 1],
mines[x + 1]?.[y - 1],
mines[x - 1]?.[y],
mines[x + 1]?.[y],
mines[x - 1]?.[y + 1],
mines[x]?.[y + 1],
mines[x + 1]?.[y + 1],
];
return neighbors.filter((n) => n).length;
};
export const serverToClientGame = (game: ServerGame): ClientGame => {
return {
user: game.user,
uuid: game.uuid,
width: game.width,
height: game.height,
isRevealed: game.isRevealed,
isFlagged: game.isFlagged,
minesCount: game.minesCount,
values: game.mines.map((_, i) =>
game.mines[0].map((_, j) => {
if (!game.isRevealed[i][j]) return -1;
return getValue(game.mines, i, j);
}),
),
lastClick: game.lastClick,
started: game.started,
stage: game.stage,
};
};

View File

@ -23,7 +23,6 @@ function useMaxToasts(max: number) {
}, [toasts, max]);
}
useGameStore.getState().resetGame(4, 4, 2);
function App() {
const game = useGameStore();
const [scores, setScores] = useState<Score[]>([]);
@ -38,6 +37,10 @@ function App() {
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [game.isGameOver]);
useEffect(() => {
game.resetGame(4, 4, 2);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
fetch("https://mb.gordon.business")
@ -75,6 +78,15 @@ function App() {
onChange={(e) => game.setName(e.target.value)}
/>
</p>
<p>
Feed:{" "}
<button
onClick={() => game.setShowFeed(!game.showFeed)}
style={{ padding: "0.5rem" }}
>
{game.showFeed ? "Shown" : "Hidden"}
</button>
</p>
</div>
<div className="scores">
{scores.slice(0, 10).map((score) => (

View File

@ -2,6 +2,7 @@ import { create } from "zustand";
import { newGame } from "./ws";
interface GameState {
showFeed: boolean;
mines: boolean[][];
minesCount: number;
isRevealed: boolean[][];
@ -13,7 +14,6 @@ interface GameState {
stage: number;
name: string;
initializeGame: (width: number, height: number, mines: number) => void;
flag: (x: number, y: number) => void;
reveal: (x: number, y: number) => boolean;
getValue: (x: number, y: number) => number;
@ -31,6 +31,7 @@ interface GameState {
triggerPostGame: () => boolean;
expandBoard: () => void;
setName: (name: string) => void;
setShowFeed: (showFeed: boolean) => void;
}
const useGameStore = create<GameState>((set, get) => ({
@ -44,41 +45,9 @@ const useGameStore = create<GameState>((set, get) => ({
height: 0,
stage: 1,
name: localStorage.getItem("name") || "No Name",
initializeGame: (width, height, mines) => {
mines = Math.min(mines, width * height);
const minesArray = Array.from({ length: width }, () =>
new Array(height).fill(false),
);
const isRevealedArray = Array.from({ length: width }, () =>
new Array(height).fill(false),
);
const isFlaggedArray = Array.from({ length: width }, () =>
new Array(height).fill(false),
);
let remainingMines = mines;
while (remainingMines > 0) {
const x = Math.floor(Math.random() * width);
const y = Math.floor(Math.random() * height);
if (!minesArray[x][y]) {
minesArray[x][y] = true;
remainingMines--;
}
}
set({
width,
height,
mines: minesArray,
isRevealed: isRevealedArray,
isFlagged: isFlaggedArray,
minesCount: mines,
isGameOver: false,
startTime: Date.now(),
});
},
showFeed: !localStorage.getItem("showFeed")
? true
: localStorage.getItem("showFeed") === "true",
flag: (x, y) => {
set((state) => {
@ -389,6 +358,10 @@ const useGameStore = create<GameState>((set, get) => ({
localStorage.setItem("name", name);
set({ name });
},
setShowFeed: (showFeed) => {
localStorage.setItem("showFeed", showFeed.toString());
set({ showFeed });
},
}));
export default useGameStore;

View File

@ -1,4 +1,5 @@
import toast from "react-hot-toast";
import useGameStore from "./GameState";
let ws: WebSocket;
@ -11,6 +12,7 @@ export const connectWS = () => {
if (data.user === name) {
return;
}
if (!useGameStore.getState().showFeed) return;
switch (data.type) {
case "new":
toast(data.user + " started a new game", {

View File

@ -20,5 +20,5 @@
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
"include": ["src", "shared"]
}

View File

@ -1 +0,0 @@
{"root":["./vite.config.ts","./backend/index.ts","./backend/migrate.ts","./backend/schema.ts","./backend/test-setup.ts","./backend/database/db.ts","./backend/database/getDb.ts","./backend/database/getTestDb.ts","./backend/services/score.test.ts","./backend/services/score.ts"],"version":"5.6.2"}