From 6532d18c91113ae7a97b26e3039912a55c708282 Mon Sep 17 00:00:00 2001 From: csd4ni3l Date: Sun, 22 Jun 2025 21:19:17 +0200 Subject: [PATCH] Add file manager quitting with escape, rle and life 1.05 loading support and saving support for all formats(currently only rle in file manager) --- game/file_manager.py | 23 +++++-- game/file_support.py | 148 +++++++++++++++++++++++++++++++++++++++++++ game/play.py | 37 ++++++----- 3 files changed, 187 insertions(+), 21 deletions(-) create mode 100644 game/file_support.py diff --git a/game/file_manager.py b/game/file_manager.py index ecf6682..0e80692 100644 --- a/game/file_manager.py +++ b/game/file_manager.py @@ -1,20 +1,21 @@ import arcade, arcade.gui, os, time -from utils.constants import button_style +from game.file_support import save_file +from utils.constants import button_style, dropdown_style from utils.preload import button_texture, button_hovered_texture from arcade.gui.experimental.scroll_area import UIScrollArea, UIScrollBar class FileManager(arcade.gui.UIView): - def __init__(self, start_directory, allowed_extensions, *args): + def __init__(self, start_directory, allowed_extensions, save=False, *args): super().__init__() self.current_directory = start_directory self.allowed_extensions = allowed_extensions self.file_buttons = [] self.submitted_content = "" - self.done = False self.args = args + self.save = save self.anchor = self.ui.add(arcade.gui.UIAnchorLayout(size_hint=(1, 1))) self.box = self.anchor.add(arcade.gui.UIBoxLayout(size_hint=(0.7, 0.7)), anchor_x="center", anchor_y="center") @@ -41,16 +42,24 @@ class FileManager(arcade.gui.UIView): self.back_button = self.anchor.add(arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='<--', style=button_style, width=100, height=50), anchor_x="left", anchor_y="top", align_x=5, align_y=-5) self.back_button.on_click = lambda event: self.change_directory(os.path.dirname(self.current_directory)) + if self.save: + self.save_filename_input = self.anchor.add(arcade.gui.UIInputText(font_name="Roboto", font_size=24, width=self.window.width / 2, height=self.window.height / 15), anchor_x="center", anchor_y="bottom", align_y=20) + # self.save_file_type_dropdown = self.anchor.add(arcade.gui.UIDropdown(options=["life_5", "life_6", "rle"], default="rle", width=self.window.width / 5, height=self.window.height / 15, primary_style=dropdown_style, dropdown_style=dropdown_style, active_style=dropdown_style), anchor_x="center", anchor_y="bottom", align_y=20, align_x=self.window.width / 7) + self.save_button = self.anchor.add(arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='Save', style=button_style, width=200, height=100), anchor_x="right", anchor_y="bottom", align_x=-35, align_y=5) + self.save_button.on_click = lambda event: self.save_content() + self.show_directory() def submit(self, content): self.submitted_content = content - self.done = True if os.path.isfile(content): from game.play import Game self.window.show_view(Game(*self.args, load_from=self.submitted_content)) + def save_content(self): + save_file(self.args[-1], f"{self.current_directory}/{self.save_filename_input.text}", "rle") + def get_content(self, directory): if not directory in self.content_cache or time.perf_counter() - self.content_cache[directory][-1] >= 30: try: @@ -109,6 +118,12 @@ class FileManager(arcade.gui.UIView): else: self.file_buttons[-1].on_click = lambda event, file=f"{self.current_directory}/{file}": self.submit(file) + def on_key_press(self, symbol: int, modifiers: int) -> bool | None: + super().on_key_press(symbol, modifiers) + if symbol == arcade.key.ESCAPE: + from game.play import Game + self.window.show_view(Game(*self.args)) + def change_directory(self, directory): if directory.startswith("//"): # Fix / paths directory = directory[1:] diff --git a/game/file_support.py b/game/file_support.py new file mode 100644 index 0000000..d2729b4 --- /dev/null +++ b/game/file_support.py @@ -0,0 +1,148 @@ +import re + +def load_life_6(offset_x, offset_y, data): + loaded_data = [] + + for line in data: + if line == "#Life 1.06": + continue + + x, y = line.split(" ") + x = int(offset_x + int(x)) + y = int(offset_y + int(y)) + loaded_data.append((y, x)) + + return loaded_data + +def save_life_6(cell_grid): + data = "#Life 1.06" + alive_cells = [(row, col) for row in range(len(cell_grid)) for col in range(len(cell_grid[row])) if cell_grid[row][col]] + + for cell in alive_cells: + data += f"\n{cell[0]} {cell[1]}" + + return data + +def load_life_5(offset_x, offset_y, data): + loaded_data = [] + + y = int(offset_y) + for line in data: + if line == "#Life 1.05" or line.startswith("#D") or line.startswith("#R") or line.startswith("#N"): + continue + + y += 1 + for x, cell in enumerate(line): + x += int(offset_x) + if cell == "*": + loaded_data.append((y, x)) + + return loaded_data + +def save_life_5(cell_grid): + data = "#Life 1.05\n#D Exported from csd4ni3l's Game Of Life viewer.\n#N\n" + + for row_list in cell_grid.values(): + for cell in row_list.values(): + data += "*" if cell else "." + + data += "\n" + + return data + +def load_rle(offset_x, offset_y, data): + loaded_data = [] + rle_data = "" + x_offset = int(offset_x) + y_offset = int(offset_y) + y = 0 + x = 0 + + for line in data: + line = line.strip() + if not line or line.startswith("#") or line.startswith("x"): + continue + + rle_data += line + + pattern = re.compile(r"(\d*)([bo$!])") + matches = pattern.findall(rle_data) + + for count_str, symbol in matches: + count = int(count_str) if count_str else 1 + if symbol == "b": + x += count + elif symbol == "o": + for _ in range(count): + loaded_data.append((y_offset + y, x_offset + x)) + x += 1 + elif symbol == "$": + y += count + x = 0 + elif symbol == "!": + break + + return loaded_data + +def save_rle(cell_grid): + live_cells = [(row, col) for row in cell_grid for col in cell_grid[row] if cell_grid[row][col]] + + if not live_cells: + return "#C Empty pattern\nx = 0, y = 0, rule = B3/S23\n!" + + min_row = min(row for row, _ in live_cells) + max_row = max(row for row, _ in live_cells) + min_col = min(col for _, col in live_cells) + max_col = max(col for _, col in live_cells) + + width = max_col - min_col + 1 + height = max_row - min_row + 1 + + data = "#C Exported from csd4ni3l's Game Of Life viewer.\n" + data += f"x = {width}, y = {height}, rule = B3/S23\n" + + lines = [] + for row in range(min_row, max_row + 1): + line = "" + run_char = None + run_length = 0 + + for col in range(min_col, max_col + 1): + alive = cell_grid.get(row, {}).get(col, False) + char = "o" if alive else "b" + + if char == run_char: + run_length += 1 + else: + if run_char is not None: + line += (str(run_length) if run_length > 1 else "") + run_char + run_char = char + run_length = 1 + + if run_char: + line += (str(run_length) if run_length > 1 else "") + run_char + + lines.append(line) + + data += "$".join(lines) + "!" + + return data + +def load_file(offset_x, offset_y, file_path): + with open(file_path, "r") as file: + data = file.read().splitlines() + if "#Life 1.06" in data: + return load_life_6(offset_x, offset_y, data) + elif "#Life 1.05" in data: + return load_life_5(offset_x, offset_y, data) + elif file_path.endswith(".rle"): + return load_rle(offset_x, offset_y, data) + +def save_file(cell_grid, file_path, file_type): + with open(file_path, "w") as file: + if file_type == "life_6": + file.write(save_life_6(cell_grid)) + elif file_type == "life_5": + file.write(save_life_5(cell_grid)) + elif file_type == "rle": + file.write(save_rle(cell_grid)) diff --git a/game/play.py b/game/play.py index 762b550..a30beb0 100644 --- a/game/play.py +++ b/game/play.py @@ -1,4 +1,5 @@ import arcade, arcade.gui, random, math, copy, time, json, os +from game.file_support import load_file from utils.constants import COLS, ROWS, CELL_SIZE, SPACING, NEIGHBORS, button_style from utils.preload import create_sound, destroy_sound, button_texture, button_hovered_texture @@ -27,7 +28,7 @@ class Game(arcade.gui.UIView): def on_show_view(self): super().on_show_view() - self.setup_grid() + self.setup_grid(load_existing=bool(self.cell_grid)) self.anchor = self.add_widget(arcade.gui.UIAnchorLayout(size_hint=(1, 1))) self.info_box = self.anchor.add(arcade.gui.UIBoxLayout(space_between=5, vertical=False), anchor_x="center", anchor_y="top") @@ -49,6 +50,10 @@ class Game(arcade.gui.UIView): self.load_button.on_click = lambda event: self.load() self.anchor.add(self.load_button, anchor_x="left", anchor_y="bottom", align_x=5, align_y=5) + self.save_button = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text="Save", style=button_style, width=200, height=100) + self.save_button.on_click = lambda event: self.save() + self.anchor.add(self.save_button, anchor_x="right", anchor_y="bottom", align_x=-5, align_y=5) + arcade.schedule(self.update_generation, 1 / self.generation_fps) def main_exit(self): @@ -58,24 +63,16 @@ class Game(arcade.gui.UIView): def setup_grid(self, load_existing=False, randomized=False): self.spritelist.clear() - for row in range(ROWS): - self.cell_grid[row] = {} - self.sprite_grid[row] = {} - if self.load_from: - loaded_data = [] - with open(self.load_from, "r") as file: - data = file.read().splitlines() - for line in data: - if line == "#Life 1.06": - continue - - x, y = line.split(" ") - x = int(COLS / 2 + int(x)) - y = int(ROWS / 2 + int(y)) - loaded_data.append((y, x)) + loaded_data = load_file(COLS / 2, ROWS / 2, self.load_from) for row in range(ROWS): + if not row in self.cell_grid: + self.cell_grid[row] = {} + + if not row in self.sprite_grid: + self.sprite_grid[row] = {} + for col in range(COLS): if self.load_from: self.cell_grid[row][col] = 1 if (row, col) in loaded_data else 0 @@ -199,8 +196,14 @@ class Game(arcade.gui.UIView): self.cell_grid[grid_row][grid_col] = 0 def load(self): + arcade.unschedule(self.update_generation) from game.file_manager import FileManager - self.window.show_view(FileManager(os.path.expanduser("~"), [".txt"], self.pypresence_client, self.generation, self.running, self.cell_grid)) + self.window.show_view(FileManager(os.path.expanduser("~"), [".txt", ".rle"], False, self.pypresence_client, self.generation, self.running, self.cell_grid)) + + def save(self): + arcade.unschedule(self.update_generation) + from game.file_manager import FileManager + self.window.show_view(FileManager(os.path.expanduser("~"), [".txt", ".rle"], True, self.pypresence_client, self.generation, self.running, self.cell_grid)) def on_draw(self): super().on_draw()