import { pool } from "./pools.js"; import { loadConfigFile, getConfig } from "../configLoader.js"; import { computeWorldSeedState } from "../worldSeed.js"; let lastSeedSlot = null; // ── Schema ──────────────────────────────────────────────────────────────────── export async function initGameSchema() { await pool.query(` CREATE TABLE IF NOT EXISTS grid_cells ( id SERIAL PRIMARY KEY, world_seed TEXT NOT NULL, x SMALLINT NOT NULL CHECK (x >= 0 AND x < 100), y SMALLINT NOT NULL CHECK (y >= 0 AND y < 100), exploitable BOOLEAN NOT NULL, has_planet BOOLEAN NOT NULL, planet_json JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (world_seed, x, y) ); CREATE INDEX IF NOT EXISTS idx_grid_cells_seed ON grid_cells (world_seed); `); await pool.query(` CREATE TABLE IF NOT EXISTS team_cooldowns ( world_seed TEXT NOT NULL, team TEXT NOT NULL CHECK (team IN ('blue', 'red')), last_reveal TIMESTAMPTZ, 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(` CREATE TABLE IF NOT EXISTS team_element_bonus ( world_seed TEXT NOT NULL, team TEXT NOT NULL CHECK (team IN ('blue', 'red')), bonus DOUBLE PRECISION NOT NULL DEFAULT 0, PRIMARY KEY (world_seed, team) ); `); await pool.query(` CREATE TABLE IF NOT EXISTS db_metadata ( id SERIAL PRIMARY KEY, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); INSERT INTO db_metadata (created_at) SELECT NOW() WHERE NOT EXISTS (SELECT 1 FROM db_metadata); `); await pool.query(` CREATE TABLE IF NOT EXISTS victory_points ( id SERIAL PRIMARY KEY, awarded_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), world_seed TEXT NOT NULL, team TEXT NOT NULL CHECK (team IN ('blue', 'red')) ); CREATE INDEX IF NOT EXISTS idx_vp_team ON victory_points (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; ALTER TABLE grid_cells ALTER COLUMN discovered_by SET DEFAULT 'blue'; DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_constraint WHERE conname = 'grid_cells_discovered_by_check' ) THEN ALTER TABLE grid_cells ADD CONSTRAINT grid_cells_discovered_by_check CHECK (discovered_by IN ('blue', 'red')); END IF; END $$; ALTER TABLE grid_cells ALTER COLUMN discovered_by SET NOT NULL; `); } // ── World-seed epoch ────────────────────────────────────────────────────────── export async function ensureSeedEpoch() { loadConfigFile(); const rot = getConfig().databaseWipeoutIntervalSeconds; const { seedSlot, worldSeed } = computeWorldSeedState(rot); if (lastSeedSlot === null) { lastSeedSlot = seedSlot; return worldSeed; } if (seedSlot !== lastSeedSlot) { // Award a victory point to the team with the highest econ score before wiping try { const expiredSeed = `swg-${lastSeedSlot}`; const econRows = await pool.query( `SELECT team, score FROM team_econ_scores WHERE world_seed = $1`, [expiredSeed] ); const scores = { blue: 0, red: 0 }; for (const row of econRows.rows) scores[row.team] = Number(row.score); if (scores.blue > 0 || scores.red > 0) { const winner = scores.blue >= scores.red ? "blue" : "red"; await pool.query( `INSERT INTO victory_points (world_seed, team) VALUES ($1, $2)`, [expiredSeed, winner] ); console.log(`[world] VP awarded to ${winner} for seed ${expiredSeed} (blue=${scores.blue.toFixed(3)}, red=${scores.red.toFixed(3)})`); } } catch (e) { console.error("[world] VP award error:", e); } 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]); await pool.query("DELETE FROM team_element_bonus WHERE world_seed != $1", [worldSeed]); console.log(`[world] Slot ${lastSeedSlot} → ${seedSlot}; grid wiped, old cooldowns cleared.`); lastSeedSlot = seedSlot; } return worldSeed; } // ── Grid cells ──────────────────────────────────────────────────────────────── export async function getGridCells(worldSeed) { const { rows } = await pool.query( `SELECT x, y, exploitable, has_planet, planet_json, discovered_by FROM grid_cells WHERE world_seed = $1`, [worldSeed] ); return rows; } export async function insertCell(seed, x, y, exploitable, hasPlanet, planetJson, team) { const { rows } = await pool.query( `INSERT INTO grid_cells (world_seed, x, y, exploitable, has_planet, planet_json, discovered_by) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (world_seed, x, y) DO NOTHING RETURNING x, y, exploitable, has_planet, planet_json, discovered_by`, [seed, x, y, exploitable, hasPlanet, planetJson, team] ); return rows[0] ?? null; } export async function getExistingCell(seed, x, y) { const { rows } = await pool.query( `SELECT x, y, exploitable, has_planet, planet_json, discovered_by FROM grid_cells WHERE world_seed = $1 AND x = $2 AND y = $3`, [seed, x, y] ); return rows[0] ?? null; } // ── Team cooldowns ──────────────────────────────────────────────────────────── export async function getTeamCooldown(worldSeed, team) { const { rows } = await pool.query( `SELECT last_reveal FROM team_cooldowns WHERE world_seed = $1 AND team = $2`, [worldSeed, team] ); return rows[0] ?? null; } export async function upsertTeamCooldown(worldSeed, team) { await pool.query( `INSERT INTO team_cooldowns (world_seed, team, last_reveal) VALUES ($1, $2, NOW()) ON CONFLICT (world_seed, team) DO UPDATE SET last_reveal = NOW()`, [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] ); } export async function getElementBonus(worldSeed) { const { rows } = await pool.query( `SELECT team, bonus FROM team_element_bonus WHERE world_seed = $1`, [worldSeed] ); const result = { blue: 0, red: 0 }; for (const row of rows) result[row.team] = Number(row.bonus); return result; } export async function setElementBonus(worldSeed, team, bonus) { await pool.query( `INSERT INTO team_element_bonus (world_seed, team, bonus) VALUES ($1, $2, $3) ON CONFLICT (world_seed, team) DO UPDATE SET bonus = EXCLUDED.bonus`, [worldSeed, team, bonus] ); } // ── DB metadata ─────────────────────────────────────────────────────────────── export async function getDbCreatedAt() { const { rows } = await pool.query( `SELECT created_at FROM db_metadata ORDER BY id ASC LIMIT 1` ); return rows[0]?.created_at ?? null; } // ── Victory points ──────────────────────────────────────────────────────────── export async function getVictoryPoints() { const { rows } = await pool.query( `SELECT team, COUNT(*) AS cnt FROM victory_points GROUP BY team` ); const result = { blue: 0, red: 0 }; for (const row of rows) result[row.team] = Number(row.cnt); return result; } // ── Scores ──────────────────────────────────────────────────────────────────── export async function getScores(worldSeed) { const { rows } = await pool.query( `SELECT discovered_by, COUNT(*) AS cnt FROM grid_cells WHERE world_seed = $1 GROUP BY discovered_by`, [worldSeed] ); return rows; }