#! /usr/bin/python
# load cgi and enable exception handling with HTML output
import cgi, cgitb; cgitb.enable()
# load cgi and enable exception handling with logging output
#import cgi, cgitb; cgitb.enable(display=0, logdir="\tmp")
import sys, re, string, binascii, random, os
from types import *
###########################################################################
### C O N S T A N T S #####################################################
###########################################################################
# message the user gets from help command
help_msg = \
"""
Hunt the Wumpus Help
Commands (space after command is optional):
- m
- move to the rooms in list, in order, halts if it
encounters an impossible move
- s
- shoot a magic arrow along the path of rooms in list,
halts if it encounters an impossible move
- l
- look around and check status, gives turn #, # of
arrows, room #, # of wumpi killed
- q
- quit the game, only for the meek
- ? orhelp
- display this text
"""
# these constants are indexes in the parameters list
NBATS = 0 # numer of superbats
NPITS = 1 # number of pits
NWUMP = 2 # number of wumpi (must be > 0)
NARRS = 3 # number of arrows
ARRNG = 4 # arrow range
PARM5 = 5 # from here down reserved for additional features later on
PARM6 = 6
PARM7 = 7
PARM8 = 8
NUMPARMS = 9
# these constants are used to identify things in a room
AN_ERROR = 0 # finding this would be bad
A_BAT = 1 # a superbat
A_PIT = 2 # a bottomless pit
A_WUMP = 3 # a wumpus
# description levels for display method of Room class
SHORT = 0
LONG = 1
# MAZES is the dictionary of available maze filenames by printable name
# Some day I want to add an illustration file name for displaying the map.
DEFAULT_MAZE="The Classic"
MAZES = {
DEFAULT_MAZE: "classic.dat",
"5x5 Grid": "grid5.dat",
"6x6 Grid": "grid6.dat",
"3-D Hypercube": "hyper3.dat",
"4-D Hypercube": "hyper4.dat",
"5-D Hypercube": "hyper5.dat",
"6-D Hypercube": "hyper6.dat",
"7-D Hypercube": "hyper7.dat",
}
INFILE_EXT = ".dat"
IMAGE_EXT = ".png"
DEFAULT_INFILE = MAZES[DEFAULT_MAZE]
# MAZEPATH is the directory path name for the maze files, MAZEWEBPATH
# is the path the webserver uses to find the same place
#MAZEPATH = "/var/www/html/wumpus/" # path on my laptop
MAZEPATH = "/home/users/web/b1741/va.flockhart/wumpus/" # path @ flockhart.org
MAZEWEBPATH = "/wumpus/"
# FN constants are the Field Names used in the web form. We use
# constants instead of just typing the strings because the interpreter
# will complain if we use FN_LOCA in one place and FN_LOC in another,
# but if we use "location" in one place and "loction" in another, the
# program will just mysteriously not work.
FN_MAZE = "maze" # the chozen maze file name
FN_LOCA = "location" # player location
FN_AMMO = "ammo" # number of shots left
FN_NWUM = "nwumpi" # number of wumpi left
FN_MOVE = "move_count" # number of moves so far
FN_TDIC = "state_key" # obfuscated dictionary of thing locations
FN_CMND = "next_cmd" # user's new command
###########################################################################
### D A T A T Y P E S ###################################################
###########################################################################
class Room:
def __init__(self, tokenlist=[]):
global room_list
self.idnum = len(room_list) + 1
self.beenthere = 0
self.contents = []
# gather the list of exits
self.exits = [] #list of room indices, not numbers
for tokenid in range(len(tokenlist)):
token = tokenlist[tokenid]
if token == ':':
break
self.exits.append(string.atoi(token) - 1)
self.exits.sort()
# collect the name, if any
self.name = None
if tokenid < (len(tokenlist) - 1):
tokenid = tokenid + 1
self.name = tokenlist[tokenid]
# collect the description, if any
self.desc = None
if tokenid < (len(tokenlist) - 1):
tokenid = tokenid + 1
self.desc = tokenlist[tokenid]
# translate \t and \n
self.desc = re.sub('\\\\t',"\t",self.desc)
self.desc = re.sub('\\\\n',"\n",self.desc)
def display(self, long = 0):
global room_list
# what's it called
if self.name:
if re.search(r"^\d+$", self.name): # name is a number
print "Room %s:" % self.name
else:
print self.name + ":"
else:
print "Room %d:" % self.idnum
print "
\n- "
# what's it look like
if self.desc and (long or not self.beenthere):
print " " + self.desc + "
"
self.beenthere = 1
# what do we detect
if self.exits:
print " %d Tunnels lead to" % len(self.exits),
for ridx in self.exits:
comma = ","
if ridx == self.exits[-1]:
comma = ""
if ridx < len(room_list):
print " " + room_list[ridx].idstr() +\
comma,
else:
print " ?" + comma,
print "
"
for thing in self.detects():
if thing == A_BAT:
print " You hear flapping wings!"
elif thing == A_PIT:
print " You feel a breeze!"
elif thing == A_WUMP:
print " You smell a wumpus!!"
print "
"
else:
print " No exits.
"
# what's in here with us
for thing in self.contents:
if thing == A_BAT:
print " Look! A huge bat!"
elif thing == A_PIT:
print " OH NO! A PIIiii..."
elif thing == A_WUMP:
print " Aaaagh! A wumpus!!"
print "
"
print "
"
def idstr(self):
if self.name:
return self.name
else:
return `self.idnum`
# how many of a thing are in this room?
def contains(self, thing):
numthing = 0
for obj in self.contents:
if obj == thing:
numthing = numthing + 1
return numthing
# what can we detect from this room?
def detects(self):
dlist = []
for r in self.exits:
dlist = dlist + room_list[r].contents
dlist.sort()
return dlist
def add(self, thing):
self.contents.append(thing)
return 1
def remove(self, thing):
for objno in range(len(self.contents)):
if self.contents[objno] == thing:
del self.contents[objno]
return 1
return 0
def remove_all(self, thing):
for objno in range(len(self.contents)):
if self.contents[objno] == thing:
del self.contents[objno]
return 1
class Player:
""" Player Object Class
This is pretty mutch a structure masquerading as an object for
now, since the current need is to group player data into a
common location, not to objectify it. Hopefully this will
evolve over time.
"""
def __init__(self, tokenlist=[]):
self.location = None
self.arrows = None
def setpos(self, pos):
self.location = pos
def setammo(self, ammo):
self.arrows = ammo
def adjammo(self, adj):
self.arrows = self.arrows + adj
class ParseStr:
""" Parsed String Object
This object class represents strings that have been collected for
the purpose of parsing.
"""
def __init__(self, newstr):
if not newstr:
sys.stderr.write("FATAL ERROR: ParseStr called " +\
"with invalid string.\n")
sys.exit()
self.strdata = newstr + " " # it's a horrible kludge,
# I admit it
self.curtok = ""
self.curpos_reset()
# curpos_incr is a wrapper function to handle changing of
# curpos. It increments it by one and then checks to make sure
# it's still within bounds - passing the end of strdata is
# considered a fatal error. It returns the new value as a
# convenience.
def curpos_incr(self):
self.curpos = self.curpos + 1
if self.curpos >= len(self.strdata):
sys.stderr.write("FATAL ERROR: Reached the end of " +\
"parse string before all data " +\
"collected.\n")
sys.exit()
return self.curpos
# wrapper function to handle resetting curpos to zero
def curpos_reset(self):
self.curpos = 0
# gtok gets the next token from infiledata and puts it in curtok, it
# also retuns it, as a convenience
def gtok(self):
# skip whitespace
while iz(string.whitespace, self.strdata[self.curpos]):
self.curpos_incr()
# if the token starts with '"' gather til the next one,
# else gather to next whitespace
self.curtok = ""
if self.strdata[self.curpos] == '"':
self.curpos_incr()
while self.strdata[self.curpos] != '"':
if self.strdata[self.curpos] == '\\':
self.curpos_incr()
self.curtok = \
self.curtok + self.strdata[self.curpos]
self.curpos_incr()
# discard the ending '"'
self.curpos_incr()
else:
while not iz(string.whitespace, \
self.strdata[self.curpos]):
self.curtok = \
self.curtok + self.strdata[self.curpos]
self.curpos_incr()
# return the new token for convenience
return self.curtok
# for as long as the tokens on the parse string are numbers, gather
# them into a list of numbers and return the whole list
def gnumlist(self):
templist = string.split(self.strdata[self.curpos:])
numlist = []
while templist and str_iz(string.digits, templist[0]):
numlist.append(string.atoi(templist[0]))
del templist[0]
self.gtok() # clear that token from the string
return numlist
# Return all remaining tokens on the string as a list. If
# firstitem is given, add it to the start of the list.
def glist(self, firstitem = None):
if firstitem and type(firstitem) == StringType:
if firstitem[0] == '"' and firstitem[-1] == '"':
templist = [firstitem[1:-1]]
else:
templist = [firstitem]
else:
templist = []
while not str_iz(string.whitespace,
self.strdata[self.curpos:]):
templist.append(self.gtok())
return templist
###########################################################################
### G L O B A L D A T A #################################################
###########################################################################
# infliname is the name of the input (.dat) file we use
infilename = ""
# room_list is the ordered (it had better be) list of room objects
room_list = []
# parameters are the global parameters supplied at the end of the .dat file
parameters = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# global player information
player = Player()
# reason for ending the game ("victory", "killed", "quit")
stopreason = ""
# wumpus headcount
number_wumpi = 0
# current move
move_count = 0
# dictionary of sanitized data from the web form
formdict = {}
###########################################################################
### F U N C T I O N S #####################################################
###########################################################################
# iz declares whether a character is a member of a set of characters
def iz(charset, testchar):
if charset.count(testchar):
return True
else:
return False
# str_iz declares whether a string is entirely made up of characters
# from a set
def str_iz(charset, teststr):
for tchr in teststr:
if not charset.count(tchr):
return 0
return 1
# give a random number from 0 to n-1 (predates the random library, not
# really needed these days)
def rnum(max = 10000):
return int(random.random() * max)
# chose an element from a list (see rnum above)
def rchoice(list):
return list[rnum(len(list))]
# place all of a certain type of thing in empty rooms
def placethings(parmidx, thing, name, empties):
global parameters, room_list
for i in range(parameters[parmidx]):
if not empties:
sys.stderr.write("ERROR: Ran out of rooms before " +\
"I ran out of %s!\n" % name)
sys.exit(1)
eidx = rchoice(range(len(empties)))
ridx = empties[eidx]
del empties[eidx]
room_list[ridx].add(thing)
return empties
# Create an obfuscated string listing the locations of all the things
# in the maze by creating it as a dictionary, converting it to a
# string and converting that string to a hexidecimal representation.
# Obviously this is easy to defeat, but we're just trying to get past
# the point where the player can hit Ctrl-U and see where every hazard
# in the maze is.
def pack_thing_locs():
thing_locs = {}
# fill the thing_locs dictionary
for thing in (A_BAT, A_PIT, A_WUMP):
thing_locs[thing] = []
for r in range(len(room_list)):
room = room_list[r]
for thing in room.contents:
thing_locs[thing].append(r)
# now pack it
stl = str(thing_locs)
return binascii.b2a_hex(stl)
# convert the hex representation of thing_locs back into a dictionary
def unpack_thing_locs(hexdata):
stl = binascii.a2b_hex(hexdata)
return eval(stl)
# move the player to newloc, return success or failure. newloc is a
# location string passed in by the player - it may be a name or it
# may be an idnum.
def move(newloc):
global player, room_list
newidx = findtunnel(player.location, newloc)
if newidx > -1:
return chloc(newidx)
else:
print "You smack into a wall.
"
look(0)
return 0
# given a room, curloc, and a string identifying another room, locstr,
# find a room reachable from curloc that has a name or idnum matching
# locstr. Return -1 if there is none.
def findtunnel(curloc, locstr):
global room_list
# first try all the names
for ridx in room_list[curloc].exits:
if room_list[ridx].name:
if locstr == room_list[ridx].name or \
locstr == string.lower(room_list[ridx].name):
return ridx
# if no names matched, try idnums
for ridx in room_list[curloc].exits:
if `room_list[ridx].idnum` == locstr:
return ridx
# none found
return -1
# change the players location to newloc, activate hazards in newloc
def chloc(newloc):
global player, room_list, stopreason
player.setpos(newloc)
room_list[newloc].display()
if room_list[newloc].contains(A_BAT):
print "The bat drags you to",
return chloc(rnum(len(room_list)))
elif room_list[newloc].contains(A_PIT):
print "You plummet to a horrible death."
stopreason = "killed"
elif room_list[newloc].contains(A_WUMP):
while (room_list[newloc].contains(A_WUMP) and not stopreason):
# 50% chance the wumpus wanders off
if rnum(2):
print "The wumpus wanders groggily away."
wumploc = rchoice(room_list[newloc].exits)
room_list[newloc].remove(A_WUMP)
room_list[wumploc].add(A_WUMP)
else:
print "The wumpus wakes up and devours you."
stopreason = "killed"
return 1
# an arrow travels from oldloc to newloc if possible. If it does, it
# kills anything there. newloc is a string representing the room from
# the players perspective. It could be a name or an idnum. oldloc is
# an index in room_list. Return the new location of the arrow or -1.
def shoot(oldloc, newloc):
global player, room_list, number_wumpi, stopreason
newlocidx = findtunnel(oldloc, newloc)
if newlocidx > -1:
if room_list[newlocidx].contains(A_WUMP):
print "You killed a wumpus!"
room_list[newlocidx].remove(A_WUMP)
number_wumpi = number_wumpi - 1
newlocidx = -1 #arrow died with honor
if number_wumpi == 0:
stopreason = "victory"
elif player.location == newlocidx:
print "You shot yourself!"
stopreason = "killed"
else:
print "You hear an arrow smack into a wall."
return newlocidx
# possibly move any wumpi in the maze
def movewumpi():
global player, stopreason
move_list = [] # planned wumpus moves
for room in room_list:
for wnum in range(room.contains(A_WUMP)):
if room.exits and rnum(2): # 50% chance
move_list.append( (room, \
rchoice(room.exits) ) )
for (oldroom, newidx) in move_list:
print "You hear something huge moving."
oldroom.remove(A_WUMP)
room_list[newidx].add(A_WUMP)
if room_list[player.location].contains(A_WUMP):
print "A wumpus trundles in and devours you!"
stopreason = "killed"
# display what the player sees when he looks around
def look(verbose, count = None):
global player, parameters
print "You are in",
room_list[player.location].display(verbose)
print "
"
print " You have %d range %d arrows.
" %\
(player.arrows, parameters[ARRNG])
if count:
print " You have made %d moves." % count
print "
\n"
# display a usage message and quit
def usage_abort():
print "\nUsage: %s [|-r []]\n" % sys.argv[0]
sys.exit(1)
# print the rest of the body & html document
def endbody():
print ' '
print "