tiebreak-core 1.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tiebreak_core/__init__.py +240 -0
- tiebreak_core/article16.py +140 -0
- tiebreak_core/calculators.py +350 -0
- tiebreak_core/display.py +56 -0
- tiebreak_core/errors.py +74 -0
- tiebreak_core/fide2024.py +1215 -0
- tiebreak_core/fide2026.py +1586 -0
- tiebreak_core/models.py +149 -0
- tiebreak_core/modifiers.py +649 -0
- tiebreak_core/py.typed +0 -0
- tiebreak_core/ranking.py +94 -0
- tiebreak_core/registry.py +147 -0
- tiebreak_core/rules.py +170 -0
- tiebreak_core/scoring.py +87 -0
- tiebreak_core/strict.py +619 -0
- tiebreak_core/team.py +1070 -0
- tiebreak_core-1.2.0.dist-info/METADATA +279 -0
- tiebreak_core-1.2.0.dist-info/RECORD +21 -0
- tiebreak_core-1.2.0.dist-info/WHEEL +5 -0
- tiebreak_core-1.2.0.dist-info/licenses/LICENSE +21 -0
- tiebreak_core-1.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tiebreak-core — standalone chess tie-break calculation library.
|
|
3
|
+
|
|
4
|
+
Extracted behavior-preserving from chess-manager ``domain/tiebreak/``.
|
|
5
|
+
No Flask, no SQLAlchemy, no persistence, no network.
|
|
6
|
+
|
|
7
|
+
Calculation (``calculate``/``calculate_all``) and ordering
|
|
8
|
+
(``ranking.rank_standings``/``order_ids``) are independent. Tournament
|
|
9
|
+
seeding stays outside the core (see ``ranking`` docstring).
|
|
10
|
+
|
|
11
|
+
Rules versioning: calculations select an explicit ruleset
|
|
12
|
+
(``legacy-0.1.0`` frozen, ``fide-2024`` frozen, ``fide-2026``
|
|
13
|
+
implemented). See ``tiebreak_core.rules`` and
|
|
14
|
+
``docs/KNOWN_LIMITATIONS.md``.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from tiebreak_core.models import (
|
|
18
|
+
GameRecord,
|
|
19
|
+
PlayerTiebreakData,
|
|
20
|
+
TiebreakResult,
|
|
21
|
+
PlayerResult,
|
|
22
|
+
StandingsResult,
|
|
23
|
+
GAME_KINDS,
|
|
24
|
+
VIRTUAL_KINDS,
|
|
25
|
+
PLAYED,
|
|
26
|
+
PAIRING_BYE,
|
|
27
|
+
FORFEIT_WIN,
|
|
28
|
+
FORFEIT_LOSS,
|
|
29
|
+
REQUESTED_BYE,
|
|
30
|
+
UNPLAYED,
|
|
31
|
+
ABSENT,
|
|
32
|
+
normalize_kind,
|
|
33
|
+
)
|
|
34
|
+
from tiebreak_core.calculators import (
|
|
35
|
+
buchholz,
|
|
36
|
+
buchholz_cut1,
|
|
37
|
+
buchholz_cut2,
|
|
38
|
+
median_buchholz,
|
|
39
|
+
median_buchholz_2,
|
|
40
|
+
sonneborn_berger,
|
|
41
|
+
progressive,
|
|
42
|
+
wins_count,
|
|
43
|
+
wins_with_black,
|
|
44
|
+
games_with_black,
|
|
45
|
+
average_rating_opponents,
|
|
46
|
+
koya,
|
|
47
|
+
direct_encounter,
|
|
48
|
+
buchholz_sum,
|
|
49
|
+
arpo,
|
|
50
|
+
calculate,
|
|
51
|
+
calculate_all,
|
|
52
|
+
TIEBREAK_REGISTRY,
|
|
53
|
+
)
|
|
54
|
+
from tiebreak_core.registry import (
|
|
55
|
+
TIEBREAK_IDS,
|
|
56
|
+
TIEBREAK_FIDE_REF,
|
|
57
|
+
DEFAULT_CRITERIA,
|
|
58
|
+
frozen_registry,
|
|
59
|
+
is_known,
|
|
60
|
+
register_criterion,
|
|
61
|
+
unregister_criterion,
|
|
62
|
+
)
|
|
63
|
+
from tiebreak_core.errors import (
|
|
64
|
+
TiebreakError,
|
|
65
|
+
UnknownCriterionError,
|
|
66
|
+
UnsupportedRulesetError,
|
|
67
|
+
UnsupportedCriterionError,
|
|
68
|
+
InvalidGameRecordError,
|
|
69
|
+
InvalidPlayerDataError,
|
|
70
|
+
DuplicatePlayerIdError,
|
|
71
|
+
RegistryError,
|
|
72
|
+
InvalidDescriptorError,
|
|
73
|
+
)
|
|
74
|
+
from tiebreak_core.strict import (
|
|
75
|
+
calculate_strict,
|
|
76
|
+
calculate_all_strict,
|
|
77
|
+
rank_standings_strict,
|
|
78
|
+
order_ids_strict,
|
|
79
|
+
calculate_descriptor_strict,
|
|
80
|
+
rank_descriptors_strict,
|
|
81
|
+
calculate_team_strict,
|
|
82
|
+
rank_teams_strict,
|
|
83
|
+
validate_game,
|
|
84
|
+
validate_player,
|
|
85
|
+
validate_players,
|
|
86
|
+
require_criteria,
|
|
87
|
+
require_ruleset,
|
|
88
|
+
)
|
|
89
|
+
from tiebreak_core.ranking import rank_standings, order_ids, sort_key
|
|
90
|
+
from tiebreak_core.modifiers import (
|
|
91
|
+
ModifierSpec,
|
|
92
|
+
parse_descriptor,
|
|
93
|
+
canonical_id,
|
|
94
|
+
calculate_descriptor,
|
|
95
|
+
rank_descriptors,
|
|
96
|
+
)
|
|
97
|
+
from tiebreak_core.team import (
|
|
98
|
+
TeamMatch,
|
|
99
|
+
TeamRecord,
|
|
100
|
+
TeamFormat,
|
|
101
|
+
TeamResult,
|
|
102
|
+
TeamStandingsResult,
|
|
103
|
+
TEAM_KINDS,
|
|
104
|
+
validate_match,
|
|
105
|
+
validate_team,
|
|
106
|
+
validate_teams,
|
|
107
|
+
canonical_team_id,
|
|
108
|
+
calculate_team,
|
|
109
|
+
rank_team_standings,
|
|
110
|
+
)
|
|
111
|
+
from tiebreak_core.article16 import (
|
|
112
|
+
Article16Policy,
|
|
113
|
+
FIDE_2024_POLICY,
|
|
114
|
+
FIDE_2026_SWISS_POLICY,
|
|
115
|
+
FIDE_2026_RR_POLICY,
|
|
116
|
+
resolve_policy,
|
|
117
|
+
)
|
|
118
|
+
from tiebreak_core.scoring import (
|
|
119
|
+
ScoringScheme,
|
|
120
|
+
STANDARD,
|
|
121
|
+
is_standard,
|
|
122
|
+
require_standard_or_explicit,
|
|
123
|
+
)
|
|
124
|
+
from tiebreak_core.rules import (
|
|
125
|
+
CALCULATION_RULES_VERSION,
|
|
126
|
+
FIDE_REFERENCE,
|
|
127
|
+
SUPPORTED_RULESETS,
|
|
128
|
+
RulesetInfo,
|
|
129
|
+
available_rulesets,
|
|
130
|
+
describe_ruleset,
|
|
131
|
+
is_supported,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
__version__ = "1.2.0"
|
|
135
|
+
__fide_reference__ = FIDE_REFERENCE
|
|
136
|
+
__rules_version__ = CALCULATION_RULES_VERSION
|
|
137
|
+
|
|
138
|
+
__all__ = [
|
|
139
|
+
"GameRecord",
|
|
140
|
+
"PlayerTiebreakData",
|
|
141
|
+
"TiebreakResult",
|
|
142
|
+
"PlayerResult",
|
|
143
|
+
"StandingsResult",
|
|
144
|
+
"GAME_KINDS",
|
|
145
|
+
"VIRTUAL_KINDS",
|
|
146
|
+
"PLAYED",
|
|
147
|
+
"PAIRING_BYE",
|
|
148
|
+
"FORFEIT_WIN",
|
|
149
|
+
"FORFEIT_LOSS",
|
|
150
|
+
"REQUESTED_BYE",
|
|
151
|
+
"UNPLAYED",
|
|
152
|
+
"ABSENT",
|
|
153
|
+
"normalize_kind",
|
|
154
|
+
"buchholz",
|
|
155
|
+
"buchholz_cut1",
|
|
156
|
+
"buchholz_cut2",
|
|
157
|
+
"median_buchholz",
|
|
158
|
+
"median_buchholz_2",
|
|
159
|
+
"sonneborn_berger",
|
|
160
|
+
"progressive",
|
|
161
|
+
"wins_count",
|
|
162
|
+
"wins_with_black",
|
|
163
|
+
"games_with_black",
|
|
164
|
+
"average_rating_opponents",
|
|
165
|
+
"koya",
|
|
166
|
+
"direct_encounter",
|
|
167
|
+
"buchholz_sum",
|
|
168
|
+
"arpo",
|
|
169
|
+
"calculate",
|
|
170
|
+
"calculate_all",
|
|
171
|
+
"TIEBREAK_REGISTRY",
|
|
172
|
+
"TIEBREAK_IDS",
|
|
173
|
+
"TIEBREAK_FIDE_REF",
|
|
174
|
+
"DEFAULT_CRITERIA",
|
|
175
|
+
"frozen_registry",
|
|
176
|
+
"is_known",
|
|
177
|
+
"register_criterion",
|
|
178
|
+
"unregister_criterion",
|
|
179
|
+
"TiebreakError",
|
|
180
|
+
"UnknownCriterionError",
|
|
181
|
+
"UnsupportedRulesetError",
|
|
182
|
+
"UnsupportedCriterionError",
|
|
183
|
+
"InvalidGameRecordError",
|
|
184
|
+
"InvalidPlayerDataError",
|
|
185
|
+
"DuplicatePlayerIdError",
|
|
186
|
+
"RegistryError",
|
|
187
|
+
"InvalidDescriptorError",
|
|
188
|
+
"ModifierSpec",
|
|
189
|
+
"parse_descriptor",
|
|
190
|
+
"canonical_id",
|
|
191
|
+
"calculate_descriptor",
|
|
192
|
+
"rank_descriptors",
|
|
193
|
+
"TeamMatch",
|
|
194
|
+
"TeamRecord",
|
|
195
|
+
"TeamFormat",
|
|
196
|
+
"TeamResult",
|
|
197
|
+
"TeamStandingsResult",
|
|
198
|
+
"TEAM_KINDS",
|
|
199
|
+
"validate_match",
|
|
200
|
+
"validate_team",
|
|
201
|
+
"validate_teams",
|
|
202
|
+
"canonical_team_id",
|
|
203
|
+
"calculate_team",
|
|
204
|
+
"rank_team_standings",
|
|
205
|
+
"Article16Policy",
|
|
206
|
+
"FIDE_2024_POLICY",
|
|
207
|
+
"FIDE_2026_SWISS_POLICY",
|
|
208
|
+
"FIDE_2026_RR_POLICY",
|
|
209
|
+
"resolve_policy",
|
|
210
|
+
"ScoringScheme",
|
|
211
|
+
"STANDARD",
|
|
212
|
+
"is_standard",
|
|
213
|
+
"require_standard_or_explicit",
|
|
214
|
+
"calculate_strict",
|
|
215
|
+
"calculate_all_strict",
|
|
216
|
+
"rank_standings_strict",
|
|
217
|
+
"order_ids_strict",
|
|
218
|
+
"calculate_descriptor_strict",
|
|
219
|
+
"rank_descriptors_strict",
|
|
220
|
+
"calculate_team_strict",
|
|
221
|
+
"rank_teams_strict",
|
|
222
|
+
"validate_game",
|
|
223
|
+
"validate_player",
|
|
224
|
+
"validate_players",
|
|
225
|
+
"require_criteria",
|
|
226
|
+
"require_ruleset",
|
|
227
|
+
"rank_standings",
|
|
228
|
+
"order_ids",
|
|
229
|
+
"sort_key",
|
|
230
|
+
"CALCULATION_RULES_VERSION",
|
|
231
|
+
"FIDE_REFERENCE",
|
|
232
|
+
"SUPPORTED_RULESETS",
|
|
233
|
+
"RulesetInfo",
|
|
234
|
+
"available_rulesets",
|
|
235
|
+
"describe_ruleset",
|
|
236
|
+
"is_supported",
|
|
237
|
+
"__version__",
|
|
238
|
+
"__fide_reference__",
|
|
239
|
+
"__rules_version__",
|
|
240
|
+
]
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Article 16 policy abstraction (C.07 §§16.1–16.6).
|
|
3
|
+
|
|
4
|
+
Scattered booleans (``mode``, ``forfeits_as_played``) currently select
|
|
5
|
+
unplayed-round behavior at each call site. This module names the
|
|
6
|
+
underlying policy so tournament regulations can explicitly override
|
|
7
|
+
the FIDE-default behavior where FIDE permits it (§16.6: *"Unless the
|
|
8
|
+
rules of a competition specify otherwise, ..."*).
|
|
9
|
+
|
|
10
|
+
An :class:`Article16Policy` is a value object describing HOW unplayed
|
|
11
|
+
rounds are managed; the engines document which policy their defaults
|
|
12
|
+
reproduce. New code paths (modifiers, team fore-variants) resolve
|
|
13
|
+
their behavior through :func:`resolve_policy`. Existing engine
|
|
14
|
+
defaults are unchanged — :func:`default_policy` is pinned by
|
|
15
|
+
conformance tests to reproduce ``fide-2024`` / ``fide-2026`` outputs
|
|
16
|
+
exactly.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from typing import Literal
|
|
22
|
+
|
|
23
|
+
from tiebreak_core.errors import InvalidPlayerDataError
|
|
24
|
+
|
|
25
|
+
DummyCapMode = Literal["uncapped", "capped"]
|
|
26
|
+
ForfeitScope = Literal["swiss", "round_robin", "as_played"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class Article16Policy:
|
|
31
|
+
"""Named unplayed-round management policy.
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
dummy_cap: ``"uncapped"`` (§16.4 2024: dummy = own score) or
|
|
35
|
+
``"capped"`` (§16.4.1–16.4.2 2026: forfeit dummies capped
|
|
36
|
+
by the scheduled opponent's adjusted score, other
|
|
37
|
+
unplayed dummies capped by draw-points × rounds).
|
|
38
|
+
late_bye_value: adjusted-score value (§16.3) of a 16.2.5
|
|
39
|
+
trailing requested bye for opponents' use (FIDE: 0.5).
|
|
40
|
+
vur_cut_preference: whether cuts prefer VUR contributions
|
|
41
|
+
(§16.5.1; FIDE: True).
|
|
42
|
+
reapply_cuts: whether the cut exception reapplies per cut
|
|
43
|
+
(§16.5.2; FIDE: True).
|
|
44
|
+
forfeit_scope: ``"swiss"`` (Article 16: forfeits are unplayed
|
|
45
|
+
rounds), ``"round_robin"`` (§15.2 carve-out), or
|
|
46
|
+
``"as_played"`` (MTB26 ``/P`` regulations opt-in).
|
|
47
|
+
keep_last_element: never cut the final remaining element
|
|
48
|
+
(documented edge guard, not a FIDE rule).
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
dummy_cap: DummyCapMode = "capped"
|
|
52
|
+
late_bye_value: float = 0.5
|
|
53
|
+
vur_cut_preference: bool = True
|
|
54
|
+
reapply_cuts: bool = True
|
|
55
|
+
forfeit_scope: ForfeitScope = "swiss"
|
|
56
|
+
keep_last_element: bool = True
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
FIDE_2024_POLICY = Article16Policy(
|
|
60
|
+
dummy_cap="uncapped",
|
|
61
|
+
late_bye_value=0.5,
|
|
62
|
+
vur_cut_preference=True,
|
|
63
|
+
reapply_cuts=True,
|
|
64
|
+
forfeit_scope="swiss",
|
|
65
|
+
keep_last_element=True,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
FIDE_2026_SWISS_POLICY = Article16Policy(
|
|
69
|
+
dummy_cap="capped",
|
|
70
|
+
late_bye_value=0.5,
|
|
71
|
+
vur_cut_preference=True,
|
|
72
|
+
reapply_cuts=True,
|
|
73
|
+
forfeit_scope="swiss",
|
|
74
|
+
keep_last_element=True,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
FIDE_2026_RR_POLICY = Article16Policy(
|
|
78
|
+
dummy_cap="capped",
|
|
79
|
+
late_bye_value=0.5,
|
|
80
|
+
vur_cut_preference=True,
|
|
81
|
+
reapply_cuts=True,
|
|
82
|
+
forfeit_scope="round_robin",
|
|
83
|
+
keep_last_element=True,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def resolve_policy(ruleset: str, mode: str = "swiss",
|
|
88
|
+
forfeits_as_played: bool = False,
|
|
89
|
+
**overrides) -> Article16Policy:
|
|
90
|
+
"""Resolve engine flags to the policy they implement.
|
|
91
|
+
|
|
92
|
+
``§16.6`` overrides pass as keyword overrides (e.g.
|
|
93
|
+
``late_bye_value=0.0``); unknown override names raise
|
|
94
|
+
:class:`InvalidPlayerDataError` (no silent misconfiguration).
|
|
95
|
+
"""
|
|
96
|
+
if ruleset == "fide-2024":
|
|
97
|
+
base = FIDE_2024_POLICY
|
|
98
|
+
elif ruleset == "fide-2026":
|
|
99
|
+
if forfeits_as_played:
|
|
100
|
+
base = Article16Policy(
|
|
101
|
+
dummy_cap="capped", late_bye_value=0.5,
|
|
102
|
+
vur_cut_preference=True, reapply_cuts=True,
|
|
103
|
+
forfeit_scope="as_played", keep_last_element=True)
|
|
104
|
+
elif mode == "round_robin":
|
|
105
|
+
base = FIDE_2026_RR_POLICY
|
|
106
|
+
elif mode == "swiss":
|
|
107
|
+
base = FIDE_2026_SWISS_POLICY
|
|
108
|
+
else:
|
|
109
|
+
raise InvalidPlayerDataError(
|
|
110
|
+
f"mode must be 'swiss' or 'round_robin', got {mode!r}")
|
|
111
|
+
else:
|
|
112
|
+
raise InvalidPlayerDataError(
|
|
113
|
+
f"Article16Policy covers fide-2024/fide-2026, "
|
|
114
|
+
f"got ruleset {ruleset!r}")
|
|
115
|
+
if not overrides:
|
|
116
|
+
return base
|
|
117
|
+
fields = set(base.__dataclass_fields__)
|
|
118
|
+
unknown = [k for k in overrides if k not in fields]
|
|
119
|
+
if unknown:
|
|
120
|
+
raise InvalidPlayerDataError(
|
|
121
|
+
f"unknown Article-16 override(s): {unknown} "
|
|
122
|
+
f"(allowed: {sorted(fields)})")
|
|
123
|
+
if ("late_bye_value" in overrides
|
|
124
|
+
and overrides["late_bye_value"] not in (0.0, 0.5)):
|
|
125
|
+
raise InvalidPlayerDataError(
|
|
126
|
+
"late_bye_value override must be 0.0 or 0.5 "
|
|
127
|
+
f"(§16.3 draw semantics), got "
|
|
128
|
+
f"{overrides['late_bye_value']!r}")
|
|
129
|
+
values = {f: getattr(base, f) for f in fields}
|
|
130
|
+
values.update(overrides)
|
|
131
|
+
return Article16Policy(**values)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
__all__ = [
|
|
135
|
+
"Article16Policy",
|
|
136
|
+
"FIDE_2024_POLICY",
|
|
137
|
+
"FIDE_2026_SWISS_POLICY",
|
|
138
|
+
"FIDE_2026_RR_POLICY",
|
|
139
|
+
"resolve_policy",
|
|
140
|
+
]
|