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.
@@ -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())