Add rotation change sound effect, add tutorial and also add it to README, fix window title, add statistics label, add custom difficulty

This commit is contained in:
2025-11-08 20:53:25 +01:00
parent 5973d1143f
commit d1b238b239
13 changed files with 146 additions and 17 deletions

View File

@@ -1,3 +1,6 @@
Sound Effect by freesound_community from Pixabay (cut to the important part)
https://pixabay.com/sound-effects/cutting-clipping-wire-copper-80373/
The Roboto Black font used in this project is licensed under the Open Font License. Read assets/fonts/OFL.txt for more information. The Roboto Black font used in this project is licensed under the Open Font License. Read assets/fonts/OFL.txt for more information.
Huge Thanks to Python for being the programming language used in this game. Huge Thanks to Python for being the programming language used in this game.

View File

@@ -1 +1,9 @@
CTC: Connect The Current is a game where you have a power source, and you have to direct the power lines to houses by rotating them into the correct direction. CTC: Connect The Current is a game where you have a power source, and you have to direct the power lines to houses by rotating them into the correct direction.
Tutorial:
In Connect the Current, you have to rotate power lines so power reaches to all of the houses.
- Every line has to be connected on all of it's sides.
- When needed, you might have to create loops of power or branches with no house linked to them.
(This is also because it's randomly generated and i couldn't find a way to generate maps with no meaningless branches)
- To rotate a line, just click on it and it will change its rotation.
- Maps are randomly generated, difficulty(size, source count, house count) depends on what you pick and grows exponentially.

BIN
assets/sound/wire.mp3 Normal file

Binary file not shown.

View File

@@ -1,7 +1,7 @@
import arcade, arcade.gui import arcade, arcade.gui
from utils.constants import ROTATIONS, NEIGHBOURS from utils.constants import ROTATIONS, NEIGHBOURS
from utils.preload import TEXTURE_MAP from utils.preload import TEXTURE_MAP, wire_sound_effect
def get_opposite(direction): def get_opposite(direction):
if direction == "l": if direction == "l":
@@ -46,7 +46,10 @@ class Cell(arcade.Sprite):
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)]
def next_rotation(self): def next_rotation(self, sfx, sfx_volume):
if sfx:
wire_sound_effect.play(volume=sfx_volume / 50)
current_index = ROTATIONS[self.cell_type].index(self.rotation) current_index = ROTATIONS[self.cell_type].index(self.rotation)
if current_index + 1 == len(ROTATIONS[self.cell_type]): if current_index + 1 == len(ROTATIONS[self.cell_type]):

View File

@@ -1,6 +1,5 @@
import random import random
from utils.constants import ROTATIONS, NEIGHBOURS, DIRECTIONS from utils.constants import ROTATIONS, NEIGHBOURS, DIRECTIONS
from collections import deque
def in_bounds(x, y, size): def in_bounds(x, y, size):
return 0 <= x < size and 0 <= y < size return 0 <= x < size and 0 <= y < size
@@ -41,8 +40,10 @@ def add_cycles(conns, num_cycles):
def pick_random_cells(size, count, avoid=None): def pick_random_cells(size, count, avoid=None):
all_cells = [(x, y) for y in range(size) for x in range(size)] all_cells = [(x, y) for y in range(size) for x in range(size)]
if avoid: if avoid:
all_cells = [c for c in all_cells if c not in avoid] all_cells = [c for c in all_cells if c not in avoid]
random.shuffle(all_cells) random.shuffle(all_cells)
return all_cells[:count] return all_cells[:count]
@@ -110,14 +111,14 @@ def generate_map(size, source_count, house_count, cycles=15):
conns = add_cycles(conns, cycles) conns = add_cycles(conns, cycles)
houses = dead_ends[:house_count] houses = dead_ends[:house_count]
available_cells = [(x, y) for y in range(size) for x in range(size) available_cells = [(x, y) for y in range(size) for x in range(size) if (x, y) not in houses]
if (x, y) not in houses]
random.shuffle(available_cells) random.shuffle(available_cells)
sources = available_cells[:source_count] sources = available_cells[:source_count]
grid = [] grid = []
for y in range(size): for y in range(size):
grid.append([]) grid.append([])
for x in range(size): for x in range(size):
if (x, y) in sources: if (x, y) in sources:
grid[-1].append("power_source") grid[-1].append("power_source")

View File

@@ -1,4 +1,4 @@
import arcade, arcade.gui import arcade, arcade.gui, json, time
from utils.constants import button_style, NEIGHBOURS from utils.constants import button_style, NEIGHBOURS
from utils.preload import button_texture, button_hovered_texture from utils.preload import button_texture, button_hovered_texture
@@ -9,23 +9,30 @@ 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):
def __init__(self, pypresence_client, difficulty): def __init__(self, pypresence_client, grid_size, source_count=None, house_count=None):
super().__init__() super().__init__()
self.pypresence_client = pypresence_client self.pypresence_client = pypresence_client
self.pypresence_client.update(state='In Game', start=self.pypresence_client.start_time) self.pypresence_client.update(state='In Game', start=self.pypresence_client.start_time)
self.difficulty = difficulty self.grid_size = grid_size
self.source_count = source_count
self.house_count = house_count
self.start = time.perf_counter()
self.wire_rotations = 0
self.cells = [] self.cells = []
self.power_sources = [] self.power_sources = []
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 = int(difficulty.split("x")[0]) self.map = generate_map(self.grid_size, int((self.grid_size * self.grid_size) / 10) if not source_count else source_count, int((self.grid_size * self.grid_size) / 5) if not house_count else house_count)
self.map = generate_map(self.grid_size, int((self.grid_size * self.grid_size) / 10), int((self.grid_size * self.grid_size) / 5))
self.spritelist = arcade.SpriteList() self.spritelist = arcade.SpriteList()
with open("settings.json", "r") as file:
self.settings = json.load(file)
def on_show_view(self): def on_show_view(self):
super().on_show_view() super().on_show_view()
@@ -36,12 +43,14 @@ class Game(arcade.gui.UIView):
self.won_label = self.anchor.add(arcade.gui.UILabel(text="You won!", font_size=48), anchor_x="center", anchor_y="center") self.won_label = self.anchor.add(arcade.gui.UILabel(text="You won!", font_size=48), anchor_x="center", anchor_y="center")
self.won_label.visible = False self.won_label.visible = False
self.info_label = self.anchor.add(arcade.gui.UILabel("Time spent: 0s Wire Rotations: 0", font_size=24), anchor_x="center", anchor_y="top")
x = (self.window.width / 2) - (self.grid_size * 64) / 2 x = (self.window.width / 2) - (self.grid_size * 64) / 2
y = (self.window.height / 2) + (self.grid_size * 64) / 2 y = (self.window.height / 2) + (self.grid_size * 64) / 2
for row in range(self.grid_size): for row in range(self.grid_size):
self.cells.append([]) self.cells.append([])
for col in range(self.grid_size): 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
@@ -126,12 +135,16 @@ class Game(arcade.gui.UIView):
continue continue
if cell.rect.point_in_rect((x, y)): if cell.rect.point_in_rect((x, y)):
cell.next_rotation() self.wire_rotations += 1
cell.next_rotation(self.settings["sfx"], self.settings.get("sfx_volume", 50))
def on_draw(self): def on_draw(self):
super().on_draw() super().on_draw()
self.spritelist.draw() self.spritelist.draw()
def on_update(self, delta_time):
self.info_label.text = f"Time left: {int(time.perf_counter() - self.start)}s Wire Rotations: {self.wire_rotations}"
def main_exit(self): def main_exit(self):
from menus.main import Main from menus.main import Main
self.window.show_view(Main(self.pypresence_client)) self.window.show_view(Main(self.pypresence_client))

View File

@@ -0,0 +1,41 @@
import arcade, arcade.gui
from utils.constants import CUSTOM_DIFFICULTY_SETTINGS, slider_style, button_style
from utils.preload import button_texture, button_hovered_texture
class CustomDifficulty(arcade.gui.UIView):
def __init__(self, pypresence_client):
super().__init__()
self.pypresence_client = pypresence_client
self.anchor = self.add_widget(arcade.gui.UIAnchorLayout(size_hint=(1, 1)))
self.box = self.anchor.add(arcade.gui.UIBoxLayout(size_between=self.window.height / 10), anchor_x="center", anchor_y="top")
self.custom_settings = {}
self.custom_setting_labels = {}
def set_custom_setting(self, key, value):
value = int(value)
self.custom_settings[key] = value
self.custom_setting_labels[key].text = f"{next(setting_list[1] for setting_list in CUSTOM_DIFFICULTY_SETTINGS if setting_list[0] == key)}: {value}"
def on_show_view(self):
super().on_show_view()
self.box.add(arcade.gui.UILabel(text="Custom Difficulty Selector", font_size=32))
self.box.add(arcade.gui.UISpace(height=self.window.height / 20))
for custom_setting_key, custom_setting_name, min_value, max_value in CUSTOM_DIFFICULTY_SETTINGS:
self.custom_settings[custom_setting_key] = int((max_value - min_value) / 2)
self.custom_setting_labels[custom_setting_key] = self.box.add(arcade.gui.UILabel(text=f"{custom_setting_name}: {int((max_value - min_value) / 2)}", font_size=28))
slider = self.box.add(arcade.gui.UISlider(step=1, min_value=min_value, max_value=max_value, value=int((max_value - min_value) / 2), style=slider_style, width=self.window.width / 2, height=self.window.height / 15))
slider._render_steps = lambda surface: None
slider.on_change = lambda event, key=custom_setting_key: self.set_custom_setting(key, event.new_value)
self.play_button = self.anchor.add(arcade.gui.UITextureButton(text="Play", style=button_style, texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 2, height=self.window.height / 10), anchor_x="center", anchor_y="bottom")
self.play_button.on_click = lambda event: self.play()
def play(self):
from game.play import Game
self.window.show_view(Game(self.pypresence_client, self.custom_settings["size"], self.custom_settings["source_count"], self.custom_settings["house_count"]))

View File

@@ -22,9 +22,17 @@ 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 ["7x7", "8x8", "9x9", "10x10", "12x12"]: for difficulty in ["7x7", "8x8", "9x9", "10x10", "11x11", "12x12", "Custom"]:
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)
if not difficulty == "Custom":
button.on_click = lambda e, difficulty=difficulty: self.play(int(difficulty.split("x")[0]))
else:
button.on_click = lambda e: self.custom_difficulty()
def custom_difficulty(self):
from menus.custom_difficulty import CustomDifficulty
self.window.show_view(CustomDifficulty(self.pypresence_client))
def play(self, difficulty): def play(self, difficulty):
from game.play import Game from game.play import Game

View File

@@ -55,6 +55,9 @@ class Main(arcade.gui.UIView):
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 = 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.tutorial_button = self.box.add(arcade.gui.UITextureButton(text="Tutorial", texture=button_texture, texture_hovered=button_hovered_texture, width=self.window.width / 2, height=self.window.height / 10, style=big_button_style))
self.tutorial_button.on_click = lambda event: self.tutorial()
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 = 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()
@@ -62,6 +65,10 @@ class Main(arcade.gui.UIView):
from menus.difficulty_selector import DifficultySelector from menus.difficulty_selector import DifficultySelector
self.window.show_view(DifficultySelector(self.pypresence_client)) self.window.show_view(DifficultySelector(self.pypresence_client))
def tutorial(self):
from menus.tutorial import Tutorial
self.window.show_view(Tutorial(self.pypresence_client))
def settings(self): def settings(self):
from menus.settings import Settings from menus.settings import Settings
self.window.show_view(Settings(self.pypresence_client)) self.window.show_view(Settings(self.pypresence_client))

37
menus/tutorial.py Normal file
View File

@@ -0,0 +1,37 @@
import arcade, arcade.gui
from utils.preload import button_texture, button_hovered_texture
from utils.constants import button_style
TUTORIAL_TEXT = """
In Connect the Current, you have to rotate power lines so power reaches to all of the houses.
- Every line has to be connected on all of it's sides.
- When needed, you might have to create loops of power or branches with no house linked to them.
(This is also because it's randomly generated and i couldn't find a way to generate maps with no meaningless branches)
- To rotate a line, just click on it and it will change its rotation.
- Maps are randomly generated, difficulty(size, source count, house count) depends on what you pick and grows exponentially.
"""
class Tutorial(arcade.gui.UIView):
def __init__(self, pypresence_client):
super().__init__()
self.pypresence_client = pypresence_client
self.pypresence_client.update(state="Checking Tutorial")
self.anchor = self.add_widget(arcade.gui.UIAnchorLayout(size_hint=(1, 1)))
self.box = self.anchor.add(arcade.gui.UIBoxLayout(space_between=20), anchor_x="center", anchor_y="top")
def main_exit(self):
from menus.main import Main
self.window.show_view(Main(self.pypresence_client))
def on_show_view(self):
super().on_show_view()
self.back_button = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='<--', style=button_style, width=100, height=50)
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.box.add(arcade.gui.UILabel(text="CTC Tutorial", font_size=40))
self.box.add(arcade.gui.UILabel(text=TUTORIAL_TEXT, font_size=20, multiline=True))

4
run.py
View File

@@ -90,10 +90,10 @@ else:
# theme_sound.play(volume=settings.get("music_volume", 50) / 100, loop=True) # theme_sound.play(volume=settings.get("music_volume", 50) / 100, loop=True)
try: try:
window = ControllerWindow(width=resolution[0], height=resolution[1], title='Game Of Life', samples=antialiasing, antialiasing=antialiasing > 0, fullscreen=fullscreen, vsync=vsync, resizable=False, style=style, visible=False) window = ControllerWindow(width=resolution[0], height=resolution[1], title='Connect the Current', samples=antialiasing, antialiasing=antialiasing > 0, fullscreen=fullscreen, vsync=vsync, resizable=False, style=style, visible=False)
except (FileNotFoundError, PermissionError) as e: except (FileNotFoundError, PermissionError) as e:
logging.warning(f"Controller support unavailable: {e}. Falling back to regular window.") logging.warning(f"Controller support unavailable: {e}. Falling back to regular window.")
window = arcade.Window(width=resolution[0], height=resolution[1], title='Game Of Life', samples=antialiasing, antialiasing=antialiasing > 0, fullscreen=fullscreen, vsync=vsync, resizable=False, style=style, visible=False) window = arcade.Window(width=resolution[0], height=resolution[1], title='Connect the Current', samples=antialiasing, antialiasing=antialiasing > 0, fullscreen=fullscreen, vsync=vsync, resizable=False, style=style, visible=False)
if vsync: if vsync:
window.set_vsync(True) window.set_vsync(True)

View File

@@ -3,6 +3,12 @@ from arcade.types import Color
from arcade.gui.widgets.buttons import UITextureButtonStyle, UIFlatButtonStyle from arcade.gui.widgets.buttons import UITextureButtonStyle, UIFlatButtonStyle
from arcade.gui.widgets.slider import UISliderStyle from arcade.gui.widgets.slider import UISliderStyle
CUSTOM_DIFFICULTY_SETTINGS = [
["source_count", "Source Count", 1, 20],
["house_count", "House Count", 1, 20],
["size", "Size", 3, 30]
]
ROTATIONS = { 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"],

View File

@@ -3,6 +3,8 @@ import arcade.gui, arcade
button_texture = arcade.gui.NinePatchTexture(64 // 4, 64 // 4, 64 // 4, 64 // 4, arcade.load_texture("assets/graphics/button.png")) 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")) button_hovered_texture = arcade.gui.NinePatchTexture(64 // 4, 64 // 4, 64 // 4, 64 // 4, arcade.load_texture("assets/graphics/button_hovered.png"))
wire_sound_effect = arcade.Sound("assets/sound/wire.mp3")
TEXTURE_MAP = { TEXTURE_MAP = {
("line", "vertical", True): arcade.load_texture("assets/graphics/powered_lines/line/vertical.png"), ("line", "vertical", True): arcade.load_texture("assets/graphics/powered_lines/line/vertical.png"),
("line", "vertical", False): arcade.load_texture("assets/graphics/unpowered_lines/line/vertical.png"), ("line", "vertical", False): arcade.load_texture("assets/graphics/unpowered_lines/line/vertical.png"),