From f51260d94fe8f9ebd1830aacd4d3dbd94fb178a2 Mon Sep 17 00:00:00 2001 From: csd4ni3l Date: Sat, 6 Dec 2025 23:35:40 +0100 Subject: [PATCH] Create a whole new system, with Scratch-like drag and drop mechanism, remove bezier, connections, and use pyglet shapes which are even faster. Nothing works yet. Add trigger, for loops and move most IFs to triggers. --- game/play.py | 56 +++- game/rules.py | 695 +++++++++++++++++---------------------------- utils/constants.py | 132 +++++---- 3 files changed, 370 insertions(+), 513 deletions(-) diff --git a/game/play.py b/game/play.py index 7dc9d29..d5bd9e8 100644 --- a/game/play.py +++ b/game/play.py @@ -35,7 +35,7 @@ class Game(arcade.gui.UIView): self.y_gravity = self.settings.get("default_y_gravity", 5) self.triggered_events = [] - self.rulesets = self.rules_box.get_rulesets() + self.rulesets, self.if_rules = self.rules_box.get_rulesets() self.sprites_box = arcade.gui.UIAnchorLayout(size_hint=(0.95, 0.9)) @@ -176,13 +176,13 @@ class Game(arcade.gui.UIView): self.triggered_events.append(["game_launch", {}]) - def get_rule_values(self, rule_num, rule_dict, rule_values, event_args): - args = [rule_values[f"{rule_num}_{user_var}_{n}"] for n, user_var in enumerate(rule_dict["user_vars"])] + 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"])] 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_num, rule_dict, rule_values, event_args): - return rule_dict["func"](*self.get_rule_values(rule_num, rule_dict, rule_values, event_args)) + 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 get_action_function(self, action_dict): ACTION_FUNCTION_DICT = { @@ -207,8 +207,8 @@ class Game(arcade.gui.UIView): return ACTION_FUNCTION_DICT[action_dict["type"]][action_dict["name"]] - def run_do_rule(self, rule_num, rule_dict, rule_values, event_args): - self.get_action_function(rule_dict["action"])(*self.get_rule_values(rule_num, rule_dict, rule_values, event_args)) + 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 on_update(self, delta_time): if self.mode == "import" and self.file_manager.submitted_content: @@ -239,8 +239,31 @@ class Game(arcade.gui.UIView): while len(self.triggered_events) > 0: trigger, trigger_args = self.triggered_events.pop(0) - for rule in self.rulesets: - ... + # 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 shape in self.shapes: for shape_b in self.shapes: @@ -277,6 +300,14 @@ class Game(arcade.gui.UIView): self.triggered_events.append(["on_mouse_move", {}]) + def on_mouse_drag(self, x, y, dx, dy, _buttons, _modifiers): + if self.mode == "rules" and arcade.MOUSE_BUTTON_MIDDLE == _buttons: + self.rules_box.camera.position -= (dx, dy) + + def on_mouse_scroll(self, x, y, scroll_x, scroll_y): + if self.mode == "rules": + 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) @@ -320,7 +351,7 @@ class Game(arcade.gui.UIView): def simulation(self): self.disable_previous() - self.rulesets = self.rules_box.get_rulesets() + self.rulesets, self.if_rules = self.rules_box.get_rulesets() self.mode = "simulation" def main_exit(self): @@ -333,6 +364,9 @@ class Game(arcade.gui.UIView): if self.mode == "simulation": self.shape_batch.draw() elif self.mode == "rules": - self.rules_box.draw() + with self.rules_box.camera.activate(): + self.rules_box.draw() + + self.rules_box.draw_unproject() self.ui.draw() \ No newline at end of file diff --git a/game/rules.py b/game/rules.py index 236c101..f0b0599 100644 --- a/game/rules.py +++ b/game/rules.py @@ -1,19 +1,23 @@ from utils.constants import ( DO_RULES, IF_RULES, - LOGICAL_OPERATORS, NON_COMPATIBLE_WHEN, NON_COMPATIBLE_DO_WHEN, VAR_NAMES, VAR_DEFAULT, - VAR_OPTIONS, - dropdown_style, - slider_style, + TRIGGER_RULES, + FOR_RULES, button_style, + DO_COLOR, + IF_COLOR, + FOR_COLOR, + TRIGGER_COLOR ) +from typing import List from utils.preload import button_texture, button_hovered_texture, trash_bin -from collections import deque, defaultdict -import arcade, arcade.gui, random +from arcade.gui.experimental.scroll_area import UIScrollArea, UIScrollBar +from dataclasses import dataclass, field +import arcade, arcade.gui, pyglet, random IF_KEYS = tuple(IF_RULES.keys()) DO_KEYS = tuple(DO_RULES.keys()) @@ -26,50 +30,20 @@ def generate_rule(rule_type): return random.choice(IF_KEYS) elif rule_type == "do": return random.choice(DO_KEYS) - else: - return random.choice(LOGICAL_OPERATORS) - + +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 cubic_bezier_point(p0, p1, p2, p3, t): - u = 1 - t - x = (u ** 3) * p0[0] + 3 * (u ** 2) * t * p1[0] + 3 * u * (t ** 2) * p2[0] + (t ** 3) * p3[0] - y = (u ** 3) * p0[1] + 3 * (u ** 2) * t * p1[1] + 3 * u * (t ** 2) * p2[1] + (t ** 3) * p3[1] - return x, y - -def cubic_bezier_points(p0, p1, p2, p3, segments=40): - return [cubic_bezier_point(p0, p1, p2, p3, i / segments) for i in range(segments + 1)] - -def connection_between(p0, p3, start_dir_y, end_dir_y): - offset = max(abs(p3[1] - p0[1]) * 0.5, 20) - c1 = (p0[0], p0[1] + start_dir_y * offset) - c2 = (p3[0], p3[1] + end_dir_y * offset) - - return cubic_bezier_points(p0, c1, c2, p3, segments=100) - -def connected_component(edges, start): - graph = defaultdict(set) - for u, v, _, __ in edges: - graph[u].add(v) - graph[v].add(u) - - seen = set([start]) - queue = deque([start]) - connected_edges = [] - - while queue: - node = queue.popleft() - for neighbor in graph[node]: - if neighbor not in seen: - seen.add(neighbor) - queue.append(neighbor) - connected_edges.append((node, neighbor)) - elif (neighbor, node) not in connected_edges and (node, neighbor) not in connected_edges: - connected_edges.append((node, neighbor)) - - return connected_edges - def get_rule_defaults(rule_type): if rule_type == "if": return { @@ -105,251 +79,139 @@ def get_rule_defaults(rule_type): for rule_key, rule_dict in DO_RULES.items() } -def backtrace(node: dict): - dependencies = [] +@dataclass +class Block: + x: float + y: float + label: str + rule_type: str + rule: str + rule_num: int + rule_values: dict[str, int | str] + children: List["Block"] = field(default_factory=list) - for prev_node_list in node.previous.values(): - for prev_node in prev_node_list: - if prev_node.rule_type == "comparison": - comparison_children = backtrace(prev_node) - dependencies.append([ - prev_node.rule_type, - prev_node.rule, - comparison_children, - prev_node.rule_num - ]) - elif prev_node.rule_type == "if": - dependencies.append([prev_node.rule_type, prev_node.rule, prev_node.rule_values, prev_node.rule_num]) - elif prev_node.rule_type == "do": - dependencies.append([prev_node.rule_type, prev_node.rule, prev_node.rule_values, prev_node.rule_num]) +class BlockRenderer: + def __init__(self, blocks: List[Block], indent: int = 10): + 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.refresh() - return dependencies - -class RuleBox(arcade.gui.UIBoxLayout): - def __init__(self, x, y, width, height, rule_num, rule_type, rule): - super().__init__(space_between=5, x=x, y=y, width=width, height=height) - - self.rule = rule - self.rule_num = rule_num - self.rule_type = rule_type - self.initialize_rule() + def refresh(self): + for shapes_list in self.shapes_by_rule_num.values(): + for shape in shapes_list: + shape.delete() - self.previous = {} + for text_list in self.text_by_rule_num.values(): + for text in text_list: + text.delete() - def initialize_rule(self): - if not self.rule_type == "comparison": - self.rule_dict = ( - IF_RULES[self.rule] if self.rule_type == "if" else DO_RULES[self.rule] - ) - self.defaults = get_rule_defaults(self.rule_type) - self.rule_values = {} - self.var_labels = {} - self.var_changers = {} + self.shapes = pyglet.graphics.Batch() + self.shapes_by_rule_num = {} + self.text_objects = [] + self.text_by_rule_num = {} + for b in self.blocks.values(): + self._build_block(b, b.x, b.y) - widget_count = 2 + len(self.rule_dict["user_vars"]) + def _build_block(self, b: Block, x: int, y: int) -> int: + is_wrap = b.rule_type != "do" + h, w = 42, 280 - self.per_widget_height = per_widget_height( - self.height, - widget_count - ) - else: - self.per_widget_height = per_widget_height( - self.height, - 2 - ) + if b.rule_type == "if": + color = IF_COLOR + elif b.rule_type == "trigger": + color = TRIGGER_COLOR + elif b.rule_type == "do": + color = DO_COLOR + elif b.rule_type == "for": + color = FOR_COLOR - self.init_ui() - - def init_ui(self): - if self.rule_type == "do": - self.previous_button, self.drag_button = self.add_extra_buttons(["IF/Comparison", "Drag"]) - elif self.rule_type == "if": - self.drag_button = self.add_extra_buttons("Drag")[0] - elif self.rule_type == "comparison": - self.previous_button_1, self.previous_button_2, self.drag_button = self.add_extra_buttons(["IF 1", "IF 2", "Drag"]) - - dropdown_options = [desc for desc, _ in self.defaults.values()] if not self.rule_type == "comparison" else LOGICAL_OPERATORS - self.desc_label = self.add( - arcade.gui.UIDropdown( - default=self.defaults[self.rule][0] if not self.rule_type == "comparison" else dropdown_options[0], - options=dropdown_options, - font_size=13, - active_style=dropdown_style, - primary_style=dropdown_style, - dropdown_style=dropdown_style, - width=self.width, - height=self.per_widget_height - ) - ) - self.desc_label.on_change = lambda event: self.change_rule_type(event.new_value) + lx, ly = x, y - h - if self.rule_type == "comparison": - self.next_button = self.add_extra_buttons("Do / Comparison")[0] - return + if b.rule_num not in self.shapes_by_rule_num: + self.shapes_by_rule_num[b.rule_num] = [] + if b.rule_num not in self.text_by_rule_num: + self.text_by_rule_num[b.rule_num] = [] + + 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) - for n, variable_type in enumerate(self.rule_dict["user_vars"]): - key = f"{variable_type}_{n}" + ny = ly + if is_wrap: + iy = ny + 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) + + bar_h = ny - 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) + bottom = pyglet.shapes.BorderedRectangle(lx, iy - 8, w, 24, 2, color, arcade.color.BLACK, batch=self.shapes) - defaults = get_rule_defaults(self.rule_type) - default_values = defaults[self.rule][1] - self.rule_values[key] = default_values[VAR_NAMES[n]] - - box = self.add( - arcade.gui.UIBoxLayout( - vertical=False, - width=self.width, - height=self.per_widget_height * 2 - ) - ) - - self.var_labels[key] = box.add( - arcade.gui.UILabel( - f"{VAR_NAMES[n]}: " if not variable_type in ["variable", "size"] else f"{VAR_NAMES[n]}: {self.rule_values[key]}", - font_size=11, - text_color=arcade.color.WHITE, - width=self.width, - height=self.per_widget_height, - ) - ) - - if variable_type in ["variable", "size"]: - slider = box.add( - arcade.gui.UISlider( - value=self.rule_values[key], - min_value=VAR_OPTIONS[variable_type][0], - max_value=VAR_OPTIONS[variable_type][1], - step=1, - style=slider_style, - width=self.width, - height=self.per_widget_height, - ) - ) - slider._render_steps = lambda surface: None - slider.on_change = ( - lambda event, - variable_type=variable_type, - n=n: self.change_var_value(variable_type, n, event.new_value) - ) - - self.var_changers[key] = slider - - else: - dropdown = box.add( - arcade.gui.UIDropdown( - default=self.rule_values[key], - options=VAR_OPTIONS[variable_type], - active_style=dropdown_style, - primary_style=dropdown_style, - dropdown_style=dropdown_style, - width=self.width, - height=self.per_widget_height, - ) - ) - dropdown.on_change = ( - lambda event, - variable_type=variable_type, - n=n: self.change_var_value(variable_type, n, event.new_value) - ) - - self.var_changers[key] = dropdown - - if self.rule_type == "if": - self.next_button = self.add_extra_buttons("Do / Comparison")[0] - - def add_extra_buttons(self, texts: list[str] | str): - if not isinstance(texts, list): - texts = [texts] - box = self + self.shapes_by_rule_num[b.rule_num].extend([bar_filled, line1, bottom]) + + return iy - 24 else: - box = self.add( - arcade.gui.UIBoxLayout( - vertical=False, - width=self.width, - height=self.per_widget_height - ) - ) + for child in b.children: + ny = self._build_block(child, lx, ny) + return ny - return [ - box.add( - arcade.gui.UITextureButton( - text=text, - width=self.width / len(texts), - height=self.per_widget_height, - style=button_style, - texture=button_texture, - texture_hovered=button_hovered_texture, - ) - ) - for text in texts - ] + 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 - def change_var_value(self, variable_type, n, value): - key = f"{variable_type}_{n}" + block = self._find_block(rule_num) - self.rule_values[key] = value + for child in block.children: + self.move_block(x, y, child.rule_num) - values = {} - for i, variable in enumerate(self.rule_dict["user_vars"]): - lookup_key = f"{variable}_{i}" - values[VAR_NAMES[i]] = self.rule_values.get( - lookup_key, VAR_DEFAULT[variable] - ) + def _find_block(self, rule_num): + if rule_num in self.blocks: + return self.blocks[rule_num] + + for block in self.blocks.values(): + found = self._find_block_recursive(block, rule_num) + if found: + return found + return None - description = self.rule_dict["description"].format_map(values) + def _find_block_recursive(self, block, rule_num): + for child in block.children: + if child.rule_num == rule_num: + return child + found = self._find_block_recursive(child, rule_num) + if found: + return found + return None - self.desc_label.text = description - - if variable_type in ["variable", "size"]: - self.var_labels[key].text = f"{VAR_NAMES[n]}: {value}" - - def change_rule_type(self, new_rule_desc): - self.rule = next(key for key, default_list in self.defaults.items() if default_list[0] == new_rule_desc) if self.rule_type != "comparison" else new_rule_desc - self.clear() - self.initialize_rule() - - def __repr__(self): - return f"<{self.rule_type} ({self.rule}, {self.rule_num})>" - -def get_connection_pos(rule_ui: RuleBox, idx): - if rule_ui.rule_type == "comparison": - if idx == 0: - button = rule_ui.previous_button_1 - y = button.top - direction = 1 - elif idx == 1: - button = rule_ui.previous_button_2 - y = button.top - direction = 1 - elif idx == 2: - button = rule_ui.next_button - y = button.bottom - direction = -1 - elif rule_ui.rule_type == "if": - button = rule_ui.next_button - y = button.bottom - direction = -1 - elif rule_ui.rule_type == "do": - button = rule_ui.previous_button - y = button.top - direction = 1 - - return (button.center_x, y), direction + def draw(self): + self.shapes.draw() + for t in self.text_objects: + t.draw() class RuleUI(arcade.gui.UIAnchorLayout): def __init__(self, window: arcade.Window): - super().__init__(size_hint=(0.95, 0.875)) + super().__init__(size_hint=(1, 0.875)) self.window = window self.current_rule_num = 0 self.rule_values = {} - self.dragged_rule_ui: RuleBox | None = None - self.rule_ui: dict[str, RuleBox] = {} - - self.connections = [] - self.to_connect = None - self.to_connect_idx = None - self.allowed_next_connection = [] + self.rulesets: dict[int, Block] = {} + + self.block_renderer = BlockRenderer(self.rulesets) + self.camera = arcade.Camera2D() + + self.dragged_rule_ui: Block | None = None self.rules_label = self.add( arcade.gui.UILabel( @@ -365,134 +227,71 @@ class RuleUI(arcade.gui.UIAnchorLayout): ) ) - self.add_button_box = self.add( - arcade.gui.UIBoxLayout(space_between=10), - anchor_x="center", - anchor_y="bottom", - ) + 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.add_if_rule_button = self.add_button_box.add( - arcade.gui.UIFlatButton( - text="Add IF rule", - width=self.window.width * 0.225, - height=self.window.height / 25, - style=dropdown_style, - ) - ) - self.add_if_rule_button.on_click = lambda event: self.add_rule("if") + self.scroll_area = UIScrollArea(size_hint=(0.95, 1)) # center on screen + self.scroll_area.scroll_speed = -50 + self.create_sidebar.add(self.scroll_area) - self.add_do_rule_button = self.add_button_box.add( - arcade.gui.UIFlatButton( - text="Add DO rule", - width=self.window.width * 0.225, - height=self.window.height / 25, - style=dropdown_style, - ) - ) - self.add_do_rule_button.on_click = lambda event: self.add_rule("do") + self.scrollbar = UIScrollBar(self.scroll_area) + self.scrollbar.size_hint = (0.075, 1) + self.create_sidebar.add(self.scrollbar) - self.add_comparison_button = self.add_button_box.add( - arcade.gui.UIFlatButton( - text="Add comparison", - width=self.window.width * 0.225, - height=self.window.height / 25, - style=dropdown_style, - ) - ) - self.add_comparison_button.on_click = lambda event: self.add_rule("comparison") + self.create_box = self.scroll_area.add(arcade.gui.UIBoxLayout(space_between=10)) - self.rule_space = self.add(arcade.gui.UIWidget(size_hint=(1, 1))) + 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.create_connected_ruleset([ - ("if", "x_position_compare"), - ("if", "y_position_compare"), - ("comparison", "and"), - ("comparison", "and"), - ("do", "move_x")], - [ - [0, 2, 0, 0], # x_position_compare 0 -> first comparison 0 - [1, 2, 0, 1], # y_position_compare 0 -> first comparison 1 - [2, 3, 2, 0], # first comparison 2 (output) -> second comparison 0 - [1, 3, 0, 1], # y_position_compare 0 -> -> second comparison 1 - [3, 4, 2, 0] # second comparison 2 (output) -> do - ] - ) + 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 set_previous(self, rule_ui_a, rule_ui_b, to_connect_idx, idx): - if rule_ui_a.rule_type == "if" and to_connect_idx == 0: - if idx not in rule_ui_b.previous: - rule_ui_b.previous[idx] = [] - rule_ui_b.previous[idx].append(rule_ui_a) - elif rule_ui_a.rule_type == "comparison": - if to_connect_idx == 2: - if idx not in rule_ui_b.previous: - rule_ui_b.previous[idx] = [] - rule_ui_b.previous[idx].append(rule_ui_a) - else: - if to_connect_idx not in rule_ui_a.previous: - rule_ui_a.previous[to_connect_idx] = [] - rule_ui_a.previous[to_connect_idx].append(rule_ui_b) - elif rule_ui_a.rule_type == "do" and idx == 0: - if idx not in rule_ui_a.previous: - rule_ui_a.previous[0] = [] - rule_ui_a.previous[0].append(rule_ui_b) - - def connection(self, rule_ui, allowed_next_connection, idx): - if self.to_connect is not None: - old_rule_ui = self.rule_ui[self.to_connect] - old_rule_type = old_rule_ui.rule_type - if ( - rule_ui.rule_type not in self.allowed_next_connection or - old_rule_type not in allowed_next_connection or - (old_rule_type == "if" and rule_ui.rule_type == "if") or - (old_rule_type == "do" and rule_ui.rule_type in ["do", "comparison"]) or - rule_ui.rule_num == self.to_connect - ): - - return - - self.connections.append([self.to_connect, rule_ui.rule_num, self.to_connect_idx, idx]) - - self.set_previous(old_rule_ui, rule_ui, self.to_connect_idx, idx) - - self.allowed_next_connection = None - self.to_connect = None - self.to_connect_idx = None - else: - self.allowed_next_connection = allowed_next_connection - self.to_connect = rule_ui.rule_num - self.to_connect_idx = idx - - def drag(self, rule_ui): - if self.dragged_rule_ui: - if self.dragged_rule_ui.rect.intersection(self.trash_sprite.rect): - self.rule_ui.pop(self.dragged_rule_ui.rule_num) - - if self.dragged_rule_ui.rule_num == self.to_connect: - self.to_connect = None - - for connection in self.connections: - if self.dragged_rule_ui.rule_num in connection[:2]: - self.connections.remove(connection) - - self.rule_space.remove(self.dragged_rule_ui) - del self.dragged_rule_ui - - self.dragged_rule_ui = None - - self.trigger_full_render() - else: - self.dragged_rule_ui = rule_ui - - def get_rulesets(self): # return actions that can happen and the dependencies needed - do_blocks = [rule_ui for rule_ui in self.rule_ui.values() if rule_ui.rule_type == "do"] - - return [backtrace(do_block) for do_block in do_blocks] + def get_rulesets(self): + # TODO: remove this + return [], [] def generate_pos(self): return random.randint( @@ -500,78 +299,96 @@ class RuleUI(arcade.gui.UIAnchorLayout): ), random.randint(self.window.height * 0.1, int(self.window.height * 0.7)) def add_rule(self, rule_type, force=None): - rule_box = RuleBox( + rule = force or generate_rule(rule_type) + rule_box = Block( *self.generate_pos(), - self.window.width * 0.15, - self.window.height * 0.15, - self.current_rule_num, + get_rule_description(rule_type, rule), rule_type, - force or generate_rule(rule_type), + rule, + self.current_rule_num, + {}, + [] ) - if rule_type == "if": - rule_box.next_button.on_click = lambda event, rule_box=rule_box: self.connection(rule_box, ["do", "comparison"], 0) - elif rule_type == "comparison": - rule_box.previous_button_1.on_click = lambda event, rule_box=rule_box: self.connection(rule_box, ["if", "comparison"], 0) - rule_box.previous_button_2.on_click = lambda event, rule_box=rule_box: self.connection(rule_box, ["if", "comparison"], 1) - rule_box.next_button.on_click = lambda event, rule_box=rule_box: self.connection(rule_box, ["do", "comparison"], 2) - elif rule_type == "do": - rule_box.previous_button.on_click = lambda event, rule_box=rule_box: self.connection(rule_box, ["if", "comparison"], 0) - - rule_box.drag_button.on_click = lambda event, rule_box=rule_box: self.drag(rule_box) - - self.rule_space.add(rule_box) - self.rule_ui[self.current_rule_num] = rule_box - self.rule_ui[self.current_rule_num].fit_content() + self.rulesets[self.current_rule_num] = rule_box self.current_rule_num += 1 + self.block_renderer.refresh() return rule_box - def create_connected_ruleset(self, rules, connections): - for rule_type, rule in rules: - self.add_rule(rule_type, rule) - - for connection in connections: - self.set_previous(self.rule_ui[connection[0]], self.rule_ui[connection[1]], connection[2], connection[3]) - - self.connections.extend(connections) - def draw(self): - self.bezier_points = [] - - for conn in self.connections: - start_id, end_id, start_conn_idx, end_conn_idx = conn - start_rule_ui = self.rule_ui[start_id] - end_rule_ui = self.rule_ui[end_id] - - start_pos, start_dir_y = get_connection_pos(start_rule_ui, start_conn_idx) - end_pos, end_dir_y = get_connection_pos(end_rule_ui, end_conn_idx) - - points = connection_between(start_pos, end_pos, start_dir_y, end_dir_y) - self.bezier_points.append(points) - - arcade.draw_line_strip(points, arcade.color.WHITE, 6) - - if self.to_connect is not None: - mouse_x, mouse_y = self.window.mouse.data.get("x", 0), self.window.mouse.data.get("y", 0) - start_pos, start_dir = get_connection_pos(self.rule_ui[self.to_connect], self.to_connect_idx) - end_pos, end_dir = (mouse_x, mouse_y), 1 - points = connection_between(start_pos, end_pos, start_dir, end_dir) - arcade.draw_line_strip(points, arcade.color.WHITE, 6) + self.block_renderer.draw() + def draw_unproject(self): self.trash_spritelist.draw() + def drag_n_drop_check(self, blocks): + for block in blocks: + if block == self.dragged_rule_ui: + 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)): + block.children.append(self.dragged_rule_ui) + del self.rulesets[self.dragged_rule_ui.rule_num] + self.block_renderer.refresh() + break + else: + self.drag_n_drop_check(block.children) + + def remove_from_parent(self, block_to_remove, parents): + for parent in parents: + if block_to_remove in parent.children: + parent.children.remove(block_to_remove) + return True + if self.remove_from_parent(block_to_remove, parent.children): + return True + return False + + def press_check(self, event, blocks): + for block in blocks: + if block == self.dragged_rule_ui: + continue + + projected_vec = self.camera.unproject((event.x, event.y)) + if arcade.LBWH(block.x, block.y - 44, 280, 44).point_in_rect((projected_vec.x, projected_vec.y)): + if block not in list(self.rulesets.values()): # its children + self.remove_from_parent(block, list(self.rulesets.values())) + self.block_renderer.refresh() + self.dragged_rule_ui = block + break + else: + self.press_check(event, block.children) + def on_event(self, event): super().on_event(event) - if isinstance(event, arcade.gui.UIMouseMovementEvent): - if self.dragged_rule_ui is not None: - self.dragged_rule_ui.center_x += event.dx - self.dragged_rule_ui.center_y += event.dy + if isinstance(event, arcade.gui.UIMouseDragEvent): + if event.buttons == arcade.MOUSE_BUTTON_LEFT: + if self.dragged_rule_ui is not None: + self.dragged_rule_ui.x += event.dx + self.dragged_rule_ui.y += event.dy + self.block_renderer.move_block(event.dx, event.dy, self.dragged_rule_ui.rule_num) + + elif isinstance(event, arcade.gui.UIMousePressEvent): + self.press_check(event, list(self.rulesets.values())) + + elif isinstance(event, arcade.gui.UIMouseReleaseEvent): + if self.dragged_rule_ui: + block_vec = self.camera.unproject((self.dragged_rule_ui.x, self.dragged_rule_ui.y)) + if self.trash_sprite.rect.intersection(arcade.LBWH(block_vec.x, block_vec.y, 280, 44)) and not self.trash_sprite._current_keyframe_index == self.trash_sprite.animation.num_frames - 1: + del self.rulesets[self.dragged_rule_ui.rule_num] + self.dragged_rule_ui = None + self.block_renderer.refresh() + return + + self.drag_n_drop_check(list(self.rulesets.values())) + + self.dragged_rule_ui = None def on_update(self, dt): - if self.dragged_rule_ui and self.trash_sprite.rect.intersection(self.dragged_rule_ui.rect): - if not self.trash_sprite._current_keyframe_index == self.trash_sprite.animation.num_frames - 1: + if self.dragged_rule_ui: + block_vec = self.camera.unproject((self.dragged_rule_ui.x, self.dragged_rule_ui.y)) + if self.trash_sprite.rect.intersection(arcade.LBWH(block_vec.x, block_vec.y, 280, 44)) and not self.trash_sprite._current_keyframe_index == self.trash_sprite.animation.num_frames - 1: self.trash_sprite.update_animation() else: self.trash_sprite.time = 0 diff --git a/utils/constants.py b/utils/constants.py index 100e297..7f4299a 100644 --- a/utils/constants.py +++ b/utils/constants.py @@ -3,12 +3,16 @@ from arcade.types import Color from arcade.gui.widgets.buttons import UITextureButtonStyle, UIFlatButtonStyle from arcade.gui.widgets.slider import UISliderStyle -LOGICAL_OPERATORS = ["and", "or"] SHAPES = ["rectangle", "circle", "triangle"] VAR_NAMES = ["a", "b", "c", "d", "e", "f", "g"] ALLOWED_INPUT = ["a", "b", "c", "d", "e", "q", "w", "s", "t"] +TRIGGER_COLOR = (255, 204, 102) +DO_COLOR = (102, 178, 255) +IF_COLOR = (144, 238, 144) +FOR_COLOR = (255, 182, 193) + COLORS = [ "BLACK", "WHITE", "GRAY", "DARK_GRAY", "CYAN", "AMBER", "AQUA", "GREEN", "LIGHT_GREEN", @@ -45,38 +49,43 @@ VAR_OPTIONS = { "comparison": COMPARISONS } -IF_RULES = { +TRIGGER_RULES = { + "every_update": { + "key": "every_update", + "description": "Every Update", + }, + "start": { + "key": "start", + "description": "On Game Start", + }, + "on_input": { + "key": "on_input", + "description": "IF {a} key is pressed", + }, "x_position_compare": { "key": "x_position_compare", "description": "IF X for {a} shape is {b} {c}", - "trigger": "every_update", "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}", - "trigger": "every_update", "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}", - "trigger": "every_update", "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]}") }, - "spawns": { "key": "spawns", "description": "IF {a} shape spawns", - "trigger": "spawn", "user_vars": ["shape_type"], "vars": ["shape_type", "event_shape_type"], "func": lambda *v: v[0] == v[1] @@ -84,7 +93,6 @@ IF_RULES = { "destroyed": { "key": "destroyed", "description": "IF {a} shape is destroyed", - "trigger": "destroyed", "user_vars": ["shape_type"], "vars": ["shape_type", "event_shape_type"], "func": lambda *v: v[0] == v[1] @@ -92,7 +100,6 @@ IF_RULES = { "x_velocity_changes": { "key": "x_velocity_changes", "description": "IF {a} shape X velocity changes", - "trigger": "x_velocity_change", "user_vars": ["shape_type"], "vars": ["shape_type", "event_shape_type"], "func": lambda *v: v[0] == v[1] @@ -100,7 +107,6 @@ IF_RULES = { "y_velocity_changes": { "key": "y_velocity_changes", "description": "IF {a} shape Y velocity changes", - "trigger": "y_velocity_change", "user_vars": ["shape_type"], "vars": ["shape_type", "event_shape_type"], "func": lambda *v: v[0] == v[1] @@ -108,7 +114,6 @@ IF_RULES = { "color_changes": { "key": "color_changes", "description": "IF {a} shape color changes", - "trigger": "color_change", "user_vars": ["shape_type"], "vars": ["shape_type", "event_shape_type"], "func": lambda *v: v[0] == v[1] @@ -116,7 +121,6 @@ IF_RULES = { "size_changes": { "key": "size_changes", "description": "IF {a} shape size changes", - "trigger": "size_change", "user_vars": ["shape_type"], "vars": ["shape_type", "event_shape_type"], "func": lambda *v: v[0] == v[1] @@ -124,7 +128,6 @@ IF_RULES = { "morphs": { "key": "morphs", "description": "IF {a} shape morphs into {b}", - "trigger": "morph", "user_vars": ["shape_type", "target_type"], "vars": ["shape_type", "target_type", "event_a_type", "event_b_type"], "func": lambda *v: (v[0] == v[2]) and (v[3] == v[1]) @@ -132,61 +135,64 @@ IF_RULES = { "collides": { "key": "collides", "description": "IF {a} shape collides with {b}", - "trigger": "collision", "user_vars": ["shape_type", "target_type"], "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", - "trigger": "on_left_click", - "user_vars": [], - "vars": [], - "func": lambda *v: True - }, - "on_right_click": { - "key": "on_right_click", - "description": "IF you right click", - "trigger": "on_right_click", - "user_vars": [], - "vars": [], - "func": lambda *v: True - }, - "on_mouse_move": { - "key": "on_mouse_move", - "description": "IF mouse moves", - "trigger": "on_mouse_move", - "user_vars": [], - "vars": [], - "func": lambda *v: True - }, - "on_input": { - "key": "on_input", - "description": "IF {a} key is pressed", - "trigger": "on_input", - "user_vars": ["key_input"], - "vars": ["key_input", "event_key"], - "func": lambda *v: v[0] == v[1] - }, - "game_launch": { - "key": "game_launch", - "description": "IF game launches", - "trigger": "game_launch", - "user_vars": [], - "vars": [], - "func": lambda *v: True - }, - "every_update": { - "key": "every_update", - "description": "Every update", - "trigger": "every_update", - "user_vars": [], - "vars": [], - "func": lambda *v: True +} + +FOR_RULES = { + "every_shape": { + "key": "every_shape", + "description": "For every shape", } } +IF_RULES = { + "x_position_compare": { + "key": "x_position_compare", + "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]}") + }, + "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]}") + }, + "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]}") + }, + "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]}") + }, + "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]}") + }, + "color_is": { + "key": "color_is", + "description": "IF color is {a}", + "user_vars": ["color"], + "vars": ["color", "shape_color"], + "func": lambda *v: v[0] == v[1] + }, +} + NON_COMPATIBLE_WHEN = [ ("spawns", "destroyed"), ("spawns", "morphs"),