pokerbots-dev 0.1.4__tar.gz → 0.1.5__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.5}/PKG-INFO +1 -1
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/pyproject.toml +1 -1
- pokerbots_dev-0.1.5/src/pokerbots_cli/commands/__init__.py +3 -0
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/src/pokerbots_cli/commands/account.py +36 -6
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/src/pokerbots_cli/commands/admin.py +1 -1
- pokerbots_dev-0.1.5/src/pokerbots_cli/commands/history.py +53 -0
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/src/pokerbots_cli/docs.md +43 -6
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/src/pokerbots_cli/engine.py +66 -11
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/src/pokerbots_cli/fmt.py +2 -1
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/tests/test_engine.py +19 -2
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/tests/test_wallet.py +1 -1
- pokerbots_dev-0.1.4/src/pokerbots_cli/commands/__init__.py +0 -3
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/.gitignore +0 -0
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/README.md +0 -0
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/src/pokerbots_cli/__init__.py +0 -0
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/src/pokerbots_cli/agent.py +0 -0
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/src/pokerbots_cli/api.py +0 -0
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/src/pokerbots_cli/auth.py +0 -0
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/src/pokerbots_cli/cards.py +0 -0
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/src/pokerbots_cli/commands/local.py +0 -0
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/src/pokerbots_cli/commands/money.py +0 -0
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/src/pokerbots_cli/house.py +0 -0
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/src/pokerbots_cli/main.py +0 -0
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/src/pokerbots_cli/template/bot.py +0 -0
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/src/pokerbots_cli/wallet.py +0 -0
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/tests/test_agent.py +0 -0
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/tests/test_cards.py +0 -0
- {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/tests/test_cli.py +0 -0
|
@@ -56,12 +56,17 @@ def stop(args):
|
|
|
56
56
|
print(f"stopped {mode}; games already queued will still play")
|
|
57
57
|
|
|
58
58
|
|
|
59
|
+
def rank(mode):
|
|
60
|
+
board = call("GET", f"/leaderboard/{mode}")
|
|
61
|
+
return f"#{board['you']} of {board['ranked']}" if board["you"] else f"unranked until {board['min_games']} games"
|
|
62
|
+
|
|
63
|
+
|
|
59
64
|
def report():
|
|
60
65
|
me = call("GET", "/me")
|
|
61
66
|
rows = [["user", me["login"]], ["versions", " ".join(f"v{v}" for v in me["versions"]) or "-"]]
|
|
62
67
|
for mode in ("paper", "live"):
|
|
63
68
|
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 "-"])
|
|
69
|
+
rows.append([mode, f"v{d['version']} rating {d['rating']:.0f} games {d['games']} today {d['today']} {rank(mode)}" if d else "-"])
|
|
65
70
|
rows += [["balance", money(me["balance"])], ["wallet", me["wallet"] or "-"]]
|
|
66
71
|
text = table(rows, labels=True)
|
|
67
72
|
games = call("GET", "/games")
|
|
@@ -76,16 +81,26 @@ def report():
|
|
|
76
81
|
return text
|
|
77
82
|
|
|
78
83
|
|
|
79
|
-
def
|
|
80
|
-
|
|
84
|
+
def leaderboard(mode):
|
|
85
|
+
board = call("GET", f"/leaderboard/{mode}")
|
|
86
|
+
live = mode == "live"
|
|
87
|
+
head = ["rank", "user", "rating", "games"] + (["won"] if live else [])
|
|
88
|
+
rows = [[i + 1, r["login"], f"{r['rating']:.0f}", r["games"]] + ([signed(r["won"], "money")] if live else [])
|
|
89
|
+
for i, r in enumerate(board["rows"])]
|
|
90
|
+
text = f"{mode} leaderboard\n\n" + (table([head] + rows, header=True) if rows else f"no bots with {board['min_games']} games yet")
|
|
91
|
+
you = f"#{board['you']} of {board['ranked']}" if board["you"] else f"unranked until {board['min_games']} games"
|
|
92
|
+
return text + "\n\n" + table([["you", you]], labels=True)
|
|
81
93
|
|
|
82
94
|
|
|
83
|
-
def
|
|
95
|
+
def screen(render):
|
|
96
|
+
"""Full-screen, flicker-free view in the homepage colours, redrawn until ctrl-c."""
|
|
97
|
+
if not sys.stdout.isatty():
|
|
98
|
+
return print(render())
|
|
84
99
|
paint = f"\x1b[48;2;{INK}m\x1b[38;2;{PAPER}m" if colored() else ""
|
|
85
100
|
sys.stdout.write("\x1b[?1049h\x1b[?25l" + paint + "\x1b[2J")
|
|
86
101
|
try:
|
|
87
102
|
while True:
|
|
88
|
-
lines = ["", *(" " + line for line in
|
|
103
|
+
lines = ["", *(" " + line for line in render().split("\n")), "", " ctrl-c to quit"]
|
|
89
104
|
sys.stdout.write("\x1b[H" + "".join(line + "\x1b[K\n" for line in lines) + "\x1b[J")
|
|
90
105
|
sys.stdout.flush()
|
|
91
106
|
time.sleep(WATCH_SECONDS)
|
|
@@ -95,8 +110,24 @@ def watch(args):
|
|
|
95
110
|
sys.stdout.write("\x1b[0m\x1b[?25h\x1b[?1049l")
|
|
96
111
|
|
|
97
112
|
|
|
113
|
+
def status(args):
|
|
114
|
+
print(report())
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def watch(args):
|
|
118
|
+
screen(report)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def top(args):
|
|
122
|
+
screen(lambda: leaderboard("live" if args.live else "paper"))
|
|
123
|
+
|
|
124
|
+
|
|
98
125
|
def register(sub):
|
|
99
126
|
sub.add_parser("login", help="sign in with GitHub").set_defaults(run=login)
|
|
127
|
+
sub.add_parser("watch", help="status, live").set_defaults(run=watch)
|
|
128
|
+
p = sub.add_parser("top", help="leaderboard, live")
|
|
129
|
+
p.add_argument("--live", action="store_true", help="live play instead of paper")
|
|
130
|
+
p.set_defaults(run=top)
|
|
100
131
|
|
|
101
132
|
p = sub.add_parser("upload", help="upload a new version")
|
|
102
133
|
p.add_argument("dir", nargs="?", default=".")
|
|
@@ -112,4 +143,3 @@ def register(sub):
|
|
|
112
143
|
p.set_defaults(run=run)
|
|
113
144
|
|
|
114
145
|
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)
|
|
@@ -10,6 +10,8 @@ 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
|
|
14
16
|
|
|
15
17
|
money
|
|
@@ -43,12 +45,34 @@ actions
|
|
|
43
45
|
("raise", n) n is your total bet this round, from min_raise to max_raise
|
|
44
46
|
|
|
45
47
|
state
|
|
46
|
-
hand
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
hand hand number, from 1
|
|
49
|
+
hands_left hands until the game ends at 100
|
|
50
|
+
street preflop, flop, turn or river
|
|
51
|
+
seat your seat, 0-5
|
|
52
|
+
players login by seat, or "house"
|
|
53
|
+
button button seat
|
|
54
|
+
small_blind small blind seat
|
|
55
|
+
big_blind big blind seat
|
|
56
|
+
blinds [small, big] amounts
|
|
57
|
+
hole your two cards, e.g. ["As", "Td"]
|
|
58
|
+
board shared cards
|
|
59
|
+
stacks_start chips by seat when this hand began
|
|
60
|
+
stacks chips behind, by seat
|
|
61
|
+
bets chips bet this round, by seat
|
|
62
|
+
current_bet the bet to match this round
|
|
63
|
+
pot chips bet this hand
|
|
64
|
+
to_call chips needed to call
|
|
65
|
+
legal actions allowed now
|
|
66
|
+
min_raise smallest raise total, when raise is legal
|
|
67
|
+
max_raise all in, when raise is legal
|
|
68
|
+
live seats still in the hand
|
|
69
|
+
all_in seats in the hand with no chips behind
|
|
70
|
+
out eliminated seats, first out first
|
|
71
|
+
actions this hand so far, [cards on board, seat, action, bet this round]
|
|
72
|
+
past finished hands this game: hand, board, shown (cards by seat,
|
|
73
|
+
null unless shown down), won (chips by seat)
|
|
74
|
+
|
|
75
|
+
cards
|
|
52
76
|
stacks chips behind, by seat
|
|
53
77
|
bets chips bet this round, by seat
|
|
54
78
|
pot chips bet this hand
|
|
@@ -77,6 +101,19 @@ game
|
|
|
77
101
|
rating
|
|
78
102
|
Elo, from 1500; each game counts as a win over every bot placed below
|
|
79
103
|
|
|
104
|
+
history
|
|
105
|
+
pb history [--since YYYY-MM-DD] [--until YYYY-MM-DD] [--paper|--live] [--all] [-o dir]
|
|
106
|
+
|
|
107
|
+
One JSON line per finished game, one file per day: history/2026-10-29.jsonl.
|
|
108
|
+
Games appear 10 minutes after they end. Default: your games, last 7 days.
|
|
109
|
+
--all adds everyone's games. You see your own hole cards and any cards
|
|
110
|
+
shown down; folded cards stay hidden. Re-running skips games you have.
|
|
111
|
+
|
|
112
|
+
game id, mode, finished_at, hands_played, you (your seat),
|
|
113
|
+
players [seat, user, place, rating_change, won ($, live)],
|
|
114
|
+
hands [hand, button, small_blind, big_blind, blinds, stacks_start,
|
|
115
|
+
hole, board, actions, shown, won]
|
|
116
|
+
|
|
80
117
|
price
|
|
81
118
|
paper free, 1,000 games/day
|
|
82
119
|
|
|
@@ -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,29 +120,40 @@ 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)
|
|
@@ -131,12 +164,34 @@ class Hand:
|
|
|
131
164
|
share, odd = divmod(pot, len(winners))
|
|
132
165
|
for i, w in enumerate(winners):
|
|
133
166
|
w.stack += share + (i < odd)
|
|
167
|
+
won[w.seat] = won.get(w.seat, 0) + share + (i < odd)
|
|
168
|
+
return won
|
|
169
|
+
|
|
170
|
+
def finish(self, won):
|
|
171
|
+
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 {}
|
|
172
|
+
summary = {
|
|
173
|
+
"hand": self.number,
|
|
174
|
+
"board": list(self.board),
|
|
175
|
+
"shown": per_seat(self.seats, shown),
|
|
176
|
+
"won": per_seat(self.seats, won, 0),
|
|
177
|
+
}
|
|
178
|
+
self.game.past.append(summary)
|
|
179
|
+
self.game.hands.append({
|
|
180
|
+
**summary,
|
|
181
|
+
"button": self.players[self.button].seat,
|
|
182
|
+
"small_blind": self.blind_seats[0],
|
|
183
|
+
"big_blind": self.blind_seats[1],
|
|
184
|
+
"blinds": list(self.blinds),
|
|
185
|
+
"stacks_start": list(self.stacks_start),
|
|
186
|
+
"hole": per_seat(self.seats, {p.seat: list(p.hole) for p in self.players}),
|
|
187
|
+
"actions": [list(a) for a in self.log],
|
|
188
|
+
})
|
|
134
189
|
|
|
135
190
|
|
|
136
|
-
def play(agents, rng=None, max_hands=MAX_HANDS):
|
|
191
|
+
def play(agents, rng=None, max_hands=MAX_HANDS, names=None):
|
|
137
192
|
rng = rng or random.Random()
|
|
138
193
|
table = [Player(seat, agent) for seat, agent in enumerate(agents)]
|
|
139
|
-
|
|
194
|
+
game = Game(list(names or [f"seat {i}" for i in range(len(table))]), max_hands)
|
|
140
195
|
button = rng.randrange(len(table))
|
|
141
196
|
hands = 0
|
|
142
197
|
while hands < max_hands and sum(p.stack > 0 for p in table) > 1:
|
|
@@ -144,8 +199,8 @@ def play(agents, rng=None, max_hands=MAX_HANDS):
|
|
|
144
199
|
alive = [p for p in table if p.stack > 0]
|
|
145
200
|
b = next((i for i, p in enumerate(alive) if p.seat >= button), 0)
|
|
146
201
|
before = {p.seat: p.stack for p in alive}
|
|
147
|
-
Hand(hands, alive, b, rng.sample(DECK, len(DECK)),
|
|
148
|
-
|
|
202
|
+
Hand(hands, alive, b, rng.sample(DECK, len(DECK)), game).play()
|
|
203
|
+
game.out += sorted((p.seat for p in alive if p.stack == 0), key=before.get)
|
|
149
204
|
button = alive[b].seat + 1
|
|
150
205
|
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] +
|
|
206
|
+
return Result([p.seat for p in standing] + game.out[::-1], hands, [p.stack for p in table], game.hands)
|
|
@@ -31,7 +31,8 @@ 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}"
|
|
35
36
|
|
|
36
37
|
|
|
37
38
|
def signed(value, unit=""):
|
|
@@ -24,13 +24,30 @@ def test_side_pots():
|
|
|
24
24
|
stacks = [100, 300, 300]
|
|
25
25
|
players = [engine.Player(seat, shove, stack) for seat, stack in enumerate(stacks)]
|
|
26
26
|
dealt = "Ah Ad Kh Kd Qh Qd 2c 7d 9h Js 4c".split()
|
|
27
|
-
engine.Hand(1, players, 0, dealt[::-1],
|
|
27
|
+
engine.Hand(1, players, 0, dealt[::-1], engine.Game(["a", "b", "c"])).play()
|
|
28
28
|
assert [p.stack for p in players] == [300, 400, 0]
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
def test_heads_up_button_posts_small_blind():
|
|
32
32
|
seen = []
|
|
33
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),
|
|
34
|
+
engine.Hand(3, players, 1, list(engine.DECK), engine.Game(["a", "b"])).play()
|
|
35
35
|
assert seen[0]["seat"] == 1 and seen[0]["bets"] == [6, 3]
|
|
36
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))
|
|
@@ -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
|
|
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
|
|
File without changes
|