Matchmaking

In competitive games it is usually desirable to match players of relatively equal skill to one another. At the moment, ROBLOX can automatically split players in to teams automatically, but that does not take the player's ability into account. In this article, we will learn how to implement an Elo rating sytem in ROBLOX and how to use a ranking to match two players against each other.

Note that this is an advanced article and combines many ROBLOX features. You can learn about all of the individual components by reading the articles in #See Also. There are a lot of code segments in this article. If you prefer to see them all at once see #Example. Lastly, if you want to see all of this in action, there is an uncopylocked version of the game described in #Sample place

Elo rating[edit]

The Elo rating system is a method of ranking players. It is seen in several games, most notably Chess. This type of rating not only helps players see how they are doing compared to others, it also allows for easy matchmaking as two players of equal rating are at roughly the same skill level. It can be also used to predict how likely one player is to win against another.

Elo rating is based purely on wins, losses, and ties (which is half a win and half a loss). When two players finish a game their respective rankings are adjusted. The amount is based on each player's ranking. If a high ranked player beats a low ranked player, the high ranked player will only get a small increase in their ranking (likewise the lower ranked player will only get a small decrease). On the other hand, if a lower ranked player wins, they will get a large increase.

We won't be going over the exact theory or math behind the Elo ranking system, but if you are interested in learning more feel free to read the official wiki page. The important points that we are going to use:

  • The result of a game will create a score for each player. This score will be 1 if the player wins, .5 if there is a tie, and 0 if the player loses.
  • Each player will have an expected score, which represents how likely they were to win based on each player's ranking. This is also going to be between 1 and 0.
  • The score and expected scores will be used to adjust the rankings. There is also a limit to how much a ranking can be adjusted (which will be referred to as the K-factor).

Matchmaking lobby[edit]

The first part of our game will be a lobby place. Lobbys are common in ROBLOX, but ours will also be the place where players are matched to one another. Once a match is made the players will be teleported to a separate arena place where they can play a simple game against each other.

Getting player rank[edit]

When a player joins a game for the first time they need to be assigned a rank. Any number can be chosen here, but we will go with 1500 in this example. This gives the player plenty of room to move both up and down the rankings without getting too close to negative numbers. If the player has played before then we want to get their rank from our DataStore. In either case we will display the player's rank in the Leaderboard.

local ratingData = game:GetService("DataStoreService"):GetDataStore("RatingData")
 
-- Handle player joining the game. If they are a new player we need to give them
-- an initial rank. For existing players we need to fetch their rank from our 
-- DataStore. Lastly, we want to display the player's rank in the Leaderboard
game.Players.PlayerAdded:connect(function(player)
	local playerData = {}
 
	-- Create a default rating of 1500 if the player's data is not in the DataStore
	ratingData:UpdateAsync(player.userId, function(oldValue)
		local newValue = oldValue
		if not newValue then
			newValue = { Rating = 1500 }
		end
		return newValue
	end)
	playerData.Score = ratingData:GetAsync(tostring(player.userId)).Rating
 
	-- Display rating in Leaderboard
	local leaderstats = Instance.new("Model", player)
	leaderstats.Name = "leaderstats"
 
	local displayRating = Instance.new("IntValue", leaderstats)
	displayRating.Name = "Rating"
	displayRating.Value = playerData.Score
end)

Rank list[edit]

When matching players it would be convenient to have a structure in place that will quickly be able to find players in the rank range of the searching player. There are many structures that would work, in this example we will use a doubly linked list. If we ensure the linked list is sorted in order of rank, all we have to do to find players is to search around the node of the searching player.

The implementation of this list is a little long, so for convenience this example will keep all the code for the Rank list in a ModuleScript.

Basic list structure[edit]

A linked list is simply a collection of nodes (which is just a table). Each node has a reference to the next node in the list. A doubly linked list features nodes that also have a link to the previous node in the list.

local rankList = {}
rankList.Top = nil
rankList.Bottom = nil
 
-- Inserts newNode before oldNode in list. Reassigns rankList.Top if oldNode at top
function rankList:InsertBefore(newNode, oldNode)
	if oldNode.Prev then
		newNode.Prev = oldNode.Prev
		oldNode.Prev.Next = newNode
	end
	newNode.Next = oldNode
	oldNode.Prev = newNode
	if rankList.Top == oldNode then
		rankList.Top = newNode
	end
	return newNode
end
 
-- Removes node from the linked list. Makes sure Top and Bottom pointing to correct nodes.
-- Also removes node from lookup dictionary
function rankList:RemoveNode(node)
	if node.Prev then
		node.Prev.Next = node.Next
		if rankList.Bottom == node then
			rankList.Bottom = node.Prev
		end
	end
	if node.Next then
		node.Next.Prev = node.Prev
		if rankList.Top == node then
			rankList.Top = node.Next
		end
	end
	keyTable[tostring(node.userId)] = nil
end

Adding player to list[edit]

At this point we just have the basic structure of a linked list, now we have to actually add the player data. For this list to work correctly when we add a player we must make sure that we insert the player in the correct place in the list based on their rank. That way the list will always be sorted which will help us later when we are searching for a match.

One other thing we will add is a dictionary that maps userIds to their corresponding node so we can quickly access any point in the list. Otherwise we would have to iterate through the list to find a player's node.

-- Dictionary mapping userIds to the corrosponding linked list node for quick lookup
local keyTable = {}
 
-- Adds player to list. Ensures list is in stored order by inserting player in correct spot
-- based on rank. Also adds player to lookup dictionary
function rankList:AddPlayer(userId, rank, startedWaiting)
	local node = {UserId = userId, Rank = rank, Age = startedWaiting, Next = nil, Prev = nil}
	keyTable[tostring(userId)] = node	
 
	-- First check if list is empty. If so just point Top and Bottom to the new node
	if not rankList.Top then
		rankList.Top = node
		rankList.Bottom = node
		return node
	else
		-- If the list is not empty find the place where the new node should go. Start
		-- with the Top node and continue through the list comparing ranks
		local currentNode = rankList.Top
 
		while currentNode do
			if currentNode.Rank	> rank then
				return rankList:InsertBefore(node, currentNode)
			end		
 
			currentNode = currentNode.Next
		end
 
		--if code got here then the node has to be inserted at the end of the list
		rankList.Bottom.Next = node
		node.Prev = rankList.Bottom
		rankList.Bottom = node
		return node
	end
end
 
-- Removes player from list
function rankList:RemovePlayer(userId)
	local playerNode = keyTable[tostring(userId)]
	rankList:RemoveNode(playerNode)
end

Finding a match[edit]

Now for the more exciting stuff, using our doubly linked list to find a match for a player. In our linked list we are storing both the player's rank and how long they have been looking for a game. When we want to find a match for a player, we will try to find players who are in a specific range of the player's rank. The player we are looking for is the player who is in the rank range who has been waiting the longest.

To do this search we will start at the node of the searching player and look both up and down the list keeping track of the player who has been waiting the longest.

-- Private search function for FindPlayerInRangeWithNode. Can search either towards
-- the top of the list or the bottom based on searchAscending
local function Search(current, rank, searchAscending, range, oldestWait)
	local retNode = nil	
	while current do
		if math.abs(rank - current.Rank) > range then
			break
		end		
		if current.Age < oldestWait then
			oldestWait = current.Age
			retNode = current
		end
		if searchAscending then 
			current = current.Next
		else
			current = current.Prev
		end
	end
	return retNode, oldestWait
end
 
-- Returns node of a player in the rank range of the player in startNode. The player who
-- is returned is the the player in the rank range who has been waiting the longest.
function rankList:FindPlayerInRangeWithNode(startNode, range)
	local oldestWait = math.huge
	local rank = startNode.Rank
	local current = startNode.Next
	local retNode = nil
 
	retNode, oldestWait = Search(startNode.Next, startNode.Rank, true, range, oldestWait)
	retNode, oldestWait = Search(startNode.Prev, startNode.Rank, false, range, oldestWait)
 
	return retNode
end
 
-- Returns user id of other player in range of userId's rank. Returns nil if no such player
-- found.
function rankList:FindPlayerInRange(userId, range)
	local playerNode = keyTable[tostring(userId)]
	if playerNode then
		local otherPlayer = rankList:FindPlayerInRangeWithNode(playerNode, range)
		if otherPlayer then 
			return otherPlayer.UserId 
		end
	end
	return nil
end


Queue[edit]

People in the game may not want to play right away, so lets implement a queue for the players to wait in. A queue will be useful as we want to find a match for people who have been waiting the longest. To do this, let's use a table which will hold both the userId's of the players in the queue, and the time that they entered the queue. Using table.insert without a position argument we can ensure that the last person in the queue is always the newest person added. We also need to make sure players can leave the queue (if they are matched against another player, leave the game, or decide to leave the queue).

-- Table to hold the queue of players looking for a game
local matchMakingQueue = {}
 
-- Add player's id to the queue as well as the time that they entered the queue
local addToMMQueue = function(playerId, enteredTime)
	local data = {}
	data.UserId = playerId
	data.EnteredQueue = enteredTime
	table.insert(matchMakingQueue, data)
end
 
-- Remove player from the queue
local removeFromMMQueue = function(playerId)
	for i, playerData in pairs(matchMakingQueue) do
		if playerData.UserId == playerId then
			table.remove(matchMakingQueue, i)
			return
		end
	end
end

Adding player to queue[edit]

To add a player to the game's queue we'll add a simple button which will fire a RemoteEvent. When the event fires we will add the player to the queue and to the linked list. If a player clicks again the player will get removed from both the queue and the list.

local button = script.Parent
local lookForGameEvent = game.ReplicatedStorage.LookForGameEvent
 
local lookForGameNextClick = true
 
-- Handle button click to add/remove player from the queue
button.MouseButton1Click:connect(function()
	if lookForGameNextClick then
		button.Text = "Looking for game. Click again to cancel"
	else
		button.Text = "Look for game."
	end
	lookForGameEvent:FireServer(lookForGameNextClick)
	lookForGameNextClick = not lookForGameNextClick	
end)

Back in the main script:

local rankedList = require(game.ServerStorage.MatchmakingRankedListModule)
local lookForGameEvent = game.ReplicatedStorage.LookForGameEvent
 
-- Remove player from list and queue when they leave the game
game.Players.PlayerRemoving:connect(function(player)
	removeFromMMQueue(player.userId)
	rankedList:RemovePlayer(player.userId)
end)
game.Players.PlayerRemoving:connect(function(player)
	removeFromMMQueue(player.userId)
	rankedList:RemovePlayer(player.userId)
end)
 
-- Handle remote event to either add or remove player from the queue
lookForGameEvent.OnServerEvent:connect(function(player, lookingForGame)
	if lookingForGame then
		print(player.Name .. " now looking for game")
		local enteredTime = os.time()
		addToMMQueue(player.userId, enteredTime)
		rankedList:AddPlayer(player.userId, player.leaderstats.Rating.Value, enteredTime)
	else
		print(player.Name .. " has left the queue")
		removeFromMMQueue(player.userId)
		rankedList:RemovePlayer(player.userId)
	end
end)

Processing queue[edit]

Now that we have the ability to add players to both our queue and linked list, we can cycle through the queue and try to match players who are in it. We also need to define a rank range to look in. In general we want to match players who are close together (say within 100 rank of each other). That said, sometimes such a player does not exist and so we need to expand the range that we are looking in. We will use the time that the player entered the queue to determine how large a rank range to search.

Note that this loop uses spawn to call the function to start a game with two matched players. This allows the matchmaking loop to continue while a game is setup (which can take some time).

-- Returns a rank range to search through based on time waiting in the queue.
-- If player has been waiting too long just return math.huge so the player can
-- be matched with anyone.
local getRange = function(timeWaiting)
	if timeWaiting < 10 then
		return 100
	elseif timeWaiting >=10 and timeWaiting < 20 then
		return 200
	elseif timeWaiting >=20 and timeWaiting <= 35 then
		return 300
	end
	return math.huge
end
 
-- Matchmaking loop. Cycles about every 5 seconds to match players.
while true do
	local now = os.time()
	-- Cycle through queue, try to find players in range
	for _, mmData in pairs(matchMakingQueue) do
		print("attempting to find match for " .. mmData.UserId)
		-- Get rank range to search
		local range = getRange(now - mmData.EnteredQueue)
		-- Use list to find a player in player's rank range
		local otherPlayerId = rankedList:FindPlayerInRange(mmData.UserId, range)
		if otherPlayerId then
			-- Another player was found. Remove both players from the queue and list so they
			-- can't be matched with anyone else
			print("found player: " .. otherPlayerId)
			rankedList:RemovePlayer(mmData.UserId)
			rankedList:RemovePlayer(otherPlayerId)
			removeFromMMQueue(mmData.UserId)
			removeFromMMQueue(otherPlayerId)
			-- Start game with the two players. This function can take some times, so spawn it
			-- in a new thread so the loop can continue.
			spawn(function() startGame(mmData.UserId, otherPlayerId) end)
		end
	end
	wait(5)
end

Create arena and teleport[edit]

Once we have two players through matchmaking we can make a unique place for them to play in. The place will be made using CreatePlaceAsync. Once it is made we can teleport one player using Teleport. We could use Teleport again to get the other player there, but while it is likely both players will go to the same instance it is not guaranteed. The safer way to handle this is to wait until the first player has teleported and use GetPlayerPlaceInstanceAsync to get the exact instance to teleport the second player to (using TeleportToPlaceInstance).

local teleportService = game:GetService("TeleportService")
local arenaPlaceTemplateId = 181238621
 
-- Creates place for game and teleports both players to it
local startGame = function(playerAId, playerBId)
	local message = ""
	print("starting game with " .. playerAId .. " and " .. playerBId)
 
	-- Get both player objects
	local playerA = nil
	local playerB = nil
	for _, player in pairs(game.Players:GetPlayers()) do
		if player.userId == playerAId then
			playerA = player
		end
		if player.userId == playerBId then
			playerB = player
		end
	end
 
	-- Create arena place and get its id
	local arenaPlaceId = game:GetService("AssetService"):CreatePlaceAsync(
		"Arena place for " .. playerA.Name .. " and " .. playerB.Name, arenaPlaceTemplateId)
 
	-- Bind OnTeleport event to playerA (who is teleported first). If that teleport is successful
	-- then we want playerB to be teleported to the same instance
	local connection = playerA.OnTeleport:connect(function(teleportState, placeId)
		if teleportState == Enum.TeleportState.Started then
			local teleportStarted = os.time()
			-- Keep checking if playerA has arrived in other instance.
			while true do
				local success, error, placeId, arenaInstanceId = teleportService:GetPlayerPlaceInstanceAsync(playerAId)
				-- If playerA is in the correct place then we can teleport playerB there as well
				if placeId == arenaPlaceId then
					teleportService:TeleportToPlaceInstance(arenaPlaceId, arenaInstanceId, playerB)
					return
				end
				wait()
			end	
		end
	end)
	wait(1)
 
	-- Teleport playerA to the arena
	teleportService:Teleport(arenaPlaceId, playerA)
end

Adjusting player rating[edit]

So far we have a lobby which can be used to find players of similar rating and put them into a game together. Now, lets take a look at the game we are sending the players to in order to see how to adjust their rating based on their performance. For example purposes this game will be very simple. Both players will be given a sword and if a player gets destroyed then the other player wins. In the rare case where both players destroy each other at the same time then the game will be considered a tie.

Setup[edit]

In this game we need to keep track of whether each player has died or not. It will also be useful to know the rating of both players. We will put all of this information in a table when players enter the game.

local players = {}
local ratingData = game:GetService("DataStoreService"):GetDataStore("RatingData")
local playerHasDied = false
 
-- Handle the PlayerAdded event to setup the players table
game.Players.PlayerAdded:connect(function(player)
 
	-- When a character has died we want to update the players table. We also want to
	-- set our global playerHasDied variable to true so we know the game has ended
	player.CharacterAdded:connect(function(character)
		character.Humanoid.Died:connect(function()
			print(player.Name .. " has died")
			players[tostring(player.userId)].Died = true
			playerHasDied = true
		end)
	end)
 
	-- Get player's rank from the datastore and set the died status in players table to false
	print("Getting player data for " .. player.Name)
	local playerData = {}
	playerData.Rating = ratingData:GetAsync(tostring(player.userId)).Rating
	playerData.Died = false
	players[tostring(player.userId)] = playerData
end)

Calculating rating change[edit]

Here is where we use Elo rating to calculate how much each player's rating should change. This function will calculate this based on several factors: each player's current rating, whether each player has died and a constant k factor. This uses the functions from Elo rating sytem.

local kfactor = 30
 
-- Use Elo rating system to cacluate how much each player's rating should change
local calculateRatingChange = function(playerA, playerB)
	-- Get each player's rating from the players table
	local playerARating = players[tostring(playerA.userId)].Rating
	local playerBRating = players[tostring(playerB.userId)].Rating
 
	-- Get whether each player has died from the players table
	local playerADied = players[tostring(playerA.userId)].Died
	local playerBDied = players[tostring(playerB.userId)].Died	
 
	-- Calculate how likely each player was to win the match. Note that expectedA + expectedB = 1
	local expectedA = 1 / (1 + math.pow(10,(playerBRating - playerARating)/400))
	local expectedB = 1 - expectedA
 
	-- Calculate a score based on how well a player has performed. Note the following values:
	-- Win = 1
	-- Tie = .5
	-- Loss = 0
	-- We start at .5 (assuming a tie). Then, if a player dies that player looses .5 score and the other
	-- player gains .5 to their score.
	local scoreA = .5
	local scoreB = .5
	if playerADied then
		scoreA = scoreA - .5
		scoreB = scoreB + .5
	end	
	if playerBDied then
		scoreA = scoreA + .5
		scoreB = scoreB - .5
	end
 
	-- Calculate how much each player's rating should change based on their score, their expected chance
	-- of winning, and finally limiting by the kfactor
	local playerAChange = kfactor * (scoreA - expectedA)
	local playerBChange = kfactor * (scoreB - expectedB)
 
	return playerAChange, playerBChange
end

Detecting game end[edit]

The last piece we need is to detect when the game is over so. Once it is we can calculate how each player's rating should change, update the DataStore, and then teleport the players back to the lobby.

local teleportService = game:GetService("TeleportService")
local lobbyId = 181194460
 
-- Wait for game to end
while not playerHasDied do
	wait()
end
print("Player has died! Time to adjust scores")
 
-- wait a moment before checking if both players have died to account for a tie
wait(1)
 
local playerA = nil
local playerB = nil
for _, player in pairs(game.Players:GetPlayers()) do
	if playerA == nil then
		playerA = player
	else
		playerB = player
	end
end
 
-- calculate how much each player's rating will change
local playerAchange, playerBchange = calculateRatingChange(playerA, playerB)
print("PlayerA points should change by " .. playerAchange)
print("PlayerB points should change by " .. playerBchange)
 
-- change each player's points and rating
adjustPlayerRating(playerA, playerAchange)
adjustPlayerRating(playerB, playerBchange)
 
print("Sending players back to lobby")
wait(5)
 
-- teleport players back to lobby
for _, player in pairs(game.Players:GetPlayers()) do
	teleportService:Teleport(lobbyId, player)
end

Example[edit]

This article used a lot of code. The scripts are below in full if you prefer not to look at individual elements. Press "Expand" to see the scripts:

Lobby Script[edit]

local lookForGameEvent = game.ReplicatedStorage.LookForGameEvent
local rankedList = require(game.ServerStorage.MatchmakingRankedListModule)
local ratingData = game:GetService("DataStoreService"):GetDataStore("RatingData")
local teleportService = game:GetService("TeleportService")
local arenaPlaceTemplateId = 181238621
 
-- Table to hold the queue of players looking for a game
local matchMakingQueue = {}
 
-- Add player's id to the queue as well as the time that they entered the queue
local addToMMQueue = function(playerId, enteredTime)
	local data = {}
	data.UserId = playerId
	data.EnteredQueue = enteredTime
	table.insert(matchMakingQueue, data)
end
 
-- Remove player from the queue
local removeFromMMQueue = function(playerId)
	for i, playerData in pairs(matchMakingQueue) do
		if playerData.UserId == playerId then
			table.remove(matchMakingQueue, i)
			return
		end
	end
end
 
-- Handle player joining the game. If they are a new player we need to give them
-- an initial rank. For existing players we need to fetch their rank from our 
-- DataStore. Lastly, we want to display the player's rank in the Leaderboard
game.Players.PlayerAdded:connect(function(player)
	local playerData = {}
 
	-- Create a default rating of 1500 if the player's data is not in the DataStore
	ratingData:UpdateAsync(player.userId, function(oldValue)
		local newValue = oldValue
		if not newValue then
			newValue = {Rating = 1500}
		end
		return newValue
	end)
	playerData.Score = ratingData:GetAsync(tostring(player.userId)).Rating
 
	-- Display rating in Leaderboard
	local leaderstats = Instance.new("Model", player)
	leaderstats.Name = "leaderstats"
 
	local displayRating = Instance.new("IntValue", leaderstats)
	displayRating.Name = "Rating"
	displayRating.Value = playerData.Score
end)
 
-- Adds player both to queue and list to search for match
local function playerSearchingForMatch(userId, rank)
	local now = os.time()
	addToMMQueue(userId, now)
	rankedList:AddPlayer(userId, rank, now)
end
 
-- Remove player from list and queue when they leave the game
game.Players.PlayerRemoving:connect(function(player)
	removeFromMMQueue(player.userId)
	rankedList:RemovePlayer(player.userId)
end)
 
-- Handle remote event to either add or remove player from the queue
lookForGameEvent.OnServerEvent:connect(function(player, lookingForGame)
	if lookingForGame then
		print(player.Name .. " now looking for game")
		local enteredTime = os.time()
		playerSearchingForMatch(player.userId, player.leaderstats.Rating.Value)
	else
		print(player.Name .. " has left the queue")
		removeFromMMQueue(player.userId)
		rankedList:RemovePlayer(player.userId)
	end
end)
 
-- Returns a rank range to search through based on time waiting in the queue.
-- If player has been waiting too long just return math.huge so the player can
-- be matched with anyone.
local getRange = function(timeWaiting)
	if timeWaiting < 10 then
		return 100
	elseif timeWaiting >=10 and timeWaiting < 20 then
		return 200
	elseif timeWaiting >=20 and timeWaiting <= 35 then
		return 300
	end
	return math.huge
end
 
-- Creates place for game and teleports both players to it
local startGame = function(playerAId, playerBId)
	local message = ""
	print("starting game with " .. playerAId .. " and " .. playerBId)
 
	-- Get both player objects
	local playerA = nil
	local playerB = nil
	for _, player in pairs(game.Players:GetPlayers()) do
		if player.userId == playerAId then
			playerA = player
		end
		if player.userId == playerBId then
			playerB = player
		end
	end
 
	-- Create arena place and get its id
	local arenaPlaceId = game:GetService("AssetService"):CreatePlaceAsync(
		"Arena place for " .. playerA.Name .. " and " .. playerB.Name, arenaPlaceTemplateId)
 
	-- Bind OnTeleport event to playerA (who is teleported first). If that teleport is successful
	-- then we want playerB to be teleported to the same instance
	local connection = playerA.OnTeleport:connect(function(teleportState, placeId)
		if teleportState == Enum.TeleportState.Started then
			local teleportStarted = os.time()
			-- Keep checking if playerA has arrived in other instance.
			while true do
				local success, error, placeId, arenaInstanceId = teleportService:GetPlayerPlaceInstanceAsync(playerAId)
				-- If playerA is in the correct place then we can teleport playerB there as well
				if placeId == arenaPlaceId then
					teleportService:TeleportToPlaceInstance(arenaPlaceId, arenaInstanceId, playerB)
					return
				end
				wait()
			end	
		end
	end)
	wait(1)
 
	-- Teleport playerA to the arena
	teleportService:Teleport(arenaPlaceId, playerA)
end
 
-- Matchmaking loop. Cycles about every 5 seconds to match players.
while true do
	local now = os.time()
	-- Cycle through queue, try to find players in range
	for _, mmData in pairs(matchMakingQueue) do
		print("attempting to find match for " .. mmData.UserId)
		-- Get rank range to search
		local range = getRange(now - mmData.EnteredQueue)
		-- Use list to find a player in player's rank range
		local otherPlayerId = rankedList:FindPlayerInRange(mmData.UserId, range)
		if otherPlayerId then
			-- Another player was found. Remove both players from the queue and list so they
			-- can't be matched with anyone else
			print("found player: " .. otherPlayerId)
			rankedList:RemovePlayer(mmData.UserId)
			rankedList:RemovePlayer(otherPlayerId)
			removeFromMMQueue(mmData.UserId)
			removeFromMMQueue(otherPlayerId)
			-- Start game with the two players. This function can take some times, so start a
			-- coroutine so the loop can continue.
			local thread = coroutine.create(function() startGame(mmData.UserId, otherPlayerId) end)
			coroutine.resume(thread)
		end
	end
	wait(5)
end

Lobby linked list module[edit]

-- Module for double linked list for matchmaking
local rankList = {}
rankList.Top = nil
rankList.Bottom = nil
 
-- Dictionary mapping userIds to the corrosponding linked list node for quick lookup
local keyTable = {}
 
-- Inserts newNode before oldNode in list. Reassigns rankList.Top if oldNode at top
function rankList:InsertBefore(newNode, oldNode)
	if oldNode.Prev then
		newNode.Prev = oldNode.Prev
		oldNode.Prev.Next = newNode
	end
	newNode.Next = oldNode
	oldNode.Prev = newNode
	if rankList.Top == oldNode then
		rankList.Top = newNode
	end
	return newNode
end
 
-- Removes node from the linked list. Makes sure Top and Bottom pointing to correct nodes.
-- Also removes node from lookup dictionary
function rankList:RemoveNode(node)
	if node.Prev then
		node.Prev.Next = node.Next
		if rankList.Bottom == node then
			rankList.Bottom = node.Prev
		end
	end
	if node.Next then
		node.Next.Prev = node.Prev
		if rankList.Top == node then
			rankList.Top = node.Next
		end
	end
	keyTable[tostring(node.userId)] = nil
end
 
-- Adds player to list. Ensures list is in stored order by inserting player in correct spot
-- based on rank. Also adds player to lookup dictionary
function rankList:AddPlayer(userId, rank, startedWaiting)
	local node = {UserId = userId, Rank = rank, Age = startedWaiting, Next = nil, Prev = nil}
	keyTable[tostring(userId)] = node	
 
	-- First check if list is empty. If so just point Top and Bottom to the new node
	if not rankList.Top then
		rankList.Top = node
		rankList.Bottom = node
		return node
	else
		-- If the list is not empty find the place where the new node should go. Start
		-- with the Top node and continue through the list comparing ranks
		local currentNode = rankList.Top
 
		while currentNode do
			if currentNode.Rank	> rank then
				return rankList:InsertBefore(node, currentNode)
			end		
 
			currentNode = currentNode.Next
		end
 
		--if code got here then the node has to be inserted at the end of the list
		rankList.Bottom.Next = node
		node.Prev = rankList.Bottom
		rankList.Bottom = node
		return node
	end
end
 
-- Removes player from list
function rankList:RemovePlayer(userId)
	local playerNode = keyTable[tostring(userId)]
	rankList:RemoveNode(playerNode)
end
 
-- Private search function for FindPlayerInRangeWithNode. Can search either towards
-- the top of the list or the bottom based on searchAscending
local function Search(current, rank, searchAscending, range, oldestWait)
	local retNode = nil	
	while current do
		if math.abs(rank - current.Rank) > range then
			break
		end		
		if current.Age < oldestWait then
			oldestWait = current.Age
			retNode = current
		end
		if searchAscending then 
			current = current.Next
		else
			current = current.Prev
		end
	end
	return retNode, oldestWait
end
 
-- Returns node of a player in the rank range of the player in startNode. The player who
-- is returned is the the player in the rank range who has been waiting the longest.
function rankList:FindPlayerInRangeWithNode(startNode, range)
	local oldestWait = math.huge
	local rank = startNode.Rank
	local current = startNode.Next
	local retNode = nil
 
	retNode, oldestWait = Search(startNode.Next, startNode.Rank, true, range, oldestWait)
	retNode, oldestWait = Search(startNode.Prev, startNode.Rank, false, range, oldestWait)
 
	return retNode
end
 
-- Returns user id of other player in range of userId's rank. Returns nil if no such player
-- found.
function rankList:FindPlayerInRange(userId, range)
	local playerNode = keyTable[tostring(userId)]
	if playerNode then
		local otherPlayer = rankList:FindPlayerInRangeWithNode(playerNode, range)
		if otherPlayer then 
			return otherPlayer.UserId 
		end
	end
	return nil
end
 
return rankList

Lobby LocalScript[edit]

local button = script.Parent
local lookForGameEvent = game.ReplicatedStorage.LookForGameEvent
 
local lookForGameNextClick = true
 
button.MouseButton1Click:connect(function()
	if lookForGameNextClick then
		button.Text = "Looking for game. Click again to cancel"
	else
		button.Text = "Look for game."
	end
	lookForGameEvent:FireServer(lookForGameNextClick)
	lookForGameNextClick = not lookForGameNextClick
 
end)

Arena Script[edit]

local players = {}
local kfactor = 30
local teleportService = game:GetService("TeleportService")
local ratingData = game:GetService("DataStoreService"):GetDataStore("RatingData")
local lobbyId = 181194460
local playerHasDied = false
 
-- Handle the PlayerAdded event to setup the players table
game.Players.PlayerAdded:connect(function(player)
 
	-- When a character has died we want to update the players table. We also want to
	-- set our global playerHasDied variable to true so we know the game has ended
	player.CharacterAdded:connect(function(character)
		character.Humanoid.Died:connect(function()
			print(player.Name .. " has died")
			players[tostring(player.userId)].Died = true
			playerHasDied = true
		end)
	end)
 
	-- Get player's rank from the datastore and set the died status in players table to false
	print("Getting player data for " .. player.Name)
	local playerData = {}
	playerData.Rating = ratingData:GetAsync(tostring(player.userId)).Rating
	playerData.Died = false
	players[tostring(player.userId)] = playerData
end)
 
-- Use Elo rating system to cacluate how much each player's rating should change
local calculateRatingChange = function(playerA, playerB)
	-- Get each player's rating from the players table
	local playerARating = players[tostring(playerA.userId)].Rating
	local playerBRating = players[tostring(playerB.userId)].Rating
 
	-- Get whether each player has died from the players table
	local playerADied = players[tostring(playerA.userId)].Died
	local playerBDied = players[tostring(playerB.userId)].Died	
 
	-- Calculate how likely each player was to win the match. Note that expectedA + expectedB = 1
	local expectedA = 1 / (1 + math.pow(10,(playerBRating - playerARating)/400))
	local expectedB = 1 - expectedA
 
	-- Calculate a score based on how well a player has performed. Note the following values:
	-- Win = 1
	-- Tie = .5
	-- Loss = 0
	-- We start at .5 (assuming a tie). Then, if a player dies that player looses .5 score and the other
	-- player gains .5 to their score.
	local scoreA = .5
	local scoreB = .5
	if playerADied then
		scoreA = scoreA - .5
		scoreB = scoreB + .5
	end	
	if playerBDied then
		scoreA = scoreA + .5
		scoreB = scoreB - .5
	end
 
	-- Calculate how much each player's rating should change based on their score, their expected chance
	-- of winning, and finally limiting by the kfactor
	local playerAChange = kfactor * (scoreA - expectedA)
	local playerBChange = kfactor * (scoreB - expectedB)
 
	return playerAChange, playerBChange
end
 
-- Update DataStore with player's new rating value
local adjustPlayerRating = function(player, rankingChange)
	ratingData:UpdateAsync(tostring(player.userId), function(oldValue)
		local newValue = oldValue
		newValue.Rating = newValue.Rating + rankingChange
		return newValue
	end)
end
 
-- Wait for two players before lowering barrier
print("Waiting for players")
while game.Players.NumPlayers < 2 do
	wait()
end
print("Done waiting for players")
 
-- Now players are in game remove barriers
for _, barrier in pairs(game.Workspace.Barriers:GetChildren()) do
	barrier.CanCollide = false
	barrier.Transparency = 1
end
 
print("Waiting for a player to die")
while not playerHasDied do
	wait()
end
print("Player has died! Time to adjust scores")
 
-- wait a moment before checking if both players have died to account for a tie
wait(1)
 
local playerA = nil
local playerB = nil
for _, player in pairs(game.Players:GetPlayers()) do
	if playerA == nil then
		playerA = player
	else
		playerB = player
	end
end
 
-- calculate how much each player's rating will change
local playerAchange, playerBchange = calculateRatingChange(playerA, playerB)
print("PlayerA points should change by " .. playerAchange)
print("PlayerB points should change by " .. playerBchange)
 
-- change each player's points and rating
adjustPlayerRating(playerA, playerAchange)
adjustPlayerRating(playerB, playerBchange)
 
print("Sending players back to lobby")
wait(5)
 
-- teleport players back to lobby
for _, player in pairs(game.Players:GetPlayers()) do
	teleportService:Teleport(lobbyId, player)
end

Sample place[edit]

Both the lobby and arena outlined in this example are both available on the ROBLOX site and are both uncopylocked:

Lobby

Arena

See Also[edit]