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.
Files changed (30) hide show
  1. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/PKG-INFO +1 -1
  2. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/pyproject.toml +1 -1
  3. pokerbots_dev-0.1.6/src/pokerbots_cli/commands/__init__.py +3 -0
  4. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/src/pokerbots_cli/commands/account.py +55 -9
  5. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/src/pokerbots_cli/commands/admin.py +1 -1
  6. pokerbots_dev-0.1.6/src/pokerbots_cli/commands/history.py +53 -0
  7. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/src/pokerbots_cli/commands/local.py +14 -0
  8. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/src/pokerbots_cli/commands/money.py +6 -4
  9. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/src/pokerbots_cli/docs.md +44 -6
  10. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/src/pokerbots_cli/engine.py +68 -12
  11. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/src/pokerbots_cli/fmt.py +12 -1
  12. pokerbots_dev-0.1.6/src/pokerbots_cli/house.py +47 -0
  13. pokerbots_dev-0.1.6/tests/test_engine.py +59 -0
  14. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/tests/test_wallet.py +1 -1
  15. pokerbots_dev-0.1.4/src/pokerbots_cli/commands/__init__.py +0 -3
  16. pokerbots_dev-0.1.4/src/pokerbots_cli/house.py +0 -19
  17. pokerbots_dev-0.1.4/tests/test_engine.py +0 -36
  18. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/.gitignore +0 -0
  19. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/README.md +0 -0
  20. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/src/pokerbots_cli/__init__.py +0 -0
  21. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/src/pokerbots_cli/agent.py +0 -0
  22. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/src/pokerbots_cli/api.py +0 -0
  23. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/src/pokerbots_cli/auth.py +0 -0
  24. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/src/pokerbots_cli/cards.py +0 -0
  25. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/src/pokerbots_cli/main.py +0 -0
  26. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/src/pokerbots_cli/template/bot.py +0 -0
  27. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/src/pokerbots_cli/wallet.py +0 -0
  28. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/tests/test_agent.py +0 -0
  29. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/tests/test_cards.py +0 -0
  30. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.6}/tests/test_cli.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pokerbots-dev
3
- Version: 0.1.4
3
+ Version: 0.1.6
4
4
  Summary: pb: command-line client for pokerbots.dev
5
5
  Project-URL: Homepage, https://pokerbots.dev
6
6
  Project-URL: Source, https://github.com/pokerbots-dev/pokerbots
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pokerbots-dev"
7
- version = "0.1.4"
7
+ version = "0.1.6"
8
8
  description = "pb: command-line client for pokerbots.dev"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1,3 @@
1
+ from . import account, admin, history, local, money
2
+
3
+ MODULES = (account, history, local, money, admin)
@@ -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
- if input(ATTEST).strip().lower() != "y":
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["balance"])], ["wallet", me["wallet"] or "-"]]
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 status(args):
80
- print(report())
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 watch(args):
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 report().split("\n")), "", " " + "ctrl-c to quit"]
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", help="operator commands")
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
- rows = [["address", address], ["balance", money(me["balance"])]]
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
- rows.append(["withdraws to", me["withdraw_to"]])
22
- print(table(rows, labels=True))
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 hand number, from 1
47
- seat your seat, 0-5
48
- button button seat
49
- blinds [small, big]
50
- hole your two cards, e.g. ["As", "Td"]
51
- board shared cards
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, seats):
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.seats = seats
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
- eligible = [p for p in live if p.committed >= level]
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
- busted = []
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)), len(table)).play()
148
- busted += sorted((p.seat for p in alive if p.stack == 0), key=before.get)
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] + busted[::-1], hands, [p.stack for p in table])
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
- return f"{cents}¢" if abs(cents) < 100 else f"${cents / 100:,.2f}"
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,3 +0,0 @@
1
- from . import account, admin, local, money
2
-
3
- MODULES = (account, local, money, admin)
@@ -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