#!/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, 2D -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 TERRAIN_FULL_TO_ABBREVS = {} for t in TERRAIN_ABBREVS: TERRAIN_FULL_TO_ABBREVS[ TERRAIN_ABBREVS[t] ] = 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.color = options.get("color","grey") 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.size = options.get("size",1.0) ## Occupies tile completely at 1.0 ## Shape self.shape = options.get("shape","block") ## One word, usu. a "geon" ## 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 = [] ## 2-int list; 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.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] 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) options.setdefault("shape","anthro") options.setdefault("color","pink") ## Output self.command = None ## A string that'll be acted on when time advances. self.command_history = [] ## Input self.sensed_events = [] ## Things I've sensed & should report. self.system_messages = [] self.client_type = options.get("client_type") ## [p]layer or [v]iewer ## Special options self.invincible = options.get("invincible",False) self.cheat_access = options.get("cheat_access",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] 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.invincible: return True ## Never mind. self.energy += amount ## Add negative value. return self.energy > 0 def FillEnergy(self): self.energy = self.energy_max def ChangeHealth(self,amount=0): """Gain/lose health. 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 = list(coords) ## 2D, in list format self.SetTerrain(terrain) self.warmth = options.get("warmth",25.0) ## C self.contents = None ## None, or an Entity/Character 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 if not already occupied.""" if self.contents: return False self.contents = entity entity.coords = [self.coords[0],self.coords[1]] return True def Take(self): """Remove entity from this tile. Return entity/None.""" entity = self.contents ## Which might be None self.contents = None return entity class Zone: """A region of space, holding and managing its contents. The landscape is stored as several parallel data layers: Terrain: Type of ground. 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.time = 0 self.size_x, self.size_y = options.get("size",ZONE_SIZE) 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",{"energy":False}) ## The log is stored as a list of (tag,text) pairs. self.log = [] self.log_max_length = 5 self.log_echo = options.get("log_echo",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.mapfile_name = options.get("mapfile") if self.mapfile_name: self.LoadZone(self.mapfile_name) self.Log("Zone created on "+time.strftime(TIME_FORMAT+" "+TIME_SHORTFORMAT)) def Log(self,text,tags=""): self.log.append((time.strftime("%H:%M"),text,tags)) if self.log_echo: if tags: print "["+tags+"] "+str(text) else: print str(text) if len(self.log) > self.log_max_length: self.WriteLog() def WriteLog(self): log_name = "Liberty Server Log.txt" exists = os.path.exists(log_name) if exists: f = open(log_name,"a") f.write("\n\n") else: f = open(log_name,"w") for line in self.log: if line[2]: f.write("["+line[0]+"]["+line[2]+"] "+line[1]+"\n") else: f.write("["+line[0]+"] "+line[1]+"\n") f.close() self.log = [] def MakeSeed(self): """Return an easy-to-remember seed for random.seed().""" seed = "" values = "0123456789ABCDEF" ## 65536 unique values 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 random flat 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),land_only=True): """Pick random empty coords within (x,y,w,h), inclusive.""" x_min = area[0] x_max = x_min + area[2] y_min = area[1] y_max = y_min + area[3] ## List all tile coordinates in this region, and pick from that list. t = [] for x in range(x_min,x_max): for y in range(y_min,y_max): t.append([x,y]) ## Pick a tile. If empty, use it; else remove it from the list. while t: n = random.randint(0,len(t)-1) x, y = t[n] ## Is this tile empty and passable? if (self.tiles[x][y].passable) and not self.tiles[x][y].contents: if (self.tiles[x][y].terrain != "water") or not land_only: return (x,y) ## Occupied. Remove it from the candidates and try again. t = t[:n] + t[n+1:] ## We're still here? Then no valid tiles are available. Bail. return False def Reset(self): """Reload the map and remove all entities and characters.""" self.time = 0 self.tiles = [] self.CreateBlankGrid() self.entities = {} self.characters = [] self.highest_ID = 0 self.rules = {"energy":False} 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("Loading zone from \""+filename+"\".","setup") print "Loading zone: "+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("Seeded the RNG with this code: "+self.seed,"setup") 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-1,self.size_y-1)) 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. terrainmap = d.get("terrainmap") 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". if not self.name: self.name = "* "+seed+" *" self.Log("Building terrain and re-seeding RNG with this code: "+seed,"setup") 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.") 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] 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]) ) ## 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("Rules: "+str(self.rules),"setup") 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("Finished loading zone \""+self.name+"\".","setup") 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(3) ## eg. "001" self.highest_ID += 1 entity = Entity(**options) self.entities[entity.ID] = entity self.PlaceAt(entity,coords) self.Log("Created an object called "+entity.name+".","setup") 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("Character demanded to be at coords "+str(coords)+" but the spot was occupied.","error") 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("Can't find a place to put a Character in the starting area!","error") return False print "Coordinates for placement: "+str(coords) if self.tiles[coords[0]][coords[1]].contents: self.Log("Can't create a Character at coords "+str(coords)+"; already occupied.","error") return False if self.rules.get("energy"): options["energy"] = self.rules["starting energy"] options["ID"] = str(self.highest_ID).zfill(3) ## eg. "001" 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("Created a new character, \""+character.name+"\", at location: "+str(character.coords)+".","setup") self.HandleNewCharacter(character) ## Optional reaction to the newcomer. return character def HandleNewCharacter(self,character): """Override if you want to do something like display a newcomer's stats.""" pass def PlaceAt(self,entity,coords): """Put this entity at these coords. Works whether or not the entity already has a location.""" if entity.coords: ## Remove it from its current tile. self.tiles[entity.coords[0]][entity.coords[1]].Take() OK = self.tiles[coords[0]][coords[1]].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("Syntax: 'go n/s/e/w'","error") 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("Couldn't move "+direction+"; out of bounds.","fail") return False ## Is the destination a passable type of terrain (ie. not a wall)? if not self.tiles[x][y].passable: self.Log("Couldn't move "+direction+"; impassible.","fail") return False ## Is the destination occupied? if self.tiles[x][y].contents: self.Log("Oof! The destination was occupied.","fail") 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("Unknown error.","error") raise SystemError("Movement somehow blocked in empty tile!") self.Log("Moved "+direction+".","success") ## Successful movement. Check hazards... terrain = self.tiles[x][y].terrain deadly = TERRAINS[terrain].get("deadly") if deadly: self.Log(entity.name+" walked into deadly "+terrain+" terrain!","fail") 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 RemoveEntityFromWorld(self,entity): """Remove an entity from play without declaring it dead.""" if entity.alive: ## Needs to be removed from self.characters too. self.characters.remove(entity) del self.entities[entity.ID] if entity.coords: ## Remove it from its current tile. self.tiles[entity.coords[0]][entity.coords[1]].Take() 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(character.name+" has died.","event,death") self.AllSense(character.ID,"died") character.energy = 0 character.alive = False else: self.Log("The Kill function should only be used for living entities.","error") 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("No free hands.","fail") 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("Syntax: 'take n/s/e/w'","error") 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("Couldn't take from "+direction+"; out of bounds.","fail") return False ## Is the target tile empty? if not self.tiles[x][y].contents: self.Log("Nothing to take there.","fail") return False ## Is the entity living or otherwise untakeable? if self.tiles[x][y].contents.alive or self.tiles[x][y].contents.anchored: self.Log("You can't take this.","fail") return False ## OK! target_entity = self.tiles[x][y].Take() ## Target's coords now == None. entity.Get(target_entity,hand) self.Log("Took #"+target_entity.ID+ " "+target_entity.name+" from "+direction+".","success") return target_entity 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(entity.name+" isn't a living entity and can't drop stuff.","fail") return False if not hand: hand = entity.GetFullHand() if not hand: self.Log(entity.name+" isn't holding anything.","fail") return False dropped_item = entity.hands.get(hand) if not dropped_item: self.Log(entity.name+" isn't holding anything in that hand.","fail") 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("Syntax: 'drop n/s/e/w'","syntax error") 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("Couldn't move "+direction+"; out of bounds.","fail") return False ## Target tile must be passable. if not self.tiles[x][y].passable: self.Log("Can't drop items onto impassible terrain.","fail") return False ## Target tile must be empty. if self.tiles[x][y].contents: item = self.tiles[x][y].contents self.Log("Target tile must be empty.","fail") return False ## OK! entity.Drop(hand) self.tiles[x][y].Put(dropped_item) self.Log("Dropped #"+dropped_item.ID+ " "+dropped_item.name+" to "+direction+".","success") return True def Test(self): """Sanity test of basic movement and action.""" 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. There are two modes: -Unlimited vision: Set by character having a vision_range of 0. There's a single "layout" line giving one-letter descriptions of the terrain at every point on the map, plus one line per entitiy. -Limited vision: Set by a nonzero vision_range. There's one line headed by * for each visible piece of terrain, plus one line per entity. eg. "* 4,2 f" for "forest at +4,+2 relative". """ if character.vision_range: x, y = character.coords t = self.tiles[x][y].terrain tiles_to_see = GetCoordsInRange((x,y),character.vision_range) entities = [] text = "" for t in tiles_to_see: rel_x = str(t[0] - x) rel_y = str(t[1] - y) text += "^ "+rel_x+","+rel_y+": "+self.tiles[t[0]][t[1]].terrain[0]+"\n" entity = self.tiles[t[0]][t[1]].contents if entity: ## e_text = "@ "+str(x)+","+str(y)+": "+str(entity.ID)+" "+entity.name ## if entity.alive: ## e_text += " Alive" ## entities.append(e_text) ## Give more sense info about entities. ## Sample: "@ 4,2: 03 acorn round brown 2 i" e_text = "@ "+str(rel_x)+","+str(rel_y)+": "+str(entity.ID)+" "+entity.name+" "+entity.color+" "+entity.shape+" "+str(entity.size) if entity.alive: e_text += " a" else: e_text += " i" entities.append(e_text) for entry in entities: text += entry+"\n" return text else: ## Character can see everything entities = [] text = "$ layout: " for y in range(self.size[1]): for x in range(self.size[0]): text += self.tiles[x][y].terrain[0] entity = self.tiles[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 def DoCommand(self,sender,command): """Act on a command string given by a Character. Commands are in the form: "physical action / speech".""" self.Log(sender.name+": "+command,"action") sender.command = "" phys = command if "/" in command: command = command.replace(" /","/").replace("/ ","/") phys, speech = command.split("/") if speech: self.Log(sender.name+": \""+speech+"\"","speech") self.AllSense(sender.ID,"says "+speech) ## Everyone hears it. if not phys: return ## No physical action chosen. words = phys.split(" ") params = len(words) - 1 ## For asking whether the right # of options was given. if not words: return verb = words[0] ## Wait/Idle: Do nothing. if verb in ("wait","idle"): ## self.Log(sender.name+" waits.","success") ## self.AllSense(sender.ID,"idle ok") pass ## Go/Move: Take a step. elif verb in ("go","move"): if not params: self.Log("Syntax: 'go n/s/e/w'","syntax error") return ok = self.Move(sender,words[1]) if ok: self.AllSense(sender.ID,"move ok") else: self.AllSense(sender.ID,"move fail") ## Take/Get: Pick up an adjacent object. elif verb in ("get","take"): if not params: self.Log("Syntax: 'get [dir]'","syntax error") taken_item = self.Take(sender,words[1]) if taken_item: sender.Sense(sender.ID,"get ok") self.AllSense(sender.ID,"gets "+taken_item.name) self.Log(sender.name+" gets "+taken_item.name+" from "+words[1]+".","success") else: self.AllSense(sender.ID,"get fail") ## Drop/Put: Drop currently held object. elif verb in ("drop","put"): if not params: self.Log("Syntax: 'drop [dir]'","syntax error") else: item_name = None if sender.held_item: item = sender.held_item success = self.Drop(sender,words[1]) if success: self.AllSense(sender.ID,"drops "+item.ID) self.Log(sender.name+" drops "+item.ID+" to "+words[1]+".","success") else: self.AllSense(sender.ID,"drop fail") ## ## Look: Get sense info on an entity, referenced by ID#. ## elif verb == "look": ## success = True ## if not params: ## self.Log("Syntax: 'look [ID]'","syntax error") ## success = False ## else: ## ID = words[1].zfill(2) ## Make the ID two digits, eg. "01". ## if not ID in self.entities: ## self.Log("ID# is invalid.","error") ## success = False ## if success: ## e = self.entities[ID] #### text = "see shape:"+e.shape+"/color:"+e.color+"/size:"+str(e.size) ## text = "see shape:"+e.shape+"/color:"+e.color+"/size:"+str(e.size) ## sender.Sense(e.ID,text) ## if success: ## self.AllSense(sender.ID,"looks_at "+ID) ## self.Log(sender.name+" looks at "+self.entities[ID].ID+" "+self.entities[ID].name+".","success") ## else: ## self.AllSense(sender.ID,"look fail") ## Eat: Eat a held item. elif verb == "eat": success = False item = sender.held_item if not item: self.Log("Must be holding an item to eat it.","error") else: food_value = item.food_value if food_value > 0: success = True sender.ChangeEnergy(food_value) elif food_value < 0: success = True sender.ChangeEnergy(food_value) """If this kills the character, that'll be noticed when energy gets drained later in this function.""" else: self.Log("The "+item.name+" is inedible.","error") if success: sender.held_item = None del self.entities[item.ID] self.Log(sender.name+" eats "+item.name+". Food value: "+str(food_value),"success") self.AllSense(sender.ID,sender.name+" eats "+item.ID) else: self.AllSense(sender.ID,"eat fail") elif verb == "reset": self.Reset() ## Unrecognized verb. else: self.Log("Unrecognized command.","error") return def HandleTurnBasedEvents(self): for character in self.characters: ## Energy rule if self.rules["energy"]: print "Draining energy from "+character.name drain = self.rules["energy drain rate"] survived = character.ChangeEnergy(-drain) if not survived: self.Kill(character) def RunTurn(self): """Advance time by one turn in the world. This means executing character commands, doing any maintenance tasks like draining energy over time, preparing and sending output, and advancing the world's timer.""" ## Has everyone entered a command, or are we skipping snoozers? ready = True if self.wait_for_everybody: for c in self.characters: if not c.command: ready = False if ready: ## Now, execute all entered commands. for c in self.characters: if c.command: self.DoCommand(c,c.command) self.HandleTurnBasedEvents() ## Eg. energy drain. ## Report their energy level, if applicable. if self.rules["energy"]: for character in self.characters: character.SenseSystemMessage("energy: "+str(character.energy)) ## Write sense output files, including the news. for character in self.characters: self.WriteOutputFile(character) ## Server upkeep. self.time += 1 class DemoInterface: """Demonstrate a graphical view of the game world.""" 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 ##### def GetCoordsInRange(point=(0,0),radius=1): """Return a list of (x,y) tuples for points within some radius. This is orthogonal-based, ie. diagonals are 2 units away. Used to decide which grid squares are visible, reachable &c.""" result = [] for y in range(point[1]-radius,point[1]+radius+1): width = 2 * (radius - abs(point[1]-y)) + 1 x_radius = radius - abs(point[1]-y) for x in range(point[0]-x_radius,point[0]+x_radius+1): result.append((x,y)) return result ##### AUTORUN ##### if __name__ == "__main__": ## Do the following if this code is run by itself: ## Demonstrate a graphical display handled within this program. ## Note that you can also write an external viewer that logs in as one. z = Zone(mapfile="demo") import pygame from pygame.locals import * screen = pygame.display.set_mode((400,400)) d = DemoInterface(world=z) d.MainLoop()