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?

200_success
145k22 gold badges190 silver badges478 bronze badges
asked Apr 14, 2015 at 19:20
\$\endgroup\$
2
  • \$\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\$ Commented Apr 14, 2015 at 19:32
  • 1
    \$\begingroup\$ No problem. Because there are no answers, you can quickly modify the comments before someone posts an answer. \$\endgroup\$ Commented Apr 14, 2015 at 20:17

1 Answer 1

3
\$\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.

answered Apr 14, 2015 at 22:07
\$\endgroup\$
1
  • \$\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\$ Commented Apr 15, 2015 at 7:54

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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