tiebreak-core 1.2.0__tar.gz
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-1.2.0/LICENSE +21 -0
- tiebreak_core-1.2.0/PKG-INFO +279 -0
- tiebreak_core-1.2.0/README.md +255 -0
- tiebreak_core-1.2.0/pyproject.toml +39 -0
- tiebreak_core-1.2.0/setup.cfg +4 -0
- tiebreak_core-1.2.0/src/tiebreak_core/__init__.py +240 -0
- tiebreak_core-1.2.0/src/tiebreak_core/article16.py +140 -0
- tiebreak_core-1.2.0/src/tiebreak_core/calculators.py +350 -0
- tiebreak_core-1.2.0/src/tiebreak_core/display.py +56 -0
- tiebreak_core-1.2.0/src/tiebreak_core/errors.py +74 -0
- tiebreak_core-1.2.0/src/tiebreak_core/fide2024.py +1215 -0
- tiebreak_core-1.2.0/src/tiebreak_core/fide2026.py +1586 -0
- tiebreak_core-1.2.0/src/tiebreak_core/models.py +149 -0
- tiebreak_core-1.2.0/src/tiebreak_core/modifiers.py +649 -0
- tiebreak_core-1.2.0/src/tiebreak_core/py.typed +0 -0
- tiebreak_core-1.2.0/src/tiebreak_core/ranking.py +94 -0
- tiebreak_core-1.2.0/src/tiebreak_core/registry.py +147 -0
- tiebreak_core-1.2.0/src/tiebreak_core/rules.py +170 -0
- tiebreak_core-1.2.0/src/tiebreak_core/scoring.py +87 -0
- tiebreak_core-1.2.0/src/tiebreak_core/strict.py +619 -0
- tiebreak_core-1.2.0/src/tiebreak_core/team.py +1070 -0
- tiebreak_core-1.2.0/src/tiebreak_core.egg-info/PKG-INFO +279 -0
- tiebreak_core-1.2.0/src/tiebreak_core.egg-info/SOURCES.txt +42 -0
- tiebreak_core-1.2.0/src/tiebreak_core.egg-info/dependency_links.txt +1 -0
- tiebreak_core-1.2.0/src/tiebreak_core.egg-info/top_level.txt +1 -0
- tiebreak_core-1.2.0/tests/test_benchmarks.py +162 -0
- tiebreak_core-1.2.0/tests/test_calculators.py +204 -0
- tiebreak_core-1.2.0/tests/test_conformance.py +200 -0
- tiebreak_core-1.2.0/tests/test_corpus.py +111 -0
- tiebreak_core-1.2.0/tests/test_de_order_invariance.py +143 -0
- tiebreak_core-1.2.0/tests/test_differential.py +241 -0
- tiebreak_core-1.2.0/tests/test_direct.py +228 -0
- tiebreak_core-1.2.0/tests/test_examples.py +33 -0
- tiebreak_core-1.2.0/tests/test_fide2024.py +290 -0
- tiebreak_core-1.2.0/tests/test_fide2026.py +1071 -0
- tiebreak_core-1.2.0/tests/test_gamekind.py +104 -0
- tiebreak_core-1.2.0/tests/test_legacy_frozen.py +137 -0
- tiebreak_core-1.2.0/tests/test_modifiers.py +354 -0
- tiebreak_core-1.2.0/tests/test_policy_scoring.py +157 -0
- tiebreak_core-1.2.0/tests/test_ranking.py +67 -0
- tiebreak_core-1.2.0/tests/test_ratings.py +122 -0
- tiebreak_core-1.2.0/tests/test_rulesets.py +56 -0
- tiebreak_core-1.2.0/tests/test_strict.py +241 -0
- tiebreak_core-1.2.0/tests/test_team.py +341 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 20kevit
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tiebreak-core
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: Standalone FIDE chess tie-break calculation library for individual and team tournaments
|
|
5
|
+
Author: 20kevit
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/20kevit/tiebreak-core
|
|
8
|
+
Project-URL: Repository, https://github.com/20kevit/tiebreak-core
|
|
9
|
+
Keywords: chess,tiebreak,tie-break,fide,tournament,swiss,round-robin
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Topic :: Games/Entertainment :: Board Games
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# tiebreak-core
|
|
26
|
+
|
|
27
|
+
[](https://github.com/20kevit/tiebreak-core/actions/workflows/ci.yml)
|
|
28
|
+
|
|
29
|
+
Standalone Python library for chess tie-break calculation, implementing the applicable FIDE C.07 semantics for individual and team tournaments.
|
|
30
|
+
|
|
31
|
+
Pure Python. Zero runtime dependencies. Deterministic. MIT licensed.
|
|
32
|
+
|
|
33
|
+
## What it is
|
|
34
|
+
|
|
35
|
+
`tiebreak-core` computes chess tie-break values and orderings from normalized tournament data you supply: per-player game records (individual) or per-team match records (team). It implements the FIDE C.07 calculation semantics — Buchholz families, Sonneborn-Berger, progressive scores, Koya, rating-based criteria, Direct Encounter, team systems (MP/GP, Board Count, Extended Sonneborn-Berger, Extended Direct Encounter, SSSC), and the generic MTB26 modifier machine (`/Cn`, `/Mn`, `/L±n`, `/P`, `/F`, `/R`, `:MP/:GP`) — under explicit, versioned rulesets.
|
|
36
|
+
|
|
37
|
+
## What it is NOT
|
|
38
|
+
|
|
39
|
+
Not a tournament manager, not a pairing engine, not a rating calculator, not a TRF file parser, not a web service, not a database layer. It owns calculation and ordering only; seeding, pairing, persistence, and file I/O belong to consumers. It is **not FIDE-approved software** — no such claim is made (see [FIDE conformity](#fide-conformity) below).
|
|
40
|
+
|
|
41
|
+
## Why it exists
|
|
42
|
+
|
|
43
|
+
Tournament applications (`chess-manager`, `pairing-core` integrations, future tools) all need the same FIDE tie-break arithmetic. `tiebreak-core` is the shared, independently testable home for it, with frozen ruleset semantics so historical results never shift under callers.
|
|
44
|
+
|
|
45
|
+
## Features
|
|
46
|
+
|
|
47
|
+
- **Individual Swiss / Round Robin / pre-determined pairings** — Article 16 unplayed-round management (categories, adjusted scores, capped/uncapped dummies, VUR-aware cuts), Swiss vs RR forfeit scope flag.
|
|
48
|
+
- **Team tournaments** — full C.07 Articles 11–13 on a dedicated `TeamMatch`/`TeamRecord` model.
|
|
49
|
+
- **Generic modifiers** — any valid MTB26 descriptor (`BH/C3`, `ARO/M2`, `KS/L+1`, `SB/C2/P`, `TPN/R`), with typed rejection of FIDE-undefined combinations.
|
|
50
|
+
- **Explicit rulesets** — `legacy-0.1.0` (frozen), `fide-2024` (frozen), `fide-2026` (current). Same input + same ruleset → same output, forever.
|
|
51
|
+
- **Typed failures** — `UnknownCriterionError`, `UnsupportedCriterionError`, `InvalidDescriptorError`, `InvalidGameRecordError`, `InvalidPlayerDataError`, and more. No silent fallback on the strict path.
|
|
52
|
+
- **Deterministic ranking** — staged ordering (points → criteria → group stages → explicit keys), no mutation of caller data.
|
|
53
|
+
|
|
54
|
+
## FIDE scope
|
|
55
|
+
|
|
56
|
+
| Area | Status |
|
|
57
|
+
|---|---|
|
|
58
|
+
| Individual criteria, C.07 Arts. 6–10 + modifiers Art. 14 | Implemented (`fide-2024`, `fide-2026`) |
|
|
59
|
+
| Unplayed rounds, Art. 16 (both editions) + RR §15.2 carve-out | Implemented |
|
|
60
|
+
| Team criteria, Arts. 11–13 (MP/GP, BC/TBR/BBE, MPvGP, ESB×4, EDE+chains, SSSC) | Implemented |
|
|
61
|
+
| Rating tables (§§8.1a/8.1b), ARO/TPR/PTP/APRO/APPO/RTNG | Implemented |
|
|
62
|
+
| Direct Encounter (§6) / Extended DE (§13.3) as group stages | Implemented |
|
|
63
|
+
| Play-off formats (Art. 3), pairing-time evaluation (C.04), norm calculations (B.01) | Consumer-owned / out of scope |
|
|
64
|
+
| Exotic scoring tables without explicit per-round scores | Rejected with a typed error |
|
|
65
|
+
|
|
66
|
+
Details: `docs/FIDE_COMPLETE_REQUIREMENTS_MATRIX.md`. Limitations: `docs/KNOWN_LIMITATIONS.md`.
|
|
67
|
+
|
|
68
|
+
## Rulesets
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from tiebreak_core import available_rulesets
|
|
72
|
+
for rs in available_rulesets():
|
|
73
|
+
print(rs.id, "-", rs.status)
|
|
74
|
+
# legacy-0.1.0 - implemented (frozen behavior-preserving extraction)
|
|
75
|
+
# fide-2026 - implemented (current C.07: §16.4 caps, RR mode, TPN/RTNG, /P)
|
|
76
|
+
# fide-2024 - implemented (frozen era semantics)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
See `docs/VERSIONING.md` for the frozen-ruleset policy.
|
|
80
|
+
|
|
81
|
+
## Installation
|
|
82
|
+
|
|
83
|
+
Requires Python ≥ 3.10. No dependencies.
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pip install tiebreak-core
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
From source:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
pip install -e .
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Quick start — individual Swiss
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from tiebreak_core import (
|
|
99
|
+
GameRecord, PlayerTiebreakData,
|
|
100
|
+
calculate_all_strict, rank_standings_strict,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
players = {
|
|
104
|
+
1: PlayerTiebreakData(1, 2000, 2.5, [
|
|
105
|
+
GameRecord(2, 1900, 1.0, "white", 1, kind="played"),
|
|
106
|
+
GameRecord(3, 1950, 0.5, "black", 2, kind="played"),
|
|
107
|
+
GameRecord(4, 1800, 1.0, "white", 3, kind="played"),
|
|
108
|
+
]),
|
|
109
|
+
2: PlayerTiebreakData(2, 1900, 1.0, [
|
|
110
|
+
GameRecord(1, 2000, 0.0, "black", 1, kind="played"),
|
|
111
|
+
GameRecord(4, 1800, 1.0, "white", 2, kind="played"),
|
|
112
|
+
GameRecord(3, 1950, 0.0, "black", 3, kind="played"),
|
|
113
|
+
]),
|
|
114
|
+
3: PlayerTiebreakData(3, 1950, 2.0, [
|
|
115
|
+
GameRecord(4, 1800, 1.0, "white", 1, kind="played"),
|
|
116
|
+
GameRecord(1, 2000, 0.5, "white", 2, kind="played"),
|
|
117
|
+
GameRecord(2, 1900, 0.5, "white", 3, kind="played"),
|
|
118
|
+
]),
|
|
119
|
+
4: PlayerTiebreakData(4, 1800, 0.0, [
|
|
120
|
+
GameRecord(3, 1950, 0.0, "black", 1, kind="played"),
|
|
121
|
+
GameRecord(2, 1900, 0.0, "black", 2, kind="played"),
|
|
122
|
+
GameRecord(1, 2000, 0.0, "black", 3, kind="played"),
|
|
123
|
+
]),
|
|
124
|
+
}
|
|
125
|
+
criteria = ["buchholz_cut1", "sonneborn_berger"]
|
|
126
|
+
|
|
127
|
+
values = calculate_all_strict(players[1], players, criteria,
|
|
128
|
+
total_rounds=3, ruleset="fide-2026")
|
|
129
|
+
# {'buchholz_cut1': 3.0, 'sonneborn_berger': 2.0}
|
|
130
|
+
|
|
131
|
+
standings = rank_standings_strict(players, criteria, total_rounds=3,
|
|
132
|
+
ruleset="fide-2026")
|
|
133
|
+
print([p.player_id for p in standings.players]) # [1, 3, 2, 4]
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Game kinds (`played`, `pairing_bye`, `forfeit_win`, `forfeit_loss`, `requested_bye`, …) drive Article 16 handling — see `docs/FIDE_DATA_SEMANTICS.md`. More: `examples/a_individual_swiss.py`, `examples/b_individual_round_robin.py`.
|
|
137
|
+
|
|
138
|
+
## Team tournaments
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from tiebreak_core import (
|
|
142
|
+
TeamFormat, TeamMatch, TeamRecord,
|
|
143
|
+
calculate_team_strict, rank_teams_strict,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
fmt = TeamFormat(mp_win=2.0, mp_draw=1.0)
|
|
147
|
+
teams = {
|
|
148
|
+
1: TeamRecord(1, 3.0, 5.0, [
|
|
149
|
+
TeamMatch(2, 1, 2, 3.0),
|
|
150
|
+
TeamMatch(2, 2, 1, 2.0)], (2.5, 1.5, 0.5, 0.5)),
|
|
151
|
+
2: TeamRecord(2, 1.0, 3.0, [
|
|
152
|
+
TeamMatch(1, 1, 0, 1.0),
|
|
153
|
+
TeamMatch(1, 2, 1, 2.0)], (1.5, 0.5, 0.5, 0.5)),
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
emmsb = calculate_team_strict(teams[1], teams, "EMMSB",
|
|
157
|
+
total_rounds=2, fmt=fmt) # 3.0
|
|
158
|
+
standings = rank_teams_strict(teams, ["EMMSB", "EDE", "BC"],
|
|
159
|
+
total_rounds=2, fmt=fmt)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Board totals (`board_points`) back the §12 codes; forfeit/PAB legs are recorded at standard-win values per the §12 intro. More: `examples/c_team_tournament.py`.
|
|
163
|
+
|
|
164
|
+
## Generic modifiers
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
from tiebreak_core import (
|
|
168
|
+
calculate_descriptor_strict, rank_descriptors_strict, parse_descriptor,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
parse_descriptor("SB/C2/P") # base SB + Cut-2 + forfeit inclusion
|
|
172
|
+
calculate_descriptor_strict(player, players, "BH/C3", total_rounds=9,
|
|
173
|
+
ruleset="fide-2026")
|
|
174
|
+
rank_descriptors_strict(players, ["BH/C1", "SB/C1", "DE", "TPN/R"],
|
|
175
|
+
total_rounds=9, ruleset="fide-2026",
|
|
176
|
+
pairing_numbers={...})
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`n=1/2` resolve to the named implementations (equivalence-tested); arbitrary valid `n`, `/L±n` Koya limits, and `/R` terminal reversal work uniformly. Invalid combinations raise `InvalidDescriptorError`. More: `examples/d_generic_modifier.py`, `examples/e_invalid_modifier.py`.
|
|
180
|
+
|
|
181
|
+
## Ruleset selection and Article 16 policy
|
|
182
|
+
|
|
183
|
+
Pass `ruleset=` explicitly on the strict path (`legacy-0.1.0`, `fide-2024`, `fide-2026`); `mode="round_robin"` selects the §15.2 regime under `fide-2026`. Unplayed-round policy is inspectable as data:
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
from tiebreak_core import resolve_policy
|
|
187
|
+
resolve_policy("fide-2026") # capped 2026 dummies
|
|
188
|
+
resolve_policy("fide-2026", mode="round_robin") # §15.2 forfeit scope
|
|
189
|
+
resolve_policy("fide-2026", late_bye_value=0.0) # explicit §16.6 override
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
More: `examples/f_article16_policy.py`, `examples/g_deterministic_ranking.py`.
|
|
193
|
+
|
|
194
|
+
## Error handling
|
|
195
|
+
|
|
196
|
+
Every failure is typed and actionable: unknown criteria (`UnknownCriterionError`), unimplemented combinations (`UnsupportedCriterionError`), malformed descriptors (`InvalidDescriptorError`), bad records (`InvalidGameRecordError` / `InvalidPlayerDataError`), bad rulesets (`UnsupportedRulesetError`), registry misuse (`RegistryError`). The legacy path preserves its frozen lenient behavior; new code should use the strict path.
|
|
197
|
+
|
|
198
|
+
## Determinism and correctness
|
|
199
|
+
|
|
200
|
+
Same input + same ruleset → same output. Calculations never mutate caller data. Ranking sorts exact internal values (presentation rounding never decides order). The suite covers official FIDE worked values, definition-derived hand calculations, differential comparison against an independent engine, property/metamorphic invariants, invalid-input matrices, and benchmarks.
|
|
201
|
+
|
|
202
|
+
## Validation and evidence
|
|
203
|
+
|
|
204
|
+
- `tests/corpus/` — official FIDE worked examples (each case names its source).
|
|
205
|
+
- `tests/test_differential.py` — independent-oracle comparison harness.
|
|
206
|
+
- `tests/test_examples.py` — every `examples/*.py` script executes in CI.
|
|
207
|
+
- Evidence classes (`PRIMARY_NORMATIVE`, `OFFICIAL_VALUE`, `INDEPENDENT_ORACLE`, `PROJECT_DERIVED`) are defined in `docs/FIDE_SOURCE_REGISTRY.md` and never mixed.
|
|
208
|
+
|
|
209
|
+
## Architecture
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
consumer (chess-manager, pairing-core callers, your app)
|
|
213
|
+
│ plain dataclasses in, immutable results out
|
|
214
|
+
▼
|
|
215
|
+
tiebreak_core
|
|
216
|
+
modifiers → MTB26 descriptor parsing + generic cut/median engine
|
|
217
|
+
fide2026 / fide2024 → individual calculation engines (versioned)
|
|
218
|
+
team → team domain (§§11–13) on TeamMatch records
|
|
219
|
+
ranking → pure staged ordering (scalar / group / terminal stages)
|
|
220
|
+
strict → validated boundary with typed errors
|
|
221
|
+
article16 / scoring → policy + point-table models
|
|
222
|
+
registry / rules / errors / models / display
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Details: `docs/ARCHITECTURE.md`, `docs/DOMAIN_MODEL.md`, `docs/adr/`.
|
|
226
|
+
|
|
227
|
+
## Package / API overview
|
|
228
|
+
|
|
229
|
+
Top-level exports (~100 names): input models (`GameRecord`, `PlayerTiebreakData`, `TeamMatch`, `TeamRecord`, `TeamFormat`), calculators (`calculate*`, `calculate_descriptor*`, `calculate_team*`), ranking (`rank_standings*`, `rank_descriptors*`, `rank_team_standings*`, `order_ids*`), rulesets (`available_rulesets`, `describe_ruleset`), policy/scoring (`resolve_policy`, `ScoringScheme`), and all error types. Full surface: `docs/API.md`.
|
|
230
|
+
|
|
231
|
+
## Documentation map
|
|
232
|
+
|
|
233
|
+
| Document | Purpose |
|
|
234
|
+
|---|---|
|
|
235
|
+
| `docs/API.md` | Complete API reference with examples |
|
|
236
|
+
| `docs/ARCHITECTURE.md` | Boundaries, layers, dependency direction |
|
|
237
|
+
| `docs/FIDE_CRITERIA_CATALOG.md` | Per-criterion formulas and status |
|
|
238
|
+
| `docs/FIDE_TIEBREAK_MODIFIERS.md` | Modifier inventory (`/Cn /Mn /L /P /F /R /Kx / :MP/:GP`) |
|
|
239
|
+
| `docs/FIDE_MTB26_CATALOG.md` | MTB26 descriptor table + core-request boundary |
|
|
240
|
+
| `docs/FIDE_SOURCE_REGISTRY.md` | Auditable source index with evidence classes |
|
|
241
|
+
| `docs/FIDE_2026_DIFF.md` | 2024→2026 word-level diff (D1–D15) |
|
|
242
|
+
| `docs/KNOWN_LIMITATIONS.md` | Explicit scope boundaries |
|
|
243
|
+
| `docs/VERSIONING.md` | Ruleset vs package versioning policy |
|
|
244
|
+
| `docs/DEVELOPMENT.md` | Development and testing guide |
|
|
245
|
+
| `CHANGELOG.md` | Release notes |
|
|
246
|
+
|
|
247
|
+
## FIDE references
|
|
248
|
+
|
|
249
|
+
Normative basis: FIDE Handbook C.07 Play-Off and Tie-Break Regulations (editions effective 1 Aug 2024 and 1 Mar 2026), the MTB26 mandatory tie-break table, FIDE Rating Regulations §§8.1a/8.1b, and FIDE TEC worked examples. Full index: `docs/FIDE_SOURCE_REGISTRY.md`.
|
|
250
|
+
|
|
251
|
+
## FIDE conformity
|
|
252
|
+
|
|
253
|
+
This library **implements the applicable FIDE C.07 calculation semantics** within its documented scope and is **validated against authoritative FIDE material** (official worked values, definition-derived tests, independent differential checks). It is **not FIDE-approved, certified, or endorsed** — approval attaches to complete tournament-helper programs through FIDE's own process (see `docs/FIDE_APPROVAL_PATH.md`), never to a calculation library alone.
|
|
254
|
+
|
|
255
|
+
## Known limitations (summary)
|
|
256
|
+
|
|
257
|
+
Exotic scoring tables require explicit per-round opponent scores (typed error otherwise); the Buchholz round-robin restriction is documented, not enforced (organizer list duty); team edge readings without retrieved official examples are documented `PROJECT_DERIVED` interpretations; rating inputs must be the tournament-start snapshot (consumer contract). Full list: `docs/KNOWN_LIMITATIONS.md`.
|
|
258
|
+
|
|
259
|
+
## Development and testing
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
pip install -e .
|
|
263
|
+
python -m pytest tests -q # full suite
|
|
264
|
+
python -m pytest tests/test_examples.py -q # examples gate
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
See `docs/DEVELOPMENT.md`. CI runs Python 3.10–3.12 plus a packaging smoke test.
|
|
268
|
+
|
|
269
|
+
## Contributing
|
|
270
|
+
|
|
271
|
+
Issues and pull requests are welcome. Preserve frozen-ruleset outputs (see `docs/VERSIONING.md`), keep the dependency footprint at zero, add regression coverage for behavior changes, and never claim FIDE approval.
|
|
272
|
+
|
|
273
|
+
## License
|
|
274
|
+
|
|
275
|
+
MIT — see `LICENSE`.
|
|
276
|
+
|
|
277
|
+
## Status
|
|
278
|
+
|
|
279
|
+
Current release: **1.2.0** (`docs/VERSIONING.md`, `CHANGELOG.md`). Stable public API; additive extensions only — output-changing corrections to published rulesets require new ruleset ids (sole pre-publication exception: the F4 DE mini-table correction, documented in `CHANGELOG.md`).
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# tiebreak-core
|
|
2
|
+
|
|
3
|
+
[](https://github.com/20kevit/tiebreak-core/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
Standalone Python library for chess tie-break calculation, implementing the applicable FIDE C.07 semantics for individual and team tournaments.
|
|
6
|
+
|
|
7
|
+
Pure Python. Zero runtime dependencies. Deterministic. MIT licensed.
|
|
8
|
+
|
|
9
|
+
## What it is
|
|
10
|
+
|
|
11
|
+
`tiebreak-core` computes chess tie-break values and orderings from normalized tournament data you supply: per-player game records (individual) or per-team match records (team). It implements the FIDE C.07 calculation semantics — Buchholz families, Sonneborn-Berger, progressive scores, Koya, rating-based criteria, Direct Encounter, team systems (MP/GP, Board Count, Extended Sonneborn-Berger, Extended Direct Encounter, SSSC), and the generic MTB26 modifier machine (`/Cn`, `/Mn`, `/L±n`, `/P`, `/F`, `/R`, `:MP/:GP`) — under explicit, versioned rulesets.
|
|
12
|
+
|
|
13
|
+
## What it is NOT
|
|
14
|
+
|
|
15
|
+
Not a tournament manager, not a pairing engine, not a rating calculator, not a TRF file parser, not a web service, not a database layer. It owns calculation and ordering only; seeding, pairing, persistence, and file I/O belong to consumers. It is **not FIDE-approved software** — no such claim is made (see [FIDE conformity](#fide-conformity) below).
|
|
16
|
+
|
|
17
|
+
## Why it exists
|
|
18
|
+
|
|
19
|
+
Tournament applications (`chess-manager`, `pairing-core` integrations, future tools) all need the same FIDE tie-break arithmetic. `tiebreak-core` is the shared, independently testable home for it, with frozen ruleset semantics so historical results never shift under callers.
|
|
20
|
+
|
|
21
|
+
## Features
|
|
22
|
+
|
|
23
|
+
- **Individual Swiss / Round Robin / pre-determined pairings** — Article 16 unplayed-round management (categories, adjusted scores, capped/uncapped dummies, VUR-aware cuts), Swiss vs RR forfeit scope flag.
|
|
24
|
+
- **Team tournaments** — full C.07 Articles 11–13 on a dedicated `TeamMatch`/`TeamRecord` model.
|
|
25
|
+
- **Generic modifiers** — any valid MTB26 descriptor (`BH/C3`, `ARO/M2`, `KS/L+1`, `SB/C2/P`, `TPN/R`), with typed rejection of FIDE-undefined combinations.
|
|
26
|
+
- **Explicit rulesets** — `legacy-0.1.0` (frozen), `fide-2024` (frozen), `fide-2026` (current). Same input + same ruleset → same output, forever.
|
|
27
|
+
- **Typed failures** — `UnknownCriterionError`, `UnsupportedCriterionError`, `InvalidDescriptorError`, `InvalidGameRecordError`, `InvalidPlayerDataError`, and more. No silent fallback on the strict path.
|
|
28
|
+
- **Deterministic ranking** — staged ordering (points → criteria → group stages → explicit keys), no mutation of caller data.
|
|
29
|
+
|
|
30
|
+
## FIDE scope
|
|
31
|
+
|
|
32
|
+
| Area | Status |
|
|
33
|
+
|---|---|
|
|
34
|
+
| Individual criteria, C.07 Arts. 6–10 + modifiers Art. 14 | Implemented (`fide-2024`, `fide-2026`) |
|
|
35
|
+
| Unplayed rounds, Art. 16 (both editions) + RR §15.2 carve-out | Implemented |
|
|
36
|
+
| Team criteria, Arts. 11–13 (MP/GP, BC/TBR/BBE, MPvGP, ESB×4, EDE+chains, SSSC) | Implemented |
|
|
37
|
+
| Rating tables (§§8.1a/8.1b), ARO/TPR/PTP/APRO/APPO/RTNG | Implemented |
|
|
38
|
+
| Direct Encounter (§6) / Extended DE (§13.3) as group stages | Implemented |
|
|
39
|
+
| Play-off formats (Art. 3), pairing-time evaluation (C.04), norm calculations (B.01) | Consumer-owned / out of scope |
|
|
40
|
+
| Exotic scoring tables without explicit per-round scores | Rejected with a typed error |
|
|
41
|
+
|
|
42
|
+
Details: `docs/FIDE_COMPLETE_REQUIREMENTS_MATRIX.md`. Limitations: `docs/KNOWN_LIMITATIONS.md`.
|
|
43
|
+
|
|
44
|
+
## Rulesets
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from tiebreak_core import available_rulesets
|
|
48
|
+
for rs in available_rulesets():
|
|
49
|
+
print(rs.id, "-", rs.status)
|
|
50
|
+
# legacy-0.1.0 - implemented (frozen behavior-preserving extraction)
|
|
51
|
+
# fide-2026 - implemented (current C.07: §16.4 caps, RR mode, TPN/RTNG, /P)
|
|
52
|
+
# fide-2024 - implemented (frozen era semantics)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
See `docs/VERSIONING.md` for the frozen-ruleset policy.
|
|
56
|
+
|
|
57
|
+
## Installation
|
|
58
|
+
|
|
59
|
+
Requires Python ≥ 3.10. No dependencies.
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install tiebreak-core
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
From source:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install -e .
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Quick start — individual Swiss
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from tiebreak_core import (
|
|
75
|
+
GameRecord, PlayerTiebreakData,
|
|
76
|
+
calculate_all_strict, rank_standings_strict,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
players = {
|
|
80
|
+
1: PlayerTiebreakData(1, 2000, 2.5, [
|
|
81
|
+
GameRecord(2, 1900, 1.0, "white", 1, kind="played"),
|
|
82
|
+
GameRecord(3, 1950, 0.5, "black", 2, kind="played"),
|
|
83
|
+
GameRecord(4, 1800, 1.0, "white", 3, kind="played"),
|
|
84
|
+
]),
|
|
85
|
+
2: PlayerTiebreakData(2, 1900, 1.0, [
|
|
86
|
+
GameRecord(1, 2000, 0.0, "black", 1, kind="played"),
|
|
87
|
+
GameRecord(4, 1800, 1.0, "white", 2, kind="played"),
|
|
88
|
+
GameRecord(3, 1950, 0.0, "black", 3, kind="played"),
|
|
89
|
+
]),
|
|
90
|
+
3: PlayerTiebreakData(3, 1950, 2.0, [
|
|
91
|
+
GameRecord(4, 1800, 1.0, "white", 1, kind="played"),
|
|
92
|
+
GameRecord(1, 2000, 0.5, "white", 2, kind="played"),
|
|
93
|
+
GameRecord(2, 1900, 0.5, "white", 3, kind="played"),
|
|
94
|
+
]),
|
|
95
|
+
4: PlayerTiebreakData(4, 1800, 0.0, [
|
|
96
|
+
GameRecord(3, 1950, 0.0, "black", 1, kind="played"),
|
|
97
|
+
GameRecord(2, 1900, 0.0, "black", 2, kind="played"),
|
|
98
|
+
GameRecord(1, 2000, 0.0, "black", 3, kind="played"),
|
|
99
|
+
]),
|
|
100
|
+
}
|
|
101
|
+
criteria = ["buchholz_cut1", "sonneborn_berger"]
|
|
102
|
+
|
|
103
|
+
values = calculate_all_strict(players[1], players, criteria,
|
|
104
|
+
total_rounds=3, ruleset="fide-2026")
|
|
105
|
+
# {'buchholz_cut1': 3.0, 'sonneborn_berger': 2.0}
|
|
106
|
+
|
|
107
|
+
standings = rank_standings_strict(players, criteria, total_rounds=3,
|
|
108
|
+
ruleset="fide-2026")
|
|
109
|
+
print([p.player_id for p in standings.players]) # [1, 3, 2, 4]
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Game kinds (`played`, `pairing_bye`, `forfeit_win`, `forfeit_loss`, `requested_bye`, …) drive Article 16 handling — see `docs/FIDE_DATA_SEMANTICS.md`. More: `examples/a_individual_swiss.py`, `examples/b_individual_round_robin.py`.
|
|
113
|
+
|
|
114
|
+
## Team tournaments
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
from tiebreak_core import (
|
|
118
|
+
TeamFormat, TeamMatch, TeamRecord,
|
|
119
|
+
calculate_team_strict, rank_teams_strict,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
fmt = TeamFormat(mp_win=2.0, mp_draw=1.0)
|
|
123
|
+
teams = {
|
|
124
|
+
1: TeamRecord(1, 3.0, 5.0, [
|
|
125
|
+
TeamMatch(2, 1, 2, 3.0),
|
|
126
|
+
TeamMatch(2, 2, 1, 2.0)], (2.5, 1.5, 0.5, 0.5)),
|
|
127
|
+
2: TeamRecord(2, 1.0, 3.0, [
|
|
128
|
+
TeamMatch(1, 1, 0, 1.0),
|
|
129
|
+
TeamMatch(1, 2, 1, 2.0)], (1.5, 0.5, 0.5, 0.5)),
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
emmsb = calculate_team_strict(teams[1], teams, "EMMSB",
|
|
133
|
+
total_rounds=2, fmt=fmt) # 3.0
|
|
134
|
+
standings = rank_teams_strict(teams, ["EMMSB", "EDE", "BC"],
|
|
135
|
+
total_rounds=2, fmt=fmt)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Board totals (`board_points`) back the §12 codes; forfeit/PAB legs are recorded at standard-win values per the §12 intro. More: `examples/c_team_tournament.py`.
|
|
139
|
+
|
|
140
|
+
## Generic modifiers
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from tiebreak_core import (
|
|
144
|
+
calculate_descriptor_strict, rank_descriptors_strict, parse_descriptor,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
parse_descriptor("SB/C2/P") # base SB + Cut-2 + forfeit inclusion
|
|
148
|
+
calculate_descriptor_strict(player, players, "BH/C3", total_rounds=9,
|
|
149
|
+
ruleset="fide-2026")
|
|
150
|
+
rank_descriptors_strict(players, ["BH/C1", "SB/C1", "DE", "TPN/R"],
|
|
151
|
+
total_rounds=9, ruleset="fide-2026",
|
|
152
|
+
pairing_numbers={...})
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`n=1/2` resolve to the named implementations (equivalence-tested); arbitrary valid `n`, `/L±n` Koya limits, and `/R` terminal reversal work uniformly. Invalid combinations raise `InvalidDescriptorError`. More: `examples/d_generic_modifier.py`, `examples/e_invalid_modifier.py`.
|
|
156
|
+
|
|
157
|
+
## Ruleset selection and Article 16 policy
|
|
158
|
+
|
|
159
|
+
Pass `ruleset=` explicitly on the strict path (`legacy-0.1.0`, `fide-2024`, `fide-2026`); `mode="round_robin"` selects the §15.2 regime under `fide-2026`. Unplayed-round policy is inspectable as data:
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
from tiebreak_core import resolve_policy
|
|
163
|
+
resolve_policy("fide-2026") # capped 2026 dummies
|
|
164
|
+
resolve_policy("fide-2026", mode="round_robin") # §15.2 forfeit scope
|
|
165
|
+
resolve_policy("fide-2026", late_bye_value=0.0) # explicit §16.6 override
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
More: `examples/f_article16_policy.py`, `examples/g_deterministic_ranking.py`.
|
|
169
|
+
|
|
170
|
+
## Error handling
|
|
171
|
+
|
|
172
|
+
Every failure is typed and actionable: unknown criteria (`UnknownCriterionError`), unimplemented combinations (`UnsupportedCriterionError`), malformed descriptors (`InvalidDescriptorError`), bad records (`InvalidGameRecordError` / `InvalidPlayerDataError`), bad rulesets (`UnsupportedRulesetError`), registry misuse (`RegistryError`). The legacy path preserves its frozen lenient behavior; new code should use the strict path.
|
|
173
|
+
|
|
174
|
+
## Determinism and correctness
|
|
175
|
+
|
|
176
|
+
Same input + same ruleset → same output. Calculations never mutate caller data. Ranking sorts exact internal values (presentation rounding never decides order). The suite covers official FIDE worked values, definition-derived hand calculations, differential comparison against an independent engine, property/metamorphic invariants, invalid-input matrices, and benchmarks.
|
|
177
|
+
|
|
178
|
+
## Validation and evidence
|
|
179
|
+
|
|
180
|
+
- `tests/corpus/` — official FIDE worked examples (each case names its source).
|
|
181
|
+
- `tests/test_differential.py` — independent-oracle comparison harness.
|
|
182
|
+
- `tests/test_examples.py` — every `examples/*.py` script executes in CI.
|
|
183
|
+
- Evidence classes (`PRIMARY_NORMATIVE`, `OFFICIAL_VALUE`, `INDEPENDENT_ORACLE`, `PROJECT_DERIVED`) are defined in `docs/FIDE_SOURCE_REGISTRY.md` and never mixed.
|
|
184
|
+
|
|
185
|
+
## Architecture
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
consumer (chess-manager, pairing-core callers, your app)
|
|
189
|
+
│ plain dataclasses in, immutable results out
|
|
190
|
+
▼
|
|
191
|
+
tiebreak_core
|
|
192
|
+
modifiers → MTB26 descriptor parsing + generic cut/median engine
|
|
193
|
+
fide2026 / fide2024 → individual calculation engines (versioned)
|
|
194
|
+
team → team domain (§§11–13) on TeamMatch records
|
|
195
|
+
ranking → pure staged ordering (scalar / group / terminal stages)
|
|
196
|
+
strict → validated boundary with typed errors
|
|
197
|
+
article16 / scoring → policy + point-table models
|
|
198
|
+
registry / rules / errors / models / display
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Details: `docs/ARCHITECTURE.md`, `docs/DOMAIN_MODEL.md`, `docs/adr/`.
|
|
202
|
+
|
|
203
|
+
## Package / API overview
|
|
204
|
+
|
|
205
|
+
Top-level exports (~100 names): input models (`GameRecord`, `PlayerTiebreakData`, `TeamMatch`, `TeamRecord`, `TeamFormat`), calculators (`calculate*`, `calculate_descriptor*`, `calculate_team*`), ranking (`rank_standings*`, `rank_descriptors*`, `rank_team_standings*`, `order_ids*`), rulesets (`available_rulesets`, `describe_ruleset`), policy/scoring (`resolve_policy`, `ScoringScheme`), and all error types. Full surface: `docs/API.md`.
|
|
206
|
+
|
|
207
|
+
## Documentation map
|
|
208
|
+
|
|
209
|
+
| Document | Purpose |
|
|
210
|
+
|---|---|
|
|
211
|
+
| `docs/API.md` | Complete API reference with examples |
|
|
212
|
+
| `docs/ARCHITECTURE.md` | Boundaries, layers, dependency direction |
|
|
213
|
+
| `docs/FIDE_CRITERIA_CATALOG.md` | Per-criterion formulas and status |
|
|
214
|
+
| `docs/FIDE_TIEBREAK_MODIFIERS.md` | Modifier inventory (`/Cn /Mn /L /P /F /R /Kx / :MP/:GP`) |
|
|
215
|
+
| `docs/FIDE_MTB26_CATALOG.md` | MTB26 descriptor table + core-request boundary |
|
|
216
|
+
| `docs/FIDE_SOURCE_REGISTRY.md` | Auditable source index with evidence classes |
|
|
217
|
+
| `docs/FIDE_2026_DIFF.md` | 2024→2026 word-level diff (D1–D15) |
|
|
218
|
+
| `docs/KNOWN_LIMITATIONS.md` | Explicit scope boundaries |
|
|
219
|
+
| `docs/VERSIONING.md` | Ruleset vs package versioning policy |
|
|
220
|
+
| `docs/DEVELOPMENT.md` | Development and testing guide |
|
|
221
|
+
| `CHANGELOG.md` | Release notes |
|
|
222
|
+
|
|
223
|
+
## FIDE references
|
|
224
|
+
|
|
225
|
+
Normative basis: FIDE Handbook C.07 Play-Off and Tie-Break Regulations (editions effective 1 Aug 2024 and 1 Mar 2026), the MTB26 mandatory tie-break table, FIDE Rating Regulations §§8.1a/8.1b, and FIDE TEC worked examples. Full index: `docs/FIDE_SOURCE_REGISTRY.md`.
|
|
226
|
+
|
|
227
|
+
## FIDE conformity
|
|
228
|
+
|
|
229
|
+
This library **implements the applicable FIDE C.07 calculation semantics** within its documented scope and is **validated against authoritative FIDE material** (official worked values, definition-derived tests, independent differential checks). It is **not FIDE-approved, certified, or endorsed** — approval attaches to complete tournament-helper programs through FIDE's own process (see `docs/FIDE_APPROVAL_PATH.md`), never to a calculation library alone.
|
|
230
|
+
|
|
231
|
+
## Known limitations (summary)
|
|
232
|
+
|
|
233
|
+
Exotic scoring tables require explicit per-round opponent scores (typed error otherwise); the Buchholz round-robin restriction is documented, not enforced (organizer list duty); team edge readings without retrieved official examples are documented `PROJECT_DERIVED` interpretations; rating inputs must be the tournament-start snapshot (consumer contract). Full list: `docs/KNOWN_LIMITATIONS.md`.
|
|
234
|
+
|
|
235
|
+
## Development and testing
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
pip install -e .
|
|
239
|
+
python -m pytest tests -q # full suite
|
|
240
|
+
python -m pytest tests/test_examples.py -q # examples gate
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
See `docs/DEVELOPMENT.md`. CI runs Python 3.10–3.12 plus a packaging smoke test.
|
|
244
|
+
|
|
245
|
+
## Contributing
|
|
246
|
+
|
|
247
|
+
Issues and pull requests are welcome. Preserve frozen-ruleset outputs (see `docs/VERSIONING.md`), keep the dependency footprint at zero, add regression coverage for behavior changes, and never claim FIDE approval.
|
|
248
|
+
|
|
249
|
+
## License
|
|
250
|
+
|
|
251
|
+
MIT — see `LICENSE`.
|
|
252
|
+
|
|
253
|
+
## Status
|
|
254
|
+
|
|
255
|
+
Current release: **1.2.0** (`docs/VERSIONING.md`, `CHANGELOG.md`). Stable public API; additive extensions only — output-changing corrections to published rulesets require new ruleset ids (sole pre-publication exception: the F4 DE mini-table correction, documented in `CHANGELOG.md`).
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tiebreak-core"
|
|
7
|
+
version = "1.2.0"
|
|
8
|
+
description = "Standalone FIDE chess tie-break calculation library for individual and team tournaments"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "20kevit" }]
|
|
13
|
+
keywords = ["chess", "tiebreak", "tie-break", "fide", "tournament", "swiss", "round-robin"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 5 - Production/Stable",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
23
|
+
"Topic :: Games/Entertainment :: Board Games",
|
|
24
|
+
"Typing :: Typed",
|
|
25
|
+
]
|
|
26
|
+
dependencies = []
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/20kevit/tiebreak-core"
|
|
30
|
+
Repository = "https://github.com/20kevit/tiebreak-core"
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.packages.find]
|
|
33
|
+
where = ["src"]
|
|
34
|
+
|
|
35
|
+
[tool.setuptools.package-data]
|
|
36
|
+
tiebreak_core = ["py.typed"]
|
|
37
|
+
|
|
38
|
+
[tool.pytest.ini_options]
|
|
39
|
+
testpaths = ["tests"]
|