mission-gaming/tactician

A modern PHP library for generating structured tournament schedules with deterministic algorithms like Round Robin, Swiss, and Pool play

Maintainers

Package info

github.com/mission-gaming/tactician

pkg:composer/mission-gaming/tactician

Transparency log

Statistics

Installs: 1 695

Dependents: 0

Suggesters: 0

Stars: 1

Open Issues: 0


README

PHP Version License Build Status codecov

Overview

A modern PHP library for generating structured schedules between participants. Ideal for tournaments (round robin, Swiss, pools) but flexible enough for any scenario where entities need to be paired or grouped into events.

Key Features:

  • ๐Ÿ† Tournament Formats: Round robin (single & multi-leg), Swiss pairing, single & double elimination, group stages
  • ๐Ÿ“Š Results & Standings: Pluggable ranking strategies, league tables, and tiebreakers (wins, Buchholz, Sonnebornโ€“Berger)
  • ๐Ÿ”ง Flexible Constraints: Built-in and custom predicate-based constraint system
  • โœ… Schedule Validation: Comprehensive validation prevents incomplete schedules
  • ๐Ÿ’พ Serialization: JSON round-tripping for schedules and participants
  • ๐ŸŽฏ Modern PHP: PHP 8.3+ with readonly classes and strict typing
  • ๐Ÿงช Test-Driven: Comprehensive test suite with Pest framework
  • ๐Ÿ“ Mathematical Accuracy: Circle method implementation for round-robin
  • ๐Ÿ›ก๏ธ Production Ready: PHPStan level 8 compliance with zero errors

Installation

Install via Composer:

composer require mission-gaming/tactician

Requirements:

  • PHP 8.3+
  • No external dependencies in production

Quick Start

<?php

use MissionGaming\Tactician\DTO\Participant;
use MissionGaming\Tactician\Scheduling\RoundRobinScheduler;
use MissionGaming\Tactician\Constraints\ConstraintSet;
use MissionGaming\Tactician\Constraints\SeedProtectionConstraint;

// Create seeded participants
$participants = [
    new Participant('celtic', 'Celtic', 1),        // Top seed
    new Participant('athletic', 'Athletic Bilbao', 2),  // 2nd seed  
    new Participant('livorno', 'AS Livorno', 3),
    new Participant('redstar', 'Red Star FC', 4),
    new Participant('rayo', 'Rayo Vallecano', 5),
    new Participant('clapton', 'Clapton Community FC', 6),
];

// Configure constraints to protect top seeds from early meetings
$constraints = ConstraintSet::create()
    ->add(new SeedProtectionConstraint(2, 0.5))  // Protect top 2 seeds for 50% of tournament
    ->build();

// Generate schedule
$scheduler = new RoundRobinScheduler($constraints);
$schedule = $scheduler->schedule($participants);

// Iterate through matches
foreach ($schedule as $event) {
    $round = $event->getRound();
    echo ($round ? "Round {$round->getNumber()}" : "No Round") . ": ";
    echo "{$event->getParticipants()[0]->getLabel()} vs {$event->getParticipants()[1]->getLabel()}\n";
}

Beyond Round Robin

Results feed standings, and standings drive the incremental engines for Swiss pairing, elimination brackets, and multi-stage tournaments:

use MissionGaming\Tactician\DTO\Result;
use MissionGaming\Tactician\Scheduling\SwissPairingEngine;
use MissionGaming\Tactician\Stage\StageState;

$engine = new SwissPairingEngine(plannedRounds: 5);

// One driver loop covers every results-driven format
$state = StageState::start($participants);
while (!$engine->isComplete($state)) {
    $pairing = $engine->pairNextRound($state);
    $results = playRound($pairing); // application-side
    $state = $state->withRoundPlayed($pairing, $results);
}

// Every format finishes as an outcome you can select from
$outcome = $engine->getOutcome($state);
$table = $outcome->getStandings();

Single and double elimination (SingleEliminationEngine, DoubleEliminationEngine) drive through the same loop, and group stages compose from pools and progression selectors (PoolDistributor, RankRangeSelector, MatchOutcomeSelector) โ€” see the usage guide. Stage state and schedules serialize to JSON (StageState::toJson(), $schedule->toJson()), so platforms persist between rounds instead of re-deriving.

Key Features

  • ๐Ÿ† Round Robin Tournaments: Circle method algorithm with balanced home/away roles
  • โ™Ÿ๏ธ Swiss Pairing: Standings-aware Monrad pairing with repeat avoidance, bye rotation, home/away balancing, and withdrawal handling
  • ๐ŸฅŠ Elimination Brackets: Single and double elimination with positional fold seeding, byes, round labels, fixed or re-seeded paths, one- or two-legged ties, and optional grand-final reset
  • ๐ŸŸ๏ธ Pools & Progression: Serpentine-seeded pools, per-pool standings, and progression selectors with ahead-of-time composition validation
  • ๐Ÿ“Š Results & Standings: Pluggable ranking strategies (win/draw/loss presets included) with wins, Buchholz, and Sonnebornโ€“Berger tiebreakers
  • ๐Ÿ”ง Flexible Constraints: Built-in constraints (rest periods, seed protection, role limits, role balance, metadata rules) plus custom predicates
  • ๐Ÿ  Multi-Leg Support: Home/away leagues with mirrored, repeated, or shuffled strategies and first-class byes
  • โœ… Schedule Validation: Mathematical validation prevents incomplete tournaments, with automatic retries over alternative orderings when constraints reject a schedule
  • ๐Ÿ’พ Serialization: JSON round-tripping for schedules, events, and participants
  • ๐Ÿ›ก๏ธ Production Ready: PHPStan level 8 compliance, comprehensive test coverage
  • โšก Memory Efficient: Iterator-based patterns for large tournaments
  • ๐ŸŽฏ Deterministic: Seeded randomization for reproducible results

Documentation

๐Ÿ“š Complete Usage Guide - Comprehensive examples and patterns
๐Ÿงฉ Framework Integration - Wiring Tactician into Symfony and Laravel applications
๐Ÿ—๏ธ Architecture - Technical design and core components
๐Ÿ›ฃ๏ธ Roadmap - Detailed development phases and use cases
๐Ÿ“– Contributing Guidelines - Development setup and contribution process
๐Ÿ“š Background - Mission Gaming story and problem space details

Sponsorship

Tag1 Consulting

Initial development of this library was sponsored by Tag1 Consulting, the absolute legends.
Tag1 blog & Tag1TeamTalks Podcast

License

This project is licensed under the MIT License - see the LICENSE file for details.