diff --git a/assets/graphics/powered_lines/corner/left_bottom.png b/assets/graphics/powered_lines/corner/left_bottom.png index 1b4df80..7062a16 100644 Binary files a/assets/graphics/powered_lines/corner/left_bottom.png and b/assets/graphics/powered_lines/corner/left_bottom.png differ diff --git a/assets/graphics/powered_lines/corner/left_top.png b/assets/graphics/powered_lines/corner/left_top.png index 047b947..f6f621b 100644 Binary files a/assets/graphics/powered_lines/corner/left_top.png and b/assets/graphics/powered_lines/corner/left_top.png differ diff --git a/assets/graphics/powered_lines/corner/right_bottom.png b/assets/graphics/powered_lines/corner/right_bottom.png index 5339967..42f564e 100644 Binary files a/assets/graphics/powered_lines/corner/right_bottom.png and b/assets/graphics/powered_lines/corner/right_bottom.png differ diff --git a/assets/graphics/powered_lines/corner/right_top.png b/assets/graphics/powered_lines/corner/right_top.png index efcda30..3cf48e6 100644 Binary files a/assets/graphics/powered_lines/corner/right_top.png and b/assets/graphics/powered_lines/corner/right_top.png differ diff --git a/assets/graphics/unpowered_lines/corner/left_bottom.png b/assets/graphics/unpowered_lines/corner/left_bottom.png index 7e01c78..c6cb296 100644 Binary files a/assets/graphics/unpowered_lines/corner/left_bottom.png and b/assets/graphics/unpowered_lines/corner/left_bottom.png differ diff --git a/assets/graphics/unpowered_lines/corner/left_top.png b/assets/graphics/unpowered_lines/corner/left_top.png index 419c5f3..3e00c5f 100644 Binary files a/assets/graphics/unpowered_lines/corner/left_top.png and b/assets/graphics/unpowered_lines/corner/left_top.png differ diff --git a/assets/graphics/unpowered_lines/corner/right_bottom.png b/assets/graphics/unpowered_lines/corner/right_bottom.png index cc058a0..db9ff7a 100644 Binary files a/assets/graphics/unpowered_lines/corner/right_bottom.png and b/assets/graphics/unpowered_lines/corner/right_bottom.png differ diff --git a/assets/graphics/unpowered_lines/corner/right_top.png b/assets/graphics/unpowered_lines/corner/right_top.png index 7c09a07..bb5de31 100644 Binary files a/assets/graphics/unpowered_lines/corner/right_top.png and b/assets/graphics/unpowered_lines/corner/right_top.png differ diff --git a/game/cell.py b/game/cell.py index 45183bc..f427d02 100644 --- a/game/cell.py +++ b/game/cell.py @@ -15,9 +15,9 @@ def get_opposite(direction): class Cell(arcade.gui.UITextureButton): 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.powered = False 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] if ( 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] ) ] - def update_value(self): - self.powered = any([neighbour.powered for neighbour in self.get_connected_neighbours()]) - def update_visual(self): self.texture = TEXTURE_MAP[(self.cell_type, self.rotation, self.powered)] self.texture_hovered = TEXTURE_MAP[(self.cell_type, self.rotation, self.powered)] diff --git a/game/level_generator.py b/game/level_generator.py new file mode 100644 index 0000000..09148b2 --- /dev/null +++ b/game/level_generator.py @@ -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 \ No newline at end of file diff --git a/game/play.py b/game/play.py index b2cdaac..f234bfc 100644 --- a/game/play.py +++ b/game/play.py @@ -5,6 +5,7 @@ from utils.preload import button_texture, button_hovered_texture from collections import deque +from game.level_generator import generate_map from game.cells import * class Game(arcade.gui.UIView): @@ -20,8 +21,10 @@ class Game(arcade.gui.UIView): self.houses = [] self.anchor = self.add_widget(arcade.gui.UIAnchorLayout(size_hint=(1, 1))) - self.grid_size = list(map(int, difficulty.split("x"))) - 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_size = int(difficulty.split("x")[0]) + 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): super().on_show_view() @@ -30,13 +33,13 @@ class Game(arcade.gui.UIView): 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) - for row in range(self.grid_size[0]): + for row in range(self.grid_size): 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 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"]: cell = PowerLine(cell_type, left_neighbour, top_neighbour) @@ -81,8 +84,9 @@ class Game(arcade.gui.UIView): queue.append(connected_neighbour) for row in self.cells: - for power_line in row: - power_line.update_visual() + for cell in row: + cell.update_visual() + def main_exit(self): from menus.main import Main diff --git a/menus/difficulty_selector.py b/menus/difficulty_selector.py index c1d93ab..1be2dc9 100644 --- a/menus/difficulty_selector.py +++ b/menus/difficulty_selector.py @@ -22,7 +22,7 @@ class DifficultySelector(arcade.gui.UIView): 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.on_click = lambda e, difficulty=difficulty: self.play(difficulty) diff --git a/menus/main.py b/menus/main.py index d533a83..a15ce0e 100644 --- a/menus/main.py +++ b/menus/main.py @@ -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.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.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() def play(self): diff --git a/run.py b/run.py index 8b9eb44..87ff882 100644 --- a/run.py +++ b/run.py @@ -18,8 +18,6 @@ from arcade.experimental.controller_window import ControllerWindow sys.excepthook = on_exception -__builtins__.print = lambda *args, **kwargs: logging.debug(" ".join(map(str, args))) - if not log_dir in os.listdir(): os.makedirs(log_dir) diff --git a/utils/constants.py b/utils/constants.py index d60fed6..d4a3334 100644 --- a/utils/constants.py +++ b/utils/constants.py @@ -7,25 +7,24 @@ ROTATIONS = { "line": ["vertical", "horizontal"], "corner": ["right_bottom", "left_bottom", "left_top", "right_top"], "t_junction": ["top_bottom_right", "left_right_bottom", "top_bottom_left", "left_right_top"], - "cross": ["cross"], - "power_source": ["cross"], - "house": ["cross"] + "cross": ["cross"] } NEIGHBOURS = { - "vertical": ["b", "t"], - "horizontal": ["l", "r"], - "left_bottom": ["l", "b"], - "right_bottom": ["r", "b"], - "left_top": ["l", "t"], - "right_top": ["r", "t"], - "top_bottom_right": ["t", "b", "r"], - "top_bottom_left": ["t", "b", "l"], - "left_right_bottom": ["l", "r", "b"], - "left_right_top": ["l", "r", "t"], - "cross": ["l", "r", "t", "b"] + "vertical": {"b", "t"}, + "horizontal": {"l", "r"}, + "left_bottom": {"l", "b"}, + "right_bottom": {"r", "b"}, + "left_top": {"l", "t"}, + "right_top": {"r", "t"}, + "top_bottom_right": {"t", "b", "r"}, + "top_bottom_left": {"t", "b", "l"}, + "left_right_bottom": {"l", "r", "b"}, + "left_right_top": {"l", "r", "t"}, + "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) log_dir = 'logs'