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.

This commit is contained in:
csd4ni3l
2025-12-06 23:35:40 +01:00
parent 15bb259a4f
commit f51260d94f
3 changed files with 370 additions and 513 deletions

View File

@@ -35,7 +35,7 @@ class Game(arcade.gui.UIView):
self.y_gravity = self.settings.get("default_y_gravity", 5) self.y_gravity = self.settings.get("default_y_gravity", 5)
self.triggered_events = [] 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)) 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", {}]) self.triggered_events.append(["game_launch", {}])
def get_rule_values(self, rule_num, rule_dict, rule_values, event_args): def get_rule_values(self, 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"])] 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"]] 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): def check_rule(self, rule_dict, rule_values, event_args):
return rule_dict["func"](*self.get_rule_values(rule_num, 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): def get_action_function(self, action_dict):
ACTION_FUNCTION_DICT = { ACTION_FUNCTION_DICT = {
@@ -207,8 +207,8 @@ class Game(arcade.gui.UIView):
return ACTION_FUNCTION_DICT[action_dict["type"]][action_dict["name"]] return ACTION_FUNCTION_DICT[action_dict["type"]][action_dict["name"]]
def run_do_rule(self, 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_num, 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): def on_update(self, delta_time):
if self.mode == "import" and self.file_manager.submitted_content: 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: while len(self.triggered_events) > 0:
trigger, trigger_args = self.triggered_events.pop(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 in self.shapes:
for shape_b 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", {}]) 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): def disable_previous(self):
if self.mode in ["import", "export"]: if self.mode in ["import", "export"]:
self.anchor.remove(self.file_manager) self.anchor.remove(self.file_manager)
@@ -320,7 +351,7 @@ class Game(arcade.gui.UIView):
def simulation(self): def simulation(self):
self.disable_previous() self.disable_previous()
self.rulesets = self.rules_box.get_rulesets() self.rulesets, self.if_rules = self.rules_box.get_rulesets()
self.mode = "simulation" self.mode = "simulation"
def main_exit(self): def main_exit(self):
@@ -333,6 +364,9 @@ class Game(arcade.gui.UIView):
if self.mode == "simulation": if self.mode == "simulation":
self.shape_batch.draw() self.shape_batch.draw()
elif self.mode == "rules": 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() self.ui.draw()

View File

@@ -1,19 +1,23 @@
from utils.constants import ( from utils.constants import (
DO_RULES, DO_RULES,
IF_RULES, IF_RULES,
LOGICAL_OPERATORS,
NON_COMPATIBLE_WHEN, NON_COMPATIBLE_WHEN,
NON_COMPATIBLE_DO_WHEN, NON_COMPATIBLE_DO_WHEN,
VAR_NAMES, VAR_NAMES,
VAR_DEFAULT, VAR_DEFAULT,
VAR_OPTIONS, TRIGGER_RULES,
dropdown_style, FOR_RULES,
slider_style,
button_style, 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 utils.preload import button_texture, button_hovered_texture, trash_bin
from collections import deque, defaultdict from arcade.gui.experimental.scroll_area import UIScrollArea, UIScrollBar
import arcade, arcade.gui, random from dataclasses import dataclass, field
import arcade, arcade.gui, pyglet, random
IF_KEYS = tuple(IF_RULES.keys()) IF_KEYS = tuple(IF_RULES.keys())
DO_KEYS = tuple(DO_RULES.keys()) DO_KEYS = tuple(DO_RULES.keys())
@@ -26,50 +30,20 @@ def generate_rule(rule_type):
return random.choice(IF_KEYS) return random.choice(IF_KEYS)
elif rule_type == "do": elif rule_type == "do":
return random.choice(DO_KEYS) 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): def per_widget_height(height, widget_count):
return 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): def get_rule_defaults(rule_type):
if rule_type == "if": if rule_type == "if":
return { return {
@@ -105,251 +79,139 @@ def get_rule_defaults(rule_type):
for rule_key, rule_dict in DO_RULES.items() for rule_key, rule_dict in DO_RULES.items()
} }
def backtrace(node: dict): @dataclass
dependencies = [] 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(): class BlockRenderer:
for prev_node in prev_node_list: def __init__(self, blocks: List[Block], indent: int = 10):
if prev_node.rule_type == "comparison": self.blocks = blocks
comparison_children = backtrace(prev_node) self.indent = indent
dependencies.append([ self.shapes = pyglet.graphics.Batch()
prev_node.rule_type, self.shapes_by_rule_num = {}
prev_node.rule, self.text_objects = []
comparison_children, self.text_by_rule_num = {}
prev_node.rule_num self.refresh()
])
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])
return dependencies def refresh(self):
for shapes_list in self.shapes_by_rule_num.values():
class RuleBox(arcade.gui.UIBoxLayout): for shape in shapes_list:
def __init__(self, x, y, width, height, rule_num, rule_type, rule): shape.delete()
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()
self.previous = {} for text_list in self.text_by_rule_num.values():
for text in text_list:
text.delete()
def initialize_rule(self): self.shapes = pyglet.graphics.Batch()
if not self.rule_type == "comparison": self.shapes_by_rule_num = {}
self.rule_dict = ( self.text_objects = []
IF_RULES[self.rule] if self.rule_type == "if" else DO_RULES[self.rule] self.text_by_rule_num = {}
) for b in self.blocks.values():
self.defaults = get_rule_defaults(self.rule_type) self._build_block(b, b.x, b.y)
self.rule_values = {}
self.var_labels = {}
self.var_changers = {}
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( if b.rule_type == "if":
self.height, color = IF_COLOR
widget_count elif b.rule_type == "trigger":
) color = TRIGGER_COLOR
else: elif b.rule_type == "do":
self.per_widget_height = per_widget_height( color = DO_COLOR
self.height, elif b.rule_type == "for":
2 color = FOR_COLOR
)
self.init_ui() lx, ly = x, y - h
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)
if self.rule_type == "comparison": if b.rule_num not in self.shapes_by_rule_num:
self.next_button = self.add_extra_buttons("Do / Comparison")[0] self.shapes_by_rule_num[b.rule_num] = []
return 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"]): ny = ly
key = f"{variable_type}_{n}" 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) self.shapes_by_rule_num[b.rule_num].extend([bar_filled, line1, bottom])
default_values = defaults[self.rule][1]
self.rule_values[key] = default_values[VAR_NAMES[n]] return iy - 24
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
else: else:
box = self.add( for child in b.children:
arcade.gui.UIBoxLayout( ny = self._build_block(child, lx, ny)
vertical=False, return ny
width=self.width,
height=self.per_widget_height
)
)
return [ def move_block(self, x, y, rule_num):
box.add( for element in self.shapes_by_rule_num[rule_num] + self.text_by_rule_num[rule_num]:
arcade.gui.UITextureButton( element.x += x
text=text, element.y += y
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 change_var_value(self, variable_type, n, value): block = self._find_block(rule_num)
key = f"{variable_type}_{n}"
self.rule_values[key] = value for child in block.children:
self.move_block(x, y, child.rule_num)
values = {} def _find_block(self, rule_num):
for i, variable in enumerate(self.rule_dict["user_vars"]): if rule_num in self.blocks:
lookup_key = f"{variable}_{i}" return self.blocks[rule_num]
values[VAR_NAMES[i]] = self.rule_values.get(
lookup_key, VAR_DEFAULT[variable] 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 def draw(self):
self.shapes.draw()
if variable_type in ["variable", "size"]: for t in self.text_objects:
self.var_labels[key].text = f"{VAR_NAMES[n]}: {value}" t.draw()
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
class RuleUI(arcade.gui.UIAnchorLayout): class RuleUI(arcade.gui.UIAnchorLayout):
def __init__(self, window: arcade.Window): 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.window = window
self.current_rule_num = 0 self.current_rule_num = 0
self.rule_values = {} self.rule_values = {}
self.dragged_rule_ui: RuleBox | None = None self.rulesets: dict[int, Block] = {}
self.rule_ui: dict[str, RuleBox] = {}
self.block_renderer = BlockRenderer(self.rulesets)
self.connections = [] self.camera = arcade.Camera2D()
self.to_connect = None
self.to_connect_idx = None self.dragged_rule_ui: Block | None = None
self.allowed_next_connection = []
self.rules_label = self.add( self.rules_label = self.add(
arcade.gui.UILabel( arcade.gui.UILabel(
@@ -365,134 +227,71 @@ class RuleUI(arcade.gui.UIAnchorLayout):
) )
) )
self.add_button_box = self.add( self.create_sidebar = self.add(arcade.gui.UIBoxLayout(size_hint=(0.15, 1), vertical=False, space_between=5), anchor_x="left", anchor_y="bottom")
arcade.gui.UIBoxLayout(space_between=10),
anchor_x="center",
anchor_y="bottom",
)
self.add_if_rule_button = self.add_button_box.add( self.scroll_area = UIScrollArea(size_hint=(0.95, 1)) # center on screen
arcade.gui.UIFlatButton( self.scroll_area.scroll_speed = -50
text="Add IF rule", self.create_sidebar.add(self.scroll_area)
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.add_do_rule_button = self.add_button_box.add( self.scrollbar = UIScrollBar(self.scroll_area)
arcade.gui.UIFlatButton( self.scrollbar.size_hint = (0.075, 1)
text="Add DO rule", self.create_sidebar.add(self.scrollbar)
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.add_comparison_button = self.add_button_box.add( self.create_box = self.scroll_area.add(arcade.gui.UIBoxLayout(space_between=10))
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.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([ self.create_box.add(arcade.gui.UISpace(height=self.window.height / 100))
("if", "x_position_compare"), self.create_box.add(arcade.gui.UILabel(text="IF Rules", font_size=18))
("if", "y_position_compare"), self.create_box.add(arcade.gui.UISpace(height=self.window.height / 200))
("comparison", "and"), for if_rule, if_rule_data in IF_RULES.items():
("comparison", "and"), create_button = self.create_box.add(arcade.gui.UITextureButton(text=if_rule_data["description"].format_map({
("do", "move_x")], "a": "a",
[ "b": "b",
[0, 2, 0, 0], # x_position_compare 0 -> first comparison 0 "c": "c"
[1, 2, 0, 1], # y_position_compare 0 -> first comparison 1 }), 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))
[2, 3, 2, 0], # first comparison 2 (output) -> second comparison 0 create_button.on_click = lambda event, if_rule=if_rule: self.add_rule("if", if_rule)
[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="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_spritelist = arcade.SpriteList()
self.trash_sprite = trash_bin 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_sprite.position = (self.window.width * 0.9, self.window.height * 0.2)
self.trash_spritelist.append(self.trash_sprite) self.trash_spritelist.append(self.trash_sprite)
def set_previous(self, rule_ui_a, rule_ui_b, to_connect_idx, idx): def get_rulesets(self):
if rule_ui_a.rule_type == "if" and to_connect_idx == 0: # TODO: remove this
if idx not in rule_ui_b.previous: return [], []
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 generate_pos(self): def generate_pos(self):
return random.randint( 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)) ), random.randint(self.window.height * 0.1, int(self.window.height * 0.7))
def add_rule(self, rule_type, force=None): 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.generate_pos(),
self.window.width * 0.15, get_rule_description(rule_type, rule),
self.window.height * 0.15,
self.current_rule_num,
rule_type, 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.current_rule_num += 1
self.block_renderer.refresh()
return rule_box 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): def draw(self):
self.bezier_points = [] self.block_renderer.draw()
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)
def draw_unproject(self):
self.trash_spritelist.draw() 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): def on_event(self, event):
super().on_event(event) super().on_event(event)
if isinstance(event, arcade.gui.UIMouseMovementEvent): if isinstance(event, arcade.gui.UIMouseDragEvent):
if self.dragged_rule_ui is not None: if event.buttons == arcade.MOUSE_BUTTON_LEFT:
self.dragged_rule_ui.center_x += event.dx if self.dragged_rule_ui is not None:
self.dragged_rule_ui.center_y += event.dy 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): def on_update(self, dt):
if self.dragged_rule_ui and self.trash_sprite.rect.intersection(self.dragged_rule_ui.rect): if self.dragged_rule_ui:
if not self.trash_sprite._current_keyframe_index == self.trash_sprite.animation.num_frames - 1: 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() self.trash_sprite.update_animation()
else: else:
self.trash_sprite.time = 0 self.trash_sprite.time = 0

View File

@@ -3,12 +3,16 @@ from arcade.types import Color
from arcade.gui.widgets.buttons import UITextureButtonStyle, UIFlatButtonStyle from arcade.gui.widgets.buttons import UITextureButtonStyle, UIFlatButtonStyle
from arcade.gui.widgets.slider import UISliderStyle from arcade.gui.widgets.slider import UISliderStyle
LOGICAL_OPERATORS = ["and", "or"]
SHAPES = ["rectangle", "circle", "triangle"] SHAPES = ["rectangle", "circle", "triangle"]
VAR_NAMES = ["a", "b", "c", "d", "e", "f", "g"] VAR_NAMES = ["a", "b", "c", "d", "e", "f", "g"]
ALLOWED_INPUT = ["a", "b", "c", "d", "e", "q", "w", "s", "t"] 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 = [ COLORS = [
"BLACK", "WHITE", "GRAY", "DARK_GRAY", "CYAN", "BLACK", "WHITE", "GRAY", "DARK_GRAY", "CYAN",
"AMBER", "AQUA", "GREEN", "LIGHT_GREEN", "AMBER", "AQUA", "GREEN", "LIGHT_GREEN",
@@ -45,38 +49,43 @@ VAR_OPTIONS = {
"comparison": COMPARISONS "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": { "x_position_compare": {
"key": "x_position_compare", "key": "x_position_compare",
"description": "IF X for {a} shape is {b} {c}", "description": "IF X for {a} shape is {b} {c}",
"trigger": "every_update",
"user_vars": ["shape_type", "comparison", "variable"], "user_vars": ["shape_type", "comparison", "variable"],
"vars": ["shape_type", "comparison", "variable", "event_shape_type", "shape_x"], "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]}") "func": lambda *v: (v[0] == v[3]) and eval(f"{v[4]} {v[1]} {v[2]}")
}, },
"y_position_compare": { "y_position_compare": {
"key": "y_position_compare", "key": "y_position_compare",
"description": "IF Y for {a} shape is {b} {c}", "description": "IF Y for {a} shape is {b} {c}",
"trigger": "every_update",
"user_vars": ["shape_type", "comparison", "variable"], "user_vars": ["shape_type", "comparison", "variable"],
"vars": ["shape_type", "comparison", "variable", "event_shape_type", "shape_y"], "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]}") "func": lambda *v: (v[0] == v[3]) and eval(f"{v[4]} {v[1]} {v[2]}")
}, },
"size_compare": { "size_compare": {
"key": "size_compare", "key": "size_compare",
"description": "IF {a} shape size is {b} {c}", "description": "IF {a} shape size is {b} {c}",
"trigger": "every_update",
"user_vars": ["shape_type", "comparison", "variable"], "user_vars": ["shape_type", "comparison", "variable"],
"vars": ["shape_type", "comparison", "variable", "event_shape_type", "shape_size"], "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[3]) and eval(f"{v[4]} {v[1]} {v[2]}")
}, },
"spawns": { "spawns": {
"key": "spawns", "key": "spawns",
"description": "IF {a} shape spawns", "description": "IF {a} shape spawns",
"trigger": "spawn",
"user_vars": ["shape_type"], "user_vars": ["shape_type"],
"vars": ["shape_type", "event_shape_type"], "vars": ["shape_type", "event_shape_type"],
"func": lambda *v: v[0] == v[1] "func": lambda *v: v[0] == v[1]
@@ -84,7 +93,6 @@ IF_RULES = {
"destroyed": { "destroyed": {
"key": "destroyed", "key": "destroyed",
"description": "IF {a} shape is destroyed", "description": "IF {a} shape is destroyed",
"trigger": "destroyed",
"user_vars": ["shape_type"], "user_vars": ["shape_type"],
"vars": ["shape_type", "event_shape_type"], "vars": ["shape_type", "event_shape_type"],
"func": lambda *v: v[0] == v[1] "func": lambda *v: v[0] == v[1]
@@ -92,7 +100,6 @@ IF_RULES = {
"x_velocity_changes": { "x_velocity_changes": {
"key": "x_velocity_changes", "key": "x_velocity_changes",
"description": "IF {a} shape X velocity changes", "description": "IF {a} shape X velocity changes",
"trigger": "x_velocity_change",
"user_vars": ["shape_type"], "user_vars": ["shape_type"],
"vars": ["shape_type", "event_shape_type"], "vars": ["shape_type", "event_shape_type"],
"func": lambda *v: v[0] == v[1] "func": lambda *v: v[0] == v[1]
@@ -100,7 +107,6 @@ IF_RULES = {
"y_velocity_changes": { "y_velocity_changes": {
"key": "y_velocity_changes", "key": "y_velocity_changes",
"description": "IF {a} shape Y velocity changes", "description": "IF {a} shape Y velocity changes",
"trigger": "y_velocity_change",
"user_vars": ["shape_type"], "user_vars": ["shape_type"],
"vars": ["shape_type", "event_shape_type"], "vars": ["shape_type", "event_shape_type"],
"func": lambda *v: v[0] == v[1] "func": lambda *v: v[0] == v[1]
@@ -108,7 +114,6 @@ IF_RULES = {
"color_changes": { "color_changes": {
"key": "color_changes", "key": "color_changes",
"description": "IF {a} shape color changes", "description": "IF {a} shape color changes",
"trigger": "color_change",
"user_vars": ["shape_type"], "user_vars": ["shape_type"],
"vars": ["shape_type", "event_shape_type"], "vars": ["shape_type", "event_shape_type"],
"func": lambda *v: v[0] == v[1] "func": lambda *v: v[0] == v[1]
@@ -116,7 +121,6 @@ IF_RULES = {
"size_changes": { "size_changes": {
"key": "size_changes", "key": "size_changes",
"description": "IF {a} shape size changes", "description": "IF {a} shape size changes",
"trigger": "size_change",
"user_vars": ["shape_type"], "user_vars": ["shape_type"],
"vars": ["shape_type", "event_shape_type"], "vars": ["shape_type", "event_shape_type"],
"func": lambda *v: v[0] == v[1] "func": lambda *v: v[0] == v[1]
@@ -124,7 +128,6 @@ IF_RULES = {
"morphs": { "morphs": {
"key": "morphs", "key": "morphs",
"description": "IF {a} shape morphs into {b}", "description": "IF {a} shape morphs into {b}",
"trigger": "morph",
"user_vars": ["shape_type", "target_type"], "user_vars": ["shape_type", "target_type"],
"vars": ["shape_type", "target_type", "event_a_type", "event_b_type"], "vars": ["shape_type", "target_type", "event_a_type", "event_b_type"],
"func": lambda *v: (v[0] == v[2]) and (v[3] == v[1]) "func": lambda *v: (v[0] == v[2]) and (v[3] == v[1])
@@ -132,61 +135,64 @@ IF_RULES = {
"collides": { "collides": {
"key": "collides", "key": "collides",
"description": "IF {a} shape collides with {b}", "description": "IF {a} shape collides with {b}",
"trigger": "collision",
"user_vars": ["shape_type", "target_type"], "user_vars": ["shape_type", "target_type"],
"vars": ["shape_type", "target_type", "event_a_type", "event_b_type"], "vars": ["shape_type", "target_type", "event_a_type", "event_b_type"],
"func": lambda *v: (v[0] == v[2]) and (v[3] == v[1]) "func": lambda *v: (v[0] == v[2]) and (v[3] == v[1])
}, },
"on_left_click": { }
"key": "on_left_click",
"description": "IF you left click", FOR_RULES = {
"trigger": "on_left_click", "every_shape": {
"user_vars": [], "key": "every_shape",
"vars": [], "description": "For every shape",
"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
} }
} }
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 = [ NON_COMPATIBLE_WHEN = [
("spawns", "destroyed"), ("spawns", "destroyed"),
("spawns", "morphs"), ("spawns", "morphs"),