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
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 309 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 334 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 301 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 322 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 295 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 321 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 287 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 305 B |
@@ -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
@@ -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
|
||||||
18
game/play.py
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||