Matchmaking

Matching rules

Matchmaking rule reference: player counts, skill windows, roles, shared preferences, and time-based relaxation, with examples.

Constraints describe what a ticket considers a valid match. The service evaluates every ticket’s rules against the complete candidate group. A match is valid only when all of those individual rule trees pass.

The rule model

Rules are JSON objects distinguished by type. Combine them using and, or, and not. Field rules read ticket metadata from meta, not individual player metadata.

Metadata alone does not separate queues. If meta.mode must match, add a fieldEqualsDeep rule for mode. The top-level environment is enforced independently and cannot be relaxed by rules.

The matcher combines at least two tickets. playerCount counts all players in those tickets; other count-based field rules generally count tickets.

Group size and composition

playerCount

Require a total number of players across the complete group, inclusive of both bounds.

{ "type": "playerCount", "min": 4, "max": 4 }

Both values are nonnegative integers, and max must be at least min. Keep a player-count rule outside any branch that you intend to relax with a timeout.

fieldEqualsDeep

Require equal values for a ticket metadata field. Values use deep equality.

{ "type": "fieldEqualsDeep", "field": "mode" }

Without min, every ticket must have the same value. With min, at least that many tickets, including the current ticket, must share its value. Missing values compare equal to one another, so validate metadata in your game when the field is required.

fieldExclusive

Require each other ticket’s field value to differ from the current ticket’s value.

{ "type": "fieldExclusive", "field": "role" }

Only the ticket carrying the restriction needs this rule. For example, a ticket with role = "hunter" can prevent a second hunter from joining. If the current ticket has no value for the field, the rule passes.

fieldPartition

Require an exact number of tickets for each declared string value.

{
  "type": "fieldPartition",
  "field": "role",
  "partitions": { "tank": 1, "healer": 1, "damage": 2 }
}

Every ticket must have a listed value. Missing or unknown values fail. Partition counts are positive integers and count tickets, not players. Use this example with solo tickets if each role slot should represent one player, and pair it with playerCount.

Proximity and shared preferences

fieldWindow

Accept numeric metadata values within an expanding distance from the current ticket’s value.

{
  "type": "fieldWindow",
  "field": "skill",
  "spreadRate": 2,
  "min": 50,
  "max": 300
}

The allowed difference is min(max, (min ?? 0) + ageSeconds × spreadRate). spreadRate is a nonnegative number in field units per second. min defaults to zero; omitted max means no cap.

The example starts at ±50 skill points and reaches ±300 after 125 seconds. Each ticket evaluates its own age and window, so every member must accept the other members. Missing or non-numeric values fail. Numeric strings are accepted, and an empty or whitespace-only string is read as 0, so send the value as a number.

fieldDistance

Use the same expanding-window model for geographic coordinates, measured in kilometers using great-circle distance.

{
  "type": "fieldDistance",
  "field": "location",
  "spreadRate": 1.5,
  "min": 50,
  "max": 500
}

The metadata value must be a two-element [latitude, longitude] array in decimal degrees. Missing or non-numeric coordinates fail. The example expands from 50 km to 500 km over five minutes; this measures geographic distance, not network latency.

fieldIncludes

Require array-valued fields to share at least one deeply equal element.

{ "type": "fieldIncludes", "field": "languages" }

For example, {"languages": ["en", "es"]} intersects {"languages": ["es", "fr"]}. Without min, the current ticket must overlap every other ticket. With min, at least that many tickets, including itself when it intersects, must overlap its array.

Pairwise overlap does not guarantee one common element across the entire group. Missing or non-array fields do not count as an overlap.

Logic and time

and, or, and not

Rule Shape Passes when
and { "type": "and", "constraints": [...] } Every child passes. An empty list passes.
or { "type": "or", "constraints": [...] } At least one child passes. An empty list fails.
not { "type": "not", "constraint": {...} } Its single child fails.

timeout

A timeout becomes true once the ticket’s age reaches after seconds. It does not cancel a search, expire a ticket, or force a match.

{ "type": "timeout", "after": 90 }

Combine it with or to relax one part of a rule. Other tickets’ constraints still need to pass. Environment isolation always remains in force.

A ranked queue

This rule requires four players and the same mode. The skill window starts at 50 points, expands by two points per second, and becomes optional after 90 seconds.

local constraints = {
    type = "and",
    constraints = {
        { type = "playerCount", min = 4, max = 4 },
        { type = "fieldEqualsDeep", field = "mode" },
        {
            type = "or",
            constraints = {
                {
                    type = "fieldWindow",
                    field = "skill",
                    spreadRate = 2,
                    min = 50,
                    max = 300,
                },
                { type = "timeout", after = 90 },
            },
        },
    },
}

Supply corresponding ticket metadata such as meta = { mode = "ranked", skill = 1200 }. Keep the player-count and mode rules outside the or so the timeout relaxes only skill proximity.

Design for your population

Rules can be valid yet impossible to satisfy with the available parties. An exact four-player queue with many three-player parties needs enough solo parties to complete them. A capped skill window cannot guarantee a match if no compatible players arrive.

Watch wait time, incoming ticket rate, and match rate together. Use gradual relaxation for preferences, retain hard requirements explicitly, and test both sparse queues and mixed party sizes before release.