From 318d5b2823f04d1c22fe926454a234553b3ca0f7 Mon Sep 17 00:00:00 2001 From: csd4ni3l Date: Thu, 16 Oct 2025 21:39:10 +0200 Subject: [PATCH] Allow connecting from both directions, use textured logic gates instead of UI to get a huge speedup, make output single output(should have been), add connection removal with right click --- assets/graphics/logic_gate_false.png | Bin 0 -> 246 bytes assets/graphics/logic_gate_input_false.png | Bin 0 -> 199 bytes assets/graphics/logic_gate_input_true.png | Bin 0 -> 410 bytes assets/graphics/logic_gate_output_false.png | Bin 0 -> 200 bytes assets/graphics/logic_gate_output_true.png | Bin 0 -> 409 bytes assets/graphics/logic_gate_true.png | Bin 0 -> 252 bytes game/play.py | 125 +++++++++++++------- menus/main.py | 2 +- utils/constants.py | 4 +- utils/preload.py | 50 ++++++++ 10 files changed, 134 insertions(+), 47 deletions(-) create mode 100644 assets/graphics/logic_gate_false.png create mode 100644 assets/graphics/logic_gate_input_false.png create mode 100644 assets/graphics/logic_gate_input_true.png create mode 100644 assets/graphics/logic_gate_output_false.png create mode 100644 assets/graphics/logic_gate_output_true.png create mode 100644 assets/graphics/logic_gate_true.png diff --git a/assets/graphics/logic_gate_false.png b/assets/graphics/logic_gate_false.png new file mode 100644 index 0000000000000000000000000000000000000000..f928c8db70b59fa53c254d397a20bf83ba3efc90 GIT binary patch literal 246 zcmeAS@N?(olHy`uVBq!ia0vp^JAhb$gAGV7WZU!-NbU7>aSW-L^Y*48U$cRLtE2GK zo@YHjCV5U(T6plpj2GF2t+Sfau%(^*) z({IaN7JI8q`}(OTeb&$Ho%z5;*y1A0To^NV`wPocP3^0DQ4G&#-lgi&-P6|s^az8e LtDnm{r-UW|dDvxT literal 0 HcmV?d00001 diff --git a/assets/graphics/logic_gate_input_false.png b/assets/graphics/logic_gate_input_false.png new file mode 100644 index 0000000000000000000000000000000000000000..8a20b0a624eeea538722976b0732d4409880b6aa GIT binary patch literal 199 zcmeAS@N?(olHy`uVBq!ia0vp^(}7rlg9S)#ja0G+QhlB-jv*P1Z?7A2H5dpm9PIva z?1$TD&$m~Uw7Q+TZm)c@q2sjZ#x>DPHcsitzi<7wbc)QG)*0M};z{eerM^DpEmnNy z1QpOb64S9$03`72BD=)Xcje2?^N&xy-MZxNx@$A9&i;AwExO9g^($@9hhO2?&TS-~ d1k`?luRnmpCH}R`cA(1`JYD@<);T3K0RWqtQX2pO literal 0 HcmV?d00001 diff --git a/assets/graphics/logic_gate_input_true.png b/assets/graphics/logic_gate_input_true.png new file mode 100644 index 0000000000000000000000000000000000000000..dcb6fc5393d269521b2138779f9dbb31aeaf4ee0 GIT binary patch literal 410 zcmeAS@N?(olHy`uVBq!ia0vp^(}7rl14uAfS#Qk%agsfPd>I(3)EF2VS{N990fib~ zFff!FFfhDI0IHh7z#v{QXIG#NP=cu>$S;_Ip=|P53lJ~K+ueoXe|!I#{XiajiKnkC z`%5kX0d)g*p1xe55MzFkUkKy*Mdh=AoMcZI$B>A_Z?8CVH5l-?TwGak?9RoQpmMFK zS8gW){>8Tjy-@Dn7AtC+`&4hToOO6>n4R2ROVw{zb+*en+kRd z6e~V+I@3CX8zRxM6DaVsV#n{hnRC~lKO@v|yLHa(tG8xU75|IR18W7VfN0U&J3TIN f{T-lrU_t9Y)*LSLw=7u$bQ*)FtDnm{r-UW|DTq!; literal 0 HcmV?d00001 diff --git a/assets/graphics/logic_gate_output_true.png b/assets/graphics/logic_gate_output_true.png new file mode 100644 index 0000000000000000000000000000000000000000..a66fc698b14413448f7277969bac9e015df4b0bc GIT binary patch literal 409 zcmeAS@N?(olHy`uVBq!ia0vp^(}7rl14uAfS#Qk%agsfPd>I(3)EF2VS{N990fib~ zFff!FFfhDI0IHh7z#v{QXIG#NP=cu>$S;_Ip=|P53lJ~K+ueoXe|!I#{XiajiKnkC z`%5kX0X5-01?S>`LX7!Eej$wK7nRQfa*{k<977@wzrAY6*I*#P?AZM2*yG+0o*Mir zF4@{|!WN!7;Tlm^yY1za#aD~8*UjIx>du1laGxCo4^MGkmszLi`F+0seE#LyRqNxH z?a?k>S^e-(tM>Ac>lJlvdsgkNemLuJDP!^?w%Kp|4SImKsg}4#l%yn|rD-#240|P4q1C!gXAU`8CS6G7^>bP0l+XkK#V3PX literal 0 HcmV?d00001 diff --git a/assets/graphics/logic_gate_true.png b/assets/graphics/logic_gate_true.png new file mode 100644 index 0000000000000000000000000000000000000000..36e76eb1dc7676abbd268ffe3b2cf079b5f55ec3 GIT binary patch literal 252 zcmeAS@N?(olHy`uVBq!ia0vp^JAhb$gAGV7WZU!-NFDZcaSW-L^Y*48U$cRT%fa^= z=_ctd-d3SqD!eWYORxHL3Iy0}D~!HgdPU{*rY);h86Mk`|G;YP*0501%^Q!e5kD-; z;%Cw*?EApwxAlj6)nD(O|0DLJz%5y9P6m_utOK2D1&cV*n72OvK6G*M#l7G2Zmb9*B(H35zKR(xUebY{7AWhkcaWw>9d>atC)(*)=p N22WQ%mvv4FO#oV2YZ(9l literal 0 HcmV?d00001 diff --git a/game/play.py b/game/play.py index 1403f64..e203e54 100644 --- a/game/play.py +++ b/game/play.py @@ -4,11 +4,11 @@ from datetime import datetime from utils.utils import cubic_bezier_points, get_gate_port_position, generate_task_text from utils.constants import button_style, dropdown_style, LOGICAL_GATES, LEVELS, SINGLE_INPUT_LOGICAL_GATES -from utils.preload import button_texture, button_hovered_texture +from utils.preload import button_texture, button_hovered_texture, logic_gate_textures -class LogicalGate(arcade.gui.UIBoxLayout): +class LogicalGate(arcade.Sprite): def __init__(self, id, x, y, gate_type, value): - super().__init__(x=x, y=y, space_between=2, vertical=False) + super().__init__(center_x=x, center_y=y, img=logic_gate_textures[gate_type][value if value is not None else 0]) self.id = id self.gate_type = gate_type @@ -17,14 +17,6 @@ class LogicalGate(arcade.gui.UIBoxLayout): self.value = value else: self.value = 0 - - self.input_add_button = self.add(arcade.gui.UIFlatButton(text="+", style=dropdown_style, height=30, width=30)) - self.input_add_button.visible = not self.gate_type == "INPUT" - - self.gate_button = self.add(arcade.gui.UIFlatButton(text=f"{gate_type} ({self.value})", style=dropdown_style, height=30, width=120)) - - self.output_add_button = self.add(arcade.gui.UIFlatButton(text="+", style=dropdown_style, height=30, width=30)) - self.output_add_button.visible = not self.gate_type == "OUTPUT" self.input: list[LogicalGate] = [] self.output: LogicalGate | None = None @@ -32,6 +24,7 @@ class LogicalGate(arcade.gui.UIBoxLayout): def calculate_value(self): if self.gate_type == "OUTPUT" and self.input: self.value = self.input[0].calculate_value() + elif self.gate_type == "INPUT": # dont set INPUT to None pass elif self.gate_type in SINGLE_INPUT_LOGICAL_GATES and len(self.input) == 1: @@ -41,7 +34,7 @@ class LogicalGate(arcade.gui.UIBoxLayout): else: self.value = None - self.gate_button.text = f"{self.gate_type} ({self.value})" + self.texture = logic_gate_textures[self.gate_type][self.value if self.value is not None else 0] return self.value def __repr__(self): @@ -51,6 +44,11 @@ class Game(arcade.gui.UIView): def __init__(self, pypresence_client, level_num): super().__init__() + self.camera = arcade.Camera2D() + self.camera.match_window() + + self.spritelist = arcade.SpriteList() + self.pypresence_client = pypresence_client self.pypresence_client.update(state="In game") @@ -58,6 +56,8 @@ class Game(arcade.gui.UIView): self.gates: list[LogicalGate] = [] self.connections = [] + self.bezier_points = [] + self.default_gate_type = "AND" self.dragged_gate = None @@ -77,7 +77,7 @@ class Game(arcade.gui.UIView): self.add_gate(random.randint(0, 200), random.randint(200, self.window.height - 100), "INPUT", requirement[2]) elif requirement[1] == "OUTPUT": for _ in range(requirement[0]): - self.add_gate(random.randint(self.window.width - 500, self.window.width - 200), random.randint(200, self.window.height - 100), "OUTPUT", requirement[2]) + self.add_gate(random.randint(self.window.width - 500, self.window.width - 350), random.randint(200, self.window.height - 100), "OUTPUT", requirement[2]) else: for _ in range(requirement[0]): self.add_gate(random.randint(300, self.window.width - 600), random.randint(200, self.window.height - 100), requirement[1]) @@ -90,7 +90,7 @@ class Game(arcade.gui.UIView): if "INPUT" in gate: func = lambda: (random.randint(0, 200), random.randint(200, self.window.height - 100)) elif gate == "OUTPUT": - func = lambda: (random.randint(self.window.width - 500, self.window.width - 200), random.randint(200, self.window.height - 100)) + func = lambda: (random.randint(self.window.width - 500, self.window.width - 350), random.randint(200, self.window.height - 100)) else: func = lambda: (random.randint(300, self.window.width - 600), random.randint(200, self.window.height - 100)) @@ -189,34 +189,45 @@ class Game(arcade.gui.UIView): with open("data.json", "w") as file: file.write(json.dumps(self.data, indent=4)) - def select_output(self, gate_id): - if self.gates[gate_id].output: - return + def add_connection(self): + output_gate = self.gates[self.selected_output] + input_gate = self.gates[self.selected_input] - self.selected_output = gate_id + output_gate.output = input_gate + input_gate.input.append(output_gate) + + self.connections.append([self.selected_output, self.selected_input]) + + self.selected_output = None self.selected_input = None + self.evaluate() + + def select_output(self, gate_id): + if gate_id == self.selected_input: + return + + if self.gates[gate_id].output: + return + + self.selected_output = gate_id + + if self.selected_input is not None: + self.add_connection() + def select_input(self, gate_id): + if gate_id == self.selected_output: + return + if self.gates[gate_id].gate_type not in SINGLE_INPUT_LOGICAL_GATES and len(self.gates[gate_id].input) == 2: return elif self.gates[gate_id].gate_type in SINGLE_INPUT_LOGICAL_GATES and len(self.gates[gate_id].input) == 1: return + self.selected_input = gate_id + if self.selected_output is not None: - self.selected_input = gate_id - - output_gate = self.gates[self.selected_output] - input_gate = self.gates[self.selected_input] - - output_gate.output = input_gate - input_gate.input.append(output_gate) - - self.connections.append([self.selected_output, self.selected_input]) - - self.selected_output = None - self.selected_input = None - - self.evaluate() + self.add_connection() def add_gate(self, x, y, gate_type, value=None): if gate_type == "INPUT 0": @@ -226,10 +237,9 @@ class Game(arcade.gui.UIView): gate_type = "INPUT" value = 1 - self.gates.append(self.add_widget(LogicalGate(len(self.gates), x, y, gate_type, value))) - - self.gates[-1].input_add_button.on_click = lambda e, gate_id=len(self.gates) - 1: self.select_input(gate_id) - self.gates[-1].output_add_button.on_click = lambda e, gate_id=len(self.gates) - 1: self.select_output(gate_id) + sprite = LogicalGate(len(self.gates), x, y, gate_type, value) + self.gates.append(sprite) + self.spritelist.append(sprite) self.evaluate() @@ -237,15 +247,35 @@ class Game(arcade.gui.UIView): arcade.gui.UIManager.on_event(self.ui, event) if isinstance(event, arcade.gui.UIMousePressEvent): - if not self.dragged_gate: + if event.button == arcade.MOUSE_BUTTON_RIGHT: + for i in range(len(self.bezier_points) - 1, -1, -1): + for point in self.bezier_points[i]: + if event.pos.distance(point) < 5: + self.gates[self.connections[i][0]].output = None + self.gates[self.connections[i][1]].input.remove(self.gates[self.connections[i][0]]) + self.gates[self.connections[i][1]].calculate_value() + + self.connections.pop(i) + self.bezier_points.pop(i) + break + + elif event.button == arcade.MOUSE_BUTTON_LEFT: for gate in self.gates: - if gate.gate_button.rect.point_in_rect((event.x, event.y)): - self.dragged_gate = gate - break + if gate.rect.point_in_rect((event.x, event.y)): + x = gate.center_x - event.x + if abs(x) < 58: + self.dragged_gate = gate + break + else: + if x > 0: + self.select_input(gate.id) + else: + self.select_output(gate.id) def on_mouse_drag(self, x, y, dx, dy, _buttons, _modifiers): if self.dragged_gate is not None: - self.dragged_gate.rect = self.dragged_gate.rect.move(dx, dy) + self.dragged_gate.center_x += dx + self.dragged_gate.center_y += dy def on_mouse_release(self, x, y, button, modifiers): self.dragged_gate = None @@ -257,10 +287,15 @@ class Game(arcade.gui.UIView): def on_key_press(self, symbol, modifiers): if symbol == arcade.key.ESCAPE: self.main_exit() - + def on_draw(self): super().on_draw() + self.camera.use() + self.spritelist.draw() + + self.bezier_points = [] + for conn in self.connections: start_id, end_id = conn start_gate = self.gates[start_id] @@ -274,5 +309,7 @@ class Game(arcade.gui.UIView): c1 = (p0[0] + offset, p0[1]) c2 = (p3[0] - offset, p3[1]) - points = cubic_bezier_points(p0, c1, c2, p3, segments=40) - arcade.draw_line_strip(points, arcade.color.WHITE, 3) \ No newline at end of file + points = cubic_bezier_points(p0, c1, c2, p3, segments=100) + self.bezier_points.append(points) + + arcade.draw_line_strip(points, arcade.color.WHITE, 6) \ No newline at end of file diff --git a/menus/main.py b/menus/main.py index 616d78b..7b61d57 100644 --- a/menus/main.py +++ b/menus/main.py @@ -1,7 +1,7 @@ import arcade, arcade.gui, asyncio, pypresence, time, copy, json -from utils.preload import button_texture, button_hovered_texture from utils.constants import big_button_style, discord_presence_id +from utils.preload import button_texture, button_hovered_texture from utils.utils import FakePyPresence class Main(arcade.gui.UIView): diff --git a/utils/constants.py b/utils/constants.py index e4cd8ed..2e43c2a 100644 --- a/utils/constants.py +++ b/utils/constants.py @@ -7,7 +7,7 @@ menu_background_color = (30, 30, 47) log_dir = 'logs' discord_presence_id = 1427213145667276840 -SINGLE_INPUT_LOGICAL_GATES = ["NOT"] +SINGLE_INPUT_LOGICAL_GATES = ["NOT", "OUTPUT"] LOGICAL_GATES = { "AND": lambda a, b: a and b, @@ -148,7 +148,7 @@ LEVELS = [ [1, "NOR"], [1, "AND"], [1, "XOR"], - [1, "OUTPUT", 1] + [1, "OUTPUT", 0] ], [ [1, "INPUT", 1], diff --git a/utils/preload.py b/utils/preload.py index 18b76e3..29cd86f 100644 --- a/utils/preload.py +++ b/utils/preload.py @@ -1,4 +1,54 @@ import arcade.gui, arcade +from utils.constants import LOGICAL_GATES + +from PIL import ImageDraw, ImageFont + button_texture = arcade.gui.NinePatchTexture(64 // 4, 64 // 4, 64 // 4, 64 // 4, arcade.load_texture("assets/graphics/button.png")) button_hovered_texture = arcade.gui.NinePatchTexture(64 // 4, 64 // 4, 64 // 4, 64 // 4, arcade.load_texture("assets/graphics/button_hovered.png")) + +true_logic_gate = arcade.load_image("assets/graphics/logic_gate_true.png") +false_logic_gate = arcade.load_image("assets/graphics/logic_gate_false.png") + +true_input_logic_gate = arcade.load_image("assets/graphics/logic_gate_input_true.png") +false_input_logic_gate = arcade.load_image("assets/graphics/logic_gate_input_false.png") + +true_output_logic_gate = arcade.load_image("assets/graphics/logic_gate_output_true.png") +false_output_logic_gate = arcade.load_image("assets/graphics/logic_gate_output_false.png") + +logic_gate_textures = {} + +font = ImageFont.truetype("assets/fonts/Roboto-Black.ttf", 14) + +for gate_name in list(LOGICAL_GATES.keys()) + ["INPUT", "OUTPUT"]: + logic_gate_textures[gate_name] = [] + + for i in range(2): + if not i: + if gate_name == "INPUT": + img = false_input_logic_gate.copy() + elif gate_name == "OUTPUT": + img = false_output_logic_gate.copy() + else: + img = false_logic_gate.copy() + else: + if gate_name == "INPUT": + img = true_input_logic_gate.copy() + elif gate_name == "OUTPUT": + img = true_output_logic_gate.copy() + else: + img = true_logic_gate.copy() + + draw = ImageDraw.Draw(img) + + bbox = draw.textbbox((0, 0), gate_name, font=font) + text_w = (bbox[2] - bbox[0]) * 1.25 + text_h = (bbox[3] - bbox[1]) * 1.25 + + width, height = img.size + text_x = (width - text_w) // 2 + text_y = (height - text_h) // 2 + + draw.text((text_x, text_y), gate_name, font=font, fill=(0, 0, 0, 255)) + + logic_gate_textures[gate_name].append(arcade.Texture(name=gate_name, image=img)) \ No newline at end of file