-
-
Save RoyallyFlushed/d7bc404d44a4957399dbbd21feca3556 to your computer and use it in GitHub Desktop.
Tournament Service code for Man City Blue Moon
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ | |
TITLE: Tournament Service Server Module | |
DESC: This module script is responsible for implementing | |
the entire Tournament logic system, such that a | |
tournament can be created and ran in any game, with | |
little to no dependencies. | |
This system should function properly and in accordance | |
with expected outcomes for a tournament system. | |
AUTHOR: Reid @RoyallyFlushed | |
CREATED: 09/01/2024 | |
MODIFIED: 19/03/2024 | |
--]] | |
--// Constants | |
local LOG_PREFIX = "$ TournamentService.%s -> %s" -- The log prefix to use for all logging | |
--// Services | |
local Players = game:GetService("Players") | |
local ReplicatedStorage = game:GetService("ReplicatedStorage") | |
--// Modules | |
local Types = require(script.Types) | |
local TreeUtil = require(ReplicatedStorage.Game.Miscellaneous.Tree) | |
local BinaryTree, Node = TreeUtil.BinaryTree, TreeUtil.Node | |
local Utility = require(ReplicatedStorage.Game.Miscellaneous.Utility) | |
local signature = Utility.signature | |
--// Extracted Constants, Enums, Types | |
local MAX_TEAMS = Types.Constants.MAX_TEAMS | |
local MIN_TEAMS = Types.Constants.MIN_TEAMS | |
local PLAYERS_PER_TEAM = Types.Constants.PLAYERS_PER_TEAM | |
local BRACKET_STATES = Types.Enums.BRACKET_STATES | |
local TOURNAMENT_STATES = Types.Enums.TOURNAMENT_STATES | |
type Array<T> = Utility.Array<T> | |
type Response = Utility.Response | |
type Bracket = Types.Bracket | |
type BracketSpaces = Types.BracketSpaces | |
type Round = Types.Round | |
type Team = Types.Team | |
type Tournament = Types.Tournament | |
local Packages = ReplicatedStorage.Packages | |
local Knit = require(Packages.Knit) | |
local Signal = require(Packages.Signal) | |
local TournamentService = Knit.CreateService { | |
Name = "TournamentService", | |
--// Server Service level signals | |
TournamentCreated = Signal.new(), | |
TournamentStarted = Signal.new(), | |
TournamentReset = Signal.new(), | |
--// Server Service stuff | |
Tournament = {} :: Tournament | |
} | |
function newBracket(teamA: Team?, teamB: Team?, roundID: number?, bracketID: number?): Bracket | |
return { | |
NodeID = -1, | |
RoundID = roundID or -1, | |
BracketID = bracketID or -1, | |
TeamA = teamA or {}, | |
TeamB = teamB or {}, | |
MatchData = nil, | |
State = BRACKET_STATES.NotStarted, | |
TimeStarted = -1, | |
TimeFinished = -1 | |
} :: Bracket | |
end | |
function getClosestHighestPower(base: number, target: number) | |
for i = 1, 20 do | |
if base ^ i >= target then | |
return base ^ i | |
end | |
end | |
end | |
function calculateTreeWithByes(numTeams: number): { Byes: number, Rounds: Array<number> } | |
local closestHighestPowerOf2 = getClosestHighestPower(2, numTeams) | |
local byes = closestHighestPowerOf2 - numTeams | |
local matchesInFirstRound = (numTeams - byes) / 2 | |
local nodesInRound = { matchesInFirstRound } | |
local current = matchesInFirstRound + byes | |
local round = 2 | |
while current > 1 do | |
current /= 2 | |
nodesInRound[round] = current | |
round += 1 | |
end | |
return { | |
Byes = byes, | |
Rounds = nodesInRound | |
} | |
end | |
function buildTournamentTree(teams: Array<Team>): TreeUtil.BinaryTree | |
assert(#teams >= MIN_TEAMS, signature(LOG_PREFIX, "Attempt to create a tree with less players than should be allowed!")) | |
assert(#teams <= MAX_TEAMS, signature(LOG_PREFIX, "Attempt to create a tree with more players than should be allowed!")) | |
type TeamNode = { | |
Team: Team, | |
Bye: boolean, | |
} | |
local tree = BinaryTree.new(Node.new(0, 0, newBracket())) | |
local treeData: { Byes: number, Rounds: Array<number> } = calculateTreeWithByes(#teams) | |
local teamData: Array<TeamNode> = {} | |
-- Set the meta values for the bracket | |
tree[0].Value.RoundID = #treeData.Rounds | |
tree[0].Value.BracketID = 1 | |
tree[0].Value.NodeID = 0 | |
-- Create team node objects | |
for teamIndex = 1, #teams do | |
teamData[teamIndex] = { | |
Team = teams[teamIndex], | |
Bye = false | |
} :: TeamNode | |
end | |
-- If we need to bye, then calculate byes, spread evenly across the array of teams | |
if treeData.Byes > 0 then | |
local teamsToBye = {} | |
-- CODE TO EVENLY DISTRIBUTE BYE'D PLAYERS - UNUSED | |
-- local midpoint = math.ceil(#teamData / 2) -- Cut the team data array in half, favouring lower as the bigger set | |
-- local byesInLower = math.ceil(treeData.Byes / 2) -- Divide the number of byes in each half, favouring the bigger set | |
-- local byesInUpper = math.floor(treeData.Byes / 2) -- Divide the rest of the byes up to the last set, but floor because we rounded above | |
-- for _ = 1, byesInLower do | |
-- local chosenIndex | |
-- repeat | |
-- chosenIndex = math.random(1, midpoint) | |
-- until not table.find(teamsToBye, chosenIndex) | |
-- table.insert(teamsToBye, chosenIndex) | |
-- teamData[chosenIndex].Bye = true | |
-- end | |
-- for _ = 1, byesInUpper do | |
-- local chosenIndex | |
-- repeat | |
-- chosenIndex = math.random(midpoint + 1, #teamData) | |
-- until not table.find(teamsToBye, chosenIndex) | |
-- table.insert(teamsToBye, chosenIndex) | |
-- teamData[chosenIndex].Bye = true | |
-- end | |
for byeIndex = 1, treeData.Byes do | |
teamData[#teamData - byeIndex + 1].Bye = true | |
end | |
end | |
-- Populate the tree with the required number of nodes | |
for roundIndex = #treeData.Rounds - 1, 1, -1 do | |
local numBrackets = treeData.Rounds[roundIndex] | |
for bracketIndex = 1, numBrackets do | |
local insertedNode = tree:BreadthFirstInsert(Node.new(0, 0, newBracket(nil, nil, roundIndex, bracketIndex))) | |
insertedNode.Value.NodeID = insertedNode.Index | |
end | |
end | |
local depthOfTree = tree:GetDepth() | |
local firstRoundNodes = tree:GetNodesAtLevel(depthOfTree) | |
local secondRoundNodes = tree:GetNodesAtLevel(depthOfTree - 1) | |
-- Define a function to try and add the given team to the bracket of the given node | |
local function addTeamToBracketOfNode(node: TreeUtil.Node, field: string, team: Team): boolean | |
assert(typeof(node.Value) == "table", signature(LOG_PREFIX, "Attempt to add player to node that isn't a bracket!")) | |
if typeof(node.Value[field]) == "table" and #node.Value[field] >= 1 then | |
return false | |
end | |
node.Value[field] = team | |
return true | |
end | |
-- Connect up team nodes to the end nodes | |
for _, team: TeamNode in teamData do | |
if team.Bye then | |
for i, node in secondRoundNodes do | |
local leftNode: TreeUtil.Node? = tree:GetLeftNode(node) | |
local rightNode: TreeUtil.Node? = tree:GetRightNode(node) | |
if not leftNode and not rightNode then | |
local success = addTeamToBracketOfNode(node, "TeamA", team.Team) | |
if success then | |
break | |
else | |
success = addTeamToBracketOfNode(node, "TeamB", team.Team) | |
table.remove(secondRoundNodes, i) | |
if success then | |
break | |
end | |
end | |
elseif not leftNode and rightNode then | |
local success = addTeamToBracketOfNode(node, "TeamA", team.Team) | |
table.remove(secondRoundNodes, i) | |
if success then | |
break | |
end | |
elseif leftNode and not rightNode then | |
local success = addTeamToBracketOfNode(node, "TeamB", team.Team) | |
table.remove(secondRoundNodes, i) | |
if success then | |
break | |
end | |
end | |
end | |
else | |
for i, node in firstRoundNodes do | |
local success = addTeamToBracketOfNode(node, "TeamA", team.Team) | |
if success then | |
break | |
else | |
success = addTeamToBracketOfNode(node, "TeamB", team.Team) | |
table.remove(firstRoundNodes, i) | |
if success then | |
break | |
end | |
end | |
end | |
end | |
end | |
-- Return back the newly created tree | |
return tree | |
end | |
function calculateEvenTree(numTeams: number): { Rounds: Array<number> } | |
local closestHighestPowerOf2 = getClosestHighestPower(2, numTeams) | |
local nodesInFirstRound = closestHighestPowerOf2 / 2 | |
local nodesInRound = { nodesInFirstRound } | |
local current = nodesInFirstRound | |
local round = 2 | |
while current > 1 do | |
current /= 2 | |
nodesInRound[round] = current | |
round += 1 | |
end | |
return { | |
Rounds = nodesInRound | |
} | |
end | |
function buildEvenTournamentTree(teams: Array<Team>) | |
assert(#teams >= MIN_TEAMS, signature(LOG_PREFIX, "Attempt to create a tree with less players than should be allowed!")) | |
assert(#teams <= MAX_TEAMS, signature(LOG_PREFIX, "Attempt to create a tree with more players than should be allowed!")) | |
local tree = BinaryTree.new(Node.new(0, 0, newBracket())) | |
local treeData: { Rounds: Array<number> } = calculateEvenTree(#teams) | |
-- Set the meta values for the bracket | |
tree[0].Value.RoundID = #treeData.Rounds | |
tree[0].Value.BracketID = 1 | |
tree[0].Value.NodeID = 0 | |
-- Populate the tree with the required number of nodes | |
for roundIndex = #treeData.Rounds - 1, 1, -1 do | |
local numBrackets = treeData.Rounds[roundIndex] | |
for bracketIndex = 1, numBrackets do | |
local insertedNode = tree:BreadthFirstInsert(Node.new(0, 0, newBracket(nil, nil, roundIndex, bracketIndex))) | |
insertedNode.Value.NodeID = insertedNode.Index | |
end | |
end | |
local depthOfTree = tree:GetDepth() | |
local firstRoundNodes = tree:GetNodesAtLevel(depthOfTree) | |
-- Define a function to try and add the given team to the bracket of the given node | |
local function addTeamToBracketOfNode(node: TreeUtil.Node, field: string, team: Team): boolean | |
assert(typeof(node.Value) == "table", signature(LOG_PREFIX, "Attempt to add player to node that isn't a bracket!")) | |
if typeof(node.Value[field]) == "table" and #node.Value[field] >= 1 then | |
return false | |
end | |
node.Value[field] = team | |
return true | |
end | |
local addedTeams: Array<Player> = {} | |
-- Try to add the teams to all the first round nodes | |
for _, team: Array<Player> in teams do | |
for i, node in firstRoundNodes do | |
local success = addTeamToBracketOfNode(node, "TeamA", team) | |
if success then | |
table.insert(addedTeams, team) | |
break | |
end | |
end | |
end | |
-- Try to add whatever teams are left to all the first round nodes | |
for _, team: Array<Player> in teams do | |
if not table.find(addedTeams, team) then | |
for i, node in firstRoundNodes do | |
local success = addTeamToBracketOfNode(node, "TeamB", team) | |
if success then | |
table.insert(addedTeams, team) | |
break | |
end | |
end | |
end | |
end | |
-- Return back the newly created tree | |
return tree | |
end | |
--[[ | |
//////////////////////////////////////// | |
|| INTERNAL API || | |
//////////////////////////////////////// | |
--]] | |
function isPlayerInBracket(bracket: Bracket, player: Player): boolean | |
return table.find(bracket.TeamA, player) ~= nil or table.find(bracket.TeamB, player) ~= nil | |
end | |
function resetTournament(self) | |
self.Tournament = { | |
State = TOURNAMENT_STATES.NotStarted, | |
TournamentTree = nil, | |
ConsolationBracket = nil, | |
Winner = nil, | |
CurrentRound = 1, | |
TotalRounds = 0, | |
SpacesAvailable = 0, | |
TimeStarted = 0, | |
TimeFinished = 0, | |
PlayerCache = {} | |
} :: Tournament | |
self.TournamentReset:Fire() | |
end | |
function getBracketsInRound(self, roundNum: number): Round | |
local treeDepth = self.Tournament.TournamentTree:GetDepth() | |
local level = treeDepth + 1 - roundNum | |
local nodesAtLevel: Round = self.Tournament.TournamentTree:GetNodesAtLevel(level) | |
local brackets: Round = {} | |
for _, node: TreeUtil.Node in nodesAtLevel do | |
table.insert(brackets, node.Value) | |
end | |
if level == 0 then | |
table.insert(brackets, self.Tournament.ConsolationBracket) | |
end | |
return brackets | |
end | |
function getBracketInRoundFromPlayer(self, player: Player, roundNum: number): Bracket? | |
for _, bracket: Bracket in getBracketsInRound(self, roundNum) do | |
if isPlayerInBracket(bracket, player) then | |
return bracket | |
end | |
end | |
end | |
function getBracketData(self, player: Player): Array<Bracket>? | |
local result = {} | |
for roundNum = 1, self.Tournament.TotalRounds do | |
local bracket: Bracket? = getBracketInRoundFromPlayer(self, player, roundNum) | |
if bracket then | |
table.insert(result, bracket) | |
end | |
end | |
return if #result > 0 then result else nil | |
end | |
function getBracket(self, roundNum: number, bracketID: number): Bracket? | |
local round: Round = getBracketsInRound(self, roundNum) | |
local targetBracket | |
for _, bracket: Bracket in round do | |
if bracket.BracketID == bracketID then | |
targetBracket = bracket | |
break | |
end | |
end | |
return targetBracket | |
end | |
function getWinnersForRound(self, roundNum: number): Array<Team>? | |
local round: Round = getBracketsInRound(self, roundNum) | |
local winners = {} | |
-- Loop through the brackets in the round, and add the players to the winners table | |
for _, bracket: Bracket in round do | |
if bracket.State == BRACKET_STATES.WinTeamA then | |
table.insert(winners, bracket.TeamA) | |
elseif bracket.State == BRACKET_STATES.WinTeamB then | |
table.insert(winners, bracket.TeamB) | |
end | |
end | |
return winners | |
end | |
function getWinnersForTournament(self): Array<Team>? | |
local round: Round = getBracketsInRound(self, self.Tournament.TotalRounds) | |
-- The final round should only contain the final bracket & the consolation bracket | |
if #round > 2 then | |
error(signature(LOG_PREFIX, "Unexpected number of brackets in final round!")) | |
end | |
local finalBracket: Bracket = round[1] | |
local consolationBracket: Bracket = round[2] | |
local result: Array<Team> = {} | |
-- Add the teams to their respective place in the result array based on who won and who lost the final bracket | |
if finalBracket.State == BRACKET_STATES.WinTeamA then | |
result[1] = finalBracket.TeamA | |
result[2] = finalBracket.TeamB | |
elseif finalBracket.State == BRACKET_STATES.WinTeamB then | |
result[1] = finalBracket.TeamB | |
result[2] = finalBracket.TeamA | |
end | |
-- Add the winner of the consolation bracket to the result as the 3rd place winner | |
if consolationBracket then | |
if consolationBracket.State == BRACKET_STATES.WinTeamA then | |
result[3] = consolationBracket.TeamA | |
elseif consolationBracket.State == BRACKET_STATES.WinTeamB then | |
result[3] = consolationBracket.TeamB | |
end | |
end | |
return result | |
end | |
function advanceBracketToNextRound(self, oldBracket: Bracket): boolean | |
if oldBracket.RoundID + 1 > self.Tournament.TotalRounds then | |
warn(signature(LOG_PREFIX, "Attempt to advance bracket to out-of-bounds round!")) | |
return false | |
end | |
local bracket: Bracket = getNextBracket(self, oldBracket) | |
if not bracket then | |
error(signature(LOG_PREFIX, "Attempt to index round with out-of-bounds index!")) | |
return false | |
end | |
local winner, loser | |
-- Figure out who the winner of the old bracket was | |
if oldBracket.State == BRACKET_STATES.Draw then | |
winner = nil | |
loser = nil | |
elseif oldBracket.State == BRACKET_STATES.WinTeamA then | |
winner = oldBracket.TeamA | |
loser = oldBracket.TeamB | |
elseif oldBracket.State == BRACKET_STATES.WinTeamB then | |
winner = oldBracket.TeamB | |
loser = oldBracket.TeamA | |
end | |
local isLeftNode: boolean = self.Tournament.TournamentTree:GetLeftIndex(bracket.NodeID) == oldBracket.NodeID | |
-- If we have no winner, then exit | |
if not winner then | |
return | |
end | |
-- Figure out which winner should be on which side | |
if isLeftNode then | |
table.move(winner, 1, #winner, #bracket.TeamA + 1, bracket.TeamA) | |
else | |
table.move(winner, 1, #winner, #bracket.TeamB + 1, bracket.TeamB) | |
end | |
-- If the next bracket isn't the last bracket, then exit, otherwise try to set the consolation bracket up | |
if bracket.RoundID + 1 <= self.Tournament.TotalRounds then | |
return | |
end | |
local consolationBracket = self.Tournament.ConsolationBracket | |
if isLeftNode then | |
table.move(loser, 1, #loser, #consolationBracket.TeamA + 1, consolationBracket.TeamA) | |
else | |
table.move(loser, 1, #loser, #consolationBracket.TeamB + 1, consolationBracket.TeamB) | |
end | |
end | |
function getNextBracket(self, oldBracket: Bracket): Bracket | |
local currentNode: TreeUtil.Node = self.Tournament.TournamentTree[oldBracket.NodeID] | |
local nextNode: TreeUtil.Node? = self.Tournament.TournamentTree[currentNode.Parent] | |
if not nextNode then | |
error(signature(LOG_PREFIX, "Attempt to get nil node from bracket ID!")) | |
end | |
return nextNode.Value | |
end | |
function addPlayerToBracket(bracket: Bracket, player: Player, field: string?): boolean | |
local fields = {} | |
local added = false | |
if field then | |
fields[1] = field | |
else | |
fields[1] = "TeamA" | |
fields[2] = "TeamB" | |
end | |
for i = 1, #fields do | |
if not bracket[fields[i]] then | |
bracket[fields[i]] = { player } | |
added = true | |
elseif #bracket[fields[i]] < PLAYERS_PER_TEAM then | |
table.insert(bracket[fields[i]], player) | |
added = true | |
end | |
if added then | |
break | |
end | |
end | |
return added | |
end | |
function getTotalSpacesAvailable(self): number | |
local openBrackets: Array<BracketSpaces> = getOpenBrackets(self) | |
local totalSpaces = 0 | |
for _, bracketData: BracketSpaces in openBrackets do | |
totalSpaces += bracketData.SpacesAvailable | |
end | |
return totalSpaces | |
end | |
function getOpenBrackets(self): Array<BracketSpaces> | |
local tree: TreeUtil.BinaryTree = self.Tournament.TournamentTree | |
local rounds: Array<Round> = {} | |
local openBrackets: Array<BracketSpaces> = {} | |
rounds[1] = getBracketsInRound(self, self.Tournament.CurrentRound) | |
rounds[2] = getBracketsInRound(self, self.Tournament.CurrentRound + 1) | |
for pass = 1, 2 do | |
local ignoreSemiFull = pass == 1 | |
for _, round: Round in rounds do | |
for _, bracket: Bracket in round do | |
if bracket.State ~= BRACKET_STATES.NotStarted then | |
continue | |
end | |
local leftIndex = tree:GetLeftIndex(bracket.NodeID) | |
local rightIndex = tree:GetRightIndex(bracket.NodeID) | |
local leftNode: TreeUtil.Node? = tree[leftIndex] | |
local rightNode: TreeUtil.Node? = tree[rightIndex] | |
-- Case #1: Bracket has a left child | |
if leftNode and not ignoreSemiFull then | |
local leftChildBracket: Bracket = leftNode.Value | |
local teamASize = leftChildBracket.TeamA and #leftChildBracket.TeamA or 0 | |
local teamBSize = leftChildBracket.TeamB and #leftChildBracket.TeamB or 0 | |
local currentBracketTeamASize = #bracket.TeamA | |
if leftChildBracket.State == BRACKET_STATES.InProgress then | |
local maxWinnerTeamSize = math.max(teamASize, teamBSize) | |
local totalOccupiedSize = maxWinnerTeamSize + currentBracketTeamASize | |
if totalOccupiedSize < PLAYERS_PER_TEAM then | |
table.insert(openBrackets, { | |
Bracket = bracket, | |
Team = "TeamA", | |
SpacesAvailable = PLAYERS_PER_TEAM - totalOccupiedSize | |
}) | |
end | |
elseif leftChildBracket.State == BRACKET_STATES.WinTeamA | |
or leftChildBracket.State == BRACKET_STATES.WinTeamB | |
or leftChildBracket.State == BRACKET_STATES.Draw | |
then | |
if #bracket.TeamA < PLAYERS_PER_TEAM then | |
table.insert(openBrackets, { | |
Bracket = bracket, | |
Team = "TeamA", | |
SpacesAvailable = PLAYERS_PER_TEAM - #bracket.TeamA | |
}) | |
end | |
end | |
elseif not leftNode then | |
-- Case #2: Bracket doesn't have left child | |
local teamASize = bracket.TeamA and #bracket.TeamA or 0 | |
local isTeamEmpty = teamASize == 0 | |
-- 2.A: First round checks to find empty team, second round adds if there is space | |
if ignoreSemiFull and isTeamEmpty or not ignoreSemiFull then | |
if teamASize < PLAYERS_PER_TEAM then | |
table.insert(openBrackets, { | |
Bracket = bracket, | |
Team = "TeamA", | |
SpacesAvailable = PLAYERS_PER_TEAM - teamASize | |
}) | |
end | |
end | |
end | |
-- Case #3: Bracket has right child | |
if rightNode and not ignoreSemiFull then | |
local rightChildBracket: Bracket = rightNode.Value | |
local teamASize = rightChildBracket.TeamA and #rightChildBracket.TeamA or 0 | |
local teamBSize = rightChildBracket.TeamB and #rightChildBracket.TeamB or 0 | |
local currentBracketTeamBSize = #bracket.TeamB | |
if rightChildBracket.State == BRACKET_STATES.InProgress then | |
local maxWinnerTeamSize = math.max(teamASize, teamBSize) | |
local totalOccupiedSize = maxWinnerTeamSize + currentBracketTeamBSize | |
if totalOccupiedSize < PLAYERS_PER_TEAM then | |
table.insert(openBrackets, { | |
Bracket = bracket, | |
Team = "TeamB", | |
SpacesAvailable = PLAYERS_PER_TEAM - totalOccupiedSize | |
}) | |
end | |
elseif rightChildBracket.State == BRACKET_STATES.WinTeamA | |
or rightChildBracket.State == BRACKET_STATES.WinTeamB | |
or rightChildBracket.State == BRACKET_STATES.Draw | |
then | |
if #bracket.TeamB < PLAYERS_PER_TEAM then | |
table.insert(openBrackets, { | |
Bracket = bracket, | |
Team = "TeamB", | |
SpacesAvailable = PLAYERS_PER_TEAM - #bracket.TeamB | |
}) | |
end | |
end | |
elseif not rightNode then | |
-- Case #4: Bracket doesn't have right child | |
-- We don't have a right node, so check to make sure there is space, and add | |
local teamBSize = bracket.TeamB and #bracket.TeamB or 0 | |
local isTeamEmpty = teamBSize == 0 | |
-- 2.A: First round checks to find empty team, second round adds if there is space | |
if ignoreSemiFull and isTeamEmpty or not ignoreSemiFull then | |
if teamBSize < PLAYERS_PER_TEAM then | |
table.insert(openBrackets, { | |
Bracket = bracket, | |
Team = "TeamB", | |
SpacesAvailable = PLAYERS_PER_TEAM - teamBSize | |
}) | |
end | |
end | |
end | |
end | |
end | |
end | |
table.sort(openBrackets, function(a: BracketSpaces, b: BracketSpaces) | |
if #a.Bracket[a.Team] == #b.Bracket[b.Team] then | |
if a.Bracket.RoundID == b.Bracket.RoundID then | |
return a.Bracket.BracketID < b.Bracket.BracketID | |
end | |
return a.Bracket.RoundID < b.Bracket.RoundID | |
else | |
return #a.Bracket[a.Team] < #b.Bracket[b.Team] | |
end | |
end) | |
return openBrackets | |
end | |
function addPlayerToTournament(self, player: Player): Bracket? | |
if self.Tournament.CurrentRound + 1 > self.Tournament.TotalRounds then | |
warn(signature(LOG_PREFIX, "Attempt to join out-of-bounds bracket!")) | |
return | |
end | |
if self.Tournament.PlayerCache[player] then | |
return | |
end | |
local openBrackets: Array<BracketSpaces> = getOpenBrackets(self) | |
if #openBrackets == 0 then | |
return | |
end | |
local addedBracket | |
for _, bracketData: BracketSpaces in openBrackets do | |
if addPlayerToBracket(bracketData.Bracket, player, bracketData.Team) then | |
addedBracket = bracketData.Bracket | |
break | |
end | |
end | |
if addedBracket then | |
self.Tournament.PlayerCache[player] = true | |
self.Tournament.SpacesAvailable = getTotalSpacesAvailable(self) | |
return addedBracket | |
end | |
end | |
function setWinnerForBracket(self, bracket: Bracket, winner: number): boolean | |
if not bracket then | |
warn(signature(LOG_PREFIX, "Attempt to set winner for out-of-bounds bracket!")) | |
return false | |
end | |
if bracket.State == BRACKET_STATES.NotStarted then | |
warn(signature(LOG_PREFIX, "Attempt to set winner for non-started bracket!")) | |
return false | |
end | |
if bracket.State == BRACKET_STATES.WinTeamA | |
or bracket.State == BRACKET_STATES.WinTeamB | |
or bracket.State == BRACKET_STATES.Draw | |
then | |
warn(signature(LOG_PREFIX, "Attempt to set winner of already finished bracket!")) | |
return false | |
end | |
-- Set the winner based on the team value | |
if winner == BRACKET_STATES.Draw then | |
bracket.State = BRACKET_STATES.Draw | |
elseif winner == BRACKET_STATES.WinTeamA then | |
bracket.State = BRACKET_STATES.WinTeamA | |
elseif winner == BRACKET_STATES.WinTeamB then | |
bracket.State = BRACKET_STATES.WinTeamB | |
end | |
bracket.TimeFinished = DateTime.now() | |
local consolationBracket = self.Tournament.ConsolationBracket | |
-- If the bracket isn't the consolation bracket, then try to either stop the tournament (if final), or advance to next round (if any other) | |
if bracket ~= consolationBracket then | |
if bracket.RoundID + 1 <= self.Tournament.TotalRounds then | |
advanceBracketToNextRound(self, bracket) | |
self.Tournament.SpacesAvailable = getTotalSpacesAvailable(self) | |
elseif not consolationBracket | |
or consolationBracket.State == BRACKET_STATES.Draw | |
or consolationBracket.State == BRACKET_STATES.WinTeamA | |
or consolationBracket.State == BRACKET_STATES.WinTeamB | |
then | |
stopTournament(self) | |
return true | |
end | |
else -- The bracket is the consolation bracket, so stop the tournament if the final bracket has finished | |
local rootNode = self.Tournament.TournamentTree:GetRootNode() | |
local rootBracket = rootNode.Value | |
if rootBracket.State == BRACKET_STATES.Draw | |
or rootBracket.State == BRACKET_STATES.WinTeamA | |
or rootBracket.State == BRACKET_STATES.WinTeamB | |
then | |
stopTournament(self) | |
return true | |
end | |
end | |
return true | |
end | |
function startTournament(self) | |
self.Tournament.CurrentRound = 1 | |
self.Tournament.TimeStarted = DateTime.now() | |
self.Tournament.State = TOURNAMENT_STATES.InProgress | |
local round: Round = getBracketsInRound(self, self.Tournament.CurrentRound) | |
for _, bracket: Bracket in round do | |
startBracket(self, bracket) | |
end | |
self.Tournament.SpacesAvailable = getTotalSpacesAvailable(self) | |
self.TournamentStarted:Fire() | |
end | |
function createTournament(self, teams: Array<Team>) | |
assert(#teams <= MAX_TEAMS, signature(LOG_PREFIX, "Attempt to create tournament with too many teams!")) | |
self.Tournament = { | |
State = TOURNAMENT_STATES.NotStarted, | |
TournamentTree = buildEvenTournamentTree(teams), | |
ConsolationBracket = newBracket(nil, nil, 0, 2), | |
Winner = nil, | |
CurrentRound = 1, | |
TotalRounds = 0, | |
SpacesAvailable = 0, | |
TimeStarted = 0, | |
TimeFinished = 0, | |
PlayerCache = {}, | |
} :: Tournament | |
self.Tournament.TotalRounds = self.Tournament.TournamentTree:GetDepth() + 1 | |
self.Tournament.ConsolationBracket.RoundID = self.Tournament.TotalRounds | |
-- Fire the event | |
self.TournamentCreated:Fire() | |
-- Go through all added players, and add them to the cache | |
for _, team: Team in teams do | |
for _, player: Player in team do | |
self.Tournament.PlayerCache[player] = true | |
end | |
end | |
-- Calculate the spaces left | |
self.Tournament.SpacesAvailable = getTotalSpacesAvailable(self) | |
end | |
function stopTournament(self) | |
self.Tournament.TimeFinished = DateTime.now() | |
self.Tournament.State = TOURNAMENT_STATES.Finished | |
local round: Round = getBracketsInRound(self, self.Tournament.CurrentRound) | |
for _, bracket: Bracket in round do | |
if bracket.State == BRACKET_STATES.InProgress then | |
bracket.State = BRACKET_STATES.Stopped | |
bracket.TimeFinished = DateTime.now() | |
end | |
end | |
end | |
function startBracket(self, bracket: Bracket) | |
bracket.TimeStarted = DateTime.now() | |
bracket.State = BRACKET_STATES.InProgress | |
local teamA = bracket.TeamA or {} | |
local teamB = bracket.TeamB or {} | |
if #teamA == 0 and #teamB == 0 then | |
setWinnerForBracket(self, bracket, BRACKET_STATES.Draw) | |
elseif #teamA == 0 then | |
setWinnerForBracket(self, bracket, BRACKET_STATES.WinTeamB) | |
elseif #teamB == 0 then | |
setWinnerForBracket(self, bracket, BRACKET_STATES.WinTeamA) | |
end | |
end | |
function startNextRound(self) | |
local oldRound: Round = getBracketsInRound(self, self.Tournament.CurrentRound) | |
for _, bracket: Bracket in oldRound do | |
if bracket.State == BRACKET_STATES.NotStarted | |
or bracket.State == BRACKET_STATES.InProgress | |
then | |
warn(signature(LOG_PREFIX, "Attempt to start next round when some brackets were not ended!")) | |
return false | |
end | |
end | |
self.Tournament.CurrentRound += 1 | |
local round: Round = getBracketsInRound(self, self.Tournament.CurrentRound) | |
for _, bracket: Bracket in round do | |
startBracket(self, bracket) | |
end | |
self.Tournament.SpacesAvailable = getTotalSpacesAvailable(self) | |
end | |
function kickPlayerFromTournament(self, player: Player): boolean | |
local playerBrackets: Array<Bracket>? = getBracketData(self, player) | |
if not playerBrackets then | |
return false | |
end | |
-- Look through all player brackets and remove the player from all brackets necessary | |
for _, bracket: Bracket in playerBrackets do | |
if bracket.State == BRACKET_STATES.WinTeamA | |
or bracket.State == BRACKET_STATES.WinTeamB | |
or bracket.State == BRACKET_STATES.Draw | |
or bracket.State == BRACKET_STATES.Stopped | |
then | |
continue | |
end | |
if bracket.RoundID >= self.Tournament.CurrentRound then | |
if bracket.TeamA and table.find(bracket.TeamA, player) then | |
local index = table.find(bracket.TeamA, player) | |
table.remove(bracket.TeamA, index) | |
if #bracket.TeamA == 0 and bracket.State == BRACKET_STATES.InProgress then | |
setWinnerForBracket(self, bracket, BRACKET_STATES.WinTeamB) | |
else | |
self.Tournament.SpacesAvailable = getTotalSpacesAvailable(self) | |
end | |
elseif bracket.TeamB and table.find(bracket.TeamB, player) then | |
local index = table.find(bracket.TeamB, player) | |
table.remove(bracket.TeamB, index) | |
if #bracket.TeamB == 0 and bracket.State == BRACKET_STATES.InProgress then | |
setWinnerForBracket(self, bracket, BRACKET_STATES.WinTeamA) | |
else | |
self.Tournament.SpacesAvailable = getTotalSpacesAvailable(self) | |
end | |
end | |
end | |
end | |
return true | |
end | |
--[[ | |
//////////////////////////////////////// | |
|| SERVER API || | |
//////////////////////////////////////// | |
--]] | |
function TournamentService:KnitInit() | |
assert(MAX_TEAMS <= 2^4, signature(LOG_PREFIX, "MAX_TEAMS should be less than or equal to 16!")) | |
assert(MIN_TEAMS >= 2^2, signature(LOG_PREFIX, "MIN_TEAMS should be more than or equal to 4!")) | |
-- Start by creating a blank tournament object | |
resetTournament(self) | |
end | |
function TournamentService:GetTournamentTree(): TreeUtil.BinaryTree? | |
if self.Tournament.TournamentTree then | |
return self.Tournament.TournamentTree | |
end | |
end | |
function TournamentService:GetConsolationBracket(): Bracket? | |
if self.Tournament.ConsolationBracket then | |
return self.Tournament.ConsolationBracket | |
end | |
end | |
function TournamentService:CreateTournament(teams: Array<Team>): boolean | |
assert(typeof(teams) == "table", signature(LOG_PREFIX, `Expected teams to be a table, got {typeof(teams)}!`)) | |
if #teams > MAX_TEAMS then | |
warn(signature(LOG_PREFIX, "Attempt to create tournament with more than the maximum number of teams!")) | |
return false | |
end | |
if #teams < MIN_TEAMS then | |
warn(signature(LOG_PREFIX, "Attempt to create tournament with less than the minimum number of teams!")) | |
return false | |
end | |
if self.Tournament.State ~= TOURNAMENT_STATES.NotStarted then | |
warn(signature(LOG_PREFIX, "Tried to create a new tournament when one is already active!")) | |
return false | |
end | |
createTournament(self, teams) | |
return true | |
end | |
function TournamentService:StartTournament(): boolean | |
if self.Tournament.State ~= TOURNAMENT_STATES.NotStarted then | |
warn(signature(LOG_PREFIX, "Tried to start already active tournament!")) | |
return false | |
end | |
if not self.Tournament.TournamentTree then | |
warn(signature(LOG_PREFIX, "Attempt to start tournament with no tree!")) | |
return false | |
end | |
startTournament(self) | |
return true | |
end | |
function TournamentService:StopTournament(): boolean | |
if self.Tournament.State ~= TOURNAMENT_STATES.InProgress then | |
warn(signature(LOG_PREFIX, "Tried to stop an already inactive tournament!")) | |
return false | |
end | |
stopTournament(self) | |
return true | |
end | |
function TournamentService:ResetTournament(): boolean | |
if self.Tournament.State == TOURNAMENT_STATES.InProgress then | |
warn(signature(LOG_PREFIX, "Attempt to reset an in-progress tournament!")) | |
return false | |
end | |
resetTournament(self) | |
return true | |
end | |
function TournamentService:StartNextRound(): boolean | |
if self.Tournament.CurrentRound + 1 > self.Tournament.TotalRounds then | |
warn(signature(LOG_PREFIX, "Tried to start an out-of-bounds round!")) | |
return false | |
end | |
startNextRound(self) | |
return true | |
end | |
function TournamentService:JoinTournament(player: Player): Bracket? | |
if self.Tournament.PlayerCache[player] then | |
warn(signature(LOG_PREFIX, "Attempt to add player to the tournament that has previously participated!")) | |
return | |
end | |
return addPlayerToTournament(self, player) | |
end | |
function TournamentService:KickPlayer(player: Player): boolean | |
if not player then | |
return false | |
end | |
return kickPlayerFromTournament(self, player) | |
end | |
function TournamentService:GetCurrentRound(): number? | |
return self.Tournament.CurrentRound | |
end | |
function TournamentService:GetTotalRounds(): number? | |
return self.Tournament.TotalRounds | |
end | |
function TournamentService:GetBracket(roundNum: number, bracketID: number): Bracket? | |
assert(typeof(roundNum) == "number", signature(LOG_PREFIX, `Expected roundNum to be number, got {typeof(roundNum)}!`)) | |
assert(typeof(bracketID) == "number", signature(LOG_PREFIX, `Expected bracketID to be number, got {typeof(bracketID)}!`)) | |
return getBracket(self, roundNum, bracketID) | |
end | |
function TournamentService:SetWinnerForBracket(roundNum: number, bracketID: number, winner: number): boolean | |
assert(typeof(roundNum) == "number", signature(LOG_PREFIX, `Expected roundNum to be number, got {typeof(roundNum)}!`)) | |
assert(typeof(bracketID) == "number", signature(LOG_PREFIX, `Expected bracketID to be number, got {typeof(bracketID)}!`)) | |
if not winner then | |
return | |
end | |
return setWinnerForBracket(self, getBracket(self, roundNum, bracketID), winner) | |
end | |
function TournamentService:GetBracketInRoundFromPlayer(player: Player, roundNum: number): Bracket? | |
assert(typeof(roundNum) == "number", signature(LOG_PREFIX, `Expected roundNum to be number, got {typeof(roundNum)}!`)) | |
if not player then | |
return | |
end | |
return getBracketInRoundFromPlayer(self, player, roundNum) | |
end | |
function TournamentService:GetBracketsForPlayer(player: Player): Array<Bracket>? | |
if not player then | |
return | |
end | |
return getBracketData(self, player) | |
end | |
function TournamentService:GetBracketsInRound(roundNum: number): Round? | |
assert(typeof(roundNum) == "number", signature(LOG_PREFIX, `Expected roundNum to be number, got {typeof(roundNum)}!`)) | |
if 0 > roundNum or roundNum > self.Tournament.TotalRounds then | |
warn(signature(LOG_PREFIX, "Attempt to get brackets for out-of-bounds round!")) | |
return | |
end | |
return getBracketsInRound(self, roundNum) | |
end | |
function TournamentService:GetBracketsInCurrentRound(): Round? | |
if self.Tournament.State == TOURNAMENT_STATES.NotStarted then | |
return | |
end | |
return getBracketsInRound(self, self.Tournament.CurrentRound) | |
end | |
function TournamentService:GetWinnersForRound(roundNum: number): Array<Team>? | |
assert(typeof(roundNum) == "number", signature(LOG_PREFIX, `Expected roundNum to be number, got {typeof(roundNum)}!`)) | |
if self.Tournament.CurrentRound < roundNum then | |
return | |
end | |
return getWinnersForRound(self, roundNum) | |
end | |
function TournamentService:GetWinnersForTournament(): Array<Team>? | |
if self.Tournament.State ~= TOURNAMENT_STATES.Finished then | |
warn(signature(LOG_PREFIX, "Tried to get winner from unfinished tournament!")) | |
return | |
end | |
return getWinnersForTournament(self) | |
end | |
function TournamentService:GetBracketTreeIndex(bracket: Bracket): number? | |
local tree: BinaryTree? = self:GetTournamentTree() | |
if not tree then | |
return | |
end | |
local node: Node? = tree[bracket.NodeID] | |
if not node then | |
return | |
end | |
local numNodesBelow = 0 | |
for level = node.Level + 1, tree:GetDepth() do | |
numNodesBelow += #tree:GetNodesAtLevel(level) | |
end | |
return bracket.BracketID + numNodesBelow | |
end | |
-- --[[ | |
-- //////////////////////////////////////// | |
-- || CLIENT API || | |
-- //////////////////////////////////////// | |
-- --]] | |
function TournamentService.Client:RequestJoinTournament(player: Player): Response? | |
local response: Response = { | |
Success = true, | |
Body = {} | |
} | |
-- Ask server to join tournament | |
local bracket: Bracket? = self.Server:JoinTournament(player) | |
-- If we don't have a bracket, return failed response | |
if not bracket then | |
response.Success = false | |
return response | |
end | |
-- Set the body of the response to the bracket | |
response.Body = bracket | |
-- Return the bracket | |
return response | |
end | |
return TournamentService |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment