Sports Manager Gem
The sports-manager
gem is a powerful tool designed to generate and manage tournament schedules. It handles complex scheduling tasks, considering various constraints such as court availability, game length, rest breaks, and participant availability. Under the hood, it leverages the csp-resolver
gem to solve these complex Constraint Satisfaction Problems (CSPs).
Getting Started
Requirements
- Ruby >= 2.5.8
Installing
You can install using the following command:
gem install "sports-manager"
Or add the following line to your Gemfile:
gem "sports-manager"
Then install it:
$ bundle install
Usage
Setting Up a Tournament
To set up a tournament, you need to provide the following information:
require 'sports-manager'
days = {
'2023-09-09': { start: 9, end: 20 },
'2023-09-10': { start: 9, end: 13 }
}
courts = 2
game_length = 60
rest_break = 30
single_day_matches = false
subscriptions = {
mens_single: [
{ id: 1, name: 'João' }, { id: 2, name: 'Marcelo' },
{ id: 3, name: 'José' }, { id: 4, name: 'Pedro' },
{ id: 5, name: 'Carlos' }, { id: 6, name: 'Leandro' },
{ id: 7, name: 'Leonardo' }, { id: 8, name: 'Cláudio' },
{ id: 9, name: 'Alexandre' }, { id: 10, name: 'Daniel' },
{ id: 11, name: 'Marcos' }, { id: 12, name: 'Henrique' },
{ id: 13, name: 'Joaquim' }, { id: 14, name: 'Alex' },
{ id: 15, name: 'Bruno' }, { id: 16, name: 'Fábio' }
]
}
matches = {
mens_single: [
[1, 16],
[2, 15],
[3, 14],
[4, 13],
[5, 12],
[6, 11],
[7, 10],
[8, 9]
]
}
solution = SportsManager::TournamentGenerator.new(format: :cli)
.add_days(days)
.add_courts(courts)
.add_game_length(game_length)
.add_rest_break(rest_break)
.enable_single_day_matches(single_day_matches)
.add_subscriptions(subscriptions)
.single_elimination_algorithm
.add_matches(matches)
You can also pass already generated matches to the generator, it's useful when your already have the matches generated by another system, but you still want to generate the schedule.
params = {
when: {
'2023-09-09': { start: 9, end: 20 },
'2023-09-10': { start: 9, end: 13 }
},
courts: 2,
game_length: 60,
rest_brake: 30,
single_day_matches: false,
subscriptions: {
mens_single: [
{ id: 1, name: 'João' }, { id: 2, name: 'Marcelo' },
{ id: 3, name: 'José' }, { id: 4, name: 'Pedro' },
{ id: 5, name: 'Carlos' }, { id: 6, name: 'Leandro' },
{ id: 7, name: 'Leonardo' }, { id: 8, name: 'Cláudio' },
{ id: 9, name: 'Alexandre' }, { id: 10, name: 'Daniel' },
{ id: 11, name: 'Marcos' }, { id: 12, name: 'Henrique' },
{ id: 13, name: 'Joaquim' }, { id: 14, name: 'Alex' },
{ id: 15, name: 'Bruno' }, { id: 16, name: 'Fábio' }
]
},
matches: {
mens_single: [
{ id: 1, participants: [1, 16], },
{ id: 2, participants: [2, 15], },
{ id: 3, participants: [3, 14], },
{ id: 4, participants: [4, 13], },
{ id: 5, participants: [5, 12], },
{ id: 6, participants: [6, 11], },
{ id: 7, participants: [7, 10], },
{ id: 8, participants: [8, 9], },
{ id: 9, depends_on: [1, 2], round: 1 },
{ id: 10, depends_on: [3, 4], round: 1 },
{ id: 11, depends_on: [5, 6], round: 1 },
{ id: 12, depends_on: [7, 8], round: 1 },
{ id: 13, depends_on: [9, 10], round: 2 },
{ id: 14, depends_on: [11, 12], round: 2},
{ id: 15, depends_on: [13, 14], round: 2},
]
}
}
Configuration methods
-
add_days(days)
: Adds the tournament days. -
add_day(day, start, end)
: Adds a single tournament day. -
add_courts(courts)
: Adds the number of available courts. -
add_game_length(game_length)
: Adds the duration of each game in minutes. -
add_rest_break(rest_break)
: Adds the rest time between player matches in minutes. -
enable_single_day_matches(single_day_matches)
: Sets if all matches should be on the same day. -
add_subscriptions(subscriptions)
: Adds the players or teams participating in each category. -
add_subscription(category, subscription)
: Adds a single player or team to a category. -
add_subscriptions_per_category(subscriptions_per_category)
: Adds the players or teams participating per category. -
add_matches(matches)
: Adds the first matchups for each category. -
add_match(category, match)
: Adds a single match to a category. -
add_matches_per_category(category, matches_per_category)
: Adds the first matchups per category. -
single_elimination_algorithm
: Sets the single elimination algorithm(this option is already default).
Running Example Tournaments
The gem comes with predefined example tournaments:
solution = SportsManager::TournamentGenerator.example(:simple)
solution = SportsManager::TournamentGenerator.example(:complex)
#complete, minimal, ...
Output Formats
You can choose different output formats:
# CLI format (default)
SportsManager::TournamentGenerator.new(format: :cli)
# Mermaid format (for visual diagrams)
SportsManager::TournamentGenerator.new(format: :mermaid)
Output examples
require 'sports-manager'
days = {
'2023-09-09': { start: 9, end: 20 },
'2023-09-10': { start: 9, end: 13 }
}
courts = 2
game_length = 60
rest_break = 30
single_day_matches = false
subscriptions = {
mens_single: [
{ id: 1, name: 'João' }, { id: 2, name: 'Marcelo' },
{ id: 3, name: 'José' }, { id: 4, name: 'Pedro' },
{ id: 5, name: 'Carlos' }, { id: 6, name: 'Leandro' },
{ id: 7, name: 'Leonardo' }, { id: 8, name: 'Cláudio' },
{ id: 9, name: 'Alexandre' }, { id: 10, name: 'Daniel' },
{ id: 11, name: 'Marcos' }, { id: 12, name: 'Henrique' },
{ id: 13, name: 'Joaquim' }, { id: 14, name: 'Alex' },
{ id: 15, name: 'Bruno' }, { id: 16, name: 'Fábio' }
]
}
matches = {
mens_single: [
[1, 16],
[2, 15],
[3, 14],
[4, 13],
[5, 12],
[6, 11],
[7, 10],
[8, 9]
]
}
solution = SportsManager::TournamentGenerator.new(format: :cli)
.add_days(days)
.add_courts(courts)
.add_game_length(game_length)
.add_rest_break(rest_break)
.enable_single_day_matches(single_day_matches)
.add_subscriptions(subscriptions)
.single_elimination_algorithm
.add_matches(matches)
.call
Tournament Timetable:
Solution 1
category | id | round | participants | court | time
------------|----|-------|-----------------------|-------|---------------
mens_single | 1 | 0 | João vs. Fábio | 0 | 09/09 at 09:00
mens_single | 2 | 0 | Marcelo vs. Bruno | 1 | 09/09 at 09:00
mens_single | 3 | 0 | José vs. Alex | 0 | 09/09 at 10:00
mens_single | 4 | 0 | Pedro vs. Joaquim | 1 | 09/09 at 10:00
mens_single | 5 | 0 | Carlos vs. Henrique | 0 | 09/09 at 11:00
mens_single | 6 | 0 | Leandro vs. Marcos | 1 | 09/09 at 11:00
mens_single | 7 | 0 | Leonardo vs. Daniel | 0 | 09/09 at 12:00
mens_single | 8 | 0 | Cláudio vs. Alexandre | 1 | 09/09 at 12:00
mens_single | 9 | 1 | M1 vs. M2 | 0 | 09/09 at 13:00
mens_single | 10 | 1 | M3 vs. M4 | 1 | 09/09 at 13:00
mens_single | 11 | 1 | M5 vs. M6 | 0 | 09/09 at 14:00
mens_single | 12 | 1 | M7 vs. M8 | 1 | 09/09 at 14:00
mens_single | 13 | 2 | M9 vs. M10 | 0 | 09/09 at 15:00
mens_single | 14 | 2 | M11 vs. M12 | 1 | 09/09 at 15:30
mens_single | 15 | 2 | M13 vs. M14 | 0 | 09/09 at 17:00
Total solutions: 1
require 'sports-manager'
days = {
'2023-09-09': { start: 9, end: 20 },
'2023-09-10': { start: 9, end: 13 }
}
courts = 2
game_length = 60
rest_break = 30
single_day_matches = false
subscriptions = {
mens_single: [
{ id: 1, name: 'João' }, { id: 2, name: 'Marcelo' },
{ id: 3, name: 'José' }, { id: 4, name: 'Pedro' },
{ id: 5, name: 'Carlos' }, { id: 6, name: 'Leandro' },
{ id: 7, name: 'Leonardo' }, { id: 8, name: 'Cláudio' },
{ id: 9, name: 'Alexandre' }, { id: 10, name: 'Daniel' },
{ id: 11, name: 'Marcos' }, { id: 12, name: 'Henrique' },
{ id: 13, name: 'Joaquim' }, { id: 14, name: 'Alex' },
{ id: 15, name: 'Bruno' }, { id: 16, name: 'Fábio' }
]
}
matches = {
mens_single: [
[1, 16],
[2, 15],
[3, 14],
[4, 13],
[5, 12],
[6, 11],
[7, 10],
[8, 9]
]
}
solution = SportsManager::TournamentGenerator.new(format: :mermaid)
.add_days(days)
.add_courts(courts)
.add_game_length(game_length)
.add_rest_break(rest_break)
.enable_single_day_matches(single_day_matches)
.add_subscriptions(subscriptions)
.single_elimination_algorithm
.add_matches(matches)
.call
Solutions:
--------------------------------------------------------------------------------
Solutions 1
Gantt:
---
displayMode: compact
---
gantt
title Tournament Schedule
dateFormat DD/MM HH:mm
axisFormat %H:%M
tickInterval 1hour
section 0
MS M1: 09/09 09:00, 1h
MS M3: 09/09 10:00, 1h
MS M5: 09/09 11:00, 1h
MS M7: 09/09 12:00, 1h
MS M9: 09/09 13:00, 1h
MS M11: 09/09 14:00, 1h
MS M13: 09/09 15:00, 1h
MS M15: 09/09 17:00, 1h
section 1
MS M2: 09/09 09:00, 1h
MS M4: 09/09 10:00, 1h
MS M6: 09/09 11:00, 1h
MS M8: 09/09 12:00, 1h
MS M10: 09/09 13:00, 1h
MS M12: 09/09 14:00, 1h
MS M14: 09/09 15:30, 1h
Graph:
graph LR
classDef court0 fill:#A9F9A9, color:#000000
classDef court1 fill:#4FF7DE, color:#000000
subgraph colorscheme
direction LR
COURT0:::court0
COURT1:::court1
end
subgraph mens_single
direction LR
mens_single_1[1\nJoão vs. Fábio\n09/09 09:00]:::court0
mens_single_2[2\nMarcelo vs. Bruno\n09/09 09:00]:::court1
mens_single_3[3\nJosé vs. Alex\n09/09 10:00]:::court0
mens_single_4[4\nPedro vs. Joaquim\n09/09 10:00]:::court1
mens_single_5[5\nCarlos vs. Henrique\n09/09 11:00]:::court0
mens_single_6[6\nLeandro vs. Marcos\n09/09 11:00]:::court1
mens_single_7[7\nLeonardo vs. Daniel\n09/09 12:00]:::court0
mens_single_8[8\nCláudio vs. Alexandre\n09/09 12:00]:::court1
mens_single_9[9\nM1 vs. M2\n09/09 13:00]:::court0
mens_single_10[10\nM3 vs. M4\n09/09 13:00]:::court1
mens_single_11[11\nM5 vs. M6\n09/09 14:00]:::court0
mens_single_12[12\nM7 vs. M8\n09/09 14:00]:::court1
mens_single_13[13\nM9 vs. M10\n09/09 15:00]:::court0
mens_single_14[14\nM11 vs. M12\n09/09 15:30]:::court1
mens_single_15[15\nM13 vs. M14\n09/09 17:00]:::court0
mens_single_1 --> mens_single_9
mens_single_2 --> mens_single_9
mens_single_3 --> mens_single_10
mens_single_4 --> mens_single_10
mens_single_5 --> mens_single_11
mens_single_6 --> mens_single_11
mens_single_7 --> mens_single_12
mens_single_8 --> mens_single_12
mens_single_9 --> mens_single_13
mens_single_10 --> mens_single_13
mens_single_11 --> mens_single_14
mens_single_12 --> mens_single_14
mens_single_13 --> mens_single_15
mens_single_14 --> mens_single_15
end
--------------------------------------------------------------------------------
Total solutions: 1
graph LR
classDef court0 fill:#A9F9A9, color:#000000
classDef court1 fill:#4FF7DE, color:#000000
subgraph colorscheme
direction LR
COURT0:::court0
COURT1:::court1
end
subgraph mens_single
direction LR
mens_single_1[1\nJoão vs. Fábio\n09/09 09:00]:::court0
mens_single_2[2\nMarcelo vs. Bruno\n09/09 09:00]:::court1
mens_single_3[3\nJosé vs. Alex\n09/09 10:00]:::court0
mens_single_4[4\nPedro vs. Joaquim\n09/09 10:00]:::court1
mens_single_5[5\nCarlos vs. Henrique\n09/09 11:00]:::court0
mens_single_6[6\nLeandro vs. Marcos\n09/09 11:00]:::court1
mens_single_7[7\nLeonardo vs. Daniel\n09/09 12:00]:::court0
mens_single_8[8\nCláudio vs. Alexandre\n09/09 12:00]:::court1
mens_single_9[9\nM1 vs. M2\n09/09 13:00]:::court0
mens_single_10[10\nM3 vs. M4\n09/09 13:00]:::court1
mens_single_11[11\nM5 vs. M6\n09/09 14:00]:::court0
mens_single_12[12\nM7 vs. M8\n09/09 14:00]:::court1
mens_single_13[13\nM9 vs. M10\n09/09 15:00]:::court0
mens_single_14[14\nM11 vs. M12\n09/09 15:30]:::court1
mens_single_15[15\nM13 vs. M14\n09/09 17:00]:::court0
mens_single_1 --> mens_single_9
mens_single_2 --> mens_single_9
mens_single_3 --> mens_single_10
mens_single_4 --> mens_single_10
mens_single_5 --> mens_single_11
mens_single_6 --> mens_single_11
mens_single_7 --> mens_single_12
mens_single_8 --> mens_single_12
mens_single_9 --> mens_single_13
mens_single_10 --> mens_single_13
mens_single_11 --> mens_single_14
mens_single_12 --> mens_single_14
mens_single_13 --> mens_single_15
mens_single_14 --> mens_single_15
end
Contributing
See our CONTRIBUTING guidelines.
Code of Conduct
We expect that everyone participating in any way with this project follows our Code of Conduct.
License
This project is licensed under the MIT License.