Compare commits

..

14 Commits

Author SHA1 Message Date
csd4ni3l
6d11283de4 Update demo video link 2025-12-07 23:54:28 +01:00
csd4ni3l
c25ffe1a62 Fix file manager sprite add submitting with / at the end, fix change_size not working for textured, make texturedrectangles actually work, fix importing breaking rule system and refresh sprites after import, add separate import and export file managers, reset submitted content of file managers after submit, fix TexturedRectangles staying at 0, 0, fix some performance issues and update README 2025-12-07 23:45:20 +01:00
csd4ni3l
3a7e40d833 fix some file manager stuff, add sprite adding, convert values to float before using them, add TexturedRectangles so custom sprites work, remove morphing, fix DO blocks and some others not having vars, make blocks bigger, fix trash can not working most of the time, add more key inputs 2025-12-07 22:43:07 +01:00
csd4ni3l
b74115b489 Fix importing and exporting, add variables (scratch blocks as well), recursively execute the rules, reset x and y gravity and events when switching to simulation, remove rule generation, fix indentation and padding, remove bloat, fix bugs 2025-12-07 19:13:46 +01:00
csd4ni3l
fe7b42ec40 fix dragging out from block children not working 2025-12-07 12:11:35 +01:00
csd4ni3l
f51260d94f 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. 2025-12-06 23:35:40 +01:00
csd4ni3l
15bb259a4f Fix connected component giving a single edge, add backtracking for rulesets, add previous property, add a better default ruleset, fix trash only working on hover and not intersection, fix dragging, update play.py a bit to be more compatible 2025-12-06 13:40:08 +01:00
csd4ni3l
9df46b2ab6 fix rule ui not being removed, remove background from trash bin 2025-12-05 22:29:42 +01:00
csd4ni3l
937c8b332c Add trash can to remove elements, remove old rule loading code, make generate_rule modular, make connection_between have straight at the start, moveComparisonBox to RuleBox, add extra buttons, add get_connection_pos, add connections and dragging 2025-12-05 22:22:50 +01:00
csd4ni3l
741f81b198 Improve filemanager layout and positioning, dont check mouse events unless in simulation, work more in rules, not working fully yet, of course. but convert it to a space where rules have pairs, and move the logic from straight rulesets 2025-12-04 16:32:28 +01:00
csd4ni3l
44f2f3bf51 Update requirements.txt, add saving and loading, add file manager, convert each UI group to a category controlled by buttons at the bottom, fix crash induced by non shape_action if rules not getting shape_type 2025-12-02 20:24:01 +01:00
csd4ni3l
b2caf219d6 add demo video to README 2025-11-24 19:18:56 +01:00
csd4ni3l
46956bf247 make rulesets per page 1 to avoid dropdowns overflowing 2025-11-24 19:07:02 +01:00
csd4ni3l
5982b1326a Add settings for default values instead of constants, remove sfx from settings, add music, add missing collision event, remove 1366x768 as an allowed resolution 2025-11-24 18:58:25 +01:00
14 changed files with 1425 additions and 578 deletions

View File

@@ -1,5 +1,10 @@
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)
Huge Thanks to Python for being the programming language used in this game.
https://www.python.org/

View File

@@ -1,4 +1,9 @@
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.
**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!
The project is a huge WIP! You can't do much yet, but you have basic rules and simulation.
The game has Scratch-like blocks and conditions, and everything you probably need for a simple game.
The problem: it hasn't been well tested.
## Speed is a big problem of the project, especially collision detections, which are very expensive.
I am not using spatial hashing right now, and i had no time to optimize the code. I'm sorry.
[![Demo Video](https://img.youtube.com/vi/iPXQfllqsvs/hqdefault.jpg)](https://youtu.be/iPXQfllqsvs)

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
assets/sound/music.ogg Normal file

Binary file not shown.

127
game/file_manager.py Normal file
View File

@@ -0,0 +1,127 @@
import arcade, arcade.gui, os, time
from utils.constants import button_style
from utils.preload import button_texture, button_hovered_texture
from arcade.gui.experimental.scroll_area import UIScrollArea, UIScrollBar
class FileManager(arcade.gui.UIAnchorLayout):
def __init__(self, width, height, size_hint, allowed_extensions):
super().__init__(size_hint=size_hint, vertical=False)
self.filemanager_width = width
self.filemanager_height = height
self.current_directory = os.path.expanduser("~")
self.allowed_extensions = allowed_extensions
self.file_buttons = []
self.submitted_content = ""
self.mode = None
self.content_cache = {}
self.pre_cache_contents()
self.current_directory_label = self.add(arcade.gui.UILabel(text=self.current_directory, font_name="Roboto", font_size=22), anchor_x="center", anchor_y="top", align_y=-10)
self.scroll_area = UIScrollArea(size_hint=(0.665, 0.7)) # center on screen
self.scroll_area.scroll_speed = -50
self.add(self.scroll_area, anchor_x="center", anchor_y="center", align_y=self.filemanager_width * 0.025)
self.scrollbar = UIScrollBar(self.scroll_area)
self.scrollbar.size_hint = (0.02, 1)
self.add(self.scrollbar, anchor_x="right", anchor_y="bottom")
self.files_box = arcade.gui.UIBoxLayout(space_between=5)
self.scroll_area.add(self.files_box)
self.bottom_box = self.add(arcade.gui.UIBoxLayout(space_between=5), anchor_x="center", anchor_y="bottom", align_y=5)
self.filename_label = self.bottom_box.add(arcade.gui.UILabel(text="Filename:", font_name="Roboto", font_size=17))
self.filename_input = self.bottom_box.add(arcade.gui.UIInputText(width=self.filemanager_width * 0.35, height=self.filemanager_height * 0.05).with_border(color=arcade.color.WHITE))
self.submit_button = self.bottom_box.add(arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text="Submit", style=button_style, width=self.filemanager_width * 0.5, height=self.filemanager_height * 0.05))
self.submit_button.on_click = lambda event: self.submit(self.current_directory)
self.submit_button.visible = False
self.filename_label.visible = False
self.filename_input.visible = False
self.show_directory()
def change_mode(self, mode):
self.mode = mode
self.filename_input.visible = self.mode == "export"
self.filename_label.visible = self.mode == "export"
self.submit_button.visible = self.mode == "export"
def submit(self, content):
self.submitted_content = content if self.mode == "import" else os.path.join(content, self.filename_input.text)
def get_content(self, directory):
if not directory in self.content_cache or time.perf_counter() - self.content_cache[directory][-1] >= 30:
try:
entries = os.listdir(directory)
except PermissionError:
return None
filtered = [
entry for entry in entries
if (os.path.isdir(os.path.join(directory, entry)) and not "." in entry) or
os.path.splitext(entry)[1].lower() in self.allowed_extensions
]
sorted_entries = sorted(
filtered,
key=lambda x: (0 if os.path.isdir(os.path.join(directory, x)) else 1, x.lower())
)
self.content_cache[directory] = sorted_entries
self.content_cache[directory].append(time.perf_counter())
return self.content_cache[directory][:-1]
def pre_cache_contents(self):
for directory in self.walk_limited_depth(self.current_directory):
self.get_content(directory)
def walk_limited_depth(self, start_dir, max_depth=2):
start_dir = os.path.abspath(start_dir)
def _walk(current_dir, current_depth):
if current_depth > max_depth:
return
yield current_dir
try:
with os.scandir(current_dir) as it:
for entry in it:
if entry.is_dir(follow_symlinks=False):
yield from _walk(entry.path, current_depth + 1)
except PermissionError:
pass # skip directories you can't access
return _walk(start_dir, 0)
def show_directory(self):
self.files_box.clear()
self.file_buttons.clear()
self.current_directory_label.text = self.current_directory
self.file_buttons.append(self.files_box.add(arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text="Go up", style=button_style, width=self.filemanager_width / 1.5, height=self.filemanager_height * 0.05,)))
self.file_buttons[-1].on_click = lambda event, directory=self.current_directory: self.change_directory(os.path.dirname(directory))
for file in self.get_content(self.current_directory):
self.file_buttons.append(self.files_box.add(arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text=file, style=button_style, width=self.filemanager_width / 1.5, height=self.filemanager_height * 0.05,)))
if os.path.isdir(os.path.join(self.current_directory, file)):
self.file_buttons[-1].on_click = lambda event, directory=os.path.join(self.current_directory, file): self.change_directory(directory)
else:
self.file_buttons[-1].on_click = lambda event, file=os.path.join(self.current_directory, file): self.submit(file)
def change_directory(self, directory):
if directory.startswith("//"): # Fix / paths
directory = directory[1:]
self.current_directory = directory
self.show_directory()

View File

@@ -1,10 +1,13 @@
import arcade, arcade.gui, pyglet, random
import arcade, arcade.gui, pyglet, random, json
from utils.preload import SPRITE_TEXTURES
from utils.constants import slider_style, dropdown_style, VAR_NAMES, VAR_DEFAULT, DEFAULT_X_GRAVITY, DEFAULT_Y_GRAVITY, VAR_OPTIONS, DO_RULES, IF_RULES, SHAPES, ALLOWED_INPUT
from dataclasses import asdict
from game.rules import generate_ruleset
from game.sprites import BaseShape, Rectangle, Circle, Triangle
from utils.preload import SPRITE_TEXTURES, button_texture, button_hovered_texture
from utils.constants import button_style, DO_RULES, IF_RULES, SPRITES, ALLOWED_INPUT
from game.rules import RuleUI, Block, VarBlock
from game.sprites import BaseRectangle, BaseShape, Rectangle, Circle, Triangle, TexturedRectangle
from game.file_manager import FileManager
class Game(arcade.gui.UIView):
def __init__(self, pypresence_client):
@@ -13,34 +16,58 @@ class Game(arcade.gui.UIView):
self.pypresence_client = pypresence_client
self.pypresence_client.update(state="Causing Chaos")
with open("settings.json", "r") as file:
self.settings = json.load(file)
self.anchor = self.add_widget(arcade.gui.UIAnchorLayout(size_hint=(1, 1)))
self.rules_box = arcade.gui.UIBoxLayout(align="center", size_hint=(0.25, 0.95)).with_background(color=arcade.color.DARK_GRAY)
self.anchor.add(self.rules_box, anchor_x="right", anchor_y="center", align_x=-self.window.height * 0.025)
self.rules_box = RuleUI(self.window)
self.sprites_box = self.anchor.add(arcade.gui.UIBoxLayout(size_hint=(0.15, 0.95), align="center", space_between=10).with_background(color=arcade.color.DARK_GRAY), anchor_x="left", anchor_y="center", align_x=self.window.height * 0.025)
self.import_file_manager = FileManager(self.window.width * 0.95, self.window.height * 0.875, (0.95, 0.875), [".json"]).with_border()
self.import_file_manager.change_mode("import")
self.x_gravity = DEFAULT_X_GRAVITY
self.y_gravity = DEFAULT_Y_GRAVITY
self.export_file_manager = FileManager(self.window.width * 0.95, self.window.height * 0.875, (0.95, 0.875), [".json"]).with_border()
self.export_file_manager.change_mode("export")
self.current_ruleset_num = 0
self.current_ruleset_page = 0
self.rulesets_per_page = 3
self.rulesets = {}
self.rule_values = {}
self.ui_selector_box = self.anchor.add(arcade.gui.UIBoxLayout(vertical=False, space_between=self.window.width / 100), anchor_x="left", anchor_y="bottom", align_y=5, align_x=self.window.width / 100)
self.add_ui_selector("Simulation", lambda event: self.simulation())
self.add_ui_selector("Rules", lambda event: self.rules())
self.add_ui_selector("Sprites", lambda event: self.sprites())
self.add_ui_selector("Import", lambda event: self.import_file())
self.add_ui_selector("Export", lambda event: self.export_file())
self.mode = "simulation"
self.x_gravity = self.settings.get("default_x_gravity", 0)
self.y_gravity = self.settings.get("default_y_gravity", 5)
self.triggered_events = []
self.rule_labels = {}
self.rule_var_changers = {}
self.rule_boxes = {}
self.rulesets = self.rules_box.rulesets
self.sprite_add_filemanager = FileManager(self.window.width * 0.9, self.window.height * 0.75, (0.9, 0.75), [".png", ".jpg", ".jpeg", ".bmp", ".gif"])
self.sprite_add_filemanager.change_mode("import")
self.sprite_add_ui = arcade.gui.UIBoxLayout(size_hint=(0.95, 0.9), space_between=10)
self.sprite_add_ui.add(arcade.gui.UILabel(text="Add Sprite", font_size=24, text_color=arcade.color.WHITE))
self.sprite_add_ui.add(arcade.gui.UILabel(text="Sprite Name:", font_size=18, text_color=arcade.color.WHITE))
self.sprite_name_input = self.sprite_add_ui.add(arcade.gui.UIInputText(width=self.window.width * 0.4, height=self.window.height * 0.05).with_border(color=arcade.color.WHITE))
self.sprite_add_ui.add(arcade.gui.UILabel(text="Select a texture for the sprite:", font_size=18, text_color=arcade.color.WHITE))
self.sprite_add_ui.add(self.sprite_add_filemanager, anchor_x="center", anchor_y="bottom", align_y=25)
self.sprites_ui = arcade.gui.UIAnchorLayout(size_hint=(0.95, 0.9))
self.sprite_types = SPRITES
self.shapes = []
self.shape_batch = pyglet.graphics.Batch()
self.rules_content_box = None
self.nav_buttons_box = None
self.simulation()
def add_ui_selector(self, button_text, on_click):
button = self.ui_selector_box.add(arcade.gui.UITextureButton(text=button_text, width=self.window.width / 5.5, height=self.window.height / 15, style=button_style, texture=button_texture, texture_hovered=button_hovered_texture))
button.on_click = on_click
def move_x(self, a, shape):
a = float(a)
if isinstance(shape, Triangle):
shape.x += a
shape.x2 += a
@@ -49,6 +76,7 @@ class Game(arcade.gui.UIView):
shape.x += a
def move_y(self, a, shape):
a = float(a)
if isinstance(shape, Triangle):
shape.y += a
shape.y2 += a
@@ -57,6 +85,7 @@ class Game(arcade.gui.UIView):
shape.y += a
def change_x(self, a, shape):
a = float(a)
if isinstance(shape, Triangle):
offset_x2 = shape.x2 - shape.x
offset_x3 = shape.x3 - shape.x
@@ -68,6 +97,7 @@ class Game(arcade.gui.UIView):
shape.x = a
def change_y(self, a, shape):
a = float(a)
if isinstance(shape, Triangle):
offset_y2 = shape.y2 - shape.y
offset_y3 = shape.y3 - shape.y
@@ -79,24 +109,28 @@ class Game(arcade.gui.UIView):
shape.y = a
def change_x_velocity(self, a, shape):
a = float(a)
shape.x_velocity = a
self.triggered_events.append(["x_velocity_change", {"event_shape_type": shape.shape_type, "shape_size": shape.shape_size, "shape_x": shape.x, "shape_y": shape.y, "shape": shape, "shape_color": shape.color}])
self.triggered_events.append(["x_velocity_change", {"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}])
def change_y_velocity(self, a, shape):
a = float(a)
shape.y_velocity = a
self.triggered_events.append(["y_velocity_change", {"event_shape_type": shape.shape_type, "shape_size": shape.shape_size, "shape_x": shape.x, "shape_y": shape.y, "shape": shape, "shape_color": shape.color}])
self.triggered_events.append(["y_velocity_change", {"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}])
def change_x_gravity(self, a):
a = float(a)
self.x_gravity = a
self.triggered_events.append(["x_gravity_change", {}])
def change_y_gravity(self, a):
a = float(a)
self.y_gravity = a
self.triggered_events.append(["y_gravity_change", {}])
def change_color(self, a, shape):
shape.color = getattr(arcade.color, a)
self.triggered_events.append(["color_change", {"event_shape_type": shape.shape_type, "shape_size": shape.shape_size, "shape_x": shape.x, "shape_y": shape.y, "shape": shape, "shape_color": shape.color}])
shape.shape_color = getattr(arcade.color, a)
self.triggered_events.append(["color_change", {"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}])
def destroy(self, shape: BaseShape):
self.triggered_events.append(["destroyed", {"event_shape_type": shape.shape_type}])
@@ -105,9 +139,10 @@ class Game(arcade.gui.UIView):
shape.delete()
def change_size(self, a, shape):
a = float(a)
if isinstance(shape, Circle):
shape.radius = a
elif isinstance(shape, Rectangle):
elif isinstance(shape, BaseRectangle):
shape.width = a
shape.height = a
elif isinstance(shape, Triangle):
@@ -122,7 +157,7 @@ class Game(arcade.gui.UIView):
shape.x3 += size
shape.y3 += size
self.triggered_events.append(["size_change", {"event_shape_type": shape.shape_type, "shape_size": shape.shape_size, "shape_x": shape.x, "shape_y": shape.y, "shape": shape, "shape_color": shape.color}])
self.triggered_events.append(["size_change", {"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}])
def spawn(self, shape_type):
x, y = random.randint(int(self.window.width * 0.15) + 50, int(self.window.width * 0.75) - 50), random.randint(100, self.window.height - 100)
@@ -136,219 +171,64 @@ class Game(arcade.gui.UIView):
elif shape_type == "triangle":
self.shapes.append(Triangle(x, y, x + 10, y, x + 5, y + 10, color=arcade.color.WHITE, batch=self.shape_batch))
else:
self.shapes.append(TexturedRectangle(pyglet.image.load(self.sprite_types[shape_type]), x, y, batch=self.shape_batch, shape_type=shape_type))
shape = self.shapes[-1]
self.triggered_events.append(["spawn", {"event_shape_type": shape.shape_type, "shape_size": shape.shape_size, "shape_x": shape.x, "shape_y": shape.y, "shape": shape, "shape_color": shape.shape_color}])
self.triggered_events.append(["spawns", {"event_shape_type": shape.shape_type, "shape_size": shape.shape_size, "shape_x": shape.x, "shape_y": shape.y, "shape": shape, "shape_color": shape.color}])
def add_sprite(self):
self.disable_previous()
def morph(self, a, shape):
old_shape_x, old_shape_y, old_shape_size, old_shape_color = shape.x, shape.y, shape.shape_size, shape.shape_color
self.destroy(shape)
self.mode = "sprite_add"
if a == "circle":
self.shapes.append(Circle(old_shape_x, old_shape_y, old_shape_size, color=getattr(arcade.color, old_shape_color), batch=self.shape_batch))
self.anchor.add(self.sprite_add_ui, anchor_x="center", anchor_y="center")
elif a == "rectangle":
self.shapes.append(Rectangle(old_shape_x, old_shape_y, width=old_shape_size, height=old_shape_size, color=getattr(arcade.color, old_shape_color), batch=self.shape_batch))
def check_selection(delta_time):
if self.sprite_add_filemanager.submitted_content:
texture = arcade.load_texture(self.sprite_add_filemanager.submitted_content)
elif a == "triangle":
self.shapes.append(Triangle(old_shape_x, old_shape_y, old_shape_x + old_shape_size, old_shape_y, old_shape_x + int(old_shape_size / 2), old_shape_y + old_shape_size, color=getattr(arcade.color, old_shape_color), batch=self.shape_batch))
SPRITE_TEXTURES[self.sprite_name_input.text] = texture
SPRITES[self.sprite_name_input.text] = self.sprite_add_filemanager.submitted_content
def get_rule_defaults(self, rule_type):
if rule_type == "if":
return {
rule_key: (
rule_dict["description"].format_map({VAR_NAMES[n]: VAR_NAMES[n] for n, variable in enumerate(rule_dict["user_vars"])}),
{VAR_NAMES[n]: VAR_DEFAULT[variable] for n, variable in enumerate(rule_dict["user_vars"])}
)
for rule_key, rule_dict in IF_RULES.items()
}
elif rule_type == "do":
return {
rule_key: (
rule_dict["description"].format_map({VAR_NAMES[n]: VAR_NAMES[n] for n, variable in enumerate(rule_dict["user_vars"])}),
{VAR_NAMES[n]: VAR_DEFAULT[variable] for n, variable in enumerate(rule_dict["user_vars"])}
)
for rule_key, rule_dict in DO_RULES.items()
}
def create_rule_ui(self, rule_box: arcade.gui.UIBoxLayout, rule, rule_type, rule_num=1):
defaults = self.get_rule_defaults(rule_type)
rule_dict = IF_RULES[rule] if rule_type == "if" else DO_RULES[rule]
ruleset_num = self.current_ruleset_num
default_values = defaults[rule][1]
self.sprites_grid.clear()
dropdown_options = [desc for desc, _ in defaults.values()]
desc_label = rule_box.add(arcade.gui.UIDropdown(default=defaults[rule][0], options=dropdown_options, font_size=13, width=self.window.width * 0.225, active_style=dropdown_style, primary_style=dropdown_style, dropdown_style=dropdown_style))
desc_label.on_change = lambda event, rule_type=rule_type, ruleset_num=ruleset_num, rule_num=rule_num: self.change_rule_type(ruleset_num, rule_num, rule_type, event.new_value)
self.rule_labels[f"{self.current_ruleset_num}_{rule_num}_desc"] = desc_label
for n, shape in enumerate(SPRITES):
row, col = n % 8, n // 8
box = self.sprites_grid.add(arcade.gui.UIBoxLayout(), row=row, column=col)
box.add(arcade.gui.UILabel(text=shape, font_size=16, text_color=arcade.color.WHITE))
box.add(arcade.gui.UIImage(texture=SPRITE_TEXTURES[shape], width=self.window.width / 15, height=self.window.width / 15))
for n, variable_type in enumerate(rule_dict["user_vars"]):
key = f"{self.current_ruleset_num}_{rule_num}_{variable_type}_{n}"
self.anchor.remove(self.sprite_add_ui)
arcade.unschedule(check_selection)
self.rule_values[key] = default_values[VAR_NAMES[n]]
label = rule_box.add(arcade.gui.UILabel(f'{VAR_NAMES[n]}: {default_values[VAR_NAMES[n]]}', font_size=11, width=self.window.width * 0.225, height=self.window.height / 30))
self.rule_labels[key] = label
if variable_type in ["variable", "size"]:
slider = rule_box.add(arcade.gui.UISlider(value=default_values[VAR_NAMES[n]], min_value=VAR_OPTIONS[variable_type][0], max_value=VAR_OPTIONS[variable_type][1], step=1, style=slider_style, width=self.window.width * 0.225, height=self.window.height / 30))
slider._render_steps = lambda surface: None
slider.on_change = lambda event, variable_type=variable_type, rule=rule, rule_type=rule_type, ruleset_num=ruleset_num, rule_num=rule_num, n=n: self.change_rule_value(ruleset_num, rule_num, rule, rule_type, variable_type, n, event.new_value)
self.rule_var_changers[key] = slider
else:
dropdown = rule_box.add(arcade.gui.UIDropdown(default=default_values[VAR_NAMES[n]], options=VAR_OPTIONS[variable_type], active_style=dropdown_style, primary_style=dropdown_style, dropdown_style=dropdown_style, width=self.window.width * 0.225, height=self.window.height / 30))
dropdown.on_change = lambda event, variable_type=variable_type, rule=rule, rule_type=rule_type, ruleset_num=ruleset_num, rule_num=rule_num, n=n: self.change_rule_value(ruleset_num, rule_num, rule, rule_type, variable_type, n, event.new_value)
self.rule_var_changers[key] = dropdown
def change_rule_type(self, ruleset_num, rule_num, rule_type, new_rule_text):
defaults = self.get_rule_defaults(rule_type)
new_rule_name = next(key for key, default_list in defaults.items() if default_list[0] == new_rule_text)
ruleset = self.rulesets[ruleset_num]
if len(ruleset) == 2:
if rule_type == "if":
ruleset[0] = new_rule_name
else:
ruleset[1] = new_rule_name
else:
if rule_type == "if":
if rule_num == 1:
ruleset[0] = new_rule_name
else:
ruleset[2] = new_rule_name
else:
ruleset[3] = new_rule_name
self.rebuild_ruleset_ui(ruleset_num)
def rebuild_ruleset_ui(self, ruleset_num):
rule_box = self.rule_boxes[ruleset_num]
keys_to_remove = [k for k in self.rule_labels.keys() if k.startswith(f"{ruleset_num}_")]
for key in keys_to_remove:
del self.rule_labels[key]
keys_to_remove = [k for k in self.rule_var_changers.keys() if k.startswith(f"{ruleset_num}_")]
for key in keys_to_remove:
del self.rule_var_changers[key]
keys_to_remove = [k for k in self.rule_values.keys() if k.startswith(f"{ruleset_num}_")]
for key in keys_to_remove:
del self.rule_values[key]
rule_box.clear()
ruleset = self.rulesets[ruleset_num]
old_ruleset_num = self.current_ruleset_num
self.current_ruleset_num = ruleset_num
if len(ruleset) == 2:
self.create_rule_ui(rule_box, ruleset[0], "if")
self.create_rule_ui(rule_box, ruleset[1], "do", 2)
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", 2)
self.create_rule_ui(rule_box, ruleset[3], "do", 3)
self.current_ruleset_num = old_ruleset_num
def add_ruleset(self, ruleset):
rule_box = arcade.gui.UIBoxLayout(space_between=5, align="left").with_background(color=arcade.color.DARK_SLATE_GRAY)
self.rule_boxes[self.current_ruleset_num] = rule_box
if len(ruleset) == 2:
self.rulesets[self.current_ruleset_num] = ruleset
self.create_rule_ui(rule_box, ruleset[0], "if")
self.create_rule_ui(rule_box, ruleset[1], "do", 2)
else:
self.rulesets[self.current_ruleset_num] = ruleset
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", 2)
self.create_rule_ui(rule_box, ruleset[3], "do", 3)
def refresh_rules_display(self):
self.rules_content_box.clear()
sorted_keys = sorted(self.rule_boxes.keys())
start_idx = self.current_ruleset_page * self.rulesets_per_page
end_idx = start_idx + self.rulesets_per_page
visible_keys = sorted_keys[start_idx:end_idx]
for key in visible_keys:
self.rules_content_box.add(self.rule_boxes[key])
self.rules_content_box.add(arcade.gui.UISpace(height=self.window.height / 50))
def next_page(self, event):
sorted_keys = sorted(self.rule_boxes.keys())
max_page = (len(sorted_keys) - 1) // self.rulesets_per_page
if self.current_ruleset_page < max_page:
self.current_ruleset_page += 1
self.refresh_rules_display()
def prev_page(self, event):
if self.current_ruleset_page > 0:
self.current_ruleset_page -= 1
self.refresh_rules_display()
arcade.schedule(check_selection, 0.1)
def on_show_view(self):
super().on_show_view()
self.rules_box.add(arcade.gui.UILabel(text="Rules", font_size=20, text_color=arcade.color.BLACK))
self.rules_box.add(arcade.gui.UISpace(height=self.window.height / 70, width=self.window.width * 0.25))
self.sprites_ui.add(arcade.gui.UILabel(text="Sprites", font_size=24, text_color=arcade.color.WHITE), anchor_x="center", anchor_y="top")
add_simple_rule_button = self.rules_box.add(arcade.gui.UIFlatButton(text="Add Simple rule", width=self.window.width * 0.225, height=self.window.height / 25, style=dropdown_style))
add_simple_rule_button.on_click = lambda event: self.add_rule("simple")
self.sprites_grid = self.sprites_ui.add(arcade.gui.UIGridLayout(columns=8, row_count=8, align="left", vertical_spacing=10, horizontal_spacing=10, size_hint=(0.95, 0.85), width=self.window.width * 0.95, height=self.window.height * 0.85), anchor_x="center", anchor_y="center")
self.rules_box.add(arcade.gui.UISpace(height=self.window.height / 85))
for n, shape in enumerate(SPRITES):
row, col = n % 8, n // 8
box = self.sprites_grid.add(arcade.gui.UIBoxLayout(), row=row, column=col)
box.add(arcade.gui.UILabel(text=shape, font_size=16, text_color=arcade.color.WHITE))
box.add(arcade.gui.UIImage(texture=SPRITE_TEXTURES[shape], width=self.window.width / 15, height=self.window.width / 15))
add_advanced_rule_button = self.rules_box.add(arcade.gui.UIFlatButton(text="Add Advanced rule", width=self.window.width * 0.225, height=self.window.height / 25, style=dropdown_style))
add_advanced_rule_button.on_click = lambda event: self.add_rule("advanced")
add_sprite_button = self.sprites_ui.add(arcade.gui.UITextureButton(text="Add Sprite", width=self.window.width / 2, height=self.window.height / 10, texture=button_texture, texture_hovered=button_hovered_texture, style=button_style), anchor_x="center", anchor_y="bottom", align_y=10)
add_sprite_button.on_click = lambda event: self.add_sprite()
self.rules_box.add(arcade.gui.UISpace(height=self.window.height / 70))
self.triggered_events.append(["start", {}])
self.nav_buttons_box = self.rules_box.add(arcade.gui.UIBoxLayout(vertical=False, space_between=10))
prev_button = self.nav_buttons_box.add(arcade.gui.UIFlatButton(text="Previous", width=self.window.width * 0.1, height=self.window.height / 25, style=dropdown_style))
prev_button.on_click = self.prev_page
next_button = self.nav_buttons_box.add(arcade.gui.UIFlatButton(text="Next", width=self.window.width * 0.1, height=self.window.height / 25, style=dropdown_style))
next_button.on_click = self.next_page
self.rules_box.add(arcade.gui.UISpace(height=self.window.height / 70))
self.rules_content_box = self.rules_box.add(arcade.gui.UIBoxLayout(align="center"))
self.add_rule(None, ["on_left_click", "spawn"])
self.refresh_rules_display()
self.sprites_box.add(arcade.gui.UILabel(text="Sprites", font_size=24, text_color=arcade.color.BLACK))
self.sprites_box.add(arcade.gui.UISpace(height=self.window.height / 50))
for shape in SHAPES:
self.sprites_box.add(arcade.gui.UILabel(text=shape, font_size=16, text_color=arcade.color.BLACK))
self.sprites_box.add(arcade.gui.UIImage(texture=SPRITE_TEXTURES[shape], width=self.window.width / 15, height=self.window.width / 15))
self.triggered_events.append(["game_launch", {}])
def add_rule(self, ruleset_type=None, force=None):
self.rulesets[self.current_ruleset_num] = generate_ruleset(ruleset_type) if not force else force
self.add_ruleset(self.rulesets[self.current_ruleset_num])
self.current_ruleset_num += 1
if self.rules_content_box:
self.refresh_rules_display()
def get_rule_values(self, ruleset_num, rule_num, rule_dict, event_args):
args = [self.rule_values[f"{ruleset_num}_{rule_num}_{user_var}_{n}"] for n, user_var in enumerate(rule_dict["user_vars"])]
def get_vars(self, rule_dict, vars, event_args):
args = [vars[n].value for n in range(len(rule_dict["user_vars"]))]
return args + [event_args[var] for var in rule_dict.get("vars", []) if not var in rule_dict["user_vars"]]
def check_rule(self, ruleset_num, rule_num, rule_dict, event_args):
return rule_dict["func"](*self.get_rule_values(ruleset_num, rule_num, rule_dict, event_args))
def check_rule(self, rule_dict, vars, event_args):
return rule_dict["func"](*self.get_vars(rule_dict, vars, event_args))
def get_action_function(self, action_dict):
ACTION_FUNCTION_DICT = {
@@ -366,115 +246,244 @@ class Game(arcade.gui.UIView):
"change_y_velocity": self.change_y_velocity,
"change_color": self.change_color,
"change_size": self.change_size,
"destroy": self.destroy,
"morph": self.morph
"destroy": self.destroy
}
}
return ACTION_FUNCTION_DICT[action_dict["type"]][action_dict["name"]]
def run_do_rule(self, ruleset_num, rule_num, rule_dict, event_args):
self.get_action_function(rule_dict["action"])(*self.get_rule_values(ruleset_num, rule_num, rule_dict, event_args))
def run_do_rule(self, rule_dict, vars, event_args):
self.get_action_function(rule_dict["action"])(*self.get_vars(rule_dict, vars, event_args))
def recursive_execute_rule(self, rule, trigger_args):
for child_rule in rule.children:
child_rule_type = child_rule.rule_type
if child_rule_type == "for": # TODO: Extend this when i add more FOR loop types
if child_rule.rule == "every_shape":
for shape in self.shapes:
event_args = trigger_args.copy()
event_args.update({"event_shape_type": shape.shape_type, "shape_size": shape.shape_size, "shape_x": shape.x, "shape_y": shape.y, "shape": shape, "shape_color": shape.shape_color})
self.recursive_execute_rule(child_rule, event_args)
elif child_rule_type == "if":
if self.check_rule(IF_RULES[child_rule.rule], child_rule.vars, trigger_args):
self.recursive_execute_rule(child_rule, trigger_args)
elif child_rule_type == "do":
self.run_do_rule(DO_RULES[child_rule.rule], child_rule.vars, trigger_args)
def get_max_rule_num(self):
max_num = -1
def recurse(block: Block):
nonlocal max_num
max_num = max(max_num, block.rule_num)
for child in block.children:
recurse(child)
for block in self.rulesets.values():
recurse(block)
return max_num
def dict_to_block(self, block_dict):
kwargs = block_dict.copy()
kwargs["children"] = [self.dict_to_block(child) for child in block_dict.get("children", [])]
kwargs["vars"] = [VarBlock(**var) for var in block_dict.get("vars", [])]
return Block(**kwargs)
def on_update(self, delta_time):
if self.mode == "import" and self.import_file_manager.submitted_content:
with open(self.import_file_manager.submitted_content, "r") as file:
data = json.load(file)
self.import_file_manager.submitted_content = None
self.triggered_events = []
self.rulesets = {}
if not data:
self.add_widget(arcade.gui.UIMessageBox(message_text="Invalid file. Could not import rules.", width=self.window.width * 0.5, height=self.window.height * 0.25))
return
for rule_num, ruleset in data["rules"].items():
block = self.dict_to_block(ruleset)
self.rulesets[int(rule_num)] = block
self.sprite_types = data["sprites"]
for sprite_name, sprite_path in self.sprite_types.items():
if not sprite_name in SPRITE_TEXTURES:
SPRITE_TEXTURES[sprite_name] = arcade.load_texture(sprite_path)
SPRITES[sprite_name] = sprite_path
self.sprites_grid.clear()
for n, shape in enumerate(SPRITES):
row, col = n % 8, n // 8
box = self.sprites_grid.add(arcade.gui.UIBoxLayout(), row=row, column=col)
box.add(arcade.gui.UILabel(text=shape, font_size=16, text_color=arcade.color.WHITE))
box.add(arcade.gui.UIImage(texture=SPRITE_TEXTURES[shape], width=self.window.width / 15, height=self.window.width / 15))
self.rules_box.rulesets = self.rulesets
self.rules_box.block_renderer.blocks = self.rulesets
self.rules_box.current_rule_num = self.get_max_rule_num() + 1
self.rules_box.block_renderer.refresh()
self.rules()
if self.mode == "export" and self.export_file_manager.submitted_content:
with open(self.export_file_manager.submitted_content, "w") as file:
file.write(json.dumps(
{
"rules": {
rule_num: asdict(block) for rule_num, block in self.rulesets.items()
},
"sprites": self.sprite_types
},
indent=4))
self.export_file_manager.submitted_content = None
self.add_widget(arcade.gui.UIMessageBox(message_text="Rules and Sprites exported successfully!", width=self.window.width * 0.5, height=self.window.height * 0.25))
if not self.mode == "simulation":
return
self.triggered_events.append(["every_update", {}])
while len(self.triggered_events) > 0:
trigger, trigger_args = self.triggered_events.pop(0)
for key, ruleset in self.rulesets.items():
if len(ruleset) == 2:
if_rule_dict = IF_RULES[ruleset[0]]
do_rule_dict = DO_RULES[ruleset[1]]
if not if_rule_dict["trigger"] == trigger:
for rule_num, rule in self.rulesets.items():
if not rule.rule_type == "trigger" or not trigger == rule.rule:
continue
if do_rule_dict["action"]["type"] == "shape_action":
for shape in self.shapes:
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.color})
self.recursive_execute_rule(rule, trigger_args)
if self.check_rule(key, 1, if_rule_dict, event_args):
self.run_do_rule(key, 2, do_rule_dict, event_args)
else:
event_args = trigger_args.copy()
if self.check_rule(key, 1, if_rule_dict, event_args):
self.run_do_rule(key, 2, do_rule_dict, event_args)
else:
if_rule_dicts = IF_RULES[ruleset[0]], IF_RULES[ruleset[2]]
do_rule_dict = DO_RULES[ruleset[3]]
if not (if_rule_dicts[0]["trigger"] == trigger and if_rule_dicts[0]["trigger"] == trigger):
continue
if do_rule_dict["action"]["type"] == "shape_action":
for shape in self.shapes:
event_args = trigger_args
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.color})
if ruleset[1] == "and":
if self.check_rule(key, 1, if_rule_dicts[0], event_args) and self.check_rule(key, 2, if_rule_dicts[1], event_args):
self.run_do_rule(key, 3, do_rule_dict, event_args)
elif ruleset[1] == "or":
if self.check_rule(key, 1, if_rule_dicts[0], event_args) or self.check_rule(key, 2, if_rule_dicts[1], event_args):
self.run_do_rule(key, 3, do_rule_dict, event_args)
else:
event_args = trigger_args
if ruleset[1] == "and":
if self.check_rule(key, 1, if_rule_dicts[0], event_args) and self.check_rule(key, 2, if_rule_dicts[1], event_args):
self.run_do_rule(key, 3, do_rule_dict, event_args)
elif ruleset[1] == "or":
if self.check_rule(key, 1, if_rule_dicts[0], event_args) or self.check_rule(key, 2, if_rule_dicts[1], event_args):
self.run_do_rule(key, 3, do_rule_dict, event_args)
has_collision_rules = any(
rule.rule_type == "trigger" and rule.rule == "collision"
for rule in self.rulesets.values()
)
for shape in self.shapes:
shape.update(self.x_gravity, self.y_gravity)
if has_collision_rules:
for i, shape in enumerate(self.shapes):
for shape_b in self.shapes[i+1:]:
if shape.check_collision(shape_b):
self.triggered_events.append(["collision", {
"event_a_type": shape.shape_type,
"event_b_type": shape_b.shape_type,
"shape_size": shape.shape_size,
"shape_x": shape.x,
"shape_y": shape.y,
"shape": shape,
"shape_color": shape.shape_color
}])
for shape in self.shapes[:]:
if shape.x < 0 or shape.x > self.window.width or shape.y < 0 or shape.y > self.window.height:
self.destroy(shape)
def change_rule_value(self, ruleset_num, rule_num, rule, rule_type, variable_type, n, value):
rule_dict = IF_RULES[rule] if rule_type == "if" else DO_RULES[rule]
key = f"{ruleset_num}_{rule_num}_{variable_type}_{n}"
self.rule_values[key] = value
values = {}
for i, variable in enumerate(rule_dict["user_vars"]):
lookup_key = f"{ruleset_num}_{rule_num}_{variable}_{i}"
values[VAR_NAMES[i]] = self.rule_values.get(lookup_key, VAR_DEFAULT[variable])
description = rule_dict["description"].format_map(values)
self.rule_labels[f"{ruleset_num}_{rule_num}_desc"].text = description
self.rule_labels[key].text = f'{VAR_NAMES[n]}: {value}'
if len(self.shapes) > self.settings.get("max_shapes", 120):
for shape in self.shapes[:-self.settings.get("max_shapes", 120)]:
self.destroy(shape)
def on_key_press(self, symbol, modifiers):
if symbol == arcade.key.ESCAPE:
self.main_exit()
elif symbol in [ord(key) for key in ALLOWED_INPUT]:
elif self.mode == "simulation" and symbol in [ord(key) if len(key) == 1 else getattr(arcade.key, key.upper()) for key in ALLOWED_INPUT]:
self.triggered_events.append(["on_input", {"event_key": chr(symbol)}])
def on_mouse_press(self, x, y, button, modifiers):
if not self.mode == "simulation":
return
if button == arcade.MOUSE_BUTTON_LEFT:
self.triggered_events.append(["on_left_click", {}])
elif button == arcade.MOUSE_BUTTON_RIGHT:
elif self.mode == "simulation" and button == arcade.MOUSE_BUTTON_RIGHT:
self.triggered_events.append(["on_right_click", {}])
def on_mouse_motion(self, x, y, button, modifiers):
if not self.mode == "simulation":
return
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 == "import":
self.anchor.remove(self.import_file_manager)
elif self.mode == "export":
self.anchor.remove(self.export_file_manager)
elif self.mode == "rules":
self.anchor.remove(self.rules_box)
elif self.mode == "sprites":
self.anchor.remove(self.sprites_ui)
elif self.mode == "sprite_add":
self.anchor.remove(self.sprite_add_ui)
self.anchor.trigger_full_render()
def rules(self):
self.disable_previous()
self.mode = "rules"
self.anchor.add(self.rules_box, anchor_x="center", anchor_y="top")
def export_file(self):
self.disable_previous()
self.mode = "export"
self.anchor.add(self.export_file_manager, anchor_x="center", anchor_y="top", align_y=-self.window.height * 0.025)
def import_file(self):
self.disable_previous()
self.mode = "import"
self.anchor.add(self.import_file_manager, anchor_x="center", anchor_y="top", align_y=-self.window.height * 0.025)
def sprites(self):
self.disable_previous()
self.mode = "sprites"
self.anchor.add(self.sprites_ui, anchor_x="center", anchor_y="top")
def simulation(self):
self.disable_previous()
self.x_gravity = self.settings.get("default_x_gravity", 0)
self.y_gravity = self.settings.get("default_y_gravity", 5)
self.triggered_events = []
self.rulesets = self.rules_box.rulesets
self.mode = "simulation"
def main_exit(self):
from menus.main import Main
self.window.show_view(Main(self.pypresence_client))
def on_draw(self):
self.window.clear()
if self.mode == "simulation":
self.shape_batch.draw()
elif self.mode == "rules":
with self.rules_box.camera.activate():
self.rules_box.draw()
self.rules_box.draw_unproject()
self.ui.draw()

View File

@@ -1,49 +1,578 @@
from utils.constants import DO_RULES, IF_RULES, LOGICAL_OPERATORS, NON_COMPATIBLE_WHEN, NON_COMPATIBLE_DO_WHEN
from utils.constants import (
DO_RULES,
IF_RULES,
TRIGGER_RULES,
FOR_RULES,
NEEDS_SHAPE,
PROVIDES_SHAPE,
button_style,
slider_style,
dropdown_style,
DO_COLOR,
IF_COLOR,
FOR_COLOR,
TRIGGER_COLOR,
RULE_DEFAULTS,
VAR_TYPES
)
from typing import List
from utils.preload import button_texture, button_hovered_texture, trash_bin
from arcade.gui.experimental.scroll_area import UIScrollArea, UIScrollBar
from dataclasses import dataclass, field
import arcade, arcade.gui, pyglet, random, re
import random
def get_rule_dict(rule_type):
if rule_type == "if":
return IF_RULES
elif rule_type == "for":
return FOR_RULES
elif rule_type == "trigger":
return TRIGGER_RULES
elif rule_type == "do":
return DO_RULES
IF_KEYS = tuple(IF_RULES.keys())
DO_KEYS = tuple(DO_RULES.keys())
@dataclass
class VarBlock:
x: float
y: float
label: str
var_type: str
connected_rule_num: str
value: str | int
BAD_WHEN = {tuple(sorted(pair)) for pair in NON_COMPATIBLE_WHEN}
BAD_DO_WHEN = {tuple(pair) for pair in NON_COMPATIBLE_DO_WHEN}
@dataclass
class Block:
x: float
y: float
label: str
rule_type: str
rule: str
rule_num: int
vars: List["VarBlock"] = field(default_factory=list)
children: List["Block"] = field(default_factory=list)
def generate_ruleset(ruleset_type):
when_a = random.choice(IF_KEYS)
class BlockRenderer:
def __init__(self, blocks: List[Block], indent: int = 12):
self.blocks = blocks
self.indent = indent
self.shapes = pyglet.graphics.Batch()
self.shapes_by_rule_num = {}
self.text_objects = []
self.text_by_rule_num = {}
self.var_widgets = {}
self.refresh()
if ruleset_type == "advanced":
valid_b = [
b for b in IF_KEYS
if b != when_a and tuple(sorted((when_a, b))) not in BAD_WHEN
]
def refresh(self):
for shapes_list in self.shapes_by_rule_num.values():
for shape in shapes_list:
shape.delete()
if not valid_b:
return [when_a, random.choice(DO_KEYS)]
for text_list in self.text_by_rule_num.values():
for text in text_list:
text.delete()
when_b = random.choice(valid_b)
logical = random.choice(LOGICAL_OPERATORS)
self.shapes = pyglet.graphics.Batch()
self.shapes_by_rule_num = {}
self.text_objects = []
self.text_by_rule_num = {}
self.var_widgets = {}
for b in self.blocks.values():
self._build_block(b, b.x, b.y)
def _build_var_ui(self, var: VarBlock, x: int, y: int, rule_num: int) -> tuple:
var_width = max(60, len(str(var.value)) * 8 + 20)
var_height = 24
var_color = (255, 255, 255)
var_rect = pyglet.shapes.BorderedRectangle(
x, y - var_height // 2, var_width, var_height,
2, var_color, arcade.color.BLACK, batch=self.shapes
)
var_text = pyglet.text.Label(
text=str(var.value),
x=x + var_width // 2,
y=y,
color=arcade.color.BLACK,
font_size=10,
anchor_x='center',
anchor_y='center'
)
if rule_num not in self.shapes_by_rule_num:
self.shapes_by_rule_num[rule_num] = []
if rule_num not in self.text_by_rule_num:
self.text_by_rule_num[rule_num] = []
if rule_num not in self.var_widgets:
self.var_widgets[rule_num] = []
self.shapes_by_rule_num[rule_num].append(var_rect)
self.text_by_rule_num[rule_num].append(var_text)
self.text_objects.append(var_text)
self.var_widgets[rule_num].append({
'var': var,
'rect': var_rect,
'text': var_text,
'x': x,
'y': y,
'width': var_width,
'height': var_height
})
return var_width, var_height
def _build_block_with_vars(self, b: Block, x: int, y: int) -> None:
lx, ly = x, y - 42
current_x = lx + 14
current_y = ly + 28
pattern = r'(?:^| )([a-z])(?= |$)'
parts = re.split(pattern, b.label)
var_index = 0
for i, part in enumerate(parts):
if i % 2 == 0:
if part:
text_obj = pyglet.text.Label(
text=part,
x=current_x,
y=current_y - 3,
color=arcade.color.BLACK,
font_size=12,
weight="bold"
)
self.text_objects.append(text_obj)
self.text_by_rule_num[b.rule_num].append(text_obj)
current_x += len(part) * 12
else:
when_b = None
logical = None
if var_index < len(b.vars):
var = b.vars[var_index]
var_width, var_height = self._build_var_ui(
var, current_x, current_y, b.rule_num
)
current_x += var_width + 10
var_index += 1
if when_b:
valid_do = [
d for d in DO_KEYS
if (when_a, d) not in BAD_DO_WHEN
and (when_b, d) not in BAD_DO_WHEN
and (d, when_a) not in BAD_DO_WHEN
and (d, when_b) not in BAD_DO_WHEN
]
def _build_block(self, b: Block, x: int, y: int) -> int:
is_wrap = b.rule_type != "do"
h, w = 42, 380
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
lx, ly = x, y - h
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)
if b.vars:
self._build_block_with_vars(b, x, y)
else:
valid_do = [
d for d in DO_KEYS
if (when_a, d) not in BAD_DO_WHEN
and (d, when_a) not in BAD_DO_WHEN
]
text_obj = pyglet.text.Label(
text=b.label,
x=lx + 7,
y=ly + 20,
color=arcade.color.BLACK,
font_size=12,
weight="bold"
)
self.text_objects.append(text_obj)
self.text_by_rule_num[b.rule_num].append(text_obj)
do = random.choice(valid_do)
next_y = ly
if is_wrap:
iy = next_y
for child in b.children:
child.x = lx + self.indent + 5
child.y = iy
iy = self._build_block(child, lx + self.indent + 5, iy)
if logical:
return [when_a, logical, when_b, do]
bar_h = next_y - iy
bar_filled = pyglet.shapes.Rectangle(lx + 2, iy + 2, self.indent, bar_h, color, batch=self.shapes)
line1 = pyglet.shapes.Line(lx, next_y, lx, iy, 2, arcade.color.BLACK, batch=self.shapes)
bottom = pyglet.shapes.BorderedRectangle(lx, iy - 8, w, 24, 2, color, arcade.color.BLACK, batch=self.shapes)
self.shapes_by_rule_num[b.rule_num].extend([bar_filled, line1, bottom])
return iy - 24
else:
return [when_a, do]
for child in b.children:
child.x = lx
child.y = next_y
ly = self._build_block(child, lx, next_y)
return ly - 16
def move_block(self, x, y, rule_num):
for element in self.shapes_by_rule_num[rule_num] + self.text_by_rule_num[rule_num]:
element.x += x
element.y += y
if rule_num in self.var_widgets:
for widget in self.var_widgets[rule_num]:
widget['x'] += x
widget['y'] += y
block = self._find_block(rule_num)
for child in block.children:
self.move_block(x, y, child.rule_num)
def get_var_at_position(self, x, y):
for rule_num, widgets in self.var_widgets.items():
for widget in widgets:
wx, wy = widget['x'], widget['y']
ww, wh = widget['width'], widget['height']
if (wx <= x <= wx + ww and
wy - wh // 2 <= y <= wy + wh // 2):
return widget['var'], rule_num
return None, None
def _find_block(self, rule_num):
if rule_num in self.blocks:
return self.blocks[rule_num]
for block in self.blocks.values():
found = self._find_block_recursive(block, rule_num)
if found:
return found
return None
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
def draw(self):
self.shapes.draw()
for t in self.text_objects:
t.draw()
class VarEditDialog(arcade.gui.UIAnchorLayout):
def __init__(self, var: VarBlock, on_save, on_cancel):
super().__init__()
self.var = var
self.on_save_callback = on_save
self.on_cancel_callback = on_cancel
self.background = self.add(
arcade.gui.UISpace(color=(0, 0, 0, 180)),
anchor_x="center",
anchor_y="center"
)
dialog_box = arcade.gui.UIBoxLayout(
space_between=10,
width=300,
height=200
)
dialog_box.with_padding(all=20)
dialog_box.with_background(color=(60, 60, 80))
dialog_box.add(arcade.gui.UILabel(
text=f"Edit {var.label}",
font_size=16,
text_color=arcade.color.WHITE
))
if var.var_type == "variable":
self.input_field = arcade.gui.UIInputText(
text=str(var.value),
width=260,
height=40
)
dialog_box.add(self.input_field)
elif var.var_type in ["shape_type", "target_type", "color", "key_input", "comparison"]:
from utils.constants import VAR_OPTIONS
options = VAR_OPTIONS[var.var_type]
self.dropdown = arcade.gui.UIDropdown(
default=str(var.value),
options=options,
width=260,
height=40,
style=dropdown_style
)
dialog_box.add(self.dropdown)
elif var.var_type == "size":
self.slider = arcade.gui.UISlider(
value=int(var.value),
min_value=1,
max_value=200,
width=260,
height=40,
style=slider_style
)
dialog_box.add(self.slider)
button_layout = arcade.gui.UIBoxLayout(vertical=False, space_between=10)
save_btn = arcade.gui.UIFlatButton(
text="Save",
width=125,
height=40
)
save_btn.on_click = self._on_save
cancel_btn = arcade.gui.UIFlatButton(
text="Cancel",
width=125,
height=40
)
cancel_btn.on_click = self._on_cancel
button_layout.add(save_btn)
button_layout.add(cancel_btn)
dialog_box.add(button_layout)
self.add(dialog_box, anchor_x="center", anchor_y="center")
def _on_save(self, event):
if hasattr(self, 'input_field'):
try:
self.var.value = int(self.input_field.text)
except ValueError:
self.var.value = self.input_field.text
elif hasattr(self, 'dropdown'):
self.var.value = self.dropdown.value
elif hasattr(self, 'slider'):
self.var.value = int(self.slider.value)
self.on_save_callback()
def _on_cancel(self, event):
self.on_cancel_callback()
class RuleUI(arcade.gui.UIAnchorLayout):
def __init__(self, window: arcade.Window):
super().__init__(size_hint=(1, 0.875))
self.window = window
self.current_rule_num = 0
self.rule_values = {}
self.var_edit_dialog = None
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(
text="Rules", font_size=20, text_color=arcade.color.WHITE
),
anchor_x="center",
anchor_y="top"
)
self.add(
arcade.gui.UISpace(
height=self.window.height / 70, width=self.window.width * 0.25
)
)
self.create_sidebar = self.add(arcade.gui.UIBoxLayout(size_hint=(0.15, 1), vertical=False, space_between=5), anchor_x="left", anchor_y="bottom")
self.scroll_area = UIScrollArea(size_hint=(0.95, 1)) # center on screen
self.scroll_area.scroll_speed = 0
self.create_sidebar.add(self.scroll_area)
self.scrollbar = UIScrollBar(self.scroll_area)
self.scrollbar.size_hint = (0.075, 1)
self.create_sidebar.add(self.scrollbar)
self.create_box = self.scroll_area.add(arcade.gui.UIBoxLayout(space_between=10))
self.add_rule_create_box("trigger")
self.add_rule_create_box("if")
self.add_rule_create_box("do")
self.add_rule_create_box("for")
self.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 add_rule_create_box(self, rule_type):
self.create_box.add(arcade.gui.UISpace(height=self.window.height / 100))
self.create_box.add(arcade.gui.UILabel(text=f"{rule_type.capitalize()} Rules", font_size=18))
self.create_box.add(arcade.gui.UISpace(height=self.window.height / 200))
for rule in get_rule_dict(rule_type):
create_button = self.create_box.add(arcade.gui.UITextureButton(text=RULE_DEFAULTS[rule_type][rule][0], width=self.window.width * 0.135, multiline=True, height=self.window.height * 0.05, style=button_style, texture=button_texture, texture_hovered=button_hovered_texture))
create_button.on_click = lambda event, rule=rule: self.add_rule(rule_type, rule)
def generate_pos(self):
return random.randint(
self.window.width * 0.1, int(self.window.width * 0.9)
), random.randint(self.window.height * 0.1, int(self.window.height * 0.7))
def add_rule(self, rule_type, rule):
rule_dict = get_rule_dict(rule_type)[rule]
rule_box = Block(
*self.generate_pos(),
RULE_DEFAULTS[rule_type][rule][0],
rule_type,
rule,
self.current_rule_num,
[
VarBlock(
*self.generate_pos(),
VAR_TYPES[var_type],
var_type,
self.current_rule_num,
RULE_DEFAULTS[rule_type][rule][1][n]
)
for n, var_type in enumerate(rule_dict["user_vars"])
],
[]
)
self.rulesets[self.current_rule_num] = rule_box
self.current_rule_num += 1
self.block_renderer.refresh()
return rule_box
def draw(self):
self.block_renderer.draw()
def draw_unproject(self):
self.trash_spritelist.draw()
def drag_n_drop_check(self, blocks):
if self.dragged_rule_ui.rule_type == "trigger":
return
for block in blocks:
if block == self.dragged_rule_ui or (self.dragged_rule_ui.rule in NEEDS_SHAPE and block.rule not in PROVIDES_SHAPE):
continue
if arcade.LBWH(block.x, block.y - 44, 380, 44).intersection(arcade.LBWH(self.dragged_rule_ui.x, self.dragged_rule_ui.y - 44, 380, 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:
self.rulesets[block_to_remove.rule_num] = block_to_remove
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, 380, 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):
if self.var_edit_dialog:
super().on_event(event)
return
super().on_event(event)
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):
projected_vec = self.camera.unproject((event.x, event.y))
var, _ = self.block_renderer.get_var_at_position(projected_vec.x, projected_vec.y)
if var:
self.open_var_edit_dialog(var)
return
self.press_check(event, list(self.rulesets.values()))
elif isinstance(event, arcade.gui.UIMouseReleaseEvent):
if self.dragged_rule_ui:
block_screen_pos = self.camera.project((self.dragged_rule_ui.x, self.dragged_rule_ui.y))
block_rect = arcade.LBWH(block_screen_pos[0], block_screen_pos[1], 380, 44)
trash_rect = arcade.LBWH(
self.trash_sprite.center_x - self.trash_sprite.width / 2,
self.trash_sprite.center_y - self.trash_sprite.height / 2,
self.trash_sprite.width,
self.trash_sprite.height
)
if block_rect.intersection(trash_rect):
self.remove_from_parent(self.dragged_rule_ui, list(self.rulesets.values()))
if self.dragged_rule_ui.rule_num in self.rulesets:
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 open_var_edit_dialog(self, var: VarBlock):
def on_save():
self.close_var_edit_dialog()
self.block_renderer.refresh()
def on_cancel():
self.close_var_edit_dialog()
self.var_edit_dialog = VarEditDialog(var, on_save, on_cancel)
self.add(self.var_edit_dialog)
def close_var_edit_dialog(self):
if self.var_edit_dialog:
self.remove(self.var_edit_dialog)
self.var_edit_dialog = None
self.trigger_full_render()
def on_update(self, dt):
if self.dragged_rule_ui:
block_screen_pos = self.camera.project((self.dragged_rule_ui.x, self.dragged_rule_ui.y))
if self.trash_sprite.rect.intersection(arcade.LBWH(block_screen_pos[0], block_screen_pos[1], 380, 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
self.trash_sprite.update_animation()

View File

@@ -1,12 +1,16 @@
import pyglet, arcade.color
import pyglet, arcade.color, math, json
from utils.constants import DEFAULT_X_VELOCITY, DEFAULT_Y_VELOCITY
# I am so sorry but this file has AI code cause i didn't have enough time to implement collision :C
with open("settings.json", "r") as file:
settings = json.load(file)
class BaseShape():
def __init__(self):
self.shape_type = ""
self.x_velocity = DEFAULT_X_VELOCITY
self.y_velocity = DEFAULT_Y_VELOCITY
self.x_velocity = settings.get("default_x_velocity", 0)
self.y_velocity = settings.get("default_y_velocity", 0)
self._shape_color = "WHITE"
def update(self, x_gravity, y_gravity):
@@ -24,6 +28,15 @@ class BaseShape():
self._shape_color = color
self.color = getattr(arcade.color, color)
def check_collision(self, other):
if isinstance(other, Circle):
return self._collides_with_circle(other)
elif isinstance(other, BaseRectangle):
return self._collides_with_rectangle(other)
elif isinstance(other, Triangle):
return self._collides_with_triangle(other)
return False
class Circle(pyglet.shapes.Circle, BaseShape):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -34,16 +47,153 @@ class Circle(pyglet.shapes.Circle, BaseShape):
def shape_size(self):
return self.radius
class Rectangle(pyglet.shapes.Rectangle, BaseShape):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
BaseShape.__init__(self)
self.shape_type = "rectangle"
def _collides_with_circle(self, other):
dx = self.x - other.x
dy = self.y - other.y
distance = math.sqrt(dx * dx + dy * dy)
return distance < (self.radius + other.radius)
def _collides_with_rectangle(self, rect):
closest_x = max(rect.x, min(self.x, rect.x + rect.width))
closest_y = max(rect.y, min(self.y, rect.y + rect.height))
dx = self.x - closest_x
dy = self.y - closest_y
distance = math.sqrt(dx * dx + dy * dy)
return distance < self.radius
def _collides_with_triangle(self, tri):
if self._point_in_triangle(self.x, self.y, tri):
return True
edges = [
(tri.x, tri.y, tri.x2, tri.y2),
(tri.x2, tri.y2, tri.x3, tri.y3),
(tri.x3, tri.y3, tri.x, tri.y)
]
for x1, y1, x2, y2 in edges:
if self._distance_to_segment(x1, y1, x2, y2) < self.radius:
return True
return False
def _distance_to_segment(self, x1, y1, x2, y2):
dx = x2 - x1
dy = y2 - y1
if dx == 0 and dy == 0:
return math.sqrt((self.x - x1)**2 + (self.y - y1)**2)
t = max(0, min(1, ((self.x - x1) * dx + (self.y - y1) * dy) / (dx * dx + dy * dy)))
closest_x = x1 + t * dx
closest_y = y1 + t * dy
return math.sqrt((self.x - closest_x)**2 + (self.y - closest_y)**2)
def _point_in_triangle(self, px, py, tri):
def sign(x1, y1, x2, y2, x3, y3):
return (x1 - x3) * (y2 - y3) - (x2 - x3) * (y1 - y3)
d1 = sign(px, py, tri.x, tri.y, tri.x2, tri.y2)
d2 = sign(px, py, tri.x2, tri.y2, tri.x3, tri.y3)
d3 = sign(px, py, tri.x3, tri.y3, tri.x, tri.y)
has_neg = (d1 < 0) or (d2 < 0) or (d3 < 0)
has_pos = (d1 > 0) or (d2 > 0) or (d3 > 0)
return not (has_neg and has_pos)
class BaseRectangle(BaseShape):
def __init__(self):
super().__init__()
@property
def shape_size(self):
return self.width
def _collides_with_circle(self, circle):
return circle._collides_with_rectangle(self)
def _collides_with_rectangle(self, other):
return (self.x < other.x + other.width and
self.x + self.width > other.x and
self.y < other.y + other.height and
self.y + self.height > other.y)
def _collides_with_triangle(self, tri):
vertices = [(tri.x, tri.y), (tri.x2, tri.y2), (tri.x3, tri.y3)]
for vx, vy in vertices:
if (self.x <= vx <= self.x + self.width and
self.y <= vy <= self.y + self.height):
return True
rect_vertices = [
(self.x, self.y),
(self.x + self.width, self.y),
(self.x + self.width, self.y + self.height),
(self.x, self.y + self.height)
]
for rx, ry in rect_vertices:
if self._point_in_triangle(rx, ry, tri):
return True
tri_edges = [
(tri.x, tri.y, tri.x2, tri.y2),
(tri.x2, tri.y2, tri.x3, tri.y3),
(tri.x3, tri.y3, tri.x, tri.y)
]
rect_edges = [
(self.x, self.y, self.x + self.width, self.y),
(self.x + self.width, self.y, self.x + self.width, self.y + self.height),
(self.x + self.width, self.y + self.height, self.x, self.y + self.height),
(self.x, self.y + self.height, self.x, self.y)
]
for t_edge in tri_edges:
for r_edge in rect_edges:
if self._segments_intersect(*t_edge, *r_edge):
return True
return False
def _point_in_triangle(self, px, py, tri):
def sign(x1, y1, x2, y2, x3, y3):
return (x1 - x3) * (y2 - y3) - (x2 - x3) * (y1 - y3)
d1 = sign(px, py, tri.x, tri.y, tri.x2, tri.y2)
d2 = sign(px, py, tri.x2, tri.y2, tri.x3, tri.y3)
d3 = sign(px, py, tri.x3, tri.y3, tri.x, tri.y)
has_neg = (d1 < 0) or (d2 < 0) or (d3 < 0)
has_pos = (d1 > 0) or (d2 > 0) or (d3 > 0)
return not (has_neg and has_pos)
def _segments_intersect(self, x1, y1, x2, y2, x3, y3, x4, y4):
def ccw(ax, ay, bx, by, cx, cy):
return (cy - ay) * (bx - ax) > (by - ay) * (cx - ax)
return (ccw(x1, y1, x3, y3, x4, y4) != ccw(x2, y2, x3, y3, x4, y4) and
ccw(x1, y1, x2, y2, x3, y3) != ccw(x1, y1, x2, y2, x4, y4))
class Rectangle(pyglet.shapes.Rectangle, BaseRectangle):
def __init__(self, *args, **kwargs):
BaseRectangle.__init__(self)
super().__init__(*args, **kwargs)
self.shape_type = "rectangle"
class TexturedRectangle(pyglet.sprite.Sprite, BaseRectangle):
def __init__(self, img, x=0, y=0, *args, **kwargs):
BaseRectangle.__init__(self)
self.shape_type = kwargs.pop("shape_type", "textured_rectangle")
super().__init__(img, x, y, *args, **kwargs)
def update(self, x_gravity, y_gravity):
BaseShape.update(self, x_gravity, y_gravity)
class Triangle(pyglet.shapes.Triangle, BaseShape):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -53,3 +203,59 @@ class Triangle(pyglet.shapes.Triangle, BaseShape):
@property
def shape_size(self):
return max(self.x, self.x2, self.x3) - min(self.x, self.x2, self.x3)
def _collides_with_circle(self, circle):
return circle._collides_with_triangle(self)
def _collides_with_rectangle(self, rect):
return rect._collides_with_triangle(self)
def _collides_with_triangle(self, other):
vertices_self = [(self.x, self.y), (self.x2, self.y2), (self.x3, self.y3)]
vertices_other = [(other.x, other.y), (other.x2, other.y2), (other.x3, other.y3)]
for vx, vy in vertices_self:
if self._point_in_triangle(vx, vy, other):
return True
for vx, vy in vertices_other:
if self._point_in_triangle(vx, vy, self):
return True
edges_self = [
(self.x, self.y, self.x2, self.y2),
(self.x2, self.y2, self.x3, self.y3),
(self.x3, self.y3, self.x, self.y)
]
edges_other = [
(other.x, other.y, other.x2, other.y2),
(other.x2, other.y2, other.x3, other.y3),
(other.x3, other.y3, other.x, other.y)
]
for e1 in edges_self:
for e2 in edges_other:
if self._segments_intersect(*e1, *e2):
return True
return False
def _point_in_triangle(self, px, py, tri):
def sign(x1, y1, x2, y2, x3, y3):
return (x1 - x3) * (y2 - y3) - (x2 - x3) * (y1 - y3)
d1 = sign(px, py, tri.x, tri.y, tri.x2, tri.y2)
d2 = sign(px, py, tri.x2, tri.y2, tri.x3, tri.y3)
d3 = sign(px, py, tri.x3, tri.y3, tri.x, tri.y)
has_neg = (d1 < 0) or (d2 < 0) or (d3 < 0)
has_pos = (d1 > 0) or (d2 > 0) or (d3 > 0)
return not (has_neg and has_pos)
def _segments_intersect(self, x1, y1, x2, y2, x3, y3, x4, y4):
def ccw(ax, ay, bx, by, cx, cy):
return (cy - ay) * (bx - ax) > (by - ay) * (cx - ax)
return (ccw(x1, y1, x3, y3, x4, y4) != ccw(x2, y2, x3, y3, x4, y4) and
ccw(x1, y1, x2, y2, x3, y3) != ccw(x1, y1, x2, y2, x4, y4))

View File

@@ -52,10 +52,10 @@ class Main(arcade.gui.UIView):
self.title_label = self.box.add(arcade.gui.UILabel(text="Chaos Protocol", font_name="Roboto", font_size=48))
self.play_button = self.box.add(arcade.gui.UITextureButton(text="Play", texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 2, height=150, style=big_button_style))
self.play_button = self.box.add(arcade.gui.UITextureButton(text="Play", texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 2, height=self.window.height / 10, style=big_button_style))
self.play_button.on_click = lambda event: self.play()
self.settings_button = self.box.add(arcade.gui.UITextureButton(text="Settings", texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 2, height=150, style=big_button_style))
self.settings_button = self.box.add(arcade.gui.UITextureButton(text="Settings", texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 2, height=self.window.height / 10, style=big_button_style))
self.settings_button.on_click = lambda event: self.settings()
def play(self):

View File

@@ -1,12 +1,12 @@
# This file was autogenerated by uv via the following command:
# uv pip compile pyproject.toml -o requirements.txt
arcade==3.2.0
# via game-name (pyproject.toml)
arcade==3.3.3
# via chaos-protocol (pyproject.toml)
attrs==25.3.0
# via pytiled-parser
cffi==1.17.1
# via pymunk
pillow==11.0.0
pillow==11.3.0
# via arcade
pycparser==2.22
# via cffi
@@ -15,7 +15,7 @@ pyglet==2.1.6
pymunk==6.9.0
# via arcade
pypresence==4.3.0
# via game-name (pyproject.toml)
# via chaos-protocol (pyproject.toml)
pytiled-parser==2.2.9
# via arcade
typing-extensions==4.14.1

7
run.py
View File

@@ -10,11 +10,10 @@ script_dir = os.path.dirname(os.path.abspath(__file__))
pyglet.resource.path.append(script_dir)
pyglet.font.add_directory(os.path.join(script_dir, 'assets', 'fonts'))
from utils.utils import get_closest_resolution, print_debug_info, on_exception
from utils.constants import log_dir, menu_background_color
from menus.main import Main
# from utils.preload import theme_sound # needed for preload
from utils.preload import theme_sound # needed for preload
from arcade.experimental.controller_window import ControllerWindow
sys.excepthook = on_exception
@@ -87,8 +86,8 @@ else:
with open("settings.json", "w") as file:
file.write(json.dumps(settings))
# if settings.get("music", True):
# theme_sound.play(volume=settings.get("music_volume", 50) / 100, loop=True)
if settings.get("music", True):
theme_sound.play(volume=settings.get("music_volume", 50) / 100, loop=True)
try:
window = ControllerWindow(width=resolution[0], height=resolution[1], title='Chaos Protocol', samples=antialiasing, antialiasing=antialiasing > 0, fullscreen=fullscreen, vsync=vsync, resizable=False, style=style, visible=False)

View File

@@ -1,19 +1,26 @@
import arcade.color
import os
import arcade.color, operator
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"]
# Get the directory where this module is located
_module_dir = os.path.dirname(os.path.abspath(__file__))
_assets_dir = os.path.join(os.path.dirname(_module_dir), 'assets')
SPRITES = {
os.path.splitext(file_name)[0]: os.path.join(_assets_dir, 'graphics', 'sprites', file_name)
for file_name in os.listdir(os.path.join(_assets_dir, 'graphics', 'sprites'))
}
VAR_NAMES = ["a", "b", "c", "d", "e", "f", "g"]
DEFAULT_X_GRAVITY = 0
DEFAULT_Y_GRAVITY = 2
ALLOWED_INPUT = ["a", "b", "c", "d", "e", "q", "w", "s", "t", "space", "left", "right", "up", "down"]
DEFAULT_X_VELOCITY = 0
DEFAULT_Y_VELOCITY = 0
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",
@@ -31,9 +38,18 @@ COLORS = [
COMPARISONS = [">", ">=", "<", "<=", "==", "!="]
OPS = {
">": operator.gt,
"<": operator.lt,
">=": operator.ge,
"<=": operator.le,
"==": operator.eq,
"!=": operator.ne,
}
VAR_DEFAULT = {
"shape_type": SHAPES[0],
"target_type": SHAPES[1],
"shape_type": "rectangle",
"target_type": "circle",
"variable": 0,
"color": "WHITE",
"size": 10,
@@ -42,8 +58,8 @@ VAR_DEFAULT = {
}
VAR_OPTIONS = {
"shape_type": SHAPES,
"target_type": SHAPES,
"shape_type": SPRITES,
"target_type": SPRITES,
"variable": (-700, 700),
"color": COLORS,
"size": (1, 200),
@@ -51,38 +67,41 @@ VAR_OPTIONS = {
"comparison": COMPARISONS
}
IF_RULES = {
"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]}")
},
VAR_TYPES = {
"shape_type": "Shape Type",
"target_type": "Target Type",
"variable": "Variable",
"color": "Color",
"size": "Size",
"key_input": "Key Input",
"comparison": "Comparison"
}
"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]}")
TRIGGER_RULES = {
"every_update": {
"key": "every_update",
"user_vars": [],
"vars": [],
"description": "Every Update",
"func": lambda *v: True
},
"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]}")
"start": {
"key": "start",
"user_vars": [],
"vars": [],
"description": "On Game Start",
"func": lambda *v: True
},
"on_input": {
"key": "on_input",
"user_vars": ["key_input"],
"vars": ["key_input", "event_key"],
"description": "IF {a} key is pressed",
"func": lambda *v: v[0] == v[1]
},
"spawns": {
"key": "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]
@@ -90,39 +109,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]
},
"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]
},
"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]
},
"x_gravity_changes": {
"key": "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": {
"key": "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]
@@ -130,7 +116,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]
@@ -138,23 +123,13 @@ 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]
},
"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])
},
"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])
@@ -162,7 +137,6 @@ IF_RULES = {
"on_left_click": {
"key": "on_left_click",
"description": "IF you left click",
"trigger": "on_left_click",
"user_vars": [],
"vars": [],
"func": lambda *v: True
@@ -170,7 +144,6 @@ IF_RULES = {
"on_right_click": {
"key": "on_right_click",
"description": "IF you right click",
"trigger": "on_right_click",
"user_vars": [],
"vars": [],
"func": lambda *v: True
@@ -178,139 +151,72 @@ IF_RULES = {
"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",
}
FOR_RULES = {
"every_shape": {
"key": "every_shape",
"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
"description": "For every shape",
}
}
NON_COMPATIBLE_WHEN = [
("spawns", "destroyed"),
("spawns", "morphs"),
("spawns", "collides"),
("spawns", "x_velocity_changes"),
("spawns", "y_velocity_changes"),
("spawns", "x_gravity_changes"),
("spawns", "y_gravity_changes"),
("spawns", "color_changes"),
("spawns", "size_changes"),
("destroyed", "morphs"),
("destroyed", "collides"),
("destroyed", "x_velocity_changes"),
("destroyed", "y_velocity_changes"),
("destroyed", "x_gravity_changes"),
("destroyed", "y_gravity_changes"),
("destroyed", "color_changes"),
("destroyed", "size_changes"),
("morphs", "collides"),
("morphs", "x_velocity_changes"),
("morphs", "y_velocity_changes"),
("morphs", "x_gravity_changes"),
("morphs", "y_gravity_changes"),
("morphs", "color_changes"),
("morphs", "size_changes"),
("collides", "destroyed"),
("collides", "morphs"),
("every_update", "spawns"),
("every_update", "destroyed"),
("every_update", "morphs"),
("every_update", "collides"),
("every_update", "x_velocity_changes"),
("every_update", "y_velocity_changes"),
("every_update", "x_gravity_changes"),
("every_update", "y_gravity_changes"),
("every_update", "color_changes"),
("every_update", "size_changes"),
("every_update", "game_launch"),
("game_launch", "spawns"),
("game_launch", "destroyed"),
("game_launch", "morphs"),
("game_launch", "collides"),
("game_launch", "x_velocity_changes"),
("game_launch", "y_velocity_changes"),
("game_launch", "x_gravity_changes"),
("game_launch", "y_gravity_changes"),
("game_launch", "color_changes"),
("game_launch", "size_changes"),
]
NON_COMPATIBLE_DO_WHEN = [
("destroyed", "change_x"),
("destroyed", "change_y"),
("destroyed", "move_x"),
("destroyed", "move_y"),
("destroyed", "change_x_velocity"),
("destroyed", "change_y_velocity"),
("destroyed", "change_x_gravity"),
("destroyed", "change_y_gravity"),
("destroyed", "change_color"),
("destroyed", "change_size"),
("destroyed", "morph_into"),
("destroyed", "destroy"),
("morphs", "morph_into"),
("x_velocity_changes", "change_x_velocity"),
("y_velocity_changes", "change_y_velocity"),
("color_changes", "change_color"),
("size_changes", "change_size"),
("every_update", "change_x"),
("every_update", "change_y"),
("every_update", "move_x"),
("every_update", "move_y"),
("every_update", "change_x_velocity"),
("every_update", "change_y_velocity"),
("every_update", "change_color"),
("every_update", "change_size"),
("every_update", "destroy"),
("every_update", "morph_into"),
("game_launch", "change_x"),
("game_launch", "change_y"),
("game_launch", "move_x"),
("game_launch", "move_y"),
("game_launch", "change_x_velocity"),
("game_launch", "change_y_velocity"),
("game_launch", "change_x_gravity"),
("game_launch", "change_y_gravity"),
("game_launch", "change_color"),
("game_launch", "change_size"),
("game_launch", "destroy"),
("game_launch", "morph_into")
]
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: OPS[v[0]](v[2], v[1])
},
"y_position_compare": {
"key": "y_position_compare",
"description": "IF Y is {a} {b}",
"user_vars": ["comparison", "variable"],
"vars": ["comparison", "variable", "shape_y"],
"func": lambda *v: OPS[v[0]](v[2], v[1])
},
"size_compare": {
"key": "size_compare",
"description": "IF size is {a} {b}",
"user_vars": ["comparison", "variable"],
"vars": ["comparison", "variable", "shape_size"],
"func": lambda *v: OPS[v[0]](v[2], v[1])
},
"x_velocity_compare": {
"key": "x_velocity_compare",
"description": "IF X velocity is {a} {b}",
"user_vars": ["comparison", "variable"],
"vars": ["comparison", "variable", "shape_x_velocity"],
"func": lambda *v: OPS[v[0]](v[2], v[1])
},
"y_velocity_compare": {
"key": "y_velocity_compare",
"description": "IF Y velocity is {a} {b}",
"user_vars": ["comparison", "variable"],
"vars": ["comparison", "variable", "shape_y_velocity"],
"func": lambda *v: OPS[v[0]](v[2], 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]
},
"shape_type_is": {
"key": "shape_type_is",
"description": "IF shape type is {a}",
"user_vars": ["shape_type"],
"vars": ["shape_type", "event_shape_type"],
"func": lambda *v: v[0] == v[1]
},
}
DO_RULES = {
"change_x": {
@@ -385,14 +291,6 @@ DO_RULES = {
"vars": ["shape"]
},
"morph_into": {
"key": "morph_into",
"description": "Morph this into {a}",
"action": {"type": "shape_action", "name": "morph"},
"user_vars": ["shape_type"],
"vars": ["shape", "shape_type"]
},
"change_x_gravity": {
"key": "change_x_gravity",
"description": "Change X gravity to {a}",
@@ -418,6 +316,67 @@ DO_RULES = {
}
}
PROVIDES_SHAPE = [
# Trigger
"spawns",
"color_changes",
"size_changes",
"collides",
# IFs, technically, these need and provide a shape to the next rule
"x_position_compare",
"y_position_compare",
"size_compare",
"x_velocity_compare",
"y_velocity_compare",
"color_is",
"shape_type_is",
# FOR
"every_shape"
]
NEEDS_SHAPE = [
# IF
"x_position_compare",
"y_position_compare",
"size_compare",
"x_velocity_compare",
"y_velocity_compare",
"color_is",
"shape_type_is",
# DO
"change_x",
"change_y",
"move_x",
"move_y",
"change_x_velocity",
"change_y_velocity",
"change_size",
"destroy",
]
RULE_DEFAULTS = {
rule_type: {
rule_key: (
rule_dict["description"].format_map(
{
VAR_NAMES[n]: VAR_NAMES[n]
for n, variable in enumerate(rule_dict["user_vars"])
}
),
[
VAR_DEFAULT[variable]
for variable in rule_dict["user_vars"]
],
)
for rule_key, rule_dict in rule_var.items()
}
for rule_type, rule_var in [("if", IF_RULES), ("do", DO_RULES), ("trigger", TRIGGER_RULES), ("for", FOR_RULES)]
}
menu_background_color = (30, 30, 47)
log_dir = 'logs'
discord_presence_id = 1440807203094138940
@@ -438,16 +397,21 @@ slider_style = {'normal': slider_default_style, 'hover': slider_hover_style, 'pr
settings = {
"Graphics": {
"Window Mode": {"type": "option", "options": ["Windowed", "Fullscreen", "Borderless"], "config_key": "window_mode", "default": "Windowed"},
"Resolution": {"type": "option", "options": ["1366x768", "1440x900", "1600x900", "1920x1080", "2560x1440", "3840x2160"], "config_key": "resolution"},
"Resolution": {"type": "option", "options": ["1440x900", "1600x900", "1920x1080", "2560x1440", "3840x2160"], "config_key": "resolution"},
"Anti-Aliasing": {"type": "option", "options": ["None", "2x MSAA", "4x MSAA", "8x MSAA", "16x MSAA"], "config_key": "anti_aliasing", "default": "4x MSAA"},
"VSync": {"type": "bool", "config_key": "vsync", "default": True},
"FPS Limit": {"type": "slider", "min": 0, "max": 480, "config_key": "fps_limit", "default": 60},
},
"Sound": {
"Music": {"type": "bool", "config_key": "music", "default": True},
"SFX": {"type": "bool", "config_key": "sfx", "default": True},
"Music Volume": {"type": "slider", "min": 0, "max": 100, "config_key": "music_volume", "default": 50},
"SFX Volume": {"type": "slider", "min": 0, "max": 100, "config_key": "sfx_volume", "default": 50},
},
"Game": {
"Default X velocity": {"type": "slider", "min": -999, "max": 999, "config_key": "default_x_velocity", "default": 0},
"Default Y velocity": {"type": "slider", "min": -999, "max": 999, "config_key": "default_y_velocity", "default": 0},
"Default X gravity": {"type": "slider", "min": -999, "max": 999, "config_key": "default_x_gravity", "default": 0},
"Default Y gravity": {"type": "slider", "min": -999, "max": 999, "config_key": "default_y_gravity", "default": 5},
"Max Shapes": {"type": "slider", "min": 0, "max": 999, "config_key": "max_shapes", "default": 120},
},
"Miscellaneous": {
"Discord RPC": {"type": "bool", "config_key": "discord_rpc", "default": True},

View File

@@ -8,7 +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'))
trash_bin = arcade.load_animated_gif(os.path.join(_assets_dir, 'graphics', 'trash_bin.gif'))

View File

@@ -41,7 +41,7 @@ def on_exception(*exc_info):
logging.error(f"Unhandled exception:\n{''.join(traceback.format_exception(exc_info[1], limit=None))}")
def get_closest_resolution():
allowed_resolutions = [(1366, 768), (1440, 900), (1600,900), (1920,1080), (2560,1440), (3840,2160)]
allowed_resolutions = [(1440, 900), (1600,900), (1920,1080), (2560,1440), (3840,2160)]
screen_width, screen_height = arcade.get_screens()[0].width, arcade.get_screens()[0].height
if (screen_width, screen_height) in allowed_resolutions:
if not allowed_resolutions.index((screen_width, screen_height)) == 0: