#!/usr/bin/env/python """ Worldsim "Liberty" by Kris Schnee An easy-to-use game world. The goal is to have a simple environment following the MVC design structure, meaning it can be accessed by any interface or display system. Design assumptions: -Tile-based, 2.5D (2D layers) -Turn-based -Some detail and interactivity on objects -Expandable for greater complexity as desired -Focused on the model only; gameplay and graphics are to be built atop this. """ __author__ = "Kris Schnee" __license__ = "GPL v3" ##### DEPENDENCIES ##### ## Standard import random import time import os ## Third-party ## Mine ##### CONSTANTS ##### ZONE_SIZE = (100, 100) """Terrain types. Some cost energy to enter if the "energy" rule is in play. Others are impassible or even kill any character that enters.""" TERRAINS = {"water":{"cost":3}, "grass":{}, "sand":{}, "dirt":{}, "block":{"passable":False}, "forest":{"cost":2}, "mountains":{"passable":False}, "iron":{}, "oak":{}, "pit":{"passable":False}, "lava":{"deadly":True}, } TERRAIN_ABBREVS = {} for t in TERRAINS: TERRAINS[t].setdefault("cost",1) TERRAINS[t].setdefault("passable",True) TERRAINS[t].setdefault("abbrev",t[0]) TERRAIN_ABBREVS[TERRAINS[t]["abbrev"]] = t TIME_FORMAT = "%Y %b %d %H:%M:%S" TIME_SHORTFORMAT = "%H:%M" ZONE_DIRECTORY = "zones" ##### CLASSES ##### class Entity: """A physical object within the world.""" def __init__(self,**options): ## Basic properties self.name = options.get("name","Entity") self.ID = options.get("ID") ## Assigned by the World self.alive = False ## False for dead characters & inanimate objects self.description = options.get("description","") ## Text about this self.desc = options.get("desc",self.description[:12]) ## Abbrev. self.color = options.get("color",(255,255,255)) self.warmth = options.get("warmth",25.0) ## C ## Sense properties: 0.0 to 1.0 in most cases. ## Size: I define a size unit as one tile-width or one meter. ## A game can assume a completely different scale though. self.bulk = options.get("bulk",1.0) ## Occupies tile completely at 1.0 ## Shape self.shape_solid = options.get("shape_solid",1.0) ## vs. spindly/holey self.shape_blocky = options.get("shape_blocky",1.0) ## vs. curvy self.shape_still = options.get("shape_still",1.0) ## vs. moving parts self.shape_square = options.get("shape_square",1.0) ## vs. oblong ## Texture (when touched) self.tex_hard = options.get("tex_hard",0.0) self.tex_rough = options.get("tex_rough",0.0) ## Chemoreceptor data self.smell = options.get("smell",(0,0,0)) ## 3 arbitrary receptor types self.taste = options.get("taste",(0,0,0,0)) ## sweet/sour/salty/bitter ## Location self.coords = [None,None,None] ## Read only; changed by World only self.facing = options.get("facing",0) ## 0-359. 0 = East, 90 = North ## Item-related properties ## Energy restored when eaten. self.food_value = options.get("food_value",0) ## Lock codes begin with L, keys with K. "K01" opens lock "L01". self.lock_code = options.get("lock_code",None) ## Inventory represents the ability to hold items inside this one. self.inventory = [] self.inventory_max_items = options.get("inventory_max_items",0) self.inventory_max_mass = options.get("inventory_max_mass",0) self.wearable_on = [] ## Names of body parts this can be worn on self.anchored = options.get("anchored",False) ## Can't take or move it self.mass = options.get("mass",1.0) ## Nominally 1 Kg self.floats = options.get("floats",False) ## In water self.book_text = options.get("book_text",{}) ## Readble topics ## Image can hold a filename or Pygame Surface reference. ## This module does not deal directly with graphics, but for my own ## purposes I will assign Surface refs in the world-loading process. self.image = options.get("image","default") def Get(self,entity): """Put this entity into my inventory. It effectively vanishes from the world, meaning it has no coordinates and can't be sensed/interacted with directly.""" if len(self.inventory) == self.inventory_max_items: return False elif sum([i.mass for i in self.inventory]) + entity.mass > self.inventory_max_mass: return False else: self.inventory.append(entity) entity.coords = (None,None,None) return True def Drop(self,entity): """Remove this entity from my inventory. The function calling this should put the item back into the world.""" if not entity in self.inventory: raise SystemError("Tried to drop \""+entity.name+"\", but I don't have that.") self.inventory.remove(entity) return True class Character( Entity ): """An Entity that can perform actions.""" def __init__(self,**options): self.health_max = options.get("health_max",100) self.health = self.health_max self.energy_max = options.get("energy_max",100) self.energy = self.energy_max self.vision_range = options.get("vision_range",5) options.setdefault("inventory_max_items",3) options.setdefault("inventory_max_mass",10.0) ## Output self.command = None ## When all characters enter a command, turn starts. self.command_history = [] ## Input self.sensed_events = [] ## Things I've sensed & should report. self.system_messages = [] ## Special options self.invincible = options.get("invincible",False) self.hands = {"hand_l":None, "hand_r":None, } Entity.__init__(self,**options) self.alive = True def GetFreeHand(self): """Return the first hand available for holding stuff, or None.""" if not self.hands["hand_l"]: return "hand_l" elif not self.hands["hand_r"]: return "hand_r" else: return None def GetFullHand(self): """Return the first occupied hand, or None.""" if self.hands["hand_l"]: return "hand_l" elif self.hands["hand_r"]: return "hand_r" else: return None def Get(self,entity,hand=None): """Put this entity into my hands. It effectively vanishes from the world, meaning it has no coordinates and can't be sensed/interacted with directly.""" if not hand: hand = self.GetFreeHand() if not hand: return False ## Hands full self.hands[hand] = entity entity.coords = (None,None,None) return True def Drop(self,hand=None): """Drop what I'm holding and return it.""" if not hand: hand = self.GetFullHand() if not hand: return False ## Not holding anything item = self.hands[hand] self.hands[hand] = None return item def Sense(self,sender,what): """Get information about the world.""" self.sensed_events.append(sender+": "+what) def SenseSystemMessage(self,what): """Record a special message.""" self.system_messages.append(what) def ClearInput(self): self.sensed_events = [] self.system_messages = [] def SetCommand(self,command): """This is what I'll do this turn...""" self.command = command self.command_history.append(command) self.command_history = self.command_history[-100:] ## Keep last 100 def ChangeEnergy(self,amount=0): """Gain/lose energy. Returns True if I'm still at >0 energy.""" if amount > 0: self.energy = min(self.energy + amount, self.energy_max) return True else: if self.proctor: return True ## Never mind. self.energy += amount ## Add negative value. return self.energy > 0 def FillEnergy(self): self.energy = self.energy_max def ChangeEnergy(self,amount=0): """Gain/lose energy. Returns True if I'm still at >0 energy.""" if amount > 0: self.energy = min(self.energy + amount, self.energy_max) return True else: if self.invincible: return True ## Never mind. self.health += amount ## Add negative value. return self.health > 0 def FillHealth(self): self.health = self.health_max class Tile: """A grid square that's part of a zone.""" def __init__(self,coords,terrain="w",**options): self.coords = coords self.SetTerrain(terrain) self.height = options.get("height",0) ## Decimeters (.1m) self.warmth = options.get("warmth",25.0) ## C self.contents = [] ## A stack of entities, bottom to top def SetTerrain(self,terrain): if not terrain in TERRAINS: terrain = TERRAIN_ABBREVS[terrain] self.terrain = terrain self.passable = TERRAINS[terrain]["passable"] self.cost = TERRAINS[terrain].get("cost",0) self.deadly = TERRAINS[terrain].get("deadly",False) def Put(self,entity): """Put entity here atop whatever's already here. Unrealistic in that it allows an infinite amount of stuff to be stacked in one tile. Atop a marshmallow. Or a Character. I considered having stacks spread out, but that's too complex for my purposes. Also note that having an anvil fall on your head currently causes no harm.""" self.contents.append(entity) entity.coords = self.coords return True def Take(self,entity=None): """Remove entity from this tile. Return entity/None.""" if not entity: ## Take whatever is on top, if anything. if self.contents: return self.contents.pop() else: ## Return this entity if it's there, or None. if entity in self.contents: self.contents.remove(entity) return entity else: return None class Zone: """A region of space, holding and managing its contents. The landscape is stored as several parallel data layers: Terrain: Type of ground. Height: Can affect graphics and movement. Wall: Any spot marked as True here is totally impassible. Script: Triggers a script when something tries to move into it. """ def __init__(self,**options): self.name = options.get("name","World") self.size_x, self.size_y = options.get("size",ZONE_SIZE) self.mapfile_name = options.get("mapfile") if self.mapfile_name: self.LoadMap(self.mapfile_name) random_map = options.get("random",True) self.tiles = [] self.CreateBlankGrid() ## Entity IDs ## A guaranteed-unique-per-session ID number for each Entity. ## By default, the IDs are not revealed through sense data. self.highest_ID = 0 ## Dictionary of all entities in the world by ID#. Includes Characters. self.entities = {} ## A list of all Characters. self.characters = [] ## Definitions of recurring items, eg. if you want 10 identical rocks. self.object_types = {} ## Any special rules that apply. self.rules = options.get("rules",{}) ## The log is stored as a list of (tag,text) pairs. self.log = [] self.echo_log = options.get("echo_log",True) ## Print when logging? ## Starting area within a map: X,Y,width, height (counting southward). ## By default, Characters are randomly placed within this area. ## They will be placed randomly if there is no place to put them here. self.starting_area = options.get("starting_area",(45,45,10,10)) ## Load zone self.name = "Zone" ## The zone's actual name, not the filename self.zone_file_name = options.get("zone_file_name") if self.zone_file_name: self.LoadLevel(self.zone_filename) def Log(self,tags,text=""): """Record an event.""" self.log.append((time.strftime(TIME_SHORTFORMAT,time.localtime()),tags,str(text))) if self.echo_log: print "["+tags+"] "+str(text) def WriteLog(self,filename="log.txt"): if not filename.endswith(".txt"): filename += ".txt" f = open(filename,"w") f.write("------ Log file for worldsim \"Morningside\" ------\n") for entry in self.log: f.write(entry[0]+" ["+entry[1].title()+"] "+str(entry[2])+"\n") f.write("Log saved at time: "+time.strftime(TIME_FORMAT,time.localtime())) f.close() def MakeSeed(self): """Return an easy-to-remember seed for random.seed().""" seed = "" values = "0123456789ABCDEF" random.seed() for n in range(4): seed += values[random.randint(0,15)] return seed def CreateBlankGrid(self): """Make a blank Tile list.""" self.tiles = [] for x in range(self.size_x): self.tiles.append([]) for y in range(self.size_y): self.tiles[-1].append( Tile((x,y),"g") ) def SetRandomTerrain(self,seed=None): """Establish the terrain.""" if seed: random.seed(seed) for x in range(self.size_x): for y in range(self.size_y): n = random.randint(0,99) if n < 20: terrain = "water" elif n < 30: terrain = "dirt" elif n < 40: terrain = "sand" elif n < 46: terrain = "forest" else: terrain = "grass" self.tiles[x][y].SetTerrain(terrain) for t in range(3): for x in range(1,self.size_x-1): for y in range(1,self.size_y-1): n = random.randint(0,99) if n < 30: self.tiles[x][y].SetTerrain(self.tiles[x-1][y].terrain) elif n < 60: self.tiles[x][y].SetTerrain(self.tiles[x][y-1].terrain) elif n < 80: self.tiles[x][y].SetTerrain(self.tiles[x+1][y].terrain) else: self.tiles[x][y].SetTerrain(self.tiles[x][y+1].terrain) def PickRandomTileInArea(self,area=(0,0,10,10),return_tile=False): """Pick a random empty spot within (x,y,w,h).""" tries = 0 x_min = area[0] x_max = x_min + area[2] y_min = area[1] y_max = x_max + area[3] t = [] ## All tiles not tried in this region. for x in range(x_min,x_max+1): for y in range(y_max,y_max+1): t.append((x,y)) while t: xy = t[ random.randint(0,len(t)-1) ] if not self.tiles[xy[0]][xy[1]].contents: ## This tile is empty -- good. if return_tile: return self.tiles[xy[0]][xy[1]] else: return xy ## Occupied. Remove it from the candidates and try again. t.remove(xy) ## We're still here? Then every possible tile is occupied. Bail. return False def Reset(self): """Reload the map and remove all entities and characters.""" self.tiles = [] self.CreateBlankGrid() self.entities = {} self.characters = [] self.highest_ID = 0 if self.level_name: self.LoadLevel(self.level_name) def LoadZone(self,filename="default_zone.txt"): """Set the terrain and characters based on a file. Each line in a level file should follow one of these conventions: # starts with "#" -- Comment. Is ignored. key:value -- sets one of the level's features such as the tile layout. *object_name:param1=spam,param2=bacon,param3=eggs -- Defines an object that will be put on the board. Also, any line ending with ; will be considered continued on the next line. (";\n" --> "") This feature makes it easier to type in level layouts with a text editor if you want. When adding objects, you can use the parameter "copy=True" to imitate an existing object. Eg. "*rock:coords=4,2/copy=True" will make a rock identical to a rock you've already described, but for any parameters you've specified (here, the coords). """ ## First, clear existing data. self.tiles = [] self.entities = [] self.characters = [] self.rules = {} ## Extract data from a text file. Each line is eg. "key: value". self.Log("setup","Loading zone from \""+filename+"\".") if not "." in filename: filename += ".txt" self.zone_file_name = filename filename = os.path.join(ZONE_DIRECTORY,filename) try: f = open(filename,"r") text = f.read() f.close() except: raise SystemError("The map file "+str(filename)+" couldn't be read.") ## A little cleanup. text = text.replace("_"," ").replace(";\n","") ## Break into lines and form a dictionary (d) of key:value pairs. lines = text.split("\n") d = {} entities_to_make = [] ## We'll make them after this part. for line in [l for l in lines if l and l[0] != "#"]: kv = line.split(":") key = kv[0].rstrip(" ").lower().replace("_"," ") value = kv[1].lstrip(" ") if key.startswith("*"): ## Special case: Object description entities_to_make.append((key[1:],value)) else: d[key] = value ## Now we start processing certain info from d. self.name = d.get("name","Untitled Zone") self.Log("setup","Name: "+self.name) value = d.get("notes") if value: self.Log("setup","Notes: "+value) ## Seed value: Sets randomness generator. Don't use w/ a random layout. value = d.get("seed") if value: self.seed = value else: self.seed = self.MakeSeed() random.seed(self.seed) self.Log("setup","Seeded the RNG with this code: "+self.seed) value = d.get("size") if value: s = tuple(value.split(",")) self.size_x, self.size_y = (int(s[0]),int(s[1])) self.starting_area = d.get("starting area",(0,0,self.size_x,self.size_y)) value = d.get("energy") if value: self.rules["energy"] = True self.rules["starting energy"] = int(d.setdefault("starting energy",100)) self.rules["energy drain rate"] = int(d.setdefault("energy drain rate",1)) else: self.rules["energy"] = False value = d.get("goal") if value: self.rules["goal"] = value ## Now build a set of Tile objects based on terrain and height. terrainmap = d.get("terrainmap") heightmap = d.get("heightmap").split(",") if terrainmap.startswith("random"): ## Special: Set the land to random flat terrain. seed = self.seed r = terrainmap.split(" ") if (len(r) > 1) and r[1]: seed = r[1].lower() ## Use second word given for a seed, if one is given. ## Eg. if d["terrain"] = "random squirrel", seed = "squirrel". self.name = "* "+seed+" *" self.CreateBlankGrid() self.SetRandomTerrain(seed) else: try: assert len(terrainmap) == self.size_x*self.size_y except: raise SystemError("Terrain map length doesn't match zone's stated size. Should be a string, x*y long.") try: assert len(heightmap) == self.size_x*self.size_y except: raise SystemError("Heightmap length doesn't match sone's stated size. Should be a comma-separated list, x*y long.") terrain_types = TERRAIN_ABBREVS.keys() self.tiles = [] for x in range(self.size_x): self.tiles.append([]) for y in range(self.size_y): terrain = terrainmap[(y*self.size_x) + x] h = heightmap[(y*self.size_x) + x] if not terrain in terrain_types: self.Log("error,io","I don't recognize the terrain type '"+terrain+"'; allowed types are: "+str(terrain_types)) self.tiles[-1].append( Tile((x,y),TERRAIN_ABBREVS[terrain],height=h) ) ## Place entities on those Tiles. for e in entities_to_make: ## Format: "*name:color=?,shape=?,other_parameter=?" ## Example: "*acorn:,color=brown,shape=round" ## Note: Text has already had any "_" replaced with " ". params = {"name":e[0],"coords":"?"} keys_that_are_for_numbers = ["food value","size"] keys_that_are_for_bool = [] p = e[1].replace(", ",",").replace(":","=").split("/") for item in p: i = item.split("=") params[i[0]] = i[1] if i[0] == "coords": if i[1].startswith("?"): ## Special: put it randomly in a certain area. if len(i) == 1: ## Just a ? was given area = (0,0,self.size_x,self.size_y) else: a = i[1:].split(",") area = [int(a[0]),int(a[1]),int(a[2]),int(a[3])] params["coords"] = self.PickRandomTileInArea(area) else: coords = i[1].split(",") params["coords"] = [int(coords[0]),int(coords[1])] elif i[0] in keys_that_are_for_numbers: params[i[0]] = int(i[1]) elif i[0] in keys_that_are_for_bool: if i[1].lower() == "true": params[i[0]] = True else: params[i[0]] = False if params.get("copy"): ## Make this object based on one already defined. try: copy = self.object_types[params["name"]].copy() except: raise SystemError("The zone file requested a copy of an object called '"+params["name"]+", but that object wasn't already defined.") copy.update(params) params = copy else: ## Store this object's stats in case copies are requested. self.object_types[params["name"]] = params ## Now we actually build the object and add it to the world. entity = self.Create(**params) ## Set up some optional rules. if self.rules: self.Log("setup","Rules: "+str(self.rules)) if "goal" in self.rules: ## Replace it with a parsed version. goal = self.rules["goal"].split(" ") verb = goal[1] if verb == "survive": ## Alive after N turns. goal_parsed = {"rule":"survive","time":goal[0]} elif verb == "absent": ## No entity like this exists. goal_parsed = {"rule":"absent","subject":goal[0]} elif verb == "holding": ## Subject holds target entity. goal_parsed = {"rule":"holding","subject":goal[0],"target":goal[2]} elif verb == "at": goal_parsed = {"rule":"at","subject":goal[0],"where":goal[2]} self.rules["goal"] = goal_parsed self.Log("setup","Finished loading zone \""+self.name+"\".") def Create(self,**options): """Create an Entity and place it in the world.""" coords = options.get("coords",(0,0)) if self.tiles[coords[0]][coords[1]].contents: self.Log("error","Can't create an Entity at coords "+str(coords)+"; already occupied.") return None options["ID"] = str(self.highest_ID).zfill(2) ## eg. "01" self.highest_ID += 1 entity = Entity(**options) self.entities[entity.ID] = entity self.PlaceAt(entity,coords) self.Log("setup","Created an object called "+entity.name+".") return entity def CreateCharacter(self,**options): """Like Create. Characters are placed on a specified spot (if empty) or a random empty spot in the zone's starting area.""" coords = options.get("coords") if coords and self.tiles[coords[0]][coords[1]].contents: ## Character requested this spot, but it's not empty. if options.get("must_be_here"): self.Log("error","Character demanded to be at coords "+str(coords)+" but the spot was occupied.") return False else: coords = None if not coords: ## No desired coords, or coords unavailable, or selection disallowed ## Assign a random spot in the starting area. coords = self.PickRandomTileInArea(self.starting_area) if not coords: self.Log("error","Can't find a place to put a Character in the starting area!") return False if self.tiles[coords[0]][coords[1]].contents: self.Log("error","Can't create a Character at coords "+str(coords)+"; already occupied.") return False if self.rules.get("energy"): options["energy"] = self.rules["starting energy"] options["ID"] = str(self.highest_ID).zfill(2) ## eg. "01" self.highest_ID += 1 character = Character(**options) self.entities[character.ID] = character self.PlaceAt(character,coords) self.characters.append(character) character.SenseSystemMessage("you: "+character.ID) character.SenseSystemMessage("zone_size: "+str(self.size_x)+","+str(self.size_y)) character.SenseSystemMessage("rules: "+str(self.rules)) self.Log("setup","Created a new character, \""+character.name+"\", at location: "+str(character.coords)+".") return character def PlaceAt(self,entity,coords): """Put this entity at these coords. Works whether or not the entity already has a location.""" x,y = coords if entity.coords: ## Remove it from its current tile. self.tiles[x][y].Take() OK = self.tiles[x][y].Put(entity) return OK def Move(self,entity,direction): """Try to move in this direction (n/s/e/w). Return T/F.""" ## Where is the entity trying to go? x,y = entity.coords[0], entity.coords[1] if direction == "n": y -= 1 elif direction == "s": y += 1 elif direction == "w": x -= 1 elif direction == "e": x += 1 else: ## Syntax error. self.Log("error","Syntax: 'go n/s/e/w'") return False ## Is the destination outside the world boundaries? if (x<0) or (y<0) or (x>=self.size_x) or (y>=self.size_y): self.Log("fail","Couldn't move "+direction+"; out of bounds.") return False ## Is the destination a passable type of terrain (ie. not a wall)? if not self.tiles[x][y].passable: self.Log("fail","Couldn't move "+direction+"; impassible.") return False ## Is the destination occupied? if self.tiles[x][y].contents: self.Log("fail","Oof! The destination was occupied.") return False ## It's OK to move. Do it! self.tiles[entity.coords[0]][entity.coords[1]].Take() OK = self.tiles[x][y].Put(entity) if not OK: self.Log("error","Unknown error.") raise SystemError("Movement somehow blocked in empty tile!") self.Log("success","Moved "+direction+".") ## Successful movement. Check hazards... terrain = self.tiles[x][y].terrain deadly = TERRAINS[terrain].get("deadly") if deadly: self.Log("fail",entity.name+" walked into deadly "+terrain+" terrain!") self.Kill(entity) if self.rules.get("energy"): cost = TERRAINS[terrain].get("cost") survived = entity.ChangeEnergy(-cost) if not survived: self.Kill(entity) return True def Kill(self,character): """Destroy an entity. If the entity was alive, ie. a character, it can no longer play, but it stays on the board and is treated like an object. Non-living entities that're destroyed get removed from play.""" if character.alive: self.Log("death",character.name+" has died.") self.AllSense(character.ID,"died") character.energy = 0 character.alive = False else: self.Log("error","The Kill function should only be used for living entities.") def CheckGoal(self): """Send a victory message if a goal condition is met.""" goal = self.rules.get("goal") if not goal: return ## N/A victory = None try: rule = goal["rule"] if rule == "absent": ## No entitiy by this name exists. victory = True subject = goal for e in self.entities: if e.name == subject: victory = False break elif e.held_item and (e.held_item.name == subject): victory = False break elif rule == "holding": ## Subject is holding named entity. holder = None for e in self.entities: if e.name == subject or (subject == "player" and e.alive): holder = e break if not holder: return if holder.held_item and (holder.held_item.name == goal.get("target")): victory = True elif rule == "at": ## Player, or an entity by this name, is at X,Y. subject = goal["subject"] where = tuple(goal["where"]) tile = self.tiles[where[0]][where[1]] if not tile.contents: return ## Nothing is here. if tile.contents.name == subject or (subject == "player" and tile.contents.alive): victory = True elif rule == "alive": ## Player is alive after N turns. time = goal["time"] if self.time >= time: victory = True except: raise SystemError("Couldn't understand the goal: "+str(goal)+". See instructions for help.") if victory: selfLog("system","The goal is met. You win!") for c in self.characters: c.SenseSystemMessage("victory") def Take(self,entity,direction): """Try to take a non-living entity in this direction. Return T/F.""" ## Is the taker already carrying something? hand = entity.GetFreeHand() if not hand: self.Log("fail","No free hands.") return False ## Where is the entity trying to take from? x,y = entity.coords[0], entity.coords[1] if direction == "n": y -= 1 elif direction == "s": y += 1 elif direction == "w": x -= 1 elif direction == "e": x += 1 else: ## Syntax error. self.Log("error","Syntax: 'take n/s/e/w'") return False ## Is the target outside the world boundaries? if (x<0) or (y<0) or (x>=self.size_x) or (y>=self.size_y): self.Log("fail","Couldn't take from "+direction+"; out of bounds.") return False ## Is the target tile empty? if not self.tiles[x][y].contents: self.Log("fail","Nothing to take there.") return False ## Is the entity living or otherwise untakeable? if self.tiles[x][y].contents[-1].alive or self.tiles[x][y].contents[-1].anchored: self.Log("fail","You can't take this.") return False ## OK! target_entity = self.tiles[x][y].Take() ## Target's coords now == None. entity.Get(target_entity,hand) self.Log("success","Took #"+target_entity.ID+ " "+target_entity.name+" from "+direction+".") return True def Drop(self,entity,direction,hand=None): """Try to drop a held item from this character. Return T/F.""" if not entity.alive: self.Log("fail",entity.name+" isn't a living entity and can't drop stuff.") return False if not hand: hand = entity.GetFullHand() if not hand: self.Log("fail",entity.name+" isn't holding anything.") return False dropped_item = entity.hands.get(hand) if not dropped_item: self.Log("fail",entity.name+" isn't holding anything in that hand.") return False ## Where is the entity trying to drop to? x,y = entity.coords[0], entity.coords[1] if direction == "n": y -= 1 elif direction == "s": y += 1 elif direction == "w": x -= 1 elif direction == "e": x += 1 else: ## Syntax error. self.Log("errorSYN","Syntax: 'drop n/s/e/w'") return False ## Is the target outside the world boundaries? if (x<0) or (y<0) or (x>=self.size_x) or (y>=self.size_y): self.Log("fail","Couldn't move "+direction+"; out of bounds.") return False ## Target tile must be passable. if not self.tiles[x][y].passable: self.Log("fail","Can't drop items onto impassible terrain.") return False ## Target tile must be empty. if self.tiles[x][y].contents: item = self.tiles[x][y].contents self.Log("fail","Target tile must be empty.") return False ## OK! entity.Drop(hand) self.tiles[x][y].Put(dropped_item) self.Log("success","Dropped #"+dropped_item.ID+ " "+dropped_item.name+" to "+direction+".") return True def Test(self): petunias = self.Create(name="Bowl of Petunias",coords=(4,2)) assert petunias.coords == (4,2) assert petunias in self.tiles[4][2].contents assert self.tiles[4][3].contents == [] self.Move(petunias,"e") assert petunias.coords == (5,2) self.Move(petunias,"s") assert petunias.coords == (5,3) whale_error = self.CreateCharacter(name="Whale",coords=(5,3),must_be_here=True) if whale_error: raise SystemError("Cetacean Superposition Error.") whale = self.CreateCharacter(name="Whale",coords=(5,4)) assert whale in self.tiles[5][4].contents assert whale.coords == (5,4) assert self.Move(whale,"n") == False assert self.Move(whale,"w") == True self.Move(whale,"e") assert self.Take(whale,"n") assert whale.hands["hand_l"] ## A whale holding flowers in its left hand assert whale.GetFreeHand() == "hand_r" assert self.Drop(whale,"s") assert not whale.GetFullHand() assert self.tiles[5][5].contents print "Test complete. Log follows; errors are intentional." for line in self.log: print line def AllSense(self,sender_ID,what): """Tell all characters about an event.""" for c in self.characters: c.Sense(sender_ID,what) def SenseWorld(self,character): """Return data representing what can be seen. For now, assume that any character can see everything. That means unlimited vision range, plus awareness of entities held by others.""" if character.vision_range: pass ## NYI else: ## Character can see everything entities = [] text = "$ layout: " ## for x in range(self.size[0]): ## for y in range(self.size[1]): ## text += self.world[x][y].terrain[0] ## entity = self.world[x][y].contents ## if entity: ## e_text = "@ "+str(x)+","+str(y)+": "+str(entity.ID)+" "+entity.name ## if entity.alive: ## e_text += " Alive" ## entities.append(e_text) for y in range(self.size[1]): for x in range(self.size[0]): text += self.world[x][y].terrain[0] entity = self.world[x][y].contents if entity: e_text = "@ "+str(x)+","+str(y)+": "+str(entity.ID)+" "+entity.name if entity.alive: e_text += " Alive" entities.append(e_text) ## Make the layout slightly human-readable while still being 1 line. text += "|" ## Strip | when reading this output. text += "\n" for entry in entities: text += entry+"\n" return text class DemoInterface: def __init__(self,**options): self.world = options.get("world") self.size_x = self.world.size_x self.size_y = self.world.size_y ## Build dummy graphical tiles, just colored squares. self.tiles = {} ## A set of graphic tiles to display self.tile_size = 4 g = pygame.surface.Surface((self.tile_size,self.tile_size)) ## "grass" g.fill((0,255,0)) w = pygame.surface.Surface((self.tile_size,self.tile_size)) ## "water" w.fill((0,0,255)) s = pygame.surface.Surface((self.tile_size,self.tile_size)) ## "sand" s.fill((240,240,127)) f = pygame.surface.Surface((self.tile_size,self.tile_size)) ## "forest" f.fill((32,200,32)) d = pygame.surface.Surface((self.tile_size,self.tile_size)) ## "dirt" d.fill((200,180,127)) p = pygame.surface.Surface((self.tile_size,self.tile_size)) pygame.draw.circle(p,(255,0,255),(self.tile_size/2,self.tile_size/2),10) self.tiles["grass"] = g self.tiles["water"] = w self.tiles["sand"] = s self.tiles["forest"] = f self.tiles["dirt"] = d self.tiles["player"] = p self.BuildLandscape() def BuildLandscape(self): """Build a list of references to the tiles based on zone's terrain.""" self.landscape = [] ## Which tile goes where for x in range(self.size_x): self.landscape.append([]) for y in range(self.size_y): data = self.tiles[ self.world.tiles[x][y].terrain ] self.landscape[-1].append(data) def Draw(self): for x in range(self.size_x): for y in range(self.size_y): t = self.landscape[x][y] screen.blit(t,((x*self.tile_size),(y*self.tile_size))) pygame.display.update() def Logic(self): pass def MainLoop(self): while True: for event in pygame.event.get(): if event.type == KEYDOWN: return self.Draw() self.Logic() ##### OTHER FUNCTIONS ##### ##### AUTORUN ##### if __name__ == "__main__": ## Do the following if this code is run by itself: z = Zone() ## import pygame ## screen = pygame.display.set_mode((400,400)) ## d = DemoInterface(world=z)