Private
Public Access
1
0

feat: Adding economic system to do scoring

This commit is contained in:
gauvainboiche
2026-03-30 11:28:47 +02:00
parent b19fb262a4
commit c0f66d8cc0
16 changed files with 1303 additions and 145 deletions

View File

@@ -7,12 +7,13 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
const CONFIG_FILE_PATH =
process.env.CONFIG_FILE_PATH ?? path.join(__dirname, "..", "config", "game.settings.json");
/** @type {{ clickCooldownSeconds: number, databaseWipeoutIntervalSeconds: number, debugModeForTeams: boolean, configReloadIntervalSeconds: number }} */
/** @type {{ clickCooldownSeconds: number, databaseWipeoutIntervalSeconds: number, debugModeForTeams: boolean, configReloadIntervalSeconds: number, resourceWorth: object }} */
let cached = {
clickCooldownSeconds: 5,
databaseWipeoutIntervalSeconds: 21600,
debugModeForTeams: true,
configReloadIntervalSeconds: 30,
resourceWorth: { common: {}, rare: {} },
};
let lastMtimeMs = 0;
@@ -46,6 +47,9 @@ export function loadConfigFile() {
if (typeof j.configReloadIntervalSeconds === "number" && j.configReloadIntervalSeconds >= 5) {
cached.configReloadIntervalSeconds = j.configReloadIntervalSeconds;
}
if (j.resourceWorth && typeof j.resourceWorth === "object") {
cached.resourceWorth = j.resourceWorth;
}
lastMtimeMs = st.mtimeMs;
} catch (e) {
if (e.code === "ENOENT") {

View File

@@ -29,6 +29,14 @@ export async function initGameSchema() {
PRIMARY KEY (world_seed, team)
);
`);
await pool.query(`
CREATE TABLE IF NOT EXISTS team_econ_scores (
world_seed TEXT NOT NULL,
team TEXT NOT NULL CHECK (team IN ('blue', 'red')),
score DOUBLE PRECISION NOT NULL DEFAULT 0,
PRIMARY KEY (world_seed, team)
);
`);
await pool.query(`
ALTER TABLE grid_cells ADD COLUMN IF NOT EXISTS discovered_by TEXT;
UPDATE grid_cells SET discovered_by = 'blue' WHERE discovered_by IS NULL;
@@ -59,6 +67,7 @@ export async function ensureSeedEpoch() {
if (seedSlot !== lastSeedSlot) {
await pool.query("TRUNCATE grid_cells RESTART IDENTITY");
await pool.query("DELETE FROM team_cooldowns WHERE world_seed != $1", [worldSeed]);
await pool.query("DELETE FROM team_econ_scores WHERE world_seed != $1", [worldSeed]);
console.log(`[world] Slot ${lastSeedSlot}${seedSlot}; grid wiped, old cooldowns cleared.`);
lastSeedSlot = seedSlot;
}
@@ -115,6 +124,29 @@ export async function upsertTeamCooldown(worldSeed, team) {
);
}
// ── Economic scores ───────────────────────────────────────────────────────────
export async function getEconScores(worldSeed) {
const { rows } = await pool.query(
`SELECT team, score FROM team_econ_scores WHERE world_seed = $1`,
[worldSeed]
);
const result = { blue: 0, red: 0 };
for (const row of rows) result[row.team] = Number(row.score);
return result;
}
export async function addEconScore(worldSeed, team, delta) {
if (delta <= 0) return;
await pool.query(
`INSERT INTO team_econ_scores (world_seed, team, score)
VALUES ($1, $2, $3)
ON CONFLICT (world_seed, team) DO UPDATE
SET score = team_econ_scores.score + EXCLUDED.score`,
[worldSeed, team, delta]
);
}
// ── Scores ────────────────────────────────────────────────────────────────────
export async function getScores(worldSeed) {

View File

@@ -1,3 +1,4 @@
import "dotenv/config";
import { loadConfigFile, getConfig } from "./configLoader.js";
import { initGameSchema, ensureSeedEpoch } from "./db/gameDb.js";
import { initUsersSchema } from "./db/usersDb.js";

View File

@@ -9,6 +9,8 @@ import {
getTeamCooldown,
upsertTeamCooldown,
getScores,
getEconScores,
addEconScore,
} from "../db/gameDb.js";
import { computeCell, rowToCellPayload } from "../helpers/cell.js";
@@ -43,6 +45,7 @@ router.get("/config", async (req, res) => {
seedPeriodEndsAtUtc: ws.seedPeriodEndsAtUtc,
seedPeriodStartsAtUtc: ws.seedPeriodStartsAtUtc,
teamCooldownRemaining,
resourceWorth: cfg.resourceWorth ?? { common: {}, rare: {} },
});
} catch (e) {
console.error(e);
@@ -134,6 +137,49 @@ router.post("/cell/reveal", async (req, res) => {
}
});
// 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 {