tango

package module
v0.0.0-...-ce47a45 Latest Latest
Warning

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

Go to latest
Published: Jun 4, 2025 License: MIT Imports: 7 Imported by: 0

README

Tango

codecov

⚠️ Heads Up

This is an experimental/toy project. I built it to explore some ideas around matchmaking using concurrent patterns in Go. While it works, I wouldn't recommend using it in production. Feel free to poke around and maybe grab some ideas for your own projects.

How Tango Works

Flow Overview

When you start Tango, it spins up three main background processes:

  1. An operation processor that handles new players/hosts and other match-related operations
  2. A timeout checker that removes players with expired matching timeouts
  3. A stats updater that periodically collects matchup statistics

Note: The diagrams below only show the operation processor and timeout checker for simplicity.

                               ┌── Host → Creates new match
Player Enqueue → Operation ────┤
                               └── Player → Attempts to join existing match

Players can be either hosts (create matches) or joiners (look for matches). When a host joins:

  • A match is created immediately
  • Tango starts actively looking for suitable players to join this match

When a regular player joins, Tango keeps trying to find a suitable match until either:

  • A match is found
  • The player times out
  • The context is cancelled
Matchmaking Flow
%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'arial', 'fontSize': '16px', 'textColor': '#2A4365' }}}%%
flowchart TB
    subgraph Matchmaking
        Start[Start Tango] --> Queue{Player Queue}
        Queue -->|Host| CreateMatch[Create Match]
        Queue -->|Player| WorkerPool[Worker Pool]
        
        CreateMatch -->|From Pool| NewMatch[New Match]
        NewMatch --> SeekPlayers[Seek Players]
        
        WorkerPool -->|Attempts| FindMatch[Find Match]
        FindMatch -->|Success| JoinMatch[Join Match]
        FindMatch -->|No Match| Retry((Retry))
        Retry --> FindMatch
        
        SeekPlayers -->|Match Found| JoinMatch
        
        JoinMatch --> Stats[Update Stats]
    end

    style Start fill:#48BB78,color:#2F855A
    style Queue fill:#667EEA,color:#F7FAFC
    style WorkerPool fill:#9F7AEA,color:#F7FAFC
    style JoinMatch fill:#48BB78,color:#2F855A
    style Stats fill:#4299E1,color:#F7FAFC
    style Matchmaking fill:#EBF8FF,color:#2C5282
Cleanup Flow
%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'arial', 'fontSize': '16px', 'textColor': '#2A4365' }}}%%
flowchart TB
    subgraph Cleanup
        Timer[Timeout Checker] --> CheckPlayers{Check Players}
        CheckPlayers -->|Expired| RemovePlayer[Remove Player]
        CheckPlayers -->|Active| Continue[Continue Checking]
        
        RemovePlayer -->|Is Host| CleanMatch[Cleanup Match]
        RemovePlayer -->|Not Host| DeletePlayer[Delete Player]
        
        CleanMatch --> Events[Emit Events]
        CleanMatch --> ReturnMatchPool[Return Match to Pool]
        CleanMatch --> UpdateStats[Update Stats]
        
        DeletePlayer --> Events
        DeletePlayer --> UpdateStats
    end

    style Timer fill:#4299E1,color:#F7FAFC
    style ReturnMatchPool fill:#ED64A6,color:#F7FAFC
    style CleanMatch fill:#9F7AEA,color:#F7FAFC
    style DeletePlayer fill:#ED8936,color:#F7FAFC
    style Events fill:#48BB78,color:#2F855A
    style UpdateStats fill:#667EEA,color:#F7FAFC
    style Cleanup fill:#F0FFF4,color:#2C5282
Setup and Lifecycle
import "github.com/alesr/tango"

// Create a new Tango instance
tango := tango.New(
    tango.WithOperationBufferSize(100),
    tango.WithDefaultTimeout(5*time.Second),
    // More options available...
)

// Start the service
err := tango.Start()

// Shut it down!
err := tango.Shutdown(ctx)
Player Management
// Create a new player
player := tango.NewPlayer(
    "player-1",     // ID
    false,          // IsHost
    tango.Mode1v1,  // Game mode
    deadline,       // Timeout (how long it should look for a match)
    []string{"tag"} // Optional tags (not in use yet)
)

// Add to matchmaking queue
err := tango.Enqueue(ctx, player)

// Remove from system
err := tango.RemovePlayer("player-1")
Match Operations
// Get all active matches
matches := tango.ListMatches()
Stats

You can call the Stats() method at any time to retrieve match statistics:

stats, _ := tango.Stats(ctx)

fmt.Printf("Total Players: %d\n", stats.TotalPlayers)
fmt.Printf("Total Matches: %d\n", stats.TotalMatches)
Configuration Options
  • WithLogger: Custom logger for the service
  • WithOperationBufferSize: Size of the operation channel buffer
  • WithMatchBufferSize: Size of the match channel buffer
  • WithAttemptToJoinFrequency: How often to try matching players
  • WithCheckDeadlinesFrequency: How often to check for timeouts
  • WithDefaultTimeout: Default operation timeout
Game Modes
  • GameMode1v1: 1 host + 1 player
  • GameMode2v2: 1 host + 3 players
  • GameMode3v3: 1 host + 5 players

Future Improvements

Some cool ideas I'd like to explore:

  • Use tags for smarter matching (skill levels, regions, etc.)
  • Add match status notifications (websockets maybe?)
  • Match lifetime management (auto-cleanup after game ends)
  • Match history tracking
  • Support for tournament-style matchmaking
  • Custom matching rules
  • Support for teams

Got more ideas? I'd love to hear them!

Load Testing

To make sure Tango works well under different conditions, i've set up some load tests. These tests are there to simulate various scenarios and see how the system handles them.

Running Load Tests

You can run the load tests using this command:

make test-load

This will execute all the test cases in the loadtest package.

Configuring Load Tests

The load testing framework is flexible. You can adjust various parameters like the number of requests, concurrency level, duration, and more through a configuration struct:

type Config struct {
    Requests           int
    ConcurrentRequests int
    Duration           time.Duration
    StatsInterval      time.Duration
    RequestsPerSecond  float64
    Scenario           string
}
Example Scenarios

I have a few predefined scenarios to simulate different types of workloads:

  • High Concurrency: Simulates many concurrent requests with a specified rate limit.
  • Host Heavy: Focuses on creating matches where most players are hosts.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var AllGameModes = map[GameMode]struct{}{
	GameModeUnknown: {},
	GameMode1v1:     {},
	GameMode2v2:     {},
	GameMode3v3:     {},
}

AllGameModes contains a map of all available game modes

Functions

This section is empty.

Types

type Event

type Event struct {
	Type      EventType
	Timestamp time.Time
	PlayerID  string
	MatchID   string
	GameMode  GameMode
	Data      map[string]any // Additional context-specific data
}

type EventType

type EventType uint8
const (
	EventPlayerJoinedQueue EventType = iota + 1
	EventPlayerLeftQueue
	EventPlayerJoinedMatch
	EventPlayerSearchExpired
	EventMatchCreated
	EventMatchCompleted
	EventMatchPopulated // All slots filled
	EventMatchPlayerLeft
	EventMatchExpired
)

func (EventType) String

func (et EventType) String() string

String returns a human-readable representation of the event type

type GameMode

type GameMode uint8

GameMode represents different types of game modes available

const (
	GameModeUnknown GameMode = iota
	GameMode1v1
	GameMode2v2
	GameMode3v3
)

func (GameMode) String

func (g GameMode) String() string

String returns the string representation of a GameMode

type GameModeStats

type GameModeStats struct {
	ActiveMatches int
	OpenSlots     int
	PlayersJoined int
}

type InvalidQueueSizeError

type InvalidQueueSizeError struct{ TangoError }

Enumerate package errors.

type Option

type Option func(*Tango)

Options defines the function for applying optional configuration to the Tango instance.

func WithAttemptToJoinFrequency

func WithAttemptToJoinFrequency(frequency time.Duration) Option

WithAttemptToJoinFrequency sets the frequency for matching attempts.

func WithCheckDeadlinesFrequency

func WithCheckDeadlinesFrequency(frequency time.Duration) Option

WithCheckDeadlinesFrequency sets the frequency for checking player deadlines.

func WithDefaultTimeout

func WithDefaultTimeout(timeout time.Duration) Option

WithDefaultTimeout sets the default operation timeout.

func WithLogger

func WithLogger(logger *slog.Logger) Option

WithLogger sets the logger for Tango.

func WithMatchBufferSize

func WithMatchBufferSize(size int) Option

WithMatchBufferSize sets the buffer size for match channels.

func WithOperationBufferSize

func WithOperationBufferSize(size int) Option

WithOperationBufferSize sets the buffer size for operation channels.

func WithStatsUpdateInterval

func WithStatsUpdateInterval(interval time.Duration) Option

WithStatsUpdateInterval sets how frequently stats are updated

func WithWorkerPool

func WithWorkerPool(numWorkers, jobBufferSize int) Option

WithWorkerPool sets the configuration for the matchmaking worker pool.

type Player

type Player struct {
	ID        string
	IsHosting bool
	GameMode  GameMode
	// contains filtered or unexported fields
}

Player represents a player attempting to join or host a match.

func NewPlayer

func NewPlayer(id string, isHosting bool, gameMode GameMode, timeout time.Time, tags []string) Player

NewPlayer creates a new Player instance.

func (Player) String

func (p Player) String() string

String returns a string representation of the Player.

type PlayerAlreadyEnqueuedError

type PlayerAlreadyEnqueuedError struct{ TangoError }

Enumerate package errors.

type PlayerNotFoundError

type PlayerNotFoundError struct{ TangoError }

Enumerate package errors.

type ServiceAlreadyStartedError

type ServiceAlreadyStartedError struct{ TangoError }

Enumerate package errors.

type ServiceNotStartedError

type ServiceNotStartedError struct{ TangoError }

Enumerate package errors.

type Stats

type Stats struct {
	MatchesByGameMode map[GameMode]GameModeStats
	TotalMatches      int
	TotalPlayers      int
}

Stats holds information about the current state of matches

type Tango

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

Tango manages players, matches, and matchmaking.

func New

func New(opts ...Option) *Tango

New creates and initializes a new Tango instance with the provided options.

func (*Tango) Enqueue

func (t *Tango) Enqueue(ctx context.Context, player Player) error

Enqueue adds a player to the matchmaking queue.

func (*Tango) ListMatches

func (t *Tango) ListMatches() ([]*match, error)

ListMatches returns a list of all active matches.

func (*Tango) RemovePlayer

func (t *Tango) RemovePlayer(playerID string) error

RemovePlayer removes a player from the system.

func (*Tango) Shutdown

func (t *Tango) Shutdown(ctx context.Context) error

Shutdown gracefully shuts down the service.

func (*Tango) Start

func (t *Tango) Start() error

Start initializes and starts all background processing.

func (*Tango) Stats

func (t *Tango) Stats(ctx context.Context) (Stats, error)

Stats returns current statistics about matches and players

type TangoError

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

TangoError represents a base error type for the package.

func (TangoError) Error

func (e TangoError) Error() string

Error returns the string representation to a TangoErroor.

Directories

Path Synopsis
pkg

Jump to

Keyboard shortcuts

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