flexi

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 27, 2026 License: MIT Imports: 12 Imported by: 0

README

flexi

Go Reference

flexi is an in-memory matchmaking library for Go that is compatible with Amazon GameLift FlexMatch rule sets.

It accepts the same JSON rule set you would pass to AWS's CreateMatchmakingRuleSet API and evaluates tickets locally — no GameLift hosting, no AWS connectivity, no networking. Drop the rule set in, enqueue tickets, drive the matchmaker, get matches.

Features

  • AWS-compatible JSON: feed the same rule set documented in the FlexMatch developer guide, including the AWS property-expression dialect (teams[red].players.attributes[skill]). ruleLanguageVersion is required and must be "1.0".
  • All eight rule kinds: comparison, distance, absoluteSort, distanceSort, batchDistance, collection, latency, compound.
  • All four player attribute types: string, number, string_list, string_number_map, with default values applied to players that omit an attribute. A value whose kind disagrees with the declared type is rejected at Enqueue; undeclared attributes pass through.
  • Property-expression aggregations: min, max, avg, median, sum, count, stddev, flatten, set_intersection, with per-team nesting (avg(teams[*].players.attributes[skill]) → one result per team).
  • partyAggregation: min / max / avg (and union / intersection for collection) on multi-player tickets.
  • Compound statements: AWS string form with and / or / not / xor, e.g. "or(and(A,B), not(C))".
  • Algorithm block: exhaustiveSearch and balanced strategies, batchingPreference (random / sorted / largestPopulation / fastestRegion), sortByAttributes, backfillPriority, expansionAgeSelection.
  • Expansions: rule values, team sizes (teams[Red,Blue].minPlayers), and algorithm fields loosen automatically as tickets wait.
  • Ticket status & player acceptance: FlexMatch-compatible lifecycle (QUEUEDREQUIRES_ACCEPTANCEPLACINGCOMPLETED, plus SEARCHING / CANCELLED / TIMED_OUT) driven by acceptanceRequired / acceptanceTimeoutSeconds / requestTimeoutSeconds on the rule set. On a failed acceptance (reject or acceptance timeout) the tickets that did accept return to SEARCHING for re-matching while the rest are CANCELLED; TIMED_OUT is reserved for the request-level requestTimeoutSeconds, mirroring AWS.
  • Injectable clock: tests advance time deterministically, no time.Sleep.
  • Zero external dependencies at runtime (testify is test-only). No network or persistence.
  • Goroutine-safe: producers may Enqueue / Cancel / Accept / Reject while another goroutine drives Tick.

Backfill of in-progress matches is intentionally out of scope.

Installation

go get github.com/moepig/flexi

Requires Go 1.26 or later.

Quick start

package main

import (
    "fmt"
    "github.com/moepig/flexi"
)

const ruleset = `{
  "name": "skill-balance",
  "ruleLanguageVersion": "1.0",
  "playerAttributes": [{"name": "skill", "type": "number"}],
  "teams": [
    {"name": "red",  "minPlayers": 2, "maxPlayers": 2},
    {"name": "blue", "minPlayers": 2, "maxPlayers": 2}
  ],
  "rules": [
    {
      "name": "FairSkill",
      "type": "distance",
      "measurements": ["avg(teams[red].players.attributes[skill])"],
      "referenceValue": "avg(teams[blue].players.attributes[skill])",
      "maxDistance": 10
    }
  ]
}`

func main() {
    mm, err := flexi.New([]byte(ruleset))
    if err != nil {
        panic(err)
    }

    for i, skill := range []float64{50, 52, 49, 51} {
        _ = mm.Enqueue(flexi.Ticket{
            ID: fmt.Sprintf("t%d", i),
            Players: []flexi.Player{{
                ID:         fmt.Sprintf("p%d", i),
                Attributes: flexi.Attributes{"skill": flexi.Number(skill)},
            }},
        })
    }

    matches, _ := mm.Tick()
    for _, m := range matches {
        fmt.Println("red:",  m.Teams["red"])
        fmt.Println("blue:", m.Teams["blue"])
    }
}

Driving the matchmaker

Matchmaker does not start any goroutines or timers — the caller drives it by invoking Tick(). This keeps tests deterministic and lets you integrate with whatever scheduling, observability, and shutdown story you already have.

ticker := time.NewTicker(time.Second)
defer ticker.Stop()

for range ticker.C {
    matches, err := mm.Tick()
    if err != nil {
        log.Printf("flexi tick: %v", err)
        continue
    }
    for _, m := range matches {
        dispatch(m) // hand off to your game server
    }
}

Tick returns every match that can be formed at this moment. Tickets consumed by a returned match are removed from the queue atomically; everything else stays queued for a future tick.

Time and expansions

Anything time-dependent — most importantly the FlexMatch expansions block — reads the current time through a Clock. The default is the system clock; tests should pass WithClock(NewFakeClock(...)) so they can advance time without sleeping.

clock := flexi.NewFakeClock(time.Now())
mm, _ := flexi.New(rulesetJSON, flexi.WithClock(clock))

mm.Enqueue(ticket)
matches, _ := mm.Tick()           // before any expansion step

clock.Advance(60 * time.Second)
matches, _ = mm.Tick()            // expansion steps with waitTimeSeconds<=60 applied

A richer rule set example

{
  "name": "example",
  "ruleLanguageVersion": "1.0",
  "playerAttributes": [
    {"name": "skill", "type": "number"},
    {"name": "modes", "type": "string_list"}
  ],
  "algorithm": {
    "strategy": "balanced",
    "balancedAttribute": "skill"
  },
  "teams": [
    {"name": "team", "minPlayers": 3, "maxPlayers": 3, "quantity": 2}
  ],
  "rules": [
    {
      "name": "FairSkill",
      "type": "distance",
      "measurements": ["avg(teams[team_1].players.attributes[skill])"],
      "referenceValue": "avg(teams[team_2].players.attributes[skill])",
      "maxDistance": 10
    },
    {
      "name": "ModeOverlap",
      "type": "collection",
      "measurements": ["set_intersection(players.attributes[modes])"],
      "operation": "reference_intersection_count",
      "referenceValue": ["TDM", "CTF", "FFA"],
      "minCount": 1
    },
    {"name": "Ping", "type": "latency", "maxLatency": 150},
    {
      "name": "All",
      "type": "compound",
      "statement": "and(FairSkill, ModeOverlap, Ping)"
    }
  ],
  "expansions": [
    {
      "target": "rules[FairSkill].maxDistance",
      "steps": [
        {"waitTimeSeconds": 30, "value": 50},
        {"waitTimeSeconds": 60, "value": 200}
      ]
    }
  ]
}

FlexMatch compliance notes

A few rule types have subtle AWS semantics that flexi follows deliberately:

  • collection operations
    • contains — counts how many times the reference value appears across the (flattened) measurement and bounds that count with minCount/maxCount (e.g. "no more than 5 medics in a match"). With no bound it just requires the value to be present.
    • intersection — counts the values shared by every player's collection and takes no referenceValue (e.g. "all players share at least one game mode", minCount: 1).
    • reference_intersection_count — requires each player's collection to intersect the referenceValue collection within minCount/maxCount. The referenceValue may be a literal array or a property expression such as set_intersection(flatten(teams[*].players.attributes[preferredOpponents])).
  • batchDistance — a numeric attribute is grouped by spread (maxDistance); a string attribute is grouped by value equivalency, where the distance is (distinct values) - 1. A string batchDistance with no maxDistance therefore requires every player to share the same value (the AWS "SameGameMode" form). (minDistance is a distance-rule property and is not accepted on batchDistance.)
  • maxDistance / minDistance accept either a JSON number (500) or a string-encoded number ("500"); the AWS docs use both forms.

Ticket status and player acceptance

Every ticket has a FlexMatch-compatible TicketStatus, queryable with Matchmaker.Status(id):

Enqueue                        → QUEUED
Tick (acceptanceRequired=false) → PLACING                           (Match returned)
Tick (acceptanceRequired=true)  → REQUIRES_ACCEPTANCE               (Proposal created)
  all Accept, then Tick        → PLACING                           (Match returned)
  Cancel                       → CANCELLED                         (whole proposal)
  any Reject / acceptance      → rejecter/non-responder CANCELLED;
    timeout                      fully-accepted siblings SEARCHING (re-queued)
requestTimeoutSeconds elapsed  → TIMED_OUT
MarkCompleted (from PLACING)   → COMPLETED

SEARCHING marks a ticket that accepted a proposed match which then failed to gather every required acceptance: it is returned to the queue and re-matched by the next Tick. TIMED_OUT is reached only when a ticket exceeds the rule set's requestTimeoutSeconds while searching — an acceptance failure (reject or acceptance timeout) terminates the non-accepting tickets as CANCELLED, matching AWS FlexMatch. FAILED is defined for parity with the AWS API but is not produced by the current implementation.

Because this library operates as FlexMatch standalone (no game-session placement), the terminal success status Tick assigns is PLACING. Promote a ticket to COMPLETED with MarkCompleted(id) once your own placement pipeline has attached connection information.

Enable the acceptance flow by setting the two standard FlexMatch fields on the rule set:

{
  ...
  "acceptanceRequired": true,
  "acceptanceTimeoutSeconds": 60
}

Then drive the extra state machine:

matches, _ := mm.Tick()            // acceptanceRequired=true → no matches yet
for _, p := range mm.PendingAcceptances() {
    for _, id := range p.TicketIDs {
        for _, pl := range p.Teams[...] /* surface to your players */ {
            if accepted { mm.Accept(id, pl.ID) } else { mm.Reject(id, pl.ID) }
        }
    }
}
matches, _ = mm.Tick()             // fully-accepted proposals are now returned

When a proposed match fails acceptance — a player Rejects, or acceptanceTimeoutSeconds elapses before everyone responds — flexi follows AWS FlexMatch by splitting the proposal's tickets:

  • Tickets on which every player had already accepted return to the queue in SEARCHING and are re-considered by the next Tick. Matchmaker.StatusReason(id) reports StatusReasonAcceptanceFailed for them, so a caller polling Status can tell a re-entering ticket apart from a freshly enqueued one (and emit the corresponding MatchmakingSearching event).
  • The ticket(s) that caused the failure — the rejecting player's ticket, or any ticket whose players never responded — move to CANCELLED, whether the failure was a reject or an acceptance timeout. (FlexMatch reserves TIMED_OUT for the request-level deadline below, not for acceptance failures.) Resubmit with a fresh ticket ID for another attempt.
mm.Accept("a", "alice")
mm.Reject("b", "bob")              // proposal fails acceptance
mm.Status("a")                      // SEARCHING  (re-queued)
mm.StatusReason("a")                // StatusReasonAcceptanceFailed, true
mm.Status("b")                      // CANCELLED  (terminal)
mm.Tick()                           // re-matches "a" against the pool

A user-initiated Cancel on any participating ticket is different: it always dissolves the whole proposal, terminating every ticket as CANCELLED.

Request timeout

Set requestTimeoutSeconds on the rule set to bound how long a ticket may stay in matchmaking overall:

{
  ...
  "requestTimeoutSeconds": 60
}

Any QUEUED or re-queued (SEARCHING) ticket that has been waiting longer than this — measured from its original Enqueue, so the clock keeps running across an acceptance-failure re-queue — moves to TIMED_OUT on the next Tick. The deadline applies whether or not acceptanceRequired is set; tickets currently held in a proposal (REQUIRES_ACCEPTANCE) are governed by acceptanceTimeoutSeconds instead. Zero (the default) disables it.

Rule evaluation metrics

FlexMatch's match events (PotentialMatchCreated, MatchmakingTimedOut, MatchmakingCancelled) include ruleEvaluationMetrics — per-rule {ruleName, passedCount, failedCount} tallies. flexi reproduces these:

  • Every Match and Proposal carries RuleEvaluationMetrics, the pass/fail counts accumulated by the search that formed that (candidate) match. This maps to PotentialMatchCreated.
  • Matchmaker.RuleMetrics(id) returns the cumulative per-rule tallies for a ticket across every Tick it took part in, retained through terminal states. Use it for the MatchmakingTimedOut / MatchmakingCancelled events of tickets that never made it into a match.
matches, _ := mm.Tick()
for _, rm := range matches[0].RuleEvaluationMetrics {
    fmt.Printf("%s: passed=%d failed=%d\n", rm.RuleName, rm.PassedCount, rm.FailedCount)
}

// For a timed-out / cancelled ticket:
if metrics, ok := mm.RuleMetrics("ticket-1"); ok {
    // metrics[i].RuleName / PassedCount / FailedCount
}

Rule names match those declared in the rule set's rules block; a compound rule is reported once (its child evaluations are not listed separately). Each rule-set evaluation counts every rule (no short-circuit) so failedCount is complete. Tickets never involved in an evaluation report no metrics (the slice is nil / RuleMetrics returns false), keeping the addition backward compatible.

API at a glance

Full reference is on pkg.go.dev. The most-used surface:

Symbol Description
flexi.New(rulesetJSON, opts...) Parse a rule set JSON document and return a Matchmaker.
flexi.WithClock(c) Option that overrides the time source.
Matchmaker.Enqueue(t) Add a ticket to the queue.
Matchmaker.Cancel(id) Remove a queued ticket, or dissolve a proposal it is part of.
Matchmaker.Tick() Expire timed-out proposals, resolve accepted ones, and form new matches.
Matchmaker.Pending() Count of tickets currently in QUEUED.
Matchmaker.Status(id) Current TicketStatus for a ticket.
Matchmaker.StatusReason(id) Supplementary StatusReason (e.g. acceptance-failure re-queue), when one applies.
Matchmaker.RuleMetrics(id) Cumulative per-rule pass/fail tallies for a ticket (ruleEvaluationMetrics).
Matchmaker.PendingAcceptances() Snapshot of proposals in REQUIRES_ACCEPTANCE.
Matchmaker.Accept(id, playerID) / Reject(id, playerID) Record a player's decision on a proposed match.
Matchmaker.MarkCompleted(id) Promote a PLACING ticket to COMPLETED.
flexi.Number / String / StringList / StringNumberMap Constructors for the four Attribute variants.
flexi.NewFakeClock(t) Test clock you can Advance or Set.

Testing

go test ./... -race

Each layer (rule set parser, expression evaluator, rule evaluators, expansions, algorithm, queue) has its own unit tests. End-to-end scenarios driven through the public API live in examples_test.go. Tests use stretchr/testify.

License

See LICENSE for license terms.

Documentation

Overview

Package flexi implements an in-memory matchmaking engine compatible with Amazon GameLift FlexMatch rule sets.

The engine accepts the AWS FlexMatch rule set JSON document — the same payload passed to CreateMatchmakingRuleSet's RuleSetBody parameter — and evaluates matchmaking tickets against it, producing matches whose teams satisfy every configured rule.

Scope

flexi targets FlexMatch's "standalone" use case: pure rule evaluation with no GameLift hosting integration, no networking, and no persistence. The ticket queue is held in memory only.

The rule set must declare ruleLanguageVersion "1.0" (the only version AWS FlexMatch supports); a missing or different value is rejected.

Supported rule set features:

  • Player attribute types: string, number, string_list, string_number_map, with default values applied to players that omit an attribute. A value whose kind disagrees with the declared type is rejected at Matchmaker.Enqueue; attributes not declared in the rule set are carried through unchecked.
  • Property expressions in the AWS dialect, e.g. teams[red].players.attributes[skill]; aggregations min, max, avg, median, sum, count, stddev, flatten, set_intersection, with per-team nesting for multi-team scopes (teams[a,b], teams[*]).
  • Algorithm strategies: exhaustiveSearch, balanced (with balancedAttribute).
  • Algorithm batchingPreference (random, sorted, largestPopulation, fastestRegion), sortByAttributes, backfillPriority, expansionAgeSelection.
  • Teams with minPlayers, maxPlayers, and quantity (multi-instance teams).
  • All eight FlexMatch rule kinds: comparison, distance, absoluteSort, distanceSort, batchDistance, collection, latency, compound (with a statement string using and/or/not/xor).
  • partyAggregation (min/max/avg, or union/intersection for collection) for multi-player tickets.

A few rule types follow the AWS semantics precisely enough to be worth spelling out:

  • collection: "contains" counts how many times the reference value occurs in the measurement (bounded by minCount/maxCount); "intersection" counts the values shared by every player's collection and takes no referenceValue; "reference_intersection_count" requires each player's collection to intersect the reference value within minCount/maxCount.
  • batchDistance: a numeric attribute is grouped by spread (maxDistance); a string attribute is grouped by value equivalency, and with no maxDistance it requires every player to share one value.
  • maxDistance / minDistance accept either a JSON number or a string-encoded number (e.g. "500"), matching the inconsistent AWS documentation.
  • Time-driven expansions that loosen rule values, team sizes, or algorithm fields once a ticket has been waiting long enough.
  • Rule evaluation metrics (FlexMatch's ruleEvaluationMetrics): per-rule pass/fail tallies on each Match and Proposal, plus cumulative per-ticket totals via Matchmaker.RuleMetrics.

Backfill of in-progress matches is intentionally out of scope; the backfillPriority field is validated but does not change matching behaviour.

Quick start

mm, err := flexi.New(rulesetJSON)
if err != nil { ... }

mm.Enqueue(flexi.Ticket{
    ID: "ticket-1",
    Players: []flexi.Player{{
        ID: "alice",
        Attributes: flexi.Attributes{"skill": flexi.Number(1500)},
        Latencies:  map[string]int{"us-east-1": 35},
    }},
})

matches, err := mm.Tick()
for _, m := range matches {
    // m.Teams maps team name -> assigned players
    // m.TicketIDs lists tickets consumed by the match
}

Driving the matchmaker

Matchmaker has no internal goroutines or timers. Callers drive it by invoking Matchmaker.Tick, which returns every match that can be formed against the current queue. This keeps tests deterministic and lets callers integrate with whatever scheduling, observability, or shutdown story they already have. A typical production loop looks like:

t := time.NewTicker(time.Second)
for range t.C {
    matches, err := mm.Tick()
    // dispatch matches, log err
}

Time and expansions

Anything that depends on elapsed time (most importantly the FlexMatch "expansions" block) reads the current time through a Clock. The default is SystemClock; tests should pass WithClock with a FakeClock so they can advance time deterministically without sleeping.

Concurrency

All Matchmaker methods are safe for concurrent use. The queue is protected by an internal mutex, so producers may Enqueue/Cancel from any goroutine while another goroutine drives Tick. Tick itself is intended to be called from a single goroutine — concurrent ticks are safe but compete for the same ticket pool.

Index

Constants

View Source
const (
	// AttrUnknown is the zero value and should not appear in well-formed input.
	AttrUnknown = core.AttrUnknown
	// AttrString corresponds to FlexMatch's "string" attribute type.
	AttrString = core.AttrString
	// AttrNumber corresponds to FlexMatch's "number" attribute type.
	AttrNumber = core.AttrNumber
	// AttrStringList corresponds to FlexMatch's "string_list" attribute type.
	AttrStringList = core.AttrStringList
	// AttrStringNumberMap corresponds to FlexMatch's "string_number_map" type.
	AttrStringNumberMap = core.AttrStringNumberMap
)

AttributeKind constants. Use String, Number, StringList, or StringNumberMap to construct attributes rather than setting Kind by hand.

View Source
const (
	StatusQueued             = core.StatusQueued
	StatusSearching          = core.StatusSearching
	StatusRequiresAcceptance = core.StatusRequiresAcceptance
	StatusPlacing            = core.StatusPlacing
	StatusCompleted          = core.StatusCompleted
	StatusFailed             = core.StatusFailed
	StatusCancelled          = core.StatusCancelled
	StatusTimedOut           = core.StatusTimedOut
)
View Source
const (
	// StatusReasonAcceptanceFailed is set on a ticket that has returned to
	// [StatusSearching] because a proposed match it had accepted failed to
	// gather every required acceptance (a sibling player rejected or timed
	// out). It corresponds to FlexMatch's MatchmakingSearching event being
	// re-emitted with a status reason after a proposed match fails.
	StatusReasonAcceptanceFailed = core.StatusReasonAcceptanceFailed
)

Variables

View Source
var ErrDuplicateTicket = queue.ErrDuplicateTicket

ErrDuplicateTicket is returned by Matchmaker.Enqueue when a ticket with the same ID is already in the queue.

View Source
var ErrInvalidRuleSet = ruleset.ErrInvalidRuleSet

ErrInvalidRuleSet is returned by New when the rule set JSON is malformed or fails semantic validation. It is suitable for use with errors.Is:

if errors.Is(err, flexi.ErrInvalidRuleSet) { ... }
View Source
var ErrTicketNotPlacing = errors.New("flexi: ticket is not in PLACING")

ErrTicketNotPlacing is returned by MarkCompleted when the ticket is not in StatusPlacing.

View Source
var ErrUnknownPlayer = errors.New("flexi: player is not part of ticket")

ErrUnknownPlayer is returned by Accept / Reject when the player id is not part of the referenced ticket.

View Source
var ErrUnknownProposal = errors.New("flexi: ticket is not in a pending proposal")

ErrUnknownProposal is returned by Accept / Reject when the ticket is not part of any active proposal (either it was never proposed, or the proposal has already been resolved).

View Source
var ErrUnknownTicket = queue.ErrUnknownTicket

ErrUnknownTicket is returned by Matchmaker.Cancel or Matchmaker.Status when no ticket with the given ID is currently tracked.

Functions

This section is empty.

Types

type Attribute

type Attribute = core.Attribute

Attribute is a tagged union mirroring a single FlexMatch player attribute value. Only the field selected by Kind is meaningful; other fields are zero.

Construct attributes with String, Number, StringList, or StringNumberMap rather than building the struct literally.

func Number

func Number(v float64) Attribute

Number returns an Attribute of kind AttrNumber holding v.

func String

func String(v string) Attribute

String returns an Attribute of kind AttrString holding v.

func StringList

func StringList(v ...string) Attribute

StringList returns an Attribute of kind AttrStringList holding a copy of v. Subsequent mutations of v will not affect the returned attribute.

func StringNumberMap

func StringNumberMap(v map[string]float64) Attribute

StringNumberMap returns an Attribute of kind AttrStringNumberMap holding a copy of v. Subsequent mutations of v will not affect the returned attribute.

type AttributeKind

type AttributeKind = core.AttributeKind

AttributeKind identifies which variant an Attribute holds. It mirrors the four player attribute types FlexMatch supports: string, number, string_list, and string_number_map.

type Attributes

type Attributes = core.Attributes

Attributes is a player's attribute bag, keyed by attribute name. The keys must match the names declared in the rule set's playerAttributes block for the rules to find them.

type Clock

type Clock interface {
	// Now returns the current time as observed by this Clock.
	Now() time.Time
}

Clock is the source of "now" used by the matchmaker.

It exists so that callers — and especially tests — can control what time looks like. All time-dependent behaviour in flexi (computing how long a ticket has been waiting, deciding which expansion step is active) reads the current time through this interface rather than calling time.Now directly.

Implementations must be safe for concurrent use.

type FakeClock

type FakeClock struct {
	// contains filtered or unexported fields
}

FakeClock is a Clock whose value is set explicitly and never advances on its own. It is intended for tests: construct one with NewFakeClock, then call FakeClock.Advance or FakeClock.Set to move time forward between matchmaker ticks.

FakeClock is safe for concurrent use.

func NewFakeClock

func NewFakeClock(t time.Time) *FakeClock

NewFakeClock returns a FakeClock anchored at t.

Choose any wall-clock value you find convenient for diagnostics; flexi only looks at differences between Now() readings, never at absolute time.

func (*FakeClock) Advance

func (c *FakeClock) Advance(d time.Duration)

Advance moves the FakeClock forward by d.

Use this between calls to Matchmaker.Tick in tests to simulate the passage of time and trigger expansion steps.

func (*FakeClock) Now

func (c *FakeClock) Now() time.Time

Now returns the FakeClock's current value.

func (*FakeClock) Set

func (c *FakeClock) Set(t time.Time)

Set replaces the FakeClock's current value with t.

Useful when a test needs to jump to an absolute moment rather than a relative offset.

type Match

type Match = core.Match

Match is a successful pairing of tickets into a complete game.

Teams maps the team name (as it appeared in the rule set) to the players assigned to that team. When a rule set declares quantity > 1 for a team, the resulting names are suffixed with "_1", "_2", and so on. TicketIDs lists every ticket consumed to form the match, sorted lexicographically for stable test output.

type Matchmaker

type Matchmaker struct {
	// contains filtered or unexported fields
}

Matchmaker forms matches from in-memory tickets according to a parsed FlexMatch rule set.

Construct one with New, add tickets with Matchmaker.Enqueue, and call Matchmaker.Tick periodically to drain matches that are now satisfiable.

When the rule set sets acceptanceRequired=true, Tick produces Proposal candidates instead of final [Match]es. Callers route each proposal through Matchmaker.Accept / Matchmaker.Reject and drive Matchmaker.Tick again; once every player on every ticket of a proposal has accepted, the next Tick returns the corresponding Match.

If a proposed match fails acceptance — a player rejects, or the acceptanceTimeoutSeconds window elapses — flexi mirrors AWS FlexMatch: tickets on which every player had accepted return to the queue in StatusSearching (carrying StatusReasonAcceptanceFailed) for the next Tick to re-match, while the ticket(s) that caused the failure (a player who rejected or never responded) move to StatusCancelled.

Independently, a ticket that stays in matchmaking longer than the rule set's requestTimeoutSeconds fails with StatusTimedOut. This request-level deadline applies to queued and re-queued tickets regardless of acceptanceRequired.

Matchmaker has no internal goroutines or timers — all work happens on the goroutine that calls Tick. The queue, status map, and proposals are protected by a mutex so producers may Enqueue/Cancel/Accept concurrently with a ticking loop.

func New

func New(rulesetJSON []byte, opts ...Option) (*Matchmaker, error)

New parses a FlexMatch rule set JSON document and returns a Matchmaker ready to accept tickets.

rulesetJSON must be the same JSON body accepted by GameLift's CreateMatchmakingRuleSet API (the RuleSetBody parameter). Parsing or validation failures are reported as errors that wrap ErrInvalidRuleSet.

Options may be supplied to override defaults; see WithClock.

func (*Matchmaker) Accept

func (m *Matchmaker) Accept(ticketID, playerID string) error

Accept records that playerID, who must be part of the ticket ticketID, has accepted the proposed match. Once every player on every ticket in the proposal has accepted, the proposal is resolved on the next [Tick]: the tickets move to StatusPlacing and the corresponding Match is returned.

Returns ErrUnknownTicket if the ticket is not tracked, ErrUnknownProposal if the ticket is not currently in a pending proposal, or ErrUnknownPlayer if playerID is not a member of the ticket.

func (*Matchmaker) Cancel

func (m *Matchmaker) Cancel(ticketID string) error

Cancel removes the ticket with the given ID from the matchmaker and marks it StatusCancelled. It returns ErrUnknownTicket if no such ticket is currently tracked. A ticket that is queued (StatusQueued) or has been re-queued after a failed acceptance (StatusSearching) is removed directly.

If the ticket is part of an active proposal, the entire proposal is torn down: every sibling ticket is also marked StatusCancelled, matching FlexMatch's behaviour when any member of a proposed match drops out. Unlike a reject or acceptance timeout, a user-initiated cancel never re-queues a sibling, even one that had already accepted.

Cancelling a ticket that has already been consumed by a match (and is now StatusPlacing or StatusCompleted) is rejected with ErrUnknownTicket.

func (*Matchmaker) Enqueue

func (m *Matchmaker) Enqueue(t Ticket) error

Enqueue adds t to the matchmaking queue and records its status as StatusQueued.

The ticket's EnqueuedAt field is set from the configured Clock; any value supplied by the caller is overwritten so that wait-time calculations remain consistent. The ticket must have a non-empty ID and at least one player; otherwise an error is returned and the ticket is not enqueued.

Enqueue returns ErrDuplicateTicket (wrapped) if a ticket with the same ID is already queued, in a proposal, or in a recent terminal state.

func (*Matchmaker) MarkCompleted

func (m *Matchmaker) MarkCompleted(ticketID string) error

MarkCompleted transitions ticketID from StatusPlacing to StatusCompleted. This is the equivalent of a standalone-mode caller reporting that they have finished placing the match into a game session and connection details are now available on their side.

Returns ErrUnknownTicket if the ticket is not tracked or ErrTicketNotPlacing if it is in any other status.

func (*Matchmaker) Pending

func (m *Matchmaker) Pending() int

Pending returns the number of tickets currently in StatusQueued. Tickets held in a proposal (StatusRequiresAcceptance) are not counted.

func (*Matchmaker) PendingAcceptances

func (m *Matchmaker) PendingAcceptances() []Proposal

PendingAcceptances returns a snapshot of every proposal currently awaiting acceptance. The slice is newly allocated and safe for the caller to mutate.

func (*Matchmaker) Reject

func (m *Matchmaker) Reject(ticketID, playerID string) error

Reject records a player's rejection of a proposed match, ending the proposal. The proposal's tickets are then split following AWS FlexMatch: any ticket on which every player had already accepted is returned to the queue in StatusSearching (carrying StatusReasonAcceptanceFailed) so the next Matchmaker.Tick can re-match it, while every other ticket — including the one carrying the rejecting player and any whose players never responded — moves to StatusCancelled. An acceptance timeout (see Matchmaker.Tick) splits the proposal the same way.

Errors match Matchmaker.Accept.

func (*Matchmaker) RuleMetrics added in v0.0.2

func (m *Matchmaker) RuleMetrics(ticketID string) ([]RuleMetric, bool)

RuleMetrics returns the cumulative rule-evaluation metrics accumulated for ticketID across every Tick in which it participated in match formation, together with true. If the ticket has never been involved in an evaluation (for example it was cancelled while still only queued, or no such ticket is tracked), it returns nil and false.

The metrics support FlexMatch's MatchmakingTimedOut and MatchmakingCancelled parity: like [Status], they are retained through terminal states (TIMED_OUT, CANCELLED, PLACING, COMPLETED) and are not evicted automatically. Each entry is named after a top-level rule in the rule set; the returned slice is a copy the caller may mutate freely.

func (*Matchmaker) Status

func (m *Matchmaker) Status(ticketID string) (TicketStatus, error)

Status returns the current TicketStatus for ticketID. If no ticket with that id is currently tracked by the matchmaker, ErrUnknownTicket is returned.

Note that the matchmaker retains status for tickets past queue removal only while they are in a terminal state reachable from this matchmaker (CANCELLED, TIMED_OUT, PLACING, COMPLETED). Eviction of terminal state is not automatic; call sites should not rely on long-term retention.

func (*Matchmaker) StatusReason added in v0.1.0

func (m *Matchmaker) StatusReason(ticketID string) (StatusReason, bool)

StatusReason returns the supplementary StatusReason for ticketID together with true when one applies, and "", false otherwise.

A reason is currently set only while a ticket is in StatusSearching after a proposed match it had accepted failed to gather every required acceptance; in that case the reason is StatusReasonAcceptanceFailed. This lets a caller polling Matchmaker.Status distinguish a ticket re-entering matchmaking after a failed acceptance (FlexMatch's MatchmakingSearching with a status reason) from one that was freshly enqueued. The reason clears as soon as the ticket leaves StatusSearching (for example when the next Tick re-matches it).

func (*Matchmaker) Tick

func (m *Matchmaker) Tick() ([]Match, error)

Tick drives the matchmaker forward by one step.

In order, Tick:

  1. Expires any proposals whose acceptanceTimeoutSeconds has elapsed: their fully-accepted tickets return to the queue in StatusSearching and the rest move to StatusCancelled.
  2. Resolves any proposals that have been fully accepted, moving tickets to StatusPlacing and returning the corresponding Match values.
  3. Fails any queued or re-queued ticket that has been in matchmaking longer than requestTimeoutSeconds, moving it to StatusTimedOut.
  4. Applies FlexMatch expansions based on the oldest queued ticket's wait time.
  5. Runs the matching algorithm over the remaining queued tickets. When acceptanceRequired=false, each result becomes a Match returned in this tick; when true, each result is held as a new proposal and its tickets move to StatusRequiresAcceptance.

Tick returns nil, nil when nothing resolves in this step. A non-nil error indicates a configuration problem (for example, an expansion targeting a field that no longer exists).

type Option

type Option func(*config)

Option configures a Matchmaker at construction time. Pass any number of options to New.

func WithClock

func WithClock(c Clock) Option

WithClock injects a custom Clock in place of the default SystemClock. Tests should pass a FakeClock so that expansions and ticket wait times can be controlled deterministically.

type Player

type Player = core.Player

Player is a single participant in a Ticket. ID identifies the player within a match (it is echoed back in Match.Teams). Attributes carry the values referenced by rule expressions; their keys must match names declared in the rule set's playerAttributes block. Latencies maps an AWS region name to the player's measured latency in milliseconds; it is consulted by latency rules.

Players are passed by value and may be safely re-used across tickets.

type Proposal

type Proposal struct {
	Teams     map[string][]core.Player
	TicketIDs []string
	CreatedAt time.Time
	// RuleEvaluationMetrics holds the per-rule pass/fail tallies accumulated
	// by the search that formed this candidate match, for PotentialMatchCreated
	// parity. It is nil when the rule set declares no rules.
	RuleEvaluationMetrics []core.RuleMetric
}

Proposal describes a candidate match awaiting player acceptance. It is returned by Matchmaker.PendingAcceptances so callers can surface the pending decision to their players.

Teams mirrors [Match.Teams] for the candidate. TicketIDs lists every ticket participating in the proposal. CreatedAt is the clock time at which the proposal was formed; together with the rule set's acceptanceTimeoutSeconds it determines when the proposal times out.

type RuleMetric added in v0.0.2

type RuleMetric = core.RuleMetric

RuleMetric reports how many times a rule passed or failed while matchmaking processed a set of tickets. It corresponds to one entry of the ruleEvaluationMetrics array in FlexMatch's PotentialMatchCreated, MatchmakingTimedOut, and MatchmakingCancelled events. RuleName matches a rule name declared in the rule set. It is exposed on Match and Proposal for a formed/candidate match, and via Matchmaker.RuleMetrics for the cumulative per-ticket totals.

type StatusReason added in v0.1.0

type StatusReason = core.StatusReason

StatusReason gives supplementary context for a ticket's status, mirroring MatchmakingTicket.StatusReason in the GameLift API. It is reported by Matchmaker.StatusReason.

type SystemClock

type SystemClock struct{}

SystemClock is the default Clock; it returns the wall-clock time from the standard library's time.Now.

SystemClock has no state and a zero value is ready to use.

func (SystemClock) Now

func (SystemClock) Now() time.Time

Now returns time.Now().

type Ticket

type Ticket = core.Ticket

Ticket is a matchmaking request submitted to the engine. A ticket may contain a single player (a "solo" request) or several players who must be matched together as a party.

ID must be unique among queued tickets. EnqueuedAt is filled in by Matchmaker.Enqueue from the configured Clock; any value set by the caller is overwritten.

type TicketStatus

type TicketStatus = core.TicketStatus

TicketStatus is the FlexMatch-compatible ticket lifecycle state.

Values mirror the MatchmakingTicket.Status values in the AWS GameLift API. Because this library runs FlexMatch in standalone mode (no game session placement), the terminal success status is StatusPlacing; callers that have attached connection information to the ticket may promote it to StatusCompleted via Matchmaker.MarkCompleted.

Transitions:

  • Enqueue → StatusQueued
  • Tick forms a candidate (acceptanceRequired=true) → StatusRequiresAcceptance
  • All players Accept → StatusPlacing (Match returned)
  • acceptanceRequired=false + Tick → StatusPlacing (Match returned)
  • MarkCompleted → StatusCompleted
  • Cancel on queued / searching → StatusCancelled
  • Reject / acceptance timeout: rejecting or non-responding ticket → StatusCancelled; fully-accepted siblings → StatusSearching (re-queued)
  • requestTimeoutSeconds elapsed → StatusTimedOut

StatusSearching is assigned to a ticket that is returned to the queue after a proposed match it accepted failed to gather every required acceptance (see Matchmaker.StatusReason); such a ticket is re-considered by the next Matchmaker.Tick. StatusTimedOut is reached when a ticket exceeds the rule set's requestTimeoutSeconds while in matchmaking — note that an acceptance failure (reject or acceptance timeout) does not produce it; those terminate as StatusCancelled, matching FlexMatch. StatusFailed is defined for parity with FlexMatch but is not produced by the current implementation.

Directories

Path Synopsis
internal
algorithm
Package algorithm builds matches from a queue of tickets according to a FlexMatch rule set's algorithm settings.
Package algorithm builds matches from a queue of tickets according to a FlexMatch rule set's algorithm settings.
core
Package core holds value types shared between the public flexi package and the internal sub-packages, defined here to break an import cycle.
Package core holds value types shared between the public flexi package and the internal sub-packages, defined here to break an import cycle.
expansion
Package expansion applies time-driven loosening of rule set values, as declared by the FlexMatch "expansions" block.
Package expansion applies time-driven loosening of rule set values, as declared by the FlexMatch "expansions" block.
expr
Package expr implements a parser and evaluator for FlexMatch property expressions used inside rule measurements and reference values, e.g.
Package expr implements a parser and evaluator for FlexMatch property expressions used inside rule measurements and reference values, e.g.
queue
Package queue is the in-memory ticket store used by the matchmaker.
Package queue is the in-memory ticket store used by the matchmaker.
rule
Package rule converts parsed ruleset.Rule entries into evaluators that answer "does this candidate match satisfy me?".
Package rule converts parsed ruleset.Rule entries into evaluators that answer "does this candidate match satisfy me?".
ruleset
Package ruleset parses and validates AWS GameLift FlexMatch rule set JSON documents (the same payload accepted by CreateMatchmakingRuleSet).
Package ruleset parses and validates AWS GameLift FlexMatch rule set JSON documents (the same payload accepted by CreateMatchmakingRuleSet).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL