Skip to content

Instantly share code, notes, and snippets.

@RoyallyFlushed
Created July 1, 2024 14:26
Show Gist options
  • Save RoyallyFlushed/d7bc404d44a4957399dbbd21feca3556 to your computer and use it in GitHub Desktop.
Save RoyallyFlushed/d7bc404d44a4957399dbbd21feca3556 to your computer and use it in GitHub Desktop.
Tournament Service code for Man City Blue Moon
--[[
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