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 "]); }); });