Compare commits

..

2 Commits

4 changed files with 85 additions and 44 deletions

View File

@@ -1,6 +1,9 @@
Chaos Protocol is a simulation game where you have a rule engine and objects, which you can apply rules to! By default, the game launches with random rules.
**Chaos Protocol** is a simulation game where you have a rule engine and objects, which you can apply rules to! By default, the game launches with random rules.
Basically a framework of sorts, which can even be random!
The project is a huge WIP! You can't do much yet, but you have basic rules and simulation.
The game has Scratch-like blocks and conditions, and everything you probably need for a simple game.
The problem: it hasn't been well tested.
[![Demo Video](https://img.youtube.com/vi/iMB4mmjTIB4/hqdefault.jpg)](https://youtu.be/iMB4mmjTIB4)
## Speed is a big problem of the project, especially collision detections, which are very expensive.
I am not using spatial hashing right now, and i had no time to optimize the code. I'm sorry.
[![Demo Video](https://img.youtube.com/vi/iPXQfllqsvs/hqdefault.jpg)](https://youtu.be/iPXQfllqsvs)

View File

@@ -55,8 +55,8 @@ class FileManager(arcade.gui.UIAnchorLayout):
self.submit_button.visible = self.mode == "export"
def submit(self, content):
self.submitted_content = content if self.mode == "import" else f"{content}/{self.filename_input.text}"
self.submitted_content = content if self.mode == "import" else os.path.join(content, self.filename_input.text)
def get_content(self, directory):
if not directory in self.content_cache or time.perf_counter() - self.content_cache[directory][-1] >= 30:
try:
@@ -113,11 +113,11 @@ class FileManager(arcade.gui.UIAnchorLayout):
for file in self.get_content(self.current_directory):
self.file_buttons.append(self.files_box.add(arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text=file, style=button_style, width=self.filemanager_width / 1.5, height=self.filemanager_height * 0.05,)))
if os.path.isdir(f"{self.current_directory}/{file}"):
self.file_buttons[-1].on_click = lambda event, directory=f"{self.current_directory}/{file}": self.change_directory(directory)
if os.path.isdir(os.path.join(self.current_directory, file)):
self.file_buttons[-1].on_click = lambda event, directory=os.path.join(self.current_directory, file): self.change_directory(directory)
else:
self.file_buttons[-1].on_click = lambda event, file=f"{self.current_directory}/{file}": self.submit(file)
self.file_buttons[-1].on_click = lambda event, file=os.path.join(self.current_directory, file): self.submit(file)
def change_directory(self, directory):
if directory.startswith("//"): # Fix / paths
directory = directory[1:]

View File

@@ -6,7 +6,7 @@ from utils.preload import SPRITE_TEXTURES, button_texture, button_hovered_textur
from utils.constants import button_style, DO_RULES, IF_RULES, SPRITES, ALLOWED_INPUT
from game.rules import RuleUI, Block, VarBlock
from game.sprites import BaseShape, Rectangle, Circle, Triangle, TexturedRectangle
from game.sprites import BaseRectangle, BaseShape, Rectangle, Circle, Triangle, TexturedRectangle
from game.file_manager import FileManager
class Game(arcade.gui.UIView):
@@ -23,7 +23,11 @@ class Game(arcade.gui.UIView):
self.rules_box = RuleUI(self.window)
self.file_manager = FileManager(self.window.width * 0.95, self.window.height * 0.875, (0.95, 0.875), [".json"]).with_border()
self.import_file_manager = FileManager(self.window.width * 0.95, self.window.height * 0.875, (0.95, 0.875), [".json"]).with_border()
self.import_file_manager.change_mode("import")
self.export_file_manager = FileManager(self.window.width * 0.95, self.window.height * 0.875, (0.95, 0.875), [".json"]).with_border()
self.export_file_manager.change_mode("export")
self.ui_selector_box = self.anchor.add(arcade.gui.UIBoxLayout(vertical=False, space_between=self.window.width / 100), anchor_x="left", anchor_y="bottom", align_y=5, align_x=self.window.width / 100)
self.add_ui_selector("Simulation", lambda event: self.simulation())
@@ -40,6 +44,7 @@ class Game(arcade.gui.UIView):
self.rulesets = self.rules_box.rulesets
self.sprite_add_filemanager = FileManager(self.window.width * 0.9, self.window.height * 0.75, (0.9, 0.75), [".png", ".jpg", ".jpeg", ".bmp", ".gif"])
self.sprite_add_filemanager.change_mode("import")
self.sprite_add_ui = arcade.gui.UIBoxLayout(size_hint=(0.95, 0.9), space_between=10)
self.sprite_add_ui.add(arcade.gui.UILabel(text="Add Sprite", font_size=24, text_color=arcade.color.WHITE))
@@ -137,7 +142,7 @@ class Game(arcade.gui.UIView):
a = float(a)
if isinstance(shape, Circle):
shape.radius = a
elif isinstance(shape, Rectangle):
elif isinstance(shape, BaseRectangle):
shape.width = a
shape.height = a
elif isinstance(shape, Triangle):
@@ -167,10 +172,9 @@ class Game(arcade.gui.UIView):
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(shape_type, img=SPRITE_TEXTURES.get(shape_type, SPRITE_TEXTURES["rectangle"]), x=x, y=y, batch=self.shape_batch))
self.shapes.append(TexturedRectangle(pyglet.image.load(self.sprite_types[shape_type]), x, y, batch=self.shape_batch, shape_type=shape_type))
shape = self.shapes[-1]
self.triggered_events.append(["spawn", {"event_shape_type": shape.shape_type, "shape_size": shape.shape_size, "shape_x": shape.x, "shape_y": shape.y, "shape": shape, "shape_color": shape.shape_color}])
def add_sprite(self):
@@ -283,12 +287,20 @@ class Game(arcade.gui.UIView):
recurse(block)
return max_num
def dict_to_block(self, block_dict):
kwargs = block_dict.copy()
kwargs["children"] = [self.dict_to_block(child) for child in block_dict.get("children", [])]
kwargs["vars"] = [VarBlock(**var) for var in block_dict.get("vars", [])]
return Block(**kwargs)
def on_update(self, delta_time):
if self.mode == "import" and self.file_manager.submitted_content:
with open(self.file_manager.submitted_content, "r") as file:
if self.mode == "import" and self.import_file_manager.submitted_content:
with open(self.import_file_manager.submitted_content, "r") as file:
data = json.load(file)
self.import_file_manager.submitted_content = None
self.triggered_events = []
self.rulesets = {}
@@ -296,26 +308,34 @@ class Game(arcade.gui.UIView):
self.add_widget(arcade.gui.UIMessageBox(message_text="Invalid file. Could not import rules.", width=self.window.width * 0.5, height=self.window.height * 0.25))
return
for rule_num, ruleset in data["rulesets"].items():
kwargs = ruleset
kwargs["children"] = [Block(**child) for child in ruleset["children"]]
kwargs["vars"] = [VarBlock(**var) for var in ruleset["vars"]]
block = Block(**kwargs)
self.rulesets[rule_num] = block
for rule_num, ruleset in data["rules"].items():
block = self.dict_to_block(ruleset)
self.rulesets[int(rule_num)] = block
self.sprite_types = data.get("sprites", SPRITES)
self.sprite_types = data["sprites"]
for sprite_name, sprite_path in self.sprite_types.items():
if not sprite_name in SPRITE_TEXTURES:
SPRITE_TEXTURES[sprite_name] = arcade.load_texture(sprite_path)
SPRITES[sprite_name] = sprite_path
self.sprites_grid.clear()
for n, shape in enumerate(SPRITES):
row, col = n % 8, n // 8
box = self.sprites_grid.add(arcade.gui.UIBoxLayout(), row=row, column=col)
box.add(arcade.gui.UILabel(text=shape, font_size=16, text_color=arcade.color.WHITE))
box.add(arcade.gui.UIImage(texture=SPRITE_TEXTURES[shape], width=self.window.width / 15, height=self.window.width / 15))
self.rules_box.rulesets = self.rulesets
self.rules_box.block_renderer.blocks = self.rulesets
self.rules_box.current_rule_num = self.get_max_rule_num() + 1
self.rules_box.block_renderer.refresh()
self.rules()
if self.mode == "export" and self.file_manager.submitted_content:
with open(self.file_manager.submitted_content, "w") as file:
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": {
@@ -326,6 +346,8 @@ class Game(arcade.gui.UIView):
},
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":
@@ -342,13 +364,29 @@ class Game(arcade.gui.UIView):
self.recursive_execute_rule(rule, trigger_args)
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}])
has_collision_rules = any(
rule.rule_type == "trigger" and rule.rule == "collision"
for rule in self.rulesets.values()
)
for shape in self.shapes:
shape.update(self.x_gravity, self.y_gravity)
if has_collision_rules:
for i, shape in enumerate(self.shapes):
for shape_b in self.shapes[i+1:]:
if shape.check_collision(shape_b):
self.triggered_events.append(["collision", {
"event_a_type": shape.shape_type,
"event_b_type": shape_b.shape_type,
"shape_size": shape.shape_size,
"shape_x": shape.x,
"shape_y": shape.y,
"shape": shape,
"shape_color": shape.shape_color
}])
for shape in self.shapes[:]:
if shape.x < 0 or shape.x > self.window.width or shape.y < 0 or shape.y > self.window.height:
self.destroy(shape)
@@ -386,8 +424,10 @@ class Game(arcade.gui.UIView):
self.rules_box.camera.zoom *= 1 + scroll_y * 0.1
def disable_previous(self):
if self.mode in ["import", "export"]:
self.anchor.remove(self.file_manager)
if self.mode == "import":
self.anchor.remove(self.import_file_manager)
elif self.mode == "export":
self.anchor.remove(self.export_file_manager)
elif self.mode == "rules":
self.anchor.remove(self.rules_box)
elif self.mode == "sprites":
@@ -408,17 +448,13 @@ class Game(arcade.gui.UIView):
self.disable_previous()
self.mode = "export"
self.file_manager.change_mode("export")
self.anchor.add(self.file_manager, anchor_x="center", anchor_y="top", align_y=-self.window.height * 0.025)
self.anchor.add(self.export_file_manager, anchor_x="center", anchor_y="top", align_y=-self.window.height * 0.025)
def import_file(self):
self.disable_previous()
self.mode = "import"
self.file_manager.change_mode("import")
self.anchor.add(self.file_manager, anchor_x="center", anchor_y="top", align_y=-self.window.height * 0.025)
self.anchor.add(self.import_file_manager, anchor_x="center", anchor_y="top", align_y=-self.window.height * 0.025)
def sprites(self):
self.disable_previous()

View File

@@ -31,7 +31,7 @@ class BaseShape():
def check_collision(self, other):
if isinstance(other, Circle):
return self._collides_with_circle(other)
elif isinstance(other, Rectangle):
elif isinstance(other, BaseRectangle):
return self._collides_with_rectangle(other)
elif isinstance(other, Triangle):
return self._collides_with_triangle(other)
@@ -106,7 +106,6 @@ class Circle(pyglet.shapes.Circle, BaseShape):
return not (has_neg and has_pos)
class BaseRectangle(BaseShape):
def __init__(self):
super().__init__()
@@ -182,15 +181,18 @@ class BaseRectangle(BaseShape):
class Rectangle(pyglet.shapes.Rectangle, BaseRectangle):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
BaseRectangle.__init__(self)
super().__init__(*args, **kwargs)
self.shape_type = "rectangle"
class TexturedRectangle(pyglet.sprite.Sprite, BaseRectangle):
def __init__(self, shape_type, *args, **kwargs):
super().__init__(*args, **kwargs)
def __init__(self, img, x=0, y=0, *args, **kwargs):
BaseRectangle.__init__(self)
self.shape_type = shape_type
self.shape_type = kwargs.pop("shape_type", "textured_rectangle")
super().__init__(img, x, y, *args, **kwargs)
def update(self, x_gravity, y_gravity):
BaseShape.update(self, x_gravity, y_gravity)
class Triangle(pyglet.shapes.Triangle, BaseShape):
def __init__(self, *args, **kwargs):