Ruleset Examples
Overview
The ruleset of PGOS can cover a variety of matchmaking scenarios. The following examples demonstrate the ruleset structure and property expression grammar. Copy these rulesets and customize them on your demands.
For more information on the ruleset editor and reference, see the following links:
Example 1. Match players with close MMR rankings
This example illustrates how to set up two equally matched teams of players with the following instructions:
- Create two teams of players.
- Try to find 3 players for each team.
- Two teams have an equal number of players.
- Include a player's MMR value (if not provided, default to 1000)
- Pair players with similar MMR values. Ensure that both teams have an average player skill within 0.5 points.
- If the match is not filled quickly, relax the following restrictions to complete a match in a reasonable time.
- After 5 seconds, expand the lower limit of team size to 2 and the upper limit of MMR difference to 50.
- After 10 seconds, expand the lower limit of team size to 1 and the upper limit of MMR difference to 100.
- Even when the expansion is triggered, the matchmaker will still try to find a match that best fits the rules. It will try to fulfill the team, and find players with as close MMR as possible. But if the team cannot be fulfilled and MMR varies but they satisfy the minimum limit of each rule, the matchmaker will finally output the match.
- Since both teams have identical structures, you could opt to create just one team definition and set the team quantity to
2
. In this scenario, if you set the quantity of teamred
to2
, the output teams will be named asred_001
andred_002
. - When you are not sure about the output of the expression you write, feel free to validate it in Ruleset Debugger.
- When you are not sure about the effect of the ruleset you edit, feel free to simulate the whole matchmaking process using Matchmaking Simulator.
{
"version":"v1.0",
"playerAttributes":[
{
"name":"mmr",
"type":"number",
"default":1000
}
],
"teams":[
{
"name":"red",
"maxPlayers":3,
"minPlayers":3
},
{
"name":"blue",
"maxPlayers":3,
"minPlayers":3
}
],
"rules":[
{
"name":"mmr_calc",
"type":"distanceRule",
// get mmr values for players in each team, and flatten into a one-dimension list
"measurements":[
"flatten(teams[*].players.playerAttributes[mmr])"
],
// get the average mmr for all players
"referenceValue":"avg(flatten(teams[*].players.playerAttributes[mmr]))",
"minDistance":0,
"maxDistance":0.5
},
{
"name":"equal_team_size",
"type":"comparisonRule",
// get the size of blue team
"measurements":[
"count(teams[blue].players)"
],
// compare with the size of red team
"referenceValue":"count(teams[red].players)",
"operation":"="
}
],
"expansions":[
{
// select the minPlayers of each team
"target":"teams[*].minPlayers",
"steps":[
{
"waitTimeSeconds":5,
"value":2
},
{
"waitTimeSeconds":15,
"value":1
}
]
},
{
// select the maxDistance of specific rule
"target":"rules[mmr_calc].maxDistance",
"steps":[
{
"waitTimeSeconds":5,
"value":50
},
{
"waitTimeSeconds":15,
"value":100
}
]
}
]
}
Example 2. Group players by the map they choose
This example illustrates how to set up a variable number of teams with the following instructions:
- Create 10~20 teams of players.
- Include 3~4 players in each team.
- Final teams must have the same number of players.
- There are 10~20 teams, determined according to the number of players in matchmaking.
- Variable teams will be named as
team_001
,team_002
,team_003
...
- Include a player's map name (if not provided, default to empty)
- Choose players who choose the same map.
- If the match is not filled quickly, relax the team quantity requirement to complete a match in a reasonable time.
- After 10 seconds, expand the minimum quantity of teams to 8.
- After 30 seconds, expand the minimum quantity of teams to 5.
The map_binding
rule ensures the players have the same map
property.
Inside the matchmaking system, the player attributes will be indexed to optimize the performance of the matchmaker choosing tickets based on dynamic needs. The ruleset is not only checked when the players are grouped but also applied during the matching process.
The matchmaker uses a linear programming method to reduce the time cost of matchmaking and performs well when facing a huge amount of players.
{
"version":"v1.0",
"playerAttributes":[
{
"name":"map",
"type":"string",
"default":""
}
],
"teams":[
{
"name":"team",
"maxPlayers":4,
"minPlayers":3,
"maxQuantity":20,
"minQuantity":10
}
],
"rules":[
{
"name":"map_binding",
"type":"comparisonRule",
// get the map of players in each team and flatten into a one-dimension list
"measurements":[
"flatten(teams[*].players.playerAttributes[map])"
],
// the map of these players must be the same
"operation":"="
},
{
"name":"equal_team_size",
"type":"comparisonRule",
// get the size of each team, as a one-dimension list
"measurements":[
"count(teams[*].players)"
],
// the size of each team must be the same
"operation":"="
}
],
"expansions":[
{
// select the minQuantity of each team
"target":"teams[*].minQuantity",
"steps":[
{
"waitTimeSeconds":10,
"value":8
},
{
"waitTimeSeconds":30,
"value":5
}
]
}
]
}
Example 3. Premade teams preferentially match with other premade teams
This example illustrates how to match premade teams with the same number of players first, and relaxes the restriction by time to let group players and solo players mix together.
- Create 4 teams of players.
- Each team has 4 players.
- Variable teams will be named as
team_001
,team_002
,team_003
andteam_004
.
- Include a property named
partysize
, which is the size of the group. - Premade teams will preferentially match with other premade teams.
- Try to match a 4-player premade team with other 4-player premade teams, and match a 2-player premade team with other 2-player premade teams. And the ruleset also applies to solo players, so a solo player should match other solo players.
- After 30 seconds, relax the upper limit of the difference of premade team size to
1
to let 2-player premade teams and solo players match together. - After 60 seconds, relax the upper limit of the difference of premade team size to
2
to let 4-player premade teams and 2-player premade teams match together. - After 90 seconds, relax the upper limit of the difference of premade team size to
3
to theoretically randomly mix premade teams and non-premade teams.
- After 30 seconds, relax the upper limit of the difference of premade team size to
Please note that the partysize
is provided by the game client, which can affect the final matchmaking result. For solo players, the value should be 1
, for duo players the value should be 2
, and for quad players, the value should be 4
.
{
"version": "v1.0",
"playerAttributes": [
{
// the value should be the party size when premade team players enter matchmaking
"name": "partysize",
"type": "number",
"default": 1
}
],
"teams": [
{
"name": "team",
"minPlayers": 4,
"maxPlayers": 4,
"minQuantity": 4,
"maxQuantity": 4
}
],
"rules": [
{
"name": "partyRule",
"type": "distanceRule",
// get the max party size of existing premade teams
"measurements": [
"max(flatten(teams[*].players.playerAttributes[partysize]))"
],
// get the min party size of existing premade teams
"referenceValue": "min(flatten(teams[*].players.playerAttributes[partysize]))",
// limit the difference of party size close to zero
"maxDistance": 0.1,
"partyAggregation": "max"
}
],
"expansions": [
{
// relax the difference of party size to let premade teams and non-premade teams match together
"target": "rules[partyRule].maxDistance",
"steps": [
{
"waitTimeSeconds": 30,
"value": 1
},
{
"waitTimeSeconds": 60,
"value": 2
},
{
"waitTimeSeconds": 90,
"value": 3
}
]
}
]
}
Example 4. Allow specific range of players and limit the team size equally
This example illustrates how to let a small number of players match together in the development stage.
- Create four 4-player teams.
- The ruleset should allow 4~16 players to match together, instead of separated in multiple matches.
If you directly set minPlayers
of each team to 1
, if 8 players enter matchmaking one after another, there is a certain probability that the 8 players are separated into two matches, each containing 4 players. Because the minPlayers
is 1
, and when the matchmaker works, some players who enter matchmaking later may not be in the ticket pool.
So in this scenario, the safe way to allow 4~16 players to match together is to utilize the expansion mechanism to create a tolerable time between the players entering matchmaking one after another.
{
"version": "v1.0",
"playerAttributes": [],
"teams": [
{
"name": "A",
"minPlayers": 4,
"maxPlayers": 4
},
{
"name": "B",
"minPlayers": 4,
"maxPlayers": 4
},
{
"name": "C",
"minPlayers": 4,
"maxPlayers": 4
},
{
"name": "D",
"minPlayers": 4,
"maxPlayers": 4
}
],
"rules": [],
"expansions": [
{
// select the minPlayers of each team
"target": "teams[*].minPlayers",
"steps": [
{
"waitTimeSeconds": 30,
"value": 3
},
{
"waitTimeSeconds": 60,
"value": 2
},
{
"waitTimeSeconds": 90,
"value": 1
}
]
}
]
}
Example 5. Match three teams with different sides
This example illustrates how to set different sides in a match.
- Create three teams of players, each has four players.
- Include a player's MMR value (if not provided, default to 1000).
- Include a player's side value (should be either
ghost
orhuman
) - There are two teams of
human
and one team ofghost
.
{
"version": "v1.0",
"playerAttributes": [
{
"name": "level",
"type": "number",
"default": 0
},
{
"name": "side",
"type": "string",
"default": ""
}
],
"teams": [
{
"name": "red",
"minPlayers": 3,
"maxPlayers": 3
},
{
"name": "green",
"minPlayers": 10,
"maxPlayers": 10
},
{
"name": "blue",
"minPlayers": 10,
"maxPlayers": 10
}
],
"rules": [
{
"name": "mmr_calc",
"type": "distanceRule",
// get mmr values for players in each team, and flatten into a one-dimension list
"measurements": [
"flatten(teams[*].players.playerAttributes[level])"
],
// get the average mmr for all players
"referenceValue": "avg(flatten(teams[*].players.playerAttributes[level]))",
"maxDistance": 3
},
{
"name": "side_binding",
"type": "comparisonRule",
// get the side value of each team, as a two-dimension list
// inside the two-dimension list, the sides inside a team must be the same
// feel free to validate the possible result of your expression using `ruleset debugger`
"measurements": [
"teams[*].players.playerAttributes[side]"
],
"operation": "="
},
{
"name": "side_binding_red",
"type": "comparisonRule",
// get the side value of red team, as a one-dimension list
"measurements": [
"teams[red].players.playerAttributes[side]"
],
// red team are all attackers
"referenceValue": "ghost",
"operation": "="
},
{
"name": "side_binding_green",
"type": "comparisonRule",
// get the side value of green team, as a one-dimension list
"measurements": [
"teams[green].players.playerAttributes[side]"
],
// green team are all attackers
"referenceValue": "human",
"operation": "="
},
{
"name": "side_binding_blue",
"type": "comparisonRule",
// get the side value of blue team, as a one-dimension list
"measurements": [
"teams[blue].players.playerAttributes[side]"
],
// blue team are all attackers
"referenceValue": "human",
"operation": "="
}
],
"expansions": []
}
Example 6. Battle Royale
This example illustrates how to set up rules with a variable quantity of teams.
- Create a team with 4 players inside
- Limit the quantity of teams between 10~20
- Relax the limit by time gradually by allowing fewer teams to form a battle
{
"version": "v1.0",
"playerAttributes": [],
"teams": [
{
"name": "squad",
"minPlayers": 4,
"maxPlayers": 4,
"minQuantity": 10,
"maxQuantity": 20
}
],
"rules": [],
"expansions": [
{
"target": "teams[squad].minQuantity",
"steps": [
{
"waitTimeSeconds": 30,
"value": 7
},
{
"waitTimeSeconds": 50,
"value": 5
},
{
"waitTimeSeconds": 70,
"value": 3
}
]
}
]
}
Example 7. Match Players Initiating Matchmaking Within A Short Time Into The Same Battle
This example illustrates how to group players who started matchmaking within a short time into the same match using expansion
feature.
- This ruleset wants 1~6 players for each battle
- Limit the team to at least 6 players
- Expand the limitation from 6 to 1 gradually
If the minPlayers
configuration is set to 1 without using the expansion
feature, the matchmaking algorithm will immediately start and generate a game session that meets the minPlayers
requirement for the first player who initiates matchmaking. Even if backfill is attempted to add subsequent players to the game session, there is a tendency to generate new game sessions and backfill older ones, which cannot guarantee that all new players will be backfilled into the older game session.
By using the expansion feature, the player who initiates matchmaking first can wait briefly and start the game session with other players who initiate matchmaking within a short period of time.
{
"version": "v1.0",
"playerAttributes": [],
"teams": [
{
"name": "SoloTeam",
"minPlayers": 6,
"maxPlayers": 6,
"minQuantity": 1,
"maxQuantity": 1
}
],
"rules": [],
"expansions": [
{
"target": "teams[SoloTeam].minPlayers",
"steps": [
{
"waitTimeSeconds": 1,
"value": 5
}
]
},
{
"target": "teams[SoloTeam].minPlayers",
"steps": [
{
"waitTimeSeconds": 2,
"value": 4
}
]
},
{
"target": "teams[SoloTeam].minPlayers",
"steps": [
{
"waitTimeSeconds": 3,
"value": 3
}
]
},
{
"target": "teams[SoloTeam].minPlayers",
"steps": [
{
"waitTimeSeconds": 4,
"value": 2
}
]
},
{
"target": "teams[SoloTeam].minPlayers",
"steps": [
{
"waitTimeSeconds": 5,
"value": 1
}
]
}
]
}
Example 8. Multi-mode, multi-map matchmaking
This example demonstrates how to implement multi-mode, multi-map matchmaking. The requirements are as follows:
- When players initiate matchmaking, they can choose from 3 modes and n maps, and are allowed to select multiple times.
- The 3 game modes have different gameplay, which is reflected in the matchmaking rules as different team structures.
- Mode 1 is a two-team PvP with 2 players per team.
- Mode 2 is a single-team PvE with a total of 4 to 8 players.
- Mode 3 is a battle royale with m teams each with 4 players, but m can range from 4 to 8.
- Map selection does not affect team structure.
To implement these requirements using rulesets, the following approach can be taken:
- Use a bit-based number to represent the different maps selected by players. Assign a number to each map, with each bit representing whether the map is selected, and store it as the "map" attribute. Then write an expression to perform a bitwise AND operation on the "map" attribute, creating a comparison rule that requires the result to be greater than zero. This ensures that there is an intersection in the maps selected by all players.
- For the 3 game modes, define three Rulesets and create a MatchConfig. Assign a separate Placer for each Ruleset, placing them on different Fleets to support different gameplay. Since different game modes have different numbers of online players, mixing Fleets helps improve resource utilization.
There are a few things to note:
- Set
bitmap
field to true when using the number type attribute in bit-wise operations. - Since different mode has different team structures, multiple rulesets are needed and each mode has a unique attribute. When the client selects the mode, set the corresponding attribute.
- When associating multiple rulesets to a match configuration, all the rulesets must share the same player attributes definition.
Sub Ruleset 1
{
"version": "v1.0",
"playerAttributes": [
{
"name": "map",
"type": "number",
"bitmap": true,
"default": 0
},
{
"name": "mode1",
"type": "number",
"bitmap": false,
"default": 0
},
{
"name": "mode2",
"type": "number",
"bitmap": false,
"default": 0
},
{
"name": "mode3",
"type": "number",
"bitmap": false,
"default": 0
}
],
"teams": [
{
"name": "red",
"minPlayers": 2,
"maxPlayers": 2,
"minQuantity": 1,
"maxQuantity": 1
},
{
"name": "blue",
"minPlayers": 2,
"maxPlayers": 2,
"minQuantity": 1,
"maxQuantity": 1
}
],
"rules": [
{
"name": "map_intersection",
"type": "comparisonRule",
"description": "",
"measurements": [
"and(flatten(teams[*].players.playerAttributes[map]))"
],
"referenceValue": "0",
"operation": ">"
},
{
"name": "mode_selection",
"type": "comparisonRule",
"description": "",
"measurements": [
"flatten(teams[*].players.playerAttributes[mode1])"
],
"referenceValue": "1",
"operation": "="
}
],
"expansions": []
}
Sub Ruleset 2
{
"version": "v1.0",
"playerAttributes": [
{
"name": "map",
"type": "number",
"bitmap": true,
"default": 0
},
{
"name": "mode1",
"type": "number",
"bitmap": false,
"default": 0
},
{
"name": "mode2",
"type": "number",
"bitmap": false,
"default": 0
},
{
"name": "mode3",
"type": "number",
"bitmap": false,
"default": 0
}
],
"teams": [
{
"name": "one",
"minPlayers": 4,
"maxPlayers": 8,
"minQuantity": 1,
"maxQuantity": 1
}
],
"rules": [
{
"name": "map_intersection",
"type": "comparisonRule",
"description": "",
"measurements": [
"and(flatten(teams[*].players.playerAttributes[map]))"
],
"referenceValue": "0",
"operation": ">"
},
{
"name": "mode_selection",
"type": "comparisonRule",
"description": "",
"measurements": [
"flatten(teams[*].players.playerAttributes[mode2])"
],
"referenceValue": "1",
"operation": "="
}
],
"expansions": []
}
Sub Ruleset 3
{
"version": "v1.0",
"playerAttributes": [
{
"name": "map",
"type": "number",
"bitmap": true,
"default": 0
},
{
"name": "mode1",
"type": "number",
"bitmap": false,
"default": 0
},
{
"name": "mode2",
"type": "number",
"bitmap": false,
"default": 0
},
{
"name": "mode3",
"type": "number",
"bitmap": false,
"default": 0
}
],
"teams": [
{
"name": "one",
"minPlayers": 4,
"maxPlayers": 4,
"minQuantity": 4,
"maxQuantity": 8
}
],
"rules": [
{
"name": "map_intersection",
"type": "comparisonRule",
"description": "",
"measurements": [
"and(flatten(teams[*].players.playerAttributes[map]))"
],
"referenceValue": "0",
"operation": ">"
},
{
"name": "mode_selection",
"type": "comparisonRule",
"description": "",
"measurements": [
"flatten(teams[*].players.playerAttributes[mode3])"
],
"referenceValue": "1",
"operation": "="
}
],
"expansions": []
}
Then create a match configuration and add all three rulesets.
Example 9. P2P matchmaking considering the NAT type of network
In peer-to-peer (P2P) multiplayer games, players’ NAT types are typically detected and fed into matchmaking rules. NAT types are categorized into four common configurations:
- Full-cone NAT (Type 1)
- Definition: Allows inbound traffic from any external IP/port to the mapped internal port.
- Network Discoverability: Highest compatibility for direct connections.
- Restricted-cone NAT (Type 2)
- Definition: Restricts inbound traffic to only the external IP address that the internal host previously communicated with.
- Port-Restricted-cone NAT (Type 3)
- Definition: Extends Restricted-cone by also requiring the external port to match the one used in prior outbound communication.
- Symmetric NAT (Type 4)
- Definition: Assigns a unique external port for each destination IP/port pair, severely limiting direct peer-to-peer connectivity.
To ensure optimal connectivity:
- Ideal Scenario: A game session should include at least one player with Full-cone NAT (
natType=1
). - Fallback Logic: If no Full-cone players are available, the matchmaker progressively downgrades the NAT requirement in this order:
- Restricted-cone (
natType=2
) → Port-Restricted-cone (natType=3
) → Symmetric (natType=4
).
- Restricted-cone (
In the following example, the property natType
is a numeric value with the following mappings:
- 1: Full-cone NAT
- 2: Restricted-cone NAT
- 3: Port-Restricted-cone NAT
- 4: Symmetric NAT
The following examples achieve this requirement using different rule types.
Utilizing Collection Rule
By using the collection rule, we can obtain the NAT type list of players in a game, and limit the number of elements with a value of "1" (the optimal NAT type) to be at least 1.
Then, we can gradually relax this standard over time to "2", "3", and "4" respectively, to ensure that games can be formed within a certain period.
{
"version": "v1.0",
"playerAttributes": [
{
"name": "natType",
"type": "number",
"default": 4,
"partyAggregation": "each"
}
],
"teams": [
{
"name": "red",
"minPlayers": 4,
"maxPlayers": 4,
},
{
"name": "blue",
"minPlayers": 4,
"maxPlayers": 4,
}
],
"rules": [
{
"name": "natRule",
"type": "collectionRule",
"description": "",
"measurements": [
"flatten(teams[*].players.playerAttributes[natType])"
],
"referenceValue": "1",
"operation": "contains",
"maxCount": 10,
"minCount": 1
}
],
"expansions": [
{
"target": "rules[natRule].referenceValue",
"steps": [
{
"waitTimeSeconds": 10,
"value": "2"
},
{
"waitTimeSeconds": 20,
"value": "3"
},
{
"waitTimeSeconds": 30,
"value": "4"
}
]
}
]
}
Utilizing Distance Rule
By using the distance rule, we can obtain the NAT type list of players in a game and limit the minimum value in the list to be less than or equal to 1 to ensure the best NAT type.
Moreover, we can gradually relax this restriction over time, gradually increasing the maximum difference to "2", "3", and "4" respectively, so as to ensure that games can be formed within a certain period of time.
{
"version": "v1.0",
"playerAttributes": [
{
"name": "natType",
"type": "number",
"default": 4,
"partyAggregation": "min"
}
],
"teams": [
{
"name": "red",
"minPlayers": 4,
"maxPlayers": 4,
},
{
"name": "blue",
"minPlayers": 4,
"maxPlayers": 4,
}
],
"rules": [
{
"name": "natRule",
"type": "distanceRule",
"description": "",
"measurements": [
"min(flatten(teams[*].players.playerAttributes[natType]))"
],
"referenceValue": "1",
"maxDistance": 0.1,
"partyAggregation": "min"
}
],
"expansions": [
{
"target": "rules[natRule].maxDistance",
"steps": [
{
"waitTimeSeconds": 10,
"value": 1
},
{
"waitTimeSeconds": 15,
"value": 2
},
{
"waitTimeSeconds": 20,
"value": 3
},
{
"waitTimeSeconds": 30,
"value": 4
}
]
}
]
}