A modular tournament management system developed according to SOLID principles, accommodating n-player games, dynamic plugin loading, and various matchmaking strategies.
- N-Player Game Support - Accommodates matches with 2, 3, 4, or more players in parallel
- Dynamic Plugin System - Load user-defined matchmaking strategies and scoring calculators at runtime
- SOLID Architecture - Clean, readable codebase adhering to industry best practices
- Multiple Tournament Formats - Round-robin, single elimination, Swiss system, and free-for-all
- Flexible Scoring Systems - Standard, three-point, ranking-based, percentage, and custom calculators
- Persistent Storage - SQLite database with clean repository pattern implementation
- GUI Interface - Tkinter-based interface for complete tournament management
- Real-time Statistics - Comprehensive tracking of wins, losses, draws, points, and rankings
- Python 3.8 or later
- Flask
- Standard library only (no dependencies)
git clone https://github.com/yourusername/tournament-matchmaking-system.git
cd tournament-matchmaking-systempython tournament_app.py
tournament-matchmaking-system/
|──app.py # Flask GUI
|──templates/
└── index.html # GUI Template
├── tournament_core.py # Core interfaces and abstract base classes
├── tournament_strategies.py # Built-in matchmaking strategy implementations
├── tournament_calculators.py # Built-in points calculator implementations
├── tournament_repository.py # SQLite repository implementation
├── tournament_service.py # Business logic service layer
├── plugin_loader.py # Dynamic plugin loading system
├── tournament_app.py # GUI application entry point
├── plugins/ # Custom plugin directory
│ ├── example_strategy.py
│ └── example_calculator.py
├── tournament.db # SQLite database (auto-generated)
└── README.md
- Player Management - Add players to the system
- Tournament Creation - Initialize a new tournament
- Player Assignment - Add players to the tournament roster
- Round Creation - Choose matchmaking strategy and set parameters
- Result Recording - Input match results as they finish
- Statistics Review - Review current standings and player statistics
python tournament_app.pyOR
python app.pyGuarantees each player plays against every other player a single time. Appropriate for league tournaments where detailed matchups are necessary.
Configuration:
strategy = "roundrobin"
players_per_match = 2 # or n for combinationsOptimal Use Cases:
- League tournaments
- Fair play mandates
- Small to medium-sized player pools
Classic bracket-style knockout competition. Winners proceed; losers are removed from competition.
Configuration:
strategy = "knockout"
players_per_match = 2 # supports n-player eliminationsOptimal Use Cases:
- Championship brackets
- Time-limited tournaments
- Unambiguous winner determination
Matches players with the same performance history who have not faced each other before. Typically applied to chess tournaments.
Configuration:
strategy = "swiss"
players_per_match = 2Optimal Use Cases:
- Chess competitions
- Competitive esports leagues
- Big player bases
One match involving all participants at once. Applicable to battle royale or multiplayer celebration games.
Configuration:
strategy = "freeforall"
players_per_match = all_available_playersOptimal Use Cases:
- Battle royale games
- Party game events
- Multiplayer board games
| Calculator | Win Points | Draw Points | Loss Points | Primary Application |
|---|---|---|---|---|
| Standard | 1.0 | 0.5 | 0.0 | Chess, general competitions |
| Three-Point | 3.0 | 1.0 | 0.0 | Soccer/football leagues |
| Ranking | n-(rank-1) | N/A | N/A | Multi-player positional games |
| Percentage | (defeated/total)×100 | N/A | N/A | Battle royale formats |
| Custom | Configurable | Configurable | Configurable | Specialized requirements |
Define plugins/custom_strategy.py:
from tournament_core import IMatchmakingStrategy, Match, RoundConfig
from tournament_core import generate_id, now_iso
from typing import List, Dict, Any
class SkillBasedStrategy(IMatchmakingStrategy):
"""Pairs players by skill ratings."""
def __init__(self, repository):
self.repository = repository
def get_strategy_name(self) -> str:
return "skill_based"
def supports_players_per_match(self, n: int) -> bool:
return n == 2
def create_matches(self,
tournament_id: str,
round_id: str,
available_players: List[str],
config: RoundConfig) -> Dict[str, Any]:
# Get player statistics
stats = self.repository.get_stats(tournament_id)
player_scores = {s['player_id']: s['points'] for s in stats}
# Sort by performance
sorted_players = sorted(
available_players,
key=lambda p: player_scores.get(p, 0),
reverse=True
)
matches = []
while len(sorted_players) >= 2:
p1 = sorted_players.pop(0)
p2 = sorted_players.pop(0)
match = Match(
id=generate_id(),
round_id=round_id,
tournament_id=tournament_id,
player_ids=[p1, p2],
scheduled_at=now_iso(),
players_per_match=2
)
matches.append(match)
self.repository.save_match(match)
return {
"matches": matches,
"waiting_players": sorted_players,
"metadata": {"pairing_method": "skill_based"}
}Make plugins/custom_calculator.py:
from tournament_core import IPointsCalculator, Match, MatchResult
class WeightedPointsCalculator(IPointsCalculator):
"""Uses weighted scoring according to match complexity."""
def __init__(self, weight_factor: float = 1.5):
self.weight_factor = weight_factor
def get_calculator_name(self) -> str:
return "weighted"
def calculate_points(self,
player_id: str,
match: Match,
result: MatchResult) -> float:
base_points = 0.0
if result.is_draw:
base_points = 0.5
elif player_id in result.winner_ids:
base_points = 1.0
# Apply weight based on match complexity
complexity_factor = len(match.player_ids) / 2.0
weighted_points = base_points * complexity_factor * self.weight_factor
return weighted_pointsPlugins are automatically discovered when placed in the plugins/ directory. Click "Load Plugins" in the GUI or restart the application to activate new plugins.
Every component possesses a single, well-defined responsibility:
TournamentService: Orchestration of business logicSQLiteTournamentRepository: Operations of data persistenceRoundRobinStrategy: Round-robin matchmaking logic implementationStandardPointsCalculator: Points calculation logic implementation
The system is closed to modification of core behavior but open to extension via plugins.
Every strategy implementation is substitutable via the IMatchmakingStrategy interface. Every calculator is substitutable via the IPointsCalculator interface.
Interfaces are narrow and single-purpose:
IMatchmakingStrategy: Matchmaking actions onlyIPointsCalculator: Points calculation onlyITournamentRepository: Data actions only
High-level modules rely on abstractions instead of concrete implementations. Dependencies are injected via constructors.
┌─────────────────────────────────────────┐
│ Presentation Layer │
│ (Tkinter GUI Application) │
└───────────────────┬─────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Service Layer │
│ (TournamentService) │
│ Business Logic Orchestration │
└──┬──────────────┬──────────────┬────────┘
│ │ │
▼ ▼ ▼
┌────────┐ ┌──────────┐ ┌────────────┐
│Strategy│ │Calculator│ │ Repository │
│Registry│ │ Registry │ │ (SQLite) │
└────┬───┘ └─────┬────┘ └──────┬─────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────┐
│ Plugin System │
│ Dynamic Module Loading │
└─────────────────────────────────────────┘
CREATE TABLE players (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
created_at TEXT
);CREATE TABLE tournaments (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
created_at TEXT,
default_calculator TEXT DEFAULT 'standard'
);CREATE TABLE matches (
id TEXT PRIMARY KEY,
round_id TEXT,
tournament_id TEXT,
player_ids TEXT,
scheduled_at TEXT,
result TEXT,
winner_ids TEXT,
rankings TEXT,
auto_bye INTEGER DEFAULT 0,
players_per_match INTEGER DEFAULT 2
);CREATE TABLE stats (
player_id TEXT,
tournament_id TEXT,
wins REAL DEFAULT 0,
draws REAL DEFAULT 0,
losses REAL DEFAULT 0,
matches_played INTEGER DEFAULT 0,
points REAL DEFAULT 0,
PRIMARY KEY(player_id, tournament_id)
);from abc import ABC, abstractmethod
class IMatchmakingStrategy(ABC):
@abstractmethod
def get_strategy_name(self) -> str:
""""Returns whether this strategy supports n-players per match.""""
pass
@abstractmethod
def supports_players_per_match(self, n: int) -> bool:
""""Determines if strategy supports n-player matches.""".
pass
@abstractmethod
def create_matches(self,
tournament_id: str,
round_id: str,
available_players: List[str],
config: RoundConfig) -> Dict[str, Any]:
"""
Constructs matches for a tournament round.
Returns:
Dictionary with:
- matches: List[Match]
- waiting_players: List[str]
- metadata: Dict[str, Any]
"""
passfrom abc import ABC, abstractmethod
class IPointsCalculator(ABC):
"""
Points calculator abstract interface
"""
@abstractmethod
def get_calculator_name(self) -> str:
"""Returns unique identifier for this calculator."""
pass
@abstractmethod
def calculate_points(self,
player_id: str,
match: Match,
result: MatchResult) -> float:
"""Calculates points earned by player in given match."""
pass# Player Operations
def create_player(name: str) -> str
def list_players() -> List[Player]
# Tournament Operations
def create_tournament(name: str) -> str
def add_player_to_tournament(tournament_id: str, player_id: str) -> None
def get_standings(tournament_id: str) -> List[Dict[str, Any]]
# Round Operations
def create_round(config: RoundConfig) -> Dict[str, Any]
def record_match_result(match_id: str, result: MatchResult) -> None
# Plugin Operations
def list_available_strategies() -> List[str]
def list_available_calculators() -> List[str]
def get_strategies_for_player_count(n: int) -> List[str]
def set_default_calculator(calculator_name: str) -> NoneStrategy: Swiss System
Players per Match: 2
Calculator: Standard (1 point per win, 0.5 per draw)
Number of Rounds: 7-9 rounds
Strategy: Round Robin
Players per Match: 4
Calculator: Ranking (1st=4pts, 2nd=3pts, 3rd=2pts, 4th=1pt)
Number of Rounds: All combinations
Strategy: Knockout
Players per Match: 2 (or team size)
Calculator: Three-Point
Number of Rounds: Log2(n) rounds to finals
Strategy: Free-for-All
Number of Players: Multiple qualification rounds
Players per Match: All participants or large groups
Calculator: Percentage-based placement scoring
Symptom: Custom plugin does not show up in strategy/calculator dropdown
Resolution:
- Check file is present in
plugins/directory - Check class is subclassing correct interface
- Check constructor signature is correct
- Press "Load Plugins" button or restart app
- Look for error messages in console output
Symptom: Cannot create new tournament round
Resolution:
- Confirm chosen strategy accommodates configured number of players
- Confirm adequate number of active players
- Finish all outstanding matches from last rounds
- Confirm tournament state is valid for new rounds
Symptom: Standings show inexact point counts
Resolution:
- Confirm correct calculator used for tournament
- Confirm all match results contain necessary fields
- In n-player matches, confirm rankings dictionary contains all players
- Recalculate statistics manually if needed
- Player Capacity: Handles 1000+ players efficiently
- Database Operations: Proper indexes optimized
- Plugin Loading: Lazy loading with cache
- Statistics Calculation: Incremental updates to reduce overhead
Pull requests are accepted using these steps:
- Fork repository
- Create a feature branch:
git checkout -b feature/new-feature - Commit with good messages:
git commit -m 'Add new feature' - Push to branch:
git push origin feature/new-feature - Create Pull Request with full description
- New matchmaking approaches
- New scoring calculator implementations
- Creating mobile application wrapper
- Documentation enhancements
- Test coverage
- Performance improvements
This project is under the MIT License.
MIT License
Copyright (c) SATYENDHRAN 2024
Permission is absolutely granted, at no charge, to any individual obtaining a copy
of this software and the accompanying documentation files (the "Software") to
deal in the Software without limitation, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to allow persons to whom the Software is
provided to do so, on the condition that:
The following copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
- Issue Tracking: GitHub Issues
- Discussion Forum: GitHub Discussions
- Email Contact: satyendhran74@gmail.com
This project shows examples of professional software engineering practices:
- Design Patterns: Strategy, Factory, Repository, Registry patterns
- SOLID Principles: All five principles in everyday use
- Clean Architecture: Layered design with proper separation of concerns
- Dependency Injection: Constructor-based dependency management
- Plugin Systems: Dynamic module registration and loading
- Database Design: Normalized schema with JSON flexibility
- GUI Development: Event-driven programming using Tkinter
Ideal for academic research, portfolio work, and learning modern software architecture concepts.
This project has been designed as per industry best practices and modern software engineering concepts. It is as much a functional tournament management system as it is an educational guide to clean code architecture.