feat: add game system
All checks were successful
deploy to cloudflare pages / deploy (push) Successful in 37s
All checks were successful
deploy to cloudflare pages / deploy (push) Successful in 37s
This commit is contained in:
parent
524316d864
commit
14e38cb573
7 changed files with 175 additions and 38 deletions
|
@ -1,5 +1,5 @@
|
|||
import { error } from "@sveltejs/kit";
|
||||
import type { GameData, GameOptions } from "./types";
|
||||
import type { CheckResponse, ClientGameData, GameOptions, ServerGameData } from "./types";
|
||||
|
||||
type FetchType = typeof fetch;
|
||||
|
||||
|
@ -15,28 +15,57 @@ let lazyTram: [GeoJSON.Feature, string][] | null = null;
|
|||
let lazyStops: GeoJSON.Feature[] | null = null;
|
||||
|
||||
export async function createGame(
|
||||
stop: GeoJSON.Feature,
|
||||
mode: string,
|
||||
stops: GeoJSON.Feature[],
|
||||
kv: KVNamespace<string>,
|
||||
): Promise<GameData> {
|
||||
): Promise<ClientGameData> {
|
||||
const uuid = crypto.randomUUID();
|
||||
const stopNames = stops.map((s) => s.properties!.nom);
|
||||
|
||||
await kv.put(`game:${uuid}`, JSON.stringify(stop), { expirationTtl: 600 });
|
||||
const serverData: ServerGameData = {
|
||||
mode,
|
||||
stops,
|
||||
distances: new Array(stops.length).fill(-1),
|
||||
scores: new Array(stops.length).fill(-1),
|
||||
};
|
||||
|
||||
await kv.put(`game:${uuid}`, JSON.stringify(serverData), { expirationTtl: 600 });
|
||||
|
||||
return {
|
||||
gameId: uuid,
|
||||
stopName: stop.properties!.nom,
|
||||
stopNames,
|
||||
};
|
||||
}
|
||||
|
||||
export async function stopGame(
|
||||
export async function saveScore(
|
||||
uuid: string,
|
||||
index: number,
|
||||
distanceCalc: (latlng: [number, number]) => number,
|
||||
scoreCalc: (dist: number) => number,
|
||||
kv: KVNamespace<string>,
|
||||
): Promise<GeoJSON.Feature | null> {
|
||||
const stop: GeoJSON.Feature | null = await kv.get(`game:${uuid}`, "json");
|
||||
): Promise<CheckResponse | null> {
|
||||
const serverData: ServerGameData | null = await kv.get(`game:${uuid}`, "json");
|
||||
if (!serverData) return null;
|
||||
|
||||
if (stop) await kv.delete(`game:${uuid}`);
|
||||
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]];
|
||||
|
||||
return stop;
|
||||
const distance = Math.round(distanceCalc(latlng));
|
||||
const score = Math.round(scoreCalc(distance));
|
||||
|
||||
serverData.distances[index] = distance;
|
||||
serverData.scores[index] = score;
|
||||
await kv.put(`game:${uuid}`, JSON.stringify(serverData), { expirationTtl: 600 });
|
||||
|
||||
return {
|
||||
distance,
|
||||
score,
|
||||
solution: latlng,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function getMetro(fetch: FetchType): Promise<[GeoJSON.Feature, string][]> {
|
||||
|
@ -127,3 +156,15 @@ export function parseOptions(url: URL): GameOptions {
|
|||
tram: url.searchParams.has("tram"),
|
||||
};
|
||||
}
|
||||
|
||||
// https://underscorejs.org/docs/modules/sample.html
|
||||
export function sample<T>(arr: T[], n: number): T[] {
|
||||
const last = arr.length - 1;
|
||||
for (let index = 0; index < n; index++) {
|
||||
const rand = index + Math.floor(Math.random() * (last - index + 1));
|
||||
const temp = arr[index];
|
||||
arr[index] = arr[rand];
|
||||
arr[rand] = temp;
|
||||
}
|
||||
return arr.slice(0, n);
|
||||
}
|
||||
|
|
|
@ -9,13 +9,21 @@ export interface MapData {
|
|||
stops: [number, number][];
|
||||
}
|
||||
|
||||
export interface GameData {
|
||||
stopName: string;
|
||||
export interface ServerGameData {
|
||||
mode: string;
|
||||
stops: GeoJSON.Feature[];
|
||||
distances: number[];
|
||||
scores: number[];
|
||||
}
|
||||
|
||||
export interface ClientGameData {
|
||||
stopNames: string[];
|
||||
gameId: string;
|
||||
}
|
||||
|
||||
export interface CheckData {
|
||||
gameId: string;
|
||||
index: number;
|
||||
latlng: [number, number];
|
||||
}
|
||||
|
||||
|
|
|
@ -1,25 +1,24 @@
|
|||
import { error } from "@sveltejs/kit";
|
||||
import { stopGame } from "$lib";
|
||||
import { saveScore } from "$lib";
|
||||
import type { RequestHandler } from "./$types";
|
||||
import type { CheckData, CheckResponse } from "$lib/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 data: CheckData = await request.json();
|
||||
const stop = await stopGame(data.gameId, kv);
|
||||
|
||||
if (stop) {
|
||||
// GeoJSON data is LonLat, not LatLon
|
||||
const coords = stop.geometry.type === "Point" ? stop.geometry.coordinates : [0, 0];
|
||||
const latlng: [number, number] = [coords[1], coords[0]];
|
||||
const solution = await saveScore(
|
||||
data.gameId,
|
||||
data.index,
|
||||
(solution) => calcDistance(data.latlng, solution),
|
||||
(distance) => calculatePoints(distance),
|
||||
kv,
|
||||
);
|
||||
|
||||
const distance = calcDistance(data.latlng, latlng);
|
||||
const score = calculatePoints(distance);
|
||||
|
||||
const res: CheckResponse = { solution: latlng, distance, score };
|
||||
return Response.json(res);
|
||||
if (solution) {
|
||||
return Response.json(solution);
|
||||
} else {
|
||||
return new Response(null, { status: 404 });
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { error } from "@sveltejs/kit";
|
||||
import type { RequestHandler } from "./$types";
|
||||
import { createGame, getMergedStops, getMetro, getTram, parseOptions } from "$lib";
|
||||
import { createGame, getMergedStops, getMetro, getTram, parseOptions, sample } from "$lib";
|
||||
|
||||
export const GET: RequestHandler = async ({ fetch, url, platform }) => {
|
||||
const kv = platform?.env?.TCL_GUESSR_KV;
|
||||
|
@ -15,13 +15,12 @@ export const GET: RequestHandler = async ({ fetch, url, platform }) => {
|
|||
const lineCodes = new Set<string>(lineColors.map(([f]) => f.properties!.code_ligne));
|
||||
const crossingStops = await getMergedStops(fetch, lineCodes);
|
||||
|
||||
const randomStop = crossingStops[Math.floor(Math.random() * crossingStops.length)];
|
||||
|
||||
if (!randomStop) {
|
||||
const randomStops = sample(crossingStops, 5);
|
||||
if (!randomStops) {
|
||||
error(400, "could not select random stop");
|
||||
}
|
||||
|
||||
const gameData = await createGame(randomStop, kv);
|
||||
const gameData = await createGame(options.mode, randomStops, kv);
|
||||
|
||||
return Response.json(gameData);
|
||||
};
|
||||
|
|
|
@ -1,19 +1,22 @@
|
|||
<script lang="ts">
|
||||
import type { CheckData, CheckResponse, GameData, MapData } from "$lib/types";
|
||||
import type { CheckData, CheckResponse, ClientGameData, MapData } from "$lib/types";
|
||||
import { page } from "$app/stores";
|
||||
import L from "leaflet";
|
||||
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css";
|
||||
import "leaflet-defaulticon-compatibility";
|
||||
import { goto } from "$app/navigation";
|
||||
|
||||
const zoom = 13;
|
||||
const center = L.latLng(45.742858495, 4.86163814);
|
||||
const center: [number, number] = [45.742858495, 4.86163814];
|
||||
|
||||
let mapPromise = $state(fetchMap());
|
||||
let gamePromise = $state(fetchGame());
|
||||
let results: CheckResponse | null = $state(null);
|
||||
|
||||
let currentIndex = $state(0);
|
||||
|
||||
let map: L.Map | null = $state(null);
|
||||
let playerMarker: L.Marker | null = $state(null);
|
||||
let solutionMarker: L.Marker | null = $state(null);
|
||||
|
@ -23,7 +26,7 @@
|
|||
return fetch("/api/map?" + $page.url.searchParams).then((r) => r.json());
|
||||
}
|
||||
|
||||
async function fetchGame(): Promise<GameData> {
|
||||
async function fetchGame(): Promise<ClientGameData> {
|
||||
return fetch("/api/game?" + $page.url.searchParams).then((r) => r.json());
|
||||
}
|
||||
|
||||
|
@ -70,6 +73,7 @@
|
|||
|
||||
const checkData: CheckData = {
|
||||
gameId: gameData.gameId,
|
||||
index: currentIndex,
|
||||
latlng: [playerMarker.getLatLng().lat, playerMarker.getLatLng().lng],
|
||||
};
|
||||
|
||||
|
@ -89,14 +93,18 @@
|
|||
|
||||
map.flyToBounds(solutionLine.getBounds(), { duration: 1 });
|
||||
|
||||
solutionMarker = L.marker(res.solution).bindPopup(gameData.stopName).addTo(map).openPopup();
|
||||
solutionMarker = L.marker(res.solution)
|
||||
.bindPopup(gameData.stopNames[currentIndex])
|
||||
.addTo(map)
|
||||
.openPopup();
|
||||
} else {
|
||||
alert("it seems than an error occurred");
|
||||
}
|
||||
}
|
||||
|
||||
function restartGame() {
|
||||
async function restartGame() {
|
||||
if (!map) return;
|
||||
const gameData = await gamePromise;
|
||||
|
||||
playerMarker?.removeFrom(map);
|
||||
playerMarker = null;
|
||||
|
@ -110,7 +118,11 @@
|
|||
map.setView(center, zoom);
|
||||
results = null;
|
||||
|
||||
gamePromise = fetchGame();
|
||||
if (gameData.stopNames.length <= currentIndex + 1) {
|
||||
goto("/results?" + new URLSearchParams({ gameId: gameData.gameId }));
|
||||
} else {
|
||||
currentIndex++;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -120,19 +132,19 @@
|
|||
|
||||
<div><button disabled>Loading...</button></div>
|
||||
{:then [, gameData]}
|
||||
<h1>{gameData.stopName}</h1>
|
||||
<h1>{gameData.stopNames[currentIndex]}</h1>
|
||||
|
||||
<div>
|
||||
{#if results === null}
|
||||
<button onclick={checkLocation} disabled={!playerMarker}>SUBMIT</button>
|
||||
{:else}
|
||||
<button onclick={restartGame}>RESTART</button>
|
||||
<button onclick={restartGame}>NEXT</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/await}
|
||||
|
||||
<span class="results" hidden={!results}>
|
||||
{Math.floor(results?.score ?? 0)} points! Vous etiez à {Math.floor(results?.distance ?? 0)}m.
|
||||
{results?.score} points! Vous etiez à {results?.distance}m.
|
||||
</span>
|
||||
|
||||
<div id="map"></div>
|
||||
|
|
16
src/routes/results/+page.server.ts
Normal file
16
src/routes/results/+page.server.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import type { PageServerLoad } from "./$types";
|
||||
import { error } from "@sveltejs/kit";
|
||||
import type { ServerGameData } from "$lib/types";
|
||||
|
||||
export const load: PageServerLoad = async ({ url, platform }) => {
|
||||
const kv = platform?.env?.TCL_GUESSR_KV;
|
||||
if (!kv) error(500, "could not connect to kv");
|
||||
|
||||
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");
|
||||
|
||||
return { gameData, gameId };
|
||||
};
|
62
src/routes/results/+page.svelte
Normal file
62
src/routes/results/+page.svelte
Normal file
|
@ -0,0 +1,62 @@
|
|||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import type { PageData } from "./$types";
|
||||
|
||||
interface Props {
|
||||
data: PageData;
|
||||
}
|
||||
|
||||
const props: Props = $props();
|
||||
const gameData = props.data.gameData;
|
||||
|
||||
const totalScore = gameData.scores.reduce((a, b) => a + b);
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<h1>yippee !!!</h1>
|
||||
|
||||
<h2>score total: {totalScore}</h2>
|
||||
|
||||
<span>mode: {gameData.mode}</span>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
{#each gameData.stops as stop, i}
|
||||
<tr>
|
||||
<td>{stop.properties!.nom}</td>
|
||||
<td>{gameData.scores[i]} points</td>
|
||||
<td>{gameData.distances[i]} metres</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<span class="small">id de partie: {props.data.gameId}</span>
|
||||
|
||||
<button class="restart" onclick={() => goto("/")}>RELANCER !!!</button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
td {
|
||||
border: 1px solid black;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.small {
|
||||
padding: 20px;
|
||||
font-size: 12px;
|
||||
color: darkgray;
|
||||
}
|
||||
|
||||
.restart {
|
||||
width: 600px;
|
||||
height: 75px;
|
||||
}
|
||||
</style>
|
Loading…
Add table
Add a link
Reference in a new issue