7
\$\begingroup\$

I've written a simple text game in Python to show you. I would be glad if you would spare a quick look and point out the style errors so I wouldn't pick up bad habits.

""" A text game "Bob, the Cookie Cooker". """

import random

COINS_FOR_WIN        = 1000000
FINE_COOKIE_PRICE    =      30
POOR_COOKIE_PRICE    =      10
PRICE_DEVIATION      =       5
HUGE_DOUGH_AMOUNT    =    1000
DOUGH_PRICE          =      10
MAX_LOAN             =   10000
LOAN_PRICE           =     100
TICKET_PRICE         =       1
LOTTERY_PRIZE        =       2
BEG_MONEY            =       8
TIME_COOKIE_COOK     =       1
TIME_BIG_DEAL        =      60
TIME_SMALL_DEAL      =      30
TIME_GRANDMA         =      60
TIME_BANK_SERVICE    =       5
TIME_LOTTERY         =       1
TIME_BEGGING         =    1440
CHANCE_LOTTERY       =    1000
CHANCE_SKILL_COOK    =    1000
CHANCE_SKILL_GRANDMA =     100

cookies = 0
minutes = 0
skill   = 0
coins   = 0
dough   = 0
bribe   = 1
debt    = 0
term    = 0

def main():
    """ The main function. """
    intro()
    while menu():
        if check_for_end():
            break

def intro():
    """ Print's out a welcoming string. """
    print ( """
                                    .--,--.
                                    `.  ,.'          Bob,
                                     |___|    the Cookie Cooker
                                     :o o:   O      
                                    _`~^~'_  |  
                                  /'   ^   `\=)
                                .'  _______ '~|
                                `(<=|     |= /'
                                    |     |
                                    |_____|
                             ~~~~~~~ ===== ~~~~~~~~
            """ )
    print ("\tWelcome, my dear player! I am Bob, the Cookie Cooker! I dream to")
    print ("\tcook my way to the top.  But I am not a smart guy, so that's why")
    print ("\tI'm talking to You. I need your guidance. Please, turn me into a")
    print ("\tCookie Master!  Help me to become rich and famous!  So let's go!")
    print ()

def menu():
    """ Player selects action he/she wants to do. """
    methods = { 1: work, 2: buy, 3: grandma, 4: bank, 5: lottery, 6: beg}
    print ("Cookies made: {}. Game-time spent: {}.".format(cookies, minutes))
    print ("Skill: {}%. Coins: {}. Dough: {} kg.".format(skill, coins, dough))
    print ( """
                 Action:
                         1. Go to work.
                         2. Go to supermarket.
                         3. Visit grandma.
                         4. Visit bank.
                         5. Buy a lottery ticket.
                         6. Beg for money.
                         7. Exit.
            """ )
    selection = get_valid_int(1, 7)
    if selection != 7:
        methods[selection]()
        return True
    else:
        return False

def get_valid_int(min_bound, max_bound):
    """ Prompts input until a positive integer in provided range is entered. """
    while True:
        user_input = input(">> ")
        if is_int(user_input):
            user_input = int(user_input)
            if min_bound != None and user_input < min_bound:
                print ("Number is too small.")
                continue
            if max_bound != None and user_input > max_bound:
                print ("Number is too big.")
                continue
            break
    return user_input

def is_int(string):
    """ Returns if string can be converted into a positive integer or 0. """
    try:
        value = int(string)
        return True
    except ValueError:
        print ("Input is not n integer.")
        return False

def work():
    """ Player cooks & sells specified amount of cookies. """
    print ("How many cookies do you want to cook?")
    amount = get_valid_int(0, None)
    cookie_loop(amount)

def cookie_loop(amount):
    """ Tries to cook and sell the specified amount of cookies. """
    global minutes
    for i in range(amount):
        minutes += TIME_COOKIE_COOK
        if dough > 0:
            cook_cookie(i)
        else:
            be_confused(i)

def cook_cookie(i):
    """ Cooks and sells a cookie. """
    global cookies, skill, coins, dough
    cookies += 1
    dough   -= 1
    price = random.randrange(PRICE_DEVIATION)
    if random.randrange(101) > skill:
        # A poor cookie is cooked
        price += POOR_COOKIE_PRICE
        print ("You sell a poor cookie #{} for {} coins.".format(i+1, price))
    else:
        # A fine cookie is cooked
        price += FINE_COOKIE_PRICE
        print ("You sell a FINE cookie #{} for {} coins.".format(i+1, price))
    coins += price
    # There's a 1 in a CHANCE_SKILL_COOK to improve your skill by 1%:
    if random.randrange(CHANCE_SKILL_COOK) == 42 and skill < 100:
        skill += 1

def be_confused(i):
    """ Stands confused for no dough is left. """
    print ("Minute #{} passes. You're confused - no dough is left.".format(i+1))

def buy():
    """ Player visits the supermarket to buy dough. """
    global minutes, coins, dough
    print ("Hello! We are the biggest supermarket in town. We only sell dough.")
    print ("How much would you like?")
    offer = get_valid_int(0, None)
    if offer == 0:
        print ("Well, okay then.")
    elif offer > HUGE_DOUGH_AMOUNT and offer * (DOUGH_PRICE / 2) <= coins:
        # Big deals come 50% off
        print ("Wow. That's a big deal! It's 50% for big clients. Here you go!")
        coins -= int (offer * (DOUGH_PRICE / 2) )
        minutes += TIME_BIG_DEAL
    elif offer * DOUGH_PRICE <= coins:
        # Standart deals for standart clients
        print ("Thank you for using our services. Come again!")
        coins -= int (offer * DOUGH_PRICE)
        minutes += TIME_SMALL_DEAL
    else:
        print ("I appreciate your intentions, but you don't have enough money.")
        return
    dough += offer

def grandma():
    global minutes, skill, coins, bribe
    """ Player visits grandma to get some cookie cooking experience. """
    print ("Gradma: howdy son! Me sees ya wanna get some tips, so tell me ")
    print ("how much ya hearts says you shoulda give me.")
    offer = get_valid_int(0, coins)
    if offer < bribe:
        print ("Ya greedy bastard! You think wisdom comes for free?")
        print ("Me teach ya a lesson and take your money for nothin'.")
    else:
        bribe += offer
        print ("Grandma tells you a secret cookie cooking secret.")
        if random.randrange(CHANCE_SKILL_GRANDMA) == 7 and skill <= 95:
            # There's 1 in CHANCE_SKILL_GRANDMA to increase your skill by 5%
            skill += 5
    minutes += TIME_GRANDMA
    coins -= offer

def bank():
    """ Player goes to bank to loan some coins or to repay his debts. """
    print ("Bank: we loan up to {} for {} coins.".format(MAX_LOAN, LOAN_PRICE))
    print ("Don't forget that we find everyone who doesn't repay in time.")
    print ("Enter '1' to request a loan.")
    print ("Enter '2' to repay your debt.")
    print ("Enter '3' for a term reminder.")
    print ("Enter '4' to exit the bank.")
    selection = get_valid_int(1, 4)
    if selection == 1:
        take_a_loan()
    elif selection == 2:
        repay_debt()
    elif selection == 3:
        if debt == 0:
            print ("You've got no debts!")
        else:
            print ("Repay your debts before minute #{}.".format(term))
    else:
        print ("Bank says you a cold official good bye.")

def take_a_loan():
    """ Player tries to take a loan at bank. """
    global minutes, coins, term, debt
    if debt != 0:
        print ("We can't give you another loan. You already have debts.")
    elif coins < LOAN_PRICE:
        print ("You don't have enough yellows to purchase a loan.")
    else:
        debt = get_valid_int(0, MAX_LOAN)
        if debt > 0:
            minutes += TIME_BANK_SERVICE
            term = int (minutes + debt / 2)
            coins += debt
            print ("Thank you for using our services. Repay in time.")
            print ("You have to repay the debt before minute #{}.".format(term))

def repay_debt():
    """ Player tries to repay debt at bank. """
    global minutes, coins, term, debt
    if debt == 0:
        print ("You've got no debt to repay.")
    elif coins >= debt:
        minutes += TIME_BANK_SERVICE
        coins -= debt
        debt = 0
        term = 0
        print ("Thank you. Come again.")
    else:
        print ("You don't have enough money to repay your debt.")

def lottery():
    """ Player plays lottery. """
    global minutes, coins
    print ("Enter nothing to purchase a lottery ticket, anything else to exit.")
    while input(">> ") == "":
        if coins >= TICKET_PRICE:
            minutes += TIME_LOTTERY
            coins -= TICKET_PRICE
            print ("You bug a ticket for {}.".format(TICKET_PRICE))
            if random.randrange(CHANCE_LOTTERY) == 666:
                # There's 1 in CHANCE_LOTTERY to win a lottery
                coins += LOTTERY_PRIZE
                print ("You won {}.".format(LOTTERY_PRIZE))
            else:
                print ("You didn't win.")
        else:
            print ("You don't have enough money to purchase a lottery ticket.")
            break

def beg():
    """ Player goes on the streets begging people for coins. """
    global minutes, coins
    beg_money = random.randrange(BEG_MONEY)
    print ("You beg people for money on the streets for whole day.")
    print ("People give you this many coins: {}.".format(beg_money))
    minutes += TIME_BEGGING
    coins += beg_money

def check_for_end():
    """ Checks if player won or lost. """
    if term > minutes:
        print ("GAME OVER. You forgot to pay your debt and bank got you.")
        return True
    elif coins > COINS_FOR_WIN:
        print ("GAME OVER. You won in {} minutes.".format(minutes))
        return True
    else:
        return False

if __name__ == "__main__":
    main()

Writing a program in a single source file raises worrying things:

  • Use of global variables (how would you eradicate them here?)
  • Everything is very diverse, there's little unification over type of content.
  • The navigation gets more and more difficult as file expands.

To fight these problems I found a following solution:

import sharedclass # Global stuff in a class shared by multiple game states
import gamestate1  # Holds a game state class with its vars, defs and everything
import gamestate2  # Game state holds a class with run method
import gamestate3  # Run method returns an int that tells which stage to switch to

shared = sharedclass.Shared()
associations = [ gamestate1.Game_State1(),
                 gamestate2.Game_State2(),
                 gamestate1.Game_State3()]

index = 0
while shared.is_running:
    index = associations[index].run(shared)
    if index == -1:
        break

What do you think of it? Is it an OK program design?

\$\endgroup\$
  • \$\begingroup\$ Oh, I'm sorry. I left comments like # There's 1 in 1000 chance to win a lottery when 1000 was a magic number and not a constant. Then I changed them but forgot to modify the comments. Sorry. \$\endgroup\$ – Marius Macijauskas Apr 14 '15 at 19:32
  • 1
    \$\begingroup\$ No problem. Because there are no answers, you can quickly modify the comments before someone posts an answer. \$\endgroup\$ – Hosch250 Apr 14 '15 at 20:17
4
\$\begingroup\$

This is a very good start - the code is broken up into logical functions, generally follows the style guide and includes explanatory docstrings. Well done!


One of the pet peeves in Python's style guide, which was being discussed on SO a short while ago, is using whitespace to line up operators, e.g.:

COINS_FOR_WIN        = 1000000
FINE_COOKIE_PRICE    =      30

would generally be written:

COINS_FOR_WIN = 1000000
FINE_COOKIE_PRICE = 30

This reduces the maintenance overhead if you add or rename a constant, or change one of the values.


You can use multiline strings and textwrap.dedent (see Avoiding Python multiline string indentation), rather than line after line of print, to neaten up the blocks of text:

print(textwrap.dedent('''
                                .--,--.
                                `.  ,.'          Bob,
                                 |___|    the Cookie Cooker
                                 :o o:   O      
                                _`~^~'_  |  
                              /'   ^   `\=)
                            .'  _______ '~|
                            `(<=|     |= /'
                                |     |
                                |_____|
                         ~~~~~~~ ===== ~~~~~~~~
       Welcome, my dear player! I am Bob, the Cookie Cooker! I dream to
       ...
'''))

There are generally two ways to avoid global state:

  1. Pass all necessary state into and out of a function explicitly; or
  2. Encapsulate the state in another object (e.g. a dictionary or class).

For the first, for example:

def cookie_loop(amount, minutes):
    """ Tries to cook and sell the specified amount of cookies. """
    for i in range(amount):
        minutes += TIME_COOKIE_COOK
        if dough > 0:
            cook_cookie(i)
        else:
            be_confused(i)
    return minutes

As the required state is passed in and returned explicitly, there's no need for global, but you now need to call e.g. minutes = cookie_loop(amount, minutes) rather than cookie_loop(amount) in work.

However, note that some functions take and modify many different parts of the state, so passing these all explicitly back and forth would quickly get out of hand:

minutes, coins, bribe = grandma(minutes, skill, coins, bribe)

This suggests encapsulation might be helpful, e.g. you could have a dictionary:

game_state = dict(
    cookies=0,
    minutes=0,
    skill=0,
    coins=0,
    dough=0,
    bribe=1,
    debt=0,
    term=0,
)

Now you only pass a single state parameter around, then e.g. state['minutes'] += TIME_COOKIE_COOK. Alternatively, you could look into OOP, and develop a CookieGame class that holds all of the state and provides methods for manipulating it, rather than passing it around to different functions.


In terms of splitting it up into smaller parts, note that the various tasks could be standalone functions, that take and mutate a single object representing a player (which holds e.g. the player's skill and money) and return the elapsed time, and could live in separate modules. For example:

from grandma import visit_grandma
from kitchen import bake_cookies
from supermarket import buy_dough

# import other standalone tasks

TASKS = {
    'Visit grandma': visit_grandma,
    'Bake some cookies': bake_cookies,
    'Buy some dough': buy_dough,
    # build dictionary of tasks
}

# user makes a choice from the keys

minutes += TASKS[key_choice](user)  # call task function with user object

This should make adding a new task as easy as importing the appropriate function and adding it to TASKS. Here e.g. grandma.py would hold all of the constants specific to that task, along with the task function (and any supporting sub-functions). Utility functions like get_valid_int would be in a separate file, e.g. utils.py, so that any task module can access them.

\$\endgroup\$
  • \$\begingroup\$ Thank you! A great answer indeed. I'm going to use the lessons I learned in my new project I plan to start and finish today. \$\endgroup\$ – Marius Macijauskas Apr 15 '15 at 7:54

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.