Skip to content

Commit

Permalink
support ref settings
Browse files Browse the repository at this point in the history
  • Loading branch information
craigkai committed Jan 24, 2024
1 parent 242aebc commit 4f3542d
Show file tree
Hide file tree
Showing 11 changed files with 162 additions and 91 deletions.
18 changes: 2 additions & 16 deletions src/app.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,10 @@ declare global {
type MatchRow = z.infer<typeof matchesRowSchema> & {
matches_team1_fkey: { name: string };
matches_team2_fkey: { name: string };
matches_ref_fkey: { name: string };
};

interface Match {
round: number;
match: number;
player1: string | number | null;
player2: string | number | null;
win?: {
round: number;
match: number;
};
loss?: {
round: number;
match: number;
};
}

type UserMatch = Partial<Match> & {
type UserMatch = Partial<MatchRow> & {
court: number;
round: number;
event_id: number;
Expand Down
24 changes: 11 additions & 13 deletions src/app.html
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/vball.svg" />
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
<title>Volleyman(ager)</title>
</head>

<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/vball.svg" />
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
<title>Volleyman(ager)</title>
</head>

<body data-sveltekit-preload-data="hover" class="bg-white dark:bg-nord-0">
<div style="display: contents">%sveltekit.body%</div>
</body>

</html>
<body data-sveltekit-preload-data="hover" class="bg-white dark:bg-nord-0">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
8 changes: 8 additions & 0 deletions src/lib/components/tournament/Matches.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
async function checkGenerateMatches() {
if (($matches?.matches?.length ?? 0) > 0) {
showGenerateMatchesAlert = true;
} else {
generateMatches();
}
}
Expand Down Expand Up @@ -68,6 +70,9 @@
<TableHead>
{#each Array(tournament.courts) as _, i}
<TableHeadCell>Court {i + 1}</TableHeadCell>
{#if tournament.refs === 'provided'}
<TableHeadCell>Ref</TableHeadCell>
{/if}
{/each}
</TableHead>
<TableBody>
Expand Down Expand Up @@ -104,6 +109,9 @@
<ViewMatch {matches} {match} {readOnly} />
</div>
</TableBodyCell>
{#if tournament.refs === 'teams'}
<TableBodyCell>{match?.matches_ref_fkey?.name}</TableBodyCell>
{/if}
{/each}
</TableBodyRow>
{/each}
Expand Down
4 changes: 2 additions & 2 deletions src/lib/components/tournament/View.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@
<TableBodyRow>
<TableBodyCell>{match.round}</TableBodyCell>
<TableBodyCell>tbd</TableBodyCell>
<TableBodyCell>{match.player1}</TableBodyCell>
<TableBodyCell>{match.player2}</TableBodyCell>
<TableBodyCell>{match.team1}</TableBodyCell>
<TableBodyCell>{match.team2}</TableBodyCell>
</TableBodyRow>
{/each}
</TableBody>
Expand Down
9 changes: 8 additions & 1 deletion src/lib/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,14 @@ export class Event {
* @throws {Error} - Throws an error if the event data does not have all required values.
*/
async create(input: Event): Promise<Event> {
if (!input.name || !input.date || !input.pools || !input.courts || !input.scoring || !input.refs) {
if (
!input.name ||
!input.date ||
!input.pools ||
!input.courts ||
!input.scoring ||
!input.refs
) {
error(400, `Tournament create call does not have all required values`);
}
const currentUser = await this.databaseService.getCurrentUser();
Expand Down
74 changes: 67 additions & 7 deletions src/lib/matches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,18 @@ export class Matches implements Writable<Matches> {
}

async create(
{ pools, courts }: Partial<EventRow>,
{ pools, courts, refs }: Partial<EventRow>,
teams: TeamRow[]
): Promise<Matches | undefined> {
if (!teams || teams.length === 0) {
console.error("Can't generate matches without Teams");
error(500, Error("Can't generate matches without Teams"));
}

if (teams.length <= 2 && refs === 'teams') {
throw new Error('Cannot have refs with less than 3 teams');
}

if (!pools || pools <= 0) {
console.error("Can't generate matches without Pools");
error(500, Error("Can't generate matches without Pools"));
Expand All @@ -103,11 +107,11 @@ export class Matches implements Writable<Matches> {
}

try {
let matches: Match[] = [];
let matches: Partial<MatchRow>[] = [];
// If we have more pool play games than matches we got
// back, then we need to generate some more.
while (matches.length < pools * teams.length) {
matches = matches.concat(RoundRobin(teams));
matches = matches.concat(RoundRobin(teams.map((t) => t.id)));
}
// Delete all old matches as they are now invalid
await this.databaseService.deleteMatchesByEvent(this.event_id);
Expand All @@ -118,7 +122,14 @@ export class Matches implements Writable<Matches> {

let totalRounds = 0;
const userMatches: UserMatch[] = [];
matches.forEach((match: Match) => {

const teamsPerRound: { number: number[] } = { 0: [] };
matches.forEach((match: Partial<MatchRow>) => {
if (match.team1 === 0 || match.team2 === 0) {
// bye
return;
}

// Short circuit if we have more matches than pool play games
// (you don't play every team).
if (pools && userMatches.length === pools * (teams.length / 2)) {
Expand All @@ -132,22 +143,42 @@ export class Matches implements Writable<Matches> {
totalRounds = totalRounds + 1;
}

teamsPerRound[round] = teamsPerRound[round]
? teamsPerRound[round].concat(match.team1, match.team2)
: [match.team1, match.team2];

match.court = courts - courtsAvailable;
match.round = round;

courtsAvailable = courtsAvailable - 1;
if (teamsAvailable >= 2) {
userMatches.push({
event_id: this.event_id,
team1: match?.player1?.id,
team2: match?.player2?.id,
team1: match.team1 as number,
team2: match.team2 as number,
court: match.court,
round: match.round
round: match.round,
ref: match.ref
});
}
teamsAvailable = teamsAvailable - 2;
});

if (refs === 'teams') {
Object.keys(teamsPerRound).forEach((round: string) => {
userMatches.forEach((match: UserMatch) => {
if (match.round === Number(round)) {
const ref = this.determineReferee(
teamsPerRound[round],
teams.map((t) => t.id),
userMatches
);
match.ref = ref;
}
});
});
}

// Call multi insert:
const res = await this.databaseService.insertMatches(userMatches);
if (res) {
Expand All @@ -171,6 +202,35 @@ export class Matches implements Writable<Matches> {
}
return updatedMatch;
}

determineReferee(
teamsPerRound: [{ number: number[] }],
teams: number[],
previousMatches: Partial<MatchRow>[]
): number {
// Exclude teams playing in the current round from the referee selection
const availableTeams = teams.filter((team) => !teamsPerRound.includes(team) && team !== 0);

// Exclude teams that have already refereed in previous matches
const teamsWithPreviousReferee = new Set(previousMatches.map((match) => match.ref));
const availableTeamsByRefsCount: { [key: number]: number } = availableTeams.reduce(
(acc, team) => {
if (teamsWithPreviousReferee.has(team)) {
acc[team] = acc[team] ? acc[team] + 1 : 1;
} else {
acc[team] = 0;
}
return acc;
},
{}
);
const availableTeamsOrdered = Object.keys(availableTeamsByRefsCount).sort(
(a, b) => availableTeamsByRefsCount[b] - availableTeamsByRefsCount[a]
);

// Choose a referee from the remaining available teams
return Number(availableTeamsOrdered[0]);
}
}

if (import.meta.vitest) {
Expand Down
62 changes: 33 additions & 29 deletions src/lib/roundRobin.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,63 @@
import { shuffle } from './shuffle';

export function RoundRobin(
players: TeamRow[],
players: number[],
startingRound: number = 1,
ordered: boolean = false
): Match[] {
let matches: Match[] = [];
let playerArray: TeamRow[] = [];
): Partial<MatchRow>[] {
let matches: Partial<MatchRow>[] = [];
let teamArray: number[] = [];

if (Array.isArray(players)) {
playerArray = ordered ? players : shuffle(players);
teamArray = ordered ? players : shuffle(players);
} else {
playerArray = [...new Array(players)].map((_, i) => i + 1);
teamArray = [...new Array(players)].map((_, i) => i + 1);
}
if (playerArray.length % 2 === 1) {
playerArray.push(null);

if (teamArray.length % 2 === 1) {
teamArray.push(0); // Add a dummy team
}
for (let r = startingRound; r < startingRound + playerArray.length - 1; r++) {
const round = [];
for (let i = 0; i < playerArray.length / 2; i++) {
round.push({
round: r,
match: i + 1,
player1: null,
player2: null
});

for (let r = startingRound; r < startingRound + teamArray.length - 1; r++) {
const round: Partial<MatchRow>[] = [];
for (let i = 0; i < teamArray.length / 2; i++) {
const match: Partial<MatchRow> = {
round: r
};

round.push(match);
}

if (r === startingRound) {
round.forEach((m, i) => {
m.player1 = playerArray[i];
m.player2 = playerArray[playerArray.length - i - 1];
m.team1 = teamArray[i];
m.team2 = teamArray[teamArray.length - i - 1];
});
} else {
const prevRound = matches.filter((m) => m.round === r - 1);
const indexFind = (idx: number) => {
if (idx + playerArray.length / 2 > playerArray.length - 2) {
return idx + 1 - playerArray.length / 2;
if (idx + teamArray.length / 2 > teamArray.length - 2) {
return idx + 1 - teamArray.length / 2;
} else {
return idx + playerArray.length / 2;
return idx + teamArray.length / 2;
}
};

for (let i = 0; i < round.length; i++) {
const prev = prevRound[i];
const curr = round[i];

if (i === 0) {
if (prev.player2 === playerArray[playerArray.length - 1]) {
curr.player1 = playerArray[playerArray.length - 1];
curr.player2 = playerArray[indexFind(playerArray.findIndex((p) => p === prev.player1))];
if (prev.team2 === teamArray[teamArray.length - 1]) {
curr.team1 = teamArray[teamArray.length - 1];
curr.team2 = teamArray[indexFind(teamArray.findIndex((p) => p === prev.team1))];
} else {
curr.player2 = playerArray[playerArray.length - 1];
curr.player1 = playerArray[indexFind(playerArray.findIndex((p) => p === prev.player2))];
curr.team2 = teamArray[teamArray.length - 1];
curr.team1 = teamArray[indexFind(teamArray.findIndex((p) => p === prev.team2))];
}
} else {
curr.player1 = playerArray[indexFind(playerArray.findIndex((p) => p === prev.player1))];
curr.player2 = playerArray[indexFind(playerArray.findIndex((p) => p === prev.player2))];
curr.team1 = teamArray[indexFind(teamArray.findIndex((p) => p === prev.team1))];
curr.team2 = teamArray[indexFind(teamArray.findIndex((p) => p === prev.team2))];
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/lib/shuffle.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export function shuffle(arr: TeamRow[]): TeamRow[] {
export function shuffle(arr: number[]): number[] {
const a = [...arr];
for (let i = a.length - 1; i > 0; i--) {
const z = Math.floor(Math.random() * (i + 1));
Expand Down
Loading

1 comment on commit 4f3542d

@vercel
Copy link

@vercel vercel bot commented on 4f3542d Jan 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

volleyman – ./

volleyman-git-main-craigkai.vercel.app
volleyman-craigkai.vercel.app
volleyman.vercel.app

Please sign in to comment.