Files
chaos-protocol/game/rules.py

395 lines
15 KiB
Python

from utils.constants import (
DO_RULES,
IF_RULES,
NON_COMPATIBLE_WHEN,
NON_COMPATIBLE_DO_WHEN,
VAR_NAMES,
VAR_DEFAULT,
TRIGGER_RULES,
FOR_RULES,
button_style,
DO_COLOR,
IF_COLOR,
FOR_COLOR,
TRIGGER_COLOR
)
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
IF_KEYS = tuple(IF_RULES.keys())
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":
return random.choice(IF_KEYS)
elif rule_type == "do":
return random.choice(DO_KEYS)
def get_rule_description(rule_type, rule):
if rule_type == "if":
return IF_RULES[rule]["description"]
if rule_type == "for":
return FOR_RULES[rule]["description"]
if rule_type == "trigger":
return TRIGGER_RULES[rule]["description"]
if rule_type == "do":
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
class Block:
x: float
y: float
label: str
rule_type: str
rule: str
rule_num: int
rule_values: dict[str, int | str]
children: List["Block"] = field(default_factory=list)
class BlockRenderer:
def __init__(self, blocks: List[Block], indent: int = 10):
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.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 = {}
for b in self.blocks.values():
self._build_block(b, b.x, b.y)
def _build_block(self, b: Block, x: int, y: int) -> int:
is_wrap = b.rule_type != "do"
h, w = 42, 280
if b.rule_type == "if":
color = IF_COLOR
elif b.rule_type == "trigger":
color = TRIGGER_COLOR
elif b.rule_type == "do":
color = DO_COLOR
elif b.rule_type == "for":
color = FOR_COLOR
lx, ly = x, y - h
if b.rule_num not in self.shapes_by_rule_num:
self.shapes_by_rule_num[b.rule_num] = []
if b.rule_num not in self.text_by_rule_num:
self.text_by_rule_num[b.rule_num] = []
rect = pyglet.shapes.BorderedRectangle(lx, ly, w, h, 2, color, arcade.color.BLACK, batch=self.shapes)
self.shapes_by_rule_num[b.rule_num].append(rect)
text_obj = pyglet.text.Label(text=b.label, x=lx + 10, 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
if is_wrap:
iy = ny
for child in b.children:
child.x = lx + self.indent + 5
child.y = iy - 2
iy = self._build_block(child, lx + self.indent + 5, iy - 2)
bar_h = ny - iy
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)
bottom = pyglet.shapes.BorderedRectangle(lx, iy - 8, w, 24, 2, color, arcade.color.BLACK, batch=self.shapes)
self.shapes_by_rule_num[b.rule_num].extend([bar_filled, line1, bottom])
return iy - 24
else:
for child in b.children:
ny = self._build_block(child, lx, ny)
return ny
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
block = self._find_block(rule_num)
for child in block.children:
self.move_block(x, y, child.rule_num)
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 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.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 = -50
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.create_box.add(arcade.gui.UISpace(height=self.window.height / 100))
self.create_box.add(arcade.gui.UILabel(text="Trigger Rules", font_size=18))
self.create_box.add(arcade.gui.UISpace(height=self.window.height / 200))
for trigger_rule, trigger_rule_data in TRIGGER_RULES.items():
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_sprite = trash_bin
self.trash_sprite.scale = 0.5
self.trash_sprite.position = (self.window.width * 0.9, self.window.height * 0.2)
self.trash_spritelist.append(self.trash_sprite)
def get_rulesets(self):
# TODO: remove this
return [], []
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, force=None):
rule = force or generate_rule(rule_type)
rule_box = Block(
*self.generate_pos(),
get_rule_description(rule_type, rule),
rule_type,
rule,
self.current_rule_num,
{},
[]
)
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):
for block in blocks:
if block == self.dragged_rule_ui:
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)):
block.children.append(self.dragged_rule_ui)
del self.rulesets[self.dragged_rule_ui.rule_num]
self.block_renderer.refresh()
break
else:
self.drag_n_drop_check(block.children)
def remove_from_parent(self, block_to_remove, parents):
for parent in parents:
if block_to_remove in parent.children:
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, 280, 44).point_in_rect((projected_vec.x, projected_vec.y)):
if block not in list(self.rulesets.values()): # its children
self.remove_from_parent(block, list(self.rulesets.values()))
self.block_renderer.refresh()
self.dragged_rule_ui = block
break
else:
self.press_check(event, block.children)
def on_event(self, event):
super().on_event(event)
if isinstance(event, arcade.gui.UIMouseDragEvent):
if event.buttons == arcade.MOUSE_BUTTON_LEFT:
if self.dragged_rule_ui is not None:
self.dragged_rule_ui.x += event.dx
self.dragged_rule_ui.y += event.dy
self.block_renderer.move_block(event.dx, event.dy, self.dragged_rule_ui.rule_num)
elif isinstance(event, arcade.gui.UIMousePressEvent):
self.press_check(event, list(self.rulesets.values()))
elif isinstance(event, arcade.gui.UIMouseReleaseEvent):
if self.dragged_rule_ui:
block_vec = self.camera.unproject((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]
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 on_update(self, dt):
if self.dragged_rule_ui:
block_vec = self.camera.unproject((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:
self.trash_sprite.update_animation()
else:
self.trash_sprite.time = 0
self.trash_sprite.update_animation()