# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse # SPDX-License-Identifier: GPL-3.0-or-later from json import JSONDecodeError from random import randint from typing import Any, Optional, List import curses import json import os import sys from .entities.player import Player from .enums import GameMode, KeyValues, DisplayActions from .interfaces import Map, Logs from .resources import ResourceManager from .settings import Settings from . import menus from .translations import gettext as _, Translator class Game: """ The game object controls all actions in the game. """ maps: List[Map] map_index: int player: Player screen: Any # display_actions is a display interface set by the bootstrapper display_actions: callable def __init__(self) -> None: """ Init the game. """ self.state = GameMode.MAINMENU self.waiting_for_friendly_key = False self.is_in_store_menu = True self.settings = Settings() self.settings.load_settings() self.settings.write_settings() Translator.setlocale(self.settings.LOCALE) self.main_menu = menus.MainMenu() self.settings_menu = menus.SettingsMenu() self.settings_menu.update_values(self.settings) self.inventory_menu = menus.InventoryMenu() self.store_menu = menus.StoreMenu() self.logs = Logs() self.message = None def new_game(self) -> None: """ Create a new game on the screen. """ # TODO generate a new map procedurally self.maps = [] self.map_index = 0 self.map = Map.load(ResourceManager.get_asset_path("example_map.txt")) self.map.logs = self.logs self.logs.clear() self.player = Player() self.map.add_entity(self.player) self.player.move(self.map.start_y, self.map.start_x) self.map.spawn_random_entities(randint(3, 10)) self.inventory_menu.update_player(self.player) @property def map(self) -> Map: """ Return the current map where the user is. """ return self.maps[self.map_index] @map.setter def map(self, m: Map) -> None: """ Redefine the current map. """ if len(self.maps) == self.map_index: # Insert new map self.maps.append(m) # Redefine the current map self.maps[self.map_index] = m def run(self, screen: Any) -> None: # pragma no cover """ Main infinite loop. We wait for the player's action, then we do what that should be done when the given key gets pressed. """ screen.refresh() while True: screen.erase() screen.noutrefresh() self.display_actions(DisplayActions.REFRESH) curses.doupdate() key = screen.getkey() if key == "KEY_MOUSE": _ignored1, x, y, _ignored2, _ignored3 = curses.getmouse() self.display_actions(DisplayActions.MOUSE, y, x) else: self.handle_key_pressed( KeyValues.translate_key(key, self.settings), key) def handle_key_pressed(self, key: Optional[KeyValues], raw_key: str = '')\ -> None: """ Indicates what should be done when the given key is pressed, according to the current game state. """ if self.message: self.message = None self.display_actions(DisplayActions.REFRESH) return if self.state == GameMode.PLAY: if self.waiting_for_friendly_key: # The player requested to talk with a friendly entity self.handle_friendly_entity_chat(key) else: self.handle_key_pressed_play(key) elif self.state == GameMode.INVENTORY: self.handle_key_pressed_inventory(key) elif self.state == GameMode.MAINMENU: self.handle_key_pressed_main_menu(key) elif self.state == GameMode.SETTINGS: self.settings_menu.handle_key_pressed(key, raw_key, self) elif self.state == GameMode.STORE: self.handle_key_pressed_store(key) self.display_actions(DisplayActions.REFRESH) def handle_key_pressed_play(self, key: KeyValues) -> None: """ In play mode, arrows or zqsd move the main character. """ if key == KeyValues.UP: if self.player.move_up(): self.map.tick() elif key == KeyValues.DOWN: if self.player.move_down(): self.map.tick() elif key == KeyValues.LEFT: if self.player.move_left(): self.map.tick() elif key == KeyValues.RIGHT: if self.player.move_right(): self.map.tick() elif key == KeyValues.INVENTORY: self.state = GameMode.INVENTORY elif key == KeyValues.SPACE: self.state = GameMode.MAINMENU elif key == KeyValues.CHAT: # Wait for the direction of the friendly entity self.waiting_for_friendly_key = True elif key == KeyValues.WAIT: self.map.tick() elif key == KeyValues.LADDER: self.handle_ladder() def handle_ladder(self) -> None: """ The player pressed the ladder key to switch map """ # On a ladder, we switch level y, x = self.player.y, self.player.x if not self.map.tiles[y][x].is_ladder(): return # We move up on the ladder of the beginning, # down at the end of the stage move_down = y != self.map.start_y and x != self.map.start_x old_map = self.map self.map_index += 1 if move_down else -1 if self.map_index == -1: self.map_index = 0 return while self.map_index >= len(self.maps): # TODO: generate a new map self.maps.append(Map.load(ResourceManager.get_asset_path( "example_map_2.txt"))) new_map = self.map new_map.floor = self.map_index old_map.remove_entity(self.player) new_map.add_entity(self.player) if move_down: self.player.move(self.map.start_y, self.map.start_x) self.logs.add_message( _("The player climbs down to the floor {floor}.") .format(floor=-self.map_index)) else: # Find the ladder of the end of the game ladder_y, ladder_x = -1, -1 for y in range(self.map.height): for x in range(self.map.width): if (y, x) != (self.map.start_y, self.map.start_x) \ and self.map.tiles[y][x].is_ladder(): ladder_y, ladder_x = y, x break self.player.move(ladder_y, ladder_x) self.logs.add_message( _("The player climbs up the floor {floor}.") .format(floor=-self.map_index)) self.display_actions(DisplayActions.UPDATE) def handle_friendly_entity_chat(self, key: KeyValues) -> None: """ If the player is talking to a friendly entity, we get the direction where the entity is, then we interact with it. """ if not self.waiting_for_friendly_key: return self.waiting_for_friendly_key = False if key == KeyValues.UP: xp = self.player.x yp = self.player.y - 1 elif key == KeyValues.DOWN: xp = self.player.x yp = self.player.y + 1 elif key == KeyValues.LEFT: xp = self.player.x - 1 yp = self.player.y elif key == KeyValues.RIGHT: xp = self.player.x + 1 yp = self.player.y else: return if self.map.entity_is_present(yp, xp): for entity in self.map.entities: if entity.is_friendly() and entity.x == xp and \ entity.y == yp: msg = entity.talk_to(self.player) self.logs.add_message(msg) if entity.is_merchant(): self.state = GameMode.STORE self.is_in_store_menu = True self.store_menu.update_merchant(entity) self.display_actions(DisplayActions.UPDATE) def handle_key_pressed_inventory(self, key: KeyValues) -> None: """ In the inventory menu, we can interact with items or close the menu. """ if key == KeyValues.SPACE or key == KeyValues.INVENTORY: self.state = GameMode.PLAY elif key == KeyValues.UP: self.inventory_menu.go_up() elif key == KeyValues.DOWN: self.inventory_menu.go_down() if self.inventory_menu.values and not self.player.dead: if key == KeyValues.USE: self.inventory_menu.validate().use() elif key == KeyValues.EQUIP: self.inventory_menu.validate().equip() elif key == KeyValues.DROP: self.inventory_menu.validate().drop() # Ensure that the cursor has a good position self.inventory_menu.position = min(self.inventory_menu.position, len(self.inventory_menu.values) - 1) def handle_key_pressed_store(self, key: KeyValues) -> None: """ In a store menu, we can buy items or close the menu. """ menu = self.store_menu if self.is_in_store_menu else self.inventory_menu if key == KeyValues.SPACE or key == KeyValues.INVENTORY: self.state = GameMode.PLAY elif key == KeyValues.UP: menu.go_up() elif key == KeyValues.DOWN: menu.go_down() elif key == KeyValues.LEFT: self.is_in_store_menu = False self.display_actions(DisplayActions.UPDATE) elif key == KeyValues.RIGHT: self.is_in_store_menu = True self.display_actions(DisplayActions.UPDATE) if menu.values and not self.player.dead: if key == KeyValues.ENTER: item = menu.validate() owner = self.store_menu.merchant if self.is_in_store_menu \ else self.player buyer = self.player if self.is_in_store_menu \ else self.store_menu.merchant flag = item.be_sold(buyer, owner) if not flag: self.message = _("The buyer does not have enough money") self.display_actions(DisplayActions.UPDATE) # Ensure that the cursor has a good position menu.position = min(menu.position, len(menu.values) - 1) def handle_key_pressed_main_menu(self, key: KeyValues) -> None: """ In the main menu, we can navigate through options. """ if key == KeyValues.DOWN: self.main_menu.go_down() if key == KeyValues.UP: self.main_menu.go_up() if key == KeyValues.ENTER: option = self.main_menu.validate() if option == menus.MainMenuValues.START: self.new_game() self.display_actions(DisplayActions.UPDATE) self.state = GameMode.PLAY if option == menus.MainMenuValues.RESUME: self.state = GameMode.PLAY elif option == menus.MainMenuValues.SAVE: self.save_game() elif option == menus.MainMenuValues.LOAD: self.load_game() elif option == menus.MainMenuValues.SETTINGS: self.state = GameMode.SETTINGS elif option == menus.MainMenuValues.EXIT: sys.exit(0) def save_state(self) -> dict: """ Saves the game to a dictionary """ return self.map.save_state() def load_state(self, d: dict) -> None: """ Loads the game from a dictionary """ try: self.map.load_state(d) except KeyError: self.message = _("Some keys are missing in your save file.\n" "Your save seems to be corrupt. It got deleted.") os.unlink(ResourceManager.get_config_path("save.json")) self.display_actions(DisplayActions.UPDATE) return players = self.map.find_entities(Player) if not players: self.message = _("No player was found on this map!\n" "Maybe you died?") self.player.health = 0 self.display_actions(DisplayActions.UPDATE) return self.player = players[0] self.display_actions(DisplayActions.UPDATE) def load_game(self) -> None: """ Loads the game from a file """ file_path = ResourceManager.get_config_path("save.json") if os.path.isfile(file_path): with open(file_path, "r") as f: try: state = json.loads(f.read()) self.load_state(state) except JSONDecodeError: self.message = _("The JSON file is not correct.\n" "Your save seems corrupted. " "It got deleted.") os.unlink(file_path) self.display_actions(DisplayActions.UPDATE) def save_game(self) -> None: """ Saves the game to a file """ with open(ResourceManager.get_config_path("save.json"), "w") as f: f.write(json.dumps(self.save_state()))