feat: add difficulty selector
All checks were successful
deploy to cloudflare pages / deploy (push) Successful in 33s

This commit is contained in:
uku 2024-10-25 15:00:50 +02:00
parent 2255355f21
commit c3dec90264
Signed by: uku
SSH key fingerprint: SHA256:4P0aN6M8ajKukNi6aPOaX0LacanGYtlfjmN+m/sHY/o
7 changed files with 175 additions and 139 deletions

View file

@ -1,13 +1,16 @@
export interface GameOptions {
mode: "easy" | "hard" | "extreme demon ultra miguel";
}
export interface GameData { export interface GameData {
center: [number, number];
lines: [GeoJSON.Feature, string][]; lines: [GeoJSON.Feature, string][];
stops: GeoJSON.Feature[]; stops: GeoJSON.Feature[];
stopName: string; stopName: string;
stopId: string; gameId: string;
} }
export interface CheckData { export interface CheckData {
stopId: string; gameId: string;
latlng: [number, number]; latlng: [number, number];
} }
@ -27,6 +30,20 @@ const stopsUrl =
let lazyLines: GeoJSON.FeatureCollection | null = null; let lazyLines: GeoJSON.FeatureCollection | null = null;
let lazyStops: GeoJSON.Feature[] | null = null; let lazyStops: GeoJSON.Feature[] | null = null;
const games: Record<string, GeoJSON.Feature> = {};
export function createGame(stop: GeoJSON.Feature): string {
const uuid = crypto.randomUUID();
games[uuid] = stop;
return uuid;
}
export function stopGame(uuid: string): GeoJSON.Feature | null {
const stop = games[uuid];
delete games[uuid];
return stop;
}
export async function getLines(fetch: FetchType): Promise<GeoJSON.FeatureCollection> { export async function getLines(fetch: FetchType): Promise<GeoJSON.FeatureCollection> {
return lazyLines ?? (lazyLines = await fetch(linesUrl).then((r) => r.json())); return lazyLines ?? (lazyLines = await fetch(linesUrl).then((r) => r.json()));
} }

View file

@ -1,127 +1,37 @@
<script lang="ts"> <script lang="ts">
import type { PageData } from "./$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 type { CheckData, CheckResponse } from "$lib";
interface Props {
data: PageData;
}
let { data }: Props = $props();
const hidden = !$page.url.searchParams.has("debug");
const center = L.latLng(data.gameData.center);
const linesJson = data.gameData.lines.map(([feature, color]) => {
return L.geoJSON(feature, { style: { color } });
});
const pointsJson = data.gameData.stops.map((f) => L.geoJSON(f));
let latlng = $state(L.latLng(0, 0));
let results: CheckResponse | null = $state(null);
let lines = $state(true);
let labels = $state(false);
let map: L.Map | null = $state(null);
let playerMarker: L.Marker | null = $state(null);
let tileLayer = $derived(
L.tileLayer(
`https://basemaps.cartocdn.com/light_${labels ? "all" : "nolabels"}/{z}/{x}/{y}{r}.png`,
{
attribution:
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>',
maxZoom: 20,
},
),
);
$effect(() => {
if (!map) return;
tileLayer.addTo(map);
if (lines) {
linesJson.forEach((line) => line.addTo(map!));
pointsJson.forEach((p) => p.addTo(map!));
} else {
linesJson.forEach((line) => line.removeFrom(map!));
pointsJson.forEach((p) => p.removeFrom(map!));
}
});
function createMap(node: HTMLElement) {
map = L.map(node).setView(center, 13);
playerMarker = L.marker([0, 0]).addTo(map);
map.on("click", (e) => {
if (map && playerMarker && !results) {
playerMarker.setLatLng(e.latlng);
latlng = e.latlng;
}
});
}
async function checkLocation() {
const checkData: CheckData = {
stopId: data.gameData.stopId,
latlng: [latlng.lat, latlng.lng],
};
const response = await fetch("/api/check", {
method: "POST",
body: JSON.stringify(checkData),
});
if (response.ok && map) {
const res: CheckResponse = await response.json();
L.marker(res.solution).bindPopup(data.gameData.stopName).addTo(map).openPopup();
results = res;
}
}
</script> </script>
<div class="container"> <div class="container">
<h1>{data.gameData.stopName}</h1> <h1>TCL-Guessr</h1>
<div hidden={results !== null}> <form action="/game" method="GET">
<button onclick={checkLocation} disabled={latlng.lat === 0 || latlng.lng === 0}>SUBMIT</button> <label>
</div> difficulté: <select name="mode">
<option value="easy">Facile (pour les nuls)</option>
<option value="hard">Dur (pour les gigaillards)</option>
<option value="extreme demon ultra miguel">
EXTREME DEMON ULTRA MIGUEL DE LA MORT QUI TUE
</option>
</select>
</label>
<div class="results" hidden={!results}> <input type="submit" value="LANCER LA PARTIE" />
{Math.floor(results?.score ?? 0)} points! Vous etiez à {Math.floor(results?.distance ?? 0)}m. </form>
</div>
<div {hidden}>
<label>lines: <input type="checkbox" bind:checked={lines} /></label>
<label>labels: <input type="checkbox" bind:checked={labels} /></label>
</div>
<div id="map" use:createMap></div>
</div> </div>
<style> <style>
.container { .container {
padding: 16px;
}
form {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 10px;
height: 100%; width: fit-content;
text-align: center;
} }
.results { input[type="submit"] {
color: green; height: 50px;
font-size: 20px;
}
#map {
flex-grow: 1;
width: 100%;
} }
</style> </style>

View file

@ -1,11 +0,0 @@
import type { PageLoad } from "./$types";
import type { GameData } from "$lib";
export const load: PageLoad = async ({ fetch }) => {
const res = await fetch("/api/data");
const gameData: GameData = await res.json();
return { gameData };
};
export const ssr = false;

View file

@ -1,11 +1,9 @@
import { getStops, type CheckData, type CheckResponse } from "$lib"; import { stopGame, type CheckData, type CheckResponse } from "$lib";
import type { RequestHandler } from "./$types"; import type { RequestHandler } from "./$types";
export const POST: RequestHandler = async ({ fetch, request }) => { export const POST: RequestHandler = async ({ request }) => {
const stops = await getStops(fetch);
const data: CheckData = await request.json(); const data: CheckData = await request.json();
const stop = stops.find((f) => f.id === data.stopId); const stop = stopGame(data.gameId);
if (stop) { if (stop) {
// GeoJSON data is LonLat, not LatLon // GeoJSON data is LonLat, not LatLon

View file

@ -1,13 +1,17 @@
import type { RequestHandler } from "./$types"; import type { RequestHandler } from "./$types";
import { getLines, getStops, type GameData } from "$lib"; import { createGame, getLines, getStops, type GameData, type GameOptions } from "$lib";
import { error } from "@sveltejs/kit";
export const GET: RequestHandler = async ({ fetch }) => { export const GET: RequestHandler = async ({ fetch, url }) => {
const lines = await getLines(fetch); const lines = await getLines(fetch);
const stops = await getStops(fetch); const stops = await getStops(fetch);
const bbox = lines.bbox ?? [0, 0, 0, 0]; const mode = url.searchParams.get("mode");
const centerLat = (bbox[1] + bbox[3]) / 2; if (mode !== "easy" && mode !== "hard" && mode !== "extreme demon ultra miguel") {
const centerLon = (bbox[0] + bbox[2]) / 2; return error(400, "gamemode is invalid");
}
const options: GameOptions = { mode };
const lineColors: [GeoJSON.Feature, string][] = lines.features.map((f) => { const lineColors: [GeoJSON.Feature, string][] = lines.features.map((f) => {
const components = f.properties!.couleur!.split(" "); const components = f.properties!.couleur!.split(" ");
@ -28,13 +32,13 @@ export const GET: RequestHandler = async ({ fetch }) => {
}); });
const randomStop = crossingStops[Math.floor(Math.random() * crossingStops.length)]; const randomStop = crossingStops[Math.floor(Math.random() * crossingStops.length)];
const gameId = createGame(randomStop);
const data: GameData = { const data: GameData = {
center: [centerLat, centerLon], lines: options.mode === "easy" || options.mode === "hard" ? lineColors : [],
lines: lineColors, stops: options.mode === "easy" ? crossingStops : [],
stops: crossingStops,
stopName: randomStop.properties!.nom, stopName: randomStop.properties!.nom,
stopId: randomStop.id!.toString(), gameId,
}; };
return Response.json(data); return Response.json(data);

View file

@ -0,0 +1,102 @@
<script lang="ts">
import type { CheckData, CheckResponse } from "$lib";
import type { PageData } from "./$types";
import L from "leaflet";
import "leaflet/dist/leaflet.css";
import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css";
import "leaflet-defaulticon-compatibility";
interface Props {
data: PageData;
}
let { data }: Props = $props();
const center = L.latLng(45.742858495, 4.86163814);
let latlng = $state(L.latLng(0, 0));
let results: CheckResponse | null = $state(null);
let map: L.Map | null = $state(null);
function createMap(node: HTMLElement) {
map = L.map(node).setView(center, 13);
L.tileLayer(`https://basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png`, {
attribution:
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>',
maxZoom: 20,
}).addTo(map);
// we know map isn't null
data.gameData.lines.forEach(([feature, color]) => {
L.geoJSON(feature, { style: { color } }).addTo(map!);
});
data.gameData.stops.forEach((f) => L.geoJSON(f).addTo(map!));
const playerMarker = L.marker([0, 0]).addTo(map);
map.on("click", (e) => {
if (map && playerMarker && !results) {
playerMarker.setLatLng(e.latlng);
latlng = e.latlng;
}
});
}
async function checkLocation() {
const checkData: CheckData = {
gameId: data.gameData.gameId,
latlng: [latlng.lat, latlng.lng],
};
const response = await fetch("/api/check", {
method: "POST",
body: JSON.stringify(checkData),
});
if (response.ok && map) {
const res: CheckResponse = await response.json();
L.marker(res.solution).bindPopup(data.gameData.stopName).addTo(map).openPopup();
results = res;
} else {
alert("you dirty little cheater");
}
}
</script>
<div class="container">
<h1>{data.gameData.stopName}</h1>
<div hidden={results !== null}>
<button onclick={checkLocation} disabled={latlng.lat === 0 || latlng.lng === 0}>SUBMIT</button>
</div>
<div class="results" hidden={!results}>
{Math.floor(results?.score ?? 0)} points! Vous etiez à {Math.floor(results?.distance ?? 0)}m.
</div>
<div id="map" use:createMap></div>
</div>
<style>
.container {
display: flex;
flex-direction: column;
gap: 8px;
height: 100%;
text-align: center;
}
.results {
color: green;
font-size: 20px;
}
#map {
flex-grow: 1;
width: 100%;
}
</style>

16
src/routes/game/+page.ts Normal file
View file

@ -0,0 +1,16 @@
import type { PageLoad } from "./$types";
import type { GameData } from "$lib";
import { error } from "@sveltejs/kit";
export const load: PageLoad = async ({ fetch, url }) => {
const res = await fetch("/api/data?" + url.searchParams);
if (!res.ok) {
return error(400, await res.text());
} else {
const gameData: GameData = await res.json();
return { gameData };
}
};
export const ssr = false;