aboutsummaryrefslogtreecommitdiff
path: root/src/lib/logic/matchmaking.test.js
diff options
context:
space:
mode:
authorKaran Jayachandra <mail@karanjayachandra.com>2026-08-30 22:43:21 +0200
committerKaran Jayachandra <mail@karanjayachandra.com>2026-08-30 22:43:21 +0200
commitfc8d8ab8d0da95ec79056f59e14a150df6fcea89 (patch)
tree33f8050b2f2920c0f1b884a205a8aa4c81d7687c /src/lib/logic/matchmaking.test.js
parentd37ebfda883bcd03b146a85bc3e5a4657d0abd73 (diff)
Moved to sveltemain
Diffstat (limited to 'src/lib/logic/matchmaking.test.js')
-rw-r--r--src/lib/logic/matchmaking.test.js188
1 files changed, 188 insertions, 0 deletions
diff --git a/src/lib/logic/matchmaking.test.js b/src/lib/logic/matchmaking.test.js
new file mode 100644
index 0000000..0401c1c
--- /dev/null
+++ b/src/lib/logic/matchmaking.test.js
@@ -0,0 +1,188 @@
+import { describe, it, expect } from "vitest";
+import { createCourt, createRegular, createGuest } from "./players.js";
+import {
+ shuffle,
+ getPossibleGameCount,
+ selectPlayers,
+ pairPlayersIntoTeams,
+ addBlockedGames,
+ addInactiveGames,
+ addActiveGames,
+ proposeGames,
+ collectConfirmedPlayerNames,
+ isPlaceholderGame,
+} from "./matchmaking.js";
+
+describe("shuffle", () => {
+ it("returns an array with the same elements, without mutating the input", () => {
+ const original = [1, 2, 3, 4, 5];
+ const copy = [...original];
+ const result = shuffle(original);
+ expect(original).toEqual(copy);
+ expect(result.slice().sort()).toEqual(original.slice().sort());
+ });
+});
+
+describe("getPossibleGameCount", () => {
+ it("is limited by available courts", () => {
+ expect(getPossibleGameCount(40, 2)).toBe(2);
+ });
+ it("is limited by available players (4 per game)", () => {
+ expect(getPossibleGameCount(5, 10)).toBe(1);
+ });
+ it("is zero when there are no active courts", () => {
+ expect(getPossibleGameCount(40, 0)).toBe(0);
+ });
+ it("is zero when there are fewer than 4 active players", () => {
+ expect(getPossibleGameCount(3, 10)).toBe(0);
+ });
+});
+
+describe("selectPlayers", () => {
+ it("prefers players with fewer games played", () => {
+ const regulars = [
+ createRegular(0, true, 5, 10, "Most", "Games"),
+ createRegular(1, true, 5, 0, "Fewest", "Games"),
+ ];
+ const selected = selectPlayers(regulars, [], 1);
+ expect(selected).toHaveLength(1);
+ expect(selected[0].name).toBe("Fewest Games");
+ });
+
+ it("sorts the final selection by level ascending", () => {
+ const regulars = [
+ createRegular(0, true, 8, 0, "High", "Level"),
+ createRegular(1, true, 2, 0, "Low", "Level"),
+ createRegular(2, true, 5, 0, "Mid", "Level"),
+ ];
+ const selected = selectPlayers(regulars, [], 3);
+ expect(selected.map((p) => p.level)).toEqual([2, 5, 8]);
+ });
+});
+
+describe("pairPlayersIntoTeams", () => {
+ it("cross-pairs players by rank (0&3 vs 1&2)", () => {
+ const players = [{ name: "A" }, { name: "B" }, { name: "C" }, { name: "D" }];
+ const [teamOne, teamTwo] = pairPlayersIntoTeams(players);
+ expect(teamOne.playerOne.name).toBe("A");
+ expect(teamOne.playerTwo.name).toBe("D");
+ expect(teamTwo.playerOne.name).toBe("B");
+ expect(teamTwo.playerTwo.name).toBe("C");
+ });
+});
+
+describe("addBlockedGames / addInactiveGames", () => {
+ it("marks games for inactive courts as RESERVED and inactive", () => {
+ const courts = [createCourt(3, false)];
+ const [game] = addBlockedGames(courts);
+ expect(game.id).toBe(3);
+ expect(game.status).toBe(false);
+ expect(isPlaceholderGame(game)).toBe(true);
+ expect(game.teamOne.playerOne.firstName).toBe("RESERVED");
+ });
+
+ it("marks games for unused active courts as placeholders", () => {
+ const courts = [createCourt(1, true)];
+ const [game] = addInactiveGames(courts);
+ expect(game.teamOne.playerOne.firstName).toBe("---");
+ });
+});
+
+describe("addActiveGames", () => {
+ it("creates one game per used court from the selected players", () => {
+ const usedCourts = [createCourt(0, true), createCourt(1, true)];
+ const players = Array.from({ length: 8 }, (_, i) =>
+ createRegular(i, true, 1, 0, `P${i}`, "")
+ );
+ const games = addActiveGames(usedCourts, players);
+ expect(games).toHaveLength(2);
+ expect(games[0].id).toBe(0);
+ expect(games[1].id).toBe(1);
+ expect(games[0].teamOne.playerOne.name).toBe("P0 ");
+ });
+});
+
+describe("proposeGames", () => {
+ it("never assigns more games than active courts", () => {
+ const courts = [createCourt(0, true), createCourt(1, true)];
+ const regulars = Array.from({ length: 20 }, (_, i) =>
+ createRegular(i, true, 1, 0, `R${i}`, "")
+ );
+ const games = proposeGames({
+ courts,
+ regulars,
+ guests: [],
+ skillFactor: 10,
+ });
+ expect(games).toHaveLength(2);
+ });
+
+ it("does not skip the court right after the used ones (no off-by-one gap)", () => {
+ const courts = [
+ createCourt(0, true),
+ createCourt(1, true),
+ createCourt(2, true),
+ ];
+ // Only enough players for exactly 1 active game, so courts 1 and 2
+ // should both come back as unused-but-active placeholder games.
+ const regulars = Array.from({ length: 4 }, (_, i) =>
+ createRegular(i, true, 1, 0, `R${i}`, "")
+ );
+ const games = proposeGames({
+ courts,
+ regulars,
+ guests: [],
+ skillFactor: 10,
+ });
+ expect(games.map((g) => g.id)).toEqual([0, 1, 2]);
+ expect(games[1].teamOne.playerOne.firstName).toBe("---");
+ expect(games[2].teamOne.playerOne.firstName).toBe("---");
+ });
+
+ it("gives reserved (inactive) courts a RESERVED placeholder", () => {
+ const courts = [createCourt(0, false)];
+ const games = proposeGames({
+ courts,
+ regulars: [],
+ guests: [],
+ skillFactor: 10,
+ });
+ expect(games).toHaveLength(1);
+ expect(games[0].teamOne.playerOne.firstName).toBe("RESERVED");
+ });
+
+ it("applies the skill factor to guests before pairing", () => {
+ const courts = [createCourt(0, true)];
+ const guests = [
+ createGuest(0, true, 0), // Beginner, level 1
+ createGuest(6, true, 0), // Novice, level 3
+ createGuest(12, true, 0), // Intermediate, level 6
+ createGuest(1, true, 0), // Beginner, level 1
+ ];
+ const games = proposeGames({
+ courts,
+ regulars: [],
+ guests,
+ skillFactor: 1,
+ });
+ expect(games).toHaveLength(1);
+ expect(games[0].status).toBe(false);
+ });
+});
+
+describe("collectConfirmedPlayerNames", () => {
+ it("excludes placeholder (RESERVED/---) games from the name list", () => {
+ const usedCourts = [createCourt(0, true)];
+ const players = [
+ createRegular(0, true, 1, 0, "A", ""),
+ createRegular(1, true, 1, 0, "B", ""),
+ createRegular(2, true, 1, 0, "C", ""),
+ createRegular(3, true, 1, 0, "D", ""),
+ ];
+ const realGame = addActiveGames(usedCourts, players)[0];
+ const [reservedGame] = addBlockedGames([createCourt(1, false)]);
+
+ const names = collectConfirmedPlayerNames([realGame, reservedGame]);
+ expect(names).toEqual(["A ", "D ", "B ", "C "]);
+ });
+});