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):