diff --git a/migrations/0002_add_scores_tables.sql b/migrations/0002_add_scores_tables.sql new file mode 100644 index 0000000..3f62286 --- /dev/null +++ b/migrations/0002_add_scores_tables.sql @@ -0,0 +1,19 @@ +-- Migration number: 0002 2024-11-21T10:59:43.394Z +CREATE TABLE game ( + `id` TEXT PRIMARY KEY, -- uuid + `user_id` TEXT NOT NULL, + `mode` TEXT NOT NULL, + `time` INTEGER NOT NULL, + `total_score` INTEGER NOT NULL, + FOREIGN KEY(user_id) REFERENCES user(id) +); + +create table round ( + `id` INTEGER PRIMARY KEY ASC, -- rowid alias, should be automatically assigned + `game_id` TEXT NOT NULL, + `points` INTEGER NOT NULL, + `distance` INTEGER NOT NULL, + `stop_name` TEXT NOT NULL, + FOREIGN KEY(game_id) REFERENCES game(id) +); + diff --git a/src/app.d.ts b/src/app.d.ts index e249b04..13b2301 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -13,7 +13,6 @@ declare global { } interface Platform { env?: { - TCL_GUESSR_KV: KVNamespace; TCL_GUESSR_D1: D1Database; }; } diff --git a/src/lib/index.ts b/src/lib/index.ts index 4973935..eda127d 100644 --- a/src/lib/index.ts +++ b/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, + db: D1Database, ): Promise { 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, + db: D1Database, ): Promise { - 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 { +async function getStops(): Promise { 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 { return lazyStops; } -export async function getMergedStops( - fetch: FetchType, - lineCodes: Set, -): Promise { - const stops = await getStops(fetch); +export async function getMergedStops(lineCodes: Set): Promise { + 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 { + 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") { diff --git a/src/lib/types.ts b/src/lib/types.ts index b856c85..82d34f6 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -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]; } diff --git a/src/routes/api/check/+server.ts b/src/routes/api/check/+server.ts index 0d888f8..981dc13 100644 --- a/src/routes/api/check/+server.ts +++ b/src/routes/api/check/+server.ts @@ -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); } }; diff --git a/src/routes/api/game/+server.ts b/src/routes/api/game/+server.ts index c7901a8..2fd6e6a 100644 --- a/src/routes/api/game/+server.ts +++ b/src/routes/api/game/+server.ts @@ -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(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); }; diff --git a/src/routes/api/map/+server.ts b/src/routes/api/map/+server.ts index e82590b..29e1a53 100644 --- a/src/routes/api/map/+server.ts +++ b/src/routes/api/map/+server.ts @@ -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(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; diff --git a/src/routes/game/+page.svelte b/src/routes/game/+page.svelte index fb56357..4de3b1b 100644 --- a/src/routes/game/+page.svelte +++ b/src/routes/game/+page.svelte @@ -71,7 +71,7 @@ const checkData: CheckData = { gameId: gameData.gameId, - index: currentIndex, + stopName: gameData.stopNames[currentIndex], latlng: [playerMarker.getLatLng().lat, playerMarker.getLatLng().lng], }; diff --git a/src/routes/results/+page.server.ts b/src/routes/results/+page.server.ts index cabd1b0..61c10eb 100644 --- a/src/routes/results/+page.server.ts +++ b/src/routes/results/+page.server.ts @@ -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(); - return { gameData, gameId }; + return { rounds: results, gameId }; }; diff --git a/src/routes/results/+page.svelte b/src/routes/results/+page.svelte index 4c0566e..fd84395 100644 --- a/src/routes/results/+page.svelte +++ b/src/routes/results/+page.svelte @@ -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;
@@ -17,15 +17,15 @@

score total: {totalScore}

- mode: {gameData.mode} + mode: {mode} - {#each gameData.stops as stop, i} + {#each props.data.rounds as round} - - - + + + {/each} diff --git a/wrangler.toml b/wrangler.toml index cefc2c9..c8886b5 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -2,10 +2,6 @@ name = "tcl-guessr" compatibility_date = "2024-09-25" pages_build_output_dir = ".svelte-kit/cloudflare" -[[kv_namespaces]] -binding = "TCL_GUESSR_KV" -id = "b2d8980ac3a74a80854c35bf9569dbf8" - [[d1_databases]] binding = "TCL_GUESSR_D1" database_name = "tcl-guessr"
{stop.properties!.nom}{gameData.scores[i]} points{gameData.distances[i]} metres{round.stop_name}{round.points} points{round.distance} metres