feat: use d1 for game storage
This commit is contained in:
parent
ee5ed04bcb
commit
5423802ae5
11 changed files with 133 additions and 96 deletions
1
src/app.d.ts
vendored
1
src/app.d.ts
vendored
|
@ -13,7 +13,6 @@ declare global {
|
|||
}
|
||||
interface Platform {
|
||||
env?: {
|
||||
TCL_GUESSR_KV: KVNamespace;
|
||||
TCL_GUESSR_D1: D1Database;
|
||||
};
|
||||
}
|
||||
|
|
122
src/lib/index.ts
122
src/lib/index.ts
|
@ -1,7 +1,5 @@
|
|||
import { error } from "@sveltejs/kit";
|
||||
import type { CheckResponse, ClientGameData, GameOptions, ServerGameData } from "./types";
|
||||
|
||||
type FetchType = typeof fetch;
|
||||
import type { CheckResponse, ClientGameData, GameOptions } from "./types";
|
||||
|
||||
const metroUrl =
|
||||
"https://data.grandlyon.com/geoserver/sytral/ows?SERVICE=WFS&VERSION=2.0.0&request=GetFeature&typename=sytral:tcl_sytral.tcllignemf_2_0_0&outputFormat=application/json&SRSNAME=EPSG:4171&sortBy=gid";
|
||||
|
@ -15,21 +13,25 @@ let lazyTram: [GeoJSON.Feature, string][] | null = null;
|
|||
let lazyStops: GeoJSON.Feature[] | null = null;
|
||||
|
||||
export async function createGame(
|
||||
userId: string,
|
||||
mode: string,
|
||||
stops: GeoJSON.Feature[],
|
||||
kv: KVNamespace<string>,
|
||||
db: D1Database,
|
||||
): Promise<ClientGameData> {
|
||||
const uuid = crypto.randomUUID();
|
||||
const stopNames = stops.map((s) => s.properties!.nom);
|
||||
|
||||
const serverData: ServerGameData = {
|
||||
mode,
|
||||
stops,
|
||||
distances: new Array(stops.length).fill(-1),
|
||||
scores: new Array(stops.length).fill(-1),
|
||||
};
|
||||
const gameSql = db
|
||||
.prepare(
|
||||
"INSERT INTO game (id, user_id, mode, time, total_score) VALUES (?, ?, ?, unixepoch('now'), 0)",
|
||||
)
|
||||
.bind(uuid, userId, mode);
|
||||
|
||||
await kv.put(`game:${uuid}`, JSON.stringify(serverData), { expirationTtl: 600 });
|
||||
const roundSql = db.prepare(
|
||||
"INSERT INTO round (game_id, points, distance, stop_name) VALUES (?, -1, -1, ?)",
|
||||
);
|
||||
|
||||
await db.batch([gameSql, ...stopNames.map((s) => roundSql.bind(uuid, s))]);
|
||||
|
||||
return {
|
||||
gameId: uuid,
|
||||
|
@ -39,36 +41,42 @@ export async function createGame(
|
|||
|
||||
export async function saveScore(
|
||||
uuid: string,
|
||||
index: number,
|
||||
stopName: string,
|
||||
distanceCalc: (latlng: [number, number]) => number,
|
||||
scoreCalc: (dist: number) => number,
|
||||
kv: KVNamespace<string>,
|
||||
db: D1Database,
|
||||
): Promise<CheckResponse | null> {
|
||||
const serverData: ServerGameData | null = await kv.get(`game:${uuid}`, "json");
|
||||
if (!serverData) return null;
|
||||
const stop = await findStopByName(stopName);
|
||||
if (stop === null) return null;
|
||||
|
||||
if (serverData.scores[index] != -1) {
|
||||
return null;
|
||||
} else {
|
||||
const coords = (serverData.stops[index].geometry as GeoJSON.Point).coordinates;
|
||||
const latlng: [number, number] = [coords[1], coords[0]];
|
||||
const row = await db
|
||||
.prepare("SELECT id, game_id, stop_name, points FROM round WHERE game_id = ? AND stop_name = ?")
|
||||
.bind(uuid, stopName)
|
||||
.first();
|
||||
|
||||
const distance = Math.round(distanceCalc(latlng));
|
||||
const score = Math.round(scoreCalc(distance));
|
||||
if (row === null || row.points !== -1) return null;
|
||||
|
||||
serverData.distances[index] = distance;
|
||||
serverData.scores[index] = score;
|
||||
await kv.put(`game:${uuid}`, JSON.stringify(serverData), { expirationTtl: 600 });
|
||||
const coords = (stop.geometry as GeoJSON.Point).coordinates;
|
||||
const latlng: [number, number] = [coords[1], coords[0]];
|
||||
|
||||
return {
|
||||
distance,
|
||||
score,
|
||||
solution: latlng,
|
||||
};
|
||||
}
|
||||
const distance = Math.round(distanceCalc(latlng));
|
||||
const score = Math.round(scoreCalc(distance));
|
||||
|
||||
await db.batch([
|
||||
db
|
||||
.prepare("UPDATE round SET points = ?, distance = ? WHERE id = ?")
|
||||
.bind(score, distance, row.id),
|
||||
db.prepare("UPDATE game SET total_score = total_score + ? WHERE id = ?").bind(score, uuid),
|
||||
]);
|
||||
|
||||
return {
|
||||
distance,
|
||||
score,
|
||||
solution: latlng,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getMetro(fetch: FetchType): Promise<[GeoJSON.Feature, string][]> {
|
||||
export async function getMetro(): Promise<[GeoJSON.Feature, string][]> {
|
||||
if (!lazyMetro) {
|
||||
const metro: GeoJSON.FeatureCollection = await fetch(metroUrl).then((r) => r.json());
|
||||
|
||||
|
@ -81,7 +89,7 @@ export async function getMetro(fetch: FetchType): Promise<[GeoJSON.Feature, stri
|
|||
return lazyMetro;
|
||||
}
|
||||
|
||||
export async function getTram(fetch: FetchType): Promise<[GeoJSON.Feature, string][]> {
|
||||
export async function getTram(): Promise<[GeoJSON.Feature, string][]> {
|
||||
if (!lazyTram) {
|
||||
const tram: GeoJSON.FeatureCollection = await fetch(tramUrl).then((r) => r.json());
|
||||
|
||||
|
@ -94,7 +102,7 @@ export async function getTram(fetch: FetchType): Promise<[GeoJSON.Feature, strin
|
|||
return lazyTram;
|
||||
}
|
||||
|
||||
async function getStops(fetch: FetchType): Promise<GeoJSON.Feature[]> {
|
||||
async function getStops(): Promise<GeoJSON.Feature[]> {
|
||||
if (!lazyStops) {
|
||||
const stops: GeoJSON.FeatureCollection = await fetch(stopsUrl).then((r) => r.json());
|
||||
lazyStops = stops.features;
|
||||
|
@ -103,11 +111,8 @@ async function getStops(fetch: FetchType): Promise<GeoJSON.Feature[]> {
|
|||
return lazyStops;
|
||||
}
|
||||
|
||||
export async function getMergedStops(
|
||||
fetch: FetchType,
|
||||
lineCodes: Set<string>,
|
||||
): Promise<GeoJSON.Feature[]> {
|
||||
const stops = await getStops(fetch);
|
||||
export async function getMergedStops(lineCodes: Set<string>): Promise<GeoJSON.Feature[]> {
|
||||
const stops = await getStops();
|
||||
|
||||
const crossingStops = stops.filter((f) => {
|
||||
if (f.properties?.desserte) {
|
||||
|
@ -124,26 +129,37 @@ export async function getMergedStops(
|
|||
const mergedStops = Array.from(new Set(crossingStops.map((f) => f.properties?.nom))).map(
|
||||
(nom) => {
|
||||
const matching = crossingStops.filter((f) => f.properties?.nom === nom);
|
||||
|
||||
const longs = matching.map((f) => (f.geometry as GeoJSON.Point).coordinates[0]);
|
||||
const lonAvg = longs.reduce((a, b) => a + b) / longs.length;
|
||||
|
||||
const lats = matching.map((f) => (f.geometry as GeoJSON.Point).coordinates[1]);
|
||||
const latAvg = lats.reduce((a, b) => a + b) / lats.length;
|
||||
|
||||
const desserte = matching.map((f) => f.properties?.desserte).join(",");
|
||||
|
||||
const feature = matching[0];
|
||||
feature.geometry = { type: "Point", coordinates: [lonAvg, latAvg] };
|
||||
if (feature.properties) feature.properties.desserte = desserte;
|
||||
|
||||
return feature;
|
||||
return mergeStops(matching)!;
|
||||
},
|
||||
);
|
||||
|
||||
return mergedStops;
|
||||
}
|
||||
|
||||
async function findStopByName(name: string): Promise<GeoJSON.Feature | null> {
|
||||
const stops = await getStops();
|
||||
|
||||
return mergeStops(stops.filter((f) => f.properties?.nom === name));
|
||||
}
|
||||
|
||||
function mergeStops(stops: GeoJSON.Feature[]): GeoJSON.Feature | null {
|
||||
if (stops.length === 0) return null;
|
||||
|
||||
const longs = stops.map((f) => (f.geometry as GeoJSON.Point).coordinates[0]);
|
||||
const lonAvg = longs.reduce((a, b) => a + b) / longs.length;
|
||||
|
||||
const lats = stops.map((f) => (f.geometry as GeoJSON.Point).coordinates[1]);
|
||||
const latAvg = lats.reduce((a, b) => a + b) / lats.length;
|
||||
|
||||
const desserte = stops.map((f) => f.properties?.desserte).join(",");
|
||||
|
||||
const feature = stops[0];
|
||||
feature.geometry = { type: "Point", coordinates: [lonAvg, latAvg] };
|
||||
if (feature.properties) feature.properties.desserte = desserte;
|
||||
|
||||
return feature;
|
||||
}
|
||||
|
||||
export function parseOptions(url: URL): GameOptions {
|
||||
const mode = url.searchParams.get("mode");
|
||||
if (mode !== "easy" && mode !== "hard" && mode !== "extreme demon ultra miguel") {
|
||||
|
|
|
@ -9,13 +9,6 @@ export interface MapData {
|
|||
stops: [number, number][];
|
||||
}
|
||||
|
||||
export interface ServerGameData {
|
||||
mode: string;
|
||||
stops: GeoJSON.Feature[];
|
||||
distances: number[];
|
||||
scores: number[];
|
||||
}
|
||||
|
||||
export interface ClientGameData {
|
||||
stopNames: string[];
|
||||
gameId: string;
|
||||
|
@ -23,7 +16,7 @@ export interface ClientGameData {
|
|||
|
||||
export interface CheckData {
|
||||
gameId: string;
|
||||
index: number;
|
||||
stopName: string;
|
||||
latlng: [number, number];
|
||||
}
|
||||
|
||||
|
|
|
@ -4,23 +4,23 @@ import type { RequestHandler } from "./$types";
|
|||
import type { CheckData } from "$lib/types";
|
||||
|
||||
export const POST: RequestHandler = async ({ request, platform }) => {
|
||||
const kv = platform?.env?.TCL_GUESSR_KV;
|
||||
if (!kv) error(500, "could not connect to kv");
|
||||
const db = platform?.env?.TCL_GUESSR_D1;
|
||||
if (!db) error(500, "could not connect to d1");
|
||||
|
||||
const data: CheckData = await request.json();
|
||||
|
||||
const solution = await saveScore(
|
||||
data.gameId,
|
||||
data.index,
|
||||
data.stopName,
|
||||
(solution) => calcDistance(data.latlng, solution),
|
||||
(distance) => calculatePoints(distance),
|
||||
kv,
|
||||
db,
|
||||
);
|
||||
|
||||
if (solution) {
|
||||
return Response.json(solution);
|
||||
} else {
|
||||
return new Response(null, { status: 404 });
|
||||
return error(404);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -2,25 +2,28 @@ import { error } from "@sveltejs/kit";
|
|||
import type { RequestHandler } from "./$types";
|
||||
import { createGame, getMergedStops, getMetro, getTram, parseOptions, sample } from "$lib";
|
||||
|
||||
export const GET: RequestHandler = async ({ fetch, url, platform }) => {
|
||||
const kv = platform?.env?.TCL_GUESSR_KV;
|
||||
if (!kv) error(500, "could not connect to kv");
|
||||
export const GET: RequestHandler = async ({ url, platform, locals }) => {
|
||||
const db = platform?.env?.TCL_GUESSR_D1 ?? null;
|
||||
if (db === null) error(500, "could not connect to d1");
|
||||
|
||||
const user = locals.user;
|
||||
if (user === null) error(401, "not logged in");
|
||||
|
||||
const options = parseOptions(url);
|
||||
const lineColors: [GeoJSON.Feature, string][] = [];
|
||||
|
||||
if (options.metro) lineColors.push(...(await getMetro(fetch)));
|
||||
if (options.tram) lineColors.push(...(await getTram(fetch)));
|
||||
if (options.metro) lineColors.push(...(await getMetro()));
|
||||
if (options.tram) lineColors.push(...(await getTram()));
|
||||
|
||||
const lineCodes = new Set<string>(lineColors.map(([f]) => f.properties!.code_ligne));
|
||||
const crossingStops = await getMergedStops(fetch, lineCodes);
|
||||
const crossingStops = await getMergedStops(lineCodes);
|
||||
|
||||
const randomStops = sample(crossingStops, 5);
|
||||
if (!randomStops) {
|
||||
error(400, "could not select random stop");
|
||||
}
|
||||
|
||||
const gameData = await createGame(options.mode, randomStops, kv);
|
||||
const gameData = await createGame(user.id, options.mode, randomStops, db);
|
||||
|
||||
return Response.json(gameData);
|
||||
};
|
||||
|
|
|
@ -2,21 +2,21 @@ import { getMergedStops, getMetro, getTram, parseOptions } from "$lib";
|
|||
import type { MapData } from "$lib/types";
|
||||
import type { RequestHandler } from "./$types";
|
||||
|
||||
export const GET: RequestHandler = async ({ fetch, url }) => {
|
||||
export const GET: RequestHandler = async ({ url }) => {
|
||||
const options = parseOptions(url);
|
||||
const mapData: MapData = { lines: [], stops: [] };
|
||||
|
||||
if (options.mode !== "extreme demon ultra miguel") {
|
||||
const lineColors: [GeoJSON.Feature, string][] = [];
|
||||
|
||||
if (options.metro) lineColors.push(...(await getMetro(fetch)));
|
||||
if (options.tram) lineColors.push(...(await getTram(fetch)));
|
||||
if (options.metro) lineColors.push(...(await getMetro()));
|
||||
if (options.tram) lineColors.push(...(await getTram()));
|
||||
|
||||
mapData.lines = lineColors;
|
||||
|
||||
if (options.mode === "easy") {
|
||||
const lineCodes = new Set<string>(lineColors.map(([f]) => f.properties!.code_ligne));
|
||||
const crossingStops = await getMergedStops(fetch, lineCodes);
|
||||
const crossingStops = await getMergedStops(lineCodes);
|
||||
|
||||
const strippedStops: [number, number][] = crossingStops.map((f) => {
|
||||
const coords = (f.geometry as GeoJSON.Point).coordinates;
|
||||
|
|
|
@ -71,7 +71,7 @@
|
|||
|
||||
const checkData: CheckData = {
|
||||
gameId: gameData.gameId,
|
||||
index: currentIndex,
|
||||
stopName: gameData.stopNames[currentIndex],
|
||||
latlng: [playerMarker.getLatLng().lat, playerMarker.getLatLng().lng],
|
||||
};
|
||||
|
||||
|
|
|
@ -1,16 +1,27 @@
|
|||
import type { PageServerLoad } from "./$types";
|
||||
import { error } from "@sveltejs/kit";
|
||||
import type { ServerGameData } from "$lib/types";
|
||||
|
||||
interface JoinedRoundScore {
|
||||
mode: string;
|
||||
total_score: number;
|
||||
points: number;
|
||||
distance: number;
|
||||
stop_name: string;
|
||||
}
|
||||
|
||||
export const load: PageServerLoad = async ({ url, platform }) => {
|
||||
const kv = platform?.env?.TCL_GUESSR_KV;
|
||||
if (!kv) error(500, "could not connect to kv");
|
||||
const db = platform?.env?.TCL_GUESSR_D1;
|
||||
if (!db) error(500, "could not connect to d1");
|
||||
|
||||
const gameId = url.searchParams.get("gameId");
|
||||
if (!gameId) error(400, "gameId was not specified");
|
||||
|
||||
const gameData: ServerGameData | null = await kv.get(`game:${gameId}`, "json");
|
||||
if (!gameData) error(404, "could not fetch game data");
|
||||
const { results } = await db
|
||||
.prepare(
|
||||
"SELECT game.mode, game.total_score, round.points, round.distance, round.stop_name FROM game INNER JOIN round ON round.game_id = game.id WHERE game.id = ?;",
|
||||
)
|
||||
.bind(gameId)
|
||||
.all<JoinedRoundScore>();
|
||||
|
||||
return { gameData, gameId };
|
||||
return { rounds: results, gameId };
|
||||
};
|
||||
|
|
|
@ -7,9 +7,9 @@
|
|||
}
|
||||
|
||||
const props: Props = $props();
|
||||
const gameData = props.data.gameData;
|
||||
|
||||
const totalScore = gameData.scores.reduce((a, b) => a + b);
|
||||
const totalScore = props.data.rounds[0].total_score;
|
||||
const mode = props.data.rounds[0].mode;
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
|
@ -17,15 +17,15 @@
|
|||
|
||||
<h2>score total: {totalScore}</h2>
|
||||
|
||||
<span>mode: {gameData.mode}</span>
|
||||
<span>mode: {mode}</span>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
{#each gameData.stops as stop, i}
|
||||
{#each props.data.rounds as round}
|
||||
<tr>
|
||||
<td>{stop.properties!.nom}</td>
|
||||
<td>{gameData.scores[i]} points</td>
|
||||
<td>{gameData.distances[i]} metres</td>
|
||||
<td>{round.stop_name}</td>
|
||||
<td>{round.points} points</td>
|
||||
<td>{round.distance} metres</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue