pokerbots-dev 0.1.4__tar.gz → 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.
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/PKG-INFO +1 -1
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/pyproject.toml +1 -1
- pokerbots_dev-0.1.6/src/pokerbots_cli/commands/__init__.py +3 -0
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/src/pokerbots_cli/commands/account.py +55 -9
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/src/pokerbots_cli/commands/admin.py +1 -1
- pokerbots_dev-0.1.6/src/pokerbots_cli/commands/history.py +53 -0
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/src/pokerbots_cli/commands/local.py +14 -0
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/src/pokerbots_cli/commands/money.py +6 -4
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/src/pokerbots_cli/docs.md +44 -6
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/src/pokerbots_cli/engine.py +68 -12
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/src/pokerbots_cli/fmt.py +12 -1
- pokerbots_dev-0.1.6/src/pokerbots_cli/house.py +47 -0
- pokerbots_dev-0.1.6/tests/test_engine.py +59 -0
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/tests/test_wallet.py +1 -1
- pokerbots_dev-0.1.4/src/pokerbots_cli/commands/__init__.py +0 -3
- pokerbots_dev-0.1.4/src/pokerbots_cli/house.py +0 -19
- pokerbots_dev-0.1.4/tests/test_engine.py +0 -36
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/.gitignore +0 -0
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/README.md +0 -0
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/src/pokerbots_cli/__init__.py +0 -0
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/src/pokerbots_cli/agent.py +0 -0
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/src/pokerbots_cli/api.py +0 -0
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/src/pokerbots_cli/auth.py +0 -0
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/src/pokerbots_cli/cards.py +0 -0
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/src/pokerbots_cli/main.py +0 -0
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/src/pokerbots_cli/template/bot.py +0 -0
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/src/pokerbots_cli/wallet.py +0 -0
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/tests/test_agent.py +0 -0
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/tests/test_cards.py +0 -0
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/tests/test_cli.py +0 -0
|
@@ -6,10 +6,11 @@ from pathlib import Path
|
|
|
6
6
|
|
|
7
7
|
from .. import auth
|
|
8
8
|
from ..api import call
|
|
9
|
-
from ..fmt import INK, PAPER, colored, money, place, signed, table, when
|
|
9
|
+
from ..fmt import INK, PAPER, colored, held, money, place, signed, table, when
|
|
10
10
|
|
|
11
11
|
MAX_UPLOAD = 1 << 20
|
|
12
12
|
WATCH_SECONDS = 5
|
|
13
|
+
SEATS = 6
|
|
13
14
|
ATTEST = "I am 18 or older, and paid skill competitions are allowed where I am. [y/N] "
|
|
14
15
|
|
|
15
16
|
|
|
@@ -43,7 +44,11 @@ def upload(args):
|
|
|
43
44
|
def deploy(args):
|
|
44
45
|
mode = "live" if args.live else "paper"
|
|
45
46
|
if mode == "live" and not call("GET", "/me")["attested"]:
|
|
46
|
-
|
|
47
|
+
try:
|
|
48
|
+
agreed = input(ATTEST).strip().lower() == "y"
|
|
49
|
+
except EOFError:
|
|
50
|
+
agreed = False
|
|
51
|
+
if not agreed:
|
|
47
52
|
sys.exit("not deployed")
|
|
48
53
|
call("POST", "/attest")
|
|
49
54
|
call("POST", "/deployments", {"mode": mode, "version": int(args.version.lstrip("v"))})
|
|
@@ -56,13 +61,29 @@ def stop(args):
|
|
|
56
61
|
print(f"stopped {mode}; games already queued will still play")
|
|
57
62
|
|
|
58
63
|
|
|
64
|
+
def rank(mode):
|
|
65
|
+
board = call("GET", f"/leaderboard/{mode}")
|
|
66
|
+
return f"#{board['you']} of {board['ranked']}" if board["you"] else f"unranked until {board['min_games']} games"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def note(me, mode):
|
|
70
|
+
"""Why live games might not be starting."""
|
|
71
|
+
if mode != "live":
|
|
72
|
+
return ""
|
|
73
|
+
if me["balance"] < me["buy_in"]:
|
|
74
|
+
return f" paused: balance below {money(me['buy_in'])}"
|
|
75
|
+
if me["live_ready"] < SEATS:
|
|
76
|
+
return f" waiting for players ({me['live_ready']} of {SEATS} ready)"
|
|
77
|
+
return ""
|
|
78
|
+
|
|
79
|
+
|
|
59
80
|
def report():
|
|
60
81
|
me = call("GET", "/me")
|
|
61
82
|
rows = [["user", me["login"]], ["versions", " ".join(f"v{v}" for v in me["versions"]) or "-"]]
|
|
62
83
|
for mode in ("paper", "live"):
|
|
63
84
|
d = me["deployments"].get(mode)
|
|
64
|
-
rows.append([mode, f"v{d['version']} rating {d['rating']:.0f} games {d['games']} today {d['today']}" if d else "-"])
|
|
65
|
-
rows += [["balance", money(me[
|
|
85
|
+
rows.append([mode, f"v{d['version']} rating {d['rating']:.0f} games {d['games']} today {d['today']} {rank(mode)}{note(me, mode)}" if d else "-"])
|
|
86
|
+
rows += [["balance", f"{money(me['balance'])} on pokerbots, for live buy-ins"], ["wallet", held(me)]]
|
|
66
87
|
text = table(rows, labels=True)
|
|
67
88
|
games = call("GET", "/games")
|
|
68
89
|
if games:
|
|
@@ -76,16 +97,26 @@ def report():
|
|
|
76
97
|
return text
|
|
77
98
|
|
|
78
99
|
|
|
79
|
-
def
|
|
80
|
-
|
|
100
|
+
def leaderboard(mode):
|
|
101
|
+
board = call("GET", f"/leaderboard/{mode}")
|
|
102
|
+
live = mode == "live"
|
|
103
|
+
head = ["rank", "user", "rating", "games"] + (["won"] if live else [])
|
|
104
|
+
rows = [[i + 1, r["login"], f"{r['rating']:.0f}", r["games"]] + ([signed(r["won"], "money")] if live else [])
|
|
105
|
+
for i, r in enumerate(board["rows"])]
|
|
106
|
+
text = f"{mode} leaderboard\n\n" + (table([head] + rows, header=True) if rows else f"no bots with {board['min_games']} games yet")
|
|
107
|
+
you = f"#{board['you']} of {board['ranked']}" if board["you"] else f"unranked until {board['min_games']} games"
|
|
108
|
+
return text + "\n\n" + table([["you", you]], labels=True)
|
|
81
109
|
|
|
82
110
|
|
|
83
|
-
def
|
|
111
|
+
def screen(render):
|
|
112
|
+
"""Full-screen, flicker-free view in the homepage colours, redrawn until ctrl-c."""
|
|
113
|
+
if not sys.stdout.isatty():
|
|
114
|
+
return print(render())
|
|
84
115
|
paint = f"\x1b[48;2;{INK}m\x1b[38;2;{PAPER}m" if colored() else ""
|
|
85
116
|
sys.stdout.write("\x1b[?1049h\x1b[?25l" + paint + "\x1b[2J")
|
|
86
117
|
try:
|
|
87
118
|
while True:
|
|
88
|
-
lines = ["", *(" " + line for line in
|
|
119
|
+
lines = ["", *(" " + line for line in render().split("\n")), "", " ctrl-c to quit"]
|
|
89
120
|
sys.stdout.write("\x1b[H" + "".join(line + "\x1b[K\n" for line in lines) + "\x1b[J")
|
|
90
121
|
sys.stdout.flush()
|
|
91
122
|
time.sleep(WATCH_SECONDS)
|
|
@@ -95,8 +126,24 @@ def watch(args):
|
|
|
95
126
|
sys.stdout.write("\x1b[0m\x1b[?25h\x1b[?1049l")
|
|
96
127
|
|
|
97
128
|
|
|
129
|
+
def status(args):
|
|
130
|
+
print(report())
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def watch(args):
|
|
134
|
+
screen(report)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def top(args):
|
|
138
|
+
screen(lambda: leaderboard("live" if args.live else "paper"))
|
|
139
|
+
|
|
140
|
+
|
|
98
141
|
def register(sub):
|
|
99
142
|
sub.add_parser("login", help="sign in with GitHub").set_defaults(run=login)
|
|
143
|
+
sub.add_parser("watch", help="status, live").set_defaults(run=watch)
|
|
144
|
+
p = sub.add_parser("top", help="leaderboard, live")
|
|
145
|
+
p.add_argument("--live", action="store_true", help="live play instead of paper")
|
|
146
|
+
p.set_defaults(run=top)
|
|
100
147
|
|
|
101
148
|
p = sub.add_parser("upload", help="upload a new version")
|
|
102
149
|
p.add_argument("dir", nargs="?", default=".")
|
|
@@ -112,4 +159,3 @@ def register(sub):
|
|
|
112
159
|
p.set_defaults(run=run)
|
|
113
160
|
|
|
114
161
|
sub.add_parser("status", help="your bot, rating, balance and recent games").set_defaults(run=status)
|
|
115
|
-
sub.add_parser("watch", help="status, live").set_defaults(run=watch)
|
|
@@ -26,7 +26,7 @@ def decide(args):
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
def register(sub):
|
|
29
|
-
p = sub.add_parser("admin"
|
|
29
|
+
p = sub.add_parser("admin")
|
|
30
30
|
admin_sub = p.add_subparsers(metavar="command", required=True)
|
|
31
31
|
admin_sub.add_parser("stats").set_defaults(run=stats)
|
|
32
32
|
admin_sub.add_parser("withdrawals", help="pending withdrawals").set_defaults(run=withdrawals)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from datetime import date, timedelta
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from urllib.parse import urlencode
|
|
5
|
+
|
|
6
|
+
from ..api import call
|
|
7
|
+
|
|
8
|
+
DEFAULT_DAYS = 7
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def known_ids(directory):
|
|
12
|
+
return {json.loads(line)["id"] for f in directory.glob("*.jsonl") for line in f.read_text().splitlines() if line}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def history(args):
|
|
16
|
+
out = Path(args.out)
|
|
17
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
18
|
+
have = known_ids(out)
|
|
19
|
+
params = {
|
|
20
|
+
"since": args.since or (date.today() - timedelta(days=DEFAULT_DAYS)).isoformat(),
|
|
21
|
+
"until": args.until or date.today().isoformat(),
|
|
22
|
+
}
|
|
23
|
+
if args.live or args.paper:
|
|
24
|
+
params["mode"] = "live" if args.live else "paper"
|
|
25
|
+
if args.all:
|
|
26
|
+
params["all"] = "1"
|
|
27
|
+
saved = skipped = 0
|
|
28
|
+
after = None
|
|
29
|
+
while True:
|
|
30
|
+
page = call("GET", "/history?" + urlencode({**params, **({"after": after} if after else {})}))
|
|
31
|
+
for game in page["games"]:
|
|
32
|
+
if game["id"] in have:
|
|
33
|
+
skipped += 1
|
|
34
|
+
continue
|
|
35
|
+
with open(out / f"{game['finished_at'][:10]}.jsonl", "a") as f:
|
|
36
|
+
f.write(json.dumps(game) + "\n")
|
|
37
|
+
saved += 1
|
|
38
|
+
after = page["next"]
|
|
39
|
+
if not after:
|
|
40
|
+
break
|
|
41
|
+
print(f"saved {saved} games to {out}/" + (f", {skipped} already there" if skipped else ""))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def register(sub):
|
|
45
|
+
p = sub.add_parser("history", help="download finished games as JSON lines")
|
|
46
|
+
p.add_argument("--since", metavar="YYYY-MM-DD", help=f"default {DEFAULT_DAYS} days ago")
|
|
47
|
+
p.add_argument("--until", metavar="YYYY-MM-DD", help="default today")
|
|
48
|
+
mode = p.add_mutually_exclusive_group()
|
|
49
|
+
mode.add_argument("--paper", action="store_true")
|
|
50
|
+
mode.add_argument("--live", action="store_true")
|
|
51
|
+
p.add_argument("--all", action="store_true", help="every player's games, not just yours")
|
|
52
|
+
p.add_argument("-o", "--out", default="history", help="directory, default ./history")
|
|
53
|
+
p.set_defaults(run=history)
|
|
@@ -2,6 +2,7 @@ import importlib.util
|
|
|
2
2
|
import pydoc
|
|
3
3
|
import random
|
|
4
4
|
import shutil
|
|
5
|
+
import subprocess
|
|
5
6
|
import sys
|
|
6
7
|
from importlib import resources
|
|
7
8
|
from pathlib import Path
|
|
@@ -67,6 +68,17 @@ def test(args):
|
|
|
67
68
|
return 1
|
|
68
69
|
|
|
69
70
|
|
|
71
|
+
def upgrade(args):
|
|
72
|
+
"""Upgrade with whichever installer pb came from; --refresh skips uv's stale package index."""
|
|
73
|
+
if shutil.which("uv"):
|
|
74
|
+
command = ["uv", "tool", "upgrade", "--refresh", "pokerbots-dev"]
|
|
75
|
+
elif shutil.which("pipx"):
|
|
76
|
+
command = ["pipx", "upgrade", "pokerbots-dev"]
|
|
77
|
+
else:
|
|
78
|
+
command = [sys.executable, "-m", "pip", "install", "--upgrade", "pokerbots-dev"]
|
|
79
|
+
return subprocess.call(command)
|
|
80
|
+
|
|
81
|
+
|
|
70
82
|
def register(sub):
|
|
71
83
|
p = sub.add_parser("docs", help="show the docs")
|
|
72
84
|
p.add_argument("--plain", action="store_true", help="no pager, for agents")
|
|
@@ -81,3 +93,5 @@ def register(sub):
|
|
|
81
93
|
p.add_argument("--games", type=int, default=100)
|
|
82
94
|
p.add_argument("--seed", type=int)
|
|
83
95
|
p.set_defaults(run=test)
|
|
96
|
+
|
|
97
|
+
sub.add_parser("upgrade", help="upgrade pb to the latest version").set_defaults(run=upgrade)
|
|
@@ -2,7 +2,7 @@ import sys
|
|
|
2
2
|
|
|
3
3
|
from .. import wallet as keys
|
|
4
4
|
from ..api import call
|
|
5
|
-
from ..fmt import money, parse_money, table
|
|
5
|
+
from ..fmt import held, money, parse_money, table
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def registered_key():
|
|
@@ -16,10 +16,12 @@ def registered_key():
|
|
|
16
16
|
|
|
17
17
|
def wallet(args):
|
|
18
18
|
_, address, me = registered_key()
|
|
19
|
-
|
|
19
|
+
me = call("GET", "/me")
|
|
20
|
+
yours = [["address", address], ["send", "USDC on Solana only (not SOL)"], ["wallet", held(me)]]
|
|
21
|
+
ours = [["balance", f"{money(me['balance'])} on pokerbots, for live buy-ins"]]
|
|
20
22
|
if me["withdraw_to"] and me["withdraw_to"] != address:
|
|
21
|
-
|
|
22
|
-
print(table(
|
|
23
|
+
ours.append(["withdraws to", me["withdraw_to"]])
|
|
24
|
+
print(table(yours, labels=True) + "\n" + "-" * 3 + "\n" + table(ours, labels=True))
|
|
23
25
|
|
|
24
26
|
|
|
25
27
|
def deposit(args):
|
|
@@ -10,7 +10,10 @@ start
|
|
|
10
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
|
+
pb top [--live] leaderboard: rating order, 20+ games; live shows won
|
|
14
|
+
pb history download your finished games, see history below
|
|
13
15
|
pb docs [--plain] show this
|
|
16
|
+
pb upgrade upgrade pb
|
|
14
17
|
|
|
15
18
|
money
|
|
16
19
|
pb wallet your Solana wallet address and balance
|
|
@@ -43,12 +46,34 @@ actions
|
|
|
43
46
|
("raise", n) n is your total bet this round, from min_raise to max_raise
|
|
44
47
|
|
|
45
48
|
state
|
|
46
|
-
hand
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
hand hand number, from 1
|
|
50
|
+
hands_left hands until the game ends at 100
|
|
51
|
+
street preflop, flop, turn or river
|
|
52
|
+
seat your seat, 0-5
|
|
53
|
+
players login by seat, or "house"
|
|
54
|
+
button button seat
|
|
55
|
+
small_blind small blind seat
|
|
56
|
+
big_blind big blind seat
|
|
57
|
+
blinds [small, big] amounts
|
|
58
|
+
hole your two cards, e.g. ["As", "Td"]
|
|
59
|
+
board shared cards
|
|
60
|
+
stacks_start chips by seat when this hand began
|
|
61
|
+
stacks chips behind, by seat
|
|
62
|
+
bets chips bet this round, by seat
|
|
63
|
+
current_bet the bet to match this round
|
|
64
|
+
pot chips bet this hand
|
|
65
|
+
to_call chips needed to call
|
|
66
|
+
legal actions allowed now
|
|
67
|
+
min_raise smallest raise total, when raise is legal
|
|
68
|
+
max_raise all in, when raise is legal
|
|
69
|
+
live seats still in the hand
|
|
70
|
+
all_in seats in the hand with no chips behind
|
|
71
|
+
out eliminated seats, first out first
|
|
72
|
+
actions this hand so far, [cards on board, seat, action, bet this round]
|
|
73
|
+
past finished hands this game: hand, board, shown (cards by seat,
|
|
74
|
+
null unless shown down), won (chips by seat)
|
|
75
|
+
|
|
76
|
+
cards
|
|
52
77
|
stacks chips behind, by seat
|
|
53
78
|
bets chips bet this round, by seat
|
|
54
79
|
pot chips bet this hand
|
|
@@ -77,6 +102,19 @@ game
|
|
|
77
102
|
rating
|
|
78
103
|
Elo, from 1500; each game counts as a win over every bot placed below
|
|
79
104
|
|
|
105
|
+
history
|
|
106
|
+
pb history [--since YYYY-MM-DD] [--until YYYY-MM-DD] [--paper|--live] [--all] [-o dir]
|
|
107
|
+
|
|
108
|
+
One JSON line per finished game, one file per day: history/2026-10-29.jsonl.
|
|
109
|
+
Games appear 10 minutes after they end. Default: your games, last 7 days.
|
|
110
|
+
--all adds everyone's games. You see your own hole cards and any cards
|
|
111
|
+
shown down; folded cards stay hidden. Re-running skips games you have.
|
|
112
|
+
|
|
113
|
+
game id, mode, finished_at, hands_played, you (your seat),
|
|
114
|
+
players [seat, user, place, rating_change, won ($, live)],
|
|
115
|
+
hands [hand, button, small_blind, big_blind, blinds, stacks_start,
|
|
116
|
+
hole, board, actions, shown, won]
|
|
117
|
+
|
|
80
118
|
price
|
|
81
119
|
paper free, 1,000 games/day
|
|
82
120
|
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import random
|
|
2
|
-
from dataclasses import dataclass
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
3
|
|
|
4
4
|
from .cards import DECK, best
|
|
5
5
|
|
|
6
6
|
STACK = 1000
|
|
7
7
|
MAX_HANDS = 100
|
|
8
|
+
STREETS = ("preflop", "flop", "turn", "river")
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
@dataclass
|
|
@@ -28,31 +29,52 @@ class Player:
|
|
|
28
29
|
self.committed += chips
|
|
29
30
|
|
|
30
31
|
|
|
32
|
+
@dataclass
|
|
33
|
+
class Game:
|
|
34
|
+
"""What every hand of one game shares: names, finished hands, eliminations."""
|
|
35
|
+
names: list
|
|
36
|
+
max_hands: int = MAX_HANDS
|
|
37
|
+
past: list = field(default_factory=list) # public summaries, as bots see them
|
|
38
|
+
hands: list = field(default_factory=list) # full records, every hole card
|
|
39
|
+
out: list = field(default_factory=list)
|
|
40
|
+
|
|
41
|
+
|
|
31
42
|
@dataclass
|
|
32
43
|
class Result:
|
|
33
44
|
places: list
|
|
34
45
|
hands: int
|
|
35
46
|
stacks: list
|
|
47
|
+
history: list
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def per_seat(n, values=None, default=None):
|
|
51
|
+
out = [default] * n
|
|
52
|
+
for seat, value in (values or {}).items():
|
|
53
|
+
out[seat] = value
|
|
54
|
+
return out
|
|
36
55
|
|
|
37
56
|
|
|
38
57
|
class Hand:
|
|
39
|
-
def __init__(self, number, players, button, deck,
|
|
58
|
+
def __init__(self, number, players, button, deck, game):
|
|
40
59
|
self.number = number
|
|
41
60
|
self.players = players
|
|
42
61
|
self.button = button
|
|
43
62
|
self.deck = deck
|
|
44
|
-
self.
|
|
63
|
+
self.game = game
|
|
64
|
+
self.seats = len(game.names)
|
|
45
65
|
self.blinds = [number, 2 * number]
|
|
46
66
|
self.board = []
|
|
47
67
|
self.log = []
|
|
48
68
|
|
|
49
69
|
def play(self):
|
|
50
70
|
n = len(self.players)
|
|
71
|
+
self.stacks_start = per_seat(self.seats, {p.seat: p.stack for p in self.players}, 0)
|
|
51
72
|
for p in self.players:
|
|
52
73
|
p.hole = (self.deck.pop(), self.deck.pop())
|
|
53
74
|
p.bet = p.committed = 0
|
|
54
75
|
p.folded = False
|
|
55
76
|
small = self.button if n == 2 else (self.button + 1) % n
|
|
77
|
+
self.blind_seats = [self.players[small].seat, self.players[(small + 1) % n].seat]
|
|
56
78
|
for i, blind in zip((small, (small + 1) % n), self.blinds):
|
|
57
79
|
self.players[i].put(blind)
|
|
58
80
|
self.record(self.players[i], "blind")
|
|
@@ -64,7 +86,7 @@ class Hand:
|
|
|
64
86
|
p.bet = 0
|
|
65
87
|
self.board += [self.deck.pop() for _ in range(count)]
|
|
66
88
|
self.betting((self.button + 1) % n, 0)
|
|
67
|
-
self.award()
|
|
89
|
+
self.finish(self.award())
|
|
68
90
|
|
|
69
91
|
def betting(self, start, current):
|
|
70
92
|
order = self.players[start:] + self.players[:start]
|
|
@@ -77,7 +99,7 @@ class Hand:
|
|
|
77
99
|
continue
|
|
78
100
|
to_call = current - p.bet
|
|
79
101
|
legal = ["fold", "call"] if to_call > 0 else ["check"]
|
|
80
|
-
state = self.state(p, legal, to_call)
|
|
102
|
+
state = self.state(p, legal, to_call, current)
|
|
81
103
|
if others and p.stack > to_call:
|
|
82
104
|
legal.append("raise")
|
|
83
105
|
state["max_raise"] = p.bet + p.stack
|
|
@@ -98,45 +120,79 @@ class Hand:
|
|
|
98
120
|
def record(self, p, action):
|
|
99
121
|
self.log.append([len(self.board), p.seat, action, p.bet])
|
|
100
122
|
|
|
101
|
-
def state(self, p, legal, to_call):
|
|
123
|
+
def state(self, p, legal, to_call, current):
|
|
102
124
|
stacks, bets = [0] * self.seats, [0] * self.seats
|
|
103
125
|
for q in self.players:
|
|
104
126
|
stacks[q.seat], bets[q.seat] = q.stack, q.bet
|
|
105
127
|
return {
|
|
106
128
|
"hand": self.number,
|
|
129
|
+
"hands_left": self.game.max_hands - self.number,
|
|
130
|
+
"street": STREETS[max(0, len(self.board) - 2)],
|
|
107
131
|
"seat": p.seat,
|
|
132
|
+
"players": list(self.game.names),
|
|
108
133
|
"button": self.players[self.button].seat,
|
|
134
|
+
"small_blind": self.blind_seats[0],
|
|
135
|
+
"big_blind": self.blind_seats[1],
|
|
109
136
|
"blinds": list(self.blinds),
|
|
110
137
|
"hole": list(p.hole),
|
|
111
138
|
"board": list(self.board),
|
|
139
|
+
"stacks_start": list(self.stacks_start),
|
|
112
140
|
"stacks": stacks,
|
|
113
141
|
"bets": bets,
|
|
142
|
+
"current_bet": current,
|
|
114
143
|
"pot": sum(q.committed for q in self.players),
|
|
115
144
|
"to_call": to_call,
|
|
116
145
|
"legal": legal,
|
|
117
146
|
"live": [q.seat for q in self.players if not q.folded],
|
|
147
|
+
"all_in": [q.seat for q in self.players if not q.folded and q.stack == 0],
|
|
148
|
+
"out": list(self.game.out),
|
|
118
149
|
"actions": [list(a) for a in self.log],
|
|
150
|
+
"past": list(self.game.past),
|
|
119
151
|
}
|
|
120
152
|
|
|
121
153
|
def award(self):
|
|
122
154
|
live = [p for p in self.players if not p.folded]
|
|
123
155
|
strength = {p.seat: best(p.hole + tuple(self.board)) for p in live} if len(live) > 1 else {}
|
|
156
|
+
won = {}
|
|
124
157
|
previous = 0
|
|
125
158
|
for level in sorted({p.committed for p in self.players}):
|
|
126
159
|
pot = sum(min(p.committed, level) - min(p.committed, previous) for p in self.players)
|
|
127
160
|
previous = level
|
|
128
|
-
|
|
161
|
+
# nobody live matched this level: the chips were uncalled and go back to whoever bet them
|
|
162
|
+
eligible = [p for p in live if p.committed >= level] or [p for p in self.players if p.committed >= level]
|
|
129
163
|
top = max(strength.get(p.seat, ()) for p in eligible)
|
|
130
164
|
winners = [p for p in eligible if strength.get(p.seat, ()) == top]
|
|
131
165
|
share, odd = divmod(pot, len(winners))
|
|
132
166
|
for i, w in enumerate(winners):
|
|
133
167
|
w.stack += share + (i < odd)
|
|
168
|
+
won[w.seat] = won.get(w.seat, 0) + share + (i < odd)
|
|
169
|
+
return won
|
|
170
|
+
|
|
171
|
+
def finish(self, won):
|
|
172
|
+
shown = {p.seat: list(p.hole) for p in self.players if not p.folded} if sum(not p.folded for p in self.players) > 1 else {}
|
|
173
|
+
summary = {
|
|
174
|
+
"hand": self.number,
|
|
175
|
+
"board": list(self.board),
|
|
176
|
+
"shown": per_seat(self.seats, shown),
|
|
177
|
+
"won": per_seat(self.seats, won, 0),
|
|
178
|
+
}
|
|
179
|
+
self.game.past.append(summary)
|
|
180
|
+
self.game.hands.append({
|
|
181
|
+
**summary,
|
|
182
|
+
"button": self.players[self.button].seat,
|
|
183
|
+
"small_blind": self.blind_seats[0],
|
|
184
|
+
"big_blind": self.blind_seats[1],
|
|
185
|
+
"blinds": list(self.blinds),
|
|
186
|
+
"stacks_start": list(self.stacks_start),
|
|
187
|
+
"hole": per_seat(self.seats, {p.seat: list(p.hole) for p in self.players}),
|
|
188
|
+
"actions": [list(a) for a in self.log],
|
|
189
|
+
})
|
|
134
190
|
|
|
135
191
|
|
|
136
|
-
def play(agents, rng=None, max_hands=MAX_HANDS):
|
|
192
|
+
def play(agents, rng=None, max_hands=MAX_HANDS, names=None):
|
|
137
193
|
rng = rng or random.Random()
|
|
138
194
|
table = [Player(seat, agent) for seat, agent in enumerate(agents)]
|
|
139
|
-
|
|
195
|
+
game = Game(list(names or [f"seat {i}" for i in range(len(table))]), max_hands)
|
|
140
196
|
button = rng.randrange(len(table))
|
|
141
197
|
hands = 0
|
|
142
198
|
while hands < max_hands and sum(p.stack > 0 for p in table) > 1:
|
|
@@ -144,8 +200,8 @@ def play(agents, rng=None, max_hands=MAX_HANDS):
|
|
|
144
200
|
alive = [p for p in table if p.stack > 0]
|
|
145
201
|
b = next((i for i, p in enumerate(alive) if p.seat >= button), 0)
|
|
146
202
|
before = {p.seat: p.stack for p in alive}
|
|
147
|
-
Hand(hands, alive, b, rng.sample(DECK, len(DECK)),
|
|
148
|
-
|
|
203
|
+
Hand(hands, alive, b, rng.sample(DECK, len(DECK)), game).play()
|
|
204
|
+
game.out += sorted((p.seat for p in alive if p.stack == 0), key=before.get)
|
|
149
205
|
button = alive[b].seat + 1
|
|
150
206
|
standing = sorted((p for p in table if p.stack > 0), key=lambda p: -p.stack)
|
|
151
|
-
return Result([p.seat for p in standing] +
|
|
207
|
+
return Result([p.seat for p in standing] + game.out[::-1], hands, [p.stack for p in table], game.hands)
|
|
@@ -31,7 +31,18 @@ def table(rows, labels=False, header=False):
|
|
|
31
31
|
|
|
32
32
|
def money(micro):
|
|
33
33
|
cents = round(micro / 10_000)
|
|
34
|
-
|
|
34
|
+
sign = "-" if cents < 0 else ""
|
|
35
|
+
return f"{cents}¢" if abs(cents) < 100 else f"{sign}${abs(cents) / 100:,.2f}"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def held(me):
|
|
39
|
+
"""The wallet line: USDC in the player's own wallet, with a hint when it is SOL instead."""
|
|
40
|
+
if me.get("wallet_usdc") is None:
|
|
41
|
+
return "-"
|
|
42
|
+
text = f"{money(me['wallet_usdc'])} USDC in your wallet, ready to deposit"
|
|
43
|
+
if not me["wallet_usdc"] and me.get("wallet_sol"):
|
|
44
|
+
text += f" ({me['wallet_sol']} SOL there; deposits need USDC)"
|
|
45
|
+
return text
|
|
35
46
|
|
|
36
47
|
|
|
37
48
|
def signed(value, unit=""):
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import random
|
|
2
|
+
|
|
3
|
+
from .cards import RANKS, best
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def strength(state):
|
|
7
|
+
"""Rough 0-1 hand strength: hole cards preflop, then the made hand."""
|
|
8
|
+
hole = state["hole"]
|
|
9
|
+
if not state["board"]:
|
|
10
|
+
high, low = sorted((RANKS.index(c[0]) for c in hole), reverse=True)
|
|
11
|
+
suited = hole[0][1] == hole[1][1]
|
|
12
|
+
return min(1, (high + low) / 24 + 0.35 * (high == low) + 0.05 * suited)
|
|
13
|
+
return min(1, best(hole + state["board"])[0] / 4)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Style:
|
|
17
|
+
"""A simple, sensible bot: calls with enough strength or pot odds, raises its strong hands."""
|
|
18
|
+
|
|
19
|
+
call_at = raise_at = aggression = 0.0
|
|
20
|
+
|
|
21
|
+
def __init__(self, rng=None):
|
|
22
|
+
self.rng = rng or random.Random()
|
|
23
|
+
|
|
24
|
+
def act(self, s):
|
|
25
|
+
hand = strength(s)
|
|
26
|
+
if "raise" in s["legal"] and hand >= self.raise_at and self.rng.random() < self.aggression:
|
|
27
|
+
return ("raise", min(s["max_raise"], max(s["min_raise"], s["current_bet"] + s["pot"] // 2)))
|
|
28
|
+
if "check" in s["legal"]:
|
|
29
|
+
return ("check",)
|
|
30
|
+
odds = s["to_call"] / (s["pot"] + s["to_call"])
|
|
31
|
+
return ("call",) if hand >= max(self.call_at, odds) else ("fold",)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Tight(Style):
|
|
35
|
+
call_at, raise_at, aggression = 0.6, 0.75, 0.7
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Loose(Style):
|
|
39
|
+
call_at, raise_at, aggression = 0.3, 0.5, 0.5
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Solid(Style):
|
|
43
|
+
call_at, raise_at, aggression = 0.45, 0.65, 0.5
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
HOUSE = {"tight": Tight, "loose": Loose, "solid": Solid}
|
|
47
|
+
BOTS = (Tight, Loose, Solid, Tight, Loose)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import random
|
|
2
|
+
|
|
3
|
+
from pokerbots_cli import engine
|
|
4
|
+
from pokerbots_cli.agent import Agent
|
|
5
|
+
from pokerbots_cli.house import BOTS, Loose
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def shove(state):
|
|
9
|
+
if "raise" in state["legal"]:
|
|
10
|
+
return ("raise", state["max_raise"])
|
|
11
|
+
return ("call",) if "call" in state["legal"] else ("check",)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_games_conserve_chips_and_rank_everyone():
|
|
15
|
+
rng = random.Random(1)
|
|
16
|
+
for _ in range(20):
|
|
17
|
+
result = engine.play([Agent(Loose(rng))] + [Agent(cls()) for cls in BOTS], rng)
|
|
18
|
+
assert sum(result.stacks) == 6 * engine.STACK
|
|
19
|
+
assert sorted(result.places) == list(range(6))
|
|
20
|
+
assert result.hands <= engine.MAX_HANDS
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_side_pots():
|
|
24
|
+
stacks = [100, 300, 300]
|
|
25
|
+
players = [engine.Player(seat, shove, stack) for seat, stack in enumerate(stacks)]
|
|
26
|
+
dealt = "Ah Ad Kh Kd Qh Qd 2c 7d 9h Js 4c".split()
|
|
27
|
+
engine.Hand(1, players, 0, dealt[::-1], engine.Game(["a", "b", "c"])).play()
|
|
28
|
+
assert [p.stack for p in players] == [300, 400, 0]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_heads_up_button_posts_small_blind():
|
|
32
|
+
seen = []
|
|
33
|
+
players = [engine.Player(s, lambda st: seen.append(st) or ("fold",)) for s in range(2)]
|
|
34
|
+
engine.Hand(3, players, 1, list(engine.DECK), engine.Game(["a", "b"])).play()
|
|
35
|
+
assert seen[0]["seat"] == 1 and seen[0]["bets"] == [6, 3]
|
|
36
|
+
assert [p.stack for p in players] == [1003, 997]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_state_and_history():
|
|
40
|
+
seen = []
|
|
41
|
+
|
|
42
|
+
def agent(state):
|
|
43
|
+
seen.append(state)
|
|
44
|
+
return shove(state) if state["seat"] == 0 else ("call",) if "call" in state["legal"] else ("check",)
|
|
45
|
+
|
|
46
|
+
result = engine.play([agent] * 6, random.Random(3), names=list("abcdef"))
|
|
47
|
+
last = seen[-1]
|
|
48
|
+
assert last["players"] == list("abcdef") and last["street"] in engine.STREETS
|
|
49
|
+
assert len(last["past"]) == last["hand"] - 1 and last["hands_left"] == engine.MAX_HANDS - last["hand"]
|
|
50
|
+
assert len(result.history) == result.hands
|
|
51
|
+
for h in result.history:
|
|
52
|
+
assert sum(h["won"]) > 0
|
|
53
|
+
assert all(h["shown"][s] in (None, h["hole"][s]) for s in range(6))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_uncalled_chips_return():
|
|
57
|
+
players = [engine.Player(0, lambda s: ("fold",), 14), engine.Player(1, lambda s: ("fold",), 1000)]
|
|
58
|
+
engine.Hand(67, players, 1, list(engine.DECK), engine.Game(["a", "b"])).play()
|
|
59
|
+
assert [p.stack for p in players] == [28, 986]
|
|
@@ -20,5 +20,5 @@ def test_sign_fills_our_slot_only():
|
|
|
20
20
|
def test_b58_and_money():
|
|
21
21
|
assert wallet.b58(bytes(32)) == "1" * 32
|
|
22
22
|
assert wallet.b58(b"\x00\x01") == "12"
|
|
23
|
-
assert [money(100_000), money(370_000), money(4_200_000)] == ["10¢", "37¢", "$4.20"]
|
|
23
|
+
assert [money(100_000), money(370_000), money(4_200_000), money(-1_100_000)] == ["10¢", "37¢", "$4.20", "-$1.10"]
|
|
24
24
|
assert parse_money("$0.10") == 100_000
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import random
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class Caller:
|
|
5
|
-
def act(self, state):
|
|
6
|
-
return ("call",) if "call" in state["legal"] else ("check",)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class Raiser:
|
|
10
|
-
def __init__(self, rng=None):
|
|
11
|
-
self.rng = rng or random.Random()
|
|
12
|
-
|
|
13
|
-
def act(self, state):
|
|
14
|
-
if "raise" in state["legal"] and self.rng.random() < 0.3:
|
|
15
|
-
return ("raise", min(state["max_raise"], state["min_raise"] + self.rng.randint(0, state["pot"])))
|
|
16
|
-
return Caller().act(state)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
BOTS = (Caller, Raiser, Caller, Raiser, Caller)
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import random
|
|
2
|
-
|
|
3
|
-
from pokerbots_cli import engine
|
|
4
|
-
from pokerbots_cli.agent import Agent
|
|
5
|
-
from pokerbots_cli.house import BOTS, Raiser
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def shove(state):
|
|
9
|
-
if "raise" in state["legal"]:
|
|
10
|
-
return ("raise", state["max_raise"])
|
|
11
|
-
return ("call",) if "call" in state["legal"] else ("check",)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def test_games_conserve_chips_and_rank_everyone():
|
|
15
|
-
rng = random.Random(1)
|
|
16
|
-
for _ in range(20):
|
|
17
|
-
result = engine.play([Agent(Raiser(rng))] + [Agent(cls()) for cls in BOTS], rng)
|
|
18
|
-
assert sum(result.stacks) == 6 * engine.STACK
|
|
19
|
-
assert sorted(result.places) == list(range(6))
|
|
20
|
-
assert result.hands <= engine.MAX_HANDS
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def test_side_pots():
|
|
24
|
-
stacks = [100, 300, 300]
|
|
25
|
-
players = [engine.Player(seat, shove, stack) for seat, stack in enumerate(stacks)]
|
|
26
|
-
dealt = "Ah Ad Kh Kd Qh Qd 2c 7d 9h Js 4c".split()
|
|
27
|
-
engine.Hand(1, players, 0, dealt[::-1], 3).play()
|
|
28
|
-
assert [p.stack for p in players] == [300, 400, 0]
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def test_heads_up_button_posts_small_blind():
|
|
32
|
-
seen = []
|
|
33
|
-
players = [engine.Player(s, lambda st: seen.append(st) or ("fold",)) for s in range(2)]
|
|
34
|
-
engine.Hand(3, players, 1, list(engine.DECK), 2).play()
|
|
35
|
-
assert seen[0]["seat"] == 1 and seen[0]["bets"] == [6, 3]
|
|
36
|
-
assert [p.stack for p in players] == [1003, 997]
|
|
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
|