From b74115b489d675b788ebdce9256743c8673039de Mon Sep 17 00:00:00 2001 From: csd4ni3l Date: Sun, 7 Dec 2025 19:13:46 +0100 Subject: [PATCH] Fix importing and exporting, add variables (scratch blocks as well), recursively execute the rules, reset x and y gravity and events when switching to simulation, remove rule generation, fix indentation and padding, remove bloat, fix bugs --- game/play.py | 119 ++++++++----- game/rules.py | 427 +++++++++++++++++++++++++++++++-------------- utils/constants.py | 271 ++++++++++++++-------------- 3 files changed, 498 insertions(+), 319 deletions(-) diff --git a/game/play.py b/game/play.py index d5bd9e8..b977314 100644 --- a/game/play.py +++ b/game/play.py @@ -1,9 +1,11 @@ import arcade, arcade.gui, pyglet, random, json +from dataclasses import asdict + from utils.preload import SPRITE_TEXTURES, button_texture, button_hovered_texture from utils.constants import button_style, DO_RULES, IF_RULES, SHAPES, ALLOWED_INPUT -from game.rules import RuleUI +from game.rules import RuleUI, Block, VarBlock from game.sprites import BaseShape, Rectangle, Circle, Triangle from game.file_manager import FileManager @@ -35,9 +37,10 @@ class Game(arcade.gui.UIView): self.y_gravity = self.settings.get("default_y_gravity", 5) self.triggered_events = [] - self.rulesets, self.if_rules = self.rules_box.get_rulesets() + self.rulesets = self.rules_box.rulesets self.sprites_box = arcade.gui.UIAnchorLayout(size_hint=(0.95, 0.9)) + self.sprite_types = SHAPES self.shapes = [] self.shape_batch = pyglet.graphics.Batch() @@ -174,15 +177,17 @@ class Game(arcade.gui.UIView): 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.triggered_events.append(["game_launch", {}]) + self.sprites_box.add(arcade.gui.UITextureButton(text="Add Sprite", width=self.window.width / 2, height=self.window.height / 10, texture=button_texture, texture_hovered=button_hovered_texture, style=button_style)) - def get_rule_values(self, rule_dict, rule_values, event_args): - args = [rule_values[f"{user_var}_{n}"] for n, user_var in enumerate(rule_dict["user_vars"])] + self.triggered_events.append(["start", {}]) + + def get_vars(self, rule_dict, vars, event_args): + args = [vars[n].value for n in range(len(rule_dict["user_vars"]))] return args + [event_args[var] for var in rule_dict.get("vars", []) if not var in rule_dict["user_vars"]] - def check_rule(self, rule_dict, rule_values, event_args): - return rule_dict["func"](*self.get_rule_values(rule_dict, rule_values, event_args)) + def check_rule(self, rule_dict, vars, event_args): + return rule_dict["func"](*self.get_vars(rule_dict, vars, event_args)) def get_action_function(self, action_dict): ACTION_FUNCTION_DICT = { @@ -207,29 +212,69 @@ class Game(arcade.gui.UIView): return ACTION_FUNCTION_DICT[action_dict["type"]][action_dict["name"]] - def run_do_rule(self, rule_dict, rule_values, event_args): - self.get_action_function(rule_dict["action"])(*self.get_rule_values(rule_dict, rule_values, event_args)) + def run_do_rule(self, rule_dict, vars, event_args): + self.get_action_function(rule_dict["action"])(*self.get_vars(rule_dict, vars, event_args)) + + def recursive_execute_rule(self, rule, trigger_args): + for child_rule in rule.children: + child_rule_type = child_rule.rule_type + + if child_rule_type == "for": # TODO: Extend this when i add more FOR loop types + if child_rule.rule == "every_shape": + for shape in self.shapes: + event_args = trigger_args.copy() + event_args.update({"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}) + + self.recursive_execute_rule(child_rule, event_args) + + elif child_rule_type == "if": + if self.check_rule(IF_RULES[child_rule.rule], child_rule.vars, trigger_args): + self.recursive_execute_rule(child_rule, trigger_args) + + elif child_rule_type == "do": + self.run_do_rule(DO_RULES[child_rule.rule], child_rule.vars, trigger_args) + + def get_max_rule_num(self): + max_num = -1 + + def recurse(block: Block): + nonlocal max_num + max_num = max(max_num, block.rule_num) + for child in block.children: + recurse(child) + + for block in self.rulesets.values(): + recurse(block) + + return max_num 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: data = json.load(file) - if not data or not "rulesets" in data or not "rule_values" in data: - 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 - - self.rule_values = data["rule_values"] self.triggered_events = [] - # TODO: add rule loading here + if not data: + 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.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 + + self.rules_box.rulesets = 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: - file.write(json.dumps({ - "rulesets": self.rulesets, - "rule_values": self.rule_values - }, indent=4)) + file.write(json.dumps({rule_num: asdict(block) for rule_num, block in self.rulesets.items()}, indent=4)) if not self.mode == "simulation": return @@ -239,31 +284,11 @@ class Game(arcade.gui.UIView): while len(self.triggered_events) > 0: trigger, trigger_args = self.triggered_events.pop(0) - # In the new version, a DO rule's dependencies are the ruleset itself which trigger it - # Since there could be multiple IFs that depend on each other, we need to get the entrypoint values first and then interpret the tree. - event_args = trigger_args - - if_rule_values = {} - - for if_rule in self.if_rules: - if_rule_dict = IF_RULES[if_rule[0]] - if "shape_type" in if_rule_dict["user_vars"]: - is_true = False - for shape in self.shapes: - if is_true: - break - - event_args = trigger_args.copy() - if not "event_shape_type" in trigger_args: - event_args.update({"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}) - - is_true = self.check_rule(if_rule_dict, if_rule[1], trigger_args) - - if_rule_values[if_rule[2]] = is_true - - else: - event_args = trigger_args.copy() - if_rule_values[if_rule[2]] = self.check_rule(if_rule_dict, if_rule[1], trigger_args) + for rule_num, rule in self.rulesets.items(): + if not rule.rule_type == "trigger" or not trigger == rule.rule: + continue + + self.recursive_execute_rule(rule, trigger_args) for shape in self.shapes: for shape_b in self.shapes: @@ -350,8 +375,10 @@ class Game(arcade.gui.UIView): def simulation(self): self.disable_previous() - - self.rulesets, self.if_rules = self.rules_box.get_rulesets() + self.x_gravity = self.settings.get("default_x_gravity", 0) + self.y_gravity = self.settings.get("default_y_gravity", 5) + self.triggered_events = [] + self.rulesets = self.rules_box.rulesets self.mode = "simulation" def main_exit(self): diff --git a/game/rules.py b/game/rules.py index e518dd5..a4164f8 100644 --- a/game/rules.py +++ b/game/rules.py @@ -1,83 +1,44 @@ from utils.constants import ( DO_RULES, IF_RULES, - NON_COMPATIBLE_WHEN, - NON_COMPATIBLE_DO_WHEN, - VAR_NAMES, - VAR_DEFAULT, TRIGGER_RULES, FOR_RULES, + NEEDS_SHAPE, + PROVIDES_SHAPE, button_style, + slider_style, + dropdown_style, DO_COLOR, IF_COLOR, FOR_COLOR, - TRIGGER_COLOR + TRIGGER_COLOR, + RULE_DEFAULTS, + VAR_TYPES ) from typing import List from utils.preload import button_texture, button_hovered_texture, trash_bin from arcade.gui.experimental.scroll_area import UIScrollArea, UIScrollBar from dataclasses import dataclass, field -import arcade, arcade.gui, pyglet, random +import arcade, arcade.gui, pyglet, random, re -IF_KEYS = tuple(IF_RULES.keys()) -DO_KEYS = tuple(DO_RULES.keys()) - -BAD_WHEN = {tuple(sorted(pair)) for pair in NON_COMPATIBLE_WHEN} -BAD_DO_WHEN = {tuple(pair) for pair in NON_COMPATIBLE_DO_WHEN} - -def generate_rule(rule_type): +def get_rule_dict(rule_type): if rule_type == "if": - return random.choice(IF_KEYS) + return IF_RULES + elif rule_type == "for": + return FOR_RULES + elif rule_type == "trigger": + return TRIGGER_RULES elif rule_type == "do": - return random.choice(DO_KEYS) + return DO_RULES -def get_rule_description(rule_type, rule): - if rule_type == "if": - return IF_RULES[rule]["description"] - if rule_type == "for": - return FOR_RULES[rule]["description"] - if rule_type == "trigger": - return TRIGGER_RULES[rule]["description"] - if rule_type == "do": - return DO_RULES[rule]["description"] - -def per_widget_height(height, widget_count): - return height // widget_count - -def get_rule_defaults(rule_type): - if rule_type == "if": - return { - rule_key: ( - rule_dict["description"].format_map( - { - VAR_NAMES[n]: VAR_NAMES[n] - for n, variable in enumerate(rule_dict["user_vars"]) - } - ), - { - VAR_NAMES[n]: VAR_DEFAULT[variable] - for n, variable in enumerate(rule_dict["user_vars"]) - }, - ) - for rule_key, rule_dict in IF_RULES.items() - } - - elif rule_type == "do": - return { - rule_key: ( - rule_dict["description"].format_map( - { - VAR_NAMES[n]: VAR_NAMES[n] - for n, variable in enumerate(rule_dict["user_vars"]) - } - ), - { - VAR_NAMES[n]: VAR_DEFAULT[variable] - for n, variable in enumerate(rule_dict["user_vars"]) - }, - ) - for rule_key, rule_dict in DO_RULES.items() - } +@dataclass +class VarBlock: + x: float + y: float + label: str + var_type: str + connected_rule_num: str + value: str | int @dataclass class Block: @@ -87,17 +48,18 @@ class Block: rule_type: str rule: str rule_num: int - rule_values: dict[str, int | str] + vars: List["VarBlock"] = field(default_factory=list) children: List["Block"] = field(default_factory=list) class BlockRenderer: - def __init__(self, blocks: List[Block], indent: int = 10): + def __init__(self, blocks: List[Block], indent: int = 12): self.blocks = blocks self.indent = indent self.shapes = pyglet.graphics.Batch() self.shapes_by_rule_num = {} self.text_objects = [] self.text_by_rule_num = {} + self.var_widgets = {} self.refresh() def refresh(self): @@ -113,9 +75,86 @@ class BlockRenderer: self.shapes_by_rule_num = {} self.text_objects = [] self.text_by_rule_num = {} + self.var_widgets = {} for b in self.blocks.values(): self._build_block(b, b.x, b.y) + def _build_var_ui(self, var: VarBlock, x: int, y: int, rule_num: int) -> tuple: + var_width = max(60, len(str(var.value)) * 8 + 20) + var_height = 24 + var_color = (255, 255, 255) + var_rect = pyglet.shapes.BorderedRectangle( + x, y - var_height // 2, var_width, var_height, + 2, var_color, arcade.color.BLACK, batch=self.shapes + ) + + var_text = pyglet.text.Label( + text=str(var.value), + x=x + var_width // 2, + y=y, + color=arcade.color.BLACK, + font_size=10, + anchor_x='center', + anchor_y='center' + ) + + if rule_num not in self.shapes_by_rule_num: + self.shapes_by_rule_num[rule_num] = [] + if rule_num not in self.text_by_rule_num: + self.text_by_rule_num[rule_num] = [] + if rule_num not in self.var_widgets: + self.var_widgets[rule_num] = [] + + self.shapes_by_rule_num[rule_num].append(var_rect) + self.text_by_rule_num[rule_num].append(var_text) + self.text_objects.append(var_text) + + self.var_widgets[rule_num].append({ + 'var': var, + 'rect': var_rect, + 'text': var_text, + 'x': x, + 'y': y, + 'width': var_width, + 'height': var_height + }) + + return var_width, var_height + + def _build_block_with_vars(self, b: Block, x: int, y: int) -> None: + lx, ly = x, y - 42 + + current_x = lx + 10 + current_y = ly + 28 + + pattern = r' ([a-z]) ' + parts = re.split(pattern, b.label) + + var_index = 0 + for i, part in enumerate(parts): + if i % 2 == 0: + if part: + text_obj = pyglet.text.Label( + text=part, + x=current_x, + y=current_y - 3, + color=arcade.color.BLACK, + font_size=12, + weight="bold" + ) + self.text_objects.append(text_obj) + self.text_by_rule_num[b.rule_num].append(text_obj) + + current_x += len(part) * 10 + else: + if var_index < len(b.vars): + var = b.vars[var_index] + var_width, var_height = self._build_var_ui( + var, current_x, current_y, b.rule_num + ) + current_x += var_width + 7 + var_index += 1 + def _build_block(self, b: Block, x: int, y: int) -> int: is_wrap = b.rule_type != "do" h, w = 42, 280 @@ -129,7 +168,7 @@ class BlockRenderer: elif b.rule_type == "for": color = FOR_COLOR - lx, ly = x, y - h + lx, ly = x, y - h if b.rule_num not in self.shapes_by_rule_num: self.shapes_by_rule_num[b.rule_num] = [] @@ -139,21 +178,31 @@ class BlockRenderer: rect = pyglet.shapes.BorderedRectangle(lx, ly, w, h, 2, color, arcade.color.BLACK, batch=self.shapes) self.shapes_by_rule_num[b.rule_num].append(rect) - text_obj = pyglet.text.Label(text=b.label, x=lx + 10, y=ly + 20, color=arcade.color.BLACK, font_size=12, weight="bold") - self.text_objects.append(text_obj) - self.text_by_rule_num[b.rule_num].append(text_obj) + if b.vars: + self._build_block_with_vars(b, x, y) + else: + text_obj = pyglet.text.Label( + text=b.label, + x=lx + 7, + y=ly + 20, + color=arcade.color.BLACK, + font_size=12, + weight="bold" + ) + self.text_objects.append(text_obj) + self.text_by_rule_num[b.rule_num].append(text_obj) - ny = ly + next_y = ly if is_wrap: - iy = ny + iy = next_y for child in b.children: child.x = lx + self.indent + 5 - child.y = iy - 2 - iy = self._build_block(child, lx + self.indent + 5, iy - 2) + child.y = iy + iy = self._build_block(child, lx + self.indent + 5, iy) - bar_h = ny - iy + bar_h = next_y - iy bar_filled = pyglet.shapes.Rectangle(lx + 2, iy + 2, self.indent, bar_h, color, batch=self.shapes) - line1 = pyglet.shapes.Line(lx, ny, lx, iy, 2, arcade.color.BLACK, batch=self.shapes) + line1 = pyglet.shapes.Line(lx, next_y, lx, iy, 2, arcade.color.BLACK, batch=self.shapes) bottom = pyglet.shapes.BorderedRectangle(lx, iy - 8, w, 24, 2, color, arcade.color.BLACK, batch=self.shapes) self.shapes_by_rule_num[b.rule_num].extend([bar_filled, line1, bottom]) @@ -161,19 +210,36 @@ class BlockRenderer: return iy - 24 else: for child in b.children: - ny = self._build_block(child, lx, ny) - return ny + child.x = lx + child.y = next_y + ly = self._build_block(child, lx, next_y) + return ly - 16 def move_block(self, x, y, rule_num): for element in self.shapes_by_rule_num[rule_num] + self.text_by_rule_num[rule_num]: element.x += x element.y += y + + if rule_num in self.var_widgets: + for widget in self.var_widgets[rule_num]: + widget['x'] += x + widget['y'] += y block = self._find_block(rule_num) for child in block.children: self.move_block(x, y, child.rule_num) + def get_var_at_position(self, x, y): + for rule_num, widgets in self.var_widgets.items(): + for widget in widgets: + wx, wy = widget['x'], widget['y'] + ww, wh = widget['width'], widget['height'] + if (wx <= x <= wx + ww and + wy - wh // 2 <= y <= wy + wh // 2): + return widget['var'], rule_num + return None, None + def _find_block(self, rule_num): if rule_num in self.blocks: return self.blocks[rule_num] @@ -198,6 +264,101 @@ class BlockRenderer: for t in self.text_objects: t.draw() +class VarEditDialog(arcade.gui.UIAnchorLayout): + def __init__(self, var: VarBlock, on_save, on_cancel): + super().__init__() + self.var = var + self.on_save_callback = on_save + self.on_cancel_callback = on_cancel + + self.background = self.add( + arcade.gui.UISpace(color=(0, 0, 0, 180)), + anchor_x="center", + anchor_y="center" + ) + + dialog_box = arcade.gui.UIBoxLayout( + space_between=10, + width=300, + height=200 + ) + + dialog_box.with_padding(all=20) + dialog_box.with_background(color=(60, 60, 80)) + + dialog_box.add(arcade.gui.UILabel( + text=f"Edit {var.label}", + font_size=16, + text_color=arcade.color.WHITE + )) + + if var.var_type == "variable": + self.input_field = arcade.gui.UIInputText( + text=str(var.value), + width=260, + height=40 + ) + dialog_box.add(self.input_field) + elif var.var_type in ["shape_type", "target_type", "color", "key_input", "comparison"]: + from utils.constants import VAR_OPTIONS + options = VAR_OPTIONS[var.var_type] + self.dropdown = arcade.gui.UIDropdown( + default=str(var.value), + options=options, + width=260, + height=40, + style=dropdown_style + ) + dialog_box.add(self.dropdown) + elif var.var_type == "size": + self.slider = arcade.gui.UISlider( + value=int(var.value), + min_value=1, + max_value=200, + width=260, + height=40, + style=slider_style + ) + dialog_box.add(self.slider) + + button_layout = arcade.gui.UIBoxLayout(vertical=False, space_between=10) + + save_btn = arcade.gui.UIFlatButton( + text="Save", + width=125, + height=40 + ) + save_btn.on_click = self._on_save + + cancel_btn = arcade.gui.UIFlatButton( + text="Cancel", + width=125, + height=40 + ) + cancel_btn.on_click = self._on_cancel + + button_layout.add(save_btn) + button_layout.add(cancel_btn) + dialog_box.add(button_layout) + + self.add(dialog_box, anchor_x="center", anchor_y="center") + + def _on_save(self, event): + if hasattr(self, 'input_field'): + try: + self.var.value = int(self.input_field.text) + except ValueError: + self.var.value = self.input_field.text + elif hasattr(self, 'dropdown'): + self.var.value = self.dropdown.value + elif hasattr(self, 'slider'): + self.var.value = int(self.slider.value) + + self.on_save_callback() + + def _on_cancel(self, event): + self.on_cancel_callback() + class RuleUI(arcade.gui.UIAnchorLayout): def __init__(self, window: arcade.Window): super().__init__(size_hint=(1, 0.875)) @@ -205,6 +366,7 @@ class RuleUI(arcade.gui.UIAnchorLayout): self.window = window self.current_rule_num = 0 self.rule_values = {} + self.var_edit_dialog = None self.rulesets: dict[int, Block] = {} @@ -230,7 +392,7 @@ class RuleUI(arcade.gui.UIAnchorLayout): self.create_sidebar = self.add(arcade.gui.UIBoxLayout(size_hint=(0.15, 1), vertical=False, space_between=5), anchor_x="left", anchor_y="bottom") self.scroll_area = UIScrollArea(size_hint=(0.95, 1)) # center on screen - self.scroll_area.scroll_speed = -50 + self.scroll_area.scroll_speed = 0 self.create_sidebar.add(self.scroll_area) self.scrollbar = UIScrollBar(self.scroll_area) @@ -239,74 +401,49 @@ class RuleUI(arcade.gui.UIAnchorLayout): self.create_box = self.scroll_area.add(arcade.gui.UIBoxLayout(space_between=10)) - self.create_box.add(arcade.gui.UISpace(height=self.window.height / 100)) - self.create_box.add(arcade.gui.UILabel(text="Trigger Rules", font_size=18)) - self.create_box.add(arcade.gui.UISpace(height=self.window.height / 200)) - for trigger_rule, trigger_rule_data in TRIGGER_RULES.items(): - create_button = self.create_box.add(arcade.gui.UITextureButton(text=trigger_rule_data["description"].format_map({ - "a": "a", - "b": "b", - "c": "c" - }), width=self.window.width * 0.125, multiline=True, height=self.window.height * 0.05, style=button_style, texture=button_texture, texture_hovered=button_hovered_texture)) - create_button.on_click = lambda event, trigger_rule=trigger_rule: self.add_rule("trigger", trigger_rule) + self.add_rule_create_box("trigger") + self.add_rule_create_box("if") + self.add_rule_create_box("do") + self.add_rule_create_box("for") - self.create_box.add(arcade.gui.UISpace(height=self.window.height / 100)) - self.create_box.add(arcade.gui.UILabel(text="IF Rules", font_size=18)) - self.create_box.add(arcade.gui.UISpace(height=self.window.height / 200)) - for if_rule, if_rule_data in IF_RULES.items(): - create_button = self.create_box.add(arcade.gui.UITextureButton(text=if_rule_data["description"].format_map({ - "a": "a", - "b": "b", - "c": "c" - }), width=self.window.width * 0.135, multiline=True, height=self.window.height * 0.05, style=button_style, texture=button_texture, texture_hovered=button_hovered_texture)) - create_button.on_click = lambda event, if_rule=if_rule: self.add_rule("if", if_rule) - - self.create_box.add(arcade.gui.UISpace(height=self.window.height / 100)) - self.create_box.add(arcade.gui.UILabel(text="DO Rules", font_size=18)) - self.create_box.add(arcade.gui.UISpace(height=self.window.height / 200)) - for do_rule, do_rule_data in DO_RULES.items(): - create_button = self.create_box.add(arcade.gui.UITextureButton(text=do_rule_data["description"].format_map({ - "a": "a", - "b": "b", - "c": "c" - }), width=self.window.width * 0.135, multiline=True, height=self.window.height * 0.05, style=button_style, texture=button_texture, texture_hovered=button_hovered_texture)) - create_button.on_click = lambda event, do_rule=do_rule: self.add_rule("do", do_rule) - - self.create_box.add(arcade.gui.UISpace(height=self.window.height / 100)) - self.create_box.add(arcade.gui.UILabel(text="For Rules", font_size=18)) - self.create_box.add(arcade.gui.UISpace(height=self.window.height / 200)) - for for_rule, for_rule_data in FOR_RULES.items(): - create_button = self.create_box.add(arcade.gui.UITextureButton(text=for_rule_data["description"].format_map({ - "a": "a", - "b": "b", - "c": "c" - }), width=self.window.width * 0.135, multiline=True, height=self.window.height * 0.05, style=button_style, texture=button_texture, texture_hovered=button_hovered_texture)) - create_button.on_click = lambda event, for_rule=for_rule: self.add_rule("for", for_rule) - self.trash_spritelist = arcade.SpriteList() self.trash_sprite = trash_bin self.trash_sprite.scale = 0.5 self.trash_sprite.position = (self.window.width * 0.9, self.window.height * 0.2) self.trash_spritelist.append(self.trash_sprite) - def get_rulesets(self): - # TODO: remove this - return [], [] + def add_rule_create_box(self, rule_type): + self.create_box.add(arcade.gui.UISpace(height=self.window.height / 100)) + self.create_box.add(arcade.gui.UILabel(text=f"{rule_type.capitalize()} Rules", font_size=18)) + self.create_box.add(arcade.gui.UISpace(height=self.window.height / 200)) + for rule in get_rule_dict(rule_type): + create_button = self.create_box.add(arcade.gui.UITextureButton(text=RULE_DEFAULTS[rule_type][rule][0], width=self.window.width * 0.135, multiline=True, height=self.window.height * 0.05, style=button_style, texture=button_texture, texture_hovered=button_hovered_texture)) + create_button.on_click = lambda event, rule=rule: self.add_rule(rule_type, rule) def generate_pos(self): return random.randint( self.window.width * 0.1, int(self.window.width * 0.9) ), random.randint(self.window.height * 0.1, int(self.window.height * 0.7)) - def add_rule(self, rule_type, force=None): - rule = force or generate_rule(rule_type) + def add_rule(self, rule_type, rule): + rule_dict = get_rule_dict(rule_type)[rule] rule_box = Block( *self.generate_pos(), - get_rule_description(rule_type, rule), + RULE_DEFAULTS[rule_type][rule][0], rule_type, rule, self.current_rule_num, - {}, + [ + VarBlock( + *self.generate_pos(), + VAR_TYPES[var_type], + var_type, + self.current_rule_num, + RULE_DEFAULTS[rule_type][rule][1][n] + ) + + for n, var_type in enumerate(rule_dict["user_vars"]) + ], [] ) @@ -323,8 +460,11 @@ class RuleUI(arcade.gui.UIAnchorLayout): self.trash_spritelist.draw() def drag_n_drop_check(self, blocks): + if self.dragged_rule_ui.rule_type == "trigger": + return + for block in blocks: - if block == self.dragged_rule_ui: + if block == self.dragged_rule_ui or (self.dragged_rule_ui.rule in NEEDS_SHAPE and block.rule not in PROVIDES_SHAPE): continue if arcade.LBWH(block.x, block.y - 44, 280, 44).intersection(arcade.LBWH(self.dragged_rule_ui.x, self.dragged_rule_ui.y - 44, 280, 44)): @@ -361,6 +501,10 @@ class RuleUI(arcade.gui.UIAnchorLayout): self.press_check(event, block.children) def on_event(self, event): + if self.var_edit_dialog: + super().on_event(event) + return + super().on_event(event) if isinstance(event, arcade.gui.UIMouseDragEvent): @@ -371,6 +515,13 @@ class RuleUI(arcade.gui.UIAnchorLayout): self.block_renderer.move_block(event.dx, event.dy, self.dragged_rule_ui.rule_num) elif isinstance(event, arcade.gui.UIMousePressEvent): + projected_vec = self.camera.unproject((event.x, event.y)) + var, _ = self.block_renderer.get_var_at_position(projected_vec.x, projected_vec.y) + + if var: + self.open_var_edit_dialog(var) + return + self.press_check(event, list(self.rulesets.values())) elif isinstance(event, arcade.gui.UIMouseReleaseEvent): @@ -386,6 +537,23 @@ class RuleUI(arcade.gui.UIAnchorLayout): self.dragged_rule_ui = None + def open_var_edit_dialog(self, var: VarBlock): + def on_save(): + self.close_var_edit_dialog() + self.block_renderer.refresh() + + def on_cancel(): + self.close_var_edit_dialog() + + self.var_edit_dialog = VarEditDialog(var, on_save, on_cancel) + self.add(self.var_edit_dialog) + + def close_var_edit_dialog(self): + if self.var_edit_dialog: + self.remove(self.var_edit_dialog) + self.var_edit_dialog = None + self.trigger_full_render() + def on_update(self, dt): if self.dragged_rule_ui: block_vec = self.camera.unproject((self.dragged_rule_ui.x, self.dragged_rule_ui.y)) @@ -393,4 +561,5 @@ class RuleUI(arcade.gui.UIAnchorLayout): self.trash_sprite.update_animation() else: self.trash_sprite.time = 0 - self.trash_sprite.update_animation() \ No newline at end of file + self.trash_sprite.update_animation() + diff --git a/utils/constants.py b/utils/constants.py index 7f4299a..52f5561 100644 --- a/utils/constants.py +++ b/utils/constants.py @@ -1,4 +1,4 @@ -import arcade.color +import arcade.color, operator from arcade.types import Color from arcade.gui.widgets.buttons import UITextureButtonStyle, UIFlatButtonStyle from arcade.gui.widgets.slider import UISliderStyle @@ -29,6 +29,15 @@ COLORS = [ COMPARISONS = [">", ">=", "<", "<=", "==", "!="] +OPS = { + ">": operator.gt, + "<": operator.lt, + ">=": operator.ge, + "<=": operator.le, + "==": operator.eq, + "!=": operator.ne, +} + VAR_DEFAULT = { "shape_type": SHAPES[0], "target_type": SHAPES[1], @@ -49,39 +58,37 @@ VAR_OPTIONS = { "comparison": COMPARISONS } +VAR_TYPES = { + "shape_type": "Shape Type", + "target_type": "Target Type", + "variable": "Variable", + "color": "Color", + "size": "Size", + "key_input": "Key Input", + "comparison": "Comparison" +} + TRIGGER_RULES = { "every_update": { "key": "every_update", + "user_vars": [], + "vars": [], "description": "Every Update", + "func": lambda *v: True }, "start": { "key": "start", + "user_vars": [], + "vars": [], "description": "On Game Start", + "func": lambda *v: True }, "on_input": { "key": "on_input", + "user_vars": ["key_input"], + "vars": ["key_input", "event_key"], "description": "IF {a} key is pressed", - }, - "x_position_compare": { - "key": "x_position_compare", - "description": "IF X for {a} shape is {b} {c}", - "user_vars": ["shape_type", "comparison", "variable"], - "vars": ["shape_type", "comparison", "variable", "event_shape_type", "shape_x"], - "func": lambda *v: (v[0] == v[3]) and eval(f"{v[4]} {v[1]} {v[2]}") - }, - "y_position_compare": { - "key": "y_position_compare", - "description": "IF Y for {a} shape is {b} {c}", - "user_vars": ["shape_type", "comparison", "variable"], - "vars": ["shape_type", "comparison", "variable", "event_shape_type", "shape_y"], - "func": lambda *v: (v[0] == v[3]) and eval(f"{v[4]} {v[1]} {v[2]}") - }, - "size_compare": { - "key": "size_compare", - "description": "IF {a} shape size is {b} {c}", - "user_vars": ["shape_type", "comparison", "variable"], - "vars": ["shape_type", "comparison", "variable", "event_shape_type", "shape_size"], - "func": lambda *v: (v[0] == v[3]) and eval(f"{v[4]} {v[1]} {v[2]}") + "func": lambda *v: v[0] == v[1] }, "spawns": { "key": "spawns", @@ -97,20 +104,6 @@ TRIGGER_RULES = { "vars": ["shape_type", "event_shape_type"], "func": lambda *v: v[0] == v[1] }, - "x_velocity_changes": { - "key": "x_velocity_changes", - "description": "IF {a} shape X velocity changes", - "user_vars": ["shape_type"], - "vars": ["shape_type", "event_shape_type"], - "func": lambda *v: v[0] == v[1] - }, - "y_velocity_changes": { - "key": "y_velocity_changes", - "description": "IF {a} shape Y velocity changes", - "user_vars": ["shape_type"], - "vars": ["shape_type", "event_shape_type"], - "func": lambda *v: v[0] == v[1] - }, "color_changes": { "key": "color_changes", "description": "IF {a} shape color changes", @@ -139,11 +132,34 @@ TRIGGER_RULES = { "vars": ["shape_type", "target_type", "event_a_type", "event_b_type"], "func": lambda *v: (v[0] == v[2]) and (v[3] == v[1]) }, + "on_left_click": { + "key": "on_left_click", + "description": "IF you left click", + "user_vars": [], + "vars": [], + "func": lambda *v: True + }, + "on_right_click": { + "key": "on_right_click", + "description": "IF you right click", + "user_vars": [], + "vars": [], + "func": lambda *v: True + }, + "on_mouse_move": { + "key": "on_mouse_move", + "description": "IF mouse moves", + "user_vars": [], + "vars": [], + "func": lambda *v: True + }, } FOR_RULES = { "every_shape": { "key": "every_shape", + "user_vars": [], + "vars": [], "description": "For every shape", } } @@ -154,35 +170,35 @@ IF_RULES = { "description": "IF X is {a} {b}", "user_vars": ["comparison", "variable"], "vars": ["comparison", "variable", "shape_x"], - "func": lambda *v: eval(f"{v[2]} {v[0]} {v[1]}") + "func": lambda *v: OPS[v[0]](v[2], v[1]) }, "y_position_compare": { "key": "y_position_compare", "description": "IF Y is {a} {b}", "user_vars": ["comparison", "variable"], "vars": ["comparison", "variable", "shape_y"], - "func": lambda *v: eval(f"{v[2]} {v[0]} {v[1]}") + "func": lambda *v: OPS[v[0]](v[2], v[1]) }, "size_compare": { "key": "size_compare", "description": "IF size is {a} {b}", "user_vars": ["comparison", "variable"], "vars": ["comparison", "variable", "shape_size"], - "func": lambda *v: eval(f"{v[2]} {v[0]} {v[1]}") + "func": lambda *v: OPS[v[0]](v[2], v[1]) }, "x_velocity_compare": { "key": "x_velocity_compare", "description": "IF X velocity is {a} {b}", "user_vars": ["comparison", "variable"], "vars": ["comparison", "variable", "shape_x_velocity"], - "func": lambda *v: eval(f"{v[2]} {v[0]} {v[1]}") + "func": lambda *v: OPS[v[0]](v[2], v[1]) }, "y_velocity_compare": { "key": "y_velocity_compare", "description": "IF Y velocity is {a} {b}", "user_vars": ["comparison", "variable"], "vars": ["comparison", "variable", "shape_y_velocity"], - "func": lambda *v: eval(f"{v[2]} {v[0]} {v[1]}") + "func": lambda *v: OPS[v[0]](v[2], v[1]) }, "color_is": { "key": "color_is", @@ -191,111 +207,15 @@ IF_RULES = { "vars": ["color", "shape_color"], "func": lambda *v: v[0] == v[1] }, + "shape_type_is": { + "key": "shape_type_is", + "description": "IF shape type is {a}", + "user_vars": ["shape_type"], + "vars": ["shape_type", "event_shape_type"], + "func": lambda *v: v[0] == v[1] + }, } -NON_COMPATIBLE_WHEN = [ - ("spawns", "destroyed"), - ("spawns", "morphs"), - ("spawns", "collides"), - ("spawns", "x_velocity_changes"), - ("spawns", "y_velocity_changes"), - ("spawns", "x_gravity_changes"), - ("spawns", "y_gravity_changes"), - ("spawns", "color_changes"), - ("spawns", "size_changes"), - - ("destroyed", "morphs"), - ("destroyed", "collides"), - ("destroyed", "x_velocity_changes"), - ("destroyed", "y_velocity_changes"), - ("destroyed", "x_gravity_changes"), - ("destroyed", "y_gravity_changes"), - ("destroyed", "color_changes"), - ("destroyed", "size_changes"), - - ("morphs", "collides"), - ("morphs", "x_velocity_changes"), - ("morphs", "y_velocity_changes"), - ("morphs", "x_gravity_changes"), - ("morphs", "y_gravity_changes"), - ("morphs", "color_changes"), - ("morphs", "size_changes"), - - ("collides", "destroyed"), - ("collides", "morphs"), - - ("every_update", "spawns"), - ("every_update", "destroyed"), - ("every_update", "morphs"), - ("every_update", "collides"), - ("every_update", "x_velocity_changes"), - ("every_update", "y_velocity_changes"), - ("every_update", "x_gravity_changes"), - ("every_update", "y_gravity_changes"), - ("every_update", "color_changes"), - ("every_update", "size_changes"), - ("every_update", "game_launch"), - - ("game_launch", "spawns"), - ("game_launch", "destroyed"), - ("game_launch", "morphs"), - ("game_launch", "collides"), - ("game_launch", "x_velocity_changes"), - ("game_launch", "y_velocity_changes"), - ("game_launch", "x_gravity_changes"), - ("game_launch", "y_gravity_changes"), - ("game_launch", "color_changes"), - ("game_launch", "size_changes"), -] - -NON_COMPATIBLE_DO_WHEN = [ - ("destroyed", "change_x"), - ("destroyed", "change_y"), - ("destroyed", "move_x"), - ("destroyed", "move_y"), - ("destroyed", "change_x_velocity"), - ("destroyed", "change_y_velocity"), - ("destroyed", "change_x_gravity"), - ("destroyed", "change_y_gravity"), - ("destroyed", "change_color"), - ("destroyed", "change_size"), - ("destroyed", "morph_into"), - ("destroyed", "destroy"), - - ("morphs", "morph_into"), - - ("x_velocity_changes", "change_x_velocity"), - ("y_velocity_changes", "change_y_velocity"), - - ("color_changes", "change_color"), - - ("size_changes", "change_size"), - - ("every_update", "change_x"), - ("every_update", "change_y"), - ("every_update", "move_x"), - ("every_update", "move_y"), - ("every_update", "change_x_velocity"), - ("every_update", "change_y_velocity"), - ("every_update", "change_color"), - ("every_update", "change_size"), - ("every_update", "destroy"), - ("every_update", "morph_into"), - - ("game_launch", "change_x"), - ("game_launch", "change_y"), - ("game_launch", "move_x"), - ("game_launch", "move_y"), - ("game_launch", "change_x_velocity"), - ("game_launch", "change_y_velocity"), - ("game_launch", "change_x_gravity"), - ("game_launch", "change_y_gravity"), - ("game_launch", "change_color"), - ("game_launch", "change_size"), - ("game_launch", "destroy"), - ("game_launch", "morph_into") -] - DO_RULES = { "change_x": { "key": "change_x", @@ -402,6 +322,69 @@ DO_RULES = { } } +PROVIDES_SHAPE = [ + # Trigger + "spawns", + "color_changes", + "size_changes", + "morphs", + "collides", + + # IFs, technically, these need and provide a shape to the next rule + "x_position_compare", + "y_position_compare", + "size_compare", + "x_velocity_compare", + "y_velocity_compare", + "color_is", + "shape_type_is", + + # FOR + "every_shape" +] + +NEEDS_SHAPE = [ + # IF + "x_position_compare", + "y_position_compare", + "size_compare", + "x_velocity_compare", + "y_velocity_compare", + "color_is", + "shape_type_is", + + # DO + "change_x", + "change_y", + "move_x", + "move_y", + "change_x_velocity", + "change_y_velocity", + "change_size", + "destroy", + "morph_into" +] + +RULE_DEFAULTS = { + rule_type: { + rule_key: ( + rule_dict["description"].format_map( + { + VAR_NAMES[n]: VAR_NAMES[n] + for n, variable in enumerate(rule_dict["user_vars"]) + } + ), + [ + VAR_DEFAULT[variable] + for variable in rule_dict["user_vars"] + ], + ) + for rule_key, rule_dict in rule_var.items() + } + + for rule_type, rule_var in [("if", IF_RULES), ("do", DO_RULES), ("trigger", TRIGGER_RULES), ("for", FOR_RULES)] +} + menu_background_color = (30, 30, 47) log_dir = 'logs' discord_presence_id = 1440807203094138940