interface Match { state: string; score?: number[]; result?: string; } import { Utils } from '../../lib/utils'; import type { TournamentPlayer } from './index'; export class RoundRobin { readonly name: string; readonly isDrawingSupported: boolean; readonly isDoubles: boolean; isBracketFrozen: boolean; players: TournamentPlayer[]; matches: (Match | null)[][]; totalPendingMatches: number; perPlayerPendingMatches: number; matchesPerPlayer?: number; constructor(isDoubles: string) { this.name = "Round Robin"; this.isDrawingSupported = true; this.isDoubles = !!isDoubles; this.isBracketFrozen = false; this.players = []; this.matches = []; this.totalPendingMatches = -1; this.perPlayerPendingMatches = -1; if (isDoubles) this.name = "Double " + this.name; } getPendingBracketData(players: TournamentPlayer[]) { return { type: 'table', tableHeaders: { cols: players.slice(0), rows: players.slice(0), }, tableContents: players.map( (p1, row) => players.map((p2, col) => { if (!this.isDoubles && col >= row) return null; if (p1 === p2) return null; return { state: 'unavailable', }; }) ), scores: players.map(player => 0), }; } getBracketData() { const players = this.players; return { type: 'table', tableHeaders: { cols: players.slice(0), rows: players.slice(0), }, tableContents: players.map( (p1, row) => players.map((p2, col) => { if (!this.isDoubles && col >= row) return null; if (p1 === p2) return null; const match = this.matches[row][col]; if (!match) return null; const cell: any = { state: match.state, }; if (match.state === 'finished' && match.score) { cell.result = match.result; cell.score = match.score.slice(0); } return cell; }) ), scores: players.map(player => player.score), }; } freezeBracket(players: TournamentPlayer[]) { this.players = players; this.isBracketFrozen = true; this.matches = players.map( (p1, row) => players.map((p2, col) => { if (!this.isDoubles && col >= row) return null; if (p1 === p2) return null; return { state: 'available' }; }) ); this.matchesPerPlayer = players.length - 1; // total matches = total players * matches per player / players per match // alternatively: the (playercount)th triangular number this.totalPendingMatches = players.length * this.matchesPerPlayer / 2; if (this.isDoubles) { this.totalPendingMatches *= 2; this.matchesPerPlayer *= 2; } } disqualifyUser(user: TournamentPlayer) { if (!this.isBracketFrozen) return 'BracketNotFrozen'; const playerIndex = this.players.indexOf(user); for (const [col, match] of this.matches[playerIndex].entries()) { if (!match || match.state !== 'available') continue; const p2 = this.players[col]; match.state = 'finished'; match.result = 'loss'; match.score = [0, 1]; p2.score += 1; p2.games += 1; this.totalPendingMatches--; if (this.matchesPerPlayer && p2.games === this.matchesPerPlayer) { p2.sendRoom(`|tournament|update|{"isJoined":false}`); p2.game.updatePlayer(p2, null); } } for (const [row, challenges] of this.matches.entries()) { const match = challenges[playerIndex]; if (!match || match.state !== 'available') continue; const p1 = this.players[row]; match.state = 'finished'; match.result = 'win'; match.score = [1, 0]; p1.score += 1; p1.games += 1; this.totalPendingMatches--; if (this.matchesPerPlayer && p1.games === this.matchesPerPlayer) { p1.sendRoom(`|tournament|update|{"isJoined":false}`); p1.game.updatePlayer(p1, null); } } user.game.updatePlayer(user, null); } getAvailableMatches() { if (!this.isBracketFrozen) return 'BracketNotFrozen'; const matches: [TournamentPlayer, TournamentPlayer][] = []; for (const [row, challenges] of this.matches.entries()) { const p1 = this.players[row]; for (const [col, match] of challenges.entries()) { const p2 = this.players[col]; if (!match) continue; if (match.state === 'available' && !p1.isBusy && !p2.isBusy) { matches.push([p1, p2]); } } } return matches; } setMatchResult([p1, p2]: [TournamentPlayer, TournamentPlayer], result: string, score: number[]) { if (!this.isBracketFrozen) return 'BracketNotFrozen'; if (!['win', 'loss', 'draw'].includes(result)) return 'InvalidMatchResult'; const row = this.players.indexOf(p1); const col = this.players.indexOf(p2); if (row < 0 || col < 0) return 'UserNotAdded'; const match = this.matches[row][col]; if (!match || match.state !== 'available') return 'InvalidMatch'; match.state = 'finished'; match.result = result; match.score = score.slice(0); this.totalPendingMatches--; if (this.matchesPerPlayer) { if (p1.games === this.matchesPerPlayer) { p1.sendRoom(`|tournament|update|{"isJoined":false}`); p1.game.updatePlayer(p1, null); } if (p2.games === this.matchesPerPlayer) { p2.sendRoom(`|tournament|update|{"isJoined":false}`); p2.game.updatePlayer(p2, null); } } } isTournamentEnded() { return this.isBracketFrozen && this.totalPendingMatches === 0; } getResults() { if (!this.isTournamentEnded()) return 'TournamentNotEnded'; const sortedScores = Utils.sortBy([...this.players], p => -p.score); const results: TournamentPlayer[][] = []; let currentScore = sortedScores[0].score; let currentRank: TournamentPlayer[] = []; results.push(currentRank); for (const player of sortedScores) { if (player.score < currentScore) { currentScore = player.score; currentRank = []; results.push(currentRank); } currentRank.push(player); } return results; } }