Spaces:
Running
Running
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; | |
} | |
} | |