Ex52 - My version of gothonweb

Hi everyone, I decided to share my solution as an exercise… so I could briefly present how I put it down and also have some useful feedback possibly.

My idea was to inherit from the Room class to define a child ExpireRoom(Room) class where to implement some additional features requested like guessing a code within a certain number of attempts as in escape_pod and laser_weapon_armory.
I know inheritance isn’t the best and that composition should be preferred but I wanted to keep the app.py file as it was, as much as possible because I reckoned there was no way to improve its readability and it was already pretty straightforward and compact. Inheritance allowed me to do so because the app.py code can keep working exactly the same, no matter what kind of Room istance it deals with. If app.py goes through an ExpireRoom istance, overridden methods perform different features instead of a normal Room basic features.

I also fixed the Room.go method so it can react to the presence of ‘*’ key, if such a special key is in paths dict.

Last but not least I decided to fix global functions name_room and load_room restricting their action to Room istances, instead of all kinds of global variables.

Here is my code of planisphere.py:

from random import randint
from random import sample

class RoomCallError(Exception):
    pass

class Room(object):

    def __init__(self, name, description):
        self.name = name
        self.description = description
        self.paths = {}

    # Method go was fixed in order to work with '*' key.
    # When '*' is a key, it indicates path for unknown directions
    # (which can be != from default None)
    # If paths dict hasn't got a '*' key, then the method will return None,
    # allowing reload of the same room as expected in app.py
    def go(self, direction):
        if '*' in self.paths.keys():
            if direction in self.paths.keys():
                return self.paths.get(direction)
            else:
                return self.path.get('*')

        else:
            return self.paths.get(direction, None)


        if direction in self.add_paths.keys():
            return self.paths.get(direction, None)
        else:
            return self.paths.get('*')

    def add_paths(self, paths):
        self.paths.update(paths)

# This class ExpireRoom is-a Room. Inheritance from class room was chosen
# because composition would have led to more changes in the code. I think
# it is more logical in this case.
class ExpireRoom(Room):

    # __init__ method was overridden to define additional attributes
    def __init__(self, name, description, max_attempts, code, faildestination):
        self.name = name
        self.description = description
        self.paths = {}
        # Additional attributes for ExpireRoom
        self.attempts = 0
        self.code = code
        self.max_attempts = max_attempts
        self.faildestination = faildestination
        # writes the code on a log file
        self.logfile = open(f"{self.name}_log.txt", 'w')
        self.logfile.write(f"{self.code}\n")
        self.logfile.close()

    # The following resetcode method is defined only for the child class
    # This method allows to refresh the code whenever necessary (for example
    # once the user lost and wants to play again, or after winning)
    # The method resetcode also resets and readds paths in order to refresh
    # the code path
    def resetcode(self, code, paths):
        self.attempts = 0
        self.code = code
        # Also paths must be refreshed
        self.paths.clear()
        self.add_paths(paths)
        # writes the code on a log file
        self.logfile = open("log.txt", 'a')
        self.logfile.write(f"{self.code}\n")
        self.logfile.close()

    # Check_code_expired method is defined only for the child class
    # Increments attempts and checks if code is expired
    def check_code_expired(self):
        self.attempts += 1

        # in attempts == max_attempts condition, the new path ('*') has to be
        # added so this last chance becomes critical due to the presence of '*'
        if self.attempts >= self.max_attempts:
            self.add_paths({'*': self.faildestination})
            return True
        else:
            return False

    def go(self, direction):
        # First off updates expiration state of the code
        self.check_code_expired()

        if '*' in self.paths.keys():
            if direction in self.paths.keys():
                return self.paths.get(direction)
            else:
                return self.paths.get('*')

        else:
            return self.paths.get(direction, None)


        if direction in self.add_paths.keys():
            return self.paths.get(direction, None)
        else:
            return self.paths.get('*')

# Possible death quotes, derived from map.py in previous exercise
quips = ["You died. You kinda suck at this.",
         "Your Mom would be proud...if she were smarter.",
         "Such a luser.",
         "I have a small puppy that's better at this.",
         "You're worse than your Dad's jokes."
]

# Only inizialize death Room istance with one static sample quote
generic_death = Room("Death", sample(quips, 1)[0])

central_corridor = Room("Central Corridor",
"""
The Gothons of Planet Percal #25 have invaded your ship and destroyed
your entire crew. You are the last surviving member and your last
mission is to get the neutron destruct bomb from the Weapons Armory, put
it in the bridge, and blow the ship up after getting into an escape pod.

You're running down the central corridor to the Weapons Armory when a
Gothon jumps out, red scaly skin, dark grimy teeth, and evil clown
costume flowing around his hate filled body. He's blocking the door to
the Armory and about to pull a weapon to blast you.
""")

# This is an istance of child class ExpireRoom. __init__ overriden method takes
# 3 more arguments
laser_weapon_armory = ExpireRoom("Laser Weapon Armory",
"""
Lucky for you they made you learn Gothon insults in the academy. You
tell the one Gothon joke you know: Lbhe zbgure vf fb sng, jura fur fvgf
nebhaq gur ubhfr, fur fvgf nebhaq gur ubhfr. The Gothon stops, tries
not to laugh, then busts out laughing and can't move. While he's
laughing you run up and shoot him square in the head putting him down,
then jump through the Weapon Armory Door.

You do a dive roll into the Weapon Armory, crouch and scan the room for
more Gothons that might be hiding. It's dead quiet, too quiet. You
stand up and run to the far side of the room and find the neutron bomb
in its container. There's a keypad lock on the box and you need the
code to get the bomb out. If you get the code wrong 10 times then the
lock closes forever and you can't get the bomb. The code is 3 digits.
""",
10, f"{randint(0, 9)}{randint(0, 9)}{randint(0, 9)}", generic_death)


the_bridge = Room("The Bridge",
"""
The container clicks open and the seal breaks, letting gas out. You
grab the neutron bomb and run as fast as you can to the bridge where you
must place it in the right spot.

You burst onto the Bridge with the neutron destruct bomb under your arm
and surprise 5 Gothons who are trying to take control of the ship. Each
of them has an even uglier clown costume than the last. They haven't
pulled their weapons out yet, as they see the active bomb under your arm
and don't want to set it off.
""")

the_end_winner = Room("The End (winner)",
"""
You jump into pod 2 and hit the eject button. The pod easily slides out
into space heading to the planet below. As it flies to the planet, you
look back and see your ship implode the explode like a bright star,
taking out the Gothon ship at the same time. You won!
""")


the_end_loser = Room("The End (loser)",
"""
You jump into a random pod and hit the eject button. The pod escapes
out into the void of space, then implodes as the hull ruptures, crushing
your body into jam jelly.
"""
)

# Also this is a istance of ExpireRoom and has 3 more attributes to inizialize
escape_pod = ExpireRoom("Escape Pod",
"""
You point your blaster at the bomb under your arm and the Gothons put
their hands up and start to sweat. You inch backward to the door, open
it, and then carefully place the bomb on the floor, pointing your
blaster at it. You then jump back through the door, punch the close
button and blast the lock so the Gothons can't get out. Now that the
bomb is placed you run to the escape pod to get off this tin can.

You rush through the ship desperately trying to make it to the escape
pod before the whole ship explodes. It seems like hardly any Gothons
are on the ship, so your run is clear of interference. You get to the
chamber with the escape pods, and now need to pick one to take. Some of
them could be damaged but you don't have time to look. There's 5 pods,
which on do you take?
""",
1, f"{randint(1, 5)}", the_end_loser)

# code attribute of laser_weapon_armory is the key of a new path
# Warning! Every time that code is reset by the resetcode method, this path has
# to be passed to the method resetcode (see def resetcode)
escape_pod.add_paths({
    escape_pod.code: the_end_winner
    # '*': the_end_loser was eliminated because it is included by the ExpireRoom
    # features (go method override)
})

the_bridge.add_paths({
    'throw the bomb': generic_death,
    'slowly place the bomb': escape_pod
})

# code attribute of laser_weapon_armory is the key of a new path
# Warning! Every time that code is reset by the resetcode method, this path has
# to be passed to the method resetcode (see def resetcode)
laser_weapon_armory.add_paths({
    laser_weapon_armory.code: the_bridge
})

central_corridor.add_paths({
    'shoot!': generic_death,
    'dodge!': generic_death,
    'tell a joke': laser_weapon_armory
})

START = 'central_corridor'

# Definition of rooms list
# checks on globals() are thus limited to the rooms list
rooms = [central_corridor,
         laser_weapon_armory,
         the_bridge,
         escape_pod,
         the_end_winner,
         the_end_loser,
         generic_death
]

def load_room(name):

    # Checks that the corresponding global variable is in rooms
    if globals().get(name) in rooms:
        return globals().get(name)
    else:
        raise RoomCallError

def name_room(room):

    if room in rooms:

        for key, value in globals().items():
            # Checks that the corresponding global variable is in rooms
            if value == room:
                return key
    else:
        raise RoomCallError

I will link also the app.py code:

from flask import Flask, session, redirect, url_for, escape, request
from flask import render_template
from gothonweb import planisphere
from gothonweb.planisphere import quips
from random import sample
from random import randint

app = Flask(__name__)

@app.route("/")
def index():
    # this is used to "setup" the session with starting values
    session['room_name'] = planisphere.START
    return redirect(url_for("game"))

@app.route("/game", methods=['GET', 'POST'])
def game():
    room_name = session.get('room_name')

    if room_name == "generic_death":
        # The code in laser_weapon_armory is refreshed
        newcode = f"{randint(0, 9)}{randint(0, 9)}{randint(0, 9)}"
        planisphere.laser_weapon_armory.resetcode(newcode, {newcode: planisphere.the_bridge})
        # The code (pod) in escape_pod is refreshed
        newpod = f"{randint(1, 5)}"
        planisphere.escape_pod.resetcode(newpod, {newpod: planisphere.the_end_winner})

    if request.method == "GET":
        room = planisphere.load_room(room_name)
        # Refresh generic_death, to sort a new quip, to show in case of death
        planisphere.generic_death.__init__("Death", sample(quips, 1)[0])
        return render_template("show_room.html", room=room)
    # request.method == "POST"
    else:
        action = request.form.get('action')

        if room_name and action:
            room = planisphere.load_room(room_name)
            next_room = room.go(action)

            if not next_room:
                session['room_name'] = planisphere.name_room(room)
            else:
                session['room_name'] = planisphere.name_room(next_room)

        return redirect(url_for("game"))


# YOU SHOULD CHANGE THIS IF YOU PUT ON THE INTERNET (added fb87w at the end of it)
app.secret_key = 'A0Zr98j/3yX R~XHH!jmN]LWX/,?RTfb87w'

if __name__ == "__main__":
    app.run()

Hey, looking good. I can’t really see much wrong with this at this stage. I guess your done!

1 Like
A free service run by Zed A. Shaw for learncodethehardway.org.