2
\$\begingroup\$

This is a second shot at creating a Martingale betting simulator. The original code needed such heavy refactoring that I just started from scratch.

Script simulates the Martingale betting strategy of betting a fixed amount until a loss occurs, at which point the bet is doubled to make up for the loss. This continues until a win occurs, after a win the bet is reset to the original bet value. I set the odds to mimic Blackjack (49% chance of a win). For simplicity, the amount won or lost in a round is equal to the bet. The simulation ends when the specified number of rounds has elapsed, the size of the next bet is larger than the current available funds, the available funds reach 0, or the goal profit is met.

I'm brand new to Python and coding in general, so any feedback would be appreciated.

import random


def main(rounds=10, bet=25, goal_profit=1000, each=False, end_script_prt=True):
    """Runs a simulation of the Martingale betting strategy over a specified number of rounds.
    each = True will print a summary of each round's results. end_script_prt = True will
    print a summary of the game's results"""
    original_bet = bet
    current_bet = bet
    starting_funds = 5000
    current_profit = 0
    current_funds = starting_funds
    round_n = 0
    wins = 0
    losses = 0

    while current_profit < goal_profit\
            and round_n < rounds\
            and current_funds > 0\
            and current_funds > current_bet:
        round_n += 1
        rng_v = rng()
        current_funds += change_current_funds(current_bet, rng_v)
        current_profit = current_profits(current_funds, starting_funds)
        if winloss_generator(rng_v) == 'win':
            wins += 1
        elif winloss_generator(rng_v) == 'loss':
            losses += 1
        if each:
            print('ROUND:', round_n, 'of', rounds)
            print('BET:', current_bet)
            print('OUTCOME:', winloss_generator(rng_v).capitalize())
            print('WINS/LOSSES:', wins, 'Wins', losses, 'losses')
            print('CURRENT FUNDS:', current_funds)
            print('CURRENT PROFIT:', current_profit)
            print()
        current_bet = change_bet(original_bet, current_bet, rng_v)
    print()
    if end_script_prt:
        end_script(round_n, wins, losses, starting_funds, current_funds, goal_profit, current_profit)
    return change_iterated_winloss(current_profit, goal_profit)


def rng():
    """Returns random number"""
    return random.random()


def winloss_generator(rng_v):
    """Returns win/loss condition"""
    if rng_v <= .49:
        return 'win'
    if rng_v > .49:
        return 'loss'


def change_current_funds(current_bet, rng_v):
    """Returns change in funds resulting from round outcome"""
    if winloss_generator(rng_v) == 'win':
        return current_bet
    if winloss_generator(rng_v) == 'loss':
        return current_bet * -1


def change_bet(original_bet, current_bet, rng_v):
    """Returns updated bet value
    If outcome is a win, bet is reset to original value. If outcome is a loss bet is doubled"""
    if winloss_generator(rng_v) == 'win':
        return original_bet
    if winloss_generator(rng_v) == 'loss':
        return current_bet * 2


def current_profits(current_funds, starting_funds):
    """Returns current profit"""
    return current_funds - starting_funds


def end_script(round_n, wins, losses, starting_funds, current_funds, goal_profit, current_profit):
    """Prints final outcome of the game and summary of the results"""
    print('*************************')
    if current_profit >= goal_profit:
        print('YOU WIN!')
    else:
        print('YOU LOSE')
    print('TOTAL ROUNDS:', round_n)
    print('WIN/LOSS RECORD:', wins, 'Wins', losses, 'Losses')
    print('STARTING FUNDS:', starting_funds)
    print('ENDING FUNDS: ', current_funds)
    print('GOAL PROFIT:', goal_profit)
    print('ENDING PROFIT:', current_profit)


def change_iterated_winloss(current_profit, goal_profit):
    """Returns game's win/loss outcome as a string"""
    if current_profit >= goal_profit:
        w_l = 'w'
        return w_l
    else:
        w_l = 'l'
        return w_l


def iterated_winloss_count(iterations, each=False):
    """Returns a summary of the total win/loss record across game iterations. each = True will
    print each game result individually"""
    total_wins = 0
    total_losses = 0
    for x in range(iterations):
        game_outcome = main(1000, 25, 5000, False, False)
        if game_outcome == 'w':
            total_wins += 1
            if each:
                print('WIN!')
        if game_outcome == 'l':
            total_losses += 1
            if each:
                print('LOSS')
    print()
    print('WINS/LOSSES OVER', iterations, 'ITERATIONS:', total_wins, 'Wins', total_losses, 'Losses')


#  Single Game
if True:
    main(1000, 25, 5000, True, True)

#  Iterated Games
if False:
    iterated_winloss_count(1000, False)

\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

You have a few functions with an unusual conditional structure. I might be misunderstanding something, but I assume the player either wins or loses. In that light, there's no purpose to the second conditional check: just use else.

def winloss_generator(rng_v):
    if rng_v <= .49:            # win
        return 'win'
    if rng_v > .49:             # else loss
        return 'loss'

It's not the most common problem among people learning, but you've substantially over-factored the calculations, making a simple thing hard to understand. A random number is generated, and it will determine win/loss (but we don't remember the outcome); then that random number weaves its way through multiple steps, each of them calling winloss_generator(RANDOM_NUM), which tells us (again) whether it's a win or loss. Instead, directly in main(), just determine the outcome immediately.

won = (random.random() <= .49)

With that variable remembered, the rest of the calculations are easy to understand all in one place. When computations are fairly simple, closely related, and easy to follow as a group, keep them together as a general rule:

current_funds += current_bet * (1 if won else -1)
current_profit = current_funds - starting_funds
wins += int(won)
losses += int(not won)
...
current_bet = original_bet if won else 2 * current_bet

In addition to over-factoring the algorithm, you under-factored the code from the perspective of side effects (printing being the side effect here). The general idea is to corral the side effects in the program's thin outer layer. The rest of the code can then operate purely in the realm of data-returning functions (that's the hope, at least; reality does pose challenges). Here's one way your code might be refactored to impose that separation.

from collections import namedtuple

# A simple data object to store all relevant details
# for one round of play.
Round = namedtuple('Round', 'round_n won funds profit wins losses bet')

# Entry point.
def main():

    # Run the rounds and get data back. No printing yet.
    rounds = list(martingale(200, 25, 5000))

    # Analyze the results as much as we like.
    for r in rounds:
        ...

    # Report the results. Printing OK here.
    for r in rounds:
        print(...)

# The algorithmic code of the betting strategy.
# It sticks fairly close to your original code, absent side effects.
def martingale(rounds=10, bet=25, goal_profit=1000):
    funds = 5000
    orig_bet = bet
    orig_funds = funds
    profit = 0
    round_n = 0
    wins = 0
    losses = 0
    while profit < goal_profit and round_n < rounds and funds > bet:
        round_n += 1
        won = (random.random() <= .49)
        funds += bet * (1 if won else -1)
        profit = funds - orig_funds
        wins += int(won)
        losses += int(not won)
        yield Round(round_n, won, funds, profit, wins, losses, bet)
        bet = orig_bet if won else 2 * bet

That design is better because you can do more things with it. Imagine adding support for command-line arguments to define the input parameters, plus various options for different types of analysis or reporting. You can add all of that without altering the algorithmic code in martingale(). It's also feasible now to write another function implementing a different betting strategy: it too would return rounds, and you could analyze them with the same code used on martingale(). Finally, another thing you might want is to be able to test a function like this: either in an automated fashion or ad hoc in debugging sessions. Both are easier if you can call the function and get back an answer-as-data rather than a pile of printed text. Save the printing for later: first get the data right.

\$\endgroup\$
1
  • \$\begingroup\$ Thank you very much! I appreciate the feedback \$\endgroup\$ Commented Sep 22, 2020 at 13:06

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.