Add saving/loading, update tutorial to include removal, add LABEL gate, draw ui over sprites and only activate camera for sprites, use rounded buttons in tools box

This commit is contained in:
csd4ni3l
2025-10-18 22:31:01 +02:00
parent 37a481aeab
commit be262bf253
5 changed files with 166 additions and 37 deletions

1
.gitignore vendored
View File

@@ -181,3 +181,4 @@ logs/
logs
settings.json
data.json
saves/

View File

@@ -3,7 +3,7 @@ import arcade, arcade.gui, random, datetime, os, json
from datetime import datetime
from utils.utils import cubic_bezier_points, get_gate_port_position, generate_task_text, multi_gate
from utils.constants import button_style, dropdown_style, LOGICAL_GATES, LEVELS, SINGLE_INPUT_LOGICAL_GATES
from utils.constants import button_style, LOGICAL_GATES, LEVELS, SINGLE_INPUT_LOGICAL_GATES
from utils.preload import button_texture, button_hovered_texture, logic_gate_textures
class LogicalGate(arcade.Sprite):
@@ -57,7 +57,7 @@ class Game(arcade.gui.UIView):
self.level_num = level_num
self.gates: list[LogicalGate] = []
self.gates: list[LogicalGate | arcade.gui.UIInputText] = []
self.connections = []
self.bezier_points = []
@@ -68,7 +68,7 @@ class Game(arcade.gui.UIView):
self.selected_output = None
self.anchor = self.add_widget(arcade.gui.UIAnchorLayout(size_hint=(1, 1)))
self.tools_box = self.anchor.add(arcade.gui.UIBoxLayout(space_between=5), anchor_x="right", anchor_y="center", align_x=-5)
self.tools_box = self.anchor.add(arcade.gui.UIBoxLayout(space_between=5), anchor_x="right", anchor_y="center", align_x=-10)
if not level_num == -1:
self.task_label = self.anchor.add(arcade.gui.UILabel(text=generate_task_text(LEVELS[level_num]), font_size=20, multiline=True), anchor_x="center", anchor_y="top", align_y=-15)
@@ -85,8 +85,8 @@ class Game(arcade.gui.UIView):
else:
self.task_label = self.anchor.add(arcade.gui.UILabel(text="Task: Have fun! Do whatever you want!", font_size=20), anchor_x="center", anchor_y="top", align_y=-15)
for gate in list(LOGICAL_GATES.keys()) + ["INPUT", "OUTPUT"]:
button = self.tools_box.add(arcade.gui.UIFlatButton(width=self.window.width * 0.125, height=self.window.height * 0.075, text=f"Create {gate} gate", style=dropdown_style))
for gate in list(LOGICAL_GATES.keys()) + ["INPUT", "OUTPUT", "LABEL"]:
button = self.tools_box.add(arcade.gui.UITextureButton(width=self.window.width * 0.125, height=self.window.height * 0.05, text=f"Create {gate} gate", style=button_style, texture=button_texture, texture_hovered=button_hovered_texture))
if "INPUT" in gate:
func = lambda: (random.randint(50, 200), random.randint(200, self.window.height - 100))
@@ -97,10 +97,17 @@ class Game(arcade.gui.UIView):
button.on_click = lambda event, func=func, gate=gate: self.add_gate(*func(), gate)
screenshot_button = self.tools_box.add(arcade.gui.UIFlatButton(width=self.window.width * 0.125, height=self.window.height * 0.075, text="Screenshot", style=dropdown_style))
screenshot_button = self.tools_box.add(arcade.gui.UITextureButton(width=self.window.width * 0.125, height=self.window.height * 0.05, text="Screenshot", style=button_style, texture=button_texture, texture_hovered=button_hovered_texture))
screenshot_button.on_click = lambda event: self.screenshot()
hide_button = self.tools_box.add(arcade.gui.UIFlatButton(width=self.window.width * 0.125, height=self.window.height * 0.075, text="Hide", style=dropdown_style))
if level_num == -1:
load_button = self.tools_box.add(arcade.gui.UITextureButton(width=self.window.width * 0.125, height=self.window.height * 0.05, text="Load", style=button_style, texture=button_texture, texture_hovered=button_hovered_texture))
load_button.on_click = lambda event: self.show_load_ui()
save_button = self.tools_box.add(arcade.gui.UITextureButton(width=self.window.width * 0.125, height=self.window.height * 0.05, text="Save", style=button_style, texture=button_texture, texture_hovered=button_hovered_texture))
save_button.on_click = lambda event: self.save()
hide_button = self.tools_box.add(arcade.gui.UITextureButton(width=self.window.width * 0.125, height=self.window.height * 0.05, text="Hide", style=button_style, texture=button_texture, texture_hovered=button_hovered_texture))
hide_button.on_click = lambda event: self.hide_show_panel()
self.back_button = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='<--', style=button_style, width=100, height=50)
@@ -116,8 +123,94 @@ class Game(arcade.gui.UIView):
if not "completed_levels" in self.data:
self.data["completed_levels"] = []
def on_mouse_scroll(self, x, y, scroll_x, scroll_y):
self.camera.zoom += scroll_y * 0.1
self.ui.on_event = self.on_event
def save(self):
now = datetime.now()
timestamp = now.strftime("%Y-%m-%d_%H-%M-%S")
data = []
for gate in self.gates:
if gate.gate_type != "LABEL":
data.append([gate.id, gate.center_x, gate.center_y, gate.gate_type, gate.value, [input_gate.id for input_gate in gate.input], gate.output.id if gate.output else None])
else:
data.append([gate.id, gate.center_x, gate.center_y, gate.gate_type, gate.text])
with open(f"saves/{timestamp}-save.json", "w") as file:
file.write(json.dumps(data, indent=4))
self.add_widget(arcade.gui.UIMessageBox(
width=self.window.width / 2,
height=self.window.height / 2,
message_text=f"Level was succesfully saved as {timestamp}-save.json in the current directory!",
title="Save successful.",
buttons=("OK",)
))
def close_load_ui(self):
self.anchor.remove(self.load_ui_box)
del self.load_ui_box
self.ui._requires_render = True # for some reason, it doesn't automatically mark it as render required?
def show_load_ui(self):
self.load_ui_box = self.anchor.add(arcade.gui.UIBoxLayout(size_hint=(0.75, 0.75), space_between=5).with_background(color=arcade.color.DARK_GRAY), anchor_x="center", anchor_y="center", align_x=-self.window.width / 12)
self.load_ui_box.add(arcade.gui.UILabel(text="Pick save to load", font_size=28, text_color=arcade.color.BLACK))
for save_filename in os.listdir("saves"):
button = self.load_ui_box.add(arcade.gui.UITextureButton(text=save_filename, style=button_style, texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 2, height=self.window.height / 20))
button.on_click = lambda event, save_filename=save_filename: self.load(save_filename)
close_button = self.load_ui_box.add(arcade.gui.UITextureButton(text="Close", style=button_style, texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 2, height=self.window.height / 20))
close_button.on_click = lambda event: self.close_load_ui()
def load(self, save_filename):
self.gates.clear()
[self.ui.remove(gate) for gate in self.gates if gate.gate_type == "LABEL"]
self.spritelist.clear()
self.connections.clear()
with open(f"saves/{save_filename}", "r") as file:
data = json.load(file)
for gate in data:
if gate[3] != "LABEL":
sprite = LogicalGate(gate[0], gate[1], gate[2], gate[3])
sprite.value = gate[4]
sprite.input = gate[5]
sprite.output = gate[6]
self.gates.append(sprite)
self.spritelist.append(sprite)
else:
label = self.add_widget(arcade.gui.UIInputText(text=gate[4], x=gate[1], y=gate[2]))
self.gates.append(label)
label.id = gate[0]
label.gate_type = "LABEL"
for gate_cls in self.gates:
if gate_cls.gate_type != "LABEL":
gate_cls.input = [self.gates[input_id] for input_id in gate_cls.input]
gate_cls.output = self.gates[gate_cls.output] if gate_cls.output else None
for gate_x in self.gates:
if gate_x.gate_type == "LABEL":
continue
for gate_y in self.gates:
if gate_x == gate_y or gate_y.gate_type == "LABEL":
continue
if gate_x in gate_y.input:
self.connections.append([gate_x.id, gate_y.id])
self.evaluate()
self.close_load_ui()
def screenshot(self):
self.tools_box.visible = False
@@ -159,6 +252,9 @@ class Game(arcade.gui.UIView):
outputs = []
for gate in self.gates:
if gate.gate_type == "LABEL":
continue
if not gate.output:
gate.calculate_value()
@@ -236,9 +332,15 @@ class Game(arcade.gui.UIView):
self.add_connection()
def add_gate(self, x, y, gate_type):
sprite = LogicalGate(len(self.gates), x, y, gate_type)
self.gates.append(sprite)
self.spritelist.append(sprite)
if gate_type != "LABEL":
sprite = LogicalGate(len(self.gates), x, y, gate_type)
self.gates.append(sprite)
self.spritelist.append(sprite)
else:
label = self.add_widget(arcade.gui.UIInputText(text="Placeholder", x=x, y=y))
self.gates.append(label)
label.id = len(self.gates)
label.gate_type = "LABEL"
self.evaluate()
@@ -250,6 +352,20 @@ class Game(arcade.gui.UIView):
return cubic_bezier_points(p0, c1, c2, p3, segments=100)
def on_mouse_scroll(self, x, y, scroll_x, scroll_y):
self.camera.zoom += scroll_y * 0.1
def on_event(self, event):
arcade.gui.UIManager.on_event(self.ui, event)
if isinstance(event, arcade.gui.UIOnClickEvent):
unprojected_vec = self.camera.unproject((event.x, event.y))
world_vec = arcade.math.Vec2(unprojected_vec.x, unprojected_vec.y)
for gate in self.gates:
if gate.rect.point_in_rect(world_vec):
self.dragged_gate = gate
def on_mouse_press(self, x, y, button, modifiers):
unprojected_vec = self.camera.unproject((x, y))
world_vec = arcade.math.Vec2(unprojected_vec.x, unprojected_vec.y)
@@ -288,8 +404,11 @@ class Game(arcade.gui.UIView):
self.camera.position = self.camera.position - arcade.math.Vec2(dx / self.camera.zoom, dy / self.camera.zoom)
elif self.dragged_gate is not None:
self.dragged_gate.center_x += dx / self.camera.zoom
self.dragged_gate.center_y += dy / self.camera.zoom
if not isinstance(self.dragged_gate, arcade.gui.UIInputText):
self.dragged_gate.center_x += dx / self.camera.zoom
self.dragged_gate.center_y += dy / self.camera.zoom
else:
self.dragged_gate.rect = self.dragged_gate.rect.move(dx / self.camera.zoom, dy / self.camera.zoom)
def on_mouse_release(self, x, y, button, modifiers):
self.dragged_gate = None
@@ -303,29 +422,31 @@ class Game(arcade.gui.UIView):
self.main_exit()
def on_draw(self):
super().on_draw()
self.window.clear()
self.camera.use()
self.spritelist.draw()
with self.camera.activate():
self.spritelist.draw()
self.bezier_points = []
self.bezier_points = []
for conn in self.connections:
start_id, end_id = conn
start_gate = self.gates[start_id]
end_gate = self.gates[end_id]
for conn in self.connections:
start_id, end_id = conn
start_gate = self.gates[start_id]
end_gate = self.gates[end_id]
points = self.connection_between(get_gate_port_position(start_gate, "output"), get_gate_port_position(end_gate, "input"))
self.bezier_points.append(points)
points = self.connection_between(get_gate_port_position(start_gate, "output"), get_gate_port_position(end_gate, "input"))
self.bezier_points.append(points)
arcade.draw_line_strip(points, arcade.color.WHITE, 6)
arcade.draw_line_strip(points, arcade.color.WHITE, 6)
mouse_x, mouse_y = self.window.mouse.data.get("x", 0), self.window.mouse.data.get("y", 0)
mouse_x, mouse_y = self.window.mouse.data.get("x", 0), self.window.mouse.data.get("y", 0)
if self.selected_input is not None and self.selected_output is None:
points = self.connection_between(get_gate_port_position(self.gates[self.selected_input], "input"), (mouse_x, mouse_y))
arcade.draw_line_strip(points, arcade.color.WHITE, 6)
if self.selected_input is not None and self.selected_output is None:
points = self.connection_between(get_gate_port_position(self.gates[self.selected_input], "input"), (mouse_x, mouse_y))
arcade.draw_line_strip(points, arcade.color.WHITE, 6)
if self.selected_output is not None and self.selected_input is None:
points = self.connection_between(get_gate_port_position(self.gates[self.selected_output], "output"), (mouse_x, mouse_y))
arcade.draw_line_strip(points, arcade.color.WHITE, 6)
if self.selected_output is not None and self.selected_input is None:
points = self.connection_between(get_gate_port_position(self.gates[self.selected_output], "output"), (mouse_x, mouse_y))
arcade.draw_line_strip(points, arcade.color.WHITE, 6)
self.ui.draw()

View File

@@ -15,7 +15,8 @@ class Tutorial(arcade.gui.UIView):
self.instructions_label = self.anchor.add(arcade.gui.UILabel(text="""How to play:
- You can move gates by dragging their buttons (not the plus ones)
- To create connections, click on the + buttons, left is the input, right is the output
- To create connections, click on the + buttons (left for input, right for output)
- To remove connections, right click the connnection line
- On levels, a node has to have 2 inputs(Except the OUTPUT and NOT node), but only 1 output
- On DIY mode, a node can have more than 2 inputs, except for OUTPUT and NOT
- You can change an INPUT's gate value by clicking on it

5
run.py
View File

@@ -5,7 +5,7 @@ pyglet.options.debug_gl = False
import logging, datetime, os, json, sys, arcade
from utils.utils import get_closest_resolution, print_debug_info, on_exception
from utils.constants import log_dir, menu_background_color
from utils.constants import log_dir, save_dir, menu_background_color
from menus.main import Main
sys.excepthook = on_exception
@@ -16,6 +16,9 @@ pyglet.font.add_directory('./assets/fonts')
if not log_dir in os.listdir():
os.makedirs(log_dir)
if not save_dir in os.listdir():
os.makedirs(save_dir)
while len(os.listdir(log_dir)) >= 5:
files = [(file, os.path.getctime(os.path.join(log_dir, file))) for file in os.listdir(log_dir)]
oldest_file = sorted(files, key=lambda x: x[1])[0][0]

View File

@@ -3,8 +3,10 @@ from arcade.types import Color
from arcade.gui.widgets.buttons import UITextureButtonStyle, UIFlatButtonStyle
from arcade.gui.widgets.slider import UISliderStyle
menu_background_color = (30, 30, 47)
log_dir = 'logs'
save_dir = 'saves'
menu_background_color = (30, 30, 47)
discord_presence_id = 1427213145667276840
SINGLE_INPUT_LOGICAL_GATES = ["NOT", "OUTPUT"]
@@ -318,4 +320,5 @@ settings = {
},
"Credits": {}
}
settings_start_category = "Graphics"