#!/usr/bin/env/python """ IFEngine by Kris Schnee A working outline of an Interactive Fiction (text adventure) engine. Includes the ability to create places, things and creatures, and to move around taking and dropping things. Done in one night for fun. """ __author__ = "Kris Schnee" __license__ = "Public Domain" ##### DEPENDENCIES ##### ## Standard ## Third-party ## Mine ##### CONSTANTS ##### DIRECTIONS = (("North",0),("East",1),("South",2),("West",3)) DEFAULT_EXITS = [True,True,True,True] NORTH = 0 EAST = 1 SOUTH = 2 WEST = 3 PLACE_DEFAULT_NAME = "Wilderness" PLACE_DEFAULT_DESC = "This is empty land. Buy some DLC, maybe?" PLACE_DEFAULT_DISTANT_DESC = "Empty land" ALLOW_ENTERING_WILDERNESS = True COMMANDS = { "quit":{}, "go": {"params":1}, "take": {"params":1}, "drop": {"params":1}, } ##### CLASSES ##### class Thing: def __init__(self,**options): self.name = options.get("name","Thing") self.nicknames = options.get("nicknames",[]) self.coords = options.get("coords",None) self.ID = options.get("ID",None) self.desc = options.get("desc") def IsCreature(self): return False class Creature( Thing ): def __init__(self,**options): options.setdefault("name","Creature") Thing.__init__(self,**options) self.inventory = [] ## Things carried. def IsCreature(self): return True def Get(self,thing): """Add to inventory. Does not change its coordinates. Currently assume inventory is infinite.""" if thing in self.inventory: raise SystemError("Tried to add an item to inventory twice.") self.inventory.append(thing) return True def Drop(self,thing): """Remove from inventory.""" self.inventory.remove(thing) def FindThingInInventoryByName(self,name): name = name.lower() for thing in self.inventory: if thing.name.lower() == name: return thing class Place: def __init__(self,**options): ## Basic info self.name = options.get("name","Wilderness") self.coords = tuple(options.get("coords",(0,0))) self.plane = options.get("plane","main") ## What does it look like? ## Basic info anyone can see. self.desc = options.get("desc","Featureless wilderness.") ## Basic info as seen from other Places. self.distant_desc = options.get("distant_desc","Empty land") ## Outdoors? Affects flying, weather, and "skybox" imagery. self.outdoors = options.get("outdoors",True) ## What is here? self.things = [] self.creatures = [] ## Exits? self.exits = options.get("exits",DEFAULT_EXITS[:]) ## Copy the default. def AddThing(self,thing): if thing in self.things: return False self.things.append(thing) thing.coords = self.coords return True def RemoveThing(self,thing): if not thing in self.things: return False self.things.remove(thing) thing.coords = None return True def AddCreature(self,creature): if creature in self.creatures: return False self.creatures.append(creature) creature.coords = self.coords return True def RemoveCreature(self,creature): if not creature in self.creatures: return False self.creatures.remove(creature) creature.coords = None return True def Add(self,entity): if entity.IsCreature(): return self.AddCreature(entity) else: return self.AddThing(entity) def Remove(self,entity): if entity.IsCreature(): return self.RemoveCreature(entity) else: return self.RemoveThing(entity) class World: def __init__(self): self.places = {} self.things = {} self.creatures = {} self.next_free_thing_ID = 0 self.next_free_creature_ID = 0 def GetNextFreeID(self,is_creature=False): """Find an ID number for a Thing/Creature. ID numbers start at 1, and are given as strings "T#" or "C#". Thing and Creature ID numbers are separate; T1 and C1 can coexist.""" if is_creature: self.next_free_creature_ID += 1 return "C" + str(self.next_free_creature_ID) else: self.next_free_thing_ID += 1 return "T" + str(self.next_free_thing_ID) def MakePlace(self,**options): """Create and place a Place.""" options["name"] = options.get("name",PLACE_DEFAULT_NAME).title() ## eg. "castle"->"Castle" place = Place(**options) self.places[place.coords] = place return place def MakeThing(self,**options): """Create and place a Thing.""" ID = self.GetNextFreeID(False) options["ID"] = ID thing = Thing(**options) self.things[ID] = thing coords = options.get("coords") self.Put(thing,coords,False) return thing def MakeCreature(self,**options): """Create and place a Creature.""" ID = self.GetNextFreeID(True) options["ID"] = ID creature = Creature(**options) self.creatures[ID] = creature coords = options.get("coords") self.Put(creature,coords,True) return creature def Put(self,entity,coords,is_creature): """Add this Thing/Creature to a Place. This function doesn't remove it from anywhere, so do that with Remove or you'll have the entity existing in two Places at once.""" place = self.places.get(coords) if not place: return False if is_creature: place.AddCreature(entity) else: place.AddThing(entity) def Remove(self,entity): """Remove Thing/Creature from its current Place.""" coords = entity.coords if not coords: return False place = self.places.get(coords) if not place: return False place.Remove(entity) def GetAdjacentCoords(self,coords,direction): """Return coords n/e/s/w of the given coords.""" if direction == NORTH: return (coords[0],coords[1]-1) elif direction == EAST: return (coords[0]+1,coords[1]) elif direction == SOUTH: return (coords[0],coords[1]+1) elif direction == WEST: return (coords[0]-1,coords[1]) else: raise SystemError("Invalid direction.") def Move(self,entity,direction): """Move Thing/Creature to an adjacent Place. If the location in this direction doesn't exist, and if ALLOW_ENTERING_WILDERNESS, a new Place will be created.""" ## Find the destination, possibly creating one. target_coords = self.GetAdjacentCoords(entity.coords,direction) destination = self.places.get(target_coords) if not destination: if ALLOW_ENTERING_WILDERNESS: destination = self.MakePlace(coords=target_coords) else: return False ## Remove entity from current location and place in new one. place = self.places.get(entity.coords) place.Remove(entity) destination.Add(entity) def FindThingByName(self,place,name): name = name.lower() for thing in place.things: if thing.name.lower() == name: return thing elif name in thing.nicknames: return thing def Take(self,taker,thing): """Have taker pick up this object.""" if thing.IsCreature(): return False if taker.coords != thing.coords: return False ok = taker.Get(thing) print "OK: "+str(ok) if ok: self.Remove(thing) return True else: return False def Drop(self,dropper,thing): if not thing in dropper.inventory: raise SystemError("That thing wasn't in the dropper's inventory.") place = self.places.get(dropper.coords) dropper.Drop(thing) place.Add(thing) def GetPlaceExitText(self,place,show_hidden=False): """Builds a string saying what's in each direction from a Place.""" text = "" for d in range(4): ## Eg. "North: a grassy field" or "East: (Blocked)" text += DIRECTIONS[d][0] + ": " exit_open = place.exits[d] if exit_open: adj_coords = self.GetAdjacentCoords(place.coords,d) destination = self.places.get(adj_coords) if destination: text += destination.distant_desc else: text += PLACE_DEFAULT_DISTANT_DESC text += "\n" else: text += "(Blocked)" return text def ListThingsHere(self,place): return ", ".join([t.name for t in place.things]) def ListCreaturesHere(self,place,player): return ", ".join([c.name for c in place.creatures if (c is not player)]) class Engine( World ): def __init__(self): World.__init__(self) self.player = None def SetPlayer(self,creature): """Player will control this Creaure. Give entity ref, not ID.""" self.player = creature ## Console-based interface. Should be superseded by a Pygame UI. def HandleCommand(self,words,creature=None): """Carry out a command by this creature. Default: self.player. Assumes that the correct number of arguments is given.""" c = creature or self.player place = self.places.get(c.coords) if words[0] == "help": print COMMANDS[words[1].get("help","No help available for that command.")] elif words[0] == "error_unknown_command": print "Unknown command." elif words[0] == "go": d = words[1] if d == "n": self.Move(p,NORTH) elif d == "e": self.Move(p,EAST) elif d == "s": self.Move(p,SOUTH) else: self.Move(p,WEST) elif words[0] == "take": target = self.FindThingByName(place,words[1]) if not target: print "You don't see \"" + words[1] + "\"." return else: ok = self.Take(c,target) if ok: print "Taken." else: print "Couldn't take that." elif words[0] == "drop": target = c.FindThingInInventoryByName(words[1]) if not target: print "You don't have that." return else: self.Drop(c,target) print "Dropped." elif words[0] in ("q","quit"): print "Quitting." return True def ParseCommand(self,command): """Turn a command string into a regularly formatted tuple.""" ## Note: Any modified commands produced by this function should be in ## list format, because ("quit")[0] == "q" while ["quit"][0] == "quit". words = command.split(" ") if not words: return words[0] = words[0].lower() if words[0] in "news": words = ["go",words[0]] elif words[0] == "t": words[0] = "take" elif words[0] in ("q","quit"): words = ["quit"] command_info = COMMANDS.get(words[0]) if command_info == None: return ["error_unknown_command"] ## Error: Invalid command. if len(words)-1 < command_info.get("params",0): return ["help",words[0]] ## Error: not enough parameters given. return words def ConsoleInterface(self): """A text interface using the Python console. Lets the player have their character walk around and do things.""" assert self.player ## There's a player... assert self.player.coords ## And they're in the world. done = False while not done: place = self.places[ self.player.coords ] print "\n* " + place.name + " *\n" + place.desc + "\n* Exits: *" print self.GetPlaceExitText(place,False) print "-Things Here: " + self.ListThingsHere(place) print "-Creatures: " + self.ListCreaturesHere(place,p) command = raw_input("> ") words = self.ParseCommand(command) done = self.HandleCommand(words) ##### OTHER FUNCTIONS ##### ##### AUTORUN ##### if __name__ == "__main__": ## Do the following if this code is run by itself: w = Engine() ## Some sample data. w.MakePlace(name="Snowfield",coords=(0,0),desc="A chessboard plaza in blue and white marble, with snow falling gently.",distant_desc="a snowy plaza") w.MakePlace(name="Statue Garden",coords=(0,1),desc="A garden of statues. They all look posed in expressions of surprise and horror.",distant_desc="a statue garden") w.MakePlace(name="Snack Shack",coords=(0,-1),desc="A shack selling hot chocolate and apple pies.",distant_desc="a snack stand") w.MakeThing(name="apple",coords=(0,-1)) p = w.MakeCreature(name="Skioros",coords=(0,0)) ## Run the game using the console interface. w.SetPlayer(p) w.ConsoleInterface()