From fc8d8ab8d0da95ec79056f59e14a150df6fcea89 Mon Sep 17 00:00:00 2001 From: Karan Jayachandra Date: Sun, 30 Aug 2026 22:43:21 +0200 Subject: Moved to svelte --- src/lib/logic/matchmaking.test.js | 188 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 src/lib/logic/matchmaking.test.js (limited to 'src/lib/logic/matchmaking.test.js') 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 "]); + }); +}); -- cgit v1.3.1