diff --git a/CREDITS b/CREDITS index 5c54168..76ce592 100644 --- a/CREDITS +++ b/CREDITS @@ -1,3 +1,6 @@ +Trash Can icon by Icons8 +https://icons8.com/icon/rdRR1tq1xIo1/trash-can + The Roboto Black font used in this project is licensed under the Open Font License. Read assets/fonts/OFL.txt for more information. Thanks to OpenGameArt and pixelsphere.org / The Cynic Project for the music! (https://opengameart.org/content/crystal-cave-mysterious-ambience-seamless-loop) diff --git a/assets/graphics/trash_bin.gif b/assets/graphics/trash_bin.gif new file mode 100644 index 0000000..4ee84b5 Binary files /dev/null and b/assets/graphics/trash_bin.gif differ diff --git a/game/play.py b/game/play.py index 50e01b1..412c640 100644 --- a/game/play.py +++ b/game/play.py @@ -1,7 +1,7 @@ import arcade, arcade.gui, pyglet, random, json from utils.preload import SPRITE_TEXTURES, button_texture, button_hovered_texture -from utils.constants import button_style, dropdown_style, DO_RULES, IF_RULES, SHAPES, ALLOWED_INPUT, menu_background_color +from utils.constants import button_style, DO_RULES, IF_RULES, SHAPES, ALLOWED_INPUT from game.rules import RuleUI from game.sprites import BaseShape, Rectangle, Circle, Triangle @@ -35,8 +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.rulesets - self.rule_values = self.rules_box.rule_values + self.rulesets = self.rules_box.get_rulesets() self.sprites_box = arcade.gui.UIAnchorLayout(size_hint=(0.95, 0.9)) @@ -165,8 +164,6 @@ class Game(arcade.gui.UIView): def on_show_view(self): super().on_show_view() - # self.rules_box.add_rule(None, ["on_left_click", "spawn"]) - self.sprites_box.add(arcade.gui.UILabel(text="Sprites", font_size=24, text_color=arcade.color.WHITE), anchor_x="center", anchor_y="top") self.sprites_grid = self.sprites_box.add(arcade.gui.UIGridLayout(columns=8, row_count=8, align="left", vertical_spacing=10, horizontal_spacing=10, size_hint=(0.95, 0.85)), anchor_x="center", anchor_y="center").with_border() @@ -224,23 +221,8 @@ class Game(arcade.gui.UIView): self.rule_values = data["rule_values"] self.triggered_events = [] - self.current_ruleset_num = 0 - self.current_ruleset_page = 0 - self.rules_content_box.clear() - - for rule_box in self.rule_boxes.values(): - rule_box.clear() - del rule_box - - self.rule_labels = {} - self.rule_var_changers = {} - self.rule_boxes = {} - - for ruleset in data["rulesets"].values(): - self.add_ruleset(ruleset) - - self.refresh_rules_display() + # TODO: add rule loading here if self.mode == "export" and self.file_manager.submitted_content: with open(self.file_manager.submitted_content, "w") as file: @@ -386,6 +368,7 @@ class Game(arcade.gui.UIView): def simulation(self): self.disable_previous() + self.rulesets = self.rules_box.get_rulesets() self.mode = "simulation" def main_exit(self): @@ -397,5 +380,7 @@ class Game(arcade.gui.UIView): if self.mode == "simulation": self.shape_batch.draw() + elif self.mode == "rules": + self.rules_box.draw() self.ui.draw() \ No newline at end of file diff --git a/game/rules.py b/game/rules.py index 807e820..6d092fb 100644 --- a/game/rules.py +++ b/game/rules.py @@ -11,7 +11,8 @@ from utils.constants import ( slider_style, button_style, ) -from utils.preload import button_texture, button_hovered_texture +from utils.preload import button_texture, button_hovered_texture, trash_bin +from collections import deque, defaultdict import arcade, arcade.gui, random IF_KEYS = tuple(IF_RULES.keys()) @@ -20,14 +21,13 @@ 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_if_rule(): - return random.choice(IF_KEYS) - -def generate_do_rule(): - return random.choice(DO_KEYS) - -def generate_comparison(): - return random.choice(LOGICAL_OPERATORS) +def generate_rule(rule_type): + if rule_type == "if": + return random.choice(IF_KEYS) + elif rule_type == "do": + return random.choice(DO_KEYS) + else: + return random.choice(LOGICAL_OPERATORS) def per_widget_height(height, widget_count): return height // widget_count @@ -41,14 +41,31 @@ def cubic_bezier_point(p0, p1, p2, p3, t): 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): - dx = p3[0] - p0[0] - offset = max(60, abs(dx) * 0.45) - c1 = (p0[0] + offset, p0[1]) - c2 = (p3[0] - offset, p3[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]) + + while queue: + node = queue.popleft() + for neighbor in graph[node]: + if neighbor not in seen: + seen.add(neighbor) + queue.append(neighbor) + + return list(seen) + def get_rule_defaults(rule_type): if rule_type == "if": return { @@ -84,58 +101,65 @@ def get_rule_defaults(rule_type): for rule_key, rule_dict in DO_RULES.items() } - -class ComparisonBox(arcade.gui.UITextureButton): - def __init__(self, x, y, comparison, rule_num): - super().__init__( - x=x, - y=y, - text=comparison, - style=button_style, - texture=button_texture, - texture_hovered=button_hovered_texture, - ) - - self.rule_num = rule_num - class RuleBox(arcade.gui.UIBoxLayout): def __init__(self, x, y, width, height, rule_num, rule_type, rule): - super().__init__(space_between=10, x=x, y=y, width=width, height=height) + 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.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.initialize_rule() - self.per_widget_height = per_widget_height( - self.height, 2 + 2 * len(self.rule_dict["user_vars"]) - ) + 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 = {} + + widget_count = 2 + len(self.rule_dict["user_vars"]) + + self.per_widget_height = per_widget_height( + self.height, + widget_count + ) + else: + self.per_widget_height = per_widget_height( + self.height, + 2 + ) self.init_ui() - + def init_ui(self): - dropdown_options = [desc for desc, _ in self.defaults.values()] + 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], + 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, rule_type=self.rule_type, rule_num=self.rule_num: self.change_rule_type(rule_num, rule_type, event.new_value) - - if self.rule_type == "do": - self.add_connection_button() + self.desc_label.on_change = lambda event: self.change_rule_type(event.new_value) + + if self.rule_type == "comparison": + self.next_button = self.add_extra_buttons("Do / Comparison")[0] + return for n, variable_type in enumerate(self.rule_dict["user_vars"]): key = f"{variable_type}_{n}" @@ -144,9 +168,17 @@ class RuleBox(arcade.gui.UIBoxLayout): default_values = defaults[self.rule][1] self.rule_values[key] = default_values[VAR_NAMES[n]] - self.var_labels[key] = self.add( + 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]}: {self.rule_values[key]}", + 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, @@ -155,7 +187,7 @@ class RuleBox(arcade.gui.UIBoxLayout): ) if variable_type in ["variable", "size"]: - slider = self.add( + slider = box.add( arcade.gui.UISlider( value=self.rule_values[key], min_value=VAR_OPTIONS[variable_type][0], @@ -176,7 +208,7 @@ class RuleBox(arcade.gui.UIBoxLayout): self.var_changers[key] = slider else: - dropdown = self.add( + dropdown = box.add( arcade.gui.UIDropdown( default=self.rule_values[key], options=VAR_OPTIONS[variable_type], @@ -196,18 +228,34 @@ class RuleBox(arcade.gui.UIBoxLayout): self.var_changers[key] = dropdown if self.rule_type == "if": - self.add_connection_button() + self.next_button = self.add_extra_buttons("Do / Comparison")[0] - def add_connection_button(self): - self.connection_button = self.add( - arcade.gui.UITextureButton( - text="+", - width=self.width, - style=button_style, - texture=button_texture, - texture_hovered=button_hovered_texture, + def add_extra_buttons(self, texts: list[str] | str): + if not isinstance(texts, list): + texts = [texts] + box = self + else: + box = self.add( + arcade.gui.UIBoxLayout( + vertical=False, + width=self.width, + height=self.per_widget_height + ) ) - ) + + 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 change_var_value(self, variable_type, n, value): key = f"{variable_type}_{n}" @@ -224,28 +272,62 @@ class RuleBox(arcade.gui.UIBoxLayout): description = self.rule_dict["description"].format_map(values) self.desc_label.text = description - self.var_labels[key].text = f"{VAR_NAMES[n]}: {value}" + 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 get_connection_pos(rule_ui: RuleBox, idx): + if rule_ui.rule_type == "comparison": + if idx == 1: + button = rule_ui.previous_button_1 + y = button.top + direction = 1 + elif idx == 2: + button = rule_ui.previous_button_2 + y = button.top + direction = 1 + else: + 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): - def __init__(self, window): + def __init__(self, window: arcade.Window): super().__init__(size_hint=(0.95, 0.875)) self.window = window self.current_rule_num = 0 self.rule_values = {} - self.dragged_rule_ui = None - self.rule_ui: dict[str, RuleBox | ComparisonBox] = {} + self.dragged_rule_ui: RuleBox | None = None + self.rule_ui: dict[str, RuleBox] = {} + self.connections = [] - self.to_connect = [] + self.to_connect = None + self.to_connect_idx = None + self.allowed_next_connection = [] self.rules_label = self.add( arcade.gui.UILabel( text="Rules", font_size=20, text_color=arcade.color.WHITE ), anchor_x="center", - anchor_y="top", + anchor_y="top" ) self.add( @@ -268,7 +350,7 @@ class RuleUI(arcade.gui.UIAnchorLayout): style=dropdown_style, ) ) - self.add_if_rule_button.on_click = lambda event: self.add_if_rule() + self.add_if_rule_button.on_click = lambda event: self.add_rule("if") self.add_do_rule_button = self.add_button_box.add( arcade.gui.UIFlatButton( @@ -278,7 +360,7 @@ class RuleUI(arcade.gui.UIAnchorLayout): style=dropdown_style, ) ) - self.add_do_rule_button.on_click = lambda event: self.add_do_rule() + self.add_do_rule_button.on_click = lambda event: self.add_rule("do") self.add_comparison_button = self.add_button_box.add( arcade.gui.UIFlatButton( @@ -288,27 +370,63 @@ class RuleUI(arcade.gui.UIAnchorLayout): style=dropdown_style, ) ) - self.add_comparison_button.on_click = lambda event: self.add_comparison() + 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.trash_image = self.add(arcade.gui.UIImage(texture=trash_texture), anchor_x="right", anchor_y="bottom") + # self.create_connected_ruleset([("if", "x_position_compare"), ("do", "move_x")]) - def connection(self, rule_ui): - if len(self.to_connect) == 1: - rule_type = self.to_connect[0].rule_type + self.trash_spritelist = arcade.SpriteList() + self.trash_sprite = trash_bin + self.trash_sprite.position = (self.window.width * 0.9, self.window.height * 0.2) + self.trash_spritelist.append(self.trash_sprite) + + def connection(self, rule_ui, allowed_next_connection, idx): + if self.to_connect is not None: + old_rule_type = self.rule_ui[self.to_connect].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 + ): - if (rule_type == "if" and rule_ui.rule_type == "if") or (rule_type == "do" and rule_type in ["do", "comparison"]): return - self.to_connect.append(rule_ui.rule_num) + self.connections.append([self.to_connect, rule_ui.rule_num, 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 - if len(self.to_connect) == 2: - self.connections.append(self.to_connect) - self.to_connect = [] + 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: + self.connections.remove(connection) + + self.remove(self.dragged_rule_ui) + del self.dragged_rule_ui + + self.dragged_rule_ui = None + else: + self.dragged_rule_ui = rule_ui + + def get_rulesets(self): + if self.connections: + components = connected_component(self.connections, 0) + print(components) - @property - def rulesets(self): # dinamically generate them? maybe bad idea? return {} def generate_pos(self): @@ -316,73 +434,82 @@ class RuleUI(arcade.gui.UIAnchorLayout): 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_if_rule(self): + def add_rule(self, rule_type, force=None): rule_box = RuleBox( *self.generate_pos(), - self.window.width * 0.2, - self.window.height * 0.1, + self.window.width * 0.15, + self.window.height * 0.15, self.current_rule_num, - "if", - generate_if_rule(), + rule_type, + force or generate_rule(rule_type), ) + if rule_type == "if": + rule_box.next_button.on_click = lambda event, rule_box=rule_box: self.connection(rule_box, ["do", "comparison"], 1) + elif rule_type == "comparison": + rule_box.previous_button_1.on_click = lambda event, rule_box=rule_box: self.connection(rule_box, ["if", "comparison"], 1) + rule_box.previous_button_2.on_click = lambda event, rule_box=rule_box: self.connection(rule_box, ["if", "comparison"], 2) + rule_box.next_button.on_click = lambda event, rule_box=rule_box: self.connection(rule_box, ["do", "comparison"], 3) + elif rule_type == "do": + rule_box.previous_button.on_click = lambda event, rule_box=rule_box: self.connection(rule_box, ["if", "comparison"], 1) + + 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.current_rule_num += 1 - def add_do_rule(self): - rule_box = RuleBox( - *self.generate_pos(), - self.window.width * 0.2, - self.window.height * 0.1, - self.current_rule_num, - "do", - generate_do_rule(), - ) + return rule_box - self.rule_space.add(rule_box) - self.rule_ui[self.current_rule_num] = rule_box - self.current_rule_num += 1 + def create_connected_ruleset(self, rules): + previous = None - def add_comparison(self): - comparison_box = ComparisonBox( - *self.generate_pos(), generate_comparison(), self.current_rule_num - ) + for rule_type, rule in rules: + rule_box = self.add_rule(rule_type, rule) - self.rule_ui[self.current_rule_num] = comparison_box - self.add(comparison_box) - self.current_rule_num += 1 + if previous: + self.connections.append((previous.rule_num, rule_box.rule_num)) + + previous = rule_box def draw(self): self.bezier_points = [] for conn in self.connections: - start_id, end_id = conn + 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_rule_ui.top if start_rule_ui.rule_type == "do" else start_rule_ui.bottom - end_pos = end_rule_ui.top if end_rule_ui.rule_type == "do" else end_rule_ui.bottom + 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 = self.connection_between(start_pos, end_pos) + 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.trash_spritelist.draw() + def on_event(self, event): super().on_event(event) - if isinstance(event, arcade.gui.UIMouseDragEvent): + 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 - elif isinstance(event, arcade.gui.UIMouseReleaseEvent): - self.dragged_rule_ui = None - elif isinstance(event, arcade.gui.UIMousePressEvent): - if event.button == arcade.MOUSE_BUTTON_RIGHT: - ... - elif event.button == arcade.MOUSE_BUTTON_LEFT: - for rule_ui in self.rule_ui.values(): - if rule_ui.rect.point_in_rect((event.x, event.y)): - self.dragged_rule_ui = rule_ui + def on_update(self, dt): + if self.dragged_rule_ui and self.trash_sprite.rect.point_in_rect((self.window.mouse.data["x"], self.window.mouse.data["y"])): + if 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 + self.trash_sprite.update_animation() \ No newline at end of file diff --git a/utils/preload.py b/utils/preload.py index 667c114..9909156 100644 --- a/utils/preload.py +++ b/utils/preload.py @@ -8,9 +8,10 @@ button_texture = arcade.gui.NinePatchTexture(64 // 4, 64 // 4, 64 // 4, 64 // 4, button_hovered_texture = arcade.gui.NinePatchTexture(64 // 4, 64 // 4, 64 // 4, 64 // 4, arcade.load_texture(os.path.join(_assets_dir, 'graphics', 'button_hovered.png'))) SPRITE_TEXTURES = { - "circle": arcade.load_texture(os.path.join(_assets_dir, 'graphics', 'sprites', 'circle.png')), - "rectangle": arcade.load_texture(os.path.join(_assets_dir, 'graphics', 'sprites', 'rectangle.png')), - "triangle": arcade.load_texture(os.path.join(_assets_dir, 'graphics', 'sprites', 'triangle.png')), + os.path.splitext(file_name)[0]: arcade.load_texture(os.path.join(_assets_dir, 'graphics', 'sprites', file_name)) + for file_name in os.listdir(os.path.join(_assets_dir, 'graphics', 'sprites')) } -theme_sound = arcade.Sound(os.path.join(_assets_dir, 'sound', 'music.ogg')) \ No newline at end of file +theme_sound = arcade.Sound(os.path.join(_assets_dir, 'sound', 'music.ogg')) + +trash_bin = arcade.load_animated_gif(os.path.join(_assets_dir, 'graphics', 'trash_bin.gif')) \ No newline at end of file