feat(score): Added a scoring system to log 10 last victories
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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();
|
||||||
|
|||||||
@@ -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
@@ -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 };
|
||||||
|
|||||||
@@ -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;
|
||||||
Reference in New Issue
Block a user