feat(score): Added a scoring system to log 10 last victories
This commit is contained in:
@@ -274,6 +274,14 @@
|
||||
</div>
|
||||
</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 -->
|
||||
<details class="panel panelCollapsible">
|
||||
<summary class="panelTitle panelTitleSummary">👥 Joueurs actifs</summary>
|
||||
|
||||
@@ -157,3 +157,9 @@ export async function apiCaptureCell(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
@@ -1,6 +1,6 @@
|
||||
import { fnv1a32, hash2u32, mulberry32 } from "./rng.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";
|
||||
|
||||
// ── Constants ─────────────────────────────────────────────────────────────────
|
||||
@@ -265,6 +265,7 @@ const playerListTableWrapEl = document.getElementById("playerListTableWrap");
|
||||
const playerListSearchEl = document.getElementById("playerListSearch");
|
||||
const playerListSortBtnEl = document.getElementById("playerListSortBtn");
|
||||
const mapAnimEl = document.getElementById("mapAnim");
|
||||
const roundHistoryTableBodyEl = document.getElementById("roundHistoryTableBody");
|
||||
// ── Cell helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
export function cellKey(x, y) { return `${x},${y}`; }
|
||||
@@ -369,6 +370,55 @@ export async function loadVictoryPoints() {
|
||||
} 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() {
|
||||
try {
|
||||
const { blue, red } = await apiFetchActivePlayers();
|
||||
|
||||
+7
-1
@@ -10,6 +10,7 @@ import {
|
||||
updateEconomyDisplay,
|
||||
loadEconScores,
|
||||
loadVictoryPoints,
|
||||
loadRoundHistory,
|
||||
loadDbInfo,
|
||||
loadElementBonus,
|
||||
loadMilitaryDeductions,
|
||||
@@ -98,7 +99,10 @@ function startFallbackPolling() {
|
||||
fallbackPollingEnabled = true;
|
||||
scheduleConfigPoll();
|
||||
scheduleScorePoll();
|
||||
victoryPollTimer = setInterval(loadVictoryPoints, 30_000);
|
||||
victoryPollTimer = setInterval(async () => {
|
||||
await loadVictoryPoints();
|
||||
await loadRoundHistory();
|
||||
}, 30_000);
|
||||
gridPollTimer = setInterval(refreshGridDisplay, 1_000);
|
||||
}
|
||||
|
||||
@@ -113,6 +117,7 @@ async function syncFromServer() {
|
||||
try {
|
||||
await refreshFromServer();
|
||||
await loadVictoryPoints();
|
||||
await loadRoundHistory();
|
||||
} finally {
|
||||
syncInFlight = false;
|
||||
}
|
||||
@@ -210,6 +215,7 @@ async function boot() {
|
||||
await fetchAndApplyActivePlayers();
|
||||
await loadEconScores();
|
||||
await loadVictoryPoints();
|
||||
await loadRoundHistory();
|
||||
await loadDbInfo();
|
||||
await loadElementBonus();
|
||||
await loadMilitaryDeductions();
|
||||
|
||||
@@ -1398,3 +1398,74 @@ canvas {
|
||||
user-select: none;
|
||||
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
@@ -134,6 +134,17 @@ export async function initGameSchema() {
|
||||
await pool.query(`
|
||||
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 ──────────────────────────────────────────────────────────
|
||||
@@ -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)})`);
|
||||
}
|
||||
// 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) {
|
||||
console.error("[world] VP award error:", e);
|
||||
}
|
||||
@@ -347,10 +363,27 @@ export async function getDbCreatedAt() {
|
||||
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 ────────────────────────────────────────────────────────────
|
||||
|
||||
export async function getVictoryPoints() {
|
||||
const { rows } = await pool.query(
|
||||
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 };
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
checkTeamVisibility,
|
||||
insertTeamVisibility,
|
||||
getTeamVisibleCells,
|
||||
getRoundHistory,
|
||||
} from "../db/gameDb.js";
|
||||
import {
|
||||
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;
|
||||
Reference in New Issue
Block a user