Scaffold Regress: Fastify+SQLite server and Vue frontend for the Ingress-style BTC Map capture game
- Server: NIP-98 nostr auth (session cookies), BTC Map sync, claim/link/field routes, physical-presence checks via geolocation distance. 31 tests passing. - Frontend: Leaflet map, team pick, claim/link UI, Archipelago identity bridge vendored (nostr-provider.js) for dashboard launch support. - Verified end-to-end against the live BTC Map API (167 real Madeira places) and via genuine NIP-98-signed HTTP requests against the built app.
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
import type { LatLon } from './geo.js';
|
||||
import { triangleAreaKm2 } from './geo.js';
|
||||
|
||||
export interface LinkEdge {
|
||||
fromPlaceId: number;
|
||||
toPlaceId: number;
|
||||
team: string;
|
||||
}
|
||||
|
||||
export interface Field {
|
||||
team: string;
|
||||
placeIds: [number, number, number];
|
||||
areaKm2: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every set of three mutually-linked, same-team places forms a control field —
|
||||
* mirrors Ingress's triangle-fill rule. Recomputed from scratch on read rather
|
||||
* than persisted, since claims/links can invalidate fields from several call
|
||||
* sites and a derived view can't go stale.
|
||||
*/
|
||||
export function computeFields(links: LinkEdge[], placeCoords: Map<number, LatLon>): Field[] {
|
||||
const adjacencyByTeam = new Map<string, Map<number, Set<number>>>();
|
||||
for (const link of links) {
|
||||
if (!adjacencyByTeam.has(link.team)) adjacencyByTeam.set(link.team, new Map());
|
||||
const adj = adjacencyByTeam.get(link.team)!;
|
||||
if (!adj.has(link.fromPlaceId)) adj.set(link.fromPlaceId, new Set());
|
||||
if (!adj.has(link.toPlaceId)) adj.set(link.toPlaceId, new Set());
|
||||
adj.get(link.fromPlaceId)!.add(link.toPlaceId);
|
||||
adj.get(link.toPlaceId)!.add(link.fromPlaceId);
|
||||
}
|
||||
|
||||
const fields: Field[] = [];
|
||||
for (const [team, adj] of adjacencyByTeam) {
|
||||
const nodes = [...adj.keys()].sort((x, y) => x - y);
|
||||
for (const u of nodes) {
|
||||
const uNeighbors = [...adj.get(u)!].filter((v) => v > u);
|
||||
for (const v of uNeighbors) {
|
||||
const vNeighbors = adj.get(v)!;
|
||||
for (const w of uNeighbors) {
|
||||
if (w <= v || !vNeighbors.has(w)) continue;
|
||||
const a = placeCoords.get(u);
|
||||
const b = placeCoords.get(v);
|
||||
const c = placeCoords.get(w);
|
||||
if (!a || !b || !c) continue;
|
||||
fields.push({ team, placeIds: [u, v, w], areaKm2: triangleAreaKm2(a, b, c) });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
export interface TeamScore {
|
||||
team: string;
|
||||
claimedPlaces: number;
|
||||
links: number;
|
||||
fields: number;
|
||||
areaKm2: number;
|
||||
}
|
||||
|
||||
export function summarizeScores(
|
||||
teams: string[],
|
||||
claimCounts: Map<string, number>,
|
||||
linkCounts: Map<string, number>,
|
||||
fields: Field[],
|
||||
): TeamScore[] {
|
||||
return teams.map((team) => {
|
||||
const teamFields = fields.filter((f) => f.team === team);
|
||||
return {
|
||||
team,
|
||||
claimedPlaces: claimCounts.get(team) ?? 0,
|
||||
links: linkCounts.get(team) ?? 0,
|
||||
fields: teamFields.length,
|
||||
areaKm2: teamFields.reduce((sum, f) => sum + f.areaKm2, 0),
|
||||
};
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user