Import Melee matches exports, add tests and local podman stack
- Parse Melee matches exports (one round per file, byes, null GameWins); one import form detects matches vs standings exports. Re-importing a round replaces just that round and keeps entered decks. - Per-player deck entry for sessions scored from matches. - node:test suite with anonymised fixtures; lockfile. - scripts/local.sh runs the compose stack under podman with its own .env.local, project name and volume. - README and CLAUDE.md docs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a7f440c658
commit
985c85bcef
15 files changed
+2450
-27
No files matched your search
+27
-4
@@ -47,13 +47,17 @@ export function parseTable(input: string): Rec[] {
|
||||
return body.map((r) => Object.fromEntries(head.map((h, i) => [h, r[i] ?? ""])));
|
||||
}
|
||||
|
||||
// Melee's standings export holds one snapshot per round, so use the final round. Real names are used
|
||||
// (falling back to display name, then username).
|
||||
// Real names are used (falling back to display name, then username).
|
||||
const playerName = (team: Rec | undefined) => {
|
||||
const p = team?.Players?.[0] ?? {};
|
||||
return String(p.Name || p.DisplayName || p.Username || "").trim();
|
||||
};
|
||||
|
||||
// Melee's standings export holds one snapshot per round, so use the final round.
|
||||
function melee(rows: Rec[]): Row[] {
|
||||
const last = rows.reduce((m, r) => Math.max(m, Number(r.RoundNumber) || 0), 0);
|
||||
return rows.filter((r) => Number(r.RoundNumber) === last).flatMap((r): Row[] => {
|
||||
const p = r.Team?.Players?.[0] ?? {};
|
||||
const name = String(p.Name || p.DisplayName || p.Username || "").trim();
|
||||
const name = playerName(r.Team);
|
||||
if (!name) return [];
|
||||
const nums = [r.Points, r.MatchWins, r.MatchDraws, r.MatchLosses].map(Number);
|
||||
if (!nums.every((n) => Number.isInteger(n) && n >= 0 && n < 1e6)) throw new InputError(`Bad numbers for ${name}`);
|
||||
@@ -78,3 +82,22 @@ export function extract(input: Rec[]): Row[] {
|
||||
return [{ name, points: Math.round(pts), deck: dc ? String(r[dc] ?? "").trim() : "", wdl: null }];
|
||||
});
|
||||
}
|
||||
|
||||
// Melee's matches export: one round per file, a bye is a single competitor. GameWins is null for 0, so read
|
||||
// GameWinsAndGameByes. Unreported matches are skipped.
|
||||
export type Match = { round: number; a: string; b: string | null; w1: number; w2: number; draws: number };
|
||||
export const isMatchExport = (rows: Rec[]) => rows.length > 0 && Array.isArray(rows[0].Competitors) && "RoundNumber" in rows[0];
|
||||
|
||||
export function extractMatches(rows: Rec[]): Match[] {
|
||||
const out = rows.filter((m) => m.HasResult).map((m): Match => {
|
||||
const cs = [...(m.Competitors ?? [])].sort((x, y) => (Number(x.SortOrder) || 0) - (Number(y.SortOrder) || 0));
|
||||
const [a, b] = cs.map((c) => playerName(c.Team));
|
||||
if (!a || cs.length > 2 || (cs.length === 2 && !b)) throw new InputError(`Round ${m.RoundNumber}: only 1-on-1 matches with named players are supported`);
|
||||
const nums = [m.RoundNumber, cs[0].GameWinsAndGameByes, cs[1]?.GameWinsAndGameByes ?? 0, m.GameDraws ?? 0].map(Number);
|
||||
if (!nums.every((n) => Number.isInteger(n) && n >= 0 && n <= 99)) throw new InputError(`Bad numbers in the match for ${a}`);
|
||||
const [round, w1, w2, draws] = nums;
|
||||
return b ? { round, a, b, w1, w2, draws } : { round, a, b: null, w1: 2, w2: 0, draws: 0 };
|
||||
});
|
||||
if (!out.length) throw new InputError("No completed matches in that file");
|
||||
return out;
|
||||
}
|
||||
Reference in new issue
Block a user