import logging import os import re from pprint import pprint from typing import Tuple, Optional, List, Dict import discord from bot import exceptions, constants logger = logging.getLogger(__file__) logger.setLevel(constants.LOGGING_LEVEL) class Card(object): _suits = {'H': 'Hearts', 'S': 'Spades', 'C': 'Clubs', 'D': 'Diamonds'} _symbols = {'10': 'Ten', '9': 'Nine', '8': 'Eight', '7': 'Seven', '6': 'Six', '5': 'Five', '4': 'Four', '3': 'Three', '2': 'Two', 'k': 'King', 'q': 'Queen', 'j': 'Jack', 'a': 'Ace'} EMOTE_REGEX = re.compile(r'<:([A-z0-9]+):\d+>') VALUE_PATTERN = re.compile(r'Value: (?:Soft )?(\d+)') def __init__(self, card: str) -> None: if 4 <= len(card) <= 1: raise exceptions.InvalidCard(f'By length, {card} is invalid. Identifiers are 2 to 3 characters long.') self.raw_card = card self.symbol, self.suit = self.parts() @property def value(self, safe: bool = True, unsafe_default: int = 0) -> int: """ Attempts to determine the numerical value of the card. Aces and unknown cards will raise an exception, unless the safe boolean is set to False. """ if self.isAce(): if safe: raise exceptions.NoAceValue( 'The Ace has multiple values (1 and 11) in Blackjack. Special handling is required.') return 0 elif self.isFace(): return 10 elif self.isNumerical(): return int(self.symbol) elif safe: raise exceptions.IndetermineValue('Could not determine the numeric value of this card.') return unsafe_default def isAce(self) -> bool: """Returns whether or not the card is a Ace card.""" return self.symbol == 'a' def isNumerical(self) -> bool: """Returns whether or not the card is numerical (not face or ace).""" return self.symbol.isnumeric() def isFace(self) -> bool: """Returns whether or not the card is a face card (Queen, King, or Jack)""" return self.symbol in ['q', 'k', 'j'] def parts(self) -> Tuple[str, str]: """ Separates the raw card identifier into it's symbol and suit. Handles aces, face cards and numerical cards with any of the four suites. """ match = re.match(r'^(\d{1,2}|[aqkj])([cdhs])$', self.raw_card, flags=re.IGNORECASE) return match.group(1), match.group(2) @classmethod def parse_cards(cls, card_str: discord.embeds.EmbedProxy) -> Tuple[int, List['Card']]: """Given a EmbedProxy relating to a Blackjack Embed, finds a returns a list of Card objects and the value.""" card_matches = re.finditer(cls.EMOTE_REGEX, card_str.value) value = re.search(cls.VALUE_PATTERN, card_str.value) cards = [] for card in card_matches: identifier = card.group(1) if identifier != 'cardBack': cards.append(Card(identifier)) return int(value.group(1)), cards def __repr__(self) -> str: return f'Card({self._symbols[self.symbol]} of {self._suits[self.suit]})' def generate_table_structure(filename: str, column_keys: List[str], row_keys: List[str]) -> Dict[Tuple[str, str], str]: data = {} logger.debug(f'Generating table structure with {filename}') with open(os.path.join(constants.STATIC_DIR, filename)) as hard_file: raw_data = [list(line) for line in hard_file.read().split('\n') if len(line) > 0] for x, col_key in enumerate(column_keys): for y, row_key in enumerate(row_keys): data[(col_key, row_key)] = raw_data[y][x] return data class Blackjack(object): # The row and column headers for the hair, soft and pair baseline tables. hard_column = ['2', '3', '4', '5', '6', '7', '8', '9', 'T', 'A'] hard_row = ['20', '19', '18', '17', '16', '15', '14', '13', '12', '11', '10', '9', '8', '7', '6', '5'] soft_column = ['2', '3', '4', '5', '6', '7', '8', '9', 'T', 'A'] soft_row = ['A-9', 'A-8', 'A-7', 'A-6', 'A-5', 'A-4', 'A-3', 'A-2'] pair_column = ['2', '3', '4', '5', '6', '7', '8', '9', 'T', 'A'] pair_row = ['A-A', 'T-T', '9-9', '8-8', '7-7', '6-6', '5-5', '4-4', '3-3', '2-2'] hard_data = generate_table_structure('baseline_hard.dat', hard_column, hard_row) soft_data = generate_table_structure('baseline_soft.dat', soft_column, soft_row) pair_data = generate_table_structure('baseline_pairs.dat', pair_column, pair_row) HARD = 0 SOFT = 1 PAIR = 2 @staticmethod def choose(options: Tuple[bool, bool, bool, bool], cards: Tuple[str, Optional[str], Tuple[int, bool]], dealer: Tuple[str, Optional[str], Tuple[int, bool]]) -> str: """With all information presented, calculates the final decision.""" def get_ideal_choice(self, cards: List[Card], dealer: Tuple[str, Optional[str], Tuple[int, bool]]) -> str: """Gets the ideal baseline strategy choice based on my cards and the bot's cards.""" def options_convert(self, choice: str, options: Tuple[bool, bool, bool, bool]): """Converts the choice to the best possible choice based on the options given by the bot.""" @classmethod def access(cls, table: int, key: Tuple[str, str]) -> str: """ Access table data given a column and row key. :param key: :param table: :return: """ if table == cls.HARD: return cls.hard_data[key] if table == cls.SOFT: return cls.soft_data[key] if table == cls.PAIR: return cls.pair_data[key]