From c25ffe1a623fc4ac855a0c938336b9f786bc06d3 Mon Sep 17 00:00:00 2001 From: csd4ni3l Date: Sun, 7 Dec 2025 23:45:20 +0100 Subject: [PATCH] Fix file manager sprite add submitting with / at the end, fix change_size not working for textured, make texturedrectangles actually work, fix importing breaking rule system and refresh sprites after import, add separate import and export file managers, reset submitted content of file managers after submit, fix TexturedRectangles staying at 0, 0, fix some performance issues and update README --- README.md | 8 +++- game/file_manager.py | 12 +++--- game/play.py | 94 ++++++++++++++++++++++++++++++-------------- game/sprites.py | 14 ++++--- 4 files changed, 85 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 4c36d0b..375dca9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ -Chaos Protocol is a simulation game where you have a rule engine and objects, which you can apply rules to! By default, the game launches with random rules. +**Chaos Protocol** is a simulation game where you have a rule engine and objects, which you can apply rules to! By default, the game launches with random rules. Basically a framework of sorts, which can even be random! -The project is a huge WIP! You can't do much yet, but you have basic rules and simulation. +The game has Scratch-like blocks and conditions, and everything you probably need for a simple game. +The problem: it hasn't been well tested. + +## Speed is a big problem of the project, especially collision detections, which are very expensive. +I am not using spatial hashing right now, and i had no time to optimize the code. I'm sorry. [![Demo Video](https://img.youtube.com/vi/iMB4mmjTIB4/hqdefault.jpg)](https://youtu.be/iMB4mmjTIB4) \ No newline at end of file diff --git a/game/file_manager.py b/game/file_manager.py index f6b3b3f..cc536a8 100644 --- a/game/file_manager.py +++ b/game/file_manager.py @@ -55,8 +55,8 @@ class FileManager(arcade.gui.UIAnchorLayout): self.submit_button.visible = self.mode == "export" def submit(self, content): - self.submitted_content = content if self.mode == "import" else f"{content}/{self.filename_input.text}" - + self.submitted_content = content if self.mode == "import" else os.path.join(content, self.filename_input.text) + def get_content(self, directory): if not directory in self.content_cache or time.perf_counter() - self.content_cache[directory][-1] >= 30: try: @@ -113,11 +113,11 @@ class FileManager(arcade.gui.UIAnchorLayout): for file in self.get_content(self.current_directory): self.file_buttons.append(self.files_box.add(arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text=file, style=button_style, width=self.filemanager_width / 1.5, height=self.filemanager_height * 0.05,))) - if os.path.isdir(f"{self.current_directory}/{file}"): - self.file_buttons[-1].on_click = lambda event, directory=f"{self.current_directory}/{file}": self.change_directory(directory) + if os.path.isdir(os.path.join(self.current_directory, file)): + self.file_buttons[-1].on_click = lambda event, directory=os.path.join(self.current_directory, file): self.change_directory(directory) else: - self.file_buttons[-1].on_click = lambda event, file=f"{self.current_directory}/{file}": self.submit(file) - + self.file_buttons[-1].on_click = lambda event, file=os.path.join(self.current_directory, file): self.submit(file) + def change_directory(self, directory): if directory.startswith("//"): # Fix / paths directory = directory[1:] diff --git a/game/play.py b/game/play.py index d0f648f..9172c8f 100644 --- a/game/play.py +++ b/game/play.py @@ -6,7 +6,7 @@ from utils.preload import SPRITE_TEXTURES, button_texture, button_hovered_textur from utils.constants import button_style, DO_RULES, IF_RULES, SPRITES, ALLOWED_INPUT from game.rules import RuleUI, Block, VarBlock -from game.sprites import BaseShape, Rectangle, Circle, Triangle, TexturedRectangle +from game.sprites import BaseRectangle, BaseShape, Rectangle, Circle, Triangle, TexturedRectangle from game.file_manager import FileManager class Game(arcade.gui.UIView): @@ -23,7 +23,11 @@ class Game(arcade.gui.UIView): self.rules_box = RuleUI(self.window) - self.file_manager = FileManager(self.window.width * 0.95, self.window.height * 0.875, (0.95, 0.875), [".json"]).with_border() + self.import_file_manager = FileManager(self.window.width * 0.95, self.window.height * 0.875, (0.95, 0.875), [".json"]).with_border() + self.import_file_manager.change_mode("import") + + self.export_file_manager = FileManager(self.window.width * 0.95, self.window.height * 0.875, (0.95, 0.875), [".json"]).with_border() + self.export_file_manager.change_mode("export") self.ui_selector_box = self.anchor.add(arcade.gui.UIBoxLayout(vertical=False, space_between=self.window.width / 100), anchor_x="left", anchor_y="bottom", align_y=5, align_x=self.window.width / 100) self.add_ui_selector("Simulation", lambda event: self.simulation()) @@ -40,6 +44,7 @@ class Game(arcade.gui.UIView): self.rulesets = self.rules_box.rulesets self.sprite_add_filemanager = FileManager(self.window.width * 0.9, self.window.height * 0.75, (0.9, 0.75), [".png", ".jpg", ".jpeg", ".bmp", ".gif"]) + self.sprite_add_filemanager.change_mode("import") self.sprite_add_ui = arcade.gui.UIBoxLayout(size_hint=(0.95, 0.9), space_between=10) self.sprite_add_ui.add(arcade.gui.UILabel(text="Add Sprite", font_size=24, text_color=arcade.color.WHITE)) @@ -137,7 +142,7 @@ class Game(arcade.gui.UIView): a = float(a) if isinstance(shape, Circle): shape.radius = a - elif isinstance(shape, Rectangle): + elif isinstance(shape, BaseRectangle): shape.width = a shape.height = a elif isinstance(shape, Triangle): @@ -167,10 +172,9 @@ class Game(arcade.gui.UIView): self.shapes.append(Triangle(x, y, x + 10, y, x + 5, y + 10, color=arcade.color.WHITE, batch=self.shape_batch)) else: - self.shapes.append(TexturedRectangle(shape_type, img=SPRITE_TEXTURES.get(shape_type, SPRITE_TEXTURES["rectangle"]), x=x, y=y, batch=self.shape_batch)) - + self.shapes.append(TexturedRectangle(pyglet.image.load(self.sprite_types[shape_type]), x, y, batch=self.shape_batch, shape_type=shape_type)) + shape = self.shapes[-1] - self.triggered_events.append(["spawn", {"event_shape_type": shape.shape_type, "shape_size": shape.shape_size, "shape_x": shape.x, "shape_y": shape.y, "shape": shape, "shape_color": shape.shape_color}]) def add_sprite(self): @@ -283,12 +287,20 @@ class Game(arcade.gui.UIView): recurse(block) return max_num + + def dict_to_block(self, block_dict): + kwargs = block_dict.copy() + kwargs["children"] = [self.dict_to_block(child) for child in block_dict.get("children", [])] + kwargs["vars"] = [VarBlock(**var) for var in block_dict.get("vars", [])] + return Block(**kwargs) def on_update(self, delta_time): - if self.mode == "import" and self.file_manager.submitted_content: - with open(self.file_manager.submitted_content, "r") as file: + if self.mode == "import" and self.import_file_manager.submitted_content: + with open(self.import_file_manager.submitted_content, "r") as file: data = json.load(file) + self.import_file_manager.submitted_content = None + self.triggered_events = [] self.rulesets = {} @@ -296,26 +308,34 @@ class Game(arcade.gui.UIView): self.add_widget(arcade.gui.UIMessageBox(message_text="Invalid file. Could not import rules.", width=self.window.width * 0.5, height=self.window.height * 0.25)) return - for rule_num, ruleset in data["rulesets"].items(): - kwargs = ruleset - kwargs["children"] = [Block(**child) for child in ruleset["children"]] - kwargs["vars"] = [VarBlock(**var) for var in ruleset["vars"]] - block = Block(**kwargs) - self.rulesets[rule_num] = block + for rule_num, ruleset in data["rules"].items(): + block = self.dict_to_block(ruleset) + self.rulesets[int(rule_num)] = block - self.sprite_types = data.get("sprites", SPRITES) + self.sprite_types = data["sprites"] for sprite_name, sprite_path in self.sprite_types.items(): if not sprite_name in SPRITE_TEXTURES: SPRITE_TEXTURES[sprite_name] = arcade.load_texture(sprite_path) + + SPRITES[sprite_name] = sprite_path + self.sprites_grid.clear() + + for n, shape in enumerate(SPRITES): + row, col = n % 8, n // 8 + box = self.sprites_grid.add(arcade.gui.UIBoxLayout(), row=row, column=col) + box.add(arcade.gui.UILabel(text=shape, font_size=16, text_color=arcade.color.WHITE)) + box.add(arcade.gui.UIImage(texture=SPRITE_TEXTURES[shape], width=self.window.width / 15, height=self.window.width / 15)) + self.rules_box.rulesets = self.rulesets + self.rules_box.block_renderer.blocks = self.rulesets self.rules_box.current_rule_num = self.get_max_rule_num() + 1 self.rules_box.block_renderer.refresh() self.rules() - if self.mode == "export" and self.file_manager.submitted_content: - with open(self.file_manager.submitted_content, "w") as file: + if self.mode == "export" and self.export_file_manager.submitted_content: + with open(self.export_file_manager.submitted_content, "w") as file: file.write(json.dumps( { "rules": { @@ -326,6 +346,8 @@ class Game(arcade.gui.UIView): }, indent=4)) + self.export_file_manager.submitted_content = None + self.add_widget(arcade.gui.UIMessageBox(message_text="Rules and Sprites exported successfully!", width=self.window.width * 0.5, height=self.window.height * 0.25)) if not self.mode == "simulation": @@ -342,13 +364,29 @@ class Game(arcade.gui.UIView): self.recursive_execute_rule(rule, trigger_args) - for shape in self.shapes: - for shape_b in self.shapes: - if shape.check_collision(shape_b): - self.triggered_events.append(["collision", {"event_a_type": shape.shape_type, "event_b_type": shape.shape_type, "shape_size": shape.shape_size, "shape_x": shape.x, "shape_y": shape.y, "shape": shape, "shape_color": shape.color}]) + has_collision_rules = any( + rule.rule_type == "trigger" and rule.rule == "collision" + for rule in self.rulesets.values() + ) + for shape in self.shapes: shape.update(self.x_gravity, self.y_gravity) + if has_collision_rules: + for i, shape in enumerate(self.shapes): + for shape_b in self.shapes[i+1:]: + if shape.check_collision(shape_b): + self.triggered_events.append(["collision", { + "event_a_type": shape.shape_type, + "event_b_type": shape_b.shape_type, + "shape_size": shape.shape_size, + "shape_x": shape.x, + "shape_y": shape.y, + "shape": shape, + "shape_color": shape.shape_color + }]) + + for shape in self.shapes[:]: if shape.x < 0 or shape.x > self.window.width or shape.y < 0 or shape.y > self.window.height: self.destroy(shape) @@ -386,8 +424,10 @@ class Game(arcade.gui.UIView): self.rules_box.camera.zoom *= 1 + scroll_y * 0.1 def disable_previous(self): - if self.mode in ["import", "export"]: - self.anchor.remove(self.file_manager) + if self.mode == "import": + self.anchor.remove(self.import_file_manager) + elif self.mode == "export": + self.anchor.remove(self.export_file_manager) elif self.mode == "rules": self.anchor.remove(self.rules_box) elif self.mode == "sprites": @@ -408,17 +448,13 @@ class Game(arcade.gui.UIView): self.disable_previous() self.mode = "export" - - self.file_manager.change_mode("export") - self.anchor.add(self.file_manager, anchor_x="center", anchor_y="top", align_y=-self.window.height * 0.025) + self.anchor.add(self.export_file_manager, anchor_x="center", anchor_y="top", align_y=-self.window.height * 0.025) def import_file(self): self.disable_previous() self.mode = "import" - - self.file_manager.change_mode("import") - self.anchor.add(self.file_manager, anchor_x="center", anchor_y="top", align_y=-self.window.height * 0.025) + self.anchor.add(self.import_file_manager, anchor_x="center", anchor_y="top", align_y=-self.window.height * 0.025) def sprites(self): self.disable_previous() diff --git a/game/sprites.py b/game/sprites.py index a8a4faa..ee27731 100644 --- a/game/sprites.py +++ b/game/sprites.py @@ -31,7 +31,7 @@ class BaseShape(): def check_collision(self, other): if isinstance(other, Circle): return self._collides_with_circle(other) - elif isinstance(other, Rectangle): + elif isinstance(other, BaseRectangle): return self._collides_with_rectangle(other) elif isinstance(other, Triangle): return self._collides_with_triangle(other) @@ -106,7 +106,6 @@ class Circle(pyglet.shapes.Circle, BaseShape): return not (has_neg and has_pos) - class BaseRectangle(BaseShape): def __init__(self): super().__init__() @@ -182,15 +181,18 @@ class BaseRectangle(BaseShape): class Rectangle(pyglet.shapes.Rectangle, BaseRectangle): def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) BaseRectangle.__init__(self) + super().__init__(*args, **kwargs) self.shape_type = "rectangle" class TexturedRectangle(pyglet.sprite.Sprite, BaseRectangle): - def __init__(self, shape_type, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, img, x=0, y=0, *args, **kwargs): BaseRectangle.__init__(self) - self.shape_type = shape_type + self.shape_type = kwargs.pop("shape_type", "textured_rectangle") + super().__init__(img, x, y, *args, **kwargs) + + def update(self, x_gravity, y_gravity): + BaseShape.update(self, x_gravity, y_gravity) class Triangle(pyglet.shapes.Triangle, BaseShape): def __init__(self, *args, **kwargs):