-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtournament_core.py
More file actions
324 lines (250 loc) · 8.28 KB
/
tournament_core.py
File metadata and controls
324 lines (250 loc) · 8.28 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
import datetime
import uuid
from abc import ABC, abstractmethod
from dataclasses import dataclass
from enum import Enum
from typing import Any
@dataclass
class Player:
"""Value object representing a player."""
id: str
name: str
created_at: str
date_of_birth: str | None = None
category: str | None = None
email: str | None = None
rating: float = 1500.0
rating_deviation: float = 350.0
rating_volatility: float = 0.06
@dataclass
class Match:
"""Value object representing a match between players."""
id: str
round_id: str
tournament_id: str
player_ids: list[str]
scheduled_at: str
result: str | None = None
winner_ids: list[str] | None = None
rankings: dict[str, int] | None = None
auto_bye: bool = False
players_per_match: int = 2
board_no: int | None = None
colors: list[str] | None = None
appeal_status: str | None = None
appeal_reason: str | None = None
forfeit_player_id: str | None = None
@dataclass
class MatchResult:
"""Value object for match results."""
match_id: str
winner_ids: list[str]
rankings: dict[str, int]
is_draw: bool = False
@dataclass
class RoundConfig:
"""Configuration for creating a round."""
tournament_id: str
round_type: str
players_per_match: int = 2
force_create: bool = False
additional_params: dict[str, Any] | None = None
start_time: str | None = None
end_time: str | None = None
class RoundCompletionPolicy(str, Enum):
"""Determines whether a new round can start with pending matches.
STRICT (default) — all matches in the previous round must be
completed before a new round can be created.
FLEXIBLE — a new round can be created even if the
previous round has unfinished matches
(e.g. FIDE Swiss allows this).
"""
STRICT = "strict"
FLEXIBLE = "flexible"
@dataclass
class TournamentConfig:
"""Per-tournament settings (extensible)."""
tournament_id: str
round_completion_policy: RoundCompletionPolicy = RoundCompletionPolicy.STRICT
min_rounds_before_withdrawal: int = 0
default_calculator: str = "standard"
default_strategy: str = "swiss"
@dataclass
class TournamentTemplate:
"""Reusable tournament blueprint."""
name: str
round_type: str
players_per_match: int = 2
calculator: str = "standard"
num_rounds: int | None = None
round_completion_policy: RoundCompletionPolicy = RoundCompletionPolicy.STRICT
additional_params: dict[str, Any] | None = None
@dataclass
class TournamentPhase:
"""Links parent → child tournament for multi-stage tournaments."""
parent_tournament_id: str
child_tournament_id: str
phase_type: str
qualification_count: int = 0
class IMatchmakingStrategy(ABC):
"""
Strategy interface for different matchmaking algorithms.
Follows Strategy Pattern and Open/Closed Principle.
"""
@abstractmethod
def create_matches(
self,
tournament_id: str,
round_id: str,
available_players: list[str],
config: RoundConfig,
) -> dict[str, Any]:
"""
Create matches for a round.
Returns:
dict with keys:
- matches: list[Match]
- waiting_players: list[str]
- metadata: dict[str, Any]
"""
pass
@abstractmethod
def get_strategy_name(self) -> str:
"""Return the name of this strategy."""
pass
@abstractmethod
def supports_players_per_match(self, n: int) -> bool:
"""Check if this strategy supports n-player matches."""
pass
class IPointsCalculator(ABC):
"""
Interface for calculating points from match results.
Follows Single Responsibility Principle.
"""
@abstractmethod
def calculate_points(
self, player_id: str, match: Match, result: MatchResult
) -> float:
"""Calculate points earned by a player in a match."""
pass
@abstractmethod
def get_calculator_name(self) -> str:
"""Return the name of this calculator."""
pass
class ITournamentRepository(ABC):
"""
Repository interface for data persistence.
Follows Dependency Inversion Principle.
"""
@abstractmethod
def save_player(self, player: Player) -> None:
pass
@abstractmethod
def get_player(self, player_id: str) -> Player | None:
pass
@abstractmethod
def get_player_by_name(self, name: str) -> Player | None:
"""Get a player by name (case-insensitive)."""
pass
@abstractmethod
def delete_player(self, player_id: str) -> None:
"""Delete a player and all their tournament/stats records."""
pass
@abstractmethod
def list_players(self) -> list[Player]:
pass
@abstractmethod
def save_match(self, match: Match) -> None:
pass
@abstractmethod
def get_match(self, match_id: str) -> Match | None:
pass
@abstractmethod
def list_matches_for_round(self, round_id: str) -> list[Match]:
pass
@abstractmethod
def update_match_result(self, match_id: str, result: MatchResult) -> None:
pass
@abstractmethod
def save_tournament(self, tournament_id: str, name: str, created_at: str) -> None:
pass
@abstractmethod
def get_tournament_players(self, tournament_id: str) -> list[dict[str, Any]]:
pass
@abstractmethod
def add_player_to_tournament(self, tournament_id: str, player_id: str) -> None:
pass
@abstractmethod
def save_round(
self,
round_id: str,
tournament_id: str,
round_type: str,
ordinal: int,
created_at: str,
) -> None:
pass
@abstractmethod
def get_stats(self, tournament_id: str) -> list[dict[str, Any]]:
pass
@abstractmethod
def update_player_stats(
self, tournament_id: str, player_id: str, stats: dict[str, float]
) -> None:
pass
@abstractmethod
def eliminate_player(self, tournament_id: str, player_id: str) -> None:
"""Mark a player as eliminated from tournament."""
pass
@abstractmethod
def activate_player(self, tournament_id: str, player_id: str) -> None:
"""Mark a player as active in tournament."""
pass
@abstractmethod
def get_round_type(self, round_id: str) -> str:
"""Get the type of a round."""
pass
class MatchmakingStrategyRegistry:
"""
Registry for matchmaking strategies.
Allows dynamic loading of strategies at runtime.
"""
def __init__(self):
self._strategies: dict[str, IMatchmakingStrategy] = {}
def register(self, strategy: IMatchmakingStrategy) -> None:
"""Register a new matchmaking strategy."""
name = strategy.get_strategy_name()
self._strategies[name] = strategy
def get_strategy(self, name: str) -> IMatchmakingStrategy | None:
"""Get a strategy by name."""
return self._strategies.get(name)
def list_strategies(self) -> list[str]:
"""list all registered strategy names."""
return list(self._strategies.keys())
def get_strategies_for_player_count(self, n: int) -> list[str]:
"""Get strategies that support n-player matches."""
return [
name
for name, strategy in self._strategies.items()
if strategy.supports_players_per_match(n)
]
class PointsCalculatorRegistry:
"""Registry for points calculators."""
def __init__(self):
self._calculators: dict[str, IPointsCalculator] = {}
def register(self, calculator: IPointsCalculator) -> None:
"""Register a new points calculator."""
name = calculator.get_calculator_name()
self._calculators[name] = calculator
def get_calculator(self, name: str) -> IPointsCalculator | None:
"""Get a calculator by name."""
return self._calculators.get(name)
def list_calculators(self) -> list[str]:
"""list all registered calculator names."""
return list(self._calculators.keys())
def generate_id() -> str:
"""Generate a unique ID."""
return uuid.uuid4().hex
def now_iso() -> str:
"""Get current timestamp in ISO format."""
return datetime.datetime.now(datetime.timezone.utc).isoformat()