8
\$\begingroup\$

This is a story-based adventure program I coded by myself (forgive the length). It took a long time but it was fun. It includes loading times, changes in relationships, relationship levels, storyline and possible outcomes, user input with xbox-style options, function calling and more. Your choices determine the outcomes and scenarios you land yourself in. Relationships go up or down(+x, -x) depending on your choices. In the end, the code prints all the choices you made and the impact on your community.

import time

def long_sleep():
    for num in range(5):
        time.sleep(1)
        print('Loading...')
def short_sleep():
    for num in range(3):
        time.sleep(1)
        print('Loading...')
#loading times     
choices = []
synopsis = '''13 December 2027. A year into the zombie apocolypse, you are the young leader of a small, demoralized group in the middle of nowhere, fighting for a chance to see 
light at the end of the tunnel. By September of next year, your group has grown greatly but that does not mean that your community on the brink of collapse. You must the make 
tough political decisions to determine how your community fares.'''

vengeful = 'VENGEFUL.'
hateful = 'HATEFUL.'
disappointed = 'DISAPPOINTED.'
conflicted = 'CONFLICTED/NUETRAL.'
satisfied = 'SATISFIED.'
happy = 'HAPPY.'
prosperous = 'PROPEROUS'
#relationship levels
army_government = 'ARMY & GOVERNMENT'
civilian = 'CIVILIANS'
everybody = 'EVERYBODY'
civil_great_increase = ' This greatly improves your relationship with ' + civilian + '.'
civil_increase = ' This improves your relationship with ' + civilian + '.'
civil_slight_increase = ' This slightly improves your relationship with ' + civilian  + '.'
civil_slight_decrease = ' This slightly decreases your relationship with ' + civilian + '.'
civil_decrease = ' This worsens your relationship with ' + civilian + '.'
civil_great_decrease = ' This greatly worsens your relationship with ' + civilian + '.'
army_great_decrease = ' This greatly worsens your relationship with ' + army_government + '.'
army_decrease = ' This worsens your relationship with ' + army_government + '.'
army_slight_decrease = ' This slightly decreases your relationship with ' + army_government + '.'
army_slight_increase = ' This slightly improves your relationship with ' + army_government + '.'
army_increase = ' This improves your relationship with ' + army_government + '.'
army_great_increase = ' This greatly improves your relationship with ' + army_government + '.'
everybody_great_increase = ' This greatly improves your relationship with ' + everybody + '.'
everybody_increase = ' This improves your relationship with ' + everybody + '.'
everybody_slight_increase = ' This slightly improves your relationship with ' + everybody + '.'
everybody_slight_decrease = ' This sligtly decreases your relationship with ' + everybody + '.'
everybody_decrease = ' This worsens your relationship with ' + everybody + '.'
everybody_great_decrease = ' This greatly worsens your relationship with ' + everybody + '.'
traitor = ' ' + everybody + ' wants you dead.'
hero = ' ' + everybody + ' looks to you as a hero. '

winter = '\n' + '''29 January 2029. It is five weeks into winter and the season shows no mercy. A drought happened for a majority of the last fall and it devastated 
the food supply. As your community dives deeper into the winter, you realize that your supply will run out if consumption is not altered. You could do one of two options: reduce 
consumption among civilians, or ignore the risk and take a chance([ALTER SUPPLY]X}  {B[IGNORE RISK]).''' + '\n> '
alter_supply = '\n' + '''Your government is now seen as selfish. You took the risk to protect the important people and "do your best with the rest". You have suffered heavy 
civilian losses but your army and government losses have been few. As a result, there is division and danger in the streets. Riots breaking out, murders, arson, all happening in
your community.''' + civil_great_decrease
ignore_risk = '\n' + '''Your community did better than expected during the period. That is until you ran out of food in early March. Now you rely solely on scavenging, 
risking getting devoured by zombies in order to go another day. Half your community is either dead or lost with great amount of casualties from civilians and 
non-civilians.''' + army_great_decrease

spring = '\n' + '''27 March 2029. One way or another, you have made through the harsh winter and now must face a totally obstacle that could jeopardize your 
survival. A group of violent, hostiles target your community and threaten to overtake it if not their demands are met([DEFEND]X}  [MERGE]B}  [NEGOTIATE]A})''' + '\n> '
defend_alter_supply = '\n' + '''It was a tough battle but it was victory in the end. You sucessfully fended off the hostiles. Your army took a heavy blow but it is still intact. 
Tensions are even worse though as hostile sympathizers were suppresed all around the community.''' + civil_decrease
merge_alter_supply = '\n' + '''You have sucessfully merged with hostiles giving most of what is owned to them. Civilians have actually commended this call in hopes of being 
treated better. Nobody was harmed.''' + civil_increase + army_slight_decrease
negotiate_alter_supply = '\n' + '''You have sucessfully made a deal with the hostiles giving a large amount of resources in order to keep some peace. Your strugling-to-survive 
civilians are irate with having to deal with even worse conditions. The government and army is also starting to starve. There are few to work with and things are not 
looking up soon.''' + everybody_great_decrease
defend_ignore_risk = '\n' + '''Your whole community got destroyed. Everyone is dead, nice one chief.'''
merge_ignore_risk = '\n' + '''You have sucessfully merged with hostiles giving most of what is owned to them. Everybody sees this as the best possible option to end the 
famine.''' + everybody_increase
negotiate_ignore_risk = '\n' + '''The last portions of supplies have been all been swallowed by the hostiles. You are left with nothing, nice one chief.'''

outbreak = '\n' + '''22 May 2029. There was no problem in sight until one happened on this date. Civil-government relations even improved. West Nile virus has shaken your 
community to the core. All healthy folks are in quarantine including you do treat the sick, which could heavily strain resources, or exile them, which could skyrocket tensions 
between civilians and the government([TREAT]X}  {B[EXILE])''' + '\n> '
outbreak_treat = '\n' + '''You have treated everyone with the sickness at a cost for low meds. Another epedemic happened a few weeks later, and you lost a lot of 
people.''' + army_decrease + civil_increase
outbreak_exile = '\n' + '''You exiled the least useful to keep the most useful surviving. Though, resouces and supplies are stable and death is 
rare.''' + civil_great_decrease + army_slight_increase
population = '\n' + '''2 June 2029. Everyone is starving in a famine worse than what you imagined. Once again people are dying of starvaton and once again it is your job 
to decide what happens ([POPULATION REDUCTION]X}  {B[DESPERATION])''' + '\n> '
population_reduce = '\n' + '''Remaining civilians resent this decision grealty calling it worse than cruel. However, resource control is the best way to avoid dying 
out.''' + civil_great_decrease 
population_desperation = '\n' + '''Your community's desperate attempts at sustaining itself failed horribly. Scavenging outside the safe zone, hunting, mining, even crimes 
like robbery and cannibalism, all failed. Eventually you all died out. Nice one chief.'''
independence = '\n' + '''12 July 2029. Either everyone or the civilians came to a new home to hopefully recover from their famine. However how you all were treated is a 
different story. You all were treated with inferiority, and rebuttal. You all were embarassed, mistreated and underfed. Now, you all are fed up and now want to devise a plan. 
Folks however, are divided on whether an ([ESCAPE]X} [CRUSADE]B} [TALK]Y}) should happen.''' + '\n> '
independence_escape = '\n' + '''It took a few weeks to thoroughly plan out the escape. You gathered as much people as you could to take part. The execution was 
mostly sucessful, stripping as much supplies from the hostiles as you could. There were few deaths. The enraged, devastated hostiles kill all of your remaining folk, whether 
they were in for the plan or out. You return to home and have some heavy clean up to do''' + civil_great_increase + army_slight_increase
independence_crusade = '\n' + '''A lot of intelligence and thought was put into infiltrating and arming your people in a fight for their freedom. In the end the 
mission was accomplished. If you altered the food supply, you did not lose much. If you took the risk, you lost a great deal of people. You take over their place and make it your 
own after finding out it has closer proximity to vital resources.''' + army_great_increase + civil_slight_increase
new_independence = '\n' + '''You gather up your most persuasive minds to convince the hostiles away from their inhumane ways. This surprisingly, goes better 
than expected and with some initiative taken, your people and theirs now work together in unity.''' + everybody_increase

election = '\n' + '''25 October 2029. For the rest of the summer and the early autumn, you bounced back and for once resources are not a priority if you have made it this far.
But, the problems never stop. A new polotician named Mr. Powell looks to takeover and "lead this community in the right direction" but you know, deep down, he is tyrannical. He 
picking up steam rapidly. A lot of people want him as the leader. The election is next week and you must decide whether to sabatoge the eleciton for the safety of your 
impressionable community or think of every possible compelling argument to sway your people into safety. ([SABOTAGE]X}  {B[PERSUADE])'''  + '\n> '
election_sabatoge = '\n' + '''You have sucessfully sabatoged and won the election and there is already suspicion in the results as most people picked the new person. Everyone is 
irate with arson and riots breaking out all across the town. Everyone claims that the election was sabatoged but you hide the evidence that proves them 
right.''' + civil_great_decrease
election_persuade = '\n' + '''You try your best to convince the crowd but fail as the crowd hangs on every word Mr. Powell says. He has taken over the town. In weeks his tyrannical 
overtakes the town. Cruel actions like murdering innocent outsiders, killing the children and elderly, and decapitating those who sympathize, all take effect. Your community has 
become the opposite of what you envisioned.''' + army_slight_decrease
upgrade = '\n' + '''14 November 2029. New Independence is now your new home, where the community thrives together on working to make the town a better place to live in. Different 
types of people are divided on what should be big priority. ([ARMY]X}  [RESILIENCY]B}  [RESOURCES]Y}  [SERVICES]A})''' + '\n> '
army = '\n' + '''An EF-5 tornado directly hit your way and killed everything in its path. Nice one chief.'''
resources = '\n' + '''An EF-5 tornado directly hit your way and killed everything in its path. Nice one chief.'''
services = '\n' + '''An EF-5 tornado directly hit your way and killed everything in its path. Nice one chief.'''
resiliency = '\n' + '''You have focused you attention at making your community hard to destroy. Additions like much tougher boundaries and an underground helped prepare your 
community to sucessfully survive an EF-5 tornado directly hitting the town. But now you lost everything and will have to rebuild.''' + everybody_increase

ending_opportunity = '\n' + '''1 December 2029. After a few weeks of trying to rebuild you realize that reconstruction. You can't go back your old community beacuse there is 
nothing there. So now your stuck. You went scavenging earlier before the tornado and found a living space that could keep a person going for months. However, it keeps ONE person
going for months. You are left to decide if survival is really that important. ([ABANDON]X}  {B[PERSERVERE])''' + '\n> '
ending_abandon = '\n' + '''You left your community, with no leader, to die in exchange for your comfort. The community is irate and you will be killed on first sight if found. 
Luckily, just before your food and supplies ran out, you found another community and you had few problems. You lived there for the rest of your life.''' + traitor
ending_perservere = '\n' + '''One by one, everyone fell out. With no necessities, your community died out. You where seen as loyal for sticking with your community until the end. 
Nice one chief.''' 
ending_terrorism = '\n' + '''6 February 2029. Mr. Powell is back and he is out for revenge from losing the election. He and his militia are destroying everything and everyone in 
sight until he gets what wants. With only a few minutes before he arrives at your city hall, you and your army must decide to ([ATTACK]X}  {B[RETREAT])''' + '\n> '
ending_attack = '\n' + '''You and your remaining army fought ferociousy against the bigger opposition. In the end, you won, but at a cost, destroyed the whole city. Few survived 
being admist the exchange of explosives and gunfire. Your city is decimated but you stopped a major threat from taking over. It took two years before conditions returned to 
normal'''
ending_retreat = '\n' + '''You most peaceful decision and left with your most trusted peers. You wandered off into unknown and eventually fit in with another community. A few 
months later, you gathered up enough men to take back your old city. When you arrived, you discovered the city collapsed with decayed skeletons everywhere walked. It was a ghost 
city.'''
ending_execution = '\n' + '''11 April 2030. You are about to be hanged for sympathizing against their standards. Everyone watches outside cheering and patiently awaiting your 
death. When you asked for last words, you tried to ([CONVINCE THE CROWD]X}  {B[KILL MR. POWELL]).''' '\n> '
ending_death = '\n' + '''You have died. The people heard your short speech and were compelled and related to it deeply. They were so emotional that no little time wasted to 
overthrow Mr. Powell's government after you were executed. Soon a new leader was chosen to lead the community and ever since, they have advanced to be one of the most expansive 
good guys in the apocolypse. They even found a cure.''' + hero
ending_kill = '\n' + '''Just before somebody could do something, you grabbed a soldier's AK-47 and AKed both him and Mr.Powell. You barely managed to escape the scene. You gathered 
any remaining supporters to basically go on a warpath and kill as much army members as you could until they surrendered. You somehow succeedeed in this and basically took over by 
force. This makes the people very unhappy but over time they realize how much you care.'''

the_end = '\n' + 'THE END'

army_relationship = 0
civil_relationship = 0
hero_traitor = 15
major_change = 2
change = 1
slight = 0.5
relationships = [army_relationship, civil_relationship, hero_traitor, major_change, change, slight]
army = relationships[0]
civil = relationships[1]
hero_betray = relationships[2]
major = relationships[3]
up = relationships[4]
down = relationships[4]
slightly = relationships[5]

def roadblock():
    roadblock = 'Please enter a valid input'   
    print(roadblock)        
def story():
    situation_winter = str(input(winter))
    if situation_winter == 'X':
        short_sleep()
        print(alter_supply)
        relationships[1] -= major
        spring_alter_supply()
        choices.append('chose safety over risk')
    elif situation_winter == 'B':
        short_sleep()
        print(ignore_risk)
        relationships[0] -= major
        spring_ignore_risk()
        choices.append('chose risk over safety')
    else:
        roadblock()
def spring_alter_supply():
    situation_spring = str(input(spring))
    if situation_spring == 'X':
        short_sleep()
        print(defend_alter_supply)
        relationships[1] -= down
        sit_outbreak()
        choices.append('chose defence over all')
    elif situation_spring == 'B':
        short_sleep()
        print(merge_alter_supply)
        relationships[1] += up
        relationships[0] -= slightly
        independence_missouri()
        choices.append('chose merging over all')
    elif situation_spring == 'A':
        short_sleep()
        print(negotiate_alter_supply)
        relationships[0] -= major
        relationships[1] -= major
        populated()
        choices.append('chose negotiation over all')
    else:
        roadblock()
def spring_ignore_risk():
    situation_spring = str(input(spring))
    if situation_spring == 'X':
        short_sleep()
        print(defend_ignore_risk)
        choices.append('chose to defend and died')
    elif situation_spring == 'B':
        short_sleep()
        print(merge_ignore_risk)
        relationships[1] += up
        relationships[0] += up
        independence_missouri()
        choices.append('chose merging over all')
    elif situation_spring == 'A':
        short_sleep()
        print(negotiate_ignore_risk)
        ('chose to negotiate and starved')
    else:
        roadblock()
def sit_outbreak():
    situation_outbreak = str(input(outbreak))
    if situation_outbreak == 'X':
        short_sleep()
        print(outbreak_treat)
        relationships[0] -= down
        relationships[1] += up
        elect()
        choices.append('chose aid over resources')
    elif(situation_outbreak) == 'B':
        short_sleep()
        print(outbreak_exile)
        relationships[1] -= major
        relationships[0] += slightly
        elect()
        choices.append('chose resources over aid')
    else:
        roadblock()
def independence_missouri():
    situation_independence = str(input(independence))
    time.sleep(4)
    if situation_independence == 'X':
        short_sleep()
        print(independence_escape)
        relationships[0] += slightly
        relationships[1] += major
        elect()
        choices.append('chose to escape')
    elif situation_independence == 'B':
        short_sleep()
        print(independence_crusade)
        relationships[0] += major
        relationships[1] += slightly
        buff()
        choices.append('resorted to violence')
    elif situation_independence == 'Y':
        short_sleep()
        print(new_independence)
        relationships[0] += up
        relationships[1] += up
        buff()
        choices.append('chose to talk')
    else:
        roadblock()
def populated():
    situation_population = str(input(population))
    if situation_population == 'X':
        short_sleep()
        print(population_reduce)
        relationships[1] -= major
        elect()
        choices.append('chose survival over morals')
    elif situation_population == 'B':
        short_sleep()
        print(population_desperation)
        choices.append('tried to perservere but died')
    else:
        roadblock()
def elect():
    situation_election = str(input(election))
    if situation_election == 'X':
        short_sleep()
        print(election_sabatoge)
        relationships[1] -= major
        terrorism()
        choices.append('chose dirty play over clean')
    elif situation_election == 'B':
        short_sleep()
        print(election_persuade)
        relationships[0] -= slightly
        execute()
        choices.append('chose clean play over dirty')
    else:
        roadblock()
def buff():
    situation_upgrade = str(input(upgrade))
    if situation_upgrade == 'X':
        short_sleep()
        print(army)
        choices.append('chose army over all and died')
    elif situation_upgrade == 'B':
        short_sleep()
        print(resiliency)
        relationships[0] += up
        relationships[1] += up
        opportunity()
        choices.append('chose resiliency and survived')
    elif situation_upgrade == 'Y':
        short_sleep()
        print(resources)
        choices.append('chose resources over all and died')
    elif situation_upgrade == 'A':
        short_sleep()
        print(services)
        choices.append('chose services over all and died')
    else:
        roadblock()
def opportunity():
    situation_end_opportunity = str(input(ending_opportunity))
    if situation_end_opportunity == 'X':
        short_sleep()
        print(ending_abandon)
        relationships[0] -= hero_traitor
        relationships[1] -= hero_traitor
        print(the_end)
        choices.append('chose your self over all')
        print('\n')
        civil_left()
        army_left()
    elif situation_end_opportunity == 'B':
        short_sleep()
        print(ending_perservere)
        print(the_end)
        choices.append('chose everyone over selfishness')
        print('\n')
        civil_left()
        army_left()
    else:
        roadblock()
def terrorism():
    situation_end_terrorism = str(input(ending_terrorism))
    if situation_end_terrorism == 'X':
        short_sleep()
        print(ending_attack)
        print(the_end)
        choices.append('chose war over retreat')
        print('\n')
        civil_left()
        army_left()
    elif situation_end_terrorism == 'B':
        short_sleep()
        print(ending_retreat)
        print(the_end)
        choices.append('chose retreat over war')
        print('\n')
        civil_left()
        army_left()
    else:
        roadblock
def execute():
    situation_end_execute = str(input(ending_execution))
    if situation_end_execute== 'X':
        short_sleep()
        print(ending_death)
        relationships[0] += hero_traitor
        relationships[1] += hero_traitor
        print(the_end)
        choices.append('chose words over guns')
        print('\n')
        civil_left()
        army_left()
    elif situation_end_execute== 'B':
        short_sleep()
        print(ending_kill)
        print(the_end)
        choices.append('chose guns over words')
        print('\n')
        civil_left()
        army_left()
    else:
        roadblock()
def civil_left():
    if relationships[1] <= -8:
        print('You left the ' + civilian + ' feeling ' + vengeful)
    if relationships[1] > -8 and relationships[1] < -4:
        print('You left the ' + civilian + ' feeling ' + hateful)
    if relationships[1] >= -4 and relationships[1] < -1:
        print('You left the ' + civilian + ' feeling ' + disappointed)
    if relationships[1] >= -1 and relationships[1] < 2:
        print('You left the ' + civilian + ' feeling ' + conflicted)
    if relationships[1] >= 2 and relationships[1] < 5:
        print('You left the ' + civilian + ' feeling ' + satisfied)
    if relationships[1] >= 5 and relationships[1] < 8:
        print('You left the ' + civilian + ' feeling ' + happy)
    if relationships[1] >= 8:
        print('You left the ' + civilian + ' feeling ' + prosperous)
def army_left():
    if relationships[0] <= -7:
        print('You left the ' + army_government + ' feeling ' + vengeful)
    if relationships[0] > -7 and relationships[0] < -4:
        print('You left the ' + army_government + ' feeling ' + hateful)
    if relationships[0] >= -4 and relationships[0] < -2:
        print('You left the ' + army_government + ' feeling ' + disappointed)
    if relationships[0] >= -2 and relationships[0] < 2:
        print('You left the ' + army_government + ' feeling ' + conflicted)
    if relationships[0] >= 2 and relationships[0] < 5:
        print('You left the ' + army_government + ' feeling ' + satisfied)
    if relationships[0] >= 5 and relationships[0] < 7:
        print('You left the ' + army_government + ' feeling ' + happy)
    if relationships[0] >= 7:
        print('You left the ' + army_government + ' feeling ' + prosperous)    
print(synopsis)
time.sleep(3)
long_sleep()
story()
def choice():
    print('\n' + 'You: ')
    for decision in choices:
        print('   - ' + decision + '\n')
choice()
\$\endgroup\$
0

3 Answers 3

11
\$\begingroup\$

Style

Before diving in the actual code, some general style considerations first. Python comes with an official Style Guide. The most relevant parts for your code would be the sections on how to structure code using blank lines where appropriate (two blank lines between separate functions and classes, only single blank line within functions and classes) and the recommendations on how to document your functions using documentation strings """enclosed in triple quotes""". The code examples in the following answer will demonstrate both of these style elements.


Note: For convenience, some of the code below uses f-strings, which is a new Python feature introduced with Python 3.6. If you're not yet there, it should be obvious how to transform those pieces to use .format(...) instead.


Don't repeat yourself

Your game has a lot of duplicate text, e.g. where you start to describe the possible changes in the relationship with the other factions. I would propose to collect those templates in a "dumb" class, or maybe a dictionary if you don't like classes, and then put in the factions as you need them. This could be done like so:

class Factions:
    """Class to represent the factions found in the game"""

    ARMY = "ARMY & GOVERNMENT"
    CIVILIANS = "CIVILIANS"
    EVERYBODY = "EVERYBODY"


class RelationshipChanges:
    """Holds templates to decribe changes in relationships"""

    HEROISM = '{} looks at you as a hero.'
    GREAT_INCREASE = 'This greatly improves your relationship with {}.'
    INCREASE = 'This improves your relationship with {}.'
    SLIGHT_INCREASE = 'This slightly improves your relationship with {}.'

    SLIGHT_DECREASE = 'This slightly decreases your relationship with {}.'
    DECREASE = 'This worsens your relationship with {}.'
    GREAT_DECREASE = 'This greatly worsens your relationship with {}.'
    TREASON = '{} wants you dead.'

and then do RelationshipChanges.GREAT_INCREASE.format(Factions.CIVILIANS) instead of defining civil_great_increase and all of its companions. The code would generate

This greatly improves your relationship with CIVILIANS.

It might be also a good idea to define a function as a shorthand for this, since it's not quite a pleasure to type.

def change_relation(faction, type_of_change):
    message_template = getattr(RelationshipChanges, type_of_change.upper())
    return message_template.format(getattr(Factions, faction.upper()))

With this, change_relation("civilians", "great_increase") would generate the same output as previously seen. The function uses Python's built-in getattr(...) function to programmatically access members of the class by their name. As an example, getattr(Factions, "ARMY") would be the same as Factions.ARMY. Neat, isn't it?

If you were even more keen on saving some typing, this function would easily allow to add a "translation" dictionaries as an intermediate. These dict could then map '+++' to RelationshipChanges.GREAT_INCREASE or 'civ' to Factions.CIVILIANS and shorten the previous function call to change_relation('civ', '+++'). I will leave that as an exercise to you. See the updated version below.

The relationship levels themselves could be handled similarly.

class RelationshipLevels:
    """Class to represent the player's relationship to other factions"""

    VENGEFUL = "VENGEFUL"
    HATEFUL = "HATEFUL"
    DISAPPOINTED = "DISAPPOINTED"
    CONFLICTED = "CONFLICTED/NEUTRAL"
    SATISFIED = "SATISFIED"
    HAPPY = "HAPPY"
    PROPEROUS = "PROPEROUS"

    ALL = [VENGEFUL, HATEFUL, DISAPPOINTED, CONFLICTED, SATISFIED, HAPPY, PROSPEROUS]
    #^--- this will become handy in a moment

army_left and civil_left are another instance where you tend to repeat the same pieces of code/text over and over again. If you think about those two for a moment, the common pattern will become clear: For a given faction and its relationship score, you want to determine the relationship level. Therefor, you essentially check if the score is below a certain treshold, format the message and print it. A way to generalize that idea would be as follows:

def get_final_standing(relation_score, thresholds):
    """Determine how the faction thinks about the player at the end"""
    for threshold, feeling in zip(thresholds, RelationshipLevels.ALL):
        if relation_score <= threshold:
            return feeling

    return RelationshipLevels.ALL[-1]

The function uses zip(...) two iterate over two sequences in parallel, and stops the loop (break) if it has found the appropriate relationship level. It becomes a little bit tricky if you don't want to put an upper limit to the threshold so I decided to implement this in a way that whenever the score is greater than the last threshold you gave, the most positive (i.e. rightmost) level will be returned. To realize the same funcationality as army_left formerly implemented you would then do

final_standing = get_final_standing(relationships[Factions.CIVILIANS], (-7, -4, -2, 2, 5, 7))
print(f'You left the {Factions.ARMY} feeling {final_standing}.')

I leave civil_left as an exercise to you.

All the score increments/decrements should also be bundled somehow. At the moment you have slight, slightly, and relationships[5] to express a slight change in the score in either direction. The same pattern is more or less to be found for normal and major changes, as well as for hero/traitor. That's madness!

One way would be to put them into a class such as we did before with the other constant values. A dict might also be a viable solution. But wait! We have already started somethind related to those changes, haven't we? Well observed. Time to have another look at RelationshipChanges. At the moment this class simply holds the template message for each of the changes. With just one more level of "nesting", we can add the score modifiers as well.

ULTIMATE_SCORE_CHANGE = 15
MAJOR_SCORE_CHANGE = 2
NORMAL_SCORE_CHANGE = 1
SLIGHT_SCORE_CHANGE = 0.5

class RelationshipChanges:
    """Holds templates and modifiers to decribe changes in the relationships"""

    HEORISM = {
        'message': '{} looks at you as a hero.',
        'modifier': ULTIMATE_SCORE_CHANGE
    }
    GREAT_INCREASE = {
        'message': 'This greatly improves your relationship with {}.',
        'modifier': MAJOR_SCORE_CHANGE
    }
    INCREASE = {
        'message': 'This improves your relationship with {}.',
        'modifier': NORMAL_SCORE_CHANGE
    }
    SLIGHT_INCREASE = {
        'message': 'This slightly improves your relationship with {}.',
        'modifier': SLIGHT_SCORE_CHANGE
    }

    SLIGHT_DECREASE = {
        'message': 'This slightly decreases your relationship with {}.',
        'modifier': -SLIGHT_SCORE_CHANGE
    }
    DECREASE = {
        'message': 'This worsens your relationship with {}.',
        'modifier': -NORMAL_SCORE_CHANGE
    }
    GREAT_DECREASE = {
        'message': 'This greatly worsens your relationship with {}.',
        'modifier': -MAJOR_SCORE_CHANGE
    }
    TREASON = {
        'message': '{} wants you dead.',
        'modifier': -ULTIMATE_SCORE_CHANGE
    }

Now that those messages and the actual changes to the score are linked more closely, it would be a great moment to remove those change messages from the static game text. A benefit of this would be that if you ever decided to change the effects of an action, you would only have to do this in on place, namely on of the event functions, and not there and somewhere else hidden in all the storyline text. Since those message are IIRC merely appended to the storyline text, the output should not change significantly. Of course the implementation of change_relation has to be adapted to fit these changes, and since all that stops change_relation from actually updating the relationship score is not knowing about relationships it is easy to adapt it to do more repetive work for us:

def change_relation(relationships, faction, type_of_change):
    """Documentation omitted for brevity"""
    type_translation = {
        "---": "great_decrease", "--": "decrease", "-": "slight_decrease",
        "+++": "great_increase", "++": "increase", "+": "slight_increase"
    }
    if type_of_change in type_translation:
        # only apply the translation if it's own of ---/--/.../+++
        type_of_change = type_translation[type_of_change]

    change_descr = getattr(RelationshipChanges, type_of_change.upper())
    faction_name = getattr(Factions, faction.upper())
    relationships[faction_name] += change_descr['modifier']

    return change_descr['message'].format(faction_name)

You can now use something like print(change_relation(relationships, "civilians", "---")) to adapt the game state and tell the user about the consequences of his/her decision. (Note: The code above builds upon a change to relationships that will be explained in the following section.)

Make it harder to be wrong

Ah, that damn army ... where was their score in relationships again? Was it the first or the second position? Maybe the third?

To avoid situations like this, I would recommend to use a dictionary. This leaves you with something like relationships = {"army": 0, "civil": 0} or even relationships = {Factions.ARMY: 0, Factions.CIVILIANS: 0}. Using relationships[Factions.ARMY] leaves absolutely no doubt what you're trying to do. It will also make it waaaay easier to spot copy and paste errors.

Avoid globals

Global variables are best avoided, since it's harder to see which parts of the code modify them, which leads to all kind of problems. The core object of your game would be relationships and it would be easy to transform all your game functions to accept it as an argument instead of relying on it to be present on a global scope. The most common approach would be to somehow define a main function which does all the needed initialization stuff, like displaying the synopsis or initializing relationships. relationships is then passed to story which again passes it onwards depending on how the player chooses his actions.

All the game text should wrightfully stay in global variables. For them I would recommend to CAPITALIZE_THEIR_NAMES to make it clear that they are supposed to be used/seen as constant values.

User input handling

At the moment the user input handling is not very robust. Once you enter an invalid command, e.g. by smashing the enter key to long, the program bails out and you have to start all over. This can be very annoying. A better approach would be to ask for invalid input several times and only bail out if a termination character like q/Q is entered or the user did not provide a valid input six times in a row. An implementation of this approach might look like:

def prompt_for_input(prompt, valid_inputs, max_tries=6):
    print(prompt)
    for _ in range(max_tries):
        user_input = input('> ').upper()
        if user_input in valid_inputs:
            return user_input
        if user_input == 'Q':
            break
        # the input was not valid, show the roadblock
        roadblock()
    # Either Q or excessive retrying brought us here
    print('Seems like you are not willing to play. Goodbye!')
    sys.exit(0)

Mini demo

The answer contains some a lof of proposals that change the code drastically. Since you requested to see more code to better understand it and the answer is already quite long, I decided to implement a reduced version of your game that implements those proposed changes and upload it into a gist. The gist is hidden from search engines but accessible to everyone with the link.

\$\endgroup\$
2
  • \$\begingroup\$ I like your more efficient view of it but if I'm being honest, I understood half of what you proposed. It probably is my fault because I am still a beginner. I would like to your version of the code so that I can dissect it and maybe understand how it is more efficient. P.S. I meant to type functions so I replaced Factions with SocialStatus. \$\endgroup\$
    – notak
    Jun 22, 2019 at 17:04
  • \$\begingroup\$ The demo section now includes a link to a gist where you can find a reduced version of your game with those changes in place. I hope that helps. \$\endgroup\$
    – AlexV
    Jun 23, 2019 at 21:09
8
\$\begingroup\$

I'm on my phone, so it's hard to really see this code as a whole and really take the full context of everything. I'm just going to flip through and mention things as I notice them.


At the top, you have

vengeful = 'VENGEFUL.'

And other such lines. This strikes me as odd. The only small benefit I can see is this would help the IDE auto-complete the word. Looking at how you're using it, you're forcing a ton of repetitious code in functions like civil_left. Look at that function, and look at the function below it and think about how much of those functions are the same. Not only are each of lines in the functions nearly identical, both of those functions are basically the same! Whenever you have code that's nearly identical in multiple spots, make the identical code the body of a function, and make the parts that differ parameters to the function.

How would that look? The only real part that differs is the end of that sentence where you decide what status to display. First, factor out the part that decides the word to use:

def describe_status(relation):
    if relation <= -8:
        return "vengeful"

    elif -8 < relation <= -4: # Note, you can chain comparison operators
        return "hateful"

    elif -4 < relation <= -1:
        return "disappointed"

    elif -1 < relation <= 2:
        return "conflicted" 

    #... The rest of the statuses

Then, use that function:

def civil_left():
    status = describe_status(relationship[1])

    print('You left the', civilian, 'feeling', status)

def army_left():
    status = describe_status(relationship[0])

    print('You left the', army_government, 'feeling', status)

Now, the major problem here is that you set the relationship thresholds at different levels for each. You could add a second parameter to describe_status that adds an offset to each condition to remedy that though. Note how much duplication that removed though!


I'll note that you tagged this as functional-programming, but this is far from what would be considered functional. I'm not going to go into great detail about what all FP means, but basically, if you're following FP principals, you're passing data around instead of mutating objects and carrying out side effects. All of your functions print directly, and none seem to accept any parameters. This is not good, regardless of the paradigm that you're following. You're relying entirely on global state (like relationships), and on operating through side effects (like altering relationships and using print everywhere). If you continue to code like this, you will have a very difficult time creating anything other than small projects, and debugging will increasingly become a nightmare.

Look at how describe_status operates. Every piece of data that it requires is a parameter (relation), and everything the function does is done via the data that's returned. When functions are just taking and returning data, it becomes much easier to reason about how the code works; and that's an extremely important goal. Code that's hard to understand the operation of is code that's hard to maintain and build on.


You have a lot of story Strings embedded in the code. I would save these in a file and read them from file as needed. That will make the code less bulky, and will make it so you don't need to alter the code if you want to alter the story.


I'm going to submit this before it gets closed. Good luck!

\$\endgroup\$
2
\$\begingroup\$

I would move the story/writing out of the python script and into a text file. Even better, I would put it into some JSON compliant format. This isn't the only way to do this, but this is what I came up with just now: A given JSON object describes a single scene/event. Each possible event has a unique ID, and has a list of possible transitions (certain user inputs that lead to other events). An event also has some text to display to the user of course. Here is what such a JSON text file might look like (excuse the bad story writing):

{
    "events": [
        {
            "id": "event_story_begin",
            "transitions":
                [
                    {"input": "YES", "to_event": "event_you_said_yes"},
                    {"input": "NO", "to_event": "event_you_said_no"}
                ],
            "text": "Your adventure begins here, traveller. Your squire asks you if you'd like a pear. What do you say?"
        },

        {
            "id": "event_you_said_yes",
            "transitions":
                [
                    {"input": "YES", "to_event": "event_you_said_yes"},
                    {"input": "NO", "to_event": "event_you_said_no"}
                ],
            "text": "Your squire is pleased and silently hands you a pear. He offers you another. Do you accept?"
        },

        {
            "id": "event_you_said_no",
            "transitions":
                [
                    {"input": "", "to_event": "event_story_end"}
                ],
            "text": "Your squire silently stares at you, motionless. Suddenly, with one well-placed blow, your squire swings his axe and cleaves your skull."
        },

        {
            "id": "event_story_end",
            "transitions": [],
            "text": "Your adventure ends here."
        }
    ]
}

The big advantage of doing things this way is reusability - your python script and your story are decoupled, so you can add new events to your story without changing your python script. Speaking of script, the code might look something like this:

class Event:

    def __init__(self):
        self.id = ""
        self.transitions = []
        self.text = ""

    def has_input_options(self):
        return self.transitions and self.transitions[0]["input"]

    def print_text(self):
        print(self.text)

    def print_input_options(self):
        if not self.has_input_options():
            return
        print("Options:", end=" ")
        for transition in self.transitions:
            print(f"[{transition['input']}]", end=" ")
        print()

    def get_next_event_id(self):
        while self.transitions:
            user_input = ""
            if self.has_input_options():
                user_input = input()
            try:
                transition = next(t for t in self.transitions if t["input"].lower() == user_input.lower())
            except StopIteration:
                print("Try again:", end=" ")
                continue
            else:
                return transition["to_event"]
        return ""

    @staticmethod
    def from_dict(dictionary):
        event = Event()
        event.id = dictionary["id"]
        event.transitions = dictionary["transitions"]
        event.text = dictionary["text"]
        return event

class EventManager:

    def __init__(self, filename="events.txt"):
        import json

        with open(filename) as file:
            self.data = json.load(file)

    def get_event_by_id(self, event_id):
        try:
            event_dict = next(event_dict for event_dict in self.data["events"] if event_dict["id"] == event_id)
        except StopIteration:
            return None
        else:
            return Event.from_dict(event_dict)

class Game:

    def __init__(self):

        from queue import Queue

        self.event_manager = EventManager()
        event_story_begin = self.event_manager.get_event_by_id("event_story_begin")

        self.event_queue = Queue()
        self.event_queue.put(event_story_begin)

    def play(self):

        while not self.event_queue.empty():
            current_event = self.event_queue.get()

            current_event.print_text()

            current_event.print_input_options()

            next_event_id = current_event.get_next_event_id()
            if next_event_id:
                next_event = self.event_manager.get_event_by_id(next_event_id)
                if next_event is not None:
                    self.event_queue.put(next_event)

def main():

    game = Game()
    game.play()

    return 0

if __name__ == "__main__":
    import sys
    sys.exit(main())

You have the Event class, which represents a single event or scene. It has some utility functions that make our life easier later (could seriously be improved). The EventManager class does all the JSON stuff, and allows us to generate an Event object (given an associated ID). The Game class handles the main game loop. It does this with a queue of Event objects (the game loop keeps going as long as there are more events to be processed). New Event objects are added to the queue when the current event has transitions and the user provides input. If we encounter an Event which has no transitions, we cannot add any more events to the queue. Once the queue is empty, the game loop stops, and the program terminates.

Again, my suggestion is a rough outline. If I were to make improvements I would probably move the user input stuff into the game loop directly (Event.get_next_event_id is kind of a sloppy solution, and the name is sort of deceptive).

And, if you're curious, here is the program output:

Your adventure begins here, traveller. Your squire asks you if you'd like a pear. What do you say?
Options: [YES] [NO] 
yes
Your squire is pleased and silently hands you a pear. He offers you another. Do you accept?
Options: [YES] [NO] 
yes
Your squire is pleased and silently hands you a pear. He offers you another. Do you accept?
Options: [YES] [NO] 
yes
Your squire is pleased and silently hands you a pear. He offers you another. Do you accept?
Options: [YES] [NO] 
no
Your squire silently stares at you, motionless. Suddenly, with one well-placed blow, your squire swings his axe and cleaves your skull.
Your adventure ends here.
\$\endgroup\$

Your Answer

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

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