feat(score): Added a scoring system to log 10 last victories

This commit is contained in:
gauvainboiche
2026-04-02 23:11:41 +02:00
parent bfdc836275
commit b11446cf56
7 changed files with 190 additions and 4 deletions
+8
View File
@@ -274,6 +274,14 @@
</div> </div>
</details> </details>
<!-- Round history -->
<details class="panel panelCollapsible">
<summary class="panelTitle panelTitleSummary">🏆 Tableau des scores</summary>
<div id="roundHistoryTableBody" class="roundHistoryWrap">
<p class="econEmpty">Chargement…</p>
</div>
</details>
<!-- Players list --> <!-- Players list -->
<details class="panel panelCollapsible"> <details class="panel panelCollapsible">
<summary class="panelTitle panelTitleSummary">👥 Joueurs actifs</summary> <summary class="panelTitle panelTitleSummary">👥 Joueurs actifs</summary>
+6
View File
@@ -157,3 +157,9 @@ export async function apiCaptureCell(seed, x, y) {
body: JSON.stringify({ seed, x, y }), body: JSON.stringify({ seed, x, y }),
}); });
} }
export async function apiFetchRoundHistory() {
const res = await fetch("/api/round-history");
if (!res.ok) throw new Error("round_history_fetch_failed");
return res.json();
}
+51 -1
View File
@@ -1,6 +1,6 @@
import { fnv1a32, hash2u32, mulberry32 } from "./rng.js"; import { fnv1a32, hash2u32, mulberry32 } from "./rng.js";
import { formatPlanet, generatePlanet } from "./planetGeneration.js"; import { formatPlanet, generatePlanet } from "./planetGeneration.js";
import { apiFetchConfig, apiFetchGrid, apiRevealCell, apiFetchEconScores, apiTickEconScores, apiFetchElementBonus, apiTickElementBonus, apiFetchDbInfo, apiFetchVictoryPoints, apiFetchActivePlayers, apiFetchActivePlayerNames, apiFetchMilitaryDeductions, apiMilitaryAttack, apiCaptureCell } from "./api.js"; import { apiFetchConfig, apiFetchGrid, apiRevealCell, apiFetchEconScores, apiTickEconScores, apiFetchElementBonus, apiTickElementBonus, apiFetchDbInfo, apiFetchVictoryPoints, apiFetchActivePlayers, apiFetchActivePlayerNames, apiFetchMilitaryDeductions, apiMilitaryAttack, apiCaptureCell, apiFetchRoundHistory } from "./api.js";
import { computeTeamIncome, computeTeamElementBonus, computeTeamElementBonusDetailed, renderResourceTable, renderElementBonusTable, setEconSort, getEconSort, setElemSort, getElemSort, computeTeamMilitaryDetailed, renderMilitaryTable, setMilSort, getMilSort, computeTeamIncomeByPlanet, renderResourceByPlanetTable, setPlanetSort, getPlanetSort } from "./economy.js"; import { computeTeamIncome, computeTeamElementBonus, computeTeamElementBonusDetailed, renderResourceTable, renderElementBonusTable, setEconSort, getEconSort, setElemSort, getElemSort, computeTeamMilitaryDetailed, renderMilitaryTable, setMilSort, getMilSort, computeTeamIncomeByPlanet, renderResourceByPlanetTable, setPlanetSort, getPlanetSort } from "./economy.js";
// ── Constants ───────────────────────────────────────────────────────────────── // ── Constants ─────────────────────────────────────────────────────────────────
@@ -265,6 +265,7 @@ const playerListTableWrapEl = document.getElementById("playerListTableWrap");
const playerListSearchEl = document.getElementById("playerListSearch"); const playerListSearchEl = document.getElementById("playerListSearch");
const playerListSortBtnEl = document.getElementById("playerListSortBtn"); const playerListSortBtnEl = document.getElementById("playerListSortBtn");
const mapAnimEl = document.getElementById("mapAnim"); const mapAnimEl = document.getElementById("mapAnim");
const roundHistoryTableBodyEl = document.getElementById("roundHistoryTableBody");
// ── Cell helpers ────────────────────────────────────────────────────────────── // ── Cell helpers ──────────────────────────────────────────────────────────────
export function cellKey(x, y) { return `${x},${y}`; } export function cellKey(x, y) { return `${x},${y}`; }
@@ -369,6 +370,55 @@ export async function loadVictoryPoints() {
} catch { /* ignore */ } } catch { /* ignore */ }
} }
export async function loadRoundHistory() {
if (!roundHistoryTableBodyEl) return;
try {
const history = await apiFetchRoundHistory();
renderRoundHistoryTable(history);
} catch { /* ignore */ }
}
function renderRoundHistoryTable(history) {
if (!roundHistoryTableBodyEl) return;
if (!history || history.length === 0) {
roundHistoryTableBodyEl.innerHTML = `<p class="roundHistoryEmpty">Aucune manche terminée.</p>`;
return;
}
const rows = history.map(entry => {
const blueScore = entry.blueScore;
const redScore = entry.redScore;
const blueWins = blueScore >= redScore;
const date = new Date(entry.endedAt);
const label = date.toLocaleString("fr-FR", {
day: "2-digit", month: "2-digit", year: "2-digit",
hour: "2-digit", minute: "2-digit",
});
const fmt = (v) => v >= 1e6
? (v / 1e6).toFixed(1) + " M"
: v >= 1e3
? (v / 1e3).toFixed(1) + " k"
: v.toFixed(0);
const blueClass = blueWins ? "roundHistoryCell--winner-blue" : "";
const redClass = !blueWins ? "roundHistoryCell--winner-red" : "";
return `<tr>
<td title="${entry.worldSeed}">${label}</td>
<td class="${blueClass}">${fmt(blueScore)}</td>
<td class="${redClass}">${fmt(redScore)}</td>
</tr>`;
}).join("");
roundHistoryTableBodyEl.innerHTML = `
<table class="roundHistoryTable">
<thead>
<tr>
<th></th>
<th class="roundHistoryTh--blue">Résistance</th>
<th class="roundHistoryTh--red">Premier Ordre</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>`;
}
export async function fetchAndApplyActivePlayers() { export async function fetchAndApplyActivePlayers() {
try { try {
const { blue, red } = await apiFetchActivePlayers(); const { blue, red } = await apiFetchActivePlayers();
+7 -1
View File
@@ -10,6 +10,7 @@ import {
updateEconomyDisplay, updateEconomyDisplay,
loadEconScores, loadEconScores,
loadVictoryPoints, loadVictoryPoints,
loadRoundHistory,
loadDbInfo, loadDbInfo,
loadElementBonus, loadElementBonus,
loadMilitaryDeductions, loadMilitaryDeductions,
@@ -98,7 +99,10 @@ function startFallbackPolling() {
fallbackPollingEnabled = true; fallbackPollingEnabled = true;
scheduleConfigPoll(); scheduleConfigPoll();
scheduleScorePoll(); scheduleScorePoll();
victoryPollTimer = setInterval(loadVictoryPoints, 30_000); victoryPollTimer = setInterval(async () => {
await loadVictoryPoints();
await loadRoundHistory();
}, 30_000);
gridPollTimer = setInterval(refreshGridDisplay, 1_000); gridPollTimer = setInterval(refreshGridDisplay, 1_000);
} }
@@ -113,6 +117,7 @@ async function syncFromServer() {
try { try {
await refreshFromServer(); await refreshFromServer();
await loadVictoryPoints(); await loadVictoryPoints();
await loadRoundHistory();
} finally { } finally {
syncInFlight = false; syncInFlight = false;
} }
@@ -210,6 +215,7 @@ async function boot() {
await fetchAndApplyActivePlayers(); await fetchAndApplyActivePlayers();
await loadEconScores(); await loadEconScores();
await loadVictoryPoints(); await loadVictoryPoints();
await loadRoundHistory();
await loadDbInfo(); await loadDbInfo();
await loadElementBonus(); await loadElementBonus();
await loadMilitaryDeductions(); await loadMilitaryDeductions();
+71
View File
@@ -1397,4 +1397,75 @@ canvas {
z-index: 20; z-index: 20;
user-select: none; user-select: none;
white-space: nowrap; white-space: nowrap;
}
/* ── Round history table ──────────────────────────────────────────────────── */
.roundHistoryWrap {
padding: 6px 0 4px;
}
.roundHistoryTable {
width: 100%;
border-collapse: collapse;
font-size: 12px;
table-layout: fixed;
}
.roundHistoryTable thead th {
padding: 4px 8px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
border-bottom: 1px solid rgba(255,255,255,0.1);
text-align: center;
}
.roundHistoryTable thead th:first-child {
text-align: left;
width: 36%;
}
.roundHistoryTh--blue { color: rgba(90, 200, 255, 0.9); }
.roundHistoryTh--red { color: rgba(220, 75, 85, 0.9); }
.roundHistoryTable tbody tr {
border-bottom: 1px solid rgba(255,255,255,0.04);
}
.roundHistoryTable td {
padding: 5px 8px;
text-align: center;
color: rgba(255,255,255,0.7);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.roundHistoryTable td:first-child {
text-align: left;
font-size: 10px;
color: rgba(255,255,255,0.4);
}
.roundHistoryCell--winner-blue {
background: rgba(90, 200, 255, 0.18);
color: rgba(90, 200, 255, 1);
font-weight: 700;
border-radius: 3px;
}
.roundHistoryCell--winner-red {
background: rgba(220, 75, 85, 0.18);
color: rgba(220, 75, 85, 1);
font-weight: 700;
border-radius: 3px;
}
.roundHistoryEmpty {
padding: 10px 8px;
font-size: 12px;
color: rgba(255,255,255,0.35);
font-style: italic;
} }
+35 -2
View File
@@ -134,6 +134,17 @@ export async function initGameSchema() {
await pool.query(` await pool.query(`
ALTER TABLE grid_cells ADD COLUMN IF NOT EXISTS captured_by TEXT; ALTER TABLE grid_cells ADD COLUMN IF NOT EXISTS captured_by TEXT;
`); `);
// ── Round history (one row per completed seed epoch) ─────────────────────
await pool.query(`
CREATE TABLE IF NOT EXISTS round_history (
id SERIAL PRIMARY KEY,
world_seed TEXT NOT NULL,
ended_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
blue_score DOUBLE PRECISION NOT NULL DEFAULT 0,
red_score DOUBLE PRECISION NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_round_history_ended_at ON round_history (ended_at DESC);
`);
} }
// ── World-seed epoch ────────────────────────────────────────────────────────── // ── World-seed epoch ──────────────────────────────────────────────────────────
@@ -164,6 +175,11 @@ export async function ensureSeedEpoch() {
); );
console.log(`[world] VP awarded to ${winner} for seed ${expiredSeed} (blue=${scores.blue.toFixed(3)}, red=${scores.red.toFixed(3)})`); console.log(`[world] VP awarded to ${winner} for seed ${expiredSeed} (blue=${scores.blue.toFixed(3)}, red=${scores.red.toFixed(3)})`);
} }
// Record round result in history regardless of whether scores are non-zero
await pool.query(
`INSERT INTO round_history (world_seed, blue_score, red_score) VALUES ($1, $2, $3)`,
[expiredSeed, scores.blue, scores.red]
);
} catch (e) { } catch (e) {
console.error("[world] VP award error:", e); console.error("[world] VP award error:", e);
} }
@@ -347,10 +363,27 @@ export async function getDbCreatedAt() {
return rows[0]?.created_at ?? null; return rows[0]?.created_at ?? null;
} }
// ── Round history ─────────────────────────────────────────────────────────────
export async function getRoundHistory(limit = 10) {
const { rows } = await pool.query(
`SELECT world_seed, ended_at, blue_score, red_score
FROM round_history
ORDER BY ended_at DESC
LIMIT $1`,
[limit]
);
return rows.map(r => ({
worldSeed: r.world_seed,
endedAt: r.ended_at,
blueScore: Number(r.blue_score),
redScore: Number(r.red_score),
}));
}
// ── Victory points ──────────────────────────────────────────────────────────── // ── Victory points ────────────────────────────────────────────────────────────
export async function getVictoryPoints() { export async function getVictoryPoints() { const { rows } = await pool.query(
const { rows } = await pool.query(
`SELECT team, COUNT(*) AS cnt FROM victory_points GROUP BY team` `SELECT team, COUNT(*) AS cnt FROM victory_points GROUP BY team`
); );
const result = { blue: 0, red: 0 }; const result = { blue: 0, red: 0 };
+12
View File
@@ -32,6 +32,7 @@ import {
checkTeamVisibility, checkTeamVisibility,
insertTeamVisibility, insertTeamVisibility,
getTeamVisibleCells, getTeamVisibleCells,
getRoundHistory,
} from "../db/gameDb.js"; } from "../db/gameDb.js";
import { import {
nextResetUtc, nextResetUtc,
@@ -553,4 +554,15 @@ router.get("/cell/attacks", async (req, res) => {
} }
}); });
// GET /api/round-history
router.get("/round-history", async (_req, res) => {
try {
const history = await getRoundHistory(10);
res.json(history);
} catch (e) {
console.error(e);
res.status(500).json({ error: "database_error" });
}
});
export default router; export default router;