#! /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 "

\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 " " print "" # newmazeform displays the form elements to choose a new maze and ends # the html page def newmazeform(): print '

Please select a maze:' print ' ' print ' ' endbody() ########################################################################### ### M A I N B O D Y ##################################################### ########################################################################### # load all form data form = cgi.FieldStorage() ## sanitize all your inputs!! errstr = "" for fn, lth, regex, dflt in ((FN_MAZE, 32, r"^[a-zA-Z0-9 \.\-]+$", None), (FN_LOCA, 16, r"^\d+$", None), (FN_AMMO, 8, r"^\d+$", "0"), (FN_MOVE, 8, r"^\d+$", "0"), (FN_NWUM, 8, r"^\d+$", "999"), (FN_TDIC, 0, r"^[a-f0-9]+$", ""), (FN_CMND, 255, r"^[lmsqhf\?][[a-zA-Z0-9\s]*$", "")): val = form.getfirst(fn) formdict[fn] = dflt if not val: continue if lth and len(val) > lth: errstr = "Field overflow: %s = %d" % (fn, len(val)) elif not re.search(regex, val): errstr = "Invalid characters in %s" % fn if errstr == "": formdict[fn] = val # where are we maze = formdict[FN_MAZE] location = formdict[FN_LOCA] nogame = not maze # we aren't in a game if there's no maze selected yet newgame = not location # we just picked the maze if we don't have a location yet ## All cgi scripts have to start by declaring content-type first print "Content-Type: text/html" # HTML is following print # blank line, end of headers ## our HTML header is the same except for whether we need to set focus on ## the command field onload = ' onload="setfocus()"' # as a convenience, put cursor in cmd field if nogame: onload = '' print """ Hunt the Wumpus

Hunt the Wumpus

based on Wumpus I and II by Gregory Yob, from More Basic Computer Games ed. by David H. Ahl

""" % (FN_CMND, onload) ## if this is a newgame, we present a menu of mazes to play and exit, ## otherwise, we will be playing a turn, possibky the first if nogame: newmazeform() # newmazeform() ends the html page sys.exit(0) ### Parse the maze file ### # open the input file neglen = 0 - len(INFILE_EXT) if maze[neglen:] != INFILE_EXT: maze = maze + INFILE_EXT try: infile = open(MAZEPATH + maze, "r") except: print '

Unable to open "%s%s".

' % \ (MAZEPATH,maze) endbody() sys.exit(1) # look for a matching map image mazepic = maze[:neglen] + IMAGE_EXT if mazepic in os.listdir(MAZEPATH): print '\n%s\n' %\ (MAZEWEBPATH, mazepic, mazepic[:neglen]) # read in the input file, skipping comments infiledata = "" newline = infile.readline() while newline: comment = string.find(newline, '#') if comment > 0: #if there's a comment, but not at start of line infiledata = infiledata + ' ' +\ string.strip(newline[0:comment]) elif comment == 0: #if the whole line's a comment newline = '' else: #if no comment infiledata = infiledata + ' ' + string.strip(newline) newline = infile.readline() # we've got everything we need from the input file infile.close() # parse out the rooms filedata = ParseStr(infiledata) while filedata.gtok() != "-1": # gather a room roominfo = [filedata.curtok] while filedata.gtok() != "0": roominfo.append(filedata.curtok) # add it to the global list room_list.append(Room(roominfo)) # gather the parameters for i in range(NUMPARMS): parameters[i] = string.atoi(filedata.gtok()) del filedata ## if we just started we need to locate the player and the hazards, ## otherwise, they are in hidden fields and it's time to respond ## to a move if newgame: ### Initialize the game ### empties = range(len(room_list)) # rooms that are currently empty # place bats, pits and wumpi, shrinking the empties list as we go for parmidx, thing, name in [(NBATS, A_BAT, "bats"), \ (NPITS, A_PIT, "pits"), \ (NWUMP, A_WUMP, "wumpi")]: empties = placethings(parmidx, thing, name, empties) # place the player safeones = [] for ridx in empties: if not room_list[ridx].detects(): safeones.append(ridx) if safeones: player.setpos(rchoice(safeones)) elif empties: player.setpos(rchoice(empties)) else: # we just dump out here, so we better be sure all the maps we use # leave room for the player! sys.stderr.write("ERROR: No room for player!\n") sys.exit(1) print ' ' %\ (FN_LOCA, player.location) # equip the player player.setammo(parameters[NARRS]) # initialize monster count number_wumpi = parameters[NWUMP] else: # restore the player player.setammo( int(formdict[FN_AMMO]) ) player.setpos( int(location) ) # get number of wumpi number_wumpi = int(formdict[FN_NWUM]) # get the move counter move_count = int(formdict[FN_MOVE]) # get the hazard locations, put them in thing_locs thing_locs = unpack_thing_locs(formdict[FN_TDIC]) # put the hazards back in their rooms for thing in thing_locs.keys(): for ridx in thing_locs[thing]: room_list[ridx].add(thing) print "" # If we aren't just starting a game, we should have a command from the user. # We process it here. stopreason = "" print move_count = move_count + 1 instr = formdict[FN_CMND] cmd = "" if instr != None and re.search(r"^[a-zA-Z0-9 ]+$", instr): cmdin = ParseStr(instr) cmd = string.lower(cmdin.gtok()[0]) # process command ## move ## if cmd == 'm': print "

Moving from %s...

" % room_list[player.location].idstr() rlist = cmdin.glist(cmdin.curtok[1:]) while rlist: if not move(rlist[0]): break del rlist[0] ## shoot ## elif cmd == 's': if not player.arrows: print "You have no arrows." else: player.adjammo(-1) rlist = cmdin.glist(cmdin.curtok[1:]) if len(rlist) > parameters[ARRNG]: rlist = rlist[:parameters[ARRNG]] arr_loc = player.location while arr_loc > -1 and rlist and not stopreason: arr_loc = shoot(arr_loc, rlist[0]) del rlist[0] movewumpi() ## look ## elif cmd == 'l': look(LONG, move_count) ## quit ## elif cmd == 'q': stopreason = "quit" ## save to File ## #elif cmd == 'f': # # savefilename = # rlist = cmdin.glist(cmdin.curtok[1:]) # if len(rlist): # savefilename = cmdin.curtok[1:] # # if no name, use DEFAULT_SAVEFILE # else: # savefilename = DEFAULT_SAVEFILE # dot = string.find(savefilename, '.') # # # if no extension, use SAVE_EXT # if dot == -1: # savefilename = savefilename + SAVE_EXT # # # save parameters, player, room_list, number_wumpi and # # move_count # pickle.dump( (parameters, # player, # room_list, # number_wumpi, # move_count), open(savefilename, "w")) # # stopreason = "save" ## help ## elif cmd == '?': print help_msg move_count = move_count - 1 else: print "I don'" + 't know what "%s" means.' % cmdin.curtok if rnum(3): # 3 mistakes for the price of 2! move_count = move_count - 1 # display info and end game if appropriate if stopreason == "victory": print "

You won the game in %d moves!

" % move_count elif stopreason == "killed": print "

You lose.
" if number_wumpi > 1: print "There were %d wumpi remaining." % number_wumpi if number_wumpi == 1: print "There was only one wumpus remaining." print "

" elif stopreason == "quit": print "

Suit yourself, coward.
" if number_wumpi > 1: print "There were %d wumpi remaining." % number_wumpi if number_wumpi == 1: print "There was only one wumpus remaining." print "

" elif stopreason == "save": print "

Be seeing you.

" else: # we aren't stopping if not cmd == 'l' and not cmd == 'm': look(SHORT) if stopreason: newmazeform() # newmazeform() ends the html page sys.exit(0) ## #### save our new state back to the web form ## # place bats, pits and wumpi, shrinking the empties list as we go print ' ' % pack_thing_locs() # store ammo, wumpus count, move, location & maze for fld, curval in ((FN_AMMO, player.arrows), (FN_NWUM, number_wumpi), (FN_MOVE, move_count), (FN_LOCA, player.location), (FN_MAZE, maze)): if type(curval) != type("string"): curval = str(curval) print ' ' % (fld, curval) # create the visible part of the form where the user fills in their next move print "

What's your next move?" print " " % (FN_CMND, FN_CMND) print "

" print """ """ # finish the web page and exit endbody()
Examples:
    l look around
    m 3    move to room 3
    m010 move to room 010
    m1 5 6  move through rooms 1 and 5 to room 6
    s 7 17 18 0  shoot an arrow along the path 7 --> 17 --> 18 (arrow paths end with a 0)
    q  quit (only for the meek)