Compare commits

...

5 Commits

7 changed files with 712 additions and 425 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,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)

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.875), 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
@@ -36,9 +37,9 @@ class FileManager(arcade.gui.UIAnchorLayout):
self.bottom_box = self.add(arcade.gui.UIBoxLayout(space_between=5), anchor_x="center", anchor_y="bottom", align_y=5) self.bottom_box = self.add(arcade.gui.UIBoxLayout(space_between=5), anchor_x="center", anchor_y="bottom", align_y=5)
self.filename_label = self.bottom_box.add(arcade.gui.UILabel(text="Filename:", font_name="Roboto", font_size=17)) self.filename_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.02).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.5, height=self.filemanager_width * 0.025)) 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
@@ -54,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:
@@ -109,19 +108,16 @@ 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 change_directory(self, directory): def change_directory(self, directory):
if directory.startswith("//"): # Fix / paths if directory.startswith("//"): # Fix / paths
directory = directory[1:] directory = directory[1:]

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, DO_RULES, IF_RULES, SHAPES, ALLOWED_INPUT
from game.rules import RuleUI 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):
@@ -21,7 +23,11 @@ class Game(arcade.gui.UIView):
self.rules_box = RuleUI(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())
@@ -35,9 +41,21 @@ class Game(arcade.gui.UIView):
self.y_gravity = self.settings.get("default_y_gravity", 5) self.y_gravity = self.settings.get("default_y_gravity", 5)
self.triggered_events = [] self.triggered_events = []
self.rulesets, self.if_rules = self.rules_box.get_rulesets() self.rulesets = self.rules_box.rulesets
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()
@@ -49,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
@@ -57,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
@@ -65,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
@@ -76,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
@@ -87,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", {}])
@@ -113,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):
@@ -143,46 +170,65 @@ 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))
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] 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))
def check_selection(delta_time):
elif a == "triangle": if self.sprite_add_filemanager.submitted_content:
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)) 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): def on_show_view(self):
super().on_show_view() super().on_show_view()
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_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_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() 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(SHAPES): for n, shape in enumerate(SPRITES):
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", {}]) 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, rule_dict, rule_values, event_args): self.triggered_events.append(["start", {}])
args = [rule_values[f"{user_var}_{n}"] for n, user_var in enumerate(rule_dict["user_vars"])]
def get_vars(self, rule_dict, vars, event_args):
args = [vars[n].value for n in range(len(rule_dict["user_vars"]))]
return args + [event_args[var] for var in rule_dict.get("vars", []) if not var in rule_dict["user_vars"]] return args + [event_args[var] for var in rule_dict.get("vars", []) if not var in rule_dict["user_vars"]]
def check_rule(self, rule_dict, rule_values, event_args): def check_rule(self, rule_dict, vars, event_args):
return rule_dict["func"](*self.get_rule_values(rule_dict, rule_values, 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 = {
@@ -200,36 +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, rule_dict, rule_values, event_args): def run_do_rule(self, rule_dict, vars, event_args):
self.get_action_function(rule_dict["action"])(*self.get_rule_values(rule_dict, rule_values, 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
for rule_num, ruleset in data["rules"].items():
block = self.dict_to_block(ruleset)
self.rulesets[int(rule_num)] = block
self.rule_values = data["rule_values"] self.sprite_types = data["sprites"]
self.triggered_events = [] 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)
# TODO: add rule loading here SPRITES[sprite_name] = sprite_path
self.sprites_grid.clear()
if self.mode == "export" and self.file_manager.submitted_content: for n, shape in enumerate(SPRITES):
with open(self.file_manager.submitted_content, "w") as file: row, col = n % 8, n // 8
file.write(json.dumps({ box = self.sprites_grid.add(arcade.gui.UIBoxLayout(), row=row, column=col)
"rulesets": self.rulesets, box.add(arcade.gui.UILabel(text=shape, font_size=16, text_color=arcade.color.WHITE))
"rule_values": self.rule_values box.add(arcade.gui.UIImage(texture=SPRITE_TEXTURES[shape], width=self.window.width / 15, height=self.window.width / 15))
}, indent=4))
self.rules_box.rulesets = self.rulesets
self.rules_box.block_renderer.blocks = self.rulesets
self.rules_box.current_rule_num = self.get_max_rule_num() + 1
self.rules_box.block_renderer.refresh()
self.rules()
if self.mode == "export" and self.export_file_manager.submitted_content:
with open(self.export_file_manager.submitted_content, "w") as file:
file.write(json.dumps(
{
"rules": {
rule_num: asdict(block) for rule_num, block in self.rulesets.items()
},
"sprites": self.sprite_types
},
indent=4))
self.export_file_manager.submitted_content = None
self.add_widget(arcade.gui.UIMessageBox(message_text="Rules and Sprites exported successfully!", width=self.window.width * 0.5, height=self.window.height * 0.25))
if not self.mode == "simulation": if not self.mode == "simulation":
return return
@@ -239,39 +358,35 @@ 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)
# In the new version, a DO rule's dependencies are the ruleset itself which trigger it for rule_num, rule in self.rulesets.items():
# Since there could be multiple IFs that depend on each other, we need to get the entrypoint values first and then interpret the tree. if not rule.rule_type == "trigger" or not trigger == rule.rule:
event_args = trigger_args continue
self.recursive_execute_rule(rule, trigger_args)
if_rule_values = {} has_collision_rules = any(
rule.rule_type == "trigger" and rule.rule == "collision"
for if_rule in self.if_rules: for rule in self.rulesets.values()
if_rule_dict = IF_RULES[if_rule[0]] )
if "shape_type" in if_rule_dict["user_vars"]:
is_true = False
for shape in self.shapes:
if is_true:
break
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})
is_true = self.check_rule(if_rule_dict, if_rule[1], trigger_args)
if_rule_values[if_rule[2]] = is_true
else:
event_args = trigger_args.copy()
if_rule_values[if_rule[2]] = self.check_rule(if_rule_dict, if_rule[1], trigger_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)
@@ -282,7 +397,7 @@ 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):
@@ -309,12 +424,16 @@ class Game(arcade.gui.UIView):
self.rules_box.camera.zoom *= 1 + scroll_y * 0.1 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()
@@ -329,29 +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", align_y=-self.window.height * 0.025)
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", align_y=-self.window.height * 0.025)
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.rulesets, self.if_rules = self.rules_box.get_rulesets() 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):

View File

@@ -1,83 +1,44 @@
from utils.constants import ( from utils.constants import (
DO_RULES, DO_RULES,
IF_RULES, IF_RULES,
NON_COMPATIBLE_WHEN,
NON_COMPATIBLE_DO_WHEN,
VAR_NAMES,
VAR_DEFAULT,
TRIGGER_RULES, TRIGGER_RULES,
FOR_RULES, FOR_RULES,
NEEDS_SHAPE,
PROVIDES_SHAPE,
button_style, button_style,
slider_style,
dropdown_style,
DO_COLOR, DO_COLOR,
IF_COLOR, IF_COLOR,
FOR_COLOR, FOR_COLOR,
TRIGGER_COLOR TRIGGER_COLOR,
RULE_DEFAULTS,
VAR_TYPES
) )
from typing import List from typing import List
from utils.preload import button_texture, button_hovered_texture, trash_bin from utils.preload import button_texture, button_hovered_texture, trash_bin
from arcade.gui.experimental.scroll_area import UIScrollArea, UIScrollBar from arcade.gui.experimental.scroll_area import UIScrollArea, UIScrollBar
from dataclasses import dataclass, field from dataclasses import dataclass, field
import arcade, arcade.gui, pyglet, random import arcade, arcade.gui, pyglet, random, re
IF_KEYS = tuple(IF_RULES.keys()) def get_rule_dict(rule_type):
DO_KEYS = tuple(DO_RULES.keys())
BAD_WHEN = {tuple(sorted(pair)) for pair in NON_COMPATIBLE_WHEN}
BAD_DO_WHEN = {tuple(pair) for pair in NON_COMPATIBLE_DO_WHEN}
def generate_rule(rule_type):
if rule_type == "if": if rule_type == "if":
return random.choice(IF_KEYS) 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 random.choice(DO_KEYS) return DO_RULES
def get_rule_description(rule_type, rule): @dataclass
if rule_type == "if": class VarBlock:
return IF_RULES[rule]["description"] x: float
if rule_type == "for": y: float
return FOR_RULES[rule]["description"] label: str
if rule_type == "trigger": var_type: str
return TRIGGER_RULES[rule]["description"] connected_rule_num: str
if rule_type == "do": value: str | int
return DO_RULES[rule]["description"]
def per_widget_height(height, widget_count):
return height // widget_count
def get_rule_defaults(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()
}
@dataclass @dataclass
class Block: class Block:
@@ -87,17 +48,18 @@ class Block:
rule_type: str rule_type: str
rule: str rule: str
rule_num: int rule_num: int
rule_values: dict[str, int | str] vars: List["VarBlock"] = field(default_factory=list)
children: List["Block"] = field(default_factory=list) children: List["Block"] = field(default_factory=list)
class BlockRenderer: class BlockRenderer:
def __init__(self, blocks: List[Block], indent: int = 10): def __init__(self, blocks: List[Block], indent: int = 12):
self.blocks = blocks self.blocks = blocks
self.indent = indent self.indent = indent
self.shapes = pyglet.graphics.Batch() self.shapes = pyglet.graphics.Batch()
self.shapes_by_rule_num = {} self.shapes_by_rule_num = {}
self.text_objects = [] self.text_objects = []
self.text_by_rule_num = {} self.text_by_rule_num = {}
self.var_widgets = {}
self.refresh() self.refresh()
def refresh(self): def refresh(self):
@@ -113,12 +75,89 @@ class BlockRenderer:
self.shapes_by_rule_num = {} self.shapes_by_rule_num = {}
self.text_objects = [] self.text_objects = []
self.text_by_rule_num = {} self.text_by_rule_num = {}
self.var_widgets = {}
for b in self.blocks.values(): for b in self.blocks.values():
self._build_block(b, b.x, b.y) self._build_block(b, b.x, b.y)
def _build_var_ui(self, var: VarBlock, x: int, y: int, rule_num: int) -> tuple:
var_width = max(60, len(str(var.value)) * 8 + 20)
var_height = 24
var_color = (255, 255, 255)
var_rect = pyglet.shapes.BorderedRectangle(
x, y - var_height // 2, var_width, var_height,
2, var_color, arcade.color.BLACK, batch=self.shapes
)
var_text = pyglet.text.Label(
text=str(var.value),
x=x + var_width // 2,
y=y,
color=arcade.color.BLACK,
font_size=10,
anchor_x='center',
anchor_y='center'
)
if rule_num not in self.shapes_by_rule_num:
self.shapes_by_rule_num[rule_num] = []
if rule_num not in self.text_by_rule_num:
self.text_by_rule_num[rule_num] = []
if rule_num not in self.var_widgets:
self.var_widgets[rule_num] = []
self.shapes_by_rule_num[rule_num].append(var_rect)
self.text_by_rule_num[rule_num].append(var_text)
self.text_objects.append(var_text)
self.var_widgets[rule_num].append({
'var': var,
'rect': var_rect,
'text': var_text,
'x': x,
'y': y,
'width': var_width,
'height': var_height
})
return var_width, var_height
def _build_block_with_vars(self, b: Block, x: int, y: int) -> None:
lx, ly = x, y - 42
current_x = lx + 14
current_y = ly + 28
pattern = r'(?:^| )([a-z])(?= |$)'
parts = re.split(pattern, b.label)
var_index = 0
for i, part in enumerate(parts):
if i % 2 == 0:
if part:
text_obj = pyglet.text.Label(
text=part,
x=current_x,
y=current_y - 3,
color=arcade.color.BLACK,
font_size=12,
weight="bold"
)
self.text_objects.append(text_obj)
self.text_by_rule_num[b.rule_num].append(text_obj)
current_x += len(part) * 12
else:
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: def _build_block(self, b: Block, x: int, y: int) -> int:
is_wrap = b.rule_type != "do" is_wrap = b.rule_type != "do"
h, w = 42, 280 h, w = 42, 380
if b.rule_type == "if": if b.rule_type == "if":
color = IF_COLOR color = IF_COLOR
@@ -129,7 +168,7 @@ class BlockRenderer:
elif b.rule_type == "for": elif b.rule_type == "for":
color = FOR_COLOR color = FOR_COLOR
lx, ly = x, y - h lx, ly = x, y - h
if b.rule_num not in self.shapes_by_rule_num: if b.rule_num not in self.shapes_by_rule_num:
self.shapes_by_rule_num[b.rule_num] = [] self.shapes_by_rule_num[b.rule_num] = []
@@ -139,21 +178,31 @@ class BlockRenderer:
rect = pyglet.shapes.BorderedRectangle(lx, ly, w, h, 2, color, arcade.color.BLACK, batch=self.shapes) 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) self.shapes_by_rule_num[b.rule_num].append(rect)
text_obj = pyglet.text.Label(text=b.label, x=lx + 10, y=ly + 20, color=arcade.color.BLACK, font_size=12, weight="bold") if b.vars:
self.text_objects.append(text_obj) self._build_block_with_vars(b, x, y)
self.text_by_rule_num[b.rule_num].append(text_obj) 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)
ny = ly next_y = ly
if is_wrap: if is_wrap:
iy = ny iy = next_y
for child in b.children: for child in b.children:
child.x = lx + self.indent + 5 child.x = lx + self.indent + 5
child.y = iy - 2 child.y = iy
iy = self._build_block(child, lx + self.indent + 5, iy - 2) iy = self._build_block(child, lx + self.indent + 5, iy)
bar_h = ny - iy bar_h = next_y - iy
bar_filled = pyglet.shapes.Rectangle(lx + 2, iy + 2, self.indent, bar_h, color, batch=self.shapes) bar_filled = pyglet.shapes.Rectangle(lx + 2, iy + 2, self.indent, bar_h, color, batch=self.shapes)
line1 = pyglet.shapes.Line(lx, ny, lx, iy, 2, arcade.color.BLACK, 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) 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]) self.shapes_by_rule_num[b.rule_num].extend([bar_filled, line1, bottom])
@@ -161,19 +210,36 @@ class BlockRenderer:
return iy - 24 return iy - 24
else: else:
for child in b.children: for child in b.children:
ny = self._build_block(child, lx, ny) child.x = lx
return ny child.y = next_y
ly = self._build_block(child, lx, next_y)
return ly - 16
def move_block(self, x, y, rule_num): 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]: for element in self.shapes_by_rule_num[rule_num] + self.text_by_rule_num[rule_num]:
element.x += x element.x += x
element.y += y 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) block = self._find_block(rule_num)
for child in block.children: for child in block.children:
self.move_block(x, y, child.rule_num) 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): def _find_block(self, rule_num):
if rule_num in self.blocks: if rule_num in self.blocks:
return self.blocks[rule_num] return self.blocks[rule_num]
@@ -198,6 +264,101 @@ class BlockRenderer:
for t in self.text_objects: for t in self.text_objects:
t.draw() 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): class RuleUI(arcade.gui.UIAnchorLayout):
def __init__(self, window: arcade.Window): def __init__(self, window: arcade.Window):
super().__init__(size_hint=(1, 0.875)) super().__init__(size_hint=(1, 0.875))
@@ -205,6 +366,7 @@ class RuleUI(arcade.gui.UIAnchorLayout):
self.window = window self.window = window
self.current_rule_num = 0 self.current_rule_num = 0
self.rule_values = {} self.rule_values = {}
self.var_edit_dialog = None
self.rulesets: dict[int, Block] = {} self.rulesets: dict[int, Block] = {}
@@ -230,7 +392,7 @@ class RuleUI(arcade.gui.UIAnchorLayout):
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.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 = UIScrollArea(size_hint=(0.95, 1)) # center on screen
self.scroll_area.scroll_speed = -50 self.scroll_area.scroll_speed = 0
self.create_sidebar.add(self.scroll_area) self.create_sidebar.add(self.scroll_area)
self.scrollbar = UIScrollBar(self.scroll_area) self.scrollbar = UIScrollBar(self.scroll_area)
@@ -239,74 +401,49 @@ class RuleUI(arcade.gui.UIAnchorLayout):
self.create_box = self.scroll_area.add(arcade.gui.UIBoxLayout(space_between=10)) self.create_box = self.scroll_area.add(arcade.gui.UIBoxLayout(space_between=10))
self.create_box.add(arcade.gui.UISpace(height=self.window.height / 100)) self.add_rule_create_box("trigger")
self.create_box.add(arcade.gui.UILabel(text="Trigger Rules", font_size=18)) self.add_rule_create_box("if")
self.create_box.add(arcade.gui.UISpace(height=self.window.height / 200)) self.add_rule_create_box("do")
for trigger_rule, trigger_rule_data in TRIGGER_RULES.items(): self.add_rule_create_box("for")
create_button = self.create_box.add(arcade.gui.UITextureButton(text=trigger_rule_data["description"].format_map({
"a": "a",
"b": "b",
"c": "c"
}), width=self.window.width * 0.125, 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, trigger_rule=trigger_rule: self.add_rule("trigger", trigger_rule)
self.create_box.add(arcade.gui.UISpace(height=self.window.height / 100))
self.create_box.add(arcade.gui.UILabel(text="IF Rules", font_size=18))
self.create_box.add(arcade.gui.UISpace(height=self.window.height / 200))
for if_rule, if_rule_data in IF_RULES.items():
create_button = self.create_box.add(arcade.gui.UITextureButton(text=if_rule_data["description"].format_map({
"a": "a",
"b": "b",
"c": "c"
}), 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, if_rule=if_rule: self.add_rule("if", if_rule)
self.create_box.add(arcade.gui.UISpace(height=self.window.height / 100))
self.create_box.add(arcade.gui.UILabel(text="DO Rules", font_size=18))
self.create_box.add(arcade.gui.UISpace(height=self.window.height / 200))
for do_rule, do_rule_data in DO_RULES.items():
create_button = self.create_box.add(arcade.gui.UITextureButton(text=do_rule_data["description"].format_map({
"a": "a",
"b": "b",
"c": "c"
}), 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, do_rule=do_rule: self.add_rule("do", do_rule)
self.create_box.add(arcade.gui.UISpace(height=self.window.height / 100))
self.create_box.add(arcade.gui.UILabel(text="For Rules", font_size=18))
self.create_box.add(arcade.gui.UISpace(height=self.window.height / 200))
for for_rule, for_rule_data in FOR_RULES.items():
create_button = self.create_box.add(arcade.gui.UITextureButton(text=for_rule_data["description"].format_map({
"a": "a",
"b": "b",
"c": "c"
}), 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, for_rule=for_rule: self.add_rule("for", for_rule)
self.trash_spritelist = arcade.SpriteList() self.trash_spritelist = arcade.SpriteList()
self.trash_sprite = trash_bin self.trash_sprite = trash_bin
self.trash_sprite.scale = 0.5 self.trash_sprite.scale = 0.5
self.trash_sprite.position = (self.window.width * 0.9, self.window.height * 0.2) self.trash_sprite.position = (self.window.width * 0.9, self.window.height * 0.2)
self.trash_spritelist.append(self.trash_sprite) self.trash_spritelist.append(self.trash_sprite)
def get_rulesets(self): def add_rule_create_box(self, rule_type):
# TODO: remove this self.create_box.add(arcade.gui.UISpace(height=self.window.height / 100))
return [], [] 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): def generate_pos(self):
return random.randint( return random.randint(
self.window.width * 0.1, int(self.window.width * 0.9) self.window.width * 0.1, int(self.window.width * 0.9)
), random.randint(self.window.height * 0.1, int(self.window.height * 0.7)) ), random.randint(self.window.height * 0.1, int(self.window.height * 0.7))
def add_rule(self, rule_type, force=None): def add_rule(self, rule_type, rule):
rule = force or generate_rule(rule_type) rule_dict = get_rule_dict(rule_type)[rule]
rule_box = Block( rule_box = Block(
*self.generate_pos(), *self.generate_pos(),
get_rule_description(rule_type, rule), RULE_DEFAULTS[rule_type][rule][0],
rule_type, rule_type,
rule, rule,
self.current_rule_num, 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"])
],
[] []
) )
@@ -323,11 +460,14 @@ class RuleUI(arcade.gui.UIAnchorLayout):
self.trash_spritelist.draw() self.trash_spritelist.draw()
def drag_n_drop_check(self, blocks): def drag_n_drop_check(self, blocks):
if self.dragged_rule_ui.rule_type == "trigger":
return
for block in blocks: for block in blocks:
if block == self.dragged_rule_ui: if block == self.dragged_rule_ui or (self.dragged_rule_ui.rule in NEEDS_SHAPE and block.rule not in PROVIDES_SHAPE):
continue continue
if arcade.LBWH(block.x, block.y - 44, 280, 44).intersection(arcade.LBWH(self.dragged_rule_ui.x, self.dragged_rule_ui.y - 44, 280, 44)): 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) block.children.append(self.dragged_rule_ui)
del self.rulesets[self.dragged_rule_ui.rule_num] del self.rulesets[self.dragged_rule_ui.rule_num]
self.block_renderer.refresh() self.block_renderer.refresh()
@@ -351,7 +491,7 @@ class RuleUI(arcade.gui.UIAnchorLayout):
continue continue
projected_vec = self.camera.unproject((event.x, event.y)) projected_vec = self.camera.unproject((event.x, event.y))
if arcade.LBWH(block.x, block.y - 44, 280, 44).point_in_rect((projected_vec.x, projected_vec.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 if block not in list(self.rulesets.values()): # its children
self.remove_from_parent(block, list(self.rulesets.values())) self.remove_from_parent(block, list(self.rulesets.values()))
self.block_renderer.refresh() self.block_renderer.refresh()
@@ -361,6 +501,10 @@ class RuleUI(arcade.gui.UIAnchorLayout):
self.press_check(event, block.children) self.press_check(event, block.children)
def on_event(self, event): def on_event(self, event):
if self.var_edit_dialog:
super().on_event(event)
return
super().on_event(event) super().on_event(event)
if isinstance(event, arcade.gui.UIMouseDragEvent): if isinstance(event, arcade.gui.UIMouseDragEvent):
@@ -371,13 +515,33 @@ class RuleUI(arcade.gui.UIAnchorLayout):
self.block_renderer.move_block(event.dx, event.dy, self.dragged_rule_ui.rule_num) self.block_renderer.move_block(event.dx, event.dy, self.dragged_rule_ui.rule_num)
elif isinstance(event, arcade.gui.UIMousePressEvent): 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())) self.press_check(event, list(self.rulesets.values()))
elif isinstance(event, arcade.gui.UIMouseReleaseEvent): elif isinstance(event, arcade.gui.UIMouseReleaseEvent):
if self.dragged_rule_ui: if self.dragged_rule_ui:
block_vec = self.camera.unproject((self.dragged_rule_ui.x, self.dragged_rule_ui.y)) 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_vec.x, block_vec.y, 280, 44)) and not self.trash_sprite._current_keyframe_index == self.trash_sprite.animation.num_frames - 1:
del self.rulesets[self.dragged_rule_ui.rule_num] 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.dragged_rule_ui = None
self.block_renderer.refresh() self.block_renderer.refresh()
return return
@@ -386,11 +550,29 @@ class RuleUI(arcade.gui.UIAnchorLayout):
self.dragged_rule_ui = None 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): def on_update(self, dt):
if self.dragged_rule_ui: if self.dragged_rule_ui:
block_vec = self.camera.unproject((self.dragged_rule_ui.x, self.dragged_rule_ui.y)) 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_vec.x, block_vec.y, 280, 44)) and not self.trash_sprite._current_keyframe_index == self.trash_sprite.animation.num_frames - 1: 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() self.trash_sprite.update_animation()
else: else:
self.trash_sprite.time = 0 self.trash_sprite.time = 0
self.trash_sprite.update_animation() self.trash_sprite.update_animation()

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,12 +106,10 @@ 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):
return self.width return self.width
@@ -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,12 +1,21 @@
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
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"] 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) TRIGGER_COLOR = (255, 204, 102)
DO_COLOR = (102, 178, 255) DO_COLOR = (102, 178, 255)
@@ -29,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,
@@ -40,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),
@@ -49,39 +67,37 @@ VAR_OPTIONS = {
"comparison": COMPARISONS "comparison": COMPARISONS
} }
VAR_TYPES = {
"shape_type": "Shape Type",
"target_type": "Target Type",
"variable": "Variable",
"color": "Color",
"size": "Size",
"key_input": "Key Input",
"comparison": "Comparison"
}
TRIGGER_RULES = { TRIGGER_RULES = {
"every_update": { "every_update": {
"key": "every_update", "key": "every_update",
"user_vars": [],
"vars": [],
"description": "Every Update", "description": "Every Update",
"func": lambda *v: True
}, },
"start": { "start": {
"key": "start", "key": "start",
"user_vars": [],
"vars": [],
"description": "On Game Start", "description": "On Game Start",
"func": lambda *v: True
}, },
"on_input": { "on_input": {
"key": "on_input", "key": "on_input",
"user_vars": ["key_input"],
"vars": ["key_input", "event_key"],
"description": "IF {a} key is pressed", "description": "IF {a} key is pressed",
}, "func": lambda *v: v[0] == v[1]
"x_position_compare": {
"key": "x_position_compare",
"description": "IF X for {a} shape is {b} {c}",
"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]}")
},
"y_position_compare": {
"key": "y_position_compare",
"description": "IF Y for {a} shape is {b} {c}",
"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]}")
},
"size_compare": {
"key": "size_compare",
"description": "IF {a} shape size is {b} {c}",
"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]}")
}, },
"spawns": { "spawns": {
"key": "spawns", "key": "spawns",
@@ -97,20 +113,6 @@ TRIGGER_RULES = {
"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]
}, },
"x_velocity_changes": {
"key": "x_velocity_changes",
"description": "IF {a} shape X velocity changes",
"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",
"user_vars": ["shape_type"],
"vars": ["shape_type", "event_shape_type"],
"func": lambda *v: v[0] == v[1]
},
"color_changes": { "color_changes": {
"key": "color_changes", "key": "color_changes",
"description": "IF {a} shape color changes", "description": "IF {a} shape color changes",
@@ -125,13 +127,6 @@ TRIGGER_RULES = {
"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}",
"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}",
@@ -139,11 +134,34 @@ TRIGGER_RULES = {
"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])
}, },
"on_left_click": {
"key": "on_left_click",
"description": "IF you left click",
"user_vars": [],
"vars": [],
"func": lambda *v: True
},
"on_right_click": {
"key": "on_right_click",
"description": "IF you right click",
"user_vars": [],
"vars": [],
"func": lambda *v: True
},
"on_mouse_move": {
"key": "on_mouse_move",
"description": "IF mouse moves",
"user_vars": [],
"vars": [],
"func": lambda *v: True
},
} }
FOR_RULES = { FOR_RULES = {
"every_shape": { "every_shape": {
"key": "every_shape", "key": "every_shape",
"user_vars": [],
"vars": [],
"description": "For every shape", "description": "For every shape",
} }
} }
@@ -154,35 +172,35 @@ IF_RULES = {
"description": "IF X is {a} {b}", "description": "IF X is {a} {b}",
"user_vars": ["comparison", "variable"], "user_vars": ["comparison", "variable"],
"vars": ["comparison", "variable", "shape_x"], "vars": ["comparison", "variable", "shape_x"],
"func": lambda *v: eval(f"{v[2]} {v[0]} {v[1]}") "func": lambda *v: OPS[v[0]](v[2], v[1])
}, },
"y_position_compare": { "y_position_compare": {
"key": "y_position_compare", "key": "y_position_compare",
"description": "IF Y is {a} {b}", "description": "IF Y is {a} {b}",
"user_vars": ["comparison", "variable"], "user_vars": ["comparison", "variable"],
"vars": ["comparison", "variable", "shape_y"], "vars": ["comparison", "variable", "shape_y"],
"func": lambda *v: eval(f"{v[2]} {v[0]} {v[1]}") "func": lambda *v: OPS[v[0]](v[2], v[1])
}, },
"size_compare": { "size_compare": {
"key": "size_compare", "key": "size_compare",
"description": "IF size is {a} {b}", "description": "IF size is {a} {b}",
"user_vars": ["comparison", "variable"], "user_vars": ["comparison", "variable"],
"vars": ["comparison", "variable", "shape_size"], "vars": ["comparison", "variable", "shape_size"],
"func": lambda *v: eval(f"{v[2]} {v[0]} {v[1]}") "func": lambda *v: OPS[v[0]](v[2], v[1])
}, },
"x_velocity_compare": { "x_velocity_compare": {
"key": "x_velocity_compare", "key": "x_velocity_compare",
"description": "IF X velocity is {a} {b}", "description": "IF X velocity is {a} {b}",
"user_vars": ["comparison", "variable"], "user_vars": ["comparison", "variable"],
"vars": ["comparison", "variable", "shape_x_velocity"], "vars": ["comparison", "variable", "shape_x_velocity"],
"func": lambda *v: eval(f"{v[2]} {v[0]} {v[1]}") "func": lambda *v: OPS[v[0]](v[2], v[1])
}, },
"y_velocity_compare": { "y_velocity_compare": {
"key": "y_velocity_compare", "key": "y_velocity_compare",
"description": "IF Y velocity is {a} {b}", "description": "IF Y velocity is {a} {b}",
"user_vars": ["comparison", "variable"], "user_vars": ["comparison", "variable"],
"vars": ["comparison", "variable", "shape_y_velocity"], "vars": ["comparison", "variable", "shape_y_velocity"],
"func": lambda *v: eval(f"{v[2]} {v[0]} {v[1]}") "func": lambda *v: OPS[v[0]](v[2], v[1])
}, },
"color_is": { "color_is": {
"key": "color_is", "key": "color_is",
@@ -191,111 +209,15 @@ IF_RULES = {
"vars": ["color", "shape_color"], "vars": ["color", "shape_color"],
"func": lambda *v: v[0] == v[1] "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]
},
} }
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": {
"key": "change_x", "key": "change_x",
@@ -369,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}",
@@ -402,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