caissify-pairings 0.1.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.
- caissify_pairings/__init__.py +52 -0
- caissify_pairings/__main__.py +73 -0
- caissify_pairings/base.py +57 -0
- caissify_pairings/engines/__init__.py +43 -0
- caissify_pairings/engines/dutch.py +3334 -0
- caissify_pairings/fpc.py +499 -0
- caissify_pairings/rtg.py +393 -0
- caissify_pairings/trf.py +458 -0
- caissify_pairings-0.1.0.dist-info/METADATA +241 -0
- caissify_pairings-0.1.0.dist-info/RECORD +13 -0
- caissify_pairings-0.1.0.dist-info/WHEEL +4 -0
- caissify_pairings-0.1.0.dist-info/entry_points.txt +4 -0
- caissify_pairings-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
caissify-pairings — Pluggable chess tournament pairing engines.
|
|
3
|
+
|
|
4
|
+
Public API:
|
|
5
|
+
generate_pairings(system, players, previous_pairings, round_number,
|
|
6
|
+
total_rounds, **kwargs) -> List[dict]
|
|
7
|
+
|
|
8
|
+
Engines:
|
|
9
|
+
"dutch" — FIDE Dutch System (C.04.3, Feb 2026)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
__version__ = "0.1.0"
|
|
15
|
+
|
|
16
|
+
from typing import List, Set, Tuple
|
|
17
|
+
|
|
18
|
+
from caissify_pairings.engines import available_systems, get_engine
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def generate_pairings(
|
|
22
|
+
system: str,
|
|
23
|
+
players: List[dict],
|
|
24
|
+
previous_pairings: Set[Tuple[int, int]],
|
|
25
|
+
round_number: int,
|
|
26
|
+
total_rounds: int,
|
|
27
|
+
**kwargs,
|
|
28
|
+
) -> List[dict]:
|
|
29
|
+
"""
|
|
30
|
+
Generate pairings using the specified pairing system.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
system: Engine name — "dutch", "swiss", etc.
|
|
34
|
+
players: List of player dicts (see BasePairingEngine).
|
|
35
|
+
previous_pairings: Set of (id_a, id_b) tuples from prior rounds.
|
|
36
|
+
round_number: 1-based round number to pair.
|
|
37
|
+
total_rounds: Total rounds in the tournament.
|
|
38
|
+
**kwargs: Engine-specific options (bye_value, max_byes_per_player, …).
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
List of pairing dicts with white_id, black_id, table, and optional
|
|
42
|
+
bye/bye_type fields.
|
|
43
|
+
"""
|
|
44
|
+
engine_cls = get_engine(system)
|
|
45
|
+
engine = engine_cls(
|
|
46
|
+
players=players,
|
|
47
|
+
previous_pairings=previous_pairings,
|
|
48
|
+
round_number=round_number,
|
|
49
|
+
total_rounds=total_rounds,
|
|
50
|
+
**kwargs,
|
|
51
|
+
)
|
|
52
|
+
return engine.generate_pairings()
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI entry point for caissify-pairings.
|
|
3
|
+
|
|
4
|
+
Modes:
|
|
5
|
+
caissify-pairings — read JSON from stdin, output pairings
|
|
6
|
+
caissify-pairings --check FILE.trf — FPC: check a TRF file against engine
|
|
7
|
+
caissify-pairings-check FILE.trf — FPC shortcut
|
|
8
|
+
caissify-pairings-rtg [options] — RTG: generate random tournaments
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
from caissify_pairings import generate_pairings
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main() -> None:
|
|
20
|
+
# If --check flag is present, delegate to FPC
|
|
21
|
+
if len(sys.argv) >= 2 and sys.argv[1] == "--check":
|
|
22
|
+
from caissify_pairings.fpc import main as fpc_main
|
|
23
|
+
# Shift argv so fpc_main sees the file as argv[1]
|
|
24
|
+
sys.argv = [sys.argv[0]] + sys.argv[2:]
|
|
25
|
+
fpc_main()
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
raw = sys.stdin.read()
|
|
29
|
+
if not raw.strip():
|
|
30
|
+
print(json.dumps({"error": "Empty input. Provide JSON on stdin."}), file=sys.stderr)
|
|
31
|
+
sys.exit(1)
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
data = json.loads(raw)
|
|
35
|
+
except json.JSONDecodeError as e:
|
|
36
|
+
print(json.dumps({"error": f"Invalid JSON: {e}"}), file=sys.stderr)
|
|
37
|
+
sys.exit(1)
|
|
38
|
+
|
|
39
|
+
system = data.get("system", "dutch")
|
|
40
|
+
players = data.get("players", [])
|
|
41
|
+
round_number = data.get("round_number", 1)
|
|
42
|
+
total_rounds = data.get("total_rounds", 9)
|
|
43
|
+
|
|
44
|
+
# Convert list-of-lists to set-of-tuples for previous_pairings
|
|
45
|
+
raw_pairings = data.get("previous_pairings", [])
|
|
46
|
+
previous_pairings = {(a, b) for a, b in raw_pairings}
|
|
47
|
+
|
|
48
|
+
# Forward engine-specific options
|
|
49
|
+
kwargs = {}
|
|
50
|
+
if "bye_value" in data:
|
|
51
|
+
kwargs["bye_value"] = data["bye_value"]
|
|
52
|
+
if "max_byes_per_player" in data:
|
|
53
|
+
kwargs["max_byes_per_player"] = data["max_byes_per_player"]
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
pairings = generate_pairings(
|
|
57
|
+
system=system,
|
|
58
|
+
players=players,
|
|
59
|
+
previous_pairings=previous_pairings,
|
|
60
|
+
round_number=round_number,
|
|
61
|
+
total_rounds=total_rounds,
|
|
62
|
+
**kwargs,
|
|
63
|
+
)
|
|
64
|
+
except Exception as e:
|
|
65
|
+
print(json.dumps({"error": str(e)}), file=sys.stderr)
|
|
66
|
+
sys.exit(1)
|
|
67
|
+
|
|
68
|
+
json.dump(pairings, sys.stdout, indent=2)
|
|
69
|
+
print() # trailing newline
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
if __name__ == "__main__":
|
|
73
|
+
main()
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Abstract base class for all pairing engines.
|
|
3
|
+
|
|
4
|
+
Every engine in caissify_pairings must subclass BasePairingEngine and
|
|
5
|
+
implement generate_pairings(). This guarantees a uniform I/O contract
|
|
6
|
+
across Dutch, Swiss, Burstein, and any future system.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import abc
|
|
12
|
+
from typing import Dict, List, Set, Tuple
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BasePairingEngine(abc.ABC):
|
|
16
|
+
"""
|
|
17
|
+
Abstract base for chess tournament pairing engines.
|
|
18
|
+
|
|
19
|
+
Input contract (constructor):
|
|
20
|
+
players: List[dict] — each dict must contain at minimum:
|
|
21
|
+
id, name, score, rating, starting_number,
|
|
22
|
+
color_hist, float_history, bye_count
|
|
23
|
+
previous_pairings: Set[Tuple[int, int]] — all (id_a, id_b) pairs
|
|
24
|
+
from earlier rounds (order-insensitive)
|
|
25
|
+
round_number: int — 1-based round to pair
|
|
26
|
+
total_rounds: int — total rounds in the tournament
|
|
27
|
+
|
|
28
|
+
Output contract (generate_pairings):
|
|
29
|
+
List[dict] where each dict has:
|
|
30
|
+
white_id: int — player ID assigned white
|
|
31
|
+
black_id: int | None — player ID assigned black (None = bye)
|
|
32
|
+
table: int — 1-based table number
|
|
33
|
+
bye: bool — (optional) True for bye pairings
|
|
34
|
+
bye_type: str — (optional) FIDE bye code, e.g. "U"
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
#: Override in subclass — must match the key used in the engine registry.
|
|
38
|
+
name: str = ""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
players: List[dict],
|
|
43
|
+
previous_pairings: Set[Tuple[int, int]],
|
|
44
|
+
round_number: int,
|
|
45
|
+
total_rounds: int,
|
|
46
|
+
**kwargs,
|
|
47
|
+
):
|
|
48
|
+
self.players = players
|
|
49
|
+
self.previous_pairings = previous_pairings
|
|
50
|
+
self.round_number = round_number
|
|
51
|
+
self.total_rounds = total_rounds
|
|
52
|
+
self.options = kwargs
|
|
53
|
+
|
|
54
|
+
@abc.abstractmethod
|
|
55
|
+
def generate_pairings(self) -> List[dict]:
|
|
56
|
+
"""Generate pairings for the current round. Must be implemented."""
|
|
57
|
+
...
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Engine registry — maps system names to engine classes.
|
|
3
|
+
|
|
4
|
+
To register a new engine, add it to ENGINES below.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Dict, Type
|
|
10
|
+
|
|
11
|
+
from caissify_pairings.base import BasePairingEngine
|
|
12
|
+
|
|
13
|
+
# Lazy imports to keep startup fast — engines are imported only when requested.
|
|
14
|
+
_REGISTRY: Dict[str, str] = {
|
|
15
|
+
"dutch": "caissify_pairings.engines.dutch",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
# Populated on first access
|
|
19
|
+
_CACHE: Dict[str, Type[BasePairingEngine]] = {}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_engine(system: str) -> Type[BasePairingEngine]:
|
|
23
|
+
"""Return the engine class for the given system name."""
|
|
24
|
+
if system in _CACHE:
|
|
25
|
+
return _CACHE[system]
|
|
26
|
+
|
|
27
|
+
module_path = _REGISTRY.get(system)
|
|
28
|
+
if module_path is None:
|
|
29
|
+
available = ", ".join(sorted(_REGISTRY.keys()))
|
|
30
|
+
raise ValueError(
|
|
31
|
+
f"Unknown pairing system {system!r}. Available: {available}"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
import importlib
|
|
35
|
+
mod = importlib.import_module(module_path)
|
|
36
|
+
engine_cls = mod.Engine # Convention: each engine module exposes `Engine`
|
|
37
|
+
_CACHE[system] = engine_cls
|
|
38
|
+
return engine_cls
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def available_systems() -> list[str]:
|
|
42
|
+
"""Return a sorted list of registered system names."""
|
|
43
|
+
return sorted(_REGISTRY.keys())
|