""" A metagame: Lays out the the MAP and the QUESTS for a meta-game, or "world". """ import os import sys import traceback import xml.dom.minidom import NecroQuest import GameInfo from Global import Log class RoomTypes: Quest = "quest" Empty = "empty" Treasure = "treasure" Stairway = "stairway" Teleporter = "teleporter" Sign = "sign" class MetagameClass: """ The definition of a game-world, consisting of many maps and many quests. The Player class stores the dynamic state of the world. """ def __init__(self): self.QuestCount = 0 self.MapCount = 0 self.Maps = [] self.Quests = [] self.StartMap = 0 self.StartRoom = 0 def ParseQuestFromNode(self, XMLNode): Quest = NecroQuest.QuestClass() Quest.Group = self ErrorCount = Quest.ParseFromNode(XMLNode) self.Quests.append(Quest) return ErrorCount def ParseMapFromNode(self, XMLNode): Map = MapClass() ErrorCount = Map.ParseFromNode(XMLNode) self.Maps.append(Map) return ErrorCount def SortMapsByID(self): "Called after XML load, since maps may not be seen in order" SortedList = [] for Map in self.Maps: SortedList.append((Map.ID, Map)) SortedList.sort() self.Maps = [] for Tuple in SortedList: self.Maps.append(Tuple[-1]) self.MapCount = len(self.Maps) def SortQuestsByID(self): "Called after XML load, since quests may not be seen in order" SortedList = [] MaxID = -1 for Quest in self.Quests: if Quest.ID != None: MaxID = max(MaxID, Quest.ID) NextID = MaxID + 1 Identifiers = {} for Quest in self.Quests: if Identifiers.has_key(Quest.ID): Log("* Warning: Duplicate quest ID %s"%Quest.ID) Identifiers[Quest.ID] = 1 if Quest.ID == None: Quest.ID = NextID NextID += 1 SortedList.append((Quest.ID, Quest.Name, Quest)) SortedList.sort() self.Quests = [] for Tuple in SortedList: self.Quests.append(Tuple[-1]) self.QuestCount = len(self.Quests) def Load(self, FilePath): self.Directory = os.path.split(FilePath)[0] File = open(FilePath, "rb") XML = File.read() File.close() DOMRoot = xml.dom.minidom.parseString(XML) Nodes = DOMRoot.getElementsByTagName("metagame") if len(Nodes) != 1: raise ValueError, "Error parsing metagame from %s"%FilePath # Parse the root node: Node = Nodes[0] self.Name = Node.getAttribute("name") self.StartMap = int(Node.getAttribute("StartMap")) self.StartRoom = int(Node.getAttribute("StartRoom")) # Parse the QUESTS: QuestErrorCount = 0 for QuestNode in Node.getElementsByTagName("quest"): QuestErrorCount += self.ParseQuestFromNode(QuestNode) # Parse the MAPS: MapErrorCount = 0 for MapNode in Node.getElementsByTagName("map"): MapErrorCount += self.ParseMapFromNode(MapNode) # Sanity check: No unexpected child nodes! TotalErrorCount = QuestErrorCount + MapErrorCount for ChildNode in Node.childNodes: if ChildNode.nodeType == ChildNode.ELEMENT_NODE and ChildNode.tagName not in ("map", "quest"): Log("* Warning: Unexpected Metagame child tag '%s' encountered while parsing quests"%(ChildNode.tagName)) TotalErrorCount += 1 if TotalErrorCount: Log("* %s errors encountered while parsing metagame: %s quest errors, %s map errors"%(TotalErrorCount, QuestErrorCount, MapErrorCount)) self.SortQuestsByID() self.SortMapsByID() # List unused save files: KnownSaveFiles = {} for Quest in self.Quests: KnownSaveFiles[Quest.SaveFileName.upper()] = 1 for FileName in os.listdir(self.Directory): if not KnownSaveFiles.has_key(FileName.upper()): if FileName == ".svn": pass else: Log(">>>Unknown save file: %s"%FileName) return TotalErrorCount def GetQuestByName(self, Name): LowerName = Name.lower() for Quest in self.Quests: if Quest.Name.lower() == LowerName: return Quest return None class MapType: Overworld = "overworld" LifeDungeon = "dungeon" TimeDungeon = "timedungeon" Types = [Overworld, LifeDungeon, TimeDungeon] class MapClass: """ A map, composed of various rooms and paths connecting them. The metagame world is made of multiple maps, which are connected by teleporters and gates. Some maps are dungeons, others are parts of the overworld. """ def __init__(self): self.Rooms = [] self.Doors = [] self.ID = 0 self.Name = "Map" self.Type = MapType.Overworld self.ErrorCount = 0 self.Lives = 3 # used only if MapType is LifeDungeon self.Time = 20 * 60 # used only if MapType is TimeDungeon def ParseFromFile(self, FileName): File = open(FileName, "rb") XML = File.read() File.close() DOM = xml.dom.minidom.parseString(XML) XMLNode = DOM.getElementsByTagName("map")[0] self.ParseFromNode(XMLNode) def ParseFromNode(self, XMLNode): ErrorCount = 0 self.Name = XMLNode.getAttribute("name") self.Type = XMLNode.getAttribute("type") if self.Type not in MapType.Types: Log("* WArning: Map %s has bad type %s"%(self.Name, self.Type)) if XMLNode.hasAttribute("lives"): self.Lives = int(XMLNode.getAttribute("lives")) if XMLNode.hasAttribute("time"): self.Time = int(XMLNode.getAttribute("time")) self.ID = int(XMLNode.getAttribute("ID")) RoomNodes = XMLNode.getElementsByTagName("room") for RoomNode in RoomNodes: Room = self.ParseRoomFromNode(RoomNode) if Room: self.Rooms.append(Room) PathNodes = XMLNode.getElementsByTagName("path") for PathNode in PathNodes: self.ParsePathFromNode(PathNode) return ErrorCount def ParseRoomFromNode(self, Node): Room = RoomClass(self) Room.ID = int(Node.getAttribute("ID")) Room.X = int(Node.getAttribute("x")) Room.Y = int(Node.getAttribute("y")) Room.Type = Node.getAttribute("type") Room.Notes = Node.getAttribute("notes") try: Room.Color = int(Node.getAttribute("color")) except: Room.Color = RoomColors.Gray # Parse extra information, according to room type: if Room.Type == RoomTypes.Quest: try: Room.QuestID = int(Node.getAttribute("QuestID")) except: self.ErrorCount += 1 Room.QuestID = 0 Log("Warning: Room %s has illegal quest '%s'"%(Room.ID, Node.getAttribute("QuestID"))) elif Room.Type == RoomTypes.Treasure: Room.TreasureType = Node.getAttribute("TreasureType") try: Room.TreasureAmount = int(Node.getAttribute("Amount")) except: Room.TreasureAmount = 1 elif Room.Type == RoomTypes.Stairway: Room.GotoMapID = int(Node.getAttribute("GotoMap")) Room.GotoRoomID = int(Node.getAttribute("GotoRoom")) return Room def ParsePathFromNode(self, Node): "Parse tags; assumes rooms already are parsed" StartID = int(Node.getAttribute("Start")) EndID = int(Node.getAttribute("End")) Path = PathClass(self.Rooms[StartID], self.Rooms[EndID]) self.Rooms[StartID].Paths.append(Path) self.Rooms[EndID].Paths.append(Path) # Door information: if Node.hasAttribute("DoorColor"): Path.DoorColor = int(Node.getAttribute("DoorColor")) Path.DoorClearCount = int(Node.getAttribute("DoorClearCount")) def FlattenIDs(self): RoomDict = {} IDList = [] for Room in self.Rooms: RoomDict[Room.ID] = Room IDList.append(Room.ID) IDList.sort() for IDIndex in range(len(IDList)): RoomDict[IDList[IDIndex]].ID = IDIndex Log("Flattened IDs!") def SaveToFile(self, Path): """ Save this map to a file. Keep this in synch with ParseFromNode. """ self.FlattenIDs() OutputFile = open(Path, "wb") # Save map-wide information (is it overworld or dungeon? Background art?) OutputFile.write("\n"%(self.ID, self.Name, self.Type)) # Save rooms: for Room in self.Rooms: String = " \n\n") def AddPath(self, RoomA, RoomB): Path = PathClass(RoomA, RoomB) # Purge any old paths connecting the same room (only one line between any pair): for OldPath in RoomA.Paths[:]: if OldPath.RoomA == RoomA and OldPath.RoomB == RoomB: try: RoomA.Paths.remove(OldPath) except: pass if OldPath.RoomB == RoomA and OldPath.RoomA == RoomB: try: RoomA.Paths.remove(OldPath) except: pass for OldPath in RoomB.Paths[:]: if OldPath.RoomA == RoomA and OldPath.RoomB == RoomB: RoomB.Paths.remove(OldPath) if OldPath.RoomB == RoomA and OldPath.RoomA == RoomB: RoomB.Paths.remove(OldPath) RoomA.Paths.append(Path) RoomB.Paths.append(Path) return Path def DeleteRoom(self, Room): # Remove paths: for Path in Room.Paths: if Path.RoomA == Room: Path.RoomB.Paths.remove(Path) else: Path.RoomA.Paths.remove(Path) # Remove the room itself: self.Rooms.remove(Room) def AddRoom(self, X, Y, Type = RoomTypes.Quest): Room = RoomClass(self) Room.Map = self Room.X = X Room.Y = Y Room.Type = Type MaxID = -1 for OldRoom in self.Rooms: MaxID = max(MaxID, OldRoom.ID) Room.ID = MaxID + 1 Room.QuestID = 0 # default self.Rooms.append(Room) return Room class RoomColors: Gray = 0 Red = 1 Green = 2 Blue = 3 Orange = 4 Purple = 5 Count = 6 ColorNames = {Gray: "gray", Red: "red", Green: "green", Blue: "blue", Orange: "orange", Purple: "purple"} class RoomClass: def __init__(self, Map): # List of PathClass instances: self.Paths = [] self.Map = Map self.Color = RoomColors.Gray self.Type = None self.Notes = "" self.GotoMapID = None self.GotoRoomID = None self.QuestID = None def __str__(self): return ""%self.Type def IsClear(self): return self.Map.RoomClear def GetImageName(self, ClearFlag): if self.Type == RoomTypes.Quest: ColorName = RoomColors.ColorNames[self.Color] if ClearFlag: return "ClearRoom.%s"%ColorName else: return "Room.%s"%ColorName elif self.Type == RoomTypes.Teleporter: return "Room.Teleporter" elif self.Type == RoomTypes.Treasure: return "Room.Treasure" elif self.Type == RoomTypes.Stairway: if self.GotoMapID > self.Map.ID: return "Room.StairsDown" else: return "Room.StairsUp" elif self.Type == RoomTypes.Sign: return "Room.Sign" else: return "Room.Empty" def FindNeighbor(self, XDirection, YDirection): for Path in self.Paths: if (self == Path.RoomB): OtherRoom = Path.RoomA else: OtherRoom = Path.RoomB #Log("Try (%s,%s) -> (%s,%s)"%(self.X, self.Y, OtherRoom.X, OtherRoom.Y)) if XDirection == -1 and OtherRoom.X >= self.X: continue if XDirection == 0 and OtherRoom.X != self.X: continue if XDirection == 1 and OtherRoom.X <= self.X: continue if YDirection == -1 and OtherRoom.Y >= self.Y: continue if YDirection == 0 and OtherRoom.Y != self.Y: continue if YDirection == 1 and OtherRoom.Y <= self.Y: continue return OtherRoom class PathClass: def __init__(self, RoomA, RoomB): self.RoomA = RoomA self.RoomB = RoomB self.DoorColor = None self.DoorClearCount = 0 def GetDoorImageName(self, ClearFlag): if self.DoorColor == None: return None if ClearFlag: return "Door.Open" ColorName = RoomColors.ColorNames[self.DoorColor] return "Door.%s"%ColorName if __name__ == "__main__": # Command-line invocation tests loading code, and verifies # that the XML is valid. print "Load game info:" FileName = os.path.join("Quests", "Games.xml") GameInfo.ParseGameInfoFromFile(FileName) print "Test loading the metagame:" Game = MetagameClass() ErrorCount = Game.Load(os.path.join("Quests", "Dot", "Dot.qst")) if ErrorCount: sys.exit(-1) print "%s quests:"%len(Game.Quests) for Quest in Game.Quests: print Quest.ID, Quest.Name for Stat in Quest.Stats: Name = Stat.GetNiceName() if Name == None or Name == "None": print "* Warning: Quest %s: '%s' has a bogus stat"%(Quest.ID, Quest.Name) #print "Quest %s stat %s"%(Quest.Name, Name) print "%s maps"%len(Game.Maps) for MapIndex in range(len(Game.Maps)): print "Map %s:"%MapIndex Map = Game.Maps[MapIndex] for RoomIndex in range(len(Map.Rooms)): Room = Map.Rooms[RoomIndex] print " Room %s %s %s (%s)"%(RoomIndex, Room.ID, type(Room.ID), Map.Rooms[RoomIndex])