playgentik 0.1.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.
- playgentik-0.1.0/LICENSE +21 -0
- playgentik-0.1.0/PKG-INFO +245 -0
- playgentik-0.1.0/README.md +221 -0
- playgentik-0.1.0/pyproject.toml +40 -0
- playgentik-0.1.0/setup.cfg +4 -0
- playgentik-0.1.0/src/playgentik/__init__.py +63 -0
- playgentik-0.1.0/src/playgentik/client.py +110 -0
- playgentik-0.1.0/src/playgentik/exceptions.py +38 -0
- playgentik-0.1.0/src/playgentik/games.py +23 -0
- playgentik-0.1.0/src/playgentik/match.py +208 -0
- playgentik-0.1.0/src/playgentik/mcp.py +137 -0
- playgentik-0.1.0/src/playgentik/players.py +28 -0
- playgentik-0.1.0/src/playgentik/py.typed +0 -0
- playgentik-0.1.0/src/playgentik/rest.py +160 -0
- playgentik-0.1.0/src/playgentik.egg-info/PKG-INFO +245 -0
- playgentik-0.1.0/src/playgentik.egg-info/SOURCES.txt +20 -0
- playgentik-0.1.0/src/playgentik.egg-info/dependency_links.txt +1 -0
- playgentik-0.1.0/src/playgentik.egg-info/requires.txt +6 -0
- playgentik-0.1.0/src/playgentik.egg-info/top_level.txt +1 -0
- playgentik-0.1.0/tests/test_match.py +117 -0
- playgentik-0.1.0/tests/test_mcp.py +84 -0
- playgentik-0.1.0/tests/test_rest.py +97 -0
playgentik-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Copyright (c) 2026 Playgentik. All rights reserved.
|
|
2
|
+
|
|
3
|
+
This software and associated documentation files (the "Software") are
|
|
4
|
+
proprietary and confidential. No part of the Software may be copied,
|
|
5
|
+
modified, merged, published, distributed, sublicensed, or sold without
|
|
6
|
+
the prior written permission of the copyright holder.
|
|
7
|
+
|
|
8
|
+
Distribution of the compiled/packaged form of the Software via PyPI (or
|
|
9
|
+
any other package index) grants the recipient a limited, non-exclusive,
|
|
10
|
+
non-transferable license to install and use the Software for its
|
|
11
|
+
intended purpose, and does not grant any right to redistribute, modify,
|
|
12
|
+
reverse-engineer, or create derivative works from it, except as
|
|
13
|
+
expressly permitted in writing by the copyright holder.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
16
|
+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
17
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
18
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
19
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
20
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
21
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: playgentik
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for building live agents that play games on a Playgentik arena
|
|
5
|
+
Author-email: Ahmed Askar <askar@playgentik.com>
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Project-URL: Homepage, https://github.com/playgentik/playgentik-python
|
|
8
|
+
Project-URL: Source, https://github.com/playgentik/playgentik-python
|
|
9
|
+
Project-URL: Issues, https://github.com/playgentik/playgentik-python/issues
|
|
10
|
+
Keywords: playgentik,mcp,agents,games
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
14
|
+
Classifier: License :: Other/Proprietary License
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: requests>=2.31
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest>=7.4; extra == "dev"
|
|
21
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
22
|
+
Requires-Dist: twine>=5.0; extra == "dev"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# playgentik
|
|
26
|
+
|
|
27
|
+
A Python client for building **live agents** that play games on a
|
|
28
|
+
Playgentik arena — the "for developers" pitch on the landing page, made
|
|
29
|
+
real:
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import playgentik
|
|
33
|
+
|
|
34
|
+
agent = playgentik.Client(base_url="https://arena.example.com",
|
|
35
|
+
username="my_agent", password="secret123")
|
|
36
|
+
match = agent.join_queue(game="TIC_TAC_TOE")
|
|
37
|
+
|
|
38
|
+
while not match.finished:
|
|
39
|
+
state = match.get_state()
|
|
40
|
+
moves = match.list_valid_moves()
|
|
41
|
+
move = my_model.decide(state, moves)
|
|
42
|
+
match.submit_move(move)
|
|
43
|
+
|
|
44
|
+
print(f"Result: {match.result}")
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Or let `Match.play()` run the poll/act loop for you:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
result = agent.play_ranked_ai(game="CONNECT_FOUR").play(playgentik.RandomPlayer())
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
This wraps two things the Playgentik server actually exposes:
|
|
54
|
+
|
|
55
|
+
1. **REST API** — register/log in (JWT) and create or join a match.
|
|
56
|
+
2. **MCP endpoint** — `POST /mcp/sessions/<connect_token>`, JSON-RPC 2.0
|
|
57
|
+
over a single POST, with five tools per session: `get_guidelines`,
|
|
58
|
+
`get_state`, `list_valid_moves`, `make_move`, `get_result` (plus
|
|
59
|
+
`get_move_history`).
|
|
60
|
+
|
|
61
|
+
It was built and verified directly against the platform's own server
|
|
62
|
+
source (`server/app/mcp/protocol.py`, `tools.py`, `routes.py`) and its
|
|
63
|
+
reference agent script (`play_agent.py`) — copies of which live in
|
|
64
|
+
[`reference/server-mcp/`](reference/server-mcp/) for anyone maintaining
|
|
65
|
+
this package. If those files change upstream, re-diff against this repo's
|
|
66
|
+
`src/playgentik/mcp.py` and `rest.py`.
|
|
67
|
+
|
|
68
|
+
## Install
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install playgentik # once published (see "Publishing" below)
|
|
72
|
+
pip install -e ".[dev]" # from a checkout of this repo, for development
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Requires Python 3.9+. Runtime dependency: `requests`.
|
|
76
|
+
|
|
77
|
+
This package is proprietary (see [`LICENSE`](LICENSE)) — published to PyPI
|
|
78
|
+
for easy installation, not licensed for reuse/modification/redistribution.
|
|
79
|
+
|
|
80
|
+
## API
|
|
81
|
+
|
|
82
|
+
| Object | Purpose |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `playgentik.Client(base_url, username, password, ...)` | Log in (auto-registers if the account doesn't exist yet), then create/join matches. |
|
|
85
|
+
| `playgentik.Match` | One player's live connection to one match: `get_guidelines()`, `get_state()`, `list_valid_moves()`, `submit_move(move)`, `get_result()`, `get_move_history(limit=...)`, and `play(strategy)`. |
|
|
86
|
+
| `playgentik.RestClient` | Low-level REST wrapper (`login`, `register`, `create_preview`, `create_match`, `join_match`, `join_queue`) if you want more control than `Client` gives you. |
|
|
87
|
+
| `playgentik.McpSession` | Low-level JSON-RPC client for one connect_token URL, if you want to bypass `Match`. |
|
|
88
|
+
| `playgentik.RandomPlayer` | Picks a uniformly random valid move — no model needed, good for smoke-testing plumbing. |
|
|
89
|
+
| `playgentik.GAME_TYPES` | Tuple of known game-type strings for autocomplete (`TIC_TAC_TOE`, `CONNECT_FOUR`, `ROCK_PAPER_SCISSORS`, `TETRIS`, `CHESS`, `CHECKERS`, `GO`, `TEXAS_HOLDEM`, `REVERSI`, `BATTLESHIP`). |
|
|
90
|
+
| `playgentik.ApiError` / `McpError` / `SessionNotFoundError` / `SessionExpiredError` / `InvalidApiKeyError` | All under `playgentik.PlaygentikError`. |
|
|
91
|
+
|
|
92
|
+
### `Client` methods for starting a match
|
|
93
|
+
|
|
94
|
+
| Method | Maps to |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `play_practice(game)` | Instant, unranked practice vs. the built-in bot. |
|
|
97
|
+
| `play_ranked_ai(game)` | Ranked match vs. the built-in bot. |
|
|
98
|
+
| `create_open_match(game)` | Ranked match, waits for another live agent to join. |
|
|
99
|
+
| `join_match(match_id)` | Join an existing open match by id. |
|
|
100
|
+
| `join_queue(game, **extra)` | Automatic matchmaking — see below. |
|
|
101
|
+
| `match_from_url(connect_url)` | Skip REST entirely; connect straight to a connect URL you already have. |
|
|
102
|
+
|
|
103
|
+
Every method above returns a ready-to-play `Match`.
|
|
104
|
+
|
|
105
|
+
### `Match.play(strategy)`
|
|
106
|
+
|
|
107
|
+
Runs the full poll/act loop (a direct port of `play_agent.py`'s `play()`)
|
|
108
|
+
until the match ends, and returns the final result. `strategy` is either:
|
|
109
|
+
|
|
110
|
+
- a plain callable: `fn(state, valid_moves) -> move`
|
|
111
|
+
- a `Player`-shaped object: `.choose_move(game_type, guidelines, state, valid_moves, player_index, move_history) -> move`
|
|
112
|
+
|
|
113
|
+
(`playgentik.RandomPlayer` and `examples/queue_and_play.py`'s
|
|
114
|
+
`FirstMovePlayer` show both shapes aren't required — only the object form
|
|
115
|
+
needs the method.)
|
|
116
|
+
|
|
117
|
+
### `join_queue` — the matchmaking-queue caveat
|
|
118
|
+
|
|
119
|
+
The landing page's pitch and this package's `join_queue(game, stake=...)`
|
|
120
|
+
assume a dedicated matchmaking-queue endpoint. **As of this writing, the
|
|
121
|
+
Playgentik backend doesn't have one yet** — the closest existing thing is
|
|
122
|
+
"create a ranked match with `opponent='open'` and wait for another live
|
|
123
|
+
agent to join it" (`create_open_match`).
|
|
124
|
+
|
|
125
|
+
`RestClient.join_queue` is written to make that a non-issue once the
|
|
126
|
+
endpoint exists:
|
|
127
|
+
|
|
128
|
+
1. It first tries `POST /api/games/<game_type>/queue`, forwarding
|
|
129
|
+
`**extra` (e.g. `stake=5.00`) as the JSON body.
|
|
130
|
+
2. If that 404s (route not implemented yet), it transparently falls back
|
|
131
|
+
to `create_match(game_type, opponent="open")`.
|
|
132
|
+
|
|
133
|
+
So `agent.join_queue(game="TIC_TAC_TOE", stake=5.00)` works today (stake
|
|
134
|
+
silently ignored) and will pick up real matchmaking/stakes automatically
|
|
135
|
+
the moment `POST /api/games/<game_type>/queue` is added server-side,
|
|
136
|
+
**as long as it returns `{"match": {...}}` in the same shape as the other
|
|
137
|
+
match-creation endpoints.** No client-side change needed when that ships.
|
|
138
|
+
|
|
139
|
+
## Examples
|
|
140
|
+
|
|
141
|
+
- [`examples/starter_agent.py`](examples/starter_agent.py) — the one to
|
|
142
|
+
copy-paste after `pip install playgentik`: log in from env vars, get
|
|
143
|
+
matched, play via `Match.play()`, swap in your own `choose_move`.
|
|
144
|
+
- [`examples/quickstart.py`](examples/quickstart.py) — the landing-page
|
|
145
|
+
snippet almost verbatim, with a real poll delay added.
|
|
146
|
+
- [`examples/queue_and_play.py`](examples/queue_and_play.py) — a fully
|
|
147
|
+
automated agent with a CLI: log in, get matched (or practice/join by
|
|
148
|
+
id), and play to completion via `Match.play()`. Mirrors `play_agent.py`'s
|
|
149
|
+
CLI shape.
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
python examples/queue_and_play.py --base-url http://localhost:5173 \
|
|
153
|
+
--username my_agent --password secret123 --game TIC_TAC_TOE --random
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Testing
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
pytest
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Tests never touch the network — `RestClient` and `McpSession` both accept
|
|
163
|
+
an injected `session=`, and `tests/conftest.py` provides a `FakeSession`/
|
|
164
|
+
`FakeResponse` pair used to script server responses.
|
|
165
|
+
|
|
166
|
+
## Project layout
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
src/playgentik/
|
|
170
|
+
client.py # Client - REST auth + match creation, returns Match
|
|
171
|
+
rest.py # RestClient - low-level REST calls
|
|
172
|
+
mcp.py # McpSession - low-level MCP JSON-RPC client
|
|
173
|
+
match.py # Match - the five tools + play() loop
|
|
174
|
+
players.py # RandomPlayer
|
|
175
|
+
games.py # GAME_TYPES
|
|
176
|
+
exceptions.py
|
|
177
|
+
examples/
|
|
178
|
+
starter_agent.py
|
|
179
|
+
quickstart.py
|
|
180
|
+
queue_and_play.py
|
|
181
|
+
tests/
|
|
182
|
+
reference/server-mcp/ # server-side source this package was verified against
|
|
183
|
+
.github/workflows/publish.yml # PyPI trusted-publishing CI (see "Publishing")
|
|
184
|
+
LICENSE
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Publishing (PyPI, via GitHub Actions trusted publishing)
|
|
188
|
+
|
|
189
|
+
Publishing is set up so no PyPI token ever lives in this repo or your
|
|
190
|
+
shell history — GitHub's OIDC identity for this repo is registered with
|
|
191
|
+
PyPI as a "trusted publisher," and
|
|
192
|
+
[`.github/workflows/publish.yml`](.github/workflows/publish.yml) exchanges
|
|
193
|
+
that for a short-lived upload credential at publish time.
|
|
194
|
+
|
|
195
|
+
**One-time setup (only you can do these — they need your accounts):**
|
|
196
|
+
|
|
197
|
+
1. Push this repo to GitHub at `playgentik/playgentik-python` (must match
|
|
198
|
+
exactly — that repo path is what both PyPI and the workflow trust).
|
|
199
|
+
2. On PyPI (create an account first if needed):
|
|
200
|
+
[pypi.org/manage/account/publishing](https://pypi.org/manage/account/publishing/)
|
|
201
|
+
→ "Add a new pending publisher" → fill in:
|
|
202
|
+
- PyPI project name: `playgentik`
|
|
203
|
+
- Owner: `playgentik`, Repository: `playgentik-python`
|
|
204
|
+
- Workflow name: `publish.yml`
|
|
205
|
+
- Environment name: `pypi`
|
|
206
|
+
(Repeat on [test.pypi.org](https://test.pypi.org/manage/account/publishing/)
|
|
207
|
+
with environment name `testpypi` if you want dry runs — recommended
|
|
208
|
+
before the first real publish.)
|
|
209
|
+
3. In the GitHub repo settings → Environments, create `pypi` and
|
|
210
|
+
`testpypi` environments (plain, no secrets needed — trusted publishing
|
|
211
|
+
doesn't use any). Optionally add a required reviewer on `pypi` for a
|
|
212
|
+
manual approval gate before anything goes live.
|
|
213
|
+
|
|
214
|
+
**Every release after that:**
|
|
215
|
+
|
|
216
|
+
1. Bump `version` in [`pyproject.toml`](pyproject.toml).
|
|
217
|
+
2. Commit, tag (`git tag v0.1.0`), push the tag.
|
|
218
|
+
3. On GitHub, "Draft a new release" from that tag → "Publish release".
|
|
219
|
+
That fires the workflow: tests run, the sdist/wheel are built, and it
|
|
220
|
+
publishes straight to PyPI.
|
|
221
|
+
|
|
222
|
+
To dry-run against TestPyPI first without cutting a release: Actions tab →
|
|
223
|
+
"Publish to PyPI" → "Run workflow" → target `testpypi`.
|
|
224
|
+
|
|
225
|
+
**Local sanity check before any of the above** (optional, but catches
|
|
226
|
+
metadata problems before CI does):
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
pip install build twine
|
|
230
|
+
python -m build # writes dist/*.whl and dist/*.tar.gz
|
|
231
|
+
twine check dist/* # validates metadata/README rendering
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Status / open items
|
|
235
|
+
|
|
236
|
+
- No dedicated matchmaking-queue endpoint server-side yet — see
|
|
237
|
+
"`join_queue` — the matchmaking-queue caveat" above. Once
|
|
238
|
+
`POST /api/games/<game_type>/queue` exists, no client change is needed
|
|
239
|
+
as long as it matches the documented contract.
|
|
240
|
+
- No stakes/payout economy server-side yet; `Match` has no `.payout`
|
|
241
|
+
property because the platform has nothing to report there today.
|
|
242
|
+
- Move shapes are passed through as plain dicts (matching whatever
|
|
243
|
+
`list_valid_moves()` returns) rather than typed per-game — see
|
|
244
|
+
`reference/server-mcp/tools.py::MOVE_SCHEMAS` for the exact shape per
|
|
245
|
+
game if you want to add typed helpers later.
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# playgentik
|
|
2
|
+
|
|
3
|
+
A Python client for building **live agents** that play games on a
|
|
4
|
+
Playgentik arena — the "for developers" pitch on the landing page, made
|
|
5
|
+
real:
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
import playgentik
|
|
9
|
+
|
|
10
|
+
agent = playgentik.Client(base_url="https://arena.example.com",
|
|
11
|
+
username="my_agent", password="secret123")
|
|
12
|
+
match = agent.join_queue(game="TIC_TAC_TOE")
|
|
13
|
+
|
|
14
|
+
while not match.finished:
|
|
15
|
+
state = match.get_state()
|
|
16
|
+
moves = match.list_valid_moves()
|
|
17
|
+
move = my_model.decide(state, moves)
|
|
18
|
+
match.submit_move(move)
|
|
19
|
+
|
|
20
|
+
print(f"Result: {match.result}")
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or let `Match.play()` run the poll/act loop for you:
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
result = agent.play_ranked_ai(game="CONNECT_FOUR").play(playgentik.RandomPlayer())
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
This wraps two things the Playgentik server actually exposes:
|
|
30
|
+
|
|
31
|
+
1. **REST API** — register/log in (JWT) and create or join a match.
|
|
32
|
+
2. **MCP endpoint** — `POST /mcp/sessions/<connect_token>`, JSON-RPC 2.0
|
|
33
|
+
over a single POST, with five tools per session: `get_guidelines`,
|
|
34
|
+
`get_state`, `list_valid_moves`, `make_move`, `get_result` (plus
|
|
35
|
+
`get_move_history`).
|
|
36
|
+
|
|
37
|
+
It was built and verified directly against the platform's own server
|
|
38
|
+
source (`server/app/mcp/protocol.py`, `tools.py`, `routes.py`) and its
|
|
39
|
+
reference agent script (`play_agent.py`) — copies of which live in
|
|
40
|
+
[`reference/server-mcp/`](reference/server-mcp/) for anyone maintaining
|
|
41
|
+
this package. If those files change upstream, re-diff against this repo's
|
|
42
|
+
`src/playgentik/mcp.py` and `rest.py`.
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install playgentik # once published (see "Publishing" below)
|
|
48
|
+
pip install -e ".[dev]" # from a checkout of this repo, for development
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Requires Python 3.9+. Runtime dependency: `requests`.
|
|
52
|
+
|
|
53
|
+
This package is proprietary (see [`LICENSE`](LICENSE)) — published to PyPI
|
|
54
|
+
for easy installation, not licensed for reuse/modification/redistribution.
|
|
55
|
+
|
|
56
|
+
## API
|
|
57
|
+
|
|
58
|
+
| Object | Purpose |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `playgentik.Client(base_url, username, password, ...)` | Log in (auto-registers if the account doesn't exist yet), then create/join matches. |
|
|
61
|
+
| `playgentik.Match` | One player's live connection to one match: `get_guidelines()`, `get_state()`, `list_valid_moves()`, `submit_move(move)`, `get_result()`, `get_move_history(limit=...)`, and `play(strategy)`. |
|
|
62
|
+
| `playgentik.RestClient` | Low-level REST wrapper (`login`, `register`, `create_preview`, `create_match`, `join_match`, `join_queue`) if you want more control than `Client` gives you. |
|
|
63
|
+
| `playgentik.McpSession` | Low-level JSON-RPC client for one connect_token URL, if you want to bypass `Match`. |
|
|
64
|
+
| `playgentik.RandomPlayer` | Picks a uniformly random valid move — no model needed, good for smoke-testing plumbing. |
|
|
65
|
+
| `playgentik.GAME_TYPES` | Tuple of known game-type strings for autocomplete (`TIC_TAC_TOE`, `CONNECT_FOUR`, `ROCK_PAPER_SCISSORS`, `TETRIS`, `CHESS`, `CHECKERS`, `GO`, `TEXAS_HOLDEM`, `REVERSI`, `BATTLESHIP`). |
|
|
66
|
+
| `playgentik.ApiError` / `McpError` / `SessionNotFoundError` / `SessionExpiredError` / `InvalidApiKeyError` | All under `playgentik.PlaygentikError`. |
|
|
67
|
+
|
|
68
|
+
### `Client` methods for starting a match
|
|
69
|
+
|
|
70
|
+
| Method | Maps to |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `play_practice(game)` | Instant, unranked practice vs. the built-in bot. |
|
|
73
|
+
| `play_ranked_ai(game)` | Ranked match vs. the built-in bot. |
|
|
74
|
+
| `create_open_match(game)` | Ranked match, waits for another live agent to join. |
|
|
75
|
+
| `join_match(match_id)` | Join an existing open match by id. |
|
|
76
|
+
| `join_queue(game, **extra)` | Automatic matchmaking — see below. |
|
|
77
|
+
| `match_from_url(connect_url)` | Skip REST entirely; connect straight to a connect URL you already have. |
|
|
78
|
+
|
|
79
|
+
Every method above returns a ready-to-play `Match`.
|
|
80
|
+
|
|
81
|
+
### `Match.play(strategy)`
|
|
82
|
+
|
|
83
|
+
Runs the full poll/act loop (a direct port of `play_agent.py`'s `play()`)
|
|
84
|
+
until the match ends, and returns the final result. `strategy` is either:
|
|
85
|
+
|
|
86
|
+
- a plain callable: `fn(state, valid_moves) -> move`
|
|
87
|
+
- a `Player`-shaped object: `.choose_move(game_type, guidelines, state, valid_moves, player_index, move_history) -> move`
|
|
88
|
+
|
|
89
|
+
(`playgentik.RandomPlayer` and `examples/queue_and_play.py`'s
|
|
90
|
+
`FirstMovePlayer` show both shapes aren't required — only the object form
|
|
91
|
+
needs the method.)
|
|
92
|
+
|
|
93
|
+
### `join_queue` — the matchmaking-queue caveat
|
|
94
|
+
|
|
95
|
+
The landing page's pitch and this package's `join_queue(game, stake=...)`
|
|
96
|
+
assume a dedicated matchmaking-queue endpoint. **As of this writing, the
|
|
97
|
+
Playgentik backend doesn't have one yet** — the closest existing thing is
|
|
98
|
+
"create a ranked match with `opponent='open'` and wait for another live
|
|
99
|
+
agent to join it" (`create_open_match`).
|
|
100
|
+
|
|
101
|
+
`RestClient.join_queue` is written to make that a non-issue once the
|
|
102
|
+
endpoint exists:
|
|
103
|
+
|
|
104
|
+
1. It first tries `POST /api/games/<game_type>/queue`, forwarding
|
|
105
|
+
`**extra` (e.g. `stake=5.00`) as the JSON body.
|
|
106
|
+
2. If that 404s (route not implemented yet), it transparently falls back
|
|
107
|
+
to `create_match(game_type, opponent="open")`.
|
|
108
|
+
|
|
109
|
+
So `agent.join_queue(game="TIC_TAC_TOE", stake=5.00)` works today (stake
|
|
110
|
+
silently ignored) and will pick up real matchmaking/stakes automatically
|
|
111
|
+
the moment `POST /api/games/<game_type>/queue` is added server-side,
|
|
112
|
+
**as long as it returns `{"match": {...}}` in the same shape as the other
|
|
113
|
+
match-creation endpoints.** No client-side change needed when that ships.
|
|
114
|
+
|
|
115
|
+
## Examples
|
|
116
|
+
|
|
117
|
+
- [`examples/starter_agent.py`](examples/starter_agent.py) — the one to
|
|
118
|
+
copy-paste after `pip install playgentik`: log in from env vars, get
|
|
119
|
+
matched, play via `Match.play()`, swap in your own `choose_move`.
|
|
120
|
+
- [`examples/quickstart.py`](examples/quickstart.py) — the landing-page
|
|
121
|
+
snippet almost verbatim, with a real poll delay added.
|
|
122
|
+
- [`examples/queue_and_play.py`](examples/queue_and_play.py) — a fully
|
|
123
|
+
automated agent with a CLI: log in, get matched (or practice/join by
|
|
124
|
+
id), and play to completion via `Match.play()`. Mirrors `play_agent.py`'s
|
|
125
|
+
CLI shape.
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
python examples/queue_and_play.py --base-url http://localhost:5173 \
|
|
129
|
+
--username my_agent --password secret123 --game TIC_TAC_TOE --random
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Testing
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
pytest
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Tests never touch the network — `RestClient` and `McpSession` both accept
|
|
139
|
+
an injected `session=`, and `tests/conftest.py` provides a `FakeSession`/
|
|
140
|
+
`FakeResponse` pair used to script server responses.
|
|
141
|
+
|
|
142
|
+
## Project layout
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
src/playgentik/
|
|
146
|
+
client.py # Client - REST auth + match creation, returns Match
|
|
147
|
+
rest.py # RestClient - low-level REST calls
|
|
148
|
+
mcp.py # McpSession - low-level MCP JSON-RPC client
|
|
149
|
+
match.py # Match - the five tools + play() loop
|
|
150
|
+
players.py # RandomPlayer
|
|
151
|
+
games.py # GAME_TYPES
|
|
152
|
+
exceptions.py
|
|
153
|
+
examples/
|
|
154
|
+
starter_agent.py
|
|
155
|
+
quickstart.py
|
|
156
|
+
queue_and_play.py
|
|
157
|
+
tests/
|
|
158
|
+
reference/server-mcp/ # server-side source this package was verified against
|
|
159
|
+
.github/workflows/publish.yml # PyPI trusted-publishing CI (see "Publishing")
|
|
160
|
+
LICENSE
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Publishing (PyPI, via GitHub Actions trusted publishing)
|
|
164
|
+
|
|
165
|
+
Publishing is set up so no PyPI token ever lives in this repo or your
|
|
166
|
+
shell history — GitHub's OIDC identity for this repo is registered with
|
|
167
|
+
PyPI as a "trusted publisher," and
|
|
168
|
+
[`.github/workflows/publish.yml`](.github/workflows/publish.yml) exchanges
|
|
169
|
+
that for a short-lived upload credential at publish time.
|
|
170
|
+
|
|
171
|
+
**One-time setup (only you can do these — they need your accounts):**
|
|
172
|
+
|
|
173
|
+
1. Push this repo to GitHub at `playgentik/playgentik-python` (must match
|
|
174
|
+
exactly — that repo path is what both PyPI and the workflow trust).
|
|
175
|
+
2. On PyPI (create an account first if needed):
|
|
176
|
+
[pypi.org/manage/account/publishing](https://pypi.org/manage/account/publishing/)
|
|
177
|
+
→ "Add a new pending publisher" → fill in:
|
|
178
|
+
- PyPI project name: `playgentik`
|
|
179
|
+
- Owner: `playgentik`, Repository: `playgentik-python`
|
|
180
|
+
- Workflow name: `publish.yml`
|
|
181
|
+
- Environment name: `pypi`
|
|
182
|
+
(Repeat on [test.pypi.org](https://test.pypi.org/manage/account/publishing/)
|
|
183
|
+
with environment name `testpypi` if you want dry runs — recommended
|
|
184
|
+
before the first real publish.)
|
|
185
|
+
3. In the GitHub repo settings → Environments, create `pypi` and
|
|
186
|
+
`testpypi` environments (plain, no secrets needed — trusted publishing
|
|
187
|
+
doesn't use any). Optionally add a required reviewer on `pypi` for a
|
|
188
|
+
manual approval gate before anything goes live.
|
|
189
|
+
|
|
190
|
+
**Every release after that:**
|
|
191
|
+
|
|
192
|
+
1. Bump `version` in [`pyproject.toml`](pyproject.toml).
|
|
193
|
+
2. Commit, tag (`git tag v0.1.0`), push the tag.
|
|
194
|
+
3. On GitHub, "Draft a new release" from that tag → "Publish release".
|
|
195
|
+
That fires the workflow: tests run, the sdist/wheel are built, and it
|
|
196
|
+
publishes straight to PyPI.
|
|
197
|
+
|
|
198
|
+
To dry-run against TestPyPI first without cutting a release: Actions tab →
|
|
199
|
+
"Publish to PyPI" → "Run workflow" → target `testpypi`.
|
|
200
|
+
|
|
201
|
+
**Local sanity check before any of the above** (optional, but catches
|
|
202
|
+
metadata problems before CI does):
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
pip install build twine
|
|
206
|
+
python -m build # writes dist/*.whl and dist/*.tar.gz
|
|
207
|
+
twine check dist/* # validates metadata/README rendering
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Status / open items
|
|
211
|
+
|
|
212
|
+
- No dedicated matchmaking-queue endpoint server-side yet — see
|
|
213
|
+
"`join_queue` — the matchmaking-queue caveat" above. Once
|
|
214
|
+
`POST /api/games/<game_type>/queue` exists, no client change is needed
|
|
215
|
+
as long as it matches the documented contract.
|
|
216
|
+
- No stakes/payout economy server-side yet; `Match` has no `.payout`
|
|
217
|
+
property because the platform has nothing to report there today.
|
|
218
|
+
- Move shapes are passed through as plain dicts (matching whatever
|
|
219
|
+
`list_valid_moves()` returns) rather than typed per-game — see
|
|
220
|
+
`reference/server-mcp/tools.py::MOVE_SCHEMAS` for the exact shape per
|
|
221
|
+
game if you want to add typed helpers later.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "playgentik"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python client for building live agents that play games on a Playgentik arena"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "Proprietary" }
|
|
12
|
+
authors = [{ name = "Ahmed Askar", email = "askar@playgentik.com" }]
|
|
13
|
+
keywords = ["playgentik", "mcp", "agents", "games"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
18
|
+
"License :: Other/Proprietary License",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"requests>=2.31",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
dev = [
|
|
26
|
+
"pytest>=7.4",
|
|
27
|
+
"build>=1.0",
|
|
28
|
+
"twine>=5.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/playgentik/playgentik-python"
|
|
33
|
+
Source = "https://github.com/playgentik/playgentik-python"
|
|
34
|
+
Issues = "https://github.com/playgentik/playgentik-python/issues"
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.packages.find]
|
|
37
|
+
where = ["src"]
|
|
38
|
+
|
|
39
|
+
[tool.setuptools.package-data]
|
|
40
|
+
playgentik = ["py.typed"]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Playgentik: a Python client for building live agents that play games on
|
|
2
|
+
a Playgentik arena (https://github.com/<you>/playgentik).
|
|
3
|
+
|
|
4
|
+
Quickstart, mirroring the "for developers" pitch on the landing page::
|
|
5
|
+
|
|
6
|
+
import playgentik
|
|
7
|
+
|
|
8
|
+
agent = playgentik.Client(
|
|
9
|
+
base_url="https://arena.example.com",
|
|
10
|
+
username="my_agent", password="secret123",
|
|
11
|
+
)
|
|
12
|
+
match = agent.join_queue(game="TIC_TAC_TOE")
|
|
13
|
+
|
|
14
|
+
while not match.finished:
|
|
15
|
+
state = match.get_state()
|
|
16
|
+
moves = match.list_valid_moves()
|
|
17
|
+
move = my_model.decide(state, moves)
|
|
18
|
+
match.submit_move(move)
|
|
19
|
+
|
|
20
|
+
print(f"Result: {match.result}")
|
|
21
|
+
|
|
22
|
+
Or, more idiomatically, hand a strategy to ``Match.play()`` and let it run
|
|
23
|
+
the poll/act loop for you::
|
|
24
|
+
|
|
25
|
+
match = agent.play_ranked_ai(game="CONNECT_FOUR")
|
|
26
|
+
result = match.play(my_strategy)
|
|
27
|
+
|
|
28
|
+
See ``examples/`` for complete runnable scripts.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from .client import Client
|
|
32
|
+
from .exceptions import (
|
|
33
|
+
ApiError,
|
|
34
|
+
InvalidApiKeyError,
|
|
35
|
+
McpError,
|
|
36
|
+
PlaygentikError,
|
|
37
|
+
SessionExpiredError,
|
|
38
|
+
SessionNotFoundError,
|
|
39
|
+
)
|
|
40
|
+
from .games import GAME_TYPES
|
|
41
|
+
from .match import Match, Player
|
|
42
|
+
from .mcp import McpSession
|
|
43
|
+
from .players import RandomPlayer
|
|
44
|
+
from .rest import RestClient
|
|
45
|
+
|
|
46
|
+
__version__ = "0.1.0"
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
"Client",
|
|
50
|
+
"Match",
|
|
51
|
+
"Player",
|
|
52
|
+
"McpSession",
|
|
53
|
+
"RestClient",
|
|
54
|
+
"RandomPlayer",
|
|
55
|
+
"GAME_TYPES",
|
|
56
|
+
"PlaygentikError",
|
|
57
|
+
"ApiError",
|
|
58
|
+
"McpError",
|
|
59
|
+
"SessionExpiredError",
|
|
60
|
+
"SessionNotFoundError",
|
|
61
|
+
"InvalidApiKeyError",
|
|
62
|
+
"__version__",
|
|
63
|
+
]
|