class Game: """The game logic itself. The loop and input handling are here.""" def __init__(self, screen): """Create the screen, player, assets.""" # Some technical items, first self.screen = screen self.gameScreen = curses.newpad(Constants.SCREENHEIGHT, Constants.SCREENWIDTH) self.running = True # Collections of various objects self.player = Player(self) self.walls = dict() self.doors = dict() self.decorations = dict() self.fences = dict() self.npcs = [] self.villagers = [] self.police = [] self.squares = [] # Camera self.cameraX = 0 self.cameraY = 0 # The current contents of the status line # TODO: Maybe rename this self.statusLine = "" # The bottom line contains the clock and other stuff self.bottomLine = "" # The current time and time-related variables self.hour = 7 self.minute = 59 self.turnsToNextMinute = 0 ### The actual game creation logic # Random decoration for _ in range(500): (y, x) = (random.randint(1, Constants.MAPHEIGHT - 1), random.randint(1, Constants.MAPWIDTH - 1)) self.decorations[(y, x)] = Decoration() # Create tiles for visibility and FoV self.tiles = dict() for x in range(Constants.MAPWIDTH): for y in range(Constants.MAPHEIGHT): self.tiles[(y,x)] = Tile() # Town creation self.town = Town(self, 5, 5, 3, 3) self.town2 = Town(self, 5, 88, 3, 3) # Setup the murder.. self.victim = None self.killer = None self.murderSetup() # Put together the NPC schedules self.generatePlans() def initialiseWalls(self): """Builds the correct wall graphics""" # This is one of the ugliest things I've ever written. # Don't judge me! for (y, x) in self.walls: wall = self.walls[(y, x)] if wall.character.isdigit(): continue wallUp = wallDown = wallLeft = wallRight = False # Check for walls via worst method possible try: self.walls[(y-1, x)] wallUp = True except Exception as e: pass try: self.walls[(y+1, x)] wallDown = True except Exception as e: pass try: self.walls[(y, x-1)] wallLeft = True except Exception as e: pass try: self.walls[(y, x+1)] wallRight = True except Exception as e: pass # Check for doors using the same awful method try: self.doors[(y-1, x)] wallUp = True except Exception as e: pass try: self.doors[(y+1, x)] wallDown = True except Exception as e: pass try: self.doors[(y, x-1)] wallLeft = True except Exception as e: pass try: self.doors[(y, x+1)] wallRight = True except Exception as e: pass system = platform.system() UpDown = '|' LeftRight = '-' UpLeft = '-' UpRight = '-' DownLeft = '-' DownRight = '-' DownLeftRight = '-' UpLeftRight = '-' LeftUpDown = '|' RightUpDown = '|' UpDownLeftRight = '|' # Terrible attempt at getting nice walls in Linux # UpDown = '|' # LeftRight = '-' # UpLeft = chr(0x6a) # UpRight = chr(0x6d) # DownLeft = chr(0x06b) # DownRight = chr(0x6c) # DownLeftRight = chr(0x77) # UpLeftRight = chr(0x76) # LeftUpDown = chr(0x75) # RightUpDown = chr(0x74) # UpDownLeftRight = chr(0x6e) # Smells bad, huh. if (system == 'Windows'): LeftRight = chr(0xC4) UpDown = chr(0xB3) UpLeft = chr(0xD9) UpRight = chr(0xC0) DownLeft = chr(0xBF) DownRight = chr(0xDA) DownLeftRight = chr(0xC2) UpLeftRight = chr(0xC1) LeftUpDown = chr(0xB4) RightUpDown = chr(0xC3) UpDownLeftRight = chr(0xC5) if (wallLeft or wallRight): wall.character = LeftRight else: wall.character = UpDown # Yeah.. This just happened. Next time consider bitstates if (wallUp and wallLeft): wall.character = UpLeft if (wallUp and wallRight): wall.character = UpRight if (wallDown and wallLeft): wall.character = DownLeft if (wallDown and wallRight): wall.character = DownRight if (wallDown and wallLeft and wallRight): wall.character = DownLeftRight if (wallUp and wallLeft and wallRight): wall.character = UpLeftRight if (wallLeft and wallUp and wallDown): wall.character = LeftUpDown if (wallRight and wallUp and wallDown): wall.character = RightUpDown if (wallRight and wallUp and wallDown and wallLeft): wall.character = UpDownLeftRight def mainLoop(self): """Run the game while a flag is set.""" # Initialise walls to correct characters self.initialiseWalls() # Start the main loop while (self.running): self.logic() self.draw() self.handleInput() def isInCamera(self, entityY, entityX): """ Shouldn't be a class method. Determines if we should draw a character or not.""" return (entityY >= self.cameraY and entityY < self.cameraY + Constants.GAMEHEIGHT and entityX >= self.cameraX and entityX < self.cameraX + Constants.GAMEWIDTH) def draw(self): """ Draw it all, but only the stuff that would be on the screen""" # Wipe out the screen. self.screen.erase() self.gameScreen.erase() # Sort out the camera self.cameraX = max(0, self.player.x - Constants.GAMEWIDTH // 2) self.cameraX = min(self.cameraX, Constants.MAPWIDTH - Constants.GAMEWIDTH) self.cameraY = max(0, self.player.y - Constants.GAMEHEIGHT // 2) self.cameraY = min(self.cameraY, Constants.MAPHEIGHT - Constants.GAMEHEIGHT) alwaysSeeWalls = False # Draw the floors, walls, etc. # Floors first, then we'll override them for x in range(0, Constants.MAPWIDTH): for y in range(0, Constants.MAPHEIGHT): if not self.isInCamera(y, x): continue if self.tiles[(y, x)].visible or not Constants.FOV_ENABLED: self.gameScreen.addstr( y, x, '.', Constants.COLOUR_GREEN) if (y, x) in self.decorations: decoration = self.decorations[(y, x)] self.gameScreen.addstr(y, x, decoration.character, decoration.colour) # Fences if (y, x) in self.fences: fence = self.fences[(y, x)] self.gameScreen.addstr(y, x, fence.character, fence.colour) # Doors if (y, x) in self.doors: door = self.doors[(y, x)] self.gameScreen.addstr(y, x, door.character, door.colour) if (self.tiles[(y,x)].seen and (alwaysSeeWalls or self.tiles[(y,x)].visible) or not Constants.FOV_ENABLED): if (y, x) in self.walls: wall = self.walls[(y,x)] self.gameScreen.addstr(y, x, wall.character, wall.colour) # Draw the entities like players, NPCs for npc in self.npcs: npcPos = (npc.y, npc.x) if npcPos in self.tiles: tile = self.tiles[npcPos] if (self.isInCamera(npc.y, npc.x) and tile.visible or not Constants.FOV_ENABLED): if (npc in self.villagers and self.player.notebook.isNpcKnown(npc) and npc.alive): character = str(npc.square.house.number) else: character = npc.character self.gameScreen.addstr(npc.y, npc.x, character, npc.colour) player = self.player self.gameScreen.addstr(player.y, player.x, player.character, player.colour) # Status line printing self.screen.addstr(0, 0, self.statusLine) self.statusLine = "" # Debug and bottom status stuff self.screen.addstr(Constants.GAMEHEIGHT +1, 0, self.bottomLine) if self.npcs: npc = self.npcs[0] if npc.path: path = npc.path[0] self.screen.addstr(Constants.GAMEHEIGHT+2, 1, str(path)) self.screen.noutrefresh() self.gameScreen.noutrefresh(self.cameraY, self.cameraX, 1, 1, Constants.GAMEHEIGHT, Constants.GAMEWIDTH) screen.moveCursorToPlayer(self.screen, self.player) # Blit the screen curses.doupdate() def getAnyKey(self): """Utility funciton that waits until a ANY input has been entered, does not return anything.""" self.screen.getch() def getKey(self, acceptedInputs = Constants.KEYMAP.values()): """Utility funciton that waits until a valid input has been entered.""" gotKey = False while not gotKey: got = self.screen.getch() if (got in Constants.KEYMAP and Constants.KEYMAP[got] in acceptedInputs): key = Constants.KEYMAP[got] return key def getDialogueChoice(self, numberOfChoices): """Utility funciton that waits until a valid dialogue choice has been entered.""" gotKey = False while not gotKey: got = self.screen.getch() if (got >= ord('1') and got <= ord(str(numberOfChoices))): return int(chr(got)) def printStatus(self, status, moveCursor = True): """Prints the status line. Also sets it so it doesn't get wiped until next frame""" self.statusLine = status self.screen.addstr(0, 0, " " * Constants.XRES) self.screen.addstr(0, 0, status) if moveCursor: screen.moveCursorToPlayer(self.screen, self.player) def printDescription(self, text, header = None, showAnyKeyPrompt = True): """Prints the description in a nice box before re-drawing the game on closure""" # Print the text if header: underline = '-' * Constants.DESC_BOX_WIDTH screen.printBox(self.screen, [header, underline, text], showAnyKeyPrompt) else: screen.printBox(self.screen, [text], showAnyKeyPrompt) # Wait for an input screen.moveCursorToPlayer(self.screen, self.player) self.getAnyKey() self.printStatus("") self.draw() def getYesNo(self, message = None): """Utility function for getting 'yes/no' responses.""" gotYesNo = False key = None if message: self.printStatus(message) while not gotYesNo: key = self.screen.getch() if key is ord('y') or key is ord('n'): gotYesNo = True self.printStatus("") return key is ord('y') def kickDoor(self): """Prompts for direction and attempts to kick down the door there if present.""" actionTaken = True self.printStatus("Which direction?") direction = self.screen.getch() success = random.randrange(100) > 80 playerPos = [self.player.y, self.player.x] try: direction = Constants.KEYMAP[direction] if direction == InputActions.MOVE_LEFT: playerPos[1] -= 1 elif direction == InputActions.MOVE_DOWN: playerPos[0] += 1 elif direction == InputActions.MOVE_UP: playerPos[0] -= 1 elif direction == InputActions.MOVE_RIGHT: playerPos[1] += 1 if playerPos != [self.player.y, self.player.x]: try: door = self.doors[tuple(playerPos)] if not door.closed: self.printStatus("It's open, champ.") elif success: door.locked= False door.playerOpen() self.printStatus("The door slams open!") else: self.printStatus("The door holds fast.") except: self.printStatus("No door there!") actionTaken = False else: self.printStatus("Nevermind.") actionTaken = False except: self.printStatus("Nevermind.") actionTaken = False return actionTaken def talk(self): """Prompts for direction and talks to NPC in that direction if present.""" promptText = "Navigate with left and right, cancel with Quit, select with Talk" (npc, error) = self.selectVisibleNPC(promptText, InputActions.TALK) if not npc: self.printStatus(error) return False elif not npc.alive: self.printStatus("I don't think they're in a talkative mood.") return False else: npc.beginConversation() return True def openDoor(self): self.printStatus("Which direction?") direction = self.screen.getch() playerPos = [self.player.y, self.player.x] actionTaken = True try: direction = Constants.KEYMAP[direction] if direction == InputActions.MOVE_LEFT: playerPos[1] -= 1 elif direction == InputActions.MOVE_DOWN: playerPos[0] += 1 elif direction == InputActions.MOVE_UP: playerPos[0] -= 1 elif direction == InputActions.MOVE_RIGHT: playerPos[1] += 1 if playerPos != [self.player.y, self.player.x]: try: door = self.doors[tuple(playerPos)] door.playerOpen() except: self.printStatus("No door there!") actionTaken = False else: self.printStatus("Nevermind.") actionTaken = False except: self.printStatus("Nevermind.") actionTaken = False return actionTaken def selectVisibleNPC(self, promptText, selectionAction): visibleNpcs = [npc for npc in self.npcs if self.tiles[npc.y, npc.x].visible and self.isInCamera(npc.y, npc.x)] error = "No-one in sight!" npcSelected = None if visibleNpcs: npcIdx = 0 while not npcSelected: self.printStatus(promptText, False) screen.moveCursorToEntity(self.screen, self.player, visibleNpcs[npcIdx]) key = self.getKey([InputActions.MOVE_LEFT, InputActions.MOVE_RIGHT, selectionAction, InputActions.QUIT]) if key == InputActions.MOVE_LEFT: npcIdx += 1 if npcIdx >= len(visibleNpcs): npcIdx = 0 elif key == InputActions.MOVE_RIGHT: npcIdx -= 1 if npcIdx < 0: npcIdx = len(visibleNpcs) - 1 elif key == selectionAction: npcSelected = visibleNpcs[npcIdx] elif key == InputActions.QUIT: break if not npcSelected and visibleNpcs: error = "Cancelled." return (npcSelected, error) def look(self): promptText = "Navigate with left and right, cancel with Quit, select with Look" (npc, error) = self.selectVisibleNPC(promptText, InputActions.LOOK) if not npc: self.printStatus(error) else: status = "" if self.player.notebook.isNpcKnown(npc): thatWord = "That's " if npc.alive else "That was " status = (thatWord + npc.firstName + " " + npc.lastName + "." + " " + npc.getDescription()) else: status = "You don't know who that is. " + npc.getDescription() self.printDescription(status) return False def handleInput(self): """ Wait for the player to press a key, then handle input appropriately.""" actionTaken = False while not actionTaken: key = self.getKey() # Clear the status line self.printStatus("") # Assume guilty until proven innocent. actionTaken = True # Quit? if key == InputActions.QUIT: quit = self.getYesNo("Are you sure you want to quit?") if quit: self.running = False else: actionTaken = False # Move? elif key == InputActions.MOVE_LEFT: actionTaken = self.player.attemptMove(Direction.LEFT) elif key == InputActions.MOVE_DOWN: actionTaken = self.player.attemptMove(Direction.DOWN) elif key == InputActions.MOVE_UP: actionTaken = self.player.attemptMove(Direction.UP) elif key == InputActions.MOVE_RIGHT: actionTaken = self.player.attemptMove(Direction.RIGHT) elif key == InputActions.OPEN_DOOR: actionTaken = self.openDoor() elif key == InputActions.KICK_DOOR: actionTaken = self.kickDoor() elif key == InputActions.LOOK: actionTaken = self.look() elif key == InputActions.TALK: actionTaken = self.talk() elif key == InputActions.WAIT: actionTaken = True # Do nothing. def murderSetup(self): """Picks the victim and murderer, and kills the victim""" victim = random.choice(self.villagers) self.victim = victim killer = None while True: killer = random.choice(self.villagers) if killer is not victim: self.killer = killer killer.killer = True break victim.die() house = victim.square.house # Unlock the front door doorY = house.absoluteY + house.frontDoorPos[0] doorX = house.absoluteX + house.frontDoorPos[1] self.doors[doorY, doorX].locked = False # Put the player outside the dead guy's house self.player.y = house.absoluteY + house.frontDoorPos[0] + 1 self.player.x = house.absoluteX + house.frontDoorPos[1] # Spawn some cops around the dead guy and next to our character copSpawnLocations = [(self.player.y, self.player.x + 1)] for _ in range(0, random.randint(4, 5)): y = random.randint(house.absoluteY + 1, house.absoluteY + house.height - 1) x = random.randint(house.absoluteX + 1, house.absoluteX + house.width - 1) copSpawnLocations.append((y,x)) for location in copSpawnLocations: (y, x) = location police = Police(self, y, x) self.npcs.append(police) self.police.append(police) self.player.notebook.addToKnownNpcs(police) self.printStatus("\"It's a messy one today, boss.\"") def generatePlans(self): """Generate the initial Plans for all NPCs""" # It should be pretty consistent. Like, if an NPC is visiting # another NPC's house, the vistee shouldn't make a plan to go out. for npc in self.villagers: if npc.alive: for x in range(5): randomSquareIndex = None while True: # Don't visit the dead guy, that's morbid randomSquare = random.choice(self.squares) if randomSquare.npc.alive: break visitNeighbour = Plan.VisitNeighbour(npc, randomSquare) randomHour = random.randint(0, 8) + 8 npc.plan.addPlanEntry(randomHour, 0, visitNeighbour) def logic(self): """Run all the assorted logic for all entities and advance the clock""" if self.turnsToNextMinute <= 0: self.minute += 1 if self.minute == 60: self.minute = 0 self.hour += 1 if self.hour == 24: self.hour = 0 self.turnsToNextMinute = Constants.TURNS_BETWEEN_MINUTES else: self.turnsToNextMinute -= 1 for npc in self.npcs: npc.update() for door in self.doors: self.doors[door].update() self.player.generateFov() # Update the bottom line self.bottomLine = "(" + str(self.player.x) + ", " + str(self.player.y) + ")" time = str(self.hour).zfill(2) + ":" + str(self.minute).zfill(2) self.bottomLine += " " + time