import test from "node:test"; import assert from "node:assert/strict"; import { normalizeSeries, getSeriesPointsForPlace, scoreSeriesEvent, getCountedResultsCount, buildSeriesStandings, parsePointsTable, SERIES_POINT_PRESETS } from "../src/series_logic.js"; const orc = normalizeSeries({ id: "s", name: "ÖRC Winter", club: "Östersunds RC", classIds: ["2wd"], countMode: "best", countValue: 9 }); test("ÖRC points: 100, 90, 82 ... 48, then -4 per place down to 10 from place 20", () => { assert.deepEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((place) => getSeriesPointsForPlace(orc, place)), [100, 90, 82, 75, 69, 64, 60, 56, 52, 48]); assert.deepEqual([11, 12, 13, 19, 20, 25].map((place) => getSeriesPointsForPlace(orc, place)), [44, 40, 36, 12, 10, 10]); const f1 = normalizeSeries({ pointsTable: SERIES_POINT_PRESETS.f1.table, pointsStep: 0, pointsMin: 0 }); assert.equal(getSeriesPointsForPlace(f1, 11), 0); assert.deepEqual(parsePointsTable("100, 80 70;60 x -5"), [100, 80, 70, 60]); }); test("only club members score; they are re-ranked without guests; no lap means 0 points", () => { const clubs = { a: "Östersunds RC", guest: "Other RC", b: "östersunds rc ", c: "Östersunds RC" }; const scored = scoreSeriesEvent(orc, [ { driverId: "guest", hasLap: true }, { driverId: "a", hasLap: true }, { driverId: "b", hasLap: true }, { driverId: "c", hasLap: false }, ], { getDriverClub: (id) => clubs[id] }); assert.equal(scored.has("guest"), false, "a guest races but doesn't score"); assert.deepEqual(scored.get("a"), { place: 1, points: 100, driverName: undefined }, "first member after the guest is ranked 1st"); assert.equal(scored.get("b").points, 90, "club name match ignores case and spaces"); assert.deepEqual([scored.get("c").place, scored.get("c").points], [null, 0]); const open = normalizeSeries({ club: "" }); assert.equal(scoreSeriesEvent(open, [{ driverId: "guest", hasLap: true }]).get("guest").points, 100, "no club set: everyone scores"); }); test("who scores is an explicit choice: an open series counts every driver even with a club filled in", () => { const clubs = { member: "Östersunds RC", guest: "Other RC" }; const day = [{ driverId: "guest", hasLap: true }, { driverId: "member", hasLap: true }]; const open = normalizeSeries({ club: "Östersunds RC", scoring: "all" }); assert.equal(open.scoring, "all"); assert.equal(scoreSeriesEvent(open, day, { getDriverClub: (id) => clubs[id] }).get("guest").points, 100); const members = normalizeSeries({ club: "Östersunds RC", scoring: "club" }); assert.equal(scoreSeriesEvent(members, day, { getDriverClub: (id) => clubs[id] }).get("member").points, 100); assert.equal(normalizeSeries({}).scoring, "all", "new series are open by default"); assert.equal(normalizeSeries({ club: "Östersunds RC" }).scoring, "club", "a series saved with a club before the choice existed stays members-only"); }); test("best N, or completed minus X (so a cancelled event lowers the count)", () => { assert.equal(getCountedResultsCount(orc, 12), 9); assert.equal(getCountedResultsCount(orc, 4), 4); const drop3 = normalizeSeries({ countMode: "drop", countValue: 3 }); assert.equal(getCountedResultsCount(drop3, 12), 9); assert.equal(getCountedResultsCount(drop3, 10), 7, "two cancelled events: 10 - 3"); assert.equal(getCountedResultsCount(drop3, 2), 1, "at least one result"); assert.equal(getCountedResultsCount(drop3, 0), 0); }); function series3(extra = {}) { return normalizeSeries({ id: "s", club: "", classIds: ["2wd"], eventIds: ["e1", "e2", "e3", "e4"], countMode: "best", countValue: 2, ...extra }); } const events = [ { id: "e1", name: "R1", date: "2026-11-01", classId: "2wd" }, { id: "e2", name: "R2", date: "2026-11-15", classId: "2wd" }, { id: "e3", name: "R3", date: "2026-11-29", classId: "2wd" }, { id: "e4", name: "R4", date: "2026-12-13", classId: "2wd" }, // not run yet ]; const row = (driverId) => ({ driverId, driverName: driverId.toUpperCase(), hasLap: true }); test("standings count the best results, strike the rest, and absence is a 0 that can be struck", () => { const dayResults = new Map([["e1", [row("a"), row("b")]], ["e2", [row("b"), row("a")]], ["e3", [row("a")]]]); const [table] = buildSeriesStandings(series3(), { events, dayResults, getDriverName: (id) => id.toUpperCase() }); assert.equal(table.completedCount, 3); assert.equal(table.countedCount, 2); assert.equal(table.events.length, 4, "the not-yet-run event is listed as a column"); const [first, second] = table.rows; assert.equal(first.driverName, "A"); assert.equal(first.total, 200, "two wins counted, the 2nd place struck"); assert.deepEqual(first.results.map((result) => [result.points, result.counted]), [[100, true], [90, false], [100, true]]); assert.equal(second.total, 190); assert.deepEqual(second.results.map((result) => [result.points, result.counted, Boolean(result.absent)]), [[90, true, false], [100, true, false], [0, false, true]]); assert.equal(second.starts, 2); }); test("ties: more wins first, then the latest common event, otherwise a shared position", () => { const all = normalizeSeries({ id: "s", classIds: ["2wd"], eventIds: ["e1", "e2", "e3"], countMode: "best", countValue: 3, pointsTable: [10, 10, 5], pointsStep: 0, pointsMin: 0 }); // A: 1st, 3rd, 2nd = 10+5+10 = 25 (1 win). B: 2nd, 1st, 3rd = 10+10+5 = 25 (1 win). C: 3rd, 2nd, 1st = 25. const dayResults = new Map([ ["e1", [row("a"), row("b"), row("c")]], ["e2", [row("b"), row("c"), row("a")]], ["e3", [row("c"), row("a"), row("b")]], ]); const [table] = buildSeriesStandings(all, { events, dayResults }); // Same wins/2nds/3rds for all - the latest event (e3) decides: C, A, B. assert.deepEqual(table.rows.map((entry) => [entry.driverId, entry.total, entry.rank]), [["c", 25, 1], ["a", 25, 2], ["b", 25, 3]]); const winsDecide = new Map([["e1", [row("a"), row("b")]], ["e2", [row("a"), row("b")]], ["e3", [row("b"), row("a")]]]); const twoWins = normalizeSeries({ ...all, pointsTable: [10, 10] }); assert.deepEqual(buildSeriesStandings(twoWins, { events, dayResults: winsDecide })[0].rows.map((entry) => entry.driverId), ["a", "b"], "A has two wins against one"); const shared = new Map([["e1", [row("a"), row("b")]]]); const equal = normalizeSeries({ ...all, pointsTable: [10, 10] }); const sharedRows = buildSeriesStandings(equal, { events, dayResults: shared })[0].rows; assert.equal(sharedRows[0].rank, 1); assert.equal(sharedRows[1].rank, 2, "different places in the latest event still separate them"); }); test("each chosen class gets its own table; events of other classes are ignored", () => { const multi = normalizeSeries({ id: "m", classIds: ["2wd", "4wd"], eventIds: ["e1", "x1"], countValue: 9 }); const tables = buildSeriesStandings(multi, { events: [events[0], { id: "x1", name: "R1 4WD", date: "2026-11-01", classId: "4wd" }, { id: "other", name: "Not in series", classId: "2wd" }], dayResults: new Map([["e1", [row("a")]], ["x1", [row("a"), row("b")]], ["other", [row("z")]]]), classNames: new Map([["2wd", "2WD"], ["4wd", "4WD"]]), }); assert.deepEqual(tables.map((table) => [table.className, table.rows.map((entry) => entry.driverId).join(",")]), [["2WD", "a"], ["4WD", "a,b"]]); }); test("round status follows the race's sessions", async () => { const { getSeriesRoundStatus } = await import("../src/series_logic.js"); const q = (status) => ({ type: "qualification", status }); const f = (status) => ({ type: "final", status }); assert.equal(getSeriesRoundStatus([]), "not_started"); assert.equal(getSeriesRoundStatus([q("ready"), q("ready")]), "qual_ready"); assert.equal(getSeriesRoundStatus([q("finished"), q("ready")]), "qual_running"); assert.equal(getSeriesRoundStatus([q("finished"), q("finished")]), "qual_done"); assert.equal(getSeriesRoundStatus([q("finished"), f("ready")]), "finals"); assert.equal(getSeriesRoundStatus([q("finished"), f("finished")]), "done"); }); test("qualifying heats are split as evenly as possible - the heat count follows the field", async () => { const { splitIntoBalancedGroups } = await import("../src/event_race_logic.js"); const sizes = (n, max) => splitIntoBalancedGroups(Array.from({ length: n }, (_, i) => i), max).map((group) => group.length); assert.deepEqual(sizes(9, 8), [5, 4], "never 8 + 1"); assert.deepEqual(sizes(6, 6), [6]); assert.deepEqual(sizes(13, 6), [5, 4, 4]); assert.deepEqual(sizes(0, 6), []); assert.deepEqual(splitIntoBalancedGroups(["a", "b", "c", "d", "e"], 3), [["a", "b", "c"], ["d", "e"]], "order is kept"); });