make corners straight use straight lines instead of arcs, add randomized level generator finally, make default difficulties bigger, make main menu buttons smaller, other fixes and improvements

This commit is contained in:
2025-11-08 12:52:33 +01:00
parent b698d2055e
commit 96ebdef4e3
15 changed files with 159 additions and 31 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 309 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 334 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 301 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 321 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 287 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 305 B

View File

@@ -15,9 +15,9 @@ def get_opposite(direction):
class Cell(arcade.gui.UITextureButton): class Cell(arcade.gui.UITextureButton):
def __init__(self, cell_type, left_neighbour, top_neighbour): def __init__(self, cell_type, left_neighbour, top_neighbour):
super().__init__(texture=TEXTURE_MAP[cell_type, ROTATIONS[cell_type][0], cell_type == "power_source"]) super().__init__(texture=TEXTURE_MAP[cell_type, ROTATIONS[cell_type][0] if cell_type in ROTATIONS else "cross", cell_type == "power_source"])
self.rotation = ROTATIONS[cell_type][0] self.rotation = ROTATIONS[cell_type][0] if cell_type in ROTATIONS else "cross"
self.cell_type = cell_type self.cell_type = cell_type
self.powered = False self.powered = False
self.left_neighbour, self.top_neighbour = left_neighbour, top_neighbour self.left_neighbour, self.top_neighbour = left_neighbour, top_neighbour
@@ -38,13 +38,11 @@ class Cell(arcade.gui.UITextureButton):
self.get_neighbour(neighbour_direction) for neighbour_direction in NEIGHBOURS[self.rotation] self.get_neighbour(neighbour_direction) for neighbour_direction in NEIGHBOURS[self.rotation]
if ( if (
self.get_neighbour(neighbour_direction) and self.get_neighbour(neighbour_direction) and
self.get_neighbour(neighbour_direction).cell_type != "house" and
get_opposite(neighbour_direction) in NEIGHBOURS[self.get_neighbour(neighbour_direction).rotation] get_opposite(neighbour_direction) in NEIGHBOURS[self.get_neighbour(neighbour_direction).rotation]
) )
] ]
def update_value(self):
self.powered = any([neighbour.powered for neighbour in self.get_connected_neighbours()])
def update_visual(self): def update_visual(self):
self.texture = TEXTURE_MAP[(self.cell_type, self.rotation, self.powered)] self.texture = TEXTURE_MAP[(self.cell_type, self.rotation, self.powered)]
self.texture_hovered = TEXTURE_MAP[(self.cell_type, self.rotation, self.powered)] self.texture_hovered = TEXTURE_MAP[(self.cell_type, self.rotation, self.powered)]

129
game/level_generator.py Normal file
View File

@@ -0,0 +1,129 @@
import random
from utils.constants import ROTATIONS, NEIGHBOURS, DIRECTIONS
from collections import deque
def in_bounds(x, y, size):
return 0 <= x < size and 0 <= y < size
def classify_tile(conns):
for rotation, connections in NEIGHBOURS.items():
if conns == connections:
for cell_type, rotations in ROTATIONS.items():
if rotation in rotations:
return cell_type
print(f"Unknown: {conns}")
return "cross"
def add_cycles(conns, num_cycles):
size = len(conns)
added = 0
attempts = 0
max_attempts = num_cycles * 20
while added < num_cycles and attempts < max_attempts:
attempts += 1
x, y = random.randint(0, size-1), random.randint(0, size-1)
dirs = list(DIRECTIONS.items())
random.shuffle(dirs)
for d, (dx, dy, opposite) in dirs:
nx, ny = x + dx, y + dy
if in_bounds(nx, ny, size) and d not in conns[y][x]:
conns[y][x].add(d)
conns[ny][nx].add(opposite)
added += 1
break
return conns
def pick_random_cells(size, count, avoid=None):
all_cells = [(x, y) for y in range(size) for x in range(size)]
if avoid:
all_cells = [c for c in all_cells if c not in avoid]
random.shuffle(all_cells)
return all_cells[:count]
def generate_spanning_tree_with_dead_ends(size, num_dead_ends):
if num_dead_ends > size * size - 1:
num_dead_ends = size * size - 1
grid = [[set() for _ in range(size)] for _ in range(size)]
all_cells = [(x, y) for y in range(size) for x in range(size)]
random.shuffle(all_cells)
start = all_cells[0]
stack = [start]
visited = {start}
leaf_candidates = []
while len(visited) < size * size:
if not stack:
unvisited = [c for c in all_cells if c not in visited]
if unvisited:
stack.append(unvisited[0])
visited.add(unvisited[0])
x, y = stack[-1]
dirs = list(DIRECTIONS.items())
random.shuffle(dirs)
found = False
for d, (dx, dy, opposite) in dirs:
nx, ny = x + dx, y + dy
if in_bounds(nx, ny, size) and (nx, ny) not in visited:
grid[y][x].add(d)
grid[ny][nx].add(opposite)
visited.add((nx, ny))
stack.append((nx, ny))
found = True
break
if not found:
if len(grid[y][x]) == 1:
leaf_candidates.append((x, y))
stack.pop()
leaf_nodes = leaf_candidates[:num_dead_ends]
for y in range(size):
for x in range(size):
if (x, y) not in leaf_nodes and len(grid[y][x]) < 2:
dirs = list(DIRECTIONS.items())
random.shuffle(dirs)
for d, (dx, dy, opposite) in dirs:
nx, ny = x + dx, y + dy
if in_bounds(nx, ny, size) and d not in grid[y][x]:
grid[y][x].add(d)
grid[ny][nx].add(opposite)
if len(grid[y][x]) >= 2:
break
return grid, leaf_nodes
def generate_map(size, source_count, house_count, cycles=15):
conns, dead_ends = generate_spanning_tree_with_dead_ends(size, house_count)
conns = add_cycles(conns, cycles)
houses = dead_ends[:house_count]
available_cells = [(x, y) for y in range(size) for x in range(size)
if (x, y) not in houses]
random.shuffle(available_cells)
sources = available_cells[:source_count]
grid = []
for y in range(size):
grid.append([])
for x in range(size):
if (x, y) in sources:
grid[-1].append("power_source")
elif (x, y) in houses:
grid[-1].append("house")
else:
grid[-1].append(classify_tile(conns[y][x]))
return grid

View File

@@ -5,6 +5,7 @@ from utils.preload import button_texture, button_hovered_texture
from collections import deque from collections import deque
from game.level_generator import generate_map
from game.cells import * from game.cells import *
class Game(arcade.gui.UIView): class Game(arcade.gui.UIView):
@@ -20,8 +21,10 @@ class Game(arcade.gui.UIView):
self.houses = [] self.houses = []
self.anchor = self.add_widget(arcade.gui.UIAnchorLayout(size_hint=(1, 1))) self.anchor = self.add_widget(arcade.gui.UIAnchorLayout(size_hint=(1, 1)))
self.grid_size = list(map(int, difficulty.split("x"))) self.grid_size = int(difficulty.split("x")[0])
self.power_grid = self.anchor.add(arcade.gui.UIGridLayout(horizontal_spacing=0, vertical_spacing=0, row_count=self.grid_size[0], column_count=self.grid_size[1])) self.grid = generate_map(self.grid_size, int((self.grid_size * self.grid_size) / 10), int((self.grid_size * self.grid_size) / 5))
self.power_grid = self.anchor.add(arcade.gui.UIGridLayout(horizontal_spacing=0, vertical_spacing=0, row_count=self.grid_size, column_count=self.grid_size))
def on_show_view(self): def on_show_view(self):
super().on_show_view() super().on_show_view()
@@ -30,13 +33,13 @@ class Game(arcade.gui.UIView):
self.back_button.on_click = lambda event: self.main_exit() self.back_button.on_click = lambda event: self.main_exit()
self.anchor.add(self.back_button, anchor_x="left", anchor_y="top", align_x=5, align_y=-5) self.anchor.add(self.back_button, anchor_x="left", anchor_y="top", align_x=5, align_y=-5)
for row in range(self.grid_size[0]): for row in range(self.grid_size):
self.cells.append([]) self.cells.append([])
for col in range(self.grid_size[1]): for col in range(self.grid_size):
left_neighbour = self.cells[row][col - 1] if col > 0 else None left_neighbour = self.cells[row][col - 1] if col > 0 else None
top_neighbour = self.cells[row - 1][col] if row > 0 else None top_neighbour = self.cells[row - 1][col] if row > 0 else None
cell_type = random.choice(["line", "corner", "t_junction", "cross", "power_source", "house"]) cell_type = self.grid[row][col]
if cell_type in ["line", "corner", "t_junction", "cross"]: if cell_type in ["line", "corner", "t_junction", "cross"]:
cell = PowerLine(cell_type, left_neighbour, top_neighbour) cell = PowerLine(cell_type, left_neighbour, top_neighbour)
@@ -81,8 +84,9 @@ class Game(arcade.gui.UIView):
queue.append(connected_neighbour) queue.append(connected_neighbour)
for row in self.cells: for row in self.cells:
for power_line in row: for cell in row:
power_line.update_visual() cell.update_visual()
def main_exit(self): def main_exit(self):
from menus.main import Main from menus.main import Main

View File

@@ -22,7 +22,7 @@ class DifficultySelector(arcade.gui.UIView):
self.box.add(arcade.gui.UILabel(text="Difficulty Selector", font_size=32)) self.box.add(arcade.gui.UILabel(text="Difficulty Selector", font_size=32))
for difficulty in ["3x3", "4x4", "5x5", "6x6", "9x9"]: for difficulty in ["7x7", "8x8", "9x9", "10x10", "12x12"]:
button = self.box.add(arcade.gui.UITextureButton(text=difficulty, width=self.window.width / 2, height=self.window.height / 10, texture=button_texture, texture_hovered=button_hovered_texture, style=big_button_style)) button = self.box.add(arcade.gui.UITextureButton(text=difficulty, width=self.window.width / 2, height=self.window.height / 10, texture=button_texture, texture_hovered=button_hovered_texture, style=big_button_style))
button.on_click = lambda e, difficulty=difficulty: self.play(difficulty) button.on_click = lambda e, difficulty=difficulty: self.play(difficulty)

View File

@@ -52,10 +52,10 @@ class Main(arcade.gui.UIView):
self.title_label = self.box.add(arcade.gui.UILabel(text="Connect the Current", font_name="Roboto", font_size=48)) self.title_label = self.box.add(arcade.gui.UILabel(text="Connect the Current", font_name="Roboto", font_size=48))
self.play_button = self.box.add(arcade.gui.UITextureButton(text="Play", texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 2, height=150, style=big_button_style)) self.play_button = self.box.add(arcade.gui.UITextureButton(text="Play", texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 2, height=self.window.height / 10, style=big_button_style))
self.play_button.on_click = lambda event: self.play() self.play_button.on_click = lambda event: self.play()
self.settings_button = self.box.add(arcade.gui.UITextureButton(text="Settings", texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 2, height=150, style=big_button_style)) self.settings_button = self.box.add(arcade.gui.UITextureButton(text="Settings", texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 2, height=self.window.height / 10, style=big_button_style))
self.settings_button.on_click = lambda event: self.settings() self.settings_button.on_click = lambda event: self.settings()
def play(self): def play(self):

2
run.py
View File

@@ -18,8 +18,6 @@ from arcade.experimental.controller_window import ControllerWindow
sys.excepthook = on_exception sys.excepthook = on_exception
__builtins__.print = lambda *args, **kwargs: logging.debug(" ".join(map(str, args)))
if not log_dir in os.listdir(): if not log_dir in os.listdir():
os.makedirs(log_dir) os.makedirs(log_dir)

View File

@@ -7,25 +7,24 @@ ROTATIONS = {
"line": ["vertical", "horizontal"], "line": ["vertical", "horizontal"],
"corner": ["right_bottom", "left_bottom", "left_top", "right_top"], "corner": ["right_bottom", "left_bottom", "left_top", "right_top"],
"t_junction": ["top_bottom_right", "left_right_bottom", "top_bottom_left", "left_right_top"], "t_junction": ["top_bottom_right", "left_right_bottom", "top_bottom_left", "left_right_top"],
"cross": ["cross"], "cross": ["cross"]
"power_source": ["cross"],
"house": ["cross"]
} }
NEIGHBOURS = { NEIGHBOURS = {
"vertical": ["b", "t"], "vertical": {"b", "t"},
"horizontal": ["l", "r"], "horizontal": {"l", "r"},
"left_bottom": ["l", "b"], "left_bottom": {"l", "b"},
"right_bottom": ["r", "b"], "right_bottom": {"r", "b"},
"left_top": ["l", "t"], "left_top": {"l", "t"},
"right_top": ["r", "t"], "right_top": {"r", "t"},
"top_bottom_right": ["t", "b", "r"], "top_bottom_right": {"t", "b", "r"},
"top_bottom_left": ["t", "b", "l"], "top_bottom_left": {"t", "b", "l"},
"left_right_bottom": ["l", "r", "b"], "left_right_bottom": {"l", "r", "b"},
"left_right_top": ["l", "r", "t"], "left_right_top": {"l", "r", "t"},
"cross": ["l", "r", "t", "b"] "cross": {"l", "r", "t", "b"}
} }
DIRECTIONS = {"t": (0, -1, "b"), "b": (0, 1, "t"), "l": (-1, 0, "r"), "r": (1, 0, "l")}
menu_background_color = (30, 30, 47) menu_background_color = (30, 30, 47)
log_dir = 'logs' log_dir = 'logs'