A simple, robust, and extensible matchmaking system for Elixir applications, built on OTP principles.
MatchmakingEx provides a complete workflow for matching players, including a central queue, filter-based searching, and a two-way confirmation system where both players must accept the match.
- Simple API: Easy-to-use functions for finding matches and managing player state.
- Filter-Based Matching: Match players based on custom criteria like skill, rank, or game mode.
- Two-Way Match Confirmation: Ensures both players are ready by requiring them to accept a pending match.
- Configurable Timeouts: Matches are automatically cancelled if not confirmed within a specified time.
- Automatic Re-queuing: Players who accept a cancelled match are automatically put back in the queue.
- Robust and Scalable: Built with
GenServers,Supervisors, and aRegistryfor fault-tolerance and performance.
Add matchmaking_ex to your list of dependencies in mix.exs:
def deps do
[
{:matchmaking_ex, "~> 0.1.0"}
]
endThen run:
mix deps.getIntegrating MatchmakingEx into your application involves three main steps:
- Adding it to your supervision tree
- Starting player processes
- Handling the matchmaking flow
In your application.ex:
# lib/my_app/application.ex
defmodule MyApp.Application do
use Application
@impl true
def start(_type, _args) do
children = [
# Your other application processes
{Registry, keys: :unique, name: MatchmakingEx.Registry},
{DynamicSupervisor, strategy: :one_for_one, name: MatchmakingEx.PendingMatchSupervisor},
MatchmakingEx.Server
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
endEach user who wants to find a match needs their own dedicated Player process.
# When a player with the ID "player-123" connects:
{:ok, _pid} = MatchmakingEx.Player.start_link("player-123")# Simple search with no filters
MatchmakingEx.Player.search("player-123")
# Search with filters
filters = %{rank: "gold", mode: "1v1"}
MatchmakingEx.Player.search("player-456", filters)When a match is found, both players receive a :match_ready_for_confirmation message.
# Accept the match
MatchmakingEx.Player.accept("player-123")
# Or decline
MatchmakingEx.Player.decline("player-456")If both players accept, a :match_confirmed message is sent.
If one declines or times out, a :match_cancelled message is sent and accepted players are re-queued.
stateDiagram-v2
[*] --> WaitingConfirmation : Match Started
WaitingConfirmation --> MatchConfirmed : Both Accept (✅, ✅)
note right of MatchConfirmed
Result:
- Match Confirmed
- End of cycle
end note
WaitingConfirmation --> MatchRejected : P1 Rejects (❌, Any)
note right of MatchRejected
Result:
- P1 enters cooldown
- P2 returns to queue (if accepted)
- End of cycle
end note
WaitingConfirmation --> MatchRejected : P2 Rejects (Any, ❌)
note right of MatchRejected
Result:
- P2 enters cooldown
- P1 returns to queue (if accepted)
- End of cycle
end note
WaitingConfirmation --> MatchTimeout : Timeout (15s)
note right of MatchTimeout
Result:
- No cooldown applied
- Players who accepted return to queue
- End of cycle
end note
MatchConfirmed --> [*]
MatchRejected --> [*]
MatchTimeout --> [*]
This table defines the expected outcome of each possible combination of player responses.
| P1 | P2 | Status | P1 Cooldown | P2 Cooldown | P1 Back to Queue | P2 Back to Queue | Scenario |
|---|---|---|---|---|---|---|---|
| ✅ Accept | ✅ Accept | :confirmed | No | No | No | No | 1. Both Accept |
| ✅ Accept | ❌ Reject | :rejected | No | Yes | Yes | No | 2. P1 Accepts, P2 Rejects |
| ❌ Reject | ✅ Accept | :rejected | Yes | No | No | Yes | 3. P1 Rejects, P2 Accepts |
| ❌ Reject | ❌ Reject | :rejected | Yes | Yes | No | No | 4. Both Reject |
| ⏳ Timeout | ⏳ Timeout | :timeout | No | No | No | No | 5. Both Timeout |
| ✅ Accept | ⏳ Timeout | :timeout | No | No | Yes | No | 6. P1 Accepts, P2 Timeout |
| ⏳ Timeout | ✅ Accept | :timeout | No | No | No | Yes | 7. P1 Timeout, P2 Accepts |
| ❌ Reject | ⏳ Timeout | :rejected | Yes | No | No | No | 8. P1 Rejects, P2 Timeout |
| ⏳ Timeout | ❌ Reject | :rejected | No | Yes | No | No | 9. P1 Timeout, P2 Rejects |
- Confirmed Match: Only occurs when both players accept.
- Rejection Is Punitive: Any player who rejects is placed in cooldown and removed from the queue.
- Timeout Is Not Punitive: A timeout does not penalize the player.
- Innocent Requeue: Players who accept but the match fails are re-queued.
- Rejection Overrides Timeout: If one rejects while the other times out, the rejection takes precedence.
$ iex -S mix# 1. Start processes for two players
iex> MatchmakingEx.Player.start_link("aragorn")
{:ok, #PID<...>}
iex> MatchmakingEx.Player.start_link("gimli")
{:ok, #PID<...>}
# 2. Both search with compatible filters
iex> MatchmakingEx.Player.search("aragorn", %{quest: "fellowship"})
:ok
iex> MatchmakingEx.Player.search("gimli", %{quest: "fellowship"})
:ok
# Console output:
# [Player aragorn] Match ready for confirmation! (ID: 1511933547)
# Players: ["aragorn", "gimli"]
# You have 15 seconds to accept.
# To accept: MatchmakingEx.Player.accept("aragorn")
# To decline: MatchmakingEx.Player.decline("aragorn")