Count rounds algorithm

This commit is contained in:
Kalle (Sendou)
2021-12-13 10:22:31 +02:00
parent f8437e3975
commit a4dec62b61
5 changed files with 124 additions and 28 deletions

View File

@@ -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 {

View File

@@ -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

View File

@@ -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,

View File

@@ -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();

View File

@@ -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 };
}