Compare commits

...

11 Commits

Author SHA1 Message Date
a179b27544 Fix EXE name 2025-12-13 08:00:57 +01:00
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
10 changed files with 1024 additions and 617 deletions

View File

@@ -42,7 +42,7 @@ jobs:
include-data-dir: assets=assets
include-data-files: CREDITS=CREDITS
mode: onefile
output-file: FleetCommander
output-file: ChaosProtocol
- name: Locate and rename executable (Linux)
if: matrix.os == 'ubuntu-22.04'
@@ -51,29 +51,29 @@ jobs:
echo "Searching for built Linux binary..."
# List to help debugging when paths change
ls -laR . | head -n 500 || true
BIN=$(find . -maxdepth 4 -type f -name 'FleetCommander*' -perm -u+x | head -n1 || true)
BIN=$(find . -maxdepth 4 -type f -name 'ChaosProtocol*' -perm -u+x | head -n1 || true)
if [ -z "${BIN}" ]; then
echo "ERROR: No Linux binary found after build"
exit 1
fi
echo "Found: ${BIN}"
mkdir -p build_output
cp "${BIN}" build_output/FleetCommander.bin
chmod +x build_output/FleetCommander.bin
echo "Executable ready: build_output/FleetCommander.bin"
cp "${BIN}" build_output/ChaosProtocol.bin
chmod +x build_output/ChaosProtocol.bin
echo "Executable ready: build_output/ChaosProtocol.bin"
shell: bash
- name: Locate and rename executable (Windows)
if: matrix.os == 'windows-latest'
run: |
Write-Host "Searching for built Windows binary..."
Get-ChildItem -Recurse -File -Filter 'FleetCommander*.exe' | Select-Object -First 1 | ForEach-Object {
Get-ChildItem -Recurse -File -Filter 'ChaosProtocol*.exe' | Select-Object -First 1 | ForEach-Object {
Write-Host ("Found: " + $_.FullName)
New-Item -ItemType Directory -Force -Path build_output | Out-Null
Copy-Item $_.FullName "build_output\FleetCommander.exe"
Write-Host "Executable ready: build_output\FleetCommander.exe"
Copy-Item $_.FullName "build_output\ChaosProtocol.exe"
Write-Host "Executable ready: build_output\ChaosProtocol.exe"
}
if (!(Test-Path build_output\FleetCommander.exe)) {
if (!(Test-Path build_output\ChaosProtocol.exe)) {
Write-Error "ERROR: No Windows binary found after build"
exit 1
}
@@ -83,7 +83,7 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.platform }}
path: build_output/FleetCommander.*
path: build_output/ChaosProtocol.*
release:
runs-on: ubuntu-latest
@@ -114,5 +114,6 @@ jobs:
--notes "Automated build for $TAG"
fi
# Upload the executables directly (no zip files)
gh release upload "$TAG" downloads/linux/FleetCommander.bin --clobber
gh release upload "$TAG" downloads/windows/FleetCommander.exe --clobber
gh release upload "$TAG" downloads/linux/ChaosProtocol.bin --clobber
gh release upload "$TAG" downloads/windows/ChaosProtocol.exe --clobber

View File

@@ -1,3 +1,6 @@
Trash Can icon by Icons8
https://icons8.com/icon/rdRR1tq1xIo1/trash-can
The Roboto Black font used in this project is licensed under the Open Font License. Read assets/fonts/OFL.txt for more information.
Thanks to OpenGameArt and pixelsphere.org / The Cynic Project for the music! (https://opengameart.org/content/crystal-cave-mysterious-ambience-seamless-loop)

View File

@@ -1,6 +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.
[![Demo Video](https://img.youtube.com/vi/iMB4mmjTIB4/hqdefault.jpg)](https://youtu.be/iMB4mmjTIB4)
## 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

View File

@@ -6,10 +6,11 @@ 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, allowed_extensions):
super().__init__(size_hint=(0.95, 0.9), vertical=False)
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
@@ -20,11 +21,11 @@ class FileManager(arcade.gui.UIAnchorLayout):
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=24), anchor_x="center", anchor_y="top", align_y=-5)
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.05)
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)
@@ -33,21 +34,18 @@ class FileManager(arcade.gui.UIAnchorLayout):
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=10), anchor_x="center", anchor_y="bottom", align_y=10)
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=20))
self.filename_input = self.bottom_box.add(arcade.gui.UIInputText(width=self.filemanager_width * 0.35, height=self.filemanager_width * 0.025).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.2, height=self.filemanager_width * 0.05))
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.back_button = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='<--', style=button_style, width=100, height=50)
self.back_button.on_click = lambda event: self.exit()
self.add(self.back_button, anchor_x="left", anchor_y="top", align_x=5, align_y=-5)
self.show_directory()
def change_mode(self, mode):
@@ -57,9 +55,7 @@ class FileManager(arcade.gui.UIAnchorLayout):
self.submit_button.visible = self.mode == "export"
def submit(self, content):
self.submitted_content = content if self.mode == "import" else f"{content}/{self.filename_input.text}"
self.disable()
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:
@@ -112,22 +108,15 @@ class FileManager(arcade.gui.UIAnchorLayout):
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)))
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)))
if os.path.isdir(f"{self.current_directory}/{file}"):
self.file_buttons[-1].on_click = lambda event, directory=f"{self.current_directory}/{file}": self.change_directory(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=f"{self.current_directory}/{file}": self.submit(file)
def disable(self):
self.parent.parent.disable() # The FileManager UIManager. self.parent is the FileManager UIAnchorLayout
def exit(self):
self.disable()
self.submitted_content = "exit"
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

View File

@@ -1,10 +1,12 @@
import arcade, arcade.gui, pyglet, random, json
from utils.preload import SPRITE_TEXTURES, button_texture, button_hovered_texture
from utils.constants import button_style, dropdown_style, DO_RULES, IF_RULES, SHAPES, ALLOWED_INPUT, menu_background_color
from dataclasses import asdict
from game.rules import RuleUIBox
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):
@@ -19,9 +21,13 @@ class Game(arcade.gui.UIView):
self.anchor = self.add_widget(arcade.gui.UIAnchorLayout(size_hint=(1, 1)))
self.rules_box = RuleUIBox(self.window)
self.rules_box = RuleUI(self.window)
self.file_manager = FileManager(self.window.width * 0.95, [".json"]).with_border()
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.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.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())
@@ -36,9 +42,20 @@ class Game(arcade.gui.UIView):
self.triggered_events = []
self.rulesets = self.rules_box.rulesets
self.rule_values = self.rules_box.rule_values
self.sprites_box = arcade.gui.UIAnchorLayout(size_hint=(0.95, 0.9))
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()
@@ -50,6 +67,7 @@ class Game(arcade.gui.UIView):
button.on_click = on_click
def move_x(self, a, shape):
a = float(a)
if isinstance(shape, Triangle):
shape.x += a
shape.x2 += a
@@ -58,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
@@ -66,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
@@ -77,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
@@ -88,18 +109,22 @@ 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.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.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", {}])
@@ -114,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):
@@ -145,48 +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))
shape = self.shapes[-1]
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}])
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)
def add_sprite(self):
self.disable_previous()
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.mode = "sprite_add"
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))
self.anchor.add(self.sprite_add_ui, anchor_x="center", anchor_y="center")
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))
def check_selection(delta_time):
if self.sprite_add_filemanager.submitted_content:
texture = arcade.load_texture(self.sprite_add_filemanager.submitted_content)
SPRITE_TEXTURES[self.sprite_name_input.text] = texture
SPRITES[self.sprite_name_input.text] = self.sprite_add_filemanager.submitted_content
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.anchor.remove(self.sprite_add_ui)
arcade.unschedule(check_selection)
arcade.schedule(check_selection, 0.1)
def on_show_view(self):
super().on_show_view()
self.rules_box.add_rule(None, ["on_left_click", "spawn"])
self.rules_box.refresh_rules_display()
self.sprites_ui.add(arcade.gui.UILabel(text="Sprites", font_size=24, text_color=arcade.color.WHITE), anchor_x="center", anchor_y="top")
self.sprites_box.add(arcade.gui.UILabel(text="Sprites", font_size=24, text_color=arcade.color.WHITE), anchor_x="center", anchor_y="top")
self.sprites_grid = self.sprites_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.sprites_grid = self.sprites_box.add(arcade.gui.UIGridLayout(columns=8, row_count=8, align="left", vertical_spacing=10, horizontal_spacing=10, size_hint=(0.95, 0.85)), anchor_x="center", anchor_y="center").with_border()
for n, shape in enumerate(SHAPES):
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.triggered_events.append(["game_launch", {}])
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()
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"])]
self.triggered_events.append(["start", {}])
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 = {
@@ -204,51 +246,109 @@ 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.file_manager.submitted_content:
with open(self.file_manager.submitted_content, "r") as file:
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)
if not data or not "rulesets" in data or not "rule_values" in data:
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
self.rule_values = data["rule_values"]
self.triggered_events = []
self.current_ruleset_num = 0
self.current_ruleset_page = 0
for rule_num, ruleset in data["rules"].items():
block = self.dict_to_block(ruleset)
self.rulesets[int(rule_num)] = block
self.rules_content_box.clear()
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)
for rule_box in self.rule_boxes.values():
rule_box.clear()
del rule_box
SPRITES[sprite_name] = sprite_path
self.rule_labels = {}
self.rule_var_changers = {}
self.rule_boxes = {}
self.sprites_grid.clear()
for ruleset in data["rulesets"].values():
self.add_ruleset(ruleset)
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.refresh_rules_display()
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()
if self.mode == "export" and self.file_manager.submitted_content:
with open(self.file_manager.submitted_content, "w") as file:
file.write(json.dumps({
"rulesets": self.rulesets,
"rule_values": self.rule_values
}, indent=4))
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
@@ -257,65 +357,36 @@ class Game(arcade.gui.UIView):
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:
continue
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" or "shape_type" in if_rule_dict["user_vars"]:
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.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.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:
for shape_b in self.shapes:
if shape.check_collision(shape_b):
self.triggered_events.append(["collision", {"event_a_type": shape.shape_type, "event_b_type": shape.shape_type, "shape_size": shape.shape_size, "shape_x": shape.x, "shape_y": shape.y, "shape": shape, "shape_color": shape.color}])
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)
@@ -326,25 +397,43 @@ class Game(arcade.gui.UIView):
def on_key_press(self, symbol, modifiers):
if symbol == arcade.key.ESCAPE:
self.main_exit()
elif self.mode == "simulation" and 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 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 in ["import", "export"]:
self.anchor.remove(self.file_manager)
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_box)
self.anchor.remove(self.sprites_ui)
elif self.mode == "sprite_add":
self.anchor.remove(self.sprite_add_ui)
self.anchor.trigger_full_render()
@@ -359,28 +448,27 @@ class Game(arcade.gui.UIView):
self.disable_previous()
self.mode = "export"
self.file_manager.change_mode("export")
self.anchor.add(self.file_manager, anchor_x="center", anchor_y="top")
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.file_manager.change_mode("import")
self.anchor.add(self.file_manager, anchor_x="center", anchor_y="top")
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_box, anchor_x="center", anchor_y="top")
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):
@@ -392,5 +480,10 @@ class Game(arcade.gui.UIView):
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,255 +1,578 @@
from utils.constants import DO_RULES, IF_RULES, LOGICAL_OPERATORS, NON_COMPATIBLE_WHEN, NON_COMPATIBLE_DO_WHEN, VAR_NAMES, VAR_DEFAULT, VAR_OPTIONS, dropdown_style, slider_style
import arcade, arcade.gui, random
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
IF_KEYS = tuple(IF_RULES.keys())
DO_KEYS = tuple(DO_RULES.keys())
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
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 VarBlock:
x: float
y: float
label: str
var_type: str
connected_rule_num: str
value: str | int
def generate_ruleset(ruleset_type):
when_a = random.choice(IF_KEYS)
@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)
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
]
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 not valid_b:
return [when_a, random.choice(DO_KEYS)]
def refresh(self):
for shapes_list in self.shapes_by_rule_num.values():
for shape in shapes_list:
shape.delete()
when_b = random.choice(valid_b)
logical = random.choice(LOGICAL_OPERATORS)
else:
when_b = None
logical = None
for text_list in self.text_by_rule_num.values():
for text in text_list:
text.delete()
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
]
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
]
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)
do = random.choice(valid_do)
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
)
if logical:
return [when_a, logical, when_b, do]
else:
return [when_a, do]
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:
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
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:
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)
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)
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:
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))
class RuleUIBox(arcade.gui.UIBoxLayout):
def __init__(self, window):
super().__init__(space_between=10, align="center", size_hint=(0.95, 0.75))
self.window = window
self.current_ruleset_num = 0
self.current_ruleset_page = 0
self.rulesets_per_page = 2
self.rulesets = {}
self.current_rule_num = 0
self.rule_values = {}
self.var_edit_dialog = None
self.rule_labels = {}
self.rule_var_changers = {}
self.rule_boxes = {}
self.rulesets: dict[int, Block] = {}
self.nav_buttons_box = None
self.block_renderer = BlockRenderer(self.rulesets)
self.camera = arcade.Camera2D()
self.rules_label = self.add(arcade.gui.UILabel(text="Rules", font_size=20, text_color=arcade.color.WHITE))
self.add(arcade.gui.UISpace(height=self.window.height / 70, width=self.window.width * 0.25))
self.dragged_rule_ui: Block | None = None
self.add_simple_rule_button = self.add(arcade.gui.UIFlatButton(text="Add Simple rule", width=self.window.width * 0.225, height=self.window.height / 25, style=dropdown_style))
self.add_simple_rule_button.on_click = lambda event: self.add_rule("simple")
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 / 85))
self.add(
arcade.gui.UISpace(
height=self.window.height / 70, width=self.window.width * 0.25
)
)
self.add_advanced_rule_button = self.add(arcade.gui.UIFlatButton(text="Add Advanced rule", width=self.window.width * 0.225, height=self.window.height / 25, style=dropdown_style))
self.add_advanced_rule_button.on_click = lambda event: self.add_rule("advanced")
self.create_sidebar = self.add(arcade.gui.UIBoxLayout(size_hint=(0.15, 1), vertical=False, space_between=5), anchor_x="left", anchor_y="bottom")
self.add(arcade.gui.UISpace(height=self.window.height / 85))
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.nav_buttons_box = self.add(arcade.gui.UIBoxLayout(vertical=False, space_between=10))
self.scrollbar = UIScrollBar(self.scroll_area)
self.scrollbar.size_hint = (0.075, 1)
self.create_sidebar.add(self.scrollbar)
self.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))
self.prev_button.on_click = self.prev_page
self.create_box = self.scroll_area.add(arcade.gui.UIBoxLayout(space_between=10))
self.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))
self.next_button.on_click = self.next_page
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.rules_content_box = self.add(arcade.gui.UIBoxLayout(align="center"))
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 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 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 create_rule_ui(self, rule_box: arcade.gui.UIBoxLayout, rule, rule_type, rule_num=1, is_import=False):
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]
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))
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
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, variable_type in enumerate(rule_dict["user_vars"]):
key = f"{self.current_ruleset_num}_{rule_num}_{variable_type}_{n}"
for n, var_type in enumerate(rule_dict["user_vars"])
],
[]
)
if not is_import:
self.rule_values[key] = default_values[VAR_NAMES[n]]
self.rulesets[self.current_rule_num] = rule_box
self.current_rule_num += 1
self.block_renderer.refresh()
label = rule_box.add(arcade.gui.UILabel(f'{VAR_NAMES[n]}: {self.rule_values[key]}', font_size=11, width=self.window.width * 0.225, height=self.window.height / 30))
self.rule_labels[key] = label
return rule_box
if variable_type in ["variable", "size"]:
slider = rule_box.add(arcade.gui.UISlider(value=self.rule_values[key], min_value=VAR_OPTIONS[variable_type][0], max_value=VAR_OPTIONS[variable_type][1], step=1, style=slider_style, width=self.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
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:
dropdown = rule_box.add(arcade.gui.UIDropdown(default=self.rule_values[key], options=VAR_OPTIONS[variable_type], active_style=dropdown_style, primary_style=dropdown_style, dropdown_style=dropdown_style, width=self.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
self.drag_n_drop_check(block.children)
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)
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
ruleset = self.rulesets[ruleset_num]
def press_check(self, event, blocks):
for block in blocks:
if block == self.dragged_rule_ui:
continue
if len(ruleset) == 2:
if rule_type == "if":
ruleset[0] = new_rule_name
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:
ruleset[1] = new_rule_name
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:
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.trash_sprite.time = 0
self.trash_sprite.update_animation()
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, is_import=False):
rule_box = arcade.gui.UIBoxLayout(space_between=5, align="left")
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", 1, is_import)
self.create_rule_ui(rule_box, ruleset[1], "do", 2, is_import)
else:
self.rulesets[self.current_ruleset_num] = ruleset
self.create_rule_ui(rule_box, ruleset[0], "if", 1, is_import)
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, is_import)
self.create_rule_ui(rule_box, ruleset[3], "do", 3, is_import)
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()
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}'
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()

View File

@@ -31,7 +31,7 @@ class BaseShape():
def check_collision(self, other):
if isinstance(other, Circle):
return self._collides_with_circle(other)
elif isinstance(other, Rectangle):
elif isinstance(other, BaseRectangle):
return self._collides_with_rectangle(other)
elif isinstance(other, Triangle):
return self._collides_with_triangle(other)
@@ -106,11 +106,9 @@ class Circle(pyglet.shapes.Circle, BaseShape):
return not (has_neg and has_pos)
class Rectangle(pyglet.shapes.Rectangle, BaseShape):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
BaseShape.__init__(self)
self.shape_type = "rectangle"
class BaseRectangle(BaseShape):
def __init__(self):
super().__init__()
@property
def shape_size(self):
@@ -181,6 +179,21 @@ class Rectangle(pyglet.shapes.Rectangle, BaseShape):
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)

View File

@@ -1,13 +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"]
ALLOWED_INPUT = ["a", "b", "c", "d", "e", "q", "w", "s", "t"]
ALLOWED_INPUT = ["a", "b", "c", "d", "e", "q", "w", "s", "t", "space", "left", "right", "up", "down"]
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",
@@ -25,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,
@@ -36,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),
@@ -45,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": "spawn",
"user_vars": ["shape_type"],
"vars": ["shape_type", "event_shape_type"],
"func": lambda *v: v[0] == v[1]
@@ -84,23 +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]
@@ -108,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]
@@ -116,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])
@@ -140,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
@@ -148,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
@@ -156,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": {
@@ -363,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}",
@@ -396,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

View File

@@ -8,9 +8,10 @@ button_texture = arcade.gui.NinePatchTexture(64 // 4, 64 // 4, 64 // 4, 64 // 4,
button_hovered_texture = arcade.gui.NinePatchTexture(64 // 4, 64 // 4, 64 // 4, 64 // 4, arcade.load_texture(os.path.join(_assets_dir, 'graphics', 'button_hovered.png')))
SPRITE_TEXTURES = {
"circle": arcade.load_texture(os.path.join(_assets_dir, 'graphics', 'sprites', 'circle.png')),
"rectangle": arcade.load_texture(os.path.join(_assets_dir, 'graphics', 'sprites', 'rectangle.png')),
"triangle": arcade.load_texture(os.path.join(_assets_dir, 'graphics', 'sprites', 'triangle.png')),
os.path.splitext(file_name)[0]: arcade.load_texture(os.path.join(_assets_dir, 'graphics', 'sprites', file_name))
for file_name in os.listdir(os.path.join(_assets_dir, 'graphics', 'sprites'))
}
theme_sound = arcade.Sound(os.path.join(_assets_dir, 'sound', 'music.ogg'))
trash_bin = arcade.load_animated_gif(os.path.join(_assets_dir, 'graphics', 'trash_bin.gif'))