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.
Files changed (28) hide show
  1. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/PKG-INFO +1 -1
  2. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/pyproject.toml +1 -1
  3. pokerbots_dev-0.1.5/src/pokerbots_cli/commands/__init__.py +3 -0
  4. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/src/pokerbots_cli/commands/account.py +36 -6
  5. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/src/pokerbots_cli/commands/admin.py +1 -1
  6. pokerbots_dev-0.1.5/src/pokerbots_cli/commands/history.py +53 -0
  7. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/src/pokerbots_cli/docs.md +43 -6
  8. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/src/pokerbots_cli/engine.py +66 -11
  9. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/src/pokerbots_cli/fmt.py +2 -1
  10. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/tests/test_engine.py +19 -2
  11. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/tests/test_wallet.py +1 -1
  12. pokerbots_dev-0.1.4/src/pokerbots_cli/commands/__init__.py +0 -3
  13. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/.gitignore +0 -0
  14. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/README.md +0 -0
  15. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/src/pokerbots_cli/__init__.py +0 -0
  16. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/src/pokerbots_cli/agent.py +0 -0
  17. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/src/pokerbots_cli/api.py +0 -0
  18. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/src/pokerbots_cli/auth.py +0 -0
  19. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/src/pokerbots_cli/cards.py +0 -0
  20. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/src/pokerbots_cli/commands/local.py +0 -0
  21. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/src/pokerbots_cli/commands/money.py +0 -0
  22. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/src/pokerbots_cli/house.py +0 -0
  23. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/src/pokerbots_cli/main.py +0 -0
  24. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/src/pokerbots_cli/template/bot.py +0 -0
  25. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/src/pokerbots_cli/wallet.py +0 -0
  26. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/tests/test_agent.py +0 -0
  27. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/tests/test_cards.py +0 -0
  28. {pokerbots_dev-0.1.4 → pokerbots_dev-0.1.5}/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.5
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.5"
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)
@@ -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 status(args):
80
- print(report())
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 watch(args):
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 report().split("\n")), "", " " + "ctrl-c to quit"]
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", 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)
@@ -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 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
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, 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,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
- busted = []
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)), len(table)).play()
148
- busted += sorted((p.seat for p in alive if p.stack == 0), key=before.get)
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] + busted[::-1], hands, [p.stack for p in table])
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
- 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}"
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], 3).play()
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), 2).play()
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
@@ -1,3 +0,0 @@
1
- from . import account, admin, local, money
2
-
3
- MODULES = (account, local, money, admin)
File without changes
File without changes