pokerbots-dev 0.1.2__tar.gz → 0.1.4__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.
- {pokerbots_dev-0.1.2 → pokerbots_dev-0.1.4}/PKG-INFO +1 -1
- {pokerbots_dev-0.1.2 → pokerbots_dev-0.1.4}/pyproject.toml +1 -1
- {pokerbots_dev-0.1.2 → pokerbots_dev-0.1.4}/src/pokerbots_cli/agent.py +12 -7
- {pokerbots_dev-0.1.2 → pokerbots_dev-0.1.4}/src/pokerbots_cli/api.py +1 -1
- {pokerbots_dev-0.1.2 → pokerbots_dev-0.1.4}/src/pokerbots_cli/commands/account.py +16 -7
- {pokerbots_dev-0.1.2 → pokerbots_dev-0.1.4}/src/pokerbots_cli/commands/local.py +2 -2
- {pokerbots_dev-0.1.2 → pokerbots_dev-0.1.4}/src/pokerbots_cli/commands/money.py +15 -6
- {pokerbots_dev-0.1.2 → pokerbots_dev-0.1.4}/src/pokerbots_cli/docs.md +7 -5
- pokerbots_dev-0.1.4/src/pokerbots_cli/fmt.py +55 -0
- {pokerbots_dev-0.1.2 → pokerbots_dev-0.1.4}/tests/test_agent.py +11 -1
- {pokerbots_dev-0.1.2 → pokerbots_dev-0.1.4}/tests/test_cli.py +3 -1
- pokerbots_dev-0.1.2/src/pokerbots_cli/fmt.py +0 -23
- {pokerbots_dev-0.1.2 → pokerbots_dev-0.1.4}/.gitignore +0 -0
- {pokerbots_dev-0.1.2 → pokerbots_dev-0.1.4}/README.md +0 -0
- {pokerbots_dev-0.1.2 → pokerbots_dev-0.1.4}/src/pokerbots_cli/__init__.py +0 -0
- {pokerbots_dev-0.1.2 → pokerbots_dev-0.1.4}/src/pokerbots_cli/auth.py +0 -0
- {pokerbots_dev-0.1.2 → pokerbots_dev-0.1.4}/src/pokerbots_cli/cards.py +0 -0
- {pokerbots_dev-0.1.2 → pokerbots_dev-0.1.4}/src/pokerbots_cli/commands/__init__.py +0 -0
- {pokerbots_dev-0.1.2 → pokerbots_dev-0.1.4}/src/pokerbots_cli/commands/admin.py +0 -0
- {pokerbots_dev-0.1.2 → pokerbots_dev-0.1.4}/src/pokerbots_cli/engine.py +0 -0
- {pokerbots_dev-0.1.2 → pokerbots_dev-0.1.4}/src/pokerbots_cli/house.py +0 -0
- {pokerbots_dev-0.1.2 → pokerbots_dev-0.1.4}/src/pokerbots_cli/main.py +0 -0
- {pokerbots_dev-0.1.2 → pokerbots_dev-0.1.4}/src/pokerbots_cli/template/bot.py +0 -0
- {pokerbots_dev-0.1.2 → pokerbots_dev-0.1.4}/src/pokerbots_cli/wallet.py +0 -0
- {pokerbots_dev-0.1.2 → pokerbots_dev-0.1.4}/tests/test_cards.py +0 -0
- {pokerbots_dev-0.1.2 → pokerbots_dev-0.1.4}/tests/test_engine.py +0 -0
- {pokerbots_dev-0.1.2 → pokerbots_dev-0.1.4}/tests/test_wallet.py +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import traceback
|
|
2
|
-
from time import perf_counter
|
|
2
|
+
from time import perf_counter, thread_time
|
|
3
3
|
|
|
4
|
-
LIMIT = 0.020
|
|
4
|
+
LIMIT = 0.020 # CPU seconds per decision
|
|
5
|
+
WALL = 0.100 # backstop so a bot cannot stall the table
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
def fallback(state):
|
|
@@ -19,7 +20,10 @@ def normalize(action, state):
|
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
class Agent:
|
|
22
|
-
"""Runs an untrusted bot: enforces the
|
|
23
|
+
"""Runs an untrusted bot: enforces the CPU and wall limits and returns only legal actions.
|
|
24
|
+
|
|
25
|
+
CPU time comes from bot.cpu() when the bot runs elsewhere (a sandboxed process), else this thread.
|
|
26
|
+
"""
|
|
23
27
|
|
|
24
28
|
def __init__(self, bot, limit=LIMIT):
|
|
25
29
|
self.bot = bot
|
|
@@ -30,14 +34,15 @@ class Agent:
|
|
|
30
34
|
|
|
31
35
|
def __call__(self, state):
|
|
32
36
|
self.decisions += 1
|
|
33
|
-
|
|
37
|
+
clock = getattr(self.bot, "cpu", thread_time)
|
|
38
|
+
wall, cpu = perf_counter(), clock()
|
|
34
39
|
try:
|
|
35
40
|
action = self.bot.act(state)
|
|
36
41
|
except Exception:
|
|
37
42
|
return self.fail(state, traceback.format_exc())
|
|
38
|
-
|
|
39
|
-
self.slowest = max(self.slowest,
|
|
40
|
-
if
|
|
43
|
+
wall, cpu = perf_counter() - wall, clock() - cpu
|
|
44
|
+
self.slowest = max(self.slowest, cpu)
|
|
45
|
+
if cpu > self.limit or wall > WALL:
|
|
41
46
|
self.late += 1
|
|
42
47
|
return fallback(state)
|
|
43
48
|
return normalize(action, state) or self.fail(state, f"invalid action {action!r}, legal {state['legal']}")
|
|
@@ -35,7 +35,7 @@ def call(method, path, body=None, data=None, headers=None):
|
|
|
35
35
|
message = f"{e.code} {e.reason}"
|
|
36
36
|
raise ApiError(message) from None
|
|
37
37
|
except urllib.error.URLError as e:
|
|
38
|
-
raise ApiError(
|
|
38
|
+
raise ApiError(f"cannot reach {base()}: {e.reason}") from None
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
def admin(method, path, body=None):
|
|
@@ -6,7 +6,7 @@ from pathlib import Path
|
|
|
6
6
|
|
|
7
7
|
from .. import auth
|
|
8
8
|
from ..api import call
|
|
9
|
-
from ..fmt import money, place, table
|
|
9
|
+
from ..fmt import INK, PAPER, colored, money, place, signed, table, when
|
|
10
10
|
|
|
11
11
|
MAX_UPLOAD = 1 << 20
|
|
12
12
|
WATCH_SECONDS = 5
|
|
@@ -53,7 +53,7 @@ def deploy(args):
|
|
|
53
53
|
def stop(args):
|
|
54
54
|
mode = "live" if args.live else "paper"
|
|
55
55
|
call("DELETE", f"/deployments/{mode}")
|
|
56
|
-
print(f"stopped {mode}")
|
|
56
|
+
print(f"stopped {mode}; games already queued will still play")
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
def report():
|
|
@@ -63,12 +63,16 @@ def report():
|
|
|
63
63
|
d = me["deployments"].get(mode)
|
|
64
64
|
rows.append([mode, f"v{d['version']} rating {d['rating']:.0f} games {d['games']} today {d['today']}" if d else "-"])
|
|
65
65
|
rows += [["balance", money(me["balance"])], ["wallet", me["wallet"] or "-"]]
|
|
66
|
-
text = table(rows)
|
|
66
|
+
text = table(rows, labels=True)
|
|
67
67
|
games = call("GET", "/games")
|
|
68
68
|
if games:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
]
|
|
69
|
+
live = any(g["mode"] == "live" for g in games)
|
|
70
|
+
head = ["game", "mode", "place", "hands", "rating"] + (["won"] if live else [])
|
|
71
|
+
text += "\n\n" + table([head] + [
|
|
72
|
+
[when(g["created_at"]), g["mode"], place(g["place"]), g["hands"] or "-", signed(g.get("rating_delta"))]
|
|
73
|
+
+ ([signed(g.get("net"), "money")] if live else [])
|
|
74
|
+
for g in games
|
|
75
|
+
], header=True)
|
|
72
76
|
return text
|
|
73
77
|
|
|
74
78
|
|
|
@@ -77,13 +81,18 @@ def status(args):
|
|
|
77
81
|
|
|
78
82
|
|
|
79
83
|
def watch(args):
|
|
84
|
+
paint = f"\x1b[48;2;{INK}m\x1b[38;2;{PAPER}m" if colored() else ""
|
|
85
|
+
sys.stdout.write("\x1b[?1049h\x1b[?25l" + paint + "\x1b[2J")
|
|
80
86
|
try:
|
|
81
87
|
while True:
|
|
82
|
-
|
|
88
|
+
lines = ["", *(" " + line for line in report().split("\n")), "", " " + "ctrl-c to quit"]
|
|
89
|
+
sys.stdout.write("\x1b[H" + "".join(line + "\x1b[K\n" for line in lines) + "\x1b[J")
|
|
83
90
|
sys.stdout.flush()
|
|
84
91
|
time.sleep(WATCH_SECONDS)
|
|
85
92
|
except KeyboardInterrupt:
|
|
86
93
|
pass
|
|
94
|
+
finally:
|
|
95
|
+
sys.stdout.write("\x1b[0m\x1b[?25h\x1b[?1049l")
|
|
87
96
|
|
|
88
97
|
|
|
89
98
|
def register(sub):
|
|
@@ -58,9 +58,9 @@ def test(args):
|
|
|
58
58
|
["2nd", places.count(2)],
|
|
59
59
|
["avg place", f"{sum(places) / len(places):.1f}"],
|
|
60
60
|
["decisions", mine.decisions],
|
|
61
|
-
["
|
|
61
|
+
["late", mine.late],
|
|
62
62
|
["errors", mine.errors],
|
|
63
|
-
["slowest", f"{mine.slowest * 1000:.2f} ms"],
|
|
63
|
+
["slowest", f"{mine.slowest * 1000:.2f} ms cpu"],
|
|
64
64
|
]))
|
|
65
65
|
if mine.error:
|
|
66
66
|
print(f"\nfirst error\n{mine.error}")
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
1
3
|
from .. import wallet as keys
|
|
2
4
|
from ..api import call
|
|
3
5
|
from ..fmt import money, parse_money, table
|
|
@@ -6,25 +8,32 @@ from ..fmt import money, parse_money, table
|
|
|
6
8
|
def registered_key():
|
|
7
9
|
key = keys.load_or_create()
|
|
8
10
|
address = keys.address(key)
|
|
9
|
-
|
|
11
|
+
me = call("GET", "/me")
|
|
12
|
+
if me["wallet"] != address:
|
|
10
13
|
call("PUT", "/wallet", {"address": address})
|
|
11
|
-
return key, address
|
|
14
|
+
return key, address, me
|
|
12
15
|
|
|
13
16
|
|
|
14
17
|
def wallet(args):
|
|
15
|
-
_, address = registered_key()
|
|
16
|
-
|
|
18
|
+
_, address, me = registered_key()
|
|
19
|
+
rows = [["address", address], ["balance", money(me["balance"])]]
|
|
20
|
+
if me["withdraw_to"] and me["withdraw_to"] != address:
|
|
21
|
+
rows.append(["withdraws to", me["withdraw_to"]])
|
|
22
|
+
print(table(rows, labels=True))
|
|
17
23
|
|
|
18
24
|
|
|
19
25
|
def deposit(args):
|
|
20
|
-
key, _ = registered_key()
|
|
26
|
+
key, _, _ = registered_key()
|
|
21
27
|
prepared = call("POST", "/deposits", {"amount": parse_money(args.amount)})
|
|
22
28
|
reply = call("POST", f"/deposits/{prepared['signature']}", {"transaction": keys.sign(prepared["transaction"], key)})
|
|
23
29
|
print(f"deposit {reply['status']}: {money(reply['amount'])}")
|
|
24
30
|
|
|
25
31
|
|
|
26
32
|
def withdraw(args):
|
|
27
|
-
registered_key()
|
|
33
|
+
_, address, me = registered_key()
|
|
34
|
+
if me["withdraw_to"] and me["withdraw_to"] != address:
|
|
35
|
+
sys.exit(f"withdrawals go to {me['withdraw_to']}, the wallet you last deposited from.\n"
|
|
36
|
+
f"to use this wallet instead, deposit from it once: pb deposit 0.10")
|
|
28
37
|
reply = call("POST", "/withdrawals", {"amount": parse_money(args.amount)})
|
|
29
38
|
print(f"withdrawal {reply['status']}: {money(reply['amount'])}")
|
|
30
39
|
|
|
@@ -7,7 +7,7 @@ start
|
|
|
7
7
|
pb upload [dir] upload a new version: v1, v2, ...
|
|
8
8
|
pb deploy v1 --paper play free games with v1
|
|
9
9
|
pb deploy v1 --live play for money with v1
|
|
10
|
-
pb stop --paper|--live stop playing
|
|
10
|
+
pb stop --paper|--live stop playing; queued games still play
|
|
11
11
|
pb status versions, rating, balance, recent games
|
|
12
12
|
pb watch the same, refreshed every 5 seconds
|
|
13
13
|
pb docs [--plain] show this
|
|
@@ -19,7 +19,8 @@ money
|
|
|
19
19
|
|
|
20
20
|
Your wallet key is ~/.config/pokerbots/wallet.json and never leaves your
|
|
21
21
|
machine. Send USDC on Solana to its address, then deposit. We pay the
|
|
22
|
-
network fees.
|
|
22
|
+
network fees. Withdrawals go to the wallet you last deposited from.
|
|
23
|
+
Large withdrawals may wait for review.
|
|
23
24
|
|
|
24
25
|
bot.py
|
|
25
26
|
class Bot:
|
|
@@ -29,8 +30,9 @@ bot.py
|
|
|
29
30
|
return ("fold",)
|
|
30
31
|
|
|
31
32
|
A new Bot is created for every game. act is called for every decision.
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
Each decision gets 20 ms of CPU time, and must reply within 100 ms.
|
|
34
|
+
A late reply, an error or an illegal action counts as check, or fold
|
|
35
|
+
if check is not legal. Uploads are the bot's directory,
|
|
34
36
|
up to 1 MB zipped. Bots run with the Python standard library only, no
|
|
35
37
|
network and no file writes.
|
|
36
38
|
|
|
@@ -68,7 +70,7 @@ game
|
|
|
68
70
|
blinds on hand n: n / 2n
|
|
69
71
|
ends at last bot standing or 100 hands
|
|
70
72
|
at 100 hands, places go by chips
|
|
71
|
-
20 ms per decision, else check/fold
|
|
73
|
+
20 ms CPU per decision, else check/fold
|
|
72
74
|
up to 5 games a minute per bot
|
|
73
75
|
paper tables are filled with house bots; live tables are 6 players
|
|
74
76
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
|
|
5
|
+
MICRO = 1_000_000
|
|
6
|
+
INK, PAPER = "17;17;17", "244;243;239"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def colored():
|
|
10
|
+
return sys.stdout.isatty() and not os.environ.get("NO_COLOR")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def bold(text):
|
|
14
|
+
return f"\x1b[1m{text}\x1b[22m" if colored() else text
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def table(rows, labels=False, header=False):
|
|
18
|
+
"""Aligned columns; bold the first column (labels) or first row (header)."""
|
|
19
|
+
rows = [[str(c) for c in row] for row in rows]
|
|
20
|
+
widths = [max(len(r[i]) for r in rows if i < len(r)) for i in range(max(map(len, rows)))]
|
|
21
|
+
lines = []
|
|
22
|
+
for n, r in enumerate(rows):
|
|
23
|
+
cells = [c.ljust(w) for c, w in zip(r, widths)]
|
|
24
|
+
if header and n == 0:
|
|
25
|
+
cells = [bold(c) for c in cells]
|
|
26
|
+
elif labels:
|
|
27
|
+
cells[0] = bold(cells[0])
|
|
28
|
+
lines.append(" ".join(cells).rstrip())
|
|
29
|
+
return "\n".join(lines)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def money(micro):
|
|
33
|
+
cents = round(micro / 10_000)
|
|
34
|
+
return f"{cents}¢" if abs(cents) < 100 else f"${cents / 100:,.2f}"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def signed(value, unit=""):
|
|
38
|
+
if value is None:
|
|
39
|
+
return "-"
|
|
40
|
+
return ("+" if value > 0 else "") + (money(value) if unit == "money" else f"{value:.0f}")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def when(utc):
|
|
44
|
+
return datetime.fromisoformat(utc).replace(tzinfo=timezone.utc).astimezone().strftime("%m-%d %H:%M")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def parse_money(text):
|
|
48
|
+
value = round(float(text.lstrip("$")) * MICRO)
|
|
49
|
+
if value <= 0:
|
|
50
|
+
raise ValueError("amount must be positive")
|
|
51
|
+
return value
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def place(n):
|
|
55
|
+
return {1: "1st", 2: "2nd", 3: "3rd"}.get(n, f"{n}th") if n else "-"
|
|
@@ -13,6 +13,12 @@ class Fixed:
|
|
|
13
13
|
return self.reply(state)
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
def busy(seconds):
|
|
17
|
+
end = time.thread_time() + seconds
|
|
18
|
+
while time.thread_time() < end:
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
16
22
|
def run(reply):
|
|
17
23
|
agent = Agent(Fixed(reply))
|
|
18
24
|
return agent(dict(STATE)), agent
|
|
@@ -25,8 +31,12 @@ def test_legal_actions_pass_and_raises_clamp():
|
|
|
25
31
|
|
|
26
32
|
|
|
27
33
|
def test_bad_bots_fold():
|
|
28
|
-
action, agent = run(lambda s: time.sleep(0.
|
|
34
|
+
action, agent = run(lambda s: time.sleep(0.15))
|
|
35
|
+
assert action == ("fold",) and agent.late == 1
|
|
36
|
+
action, agent = run(lambda s: busy(0.03))
|
|
29
37
|
assert action == ("fold",) and agent.late == 1
|
|
38
|
+
action, agent = run(lambda s: time.sleep(0.03) or ("call",))
|
|
39
|
+
assert action == ("call",) and agent.late == 0
|
|
30
40
|
action, agent = run(lambda s: 1 / 0)
|
|
31
41
|
assert action == ("fold",) and "ZeroDivisionError" in agent.error
|
|
32
42
|
action, agent = run(lambda s: ("check",))
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
1
3
|
import pytest
|
|
2
4
|
|
|
3
5
|
from pokerbots_cli.main import main
|
|
@@ -14,7 +16,7 @@ def test_init_then_test(tmp_path, capsys):
|
|
|
14
16
|
bot = tmp_path / "mybot"
|
|
15
17
|
assert main(["init", str(bot)]) == 0
|
|
16
18
|
assert main(["test", str(bot), "--games", "3", "--seed", "1"]) == 0
|
|
17
|
-
assert "errors
|
|
19
|
+
assert re.search(r"errors\s+0\n", capsys.readouterr().out)
|
|
18
20
|
|
|
19
21
|
|
|
20
22
|
def test_docs_plain(capsys):
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
MICRO = 1_000_000
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def table(rows):
|
|
5
|
-
rows = [[str(c) for c in row] for row in rows]
|
|
6
|
-
widths = [max(len(r[i]) for r in rows if i < len(r)) for i in range(max(map(len, rows)))]
|
|
7
|
-
return "\n".join(" ".join(c.ljust(w) for c, w in zip(r, widths)).rstrip() for r in rows)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def money(micro):
|
|
11
|
-
cents = round(micro / 10_000)
|
|
12
|
-
return f"{cents}¢" if abs(cents) < 100 else f"${cents / 100:,.2f}"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def parse_money(text):
|
|
16
|
-
value = round(float(text.lstrip("$")) * MICRO)
|
|
17
|
-
if value <= 0:
|
|
18
|
-
raise ValueError("amount must be positive")
|
|
19
|
-
return value
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def place(n):
|
|
23
|
-
return {1: "1st", 2: "2nd", 3: "3rd"}.get(n, f"{n}th") if n else "-"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|