mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-26 13:22:50 -05:00
Count rounds algorithm
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
// TODO: Warning: Text content did not match. Server: "57" Client: "56"
|
||||
|
||||
import * as React from "react";
|
||||
import { useFetcher, useLoaderData } from "remix";
|
||||
import {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { suite } from "uvu";
|
||||
import * as assert from "uvu/assert";
|
||||
import {
|
||||
createEliminationBracket,
|
||||
eliminationBracket,
|
||||
fillParticipantsWithNullTillPowerOfTwo,
|
||||
Match,
|
||||
TeamIdentifier,
|
||||
@@ -15,25 +15,25 @@ const FillParticipantsWithNull = suite(
|
||||
);
|
||||
|
||||
AmountOfTeams("Generates right amount of rounds (16 participants - SE)", () => {
|
||||
const bracket16 = createEliminationBracket(16, "SE");
|
||||
const bracket16 = eliminationBracket(16, "SE");
|
||||
assert.equal(removeMatchesWithByes(bracket16.winners).length, 15);
|
||||
assert.equal(removeMatchesWithByes(bracket16.losers).length, 0);
|
||||
});
|
||||
|
||||
AmountOfTeams("Generates right amount of rounds (16 participants - DE)", () => {
|
||||
const bracket16 = createEliminationBracket(16, "DE");
|
||||
const bracket16 = eliminationBracket(16, "DE");
|
||||
assert.equal(removeMatchesWithByes(bracket16.winners).length, 16); // not incl reset
|
||||
assert.equal(removeMatchesWithByes(bracket16.losers).length, 14);
|
||||
});
|
||||
|
||||
AmountOfTeams("Generates right amount of rounds (15 participants - DE)", () => {
|
||||
const bracket15 = createEliminationBracket(15, "DE");
|
||||
const bracket15 = eliminationBracket(15, "DE");
|
||||
assert.equal(removeMatchesWithByes(bracket15.winners).length, 15); // not incl reset
|
||||
assert.equal(removeMatchesWithByes(bracket15.losers).length, 14); // one bye
|
||||
});
|
||||
|
||||
AmountOfTeams("Generates right amount of rounds (17 participants - DE)", () => {
|
||||
const bracket17 = createEliminationBracket(17, "DE");
|
||||
const bracket17 = eliminationBracket(17, "DE");
|
||||
assert.equal(removeMatchesWithByes(bracket17.winners).length, 17); // not incl reset
|
||||
assert.equal(removeMatchesWithByes(bracket17.losers).length, 30);
|
||||
|
||||
@@ -42,24 +42,24 @@ AmountOfTeams("Generates right amount of rounds (17 participants - DE)", () => {
|
||||
});
|
||||
|
||||
AmountOfTeams("Same amount of rounds as next power of two", () => {
|
||||
const bracket17 = createEliminationBracket(17, "DE");
|
||||
const bracket32 = createEliminationBracket(32, "DE");
|
||||
const bracket17 = eliminationBracket(17, "DE");
|
||||
const bracket32 = eliminationBracket(32, "DE");
|
||||
assert.equal(bracket17.winners.length, bracket32.winners.length); // not incl reset
|
||||
assert.equal(bracket17.losers.length, bracket32.losers.length);
|
||||
});
|
||||
|
||||
Byes("Right amount of byes", () => {
|
||||
const bracket17 = createEliminationBracket(17, "DE");
|
||||
const bracket17 = eliminationBracket(17, "DE");
|
||||
assert.equal(countOpponentsWithByes(bracket17.winners), 15);
|
||||
});
|
||||
|
||||
Byes("Correct team has bye", () => {
|
||||
const bracket15 = createEliminationBracket(15, "DE");
|
||||
const bracket15 = eliminationBracket(15, "DE");
|
||||
assert.equal(teamWithBye(bracket15.winners), 1);
|
||||
});
|
||||
|
||||
Seeds("First and second seed are spread apart", () => {
|
||||
const bracket16 = createEliminationBracket(16, "DE");
|
||||
const bracket16 = eliminationBracket(16, "DE");
|
||||
assert.ok(
|
||||
[bracket16.winners[0].upperTeam, bracket16.winners[0].lowerTeam].includes(
|
||||
1
|
||||
|
||||
@@ -7,17 +7,22 @@ export interface Match {
|
||||
upperTeam?: TeamIdentifier;
|
||||
lowerTeam?: TeamIdentifier;
|
||||
winner?: TeamIdentifier;
|
||||
/** Match that leads to this match */
|
||||
match1?: Match;
|
||||
/** Match that leads to this match */
|
||||
match2?: Match;
|
||||
}
|
||||
|
||||
interface Bracket {
|
||||
export interface Bracket {
|
||||
winners: Match[];
|
||||
losers: Match[];
|
||||
participantCount: number;
|
||||
participantsWithByesCount: number;
|
||||
}
|
||||
|
||||
/** @link https://stackoverflow.com/a/59615574 */
|
||||
export function createEliminationBracket(
|
||||
/** Singe/Double Elimination bracket algorithm that handles byes
|
||||
* @link https://stackoverflow.com/a/59615574 */
|
||||
export function eliminationBracket(
|
||||
participantCount: number,
|
||||
type: "SE" | "DE"
|
||||
) {
|
||||
@@ -30,9 +35,16 @@ export function createEliminationBracket(
|
||||
const matchesWQueue: Match[] = [];
|
||||
const matchesLQueue: Match[] = [];
|
||||
const backfillQ: Match[] = [];
|
||||
|
||||
invariant(
|
||||
powerOf2(participants.length),
|
||||
"Unexpected participants length not power of two"
|
||||
);
|
||||
const bracket: Bracket = {
|
||||
winners: [],
|
||||
losers: [],
|
||||
participantCount,
|
||||
participantsWithByesCount: participants.length,
|
||||
};
|
||||
|
||||
const bracketSize = participants.length;
|
||||
@@ -47,7 +59,6 @@ export function createEliminationBracket(
|
||||
|
||||
// First round
|
||||
for (let i = 1; i <= bracketSize / 2; i++) {
|
||||
// TODO: respect seed
|
||||
const upperTeam = participants.pop();
|
||||
const lowerTeam = participants.pop();
|
||||
invariant(
|
||||
@@ -58,6 +69,10 @@ export function createEliminationBracket(
|
||||
typeof lowerTeam !== "undefined",
|
||||
"Unexpected team1 is undefined in first round"
|
||||
);
|
||||
invariant(
|
||||
!(upperTeam === "BYE" && lowerTeam === "BYE"),
|
||||
"Unexpected both teams in the first round are BYEs"
|
||||
);
|
||||
const firstRoundMatch = createMatch({
|
||||
upperTeam,
|
||||
lowerTeam,
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { test } from "uvu";
|
||||
import { suite } from "uvu";
|
||||
import * as assert from "uvu/assert";
|
||||
import { sortTeamsBySeed } from "./utils";
|
||||
import { eliminationBracket } from "./bracket";
|
||||
import { countRounds, sortTeamsBySeed } from "./utils";
|
||||
|
||||
test("Sorts teams by seed", () => {
|
||||
const SortTeams = suite("sortTeamsBySeed()");
|
||||
const CountBracketRounds = suite("countRounds()");
|
||||
|
||||
SortTeams("Sorts teams by seed", () => {
|
||||
const seeds = ["3", "2", "1"];
|
||||
const teamsToSeed = [
|
||||
{ id: "1", createdAt: "1639036511550" },
|
||||
@@ -17,7 +21,7 @@ test("Sorts teams by seed", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("Sorts teams by createdAt", () => {
|
||||
SortTeams("Sorts teams by createdAt", () => {
|
||||
const seeds: string[] = [];
|
||||
const teamsToSeed = [
|
||||
{ id: "1", createdAt: "1639036511550" },
|
||||
@@ -32,7 +36,7 @@ test("Sorts teams by createdAt", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("Sorts teams by seed and createdAt", () => {
|
||||
SortTeams("Sorts teams by seed and createdAt", () => {
|
||||
const seeds: string[] = ["3"];
|
||||
const teamsToSeed = [
|
||||
{ id: "1", createdAt: "1639036511550" },
|
||||
@@ -47,7 +51,7 @@ test("Sorts teams by seed and createdAt", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("Can handle non-existent id in seeds", () => {
|
||||
SortTeams("Can handle non-existent id in seeds", () => {
|
||||
const seeds: string[] = ["4", "3", "2"];
|
||||
const teamsToSeed = [
|
||||
{ id: "1", createdAt: "1639036511550" },
|
||||
@@ -62,11 +66,33 @@ test("Can handle non-existent id in seeds", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("Sorting works with empty arrays", () => {
|
||||
SortTeams("Sorting works with empty arrays", () => {
|
||||
const seeds: string[] = [];
|
||||
const teamsToSeed: any = [];
|
||||
|
||||
assert.equal(teamsToSeed.sort(sortTeamsBySeed(seeds)), []);
|
||||
});
|
||||
|
||||
test.run();
|
||||
CountBracketRounds("Counts bracket (DE - 38)", () => {
|
||||
const bracket = eliminationBracket(38, "DE");
|
||||
const count = countRounds(bracket);
|
||||
|
||||
assert.equal(count, { winners: 7, losers: 9 });
|
||||
});
|
||||
|
||||
CountBracketRounds("Counts bracket (DE - 10)", () => {
|
||||
const bracket = eliminationBracket(10, "DE");
|
||||
const count = countRounds(bracket);
|
||||
|
||||
assert.equal(count, { winners: 5, losers: 5 });
|
||||
});
|
||||
|
||||
CountBracketRounds("Counts bracket (DE - 16)", () => {
|
||||
const bracket = eliminationBracket(16, "DE");
|
||||
const count = countRounds(bracket);
|
||||
|
||||
assert.equal(count, { winners: 5, losers: 6 });
|
||||
});
|
||||
|
||||
SortTeams.run();
|
||||
CountBracketRounds.run();
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
export const checkInHasStarted = (checkInStartTime: string) =>
|
||||
new Date(checkInStartTime) < new Date();
|
||||
import { Bracket } from "./bracket";
|
||||
|
||||
export const sortTeamsBySeed =
|
||||
(seeds: string[]) =>
|
||||
(
|
||||
export function checkInHasStarted(checkInStartTime: string) {
|
||||
return new Date(checkInStartTime) < new Date();
|
||||
}
|
||||
|
||||
export function sortTeamsBySeed(seeds: string[]) {
|
||||
return function (
|
||||
a: { id: string; createdAt: string | Date },
|
||||
b: { id: string; createdAt: string | Date }
|
||||
) => {
|
||||
) {
|
||||
const aSeed = seeds.indexOf(a.id);
|
||||
const bSeed = seeds.indexOf(b.id);
|
||||
|
||||
@@ -24,3 +26,54 @@ export const sortTeamsBySeed =
|
||||
// finally, consider the seeds
|
||||
return aSeed - bSeed;
|
||||
};
|
||||
}
|
||||
|
||||
export function countRounds(bracket: Bracket): {
|
||||
winners: number;
|
||||
losers: number;
|
||||
} {
|
||||
let winners = 1;
|
||||
|
||||
for (let i = bracket.participantsWithByesCount; i > 1; i /= 2) {
|
||||
winners++;
|
||||
}
|
||||
|
||||
const losersMatchIds = new Set(bracket.losers.map((match) => match.id));
|
||||
let losers = 0;
|
||||
let losersMatch = bracket.losers[bracket.losers.length - 1];
|
||||
|
||||
while (true) {
|
||||
losers++;
|
||||
const match1 = losersMatch.match1;
|
||||
const match2 = losersMatch.match2;
|
||||
if (match1 && losersMatchIds.has(match1.id)) {
|
||||
losersMatch = match1;
|
||||
continue;
|
||||
} else if (match2 && losersMatchIds.has(match2.id)) {
|
||||
losersMatch = match2;
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
let matchesWithByes = 0;
|
||||
let matchesWithOpponent = 0;
|
||||
|
||||
for (const match of bracket.winners) {
|
||||
if (!match.upperTeam) break;
|
||||
if (match.upperTeam === "BYE" || match.lowerTeam === "BYE") {
|
||||
matchesWithByes++;
|
||||
continue;
|
||||
}
|
||||
|
||||
matchesWithOpponent++;
|
||||
}
|
||||
|
||||
// First round of losers is not played if certain amount of byes
|
||||
if (matchesWithByes && matchesWithByes >= matchesWithOpponent) {
|
||||
losers--;
|
||||
}
|
||||
|
||||
return { winners, losers };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user