"""poker.py: a Texas Hold'em dealer, one bot per room, with play chips, in Python. Each player's two cards go to that player alone; the deck stays with the bot, so there is nothing in the room to read. Members write "!sit" to take a seat and "!deal" to start. Python 3.10+. pip install -r requirements.txt, then python poker.py (reads .env). Its key (dashboard, New API key): rights subscribe and client_events, kind presence, channels: prefix presence-poker-. Players are told apart by the user_id your backend signed into their join token, which the server passes on with each event: {"user_id": "42", "name": "Ada"}. Nothing a player sends says who they are. Play chips only: no real money and nothing that can be cashed in. Playing for money is gambling, and needs a license. """ import asyncio import time from collections import deque from hand import best_hand, shuffled_deck from pikoman_bot import Bot, env ROOM = env("ROOM", "presence-poker-1") START_CHIPS = int(env("START_CHIPS", "1000")) BIG_BLIND = int(env("BIG_BLIND", "20")) SMALL_BLIND = BIG_BLIND // 2 TURN_SECONDS = float(env("TURN_SECONDS", "30")) PAUSE_SECONDS = float(env("PAUSE_SECONDS", "5")) # between two hands MAX_SEATS = 8 STREETS = {"preflop": (3, "flop"), "flop": (1, "turn"), "turn": (1, "river")} # the next street, and its new cards bot = Bot("Dealer", access=3) room = bot.join(ROOM) # A connection may send ten client events a second, so everything waits its turn, nine a second. # The game goes first and the chat lines after it, so a player sees the next turn at once. moves = deque() chat = deque() pumping = None # the task that sends, kept so it is not collected while it runs async def pump(): global pumping while moves or chat: send = moves.popleft() if moves else chat.popleft() await send() await asyncio.sleep(0.11) pumping = None def in_turn(lane, send): global pumping lane.append(send) if pumping is None: pumping = asyncio.create_task(pump()) def tell(event, data): in_turn(moves, lambda: room.send(event, data)) def tell_to(member, event, data): in_turn(moves, lambda: room.send_to(member, event, data)) def say(text): in_turn(chat, lambda: room.send("message", {"text": text})) class Seat: def __init__(self, user, name): self.user, self.name = user, name self.chips = START_CHIPS self.clear() def clear(self): self.bet = 0 # this street self.total = 0 # this hand self.folded = True self.cards = None def pay(self, amount): paid = min(amount, self.chips) self.chips -= paid self.bet += paid self.total += paid class Game: def __init__(self): self.seats = [] self.dealer = -1 self.playing = False # a hand is on self.stage = None self.deck = [] self.board = [] self.hand = 0 self.to_call = 0 # what everyone must have bet on this street self.min_raise = 0 self.to_act = set() # the seats that still have to answer self.turn = -1 self.move = 0 # a move names the one it answers, so a late or doubled click does nothing self.deadline = 0 self.timer = None def live(self): return [s for s in self.seats if not s.folded] def table(self): """What everyone may know, and nothing else: never the deck, never a player's cards.""" turn = self.seats[self.turn].user if self.playing and self.to_act else None return { "hand": self.hand, "stage": self.stage, "board": self.board, "pot": sum(s.total for s in self.seats), "dealer": self.seats[self.dealer].user if 0 <= self.dealer < len(self.seats) else None, "turn": turn, "move": self.move, "toCall": self.to_call, "minRaise": self.to_call + self.min_raise, "seconds": max(0, round(self.deadline - time.monotonic())), "seats": [{"user": s.user, "name": s.name, "chips": s.chips, "bet": s.bet, "folded": s.folded} for s in self.seats], } def later(self, seconds, fn): self.cancel() self.timer = asyncio.get_running_loop().call_later(seconds, fn) def cancel(self): if self.timer: self.timer.cancel() self.timer = None def start_hand(self): # Whoever is out of chips or has left the room loses the seat. The button moves to the next one that stays. here = {str(m.get("user_id")) for m in room.members.values() if isinstance(m, dict)} def stays(s): return s.chips > 0 and s.user in here button = None for i in range(1, len(self.seats) + 1): seat = self.seats[(self.dealer + i) % len(self.seats)] if stays(seat): button = seat break self.seats = [s for s in self.seats if stays(s)] if len(self.seats) < 2: self.playing = False self.stage = None self.board = [] for s in self.seats: s.clear() say("Waiting for two players: write !sit, then !deal.") return tell("table", self.table()) self.dealer = self.seats.index(button) self.playing = True self.hand += 1 self.stage = "preflop" self.deck = shuffled_deck() self.board = [] for s in self.seats: s.clear() s.folded = False s.cards = [self.deck.pop(), self.deck.pop()] # Two players: the button is the small blind and acts first before the flop. count = len(self.seats) small = self.dealer if count == 2 else (self.dealer + 1) % count big = (self.dealer + (1 if count == 2 else 2)) % count self.seats[small].pay(SMALL_BLIND) self.seats[big].pay(BIG_BLIND) self.to_call = BIG_BLIND self.min_raise = BIG_BLIND self.to_act = {s for s in self.seats if s.chips > 0} self.turn = big for s in self.seats: tell_to({"user_id": s.user}, "cards", {"hand": self.hand, "cards": s.cards}) say(f"Hand {self.hand}: {self.seats[self.dealer].name} has the button.") self.advance() def advance(self): """The next thing that happens: the next player's turn, the next street, or the end of the hand.""" self.cancel() if len(self.live()) == 1: return self.finish() self.to_act = {s for s in self.to_act if s.chips > 0 and not s.folded} if not self.to_act: return self.next_street() self.turn = (self.turn + 1) % len(self.seats) while self.seats[self.turn] not in self.to_act: self.turn = (self.turn + 1) % len(self.seats) self.move += 1 self.deadline = time.monotonic() + TURN_SECONDS # The bot's clock decides. A player who is gone or asleep checks if they can, and otherwise folds. seat = self.seats[self.turn] def out_of_time(): say(f"{seat.name} ran out of time.") self.apply(seat, "check" if seat.bet == self.to_call else "fold") self.later(TURN_SECONDS, out_of_time) tell("table", self.table()) def next_street(self): for s in self.seats: s.bet = 0 self.to_call = 0 self.min_raise = BIG_BLIND if self.stage not in STREETS: return self.finish() cards, self.stage = STREETS[self.stage] self.board = self.board + [self.deck.pop() for _ in range(cards)] self.turn = self.dealer self.to_act = {s for s in self.live() if s.chips > 0} # At most one player can still bet: nobody to bet against, so the rest of the board just comes. if len(self.to_act) < 2: self.to_act = set() tell("table", self.table()) return self.later(1.5, self.next_street) self.advance() def apply(self, seat, action, amount=0): self.cancel() if action == "fold": seat.folded = True say(f"{seat.name} folds.") elif action == "check": say(f"{seat.name} checks.") elif action == "call": seat.pay(self.to_call - seat.bet) say(f"{seat.name} calls.") else: # A raise is the total to bet this street. A smaller one than min_raise is fine only all in. if amount - self.to_call >= self.min_raise: self.min_raise = amount - self.to_call seat.pay(amount - seat.bet) self.to_call = amount self.to_act = {s for s in self.live() if s.chips > 0} say(f"{seat.name} raises to {amount}.") self.to_act.discard(seat) self.advance() def finish(self): """The pots, then the cards of those still in. Folded hands stay hidden, and so does a hand nobody called.""" self.cancel() players = self.live() showdown = len(players) > 1 best = {s: best_hand(s.cards + self.board) if len(self.board) == 5 else (None, "") for s in players} def after_button(s): return (self.seats.index(s) - self.dealer - 1) % len(self.seats) # One pot per all-in size: a player wins only as much from each other player as they put in. won = {} levels = sorted({s.total for s in players}) below = 0 for i, level in enumerate(levels): top = float("inf") if i == len(levels) - 1 else level # the last pot takes what folded players left above it pot = sum(min(s.total, top) - min(s.total, below) for s in self.seats) below = level entitled = [s for s in players if s.total >= level] if showdown: high = max(best[s][0] for s in entitled) winners = [s for s in entitled if best[s][0] == high] else: winners = entitled # A split pot's odd chips go to the first winner after the button, one each. winners.sort(key=after_button) for n, s in enumerate(winners): won[s] = won.get(s, 0) + pot // len(winners) + (1 if n < pot % len(winners) else 0) for s, chips in won.items(): s.chips += chips tell("showdown", { "hand": self.hand, "board": self.board, "players": [{"user": s.user, "name": s.name, "cards": s.cards, "hand": best[s][1]} for s in players] if showdown else [], "winners": [{"user": s.user, "name": s.name, "won": chips} for s, chips in won.items()], }) for s, chips in won.items(): say(f"{s.name} wins {chips} with {best[s][1]}." if showdown else f"{s.name} wins {chips}.") for s in self.seats: s.clear() self.to_act = set() self.to_call = 0 self.stage = "between" tell("table", self.table()) self.later(PAUSE_SECONDS, self.start_hand) game = Game() # "!sit" and "!deal" in the room. A seat taken during a hand plays from the next one. @room.on("message") def message(sender, data, info): text = str((data or {}).get("text", "")).strip() if text not in ("!sit", "!deal") or room.is_bot(sender): return user = info.get("user_id") if user is None: return say("Your join token needs a user_id to play.") seated = next((s for s in game.seats if s.user == user), None) if text == "!sit": if seated: return if len(game.seats) >= MAX_SEATS: return say("The table is full.") name = str(room.member(sender).get("name", "Someone"))[:40] game.seats.append(Seat(user, name)) say(f"{name} sits down with {START_CHIPS} chips.") tell("table", game.table()) elif seated and not game.playing: game.start_hand() # A move, sent to the bot alone: room.sendTo(bot, "move", { move: 7, action: "raise", amount: 60 }). @room.on("move") def move(sender, data, info): if not game.playing or not game.to_act or not isinstance(data, dict): return seat = game.seats[game.turn] if seat not in game.to_act or info.get("user_id") != seat.user or data.get("move") != game.move: return action, amount = data.get("action"), data.get("amount") most = seat.bet + seat.chips if action == "fold": game.apply(seat, "fold") elif action == "check" and seat.bet == game.to_call: game.apply(seat, "check") elif action == "call" and seat.bet < game.to_call: game.apply(seat, "call") elif action == "raise" and type(amount) is int and game.to_call < amount <= most: if amount - game.to_call >= game.min_raise or amount == most: game.apply(seat, "raise", amount) # Someone joins, or comes back with a new page: the table, and their own cards if they have any. @room.on_join def joined(connection, data): if not game.playing and not game.seats: return tell_to(connection, "table", game.table()) user = str(data.get("user_id")) if isinstance(data, dict) else None seat = next((s for s in game.seats if s.user == user), None) if seat and seat.cards: tell_to(connection, "cards", {"hand": game.hand, "cards": seat.cards}) print(f"Dealer in {ROOM}: {START_CHIPS} chips, blinds {SMALL_BLIND}/{BIG_BLIND}") bot.start()