From 9106a5887b1d2ed32c979065055e1d1e0122225b Mon Sep 17 00:00:00 2001 From: csd4ni3l Date: Sat, 22 Nov 2025 14:43:11 +0100 Subject: [PATCH] Add a general rule framework with constants and UI that can add rules --- README.md | 3 +- game/play.py | 106 +++++++++++++- game/rules.py | 29 ++++ game/sprites.py | 25 ++++ run.py | 2 - utils/constants.py | 335 +++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 494 insertions(+), 6 deletions(-) create mode 100644 game/rules.py create mode 100644 game/sprites.py diff --git a/README.md b/README.md index 027a313..6b56e55 100644 --- a/README.md +++ b/README.md @@ -1 +1,2 @@ -Chaos Protocol is a simulation game which has randomly generated rules each time you launch it which apply to objects. \ No newline at end of file +Chaos Protocol is a simulation game where you have a rule engine and objects, which you can apply rules to! By default, the game launches with random rules. +Basically a framework of sorts, which can even be random! \ No newline at end of file diff --git a/game/play.py b/game/play.py index ef1b1d0..92a7fd2 100644 --- a/game/play.py +++ b/game/play.py @@ -1,11 +1,111 @@ -import arcade, arcade.gui +import arcade, arcade.gui, pyglet, random -from utils.constants import menu_background_color, button_style -from utils.preload import button_texture, button_hovered_texture +from utils.constants import slider_style, dropdown_style, VAR_NAMES, VAR_DEFAULT, DEFAULT_GRAVITY, VAR_OPTIONS + +from game.rules import generate_rules, generate_rule +from game.sprites import * class Game(arcade.gui.UIView): def __init__(self, pypresence_client): super().__init__() + self.pypresence_client = pypresence_client + self.pypresence_client.update(state="Causing Chaos") + + self.anchor = self.add_widget(arcade.gui.UIAnchorLayout(size_hint=(1, 1))) + self.rules_box = self.anchor.add(arcade.gui.UIBoxLayout(align="left", size_hint=(0.25, 1)).with_background(color=arcade.color.DARK_GRAY), anchor_x="right", anchor_y="bottom") + + self.gravity = DEFAULT_GRAVITY + + self.rules = generate_rules(1) + + self.rule_labels = {} + self.rule_sliders = {} + + self.shapes = [] + self.shape_batch = pyglet.graphics.Batch() + + def move_x(self, shape, a): + shape.x += a + + def move_y(self, shape, a): + shape.y += a + + def change_x(self, shape, a): + shape.x = a + + def change_y(self, shape, a): + shape.y = a + + def change_x_velocity(self, shape, a): + shape.x_velocity = a + + def change_y_velocity(self, shape, a): + shape.y_velocity = a + + def get_default_values(self, variable_list): + return {VAR_NAMES[n]: VAR_DEFAULT[variable] for n, variable in enumerate(variable_list)} + + def spawn(self, shape): + x, y = random.randint(100, self.window.width - 100), random.randint(100, self.window.height - 100) + + if shape == "circle": + self.shapes.append(Circle(x, y, 10, color=arcade.color.WHITE, batch=self.shape_batch)) + + elif shape == "rectangle": + self.shapes.append(Rectangle(x, y, width=10, height=10, color=arcade.color.WHITE, batch=self.shape_batch)) + + elif shape == "triangle": + self.shapes.append(Triangle(x, y, x + 10, y, x + 5, y + 10, color=arcade.color.WHITE, batch=self.shape_batch)) + + def create_rule_ui(self, rule_box, rule, rule_type="if"): + default_values = {VAR_NAMES[n]: VAR_DEFAULT[variable] for n, variable in enumerate(rule["user_vars"])} + description = rule["description"].format_map(default_values) + + rule_box.add(arcade.gui.UILabel(description if rule_type == "if" else f"THEN {description}", font_size=13, width=self.window.width * 0.25)) + + for n, variable in enumerate(rule["user_vars"]): + rule_box.add(arcade.gui.UILabel(f'{VAR_NAMES[n]}: {default_values[VAR_NAMES[n]]}', font_size=11, width=self.window.width * 0.25, height=self.window.height / 25)) + + if variable in ["variable", "size"]: + slider = rule_box.add(arcade.gui.UISlider(value=default_values[VAR_NAMES[n]], min_value=VAR_OPTIONS[variable][0], max_value=VAR_OPTIONS[variable][1], step=1, style=slider_style, width=self.window.width * 0.25, height=self.window.height / 25)) + slider._render_steps = lambda surface: None + elif variable in ["shape_type", "target_type", "color"]: + dropdown = rule_box.add(arcade.gui.UIDropdown(default=default_values[VAR_NAMES[n]], options=VAR_OPTIONS[variable], active_style=dropdown_style, primary_style=dropdown_style, dropdown_style=dropdown_style, width=self.window.width * 0.25, height=self.window.height / 25)) + def create_ruleset_ui(self, ruleset): + rule_box = self.rules_box.add(arcade.gui.UIBoxLayout(space_between=5, align="left").with_background(color=arcade.color.DARK_SLATE_GRAY)) + + if len(ruleset) == 2: + self.create_rule_ui(rule_box, ruleset[0]) + self.create_rule_ui(rule_box, ruleset[1], "do") + + else: + self.create_rule_ui(rule_box, ruleset[0], "if") + + rule_box.add(arcade.gui.UILabel(ruleset[1].upper(), font_size=14, width=self.window.width * 0.25)) + + self.create_rule_ui(rule_box, ruleset[2], "if") + + self.create_rule_ui(rule_box, ruleset[3], "do") + + self.rules_box.add(arcade.gui.UISpace(height=self.window.height / 50)) + def on_show_view(self): super().on_show_view() + + add_rule_button = self.rules_box.add(arcade.gui.UIFlatButton(text="Add rule", width=self.window.width * 0.25, height=self.window.height / 15, style=dropdown_style)) + add_rule_button.on_click = lambda event: self.add_rule() + + self.rules_box.add(arcade.gui.UISpace(height=self.window.height / 50)) + + for ruleset in self.rules: + self.create_ruleset_ui(ruleset) + + def add_rule(self): + self.rules.append(generate_rule()) + self.create_ruleset_ui(self.rules[-1]) + + def on_draw(self): + super().on_draw() + + self.shape_batch.draw() \ No newline at end of file diff --git a/game/rules.py b/game/rules.py new file mode 100644 index 0000000..a864911 --- /dev/null +++ b/game/rules.py @@ -0,0 +1,29 @@ +from utils.constants import DO_RULES, IF_RULES, LOGICAL_OPERATORS, NON_COMPATIBLE_WHEN, NON_COMPATIBLE_DO_WHEN + +import random + +def generate_rule(): + when_a = random.choice(list(IF_RULES.keys())) + when_b = None + + if random.random() < 0.5: + when_b = random.choice(list(IF_RULES.keys())) + while (when_a, when_b) in NON_COMPATIBLE_WHEN or (when_b, when_a) in NON_COMPATIBLE_WHEN or when_a == when_b: + when_a = random.choice(list(IF_RULES.keys())) + when_b = random.choice(list(IF_RULES.keys())) + + logical_operation = random.choice(LOGICAL_OPERATORS) + else: + logical_operation = None + + do = random.choice(list(DO_RULES.keys())) + while (when_a, do) in NON_COMPATIBLE_DO_WHEN or (do, when_a) in NON_COMPATIBLE_DO_WHEN or (when_b, do) in NON_COMPATIBLE_DO_WHEN or (do, when_b) in NON_COMPATIBLE_DO_WHEN: + do = random.choice(list(DO_RULES.keys())) + + if logical_operation: + return [IF_RULES[when_a], logical_operation, IF_RULES[when_b], DO_RULES[do]] + else: + return [IF_RULES[when_a], DO_RULES[do]] + +def generate_rules(n): + return [generate_rule() for _ in range(n)] \ No newline at end of file diff --git a/game/sprites.py b/game/sprites.py new file mode 100644 index 0000000..fddfa98 --- /dev/null +++ b/game/sprites.py @@ -0,0 +1,25 @@ +import pyglet + +from utils.constants import DEFAULT_X_VELOCITY, DEFAULT_Y_VELOCITY + +class BaseShape(): + def __init__(self): + self.x_velocity = DEFAULT_X_VELOCITY + self.y_velocity = DEFAULT_Y_VELOCITY + + def update(self, gravity): + self.x += self.x_velocity + self.y += self.y_velocity + self.y -= gravity + +class Circle(pyglet.shapes.Circle, BaseShape): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + +class Rectangle(pyglet.shapes.Rectangle, BaseShape): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + +class Triangle(pyglet.shapes.Triangle, BaseShape): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) \ No newline at end of file diff --git a/run.py b/run.py index bd2203b..3d960c3 100644 --- a/run.py +++ b/run.py @@ -19,8 +19,6 @@ from arcade.experimental.controller_window import ControllerWindow sys.excepthook = on_exception -__builtins__.print = lambda *args, **kwargs: logging.debug(" ".join(map(str, args))) - if not log_dir in os.listdir(): os.makedirs(log_dir) diff --git a/utils/constants.py b/utils/constants.py index 49a96dd..ed04a4b 100644 --- a/utils/constants.py +++ b/utils/constants.py @@ -3,6 +3,341 @@ 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"] + +DEFAULT_GRAVITY = 5 +DEFAULT_X_VELOCITY = 0 +DEFAULT_Y_VELOCITY = 0 + +COLORS = [key for key, value in arcade.color.__dict__.items() if isinstance(value, Color)] + +VAR_DEFAULT = { + "shape_type": SHAPES[0], + "target_type": SHAPES[1], + "variable": 0, + "color": "WHITE", + "size": 10, +} + +VAR_OPTIONS = { + "shape_type": SHAPES, + "target_type": SHAPES, + "variable": (0, 2500), + "color": "WHITE", + "size": (1, 200), +} + +IF_RULES = { + "x_position": { + "description": "IF X for {a} shape is {b}", + "trigger": "every_update", + "user_vars": ["shape_type", "variable"], + "vars": ["shape_type", "variable", "event_shape_type", "shape_x"], + "func": lambda *v: (v[0] == v[2]) and (v[3] == v[1]) + }, + "y_position": { + "description": "IF Y for {a} shape is {b}", + "trigger": "every_update", + "user_vars": ["shape_type", "variable"], + "vars": ["shape_type", "variable", "event_shape_type", "shape_y"], + "func": lambda *v: (v[0] == v[2]) and (v[3] == v[1]) + }, + "color_is": { + "description": "IF {a} shape color is {b}", + "trigger": "every_update", + "user_vars": ["shape_type", "color"], + "vars": ["shape_type", "color", "event_shape_type", "shape_color"], + "func": lambda *v: (v[0] == v[2]) and (v[3] == v[1]) + }, + "size_is": { + "description": "IF {a} shape size is {b}", + "trigger": "every_update", + "user_vars": ["shape_type", "size"], + "vars": ["shape_type", "size", "event_shape_type", "shape_size"], + "func": lambda *v: (v[0] == v[2]) and (v[3] == v[1]) + }, + "spawns": { + "description": "IF {a} shape spawns", + "trigger": "spawns", + "user_vars": ["shape_type"], + "vars": ["shape_type", "event_shape_type"], + "func": lambda *v: v[0] == v[1] + }, + "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] + }, + "x_velocity_changes": { + "description": "IF {a} shape X velocity changes", + "trigger": "x_change", + "user_vars": ["shape_type"], + "vars": ["shape_type", "event_shape_type"], + "func": lambda *v: v[0] == v[1] + }, + "y_velocity_changes": { + "description": "IF {a} shape Y velocity changes", + "trigger": "y_change", + "user_vars": ["shape_type"], + "vars": ["shape_type", "event_shape_type"], + "func": lambda *v: v[0] == v[1] + }, + "x_gravity_changes": { + "description": "IF {a} shape X gravity changes", + "trigger": "gravity_x_change", + "user_vars": ["shape_type"], + "vars": ["shape_type", "event_shape_type"], + "func": lambda *v: v[0] == v[1] + }, + "y_gravity_changes": { + "description": "IF {a} shape Y gravity changes", + "trigger": "gravity_y_change", + "user_vars": ["shape_type"], + "vars": ["shape_type", "event_shape_type"], + "func": lambda *v: v[0] == v[1] + }, + "gravity_changes": { + "description": "IF gravity changes", + "user_vars": [], + "trigger": "gravity_change", + "vars": [], + "func": lambda *v: True + }, + "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] + }, + "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] + }, + "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]) + }, + "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]) + }, + "launch": { + "description": "IF game launches", + "trigger": "game_launch", + "user_vars": [], + "vars": [], + "func": lambda *v: True + }, + "every_update": { + "description": "Every update", + "trigger": "every_update", + "user_vars": [], + "vars": [], + "func": lambda *v: True + } +} + +NON_COMPATIBLE_WHEN = [ + ("spawns", "destroyed"), + ("spawns", "morphs"), + ("spawns", "collides"), + ("spawns", "x_velocity_changes"), + ("spawns", "y_velocity_changes"), + ("spawns", "x_gravity_changes"), + ("spawns", "y_gravity_changes"), + ("spawns", "gravity_changes"), + ("spawns", "color_changes"), + ("spawns", "size_changes"), + + ("destroyed", "morphs"), + ("destroyed", "collides"), + ("destroyed", "x_velocity_changes"), + ("destroyed", "y_velocity_changes"), + ("destroyed", "x_gravity_changes"), + ("destroyed", "y_gravity_changes"), + ("destroyed", "gravity_changes"), + ("destroyed", "color_changes"), + ("destroyed", "size_changes"), + + ("morphs", "collides"), + ("morphs", "x_velocity_changes"), + ("morphs", "y_velocity_changes"), + ("morphs", "x_gravity_changes"), + ("morphs", "y_gravity_changes"), + ("morphs", "gravity_changes"), + ("morphs", "color_changes"), + ("morphs", "size_changes"), + + ("collides", "destroyed"), + ("collides", "morphs"), + ("collides", "gravity_changes"), + + ("x_gravity_changes", "gravity_changes"), + ("y_gravity_changes", "gravity_changes"), + + ("color_changes", "gravity_changes"), + ("size_changes", "gravity_changes"), + + ("every_update", "spawns"), + ("every_update", "destroyed"), + ("every_update", "morphs"), + ("every_update", "collides"), + ("every_update", "x_velocity_changes"), + ("every_update", "y_velocity_changes"), + ("every_update", "x_gravity_changes"), + ("every_update", "y_gravity_changes"), + ("every_update", "gravity_changes"), + ("every_update", "color_changes"), + ("every_update", "size_changes"), + ("every_update", "launch"), + + ("launch", "spawns"), + ("launch", "destroyed"), + ("launch", "morphs"), + ("launch", "collides"), + ("launch", "x_velocity_changes"), + ("launch", "y_velocity_changes"), + ("launch", "x_gravity_changes"), + ("launch", "y_gravity_changes"), + ("launch", "gravity_changes"), + ("launch", "color_changes"), + ("launch", "size_changes"), +] + +NON_COMPATIBLE_DO_WHEN = [ + ("destroyed", "change_x"), + ("destroyed", "change_y"), + ("destroyed", "move_x"), + ("destroyed", "move_y"), + ("destroyed", "change_x_velocity"), + ("destroyed", "change_y_velocity"), + ("destroyed", "change_gravity"), + ("destroyed", "change_color"), + ("destroyed", "change_size"), + ("destroyed", "morph_into"), + ("destroyed", "destroy"), + + ("morphs", "morph_into"), + + ("gravity_changes", "change_x"), + ("gravity_changes", "change_y"), + ("gravity_changes", "move_x"), + ("gravity_changes", "move_y"), + ("gravity_changes", "change_x_velocity"), + ("gravity_changes", "change_y_velocity"), + ("gravity_changes", "change_gravity"), + ("gravity_changes", "change_color"), + ("gravity_changes", "change_size"), + ("gravity_changes", "morph_into"), + ("gravity_changes", "destroy"), + + ("x_velocity_changes", "change_x_velocity"), + ("y_velocity_changes", "change_y_velocity"), + + ("color_changes", "change_color"), + ("size_changes", "change_size"), + + ("launch", "change_x"), + ("launch", "change_y"), + ("launch", "move_x"), + ("launch", "move_y"), + ("launch", "change_x_velocity"), + ("launch", "change_y_velocity"), + ("launch", "change_gravity"), + ("launch", "change_color"), + ("launch", "change_size"), + ("launch", "destroy"), + ("launch", "morph_into") +] + +DO_RULES = { + "change_x": { + "description": "Change this shape's X to {a}", + "action": {"type": "shape_action", "name": "change_x"}, + "user_vars": ["variable"] + }, + + "change_y": { + "description": "Change this shape's Y to {a}", + "action": {"type": "shape_action", "name": "change_y"}, + "user_vars": ["variable"] + }, + + "move_x": { + "description": "Move this shape's X by {a}", + "action": {"type": "shape_action", "name": "move_x"}, + "user_vars": ["variable"] + }, + + "move_y": { + "description": "Move this shape's Y by {a}", + "action": {"type": "shape_action", "name": "move_y"}, + "user_vars": ["variable"] + }, + + "change_x_velocity": { + "description": "Change X velocity of this to {a}", + "action": {"type": "shape_action", "name": "change_x_vel"}, + "user_vars": ["variable"] + }, + + "change_y_velocity": { + "description": "Change Y velocity of this to {a}", + "action": {"type": "shape_action", "name": "change_y_vel"}, + "user_vars": ["variable"] + }, + + "change_color": { + "description": "Change this shape's color to {a}", + "action": {"type": "shape_action", "name": "change_color"}, + "user_vars": ["color"] + }, + + "change_size": { + "description": "Change this shape's size to {a}", + "action": {"type": "shape_action", "name": "change_size"}, + "user_vars": ["size"] + }, + + "destroy": { + "description": "Destroy this", + "action": {"type": "shape_action", "name": "destroy"}, + "user_vars": [] + }, + + "morph_into": { + "description": "Morph this into {a}", + "action": {"type": "shape_action", "name": "morph"}, + "user_vars": ["shape_type"] + }, + + "change_gravity": { + "description": "Change this shape's gravity to {a}", + "action": {"type": "shape_action", "name": "change_gravity"}, + "user_vars": ["variable"] + }, + + "spawn": { + "description": "Spawn {a}", + "action": {"type": "global_action", "name": "spawn"}, + "user_vars": ["shape_type"] + } +} + menu_background_color = (30, 30, 47) log_dir = 'logs' discord_presence_id = 1440807203094138940