5
\$\begingroup\$

This is a simple poker counter.

It lets players decide their actions and makes the currency calculations for them.

While adding a bit of console animation.

Can I achieve the same results with simpler code??

from string import ascii_lowercase

def get_player_names():
    '''
    Returns a List Of Playrer 
    Names Got From User Input
    '''
    i = 0
    player_names = []

    print('Enter Player Names...')
    print('Press Enter To Indicate That You\'re Done')
    while True:
        player = input()
        if player == '':
            break

        player_names.append(player)
        i += 1

        print('-'*40)
        print(f'Player#{i} Is Set To: "{player}"')
        print('-'*40)


    return player_names

def list_sequence(sequence):
    i = 0
    lengths = [len(item) for item in sequence]


    if type(sequence) is dict:
        print('-'*40)
        print('Current Currency:')
        for key in sequence:
            i += 1
            proportional_space = ' ' * ( (max(lengths) - len(key)) + 2 )
            print('-'*20)  
            print(f'{i})-{key}{proportional_space}: {sequence.get(key, 0)}')          
        print('-'*40)
    else:       
        print('Current List Of Players:')
        print('-'*26)
        for item in sequence:
            i += 1
            print(f'Player#{i} ---> "{item}"')
        print()
def start_currency(player_names):
    player_currency_dict = \
    {player: 100 for player in player_names}
    #player_currency_dict[player_names[0]] = 1
    return player_currency_dict


def list_player_statues(player_currency, player_statues):
    i = 0
    lengths = [len(player) for player in player_currency]
    for player in player_currency:

        i += 1
        currency = player_currency[player]
        state = players_statues[player]

        #formating
        proportional_length_factor= ( max(lengths) - len(player) ) + 1
        proportional_space = ' ' * proportional_length_factor
        remaining_space = 9 - len(player) - len(str(currency)) - proportional_length_factor - (3-len(str(currency)))
        proportional_hyphen = '-'*remaining_space
        #------------------------------------------------------------------------------------

        print('_'*15 + '||' + '_'*25)
        print(f'{i}.{player}{proportional_space}: {"0"*(3-len(str(currency)))}{currency} {proportional_hyphen} || {"-"*5}> {state}')
    print('_'*40)

player_names = ['Dante', 'Sam', 'Kat', 'Nancy']
#player_names = get_player_names()
player_currency = start_currency(player_names)
players_statues = {player: 'Stand By' for player in player_names}

list_sequence(player_names)
print('Each Player Gets 100 Currency For The Game')
list_sequence(player_currency)

confirm = input('Press Enter To Continue...')
print()


player_bids = {player: 10 for player in player_names}
total_bid = 10 * len(player_names)
player_currency = {player: player_currency.get(player) -10 for player in player_names}
round_over = ''


while len(player_names) > 0:

    for player in player_names:        



       player_folded = players_statues[player] == 'Folded'
       player_is_out = players_statues[player] == 'Out'

       if player_folded or player_is_out:
           continue

       players_statues[player] = 'Current Turn'

       #next player      
       player_index = player_names.index(player) + 1
       print(player_index)
       if player_index >= len(player_names):       
           player_index = 0

       next_player = player_names[player_index]
       print(next_player)
       players_statues[next_player] = 'Next Turn'   
       #------------------------------------------------ 

       list_player_statues(player_currency, players_statues)


       print()
       print('-'*40)
       print(f"{'~'*10} {player}'s Turn {'~'*10}")
       print('-'*40)      





       #getting action for each player
       while True:

            action = input()
            #print(player_bids[player])
            #print(max(player_bids))
            if action == 'ck' and player_bids[player] == max(player_bids.values()):
                bid = 0
                players_statues[player] = 'Check'
                break
            elif action == 'c':
                bid = max(player_bids.values()) - player_bids[player]
                players_statues[player] = 'Called'
                break
            elif action == 'r':
                while True:   
                    amount = input('Raise By?')
                    is_numeric = amount.isnumeric()
                    if is_numeric:
                        amount = int(amount)
                        bid = max(player_bids.values()) + amount - player_bids[player]
                        players_statues[player] = f'Raised By {amount}'
                        break
                    print('Please Enter a Number..')
                    print()
                break
            elif action == 'f':
                bid = 0   
                break
            else:
                bid = 0
                if player_bids[player] != max(player_bids.values()):
                    print("You Cant't check since a player raised the bid")
                else:
                    print('List Of Available Actions:')
                    print('"ck": Check \n"c": Call \n"r": Raise \n"f": Fold')



       #----------------------------------------------------------------------
       player_currency[player] -= bid
       player_bids[player] += bid
       total_bid += bid
       print()
       print()   
       print(f'Current Bid: {max(player_bids.values())}')
       print(f'Current Round Total: {total_bid}')
       print()
       print()           

    list_player_statues(player_currency, players_statues)
    round_over = input('Is The Round Over?')
    if round_over in ['yes', 'y']:
       winner = input('Who Won?')
       player_currency[winner] += total_bid
       total_bid = 0

       player_bids = {player: 10 for player in player_names}
       player_statues = {player: 'Stand By' for player in player_names}
       list_player_statues(player_currency, players_statues)


```
\$\endgroup\$
3
\$\begingroup\$

Tracking Players

There are a few instances where I see you are iterating over players in order to retrieve values from multiple dictionaries (player_currency_dict and player_statues). This is a hint that maybe that information should be stored in the same dictionary of players. I'd also keep the players' last bet in there as well:

# should look like this
players = {'name': [amount, state, last_bet], ...}

This way, it's really easy to keep track of those three pieces of information. To build this, you can swap out your get_player_names and start_currency functions for one simple function:

def get_player_dict(players):
    """
    Simply return a dictionary that holds players as keys and
    a list of [currency, state, last_bet]. This is a list so that you may
    edit those values
    """
    return {player: [100, 'Standby', 0] for player in players}

To get all of that information, you can use dict.items() in a loop like this:

# for keeping the list together
for name, status in players.items():
    # do things


# for full unpacking 
for name, (money, state, last_bet) in players.items():
    # do things

Why keep the list together?

I want to use a list because I can modify it in-place. This will be more clear later, but as a short example, I can do something like:

sample = {'Steve': [100, 'Standby', 0]}

def bet_10(my_list):
    my_list[0] -= 10
    my_list[2] += 10

bet_10(sample['Steve'])
sample
{'Steve': [90, 'Standby', 10]}

Displaying Game State

There are some functions you define that focus on displaying information. While this is a good use-case for a function, some of the formatting can be trimmed down. First, if you are trying to fill a string with a value, you can use ljust or rjust to fill to the right or left, respectively:

'abc'.ljust(7)
'abc    '

'abc'.rjust(7)
'    abc'

Also, magic numbers are usually a code smell. If you need to adjust them, I'd suggest passing them as parameters to make things a bit more flexible:

def show_players(players, name=20, currency=10, state=10, bet=10):
    """
    Pass the players dict and the padding for the four values,
    or just use the defaults
    """
    sts = (n.ljust(v) for n, v in zip(
        ("Name", "Currency", "State", "Bet"), (name, currency, state, bet))
    )
    header = "   {} | {} | {} | {}".format(*sts)
    print(header)
    for i, (n, (c, s, l)) in enumerate(players.items(), start=1):
        print('_'*len(header))
        n = n.ljust(name)
        c = str(c).ljust(currency)
        s = s.ljust(state)
        l = str(l).ljust(bet)
        print(f"{i}. {n} | {c} | {s} | {l}")


# Example
show_players(players)
   Name                 | Currency   | State      | Bet
______________________________________________________________
1. Dante                | 100        | Standby    | 0
______________________________________________________________
2. Sam                  | 100        | Standby    | 0
______________________________________________________________
3. Kat                  | 100        | Standby    | 0

I might prefer to show all of the info, that way, at the end of each turn I can display the state of the game and re-use this every time

The Game*

The core workings of your game execute in global scope and don't leverage a lot of functions. This makes changing code a bit more difficult. In pseudo-code, I would imagine this game would look something like:

# Each Round
loop {
    # each iteration would be a turn
    foreach player in  players{
        # Each player that hasn't folded gets a chance to bet
        case {
            player -> Raise
            player -> Fold
            player -> Call
            player -> Check
        } 
    }

    if all(bets match or all-in){
        break loop
    } else {
        continue
    }

}

Hopefully the notation isn't too wonky. This will be the basic structure for breaking down your main script.

Round Loop

You will want a loop to track each round of play. Each round will have a pool, which is the total money being bet by all players. It will also need a bid amount which you've noted already as the highest bet. You can just use another while loop to go through the players until an end condition is hit:

def play_round(players):
    """
    Loop over players in players dictionary and play
    a turn for each player. The win condition is defined by
    one of two criteria:
        For all players that have not folded they either:
            A) are All-In
            B) Have last_bet matching bid

    The pool (total) of money gambled is returned

    """
    bid, pool = 0, 0
    while True:
        for name, status in players.items():
            show_players(players)

            curr, state, last_bet = status
            # skip over non-eligible players
            if state == 'Fold' or state == 'Out':
                continue

            print(f"It is {name}'s turn!")
            print(f"The current bid is {bid}")

            # will need some sort of take_turn function
            player_bet = take_turn(name, status, bid)

            # add the appropriate bet to the pool of cash
            pool += player_bet
            # the bid is the max of the bid or the bet that was made
            bid = max(bid, player_bet)

        # Everyone's bet either matches current bid, is all-in, or has folded
        if all(
            curr==0 or last_bet==bid
            for curr, state, last_bet in players.values()
            if state != 'Fold'
        ):
            print('All bets match, ending round')
            break

    # return the pool to outer scope
    return pool

Player Turns

Each player will need a turn. This replaces the loop that gets each action (well, moves it more than replaces it) with a function. Really, there are four major actions a player can take: Check, Raise, Call, or Fold. Raise and Call will be pretty similar to one another, so that might be a function on its own. You can easily handle this with an if/else block:

def take_turn(name, status, bid):
    """
    A player is allowed the option to do one of five things:
        Raise -> Raise bid by integer amount
        Call -> Bet the same as the bid
        Fold -> Quit the hand
        Check -> Bet nothing (only applicable if bid is 0)
        All-In -> Bet everything they have

    Return amt to be added to the bid pool
    """

    # set opts as a dictionary for easy membership-testing
    # of user-selected options
    opts = {
        'c': 'check',
        'r': 'raise',
        'b': 'call',
        'a': 'all-in',
        'f': 'fold'
    }
    curr, state, last_bet = status
    if state == "All-In":
        print(f"Player {name} is already All-In")
        return 0

    while True:
        action = input(f"Which action would you choose? "
        f"({', '.join(map(': '.join, opts.items()))})").strip().lower()

        # This is a fast check to make sure inputs are valid
        # Loop again if not in options dictionary
        if action not in opts:
            print("Invalid option, please try again")
            continue

        # Otherwise, continue forward
        elif action == 'c':
            # If the player's last bet is not equal to the current one,
            # they are unable to check.
            if last_bet != bid:
                print(f"Cannot check, bid was raised to {bid}")
                continue

            print(f"Player {name} has checked")
            amt = 0

        elif action == 'a':
            print(f"Player {name} is going All-In")
            amt = all_in(status)

        elif action == 'f':
            status[1] = 'Fold'
            print(f"Player {name} has folded")
            amt = 0

        else:
            try:
                # We've abstracted Call and Raise into the same function
                # for ease of use
                amt = handle_action(name, status, action, bid)
            except (ValueError, TypeError):
                continue

        return amt

Now, we know we will want to return the bartered amount to the round loop so the money can be added to the pool. Here we just need to keep track of the current bid and the player itself. Now, the way handle_action is written depends on how the betting code is written, but in general, it's a way to handle two similar cases. Both are detailed below.

Betting

We will want a function for players to bet amounts of money. Using the general form from the example bet_10 function, you could re-organize it into something like:

def player_bet(player, amount):
    """
    use this to modify who bet what amount by deducting amount from the
    first value in the list representing the player in-place:

    >>> players = {'Steve': [100, 'Standby', 0]}
    >>> player_bet(players['Steve'], 50)
    >>> players
    {'Steve': [50, 'Standby', 50]}

    >>> player_bet(players['Steve'], 75)
    ValueError
     'Player does not have that amount to bet! Player has 50'
    """
    curr, state, last_bet = player
    if curr < amount:
        raise ValueError(
            f"Player does not have that amount to bet! Player has {curr}"
        )
    elif state == 'All-In':
        raise TypeError("Player that is All-In cannot bet")

    # decrement held money
    player[0] -= amount
    # while increasing the amount that was bet
    player[2] += amount

Using the fact that we are passing in player by reference, we can edit the values in-place so that we don't have to return anything. I am raising an exception for not having enough money because I don't want players who may have mis-typed a number to accidentally go All-In. I also might want going all in to be a special case of betting for the purpose of being explicit. This could be a matter of taste, but an all_in function might look like:

def all_in(player):
    """
    Take the amount that the player has left and
    return it, this is just in case we aren't immediately
    sure what the bet is going to be. Use a try-except so that
    going all-in is a bit more explicit and not accidental

    >>> players = {'Steve': [100, 'Standby', 0]}
    >>> initial_bet = 150
    >>> pool = 150
    >>>
    >>> try:
    ...     player = players['Steve']
    ...     player_bet(initial_bet)
    ... except ValueError:
    ...     pool += all_in(player)

    >>> pool
    250
    >>> players
    {'Steve': [0, 'All-In', 100]}
    """
    curr, *_ = player
    # set held funds to zero
    player[0] -= curr
    # add that to bet funds
    player[2] += curr
    # set status to All In
    player[1] = 'All In'
    return curr

I want to return currency because I don't want to have to worry about how much a player has when I call that function.

handle_action

Now we can roll Call and Raise into a function that I've called handle_action:

def handle_action(name, status, action, bid):
    """
    A function to help keep take_turn function from being too difficult to read,
    especially since Raise and Call are almost the same event
    """
    curr, state, last_bet = status

    # the if/elif statement here helps define two parameters
    # amt and message, and raise any exceptions otherwise
    if action == 'r':
        try:
            amt = int(
                input("Raise by how much? ")
            )
        # int function raises a TypeError on bad input
        except TypeError:
            print("Please input an integer amount of money")
            raise
        else:
            message = f"Player {name} has raised bet by {amt}"

    elif action == 'b':
        amt = bid - last_bet
        message = f"Player {name} has called current bet of {bid}"

    # Now, try the player_bet function since amt and message are set
    # and return amt only if player_bet was called successfully
    try:
        player_bet(status, amt)
        print(status)
        print(message)
    except ValueError as e:
        # Player will need to explicitly select 'a' as an option
        # to go all-in
        print(e)
        print(f"You do not have enough, if you want to go all-in, select 'a'")
        raise
    except TypeError as e:
        print(e)
        raise

    return amt

Main Function

You usually want a main function to run your program. In this case, main will be the game loop that runs each round. I've put as many comments as I can to try to be clear, this is already a pretty long answer as is:

def main(players):

    while True:
        pool = play_round(players)

        # Show results after round
        show_players(players)
        print("Round Over")

        # If all remaining players folded for some reason, this will allow the program
        # to handle that case and skip the while loop
        is_winner = any(x not in ('Fold', 'Out') for _, x, _ in players.values())

        while is_winner:
            # I like to sanitize input by at least stripping off
            # possible leading/trailing whitespace someone may have inadvertently
            # entered
            winner = input("Who was the winner? ").strip()
            try:
                winner = players[winner]
                # Prevent cheating! People who are out or folded cannot win...
                if winner[1] in ('Fold', 'Out'):
                    print("That can't be! Must be a different winner...")
                    continue

                # add the pool to the winner's available cash pile
                winner[0] += pool
            except KeyError:
                # selected winner wasn't in the players dictionary
                print("Unrecognized player, please try again")
            else:
                break
        else:
            # if `is_winner` is False, the while loop will skip to
            # this else statement
            print("How on earth was there no winner??")

        play_again = input('Play Again? [y/n] ').lower().strip() == 'y'
        if not play_again:
            print("Ok, goodbye")
            sys.exit(0)

        # Mark players with no money left as out
        no_money_left = [name for name, (money, *_) in players.items() if not money]
        if no_money_left:
            l = '\n '.join(no_money_left)
            print(f"The following players are Out: {l}")
            for player in no_money_left:
                # sets state in-place
                players[player][1] = 'Out'

        # reset previous bets and states to default values for players
        # who are still in for the next round
        for player, status in players.items():
            _, state, _ = status
            status[2] = 0
            if state != 'Out':
                status[1] = 'Standby'

if name == "main"

Usually, most programs will want a __name__ == "__main__" guard. This guard will only be True if the program is called like python poker.py but not if it is imported like from poker import main. Also, it might be helpful to pass the players names as sys args:

if __name__ == "__main__":
    import sys

    players = get_player_dict(sys.argv[1:])
    if not players:
        # since this is a cli-game anyways, might be helpful to display a 
        # use message if they called it with no players
        usage()
        sys.exit(0)

    main(players)

Where usage might look like:

def usage():
    print("Call this script by passing the names of players as")
    print("sys-args like so:")
    print(" python poker.py Alice James Steve Laura ")

Odds and Ends

Checking types of objects

It is more pythonic to use isinstance rather than type(obj) == <type>

d = {}

# do this
if isinstance(d, dict):

# not this
if type(d) == dict:

And is is a keyword to stay away from, as it doesn't do exactly what you think it does, even though in this case it gives True, that's not always the case. Take this for example:

a = []
b = []
a is b
False

# however
a == b
True

Checking for emptiness of a container

Checking to see if a dict, list, set, tuple, string, etc is empty should be done using its native __bool__ implementation like this:

d = {}

if not d:
   # empty

Tracking an index in a loop

Any time you see this kind of code:

i = 0
for item in collection:
    # do something
    i += 1

It should be refactored to use enumerate:

for i, item in enumerate(collection):
    # do something

Multi-Line Statements

Any time you have a statement that is going on for too long, you can use parentheses rather than \ to wrap a long line:

x = (1 + 2 + 3 + 4
+ 5 + 6 + 7)
| improve this answer | |
\$\endgroup\$
3
\$\begingroup\$

I am not certain what you are asking, so I will mention three ideas about your code and hope it helps.

Worry mostly about your function level

def list_sequence(sequence):

This is a tricky function, mostly because it is really:

def print_players_or_currency(player_list_or_currency_dict)

You should rarely need to look at the type of an object (with type() or isinstance()) and only when doing meta-programming. Make two functions.

Play with f-strings formatting

    for key in sequence:
        i += 1
        proportional_space = ' ' * ( (max(lengths) - len(key)) + 2 )
        print('-'*20)  
        print(f'{i})-{key}{proportional_space}: {sequence.get(key, 0)}')          

could be shorter:

for i, (key, value) in enumerate(sequence.items()):
    print(f"{'':->20}\n{i+1:2}-{key:<{max(lengths)}}: {value}")

Always use a main() function

You have about a hundred lines at the top level. The problem is that the top level creates questions about global variables, ordering declarations and so on. It is far better to throw all hundred lines inside a main function and call it. While this might seem odd to insist on, remember that the Python interpreter just runs lines of code from top to bottom while keeping track of name spaces. This means that def foo(): .... is just a statement. The problem is that the top level code creates global variables, can only call functions if the def was seen first, and so on. Save confusion: put all your code inside a function and just have imports and a main() at the top level.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ fixed, thank you. \$\endgroup\$ – Charles Merriam Jan 30 at 6:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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