8
\$\begingroup\$

I quite like this extra credit assignment I did for Zed Shaw's Learn Python The Hard Way. Its a neat little text adventure game that uses stats. I would like to eventually expand this, but let me know how the code looks so far!

# Legend Of Doobieus

import random 
from sys import exit 

#rolls 3d6
def roll_3d6():
    a = random.randint(1, 6)
    b = random.randint(1, 6) 
    c = random.randint(1, 6)
    list = [a, b, c,]
    list.sort()
    add = sum(list[0:3])
    return add

#stats
def display_stats():
    global pow, cun, per
    pow = roll_3d6()
    print "Power: ", pow
    cun = roll_3d6()
    print "Cunning: ", cun
    per = roll_3d6()
    print "Personality: ", per

# Check Random Encounter
def chk_rn():
    x = random.randint(1, 6)
    if x == 1:
        rn_ec1()

# First Encounter (main program really)
def fi_en():
    global pow, cun, per
    print"""
It smells of damp vegetation, and the air is particularly thick. You can 
hear some small animals in the distance. This was a nice place to sleep.

1. Stay close, find some cover, and wait for food to show up.

2. Explore the nearby marsh & find the nearest river, following it downstream.

3. Walk towards the large mysterious mountain in the distance. 
"""
    answer = int(raw_input(prompt))
    if answer == 1:
        cun_one = roll_3d6()
        if cun_one <= cun - 2:
            print"""
Time passes as eventually you capture some varmints. You feel slightly more 
roguish.
"""
            cun = cun + 1
            chk_rn()
            fi_en()
        else: 
            print """
Time passes and a group of slavers marches into right where you are hiding in 
the woods. They locate you, capture you, and haul you away for a lifetime of 
servitude in the main city.
Goodbye %s
""" % name
    elif answer == 2: 
        power = roll_3d6()
        if power <= pow - 4:
            print"""
You trudge through the marshes until you eventually reach a large river. 
Downstream from the river is a large temple covered in vines, you walk towards 
it. You feel more powerful."""
            pow = pow + 2
            chk_rn()
            te_en()
        else:
            print """
The vegetation here wraps itself around your legs making it impossible to move. 
You will most likely die here in the vegetation. 
Goodbye %s.
""" % name
    elif answer == 3:
        cun_two = roll_3d6()
        if cun_two <= cun:
            print """
You make your way towards the mountain and you encounter a really large group of 
devil dogs guarding the entrance to the mountain.
"""
            chk_rn()
            dd_en()
        else: 
            print"You have gotten lost and ended up right where you started."
            fi_en()

# Devil Dog Encounter
def dd_en():
    global pow, cun, per
    print"""
You have encountered a massive group of Devildogs.

1. Fight off the whole group, like a true hero

2. Try to sneak very quietly around them. 

3. Attempt to tame them like domestic animals. 

4. Accept the Devildog curse and walk through unmolested, sort of.
"""
    answer = int(raw_input(prompt))
    if answer == 1:
        power = roll_3d6()
        if power <= pow - 6:
            print """
You are a God among men and have defeated all of the surrounding Devildogs. 
Beyond the entrance to the mountain is a temple covered in vines. You 
feel more powerful.
"""
            power = power + 3
            chk_rn()
            te_en()
        else: 
            print """
You are easily overwhelmed by the devildogs and you are continuously eaten 
by devildogs for all eternity.
Goodbye %s.
""" % name
    elif answer == 2:
        sneak = roll_3d6()
        if sneak <= cun - 4:
            print """
You stealthily evade the Devildogs. Beyond the entrance to the mountain is a 
temple covered in vines. You feel more roguish.
"""
            cun = cun + 2
            chk_rn()
            te_en()
        else:
            print """
The devildogs pick up on your scent. You are easily overwhelmed by the 
devildogs and you are continuously eaten by devildogs for all eternity. 
Goodbye %s.
""" % name          
    elif answer == 3: 
        charm = roll_3d6()
        if charm <= per - 4:
            print """
The Devildogs are thrilled and have accepted you as their leader, they allow 
you to pass and will sing your praises for all eternity. Beyond the entrance 
to the mountain is a temple covered in vines. You feel more charming.
"""
            per = per + 2
            chk_rn()
            te_en()
        else:
            print """
The Devildogs have no idea what you are trying to do.You are easily 
overwhelmed by the devildogs and you are continuously eaten by devildogs 
for all eternity. 
Goodbye %s.
""" % name
    elif answer == 4:
        print"""
The Devildogs circle around you and start howling in unison. You feel your 
life forces draining away as they howl their awful song. Beyond the entrance 
to the mountain is a temple covered in vines.
"""
        pow = pow - 3
        cun = cun - 3
        per = per - 3
        chk_rn()
        te_en()
    else:
        print """
You stumble as the Devildogs take notice. You are easily overwhelmed by the 
devildogs and you are continuously eaten by devildogs for all eternity.
Goodbye %s.
""" % name

# Temple Encounter
def te_en():
    global pow, cun, per
    print """
You are now standing right in front of a large temple with many vines 
covering its cobblestone exterior. An archway precedes a small and 
peaceful garden with a main road leading to the temple. 

Next to the archway is a small gargoyle that is moving and staring at you
with a friendly smile. It is now sundown and the area starts to gleam a reddish 
glow. In the glow you can see faint white whips in the distance beyond the 
archway. It smells like fresh produce. In your head you are instantly reminded 
of a heartwarming song you remembered as a kid. 

1. Walk through the archway normally towards the temple.

2. Run through the archway trying to get to the door as quickly as possible.

3. Have a conversation with the gargoyle staring at you. 
"""
    answer = int(raw_input(prompt))
    if answer == 1:
        power = roll_3d6()
        if power <= pow - 5:
            print"""
As you walk through you immediately feel what feels like hundreds of hands 
groping at you pulling you into the ground. You stand your ground and make 
it to the door. You enter the temple. You feel more powerful.
"""
            pow = pow + 2
            in_te()
        else:
            print """
As you walk through, you are taken off guard as hundreds of hands grope you and 
pull you straight down to hell for the rest of eternity. 
Goodbye %s.
""" % name
    elif answer == 2:
        run = roll_3d6()
        if run <= cun - 6:
            print """
You sprint through and notice nothing as you quickly arrive at and enter the
temple. You feel more roguish.
"""
            cun = cun + 2
            in_te()
        else:
            print """
You trip and fall as you are running. What feels like 100 hands grabs you and 
pulls you straight down to hell for the rest of eternity.
Goodbye %s.""" % name
    elif answer == 3:
        charm = roll_3d6()
        if charm <= per - 3:
            print """
You ask the gargoyle where you are, and he cheerfully
tells you that you've arrived at the halls of destiny, and that you should
walk down the driveway. The sky returns to its normal color. You walk down 
the passageway and enter the temple. You feel more charming.
"""
            per = per + 1
            in_te()
        else:
            print """
You ask the gargoyle where you are, right as the 
expression changes on his face and tells you, 'Hell.' Right as he
says that 100 hands come up from the ground and pull you straight to hell.
Goodbye %s.
""" % name
    elif answer == "sing" or "sing song" or "Sing" or "Sing Song":
        print """
The gargoyle rejoices as you sing and exclaims he was 
just thinking about that song as he saw you. He says you are allowed
to enter the temple of destiny. The sky turns back to its normal color. You
walk down the passageway and enter the temple. You feel charming.
"""
        cun = cun + 2
        in_te()
    else: 
        print"""
You stumble as the gargoyle scowls you and screams an awful 
noise. Immediately, 100 hands appear from the ground and drag you down to 
hell as the gargoyle cackles above you. 
Goodbye %s.
""" % name

# Inside The Temple
def in_te():
    global pow, cun, per
    print"""
Inside the temple, Cobblestone lines the walls. There is one large room 
leading to another. It smells of mold, and you can hear both water dripping, 
and gears shifting. In the middle of a room stands a large minotaur with his 
head looking down carrying a massive axe. 

1. Engage the minotaur in combat.

2. Sneak along the edges of the walls to the next room.

3. Attempt to speak the the minotaur.
"""
    answer = int(raw_input(prompt))
    if answer == 1:
        fight = roll_3d6()
        if fight <= pow - 6:
            print """
You destroy the foul beast without taking injury. It screams as it dissolves 
into the ground. Several torches light up and signal safe entry into the next 
room. You feel more powerful.
"""
            pow = pow + 2
            you_win()
        else:
            print """
You find yourself unable to best to minotaur in combat and he decapitates you. 
Goodbye %s""" % name
    elif answer == 2: 
        sneak = roll_3d6()
        if sneak <= cun - 5:
            print """
You successfully sneak around the minotaur and enter the next room. 
You feel more roguish.
"""
            cun = cun + 2
            you_win()
        else: 
            print """
You step on a pressure plate and a blade emerges from the wall 
decapitating you.
Goodbye %s
""" % name
    elif answer == 3:
        charm = roll_3d6()
        if charm <= per - 5:
            print """
You ask the minotaur what his purpose is, and he exclaims to welcome you to 
your destiny. And allows you to pass. You feel more charming.
"""
            per = per + 2
            you_win()  
        else:
            print"""
You ask the minotaur what his purpose is, and he exclaims to prevent you from 
stealing the hero's destiny. He swings his axe and decapitates you. 
Goodbye %s.
""" % name
    else:
        print"""
You stumble and trip right in front of the minotaur, who takes this opportunity
to decapitate you with his axe.
Goodbye %s
""" % name
        exit

# You Win! 
def you_win():
    print """
You have entered a room covered in jewels & a large crown sits on a pedestal. 
Behind the crown is a plaque that reads:

                        ~CoNgRaTuLaTiOnS HeRo~
                  You have found the crown of destiny
                  This crown lays here to be worn 
                  By the hero of this land
                  Who has proved himself capable
                  Of surviving much hardship, &
                  Having both a sharp mind & Spirit
                  Take the crown, and henceforth 
                  Your rightful place as the ruler
                  Of Calgaria.

You pick up the crown and place it on your head. You are now the king of
Calgaria. Congratulations %s you beat the legend of doobieus!
 """ % name
    print"Your final stats were: \n", display_stats()

    print "\nThanks for playing, goodbye!"

#random encounter 1
def rn_ec1():
    global pow, cun, per
    print """
You have randomly encountered a colony of large ants
They are about your size but have pincers the size of your arms. 
Their clicking noises now envelop you...

1. Fight off the ants to the best of your ability.

2. Run away as quickly as possible.

3. Sing 'The Song of Insect Charm.'
"""
    answer = int(raw_input(prompt))
    if answer == 1:
        fight = roll_3d6()
        if fight <= pow - 4:
            print """With an amazing display of strength, you defeat all 
of your foes. You feel stronger after your victory."""
            pow = pow + 2
        else:
            print """You are eaten alive by ants and die.
Goodbye %s.""" % name
    elif answer == 2:
        trick = roll_3d6()
        if trick <= cun - 2:
            print """You outrun the giant ants and have survived.
You feel a little bit more roguish"""
            cun = cun + 1
        else:
            print """You fail to outrun the ants and are eaten alive. 
Goodbye %s""" % name
    elif answer == 3:
        charm = roll_3d6()
        if charm <= per - 2:
            print """The giant ants thank you for your lovely rendition & 
allow you to pass. You feel charming."""
            per = per + 1
        else: 
            print """The giant ants are horrified by your awful rendition of
their sacred anthem. They tear you apart mercilessly." 
Goodbye %s.""" % name 
    else:
        print """You stumble as the clicking gets closer and closer until
you are painfully eaten alive by ants.
Goodbye %s""" % name

######################## MAIN PROGRAM ###################################################
prompt = "> "

print "\n\t\tWelcome to LeGeNeD oF DoObIeUs..."

print "\n\t\tProgrammed using Python 2.7 by Ray Weiss"

name = raw_input("\nWhat is your name brave rogue? > ")

print "\nOk %s, lets roll up some character stats." % name
print "\nPress enter to roll 3D6 for each stat."
raw_input()

display_stats()

print "\nThese are your stats. They can be changed by things in game."

print "\nThese stats will have an effect on the outcomes that you choose."

print "\nYou will be presented with many choices, type in a number."

print "\nOr risk typing something different."

print "\nPress enter to start the game"
raw_input()

print"""You are %s, you are but a wandering rogue in the vast, 
mysterious, and deadly land of Calgaria. Your goal is to survive. 
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You have just woken up in the woods, its really cold, and you are hungry.
""" % name 

fi_en()
\$\endgroup\$
5
\$\begingroup\$

It's difficult to know what advice to give, since some of it is beyond what you've learnt so far (exercise 31?), but...

roll_3d6 - there's no need for the sort or list. For the number of items you have, it could just be:

def roll_3d6():
    return random.randint(1, 6) + random.randint(1, 6) + random.randint(1, 6)

or use a loop:

def roll_3d6():
    add = 0
    for _ in range(3):
        add += random.randint(1, 6) # add = add + random.randint(1, 6)
    return add

As a point to be aware of - you do not want to use existing function names as variable names - like list. Using them will cause you a lot of trouble.

display_stats - avoid the use of global if possible. You'll learn about classes, tuples, lists and dictionaries, all of which can be used and passed as parameters/returned as values.

This should, btw, be split into two parts, create_stats and display_stats. At the end, you call display_stats which resets everything and doesn't display your current stats.

In this case, since you seem to know about lists:

POWER = 0
CUNNING = 1
PERSONALITY = 2

def create_stats():
    power = roll_3d6()
    cun = roll_3d6()
    per = roll_3d6() 
    stats = [ power, cunning, personality ]
    return stats

def display_stats( stats ):
    print "Power: ", stats[POWER]
    print "Cunning: ", stats[CUNNING]
    print "Personality: ", stats[PERSONALITY]

called as:

stats = create_stats()
display_stats( stats )

This is then used as a parameter to all your functions.

def fi_en( stats ): # etc

and individual parts can be used:

stats[ POWER ] += 2 # stats[POWER] = stats[POWER] + 2

chk_rn - don't abbreviate names, it becomes a lot harder to read (and reading it will become far important than writing it). check_random_encounter is just fine as a name.

fi_en - whenever you accept input from the user, always validate it. calling int(raw_input(..)) will cause an exception if the user enters text (or even just presses return). The best way in this instance is to test against '1', '2' etc or even have a function to do so.

def ask_question( question, allowed_answers ):
    # repeat forever until a valid answer is given
    while True: 
        print question
        answer = raw_input( prompt ).lower() # test against lower case strings for simplicity
        if answer in allowed_answers:
            return answer

te_en - answer == "sing" or "sing song" or "Sing" or "Sing Song" The expression doesn't work that way. It should read (with brackets for emphasis):

( answer == "sing" ) or ( answer == "sing song" ) or ( answer == "Sing" ) or ( answer == "Sing Song" )

Also, you have the wrong stat advance. You feel charming, but cunning increases, not personality :)

in_te - exit should be exit() - calling the function, not using the value. And it isn't even needed. Try also to avoid using the from x import y form. There are occasions when it's useful, but having to write sys.exit() on one occasion instead of exit() isn't one of them :)

you_win - print("Your final stats were: \n", display_stats()). display_stats doesn't return anything, so you'll get None written.

print("Your final stats were: \n")
display_stats()

main - don't have significant code outside of a function, it's customary to put that (the main program bit) in a main function and guard it against being run when imported with, e.g.:

if __name__ == "__main__":
    main_program()

It's not significant here, but in the future with larger projects, it will be.

With regards to the whole structure of the program, I might suggest the following things which should be more or less straightforward:

  1. Define a series of global constants (variables) DEAD, FIRST_ENCOUNTER, DEVILDOG_ENCOUNTER etc.

  2. For each of your answer options, separate out the code into a separate function, which returns DEAD or the next encounter as appropriate. This will give you options to simplify the code a bit and make it more readable. You may end up with something like:

    def first_encounter():
        answer = ask_question("...etc...", ['1', '2', '3'] )
        status = DEAD
        if answer == '1':
            status = first_encounter_1()
        elif answer == '2': 
            status = first_encounter_2()
        elif answer == '3':
            status = first_encounter_3()
        if status != DEAD:
            if chk_rn() == DEAD:
                status = DEAD
        if status == FIRST_ENCOUNTER:
            first_encounter()
        elif status == TEMPLE_ENCOUNTER:
            temple_encounter()
        elif status == DEVILDOG_ENCOUNTER:
            devildog_encounter()
    
  3. After you've learnt about while loops, again rewrite your encounter functions, so that instead of calling the next encounter function directly, return the status and call the next encounter from the loop, e.g.

    def first_encounter():
        answer = ask_question("...etc...", ['1', '2', '3'] )
        status = DEAD
        if answer == '1':
            status = first_encounter_1()
        elif answer == '2': 
            status = first_encounter_2()
        elif answer == '3':
            status = first_encounter_3()
        if status != DEAD:
            if chk_rn() == DEAD:
                status = DEAD
        return status
    
    ....
    status = FIRST_ENCOUNTER
    while status != DEAD:
        if status == FIRST_ENCOUNTER:
            status = first_encounter()
        elif status == TEMPLE_ENCOUNTER:
            status = temple_encounter()
        elif status == DEVILDOG_ENCOUNTER:
            status = devildog_encounter()
        # etc
    
  4. After you've learnt about classes, revisit again and see what new changes you can make!

\$\endgroup\$
  • \$\begingroup\$ This is really a wonderful answer, lots to mess with here but thats all the fun! Thanks so much @GlennRogers! \$\endgroup\$ – lerugray Oct 3 '12 at 15:38
6
\$\begingroup\$

Glenn Rogers' answer is well-aimed at your level of expertise, so you should pay attention to his advice rather than mine, but I think you might be interested to peep ahead to see a more sophisticated approach to this kind of application.

In your program (a multiple-choice adventure game), the set of operations is highly stereotyped. There are a small number of common operations:

  1. printing a paragraph of text;
  2. presenting a numbered sequence of choices and getting the player to choose one;
  3. rolling dice and comparing the total to a statistic;

and so on. Coding up each of these operations every time makes your code repetitious, hard to follow, and hard to change (because the rules for, say, testing a statistic are distributed all over the code).

In this kind of situation, one way to make the code easier to read and follow is to use a domain-specific language to separate the implementation of the operations from their expression. (You'll sometimes see the phrase "language-oriented programming" used for this approach.)

Here's an example implementation of a domain-specific language for this kind of application:

GWYDIONS_CASTLE = '''

PAGE 1

It is only the autumn night that makes you shiver, you tell yourself, but you
know better.

The castle stands on the hill in the gathering dusk. Wrapped in your grey cloak
against the cold, you crouch on the edge of the counterscarp. Eyes peer down
from the battlements, but you need not fear discovery: they are only the empty
sockets in the skulls of the adventurers who have come before you to harrow the
fortress of the wizard Gwydion.

Soon it will be dark enough to make your move.

You have boots.

You have a cloak.

You have a dagger.

To scale the wall, turn to page 27.

To swim the moat, turn to page 24.


PAGE 2

The dagger makes no impression on the chain. "Ah, you wish to free me," says
the monk. "You have a good heart, but no blade forged of iron can cut a chain
forged with magic."

Suddenly he stands up. "Only Gwidion's death can undo his magic.  But if you
wish to defeat him, take this." He selects a tattered scroll from the shelf
above the desk and hands it to you.

Defeat Gwydion? You only meant to rob him. But you nod and take the scroll.

You have a tattered scroll.

To leave the monk and follow the stone passageway, turn to page 13.


PAGE 3

You are in a long stone passageway.

To go through the doorway on the right, turn to page 33.

To follow the passageway, turn to page 13.


PAGE 4

You push the door. It opens noiselessly on well-oiled hinges and you peer
through the gap. What you see inside takes your breath away. Golden coins in
heaps! Tapestries in silk and damask! Kingly crowns studded with polished
stones. This is what you came for.

To fill your pockets and make a run for it, turn to page 34.

To try the other door instead, turn to page 10.


PAGE 5

To tag onto the back of the troop, turn to page 6.

To stay hidden until they pass, and then try your key in the door of the keep,
turn to page 40.


PAGE 6

You follow the troop, imitating their shambolic walk as best you can. No one
seems to notice your presence. The door to the keep swings open, squealing on
its rusty hinges like a stuck pig, and you pass through one by one. The massive
door swings shut behind you.

If you have a cloak, turn to page 36.

Test your luck. If you succeed, turn to page 23.

Otherwise, turn to page 21.


PAGE 7

The courtyard is quiet in the moonlight, but you remain on your guard. Who
knows what traps Gwydion has set for unwary intruders?

To approach the keep, turn to page 41.


PAGE 8

Your boots echo loudly on the tiles. Too loud. You freeze, but the sound of
footsteps does not stop. You turn to see an armoured knight step emerging from
an alcove. Its visor is down, but somehow you doubt that there is a face behind
it.

        suit of armour (the, its) SKILL 10 STAMINA 10

If you win, turn to page 30.


PAGE 9

It looks as if the goblin dropped a key in the struggle. You pick it up.

You gained a key.

To leave the scullery by the door, turn to page 7.


PAGE 10

You push the door. It opens noiselessly on well-oiled hinges and you peer
through the gap. Inside is a study lined with tapestries, and at a desk sits a
tall white-haired man, bent in concentration over a grimoire. It is the wizard
Gwydion. His staff rests at his side.

If you have a tattered scroll, turn to page 47.

To attack Gwydion, turn to page 45.

To try the other door instead, turn to page 4.


PAGE 11

You leap through the window.

Test your luck. If you succeed, turn to page 12.

Otherwise, turn to page 32.


PAGE 12

It is a long fall, but the moat is deep. You pull yourself out and slink away
empty-handed into the night. Is that distant laughter you can hear? No
matter. You will be back.


PAGE 13

The dark passageway enters a large hall lit by a candelabra. Suits of armour
stand silently in alcoves. The floor is tiled in a black and white checkerboard
pattern, and at the far side a wide staircase ascends. Something about the room
makes you suspicious.

To cross the hall, stepping only on the white tiles, turn to page 20.

To cross the hall, stepping only on the black tiles, turn to page 20.


PAGE 14

You dart into the room and snatch the staff from the wizard's side. He snaps
awake, and makes a grab for it, but you draw back from his reach.

"Very good, {player.name}!" he says. "But with or without my staff, I am still
the wizard Gwydion!"

To fight him, turn to page 29.

To run away, turn to page 25.


PAGE 15

Two steps are all it takes to cover the distance, and then your dagger goes in
between his cervical vertebrae. The monk barely make a sound as he collapses
forward onto the desk, bleeding onto his half-copied page.

You search his cassock efficiently but find nothing. It is only then that you
notice the heavy iron chain from his leg to the desk. He was a prisoner here,
not an enemy.

To go back and follow the stone passageway, turn to page 13.


PAGE 16

You place your foot on a loose cobble and it gives way. For a moment you hang
from your fingertips, but with lightning speed you find a new foothold. Soon
you swing up through a machicolation and onto the parapet walk.

To quickly descend the stairs to the courtyard, turn to page 37.


PAGE 17

You take a ladleful and sip. Pfaughh! This isn't soup, it's laundry! There are
underclothes boiling here... and not overly clean ones.

A scraping from behind you makes you drop the ladle in alarm.  Someone is
coming through the door.

To try to sneak out past the newcomer, turn to page 22.

To stand your ground, turn to page 26.

To hide, turn to page 49.


PAGE 18

You cough. The monk turns his head. "It's nearly done," he says, "I'm writing
as fast as I can. But with this light..." He turns back to his work. It is only
then you notice the heavy iron chain linking his leg to the desk. He is a
prisoner here!

To cut the chain with your dagger, turn to page 2.

To leave the monk to his fate and follow the stone passageway, turn to page 13.


PAGE 19

You place your foot on a loose cobble and it gives way. For a moment you hang
from your fingertips, cursing the mason. And then you are gone.


PAGE 20

If you have boots, turn to page 8.

Otherwise, turn to page 30.


PAGE 21

You are still wet with water from your swim, and in the silence a drip echoes
in the stone passageway. The three shambling figures turn to look at you. You
wish that you had never seen the rotting flesh beneath their hoods.

          first zombie (the, its) SKILL 4 STAMINA 6

The second zombie is even more hideous than the first. It reaches for you with
the claws on its one good hand.

         second zombie (the, its) SKILL 5 STAMINA 6

The third zombie is more horrible than the first two put together.  It glares
at you with one eye dangling from its socket.

         third zombie (the, its) SKILL 6 STAMINA 6

If you win, turn to page 3.


PAGE 22

Test your luck. If you succeed, turn to page 42.

Otherwise, turn to page 26.


PAGE 23

The troop continues down a long stone passageway, but you scuttle through a
doorway on the right, glad to be rid of their unsettling company.

Turn to page 33.


PAGE 24

You wrap your boots in your cloak and slip silently into the water. There is a
water gate, barred with iron, but the iron is rusted and crumbling below the
waterline. You take a deep breath and squeeze through into a dark underwater
passage.

Test your stamina. If you succeed, turn to page 35.

Otherwise, turn to page 46.


PAGE 25

Clutching the wizard's staff, you run out onto the landing.  Gwydion
follows. You run down the stairs. Gwydion follows. You run across the tiled
floor. Gwydion follows, his hobnailed boots ringing on the stone.

A suit of armour steps down from an alcove and swings its sword at Gwydion. He
shatters it with a blast of lightning, but a second suit of armour is at the
wizard's back, swinging its sword. A third and a fourth step into the fray
until you can no longer see the wizard. Caught in his own trap!

Turn to page 50.


PAGE 26

The newcomer is green-skinned and ugly as sin. He lunges at you with a
staff. You have no choice but to defend yourself.

            goblin (the, his) SKILL 7 STAMINA 6

If you win, turn to page 9.


PAGE 27

The masonry has not been pointed in many years, and the stones are rough. You
climb swiftly and silently: an owl, perched in an arrow slit, is not disturbed
as you pass. But the wall is high.

Test your skill. If you succeed, turn to page 16.

Otherwise, turn to page 19.


PAGE 28

You flourish your dagger, but Gwydion taps you with his staff and you find
yourself unable to move.

Turn to page 44.


PAGE 29

Gwydion raises his hands, lightning flickering from his fingers.

            Gwydion (-, his) SKILL 10 STAMINA 12

If you win, turn to page 50.


PAGE 30

You cross the hall and ascend the stairs. On the landing there are two
doors. One has a stuffed owl on the lintel, the other a lizard in a jar.

To enter the door with the stuffed owl, turn to page 4.

To enter the door with the lizard, turn to page 10.


PAGE 31

This page intentionally left blank.


PAGE 32

It is a long fall, and the ground is hard.


PAGE 33

This is a scriptorium, with shelves stacked haphazardly with scrolls and
codices. By the light of a candle, a tonsured man sits at a writing desk,
copying a manuscript. His back is turned to you and he does not appear to have
heard him enter.

To slip back out again and follow the stone passageway, turn to page 13.

To kill the monk, turn to page 15.

To talk to the monk, turn to page 18.


PAGE 34

Just one of the crowns would set you up for life. But you close your fingers on
it and it vanishes.

You hear a mocking laugh behind you, and whirl around to see the wizard Gwydion
standing there.

"Your head will make a fair adornment for my battlements, {player.name}," he
says.

If you have a tattered scroll, turn to page 48.

To jump out of the window, turn to page 11.

To attack Gwydion, turn to page 28.


PAGE 35

You feel your way along the passage in the dark. Nothing but cold stone. Your
lungs are bursting: you must find a way out or drown.  With a last desperate
burst of energy you kick upwards and surface. Blessed air! You breathe it deep
into your lungs. You seem to have left your cloak and boots in the passage, but
at least you are alive.

You lost your boots.

You lost your cloak.

To haul yourself out of the water and look around, turn to page 38.


PAGE 36

Are you scrutinized by unseen eyes as you pass the threshold? With your grey
cloak pulled over your head, you cannot tell.

Your luck went up by 1.

Turn to page 23.


PAGE 37

The courtyard is quiet in the moonlight, but you remain on your guard. Who
knows what traps Gwydion has set for unwary intruders?

To enter a low stone building in a corner of the curtain wall, turn to page 38.

To approach the keep, turn to page 41.


PAGE 38

By the flickering light of a fire, you can see that this is a scullery. Buckets
of dirty dishes stand by the water's edge, and a giant cauldron bubbles over
the flame. There is a door in the west wall.

To drink from the cauldron, turn to page 17.

To leave the scullery by the door, turn to page 7.


PAGE 39

You start to unroll the scroll, but Gwydion dashes it out of your hand with his
staff.

Turn to page 28.


PAGE 40

The unsettling shambolic figures disappear into the keep and the door swings
shut behind them. You stay hidden for a quarter of an hour. Nothing moves. You
step silently across the drawbridge and approach the door.

The key grates in the lock but turns. You pull on the massive brass ring, and
the door opens, squealing on its rusty hinges like a stuck pig. You freeze, but
no one seems to have heard: or maybe they are used to the noise. You step
through and leave the door ajar.

Turn to page 3.


PAGE 41

Massive blocks of well-fitted grey stone make up the walls of the keep: you can
see no way to scale the walls. Nor is there a moat with unguarded water gate:
just a ditch filled with chevaux de frise.

But what's this? You crouch behind a pigsty as a troop of guards marches
towards the keep. Though maybe "marches" is the wrong word: the first one is
limping, the second has a hunched back, and the third drags his foot along the
ground.

If you have a key, turn to page 5.

This could be your only chance. To tag onto the back of the troop, turn to page
6.


PAGE 42

You flatten yourself against the wall beside the door, and hold your breath as
the newcomer enters. He is green-skinned, ugly as sin, and carrying a staff. He
approaches the cauldron and gives it a stir, sniffing the fumes.

To sneak out while his back is turned, turn to page 7.


PAGE 43

Quietly, so as not to disturb the wizard, you unroll the scroll and whisper the
words. Gradually the wizard's head nods forward until his beard is on the desk
and you can hear him snoring.

To run in and stab him, turn to page 45.

To steal his staff, turn to page 14.


PAGE 44

The wizard studies your face as you stand there paralyzed. "A most respectable
visage. I think it will look best on the western wall."


PAGE 45

Two swift steps and you plunge your dagger into his back, only for it to
shatter into pieces. Gwydion jumps up from the desk with staff in hand and taps
you with it. Suddenly you find yourself unable to move.

Turn to page 44.


PAGE 46

You feel your way along the passage in the dark. Nothing but cold stone as far
as you can swim. Your lungs are bursting: you must turn back or drown. Back to
the water gate. Where is the gap? In panic you wrench at the bars, but it is
too late....


PAGE 47

To read your scroll, turn to page 43.

To attack Gwydion, turn to page 45.

To try the other door instead, turn to page 4.


PAGE 48

To read your scroll, turn to page 39.

To jump out of the window, turn to page 11.

To attack Gwydion, turn to page 28.


PAGE 49

You crouch down behind the cauldron and watch as a goblin enters the laundry
room. He is green-skinned, ugly as sin, and carrying a staff. He approaches the
cauldron and gives it a stir, sniffing the fumes.

You leap up from your hiding place and push the goblin into the cauldron, where
he expires horribly in the boiling water and a tangle of dirty clothing.

Turn to page 9.


PAGE 50

Gwydion is dead. His staff and his books are in your hands. All you have to do
now is find his gold, subdue his minions, free his slaves, and get the treasure
safely away. It should be easy now...

'''

import re
import random
import textwrap

class GameOver(Exception):
    pass

class Roll(object):
    """
    The object `Roll(n, bonus = 0, k = 6)` represents a roll of `n`
    `k`-sided dice, plus `bonus`. When converted to a string it
    describes the roll in English.
    """
    def __init__(self, n, bonus = 0, k = 6):
        self.roll = [random.randint(1, k) for _ in range(n)]
        self.bonus = bonus
        self.total = sum(self.roll) + self.bonus

    def __str__(self):
        if len(self.roll) == 1:
            s = '{}'.format(self.roll[0])
        elif len(self.roll) == 2:
            s = '{} and {}'.format(*self.roll)
        else:
            s = '{}, and {}'.format(', '.join(self.roll[:-1]), self.roll[-1])
        if self.bonus:
            s += ', plus {}'.format(self.bonus)
        if len(self.roll) > 1 or self.bonus:
            s += ', making {}'.format(self.total)
        return s

class Player(object):
    STATS = [('skill', 1, 6), ('stamina', 1, 6), ('luck', 1, 6)]

    def __init__(self, game):
        self.game = game
        self.name = self.game.input("What is your name? ")
        self.equipment = set()
        self.initial_stats = dict()
        self.w = lambda s: game.write(s, 4)

    def rollup(self):
        self.w("Welcome, {player.name}. Let's roll up your character.")
        for stat, dice, bonus in self.STATS:
            r = Roll(dice, bonus)
            self.initial_stats[stat] = r.total
            setattr(self, stat, r.total)
            self.w("{}: you rolled {}.".format(stat.capitalize(), r))

    def test_stat(self, stat):
        w = self.w
        value = getattr(self, stat)
        w("Testing your {} ({}).".format(stat, value))
        r = Roll(2)
        if r.total > value:
            w("You rolled {}. You failed the test.".format(r))
            return False
        elif r.total == value:
            w("You rolled {}. You just made it.".format(r))
        else:
            w("You rolled {}. You passed the test.".format(r))
        if stat == 'luck':
            self.adjust_stat(stat, -1)
        return True

    def adjust_stat(self, stat, change, report = True):
        old_value = getattr(self, stat)
        new_value = max(0, min(self.initial_stats[stat], old_value + change))
        change = new_value - old_value
        setattr(self, stat, new_value)
        if report and change != 0:
            self.w("Your {} just went {} by {}. It is now {}."
                   .format(stat, 'down' if change < 0 else 'up',
                           abs(change), new_value))

class Paragraph(object):
    """
    `Paragraph(game, **kwargs)` represents a paragraph of instructions
    on a page. Call it to execute the instructions. The class property
    `re` is a regular expression matching all paragraphs of this
    type. The `kwargs` dictionary passed to the constructor must
    contain all the named groups in `re`.
    """

    def __init__(self, game, **kwargs):
        self.game = game
        self.w = lambda s: self.game.write(s, 4)
        for k, v in kwargs.iteritems():
            setattr(self, k, v)

    group_re = re.compile(r'\(?P<(\w+)>')

    @classmethod
    def group(cls):
        "Return the name of the first named group in the `re` property."
        return cls.group_re.search(cls.re).group(1)

class Fight(Paragraph):
    re = (r'(?P<monster_name>\S.*\S) \((?P<monster_article>the|an?|-),'
          r' (?P<monster_pronoun>their|his|her|its)\)'
          r' +SKILL +(?P<monster_skill>\d+) +STAMINA +(?P<monster_stamina>\d+)')

    def __call__(self):
        w = self.w
        player = self.game.player
        name = self.monster_name
        article = self.monster_article
        if self.monster_article == '-':
            fullname = name
        else:
            fullname = '{} {}'.format(self.monster_article, name)
        pronoun = self.monster_pronoun
        skill = int(self.monster_skill)
        stamina = int(self.monster_stamina)
        w('{0.name}: SKILL {0.skill} STAMINA {0.stamina}'.format(player))
        w('{}: SKILL {} STAMINA {}'.format(name.capitalize(), skill, stamina))
        while True:
            player_roll = Roll(2, player.skill)
            w("You rolled {}.".format(player_roll))
            monster_roll = Roll(2, skill)
            w("{} rolled {}.".format(fullname.capitalize(), monster_roll))
            if monster_roll.total == player_roll.total:
                w("You skirmish without damage on either side.")
                continue
            luck_adjustment = 0
            if self.game.input("Test your luck? ").lower() in ('y', 'yes'):
                luck_adjustment = 1 if player.test_stat('luck') else -1
            if monster_roll.total > player_roll.total:
                player.adjust_stat('stamina', -2 + luck_adjustment, False)
                w("{} hits you! Your stamina is now {}."
                  .format(fullname.capitalize(), player.stamina))
                if player.stamina <= 0:
                    w("You die.")
                    raise GameOver()
            else:
                stamina = max(0, stamina - (2 ** (luck_adjustment + 1)))
                w("You hit {}! {} stamina is now {}."
                  .format(fullname, pronoun.capitalize(), stamina))
                if stamina <= 0:
                    w("You killed {}.".format(fullname))
                    return

class NextPage(Paragraph):
    re = r'(?:If you win, t|Otherwise, t|T)urn to page (?P<next_page>\S+)\.'

    def __call__(self):
        return self.next_page

class TestEquipment(Paragraph):
    re = (r'If you have(?: (?:an?|your|the|some))? (?P<equip_test>\S.*\S),'
          r' turn to page (?P<equip_page>\S+)\.')

    def __call__(self):
        if self.equip_test in self.game.player.equipment:
            return self.equip_page

class TestStat(Paragraph):
    re = (r'Test your (?P<test_stat>\S+)\. If you succeed,'
          r' turn to page (?P<test_page>\S+)\.')

    def __call__(self):
        if self.game.player.test_stat(self.test_stat):
            return self.test_page

class ChangeStat(Paragraph):
    re = (r'Your (?P<stat_change>\S+) went (?P<stat_dir>up|down) by'
          r' (?P<stat_amount>\d+)\.')

    def __call__(self):
        amount = int(self.stat_amount) * (-1 if self.stat_dir == 'down' else 1)
        self.game.player.adjust_stat(self.stat_change, amount)

class GainEquipment(Paragraph):
    re = (r'You (?:gained|have)(?: (?:an?|your|the|some))?'
          r' (?P<gain_equipment>\S.*\S)\.')

    def __call__(self):
        self.game.player.equipment.add(self.gain_equipment)

class LoseEquipment(Paragraph):
    re = r'You lost(?: (?:an?|your|the|some))? (?P<lose_equipment>\S.*\S)\.'

    def __call__(self):
        self.game.player.equipment.remove(self.lose_equipment)

class Choice(Paragraph):
    re = r'(?P<choice>\S.*\S), turn to page (?P<choice_page>\S+)\.'

    def __call__(self):
        n_choices = len(self.choices)
        if n_choices == 1:
            self.w("{}, press return.".format(self.choices[0]['choice']))
        else:
            for i, c in enumerate(self.choices):
                self.w("{}, choose {}.".format(c['choice'], i + 1))
        prompt = "? "
        while True:
            inp = self.game.input(prompt)
            if n_choices == 1:
                return self.choices[0]['choice_page']
            try:
                choice = int(inp)
                if 1 <= choice and choice <= n_choices:
                    return self.choices[choice - 1]['choice_page']
            except ValueError:
                pass
            prompt = "Please enter a choice from 1 to {}> ".format(n_choices)

class Description(Paragraph):
    re = r'(?P<description>\S.*\S)'

    def __call__(self):
        self.game.write(self.description)

class Page(object):
    def __init__(self, game):
        self.game = game
        self.paras = []
        self.choice = None

    def add(self, cls, **kwargs):
        if cls == Choice:
            if not self.choice:
                self.choice = Choice(self.game, choices = [])
                self.paras.append(self.choice)
            self.choice.choices.append(kwargs)
        else:
            self.paras.append(cls(self.game, **kwargs))

    def visit(self):
        for para in self.paras:
            page = para()
            if page:
                return page
        raise GameOver()

class Game(object):
    width = 72
    para_re = re.compile(
        r'^(?:PAGE (?P<page>\S+)|{})$'
        .format('|'.join(cls.re for cls in Paragraph.__subclasses__())))

    def __init__(self, text):
        self.pages = dict()
        page = None
        for para in re.split(r'\n(?:[ \t]*\n)+', text.strip()):
            para = textwrap.dedent(para).strip().replace('\n', ' ')
            m = self.para_re.match(para).groupdict()
            if m['page']:
                if page is None: self.initial_page = m['page']
                page = self.pages[m['page']] = Page(self)
                continue
            for cls in Paragraph.__subclasses__():
                if m[cls.group()]:
                    page.add(cls, **m)
                    break

    def input(self, prompt):
        answer = raw_input(prompt)
        print('')
        if answer.lower() in ('q', 'quit'):
            raise GameOver()
        return answer

    def write(self, para, left_margin = 0):
        indent = ' ' * left_margin
        for line in textwrap.wrap(para.format(player = self.player),
                                  self.width, initial_indent = indent,
                                  subsequent_indent = indent):
            print(line)
        print('')

    def centre(self, text):
        self.write(text, (self.width - len(text)) // 2)

    def play(self):
        self.player = Player(self)
        self.player.rollup()
        self.centre('*')
        page = self.initial_page
        try:
            while True:
                page = self.pages[page].visit()
        except GameOver:
            pass

if __name__ == '__main__':
    Game(GWYDIONS_CASTLE).play()
\$\endgroup\$
  • \$\begingroup\$ Just managed to squeeze that into the 30,000-character limit! \$\endgroup\$ – Gareth Rees Oct 5 '12 at 15:39

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.