from terminal import Terminal from entity import ITEMS_DATA, Creature, Player, Enemy from actions import * from floor import COLLIDABLE, Floor, TileType import sys TEXTURES = { TileType.WALL: '#', TileType.AIR: '.', Player: '@', Enemy: 'Y' } class Game: def __init__(self): self.term = Terminal() self.player = Player() self.floor = Floor(20, 10) self.should_exit = False self.ticks = 0 self.schedule = {} self.redirect_io('prints.log', 'errors.log') self.hud_height = 3 self.menu_width = 20 self.camera = (0, 0) self.hud = 0 def __del__(self): self.restore_io() def restore_io(self) -> None: sys.stdout = self.stdout_original sys.stderr = self.stderr_original self.stdout_file.close() self.stderr_file.close() def redirect_io(self, stdout_file, stderr_file) -> None: self.stdout_original = sys.stdout self.stderr_original = sys.stderr self.stdout_file = open(stdout_file, 'w') self.stderr_file = open(stderr_file, 'w') sys.stdout = self.stdout_file sys.stderr = self.stderr_file def run(self): """ Runs the game """ self.instance_creature(3, 3, self.player) self.instance_creature(6, 4, Enemy("slime")) self.player.equip("shiv", "main_hand") self.render() while not self.should_exit: self.step() if self.action_was_performed: self.render() def instance_creature(self, x, y, creature: Creature): """ Instances a creature in space and time """ self.floor.entities.add_creature(x, y, creature) self.reschedule(creature) def reschedule(self, creature: Creature): """ Calculates the ticks in which the creature will perform its next action """ time = (ACTION_TIMING[creature.action.__class__] // creature.speed) + self.ticks if self.schedule.get(time): self.schedule[time].append(creature) else: self.schedule[time] = [creature] def render(self): """ Renders the game """ (term_width, term_height) = self.term.get_dimensions() (floor_frame_width, floor_frame_height) = (term_width - self.menu_width, term_height - self.hud_height) self.term.clear() # Floor for x in range(min(self.floor.width, floor_frame_width)): for y in range(min(self.floor.height, floor_frame_height)): if (x, y) in self.floor.entities.pos_creatures: self.term.put_char(x, y, TEXTURES[self.floor.entities.pos_creatures[(x,y)].__class__]) elif (x, y) in self.floor.entities.items: self.term.put_char(x, y, 'i') else: self.term.put_char(x, y, TEXTURES[self.floor.get_tile(x, y)]) # HUD for x in range(floor_frame_width): for y in range(floor_frame_height, term_height): self.term.put_char(x, y, ' ') self.term.put_string(0, floor_frame_height, f"HP: {self.player.hp}/{self.player.max_hp}" ) self.term.put_string(0, floor_frame_height + 1, f"MH: {'None' if not self.player.equipment['main_hand'] else self.player.equipment['main_hand']['name']}; Damage: {self.player.total_damage()}") # Menu for x in range(floor_frame_width, term_width): for y in range(term_height): self.term.put_char(x, y, ' ') self.term.put_string(floor_frame_width, 0, f"Move: {KEYBINDS['move_left']} {KEYBINDS['move_up']} {KEYBINDS['move_right']} {KEYBINDS['move_down']}") self.term.put_string(floor_frame_width, 1, f"Inventory: {KEYBINDS['inventory']}") self.term.put_string(floor_frame_width, 2, f"Quit: {KEYBINDS['quit']}") def step(self): """ Perfoms a step in the game. Sets the `action_was_performed` attribute accordingly """ self.action_was_performed = False creatures = self.schedule.get(self.ticks) if creatures: while creature := creatures[0] if len(creatures) > 0 else None: if isinstance(creature, Player): self.input_event() elif isinstance(creature, Enemy): creature.calculate_action() if self.perform(creature): creatures.pop(0) self.schedule.pop(self.ticks) self.action_was_performed = True self.ticks += 1 def perform(self, creature: Creature): """ Performs an action for a creature """ if isinstance(creature.action, Move): pos = self.floor.entities.pop_creature_ref(creature) if pos: (dest_x, dest_y) = (pos[0] + creature.action.x, pos[1] + creature.action.y) out_of_bounds = dest_x < 0 or dest_x > self.floor.width-1 or dest_y < 0 or dest_y > self.floor.height-1 if out_of_bounds \ or self.floor.get_tile(dest_x, dest_y) in COLLIDABLE \ or not self.floor.entities.add_creature(dest_x, dest_y, creature): self.floor.entities.add_creature(pos[0], pos[1], creature) return False self.reschedule(creature) return True def input_event(self): """ Waits for a game event to happen, and behaves accordingly """ keycode = self.term.get_key() if keycode == KEYBINDS["quit"]: self.should_exit = True elif keycode == KEYBINDS["inventory"]: self.hud += 1 print(self.hud, flush=True) self.input_event() elif keycode == KEYBINDS["move_right"]: self.player.action = Move(1, 0) elif keycode == KEYBINDS["move_left"]: self.player.action = Move(-1, 0) elif keycode == KEYBINDS["move_down"]: self.player.action = Move(0, 1) elif keycode == KEYBINDS["move_up"]: self.player.action = Move(0, -1) elif keycode == "KEY_RESIZE": self.render() self.input_event() KEYBINDS = { "move_right": "→", "move_left": "←", "move_down": "↓", "move_up": "↑", "quit": "q", "inventory": "i", }