Private
Public Access
1
0
Files
star-wars-wild-space/server/routes/game.js
2026-03-30 11:28:47 +02:00

200 lines
6.3 KiB
JavaScript

import express from "express";
import { getConfig } from "../configLoader.js";
import { computeWorldSeedState } from "../worldSeed.js";
import {
ensureSeedEpoch,
getGridCells,
insertCell,
getExistingCell,
getTeamCooldown,
upsertTeamCooldown,
getScores,
getEconScores,
addEconScore,
} from "../db/gameDb.js";
import { computeCell, rowToCellPayload } from "../helpers/cell.js";
const router = express.Router();
// GET /api/config
router.get("/config", async (req, res) => {
try {
const worldSeed = await ensureSeedEpoch();
const cfg = getConfig();
const rot = cfg.databaseWipeoutIntervalSeconds;
const ws = computeWorldSeedState(rot);
let teamCooldownRemaining = 0;
const team = typeof req.query.team === "string" ? req.query.team : undefined;
if (team === "blue" || team === "red") {
const row = await getTeamCooldown(worldSeed, team);
if (row) {
const secondsSince = (Date.now() - new Date(row.last_reveal).getTime()) / 1000;
if (secondsSince < cfg.clickCooldownSeconds) {
teamCooldownRemaining = Math.ceil(cfg.clickCooldownSeconds - secondsSince);
}
}
}
res.json({
clickCooldownSeconds: cfg.clickCooldownSeconds,
databaseWipeoutIntervalSeconds: rot,
debugModeForTeams: cfg.debugModeForTeams,
configReloadIntervalSeconds: cfg.configReloadIntervalSeconds,
worldSeed: ws.worldSeed,
seedPeriodEndsAtUtc: ws.seedPeriodEndsAtUtc,
seedPeriodStartsAtUtc: ws.seedPeriodStartsAtUtc,
teamCooldownRemaining,
resourceWorth: cfg.resourceWorth ?? { common: {}, rare: {} },
});
} catch (e) {
console.error(e);
res.status(500).json({ error: "config_error" });
}
});
// GET /api/grid/:seed
router.get("/grid/:seed", async (req, res) => {
const seed = decodeURIComponent(req.params.seed || "");
try {
const worldSeed = await ensureSeedEpoch();
if (seed !== worldSeed) {
return res.status(410).json({
error: "seed_expired",
worldSeed,
seedPeriodEndsAtUtc: computeWorldSeedState(
getConfig().databaseWipeoutIntervalSeconds
).seedPeriodEndsAtUtc,
});
}
const rows = await getGridCells(seed);
res.json({ seed, cells: rows });
} catch (e) {
console.error(e);
res.status(500).json({ error: "database_error" });
}
});
// POST /api/cell/reveal
router.post("/cell/reveal", async (req, res) => {
const seed = String(req.body?.seed ?? "");
const team = String(req.body?.team ?? "");
const x = Number(req.body?.x);
const y = Number(req.body?.y);
if (!seed || !Number.isInteger(x) || !Number.isInteger(y) || x < 0 || x >= 100 || y < 0 || y >= 100) {
return res.status(400).json({ error: "invalid_body" });
}
if (team !== "blue" && team !== "red") {
return res.status(400).json({ error: "invalid_team" });
}
try {
const worldSeed = await ensureSeedEpoch();
if (seed !== worldSeed) {
return res.status(410).json({ error: "seed_expired", worldSeed });
}
const cfg = getConfig();
if (cfg.clickCooldownSeconds > 0) {
const cooldownRow = await getTeamCooldown(worldSeed, team);
if (cooldownRow) {
const secondsSince = (Date.now() - new Date(cooldownRow.last_reveal).getTime()) / 1000;
if (secondsSince < cfg.clickCooldownSeconds) {
return res.status(429).json({
error: "cooldown_active",
team,
remainingSeconds: Math.ceil(cfg.clickCooldownSeconds - secondsSince),
cooldownSeconds: cfg.clickCooldownSeconds,
});
}
}
}
const cell = computeCell(seed, x, y);
const planetJson = cell.planet ? JSON.stringify(cell.planet) : null;
const inserted = await insertCell(seed, x, y, cell.exploitable, cell.hasPlanet, planetJson, team);
if (inserted) {
await upsertTeamCooldown(worldSeed, team);
return res.json(rowToCellPayload(inserted));
}
const existing = await getExistingCell(seed, x, y);
if (!existing) return res.status(500).json({ error: "insert_race" });
if (existing.discovered_by !== team) {
return res.status(409).json({
error: "taken_by_other_team",
discoveredBy: existing.discovered_by,
cell: rowToCellPayload(existing),
});
}
return res.json(rowToCellPayload(existing));
} catch (e) {
console.error(e);
res.status(500).json({ error: "database_error" });
}
});
// POST /api/admin/verify
router.post("/admin/verify", (req, res) => {
const password = String(req.body?.password ?? "");
const adminPwd = process.env.ADMIN_PASSWORD;
if (!adminPwd) {
return res.status(503).json({ ok: false, error: "not_configured" });
}
if (password && password === adminPwd) {
return res.json({ ok: true });
}
return res.status(401).json({ ok: false, error: "invalid_password" });
});
// GET /api/econ-scores
router.get("/econ-scores", async (_req, res) => {
try {
const worldSeed = await ensureSeedEpoch();
const scores = await getEconScores(worldSeed);
res.json(scores);
} catch (e) {
console.error(e);
res.status(500).json({ error: "database_error" });
}
});
// POST /api/econ-scores/tick body: { seed, blue, red }
router.post("/econ-scores/tick", async (req, res) => {
const seed = String(req.body?.seed ?? "");
const blue = Number(req.body?.blue ?? 0);
const red = Number(req.body?.red ?? 0);
try {
const worldSeed = await ensureSeedEpoch();
if (seed !== worldSeed) return res.status(410).json({ error: "seed_expired", worldSeed });
await addEconScore(worldSeed, "blue", blue);
await addEconScore(worldSeed, "red", red);
const scores = await getEconScores(worldSeed);
res.json(scores);
} catch (e) {
console.error(e);
res.status(500).json({ error: "database_error" });
}
});
// GET /api/scores
router.get("/scores", async (_req, res) => {
try {
const worldSeed = await ensureSeedEpoch();
const rows = await getScores(worldSeed);
const scores = { blue: 0, red: 0 };
for (const row of rows) {
if (row.discovered_by === "blue") scores.blue = Number(row.cnt);
if (row.discovered_by === "red") scores.red = Number(row.cnt);
}
res.json(scores);
} catch (e) {
console.error(e);
res.status(500).json({ error: "database_error" });
}
});
export default router;