2
\$\begingroup\$

Wrote a slot machine for a larger casino TUI project I'm creating. Would like some general feedback, along with some specific things:

  • Coloring: Currently I have three loops that are almost identical, only differing in the index where they stop. Is there an easier way to do this?
  • Separation: Did my best to separate specific tasks to their own functions, but I feel there is still some overlap.
  • Flexibility: At the beginning I wrote this to fit any NxN slot interface, but ended up inserting some hardcoded values for a 5x3 machine, like most are. Would like some suggestions on how to tackle this issue, especially for calculating the pre-determined lines.
import random
from termcolor import colored

class Player:
    
    def __init__(self, name: str, balance: float) -> None:
        self.name = name
        self.balance = balance

class Slot:

    def __init__(self, options: dict[str, list[float]], dim: tuple[int, int], player: Player) -> None:
        self.options = options
        self.w, self.h = dim
        self.player = player
        self.result = [[''] * self.w] * self.h
        self.total_winnings = 0.0
        self.rawlines = [
            [0, 0, 0, 0, 0], # 1
            [1, 1, 1, 1, 1], # 2
            [2, 2, 2, 2, 2], # 3
            [0, 1, 2, 1, 0], # 4
            [2, 1, 0, 1, 2], # 5
            [1, 2, 1, 0, 1], # 6
            [1, 2, 1, 2, 1], # 7
            [0, 1, 0, 1, 2], # 8
            [2, 1, 2, 1, 0], # 9
            [2, 1, 0, 1, 0], # 10
            [0, 1, 2, 1, 2], # 11
            [1, 0, 1, 2, 1], # 12
            [2, 1, 2, 1, 2], # 13
            [1, 0, 1, 0, 1], # 14
            [0, 1, 0, 1, 0], # 15
            [1, 1, 2, 1, 1], # 16
            [0, 0, 1, 0, 0], # 17
            [2, 2, 1, 2, 2], # 18
            [1, 1, 0, 1, 1], # 19
            [0, 0, 2, 0, 0], # 20
            [2, 2, 0, 2, 2], # 21
            [1, 1, 1, 2, 1], # 22
            [0, 0, 0, 1, 2], # 23
            [2, 2, 2, 1, 0], # 24
            [2, 1, 1, 1, 0] # 25
        ]

    def spin(self, bet: float):
        self.bet = bet
        if self.player.balance - bet < 0.0:
            quit("Insufficient Funds.")
        for i in range(self.h):
            self.result[i] = random.choices(list(self.options.keys()), k=self.w)
        self.__calculate_lines()
        self.player.balance = self.player.balance + (self.__check_wins() - bet)

    def __calculate_lines(self) -> None:
        # https://chiphungry.com/mad-mouse-the-zappening/
        self.lines = [[self.result[val][idx] for idx, val in enumerate(line)] for line in self.rawlines]

    def __check_lines(self) -> float:
        win = 0.0
        for idx, line in enumerate(self.lines):
            front = line[0]
            raw_line = self.rawlines[idx]
            if len(set(line)) == 1: # all
                win += self.bet * self.options[front][0]
                for idx, val in enumerate(raw_line):
                    self.result[val][idx] = colored(self.result[val][idx], "green")
            elif len(set(line[:-1])) == 1: # all but one
                win += self.bet * self.options[front][1]
                for idx, val in enumerate(raw_line[:-1]):
                    self.result[val][idx] = colored(self.result[val][idx], "green")
            elif len(set(line[:-2])) == 1: # all but two
                win += self.bet * self.options[front][2]
                for idx, val in enumerate(raw_line[:-2]):
                    self.result[val][idx] = colored(self.result[val][idx], "green")
        return win

    def __check_wins(self):
        self.win = 0.0
        self.win += self.__check_lines()
        self.total_winnings += self.win
        return self.win

    def display(self, verbose=False):
        length = ("|---" * self.w) + "|"
        z = ''.join(''.join(f"| {a} " for a in r) + '|\n' for r in self.result)
        print(f"{length}\n{z.strip()}\n{length}")
        print(f"Balance: ${self.player.balance:0.2f} | Bet: ${self.bet:0.2f} | Win: ${self.win:0.2f}")
        if verbose:
            print(f"Total Winnings: ${self.total_winnings:0.2f}")

def main():
    player = Player("Ben", 50.00)
    options = {
        'A': [3, 0.8, 0.3],
        'K': [2.5, 0.6, 0.24],
        'Q': [2, 0.5, 0.2],
        '9': [1.5, 0.4, 0.16],
        '8': [1.2, 0.3, 0.14],
        '7': [1, 0.2, 0.1]
    }
    slot = Slot(options, (5, 3), player)
    while True:
        slot.spin(1.00)
        slot.display(verbose=True)
        input()


if __name__ == '__main__':
    main()
\$\endgroup\$

1 Answer 1

6
\$\begingroup\$

Questions

Currently I have three loops that are almost identical, only differing in the index where they stop. Is there an easier way to do this?

Extract the stop-index as a variable, and then issue a single call to colored.

Did my best to separate specific tasks to their own functions, but I feel there is still some overlap.

This still needs a lot of work. For example, in your __check_lines you're both calculating a winnings sum and reformatting your results matrix.

At the beginning I wrote this to fit any NxN slot interface, but ended up inserting some hardcoded values for a 5x3 machine, like most are. Would like some suggestions on how to tackle this issue, especially for calculating the pre-determined lines.

Don't accept width and height. Your rawlines (which I call win_database and your reference calls win lines) should imply the dimensions.

The good

You've written some type hints (you could use more).

You've managed to avoid globals.

You have a properly-written main function and __main__ guard.

You've used a terminal colour library in a seemingly sensible way.

Points to work on

dim does not benefit from being a tuple; just accept two arguments.

Missing a bunch of hints, such as spin() -> None.

rawlines should be an immutable tuple of tuples, and should be a class ("static") variable rather than an instance variable.

The code is kind of a swamp of inappropriate mutation and state retention. self.result shouldn't be present on Slot at all. Either pass it around in parameters and return values, or (as I'll demonstrate) split out a second Spin class. The simple path to immutability is NamedTuple.

Don't quit. Raise a domain-specific exception.

Don't for i in range(self.h); loop like a native - the safe choice here is a generator function.

Don't double-underscore. That's reserved for name mangling; use a single underscore for private-like members.

Reduce self.player.balance = self.player.balance + by using +=.

length is not a length; it's some kind of divider string - we can think of a better variable name.

${self.player.balance:0.2f} could instead leverage Python's built-in locale currency formatting support.

Since random behaviour is crucial for this application, you should either pass in an instance of Random, or at least pass in an optional random seed for testability.

w and h should just be spelled in full. More importantly - accepting those parameters gives the caller the illusion that they can influence the width and height, but they can't! Those are hard-coded in rawlines.

It would be a good idea to write a fallback for terminals that don't support colour; something like

if termcolor.termcolor._can_do_colour():
    yield termcolor.colored(text=f' {symbol} ', color='green')
else:
    yield f'[{symbol}]'

Suggested

Demonstrating some of the above with less mutation, a little bit more OOP, etc.: this behaves identically to your old code. It took a lot of effort to figure out how the old code worked, and I suspect that there's still a bunch of simplification to the win-condition algorithm that can be done.

"""
Simulation imitating the slot machine described in
https://chiphungry.com/mad-mouse-the-zappening/
"""

import dataclasses
import functools
import locale
import random
from enum import Enum
from typing import Iterator, Iterable, NamedTuple

import termcolor


class InsufficientFundsError(ValueError):
    """
    Thrown if the user attempts to spin but doesn't have enough balance to submit the bet.
    """


@dataclasses.dataclass(slots=True)
class Player:
    name: str
    balance: float

    def pay(self, bet: float) -> None:
        new_balance = self.balance - bet
        if new_balance < 0:
            raise InsufficientFundsError()
        self.balance = new_balance

    def win(self, winnings: float) -> None:
        self.balance += winnings


# A vertical index into the result matrix
VIndex = int
# A single "card" symbol, like Q
Symbol = str
# One row in the win condition database
IndexRow = tuple[VIndex, ...]
# One row in the result matrix
SymbolRow = tuple[Symbol, ...]
# One win-product row, with a coefficient for each WinClass entry
CoefRow = tuple[float, float, float, float]
# The matrix of symbols to display to the user
ResultMatrix = tuple[SymbolRow, ...]


class WinClass(Enum):
    """
    All of the possible alternatives for win condition, in decreasing order of rarity.
    """
    ALL = 0
    ALL_BUT_1 = 1
    ALL_BUT_2 = 2
    NONE = 3


class WinCandidate(NamedTuple):
    """
    One win-candidate row, with symbols from a spin mapped onto the win condition database.
    These win candidates are not displayed to the user directly. They're used to calculate win class
    (in turn calculating the addends for winnings money).
    """

    symbols: SymbolRow
    win_class: WinClass

    @classmethod
    def map(cls, symbols: Iterable[Symbol]) -> 'WinCandidate':
        """
        From a mapped symbol row, create a WinCandidate instance.
        """
        symbols = tuple(symbols)

        # Business logic used to determine the win-class for this candidate.
        # This is somewhat of a performance bottleneck.
        all_len = len(set(symbols))
        if all_len > len(symbols) - 2:
            win_class = WinClass.NONE  # Fast path for very few matches
        elif all_len == 1:
            win_class = WinClass.ALL
        elif len(set(symbols[:-1])) == 1:
            win_class = WinClass.ALL_BUT_1
        elif len(set(symbols[:-2])) == 1:
            win_class = WinClass.ALL_BUT_2
        else:
            win_class = WinClass.NONE

        return cls(symbols=symbols, win_class=win_class)

    @property
    def front(self) -> Symbol:
        return self.symbols[0]

    def __str__(self) -> str:
        return ''.join(self.symbols)


class Spin:
    """
    A single spin of the slot machine.
    """

    # Python class slots, not slot-machine slots
    __slots__ = 'result', 'bet', 'candidates', 'winnings', 'win_database'

    def __init__(
        self,
        win_coefs_by_symbol: dict[Symbol, CoefRow],
        win_database: tuple[IndexRow, ...],
        result: ResultMatrix,
        bet: float,
    ) -> None:
        self.win_database = win_database
        self.result = result
        self.bet = bet
        self.candidates = tuple(self._get_candidates())
        self.winnings = sum(self._get_winnings(win_coefs_by_symbol))

    def _get_winnings(
        self,
        win_coefs_by_symbol: dict[Symbol, CoefRow],
    ) -> Iterator[float]:
        """
        An iterator of winning money. Call sum() on this to get the total winnings for this spin.
        """
        for candidate in self.candidates:
            coef = win_coefs_by_symbol[candidate.front][candidate.win_class.value]
            yield self.bet * coef

    def _get_candidates(self) -> Iterator[WinCandidate]:
        """
        An iterator of win candidate tuples, made by mapping the spin result symbols onto the win
        condition database. In the demo configuration, the iterator is 25 elements long and yields
        tuples of 5 symbols each.
        """
        for win_indices in self.win_database:
            yield WinCandidate.map([
                self.result[y][x]
                for x, y in enumerate(win_indices)
            ])

    @property
    def width(self) -> int:
        return len(self.result[0])

    def _format_row(self, y: int, result_row: SymbolRow) -> Iterator[str]:
        for x, symbol in enumerate(result_row):
            for candidate, win_condition in zip(self.candidates, self.win_database):
                if y != win_condition[x]:
                    continue

                win_class = candidate.win_class
                if win_class != WinClass.NONE:
                    n_excluded = win_class.value
                    if x < len(result_row) - n_excluded:
                        if termcolor.termcolor._can_do_colour():
                            yield termcolor.colored(text=f' {symbol} ', color='green')
                        else:
                            yield f'[{symbol}]'
                        break
            else:
                yield f'{symbol:^3}'

    def format_lines(self, player: Player) -> Iterator[str]:
        sep = '|---'*self.width + '|'
        yield sep
        for y, result_row in enumerate(self.result):
            middle = '|'.join(self._format_row(y=y, result_row=result_row))
            yield f'|{middle}|'
        yield sep
        yield f'Balance: {locale.currency(player.balance)} | {self}'

    def __str__(self) -> str:
        return (
            f'Bet: {locale.currency(self.bet)}'
            f' | Win: {locale.currency(self.winnings)}'
        )


class SlotMachine:
    # Python class slots, not slot-machine slots
    __slots__ = (
        'win_coefs_by_symbol', 'win_database', 'width', 'height',
        'symbols', 'total_winnings', 'rand',
    )

    def __init__(
        self,
        win_coefs_by_symbol: dict[Symbol, CoefRow],
        win_database: tuple[IndexRow, ...],
        rand: random.Random | None = None,
    ) -> None:
        self.win_coefs_by_symbol = win_coefs_by_symbol
        self.win_database = win_database
        self.symbols = tuple(win_coefs_by_symbol.keys())
        self.total_winnings = 0.  # The only thing that mutates in this class.
        self.width = len(win_database[0])

        self.height = 1 + max(
            idx
            for row in win_database
            for idx in row
        )

        if rand is None:
            self.rand = random.Random()
        else:
            self.rand = rand

    def _spin_rows(self) -> Iterator[SymbolRow]:
        """
        An iterator of output matrix rows, randomly chosen from the display symbols.
        """
        for _ in range(self.height):
            yield tuple(self.rand.choices(
                population=self.symbols,
                k=self.width,
            ))

    def spin(self, player: Player, bet: float) -> Spin:
        # Will throw if insufficient balance
        player.pay(bet)

        spin = Spin(
            win_coefs_by_symbol=self.win_coefs_by_symbol,
            win_database=self.win_database,
            result=tuple(self._spin_rows()),
            bet=bet,
        )
        self.total_winnings += spin.winnings
        player.win(spin.winnings)
        return spin

    def __str__(self) -> str:
        return f'Total Winnings: {locale.currency(self.total_winnings)}'


def main(balance: float = 50.00, visible: bool = True, pause: bool = False) -> None:
    # Set to the user's locale, for currency formatting
    locale.setlocale(category=locale.LC_ALL, locale='')

    player = Player(name='Ben', balance=balance)

    win_database = (
        (0, 0, 0, 0, 0),
        (1, 1, 1, 1, 1),
        (2, 2, 2, 2, 2),
        (0, 1, 2, 1, 0),
        (2, 1, 0, 1, 2),
        (1, 2, 1, 0, 1),
        (1, 2, 1, 2, 1),
        (0, 1, 0, 1, 2),
        (2, 1, 2, 1, 0),
        (2, 1, 0, 1, 0),
        (0, 1, 2, 1, 2),
        (1, 0, 1, 2, 1),
        (2, 1, 2, 1, 2),
        (1, 0, 1, 0, 1),
        (0, 1, 0, 1, 0),
        (1, 1, 2, 1, 1),
        (0, 0, 1, 0, 0),
        (2, 2, 1, 2, 2),
        (1, 1, 0, 1, 1),
        (0, 0, 2, 0, 0),
        (2, 2, 0, 2, 2),
        (1, 1, 1, 2, 1),
        (0, 0, 0, 1, 2),
        (2, 2, 2, 1, 0),
        (2, 1, 1, 1, 0),
    )

    win_coefs_by_symbol = {
        'A': (3.0, 0.8, 0.30, 0),
        'K': (2.5, 0.6, 0.24, 0),
        'Q': (2.0, 0.5, 0.20, 0),
        '9': (1.5, 0.4, 0.16, 0),
        '8': (1.2, 0.3, 0.14, 0),
        '7': (1.0, 0.2, 0.10, 0),
    }

    machine = SlotMachine(
        win_coefs_by_symbol, win_database,
        rand=random.Random(0),
    )

    while True:
        try:
            spin = machine.spin(player=player, bet=1.00)
        except InsufficientFundsError:
            if visible:
                print('Insufficient funds')
            break
        output = '\n'.join(spin.format_lines(player=player))
        if visible:
            print(output)
            print(machine)
        if pause:
            input('Press enter to continue.')
        if visible:
            print()


def profile() -> None:
    import cProfile
    cProfile.run('main(balance=10_000, visible=False)', sort='tottime')


if __name__ == '__main__':
    try:
        main()
        # profile()
    except KeyboardInterrupt:
        pass

Output

For the purposes of Code Review formatting, this output is shown with the non-colourised fallbacks.

|---|---|---|---|---|
| 7 | 8 | Q | K | 9 |
| Q | 8 | K | Q | 9 |
| 7 | 9 | K | 8 | 9 |
|---|---|---|---|---|
Balance: $49.00 | Bet: $1.00 | Win: $0.00
Total Winnings: $0.00

|---|---|---|---|---|
| K | 7 | 7 | 8 | 7 |
| K | 8 | 7 | 8 | Q |
| A | Q | 9 | 7 | 7 |
|---|---|---|---|---|
Balance: $48.00 | Bet: $1.00 | Win: $0.00
Total Winnings: $0.00

|---|---|---|---|---|
| Q | 7 | K | 8 | 9 |
| A | 8 | Q | 8 | 8 |
| A | Q | 7 | K | K |
|---|---|---|---|---|
Balance: $47.00 | Bet: $1.00 | Win: $0.00
Total Winnings: $0.00

|---|---|---|---|---|
| 7 | K | 9 | K | 7 |
| 8 | Q | A | K | 9 |
| 7 | A | 9 | 8 | 9 |
|---|---|---|---|---|
Balance: $46.00 | Bet: $1.00 | Win: $0.00
Total Winnings: $0.00

|---|---|---|---|---|
| 8 | 9 | 7 | 9 | 9 |
| Q | 9 | Q | 9 | K |
| K | K | 9 | 9 | Q |
|---|---|---|---|---|
Balance: $45.00 | Bet: $1.00 | Win: $0.00
Total Winnings: $0.00

|---|---|---|---|---|
| A | 8 |[7]| 7 | 7 |
|[7]|[7]| 9 | Q | 8 |
| K | 8 |[7]| 7 | 9 |
|---|---|---|---|---|
Balance: $44.20 | Bet: $1.00 | Win: $0.20
Total Winnings: $0.20

|---|---|---|---|---|
| 7 | 9 | Q | 9 | 7 |
| 7 | 8 | A | 9 | Q |
| 9 | 7 | K | 8 | A |
|---|---|---|---|---|
Balance: $43.20 | Bet: $1.00 | Win: $0.00
Total Winnings: $0.20

|---|---|---|---|---|
| K | 8 | K | 8 | A |
| A | 8 | A | 9 | 7 |
| 9 | 8 | A | 9 | 9 |
|---|---|---|---|---|
Balance: $42.20 | Bet: $1.00 | Win: $0.00
Total Winnings: $0.20

|---|---|---|---|---|
| 9 | Q | Q | 7 | A |
| A | 7 | K | A | K |
| 8 | 7 | A | Q | A |
|---|---|---|---|---|
Balance: $41.20 | Bet: $1.00 | Win: $0.00
Total Winnings: $0.20

|---|---|---|---|---|
| K | K | 9 | Q | K |
| 9 | A | A | 7 | K |
| Q | 8 | 7 | 7 | K |
|---|---|---|---|---|
Balance: $40.20 | Bet: $1.00 | Win: $0.00
Total Winnings: $0.20

|---|---|---|---|---|
| 8 | 7 | A | 8 | 7 |
| Q | K | 9 | Q | K |
| Q | Q | 9 | 9 | K |
|---|---|---|---|---|
Balance: $39.20 | Bet: $1.00 | Win: $0.00
Total Winnings: $0.20

|---|---|---|---|---|
| Q | 7 | K | 9 | A |
| 8 | Q | A | K | K |
| 7 | Q | K | Q | 7 |
|---|---|---|---|---|
Balance: $38.20 | Bet: $1.00 | Win: $0.00
Total Winnings: $0.20

|---|---|---|---|---|
| 9 | 9 | 8 | Q | Q |
| 9 | A | K | Q | K |
| 9 | Q | 7 | 9 | Q |
|---|---|---|---|---|
Balance: $37.20 | Bet: $1.00 | Win: $0.00
Total Winnings: $0.20

|---|---|---|---|---|
| Q | 8 | Q | 9 | A |
| Q | K | A | 9 | Q |
| 9 | 8 | 7 | Q | K |
|---|---|---|---|---|
Balance: $36.20 | Bet: $1.00 | Win: $0.00
Total Winnings: $0.20

|---|---|---|---|---|
| Q | 8 | 7 | 8 | K |
| 7 | 9 | A | 8 | 7 |
| Q | 8 | K | K | 8 |
|---|---|---|---|---|
Balance: $35.20 | Bet: $1.00 | Win: $0.00
Total Winnings: $0.20

|---|---|---|---|---|
| A | 8 | 9 | A | A |
| 9 | 7 | K | 7 | A |
| 7 | Q | A | K | Q |
|---|---|---|---|---|
Balance: $34.20 | Bet: $1.00 | Win: $0.00
Total Winnings: $0.20

|---|---|---|---|---|
| 8 | 7 | A | 9 | 9 |
| Q | 7 | K | A | A |
| 8 | 9 | 7 | 7 | 7 |
|---|---|---|---|---|
Balance: $33.20 | Bet: $1.00 | Win: $0.00
Total Winnings: $0.20

|---|---|---|---|---|
| A | 8 | 8 | 9 | 8 |
| 7 | 9 | Q | 9 | K |
| 9 | A | A | Q | 8 |
|---|---|---|---|---|
Balance: $32.20 | Bet: $1.00 | Win: $0.00
Total Winnings: $0.20

|---|---|---|---|---|
| 8 | Q | 9 | A | A |
| 7 | K | Q | 9 | K |
| Q | K | A | K | 9 |
|---|---|---|---|---|
Balance: $31.20 | Bet: $1.00 | Win: $0.00
Total Winnings: $0.20

|---|---|---|---|---|
| A | Q | K | Q | A |
| 7 |[Q]| 7 | 7 | Q |
|[Q]| 8 |[Q]| K | 8 |
|---|---|---|---|---|
Balance: $30.60 | Bet: $1.00 | Win: $0.40
Total Winnings: $0.60

|---|---|---|---|---|
| 7 | 7 | 9 | Q | 7 |
| 9 |[A]|[A]| 8 | A |
|[A]| Q | 9 | Q | Q |
|---|---|---|---|---|
Balance: $29.90 | Bet: $1.00 | Win: $0.30
Total Winnings: $0.90

|---|---|---|---|---|
| 9 | 8 | Q | Q | 7 |
| K | 8 | Q | Q | A |
| 7 | 8 | 7 | Q | 8 |
|---|---|---|---|---|
Balance: $28.90 | Bet: $1.00 | Win: $0.00
Total Winnings: $0.90

|---|---|---|---|---|
| A | Q | K | A | 7 |
| K | A | K | Q | 9 |
| A | 7 | 7 | A | Q |
|---|---|---|---|---|
Balance: $27.90 | Bet: $1.00 | Win: $0.00
Total Winnings: $0.90

|---|---|---|---|---|
| 8 | 7 |[Q]| 7 | K |
|[Q]|[Q]| 8 | K | 9 |
| K | Q | 7 | 7 | 8 |
|---|---|---|---|---|
Balance: $27.10 | Bet: $1.00 | Win: $0.20
Total Winnings: $1.10

|---|---|---|---|---|
| A | A | K | 8 | 7 |
| 9 | 8 | 8 | A | A |
| 7 | A | K | A | A |
|---|---|---|---|---|
Balance: $26.10 | Bet: $1.00 | Win: $0.00
Total Winnings: $1.10

|---|---|---|---|---|
| 7 | K | A | A | 9 |
| 7 | 9 | 7 | 9 | 9 |
|[8]|[8]|[8]| 7 | 8 |
|---|---|---|---|---|
Balance: $25.38 | Bet: $1.00 | Win: $0.28
Total Winnings: $1.38

|---|---|---|---|---|
| 7 | K | 9 | 9 | 8 |
| Q | 8 | Q | 9 | 7 |
| 8 | 9 | A | K | 9 |
|---|---|---|---|---|
Balance: $24.38 | Bet: $1.00 | Win: $0.00
Total Winnings: $1.38

|---|---|---|---|---|
| K | A | 7 | 7 | K |
| Q | 8 | A | 9 | A |
| K | 8 | A | A | Q |
|---|---|---|---|---|
Balance: $23.38 | Bet: $1.00 | Win: $0.00
Total Winnings: $1.38

|---|---|---|---|---|
| Q | 7 | 8 | 8 | A |
| 7 | Q | 9 | Q | A |
| Q | A | A | 9 | 9 |
|---|---|---|---|---|
Balance: $22.38 | Bet: $1.00 | Win: $0.00
Total Winnings: $1.38

|---|---|---|---|---|
| A | 9 | Q | Q | 8 |
| Q | 7 | 9 | Q | 9 |
| Q | A | 8 | 9 | 8 |
|---|---|---|---|---|
Balance: $21.38 | Bet: $1.00 | Win: $0.00
Total Winnings: $1.38

|---|---|---|---|---|
| A | 7 |[8]| 7 | 7 |
|[8]|[8]| 9 |[8]| K |
|[8]| Q |[8]| Q |[8]|
|---|---|---|---|---|
Balance: $23.98 | Bet: $1.00 | Win: $3.60
Total Winnings: $4.98

|---|---|---|---|---|
| A | A | 7 | Q | 7 |
| 7 | 9 | 9 | K | A |
| 8 | 7 | 8 | 8 | Q |
|---|---|---|---|---|
Balance: $22.98 | Bet: $1.00 | Win: $0.00
Total Winnings: $4.98

|---|---|---|---|---|
| K | A | Q | 9 | 7 |
| 7 | Q | A | 8 | 8 |
| A | Q | 8 | A | 7 |
|---|---|---|---|---|
Balance: $21.98 | Bet: $1.00 | Win: $0.00
Total Winnings: $4.98

|---|---|---|---|---|
| 7 | 8 | A | A | Q |
| A | Q | A | 7 | 8 |
| A | 7 | K | K | 8 |
|---|---|---|---|---|
Balance: $20.98 | Bet: $1.00 | Win: $0.00
Total Winnings: $4.98

|---|---|---|---|---|
|[7]| A | A | Q | A |
| 9 |[7]| K | A | Q |
|[7]| A |[7]| Q | Q |
|---|---|---|---|---|
Balance: $20.38 | Bet: $1.00 | Win: $0.40
Total Winnings: $5.38

|---|---|---|---|---|
| K | 7 | 7 | 9 | K |
| 7 | A | 9 | A | 7 |
| 7 | 8 | K | K | 8 |
|---|---|---|---|---|
Balance: $19.38 | Bet: $1.00 | Win: $0.00
Total Winnings: $5.38

|---|---|---|---|---|
| K | Q |[A]| A | A |
|[A]|[A]| Q | 9 | 8 |
|[A]| Q | 9 | A | Q |
|---|---|---|---|---|
Balance: $19.28 | Bet: $1.00 | Win: $0.90
Total Winnings: $6.28

|---|---|---|---|---|
| Q | 7 | A | Q | Q |
| 8 | K | K | K | Q |
| 7 | 8 | K | 9 | 8 |
|---|---|---|---|---|
Balance: $18.28 | Bet: $1.00 | Win: $0.00
Total Winnings: $6.28

|---|---|---|---|---|
| 8 | Q | Q | 8 | Q |
| K | Q | 7 | A | Q |
| 9 | Q | K | A | 8 |
|---|---|---|---|---|
Balance: $17.28 | Bet: $1.00 | Win: $0.00
Total Winnings: $6.28

|---|---|---|---|---|
| 9 | A | K | 8 | 9 |
| 9 | A | 8 | Q | 8 |
| 8 | K | 8 | K | 7 |
|---|---|---|---|---|
Balance: $16.28 | Bet: $1.00 | Win: $0.00
Total Winnings: $6.28

|---|---|---|---|---|
| A | 7 | A | K | 9 |
| 9 | Q | A | K | K |
| 8 | 7 | 9 | A | 8 |
|---|---|---|---|---|
Balance: $15.28 | Bet: $1.00 | Win: $0.00
Total Winnings: $6.28

|---|---|---|---|---|
| K | Q |[9]| K | 7 |
| A | Q | K | 9 | 9 |
|[9]|[9]| Q |[9]| Q |
|---|---|---|---|---|
Balance: $14.68 | Bet: $1.00 | Win: $0.40
Total Winnings: $6.68

|---|---|---|---|---|
| 8 | 8 | K | Q | 9 |
| A | 8 | Q | 9 | A |
| 8 | Q | Q | Q | Q |
|---|---|---|---|---|
Balance: $13.68 | Bet: $1.00 | Win: $0.00
Total Winnings: $6.68

|---|---|---|---|---|
| 8 | K | 9 | A | K |
| 8 | A | 9 | A | Q |
| 7 | A | 9 | A | 7 |
|---|---|---|---|---|
Balance: $12.68 | Bet: $1.00 | Win: $0.00
Total Winnings: $6.68

|---|---|---|---|---|
|[8]|[8]|[8]| A | A |
| 9 | 7 | 7 |[8]| 8 |
| K | 8 | K | A | Q |
|---|---|---|---|---|
Balance: $12.12 | Bet: $1.00 | Win: $0.44
Total Winnings: $7.12

|---|---|---|---|---|
| K | K | Q | 7 | Q |
| Q | 8 | 9 | A | 7 |
| Q | 8 | Q | 8 | Q |
|---|---|---|---|---|
Balance: $11.12 | Bet: $1.00 | Win: $0.00
Total Winnings: $7.12

|---|---|---|---|---|
| K | K | 7 | 9 | A |
| Q | K | A | K | A |
| 9 | K | 9 | 9 | 8 |
|---|---|---|---|---|
Balance: $10.12 | Bet: $1.00 | Win: $0.00
Total Winnings: $7.12

|---|---|---|---|---|
| 9 | K | 7 | Q | Q |
| 9 | 9 | 7 | 7 | 7 |
| 7 | 9 | Q | 8 | K |
|---|---|---|---|---|
Balance: $9.12 | Bet: $1.00 | Win: $0.00
Total Winnings: $7.12

|---|---|---|---|---|
| K | A | A | A | 9 |
| A | 8 | 8 | 7 | Q |
| 7 | Q | Q | A | 7 |
|---|---|---|---|---|
Balance: $8.12 | Bet: $1.00 | Win: $0.00
Total Winnings: $7.12

|---|---|---|---|---|
| 8 | K | Q | K | A |
| A | Q | K | 9 | K |
| K | 8 | 8 | K | 7 |
|---|---|---|---|---|
Balance: $7.12 | Bet: $1.00 | Win: $0.00
Total Winnings: $7.12

|---|---|---|---|---|
| K | Q | 8 | A | 7 |
| A | Q | 8 | A | 8 |
| 7 | Q | A | A | A |
|---|---|---|---|---|
Balance: $6.12 | Bet: $1.00 | Win: $0.00
Total Winnings: $7.12

|---|---|---|---|---|
| 8 | Q | 7 | Q | A |
| Q | 8 | 8 | A | K |
| Q | A | 7 | 7 | K |
|---|---|---|---|---|
Balance: $5.12 | Bet: $1.00 | Win: $0.00
Total Winnings: $7.12

|---|---|---|---|---|
| Q | 9 | K | 9 | K |
| K | Q | 9 | 9 | K |
| K | A | 8 | A | 8 |
|---|---|---|---|---|
Balance: $4.12 | Bet: $1.00 | Win: $0.00
Total Winnings: $7.12

|---|---|---|---|---|
| K | K | 8 | 9 | 8 |
| 9 | 7 | A | 9 | A |
| 8 | Q | 8 | 8 | 9 |
|---|---|---|---|---|
Balance: $3.12 | Bet: $1.00 | Win: $0.00
Total Winnings: $7.12

|---|---|---|---|---|
| K | 8 | K | 9 | 8 |
|[K]|[9]|[K]| K | 8 |
|[9]|[K]|[9]| A | A |
|---|---|---|---|---|
Balance: $2.92 | Bet: $1.00 | Win: $0.80
Total Winnings: $7.92

|---|---|---|---|---|
| Q | 7 | 9 | A |[8]|
|[8]|[8]| 9 |[8]| K |
|[8]|[8]|[8]| Q | 9 |
|---|---|---|---|---|
Balance: $5.06 | Bet: $1.00 | Win: $3.14
Total Winnings: $11.06

|---|---|---|---|---|
| 9 | 7 | A | 7 | 9 |
| Q | Q | A | K | 9 |
| 9 | Q | 7 | Q | K |
|---|---|---|---|---|
Balance: $4.06 | Bet: $1.00 | Win: $0.00
Total Winnings: $11.06

|---|---|---|---|---|
| 7 | K | 9 | 8 | 8 |
| 8 | Q | A | A | 8 |
| 7 | Q | Q | 8 | 9 |
|---|---|---|---|---|
Balance: $3.06 | Bet: $1.00 | Win: $0.00
Total Winnings: $11.06

|---|---|---|---|---|
| K | 8 | K | Q | 9 |
| 8 |[A]|[A]| 9 | A |
|[A]| K | 9 | A | A |
|---|---|---|---|---|
Balance: $2.36 | Bet: $1.00 | Win: $0.30
Total Winnings: $11.36

|---|---|---|---|---|
| 7 | Q | 7 | K | Q |
| Q | K | K | 7 | 8 |
| Q | Q | K | 9 | 8 |
|---|---|---|---|---|
Balance: $1.36 | Bet: $1.00 | Win: $0.00
Total Winnings: $11.36

|---|---|---|---|---|
| 8 | Q | A | 7 | Q |
| Q | A | A | 7 | 9 |
| 8 | A | K | 7 | 8 |
|---|---|---|---|---|
Balance: $0.36 | Bet: $1.00 | Win: $0.00
Total Winnings: $11.36

Insufficient funds

Profiling

This profile is after fixing up the low-hanging fruit on WinCandidate.win_class. There are other optimisations possible, especially if this is intended for headless simulation.

         5538495 function calls (5464053 primitive calls) in 2.292 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
   223326    0.451    0.000    0.571    0.000 288690.py:150(_format_row)
   310175    0.339    0.000    0.481    0.000 288690.py:71(map)
   322582    0.168    0.000    0.784    0.000 288690.py:134(_get_candidates)
    74442    0.150    0.000    0.150    0.000 {built-in method _locale.localeconv}
   310175    0.135    0.000    0.135    0.000 288690.py:141(<listcomp>)
   322582    0.131    0.000    0.252    0.000 288690.py:123(_get_winnings)
    37221    0.080    0.000    0.303    0.000 locale.py:265(currency)
   339265    0.076    0.000    0.101    0.000 types.py:176(__get__)
   310175    0.057    0.000    0.092    0.000 <string>:1(<lambda>)
   972468    0.056    0.000    0.056    0.000 {built-in method builtins.len}
86849/12407    0.053    0.000    0.984    0.000 {method 'join' of 'str' objects}
    37221    0.050    0.000    0.076    0.000 random.py:519(<listcomp>)
    12407    0.046    0.000    1.119    0.000 288690.py:110(__init__)
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.