#********************************************************************* # Python Adventure Writing System # Game Engine # Written by Roger Plowman (c) 1998-2001 # # This module contains the constants and classes required to create # the PAWS runtime system. This includes the game engine, parser, # global variables, and a *very* basic "thing" object and verb object. # # It assumes some sort of library will be layered on top of it before # games are developed. By default this library is Universe. # # Written By: Roger Plowman # Written On: 07/30/98 # #********************************************************************* #====================================================================== # Standard Imports #====================================================================== # We need some standard Python libraries. These aren't part of PAWS, # rather they're supplied as part of Python. It's good programming # practice not to "re-invent the wheel" when you don't have to, so # always look for a pre-written library to do your work for you! import os # Operating System specific functions import pickle # picking functions (for save/restore) import string # String handling functions import sys # System related functions import types # variable type identifiers import whrandom # Random # generator & functions #=========================================================================== # PAWS Contants #=========================================================================== # The following contants may be considered "global", that is, they are # usable anywhere in your game. #------------------ # Boolean Constants #------------------ # Notice that TRUE and SUCCESS are synonymous, as are FALSE and FAILURE. # The reason we use two different sets of words is to make the program # easier to read. # # TRUE and FALSE are used to set variables or conditions. SUCCESS and # FAILURE are used to test the success or failure of methods and # functions. TRUE = 1 # Test or condition is true SUCCESS = TRUE # Function was successful TURN_ENDS = TRUE # Verb Action causes turn to end FALSE = 0 # Test or condition is not true (it's false) FAILURE = FALSE # Function was NOT successful, it failed TURN_CONTINUES = FALSE # Verb Action doesn't end current turn TEXT_PICKLE = FALSE # Argument for pickle.dump(), file is stored as text BINARY_PICKLE = TRUE # Argument for pickle.dump(), file is stored as binary SHALLOW = TRUE # Shallow refers to displaying only the first layer of # a container's contents, regardless if nested containers # are transparent or open. #----------------- # Daemon Constants #----------------- # This are used to identify which kind of automatically running program # (daemon, fuse, or recurring fuse) is being examined. Used mainly by # the RunDaemon() function. # # Daemons run every turn once activated, fuses delay for a given number of turns, # run once, then don't run again. Recurring fuses happen every X turns. DAEMON = 0 FUSE = 1 RECURRING_FUSE = -1 #--------------------- # Game State Constants #--------------------- # These constants define the various states the game can enter. STARTING # means the game is either starting for the first time or restarting to the # very beginning (because the player typed "restart"). # # RUNNING means the game is running normally, accepting input from the # player and processing commands. # # FINISHED means the player is quitting the game. Any command that sets # the game status to FINISHED should cause the TurnHandler to fail, which # will immediately terminate the game loop and cause the PostGameWrapUp() # method to execute, just prior to ending the game and shutting down Python. STARTING = 1 RUNNING = 2 FINISHED = 3 #------------------ # Pronoun Constants #------------------ # These constants are used as keys into the PronounDict dictionary. They make # it easy to remember the numeric keys and make the code clearer. IT = 0 THEM = 1 HIM = 2 HER = 3 #------------------------- # Verb Allowance Constants #------------------------- # These constants are used to tell the verb object which "style" of # direct/indirect objects to expect. By default the verb is in state 3 # which expects no direct or indirect objects. This is part of the # disambiguation process. ALLOW_NO_DOBJS = 1 # Allow No Direct Objects ALLOW_NO_IOBJS = 2 # Allow No Indirect Objects ALLOW_ONE_DOBJ = 4 # Allow 1 Direct Object ALLOW_ONE_IOBJ = 8 # Allow 1 Indirect Object ALLOW_MULTIPLE_DOBJS = 16 # Allow Multiple Direct Objects ALLOW_MULTIPLE_IOBJS = 32 # Allow Multiple Indirect Objects ALLOW_OPTIONAL_DOBJS = 64 # Allow Optional Direct Objects #--------------------------------- # All asIF visual editor constants #--------------------------------- # asIF is a visual editor for PAWS programs. # We also define a constant, AsIf_TrueFalseType. AsIF needs to distinguish # between true/false values (which are presented with a checkbox) and # numbers (which are presented with an entry field) -- but this is a # distinction Python doesn't make on its own. So we define the constant # TrueFalseType with an arbitrary value, whose sole purpose is to tell # AsIF to use a checkbox rather than an entry field. AsIF_TrueFalseType = 1 #=========================================================================== # Utility Functions #=========================================================================== # These functions are of general use for PAWS, Universe, and the game # author themselves. #----------------------- # Append Dictionary List #----------------------- # This routine doesn't return a value, it appends a value to a dictionary # of lists. A Dictionary List is just a dictionary who's values are # actually lists. For example, the VerbsDict is a list dictionary, it # might look like this: # # {'look': [LookVerb, LookIntoVerb, LookThroughVerb], # 'quit': [QuitVerb], # 'exit': [QuitVeb] # } # # You supply the dictionary name, the key, and the value you want to # append. The key must be a string, either a single value (without any # commas) or multiple values seperated by commas. For instance: # # AppendDictList(NounsDict,"rock,stone",SmallRock) # # This would place SmallRock in the dictionary under two keys, # "rock", and "stone". def AppendDictList(Dict,Key,Value): """Appends value to a list dictionary""" #------------ # Massage Key #------------ # We have to massage the key to make it easier to work with. The # first thing we do is force the key strings to lower case # (string.lower), then split the comma delimited string into a # list of strings (string.split). # # Since this function is mainly used to add words to the verb, noun, # preposition and adjective dictionaries. We want to make sure # the developer doesn't have to worry about case sensitivity when # defining verbs and objects. WordList = string.split(string.lower(Key),",") #----------------------------- # For each word in the list... #----------------------------- # For each word in the wordlist (dictionary key) do the following... for word in WordList: #---------------------------- # Word in dictionary already? #---------------------------- # If the word is already in the dictionary we append the value # to the entry. This has the effect of adding Value to the # list of values already filed under the key. # # If the word ISN'T in the dictionary, we add the LIST of Value # to the dictionary. Notice how the Value is surrounded by # square brackets? # # This is a 'casting' trick. It forces value (which is generally # one item) to become a list of one item. This is important # because append only works with a list. If we didn't convert # Value to a list the second object added to the same key value # would cause the game to blow up with an error message. if Dict.has_key(word): Dict[word].append(Value) else: Dict[word] = [Value] #------- # Choose #------- # This function is handy for use inside Curly Brace Expressions (CBE's). # Decision must evaluate to true or false (0 or 1, empty or full, etc). If # True then TrueChoice will be returned, else FalseChoice will be returned. def Choose(Decision,TrueChoice,FalseChoice): """Ternary IIF operator, returns TrueChoice or FalseChoice based on Decision""" if Decision: return TrueChoice else: return FalseChoice #============= # Clear Screen #============= def ClearScreen(): """prints Global.MaxScreenLines lines (to clear the screen)""" for I in range(1,Global.MaxScreenLines): print Global.CurrentScreenLine = 1 #--------- # Complain #--------- # Because the action of printing a message and returning FAILURE is so # prevalent in the game (complaining) we write a simple function to make # the complaining much simpler to read. Note that TURN_CONTINUES and FAILURE # are actually the same value (0). def Complain(Text=""): """Call Say(Text) and return FAILURE""" Say(Text + " ~n") return TURN_CONTINUES #------------ # Debug Trace #------------ # This function lets you put debug tracing into the system and turn it # on or off with one switch, Global.Debug. Set it to TRUE or FALSE. def DebugTrace(Text): if not Global.Debug: return print Text #------------------------- # Debug Direct Object List #------------------------- # This function is used to list the contents of the (parsed) list of # direct objects associated with the current command. It's intended # to help debug the parser and verbs that use direct objects. def DebugDObjList(): for Object in Global.CurrentDObjList: DebugTrace("-->" + Object.Get(SDesc)) #--------------------------- # Debug Indirect Object List #--------------------------- # This function is used to list the contents of the (parsed) list of # indirect objects associated with the current command. It's intended # to help debug the parser and verbs that use indirect objects. def DebugIObjList(): for Object in Global.CurrentIObjList: DebugTrace("Debug-->" + Object.Get(SDesc)) #--------------------------- # Debug Passed Object List #--------------------------- # This function is used to list the contents of the (passed) list of # objects preceeded by Msg. It's intended to help debug the parser and verbs # that use objects. def DebugPassedObjList(Msg,ObjList): DebugTrace(Msg) for Object in ObjList: DebugTrace("-->" + Object.SDesc()) #----------------------- # Delete Dictionary List #----------------------- # This function deletes an object from a dictionary list (a dictionary who's # values are lists) and replaces it. def DeleteDictList(Dictionary,Object=None): """Delete Object From Dictionary List""" #------------------------- # Return if no real object #------------------------- # If Object is None then there's nothing to do. if Object == None: return #--------------------------- # Return If Dictionary Empty #--------------------------- # If the dictionary is empty there's nothing to do either. if len(Dictionary) == 0: return #------------------------------ # For each key in dictionary... #------------------------------ # For each key in the dictionary we retrieve the value, which should be # a list. If it isn't we skip this key. for key in Dictionary.keys(): #------------------------------ # Retrieve List From Dictionary #------------------------------ # List is the value of the dictionary key. For example: # [LookVerb, LookIntoVerb, LookUnderVerb]. List = Dictionary[key] #--------------------- # Continue If Not List #--------------------- # If this dictionary key isn't a list, we skip this key and continue # the for loop, getting the next key. if type(List) <> type([]): continue #------------------------- # Remove Object If In List #------------------------- # If object is in list, remove it. If the resulting list is 0 length the # entire dictionary entry should be deleted, otherwise the current # dictionary entry will be replaced with the new List. if Object in List: List.remove(Object) if len(List) == 0: del Dictionary[key] else: Dictionary[key] = List #------------------------------ # Delete Object From Vocabulary #------------------------------ # PAWS requries the use of a pre-written library. This library (normally # Universe) creates lots of objects, usually verbs, that sometimes need to be # overridden. To do this you need to remove all references from the vocabulary # dictionaries of the old objects (so Python can "garbage collect" them) before # replacing them with your new definition. # # This function deletes the object from the Verb, Preposition, Noun and # Adjective parser dictionaries so you can either disable the verb or replace # it with your own. It also works for objects. def DeleteObjectFromVocabulary(Object): """Deletes Passed Object From All Vocabulary Dictionaries""" DeleteDictList(Global.VerbsDict,Object) DeleteDictList(Global.PrepsDict,Object) DeleteDictList(Global.NounsDict,Object) DeleteDictList(Global.AdjsDict,Object) #------------------ # Disambiguate List #------------------ # This function actually figures out which object the player meant. It does # so by testing the object with the passed TestMethod. If the result is true # the object is kept, if false it's discarded. # # When all objects in the list have been discarded (because none of them # return true) the function prints the ErrorMethod. # # This function returns either a single object (if it can be disambiguated), # an empty list (if no objects pass the test), or a list of objects that # do pass the test. def DisambiguateList(List,TestMethod,ErrorMethod,Actor=None): """Actual Disambiguation routine""" #------------------------- # Return List Starts Empty #------------------------- # Our return list starts empty because we're going to be appending # to it. ReturnList = [] #----------------------- # Set Last Tested Object #----------------------- # If ALL of the objects in List fail the test we need to use the last # object tested as error method argument. Unfortunately the Object # variable set in the FOR loop disappears when the loop is completed. # # LastTestedObject saves the last object tested by the loop. Under certain # circumstances, that object may be None, which would cause the ErrorMethod # to crash if used as an argument, which is why we have to test for it. # # Setting LastTestedObject to None now is good programming practice, it # gives the variable existance now and sets it to a known value. LastTestedObject = None #--------------------------- # For Each Object in List... #--------------------------- # We're guaranteed that List will always be a list, since this function # is only called for ambiguous object lists. for Object in List[:]: #----------------------- # Set Last Tested Object #----------------------- LastTestedObject = Object #---------------- # Actor involved? #---------------- # Some test methods require an actor AND an object, while some dont. # IsReachable(), for example needs to calculate the path between two # objects. If an actor is needed for the test method, it will be # passed to DisambiguateListOfLists which in turn passes it to this # function. if Actor == None: TestResult = TestMethod(Object) else: TestResult = TestMethod(Object,Actor) #-------------------- # Object passes test? #-------------------- # If the object passes the test we append it to the ReturnList. If # it doesn't... # # In that case we delete it from the List. If the List loses EVERY # member we've eliminated all objects, so we say the error method # of the object we just eliminated. if TestResult == TRUE: ReturnList.append(Object) DebugTrace(" "+Object.SDesc() + " passed") else: List.remove(Object) DebugTrace(" "+Object.SDesc() + " failed") #------------------------ # No Items in ReturnList? #------------------------ # If there are no items in the return list we need to use the last object # tested and print the error condition, since every object failed! We then # return an empty list. if len(ReturnList) == 0: Say(ErrorMethod(LastTestedObject)) return [] #-------------------------------- # Exactly One item in ReturnList? #-------------------------------- # If there is exactly one item on the return list we're finished! # Instead of returning ReturnList we return just the 0'th (first) element # of ReturnList. This returns a single object instead of a list. # # If there are no objects in ReturnList or more than 1 we return the # entire list. if len(ReturnList) == 1: return ReturnList[0] else: return ReturnList[:] #---------------------------- # Disambiguate Lists of Lists #---------------------------- # This function performs "disambiguation" of the kinds of object lists # created by the parser. In other words, it intelligently chooses which # objects are intended when there's a choice. # # For instance, if the game contains three keys, a bone key, a brass key, and # a silver key but only one rock and the player says "get key and rock" the # resulting direct object list looks like: # # [ [BoneKey,BrassKey,SilverKey], [Rock] ] # # Notice the direct object list contains two other lists! Key could refer to # any of the keys, at the time of parsing there's no way to know which is # intended. Thus we say the key is ambiguous. # # Rock isn't of course, since there's only one rock in the game. # # The theory of disambiguation is complex and messy, so pay close attention. # There are three major problems we have to deal with. # # First, we're starting off with a list of lists. It's messy, but the only # way we can handle ambiguous objects. # # The second problem is that of delegation. Since the PAWS engine is intended # to be library independent we have to build an engine that can work with # anyone's library. This means both the test method(s) and the error # method(s) are supplied by the library, along with the specific # disambiguation method itself. # # Which leads to our third problem. We have no way of predicting ahead of # time which tests the library author will want to perform, or how many of # them there will be. The implication is we have to be able to handle # multiple disambiguation passes. # # There's another implication. If we handle multiple passes, then at some # point part of the list will be single objects and part of the list will # be lists! # # For example, let's say that there are two passes. The first tests to see # if the objects are known. (Assume the player doesn't know a silver key # exists in the game). # # The first pass yields: [ Rock, [BoneKey,BrassKey] ] # # Notice Rock is no longer a list, it's a single object. That's because # the aim of disambiguation is to reduce our list of lists to a simple list # of objects, if possible. Since the [Rock] list contains only one item we # convert it to an object. We've eliminated the silver key, but key is # still ambiguous. # # Second pass, are the items reachable? The bone key is in the basement, # but both the rock and the brass key are in the kitchen with the player. # # Thus our second pass yeilds: [ Rock, BrassKey ] and our disambiguation is # done. # # BUT there's a third pass, are all objects visible? Our third pass doesn't # eliminate anything and yields: [ Rock, BrassKey ]. # # And depending on the library you use there might be more passes yet. So # we have to be able to handle all three states of the list. def DisambiguateListOfLists(ListOfLists,TestMethod,ErrorMethod,Actor=None): """Break list of lists into multiple lists for DisambiguateList() function""" #------------------------------------ # Empty list automatically successful #------------------------------------ if len(ListOfLists) == 0: return SUCCESS #------------------------- # Return List Starts Empty #------------------------- # Our return list starts empty because we're going to be appending to it. ReturnList = [] #-------------------------------- # For Each item in ListOfLists... #-------------------------------- # Each item in the list of lists might be a single object (unambiguous) # or it might be a list of objects (ambiguous). The purpose of this loop # is to either directly append an unambiguous object to the return list # or pass an ambigous object list to the DisambiguateList function to see # if it can be disambiguated and append the result. for List in ListOfLists: #--------------------- # Is Object ambiguous? #--------------------- # Remember, "List" will either be a single object (unambigous) or # a list of objects with the same noun (ambiguous). If it is an # ambiguous list we pass it to the disambiguate function, which # will either return a single object if it was able to narrow it # down to one or it will return a (hopefully smaller) list of # objects. In either case we simply append the result to the # ReturnList. # # If the object isn't ambiguous we just append the object to the # return list. if type(List) == type([]): WorkList = DisambiguateList(List,TestMethod,ErrorMethod,Actor) ReturnList.append(WorkList) continue #----------------------- # Not Ambiguous, test it #----------------------- if Actor == None: TestResult = TestMethod(List) else: TestResult = TestMethod(List,Actor) #-------------------- # Object fails test? #-------------------- # If the object passes the test we append it to the ReturnList. If # it doesn't... # # In that case we delete it from the List. If the List loses EVERY # member we've eliminated all objects, so we say the error method # of the object we just eliminated. if not TestResult: DebugTrace(" "+List.SDesc() + " Failed") ListOfLists.remove(List) Say(ErrorMethod(List)) DebugTrace(" "+List.SDesc() + " unambiguous failure") continue DebugTrace(" "+List.SDesc() + " passed") ReturnList.append(List) #------------------- # Remove empty lists #------------------- # From time to time the DisambiguateList() function will eliminate EVERY # object in the list. For example, the player says "get rock" but the # rock isn't here. In that case the DisambiguateList function returns # an empty list -- []. You may have (depending on the situation) many # empty lists in your ListOfLists. # # The line below eliminates empty lists, leaving single items and # object lists untouched. How it does it is something of a Python # mystery, but rest assured it works. ReturnList = filter(None,ReturnList) #--------------------------- # Return New "List Of Lists" #--------------------------- # The result of this function is either a partially disambiguated list # or a completely disambiguated one. The list of lists may be run # through this function again with a different test method to further # disambiguate the list. # Notice the peculiar syntax we use to assign ListOfLists. The reason # we do this is extremely involved, but the short form is that Python # doesn't support "pass by reference". # # Instead it passes a copy of an object reference. ListOfLists isn't # a copy of Global.CurrenDIObjList, for instance, it *points* to it. # # But the statement "ListOfLists = ReturnList" means that instead of # assigning ReturnList to Global.DObjList as you might expect, it # actually changes where ListOfLists is *pointing* to! # # By putting [:] on the end, we change this from pointer assignment to # object reference. Since ListOfLists is still pointing to Global.DObjList # it does what we want it to (replace Global.DObjList with a copy of # ReturnList). ListOfLists[:] = ReturnList[:] if len(ReturnList) > 0: return SUCCESS else: return FAILURE #------------------ # Do (execute) code #------------------ # This function takes a string and tries to turn it into valid Python # code, then execute it. Its main purpose is for use in debugging, to # set variables and the like, but you can execute any valid Python # code fragment with it. Be careful! def DoIt(CodeString): """Takes CodeString and tries to execute it as a Python command""" try: eval(compile(CodeString,"","exec")) except: Say("Syntax Error In CodeString") return "Code Execution Complete" #------------ # Game Daemon #------------ # This function is a DAEMON, a function that will be run automatically every turn # by the game engine. All it does is increment the turn counter, but it # demonstrates how to write a daemon. def GameDaemon(): """Daemon to handle standard game chores""" Global.CurrentTurn = Global.CurrentTurn + 1 #----------------- # Get Player Input #----------------- # This routine takes the player's typed line and turns it into a list # of commands which are placed in Global.CommandsList. def GetPlayerInput(): """Get Player's command.""" #------------------- # Get Player Command #------------------- # InputString holds the string the player typed. Notice the use of # the Prompt function inside the raw_input function. # # Next, translate the InputString into a list of words, and put # that list in TempWordList. InputString = raw_input(Engine.Prompt(0)) TempWordList = string.split(InputString) #------------------------- # Handle Punctuation Marks #------------------------- # So far the list we have contains words seperated by spaces, any # punctuation marks will be on the end of individual words. The # line below makes punctuation marks their own words. TempWordList = HandlePunctuation(TempWordList) #--------------------------------------- # Break Word list into separate commands #--------------------------------------- # Since the player can type muliple commands on a single line # (such as "go west then open door" or just "go west. open door") # we have to make sure we get all commands. #-------------------------------- # For each word in Temp Word List #-------------------------------- # The FOR loop says in English: "for each word in a COPY of # TempWordList do the following... # # We use a copy because in the course of the FOR loop we're going # to completely destroy the real TempWordList. for word in TempWordList[:]: #---------------------- # Is Word a terminator? #---------------------- # Is the current word in the Global.CommandBreaksList? This # list includes all valid ways to end a command. By default # this includes all the valid ending sentence punctuation and # the word "then" (as in "Go west then open door"). Ending # sentence punctuation is assumed to a period, exclamation # point, or question mark. if word in Global.CommandBreaksList: #----------------------------- # Find word's current position #----------------------------- # We're constantly deleting words from TempWordList, so we # need to find the current position of word within the word # list. Item will contain the position of word. Item = TempWordList.index(word) #------------------------------- # Append Command To CommandsList #------------------------------- # The command to append to the commands list starts at the # beginning of TempWordList and runs up to position Item. # The line below will NOT include the actual terminating #word in the appended command. So "go west. Open door" will # append the command ["go","west"] to the command list, but # not include the period. Global.CommandsList.append(TempWordList[:Item]) #------------------------------ # Delete command from word list #------------------------------ # Since we no longer need the command we discard everything # from the beginning of TempWordList through (and including) # the terminating word. # # In English the code below reads: "Set TempWordList to # the remainder of TempWordList from the word after the # terminating word till then end of the temp word list. # # For instance: ["go","west",".","open","door"] becomes # ["open","door"]. TempWordList = TempWordList[Item+1:] else: #----------------------------------------- # Remaining TempWordList has no terminator #----------------------------------------- # This code is part of the FOR/ELSE loop. If we actually # have reached this point it means the FOR loop has reached # the end of the COPY of TempWordList, but there may still be # an extra command in process. Let's examine our example "go # west. open door" # # The copy of TempWordList being processed by the FOR loop # ended without a terminator. Therefore the REAL TempWordList # still contains ["Open" "Door"] # # To handle this situation we just append the entire remaining # TempWordList (in this case ["open","door"] to the commands # list. Global.CommandsList.append(TempWordList) #-------------------------------- # Make Sure last command is valid #-------------------------------- # Consider what Global.CommandList will look like if the player # types "go west." (ie ends with a period. This yields ["go", # "west"],[]] In other words, a bogus last command is introduced # into the list. # # Global.CommandList[-1] means "the last command in the list". If # the last command is [] (ie, nothing) then we want to get rid of # it. We do that by setting the command list to the command list # :-1. # # In English, :-1 means "from the beginning to the end -1. If the # list contained 5 commands, then :-1 would give us the first 4 # commands. if Global.CommandsList[-1] == []: Global.CommandsList = Global.CommandsList[:-1] #------------------------- # Tell caller we succeeded #------------------------- # We don't really need to indicate success or failure, since the # existing code doesn't check, but it's good programming practice # because a game author might rewrite the parser and need to know # if this routine succeeded or failed. return SUCCESS #------------------- # Handle Punctuation #------------------- # This function takes a list of words like: # # ['Joe,','go','west.'] and turns it into: # # ['Joe',',','go','west','.'] # # In other words it takes punctuation at the end of a word and inserts # it into the list at the appropriate places. This makes single # punctuation marks into "words". # # We do this because from a syntax point of view punctuation marks really # *are* seperate words! They add meaning to the sentence, especially for # the computer. def HandlePunctuation(WordList): """Finds punctuation marks at the end of words in a list and inserts them as seperate entities in the appropriate part of the list. """ #------------------------- # Define Punctuation Marks #------------------------- # These are the punctuation marks people are likely to use. We put # them in a single string for convenience, Punctuation could also # have stored a list. Punctuation = ":;!.,?" #---------------------- # For Each Word in List #---------------------- # This is a little complex, so pay attention. FOR is a loop, similar # to WHILE. But FOR acts differently. We know that WordList contains # a list of words. What the FOR loop is saying is: "For each word # in WordList put that word in Word (a variable), and do the # following..." # # A couple of cute (and extremely useful tricks). First, notice we # are actually using WordList[:] instead of simply WordList. What # this does is have FOR use a COPY of WordList, instead of WordList # itself. We need to do that because we're going to be changing # WordList, and we don't want to confuse Python. Got that? We're # changing WordList, we're giving FOR a COPY of WordList. (By the # way, WordList[:] means "WordList from the beginning to the end". for Word in WordList[:]: #--------------------------------- # Word ends in a punctuation mark? #--------------------------------- # You should be familiar with IF tests. This test says "If # the last character of Word is in Punctuation (a variable), # then do the following... # # Let's say the player typed "Joe, go west". The WordList will # be: ['Joe,','go','west']. The first time though the loop Word # will be 'Joe,'. # # To get a single letter of Word we follow it with the letter's # position, starting at 0. Word[0] is 'J', Word[1] is 'o', and # so on. # # But when you use a negative number you're counting from the # END of the string. So Word[-1] is ','. In other words Word[-1] # will ALWAYS be the last letter, no matter how long Word is! # This is the second cute trick I mentioned. if Word[-1] in Punctuation: #---------------------- # Find Word in WordList #---------------------- # We need to know where the word occurs in the list, so # we look it up. The line below sets Item equal to the Word's # position in WordList, starting at 0. Item = WordList.index(Word) #------------------------------------------ # Insert the punctuation mark into the list #------------------------------------------ # We insert the punctuation mark in front of the NEXT word's # position in the list. In our example 'Joe,' is word 0, so # we insert in front of word 1 ('go') WordList.insert(Item+1,Word[-1]) #----------------------------------------- # Replace Word With Word minus punctuation #----------------------------------------- # Last cute trick. WordList[Item] is going to be 'Joe,' # (notice the ending comma). Word[:-1] means "Word up to # but not including the last character". So we replace # "Joe," with "Joe". Neat, huh? WordList[Item] = Word[:-1] #------------------------- # Return Altered Word List #------------------------- # Ok, we've altered the wordlist the way we want it, so now we # return it to the line that called this function. return WordList #------- # Indent #------- # This function merely creates a string of "\t" (tab) characters for each # indent level passed in the argument. For example, Indent(3) returns 3 tab # characters. NOTE: TAB CHARACTERS ARE 3 SPACES, NOT ASCII CODE 9! def Indent(Level): """Returns Level # of ~t's""" RV = "" for x in range(0,Level): RV = RV + " ~t " return RV #-------------------------- # Intersection of two lists #-------------------------- # This function takes two lists and returns a list of items common to # both lists. def Intersect(list1,list2): """Returns intersection of two lists""" ReturnList = [] for x in list1: if x in list2: ReturnList.append(x) return ReturnList #-------------- # In Vocabulary #-------------- # This function returns TRUE if the passed word is in the game's vocabulary, # FALSE if it isn't. def InVocabulary(Word): if Global.NounsDict.has_key(Word): return TRUE if Global.VerbsDict.has_key(Word): return TRUE if Global.AdjsDict.has_key(Word): return TRUE if Global.PrepsDict.has_key(Word): return TRUE if Global.PronounsListDict.has_key(Word): return TRUE if Word in Global.ArticlesList: return TRUE if Word in Global.ConjunctionsList: return TRUE if Word in Global.DisjunctionsList: return TRUE if Word in Global.CommandBreaksList: return TRUE return FALSE #-------------- # Identify Noun #-------------- # This function returns a list of objects identified by the word range # in Global.ActiveCommandList. Objects are identified by zero or more # adjectives preceeding a noun. Other parts of speech are ignored by # this search. # # It is very possible for this routine to return a list within a list. # # For example, let's say the phrase was "stone". This is a noun, and # could apply to any of three objects, a small grey stone, a boulder, # or a large blue rock. In this case our list is only 1 item long--but # that item is a list of 3 objects! # # Normally the player types something like "Get stone, lamp, and knife". # This would return (assuming there's only one knife and lamp object): # # [Knife,Lamp,[SmallRock,Boulder,BlueRock]] # # This is a list of 3 items, Knife, Lamp, and the list [SmallRock, # Boulder,BlueRock]. The fact we have a list means the verb itself may # have to disambiguate further. For example, it makes little sense to # try and dig a hole with a lamp, right? def ParserIdentifyNoun(StartPos,EndPos): """Returns a list of objects identified by nouns in Global.ActiveCommandList """ #------------------- # Create Empty Lists #------------------- # We have to create lists for one object's adjectives and nouns. We # use a list for nouns (even though an object only has one) because # it's possible for more than one object to have the same noun. # # The Return list returns all objects found (the command list might # have listed multiple objects). AdjectiveList = [] NounList = [] ReturnList = [] #---------------------------------- # Loop through each word in command #---------------------------------- # In English: "For each word in the Active command list from the # starting postion up to (but not including) the ending position, # do the following..." for word in Global.ActiveCommandList[StartPos:EndPos]: #------------------- # Word is adjective? #------------------- # If the word is an adjective, look up all objects that have # that adjective and append them to the adjective's list. if Global.AdjsDict.has_key(word): AdjectiveList = Union(AdjectiveList,Global.AdjsDict[word]) DebugTrace(word + " is Adjective") #-------------- # Is word noun? #-------------- # When the word is a noun we basically append all objects that # have that noun, then (if there were adjectives) intersect the # noun and adjective list. Then we append all the objects in # the trimmed down NounList to the ReturnList (the list of # objects returned by this function). if Global.NounsDict.has_key(word): DebugTrace(word + " is Noun") NounList = Union(NounList,Global.NounsDict[word]) if len(AdjectiveList) > 0: NounList = Intersect(NounList,AdjectiveList) ReturnList.append(NounList) AdjectiveList = [] NounList = [] #-------------------------- # Return Found Objects List #-------------------------- # Now it's time to return the list of objects we parsed from the # section of the active command list. Note it's very possible for # this function to return an empty list. return ReturnList #=============================== # Run Existing Daemons And Fuses #=============================== # This function runs all daemons and fuses in Global.DaemonDict. It # does all the scheduling for fuses and recurring fuses. def RunDaemons(): """Runs all daemons/fuses in Global.DaemonDict""" #--------------------- # For Each Daemon/Fuse #--------------------- # Global.DaemonDict.keys() returns a list of the keys in the # dictionary. Each key is a function reference so DaemonFuse, # in addition to being the dictionary key for this particular # entry in Global.DaemonDict is also an indirect reference to # a function. That means the expression DaemonFuse() will actually # run the appropriate function! for DaemonFuse in Global.DaemonDict.keys(): #------------------------- # Get Original Fuse Length #------------------------- # Remember, the original fuse length will be a negative, 0, or # positive number. It's stored in the dictionary and is the # original fuse length. If negative the remaining turns will # be reset to the absolute value of this number. FuseLength = Global.DaemonDict[DaemonFuse][1] #------------------- # Get Remaining Time #------------------- # Get the remaining time. Note this is the time *before* the # fuse is reduced for the current turn. For example, if the # value was 1, then the fuse will execute THIS PASS, since # 1 minus 1 is 0. However, if the value were 2 it would # execute NEXT pass, since 2 - 1 is 1, not 0. RemainingTime = Global.DaemonDict[DaemonFuse][0] #--------------------- # Identify Daemon Type #--------------------- # Any function in Global.DaemonDict can be either a daemon, # a fuse, or a recurring fuse. A daemon runs every turn, # a fuse runs after X turns delay and is then removed from # DaemonDict, a recurring fuse runs after a delay of X turns # but is then reset to run in another X turns. if FuseLength < 0: DaemonType = RECURRING_FUSE if FuseLength == 0: DaemonType = DAEMON if FuseLength > 0: DaemonType = FUSE #----------------- # Handle If Daemon #----------------- # If the function we're examining is a daemon (has an # original fuse length of 0) then it's supposed to run every # turn. So we run it and continue, which skips the rest of # the FOR loop. Note we DO NOT return! If we returned then # only the first daemon/fuse in the dictionary would execute. if DaemonType == DAEMON: DaemonFuse() continue #---------------------- # Reduce Remaining Time #---------------------- # Since we know the function being examined is either a fuse # or a recurring fuse, we shorten the remaining time by 1. We # know we aren't dealing with a daemon because daemons are # handled above, we'd never have gotten here if it was a # daemon. # # Notice we reduce RemainingTime, then assign it back to the # dictionary. We use RemainingTime because it makes the code # easier to read and understand. RemainingTime = RemainingTime - 1 Global.DaemonDict[DaemonFuse][0] = RemainingTime #------------------------- # Continue If Time Remains #------------------------- # If there's still remaining time (RemainingTime is more than # 0) then we need do nothing further for either fuses or # recurring fuses, since they haven't "gone off" yet. if RemainingTime > 0: continue #---------------------- # Execute Fuse Function #---------------------- # If we've gotten this far it means the Remaining Time has # been exhausted, it has reached 0. Remember than DaemonFuse # contains an indirect function reference. All we have to do # to execute the function is put parentheses after it, as we # do in the code below. If DaemonFuse contains a reference to # the ClearScreen function, for instance, then # # DaemonFuse() # # would be identical to: # # ClearScreen() DaemonFuse() #---------------------- # Handle If Normal Fuse #---------------------- # If we're dealing with a regular fuse we simply call # StopDaemon and pass it DaemonFuse, which is the indirect # reference to the function we just executed. This removes it # from Global.DaemonDict. Then we continue, to skip the rest # of the FOR loop. if DaemonType == FUSE: StopDaemon(DaemonFuse) continue #------------------------- # Handle If Recurring Fuse #------------------------- # If dealing with a recurring fuse (one that resets itself # after the function is executed) then we reset RemainingTime # to the absolute value of FuseLength. This turns the negative # FuseLength into a positive value for Remaining time. # # Then we set the 0'th (first) element of the dictionary to # the remaining time. Doing it this way makes the code easier # to understand than the equivalent single line: # # Global.DaemonDict[DaemonFuse][0] = abs(FuseLength) if DaemonType == RECURRING_FUSE: RemainingTime = abs(FuseLength) Global.DaemonDict[DaemonFuse][0] = RemainingTime continue #----------------- # Return To Caller #----------------- # Notice we're returning, but we aren't returning a value to # the caller. This is reasonable, since this function just runs # daemons, and really can't say much about their statuses. return #---------------------- # Say print replacement #---------------------- # Because the print statement isn't particularly intelligent when it comes to # printing we'll have to create our own. This function makes sure that when # a word is printed it doesn't "wrap" from the right edge of the screen to # the left edge. # # In addition, if a single piece of text is printed that exceeds the number # of screen lines available, a [--more--] capability allows all text to be # read before it scrolls of the end of the screen. # # Note this function interprets no "\" characters (\n, \t, etc) but it does # recognize two commands, ~n and ~m. These must be space seperated from # the words around them. # # ~n causes a \n line break. ~m forces a "[-- more --]" message and the # screen to pause. ~n lets you format text and ~m lets you pause the # printing exactly where you want to. def Say(Text=""): """Replacement print statement""" #--------------- # Make Shortcuts #--------------- # These variables are just shortcuts to make the code shorter and easier # to read. LP means Lines Printed. If Lines printed for this piece of # text exceed Max screen lines - 1 then we pause printing to allow the # player to read the screen. MSL = Global.MaxScreenLines MSC = Global.MaxScreenColumns CSL = Global.CurrentScreenLine CSC = Global.CurrentScreenColumn LP = 0 LFAfter = FALSE #------------------ # Ignore Empty Text #------------------ # If Text is empty (nothing) then return without doing anything. if Text == "": return #------------------------- # Translate {} Expressions #------------------------- Text = Engine.XlateCBEFunction(Text) #----------------- # Create Word List #----------------- # This will break the text into a list of words which we can then use # a FOR loop on. WordList = string.split(Text) #--------------------------- # For each word in Word List #--------------------------- for Word in WordList: #------------------- # Replace ~n with \n #------------------- Word = string.replace(Word,"~n","\n") Word = string.replace(Word,"~t"," ") #------------------------------- # If it starts with a line break #------------------------------- # If the first two characters of Word are \n this means a newline # character will be printed, followed by the word. # Increment the current line # by one but don't exceed the screen # length, increment the number of lines printed and reset the # current screen column to 1. if Word == "\n": print CSL = min(CSL + 1,MSL) LP = LP + 1 CSC = 1 Word = "" #------------------------ # Screen Length Exceeded? #------------------------ # If the lines printed are about to exceed screen length then # use the raw_input function to pause. Notice we don't assign the # return value to anything, it's discarded. # # Once the player presses Enter we set lines printed to 0. if LP > MSL - 1 or Word == "~m": raw_input("[--more--]") LP = 0 CSL = 1 continue #------------------------- # Will Word Fit on screen? #------------------------- # If the word fits on the current line print it then increment the # current screen column by the length of the word and again by 1. # Each time a word is printed Python automatically puts a space after # it. Then of course the word is printed to the screen. # # If the word won't fit on the current line print a blank line, # print the word, then set the current screen column to 1, incremnt # the screen line (but don't let it go past the end of screen), and # increment the number of lines printed. if CSC + len(Word) < MSC: if len(Word) > 0: print Word, CSC = CSC + len(Word) + 1 else: print print Word, CSC = 1 + len(Word) + 1 CSL = min(CSL + 1,MSL) LP = LP + 1 #------------------------ # Update Global Variables #------------------------ # Now we update our CurrentScreenLine and CurrentScreenColumn global # variables. Global.CurrentScreenLine = CSL Global.CurrentScreenColumn = CSC #-------------- # Sentence Case #-------------- # This function acts like lower except it capitalizes the first letter of # the passed argument.Note it ALSO strips leading whitespace before # capitalizing the string! def SCase(Sentence): """Returns argument with first character upper case & rest lower case.""" if Sentence == None: return Sentence if type(Sentence) <> type(""): return Sentence NewSentence = string.lstrip(Sentence) return string.capitalize(NewSentence[0]) + NewSentence[1:] #------- # Self() #------- # Because "self" is actually only a method argument defined by # classes it can't be used in curly-brace expressions (which are # evaluated by a function). Therefore we store the "current object" # (self, in other words) in Global.CurrentObject. Because that's such # a long string, we define this function to make it easier to use. # # Therefore instead of saying something like: # # {Global.CurrentObject.TheDesc()} # # we can use the shorter: # # {Self().TheDesc()} # # They mean exactly the same thing, but Self() is a lot easier to # read AND type! # # The complement to this function is the MakeCurrent() method which # is called as part of the parsing process. def Self(): return Global.CurrentObject #------------------------ # Remove list2 from List1 #------------------------ # Returns a list containing list1 with items in list2 removed. def SetRemove(list1,list2): """Returns list1 with list2 removed.""" ReturnList = list1[:] for x in list2: if x in ReturnList: ReturnList.remove(x) return ReturnList #======================= # Start A Daemon Running #======================= # This function takes the function reference in DaemonFuse and adds it # to Global.DaemonDict. It sets the Remaining Turns appropriately as # well. # # Note FuseLength is optional, if not supplied it will default to 0, # which means run the daemon every turn. # # This function returns SUCCESS unless DaemonFuse isn't a funciton, # in which case it returns FAILURE. def StartDaemon(DaemonFuse,FuseLength = 0): """Adds a daemon/fuse to Global.DaemonDict""" #------------------------------------ # Fail If DaemonFuse isn't a function #------------------------------------ # Daemons *must* be functions, they can't be methods, strings, or # anything else. if type(DaemonFuse) <> type(StartDaemon): return FAILURE #------------------------- # Append/Update DaemonDict #------------------------- # If DaemonFuse is already in Global.DaemonDict then the remaining # turns and fuse length will be *updated*. This lets you change a # daemon into a fuse or vice versa, or reset a fuse's activation # time. # # If DaemonFuse isn't in DaemonDict it will be added. Since both # cases are handled by a single simple line of code, you can # appreciate just how powerful dictionaries are! Global.DaemonDict[DaemonFuse] = [abs(FuseLength), FuseLength] #--------------- # Return SUCCESS #--------------- return SUCCESS #====================== # Stop A Daemon Running #====================== # This function takes the function reference in DaemonFuse and removes # it from Global.DaemonDict. Obviously, this will stop the daemon/fuse # from running. # # This function returns SUCCESS unless DaemonFuse isn't a funciton, # in which case it returns FAILURE. It will also return FAILURE if # DaemonFuse isn't currently in the dictionary. def StopDaemon(DaemonFuse): """Removes a daemon/fuse from Global.DaemonDict""" #------------------------------------ # Fail If DaemonFuse isn't a function #------------------------------------ # Daemons *must* be functions, they can't be methods, strings, or # anything else. if type(DaemonFuse) <> type(StartDaemon): return FAILURE #--------------------------------------- # Fail If DaemonFuse Isn't In DaemonDict #--------------------------------------- # Return FAILURE if DaemonFuse isn't on the list. This allows us # a silent but testable way to see if the daemon was removed, or # wasn't actually on the list. if not Global.DaemonDict.has_key(DaemonFuse): return FAILURE #----------------------------------------- # Remove DaemonFuse From Global.DaemonDict #----------------------------------------- del Global.DaemonDict[DaemonFuse] #--------------- # Return SUCCESS #--------------- return SUCCESS #------------------- # Union of two lists #------------------- # Returns a list containing list1 and all items in list 2 that weren't # already in list 1. def Union(list1,list2): """Returns union of two lists""" ReturnList = list1[:] for x in list2: if not x in ReturnList: ReturnList.append(x) return ReturnList #=========================================================================== # Default Handler Functions For Engine #=========================================================================== # You'll probably replace most of these, except for the parser. #--------------------------- # Default After Turn Handler #--------------------------- # This routine is intended for actions (like daemons) that should occur # after a given amount of time has elapsed, or after a successful # player's turn, which is assumed to take a few minutes. Note this # routine is only called when the Turn handler routine above returns # SUCCESS. # # This allows you the ability to control which commands count as a turn # and which ones do not. def default_AfterTurnHandler(): """Default routine for handling after turn stuff, like daemons""" RunDaemons() #********************************************************************* # PAWS Game Skeleton # Written by Roger Plowman (c) 1998-2001 # # #********************************************************************* def default_GameSkeleton(): """Default game logic loop""" #========== # Game Loop #========== # The basic logic in IF games is simple. Get a command from the # player, figure it out, do it, then tell the player what # happened. Repeat until the player quits the game. while Global.GameState != FINISHED: #----------------------- # While Game is STARTING #----------------------- # The first time this loop executes GameState will be # STARTING so we set up the Game and set GameState to # RUNNING. if Global.GameState == STARTING: Engine.SetUpGame() Global.GameState = RUNNING #---------------------- # While Game is RUNNING #---------------------- # While the game is running (which it will do until something # sets the GameState to FINISHED) we call the # pre-turn handler then the parser. # # If the parser executes successfully we then execute the # turn hanlder, and if *that* succeeds then we execute the # after turn handler. # # The implication is that if the parser returns FAILURE # (which it might do if the command wasn't understood, the # pre-turn handler will *still* execute. Another implication # is that you can deliberately cause the turn-handler to fail # and thus avoid running the after turn handler. This gives # you better control. if Global.GameState == RUNNING: Engine.PreTurnHandler() if Engine.Parser(): if Engine.TurnHandler(): Engine.AfterTurnHandler() #============= # Wrap Up Game #============= # Once the game is over (the loop ended because Global.GameState # was set to FINISHED) we call the PostGameWrapUp method to print # a closing message, or whatever's appropriate to your game. Engine.PostGameWrapUp() #=============== # Shut down game #=============== # Finally, we quit the Python interpreter, which shuts down the # game and returns us to the operating system. sys.exit() #*************************************************************************** # END OF PAWS GAME SKELETON #*************************************************************************** #-------------- # Default Parser #--------------- # This is the big cheese as far as most developers are concerned. It # handles translating the command the player typed into recognizable # objects so the parser can then hand off execution to the objects in # question. # # It's reasonably intelligent, it handles multiple commands on a single # line, has the ability to handle multiple direct and indirect objects, # can handle preparsing and a fair degree of disambiguation, ala TADS. # # The parser can return SUCCESS or FAILURE. If it returns FAILURE # it means the command wasn't understood, SUCCESS means the command # was understood and parsed into objects that TurnHandler() can deal # with. #}}} def default_Parser(): """Default parsing routine. Almost never overridden""" #-------------- # Get a command #-------------- # The first thing we have to do is get a command to parse. That # isn't as easy as it sounds, for two reasons. First, the player # can type multiple commands on a single line, such as # "East, then open door" This is two commands "East" and "Open door". # # Second, if the player does type multiple commands on a single # line then the *game loop* has no way to tell if the player typed # the commands on one line or several, so the parser has to deal # with it. # # Once the player types a string it is broken down into one or more # commands which are put in Global.CommandsList. If there aren't # any commands in CommandsList that means we need to go back to the # player for another string. while len(Global.CommandsList) == 0: GetPlayerInput() #-------------------------- # Get Active (next) command #-------------------------- # The parser places the words from the first command in the list the # player typed (Global.CommandsList[0]) into the list the parser # actually uses to do the parsing (Global.ActiveCommandList). Global.ActiveCommandList = Global.CommandsList[0] #----------------------------- # Delete it from commands list #----------------------------- # Now that we've saved the command we're interested in, we delete # it from the CommandsList. Doing this does two things. First, it # lets us write simpler code (ALWAYS desirable) and it also allows # the parser to know when the CommandsList is empty so the parser # can ask the player to type in more. del Global.CommandsList[0] #---------------------------- # Delete Trailing Conjuctions #---------------------------- # Because of the nature of English, it's very possible for the # player to type in "go west and then open door". This makes our # active command list ['go','west','and'], which doesn't make much # sense. So we need to make sure the last word isn't a conjunction. # To gracefully handle parser abuse (like "go west and and then # open door" we use a while loop which will whittle off as many # trailing conjunctions as might prove necessary. # # Translated, the while loop says "While the last word is a # conjunction, set the ActiveCommandList to the ActiveCommandList # minus the last word." while Global.ActiveCommandList[-1] in Global.ConjunctionsList: Global.ActiveCommandList = Global.ActiveCommandList[:-1] #----------------------------- # Pre-parse the active command #----------------------------- # Now that we have winnowed a single command from the command list # we're ready to apply any pre-parsing rules to the command. if not Engine.PreParse(): return FAILURE #=================== # Break Down Command #=================== # English is a difficult language to parse. Fortunately, all we have # to parse are commands, which, while difficult, always follow a # consistant format. # # The format is: # # [Actor] VERB [direct object(s)] [preposition] [indirect object(s)] # # (We're going to ignore the inconvenient form "VERB [direct objects] # [Actor]" since it's too difficult to handle cleanly and most # people use the first form anyway...) # # All items in square brackets are optional, which means the only # item required in the command is a verb! # # This suggests the strategy the parser uses to break down the # command. First it scans the command for a verb. If it doesn't find # a verb, there's nothing more to be done, we complain and return # a FAILURE. # # Assuming we find a verb we save its location. The next thing we do # is try and find one or more prepositions, saving the location of # the first one we find. # # The locations of the verb and first preposition are landmarks in # the command. # # Consider the command: "John, dig the hole with the shovel." Since # "John" preceeds the verb, we know that John must be the actor, # you can't put anything else in front of a verb. # # Likewise we know "with" is a preposition. Therefore "the hole" is # a direct object. # # And "the shovel" is the indirect object. Translate those # (see below) and the parser's job is finished. # # There's one special case we need to look at. That's a command # where the preposition immediately follows the verb, which makes # the command's indirect object into the direct object. # # For example: "Look into chest" # # Left to itself, chest would become the command's indirect object. # However, all we have to do when the first preposition immediately # follows the verb is copy the indirect object list into the # direct object list and then zap the indirect object list. #---------- # Find Verb #---------- # A command can only have one verb in it, so break out of the FOR # loop when we find it. If we don't find it complain and return # FAILURE. # # Now to translate the following code into English. First line: # "Set PotentialVerbList to an empty list (there aren't any). # # "For each word in Global.ActiveCommandList, do the following" # # "If the current word is a verb, then set VerbLocation to the # current word's position and then append the VERB'S OBJECT (NOT # the word!) to the PotentialVerbList. (We now have one verb # object). Since a command may only have one verb we break the # FOR loop immediately, stopping our scan. # # If none of the words in the ActiveCommandList are verbs then # we print a complaint ("That sentence doesn't contain a verb!") # and immediately return a FAILURE code. This causes the game loop # to call the parser again, so the next command can be processed. PotentialVerbList = [] for word in Global.ActiveCommandList: if Global.VerbsDict.has_key(word): Global.CurrentVerbNoun = word VerbLocation = Global.ActiveCommandList.index(word) PotentialVerbList = Union(PotentialVerbList,Global.VerbsDict[word]) break else: return Complain(ParserError.NoVerb) #----------------- # Find Preposition #----------------- # If we get this far we found a verb, now we have to find its # prepositions (if any). We start at the verb's location and # move to the end of the active command list. If we don't find # one the preposition location remains the verb's location. # # This code is similar to the verb code above, except as noted # below. # # In English: "Set PrepositionList to an empty list (there aren't # any). Set FirstPrepositionLocation to VerbLocation". # # "For each word in Global.ActiveCommandList (starting at the # Verb's location and continuing to the end of the list), do the # following:" # # "If the current word is a preposition, append it to # Global.CurrentPrepList, append all objects with this preposition # to the PrepositionList, then (if this is the first preposition), # reset the FirstPrepLocation to the current word's position." # # Unlike a verb, there can be multiple prepositions in the command, # so the FOR loop continues even after we find the first preposition. Global.CurrentPrepList = [] PrepositionList = [] FirstPrepLocation = VerbLocation if len(Global.ActiveCommandList) > 1: for word in Global.ActiveCommandList[VerbLocation + 1:]: if Global.PrepsDict.has_key(word): Global.CurrentPrepList.append(word) PrepositionList = Union(PrepositionList,Global.PrepsDict[word]) if FirstPrepLocation == VerbLocation: FirstPrepLocation = Global.ActiveCommandList.index(word) #-------------------------------------------- # Player didn't use a preposition in command? #-------------------------------------------- # If the player did NOT use a preposition in the command (like # "Take" or "look", then we have a problem. This is because if # you intersect any list with an empty list you get an empty # list. # # To solve the problem PAWS requires ALL verbs to have at least one # preposition. Verbs that don't normally have a preposition are # given the preposition "nopreposition". # # Let's use "look" as an example. Let's say we have 3 verbs named # look, "look", "look into", and "look under". Therefore our # potential verb list would be [LookVerb,LookIntoVerb,LookUnderVerb]. # # Let's further assume the player typed "look". The preposition list # is empty ([]), but we can't intersect with an empty list, or we'll # just get an empty list. # # So if PrepositionList is empty, we append all the verbs in # the preposition dictionary with the name "nopreposition". This # makes the preposition list (for instance) [LookVerb, TakeVerb, # QuitVerb]. Intersection with our verb list yields exactly one # verb! (unless we screwed up and gave two different verbs the # same name and preposition(s)!) if len(PrepositionList) == 0: PrepositionList = Union(PrepositionList,Global.PrepsDict["nopreposition"]) PotentialVerbList = Intersect(PotentialVerbList,PrepositionList) #============================ # Positively Identify the Verb #============================= # It's time to narrow the verb to exactly one candidate. Be aware # it's a rule of PAWS that each verb must have a unique name and # set of prepositions. For example, you can only have one QuitVerb, # since Quit has no prepositions. You can have a look, look into, # and look under verb since each "look" verb has a different # preposition. # # There are 3 possible outcomes: # # 1) We eliminate everything. This means the player either used a #verb that doesn't have prepositions with prepositions #("quit from here") or used a verb that needed prepositions #without any (such as "dig"). We complain and fail. # # 2) We still have more than one possible verb. This generally #means a programming error (two verbs with the same verb #name and the same set of prepositions). We complain with a #bug report. Make one of the verbs use a different preposition #or give it a new name. # # 3) We only have one verb left. This is only way we continue. #------------------------------- # No Verbs, no prepositions used #------------------------------- # Remember our VerbPosition and FirstPrepPosition variables? # If the player used no prepositions the two values will be # equal. If PotentialVerbList is empty and the two values are # equal, the player typed a verb that needed one but didn't # give us one (like typing "dig" and not saying "with".) if len(PotentialVerbList) == 0 and VerbLocation == FirstPrepLocation: return Complain(ParserError.NoPreposition) #---------------------------- # No verbs, prepositions used #---------------------------- # On the other hand, say the player used a preposition with a verb # that doesn't have one (like "Quit to DOS". In that case we don't # have any intersecting verbs, but since we did have a preposition # FirstPrepPosition will be at least one greater than the # VerbPosition. (In our example "Quit To DOS" VerbPosition is 0, and # FirstPrepPosition is 1.) if len(PotentialVerbList) == 0 and VerbLocation < FirstPrepLocation: return Complain(ParserError.NoSuchVerbPreposition) #------------------------- # More than one verb left? #------------------------- # This only happens when the game developer (you) gives two different # verbs the same name and preposition. For instance you might have # LookInVerb and LookIntoVerb and mistakenly given them both "in" # when you really wanted one to have "in" and one to have "into". if len(PotentialVerbList) > 1: return Complain(ParserError.MultipleVerbPrepositions) #==================== # VERB IDENTIFIED!!!! #==================== # By reaching this point we have successfully identified one and # only one verb. We also know where the verb is, and where the # first preposition is. Given this information we can now set # the Global.CurrentVerb variable to the first element in # PotentialVerbList. Global.CurrentVerb = PotentialVerbList[0] #--------------------------- # Check to see if Again verb #--------------------------- # If the player typed a verb meaning AGAIN, then (assuming they typed # a previous command) set the current verb to the previous verb. At this # point the direct and indirect object lists haven't been disturbed, so # the previous command should execute normally. if Global.CurrentVerb == Global.Again and Global.Again <> None: if Global.PreviousVerb == None: return Complain(ParserError.NoPreviousCommand) Global.CurrentVerb = Global.PreviousVerb return SUCCESS #--------------------- # Set Previous Command #--------------------- # This will allow the above command to work when the *NEXT* command is # "again". Global.PreviousVerb = Global.CurrentVerb #=============== # Identify Actor #=============== # The current actor will always be the player's character unless # the player puts something in front of the verb. So we play a # small programming trick. # # We deliberately set the current actor to the player. Then we test # to see if the verb's position in the command was greater than 0. # # If not then the player didn't give us an actor. We don't have to # do anything else, because the current actor is already the # player's object! This is called "defaulting", it's a very useful # trick to keep code short and simple. # # If the verb's position is greater than 0 that means the player # typed the name of an actor in front of the verb. # # We have a special function called ParserIdentifyNoun() that will # identify objects in a command. You pass it the starting and # ending position within Global.ActiveCommandList and it returns # a list of corresponding objects. # # So we pass it 0 (the start of the command) and VerbPosition. # In the command "John, Take the book" this means John is the # only word scanned. Thus the function returns [JohnActor] as # a single item list of lists. # # Unfortunately, everything that uses CurrentActor expects it to # be a single item, not a list. So we employ a trick called # "type conversion" to convert it from a list to a single item. # To do that we simply assign the first element of the first list # to CurrentActor, this converts it from a list to a single item. # # The IF test below tests the type of Global.CurrentActor against # the type of an empty list. If the currentActor is a list, the # parser gives up (rather than trying to weed out which object # is the actor), complains, and returns FALSE. Global.CurrentActor = Global.Player if VerbLocation > 0: Global.CurrentActor = ParserIdentifyNoun(0,VerbLocation) Global.CurrentActor = Global.CurrentActor[0] Global.CurrentActor = Global.CurrentActor[0] if type(Global.CurrentActor) == type([]): Global.CurrentActor = Global.Player return Complain(ParserError.MultipleActors) #------------------- # Get Direct objects #------------------- # Direct objects are the objects the verb acts on. For example, # in the command "Take book", "book" is the direct object. The # parser uses a simple rule to identify direct objects, they're # everything that lie between the verb and the first preposition. # # There's a special case we need to handle, but that's actually a # special case of indirect objects (stay tuned). Notice we use our # defaulting trick again. Global.CurrentDObjList = [] if len(Global.ActiveCommandList) > 1: Global.CurrentDObjList = ParserIdentifyNoun(VerbLocation,FirstPrepLocation) #----------------- # Indirect Objects #----------------- # Indirect objects are those that follow a preposition. In the # command "dig hole with shovel", shovel is the indirect object. Global.CurrentIObjList = [] if len(Global.ActiveCommandList) > FirstPrepLocation + 1: Global.CurrentIObjList = ParserIdentifyNoun(FirstPrepLocation + 1,len(Global.ActiveCommandList) + 1) #--------------------------- # DIRECT OBJECT SPECIAL CASE #--------------------------- # There is one special case for direct objects. This occurs when # the preposition immediately follows the verb. For instance, # in the command "Look into chest", "chest" is actually a direct # object, although the parser considers it an indirect object. # # The solution is simple. When the preposition immediately follows # a verb, copy the indirect object list to the direct object list, # then clear the indirect list. if (FirstPrepLocation - VerbLocation) <= 1: Global.CurrentDObjList = Global.CurrentIObjList[:] Global.CurrentIObjList = [] #------------------ # That's All, Folks #------------------ # The parser is finished. Notice it doesn't handle disambiguation of # objects, that's left to the verbs. You'll notice the BasicVerb # object is quite sophisticated, it handles default disambiguation # for most cases, you can override specific verbs on a case by case # basis. return SUCCESS #-------------------------- # Default Post Game Wrap Up #-------------------------- # The post game wrap up lets the developer print a message after the # game is over, if they want. The default version below does absolutely # nothing. def default_PostGameWrapUp(): """Default routine for printing end of game messages""" pass #-------------------------------------- # Default Pre-Parse Active Command List #-------------------------------------- # All this routine does by default is put the text of the active # command (the command about to be parsed) into the global string # variable SaidText, which is used by the "say" verb to Say() what # the player typed. This is usually just parroting the player, but # can also be used for debugging. def default_PreParse(): """Default routine for pre-parsing a command""" #-------------------------------------------- # Preserve exact command for debugging system #-------------------------------------------- Global.SaidText = string.join(Global.ActiveCommandList[1:]) #------------------------------------------------- # Force Words In Active Command List To Lower Case #------------------------------------------------- #---------------------------------- # Initialize LastWord & CurrentWord #---------------------------------- # LastWord is the # of the last word in the active command list. Remember the # first word in the list is 0, not 1, which is why we subtract 1 from the # actual length of the active command list to find the last word #. # # This means in a 5 word command the last word is actually word #4 # # Since we want work with one word in the list at a time we need a variable # to track which word # we're currently on. We set it to 0 since the first # word in the list is 0. LastWord = len(Global.ActiveCommandList) - 1 CurrentWord = 0 #--------------------- # Lower Case Each Word #--------------------- # We use the more complicated WHILE construct instead of FOR IN so we can # lower each list item "in place". This means we avoid having to create # a new list and transfer it. # # The variable Word is purely a coding convenience. We really didn't need to # use Word here, we could have used Global.ActiveCommandList[CurrentWord], but # Word is much shorter and easier to understand. # # Notice Word is also forced to lower case using the string.lower() function. # We actually want Word in lower case because all the vocabulary dictionaries # are in lower case. Since Python says that "WEST" is different than "west" # (because of case) we have to make sure to use the same case for everything. # PAWS uses lower. # # If Word is not in the game's vocabulary we complain. Remember that the # Complain() function does two things. It types our text on the screen, but it # also returns FALSE. That means when we return to the parser the parser will # know a problem occurred, and will go back to the player for new input. # # If we *did* find Word in the vocabulary, we change the current word in the # active command list to our lower cased version and then increment # CurrentWord by 1. # # Finally notice we exempt CBE's from being vocabulary checked, since CBE's # would *NEVER* be in the vocabulary. while CurrentWord <= LastWord: Word = string.lower(Global.ActiveCommandList[CurrentWord]) if not InVocabulary(Word) and Word[0]<>"{": return Complain(ParserError.NotInVocabulary % Word) Global.ActiveCommandList[CurrentWord] = Word CurrentWord = CurrentWord + 1 #------------------- # Translate Pronouns #------------------- #------------------ # Create Empty List #------------------ # ReturnList will hold the new command, with it, him, her, or # them/all/everything translated to the appropriate object names. ReturnList = [] #------------------------- # For Each Word In Command #------------------------- # The return list is the new command resulting from the translation of # pronouns. # # We look at each word in the active command list, and if it is NOT a # pronoun we simply append it to the return list unchanged. If it's a # singular pronoun (it/him/her) we get the object's short description # and save it. # # If it's a plural pronoun (them/all/everything) we loop through the # resulting list of objects, appending each object's short description # to a variable. # # In either case we then create a list from the resulting set of words # and append them to the return list. # # In the case of "Take it" (where it refers to a rock) the resulting # new command would be "take small gray rock". If the command was "Look at # them (and them referred to a ring and a sword) then the command would # be translated to "look at gold ring sharp sword" (this command will # parse correctly, for all it's horrible English). for word in Global.ActiveCommandList: #------------------- # Empty Object Names #------------------- # This string variable holds one or more pronoun short descriptions. # For example if the command was "take it" where it was a ring, then # ObjectNames would eventually hold "gold ring". # # If the command was "take them" where them was a ring and sword then # ObjectNames would eventually hold "gold ring sharp sword". ObjectNames = "" #------------------- # Is Word A Pronoun? #------------------- # If the word is a pronoun it will be in the Global.PronounsListDict. # has_keys returns true if word is in the dictionary, false if it # isn't. if Global.PronounsListDict.has_key(word): #------------------ # Word IS A Pronoun #------------------ # By getting to this point we're convinced the word IS a pronoun # that needs to be translated. #------------------- # Get Pronoun Number #------------------- # There are 4 kinds of pronouns. They're numbered 0 to 3. The # pronouns are listed below: # # 0 - it # 1 - them/all/everything # 2 - him # 3 - her # # Everything but pronoun #1 is singular--it can refer only to a # single object. Pronoun #1 is PLURAL, it can refer to more than # one object. We have to treat singular and plural pronouns very # differently, which is why we need to know the pronoun number. # # The PronounsListDict contains a list of pronouns and their # corresponding numbers, keyed by word. PronounNumber = Global.PronounsListDict[word] #------------------------------- # Is Pronoun Singular Or Plural? #------------------------------- # Any pronoun that isn't THEM is singular. Because pronoun numbers # are important, each has been given a constant. IT = 0, THEM = 1, # HIM = 2, and HER = 3. # # Thus the IF test below could have been written: # # if PronounNumber <> 1: # # We think this way is easier to understand! if PronounNumber <> THEM: #-------------------- # Pronoun Is SINGULAR #-------------------- # If the pronoun is singular we need to get it from the # PronounsDict dictionary, using the Pronoun Number we looked # up above as the key. # # Each time objects are described to the player the parser # automatically sets the values in PronounsDict for you. The # line below sets Object to whatever object the parser last # described to the player for the pronoun in question (the # parser can store 3 objects, one for it, one for him, and one # for her. Object = Global.PronounsDict[PronounNumber] #---------------------------- # Get words to add to command #---------------------------- # ObjectNames will contain the words we're going to add to the # command, in this case the object's short description (which, # conveniently is made up of one or two adjectives and a single # noun). Notice if there is no object stored in the pronoun # dictionary we don't replace the pronouns with ANY words. if Object <> None: ObjectNames = Object.SDesc() else: #------------------ # Pronoun is PLURAL #------------------ # If the pronoun used was them, all, or everything then we take # the short description of EACH OBJECT and add it to # ObjectNames. The next step will be to add the words in # ObjectNames to the command, replacing the plural pronoun. # # Notice we do NOT add the current actor to the list! ObjectList = Global.PronounsDict[PronounNumber] for Object in ObjectList: if Object <> None and Object<>Global.CurrentActor: ObjectNames = ObjectNames + " " + Object.SDesc() #-------------------------------- # Add Object Words To Return List #-------------------------------- # ObjectNames contains all the words we want to add to the # command regardless of whether the pronoun was singular or # plural. #------------------------- # Strip leading whitespace #------------------------- # Strip off any leading spaces to make the conversion easier. ObjectNames = string.lstrip(ObjectNames) #------------------------- # Add Words To Return List #------------------------- # If ObjectNames has any words in it go ahead and convert the # string to a list, then append each word in the list to the # return list. Remember, the return list contains the translated # command. if len(ObjectNames)>0: WordList = string.split(ObjectNames) for word in WordList: ReturnList.append(word) else: #---------------------- # Word Is NOT A Pronoun #---------------------- # If the word isn't a pronoun we don't need to translate it, so # just append it to the ReturnList untouched. ReturnList.append(word) # Global.ActiveCommandList = ReturnList return SUCCESS #------------------------- # Default Pre Turn Handler #------------------------- # Although the default pre-turn handler does nothing, the developer can # replace it with one that handles "quick" actions, actions which should # happen just before the player is allowed to type their command WHETHER # OR NOT THE LAST COMMAND WAS SUCCESSFUL, OR EVEN UNDERSTOOD! # # Pre-turn handlers aren't normally required. def default_PreTurnHandler(): """Default routine for handling 'pre-turn' stuff""" pass #--------------- # Default Prompt #--------------- # This function provides the player's prompting character. We define # it first because the parser needs it. def default_Prompt(PromptArg): """Default function to return player prompt""" Global.CurrentScreenColumn = 1 Global.CurrentScreenLine = Global.CurrentScreenLine + 1 return chr(13)+chr(10)+"> " #------------------- # Default Setup Game #------------------- # This method sets up the starting parameters for the game. It places # objects, initialzies daemons, and all the rest. Note if the developer # wants they can create their own def default_SetUpGame(): """Default routine for setting up the game""" pass #--------------------- # Default Turn Handler #--------------------- # The default turn handler doesn't actually do much, it simply returns # the SUCCESS or FAILURE value returned by the current verb's Execute # method. # # In other words, if the player typed "look at rose" then all the # turnhandler does is call LookAtVerb.Execute(). LookAtVerb.Execute() # returns either SUCCESS or FAILURE, which the TurnHandler returns to # the gaming loop. # # Remember, returning SUCCESS or FAILURE does NOT mean the verb # necessarily succeeded or failed, what it means is that the # AfterTurnHandler will either be called or not! def default_TurnHandler(): """Default routine for handling player commands""" return Global.CurrentVerb.Execute() #------------------------ # Default User Setup Game #------------------------ # This method is just a place holder for the user written User set up # game method. def default_UserSetUpGame(): """Default USER routine for setting up the game""" pass #=========================================================================== # Fundamental Class #=========================================================================== # The Fundamental Class is intended to supply all classes defined in PAWS # certain "plumbing" methods and functions that allow us certain liberties # with the Python programming language. # # Certain constraints are built-in Python assumptions. Properties and methods # have differing syntax, making them impossible to interchange freely. This # has unfortunate implications, especially for game authors coming from # TADS. # # This class helps alleviate some of the more annoying problems. class ClassFundamental: """Root class of all PAWS classes. Provides additional Python 'plumbing'""" #--------------------------- # Make (self) current object #--------------------------- # Because "self" isn't really a variable in the global sense we use # this method to explicitly mark which object is "current". # Global.CurrentObject will always equal "self" and can safely # be used in place of self when creating services that need to # refer to self with {} expressions. def MakeCurrent(self): Global.CurrentObject = self #-------------- # Get Attribute #-------------- # Python has different syntax for returning the value of properties # versus the value of methods. Because we want to allow Location and # other attributes to be either a property OR a method we had to # develop this method. # # Note, only methods without arguments can be returned by this function. # "self" doesn't count as an argument. def Get(self,Attribute): """Get properties/methods you aren't certain exist.""" if not hasattr(self,Attribute): return None if type(getattr(self,Attribute)) <> type(self.Get): return getattr(self,Attribute) else: return getattr(self,Attribute)() #------------------------ # Set Instance Properties #------------------------ # To make object __init__() functions more generic (because they're # involved and somewahat difficult to extend) we created this method # to actually define an object's properties when it is instantiated. All # you have to do is extend this one in descendent classes to add # additional properties to a class instance automatically. # def SetMyProperties(self): """Sets instance properties. Called by __init__()""" pass #=========================================================================== # Parser Error Class #=========================================================================== class ClassParserError(ClassFundamental): """Defines all parser errors""" #-------------------- # Initialize function #-------------------- def __init__(self): """Create Instance Variables""" self.SetMyProperties() #------------------------ # Set Instance Properties #------------------------ def SetMyProperties(self): ClassFundamental.SetMyProperties(self) #-------- # No Verb #-------- # This error will occur when the parser can't find a word it # recognizes as a verb. If this occurs the command is terminated # (parser returns FAILURE). self.NoVerb = "There's no verb in that sentence." self.NoPreposition = "That verb needs a preposition." self.NoPreviousCommand = "You haven't done anything yet!" self.NoSuchVerbPreposition = "I don't recognize that verb/preposition(s) combination" self.MultipleVerbPrepositions = "PROGRAMMING ERROR: Two or more verbs share this verb and preposition combination." self.MultipleActors = "You can only tell one thing at a time to do something." self.DObjsNotAllowed = "This verb can't have any direct objects." self.IObjsNotAllowed = "This verb can't have any indirect objects." self.NotInVocabulary = "I don't know the word '%s'." #-------------------------- # Instantiate Parser Errors #-------------------------- ParserError = ClassParserError() #=========================================================================== # Global Class #=========================================================================== # This class defines the Global variables object. The global variables object # is used to hold all the variables that you want to get to from every part # of the program (such as the Game State). class ClassGlobal(ClassFundamental): """Holds Global Variables""" def __init__(self): """Initialize instance variables.""" self.SetMyProperties() def SetMyProperties(self): """Set Global Properties""" ClassFundamental.SetMyProperties(self) #----------- # Game State #----------- # This variable (actually a property, but who's counting?) holds the # current state of the game, which starts off as STARTING. This is # how the game logic loop knows when the game is starting or running # or finished. self.GameState = STARTING #----------------------- # Noun/Verb Dictionaries #----------------------- # The actual heart of the parsing algorhythmn is based on these two # dictionaries, so understanding them is vital to understanding the # parser. # # Every discete 'thing' in the game (like a sword or a rock) has 1 # (or more!) names associated with it. For example, let's say we # created an object we named SmallRock in the program. The player # can refer to SmallRock either with 'rock' or 'stone' or 'pebble'. # # Ok, fine. NounsDict will have 3 entries, 'rock', 'stone', and # 'pebble'. These are called the KEYS. But what value does each # key hold? # # You guessed it. # # 'rock': [SmallRock] # 'stone': [SmallRock] # 'pebble': [SmallRock] # # Now, let's say we created a second object called Boulder, that # the player can refer to as 'boulder', or 'rock', or 'stone'. Now # our dictionary looks like: # # 'rock': [SmallRock, Boulder] # 'stone': [SmallRock, Boulder] # 'pebble': [SmallRock] # 'boulder': [Boulder] # # Ok, now we know that 'rock; can be either SmallRock or Boulder. How # do we tell which one the player meant? # # If there's any doubt (for example both SmallRock and Boulder are # in the same room as the player) the parser will compare the # adjectives used. No two objects with the same name should ever # have exactly the same list of adjectives, otherwise the parser # will go off and sulk. self.NounsDict = {} # The VerbsDict works the same way, except we distinguish verbs by # the prepositions used with them. For example, 'Look', 'look into # mirror', and 'look under bed' are actually three distinct verbs, # just as Boulder and SmallRock were two distinct objects. So our # VerbDicts dictionary would contain: # # 'Look': [LookVerb, LookIntoVerb, LookUnderVerb] self.VerbsDict = {} #---------------------- # Adjectives Dictionary #---------------------- # The AdjsDict dictionary holds all objects associated with a given # adjective, just like the NounsDict dictionary, for example: # # 'small': [SmallRock, Kitten, Phial, House] # 'glass': [CrystalBall, Window] # 'large': [Troll, Cliff, Diamond] self.AdjsDict = {} #----------------------------- # Dictionary Of Active Daemons #----------------------------- # This "dictionary of lists" contains all the daemons and # fuses which are currently active (use StartDaemon() to add # daemons/fuses to the list and StopDaemon() to remove # them from the list). # # Use RunDaemon() to actually execute daemons in the list. # # The dictionary key is the indirect reference to the function # you want to run. It MUST be a function reference, an object # method won't work. In addition, you can't pass any arguments # to a daemon/fuse. # Each entry in the dictionary is a list. The elements of the # entry list is: # # 0 - Remaining Turns. How many turns remain before the daemon # activates. For a daemon that runs every turn this value # will be 0. For a fuse (function that runs once) this # value will be the number of turns before the function # is executed. Each turn this number is reduced by 1. # # 2 - Initial Fuse Length. When the daemon or fuse is added to # Global.DaemonList by RunDaemon() the Remaining Turns # value above is set to the ABSOLUTE VALUE of this number. # # 0 - If this number is 0 the function will be executed # every turn. In other words, it's a daemon just like # in TADS. # # >0 - If this number is positive (5, say) then the # remaining turns above will be set to 5 and the # function will be run 5 turns later. It will run # once and be removed from Global.DaemonList. In # other words, this is like a fuse in TADS. # # <0 - If this number is NEGATIVE (-5, say) then the # remaining turns above will be set to 5 and the # function will be run 5 turns later. However, once # run instead of being removed the remaining turns # count is RESET. Using a negative number is like # running a daemon every X turns instead of every # turn. self.DaemonDict = {} # The PrepsDict dictionary works like the AdjsDict dictionary but # holds lists of VERBS, not OBJECTS. For example: # # 'with': [DigWithVerb, AttackWithVerb] # 'from': [TakeFromVerb] # 'under': [LookUnderVerb, DigUnderVerb, SearchUnderVerb] self.PrepsDict = {} #------------------------- # Pronouns Dictionary/List #------------------------- # Pronouns work a little differently. There are 4 kinds, gender # neutral singular, gender neutral plural, male, and female singular. # # The parser uses the PronounsList to search for words in the command # which it replaces with the objects from the program dictionary. # # For instance, let's say the player said "Get Book". The parser # (once it knows the player means BlueBook) assigns BlueBook to the # PronounsDict entry 'it' (since the book is gender neutral and # singular). If the player later says 'Read it', the parser knows # "it" refers to BlueBook. self.PronounsListDict = {"it": IT, "them": THEM, "all": THEM, "everything":THEM, "him":HIM, "her":HER} self.PronounsDict = {IT: None, THEM: [], HIM: None, HER: None} # Articles aren't used, they're discarded at the present time. It is possible future # enhancements of the parser will make use of these. self.ArticlesList = ["a","an","the"] # Conjunctions are currently ignored, but future enhancements to the parser may make # of them. self.ConjunctionsList = ["and",","] # Disjunctions are currently ignored, but may be used in future enhancements to the # parser. self.DisjunctionsList = ["but","except"] # Items in this list seperate multiple commands entered on a single line. self.CommandBreaksList = ["then", ".", "?", "!"] #-------------- # Commands List #-------------- # This list holds the list of seperate commands that the player typed # on the same line, for instance "Go west then open door" is two # commands, not just one. The Commands List for this would look like # this: # # [['go','west'],['open','door']] # # In other words CommandsList[0] is ['Go','west']. self.CommandsList = [] #-------------------------- # Active Command Words List #-------------------------- # The active command words list contains a list of words for the # command currently being processed. self.ActiveCommandList = [] #---------------- # Player "Object" #---------------- # The player object is the one the player controls, it's usually # refered to as "me". When a player types a command it's usually # assumed the player object is the one that will do the command. # # Global.Player is set in the object library (Universe), but can be # easily be overridden in your game library. self.Player = None #------------------ # Screen Management #------------------ # The properties here are related to screen management and the Say() # function. self.MaxScreenLines = 25 self.MaxScreenColumns = 80 self.CurrentScreenLine = 1 self.CurrentScreenColumn = 1 #---------------- # Decoded Objects #---------------- # Once the parser figures out which objects the player's command # indicated it puts them in these variables. # # CurrentActor and CurrentVerb are both single objects. # # CurrentPrepList is a list of strings, the preposition(s) used # with the verb. For instance, 'get book from under the bed' would # have 2 prepositions, 'from' and 'under.' You generally won't need # these but if you ever do you'll have them. # # CurrentDObjList is a list of the direct object(s) the player meant, # and CurrentIObjList is a list of indirect object(s) the player # meant. # # SaidText is literally the text the player typed in for the # ActiveCommandList. As in "Say "Hello". SaidText would be # "Hello" inside quotes. self.CurrentActor = None # Object self.CurrentVerb = None # Object self.PreviousVerb = None # Object self.Again = None # Object (again verb) self.CurrentObject = None # Object (within CurrentDObjList) self.CurrentDObjList = [] # Objects self.CurrentIObjList = [] # Objects self.CurrentVerbNoun = None # Word self.CurrentPrepList = [] # WORDS self.SaidText = "" # string # The debug property is an easy way to embed (and leave) debugging trace code in your # program. In production it should be set to FALSE self.Debug = FALSE #---------------------- # Game is in Production #---------------------- # This variable allows you to enable/disable the Debug verb. # All you need to do to disable the Debug verb is set # Production to TRUE instead of FALSE. # # The default TQ.py file comes with Production set to FALSE, # so that the Debug verb works. # # When your game is finished and completely tested, change the # line in your specially created UserSetUpGame() function # replacement to TRUE. # # IMPORTANT NOTE: Do *NOT* change the value here! Do it in # your game library's UserSetUpGame() function! For example, # in Thief's Quest the library is called TQLib.py and the # function is called TQUserSetUpGame(). THAT's where you # should change it, NOT HERE. self.Production = TRUE #------------------- # Instantiate Global #------------------- Global = ClassGlobal() #=========================================================================== # Engine Class #=========================================================================== # The class ClassEngine lays out the majority of the runtime system. Along # with the Global above it makes up about 95% of the runtime system. class ClassEngine(ClassFundamental): """All the game plumbing and wiring""" #-------------------- # Initialize function #-------------------- def __init__(self): """Initialize instance variables.""" self.SetMyProperties() def SetMyProperties(self): #---------------------- # Assign method Aliases #---------------------- # We want to set up aliases for the default game engine routines. # These aliases are the "functions" called by you the developer, not # the default function itself. # For example, Prompt is the alias to default_Prompt. Note we're not # instantiating an object here, we're assigning an alias. At this # point in the code Prompt() and default_Prompt() do exactly the # same thing. # # Why bother, you ask? Because the developer (the game author--you) # can replace the default prompt with your own function. Let's say # you create a really neat function called MyPrompt(). # # At the end of your function all you have to say is: # # Engine.Prompt = MyPrompt # # and the game engine will start using your prompt instead of the # default one! Pretty neat, eh? Notice you don't follow MyPrompt # with parentheses, you want to assign the address of your function # to Prompt, not the return value! #}}} ClassFundamental.SetMyProperties(self) self.AfterTurnHandler = default_AfterTurnHandler self.GameSkeleton = default_GameSkeleton self.Parser = default_Parser self.PreParse = default_PreParse self.PreTurnHandler = default_PreTurnHandler self.Prompt = default_Prompt self.PostGameWrapUp = default_PostGameWrapUp self.RestoreFunction = None self.SaveFunction = None self.SetUpGame = default_SetUpGame self.TurnHandler = default_TurnHandler self.UserSetUpGame = default_UserSetUpGame self.Version = "1.0" self.XlateCBEFunction = None #------------------- # Instantiate Engine #------------------- Engine = ClassEngine() #=========================================================================== # Base Object #=========================================================================== # This class is used to extend all "thing" classes. It basically adds the # current object's name(s) and adjectives to the parser dictionaries. It # also provides minimal functionality to support the parser. class ClassBaseObject(ClassFundamental): """Base class for all non-verb objects (things)""" #------------------ # Instantiate Class #------------------ # This method is called ONLY when an object is instantiated. For instance # if "rock" were being defined the instantiation would look like: # # SmallRock = ClassBaseObject("rock,stone","small,grey") # # This is the equivalent of saying: # # SmallRock = ClassBaseObject.__init__("rock,stone","small,grey") # # In other words, you're actually calling the __init__() method when you # instantiate a class. def __init__(self,Name = "",Adjs = ""): """Adds object to appropriate Global lists and dicts""" #------------------------------------------------- # Append Object to Noun and Adjective Dictionaries #------------------------------------------------- AppendDictList(Global.NounsDict,Name,self) AppendDictList(Global.AdjsDict,Adjs,self) #--------- # Key Noun #--------- # The KeyNoun lets us find this object in the NounDict for # disambiguation purposes. Notice we also set the NamePhrase # to the KeyNoun, this allows us to avoid having to set it # explicitly. NounList = string.split(Name,",") if len(NounList) > 0: self.KeyNoun = NounList[0] else: self.KeyNoun = "" self.NamePhrase = self.KeyNoun #-------------------- # Set AdjectivePhrase #-------------------- AdjectiveList = string.split(Adjs,",") if len(AdjectiveList) > 0: self.AdjectivePhrase = AdjectiveList[0] else: self.AdjectivePhrase = "" #---------------------- # Set self's properties #---------------------- self.SetMyProperties() #------------------------ # Set Instance Properties #------------------------ def SetMyProperties(self): """Set instance properties""" ClassFundamental.SetMyProperties(self) #=========================================================================== # Base Verb Object #=========================================================================== # This class is used to extend all verb classes. It basically adds the # current verb's name(s) ("go","walk", etc) to the dictionary, along with # the appropriate prepositions. class ClassBaseVerbObject(ClassFundamental): """Base class for all verbs""" #------------------ # Instantiate Class #------------------ # This method is called ONLY when an object is instantiated. For instance # if "quit" were being defined the instantiation would look like: # # QuitVerb = ClassBaseVerb("quit") # # This is the equivalent of saying: # # QuitVerb = ClassBaseVerb.__init__("quit") # # In other words, you're actually calling the __init__() method when you # instantiate a class. # # Notice how we default the Preps argument? This way, a developer can # easily create verbs that have no prepositions ("quit", "save", etc). def __init__(self,Name = "",Preps = "nopreposition"): """Adds verb to appropriate Global dicts""" #----------------------------------------- # Append all verb names to verb dictionary #----------------------------------------- AppendDictList(Global.VerbsDict,Name,self) AppendDictList(Global.PrepsDict,Preps,self) self.SetMyProperties() #------------------------ # Set Instance Properties #------------------------ def SetMyProperties(self): """Set instance properties""" ClassFundamental.SetMyProperties(self) #---------------------------- # Only Allowed Direct Objects #---------------------------- # This is a "placeholder" list. If any objects are put in this # list, they become the only direct objects that can be used with # this verb, any other direct objects will cause the verb to fail. # # This allows a simple way to restrict verbs to certain direct # objects. self.OnlyAllowedDObjList = [] #------------------------------ # Only Allowed Indirect Objects #------------------------------ # This is a "placeholder" list. If any objects are put in this # list, they become the only indirect objects that can be used with # this verb, any other indirect objects will cause the verb to fail. # # This allows a simple way to restrict verbs to certain objects. self.OnlyAllowedIObjList = [] #----------------- # Object Allowance #----------------- # The object allowance property determines what the verb expects # in the way of direct and indirect objects. As you can see we # set the property by adding two of the object allowance constants # together, one for the direct and one for the indirect objects. self.ObjectAllowance = ALLOW_MULTIPLE_DOBJS + ALLOW_ONE_IOBJ #----------- # OK In Dark #----------- # Set this value to TRUE if the verb can be used in the dark. The # default is FALSE. self.OkInDark = FALSE #--------------- # Execute Method #--------------- # The execute method is small, but very flexible. The first thing it # does is call the GenericDisambiguate method. The GenericDisambiguate # method elminates direct and indirect objects that don't make sense for # the verb. In addition, it verifies that verbs that don't allow direct # and indirect objects don't have any. If it should fail it returns # failure immediately. # # Then if the SanityCheck() fails we return failure immediately. The # SanityCheck allows us a way to test each verb, we replace it the same # way we do Action(). # # Finally, if both the GenericDisambiguate() and SanityCheck() methods # are successful we return the value of Action() (which will be either # SUCCESS or FAILURE). def Execute(self): """Calls generic disambiguate and if successful, action.""" if not self.GenericDisambiguate(): return FAILURE if not self.SanityCheck(): return FAILURE return self.Action() #--------------------- # Generic Disambiguate #--------------------- # Disambiguate means to remove ambiguity (uncertainty) from something. # In this case it means remove the direct and indirect objects that # don't make sense in the current context. # # For instance, this would include objects the current actor (usually # the player) hasn't yet encountered, or couldn't know about. In this # "generic" disambiguation, we handle situations that are common to # ANY game you might write. # # We also call a specific disambiguate routine at the end of our generic # one. This "layered" approach allows us to create a default behavior # (aborting disambiguation immediately if there are no objects, and # removing unknown objects) as well as library or game specific # disambiguation (object isn't here, isn't reachable, is invisible, is # locked, etc). def GenericDisambiguate(self): """Basic disambiguation. Handles no objects, unknown objects. Calls SpecificDisambiguate""" #------------------------------- # No Direct or Indirect Objects? #------------------------------- # As a speed optimization (and code simplifier) we test to see if # both the direct and indirect object lists are empty. If they are, # there's nothing to disambiguate and we're finished, so we return # at once. This makes verbs like "quit" faster. # if len(Global.CurrentDObjList) == 0 and len(Global.CurrentIObjList) == 0: # return SUCCESS #----------------------------------- # Check for forbidden direct objects #----------------------------------- # If the verb doesn't allow direct objects, and direct objects were # used we complain and return failure. This insures the Execute() # method won't call the verb's action. if len(Global.CurrentDObjList) <> 0 and (self.ObjectAllowance & ALLOW_NO_DOBJS > 0): return Complain(ParserError.DObjsNotAllowed) #------------------------------------- # Check for forbidden indirect objects #------------------------------------- # If the verb doesn't allow indirect objects, and indirect objects # were used we complain and return failure. This insures the # Execute() method won't call the verb's action. if len(Global.CurrentIObjList) <> 0 and (self.ObjectAllowance & ALLOW_NO_IOBJS > 0): return Complain(ParserError.IObjsNotAllowed) #----------------------------- # Call Specific Disambiguation #----------------------------- # Verbs defined in the library must have a more specific # disambiguation routine defined. The library will override our # definition of the method with its own. If the disambiguation # fails for some reason we will abort the GenericDisambiguation # routine, and thus abort the command. if not self.SpecificDisambiguate(): return FAILURE return SUCCESS #---------------------- # Specific Disambiguate #---------------------- # Specific disambiguation handles removing objects specific to the # game. This routine will be overridden by the game library, or # even specific verbs within the game itself. def SpecificDisambiguate(self): """Specific disambiguation routine. Overridden by descendents""" return SUCCESS #-------------- # Verb's Action #-------------- # Although this method is a simple "placeholder" intended to be replaced # in every descendent class, the replacement methods are the ones # that actually "do" something. # # The Action() method must always return either SUCCESS or FAILURE, # SUCCESS if you want the AfterTurnHandler to run, FAILURE if you # don't. def Action(self): """The action taken by the verb. Overridden by descendants""" return FAILURE # This function is (optionally) implemented on a verb by verb basis # (maybe). It's a last ditch effort to make the action fail before # it's executed. Return SUCCESS if you want the action to execute, # FAILURE if you don't. def SanityCheck(self): """The sanity check performed by the verb before the action. Overridden by descendents""" return SUCCESS #********************************************************************* # End of PAWS Module #*********************************************************************