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-dir: assets=assets
include-data-files: CREDITS=CREDITS include-data-files: CREDITS=CREDITS
mode: onefile mode: onefile
output-file: FleetCommander output-file: ChaosProtocol
- name: Locate and rename executable (Linux) - name: Locate and rename executable (Linux)
if: matrix.os == 'ubuntu-22.04' if: matrix.os == 'ubuntu-22.04'
@@ -51,29 +51,29 @@ jobs:
echo "Searching for built Linux binary..." echo "Searching for built Linux binary..."
# List to help debugging when paths change # List to help debugging when paths change
ls -laR . | head -n 500 || true 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 if [ -z "${BIN}" ]; then
echo "ERROR: No Linux binary found after build" echo "ERROR: No Linux binary found after build"
exit 1 exit 1
fi fi
echo "Found: ${BIN}" echo "Found: ${BIN}"
mkdir -p build_output mkdir -p build_output
cp "${BIN}" build_output/FleetCommander.bin cp "${BIN}" build_output/ChaosProtocol.bin
chmod +x build_output/FleetCommander.bin chmod +x build_output/ChaosProtocol.bin
echo "Executable ready: build_output/FleetCommander.bin" echo "Executable ready: build_output/ChaosProtocol.bin"
shell: bash shell: bash
- name: Locate and rename executable (Windows) - name: Locate and rename executable (Windows)
if: matrix.os == 'windows-latest' if: matrix.os == 'windows-latest'
run: | run: |
Write-Host "Searching for built Windows binary..." 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) Write-Host ("Found: " + $_.FullName)
New-Item -ItemType Directory -Force -Path build_output | Out-Null New-Item -ItemType Directory -Force -Path build_output | Out-Null
Copy-Item $_.FullName "build_output\FleetCommander.exe" Copy-Item $_.FullName "build_output\ChaosProtocol.exe"
Write-Host "Executable ready: build_output\FleetCommander.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" Write-Error "ERROR: No Windows binary found after build"
exit 1 exit 1
} }
@@ -83,7 +83,7 @@ jobs:
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: ${{ matrix.platform }} name: ${{ matrix.platform }}
path: build_output/FleetCommander.* path: build_output/ChaosProtocol.*
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -114,5 +114,6 @@ jobs:
--notes "Automated build for $TAG" --notes "Automated build for $TAG"
fi fi
# Upload the executables directly (no zip files) # Upload the executables directly (no zip files)
gh release upload "$TAG" downloads/linux/FleetCommander.bin --clobber gh release upload "$TAG" downloads/linux/ChaosProtocol.bin --clobber
gh release upload "$TAG" downloads/windows/FleetCommander.exe --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. 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) 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! 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 from arcade.gui.experimental.scroll_area import UIScrollArea, UIScrollBar
class FileManager(arcade.gui.UIAnchorLayout): class FileManager(arcade.gui.UIAnchorLayout):
def __init__(self, width, allowed_extensions): def __init__(self, width, height, size_hint, allowed_extensions):
super().__init__(size_hint=(0.95, 0.9), vertical=False) super().__init__(size_hint=size_hint, vertical=False)
self.filemanager_width = width self.filemanager_width = width
self.filemanager_height = height
self.current_directory = os.path.expanduser("~") self.current_directory = os.path.expanduser("~")
self.allowed_extensions = allowed_extensions self.allowed_extensions = allowed_extensions
@@ -20,11 +21,11 @@ class FileManager(arcade.gui.UIAnchorLayout):
self.content_cache = {} self.content_cache = {}
self.pre_cache_contents() 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 = UIScrollArea(size_hint=(0.665, 0.7)) # center on screen
self.scroll_area.scroll_speed = -50 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 = UIScrollBar(self.scroll_area)
self.scrollbar.size_hint = (0.02, 1) 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.files_box = arcade.gui.UIBoxLayout(space_between=5)
self.scroll_area.add(self.files_box) 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_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_width * 0.025).with_border(color=arcade.color.WHITE)) 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.2, height=self.filemanager_width * 0.05))
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.on_click = lambda event: self.submit(self.current_directory)
self.submit_button.visible = False self.submit_button.visible = False
self.filename_label.visible = False self.filename_label.visible = False
self.filename_input.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() self.show_directory()
def change_mode(self, mode): def change_mode(self, mode):
@@ -57,9 +55,7 @@ class FileManager(arcade.gui.UIAnchorLayout):
self.submit_button.visible = self.mode == "export" self.submit_button.visible = self.mode == "export"
def submit(self, content): def submit(self, content):
self.submitted_content = content if self.mode == "import" else f"{content}/{self.filename_input.text}" self.submitted_content = content if self.mode == "import" else os.path.join(content, self.filename_input.text)
self.disable()
def get_content(self, directory): def get_content(self, directory):
if not directory in self.content_cache or time.perf_counter() - self.content_cache[directory][-1] >= 30: 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.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)) 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): 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))) 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(f"{self.current_directory}/{file}"): if os.path.isdir(os.path.join(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[-1].on_click = lambda event, directory=os.path.join(self.current_directory, file): self.change_directory(directory)
else: else:
self.file_buttons[-1].on_click = lambda event, file=f"{self.current_directory}/{file}": self.submit(file) self.file_buttons[-1].on_click = lambda event, file=os.path.join(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"
def change_directory(self, directory): def change_directory(self, directory):
if directory.startswith("//"): # Fix / paths if directory.startswith("//"): # Fix / paths

View File

@@ -1,10 +1,12 @@
import arcade, arcade.gui, pyglet, random, json import arcade, arcade.gui, pyglet, random, json
from utils.preload import SPRITE_TEXTURES, button_texture, button_hovered_texture from dataclasses import asdict
from utils.constants import button_style, dropdown_style, DO_RULES, IF_RULES, SHAPES, ALLOWED_INPUT, menu_background_color
from game.rules import RuleUIBox from utils.preload import SPRITE_TEXTURES, button_texture, button_hovered_texture
from game.sprites import BaseShape, Rectangle, Circle, Triangle 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 from game.file_manager import FileManager
class Game(arcade.gui.UIView): 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.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.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("Simulation", lambda event: self.simulation())
@@ -36,9 +42,20 @@ class Game(arcade.gui.UIView):
self.triggered_events = [] self.triggered_events = []
self.rulesets = self.rules_box.rulesets 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.shapes = []
self.shape_batch = pyglet.graphics.Batch() self.shape_batch = pyglet.graphics.Batch()
@@ -50,6 +67,7 @@ class Game(arcade.gui.UIView):
button.on_click = on_click button.on_click = on_click
def move_x(self, a, shape): def move_x(self, a, shape):
a = float(a)
if isinstance(shape, Triangle): if isinstance(shape, Triangle):
shape.x += a shape.x += a
shape.x2 += a shape.x2 += a
@@ -58,6 +76,7 @@ class Game(arcade.gui.UIView):
shape.x += a shape.x += a
def move_y(self, a, shape): def move_y(self, a, shape):
a = float(a)
if isinstance(shape, Triangle): if isinstance(shape, Triangle):
shape.y += a shape.y += a
shape.y2 += a shape.y2 += a
@@ -66,6 +85,7 @@ class Game(arcade.gui.UIView):
shape.y += a shape.y += a
def change_x(self, a, shape): def change_x(self, a, shape):
a = float(a)
if isinstance(shape, Triangle): if isinstance(shape, Triangle):
offset_x2 = shape.x2 - shape.x offset_x2 = shape.x2 - shape.x
offset_x3 = shape.x3 - shape.x offset_x3 = shape.x3 - shape.x
@@ -77,6 +97,7 @@ class Game(arcade.gui.UIView):
shape.x = a shape.x = a
def change_y(self, a, shape): def change_y(self, a, shape):
a = float(a)
if isinstance(shape, Triangle): if isinstance(shape, Triangle):
offset_y2 = shape.y2 - shape.y offset_y2 = shape.y2 - shape.y
offset_y3 = shape.y3 - shape.y offset_y3 = shape.y3 - shape.y
@@ -88,18 +109,22 @@ class Game(arcade.gui.UIView):
shape.y = a shape.y = a
def change_x_velocity(self, a, shape): def change_x_velocity(self, a, shape):
a = float(a)
shape.x_velocity = 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}]) 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): def change_y_velocity(self, a, shape):
a = float(a)
shape.y_velocity = 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}]) 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): def change_x_gravity(self, a):
a = float(a)
self.x_gravity = a self.x_gravity = a
self.triggered_events.append(["x_gravity_change", {}]) self.triggered_events.append(["x_gravity_change", {}])
def change_y_gravity(self, a): def change_y_gravity(self, a):
a = float(a)
self.y_gravity = a self.y_gravity = a
self.triggered_events.append(["y_gravity_change", {}]) self.triggered_events.append(["y_gravity_change", {}])
@@ -114,9 +139,10 @@ class Game(arcade.gui.UIView):
shape.delete() shape.delete()
def change_size(self, a, shape): def change_size(self, a, shape):
a = float(a)
if isinstance(shape, Circle): if isinstance(shape, Circle):
shape.radius = a shape.radius = a
elif isinstance(shape, Rectangle): elif isinstance(shape, BaseRectangle):
shape.width = a shape.width = a
shape.height = a shape.height = a
elif isinstance(shape, Triangle): elif isinstance(shape, Triangle):
@@ -145,48 +171,64 @@ class Game(arcade.gui.UIView):
elif shape_type == "triangle": 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)) 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}]) 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): def add_sprite(self):
old_shape_x, old_shape_y, old_shape_size, old_shape_color = shape.x, shape.y, shape.shape_size, shape.shape_color self.disable_previous()
self.destroy(shape)
if a == "circle": self.mode = "sprite_add"
self.shapes.append(Circle(old_shape_x, old_shape_y, old_shape_size, color=getattr(arcade.color, old_shape_color), batch=self.shape_batch))
elif a == "rectangle": self.anchor.add(self.sprite_add_ui, anchor_x="center", anchor_y="center")
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))
elif a == "triangle": def check_selection(delta_time):
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)) if self.sprite_add_filemanager.submitted_content:
texture = arcade.load_texture(self.sprite_add_filemanager.submitted_content)
def on_show_view(self): SPRITE_TEXTURES[self.sprite_name_input.text] = texture
super().on_show_view() SPRITES[self.sprite_name_input.text] = self.sprite_add_filemanager.submitted_content
self.rules_box.add_rule(None, ["on_left_click", "spawn"]) self.sprites_grid.clear()
self.rules_box.refresh_rules_display()
self.sprites_box.add(arcade.gui.UILabel(text="Sprites", font_size=24, text_color=arcade.color.WHITE), anchor_x="center", anchor_y="top") for n, shape in enumerate(SPRITES):
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):
row, col = n % 8, n // 8 row, col = n % 8, n // 8
box = self.sprites_grid.add(arcade.gui.UIBoxLayout(), row=row, column=col) 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.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)) 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", {}]) self.anchor.remove(self.sprite_add_ui)
arcade.unschedule(check_selection)
def get_rule_values(self, ruleset_num, rule_num, rule_dict, event_args): arcade.schedule(check_selection, 0.1)
args = [self.rule_values[f"{ruleset_num}_{rule_num}_{user_var}_{n}"] for n, user_var in enumerate(rule_dict["user_vars"])]
def on_show_view(self):
super().on_show_view()
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_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")
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_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.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"]] 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): def check_rule(self, rule_dict, vars, event_args):
return rule_dict["func"](*self.get_rule_values(ruleset_num, rule_num, rule_dict, event_args)) return rule_dict["func"](*self.get_vars(rule_dict, vars, event_args))
def get_action_function(self, action_dict): def get_action_function(self, action_dict):
ACTION_FUNCTION_DICT = { ACTION_FUNCTION_DICT = {
@@ -204,51 +246,109 @@ class Game(arcade.gui.UIView):
"change_y_velocity": self.change_y_velocity, "change_y_velocity": self.change_y_velocity,
"change_color": self.change_color, "change_color": self.change_color,
"change_size": self.change_size, "change_size": self.change_size,
"destroy": self.destroy, "destroy": self.destroy
"morph": self.morph
} }
} }
return ACTION_FUNCTION_DICT[action_dict["type"]][action_dict["name"]] return ACTION_FUNCTION_DICT[action_dict["type"]][action_dict["name"]]
def run_do_rule(self, 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_rule_values(ruleset_num, rule_num, rule_dict, 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): def on_update(self, delta_time):
if self.mode == "import" and self.file_manager.submitted_content: if self.mode == "import" and self.import_file_manager.submitted_content:
with open(self.file_manager.submitted_content, "r") as file: with open(self.import_file_manager.submitted_content, "r") as file:
data = json.load(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)) 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 return
self.rule_values = data["rule_values"] for rule_num, ruleset in data["rules"].items():
self.triggered_events = [] block = self.dict_to_block(ruleset)
self.current_ruleset_num = 0 self.rulesets[int(rule_num)] = block
self.current_ruleset_page = 0
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(): SPRITES[sprite_name] = sprite_path
rule_box.clear()
del rule_box
self.rule_labels = {} self.sprites_grid.clear()
self.rule_var_changers = {}
self.rule_boxes = {}
for ruleset in data["rulesets"].values(): for n, shape in enumerate(SPRITES):
self.add_ruleset(ruleset) 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: self.rules()
with open(self.file_manager.submitted_content, "w") as file:
file.write(json.dumps({ if self.mode == "export" and self.export_file_manager.submitted_content:
"rulesets": self.rulesets, with open(self.export_file_manager.submitted_content, "w") as file:
"rule_values": self.rule_values file.write(json.dumps(
}, indent=4)) {
"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": if not self.mode == "simulation":
return return
@@ -257,65 +357,36 @@ class Game(arcade.gui.UIView):
while len(self.triggered_events) > 0: while len(self.triggered_events) > 0:
trigger, trigger_args = self.triggered_events.pop(0) trigger, trigger_args = self.triggered_events.pop(0)
for 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 continue
if do_rule_dict["action"]["type"] == "shape_action" or "shape_type" in if_rule_dict["user_vars"]: self.recursive_execute_rule(rule, trigger_args)
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})
if self.check_rule(key, 1, if_rule_dict, event_args): has_collision_rules = any(
self.run_do_rule(key, 2, do_rule_dict, event_args) rule.rule_type == "trigger" and rule.rule == "collision"
else: for rule in self.rulesets.values()
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)
for shape in self.shapes: 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) 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: if shape.x < 0 or shape.x > self.window.width or shape.y < 0 or shape.y > self.window.height:
self.destroy(shape) self.destroy(shape)
@@ -326,25 +397,43 @@ class Game(arcade.gui.UIView):
def on_key_press(self, symbol, modifiers): def on_key_press(self, symbol, modifiers):
if symbol == arcade.key.ESCAPE: if symbol == arcade.key.ESCAPE:
self.main_exit() 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)}]) self.triggered_events.append(["on_input", {"event_key": chr(symbol)}])
def on_mouse_press(self, x, y, button, modifiers): def on_mouse_press(self, x, y, button, modifiers):
if not self.mode == "simulation":
return
if button == arcade.MOUSE_BUTTON_LEFT: if button == arcade.MOUSE_BUTTON_LEFT:
self.triggered_events.append(["on_left_click", {}]) self.triggered_events.append(["on_left_click", {}])
elif self.mode == "simulation" and button == arcade.MOUSE_BUTTON_RIGHT: elif self.mode == "simulation" and button == arcade.MOUSE_BUTTON_RIGHT:
self.triggered_events.append(["on_right_click", {}]) self.triggered_events.append(["on_right_click", {}])
def on_mouse_motion(self, x, y, button, modifiers): def on_mouse_motion(self, x, y, button, modifiers):
if not self.mode == "simulation":
return
self.triggered_events.append(["on_mouse_move", {}]) self.triggered_events.append(["on_mouse_move", {}])
def on_mouse_drag(self, x, y, dx, dy, _buttons, _modifiers):
if self.mode == "rules" and arcade.MOUSE_BUTTON_MIDDLE == _buttons:
self.rules_box.camera.position -= (dx, dy)
def on_mouse_scroll(self, x, y, scroll_x, scroll_y):
if self.mode == "rules":
self.rules_box.camera.zoom *= 1 + scroll_y * 0.1
def disable_previous(self): def disable_previous(self):
if self.mode in ["import", "export"]: if self.mode == "import":
self.anchor.remove(self.file_manager) self.anchor.remove(self.import_file_manager)
elif self.mode == "export":
self.anchor.remove(self.export_file_manager)
elif self.mode == "rules": elif self.mode == "rules":
self.anchor.remove(self.rules_box) self.anchor.remove(self.rules_box)
elif self.mode == "sprites": 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() self.anchor.trigger_full_render()
@@ -359,28 +448,27 @@ class Game(arcade.gui.UIView):
self.disable_previous() self.disable_previous()
self.mode = "export" self.mode = "export"
self.anchor.add(self.export_file_manager, anchor_x="center", anchor_y="top", align_y=-self.window.height * 0.025)
self.file_manager.change_mode("export")
self.anchor.add(self.file_manager, anchor_x="center", anchor_y="top")
def import_file(self): def import_file(self):
self.disable_previous() self.disable_previous()
self.mode = "import" self.mode = "import"
self.anchor.add(self.import_file_manager, anchor_x="center", anchor_y="top", align_y=-self.window.height * 0.025)
self.file_manager.change_mode("import")
self.anchor.add(self.file_manager, anchor_x="center", anchor_y="top")
def sprites(self): def sprites(self):
self.disable_previous() self.disable_previous()
self.mode = "sprites" 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): def simulation(self):
self.disable_previous() 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" self.mode = "simulation"
def main_exit(self): def main_exit(self):
@@ -392,5 +480,10 @@ class Game(arcade.gui.UIView):
if self.mode == "simulation": if self.mode == "simulation":
self.shape_batch.draw() self.shape_batch.draw()
elif self.mode == "rules":
with self.rules_box.camera.activate():
self.rules_box.draw()
self.rules_box.draw_unproject()
self.ui.draw() 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 from utils.constants import (
import arcade, arcade.gui, random DO_RULES,
IF_RULES,
IF_KEYS = tuple(IF_RULES.keys()) TRIGGER_RULES,
DO_KEYS = tuple(DO_RULES.keys()) FOR_RULES,
NEEDS_SHAPE,
BAD_WHEN = {tuple(sorted(pair)) for pair in NON_COMPATIBLE_WHEN} PROVIDES_SHAPE,
BAD_DO_WHEN = {tuple(pair) for pair in NON_COMPATIBLE_DO_WHEN} button_style,
slider_style,
def generate_ruleset(ruleset_type): dropdown_style,
when_a = random.choice(IF_KEYS) DO_COLOR,
IF_COLOR,
if ruleset_type == "advanced": FOR_COLOR,
valid_b = [ TRIGGER_COLOR,
b for b in IF_KEYS RULE_DEFAULTS,
if b != when_a and tuple(sorted((when_a, b))) not in BAD_WHEN VAR_TYPES
]
if not valid_b:
return [when_a, random.choice(DO_KEYS)]
when_b = random.choice(valid_b)
logical = random.choice(LOGICAL_OPERATORS)
else:
when_b = None
logical = None
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
]
do = random.choice(valid_do)
if logical:
return [when_a, logical, when_b, do]
else:
return [when_a, do]
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.rule_values = {}
self.rule_labels = {}
self.rule_var_changers = {}
self.rule_boxes = {}
self.nav_buttons_box = None
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.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.add(arcade.gui.UISpace(height=self.window.height / 85))
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.add(arcade.gui.UISpace(height=self.window.height / 85))
self.nav_buttons_box = self.add(arcade.gui.UIBoxLayout(vertical=False, space_between=10))
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.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.rules_content_box = self.add(arcade.gui.UIBoxLayout(align="center"))
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() 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
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": elif rule_type == "do":
return { return DO_RULES
rule_key: (
rule_dict["description"].format_map({VAR_NAMES[n]: VAR_NAMES[n] for n, variable in enumerate(rule_dict["user_vars"])}), @dataclass
{VAR_NAMES[n]: VAR_DEFAULT[variable] for n, variable in enumerate(rule_dict["user_vars"])} class VarBlock:
x: float
y: float
label: str
var_type: str
connected_rule_num: str
value: str | int
@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)
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()
def refresh(self):
for shapes_list in self.shapes_by_rule_num.values():
for shape in shapes_list:
shape.delete()
for text_list in self.text_by_rule_num.values():
for text in text_list:
text.delete()
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
) )
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, is_import=False): var_text = pyglet.text.Label(
defaults = self.get_rule_defaults(rule_type) text=str(var.value),
rule_dict = IF_RULES[rule] if rule_type == "if" else DO_RULES[rule] x=x + var_width // 2,
ruleset_num = self.current_ruleset_num y=y,
default_values = defaults[rule][1] color=arcade.color.BLACK,
font_size=10,
anchor_x='center',
anchor_y='center'
)
dropdown_options = [desc for desc, _ in defaults.values()] if rule_num not in self.shapes_by_rule_num:
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)) self.shapes_by_rule_num[rule_num] = []
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) if rule_num not in self.text_by_rule_num:
self.rule_labels[f"{self.current_ruleset_num}_{rule_num}_desc"] = desc_label self.text_by_rule_num[rule_num] = []
if rule_num not in self.var_widgets:
self.var_widgets[rule_num] = []
for n, variable_type in enumerate(rule_dict["user_vars"]): self.shapes_by_rule_num[rule_num].append(var_rect)
key = f"{self.current_ruleset_num}_{rule_num}_{variable_type}_{n}" self.text_by_rule_num[rule_num].append(var_text)
self.text_objects.append(var_text)
if not is_import: self.var_widgets[rule_num].append({
self.rule_values[key] = default_values[VAR_NAMES[n]] 'var': var,
'rect': var_rect,
'text': var_text,
'x': x,
'y': y,
'width': var_width,
'height': var_height
})
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)) return var_width, var_height
self.rule_labels[key] = label
if variable_type in ["variable", "size"]: def _build_block_with_vars(self, b: Block, x: int, y: int) -> None:
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)) lx, ly = x, y - 42
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
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: 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)) if var_index < len(b.vars):
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) var = b.vars[var_index]
self.rule_var_changers[key] = dropdown 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 change_rule_type(self, ruleset_num, rule_num, rule_type, new_rule_text): def _build_block(self, b: Block, x: int, y: int) -> int:
defaults = self.get_rule_defaults(rule_type) is_wrap = b.rule_type != "do"
new_rule_name = next(key for key, default_list in defaults.items() if default_list[0] == new_rule_text) h, w = 42, 380
ruleset = self.rulesets[ruleset_num] 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
if len(ruleset) == 2: lx, ly = x, y - h
if rule_type == "if":
ruleset[0] = new_rule_name 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: else:
ruleset[1] = new_rule_name 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: else:
if rule_type == "if": for child in b.children:
if rule_num == 1: child.x = lx
ruleset[0] = new_rule_name 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: else:
ruleset[2] = new_rule_name 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: else:
ruleset[3] = new_rule_name self.press_check(event, block.children)
self.rebuild_ruleset_ui(ruleset_num) def on_event(self, event):
if self.var_edit_dialog:
super().on_event(event)
return
def rebuild_ruleset_ui(self, ruleset_num): super().on_event(event)
rule_box = self.rule_boxes[ruleset_num]
keys_to_remove = [k for k in self.rule_labels.keys() if k.startswith(f"{ruleset_num}_")] if isinstance(event, arcade.gui.UIMouseDragEvent):
for key in keys_to_remove: if event.buttons == arcade.MOUSE_BUTTON_LEFT:
del self.rule_labels[key] 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)
keys_to_remove = [k for k in self.rule_var_changers.keys() if k.startswith(f"{ruleset_num}_")] elif isinstance(event, arcade.gui.UIMousePressEvent):
for key in keys_to_remove: projected_vec = self.camera.unproject((event.x, event.y))
del self.rule_var_changers[key] var, _ = self.block_renderer.get_var_at_position(projected_vec.x, projected_vec.y)
keys_to_remove = [k for k in self.rule_values.keys() if k.startswith(f"{ruleset_num}_")] if var:
for key in keys_to_remove: self.open_var_edit_dialog(var)
del self.rule_values[key] return
rule_box.clear() self.press_check(event, list(self.rulesets.values()))
ruleset = self.rulesets[ruleset_num] elif isinstance(event, arcade.gui.UIMouseReleaseEvent):
old_ruleset_num = self.current_ruleset_num if self.dragged_rule_ui:
self.current_ruleset_num = ruleset_num block_screen_pos = self.camera.project((self.dragged_rule_ui.x, self.dragged_rule_ui.y))
if len(ruleset) == 2: block_rect = arcade.LBWH(block_screen_pos[0], block_screen_pos[1], 380, 44)
self.create_rule_ui(rule_box, ruleset[0], "if") trash_rect = arcade.LBWH(
self.create_rule_ui(rule_box, ruleset[1], "do", 2) 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: else:
self.create_rule_ui(rule_box, ruleset[0], "if") self.trash_sprite.time = 0
rule_box.add(arcade.gui.UILabel(ruleset[1].upper(), font_size=14, width=self.window.width * 0.25)) self.trash_sprite.update_animation()
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): def check_collision(self, other):
if isinstance(other, Circle): if isinstance(other, Circle):
return self._collides_with_circle(other) return self._collides_with_circle(other)
elif isinstance(other, Rectangle): elif isinstance(other, BaseRectangle):
return self._collides_with_rectangle(other) return self._collides_with_rectangle(other)
elif isinstance(other, Triangle): elif isinstance(other, Triangle):
return self._collides_with_triangle(other) return self._collides_with_triangle(other)
@@ -106,11 +106,9 @@ class Circle(pyglet.shapes.Circle, BaseShape):
return not (has_neg and has_pos) return not (has_neg and has_pos)
class Rectangle(pyglet.shapes.Rectangle, BaseShape): class BaseRectangle(BaseShape):
def __init__(self, *args, **kwargs): def __init__(self):
super().__init__(*args, **kwargs) super().__init__()
BaseShape.__init__(self)
self.shape_type = "rectangle"
@property @property
def shape_size(self): 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 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)) 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): class Triangle(pyglet.shapes.Triangle, BaseShape):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*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.types import Color
from arcade.gui.widgets.buttons import UITextureButtonStyle, UIFlatButtonStyle from arcade.gui.widgets.buttons import UITextureButtonStyle, UIFlatButtonStyle
from arcade.gui.widgets.slider import UISliderStyle from arcade.gui.widgets.slider import UISliderStyle
LOGICAL_OPERATORS = ["and", "or"] # Get the directory where this module is located
SHAPES = ["rectangle", "circle", "triangle"] _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"] 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 = [ COLORS = [
"BLACK", "WHITE", "GRAY", "DARK_GRAY", "CYAN", "BLACK", "WHITE", "GRAY", "DARK_GRAY", "CYAN",
@@ -25,9 +38,18 @@ COLORS = [
COMPARISONS = [">", ">=", "<", "<=", "==", "!="] COMPARISONS = [">", ">=", "<", "<=", "==", "!="]
OPS = {
">": operator.gt,
"<": operator.lt,
">=": operator.ge,
"<=": operator.le,
"==": operator.eq,
"!=": operator.ne,
}
VAR_DEFAULT = { VAR_DEFAULT = {
"shape_type": SHAPES[0], "shape_type": "rectangle",
"target_type": SHAPES[1], "target_type": "circle",
"variable": 0, "variable": 0,
"color": "WHITE", "color": "WHITE",
"size": 10, "size": 10,
@@ -36,8 +58,8 @@ VAR_DEFAULT = {
} }
VAR_OPTIONS = { VAR_OPTIONS = {
"shape_type": SHAPES, "shape_type": SPRITES,
"target_type": SHAPES, "target_type": SPRITES,
"variable": (-700, 700), "variable": (-700, 700),
"color": COLORS, "color": COLORS,
"size": (1, 200), "size": (1, 200),
@@ -45,38 +67,41 @@ VAR_OPTIONS = {
"comparison": COMPARISONS "comparison": COMPARISONS
} }
IF_RULES = { VAR_TYPES = {
"x_position_compare": { "shape_type": "Shape Type",
"key": "x_position_compare", "target_type": "Target Type",
"description": "IF X for {a} shape is {b} {c}", "variable": "Variable",
"trigger": "every_update", "color": "Color",
"user_vars": ["shape_type", "comparison", "variable"], "size": "Size",
"vars": ["shape_type", "comparison", "variable", "event_shape_type", "shape_x"], "key_input": "Key Input",
"func": lambda *v: (v[0] == v[3]) and eval(f"{v[4]} {v[1]} {v[2]}") "comparison": "Comparison"
}, }
"y_position_compare": { TRIGGER_RULES = {
"key": "y_position_compare", "every_update": {
"description": "IF Y for {a} shape is {b} {c}", "key": "every_update",
"trigger": "every_update", "user_vars": [],
"user_vars": ["shape_type", "comparison", "variable"], "vars": [],
"vars": ["shape_type", "comparison", "variable", "event_shape_type", "shape_y"], "description": "Every Update",
"func": lambda *v: (v[0] == v[3]) and eval(f"{v[4]} {v[1]} {v[2]}") "func": lambda *v: True
}, },
"start": {
"size_compare": { "key": "start",
"key": "size_compare", "user_vars": [],
"description": "IF {a} shape size is {b} {c}", "vars": [],
"trigger": "every_update", "description": "On Game Start",
"user_vars": ["shape_type", "comparison", "variable"], "func": lambda *v: True
"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]}") "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": { "spawns": {
"key": "spawns", "key": "spawns",
"description": "IF {a} shape spawns", "description": "IF {a} shape spawns",
"trigger": "spawn",
"user_vars": ["shape_type"], "user_vars": ["shape_type"],
"vars": ["shape_type", "event_shape_type"], "vars": ["shape_type", "event_shape_type"],
"func": lambda *v: v[0] == v[1] "func": lambda *v: v[0] == v[1]
@@ -84,23 +109,6 @@ IF_RULES = {
"destroyed": { "destroyed": {
"key": "destroyed", "key": "destroyed",
"description": "IF {a} shape is destroyed", "description": "IF {a} shape is destroyed",
"trigger": "destroyed",
"user_vars": ["shape_type"],
"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"], "user_vars": ["shape_type"],
"vars": ["shape_type", "event_shape_type"], "vars": ["shape_type", "event_shape_type"],
"func": lambda *v: v[0] == v[1] "func": lambda *v: v[0] == v[1]
@@ -108,7 +116,6 @@ IF_RULES = {
"color_changes": { "color_changes": {
"key": "color_changes", "key": "color_changes",
"description": "IF {a} shape color changes", "description": "IF {a} shape color changes",
"trigger": "color_change",
"user_vars": ["shape_type"], "user_vars": ["shape_type"],
"vars": ["shape_type", "event_shape_type"], "vars": ["shape_type", "event_shape_type"],
"func": lambda *v: v[0] == v[1] "func": lambda *v: v[0] == v[1]
@@ -116,23 +123,13 @@ IF_RULES = {
"size_changes": { "size_changes": {
"key": "size_changes", "key": "size_changes",
"description": "IF {a} shape size changes", "description": "IF {a} shape size changes",
"trigger": "size_change",
"user_vars": ["shape_type"], "user_vars": ["shape_type"],
"vars": ["shape_type", "event_shape_type"], "vars": ["shape_type", "event_shape_type"],
"func": lambda *v: v[0] == v[1] "func": lambda *v: v[0] == v[1]
}, },
"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": { "collides": {
"key": "collides", "key": "collides",
"description": "IF {a} shape collides with {b}", "description": "IF {a} shape collides with {b}",
"trigger": "collision",
"user_vars": ["shape_type", "target_type"], "user_vars": ["shape_type", "target_type"],
"vars": ["shape_type", "target_type", "event_a_type", "event_b_type"], "vars": ["shape_type", "target_type", "event_a_type", "event_b_type"],
"func": lambda *v: (v[0] == v[2]) and (v[3] == v[1]) "func": lambda *v: (v[0] == v[2]) and (v[3] == v[1])
@@ -140,7 +137,6 @@ IF_RULES = {
"on_left_click": { "on_left_click": {
"key": "on_left_click", "key": "on_left_click",
"description": "IF you left click", "description": "IF you left click",
"trigger": "on_left_click",
"user_vars": [], "user_vars": [],
"vars": [], "vars": [],
"func": lambda *v: True "func": lambda *v: True
@@ -148,7 +144,6 @@ IF_RULES = {
"on_right_click": { "on_right_click": {
"key": "on_right_click", "key": "on_right_click",
"description": "IF you right click", "description": "IF you right click",
"trigger": "on_right_click",
"user_vars": [], "user_vars": [],
"vars": [], "vars": [],
"func": lambda *v: True "func": lambda *v: True
@@ -156,139 +151,72 @@ IF_RULES = {
"on_mouse_move": { "on_mouse_move": {
"key": "on_mouse_move", "key": "on_mouse_move",
"description": "IF mouse moves", "description": "IF mouse moves",
"trigger": "on_mouse_move",
"user_vars": [], "user_vars": [],
"vars": [], "vars": [],
"func": lambda *v: True "func": lambda *v: True
}, },
"on_input": { }
"key": "on_input",
"description": "IF {a} key is pressed", FOR_RULES = {
"trigger": "on_input", "every_shape": {
"user_vars": ["key_input"], "key": "every_shape",
"vars": ["key_input", "event_key"], "user_vars": [],
"vars": [],
"description": "For every shape",
}
}
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] "func": lambda *v: v[0] == v[1]
}, },
"game_launch": { "shape_type_is": {
"key": "game_launch", "key": "shape_type_is",
"description": "IF game launches", "description": "IF shape type is {a}",
"trigger": "game_launch", "user_vars": ["shape_type"],
"user_vars": [], "vars": ["shape_type", "event_shape_type"],
"vars": [], "func": lambda *v: v[0] == v[1]
"func": lambda *v: True
}, },
"every_update": {
"key": "every_update",
"description": "Every update",
"trigger": "every_update",
"user_vars": [],
"vars": [],
"func": lambda *v: True
} }
}
NON_COMPATIBLE_WHEN = [
("spawns", "destroyed"),
("spawns", "morphs"),
("spawns", "collides"),
("spawns", "x_velocity_changes"),
("spawns", "y_velocity_changes"),
("spawns", "x_gravity_changes"),
("spawns", "y_gravity_changes"),
("spawns", "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")
]
DO_RULES = { DO_RULES = {
"change_x": { "change_x": {
@@ -363,14 +291,6 @@ DO_RULES = {
"vars": ["shape"] "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": { "change_x_gravity": {
"key": "change_x_gravity", "key": "change_x_gravity",
"description": "Change X gravity to {a}", "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) menu_background_color = (30, 30, 47)
log_dir = 'logs' log_dir = 'logs'
discord_presence_id = 1440807203094138940 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'))) 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 = { SPRITE_TEXTURES = {
"circle": arcade.load_texture(os.path.join(_assets_dir, 'graphics', 'sprites', 'circle.png')), os.path.splitext(file_name)[0]: arcade.load_texture(os.path.join(_assets_dir, 'graphics', 'sprites', file_name))
"rectangle": arcade.load_texture(os.path.join(_assets_dir, 'graphics', 'sprites', 'rectangle.png')), for file_name in os.listdir(os.path.join(_assets_dir, 'graphics', 'sprites'))
"triangle": arcade.load_texture(os.path.join(_assets_dir, 'graphics', 'sprites', 'triangle.png')),
} }
theme_sound = arcade.Sound(os.path.join(_assets_dir, 'sound', 'music.ogg')) 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'))