def load_bundle(metadata_filename: str) -> Bundle: """Load game resources into bundle using metadata file. :param metadata_filename: name of the metadata file :return: bundle with game resources """ try: with open(metadata_filename) as metadata_file: metadata = load(metadata_file) validate(metadata, METADATA_JSON_SCHEMA) sprites = create_sprites(metadata["sprites"]) sprite_packs = create_sprite_packs(metadata["sprite_packs"], sprites) screens = create_screens(metadata["screens"], sprites) gui_consts = create_gui_consts(metadata["gui_consts"], sprites) sha1 = bytearray( metadata["levels"]["sha1"], "utf-8").zfill(SHA1_SIZE_IN_BYTES)[-SHA1_SIZE_IN_BYTES:] level_templates = create_level_templates( metadata["levels"]["level_templates"], sprite_packs) return Bundle(sha1, sprites, sprite_packs, screens, gui_consts, level_templates) except JSONDecodeError as decode_error: raise GameError( "Incorrect format of resource metadata file") from decode_error except ValidationError as validation_error: raise GameError( "Incorrect format of resource metadata file") from validation_error
def _load_profile_file(profile_file_path: Path, bundle: Bundle) -> PlayerProfile: try: logging.info("Loading existing player profile file '%s'", profile_file_path) with open(profile_file_path, "r+b") as profile_file: header = profile_file.read(len(FILE_HEADER)) if header != FILE_HEADER: raise GameError("File is not a valid player profile file") while True: sha1 = profile_file.read(SHA1_SIZE_IN_BYTES) if not sha1: break section_size = _read_int(profile_file) if sha1 == bundle.sha1: return _read_player_profile(profile_file_path, profile_file, bundle) profile_file.seek(section_size, 1) return _init_player_profile(profile_file_path, profile_file, bundle) except IOError as io_error: raise GameError("Unable to open player profile file") from io_error except EOFError as eof_error: raise GameError("Unexpected end of player profile file") from eof_error
def complete_level(self, level_score: LevelScore) -> LevelScore: """Save information about level completion to profile file. Additionally, as a reward, unlock next level. :param level_score: score of level completion :return: the previous score for the completed level """ logging.info("Updating player profile file with game progress") if not self.is_level_completed(level_score.level_num): level_to_be_unlocked = self._last_unlocked_level + 1 if self._can_unlock_level(level_to_be_unlocked): self._last_unlocked_level = level_to_be_unlocked prev_level_score = self.levels_scores[level_score.level_num] new_level_score = prev_level_score.merge_with(level_score) self.levels_scores[level_score.level_num] = new_level_score try: with open(self._profile_file_path, "r+b") as profile_file: profile_file.seek(self._file_offset) _write_int(profile_file, self._last_unlocked_level) profile_file.seek(new_level_score.level_num * LEVEL_SCORE_SIZE_IN_BYTES, 1) _write_int(profile_file, 1 if new_level_score.completed else 0) _write_int(profile_file, new_level_score.steps) _write_int(profile_file, new_level_score.pushes) _write_int(profile_file, new_level_score.time_in_ms) except IOError as io_error: raise GameError( f"Unable to update player profile file '{self._profile_file_path}'. " "Progress lost :-(") from io_error return prev_level_score
def _create_profile_file(profile_file_path: Path, bundle: Bundle) -> PlayerProfile: try: logging.info("Creating new player profile file '%s'", profile_file_path) with open(profile_file_path, "wb") as profile_file: profile_file.write(FILE_HEADER) return _init_player_profile(profile_file_path, profile_file, bundle) except IOError as io_error: raise GameError("Unable to create player profile file") from io_error
def load_game_resources(filenames: FileNames) -> Bundle: """Load Pyxel's resource file containing bundle.""" logging.info("Loading Pyxel resources file '%s'", filenames.resource_file) if not os.path.isfile(filenames.resource_file): # This is the only way we can pre-check whether pyxel.load() will fail or not # In current version of Pyxel it's not possible to react to error or capture the error # reason raise GameError( f"Unable to find Pyxel resource file '{filenames.resource_file}'") pyxel.load(filenames.resource_file) logging.info("Loading resources metadata file '%s'", filenames.metadata_file) if not os.path.isfile(filenames.metadata_file): raise GameError( f"Unable to find resources metadata file '{filenames.metadata_file}'" ) return load_bundle(filenames.metadata_file)
def create_robot(self) -> Robot: """Create robot instance based on information from tilemap about its start position.""" start = None for tile_position in self.tilemap.tiles_positions(): if self.tile_at(tile_position).is_start: start = tile_position if not start: raise GameError( f"Level {self.level_num} does not have player start tile") face_direction = Direction.UP for direction in list(Direction): if self.tile_at(start.move(direction)).is_walkable: face_direction = direction break return Robot(start, face_direction, self.sprite_packs.robot_animations)
def create_crates(self) -> Tuple[Crate, ...]: """Create a collection of crates based on information from tilemap about their initial positions. :return: collection of crates created from level template """ crates_positions = [] for tile_position in self.tilemap.tiles_positions(): if self.tile_at(tile_position).is_crate_spawn_point: crates_positions.append(tile_position) crates = [] for crate_position in crates_positions: is_initially_placed = self.tile_at( crate_position).is_crate_initially_placed crates.append( Crate(crate_position, is_initially_placed, self.sprite_packs.crate_sprites)) if not crates: raise GameError(f"Level {self.level_num} does not have any crates") return tuple(crates)
def merge_with(self, level_score: "LevelScore") -> "LevelScore": """Merge this level score with given score. In case of differences in field values prefer better score (smaller number of steps, pushes, quicker completion time) :param level_score: level score to merge with :return: newly created level score which is a result of merge """ if self == level_score: return level_score if self.level_num != level_score.level_num: raise GameError("Cannot merge scores from different levels") if not self.completed: return level_score return LevelScore( level_num=self.level_num, completed=self.completed, pushes=min(self.pushes, level_score.pushes), steps=min(self.steps, level_score.steps), time_in_ms=min(self.time_in_ms, level_score.time_in_ms))