python-reckless 0.1.6__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.
@@ -0,0 +1,5 @@
1
+ *.DS_Store
2
+ venv
3
+ .python-version
4
+ dist
5
+ notes.txt
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Your Name
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,86 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-reckless
3
+ Version: 0.1.6
4
+ Summary: A lightweight Python wrapper for the Reckless UCI chess engine
5
+ Project-URL: Homepage, https://github.com/DaveP80/python-reckless
6
+ Project-URL: Repository, https://github.com/DaveP80/python-reckless
7
+ Project-URL: Issues, https://github.com/DaveP80/python-reckless/issues
8
+ Author-email: David Paquette <davidpaq1@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: chess,chess-engine,reckless,uci
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Games/Entertainment :: Board Games
22
+ Requires-Python: >=3.9
23
+ Provides-Extra: dev
24
+ Requires-Dist: build; extra == 'dev'
25
+ Requires-Dist: pytest>=7.0; extra == 'dev'
26
+ Requires-Dist: twine; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # python-reckless
30
+
31
+ A lightweight, dependency-free Python wrapper for the
32
+ [Reckless](https://github.com/codedeliveryservice/Reckless) UCI chess engine.
33
+
34
+ `python-reckless` does **not** bundle the engine. Reckless is a standalone
35
+ compiled executable; this package launches it as a subprocess and speaks the
36
+ UCI protocol to it. You supply the binary.
37
+
38
+ ## Installing the engine
39
+
40
+ Download a prebuilt binary for your platform from the
41
+ [Reckless releases page](https://github.com/codedeliveryservice/Reckless/releases),
42
+ then make it discoverable in one of these ways:
43
+
44
+ - Put it on your `PATH` as `reckless`, **or**
45
+ - set the `RECKLESS_PATH` environment variable to its full path, **or**
46
+ - pass the path directly: `Reckless(path="/path/to/reckless")`.
47
+
48
+ ## Installing this package
49
+
50
+ ```bash
51
+ pip install python-reckless
52
+ ```
53
+
54
+ The import name is `reckless` (the distribution name is `python-reckless`).
55
+
56
+ ## Usage
57
+
58
+ ```python
59
+ from reckless import Reckless
60
+
61
+ with Reckless() as engine:
62
+ engine.new_game()
63
+ engine.set_position(moves=["e2e4", "e7e5"])
64
+ best = engine.best_move(depth=12)
65
+ print("Reckless suggests:", best)
66
+ ```
67
+
68
+ You can also search by time instead of depth:
69
+
70
+ ```python
71
+ move = engine.best_move(movetime=1000) # think for 1 second
72
+ ```
73
+ ## Development
74
+
75
+ Make changes on a feature branch and put up to date `engine.py` files and other helper files in the `src` directory.
76
+
77
+ ## Note
78
+
79
+ For a fully featured UCI client (analysis info, evaluations, multipv, async),
80
+ consider the mature [`python-chess`](https://pypi.org/project/chess/) library,
81
+ whose `chess.engine.SimpleEngine.popen_uci()` works with Reckless out of the
82
+ box. This package is intentionally tiny and focused.
83
+
84
+ ## License
85
+
86
+ MIT
@@ -0,0 +1,58 @@
1
+ # python-reckless
2
+
3
+ A lightweight, dependency-free Python wrapper for the
4
+ [Reckless](https://github.com/codedeliveryservice/Reckless) UCI chess engine.
5
+
6
+ `python-reckless` does **not** bundle the engine. Reckless is a standalone
7
+ compiled executable; this package launches it as a subprocess and speaks the
8
+ UCI protocol to it. You supply the binary.
9
+
10
+ ## Installing the engine
11
+
12
+ Download a prebuilt binary for your platform from the
13
+ [Reckless releases page](https://github.com/codedeliveryservice/Reckless/releases),
14
+ then make it discoverable in one of these ways:
15
+
16
+ - Put it on your `PATH` as `reckless`, **or**
17
+ - set the `RECKLESS_PATH` environment variable to its full path, **or**
18
+ - pass the path directly: `Reckless(path="/path/to/reckless")`.
19
+
20
+ ## Installing this package
21
+
22
+ ```bash
23
+ pip install python-reckless
24
+ ```
25
+
26
+ The import name is `reckless` (the distribution name is `python-reckless`).
27
+
28
+ ## Usage
29
+
30
+ ```python
31
+ from reckless import Reckless
32
+
33
+ with Reckless() as engine:
34
+ engine.new_game()
35
+ engine.set_position(moves=["e2e4", "e7e5"])
36
+ best = engine.best_move(depth=12)
37
+ print("Reckless suggests:", best)
38
+ ```
39
+
40
+ You can also search by time instead of depth:
41
+
42
+ ```python
43
+ move = engine.best_move(movetime=1000) # think for 1 second
44
+ ```
45
+ ## Development
46
+
47
+ Make changes on a feature branch and put up to date `engine.py` files and other helper files in the `src` directory.
48
+
49
+ ## Note
50
+
51
+ For a fully featured UCI client (analysis info, evaluations, multipv, async),
52
+ consider the mature [`python-chess`](https://pypi.org/project/chess/) library,
53
+ whose `chess.engine.SimpleEngine.popen_uci()` works with Reckless out of the
54
+ box. This package is intentionally tiny and focused.
55
+
56
+ ## License
57
+
58
+ MIT
@@ -0,0 +1,6 @@
1
+ """python-reckless: a lightweight wrapper for the Reckless UCI chess engine."""
2
+
3
+ from .engine import Reckless, RecklessError
4
+
5
+ __version__ = "0.1.6"
6
+ __all__ = ["Reckless", "RecklessError", "__version__"]
@@ -0,0 +1,163 @@
1
+ """A minimal, dependency-free wrapper around the Reckless UCI chess engine.
2
+
3
+ Reckless is a standalone compiled executable that speaks the Universal Chess
4
+ Interface (UCI) protocol over stdin/stdout. This module launches that
5
+ executable as a subprocess and exchanges UCI text commands with it.
6
+
7
+ It does NOT bundle the engine binary. You must have `reckless` available:
8
+ download a prebuilt binary from
9
+ https://github.com/codedeliveryservice/Reckless/releases and either put it on
10
+ your PATH, set the RECKLESS_PATH environment variable, or pass its path to
11
+ ``Reckless(path=...)``.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import os
17
+ import shutil
18
+ import subprocess
19
+ from typing import Iterable, Optional
20
+
21
+
22
+ class RecklessError(RuntimeError):
23
+ """Raised when the engine cannot be found or behaves unexpectedly."""
24
+
25
+
26
+ def _locate_engine(path: Optional[str]) -> str:
27
+ """Resolve the engine executable: explicit arg -> env var -> PATH."""
28
+ candidate = path or os.environ.get("RECKLESS_PATH") or shutil.which("reckless")
29
+ if not candidate:
30
+ raise RecklessError(
31
+ "Could not find the Reckless engine. Pass path=..., set the "
32
+ "RECKLESS_PATH environment variable, or put 'reckless' on your PATH. "
33
+ "Prebuilt binaries: "
34
+ "https://github.com/codedeliveryservice/Reckless/releases"
35
+ )
36
+ if not os.path.isfile(candidate) and not shutil.which(candidate):
37
+ raise RecklessError(f"Engine path does not point to a file: {candidate!r}")
38
+ return candidate
39
+
40
+
41
+ class Reckless:
42
+ """A simple handle to a running Reckless engine process.
43
+
44
+ Example
45
+ -------
46
+ >>> with Reckless() as engine:
47
+ ... move = engine.best_move(depth=12)
48
+ ... print(move)
49
+ """
50
+
51
+ def __init__(self, path: Optional[str] = None, *, start: bool = True) -> None:
52
+ self.path = _locate_engine(path)
53
+ self._proc: Optional[subprocess.Popen] = None
54
+ if start:
55
+ self.start()
56
+
57
+ # -- process lifecycle -------------------------------------------------
58
+
59
+ def start(self) -> "Reckless":
60
+ if self._proc is not None:
61
+ return self
62
+ self._proc = subprocess.Popen(
63
+ [self.path],
64
+ stdin=subprocess.PIPE,
65
+ stdout=subprocess.PIPE,
66
+ stderr=subprocess.STDOUT,
67
+ text=True,
68
+ bufsize=1, # line-buffered
69
+ )
70
+ self._handshake()
71
+ return self
72
+
73
+ def quit(self) -> None:
74
+ if self._proc is None:
75
+ return
76
+ try:
77
+ self._send("quit")
78
+ self._proc.wait(timeout=5)
79
+ except Exception:
80
+ self._proc.kill()
81
+ finally:
82
+ self._proc = None
83
+
84
+ def __enter__(self) -> "Reckless":
85
+ return self
86
+
87
+ def __exit__(self, *exc) -> None:
88
+ self.quit()
89
+
90
+ # -- low-level UCI I/O -------------------------------------------------
91
+
92
+ def _send(self, command: str) -> None:
93
+ if self._proc is None or self._proc.stdin is None:
94
+ raise RecklessError("Engine is not running.")
95
+ self._proc.stdin.write(command + "\n")
96
+ self._proc.stdin.flush()
97
+
98
+ def _read_until(self, token: str) -> str:
99
+ """Read lines until one starts with `token`; return that line."""
100
+ if self._proc is None or self._proc.stdout is None:
101
+ raise RecklessError("Engine is not running.")
102
+ while True:
103
+ line = self._proc.stdout.readline()
104
+ if not line:
105
+ raise RecklessError("Engine closed the connection unexpectedly.")
106
+ if line.strip().startswith(token):
107
+ return line.strip()
108
+
109
+ def _handshake(self) -> None:
110
+ self._send("uci")
111
+ self._read_until("uciok")
112
+ self.is_ready()
113
+
114
+ # -- public commands ---------------------------------------------------
115
+
116
+ def is_ready(self) -> bool:
117
+ """Block until the engine reports it is ready."""
118
+ self._send("isready")
119
+ self._read_until("readyok")
120
+ return True
121
+
122
+ def new_game(self) -> None:
123
+ self._send("ucinewgame")
124
+ self.is_ready()
125
+
126
+ def set_position(
127
+ self,
128
+ fen: Optional[str] = None,
129
+ moves: Optional[Iterable[str]] = None,
130
+ ) -> None:
131
+ """Set the board. Defaults to the standard start position.
132
+
133
+ `moves` is a sequence of UCI moves (e.g. ["e2e4", "e7e5"]).
134
+ """
135
+ base = f"fen {fen}" if fen else "startpos"
136
+ move_str = ""
137
+ if moves:
138
+ move_str = " moves " + " ".join(moves)
139
+ self._send(f"position {base}{move_str}")
140
+
141
+ def best_move(
142
+ self,
143
+ *,
144
+ depth: Optional[int] = None,
145
+ movetime: Optional[int] = None,
146
+ ) -> str:
147
+ """Search the current position and return the best move (UCI string).
148
+
149
+ Provide either `depth` (plies) or `movetime` (milliseconds).
150
+ Defaults to depth 10 if neither is given.
151
+ """
152
+ if depth is None and movetime is None:
153
+ depth = 10
154
+ go = "go"
155
+ if depth is not None:
156
+ go += f" depth {depth}"
157
+ if movetime is not None:
158
+ go += f" movetime {movetime}"
159
+ self._send(go)
160
+ line = self._read_until("bestmove")
161
+ # Format: "bestmove e2e4 ponder e7e5"
162
+ parts = line.split()
163
+ return parts[1] if len(parts) > 1 else ""
@@ -0,0 +1,49 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "python-reckless"
7
+ version = "0.1.6"
8
+ description = "A lightweight Python wrapper for the Reckless UCI chess engine"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [
14
+ { name = "David Paquette", email = "davidpaq1@gmail.com" },
15
+ ]
16
+ keywords = ["chess", "uci", "reckless", "chess-engine"]
17
+ classifiers = [
18
+ "Development Status :: 3 - Alpha",
19
+ "Intended Audience :: Developers",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.9",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Topic :: Games/Entertainment :: Board Games",
28
+ ]
29
+ dependencies = [] # zero runtime deps — we talk UCI ourselves
30
+
31
+ [project.optional-dependencies]
32
+ dev = [
33
+ "pytest>=7.0",
34
+ "build",
35
+ "twine",
36
+ ]
37
+
38
+ [project.urls]
39
+ Homepage = "https://github.com/DaveP80/python-reckless"
40
+ Repository = "https://github.com/DaveP80/python-reckless"
41
+ Issues = "https://github.com/DaveP80/python-reckless/issues"
42
+
43
+ # Distribution is "python-reckless"; the import package is "reckless".
44
+ # This mirrors python-dateutil -> import dateutil.
45
+ # IMPORTANT: this MUST point at where the package actually lives on disk.
46
+ # Files are in src/reckless/, so the path is "src/reckless" (hatchling strips
47
+ # the src/ prefix automatically, so it still imports as `reckless`).
48
+ [tool.hatch.build.targets.wheel]
49
+ packages = ["src/reckless"]
@@ -0,0 +1,6 @@
1
+ """python-reckless: a lightweight wrapper for the Reckless UCI chess engine."""
2
+
3
+ from .engine import Reckless, RecklessError
4
+
5
+ __version__ = "0.1.6"
6
+ __all__ = ["Reckless", "RecklessError", "__version__"]
@@ -0,0 +1,163 @@
1
+ """A minimal, dependency-free wrapper around the Reckless UCI chess engine.
2
+
3
+ Reckless is a standalone compiled executable that speaks the Universal Chess
4
+ Interface (UCI) protocol over stdin/stdout. This module launches that
5
+ executable as a subprocess and exchanges UCI text commands with it.
6
+
7
+ It does NOT bundle the engine binary. You must have `reckless` available:
8
+ download a prebuilt binary from
9
+ https://github.com/codedeliveryservice/Reckless/releases and either put it on
10
+ your PATH, set the RECKLESS_PATH environment variable, or pass its path to
11
+ ``Reckless(path=...)``.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import os
17
+ import shutil
18
+ import subprocess
19
+ from typing import Iterable, Optional
20
+
21
+
22
+ class RecklessError(RuntimeError):
23
+ """Raised when the engine cannot be found or behaves unexpectedly."""
24
+
25
+
26
+ def _locate_engine(path: Optional[str]) -> str:
27
+ """Resolve the engine executable: explicit arg -> env var -> PATH."""
28
+ candidate = path or os.environ.get("RECKLESS_PATH") or shutil.which("reckless")
29
+ if not candidate:
30
+ raise RecklessError(
31
+ "Could not find the Reckless engine. Pass path=..., set the "
32
+ "RECKLESS_PATH environment variable, or put 'reckless' on your PATH. "
33
+ "Prebuilt binaries: "
34
+ "https://github.com/codedeliveryservice/Reckless/releases"
35
+ )
36
+ if not os.path.isfile(candidate) and not shutil.which(candidate):
37
+ raise RecklessError(f"Engine path does not point to a file: {candidate!r}")
38
+ return candidate
39
+
40
+
41
+ class Reckless:
42
+ """A simple handle to a running Reckless engine process.
43
+
44
+ Example
45
+ -------
46
+ >>> with Reckless() as engine:
47
+ ... move = engine.best_move(depth=12)
48
+ ... print(move)
49
+ """
50
+
51
+ def __init__(self, path: Optional[str] = None, *, start: bool = True) -> None:
52
+ self.path = _locate_engine(path)
53
+ self._proc: Optional[subprocess.Popen] = None
54
+ if start:
55
+ self.start()
56
+
57
+ # -- process lifecycle -------------------------------------------------
58
+
59
+ def start(self) -> "Reckless":
60
+ if self._proc is not None:
61
+ return self
62
+ self._proc = subprocess.Popen(
63
+ [self.path],
64
+ stdin=subprocess.PIPE,
65
+ stdout=subprocess.PIPE,
66
+ stderr=subprocess.STDOUT,
67
+ text=True,
68
+ bufsize=1, # line-buffered
69
+ )
70
+ self._handshake()
71
+ return self
72
+
73
+ def quit(self) -> None:
74
+ if self._proc is None:
75
+ return
76
+ try:
77
+ self._send("quit")
78
+ self._proc.wait(timeout=5)
79
+ except Exception:
80
+ self._proc.kill()
81
+ finally:
82
+ self._proc = None
83
+
84
+ def __enter__(self) -> "Reckless":
85
+ return self
86
+
87
+ def __exit__(self, *exc) -> None:
88
+ self.quit()
89
+
90
+ # -- low-level UCI I/O -------------------------------------------------
91
+
92
+ def _send(self, command: str) -> None:
93
+ if self._proc is None or self._proc.stdin is None:
94
+ raise RecklessError("Engine is not running.")
95
+ self._proc.stdin.write(command + "\n")
96
+ self._proc.stdin.flush()
97
+
98
+ def _read_until(self, token: str) -> str:
99
+ """Read lines until one starts with `token`; return that line."""
100
+ if self._proc is None or self._proc.stdout is None:
101
+ raise RecklessError("Engine is not running.")
102
+ while True:
103
+ line = self._proc.stdout.readline()
104
+ if not line:
105
+ raise RecklessError("Engine closed the connection unexpectedly.")
106
+ if line.strip().startswith(token):
107
+ return line.strip()
108
+
109
+ def _handshake(self) -> None:
110
+ self._send("uci")
111
+ self._read_until("uciok")
112
+ self.is_ready()
113
+
114
+ # -- public commands ---------------------------------------------------
115
+
116
+ def is_ready(self) -> bool:
117
+ """Block until the engine reports it is ready."""
118
+ self._send("isready")
119
+ self._read_until("readyok")
120
+ return True
121
+
122
+ def new_game(self) -> None:
123
+ self._send("ucinewgame")
124
+ self.is_ready()
125
+
126
+ def set_position(
127
+ self,
128
+ fen: Optional[str] = None,
129
+ moves: Optional[Iterable[str]] = None,
130
+ ) -> None:
131
+ """Set the board. Defaults to the standard start position.
132
+
133
+ `moves` is a sequence of UCI moves (e.g. ["e2e4", "e7e5"]).
134
+ """
135
+ base = f"fen {fen}" if fen else "startpos"
136
+ move_str = ""
137
+ if moves:
138
+ move_str = " moves " + " ".join(moves)
139
+ self._send(f"position {base}{move_str}")
140
+
141
+ def best_move(
142
+ self,
143
+ *,
144
+ depth: Optional[int] = None,
145
+ movetime: Optional[int] = None,
146
+ ) -> str:
147
+ """Search the current position and return the best move (UCI string).
148
+
149
+ Provide either `depth` (plies) or `movetime` (milliseconds).
150
+ Defaults to depth 10 if neither is given.
151
+ """
152
+ if depth is None and movetime is None:
153
+ depth = 10
154
+ go = "go"
155
+ if depth is not None:
156
+ go += f" depth {depth}"
157
+ if movetime is not None:
158
+ go += f" movetime {movetime}"
159
+ self._send(go)
160
+ line = self._read_until("bestmove")
161
+ # Format: "bestmove e2e4 ponder e7e5"
162
+ parts = line.split()
163
+ return parts[1] if len(parts) > 1 else ""
@@ -0,0 +1,36 @@
1
+ """Tests for python-reckless.
2
+
3
+ These tests need a real `reckless` binary to run the engine-dependent cases.
4
+ If one isn't available they skip rather than fail, so the suite is green in CI
5
+ even without the engine installed.
6
+ """
7
+
8
+ import shutil
9
+
10
+ import pytest
11
+
12
+ from reckless import Reckless, RecklessError
13
+
14
+ HAS_ENGINE = shutil.which("reckless") is not None
15
+ needs_engine = pytest.mark.skipif(not HAS_ENGINE, reason="reckless not on PATH")
16
+
17
+
18
+ def test_missing_engine_raises():
19
+ with pytest.raises(RecklessError):
20
+ Reckless(path="/definitely/not/a/real/engine", start=False)
21
+
22
+
23
+ @needs_engine
24
+ def test_handshake_and_ready():
25
+ with Reckless() as engine:
26
+ assert engine.is_ready() is True
27
+
28
+
29
+ @needs_engine
30
+ def test_best_move_from_start():
31
+ with Reckless() as engine:
32
+ engine.new_game()
33
+ engine.set_position() # start position
34
+ move = engine.best_move(depth=8)
35
+ # A legal opening move is 4 chars of UCI, e.g. "e2e4".
36
+ assert len(move) >= 4