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.
@@ -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
+ ]