├── bot ├── __init__.py ├── card_display.py ├── gpt_player.py └── bot_poker_handler.py ├── config ├── __init__.py ├── .env_template ├── config.py └── log_config.py ├── db ├── __init__.py ├── enums.py ├── models.py └── db_utils.py ├── game ├── __init__.py ├── deck.py ├── player.py ├── card.py └── poker.py ├── docs ├── command_images │ ├── info.png │ ├── KA_play.png │ ├── player_stats.png │ ├── server_stats.png │ ├── play_poker_preview.gif │ ├── player_leaderboard.png │ └── server_leaderboard.png └── split_deck_images │ ├── red_2_top.png │ ├── red_3_top.png │ ├── red_4_top.png │ ├── red_5_top.png │ ├── red_6_top.png │ ├── red_7_top.png │ ├── red_8_top.png │ ├── red_9_top.png │ ├── black_2_top.png │ ├── black_3_top.png │ ├── black_4_top.png │ ├── black_5_top.png │ ├── black_6_top.png │ ├── black_7_top.png │ ├── black_8_top.png │ ├── black_9_top.png │ ├── club_bottom.png │ ├── red_10_top.png │ ├── red_ace_top.png │ ├── black_10_top.png │ ├── black_ace_top.png │ ├── black_jack_top.png │ ├── black_king_top.png │ ├── diamond_bottom.png │ ├── heart_bottom.png │ ├── red_jack_top.png │ ├── red_king_top.png │ ├── red_queen_top.png │ ├── spade_bottom.png │ └── black_queen_top.png ├── .gitignore ├── init_db.py ├── LICENSE ├── tests ├── test_handrank.py ├── test_card.py ├── test_deck.py ├── test_player.py ├── test_poker_logic.py ├── test_poker_gameplay.py ├── test_db_manager.py ├── test_hand_evaluation.py ├── test_gpt_player.py └── test_bot_commands.py ├── requirements.txt ├── README.md └── run.py /bot/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /db/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /game/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/command_images/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteo-psnt/PokerGPT/HEAD/docs/command_images/info.png -------------------------------------------------------------------------------- /docs/command_images/KA_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteo-psnt/PokerGPT/HEAD/docs/command_images/KA_play.png -------------------------------------------------------------------------------- /docs/command_images/player_stats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteo-psnt/PokerGPT/HEAD/docs/command_images/player_stats.png -------------------------------------------------------------------------------- /docs/command_images/server_stats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteo-psnt/PokerGPT/HEAD/docs/command_images/server_stats.png -------------------------------------------------------------------------------- /docs/split_deck_images/red_2_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteo-psnt/PokerGPT/HEAD/docs/split_deck_images/red_2_top.png -------------------------------------------------------------------------------- /docs/split_deck_images/red_3_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteo-psnt/PokerGPT/HEAD/docs/split_deck_images/red_3_top.png -------------------------------------------------------------------------------- /docs/split_deck_images/red_4_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteo-psnt/PokerGPT/HEAD/docs/split_deck_images/red_4_top.png -------------------------------------------------------------------------------- /docs/split_deck_images/red_5_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteo-psnt/PokerGPT/HEAD/docs/split_deck_images/red_5_top.png -------------------------------------------------------------------------------- /docs/split_deck_images/red_6_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteo-psnt/PokerGPT/HEAD/docs/split_deck_images/red_6_top.png -------------------------------------------------------------------------------- /docs/split_deck_images/red_7_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteo-psnt/PokerGPT/HEAD/docs/split_deck_images/red_7_top.png -------------------------------------------------------------------------------- /docs/split_deck_images/red_8_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteo-psnt/PokerGPT/HEAD/docs/split_deck_images/red_8_top.png -------------------------------------------------------------------------------- /docs/split_deck_images/red_9_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteo-psnt/PokerGPT/HEAD/docs/split_deck_images/red_9_top.png -------------------------------------------------------------------------------- /docs/split_deck_images/black_2_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteo-psnt/PokerGPT/HEAD/docs/split_deck_images/black_2_top.png -------------------------------------------------------------------------------- /docs/split_deck_images/black_3_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteo-psnt/PokerGPT/HEAD/docs/split_deck_images/black_3_top.png -------------------------------------------------------------------------------- /docs/split_deck_images/black_4_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteo-psnt/PokerGPT/HEAD/docs/split_deck_images/black_4_top.png -------------------------------------------------------------------------------- /docs/split_deck_images/black_5_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteo-psnt/PokerGPT/HEAD/docs/split_deck_images/black_5_top.png -------------------------------------------------------------------------------- /docs/split_deck_images/black_6_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteo-psnt/PokerGPT/HEAD/docs/split_deck_images/black_6_top.png -------------------------------------------------------------------------------- /docs/split_deck_images/black_7_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteo-psnt/PokerGPT/HEAD/docs/split_deck_images/black_7_top.png -------------------------------------------------------------------------------- /docs/split_deck_images/black_8_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteo-psnt/PokerGPT/HEAD/docs/split_deck_images/black_8_top.png -------------------------------------------------------------------------------- /docs/split_deck_images/black_9_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteo-psnt/PokerGPT/HEAD/docs/split_deck_images/black_9_top.png -------------------------------------------------------------------------------- /docs/split_deck_images/club_bottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteo-psnt/PokerGPT/HEAD/docs/split_deck_images/club_bottom.png -------------------------------------------------------------------------------- /docs/split_deck_images/red_10_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteo-psnt/PokerGPT/HEAD/docs/split_deck_images/red_10_top.png -------------------------------------------------------------------------------- /docs/split_deck_images/red_ace_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteo-psnt/PokerGPT/HEAD/docs/split_deck_images/red_ace_top.png -------------------------------------------------------------------------------- /docs/split_deck_images/black_10_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteo-psnt/PokerGPT/HEAD/docs/split_deck_images/black_10_top.png -------------------------------------------------------------------------------- /docs/split_deck_images/black_ace_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteo-psnt/PokerGPT/HEAD/docs/split_deck_images/black_ace_top.png -------------------------------------------------------------------------------- /docs/split_deck_images/black_jack_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteo-psnt/PokerGPT/HEAD/docs/split_deck_images/black_jack_top.png -------------------------------------------------------------------------------- /docs/split_deck_images/black_king_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteo-psnt/PokerGPT/HEAD/docs/split_deck_images/black_king_top.png -------------------------------------------------------------------------------- /docs/split_deck_images/diamond_bottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteo-psnt/PokerGPT/HEAD/docs/split_deck_images/diamond_bottom.png -------------------------------------------------------------------------------- /docs/split_deck_images/heart_bottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteo-psnt/PokerGPT/HEAD/docs/split_deck_images/heart_bottom.png -------------------------------------------------------------------------------- /docs/split_deck_images/red_jack_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteo-psnt/PokerGPT/HEAD/docs/split_deck_images/red_jack_top.png -------------------------------------------------------------------------------- /docs/split_deck_images/red_king_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteo-psnt/PokerGPT/HEAD/docs/split_deck_images/red_king_top.png -------------------------------------------------------------------------------- /docs/split_deck_images/red_queen_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteo-psnt/PokerGPT/HEAD/docs/split_deck_images/red_queen_top.png -------------------------------------------------------------------------------- /docs/split_deck_images/spade_bottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteo-psnt/PokerGPT/HEAD/docs/split_deck_images/spade_bottom.png -------------------------------------------------------------------------------- /docs/command_images/play_poker_preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteo-psnt/PokerGPT/HEAD/docs/command_images/play_poker_preview.gif -------------------------------------------------------------------------------- /docs/command_images/player_leaderboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteo-psnt/PokerGPT/HEAD/docs/command_images/player_leaderboard.png -------------------------------------------------------------------------------- /docs/command_images/server_leaderboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteo-psnt/PokerGPT/HEAD/docs/command_images/server_leaderboard.png -------------------------------------------------------------------------------- /docs/split_deck_images/black_queen_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matteo-psnt/PokerGPT/HEAD/docs/split_deck_images/black_queen_top.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | .env 3 | .vscode 4 | .idea 5 | .venv 6 | __pycache__ 7 | bot.log 8 | discord.log 9 | run_bot_dev.py 10 | scripts/* 11 | -------------------------------------------------------------------------------- /game/deck.py: -------------------------------------------------------------------------------- 1 | from game.card import * 2 | import random 3 | 4 | class Deck: 5 | def __init__(self): 6 | self.cards = [Card(rank, suit) for suit in Suit for rank in Rank] 7 | self.shuffle() 8 | 9 | def shuffle(self): 10 | random.shuffle(self.cards) 11 | 12 | def deal_card(self): 13 | return self.cards.pop() -------------------------------------------------------------------------------- /config/.env_template: -------------------------------------------------------------------------------- 1 | # API keys 2 | DISCORD_TOKEN=your_discord_bot_token 3 | OPENAI_API_KEY=your_openai_api_key 4 | 5 | # Database configuration (optional) 6 | # Remove the comment markers(#) from the block below and replace the placeholders with your actual values. 7 | # Don't replace pokerGPTdatabase 8 | 9 | # DB_HOST=your_database_host 10 | # DB_USER=your_database_user 11 | # DB_PASSWORD=your_database_password 12 | # DB_NAME=pokerGPTdatabase -------------------------------------------------------------------------------- /init_db.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from config.config import DB_HOST, DB_USER, DB_PASSWORD, DB_DATABASE 3 | from db.models import Base 4 | 5 | DATABASE_URL = f"mysql+mysqlconnector://{DB_USER}:{DB_PASSWORD}@{DB_HOST}/{DB_DATABASE}" 6 | 7 | def init_db() -> None: 8 | engine = create_engine(DATABASE_URL, echo=True) 9 | Base.metadata.create_all(engine) 10 | print("Database initialized successfully!") 11 | 12 | if __name__ == "__main__": 13 | init_db() 14 | -------------------------------------------------------------------------------- /config/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | 4 | load_dotenv() 5 | 6 | OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") 7 | TOKEN = os.getenv("DISCORD_TOKEN") 8 | DEV_TOKEN = os.getenv("DISCORD_DEV_TOKEN") 9 | 10 | # Optional DB config 11 | DB_HOST = os.getenv("DB_HOST") 12 | DB_USER = os.getenv("DB_USER") 13 | DB_PASSWORD = os.getenv("DB_PASSWORD") 14 | DB_DATABASE = os.getenv("DB_NAME") 15 | 16 | DATABASE_EXISTS = all([DB_HOST, DB_USER, DB_PASSWORD, DB_DATABASE]) 17 | -------------------------------------------------------------------------------- /config/log_config.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | # Set up a logger 4 | logger = logging.getLogger('my_app') 5 | logger.setLevel(logging.INFO) 6 | file_handler = logging.FileHandler('bot.log') 7 | file_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) 8 | logger.addHandler(file_handler) 9 | 10 | # Set up a separate logger for the Discord library 11 | discord_logger = logging.getLogger('discord') 12 | discord_logger.setLevel(logging.INFO) 13 | discord_file_handler = logging.FileHandler('discord.log') 14 | discord_file_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) 15 | discord_logger.addHandler(discord_file_handler) -------------------------------------------------------------------------------- /db/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | class GameResult(Enum): 4 | COMPLETE_WIN = "complete win" 5 | WIN = "win" 6 | COMPLETE_LOSS = "complete loss" 7 | LOSS = "loss" 8 | DRAW = "draw" 9 | IN_PROGRESS = "in progress" 10 | 11 | class HandResult(Enum): 12 | WIN = "win" 13 | LOSS = "loss" 14 | SPLIT_POT = "split pot" 15 | IN_PROGRESS = "in progress" 16 | 17 | class Round(Enum): 18 | PRE_FLOP = "pre-flop" 19 | FLOP = "flop" 20 | TURN = "turn" 21 | RIVER = "river" 22 | SHOWDOWN = "showdown" 23 | IN_PROGRESS = "in progress" 24 | 25 | class ActionType(Enum): 26 | CALL = "call" 27 | CHECK = "check" 28 | FOLD = "fold" 29 | RAISE = "raise" 30 | ALL_IN = "all-in" 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Matteo Pesenti 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/test_handrank.py: -------------------------------------------------------------------------------- 1 | from game.player import handRank 2 | 3 | def test_hand_rank_ordering(): 4 | # Test ordering of hand ranks 5 | assert handRank.HIGH_CARD < handRank.PAIR 6 | assert handRank.PAIR < handRank.TWO_PAIR 7 | assert handRank.TWO_PAIR < handRank.THREE_OF_A_KIND 8 | assert handRank.THREE_OF_A_KIND < handRank.STRAIGHT 9 | assert handRank.STRAIGHT < handRank.FLUSH 10 | assert handRank.FLUSH < handRank.FULL_HOUSE 11 | assert handRank.FULL_HOUSE < handRank.FOUR_OF_A_KIND 12 | assert handRank.FOUR_OF_A_KIND < handRank.STRAIGHT_FLUSH 13 | assert handRank.STRAIGHT_FLUSH < handRank.ROYAL_FLUSH 14 | 15 | def test_hand_rank_equality(): 16 | # Test equality comparison 17 | assert handRank.PAIR == handRank.PAIR 18 | assert handRank.STRAIGHT == handRank.STRAIGHT 19 | assert handRank.ROYAL_FLUSH == handRank.ROYAL_FLUSH 20 | 21 | # Test inequality 22 | assert handRank.PAIR != handRank.STRAIGHT 23 | assert handRank.FLUSH != handRank.FULL_HOUSE 24 | 25 | def test_hand_rank_string_representation(): 26 | # Test string representation 27 | assert str(handRank.HIGH_CARD) == "High Card" 28 | assert str(handRank.TWO_PAIR) == "Two Pair" 29 | assert str(handRank.STRAIGHT_FLUSH) == "Straight Flush" 30 | assert str(handRank.ROYAL_FLUSH) == "Royal Flush" -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.8.5 2 | aiosignal==1.3.1 3 | annotated-types==0.5.0 4 | anyio==4.2.0 5 | argon2-cffi-bindings==21.2.0 6 | arrow==1.3.0 7 | async-timeout==4.0.3 8 | attrs==23.1.0 9 | certifi==2023.7.22 10 | cffi==1.16.0 11 | charset-normalizer==3.2.0 12 | dataclasses-json==0.5.14 13 | distro==1.9.0 14 | frozenlist==1.4.0 15 | h11==0.14.0 16 | httpcore==1.0.2 17 | httpx==0.26.0 18 | idna==3.4 19 | iniconfig==2.1.0 20 | jsonpatch==1.33 21 | jsonpointer==2.4 22 | jsonschema==4.21.1 23 | jsonschema-specifications==2023.12.1 24 | langchain==0.1.1 25 | langchain-community==0.0.13 26 | langchain-core==0.1.13 27 | langchain-openai==0.0.3 28 | langsmith==0.0.83 29 | marshmallow==3.20.1 30 | more-itertools==10.6.0 31 | multidict==6.0.4 32 | mypy==1.15.0 33 | mypy-extensions==1.0.0 34 | mysql-connector-python==8.1.0 35 | numpy==1.25.2 36 | openai==1.8.0 37 | overrides==7.6.0 38 | packaging==23.2 39 | pluggy==1.5.0 40 | protobuf==4.21.12 41 | ptyprocess==0.7.0 42 | py-cord==2.4.1 43 | pycparser==2.21 44 | pydantic==2.3.0 45 | pydantic_core==2.6.3 46 | pytest==8.3.5 47 | pytest-asyncio==0.26.0 48 | python-dateutil==2.8.2 49 | python-dotenv==1.0.0 50 | python-json-logger==2.0.7 51 | PyYAML==6.0.1 52 | referencing==0.32.1 53 | regex==2023.12.25 54 | requests==2.31.0 55 | rfc3339-validator==0.1.4 56 | rfc3986-validator==0.1.1 57 | rpds-py==0.17.1 58 | six==1.16.0 59 | sniffio==1.3.0 60 | SQLAlchemy==2.0.20 61 | tenacity==8.2.3 62 | terminado==0.18.0 63 | tiktoken==0.5.2 64 | tornado==6.4 65 | tqdm==4.66.1 66 | traitlets==5.14.1 67 | typeguard==4.4.2 68 | types-python-dateutil==2.8.19.20240106 69 | typing-inspect==0.9.0 70 | typing_extensions==4.13.2 71 | urllib3==2.0.4 72 | yarl==1.9.2 73 | -------------------------------------------------------------------------------- /game/player.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | from game.deck import * 3 | 4 | 5 | class handRank(Enum): 6 | HIGH_CARD, PAIR, TWO_PAIR, THREE_OF_A_KIND, STRAIGHT, FLUSH, FULL_HOUSE, FOUR_OF_A_KIND, STRAIGHT_FLUSH, ROYAL_FLUSH = range(10) 7 | 8 | def __str__(self) -> str: 9 | return self.name.replace("_", " ").title() 10 | 11 | def __lt__(self, other): 12 | return self.value < other.value 13 | 14 | def __gt__(self, other): 15 | return self.value > other.value 16 | 17 | def __eq__(self, other): 18 | return self.value == other.value 19 | 20 | 21 | class Player: 22 | def __init__(self, player_name: str, buy_in: int): 23 | self.player_name = player_name 24 | self.stack = buy_in 25 | self.round_pot_commitment = 0 26 | self.card1 = None 27 | self.card2 = None 28 | self.hand_rank = handRank.HIGH_CARD 29 | self.hand_played = [] 30 | 31 | def print_hand(self): 32 | print(f"{self.card1}, {self.card2}") 33 | 34 | def return_hand(self): 35 | return [self.card1, self.card2] 36 | 37 | def return_long_hand(self): 38 | if self.card1 is not None and self.card2 is not None: 39 | return f"{self.card1.long_str()}, {self.card2.long_str()}" 40 | 41 | def deal_hand(self, deck: Deck): 42 | self.card1 = deck.deal_card() 43 | self.card2 = deck.deal_card() 44 | 45 | def bet(self, amount: int): 46 | self.stack -= amount 47 | self.round_pot_commitment += amount 48 | 49 | def reset(self): 50 | self.card1 = None 51 | self.card2 = None 52 | self.round_pot_commitment = 0 53 | self.hand_rank = handRank.HIGH_CARD 54 | self.hand_played = [] -------------------------------------------------------------------------------- /tests/test_card.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from game.card import Card, Rank, Suit 3 | 4 | def test_card_creation(): 5 | card = Card(Rank.ACE, Suit.SPADES) 6 | assert card.rank == Rank.ACE 7 | assert card.suit == Suit.SPADES 8 | 9 | def test_card_comparison(): 10 | ace_spades = Card(Rank.ACE, Suit.SPADES) 11 | king_hearts = Card(Rank.KING, Suit.HEARTS) 12 | 13 | # Test equality 14 | assert ace_spades == ace_spades 15 | assert ace_spades != king_hearts 16 | 17 | # Test greater than 18 | assert ace_spades > king_hearts 19 | assert not king_hearts > ace_spades 20 | 21 | # Test less than 22 | assert king_hearts < ace_spades 23 | assert not ace_spades < king_hearts 24 | 25 | # Test comparison with Rank 26 | assert ace_spades == Rank.ACE 27 | assert ace_spades != Rank.KING 28 | 29 | # Test comparison with int 30 | assert ace_spades == 0 # ACE is 0 in enum 31 | 32 | def test_card_add(): 33 | ace_spades = Card(Rank.ACE, Suit.SPADES) 34 | 35 | # Test adding int 36 | assert ace_spades + 1 == Rank.TWO 37 | assert ace_spades + 12 == Rank.KING 38 | 39 | # Test overflow 40 | assert ace_spades + 13 == Rank.ACE 41 | 42 | def test_card_string_representation(): 43 | ace_spades = Card(Rank.ACE, Suit.SPADES) 44 | ten_hearts = Card(Rank.TEN, Suit.HEARTS) 45 | 46 | assert str(ace_spades) == "AS" 47 | assert str(ten_hearts) == "10H" 48 | 49 | assert ace_spades.long_str() == "Ace of Spades" 50 | assert ten_hearts.long_str() == "10 of Hearts" 51 | 52 | def test_invalid_comparison(): 53 | card = Card(Rank.ACE, Suit.SPADES) 54 | with pytest.raises(TypeError): 55 | card == "AS" 56 | 57 | def test_card_add_wraps_mod_13(): 58 | base = Card(Rank.ACE, Suit.SPADES) 59 | for k in range(26): # Test values from 0 to 25 60 | result_rank = base + k 61 | expected_value = (base.rank.value + k) % 13 62 | assert result_rank.value == expected_value -------------------------------------------------------------------------------- /game/card.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | class Rank(Enum): 4 | ACE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE, TEN, JACK, QUEEN, KING = range(13) 5 | 6 | class Suit(Enum): 7 | SPADES, HEARTS, DIAMONDS, CLUBS = range(4) 8 | 9 | class Card: 10 | def __init__(self, rank: Rank, suit: Suit): 11 | self.rank = rank 12 | self.suit = suit 13 | 14 | def __eq__(self, other): 15 | if isinstance(other, Card): 16 | return self.rank == other.rank and self.suit == other.suit 17 | elif isinstance(other, Rank): 18 | return self.rank == other 19 | elif isinstance(other, int): 20 | return self.rank.value == other 21 | raise TypeError(f"Cannot compare Card with {type(other)}") 22 | 23 | def __lt__(self, other): 24 | if self.rank == Rank.ACE and other.rank != Rank.ACE: 25 | return False 26 | elif self.rank != Rank.ACE and other.rank == Rank.ACE: 27 | return True 28 | return self.rank.value < other.rank.value 29 | 30 | def __gt__(self, other): 31 | if self.rank == Rank.ACE and other.rank != Rank.ACE: 32 | return True 33 | elif self.rank != Rank.ACE and other.rank == Rank.ACE: 34 | return False 35 | return self.rank.value > other.rank.value 36 | 37 | # A bit dubious to use this method, but it works 38 | def __add__(self, other): 39 | if isinstance(other, Card): 40 | new_rank = self.rank.value + other.rank.value 41 | elif isinstance(other, Rank): 42 | new_rank = self.rank.value + other.value 43 | elif isinstance(other, int): 44 | new_rank = self.rank.value + other 45 | else: 46 | raise TypeError(f"Cannot add Card with {type(other)}") 47 | 48 | if new_rank > 12: 49 | new_rank -= 13 50 | return Rank(new_rank) 51 | 52 | def __str__(self): 53 | suits = ["S", "H", "D", "C"] 54 | ranks = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"] 55 | return f"{ranks[self.rank.value]}{suits[self.suit.value]}" 56 | 57 | def long_str(self): 58 | suits = ["Spades", "Hearts", "Dimonds", "Clubs"] 59 | ranks = ["Ace", "2", "3", "4", "5", "6", "7", "8", "9", "10", "Jack", "Queen", "King"] 60 | return f"{ranks[self.rank.value]} of {suits[self.suit.value]}" 61 | 62 | def __repr__(self): 63 | return self.__str__() -------------------------------------------------------------------------------- /tests/test_deck.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from game.deck import Deck 3 | from game.card import Card, Rank, Suit 4 | 5 | def test_deck_creation(): 6 | deck = Deck() 7 | # A standard deck has 52 cards 8 | assert len(deck.cards) == 52 9 | 10 | # Check that all cards are unique 11 | card_strs = [str(card) for card in deck.cards] 12 | assert len(card_strs) == len(set(card_strs)) 13 | 14 | # Verify all ranks and suits exist in the deck 15 | ranks_in_deck = {card.rank for card in deck.cards} 16 | suits_in_deck = {card.suit for card in deck.cards} 17 | 18 | assert len(ranks_in_deck) == len(Rank) 19 | assert len(suits_in_deck) == len(Suit) 20 | 21 | for rank in Rank: 22 | assert rank in ranks_in_deck 23 | 24 | for suit in Suit: 25 | assert suit in suits_in_deck 26 | 27 | def test_deck_shuffle(): 28 | deck1 = Deck() 29 | # Make a copy of the original card order 30 | original_order = deck1.cards.copy() 31 | 32 | # Shuffle the deck 33 | deck1.shuffle() 34 | 35 | # The deck should still have 52 cards 36 | assert len(deck1.cards) == 52 37 | 38 | # The order should be different (this could theoretically fail if the shuffle 39 | # returns the same order, but it's extremely unlikely) 40 | assert deck1.cards != original_order 41 | 42 | # Verify we have the same cards, just in different order 43 | assert sorted(str(c) for c in deck1.cards) == sorted(str(c) for c in original_order) 44 | 45 | def test_deal_card(): 46 | deck = Deck() 47 | original_count = len(deck.cards) 48 | 49 | # Deal a card 50 | card = deck.deal_card() 51 | 52 | # Verify card is a Card object 53 | assert isinstance(card, Card) 54 | 55 | # Deck should have one less card 56 | assert len(deck.cards) == original_count - 1 57 | 58 | # The dealt card should not be in the deck anymore 59 | assert card not in deck.cards 60 | 61 | def test_deck_exhaustion(): 62 | deck = Deck() 63 | for _ in range(52): 64 | deck.deal_card() 65 | with pytest.raises(IndexError): 66 | deck.deal_card() # Expecting IndexError when trying to deal from an empty deck 67 | 68 | def test_shuffle_randomness(pytestconfig): 69 | trials, changes = 1_000, 0 70 | original = Deck().cards[0] 71 | for _ in range(trials): 72 | deck = Deck() 73 | deck.shuffle() 74 | if deck.cards[0] != original: 75 | changes += 1 76 | assert changes >= 950 # 95 % threshold -------------------------------------------------------------------------------- /tests/test_player.py: -------------------------------------------------------------------------------- 1 | from game.player import Player, handRank 2 | from game.deck import Deck 3 | from game.card import Card, Rank, Suit 4 | 5 | def test_player_creation(): 6 | player = Player("Test Player", 1000) 7 | assert player.player_name == "Test Player" 8 | assert player.stack == 1000 9 | assert player.round_pot_commitment == 0 10 | assert player.card1 is None 11 | assert player.card2 is None 12 | assert player.hand_rank == handRank.HIGH_CARD 13 | assert player.hand_played == [] 14 | 15 | def test_deal_hand(): 16 | player = Player("Test Player", 1000) 17 | deck = Deck() 18 | 19 | player.deal_hand(deck) 20 | 21 | assert player.card1 is not None 22 | assert player.card2 is not None 23 | assert isinstance(player.card1, Card) 24 | assert isinstance(player.card2, Card) 25 | assert player.card1 != player.card2 26 | assert len(deck.cards) == 50 # 52 - 2 cards dealt 27 | 28 | def test_return_hand(): 29 | player = Player("Test Player", 1000) 30 | player.card1 = Card(Rank.ACE, Suit.SPADES) 31 | player.card2 = Card(Rank.KING, Suit.HEARTS) 32 | 33 | hand = player.return_hand() 34 | 35 | assert len(hand) == 2 36 | assert hand[0] == player.card1 37 | assert hand[1] == player.card2 38 | 39 | def test_return_long_hand(): 40 | player = Player("Test Player", 1000) 41 | player.card1 = Card(Rank.ACE, Suit.SPADES) 42 | player.card2 = Card(Rank.KING, Suit.HEARTS) 43 | 44 | long_hand = player.return_long_hand() 45 | 46 | assert long_hand == "Ace of Spades, King of Hearts" 47 | 48 | # Test with no cards 49 | player2 = Player("No Cards", 1000) 50 | assert player2.return_long_hand() is None 51 | 52 | def test_bet(): 53 | player = Player("Test Player", 1000) 54 | 55 | player.bet(100) 56 | 57 | assert player.stack == 900 58 | assert player.round_pot_commitment == 100 59 | 60 | player.bet(200) 61 | 62 | assert player.stack == 700 63 | assert player.round_pot_commitment == 300 64 | 65 | def test_reset(): 66 | player = Player("Test Player", 1000) 67 | player.card1 = Card(Rank.ACE, Suit.SPADES) 68 | player.card2 = Card(Rank.KING, Suit.HEARTS) 69 | player.round_pot_commitment = 100 70 | player.hand_rank = handRank.PAIR 71 | player.hand_played = [player.card1, player.card2] 72 | 73 | player.reset() 74 | 75 | assert player.card1 is None 76 | assert player.card2 is None 77 | assert player.round_pot_commitment == 0 78 | assert player.hand_rank == handRank.HIGH_CARD 79 | assert player.hand_played == [] 80 | # Stack should remain unchanged 81 | assert player.stack == 1000 -------------------------------------------------------------------------------- /bot/card_display.py: -------------------------------------------------------------------------------- 1 | from game.card import * 2 | 3 | # replace with your own custom emojis 4 | red_card_rank_tops = { 5 | Rank.ACE : "<:red_A_top:1128175151402471515>", 6 | Rank.TWO : "<:red_2_top:1128175103251857439>", 7 | Rank.THREE : "<:red_3_top:1128175104891834389>", 8 | Rank.FOUR : "<:red_4_top:1128175105743274046>", 9 | Rank.FIVE : "<:red_5_top:1128175106590527488>", 10 | Rank.SIX : "<:red_6_top:1128175107504865340>", 11 | Rank.SEVEN : "<:red_7_top:1128175109287444571>", 12 | Rank.EIGHT : "<:red_8_top:1128175110407344200>", 13 | Rank.NINE : "<:red_9_top:1128175111455911936>", 14 | Rank.TEN : "<:red_10_top:1128180390507585536>", 15 | Rank.JACK : "<:red_J_top:1128175150559412244>", 16 | Rank.QUEEN : "<:red_Q_top:1128175148206403634>", 17 | Rank.KING : "<:red_K_top:1128175149691183156>" 18 | } 19 | black_card_rank_tops = { 20 | Rank.ACE : "<:black_A_top:1128174953712324668>", 21 | Rank.TWO : "<:black_2_top:1128174889904394330>", 22 | Rank.THREE : "<:black_3_top:1128174891854737518>", 23 | Rank.FOUR : "<:black_4_top:1128174892722946189>", 24 | Rank.FIVE : "<:black_5_top:1128174893742161971>", 25 | Rank.SIX : "<:black_6_top:1128174894706851921>", 26 | Rank.SEVEN : "<:black_7_top:1128174895776403559>", 27 | Rank.EIGHT : "<:black_8_top:1128174896707547238>", 28 | Rank.NINE : "<:black_9_top:1128174897571573760>", 29 | Rank.TEN : "<:black_10_top:1128180389152833576>", 30 | Rank.JACK : "<:black_J_top:1128174955243253840>", 31 | Rank.QUEEN : "<:black_Q_top:1128174957415911495>", 32 | Rank.KING : "<:black_K_top:1128174956266672158>" 33 | } 34 | suit_bottoms = { 35 | Suit.SPADES: "<:spade_bottom:1128173093022613585>", 36 | Suit.HEARTS: "<:heart_bottom:1128172979973529640>", 37 | Suit.DIAMONDS: "<:diamond_bottom:1128172980833361921>", 38 | Suit.CLUBS: "<:club_bottom:1128172982469148703>" 39 | } 40 | 41 | ''' 42 | To view the unique identifiers of the custom cards in Discord, 43 | you can use the following commands in the server chat: 44 | 45 | Red Card Bottoms: 46 | 47 | \:red_A_top: 48 | \:red_2_top: 49 | \:red_3_top: 50 | \:red_4_top: 51 | \:red_5_top: 52 | \:red_6_top: 53 | \:red_7_top: 54 | \:red_8_top: 55 | \:red_9_top: 56 | \:red_10_top: 57 | \:red_J_top: 58 | \:red_Q_top: 59 | \:red_K_top: 60 | 61 | 62 | Black Card Bottoms: 63 | 64 | \:black_A_top: 65 | \:black_2_top: 66 | \:black_3_top: 67 | \:black_4_top: 68 | \:black_5_top: 69 | \:black_6_top: 70 | \:black_7_top: 71 | \:black_8_top: 72 | \:black_9_top: 73 | \:black_10_top: 74 | \:black_J_top: 75 | \:black_Q_top: 76 | \:black_K_top: 77 | 78 | Suit Card Tops: 79 | 80 | \:spade_bottom: 81 | \:heart_bottom: 82 | \:diamond_bottom: 83 | \:club_bottom: 84 | 85 | By copying and pasting these commands into the Discord chat, 86 | the respective custom cards will be displayed, 87 | and their unique identifiers will be shown in the message input field. 88 | ''' 89 | 90 | def get_cards(input_list: list[Card], small_cards: bool = False): 91 | top_row = [] 92 | bottom_row = [] 93 | for card in input_list: 94 | if card.suit == Suit.SPADES or card.suit == Suit.CLUBS: 95 | top_row.append(black_card_rank_tops[card.rank]) 96 | else: 97 | top_row.append(red_card_rank_tops[card.rank]) 98 | bottom_row.append(suit_bottoms[card.suit]) 99 | 100 | if small_cards: 101 | top_row = ["__"] + top_row + ["__"] 102 | bottom_row = ["__"] + bottom_row + ["__"] 103 | 104 | 105 | #join rows together with a new line 106 | return "\n".join(["".join(top_row), "".join(bottom_row)]) 107 | -------------------------------------------------------------------------------- /db/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from sqlalchemy import Column, Integer, String, DECIMAL, Enum, ForeignKey, TIMESTAMP, JSON 3 | from sqlalchemy.ext.declarative import declarative_base 4 | from db.enums import ActionType, Round, GameResult, HandResult 5 | 6 | Base = declarative_base() 7 | 8 | class Server(Base): 9 | __tablename__ = 'servers' 10 | id = Column(Integer, primary_key=True) 11 | host_id = Column(String(100)) 12 | server_name = Column(String(100)) 13 | total_players = Column(Integer, default=0) 14 | total_hands = Column(Integer, default=0) 15 | total_time_played = Column(Integer, default=0) 16 | total_wins = Column(Integer, default=0) 17 | total_losses = Column(Integer, default=0) 18 | total_draws = Column(Integer, default=0) 19 | net_bb_wins = Column(DECIMAL(10, 3), default=0.000) 20 | net_bb_losses = Column(DECIMAL(10, 3), default=0.000) 21 | net_bb_total = Column(DECIMAL(10, 3), default=0.000) 22 | 23 | class User(Base): 24 | __tablename__ = 'users' 25 | id = Column(Integer, primary_key=True) 26 | username = Column(String(100)) 27 | discord_id = Column(String(100)) 28 | total_hands = Column(Integer, default=0) 29 | total_games = Column(Integer, default=0) 30 | total_time_played = Column(Integer, default=0) 31 | total_wins = Column(Integer, default=0) 32 | total_losses = Column(Integer, default=0) 33 | total_draws = Column(Integer, default=0) 34 | highest_win_streak = Column(Integer, default=0) 35 | current_win_streak = Column(Integer, default=0) 36 | highest_loss_streak = Column(Integer, default=0) 37 | current_loss_streak = Column(Integer, default=0) 38 | net_bb_wins = Column(DECIMAL(10, 3), default=0.000) 39 | net_bb_losses = Column(DECIMAL(10, 3), default=0.000) 40 | net_bb_total = Column(DECIMAL(10, 3), default=0.000) 41 | 42 | class ServerUser(Base): 43 | __tablename__ = 'server_users' 44 | id = Column(Integer, primary_key=True) 45 | server_id = Column(Integer, ForeignKey('servers.id')) 46 | user_id = Column(Integer, ForeignKey('users.id')) 47 | total_hands_on_server = Column(Integer, default=0) 48 | net_bb_wins_on_server = Column(DECIMAL(10, 3), default=0.000) 49 | net_bb_losses_on_server = Column(DECIMAL(10, 3), default=0.000) 50 | net_bb_total_on_server = Column(DECIMAL(10, 3), default=0.000) 51 | 52 | class Game(Base): 53 | __tablename__ = 'games' 54 | id = Column(Integer, primary_key=True) 55 | server_id = Column(Integer, ForeignKey('servers.id')) 56 | user_id = Column(Integer, ForeignKey('users.id')) 57 | timestamp = Column(TIMESTAMP, default=datetime.now) 58 | end_timestamp = Column(TIMESTAMP) 59 | bot_version = Column(String(100), default='0.0.0') 60 | total_hands = Column(Integer, default=0) 61 | small_blind = Column(Integer) 62 | big_blind = Column(Integer) 63 | starting_stack = Column(Integer) 64 | ending_stack = Column(Integer, default=0) 65 | net_bb = Column(DECIMAL(10, 3), default=0.000) 66 | result = Column(Enum(GameResult, native_enum=False, values_callable=lambda obj: [e.value for e in obj]), default=GameResult.IN_PROGRESS.value) 67 | 68 | class Hand(Base): 69 | __tablename__ = 'hands' 70 | id = Column(Integer, primary_key=True) 71 | server_id = Column(Integer, ForeignKey('servers.id')) 72 | user_id = Column(Integer, ForeignKey('users.id')) 73 | game_id = Column(Integer, ForeignKey('games.id')) 74 | cards = Column(String(100)) 75 | gpt_cards = Column(String(100)) 76 | community_cards = Column(String(100), default='') 77 | starting_stack = Column(Integer) 78 | ending_stack = Column(Integer, default=0) 79 | net_bb = Column(DECIMAL(10, 3), default=0.000) 80 | result = Column(Enum(HandResult, native_enum=False, values_callable=lambda obj: [e.value for e in obj]), default=HandResult.IN_PROGRESS.value) 81 | end_round = Column(Enum(Round, native_enum=False, values_callable=lambda obj: [e.value for e in obj]), default=Round.IN_PROGRESS.value) 82 | 83 | class GPTAction(Base): 84 | __tablename__ = 'gpt_actions' 85 | id = Column(Integer, primary_key=True) 86 | timestamp = Column(TIMESTAMP, default=datetime.now) 87 | user_id = Column(Integer, ForeignKey('users.id')) 88 | game_id = Column(Integer, ForeignKey('games.id')) 89 | hand_id = Column(Integer, ForeignKey('hands.id')) 90 | action_type = Column(Enum(ActionType, native_enum=False, values_callable=lambda obj: [e.value for e in obj]), nullable=False) 91 | raise_amount = Column(DECIMAL(10, 2)) 92 | json_data = Column(JSON) 93 | -------------------------------------------------------------------------------- /tests/test_poker_logic.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from game.poker import Dealer, PokerGameManager 3 | from game.card import Card, Rank, Suit 4 | 5 | def test_deal_board(): 6 | dealer = Dealer(2) 7 | 8 | # Deal 3 cards (flop) 9 | dealer.deal_board(3) 10 | assert len(dealer.board) == 3 11 | 12 | # Deal 1 more card (turn) 13 | dealer.deal_board(4) 14 | assert len(dealer.board) == 4 15 | 16 | # Deal 1 more card (river) 17 | dealer.deal_board(5) 18 | assert len(dealer.board) == 5 19 | 20 | # Check that the board cards are unique 21 | assert len(set(str(card) for card in dealer.board)) == 5 22 | 23 | def test_return_community_cards(): 24 | dealer = Dealer(2) 25 | 26 | # Set specific board cards 27 | dealer.board = [ 28 | Card(Rank.ACE, Suit.SPADES), 29 | Card(Rank.KING, Suit.HEARTS), 30 | Card(Rank.QUEEN, Suit.DIAMONDS) 31 | ] 32 | 33 | # Get community cards as string 34 | community_str = dealer.return_community_cards() 35 | 36 | # Check that the string contains all cards 37 | assert "AS" in community_str 38 | assert "KH" in community_str 39 | assert "QD" in community_str 40 | 41 | def test_return_player_hand_str(): 42 | dealer = Dealer(1) 43 | 44 | # Set specific player cards 45 | dealer.players[0].card1 = Card(Rank.ACE, Suit.SPADES) 46 | dealer.players[0].card2 = Card(Rank.KING, Suit.HEARTS) 47 | 48 | # Get player hand as string 49 | hand_str = dealer.return_player_hand_str(0) 50 | 51 | # Check that the string contains both cards 52 | assert "AS" in hand_str 53 | assert "KH" in hand_str 54 | 55 | def test_all_in_call(): 56 | game = PokerGameManager(buy_in=100) 57 | 58 | # Setup scenario where player 0 has less chips than current bet 59 | game.current_bet = 75 60 | game.players[0].stack = 40 61 | game.players[0].round_pot_commitment = 10 62 | game.players[1].round_pot_commitment = 75 63 | game.current_pot = 85 # 10 + 75 64 | 65 | # Player goes all-in as a call 66 | game.player_all_in_call(0) 67 | 68 | # Player should commit all their chips 69 | assert game.players[0].stack == 0 70 | assert game.players[0].round_pot_commitment == 50 # 10 + 40 71 | 72 | # The bet should be adjusted to what the all-in player can afford 73 | assert game.current_bet == 50 74 | 75 | # Other player should get refunded the difference 76 | assert game.players[1].round_pot_commitment == 50 77 | assert game.players[1].stack == 125 78 | 79 | # Pot should reflect the changes 80 | assert game.current_pot == 100 # 50 + 50 81 | 82 | def test_all_in_raise(): 83 | game = PokerGameManager(buy_in=100) 84 | 85 | # Setup scenario 86 | game.current_bet = 20 87 | game.players[0].stack = 50 88 | game.players[0].round_pot_commitment = 10 89 | 90 | # Player goes all-in as a raise 91 | game.player_all_in_raise(0) 92 | 93 | # Check that player committed all chips 94 | assert game.players[0].stack == 0 95 | assert game.players[0].round_pot_commitment == 60 # 10 + 50 96 | 97 | # Current bet should be raised to the all-in amount 98 | assert game.current_bet == 60 99 | 100 | @pytest.mark.parametrize("opp_stack,expected_max", [ 101 | (1000, 1000), # equal stacks 102 | (400, 400), # opponent shorter 103 | ]) 104 | def test_return_min_max_raise(opp_stack, expected_max): 105 | game = PokerGameManager() 106 | game.current_bet = 50 107 | game.players[1].stack = opp_stack 108 | min_raise, max_raise = game.return_min_max_raise(0) 109 | assert min_raise == 100 # 50 * 2 110 | assert max_raise == expected_max + game.players[1].round_pot_commitment 111 | 112 | def test_player_all_in_call_refund(): 113 | game = PokerGameManager() 114 | game.current_bet = 200 115 | # player 1 has bet 200 already 116 | game.players[1].round_pot_commitment = 200 117 | # player 0 is short – only 120 total left (20 already in pot) 118 | game.players[0].round_pot_commitment = 20 119 | game.players[0].stack = 100 120 | starting_stack_p1 = game.players[1].stack 121 | 122 | game.player_all_in_call(0) 123 | 124 | # bet should drop to 120, diff refunded to player 1 125 | assert game.current_bet == 120 126 | assert game.players[0].stack == 0 127 | assert game.players[0].round_pot_commitment == 120 128 | assert game.players[1].round_pot_commitment == 120 129 | assert game.players[1].stack == starting_stack_p1 + 80 # refund 130 | 131 | def test_player_win_split_list(): 132 | game = PokerGameManager() 133 | game.current_pot = 100 134 | p0, p1 = game.players 135 | stack0, stack1 = p0.stack, p1.stack 136 | 137 | game.player_win([p0, p1]) # split 138 | assert p0.stack == stack0 + 50 139 | assert p1.stack == stack1 + 50 -------------------------------------------------------------------------------- /tests/test_poker_gameplay.py: -------------------------------------------------------------------------------- 1 | from game.poker import PokerGameManager 2 | from game.player import handRank 3 | from game.card import Card, Rank, Suit 4 | from db.enums import Round 5 | 6 | def test_initialization(): 7 | game = PokerGameManager(buy_in=1500, small_blind=10, big_blind=20) 8 | 9 | assert game.starting_stack == 1500 10 | assert game.small_blind == 10 11 | assert game.big_blind == 20 12 | assert game.button == 0 13 | assert game.current_action == 0 14 | assert game.round == Round.PRE_FLOP 15 | assert game.current_pot == 0 16 | assert game.current_bet == 0 17 | assert len(game.players) == 2 18 | assert all(player.stack == 1500 for player in game.players) 19 | 20 | def test_new_round(): 21 | game = PokerGameManager() 22 | 23 | # Simulate some betting activity 24 | game.current_pot = 100 25 | game.current_bet = 50 26 | game.button = 0 27 | game.round = Round.RIVER 28 | 29 | # Player bets to change stack and round_pot_commitment 30 | game.players[0].bet(30) 31 | game.players[1].bet(20) 32 | 33 | # Start new round 34 | game.new_round() 35 | 36 | # Check reset state 37 | assert game.current_pot == 0 38 | assert game.current_bet == 0 39 | assert game.button == 1 # Button should move to next player 40 | assert game.current_action == 1 41 | assert game.round == Round.PRE_FLOP 42 | 43 | # Players' hands should be reset 44 | assert all(player.card1 is not None for player in game.players) 45 | assert all(player.card2 is not None for player in game.players) 46 | 47 | assert game.players[0].stack == 970 48 | assert game.players[1].stack == 980 49 | 50 | def test_reset_betting(): 51 | game = PokerGameManager() 52 | 53 | # Set up betting state 54 | game.current_bet = 50 55 | game.players[0].round_pot_commitment = 50 56 | game.players[1].round_pot_commitment = 25 57 | 58 | # Reset betting 59 | game.reset_betting() 60 | 61 | # Bet should be reset 62 | assert game.current_bet == 0 63 | 64 | # Players' round_pot_commitment should be reset 65 | assert game.players[0].round_pot_commitment == 0 66 | assert game.players[1].round_pot_commitment == 0 67 | 68 | def test_player_call(): 69 | game = PokerGameManager() 70 | 71 | # Set up initial state 72 | game.current_bet = 50 73 | game.players[0].round_pot_commitment = 20 74 | game.players[0].stack = 100 75 | 76 | # Player calls 77 | game.player_call(0) 78 | 79 | # Check that correct amount was called 80 | assert game.players[0].round_pot_commitment == 50 81 | assert game.players[0].stack == 70 # 100 - (50 - 20) 82 | assert game.current_pot == 30 # The call amount 83 | 84 | def test_player_raise(): 85 | game = PokerGameManager() 86 | 87 | # Set up initial state 88 | game.current_bet = 20 89 | game.players[0].round_pot_commitment = 20 90 | game.players[0].stack = 100 91 | 92 | # Player raises to 60 93 | game.player_raise(0, 60) 94 | 95 | # Check that bet was raised correctly 96 | assert game.current_bet == 60 97 | assert game.players[0].round_pot_commitment == 60 98 | assert game.players[0].stack == 60 # 100 - (60 - 20) 99 | assert game.current_pot == 40 # The raise amount 100 | 101 | def test_player_win(): 102 | game = PokerGameManager() 103 | 104 | # Set up pot and player stacks 105 | game.current_pot = 100 106 | game.players[0].stack = 900 107 | game.players[1].stack = 1000 108 | 109 | # Player 0 wins 110 | game.player_win(0) 111 | 112 | # Check that pot was awarded correctly 113 | assert game.players[0].stack == 1000 # 900 + 100 114 | assert game.players[1].stack == 1000 # Unchanged 115 | 116 | def test_determine_winner(): 117 | game = PokerGameManager() 118 | 119 | # Set up player hands 120 | game.players[0].hand_rank = handRank.FLUSH 121 | game.players[1].hand_rank = handRank.STRAIGHT 122 | 123 | # Set hand_played for tiebreaking 124 | game.players[0].hand_played = [ 125 | Card(Rank.ACE, Suit.HEARTS), 126 | Card(Rank.KING, Suit.HEARTS), 127 | Card(Rank.QUEEN, Suit.HEARTS), 128 | Card(Rank.JACK, Suit.HEARTS), 129 | Card(Rank.NINE, Suit.HEARTS) 130 | ] 131 | 132 | game.players[1].hand_played = [ 133 | Card(Rank.KING, Suit.CLUBS), 134 | Card(Rank.QUEEN, Suit.DIAMONDS), 135 | Card(Rank.JACK, Suit.HEARTS), 136 | Card(Rank.TEN, Suit.SPADES), 137 | Card(Rank.NINE, Suit.CLUBS) 138 | ] 139 | 140 | # Determine winner 141 | winner = game.determine_winner() 142 | 143 | # Player 0 should win with a flush 144 | assert winner == game.players[0] 145 | 146 | # Test tie 147 | game.players[1].hand_rank = handRank.FLUSH 148 | game.players[1].hand_played = game.players[0].hand_played.copy() 149 | 150 | # Determine winner again 151 | winners = game.determine_winner() 152 | 153 | # Both players should tie 154 | assert isinstance(winners, list) 155 | assert len(winners) == 2 156 | assert game.players[0] in winners 157 | assert game.players[1] in winners -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Poker Discord Bot - ChatGPT 2 | 3 | Welcome to the Poker Discord Bot powered by ChatGPT! This bot allows you to play a virtual game of Texas Hold'em poker with your friends right in your Discord server. The bot utilizes the ChatGPT-4 language model developed by OpenAI to provide an interactive and dynamic poker experience. The bot handles all aspects of the game, including dealing cards, managing bets, and determining winners. The bot also incorporates error handling to ensure a smooth user experience. To use the bot in your Discord server, either host the bot yourself using the setup instructions below or use the recomended method and invite the bot to your server using the generated invite link. 4 | 5 | ## Features 6 | 7 | - **Realistic Gameplay**: Play with realistic poker rules, including betting, folding, and raising. 8 | - **Quick startup**: Start a game of Texas Hold'em poker in seconds using the `/play_poker` command. 9 | - **Player Statistics**: View player and server statistics, including win rate, total winnings, and more. 10 | - **Dynamic Gameplay**: The bot handles all aspects of the game, including dealing cards, managing bets, and determining winners. 11 | - **Error Handling**: The bot incorporates error handling to ensure a smooth user experience. 12 | - **Quick Response Times**: The bot responds to user input within seconds. 13 | 14 | ## Commands 15 | 16 | To get information about the bot, use the following command: 17 | 18 | `/info` 19 | 20 | ![Info Command](docs/command_images/info.png) 21 | 22 | --- 23 | 24 | To start a game of Texas Hold'em poker, use the following command: 25 | 26 | `/play_poker [small-blind] [big-blind] [small-cards]` 27 | 28 | - `small-blind` (optional): Set the small blind amount (default: 5, minimum: 1). 29 | - `big-blind` (optional): Set the big blind amount (default: 10, minimum: 2). 30 | - `small-cards` (optional): Use small cards (default: False). 31 | 32 | ![Play Poker Command Preview](docs/command_images/play_poker_preview.gif) 33 | 34 | --- 35 | 36 | To view the leaderboard for all players, use the following command: 37 | 38 | `/player_leaderboard` 39 | 40 | ![Player Leaderboard Command](docs/command_images/player_leaderboard.png) 41 | 42 | --- 43 | 44 | To view player statistics, use the following command: 45 | 46 | `/player_stats [username]` 47 | 48 | - `username` (optional): Chose user to view statistics for (default: yourself). 49 | 50 | ![Player Stats Command](docs/command_images/player_stats.png) 51 | 52 | --- 53 | 54 | To view the leaderboard for all servers, use the following command: 55 | 56 | `/server_leaderboard` 57 | 58 | ![Server Leaderboard Command](docs/command_images/server_leaderboard.png) 59 | 60 | --- 61 | 62 | To view server statistics, use the following command: 63 | 64 | `/server_stats [server]` 65 | 66 | - `server` (optional): Chose server to view statistics for (default: current server). 67 | 68 | ![Server Stats Command](docs/command_images/server_stats.png) 69 | 70 | ## Setup 71 | 72 | To set up the PokerGPT Discord Bot yourself, follow these steps: 73 | 74 | 1. Clone the repository and install dependencies: 75 | 76 | ```bash 77 | git clone https://github.com/matteo-psnt/PokerGPT.git 78 | cd PokerGPT 79 | python3 -m venv .venv 80 | source .venv/bin/activate 81 | pip install -r requirements.txt 82 | ``` 83 | 84 | 2. Create a new Discord application on the [Discord Developer Portal](https://discord.com/developers/applications). 85 | - Add a bot to your application and copy the bot token. 86 | 3. Generate a bot token for your application. 87 | 4. Invite the bot to your server using the generated invite link. 88 | 5. Create a copy of the `.env_template` file and change the name to `.env`. 89 | 6. In the `.env` file, fill in your Discord bot token and OpenAI API key: 90 | 91 | ```plaintext 92 | DISCORD_TOKEN=your_discord_bot_token 93 | OPENAI_API_KEY=your_openai_api_key 94 | ``` 95 | 96 | 7. (Optional) Enable Database Features 97 | 98 | To enable player/server statistics and leaderboards: 99 | 100 | Enter your MySQL credentials in `.env`: 101 | 102 | ```plaintext 103 | DB_HOST=your_database_host 104 | DB_USER=your_database_user 105 | DB_PASSWORD=your_database_password 106 | DB_NAME=pokerGPTdatabase 107 | ``` 108 | 109 | Initialize the database schema: 110 | 111 | ```bash 112 | python init_db.py 113 | ``` 114 | 115 | 8. (Optional) Change the AI Model 116 | 117 | To change the GPT model, edit the model_name variable in `bot/bot_poker_handler.py`. 118 | 119 | 9. Run the bot: 120 | 121 | ```bash 122 | python run.py 123 | ``` 124 | 125 | - Use `--no-db` flag to run the bot without database features. 126 | 127 | ### Card Emoji Setup (For displaying cards in Discord) 128 | 129 | 1. Create a new Discord server (this will host your card emojis). 130 | 2. Add the bot to this server. 131 | 3. Upload all images from the `split_deck_images` folder as custom emojis in this server. 132 | 4. Get each emoji’s unique identifier: 133 | - Open `bot/card_display.py` and find the emoji dictionary. 134 | - In Discord, type each emoji (e.g., `:ace_of_spades:`) to see its identifier, or use a Discord utility bot to fetch emoji IDs. 135 | 5. Update the emoji dictionary in `bot/card_display.py` with the correct emoji IDs from your server. 136 | 6. Test in a Discord channel to verify the cards display properly. 137 | 138 | ## Contributions 139 | 140 | Contributions to the Poker Discord Bot are welcome! If you have any suggestions, bug reports, or feature requests, please open an issue or submit a pull request on the GitHub repository. 141 | 142 | ## Disclaimer 143 | 144 | This Poker Discord Bot is provided as-is without any warranty. The developers and contributors are not responsible for any loss of virtual currency or damages resulting from the use of this bot. 145 | 146 | ## License 147 | 148 | The Poker Discord Bot is released under the [MIT License](https://opensource.org/licenses/MIT). 149 | -------------------------------------------------------------------------------- /tests/test_db_manager.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from sqlalchemy import create_engine, text 3 | from sqlalchemy.orm import sessionmaker 4 | from sqlalchemy.exc import SQLAlchemyError 5 | from db.models import Base, User, Server, Game, Hand, ServerUser, GPTAction 6 | from db.enums import ActionType, GameResult, HandResult, Round 7 | from db.db_utils import DatabaseManager 8 | from decimal import Decimal 9 | 10 | @pytest.fixture 11 | def session(): 12 | # In-memory SQLite DB for isolated tests 13 | engine = create_engine("sqlite:///:memory:") 14 | Base.metadata.create_all(engine) 15 | Session = sessionmaker(bind=engine) 16 | return Session() 17 | 18 | @pytest.fixture 19 | def db_manager(session, monkeypatch): 20 | # Monkeypatch DATABASE_EXISTS 21 | monkeypatch.setattr("db.db_utils.DATABASE_EXISTS", True) 22 | return DatabaseManager( 23 | session=session, 24 | discord_id="test_discord_id", 25 | username="test_user", 26 | host_id="test_host_id", 27 | server_name="test_server" 28 | ) 29 | 30 | def test_check_or_create_user(db_manager, session): 31 | user = session.query(User).filter_by(discord_id="test_discord_id").first() 32 | assert user is not None 33 | assert user.username == "test_user" 34 | 35 | def test_check_or_create_server(db_manager, session): 36 | server = session.query(Server).filter_by(host_id="test_host_id").first() 37 | assert server is not None 38 | assert server.server_name == "test_server" 39 | 40 | def test_initialize_game(db_manager, session): 41 | db_manager.initialize_game(small_blind=5, big_blind=10, starting_stack=1000) 42 | game = session.query(Game).first() 43 | assert game is not None 44 | assert game.small_blind == 5 45 | assert game.big_blind == 10 46 | assert game.starting_stack == 1000 47 | 48 | def test_initialize_hand_and_end_hand(db_manager, session): 49 | db_manager.initialize_game(small_blind=5, big_blind=10, starting_stack=1000) 50 | db_manager.initialize_hand(cards="AsKs", gpt_cards="AhQh", starting_stack=1000) 51 | db_manager.end_hand(ending_stack=1100, end_round=Round.PRE_FLOP) 52 | hand = session.query(Hand).first() 53 | assert hand.ending_stack == 1100 54 | assert hand.end_round == Round.PRE_FLOP 55 | assert hand.result == HandResult.WIN 56 | 57 | def test_end_game(db_manager, session): 58 | db_manager.initialize_game(small_blind=5, big_blind=10, starting_stack=1000) 59 | db_manager.end_game(ending_stack=2000) 60 | game = session.query(Game).first() 61 | assert game.ending_stack == 2000 62 | assert game.result == GameResult.COMPLETE_WIN 63 | 64 | @pytest.mark.parametrize("enum_cls,model_cls,field_name,extra_fields", [ 65 | (GameResult, Game, 'result', { 66 | 'server_id': 1, 'user_id': 1, 'small_blind': 5, 'big_blind': 10, 'starting_stack': 1000, 67 | }), 68 | (HandResult, Hand, 'result', { 69 | 'server_id': 1, 'user_id': 1, 'game_id': 1, 'cards': "AsKs", 'gpt_cards': "AhQh", 'starting_stack': 1000, 70 | }), 71 | (Round, Hand, 'end_round', { 72 | 'server_id': 1, 'user_id': 1, 'game_id': 1, 'cards': "AsKs", 'gpt_cards': "AhQh", 'starting_stack': 1000, 73 | }), 74 | (ActionType, GPTAction, 'action_type', { 75 | 'user_id': 1, 'game_id': 1, 'hand_id': 1, 76 | }), 77 | ]) 78 | def test_enum_values_stored_as_value(session, enum_cls, model_cls, field_name, extra_fields): 79 | for enum_member in enum_cls: 80 | # Insert row with enum field set to member 81 | model_kwargs = {**extra_fields, field_name: enum_member} 82 | obj = model_cls(**model_kwargs) 83 | session.add(obj) 84 | session.commit() 85 | # Find the row in the DB (assume just added, so order by id DESC) 86 | row_id = obj.id 87 | result = session.execute( 88 | text(f"SELECT {field_name} FROM {model_cls.__tablename__} WHERE id=:id"), {"id": row_id} 89 | ).scalar() 90 | # Check stored string matches the .value 91 | assert result == enum_member.value 92 | # Also confirm it's not the .name 93 | assert result != enum_member.name 94 | 95 | session.delete(obj) 96 | session.commit() 97 | 98 | def test_safe_commit_rolls_back_and_reraises(db_manager, session, monkeypatch): 99 | rollback_called = {"flag": False} 100 | 101 | def boom(): 102 | raise SQLAlchemyError("forced failure") 103 | 104 | def rollback_spy(): 105 | rollback_called["flag"] = True 106 | 107 | monkeypatch.setattr(session, "commit", boom) 108 | monkeypatch.setattr(session, "rollback", rollback_spy) 109 | 110 | with pytest.raises(SQLAlchemyError): 111 | db_manager._safe_commit() 112 | 113 | assert rollback_called["flag"] is True 114 | 115 | def seed_users(session): 116 | """ 117 | Create 12 players with descending net_bb_total so we can 118 | test 1st / 2nd / 11th ordinal helpers. 119 | """ 120 | for i, bb in enumerate(range(110, -1, -10), start=1): 121 | u = User( 122 | username=f"p{i}", 123 | discord_id=f"uid{i}", 124 | net_bb_total=bb 125 | ) 126 | session.add(u) 127 | session.commit() 128 | 129 | 130 | def seed_servers(session): 131 | for i, bb in enumerate(range(1000, -100, -100), start=1): 132 | s = Server( 133 | server_name=f"srv{i}", 134 | host_id=f"host{i}", 135 | net_bb_wins=bb 136 | ) 137 | session.add(s) 138 | session.commit() 139 | 140 | 141 | @pytest.mark.parametrize("uid,place", [("uid1", 1), ("uid2", 2), ("uid11", 11)]) 142 | def test_get_user_place(session, uid, place): 143 | seed_users(session) 144 | mgr = DatabaseManager(session, uid, uid, "someHost", "someServer") 145 | assert mgr.get_user_place() == place 146 | 147 | 148 | @pytest.mark.parametrize("host,place", [("host1", 1), ("host2", 2), ("host11", 11)]) 149 | def test_get_server_place(session, host, place): 150 | seed_servers(session) 151 | mgr = DatabaseManager(session, "uidX", "userX", host, host) # user fields irrelevant 152 | assert mgr.get_server_place() == place 153 | 154 | def test_update_wins_and_losses(db_manager, session): 155 | # Initialize game and hand 156 | db_manager.initialize_game(small_blind=5, big_blind=10, starting_stack=1000) 157 | db_manager.initialize_hand(cards="AsKs", gpt_cards="AhQh", starting_stack=1000) 158 | 159 | # Test _update_wins 160 | db_manager.end_hand(ending_stack=1100, end_round=Round.PRE_FLOP) 161 | user = session.query(User).filter_by(discord_id="test_discord_id").first() 162 | server = session.query(Server).filter_by(host_id="test_host_id").first() 163 | 164 | assert user.net_bb_wins == Decimal(10) 165 | assert user.net_bb_total == Decimal(10) 166 | assert server.net_bb_wins == Decimal(10) 167 | assert server.net_bb_total == Decimal(10) 168 | 169 | # Test _update_losses 170 | db_manager.initialize_hand(cards="AsKs", gpt_cards="AhQh", starting_stack=1000) 171 | db_manager.end_hand(ending_stack=900, end_round=Round.PRE_FLOP) 172 | 173 | user = session.query(User).filter_by(discord_id="test_discord_id").first() 174 | server = session.query(Server).filter_by(host_id="test_host_id").first() 175 | 176 | assert user.net_bb_losses == Decimal(10) 177 | assert user.net_bb_total == Decimal(0) # Wins (10) - Losses (10) 178 | assert server.net_bb_losses == Decimal(10) 179 | assert server.net_bb_total == Decimal(0) # Wins (10) - Losses (10) -------------------------------------------------------------------------------- /tests/test_hand_evaluation.py: -------------------------------------------------------------------------------- 1 | from game.poker import Dealer 2 | from game.player import handRank 3 | from game.card import Card, Rank, Suit 4 | 5 | def test_royal_flush(): 6 | dealer = Dealer(1) 7 | player = dealer.players[0] 8 | 9 | # Set up royal flush in spades 10 | player.card1 = Card(Rank.ACE, Suit.SPADES) 11 | player.card2 = Card(Rank.KING, Suit.SPADES) 12 | dealer.board = [ 13 | Card(Rank.QUEEN, Suit.SPADES), 14 | Card(Rank.JACK, Suit.SPADES), 15 | Card(Rank.TEN, Suit.SPADES), 16 | Card(Rank.TWO, Suit.HEARTS), # Irrelevant cards 17 | Card(Rank.THREE, Suit.HEARTS) 18 | ] 19 | 20 | rank, hand = dealer.get_hand_rank(player) 21 | 22 | assert rank == handRank.ROYAL_FLUSH 23 | assert len(hand) == 5 24 | assert hand[0].rank == Rank.ACE 25 | assert all(card.suit == Suit.SPADES for card in hand) 26 | 27 | def test_straight_flush(): 28 | dealer = Dealer(1) 29 | player = dealer.players[0] 30 | 31 | # Set up 9-high straight flush in hearts 32 | player.card1 = Card(Rank.NINE, Suit.HEARTS) 33 | player.card2 = Card(Rank.EIGHT, Suit.HEARTS) 34 | dealer.board = [ 35 | Card(Rank.SEVEN, Suit.HEARTS), 36 | Card(Rank.SIX, Suit.HEARTS), 37 | Card(Rank.FIVE, Suit.HEARTS), 38 | Card(Rank.TWO, Suit.DIAMONDS), # Irrelevant cards 39 | Card(Rank.THREE, Suit.CLUBS) 40 | ] 41 | 42 | rank, hand = dealer.get_hand_rank(player) 43 | 44 | assert rank == handRank.STRAIGHT_FLUSH 45 | assert len(hand) == 5 46 | assert hand[0].rank == Rank.NINE 47 | assert all(card.suit == Suit.HEARTS for card in hand) 48 | 49 | def test_four_of_a_kind(): 50 | dealer = Dealer(1) 51 | player = dealer.players[0] 52 | 53 | # Set up four kings with ace kicker 54 | player.card1 = Card(Rank.KING, Suit.HEARTS) 55 | player.card2 = Card(Rank.KING, Suit.SPADES) 56 | dealer.board = [ 57 | Card(Rank.KING, Suit.DIAMONDS), 58 | Card(Rank.KING, Suit.CLUBS), 59 | Card(Rank.ACE, Suit.HEARTS), 60 | Card(Rank.TWO, Suit.DIAMONDS), 61 | Card(Rank.THREE, Suit.CLUBS) 62 | ] 63 | 64 | rank, hand = dealer.get_hand_rank(player) 65 | 66 | assert rank == handRank.FOUR_OF_A_KIND 67 | assert len(hand) == 5 68 | assert sum(1 for card in hand if card.rank == Rank.KING) == 4 69 | assert any(card.rank == Rank.ACE for card in hand) 70 | 71 | def test_full_house(): 72 | dealer = Dealer(1) 73 | player = dealer.players[0] 74 | 75 | # Set up full house: aces full of kings 76 | player.card1 = Card(Rank.ACE, Suit.HEARTS) 77 | player.card2 = Card(Rank.ACE, Suit.SPADES) 78 | dealer.board = [ 79 | Card(Rank.ACE, Suit.DIAMONDS), 80 | Card(Rank.KING, Suit.CLUBS), 81 | Card(Rank.KING, Suit.HEARTS), 82 | Card(Rank.TWO, Suit.DIAMONDS), 83 | Card(Rank.THREE, Suit.CLUBS) 84 | ] 85 | 86 | rank, hand = dealer.get_hand_rank(player) 87 | 88 | assert rank == handRank.FULL_HOUSE 89 | assert len(hand) == 5 90 | assert sum(1 for card in hand if card.rank == Rank.ACE) == 3 91 | assert sum(1 for card in hand if card.rank == Rank.KING) == 2 92 | 93 | def test_flush(): 94 | dealer = Dealer(1) 95 | player = dealer.players[0] 96 | 97 | # Set up flush in diamonds 98 | player.card1 = Card(Rank.ACE, Suit.DIAMONDS) 99 | player.card2 = Card(Rank.KING, Suit.DIAMONDS) 100 | dealer.board = [ 101 | Card(Rank.QUEEN, Suit.DIAMONDS), 102 | Card(Rank.NINE, Suit.DIAMONDS), 103 | Card(Rank.FIVE, Suit.DIAMONDS), 104 | Card(Rank.TWO, Suit.HEARTS), 105 | Card(Rank.THREE, Suit.CLUBS) 106 | ] 107 | 108 | rank, hand = dealer.get_hand_rank(player) 109 | 110 | assert rank == handRank.FLUSH 111 | assert len(hand) == 5 112 | assert all(card.suit == Suit.DIAMONDS for card in hand) 113 | 114 | def test_straight(): 115 | dealer = Dealer(1) 116 | player = dealer.players[0] 117 | 118 | # Set up 8-high straight with mixed suits 119 | player.card1 = Card(Rank.EIGHT, Suit.HEARTS) 120 | player.card2 = Card(Rank.SEVEN, Suit.SPADES) 121 | dealer.board = [ 122 | Card(Rank.SIX, Suit.DIAMONDS), 123 | Card(Rank.FIVE, Suit.CLUBS), 124 | Card(Rank.FOUR, Suit.HEARTS), 125 | Card(Rank.TWO, Suit.HEARTS), 126 | Card(Rank.THREE, Suit.CLUBS) 127 | ] 128 | 129 | rank, hand = dealer.get_hand_rank(player) 130 | 131 | assert rank == handRank.STRAIGHT 132 | assert len(hand) == 5 133 | assert hand[0].rank == Rank.EIGHT 134 | 135 | def test_three_of_a_kind(): 136 | dealer = Dealer(1) 137 | player = dealer.players[0] 138 | 139 | # Set up three queens with ace-king kickers 140 | player.card1 = Card(Rank.QUEEN, Suit.HEARTS) 141 | player.card2 = Card(Rank.QUEEN, Suit.SPADES) 142 | dealer.board = [ 143 | Card(Rank.QUEEN, Suit.DIAMONDS), 144 | Card(Rank.ACE, Suit.CLUBS), 145 | Card(Rank.KING, Suit.HEARTS), 146 | Card(Rank.TWO, Suit.DIAMONDS), 147 | Card(Rank.THREE, Suit.CLUBS) 148 | ] 149 | 150 | rank, hand = dealer.get_hand_rank(player) 151 | 152 | assert rank == handRank.THREE_OF_A_KIND 153 | assert len(hand) == 5 154 | assert sum(1 for card in hand if card.rank == Rank.QUEEN) == 3 155 | assert any(card.rank == Rank.ACE for card in hand) 156 | assert any(card.rank == Rank.KING for card in hand) 157 | 158 | def test_two_pair(): 159 | dealer = Dealer(1) 160 | player = dealer.players[0] 161 | 162 | # Set up two pair: aces and kings with queen kicker 163 | player.card1 = Card(Rank.ACE, Suit.HEARTS) 164 | player.card2 = Card(Rank.ACE, Suit.SPADES) 165 | dealer.board = [ 166 | Card(Rank.KING, Suit.DIAMONDS), 167 | Card(Rank.KING, Suit.CLUBS), 168 | Card(Rank.QUEEN, Suit.HEARTS), 169 | Card(Rank.TWO, Suit.DIAMONDS), 170 | Card(Rank.THREE, Suit.CLUBS) 171 | ] 172 | 173 | rank, hand = dealer.get_hand_rank(player) 174 | 175 | assert rank == handRank.TWO_PAIR 176 | assert len(hand) == 5 177 | assert sum(1 for card in hand if card.rank == Rank.ACE) == 2 178 | assert sum(1 for card in hand if card.rank == Rank.KING) == 2 179 | assert any(card.rank == Rank.QUEEN for card in hand) 180 | 181 | def test_pair(): 182 | dealer = Dealer(1) 183 | player = dealer.players[0] 184 | 185 | # Set up pair of jacks with ace-king-queen kickers 186 | player.card1 = Card(Rank.JACK, Suit.HEARTS) 187 | player.card2 = Card(Rank.JACK, Suit.SPADES) 188 | dealer.board = [ 189 | Card(Rank.ACE, Suit.DIAMONDS), 190 | Card(Rank.KING, Suit.CLUBS), 191 | Card(Rank.QUEEN, Suit.HEARTS), 192 | Card(Rank.TWO, Suit.DIAMONDS), 193 | Card(Rank.THREE, Suit.CLUBS) 194 | ] 195 | 196 | rank, hand = dealer.get_hand_rank(player) 197 | 198 | assert rank == handRank.PAIR 199 | assert len(hand) == 5 200 | assert sum(1 for card in hand if card.rank == Rank.JACK) == 2 201 | assert any(card.rank == Rank.ACE for card in hand) 202 | assert any(card.rank == Rank.KING for card in hand) 203 | assert any(card.rank == Rank.QUEEN for card in hand) 204 | 205 | def test_high_card(): 206 | dealer = Dealer(1) 207 | player = dealer.players[0] 208 | 209 | # Set up ace-high with king, queen, jack, nine 210 | player.card1 = Card(Rank.ACE, Suit.HEARTS) 211 | player.card2 = Card(Rank.KING, Suit.SPADES) 212 | dealer.board = [ 213 | Card(Rank.QUEEN, Suit.DIAMONDS), 214 | Card(Rank.JACK, Suit.CLUBS), 215 | Card(Rank.NINE, Suit.HEARTS), 216 | Card(Rank.TWO, Suit.DIAMONDS), 217 | Card(Rank.THREE, Suit.CLUBS) 218 | ] 219 | 220 | rank, hand = dealer.get_hand_rank(player) 221 | 222 | assert rank == handRank.HIGH_CARD 223 | assert len(hand) == 5 224 | assert hand[0].rank == Rank.ACE 225 | assert hand[1].rank == Rank.KING 226 | assert hand[2].rank == Rank.QUEEN 227 | assert hand[3].rank == Rank.JACK 228 | assert hand[4].rank == Rank.NINE -------------------------------------------------------------------------------- /tests/test_gpt_player.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pytest 3 | from unittest.mock import MagicMock, patch 4 | from bot.gpt_player import GPTPlayer 5 | from db.enums import ActionType, Round 6 | from game.card import Card, Rank, Suit 7 | from game.poker import PokerGameManager 8 | 9 | @pytest.fixture 10 | def mock_db(): 11 | db = MagicMock() 12 | db.record_gpt_action = MagicMock() 13 | return db 14 | 15 | @pytest.fixture 16 | def mock_chain(): 17 | with patch('bot.gpt_player.ChatPromptTemplate'), \ 18 | patch('bot.gpt_player.ChatOpenAI'), \ 19 | patch('bot.gpt_player.StrOutputParser'): 20 | gpt_player = GPTPlayer(MagicMock()) 21 | gpt_player.chain = MagicMock() 22 | return gpt_player 23 | 24 | @pytest.fixture 25 | def poker_game(): 26 | game = PokerGameManager(buy_in=1000, small_blind=5, big_blind=10) 27 | # Set specific cards for testing 28 | game.players[1].card1 = Card(Rank.ACE, Suit.SPADES) 29 | game.players[1].card2 = Card(Rank.KING, Suit.HEARTS) 30 | return game 31 | 32 | def test_extract_action_raise(mock_db, poker_game): 33 | gpt_player = GPTPlayer(mock_db) 34 | 35 | # Test normal raise 36 | json_string = json.dumps({ 37 | "your_hand": "Ace of Spades, King of Hearts", 38 | "opponents_hand": "Possibly a medium pair", 39 | "thought_process": "I have a strong starting hand, I should raise", 40 | "action": "raise", 41 | "raise_amount": 30 42 | }) 43 | 44 | action, amount = gpt_player._extract_action(json_string, poker_game) 45 | 46 | assert action == ActionType.RAISE 47 | assert amount == 30 48 | mock_db.record_gpt_action.assert_called_once_with(action, 30, json_string) 49 | 50 | def test_extract_action_min_raise(mock_db, poker_game): 51 | gpt_player = GPTPlayer(mock_db) 52 | mock_db.reset_mock() 53 | 54 | # Set current bet to make min raise higher 55 | poker_game.current_bet = 40 56 | 57 | # Test raise below minimum (should be adjusted) 58 | json_string = json.dumps({ 59 | "your_hand": "Ace of Spades, King of Hearts", 60 | "opponents_hand": "Possibly a medium pair", 61 | "thought_process": "I have a strong starting hand, I should raise", 62 | "action": "raise", 63 | "raise_amount": 50 # Min raise would be 80 (40 * 2) 64 | }) 65 | 66 | action, amount = gpt_player._extract_action(json_string, poker_game) 67 | 68 | assert action == ActionType.RAISE 69 | assert amount == 80 # Should be adjusted to min raise 70 | mock_db.record_gpt_action.assert_called_once_with(action, 80, json_string) 71 | 72 | def test_extract_action_all_in(mock_db, poker_game): 73 | gpt_player = GPTPlayer(mock_db) 74 | mock_db.reset_mock() 75 | 76 | # Test raise above maximum (should become all-in) 77 | json_string = json.dumps({ 78 | "your_hand": "Ace of Spades, King of Hearts", 79 | "opponents_hand": "Possibly a medium pair", 80 | "thought_process": "I have a strong starting hand, I should raise big", 81 | "action": "raise", 82 | "raise_amount": 2000 # More than player's stack 83 | }) 84 | 85 | action, amount = gpt_player._extract_action(json_string, poker_game) 86 | 87 | assert action == ActionType.ALL_IN 88 | assert amount == 1000 # Player's stack 89 | mock_db.record_gpt_action.assert_called_once_with(action, 1000, json_string) 90 | 91 | def test_extract_action_no_raise(mock_db, poker_game): 92 | gpt_player = GPTPlayer(mock_db) 93 | mock_db.reset_mock() 94 | 95 | # Test action without raise amount 96 | json_string = json.dumps({ 97 | "your_hand": "Ace of Spades, King of Hearts", 98 | "opponents_hand": "Possibly a strong hand", 99 | "thought_process": "I shouldn't risk too much here", 100 | "action": "call" 101 | }) 102 | 103 | action, amount = gpt_player._extract_action(json_string, poker_game) 104 | 105 | assert action == ActionType.CALL 106 | assert amount is None 107 | mock_db.record_gpt_action.assert_called_once_with(action, None, json_string) 108 | 109 | def test_extract_action_invalid_json(mock_db, poker_game): 110 | gpt_player = GPTPlayer(mock_db) 111 | mock_db.reset_mock() 112 | 113 | # Test invalid JSON 114 | json_string = "This is not valid JSON" 115 | 116 | action, amount = gpt_player._extract_action(json_string, poker_game) 117 | 118 | assert action == "Default" 119 | assert amount == 0 120 | mock_db.record_gpt_action.assert_not_called() 121 | 122 | def test_pre_flop_small_blind(mock_chain, poker_game): 123 | mock_chain.chain.invoke.return_value = json.dumps({ 124 | "your_hand": "Ace of Spades, King of Hearts", 125 | "opponents_hand": "Unknown", 126 | "thought_process": "Strong starting hand, should raise", 127 | "action": "raise", 128 | "raise_amount": 30 129 | }) 130 | 131 | action, amount = mock_chain.pre_flop_small_blind(poker_game) 132 | 133 | assert action == ActionType.RAISE 134 | assert amount == 30 135 | mock_chain.chain.invoke.assert_called_once() 136 | 137 | def test_pre_flop_big_blind(mock_chain, poker_game): 138 | mock_chain.chain.invoke.return_value = json.dumps({ 139 | "your_hand": "Ace of Spades, King of Hearts", 140 | "opponents_hand": "Unknown", 141 | "thought_process": "Strong starting hand, should raise", 142 | "action": "raise", 143 | "raise_amount": 40 144 | }) 145 | 146 | action, amount = mock_chain.pre_flop_big_blind(poker_game) 147 | 148 | assert action == ActionType.RAISE 149 | assert amount == 40 150 | mock_chain.chain.invoke.assert_called_once() 151 | 152 | def test_first_to_act(mock_chain, poker_game): 153 | # Set up board 154 | poker_game.board = [ 155 | Card(Rank.TEN, Suit.SPADES), 156 | Card(Rank.JACK, Suit.HEARTS), 157 | Card(Rank.QUEEN, Suit.DIAMONDS) 158 | ] 159 | poker_game.round = Round.FLOP 160 | 161 | mock_chain.chain.invoke.return_value = json.dumps({ 162 | "your_hand": "Ace of Spades, King of Hearts", 163 | "opponents_hand": "Unknown", 164 | "thought_process": "I have a straight draw, should bet", 165 | "action": "raise", 166 | "raise_amount": 50 167 | }) 168 | 169 | action, amount = mock_chain.first_to_act(poker_game) 170 | 171 | assert action == ActionType.RAISE 172 | assert amount == 50 173 | mock_chain.chain.invoke.assert_called_once() 174 | 175 | def test_player_check(mock_chain, poker_game): 176 | # Set up board 177 | poker_game.board = [ 178 | Card(Rank.TEN, Suit.SPADES), 179 | Card(Rank.JACK, Suit.HEARTS), 180 | Card(Rank.QUEEN, Suit.DIAMONDS) 181 | ] 182 | poker_game.round = Round.FLOP 183 | 184 | mock_chain.chain.invoke.return_value = json.dumps({ 185 | "your_hand": "Ace of Spades, King of Hearts", 186 | "opponents_hand": "Unknown", 187 | "thought_process": "I have a strong draw, should check", 188 | "action": "check" 189 | }) 190 | 191 | action, amount = mock_chain.player_check(poker_game) 192 | 193 | assert action == ActionType.CHECK 194 | assert amount is None 195 | mock_chain.chain.invoke.assert_called_once() 196 | 197 | def test_player_raise(mock_chain, poker_game): 198 | # Set up board and raise 199 | poker_game.board = [ 200 | Card(Rank.TEN, Suit.SPADES), 201 | Card(Rank.JACK, Suit.HEARTS), 202 | Card(Rank.QUEEN, Suit.DIAMONDS) 203 | ] 204 | poker_game.round = Round.FLOP 205 | poker_game.current_bet = 30 206 | 207 | mock_chain.chain.invoke.return_value = json.dumps({ 208 | "your_hand": "Ace of Spades, King of Hearts", 209 | "opponents_hand": "Possibly a pair", 210 | "thought_process": "I have a straight, should call", 211 | "action": "call" 212 | }) 213 | 214 | action, amount = mock_chain.player_raise(poker_game) 215 | 216 | assert action == ActionType.CALL 217 | assert amount is None 218 | mock_chain.chain.invoke.assert_called_once() 219 | 220 | def test_player_all_in(mock_chain, poker_game): 221 | # Set up board and all-in 222 | poker_game.board = [ 223 | Card(Rank.TEN, Suit.SPADES), 224 | Card(Rank.JACK, Suit.HEARTS), 225 | Card(Rank.QUEEN, Suit.DIAMONDS), 226 | Card(Rank.KING, Suit.CLUBS) 227 | ] 228 | poker_game.round = Round.TURN 229 | poker_game.current_bet = 1000 230 | 231 | mock_chain.chain.invoke.return_value = json.dumps({ 232 | "your_hand": "Ace of Spades, King of Hearts", 233 | "opponents_hand": "Possibly a flush draw", 234 | "thought_process": "I have the nuts, should call", 235 | "action": "call" 236 | }) 237 | 238 | action, amount = mock_chain.player_all_in(poker_game) 239 | 240 | assert action == ActionType.CALL 241 | assert amount is None 242 | mock_chain.chain.invoke.assert_called_once() -------------------------------------------------------------------------------- /bot/gpt_player.py: -------------------------------------------------------------------------------- 1 | import json 2 | from langchain_openai import ChatOpenAI 3 | from langchain_core.output_parsers import StrOutputParser 4 | from langchain.prompts.chat import ChatPromptTemplate 5 | from game.poker import PokerGameManager 6 | from db.db_utils import DatabaseManager 7 | from db.enums import ActionType 8 | 9 | class GPTPlayer: 10 | def __init__(self, db: DatabaseManager, model_name="gpt-4.1-nano"): 11 | self.db = db 12 | llm = ChatOpenAI(model_name=model_name) 13 | output_parser = StrOutputParser() 14 | template = ''' 15 | Imagine you're a poker bot in a heads-up Texas Hold'em game. Your play is optimal, 16 | mixing strategic bluffs and strong hands. You raise on strength, going All-in only with the best hands. 17 | Folding against a superior opponent hand, you call and check when fitting. Remember, only "call" the ALL-IN if your hand is better. 18 | Please reply in the following JSON format: {{your_hand": "what is the current hand you are playing", 19 | "opponents_hand": "what do you think your opponent has based on how he has played", "thought_process": "what is your thought process", 20 | "action": "your action", "raise_amount": your raise amount if applicable}} 21 | Note: If the action you chose doesn't involve a raise, please do not include the "raise_amount" key in your JSON response. 22 | ''' 23 | 24 | prompt = ChatPromptTemplate.from_messages([ 25 | ("system", template), 26 | ("user", "{input}") 27 | ]) 28 | 29 | self.chain = prompt | llm | output_parser 30 | 31 | def _extract_action(self, json_string, pokerGame: PokerGameManager): 32 | min_raise, max_raise = pokerGame.return_min_max_raise(1) 33 | try: 34 | json_data = json.loads(json_string) 35 | action_str = json_data['action'].lower() 36 | action = ActionType(action_str) 37 | 38 | raise_amount = None 39 | if action == ActionType.RAISE: 40 | raise_amount = int(json_data['raise_amount']) 41 | 42 | if raise_amount < min_raise: 43 | raise_amount = min_raise 44 | 45 | elif raise_amount >= max_raise: 46 | action = ActionType.ALL_IN 47 | raise_amount = pokerGame.return_player_stack(1) 48 | 49 | self.db.record_gpt_action(action, raise_amount, json_string) 50 | return (action, raise_amount) 51 | except Exception as erro: 52 | return ("Default", 0) 53 | 54 | 55 | def pre_flop_small_blind(self, pokerGame: PokerGameManager): 56 | # return Call, Raise, Fold or All-in 57 | inputs = { 58 | 'small_blind': pokerGame.small_blind, 59 | 'big_blind': pokerGame.big_blind, 60 | 'stack': pokerGame.return_player_stack(1), 61 | 'opponents_stack': pokerGame.return_player_stack(0), 62 | 'hand': pokerGame.players[1].return_long_hand(), 63 | 'pot': pokerGame.current_pot, 64 | 'amount_to_call': pokerGame.big_blind - pokerGame.small_blind 65 | } 66 | 67 | human_template = ''' 68 | The small blind is {small_blind} chips and the big blind is {big_blind} chips. 69 | You have {stack} chips in your stack and your opponent has {opponents_stack} chips. 70 | Your hand is {hand}. The pot is {pot} chips. 71 | You are the small blind and it's your turn. 72 | It costs {amount_to_call} chips to call. 73 | What action would you take? (Call, Raise, All-in, or Fold) 74 | ''' 75 | 76 | formatted_text = human_template.format(**inputs) 77 | response = self.chain.invoke({'input': formatted_text}) 78 | return self._extract_action(response, pokerGame) 79 | 80 | def pre_flop_big_blind(self, pokerGame: PokerGameManager): 81 | # return Check, Raise, or All-in 82 | inputs = { 83 | 'small_blind': pokerGame.small_blind, 84 | 'big_blind': pokerGame.big_blind, 85 | 'stack': pokerGame.return_player_stack(1), 86 | 'opponents_stack': pokerGame.return_player_stack(0), 87 | 'hand': pokerGame.players[1].return_long_hand(), 88 | 'pot': pokerGame.current_pot, 89 | 'amount_to_call': pokerGame.big_blind - pokerGame.small_blind 90 | } 91 | 92 | human_template = ''' 93 | The small blind is {small_blind} chips and the big blind is {big_blind} chips. 94 | You have {stack} chips in your stack and your opponent has {opponents_stack} chips. 95 | Your hand is {hand}. The pot is {pot} chips. 96 | You are the small blind and it's your turn. 97 | It costs {amount_to_call} chips to call. 98 | What action would you take? (Check, Raise, or All-in) 99 | ''' 100 | 101 | formatted_text = human_template.format(**inputs) 102 | response = self.chain.invoke({'input': formatted_text}) 103 | return self._extract_action(response, pokerGame) 104 | 105 | def first_to_act(self, pokerGame: PokerGameManager): 106 | # return Check, Raise, or All-in 107 | inputs = { 108 | 'small_blind': pokerGame.small_blind, 109 | 'big_blind': pokerGame.big_blind, 110 | 'stack': pokerGame.return_player_stack(1), 111 | 'opponents_stack': pokerGame.return_player_stack(0), 112 | 'hand': pokerGame.players[1].return_long_hand(), 113 | 'pot': pokerGame.current_pot, 114 | 'round': pokerGame.round, 115 | 'community_cards': pokerGame.return_community_cards() 116 | } 117 | 118 | human_template = ''' 119 | The small blind is {small_blind} chips and the big blind is {big_blind} chips. 120 | You have {stack} chips in your stack and your opponent has {opponents_stack} chips. 121 | Your hand is {hand}. The pot is {pot} chips. 122 | It's the {round} round and you're first to act. The community cards are {community_cards}. 123 | What action would you take? (Check, Raise, or All-in) 124 | ''' 125 | 126 | formatted_text = human_template.format(**inputs) 127 | response = self.chain.invoke({'input': formatted_text}) 128 | return self._extract_action(response, pokerGame) 129 | 130 | def player_check(self, pokerGame: PokerGameManager): 131 | # return Check, Raise, or All-in 132 | inputs = { 133 | 'small_blind': pokerGame.small_blind, 134 | 'big_blind': pokerGame.big_blind, 135 | 'stack': pokerGame.return_player_stack(1), 136 | 'opponents_stack': pokerGame.return_player_stack(0), 137 | 'hand': pokerGame.players[1].return_long_hand(), 138 | 'pot': pokerGame.current_pot, 139 | 'round': pokerGame.round, 140 | 'community_cards': pokerGame.return_community_cards() 141 | } 142 | 143 | human_template = """ 144 | The small blind is {small_blind} chips and the big blind is {big_blind} chips. 145 | You have {stack} chips in your stack and your opponent has {opponents_stack} chips. 146 | Your hand is {hand}. The pot is {pot} chips. 147 | It is the {round} round and the action checks to you. The community cards are {community_cards}. 148 | Based on this information, what action would you like to take? (Check, Raise, or All-in). 149 | """ 150 | 151 | formatted_text = human_template.format(**inputs) 152 | 153 | response = self.chain.invoke({'input': formatted_text}) 154 | return self._extract_action(response, pokerGame) 155 | 156 | def player_raise(self, pokerGame: PokerGameManager): 157 | # return Call, Raise, All-in, or Fold 158 | inputs = { 159 | 'small_blind': pokerGame.small_blind, 160 | 'big_blind': pokerGame.big_blind, 161 | 'stack': pokerGame.return_player_stack(1), 162 | 'opponents_stack': pokerGame.return_player_stack(0), 163 | 'hand': pokerGame.players[1].return_long_hand(), 164 | 'pot': pokerGame.current_pot, 165 | 'round': pokerGame.round, 166 | 'community_cards': pokerGame.return_community_cards(), 167 | 'opponent_raise': pokerGame.current_bet, 168 | 'amount_to_call': pokerGame.current_bet - pokerGame.players[1].round_pot_commitment 169 | } 170 | 171 | human_template = ''' 172 | The small blind is {small_blind} chips and the big blind is {big_blind} chips. 173 | You have {stack} chips in your stack and your opponent has {opponents_stack} chips. 174 | Your hand is {hand}. The pot is {pot} chips. 175 | It's the {round} round. The community cards are {community_cards}. 176 | Your opponent has raised to {opponent_raise} chips. 177 | It costs {amount_to_call} chips to call. 178 | What action would you take? (Call, Raise, All-in, or Fold) 179 | ''' 180 | 181 | formatted_text = human_template.format(**inputs) 182 | 183 | response = self.chain.invoke({'input': formatted_text}) 184 | return self._extract_action(response, pokerGame) 185 | 186 | def player_all_in(self, pokerGame: PokerGameManager): 187 | # return Call, or Fold 188 | amount_to_call = pokerGame.current_bet - pokerGame.players[1].round_pot_commitment 189 | if amount_to_call > pokerGame.return_player_stack(1): 190 | amount_to_call = pokerGame.return_player_stack(1) 191 | inputs = { 192 | 'small_blind': pokerGame.small_blind, 193 | 'big_blind': pokerGame.big_blind, 194 | 'stack': pokerGame.return_player_stack(1), 195 | 'hand': pokerGame.players[1].return_long_hand(), 196 | 'pot': pokerGame.current_pot, 197 | 'round': pokerGame.round, 198 | 'community_cards': pokerGame.return_community_cards(), 199 | 'opponent_raise': pokerGame.current_bet, 200 | 'amount_to_call': amount_to_call 201 | } 202 | 203 | human_template = ''' 204 | The small blind is {small_blind} chips and the big blind is {big_blind} chips. 205 | You have {stack} chips in your stack. 206 | Your hand is {hand}. The pot is {pot} chips. 207 | It's the {round} round. The community cards are {community_cards}. 208 | Your opponent has gone all in for {opponent_raise} chips. 209 | It costs {amount_to_call} chips to call. 210 | What action would you take? (Call, or Fold) 211 | ''' 212 | 213 | formatted_text = human_template.format(**inputs) 214 | 215 | response = self.chain.invoke({'input': formatted_text}) 216 | return self._extract_action(response, pokerGame) 217 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import datetime 3 | import discord 4 | from discord import Option 5 | from discord.ui import Button, View 6 | from sqlalchemy.orm import sessionmaker 7 | from sqlalchemy import create_engine 8 | from config.config import TOKEN, DEV_TOKEN, DB_HOST, DB_USER, DB_PASSWORD, DB_DATABASE 9 | from config.log_config import logger 10 | from db.db_utils import DatabaseManager 11 | from game.poker import PokerGameManager 12 | from bot.bot_poker_handler import DiscordPokerManager 13 | 14 | 15 | def parse_args(): 16 | parser = argparse.ArgumentParser(description="Run PokerGPT") 17 | parser.add_argument("--dev", action="store_true", help="Use development Discord token") 18 | parser.add_argument("--no-db", action="store_true", help="Disable database-dependent commands") 19 | return parser.parse_args() 20 | 21 | 22 | def get_token(use_dev: bool): 23 | return DEV_TOKEN if use_dev and DEV_TOKEN else TOKEN 24 | 25 | 26 | DATABASE_URL = f"mysql+mysqlconnector://{DB_USER}:{DB_PASSWORD}@{DB_HOST}/{DB_DATABASE}" 27 | engine = create_engine(DATABASE_URL, pool_pre_ping=True) 28 | Session = sessionmaker(bind=engine, expire_on_commit=False) 29 | 30 | args = parse_args() 31 | token = get_token(args.dev) 32 | bot = discord.Bot() 33 | 34 | 35 | @bot.event 36 | async def on_ready(): 37 | await bot.change_presence(activity=discord.Game(name="/play_poker")) 38 | logger.info(f"Logged in as {bot.user}") 39 | print(f"Logged in as {bot.user}") 40 | 41 | 42 | @bot.event 43 | async def on_guild_join(guild): 44 | logger.info(f"Bot added to server: {guild.name}") 45 | 46 | 47 | @bot.event 48 | async def on_guild_remove(guild): 49 | logger.info(f"Bot removed from server: {guild.name}") 50 | 51 | 52 | @bot.slash_command(name="info", description="Information about the bot") 53 | async def info(ctx): 54 | logger.info(f"{ctx.author} requested info") 55 | view = View() 56 | # Buttons for info panel 57 | view.add_item(Button( 58 | label="Add to Server", 59 | url="https://discord.com/oauth2/authorize?client_id=1102638957713432708&permissions=277025773568&scope=bot%20applications.commands" 60 | )) 61 | view.add_item(Button( 62 | label="Heads-Up Poker Rules", 63 | style=discord.ButtonStyle.url, 64 | url="https://www.wikihow.com/Heads-Up-Poker" 65 | )) 66 | view.add_item(Button( 67 | label="Source Code", 68 | style=discord.ButtonStyle.url, 69 | url="https://github.com/matteo-psnt/PokerGPT" 70 | )) 71 | view.add_item(Button( 72 | label="Feedback & Suggestions", 73 | style=discord.ButtonStyle.url, 74 | url="https://forms.gle/Cbai6VHxZt4GrewS9" 75 | )) 76 | await ctx.respond( 77 | "Hello! I am PokerGPT — play Texas Hold'em vs. me with `/play_poker`.", 78 | view=view 79 | ) 80 | 81 | 82 | @bot.slash_command(name="play_poker", description="Start a heads-up Texas Hold'em game") 83 | async def play_poker( 84 | ctx, 85 | small_blind: Option(int, description="Small blind amount", default=5, min_value=1), # type: ignore 86 | big_blind: Option(int, description="Big blind amount", default=10, min_value=1), # type: ignore 87 | small_cards: Option(bool, description="Use smaller card images", default=False) # type: ignore 88 | ): 89 | logger.info(f"{ctx.author} started game SB={small_blind} BB={big_blind}") 90 | if ctx.guild is None: 91 | logger.warning(f"DM command from {ctx.author}") 92 | return await ctx.respond("This command cannot be used in DMs.", ephemeral=True) 93 | 94 | if small_blind > big_blind: 95 | return await ctx.respond("Small blind must be less than big blind.") 96 | 97 | buy_in = 100 * big_blind 98 | await ctx.respond("Starting PokerGPT…") 99 | await ctx.send(f"Both players start with {buy_in} chips.") 100 | await ctx.send(f"The small blind is {small_blind} chips and the big blind is {big_blind} chips.") 101 | 102 | poker = PokerGameManager(buy_in, small_blind, big_blind) 103 | poker.set_player_name(0, ctx.author.name) 104 | poker.set_player_name(1, "PokerGPT") 105 | poker.new_round() 106 | 107 | session = Session() 108 | 109 | db_mgr = DatabaseManager(session, ctx.author.id, ctx.author.name, ctx.guild.id, ctx.guild.name) 110 | handler = DiscordPokerManager(ctx, poker, db_mgr, small_cards, timeout=45) 111 | 112 | await handler.play_round() 113 | 114 | 115 | # Register leaderboard and stats commands only if DB is enabled 116 | if not args.no_db: 117 | @bot.slash_command(name="player_leaderboard", description="Top 10 players by BB wins vs. PokerGPT") 118 | async def player_leaderboard(ctx): 119 | logger.info(f"{ctx.author} requested player leaderboard") 120 | if ctx.guild is None: 121 | logger.warning(f"DM command from {ctx.author}") 122 | return await ctx.respond("This command cannot be used in DMs.", ephemeral=True) 123 | 124 | session = Session() 125 | db_mgr = DatabaseManager(session, ctx.author.id, ctx.author.name, ctx.guild.id, ctx.guild.name) 126 | try: 127 | top = db_mgr.get_top_players() 128 | place = db_mgr.get_user_place() 129 | stats = db_mgr.get_user_stats_of_player() 130 | embed = discord.Embed(title="🏆 PokerGPT Leaderboard", color=discord.Color.blue()) 131 | ranks = "\n".join(f"{i + 1} **{u[0]}**" for i, u in enumerate(top)) 132 | wins = "\n".join(f"{round(u[1])}" for u in top) 133 | embed.add_field(name="Player Rank", value=ranks, inline=True) 134 | embed.add_field(name="Big Blind Wins", value=wins, inline=True) 135 | if stats: 136 | suffix = "th" if 10 <= place % 100 <= 20 else {1: "st", 2: "nd", 3: "rd"}.get(place % 10, "th") 137 | embed.add_field( 138 | name="Your Stats", 139 | value=f"Current Place: **{place}{suffix}**\nNet BB Wins: **{stats[3]:.1f}**", 140 | inline=False 141 | ) 142 | await ctx.respond(embed=embed) 143 | finally: 144 | db_mgr.close() 145 | 146 | 147 | @bot.slash_command(name="player_stats", description="Show stats for you or another player") 148 | async def player_stats( 149 | ctx, 150 | username: Option(str, description="Player username", default="self") # type: ignore 151 | ): 152 | logger.info(f"{ctx.author} requested stats for {username}") 153 | if ctx.guild is None: 154 | logger.warning(f"DM command from {ctx.author}") 155 | return await ctx.respond("This command cannot be used in DMs.", ephemeral=True) 156 | 157 | session = Session() 158 | db_mgr = DatabaseManager(session, ctx.author.id, ctx.author.name, ctx.guild.id, ctx.guild.name) 159 | try: 160 | if username == "self": 161 | row = db_mgr.get_user_stats_of_player() 162 | username = ctx.author.name 163 | else: 164 | row = db_mgr.get_user_stats_by_username(username) 165 | if not row: 166 | return await ctx.respond(f"No stats for {username}", ephemeral=True) 167 | 168 | hands = max(1, row[0]) 169 | embed = discord.Embed(title=f"📊 Stats for {username}", color=discord.Color.green()) 170 | embed.add_field(name="Hands Played", value=row[0]) 171 | embed.add_field(name="Games Played", value=row[1]) 172 | embed.add_field(name="Time Played", value=str(datetime.timedelta(seconds=row[2]))) 173 | embed.add_field(name="Net BB Total", value=f"{row[3]:.1f}") 174 | embed.add_field(name="BB Wins/Losses", value=f"{row[4]:.1f} / {row[5]:.1f}") 175 | embed.add_field(name="Win/Loss/Draw", value=f"{row[6]} / {row[7]} / {row[8]}") 176 | embed.add_field(name="Win Rate", value=f"{row[6] / hands * 100:.1f}%") 177 | embed.add_field(name="BB / Hand", value=f"{row[3] / hands:.2f}") 178 | embed.add_field(name="Hi Win/Loss Streak", value=f"{row[9]} / {row[10]}") 179 | await ctx.respond(embed=embed) 180 | finally: 181 | db_mgr.close() 182 | 183 | 184 | @bot.slash_command(name="server_leaderboard", description="Top 10 servers by BB wins vs. PokerGPT") 185 | async def server_leaderboard(ctx): 186 | logger.info(f"{ctx.author} requested server leaderboard") 187 | if ctx.guild is None: 188 | logger.warning(f"DM command from {ctx.author}") 189 | return await ctx.respond("This command cannot be used in DMs.", ephemeral=True) 190 | 191 | session = Session() 192 | db_mgr = DatabaseManager(session, ctx.author.id, ctx.author.name, ctx.guild.id, ctx.guild.name) 193 | try: 194 | top = db_mgr.get_top_servers() 195 | place = db_mgr.get_server_place() 196 | stats = db_mgr.get_server_stats() 197 | embed = discord.Embed(title="🏆 PokerGPT Server Leaderboard", color=discord.Color.gold()) 198 | ranks = "\n".join(f"{i + 1} **{s[0]}**" for i, s in enumerate(top)) 199 | wins = "\n".join(f"{round(s[1])}" for s in top) 200 | embed.add_field(name="Server Rank", value=ranks, inline=True) 201 | embed.add_field(name="Big Blind Wins", value=wins, inline=True) 202 | if stats: 203 | suffix = "th" if 10 <= place % 100 <= 20 else {1: "st", 2: "nd", 3: "rd"}.get(place % 10, "th") 204 | embed.add_field( 205 | name="Your Server", 206 | value=f"Current Place: **{place}{suffix}**\nTotal BB Wins: **{stats[4]:.1f}**", 207 | inline=False 208 | ) 209 | await ctx.respond(embed=embed) 210 | finally: 211 | db_mgr.close() 212 | 213 | 214 | @bot.slash_command(name="server_stats", description="Show stats for this or another server") 215 | async def server_stats( 216 | ctx, 217 | server_name: Option(str, name="server_name", description="Server name", default="current server") # type: ignore 218 | ): 219 | logger.info(f"{ctx.author} requested server stats for {server_name}") 220 | if ctx.guild is None: 221 | logger.warning(f"DM command from {ctx.author}") 222 | return await ctx.respond("This command cannot be used in DMs.", ephemeral=True) 223 | 224 | session = Session() 225 | db_mgr = DatabaseManager(session, ctx.author.id, ctx.author.name, ctx.guild.id, ctx.guild.name) 226 | try: 227 | if server_name == "current server": 228 | row = db_mgr.get_server_stats() 229 | server_name = ctx.guild.name 230 | else: 231 | row = db_mgr.get_server_stats_by_name(server_name) 232 | if not row: 233 | return await ctx.respond(f"No stats for {server_name}", ephemeral=True) 234 | 235 | embed = discord.Embed(title=f"📊 Stats for {server_name}", color=discord.Color.purple()) 236 | embed.add_field(name="Players", value=row[0]) 237 | embed.add_field(name="Hands Played", value=row[1]) 238 | embed.add_field(name="Time Played", value=str(datetime.timedelta(seconds=row[2]))) 239 | embed.add_field(name="Net BB Total", value=f"{row[3]:.1f}") 240 | embed.add_field(name="BB Wins/Losses", value=f"{row[4]:.1f} / {row[5]:.1f}") 241 | embed.add_field(name="Win/Loss/Draw", value=f"{row[6]} / {row[7]} / {row[8]}") 242 | await ctx.respond(embed=embed) 243 | finally: 244 | db_mgr.close() 245 | 246 | if __name__ == "__main__": 247 | bot.run(token) 248 | -------------------------------------------------------------------------------- /db/db_utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from sqlalchemy.orm import Session 3 | from sqlalchemy import func 4 | from sqlalchemy.exc import SQLAlchemyError 5 | from decimal import Decimal, ROUND_HALF_UP 6 | from db.models import ActionType, User, Server, Game, Hand, GPTAction, ServerUser 7 | from db.enums import GameResult, HandResult, Round 8 | from config.config import DATABASE_EXISTS 9 | 10 | 11 | class DatabaseManager: 12 | def __init__( 13 | self, 14 | session: Session, 15 | discord_id: str, 16 | username: str, 17 | host_id: str, 18 | server_name: str, 19 | ): 20 | self.session = session 21 | self.discord_id = discord_id 22 | self.username = username 23 | self.host_id = host_id 24 | self.server_name = server_name 25 | 26 | if DATABASE_EXISTS: 27 | self._check_or_create_user() 28 | self._check_or_create_server() 29 | self._check_or_create_server_user() 30 | 31 | def _safe_commit(self): 32 | try: 33 | self.session.commit() 34 | except SQLAlchemyError: 35 | self.session.rollback() 36 | raise 37 | 38 | def _check_or_create_user(self): 39 | self.user = ( 40 | self.session.query(User) 41 | .filter_by(discord_id=self.discord_id) 42 | .first() 43 | ) 44 | if not self.user: 45 | self.user = User(discord_id=self.discord_id, username=self.username) 46 | self.session.add(self.user) 47 | self._safe_commit() 48 | elif self.user.username != self.username: 49 | self.user.username = self.username 50 | self._safe_commit() 51 | 52 | def _check_or_create_server(self): 53 | self.server = ( 54 | self.session.query(Server) 55 | .filter_by(host_id=self.host_id) 56 | .first() 57 | ) 58 | if not self.server: 59 | self.server = Server(host_id=self.host_id, server_name=self.server_name) 60 | self.session.add(self.server) 61 | self._safe_commit() 62 | elif self.server.server_name != self.server_name: 63 | self.server.server_name = self.server_name 64 | self._safe_commit() 65 | 66 | def _check_or_create_server_user(self): 67 | self.server_user = ( 68 | self.session.query(ServerUser) 69 | .filter_by(server_id=self.server.id, user_id=self.user.id) 70 | .first() 71 | ) 72 | if not self.server_user: 73 | self.server_user = ServerUser( 74 | server_id=self.server.id, user_id=self.user.id 75 | ) 76 | self.session.add(self.server_user) 77 | self.server.total_players += 1 78 | self._safe_commit() 79 | 80 | def initialize_game(self, small_blind: int, big_blind: int, starting_stack: int): 81 | if not DATABASE_EXISTS: 82 | return 83 | self.big_blind = Decimal(big_blind) 84 | self.game_starting_stack = Decimal(starting_stack) 85 | self.game = Game( 86 | server_id=self.server.id, 87 | user_id=self.user.id, 88 | small_blind=small_blind, 89 | big_blind=big_blind, 90 | starting_stack=starting_stack, 91 | bot_version="v2.0.0", 92 | ) 93 | self.session.add(self.game) 94 | self._safe_commit() 95 | 96 | def initialize_hand(self, cards: str, gpt_cards: str, starting_stack: int): 97 | if not DATABASE_EXISTS: 98 | return 99 | self.hand_starting_stack = Decimal(starting_stack) 100 | self.hand = Hand( 101 | server_id=self.server.id, 102 | user_id=self.user.id, 103 | game_id=self.game.id, 104 | cards=cards, 105 | gpt_cards=gpt_cards, 106 | starting_stack=starting_stack, 107 | ) 108 | self.session.add(self.hand) 109 | self.game.total_hands += 1 110 | self._safe_commit() 111 | 112 | def end_hand(self, ending_stack: int, end_round: Round): 113 | if not DATABASE_EXISTS: 114 | return 115 | 116 | delta = Decimal(ending_stack) - self.hand_starting_stack 117 | net_bb = (delta / self.big_blind).quantize( 118 | Decimal("0.01"), rounding=ROUND_HALF_UP 119 | ) 120 | 121 | if net_bb > 0: 122 | result = HandResult.WIN 123 | self._update_wins(net_bb) 124 | elif net_bb < 0: 125 | result = HandResult.LOSS 126 | self._update_losses(abs(net_bb)) 127 | else: 128 | result = HandResult.SPLIT_POT 129 | self._update_draws() 130 | 131 | self.hand.ending_stack = ending_stack 132 | self.hand.net_bb = net_bb 133 | self.hand.result = result 134 | self.hand.end_round = end_round 135 | self._safe_commit() 136 | 137 | def end_game(self, ending_stack: int): 138 | if not DATABASE_EXISTS: 139 | return 140 | delta = Decimal(ending_stack) - self.game_starting_stack 141 | net_bb = (delta / self.big_blind).quantize( 142 | Decimal("0.01"), rounding=ROUND_HALF_UP 143 | ) 144 | 145 | if net_bb == Decimal(100): 146 | result = GameResult.COMPLETE_WIN 147 | elif net_bb > 0: 148 | result = GameResult.WIN 149 | elif net_bb == Decimal(-100): 150 | result = GameResult.COMPLETE_LOSS 151 | elif net_bb < 0: 152 | result = GameResult.LOSS 153 | else: 154 | result = GameResult.DRAW 155 | 156 | self.game.end_timestamp = datetime.now() 157 | self.game.ending_stack = ending_stack 158 | self.game.net_bb = net_bb 159 | self.game.result = result 160 | self._safe_commit() 161 | 162 | duration = (self.game.end_timestamp - self.game.timestamp).total_seconds() 163 | duration = Decimal(duration).quantize(Decimal("0.01")) 164 | 165 | self.user.total_time_played += duration 166 | self.user.total_games += 1 167 | self.server.total_time_played += duration 168 | 169 | self._safe_commit() 170 | self.close() 171 | 172 | def _update_wins(self, net_bb: Decimal): 173 | self.server.total_hands += 1 174 | self.server.total_wins += 1 175 | self.server.net_bb_wins = Decimal(self.server.net_bb_wins) + net_bb 176 | self.server.net_bb_total = Decimal(self.server.net_bb_total) + net_bb 177 | 178 | self.user.total_hands += 1 179 | self.user.total_wins += 1 180 | self.user.current_win_streak += 1 181 | self.user.highest_win_streak = max( 182 | self.user.highest_win_streak, self.user.current_win_streak 183 | ) 184 | self.user.current_loss_streak = 0 185 | self.user.net_bb_wins = Decimal(self.user.net_bb_wins) + net_bb 186 | self.user.net_bb_total = Decimal(self.user.net_bb_total) + net_bb 187 | 188 | self.server_user.total_hands_on_server += 1 189 | self.server_user.net_bb_wins_on_server = Decimal(self.server_user.net_bb_wins_on_server) + net_bb 190 | self.server_user.net_bb_total_on_server = Decimal(self.server_user.net_bb_total_on_server) + net_bb 191 | 192 | self._safe_commit() 193 | 194 | def _update_losses(self, net_bb: Decimal): 195 | self.server.total_hands += 1 196 | self.server.total_losses += 1 197 | self.server.net_bb_losses = Decimal(self.server.net_bb_losses) + net_bb 198 | self.server.net_bb_total = Decimal(self.server.net_bb_total) - net_bb 199 | 200 | self.user.total_hands += 1 201 | self.user.total_losses += 1 202 | self.user.current_loss_streak += 1 203 | self.user.highest_loss_streak = max( 204 | self.user.highest_loss_streak, self.user.current_loss_streak 205 | ) 206 | self.user.current_win_streak = 0 207 | self.user.net_bb_losses = Decimal(self.user.net_bb_losses) + net_bb 208 | self.user.net_bb_total = Decimal(self.user.net_bb_total) - net_bb 209 | 210 | self.server_user.total_hands_on_server += 1 211 | self.server_user.net_bb_losses_on_server = Decimal(self.server_user.net_bb_losses_on_server) + net_bb 212 | self.server_user.net_bb_total_on_server = Decimal(self.server_user.net_bb_total_on_server) - net_bb 213 | 214 | self._safe_commit() 215 | 216 | def _update_draws(self): 217 | self.server.total_hands += 1 218 | self.server.total_draws += 1 219 | 220 | self.user.total_hands += 1 221 | self.user.total_draws += 1 222 | self.user.current_win_streak = 0 223 | self.user.current_loss_streak = 0 224 | 225 | self.server_user.total_hands_on_server += 1 226 | 227 | self._safe_commit() 228 | 229 | def update_community_cards(self, community_cards: str): 230 | if not DATABASE_EXISTS: 231 | return 232 | self.hand.community_cards = community_cards 233 | self._safe_commit() 234 | 235 | def record_gpt_action(self, action_type: ActionType, raise_amount: int | None, json_data: str): 236 | if not DATABASE_EXISTS: 237 | return 238 | 239 | action = GPTAction( 240 | user_id=self.user.id, 241 | game_id=self.game.id, 242 | hand_id=self.hand.id, 243 | action_type=action_type, 244 | raise_amount=raise_amount, 245 | json_data=json_data, 246 | ) 247 | self.session.add(action) 248 | self._safe_commit() 249 | 250 | def get_top_players(self, limit=10): 251 | return self.session.query(User.username, User.net_bb_total).order_by(User.net_bb_total.desc()).limit(limit).all() 252 | 253 | def get_user_stats_of_player(self): 254 | return self.session.query( 255 | User.total_hands, User.total_games, User.total_time_played, 256 | User.net_bb_total, User.net_bb_wins, User.net_bb_losses, 257 | User.total_wins, User.total_losses, User.total_draws, 258 | User.highest_win_streak, User.highest_loss_streak 259 | ).filter_by(discord_id=self.discord_id).first() 260 | 261 | def get_user_place(self): 262 | subquery = self.session.query(User.net_bb_total).filter_by(discord_id=self.discord_id).scalar_subquery() 263 | return self.session.query(func.count(User.id)).filter(User.net_bb_total > subquery).scalar() + 1 264 | 265 | def get_user_stats_by_username(self, username): 266 | return self.session.query( 267 | User.total_hands, User.total_games, User.total_time_played, 268 | User.net_bb_total, User.net_bb_wins, User.net_bb_losses, 269 | User.total_wins, User.total_losses, User.total_draws, 270 | User.highest_win_streak, User.highest_loss_streak 271 | ).filter_by(username=username).first() 272 | 273 | def get_top_servers(self, limit=10): 274 | return self.session.query(Server.server_name, Server.net_bb_wins).order_by(Server.net_bb_wins.desc()).limit(limit).all() 275 | 276 | def get_server_stats(self): 277 | return self.session.query( 278 | Server.total_players, Server.total_hands, Server.total_time_played, 279 | Server.net_bb_total, Server.net_bb_wins, Server.net_bb_losses, 280 | Server.total_wins, Server.total_losses, Server.total_draws 281 | ).filter_by(host_id=self.host_id).first() 282 | 283 | def get_server_place(self): 284 | subquery = self.session.query(Server.net_bb_wins).filter_by(host_id=self.host_id).scalar_subquery() 285 | return self.session.query(func.count(Server.id)).filter(Server.net_bb_wins > subquery).scalar() + 1 286 | 287 | def get_server_stats_by_name(self, server_name): 288 | return self.session.query( 289 | Server.total_players, Server.total_hands, Server.total_time_played, 290 | Server.net_bb_total, Server.net_bb_wins, Server.net_bb_losses, 291 | Server.total_wins, Server.total_losses, Server.total_draws 292 | ).filter_by(server_name=server_name).first() 293 | 294 | def close(self): 295 | self.session.close() 296 | -------------------------------------------------------------------------------- /tests/test_bot_commands.py: -------------------------------------------------------------------------------- 1 | import discord 2 | import pytest 3 | from unittest.mock import AsyncMock, MagicMock, patch 4 | from bot.bot_poker_handler import DiscordPokerManager 5 | from db.enums import Round 6 | from game.card import Card, Rank, Suit 7 | from game.poker import PokerGameManager 8 | 9 | @pytest.fixture 10 | def mock_ctx(): 11 | ctx = MagicMock() 12 | ctx.send = AsyncMock() 13 | ctx.respond = AsyncMock() 14 | ctx.author = MagicMock() 15 | ctx.author.name = "TestUser" 16 | return ctx 17 | 18 | @pytest.fixture 19 | def mock_db_manager(): 20 | db = MagicMock() 21 | db.initialize_game = MagicMock() 22 | db.initialize_hand = MagicMock() 23 | db.record_gpt_action = MagicMock() 24 | db.update_community_cards = MagicMock() 25 | db.end_hand = MagicMock() 26 | db.end_game = MagicMock() 27 | return db 28 | 29 | @pytest.fixture 30 | def poker_game(): 31 | game = PokerGameManager(buy_in=1000, small_blind=5, big_blind=10) 32 | return game 33 | 34 | @pytest.fixture 35 | def discord_poker_manager(mock_ctx, poker_game, mock_db_manager): 36 | with patch('bot.bot_poker_handler.GPTPlayer'): 37 | manager = DiscordPokerManager( 38 | ctx=mock_ctx, 39 | pokerGame=poker_game, 40 | db_manager=mock_db_manager, 41 | small_cards=False, 42 | timeout=60.0, 43 | model_name="test-model" 44 | ) 45 | # Mock the card display 46 | with patch('bot.bot_poker_handler.get_cards', return_value="[Card Display]"): 47 | return manager 48 | 49 | @pytest.mark.asyncio 50 | async def test_initialization(discord_poker_manager, mock_db_manager, poker_game, mock_ctx): 51 | # Check that database was initialized properly 52 | mock_db_manager.initialize_game.assert_called_once_with( 53 | poker_game.small_blind, 54 | poker_game.big_blind, 55 | poker_game.starting_stack 56 | ) 57 | 58 | # Check that manager holds the correct references 59 | assert discord_poker_manager.ctx == mock_ctx 60 | assert discord_poker_manager.pokerGame == poker_game 61 | assert discord_poker_manager.db_manager == mock_db_manager 62 | assert discord_poker_manager.small_cards is False 63 | assert discord_poker_manager.timeout == 60.0 64 | assert discord_poker_manager.model_name == "test-model" 65 | 66 | @pytest.mark.asyncio 67 | async def test_play_round(discord_poker_manager, mock_ctx, poker_game, mock_db_manager): 68 | # Mock GPT player 69 | with patch.object(discord_poker_manager, 'pre_flop', AsyncMock()) as mock_pre_flop, \ 70 | patch('bot.bot_poker_handler.GPTPlayer') as MockGPTPlayer: 71 | 72 | # Set up GPT player mock 73 | mock_gpt_player = MockGPTPlayer.return_value 74 | 75 | # Call play_round 76 | await discord_poker_manager.play_round() 77 | 78 | # Check that new round is initialized 79 | assert discord_poker_manager.gpt_action == mock_gpt_player 80 | mock_db_manager.initialize_hand.assert_called_once() 81 | mock_pre_flop.assert_called_once() 82 | 83 | @pytest.mark.asyncio 84 | async def test_deal_community_cards_flop(discord_poker_manager, mock_ctx, poker_game): 85 | # Set up player hands for testing 86 | poker_game.players[0].card1 = Card(Rank.ACE, Suit.SPADES) 87 | poker_game.players[0].card2 = Card(Rank.KING, Suit.HEARTS) 88 | poker_game.players[1].card1 = Card(Rank.QUEEN, Suit.DIAMONDS) 89 | poker_game.players[1].card2 = Card(Rank.JACK, Suit.CLUBS) 90 | 91 | # Mock the actions 92 | with patch.object(discord_poker_manager, 'pokerGPT_acts_first', AsyncMock()) as mock_gpt_acts, \ 93 | patch.object(discord_poker_manager, 'user_acts_first', AsyncMock()) as mock_user_acts: 94 | 95 | # Test flop 96 | poker_game.button = 0 # GPT acts first 97 | await discord_poker_manager.deal_community_cards(Round.FLOP) 98 | 99 | # Check that community cards were dealt 100 | assert len(poker_game.board) == 3 101 | assert mock_ctx.send.call_count >= 3 # Multiple calls to send messages 102 | mock_gpt_acts.assert_called_once() 103 | mock_user_acts.assert_not_called() 104 | 105 | # Test with player acting first 106 | mock_gpt_acts.reset_mock() 107 | mock_user_acts.reset_mock() 108 | poker_game.board = [] # Reset board 109 | poker_game.button = 1 # Player acts first 110 | 111 | await discord_poker_manager.deal_community_cards(Round.FLOP) 112 | assert len(poker_game.board) == 3 113 | mock_gpt_acts.assert_not_called() 114 | mock_user_acts.assert_called_once() 115 | 116 | @pytest.mark.asyncio 117 | async def test_showdown(discord_poker_manager, mock_ctx, poker_game, mock_db_manager): 118 | # Set up player hands and board for testing 119 | poker_game.players[0].card1 = Card(Rank.ACE, Suit.SPADES) 120 | poker_game.players[0].card2 = Card(Rank.KING, Suit.HEARTS) 121 | poker_game.players[1].card1 = Card(Rank.QUEEN, Suit.DIAMONDS) 122 | poker_game.players[1].card2 = Card(Rank.JACK, Suit.CLUBS) 123 | poker_game.board = [ 124 | Card(Rank.TEN, Suit.SPADES), 125 | Card(Rank.NINE, Suit.HEARTS), 126 | Card(Rank.EIGHT, Suit.DIAMONDS) 127 | ] 128 | poker_game.current_pot = 100 129 | 130 | # Mock the result_embed method 131 | with patch.object(discord_poker_manager, 'result_embed', return_value=discord.Embed(title="Results")): 132 | # Call showdown 133 | await discord_poker_manager.showdown() 134 | 135 | # Check that community cards were dealt to 5 136 | assert len(poker_game.board) == 5 137 | 138 | # Verify database was updated 139 | mock_db_manager.update_community_cards.assert_called_once() 140 | mock_db_manager.end_hand.assert_called_once() 141 | 142 | # Check that context methods were called 143 | assert mock_ctx.send.call_count >= 5 # Multiple messages sent 144 | mock_ctx.respond.assert_called_once_with("Play another round?") 145 | 146 | @pytest.mark.asyncio 147 | async def test_player_wins_game(discord_poker_manager, mock_ctx, poker_game, mock_db_manager): 148 | # Set up scenario where player wins the game 149 | poker_game.players[0].card1 = Card(Rank.ACE, Suit.SPADES) 150 | poker_game.players[0].card2 = Card(Rank.KING, Suit.HEARTS) 151 | poker_game.players[1].card1 = Card(Rank.QUEEN, Suit.DIAMONDS) 152 | poker_game.players[1].card2 = Card(Rank.JACK, Suit.CLUBS) 153 | poker_game.board = [ 154 | Card(Rank.ACE, Suit.HEARTS), 155 | Card(Rank.ACE, Suit.DIAMONDS), 156 | Card(Rank.KING, Suit.SPADES), 157 | Card(Rank.QUEEN, Suit.HEARTS), 158 | Card(Rank.TWO, Suit.CLUBS) 159 | ] 160 | poker_game.current_pot = 1000 161 | poker_game.players[1].stack = 0 # Bot has no chips left 162 | 163 | # Mock the result_embed method 164 | with patch.object(discord_poker_manager, 'result_embed', return_value=discord.Embed(title="Results")): 165 | # Call showdown 166 | await discord_poker_manager.showdown() 167 | 168 | # Verify game was ended 169 | mock_db_manager.end_game.assert_called_once_with(poker_game.return_player_stack(0)) 170 | 171 | # Check that victory message was sent 172 | victory_message_sent = False 173 | for call in mock_ctx.send.call_args_list: 174 | args, kwargs = call 175 | if args and "wins the game" in args[0]: 176 | victory_message_sent = True 177 | break 178 | assert victory_message_sent 179 | 180 | @pytest.mark.asyncio 181 | async def test_result_embed_fields(discord_poker_manager, mock_ctx, poker_game): 182 | poker_game.players[0].stack = 1234 183 | poker_game.players[1].stack = 4321 184 | 185 | embed: discord.Embed = discord_poker_manager.result_embed() 186 | 187 | assert embed.title == "Results" 188 | assert len(embed.fields) == 2 189 | 190 | field_map = {f.name: f.value for f in embed.fields} 191 | assert field_map["PokerGPT"] == "4321" 192 | assert field_map[mock_ctx.author.name] == "1234" 193 | 194 | @pytest.mark.asyncio 195 | async def test_preflop_gpt_cant_cover_small_blind_button0(mock_ctx, poker_game, mock_db_manager): 196 | # button=0, GPT <= small_blind → GPT all-in on SB, user auto-calls → showdown 197 | poker_game.button = 0 198 | poker_game.small_blind = 5 199 | poker_game.big_blind = 10 200 | poker_game.players[1].stack = 3 # ≤ SB 201 | m = DiscordPokerManager(mock_ctx, poker_game, mock_db_manager, small_cards=False, timeout=1.0) 202 | m.showdown = AsyncMock() 203 | 204 | await m.pre_flop() 205 | 206 | assert poker_game.players[1].round_pot_commitment == 3 207 | assert poker_game.players[0].round_pot_commitment == 3 208 | mock_ctx.send.assert_any_call( 209 | "PokerGPT can't cover small blind and is __All-in for 3 chips.__" 210 | ) 211 | mock_ctx.send.assert_any_call(f"{poker_game.players[0].player_name} calls.") 212 | m.showdown.assert_awaited_once() 213 | 214 | @pytest.mark.asyncio 215 | async def test_preflop_player_cant_cover_small_blind_button0(mock_ctx, poker_game, mock_db_manager): 216 | # button=0, user ≤ SB → user all-in on SB, GPT auto-calls → showdown 217 | poker_game.button = 0 218 | poker_game.small_blind = 5 219 | poker_game.big_blind = 10 220 | poker_game.players[0].stack = 4 # ≤ SB 221 | m = DiscordPokerManager(mock_ctx, poker_game, mock_db_manager, small_cards=False, timeout=1.0) 222 | m.showdown = AsyncMock() 223 | 224 | await m.pre_flop() 225 | 226 | assert poker_game.players[0].round_pot_commitment == 4 227 | assert poker_game.players[1].round_pot_commitment == 4 228 | mock_ctx.send.assert_any_call( 229 | f"{poker_game.players[0].player_name} can't cover small blind and is __All-in for 4 chips.__" 230 | ) 231 | mock_ctx.send.assert_any_call("PokerGPT calls.") 232 | m.showdown.assert_awaited_once() 233 | 234 | @pytest.mark.asyncio 235 | async def test_preflop_gpt_cant_cover_big_blind_button0(mock_ctx, poker_game, mock_db_manager): 236 | # button=0, user ≥ SB but GPT ≤ BB → SB & partial-BB, prompt via allInCallView 237 | poker_game.button = 0 238 | poker_game.small_blind = 5 239 | poker_game.big_blind = 10 240 | poker_game.players[0].stack = 100 241 | poker_game.players[1].stack = 8 # ≤ BB, > SB 242 | m = DiscordPokerManager(mock_ctx, poker_game, mock_db_manager, small_cards=False, timeout=1.0) 243 | m.allInCallView = lambda *_: "ALLIN_VIEW" 244 | 245 | await m.pre_flop() 246 | 247 | assert poker_game.players[0].round_pot_commitment == 5 248 | assert poker_game.players[1].round_pot_commitment == 8 249 | mock_ctx.send.assert_any_call( 250 | "What do you want to do? You are in for 5 chips, it costs 3 more to call.", 251 | view="ALLIN_VIEW" 252 | ) 253 | 254 | @pytest.mark.asyncio 255 | async def test_preflop_player_cant_cover_big_blind_button0(mock_ctx, poker_game, mock_db_manager): 256 | # button=0, user ≥ SB but ≤ BB → SB & partial-BB, prompt via allInCallView 257 | poker_game.button = 0 258 | poker_game.small_blind = 5 259 | poker_game.big_blind = 10 260 | poker_game.players[0].stack = 7 # ≥ SB, ≤ BB 261 | poker_game.players[1].stack = 100 262 | m = DiscordPokerManager(mock_ctx, poker_game, mock_db_manager, small_cards=False, timeout=1.0) 263 | m.allInCallView = lambda *_: "ALLIN_VIEW" 264 | 265 | await m.pre_flop() 266 | 267 | assert poker_game.players[0].round_pot_commitment == 5 268 | assert poker_game.players[1].round_pot_commitment == 7 269 | mock_ctx.send.assert_any_call( 270 | "What do you want to do? You are in for 5 chips, it costs 2 more to call.", 271 | view="ALLIN_VIEW" 272 | ) 273 | 274 | @pytest.mark.asyncio 275 | async def test_preflop_regular_button0_uses_call_view(mock_ctx, poker_game, mock_db_manager): 276 | # button=0, both ≥ BB → normal SB/BB, prompt via callView 277 | poker_game.button = 0 278 | poker_game.small_blind = 5 279 | poker_game.big_blind = 10 280 | poker_game.players[0].stack = 100 281 | poker_game.players[1].stack = 100 282 | m = DiscordPokerManager(mock_ctx, poker_game, mock_db_manager, small_cards=False, timeout=1.0) 283 | m.callView = lambda *_: "CALL_VIEW" 284 | 285 | await m.pre_flop() 286 | 287 | assert poker_game.players[0].round_pot_commitment == 5 288 | assert poker_game.players[1].round_pot_commitment == 10 289 | mock_ctx.send.assert_any_call( 290 | "What do you want to do? You are in for 5", 291 | view="CALL_VIEW" 292 | ) 293 | 294 | @pytest.mark.asyncio 295 | async def test_preflop_player_cant_cover_small_blind_button1(mock_ctx, poker_game, mock_db_manager): 296 | # button=1, user ≤ SB → user all-in on SB, GPT auto-calls → showdown 297 | poker_game.button = 1 298 | poker_game.small_blind = 5 299 | poker_game.big_blind = 10 300 | poker_game.players[0].stack = 3 301 | m = DiscordPokerManager(mock_ctx, poker_game, mock_db_manager, small_cards=False, timeout=1.0) 302 | m.showdown = AsyncMock() 303 | 304 | await m.pre_flop() 305 | 306 | assert poker_game.players[0].round_pot_commitment == 3 307 | assert poker_game.players[1].round_pot_commitment == 3 308 | mock_ctx.send.assert_any_call( 309 | f"{poker_game.players[0].player_name} can't cover small blind and is __All-in for 3 chips.__" 310 | ) 311 | mock_ctx.send.assert_any_call("PokerGPT calls.") 312 | m.showdown.assert_awaited_once() 313 | 314 | @pytest.mark.asyncio 315 | async def test_preflop_gpt_cant_cover_small_blind_button1(mock_ctx, poker_game, mock_db_manager): 316 | # button=1, GPT ≤ SB → GPT all-in on SB, user auto-calls → showdown 317 | poker_game.button = 1 318 | poker_game.small_blind = 5 319 | poker_game.big_blind = 10 320 | poker_game.players[1].stack = 4 321 | m = DiscordPokerManager(mock_ctx, poker_game, mock_db_manager, small_cards=False, timeout=1.0) 322 | m.showdown = AsyncMock() 323 | 324 | await m.pre_flop() 325 | 326 | assert poker_game.players[1].round_pot_commitment == 4 327 | assert poker_game.players[0].round_pot_commitment == 4 328 | mock_ctx.send.assert_any_call( 329 | "PokerGPT can't cover the small blind and is __All-in for 4 chips.__" 330 | ) 331 | mock_ctx.send.assert_any_call(f"{poker_game.players[0].player_name} calls.") 332 | m.showdown.assert_awaited_once() 333 | 334 | @pytest.mark.asyncio 335 | async def test_preflop_player_cant_cover_big_blind_button1(mock_ctx, poker_game, mock_db_manager): 336 | # button=1, user ≥ SB but ≤ BB → user all-in on BB, GPT auto-calls → showdown 337 | poker_game.button = 1 338 | poker_game.small_blind = 5 339 | poker_game.big_blind = 10 340 | poker_game.players[0].stack = 7 341 | m = DiscordPokerManager(mock_ctx, poker_game, mock_db_manager, small_cards=False, timeout=1.0) 342 | m.showdown = AsyncMock() 343 | 344 | await m.pre_flop() 345 | 346 | assert poker_game.players[0].round_pot_commitment == 7 347 | assert poker_game.players[1].round_pot_commitment == 7 348 | mock_ctx.send.assert_any_call( 349 | f"{poker_game.players[0].player_name} is __All-in for 7 chips.__" 350 | ) 351 | mock_ctx.send.assert_any_call("PokerGPT __Calls.__") 352 | m.showdown.assert_awaited_once() 353 | 354 | @pytest.mark.asyncio 355 | async def test_preflop_gpt_cant_cover_big_blind_button1(mock_ctx, poker_game, mock_db_manager): 356 | # button=1, GPT ≥ SB but ≤ BB → GPT all-in on BB, user auto-calls → showdown 357 | poker_game.button = 1 358 | poker_game.small_blind = 5 359 | poker_game.big_blind = 10 360 | poker_game.players[1].stack = 8 361 | m = DiscordPokerManager(mock_ctx, poker_game, mock_db_manager, small_cards=False, timeout=1.0) 362 | m.showdown = AsyncMock() 363 | 364 | await m.pre_flop() 365 | 366 | assert poker_game.players[1].round_pot_commitment == 8 367 | assert poker_game.players[0].round_pot_commitment == 8 368 | mock_ctx.send.assert_any_call("You put PokerGPT __All-in for 8 chips.__") 369 | mock_ctx.send.assert_any_call("PokerGPT __Calls All-in.__") 370 | m.showdown.assert_awaited_once() -------------------------------------------------------------------------------- /game/poker.py: -------------------------------------------------------------------------------- 1 | from db.enums import Round 2 | from game.player import * 3 | 4 | class Dealer: 5 | def __init__(self, num_players: int, buy_in: int = 1000): 6 | self.deck = Deck() 7 | self.players = [Player("Player " + str(_ + 1), buy_in) for _ in range(num_players)] 8 | for player in self.players: 9 | player.deal_hand(self.deck) 10 | self.board = [] 11 | 12 | # sets player name 13 | def set_player_name(self, player: int, name: str): 14 | self.players[player].player_name = name 15 | 16 | # deals a new cards 17 | def new_deal(self): 18 | self.deck = Deck() 19 | for player in self.players: 20 | player.deal_hand(self.deck) 21 | self.board = [] 22 | 23 | # deals the board with num_cards 24 | def deal_board(self, num_cards: int = 5): 25 | for _ in range(num_cards - len(self.board)): 26 | self.board.append(self.deck.deal_card()) 27 | 28 | # returns the player's stack 29 | def return_player_stack(self, player: int): 30 | return self.players[player].stack 31 | 32 | # returns the player's hand as a list 33 | def return_player_hand(self, player: int): 34 | return self.players[player].return_hand() 35 | 36 | # returns the player's hand as a str 37 | def return_player_hand_str(self, player: int): 38 | return (", ".join(map(str, self.players[player].return_hand()))) 39 | 40 | # returns the board as a string 41 | def return_community_cards(self): 42 | return(", ".join(map(str, self.board))) 43 | 44 | # returns the hand rank and the cards played for a player 45 | def get_hand_rank(self, player : Player): 46 | hand_rank = "" 47 | hand_played = [] 48 | 49 | # Get all cards 50 | all_cards = self.board.copy() 51 | all_cards.append(player.card1) 52 | all_cards.append(player.card2) 53 | 54 | # Sort cards by rank 55 | all_cards.sort(reverse=True) 56 | 57 | # Check for straight flush 58 | highest_straight_rank = -1 59 | 60 | for suit in Suit: 61 | # Get all cards of the same suit 62 | suit_cards = [card for card in all_cards if card.suit == suit] 63 | if len(suit_cards) >= 5: 64 | for i in range(len(suit_cards)): 65 | for j in range(i + 1, len(suit_cards)): 66 | for k in range(j + 1, len(suit_cards)): 67 | for l in range(k + 1, len(suit_cards)): 68 | for m in range(l + 1, len(suit_cards)): 69 | # checks for straight 70 | if (suit_cards[i] == suit_cards[j] + 1 and suit_cards[i] == suit_cards[k] + 2 and suit_cards[i] == suit_cards[l] + 3 and suit_cards[i] == suit_cards[m] + 4): 71 | 72 | # Check if the current straight is higher than the previous highest straight 73 | if suit_cards[i].rank.value > highest_straight_rank: 74 | hand_rank = handRank.STRAIGHT_FLUSH 75 | highest_straight_rank = suit_cards[i].rank.value 76 | hand_played = [suit_cards[i], suit_cards[j], suit_cards[k], suit_cards[l], suit_cards[m]] 77 | 78 | # considers edge case of Royal Flush 79 | if (suit_cards[i] == Rank.ACE and suit_cards[j] == Rank.KING and suit_cards[k] == Rank.QUEEN and 80 | suit_cards[l] == Rank.JACK and suit_cards[m] == Rank.TEN): 81 | hand_rank = handRank.ROYAL_FLUSH 82 | hand_played = [suit_cards[i], suit_cards[j], suit_cards[k], suit_cards[l], suit_cards[m]] 83 | return hand_rank, hand_played 84 | # considers edge case of ace low straight 85 | if (suit_cards[i] == Rank.ACE and suit_cards[j] == Rank.FIVE and suit_cards[k] == Rank.FOUR and 86 | suit_cards[l] == Rank.THREE and suit_cards[m] == Rank.TWO): 87 | if Rank.FIVE.value > highest_straight_rank: 88 | highest_straight_rank = Rank.FIVE.value 89 | hand_rank = handRank.STRAIGHT_FLUSH 90 | hand_played = [suit_cards[j], suit_cards[k], suit_cards[l], suit_cards[m], suit_cards[i]] 91 | if highest_straight_rank != -1: 92 | return hand_rank, hand_played 93 | 94 | 95 | # Check for four of a kind and return highest kicker 96 | for i in range(len(all_cards) - 3): 97 | if all_cards[i].rank == all_cards[i + 3].rank: 98 | hand_rank = handRank.FOUR_OF_A_KIND 99 | hand_played = all_cards[i : i + 4] 100 | if (all_cards[0] != all_cards[i]): 101 | hand_played.append(all_cards[0]) 102 | else: 103 | hand_played.append(all_cards[4]) 104 | return hand_rank, hand_played 105 | 106 | 107 | # Check for full house 108 | for i in range(len(all_cards) - 2): 109 | for i in range(len(all_cards) - 2): 110 | if all_cards[i].rank == all_cards[i + 2].rank: 111 | for j in range(len(all_cards) - 1): 112 | if all_cards[j].rank == all_cards[j + 1].rank and all_cards[i].rank != all_cards[j].rank: 113 | hand_rank = handRank.FULL_HOUSE 114 | hand_played = all_cards[i : i + 3] 115 | hand_played.append(all_cards[j]) 116 | hand_played.append(all_cards[j + 1]) 117 | return hand_rank, hand_played 118 | 119 | 120 | # Check for flush 121 | for suit in Suit: 122 | suit_cards = [card for card in all_cards if card.suit == suit] 123 | if len(suit_cards) >= 5: 124 | hand_rank = handRank.FLUSH 125 | hand_played = suit_cards[:5] 126 | return hand_rank, hand_played 127 | 128 | 129 | # Check for straight 130 | highest_straight_rank = -1 131 | 132 | for i in range(len(all_cards)): 133 | for j in range(i + 1, len(all_cards)): 134 | for k in range(j + 1, len(all_cards)): 135 | for l in range(k + 1, len(all_cards)): 136 | for m in range(l + 1, len(all_cards)): 137 | # checks for straight 138 | if (all_cards[i] == all_cards[j] + 1 and all_cards[i] == all_cards[k] + 2 and all_cards[i] == all_cards[l] + 3 and all_cards[i] == all_cards[m] + 4): 139 | 140 | # Check if the current straight is higher than the previous highest straight 141 | if all_cards[i].rank.value > highest_straight_rank: 142 | hand_rank = handRank.STRAIGHT 143 | highest_straight_rank = all_cards[i].rank.value 144 | hand_played = [all_cards[i], all_cards[j], all_cards[k], all_cards[l], all_cards[m]] 145 | 146 | # considers edge case of ace high straight 147 | if (all_cards[i] == Rank.ACE and all_cards[j] == Rank.KING and all_cards[k] == Rank.QUEEN and 148 | all_cards[l] == Rank.JACK and all_cards[m] == Rank.TEN): 149 | hand_rank = handRank.STRAIGHT 150 | hand_played = [all_cards[i], all_cards[j], all_cards[k], all_cards[l], all_cards[m]] 151 | return hand_rank, hand_played 152 | 153 | # considers edge case of ace low straight 154 | if (all_cards[i] == Rank.ACE and all_cards[j] == Rank.FIVE and all_cards[k] == Rank.FOUR and 155 | all_cards[l] == Rank.THREE and all_cards[m] == Rank.TWO): 156 | highest_straight_rank = Rank.FIVE.value 157 | hand_rank = handRank.STRAIGHT 158 | hand_played = [all_cards[j], all_cards[k], all_cards[l], all_cards[m], all_cards[i]] 159 | 160 | if highest_straight_rank != -1: 161 | return hand_rank, hand_played 162 | 163 | 164 | # Check for highest three of a kind and return highest kicker 165 | for i in range(len(all_cards) - 2): 166 | if all_cards[i].rank == all_cards[i + 2].rank: 167 | hand_rank = handRank.THREE_OF_A_KIND 168 | hand_played = all_cards[i : i + 3] 169 | if (all_cards[0] != all_cards[i] and all_cards[1] != all_cards[i]): 170 | hand_played.append(all_cards[0]) 171 | hand_played.append(all_cards[1]) 172 | elif (all_cards[0] != all_cards[i] and all_cards[4] != all_cards[i]): 173 | hand_played.append(all_cards[0]) 174 | hand_played.append(all_cards[4]) 175 | else: 176 | hand_played.append(all_cards[3]) 177 | hand_played.append(all_cards[4]) 178 | 179 | return hand_rank, hand_played 180 | 181 | 182 | # Check for two pair and return highest kicker 183 | for i in range(len(all_cards) - 1): 184 | if all_cards[i].rank == all_cards[i + 1].rank: 185 | for j in range(i + 2, len(all_cards) - 1): 186 | if all_cards[j].rank == all_cards[j + 1].rank: 187 | hand_rank = handRank.TWO_PAIR 188 | hand_played = all_cards[i : i + 2] 189 | hand_played.append(all_cards[j]) 190 | hand_played.append(all_cards[j + 1]) 191 | if (all_cards[0] != all_cards[i] and all_cards[0] != all_cards[j]): 192 | hand_played.append(all_cards[0]) 193 | elif (all_cards[2] != all_cards[i] and all_cards[2] != all_cards[j]): 194 | hand_played.append(all_cards[2]) 195 | else: 196 | hand_played.append(all_cards[4]) 197 | return hand_rank, hand_played 198 | 199 | 200 | # Check for pair and return highest kicker 201 | for i in range(len(all_cards) - 1): 202 | if all_cards[i].rank == all_cards[i + 1].rank: 203 | hand_rank = handRank.PAIR 204 | hand_played = all_cards[i : i + 2] 205 | if (all_cards[0] != all_cards[i] and all_cards[1] != all_cards[i] and all_cards[2] != all_cards[i]): 206 | hand_played.append(all_cards[0]) 207 | hand_played.append(all_cards[1]) 208 | hand_played.append(all_cards[2]) 209 | elif (all_cards[2] != all_cards[i] and all_cards[3] != all_cards[i] and all_cards[4] != all_cards[i]): 210 | hand_played.append(all_cards[2]) 211 | hand_played.append(all_cards[3]) 212 | hand_played.append(all_cards[4]) 213 | elif (all_cards[0] != all_cards[i] and all_cards[3] != all_cards[i] and all_cards[4] != all_cards[i]): 214 | hand_played.append(all_cards[0]) 215 | hand_played.append(all_cards[3]) 216 | hand_played.append(all_cards[4]) 217 | else: 218 | hand_played.append(all_cards[0]) 219 | hand_played.append(all_cards[1]) 220 | hand_played.append(all_cards[4]) 221 | return hand_rank, hand_played 222 | 223 | # Return highest card 224 | hand_rank = handRank.HIGH_CARD 225 | hand_played = all_cards[0:5] 226 | return hand_rank, hand_played 227 | 228 | # evaluate the hands of all players 229 | def evaluate_hands(self): 230 | for player in self.players: 231 | player.hand_rank, player.hand_played = self.get_hand_rank(player) 232 | 233 | # return the winner of the hand and consider tiebreakers 234 | def determine_winner(self): 235 | winner = self.players[0] 236 | for player in self.players: 237 | if player.hand_rank > winner.hand_rank: 238 | winner = player 239 | elif player.hand_rank == winner.hand_rank: 240 | for i in range(len(player.hand_played)): 241 | if player.hand_played[i] > winner.hand_played[i]: 242 | winner = player 243 | break 244 | elif player.hand_played[i] < winner.hand_played[i]: 245 | break 246 | # Check for ties 247 | tiedPlayers = [winner] 248 | for player in self.players: 249 | if player == winner: 250 | continue 251 | if player.hand_rank == winner.hand_rank: 252 | tie = True 253 | for i in range(len(player.hand_played)): 254 | if player.hand_played[i] != winner.hand_played[i]: 255 | tie = False 256 | break 257 | if tie: 258 | tiedPlayers.append(player) 259 | if len(tiedPlayers) > 1: 260 | return tiedPlayers 261 | return winner 262 | 263 | class PokerGameManager(Dealer): 264 | def __init__(self, buy_in: int = 1000, small_blind: int = 5, big_blind: int = 10): 265 | super().__init__(2, buy_in) 266 | self.starting_stack = buy_in 267 | self.small_blind = small_blind 268 | self.big_blind = big_blind 269 | self.button = 0 270 | self.current_action = 0 271 | self.round = Round.PRE_FLOP 272 | self.current_pot = 0 273 | self.current_bet = 0 274 | 275 | def return_min_max_raise(self, player: int): 276 | min_raise = self.current_bet * 2 277 | max_raise = self.players[1].stack + self.players[1].round_pot_commitment 278 | return (min_raise, max_raise) 279 | 280 | def new_round(self): 281 | self.new_deal() 282 | self.current_pot = 0 283 | self.current_bet = 0 284 | self.button = (self.button + 1) % len(self.players) 285 | self.current_action = self.button 286 | self.round = Round.PRE_FLOP 287 | 288 | def reset_betting(self): 289 | self.current_bet = 0 290 | for player in self.players: 291 | player.round_pot_commitment = 0 292 | 293 | # puts chips from player stack into the pot 294 | def player_bet(self, player: int, amount: int): 295 | self.current_pot += amount 296 | self.players[player].bet(amount) 297 | 298 | # calls the current bet 299 | def player_call(self, player: int): 300 | if self.players[player].stack + self.players[player].round_pot_commitment < self.current_bet: 301 | self.player_all_in_call(player) 302 | return 303 | amount_to_call = self.current_bet - self.players[player].round_pot_commitment 304 | self.player_bet(player, amount_to_call) 305 | 306 | # raises the current bet to the amount 307 | def player_raise(self, player: int, amount: int): 308 | self.current_bet = amount 309 | amount_raised = amount - self.players[player].round_pot_commitment 310 | self.player_bet(player, amount_raised) 311 | 312 | # player goes all in as a call and matches other player's bet 313 | def player_all_in_call(self, player: int): 314 | total_chips = self.players[player].stack + self.players[player].round_pot_commitment 315 | other_player = (player + 1) % 2 316 | if total_chips < self.current_bet: 317 | chips_not_covered = self.current_bet - total_chips 318 | self.players[other_player].round_pot_commitment = total_chips 319 | self.players[other_player].stack += chips_not_covered 320 | self.current_pot -= chips_not_covered 321 | self.current_bet = total_chips 322 | self.player_bet(player, total_chips- self.players[player].round_pot_commitment) 323 | else: 324 | self.player_call(player) 325 | 326 | # player goes all in as a raise 327 | def player_all_in_raise(self, player: int): 328 | total_raise = self.players[player].stack + self.players[player].round_pot_commitment 329 | self.player_raise(player, total_raise) 330 | 331 | # gives winning player the pot 332 | def player_win(self, player): 333 | if isinstance(player, int): 334 | self.players[player].stack += self.current_pot 335 | elif isinstance(player, Player): 336 | player.stack += self.current_pot 337 | elif isinstance(player, list): 338 | for p in player: 339 | p.stack += self.current_pot // len(player) -------------------------------------------------------------------------------- /bot/bot_poker_handler.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord import ButtonStyle, Interaction 3 | from discord.ui import InputText, View 4 | from bot.card_display import get_cards 5 | from bot.gpt_player import GPTPlayer 6 | from config.log_config import logger 7 | from db.db_utils import DatabaseManager 8 | from db.enums import ActionType, Round 9 | from game.poker import PokerGameManager 10 | 11 | 12 | class DiscordPokerManager: 13 | def __init__(self, ctx, pokerGame: PokerGameManager, db_manager: DatabaseManager, small_cards: bool, timeout: float, 14 | model_name: str = "gpt-4.1-nano"): 15 | self.ctx = ctx 16 | self.pokerGame: PokerGameManager = pokerGame 17 | self.db_manager: DatabaseManager = db_manager 18 | self.small_cards: bool = small_cards 19 | self.timeout: float = timeout 20 | self.model_name: str = model_name 21 | 22 | db_manager.initialize_game(pokerGame.small_blind, pokerGame.big_blind, pokerGame.starting_stack) 23 | 24 | async def play_round(self): 25 | self.pokerGame.new_round() 26 | self.gpt_action = GPTPlayer(self.db_manager, model_name=self.model_name) 27 | self.db_manager.initialize_hand(self.pokerGame.return_player_hand_str(0), self.pokerGame.return_player_hand_str(1), self.pokerGame.return_player_stack(0)) 28 | logger.info(f"{self.ctx.author.name} - Starting a new round.") 29 | logger.info(f"{self.ctx.author.name} - Player has {self.pokerGame.return_player_stack(0)} chips, PokerGPT has {self.pokerGame.return_player_stack(1)} chips.") 30 | await self.pre_flop() 31 | 32 | async def pre_flop(self): 33 | logger.info(f"{self.ctx.author.name} - Pre-flop") 34 | self.pokerGame.round = Round.PRE_FLOP 35 | self.pokerGame.reset_betting() 36 | await self.ctx.send("**Your Cards:**") 37 | await self.ctx.send(get_cards(self.pokerGame.return_player_hand(0), self.small_cards)) 38 | logger.info(f"{self.ctx.author.name} - Player has {self.pokerGame.return_player_hand_str(0)}, PokerGPT has {self.pokerGame.return_player_hand_str(1)}") 39 | if self.pokerGame.button == 0: # Player is small blind, PokerGPT is big blind 40 | # PokerGPT can't cover small blind 41 | if self.pokerGame.return_player_stack(1) <= self.pokerGame.small_blind: 42 | logger.info(f"{self.ctx.author.name} - PokerGPT can't cover small blind and is all-in for {self.pokerGame.return_player_stack(1)} chips.") 43 | await self.ctx.send(f"PokerGPT can't cover small blind and is __All-in for {self.pokerGame.return_player_stack(1)} chips.__") 44 | await self.ctx.send(f"{self.pokerGame.players[0].player_name} calls.") 45 | self.pokerGame.player_raise(1, self.pokerGame.return_player_stack(1)) 46 | self.pokerGame.player_call(0) 47 | return await self.showdown() 48 | 49 | # Player can't cover small blind 50 | if self.pokerGame.return_player_stack(0) <= self.pokerGame.small_blind: 51 | logger.info(f"{self.ctx.author.name} - Player can't cover small blind and is all-in for {self.pokerGame.return_player_stack(0)} chips.") 52 | await self.ctx.send(f"{self.pokerGame.players[0].player_name} can't cover small blind and is __All-in for {self.pokerGame.return_player_stack(0)} chips.__") 53 | await self.ctx.send(f"PokerGPT calls.") 54 | self.pokerGame.player_raise(0, self.pokerGame.return_player_stack(0)) 55 | self.pokerGame.player_call(1) 56 | return await self.showdown() 57 | 58 | # PokerGPT can't cover big blind 59 | if self.pokerGame.return_player_stack(1) <= self.pokerGame.big_blind: 60 | await self.ctx.send(f"{self.pokerGame.players[0].player_name} places small blind of {self.pokerGame.small_blind} chips.") 61 | await self.ctx.send(f"PokerGPT can't cover big blind and is __All-in for {self.pokerGame.return_player_stack(1)} chips.__") 62 | self.pokerGame.player_raise(0, self.pokerGame.small_blind) 63 | self.pokerGame.player_raise(1, self.pokerGame.return_player_stack(1)) 64 | view = self.allInCallView(self) 65 | await self.ctx.send(f"You have {self.pokerGame.return_player_stack(0)} chips.") 66 | await self.ctx.send(f"What do you want to do? You are in for {self.pokerGame.players[0].round_pot_commitment} chips, it costs {self.pokerGame.players[1].round_pot_commitment - self.pokerGame.players[0].round_pot_commitment} more to call.", view=view) 67 | return 68 | 69 | # Player can't cover big blind 70 | if self.pokerGame.return_player_stack(0) <= self.pokerGame.big_blind: 71 | await self.ctx.send(f"{self.pokerGame.players[0].player_name} places small blind of {self.pokerGame.small_blind} chips.") 72 | await self.ctx.send(f"PokerGPT puts you __All-in for {self.pokerGame.return_player_stack(0) + self.pokerGame.players[0].round_pot_commitment} chips.__") 73 | self.pokerGame.player_raise(0, self.pokerGame.small_blind) 74 | self.pokerGame.player_raise(1, self.pokerGame.return_player_stack(0) + self.pokerGame.players[0].round_pot_commitment) 75 | view = self.allInCallView(self) 76 | await self.ctx.send(f"You have {self.pokerGame.return_player_stack(0)} chips.") 77 | await self.ctx.send(f"What do you want to do? You are in for {self.pokerGame.players[0].round_pot_commitment} chips, it costs {self.pokerGame.players[1].round_pot_commitment - self.pokerGame.players[0].round_pot_commitment} more to call.", view=view) 78 | return 79 | 80 | # Regular scenario, both players can cover blinds 81 | await self.ctx.send(f"PokerGPT places big blind of {self.pokerGame.big_blind} chips, and {self.pokerGame.players[0].player_name} places small blind of {self.pokerGame.small_blind} chips.") 82 | self.pokerGame.player_raise(0, self.pokerGame.small_blind) 83 | self.pokerGame.player_raise(1, self.pokerGame.big_blind) 84 | 85 | view = self.callView(self) 86 | await self.ctx.send(f"You have {self.pokerGame.return_player_stack(0)} chips.") 87 | await self.ctx.send(f"What do you want to do? You are in for {self.pokerGame.players[0].round_pot_commitment}", view=view) 88 | 89 | elif self.pokerGame.button == 1: # PokerGPT is small blind, player is big blind 90 | # Player can't cover small blind 91 | if self.pokerGame.return_player_stack(0) <= self.pokerGame.small_blind: 92 | logger.info(f"{self.ctx.author.name} - Player can't cover small blind and is all-in for {self.pokerGame.return_player_stack(0)} chips.") 93 | await self.ctx.send(f"{self.pokerGame.players[0].player_name} can't cover small blind and is __All-in for {self.pokerGame.return_player_stack(0)} chips.__") 94 | await self.ctx.send(f"PokerGPT calls.") 95 | self.pokerGame.player_raise(0, self.pokerGame.return_player_stack(0)) 96 | self.pokerGame.player_call(1) 97 | return await self.showdown() 98 | 99 | # PokerGPT can't cover the small blind 100 | if self.pokerGame.return_player_stack(1) <= self.pokerGame.small_blind: 101 | await self.ctx.send(f"PokerGPT can't cover the small blind and is __All-in for {self.pokerGame.return_player_stack(1)} chips.__") 102 | await self.ctx.send(f"{self.pokerGame.players[0].player_name} calls.") 103 | self.pokerGame.player_raise(1, self.pokerGame.return_player_stack(1)) 104 | self.pokerGame.player_call(0) 105 | return await self.showdown() 106 | 107 | # Player can't cover the big blind 108 | if self.pokerGame.return_player_stack(0) <= self.pokerGame.big_blind: 109 | await self.ctx.send(f"PokerGPT places the small blind of {self.pokerGame.small_blind} chips.") 110 | await self.ctx.send(f"{self.pokerGame.players[0].player_name} is __All-in for {self.pokerGame.return_player_stack(0)} chips.__") 111 | self.pokerGame.player_raise(1, self.pokerGame.small_blind) 112 | self.pokerGame.player_raise(0, self.pokerGame.return_player_stack(0)) 113 | self.pokerGame.player_call(1) 114 | await self.ctx.send("PokerGPT __Calls.__") 115 | return await self.showdown() 116 | 117 | # PokerGPT can't cover the big blind 118 | if self.pokerGame.return_player_stack(1) <= self.pokerGame.big_blind: 119 | await self.ctx.send(f"PokerGPT places small blind of {self.pokerGame.small_blind} chips.") 120 | await self.ctx.send(f"You put PokerGPT __All-in for {self.pokerGame.return_player_stack(1) + self.pokerGame.players[1].round_pot_commitment} chips.__") 121 | await self.ctx.send(f"PokerGPT __Calls All-in.__") 122 | self.pokerGame.player_raise(0, self.pokerGame.small_blind) 123 | self.pokerGame.player_raise(1, self.pokerGame.return_player_stack(1)) 124 | self.pokerGame.player_call(0) 125 | return await self.showdown() 126 | 127 | # Regular scenario, both players can cover blinds 128 | await self.ctx.send(f"{self.pokerGame.players[0].player_name} places big blind of {self.pokerGame.big_blind} chips, and PokerGPT places small blind of {self.pokerGame.small_blind} chips.") 129 | self.pokerGame.player_raise(1, self.pokerGame.small_blind) 130 | self.pokerGame.player_raise(0, self.pokerGame.big_blind) 131 | 132 | action, raise_amount = self.gpt_action.pre_flop_small_blind(self.pokerGame) 133 | if action == ActionType.CALL: 134 | logger.info(f"{self.ctx.author.name} - PokerGPT Calls.") 135 | await self.ctx.send("PokerGPT __Calls.__") 136 | self.pokerGame.player_call(1) 137 | await self.next_action() 138 | elif action == ActionType.ALL_IN: 139 | await self.pokerGPT_all_in() 140 | elif action == ActionType.FOLD: 141 | await self.pokerGPT_fold() 142 | elif action == ActionType.RAISE: 143 | await self.pokerGPT_raise(raise_amount) 144 | else: 145 | logger.warning(f"{self.ctx.author.name} - Error move given: {action}, {raise_amount}, doing Default move of: Fold") 146 | await self.pokerGPT_fold() 147 | 148 | async def deal_community_cards(self, round_name: Round): 149 | # Set the current round and deal the community cards 150 | self.pokerGame.round = round_name 151 | self.pokerGame.reset_betting() 152 | if round_name == Round.FLOP: 153 | self.pokerGame.deal_board(3) 154 | elif round_name == Round.TURN: 155 | self.pokerGame.deal_board(4) 156 | elif round_name == Round.RIVER: 157 | self.pokerGame.deal_board(5) 158 | 159 | # Announce the community cards 160 | logger.info(f"{self.ctx.author.name} - {round_name.value.capitalize()} {self.pokerGame.return_community_cards()}") 161 | await self.ctx.send(f"**Community Cards ({round_name.value.capitalize()}):**") 162 | await self.ctx.send(get_cards(self.pokerGame.board, self.small_cards)) 163 | 164 | # Announce the current pot and player stacks 165 | await self.ctx.send(f"**Main pot:** {self.pokerGame.current_pot} chips.") 166 | await self.ctx.send(f"**{self.pokerGame.players[0].player_name} stack:** {self.pokerGame.return_player_stack(0)} chips.") 167 | await self.ctx.send(f"**PokerGPT stack:** {self.pokerGame.return_player_stack(1)} chips.") 168 | 169 | # Determine who is first to act and prompt them for their move 170 | if self.pokerGame.button == 0: 171 | await self.pokerGPT_acts_first() 172 | elif self.pokerGame.button == 1: 173 | await self.user_acts_first() 174 | 175 | async def showdown(self): 176 | await self.ctx.send("***Showdown!!***") 177 | self.pokerGame.round = Round.SHOWDOWN 178 | 179 | # Deal and Display the community cards 180 | self.pokerGame.deal_board(5) 181 | await self.ctx.send("**Community Cards:**") 182 | await self.ctx.send(get_cards(self.pokerGame.board, self.small_cards)) 183 | logger.info(f"{self.ctx.author.name} - Showdown {self.pokerGame.return_community_cards()}") 184 | 185 | # Evaluate each player's hand 186 | self.pokerGame.evaluate_hands() 187 | 188 | # Display each player's hand and hand rank 189 | for player in self.pokerGame.players: 190 | await self.ctx.send(f"{player.player_name} has:") 191 | await self.ctx.send(get_cards(player.return_hand(), self.small_cards)) 192 | 193 | await self.ctx.send(f"**{player.hand_rank}**") 194 | await self.ctx.send(get_cards(player.hand_played, self.small_cards)) 195 | logger.info(f"{self.ctx.author.name} - {player.player_name} has {player.hand_rank}") 196 | 197 | # Determine the winner(s) and handle the pot 198 | winner = self.pokerGame.determine_winner() 199 | if isinstance(winner, list): 200 | # Split pot 201 | logger.info(f"{self.ctx.author.name} - Split pot") 202 | await self.ctx.send("**Split pot!!!**") 203 | split_pot = self.pokerGame.current_pot // 2 204 | self.pokerGame.player_win(winner) 205 | await self.ctx.send(f"{self.pokerGame.players[0].player_name} wins {split_pot} chips and has {self.pokerGame.return_player_stack(0)} chips.") 206 | await self.ctx.send(f"PokerGPT wins {split_pot} chips and has {self.pokerGame.return_player_stack(1)} chips.") 207 | 208 | else: 209 | # Single winner 210 | pot = self.pokerGame.current_pot 211 | self.pokerGame.player_win(winner) 212 | logger.info(f"{self.ctx.author.name} - {winner.player_name} wins {pot} chips") 213 | await self.ctx.send(f"{winner.player_name} wins **{pot} chips** and has {winner.stack} chips.") 214 | 215 | # Check if either player is out of chips 216 | self.db_manager.update_community_cards(self.pokerGame.return_community_cards()) 217 | self.db_manager.end_hand(self.pokerGame.return_player_stack(0), Round.SHOWDOWN) 218 | embed = self.result_embed() 219 | if self.pokerGame.return_player_stack(0) == 0: 220 | await self.ctx.send(f"{self.pokerGame.players[1].player_name} wins the game! {self.pokerGame.players[0].player_name} is out of chips.", embeds=[embed]) 221 | self.db_manager.end_game(self.pokerGame.return_player_stack(0)) 222 | elif self.pokerGame.return_player_stack(1) == 0: 223 | await self.ctx.send(f"{self.pokerGame.players[0].player_name} wins the game! {self.pokerGame.players[1].player_name} is out of chips.", embeds=[embed]) 224 | self.db_manager.end_game(self.pokerGame.return_player_stack(0)) 225 | else: 226 | # Prompt to play another round 227 | await self.ctx.respond("Play another round?") 228 | await self.ctx.send("", view=self.newRoundView(self)) 229 | 230 | async def user_acts_first(self): 231 | view = self.checkView(self) 232 | await self.ctx.send(f"What do you want to do?", view=view) 233 | 234 | async def pokerGPT_acts_first(self): 235 | action, raise_amount = self.gpt_action.first_to_act(self.pokerGame) 236 | 237 | if action == ActionType.CHECK: 238 | logger.info(f"{self.ctx.author.name} - PokerGPT Checks.") 239 | await self.ctx.send("PokerGPT __Checks.__") 240 | await self.next_action() 241 | elif action == ActionType.ALL_IN: 242 | await self.pokerGPT_all_in() 243 | elif action == ActionType.RAISE: 244 | await self.pokerGPT_raise(raise_amount) 245 | else: 246 | logger.warning(f"{self.ctx.author.name} - Error move given: {action}, {raise_amount}, doing Default move of: Check") 247 | await self.ctx.send("PokerGPT __Checks.__") 248 | await self.next_action() 249 | 250 | async def user_raise(self, amount: int): 251 | logger.info(f"{self.ctx.author.name} - User raises to {amount} chips") 252 | # Raise the player's bet 253 | self.pokerGame.player_raise(0, amount) 254 | 255 | # Get GPT's move and handle it 256 | action, raise_amount = self.gpt_action.player_raise(self.pokerGame) 257 | 258 | if action == ActionType.CALL: 259 | logger.info(f"{self.ctx.author.name} - PokerGPT Calls.") 260 | await self.ctx.send("PokerGPT __Calls Raise.__") 261 | self.pokerGame.player_call(1) 262 | await self.next_action() 263 | elif action == ActionType.FOLD: 264 | await self.pokerGPT_fold() 265 | elif action == ActionType.ALL_IN: 266 | await self.pokerGPT_all_in() 267 | elif action == ActionType.RAISE: 268 | await self.pokerGPT_raise(raise_amount) 269 | else: 270 | logger.warning(f"{self.ctx.author.name} - Error move given: {action}, {raise_amount}, doing Default move of: Fold") 271 | await self.pokerGPT_fold() 272 | 273 | async def pokerGPT_raise(self, amount: int): 274 | logger.info(f"{self.ctx.author.name} - PokerGPT raises to {amount} chips") 275 | # Raise the bet and announce it 276 | await self.ctx.send(f"PokerGPT __Raises to {amount} chips.__") 277 | self.pokerGame.player_raise(1, amount) 278 | await self.ctx.send(f"**Main pot:** {self.pokerGame.current_pot} chips") 279 | 280 | # Check if the player needs to go all-in 281 | if (self.pokerGame.return_player_stack(0) + self.pokerGame.players[0].round_pot_commitment <= self.pokerGame.current_bet): 282 | await self.ctx.send(f"PokerGPT puts you __All-In for {self.pokerGame.return_player_stack(0) + self.pokerGame.players[0].round_pot_commitment} chips.__") 283 | view = self.allInCallView(self) 284 | else: 285 | view = self.callView(self) 286 | 287 | # Prompt the player for their action 288 | await self.ctx.send(f"What do you want to do? You are in for {self.pokerGame.players[0].round_pot_commitment} chips, it costs __{self.pokerGame.current_bet - self.pokerGame.players[0].round_pot_commitment} more to call.__", view=view) 289 | 290 | async def user_all_in(self): 291 | logger.info(f"{self.ctx.author.name} - User goes All-in") 292 | self.pokerGame.player_all_in_raise(0) 293 | action, raise_amount = self.gpt_action.player_all_in(self.pokerGame) 294 | 295 | if action == ActionType.CALL: 296 | logger.info(f"{self.ctx.author.name} - PokerGPT Calls All-in.") 297 | await self.ctx.send(f"PokerGPT __Calls All-in.__") 298 | self.pokerGame.player_call(1) 299 | await self.showdown() 300 | elif action == ActionType.FOLD: 301 | await self.pokerGPT_fold() 302 | else: 303 | logger.warning(f"{self.ctx.author.name} - Error move given: {action}, {raise_amount}, doing Default move of: Fold") 304 | await self.pokerGPT_fold() 305 | 306 | async def pokerGPT_all_in(self): 307 | logger.info(f"{self.ctx.author.name} - PokerGPT goes All-in") 308 | await self.ctx.send(f"PokerGPT is __All-in for {self.pokerGame.return_player_stack(1) + self.pokerGame.players[1].round_pot_commitment} chips.__") 309 | self.pokerGame.player_all_in_raise(1) 310 | view = self.allInCallView(self) 311 | await self.ctx.send(f"What do you want to do? You are in for {self.pokerGame.players[0].round_pot_commitment} chips, it is {self.pokerGame.current_bet - self.pokerGame.players[0].round_pot_commitment} more to call", view=view) 312 | 313 | async def user_fold(self): 314 | logger.info(f"{self.ctx.author.name} - User Folds.") 315 | await self.ctx.send(f"PokerGPT wins __{self.pokerGame.current_pot} chips.__") 316 | self.pokerGame.player_win(1) 317 | await self.ctx.send(f"You have {self.pokerGame.return_player_stack(0)} chips.") 318 | await self.ctx.send(f"PokerGPT has {self.pokerGame.return_player_stack(1)} chips.") 319 | await self.new_round_prompt() 320 | 321 | async def pokerGPT_fold(self): 322 | logger.info(f"{self.ctx.author.name} - PokerGPT Folds.") 323 | await self.ctx.send("PokerGPT Folds.") 324 | await self.ctx.send(f"You win __{self.pokerGame.current_pot} chips.__") 325 | self.pokerGame.player_win(0) 326 | await self.ctx.send(f"You have {self.pokerGame.return_player_stack(0)} chips.") 327 | await self.ctx.send(f"PokerGPT has {self.pokerGame.return_player_stack(1)} chips.") 328 | await self.new_round_prompt() 329 | 330 | async def new_round_prompt(self): 331 | self.db_manager.update_community_cards(self.pokerGame.return_community_cards()) 332 | self.db_manager.end_hand(self.pokerGame.return_player_stack(0), self.pokerGame.round) 333 | await self.ctx.respond("Play another round?") 334 | await self.ctx.send("", view=self.newRoundView(self)) 335 | 336 | async def move_to_next_betting_round(self): 337 | self.pokerGame.current_action = self.pokerGame.button 338 | if self.pokerGame.round == Round.PRE_FLOP: 339 | await self.deal_community_cards(Round.FLOP) 340 | elif self.pokerGame.round == Round.FLOP: 341 | await self.deal_community_cards(Round.TURN) 342 | elif self.pokerGame.round == Round.TURN: 343 | await self.deal_community_cards(Round.RIVER) 344 | elif self.pokerGame.round == Round.RIVER: 345 | await self.showdown() 346 | 347 | async def next_action(self): 348 | self.pokerGame.current_action = (self.pokerGame.current_action + 1) % 2 349 | if self.pokerGame.round == Round.PRE_FLOP: 350 | if self.pokerGame.current_bet > self.pokerGame.big_blind: 351 | await self.move_to_next_betting_round() 352 | return 353 | if self.pokerGame.button == 0: 354 | action, raise_amount = self.gpt_action.pre_flop_big_blind(self.pokerGame) 355 | 356 | if action == ActionType.CHECK: 357 | logger.info(f"{self.ctx.author.name} - PokerGPT Checks.") 358 | await self.ctx.send("PokerGPT __Checks.__") 359 | await self.move_to_next_betting_round() 360 | return 361 | elif action == ActionType.ALL_IN: 362 | await self.pokerGPT_all_in() 363 | return 364 | elif action == ActionType.RAISE: 365 | await self.pokerGPT_raise(raise_amount) 366 | return 367 | else: 368 | logger.warning(f"{self.ctx.author.name} - Error move given: {action}, {raise_amount}, doing Default move of: Check") 369 | await self.ctx.send("PokerGPT __Checks.__") 370 | await self.move_to_next_betting_round() 371 | return 372 | 373 | if self.pokerGame.button == 1: 374 | if self.pokerGame.current_action == 0: 375 | view = self.checkView(self) 376 | await self.ctx.send(f"What do you want to do?", view=view) 377 | return 378 | elif self.pokerGame.current_action == 1: 379 | await self.move_to_next_betting_round() 380 | else: 381 | if self.pokerGame.current_bet > 0: 382 | await self.move_to_next_betting_round() 383 | return 384 | if self.pokerGame.button == 0: 385 | if self.pokerGame.current_action == 1: 386 | view = self.checkView(self) 387 | await self.ctx.send(f"What do you want to do?", view=view) 388 | return 389 | elif self.pokerGame.current_action == 0: 390 | await self.move_to_next_betting_round() 391 | elif self.pokerGame.button == 1: 392 | action, raise_amount = self.gpt_action.player_check(self.pokerGame) 393 | 394 | if action == ActionType.CHECK: 395 | logger.info(f"{self.ctx.author.name} - PokerGPT Checks.") 396 | await self.ctx.send("PokerGPT __Checks.__") 397 | await self.move_to_next_betting_round() 398 | return 399 | elif action == ActionType.ALL_IN: 400 | await self.pokerGPT_all_in() 401 | return 402 | elif action == ActionType.RAISE: 403 | await self.pokerGPT_raise(raise_amount) 404 | return 405 | else: 406 | logger.warning(f"{self.ctx.author.name} - Error move given: {action}, {raise_amount}, doing Default move of: Check") 407 | await self.ctx.send("PokerGPT __Checks.__") 408 | await self.move_to_next_betting_round() 409 | return 410 | 411 | def result_embed(self): 412 | logger.info(f"{self.ctx.author.name} - Game Results Player Stack: {self.pokerGame.return_player_stack(0)} chips, PokerGPT Stack: {self.pokerGame.return_player_stack(1)} chips.") 413 | embed = discord.Embed(title="Results") 414 | embed.add_field(name="PokerGPT", value=str(self.pokerGame.return_player_stack(1))) 415 | embed.add_field(name=self.ctx.author.name, value=str(self.pokerGame.return_player_stack(0))) 416 | return embed 417 | 418 | class raiseModal(discord.ui.Modal): 419 | def __init__(self, pokerManager): 420 | super().__init__(title="Raise", timeout=pokerManager.timeout) 421 | self.ctx = pokerManager.ctx 422 | self.pokerGame = pokerManager.pokerGame 423 | self.db_manager = pokerManager.db_manager 424 | self.pokerManager = pokerManager 425 | 426 | self.add_item(InputText(label="Amount", value="", placeholder="Enter amount")) 427 | 428 | async def callback(self, interaction: discord.Interaction): 429 | if self.children[0]: 430 | amount_raised = self.children[0].value 431 | if not amount_raised.isdigit(): 432 | await interaction.response.send_message("Please enter a valid number.") 433 | return 434 | amount_raised = int(amount_raised) 435 | if (amount_raised == self.pokerGame.return_player_stack(0) + self.pokerGame.players[0].round_pot_commitment): 436 | await interaction.response.send_message("You are __All-in.__") 437 | await self.pokerManager.user_all_in() 438 | return 439 | if (amount_raised > self.pokerGame.return_player_stack(0) + self.pokerGame.players[0].round_pot_commitment): 440 | await interaction.response.send_message("You do not have enough chips.") 441 | return 442 | if amount_raised < self.pokerGame.big_blind: 443 | await interaction.response.send_message("Raise must be at least the big blind.") 444 | return 445 | if amount_raised < 2 * self.pokerGame.current_bet: 446 | await interaction.response.send_message("You must raise to at least double the current bet.") 447 | return 448 | if (amount_raised >= self.pokerGame.return_player_stack(1) + self.pokerGame.players[1].round_pot_commitment): 449 | opponent_stack = (self.pokerGame.return_player_stack(1) + self.pokerGame.players[1].round_pot_commitment) 450 | await interaction.response.edit_message(content=f"You put PokerGPT __All-in for {opponent_stack} chips.__", view=None) 451 | await self.pokerManager.user_all_in() 452 | 453 | await interaction.response.edit_message(content=f"You __Raise to {amount_raised} chips.__", view=None) 454 | await self.pokerManager.user_raise(amount_raised) 455 | 456 | class callView(View): 457 | def __init__(self, pokerManager): 458 | super().__init__(timeout=pokerManager.timeout) 459 | self.responded = False 460 | self.ctx = pokerManager.ctx 461 | self.pokerGame = pokerManager.pokerGame 462 | self.db_manager = pokerManager.db_manager 463 | self.pokerManager = pokerManager 464 | 465 | async def on_timeout(self): 466 | if not self.responded: 467 | if self.message: 468 | await self.message.edit(content="You took too long! You __Fold.__", view=None) 469 | await self.pokerManager.user_fold() 470 | 471 | async def check(self, interaction: discord.Interaction): 472 | if interaction.user: 473 | return interaction.user.id == self.ctx.author.id 474 | 475 | @discord.ui.button(label="Call", style=ButtonStyle.blurple) 476 | async def call_button_callback(self, button, interaction): 477 | if await self.check(interaction): 478 | logger.info(f"{self.ctx.author.name} - User Calls.") 479 | self.responded = True 480 | self.pokerGame.player_call(0) 481 | if self.message: 482 | await self.message.edit(content="You __Call.__", view=None) 483 | await self.pokerManager.next_action() 484 | 485 | @discord.ui.button(label="Raise", style=ButtonStyle.green) 486 | async def raise_button_callback(self, button, interaction): 487 | if await self.check(interaction): 488 | self.responded = True 489 | await interaction.response.send_modal(self.pokerManager.raiseModal(self.pokerManager)) 490 | 491 | @discord.ui.button(label="All-in", style=ButtonStyle.green) 492 | async def all_in_button_callback(self, button, interaction): 493 | if await self.check(interaction): 494 | self.responded = True 495 | if self.message: 496 | await self.message.edit( 497 | content=f"You are __All In for {self.pokerGame.return_player_stack(0) + self.pokerGame.players[0].round_pot_commitment} chips.__", view=None) 498 | await self.pokerManager.user_all_in() 499 | 500 | @discord.ui.button(label="Fold", style=ButtonStyle.red) 501 | async def fold_button_callback(self, button, interaction): 502 | if await self.check(interaction): 503 | self.responded = True 504 | if self.message: 505 | await self.message.edit(content="You __Fold.__", view=None) 506 | await self.pokerManager.user_fold() 507 | 508 | class checkView(View): 509 | def __init__(self, pokerManager): 510 | super().__init__(timeout=pokerManager.timeout) 511 | self.responded = False 512 | self.ctx = pokerManager.ctx 513 | self.pokerGame = pokerManager.pokerGame 514 | self.db_manager = pokerManager.db_manager 515 | self.pokerManager = pokerManager 516 | 517 | async def on_timeout(self): 518 | if not self.responded: 519 | logger.info(f"{self.ctx.author.name} - User Checks.") 520 | if self.message: 521 | await self.message.edit(content="You took too long! You __Check.__", view=None) 522 | await self.pokerManager.next_action() 523 | 524 | async def check(self, interaction: discord.Interaction): 525 | if interaction.user: 526 | return interaction.user.id == self.ctx.author.id 527 | 528 | @discord.ui.button(label="Check", style=ButtonStyle.blurple) 529 | async def call_button_callback(self, button, interaction): 530 | if await self.check(interaction): 531 | logger.info(f"{self.ctx.author.name} - User Checks.") 532 | self.responded = True 533 | if self.message: 534 | await self.message.edit(content="You __Check.__", view=None) 535 | await self.pokerManager.next_action() 536 | 537 | @discord.ui.button(label="Raise", style=ButtonStyle.green) 538 | async def raise_button_callback(self, button, interaction): 539 | if await self.check(interaction): 540 | self.responded = True 541 | await interaction.response.send_modal(self.pokerManager.raiseModal(self.pokerManager)) 542 | 543 | @discord.ui.button(label="All-in", style=ButtonStyle.green) 544 | async def all_in_button_callback(self, button, interaction): 545 | if await self.check(interaction): 546 | self.responded = True 547 | if self.message: 548 | await self.message.edit(content=f"You are __All-in for {self.pokerGame.return_player_stack(0) + self.pokerGame.players[0].round_pot_commitment} chips.__", view=None) 549 | await self.pokerManager.user_all_in() 550 | 551 | class allInCallView(View): 552 | def __init__(self, pokerManager: 'DiscordPokerManager'): 553 | super().__init__(timeout=pokerManager.timeout) 554 | self.responded = False 555 | self.ctx = pokerManager.ctx 556 | self.pokerGame = pokerManager.pokerGame 557 | self.db_manager = pokerManager.db_manager 558 | self.pokerManager = pokerManager 559 | 560 | async def on_timeout(self): 561 | if self.responded == False: 562 | if self.message: 563 | await self.message.edit(content="You took too long! You __Fold__.", view=None) 564 | await self.pokerManager.user_fold() 565 | 566 | async def check(self, interaction: discord.Interaction): 567 | if interaction.user: 568 | return interaction.user.id == self.ctx.author.id 569 | 570 | @discord.ui.button(label="Call All-in", style=ButtonStyle.blurple) 571 | async def call_button_callback(self, button, interaction): 572 | if await self.check(interaction): 573 | logger.info(f"{self.ctx.author.name} - User Calls All-in.") 574 | self.responded = True 575 | if self.message: 576 | await self.message.edit(content="You __Call the All-in.__", view=None) 577 | self.pokerGame.player_call(0) 578 | await self.pokerManager.showdown() 579 | 580 | @discord.ui.button(label="Fold", style=ButtonStyle.red) 581 | async def fold_button_callback(self, button, interaction): 582 | if await self.check(interaction): 583 | self.responded = True 584 | if self.message: 585 | await self.message.edit(content="You __Fold.__", view=None) 586 | await self.pokerManager.user_fold() 587 | 588 | class newRoundView(View): 589 | def __init__(self, pokerManager: 'DiscordPokerManager'): 590 | super().__init__(timeout=pokerManager.timeout) 591 | self.responded = False 592 | self.ctx = pokerManager.ctx 593 | self.pokerGame = pokerManager.pokerGame 594 | self.db_manager = pokerManager.db_manager 595 | self.pokerManager = pokerManager 596 | 597 | async def on_timeout(self): 598 | if self.responded == False: 599 | self.db_manager.end_game(self.pokerGame.return_player_stack(0)) 600 | embed = self.pokerManager.result_embed() 601 | if self.message: 602 | await self.message.edit(content="*Game Ended*", view=None, embeds=[embed]) 603 | 604 | async def check(self, interaction: Interaction): 605 | if interaction.user: 606 | return interaction.user.id == self.ctx.author.id 607 | 608 | @discord.ui.button(label="New Round", style=ButtonStyle.blurple) 609 | async def new_round_button_callback(self, button, interaction): 610 | if await self.check(interaction): 611 | self.responded = True 612 | if self.message: 613 | await self.message.edit(content="*Starting a new round.*", view=None) 614 | await self.pokerManager.play_round() 615 | 616 | @discord.ui.button(label="End Game", style=ButtonStyle.red) 617 | async def end_game_button_callback(self, button, interaction): 618 | if await self.check(interaction): 619 | self.responded = True 620 | self.db_manager.end_game(self.pokerGame.return_player_stack(0)) 621 | embed = self.pokerManager.result_embed() 622 | if self.message: 623 | await self.message.edit(content="*Game Ended*", view=None, embeds=[embed]) 624 | --------------------------------------------------------------------------------