turns-00030.parquet:51752
938d9ea57216572e797ec38e
turn 1/1o1-preview-2024-09-12EnglishIndonesia7175 words
degenerate_repetitionAbsentFinal dense release
USER
dari script ini tambahkan code agar saat meload file mid dapat memeriksa file mp3 dengan nama yang sama dengan file mid nya juga, dan ini optional, jika ada maka gunakan untuk di play untuk game, untuk bounce tetap menggunakan file mid, jika tidak ada maka gunakan saja sound dari file mid, buatkan dengan benar dan tepat : import pygame
import mido
import sys
import random
import requests
import colorsys
import os
from os.path import join, isfile
from time import time as get_current_time
from typing import Union, Optional, Any
from enum import Enum
from math import sin, pi
import math # Ditambahkan untuk fungsi matematika
pygame.mixer.init()
pygame.init()
FRAMERATE = 60
# =========================
# Customizable Configuration
# =========================
# Screen Resolution
SCREEN_WIDTH = 1280 # Ganti sesuai keinginan Anda
SCREEN_HEIGHT = 720 # Ganti sesuai keinginan Anda
# File Selection
SONG = "dashie - ultraphunk.mid" # Ganti dengan nama file MIDI Anda
# Song Information
SONG_NAME = 'SquareVibes' # Set the song name here
SONG_CREATOR = 'SUBSCRIBE :D' # Set the song creator here
# Other Customizable Settings
SQUARE_SPEED = 600 # Kecepatan kotak
SQUARE_SIZE = 50
BOUNCE_MIN_SPACING = 50 # Spasi minimum antar pantulan
DIRECTION_CHANGE = 30 # Peluang perubahan arah
PARTICLE_TRAIL = True # Aktifkan atau nonaktifkan jejak partikel
PARTICLE_SPEED = 5
BOUNCE_PEG = True
# =========================
def color_hunt():
def is_bright(hex_color):
hex_color = hex_color.lstrip('#')
rgb = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
luminance = 0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]
r, g, b = [x / 255.0 for x in rgb]
h, s, v = colorsys.rgb_to_hsv(r, g, b)
return luminance >= 130 and s >= 0.3
while True:
url = 'https://colorhunt.co/php/feed.php'
headers = {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
}
data = {
'step': 0,
'sort': 'random',
'tags': ''
}
response = requests.post(url, headers=headers, data=data)
if response.status_code == 200:
palettes = response.json()
color_pairs = []
for palette in palettes:
code = palette['code']
hex_colors = [f'#{code[i:i+6].upper()}' for i in range(0, len(code), 6)]
if len(hex_colors) == 4 and all(is_bright(hex_color) for hex_color in hex_colors):
color_pairs.append(tuple(hex_colors))
if color_pairs:
selected_color_pair = random.choice(color_pairs)
return selected_color_pair
else:
raise Exception(f"Failed to retrieve data. Status code: {response.status_code}")
selected_colors = color_hunt()
color1, color2, color3, color4 = selected_colors
class Config:
# Constants
SQUARE_SIZE = SQUARE_SIZE
PARTICLE_SPEED = PARTICLE_SPEED
# Colors
color_themes = {
"dark": {
"hallway": pygame.Color('#451952'),
"background": pygame.Color('#0F0F0F'),
"square": [
pygame.Color(color1),
pygame.Color(color2),
pygame.Color(color3),
pygame.Color(color4)
]
}
}
# Configuration
script_dir = os.path.dirname(os.path.abspath(__file__))
assets = os.path.join(script_dir, 'assets/icon.png')
song = os.path.join(script_dir, 'songs')
font = os.path.join(script_dir, 'assets/poppins-regular.ttf')
SCREEN_WIDTH = SCREEN_WIDTH
SCREEN_HEIGHT = SCREEN_HEIGHT
theme: Optional[str] = "dark"
seed: Optional[int] = None
camera_mode: Optional[int] = 2
start_playing_delay = 3000
max_notes: Optional[int] = None
bounce_min_spacing: Optional[float] = BOUNCE_MIN_SPACING
square_speed: Optional[int] = SQUARE_SPEED
volume: Optional[int] = 100
music_offset: Optional[int] = 0
direction_change_chance: Optional[int] = DIRECTION_CHANGE
theatre_mode = True
particle_trail = PARTICLE_TRAIL
do_color_bounce_pegs = BOUNCE_PEG
do_particles_on_bounce = True
# Non-configurable settings
backtrack_chance: Optional[float] = 0.02
backtrack_amount: Optional[int] = 40
square_swipe_anim_speed: Optional[int] = 4
particle_amount = 20
language = "english"
# Other random stuff
current_song = None
dt = 0.01
def get_colors():
return Config.color_themes.get(Config.theme, Config.color_themes["dark"])
def read_midi_file(file):
midi_file = mido.MidiFile(file=file)
notes = []
current_time = 0
for msg in midi_file:
if msg.type == 'note_on' and msg.velocity != 0:
timestamp = current_time + msg.time
notes.append(round(timestamp*1000)/1000)
current_time += msg.time
return notes
def remove_too_close_values(lst: list[float], threshold=30) -> list[float]:
"""Assumes the list is sorted"""
new = []
before = None
for _ in lst:
if before is None:
before = _
new.append(_)
continue
if before+threshold/1000 > _:
continue
before = _
new.append(_)
return new
def fix_overlap(rects: list[pygame.Rect], callback=None):
"""
Optimized function to merge overlapping rectangles efficiently.
"""
if callback is None:
callback = lambda _: None
rects = rects.copy()
# Sort rectangles based on x coordinate
rects.sort(key=lambda r: (r.x, r.y))
merged_rects = []
for rect in rects:
if not merged_rects:
merged_rects.append(rect)
else:
last = merged_rects[-1]
if last.colliderect(rect):
merged_rect = last.union(rect)
merged_rects[-1] = merged_rect
else:
merged_rects.append(rect)
callback("Finished merging rectangles")
return merged_rects
def get_font(size: int = 24) -> pygame.font.Font:
font_path = Config.font
return pygame.font.Font(font_path, size)
class MapLoadingFailureError(Exception):
"""The map fails to load (recurs function fails)"""
pass
class UserCancelsLoadingError(Exception):
"""User cancels the loading screen"""
pass
def interpolate_fn(n):
"""Interpolate sigmoidally from 0-1"""
n = min(max(n, 0), 1)
return sin(pi * (n - 0.5)) / 2 + 0.5
class Particle:
SPEED_VARIATION = 4
SIZE_MIN = 7
SIZE_MAX = 14
AGE_RATE = 20
SLOW_DOWN_RATE = 1.2
def __init__(self, pos: list[float], delta: list[float], invert_color: bool = False, color: Optional[tuple] = None):
self.pos = pos.copy()
self.size = random.randint(Particle.SIZE_MIN, Particle.SIZE_MAX)
self.delta = delta.copy()
self.delta[0] += random.randint(-Particle.SPEED_VARIATION, Particle.SPEED_VARIATION)/8
self.delta[1] += random.randint(-Particle.SPEED_VARIATION, Particle.SPEED_VARIATION)/8
if color is not None:
self.color = color
else:
# Warna default jika warna tidak diberikan
self.color = get_colors()["hallway"] if not invert_color else get_colors()["background"]
def age(self):
self.size -= Particle.AGE_RATE*Config.dt
self.x += self.delta[0] * Config.PARTICLE_SPEED
self.y += self.delta[1] * Config.PARTICLE_SPEED
if Config.dt != 0:
self.delta[0] /= (Particle.SLOW_DOWN_RATE+FRAMERATE) * Config.dt
self.delta[1] /= (Particle.SLOW_DOWN_RATE+FRAMERATE) * Config.dt
return self.size <= 0
@property
def x(self):
return self.pos[0]
@x.setter
def x(self, val: float):
self.pos[0] = val
@property
def y(self):
return self.pos[1]
@y.setter
def y(self, val: float):
self.pos[1] = val
@property
def rect(self):
return pygame.Rect(self.x-self.size/2, self.y-self.size/2, *(2*[self.size]))
class Bounce:
def __init__(self, sq_pos: list[float], sq_dir: list[int], time: float, bounce_dir: int):
self.square_pos = sq_pos # New square position
self.square_dir = sq_dir # New square direction
self.bounce_dir = bounce_dir # Bounce direction for squish effect; 0 or 1
self.time = time # Time during bounce
def get_collision_rect(self):
sx, sy = self.square_pos
if self.bounce_dir == 0:
# Bounce left or right wall
if self.square_dir[0] == -1:
# Right wall
return pygame.Rect(
sx+Config.SQUARE_SIZE/2+1,
sy-10,
10,
20
)
elif self.square_dir[0] == 1:
# Left wall
return pygame.Rect(
sx-10-Config.SQUARE_SIZE/2-1,
sy-10,
10,
20
)
elif self.bounce_dir == 1:
# Bounce top or bottom wall
if self.square_dir[1] == -1:
# Bottom wall
return pygame.Rect(
sx-10,
sy+Config.SQUARE_SIZE/2+1,
20,
10
)
elif self.square_dir[1] == 1:
# Top wall
return pygame.Rect(
sx-10,
sy-10-Config.SQUARE_SIZE/2-1,
20,
10
)
def copy(self) -> "Bounce":
return Bounce(self.square_pos.copy(), self.square_dir.copy(), self.time, self.bounce_dir)
def __repr__(self):
return f"<Bounce(sq_pos={self.square_pos}, sq_dir={self.square_dir}, time={self.time}, dir={self.bounce_dir})"
class Square:
def __init__(self, x: float = 0, y: float = 0, dx: int = 1, dy: int = 1):
self.pos: list[float] = [x, y]
self.dir: list[int] = [dx, dy]
self.last_bounce_time = -100
self.latest_bounce_direction = 0 # 0 = horiz, 1 = vert
self.past_colors = []
self.died = False
def register_past_color(self, col: tuple[int, int, int]):
for _ in range(max(Config.square_swipe_anim_speed, 1)):
self.past_colors.insert(0, col)
while len(self.past_colors) > Config.SQUARE_SIZE * 4 / 5:
self.past_colors.pop()
def get_surface(self, size: tuple[int, int]):
ss = int(Config.SQUARE_SIZE * 4 / 5)
surf = pygame.Surface((ss, ss))
for index, col in enumerate(self.past_colors):
y = index if self.dir_y != 1 else ss - 1 - index
pygame.draw.line(surf, col, (0, y), (ss, y))
return pygame.transform.scale(surf, size)
def copy(self) -> "Square":
new = Square(*self.pos, *self.dir)
new.last_bounce_time = self.last_bounce_time
new.latest_bounce_direction = self.latest_bounce_direction
return new
def get_current_color(self):
square_color_index = round((self.dir_x + 1) / 2 + self.dir_y + 1)
return get_colors()["square"][square_color_index % len(get_colors()["square"])]
@property
def x(self):
return self.pos[0]
@property
def y(self):
return self.pos[1]
def draw(self, screen: pygame.Surface, sqrect: pygame.Rect):
if self.died:
return
square_color_index = round((self.dir_x + 1) / 2 + self.dir_y + 1)
self.register_past_color(get_colors()["square"][square_color_index % len(get_colors()["square"])])
pygame.draw.rect(screen, (0, 0, 0), sqrect)
sq_surf = self.get_surface(
tuple(sqrect.inflate(-int(Config.SQUARE_SIZE / 5), -int(Config.SQUARE_SIZE / 5))[2:]))
screen.blit(sq_surf, sq_surf.get_rect(center=sqrect.center))
@x.setter
def x(self, val: int):
self.pos[0] = val
@y.setter
def y(self, val: int):
self.pos[1] = val
@property
def dir_x(self):
return self.dir[0]
@property
def dir_y(self):
return self.dir[1]
@property
def rect(self):
return pygame.Rect(self.x - Config.SQUARE_SIZE / 2, self.y - Config.SQUARE_SIZE / 2,
*([Config.SQUARE_SIZE] * 2))
def obey_bounce(self, bounce: Bounce):
# Planned bounces
self.pos = bounce.square_pos.copy()
self.dir = bounce.square_dir.copy()
self.latest_bounce_direction = bounce.bounce_dir
self.last_bounce_time = bounce.time
return
def reg_move(self, use_dt: bool = True):
self.x += self.dir_x * Config.square_speed * (Config.dt if use_dt else 1 / FRAMERATE)
self.y += self.dir_y * Config.square_speed * (Config.dt if use_dt else 1 / FRAMERATE)
class Camera:
def __init__(self, x: int = 0, y: int = 0):
self.x = x
self.y = y
self.locked_on_square = True
self.lock_type: CameraFollow = CameraFollow(Config.camera_mode)
def attempt_movement(self):
if not self.locked_on_square:
keys = pygame.key.get_pressed()
shift_modifier = (keys[pygame.K_LSHIFT] | keys[pygame.K_RSHIFT]) + 1
self.x += (keys[pygame.K_d] - keys[pygame.K_a]) * 500 * shift_modifier / FRAMERATE
self.y += (keys[pygame.K_s] - keys[pygame.K_w]) * 500 * shift_modifier / FRAMERATE
@property
def pos(self):
return self.x, self.y
@pos.setter
def pos(self, val: Union[tuple[int, int], list[int]]):
self.x, self.y = val
def offset(self, pos_or_rect: Union[pygame.Rect, tuple[int, int]]) -> Union[pygame.Rect, list[int]]:
if isinstance(pos_or_rect, pygame.Rect):
return pos_or_rect.move(-self.x, -self.y)
else:
return [pos_or_rect[0]-self.x, pos_or_rect[1]-self.y]
def apply_parallax(self, pos: tuple[float, float], depth: float) -> list[float]:
# Parallax factor: nearer stars move faster than distant ones
parallax_factor = 0.2 + depth * 0.8 # From 0.2 to 1.0
screen_x = (pos[0] - self.x) * parallax_factor
screen_y = (pos[1] - self.y) * parallax_factor
return [screen_x, screen_y]
def follow(self, square: Square):
# Square in center
if self.lock_type == CameraFollow.Center:
self.pos = [square.x - Config.SCREEN_WIDTH / 2, square.y - Config.SCREEN_HEIGHT / 2]
# Smooth camera
if self.lock_type == CameraFollow.Smoothed:
easing_rate = 3
self.x = (square.x - Config.SCREEN_WIDTH / 2) * easing_rate * Config.dt + self.x - easing_rate * self.x * Config.dt
self.y = (square.y - Config.SCREEN_HEIGHT / 2) * easing_rate * Config.dt + self.y - easing_rate * self.y * Config.dt
class CameraFollow(Enum):
Center = 0 # Center the square
Smoothed = 2 # Smoothed camera
class Star:
def __init__(self, pos: tuple[float, float], depth: float):
self.initial_pos = list(pos)
self.depth = depth # between 0 (far) and 1 (near)
size_min = 0.5
size_max = 2
self.size = size_min + (1 - self.depth) * (size_max - size_min)
self.phase = random.uniform(0, 2 * math.pi) # Ditambahkan untuk fase acak
self.brightness = 0 # Akan diperbarui dalam metode update
self.color = (255, 255, 255) # Warna default, akan diperbarui
def update(self, time):
# Adjust brightness over time to create blinking effect
blink_speed = 10 # can adjust to get desired speed
brightness_variation = (math.sin(blink_speed * time + self.phase) + 1) / 2 # Normalize between 0 and 1
min_brightness = 100
max_brightness = 255
self.brightness = min_brightness + brightness_variation * (max_brightness - min_brightness)
self.color = (int(self.brightness),) * 3 # Update color based on brightness
def get_screen_pos(self, camera: Camera):
parallax_factor = 0.2 + self.depth * 0.8 # from 0.2 to 1.0
screen_x = (self.initial_pos[0] - camera.x) * parallax_factor
screen_y = (self.initial_pos[1] - camera.y) * parallax_factor
return [screen_x, screen_y]
class World:
"""It's a cruel world out there"""
def __init__(self):
self.future_bounces: list[Bounce] = []
self.past_bounces: list[Bounce] = []
self.start_time = 0
self.time = 0
self.rectangles: list[pygame.Rect] = []
self.collision_times: list[float] = []
self.particles: list[Particle] = []
self.stars: list[Star] = []
self.timestamps = []
self.square = Square()
self.colors = []
self.safe_areas: list[pygame.Rect] = []
def update_time(self) -> None:
self.time = get_current_time() - self.start_time
def get_next_bounce(self) -> Bounce:
"""Also pops the bounce from the future_bounces list"""
self.past_bounces.append(self.future_bounces.pop(0))
return self.past_bounces[-1]
def add_bounce_particles(self, sp: list[float], sd: list[float], color: tuple):
for _ in range(Config.particle_amount):
new = Particle([sp[0] + random.randint(-10, 10), sp[1] + random.randint(-10, 10)], sd, color=color)
self.particles.append(new)
def handle_bouncing(self, square: Square):
if len(self.future_bounces):
if (self.time * 1000 + Config.music_offset)/1000 > self.future_bounces[0].time:
current_bounce = self.get_next_bounce()
before = square.dir.copy()
square.obey_bounce(current_bounce)
changed = square.dir.copy()
for _ in range(2):
if before[_] == changed[_]:
changed[_] = 0
else:
changed[_] = -changed[_]
if Config.do_particles_on_bounce:
square_color = square.get_current_color()
self.add_bounce_particles(square.pos, changed, color=square_color)
# Stop square at end
if len(self.future_bounces) == 0:
square.dir = [0, 0]
square.pos = current_bounce.square_pos
def generate_stars(self):
self.stars = []
# Menghitung total area aman
total_safe_area = sum(area.width * area.height for area in self.safe_areas)
star_density = 0.0001 # Sesuaikan sesuai kebutuhan
total_num_stars = int(total_safe_area * star_density)
min_distance_squared = (20) ** 2 # Menghindari clustering
# Mendapatkan bounding rectangle dari semua area aman
min_x = min(area.left for area in self.safe_areas)
max_x = max(area.right for area in self.safe_areas)
min_y = min(area.top for area in self.safe_areas)
max_y = max(area.bottom for area in self.safe_areas)
bounding_rect = pygame.Rect(min_x, min_y, max_x - min_x, max_y - min_y)
stars = []
attempts = 0
max_attempts = total_num_stars * 10
while len(stars) < total_num_stars and attempts < max_attempts:
x = random.uniform(bounding_rect.left, bounding_rect.right)
y = random.uniform(bounding_rect.top, bounding_rect.bottom)
point = (x, y)
# Memastikan titik berada dalam area aman
inside_safe_area = any(area.collidepoint(point) for area in self.safe_areas)
if inside_safe_area:
# Memastikan jarak minimum antar bintang
if all((star.initial_pos[0] - x) ** 2 + (star.initial_pos[1] - y) ** 2 >= min_distance_squared for star in stars):
depth = random.uniform(0, 1)
star = Star((x, y), depth)
stars.append(star)
attempts += 1
self.stars = stars
def gen_future_bounces(self, _start_notes: list[float], percent_update_callback):
"""Recursive function with optimized overlap checking"""
total_notes = len(_start_notes)
max_percent = 0
path = []
safe_areas = []
force_return = 0
def recurs(
square: Square,
notes: list[float],
bounces_so_far: list[Bounce] = None,
prev_index_priority=None,
t: float = 0,
depth: int = 0
) -> Union[list[Bounce], bool]:
nonlocal force_return, max_percent
if prev_index_priority is None:
prev_index_priority = [0, 1]
if bounces_so_far is None:
bounces_so_far = []
if total_notes:
gone_through_percent = (total_notes-len(notes)) * 100 // total_notes
while gone_through_percent > max_percent:
max_percent += 1
if percent_update_callback(f"{max_percent}% done generating map"):
raise UserCancelsLoadingError()
all_bounce_rects = [_bounc.get_collision_rect() for _bounc in bounces_so_far]
if len(notes) == 0:
return bounces_so_far
path_segment_start = len(path)
start_rect = square.rect.copy()
while True:
t += 1/FRAMERATE
square.reg_move(False)
path.append(square.rect)
if t > notes[0]:
# No collision (we good)
bounce_indexes = prev_index_priority
# Randomly change direction every X% of the time
if random.random() * 100 < Config.direction_change_chance:
bounce_indexes = list(bounce_indexes.__reversed__())
# Add safe area
safe_areas.append(start_rect.union(square.rect))
for direction_to_bounce in bounce_indexes:
square.dir[direction_to_bounce] *= -1
bounces_so_far.append(Bounce(square.pos.copy(), square.dir.copy(), t, direction_to_bounce))
toextend = recurs(
square=square.copy(),
notes=notes[1:],
bounces_so_far=[_b.copy() for _b in bounces_so_far],
t=t,
prev_index_priority=bounce_indexes.copy(),
depth=depth+1
)
if toextend:
return toextend
else:
bounces_so_far = bounces_so_far[:-1]
square.dir[direction_to_bounce] *= -1
# Backtrack if necessary
if force_return:
force_return -= 1
while len(path) != path_segment_start:
path.pop()
return False
continue
while len(path) != path_segment_start:
path.pop()
return False
othercheck = False
if len(bounces_so_far):
othercheck = bounces_so_far[-1].get_collision_rect().collidelist(path[:-10])+1
if square.rect.collidelist(all_bounce_rects) != -1 or othercheck:
if depth > 200:
if random.random() < Config.backtrack_chance:
max_percent -= (Config.backtrack_amount * 100 // total_notes) + 1
force_return = Config.backtrack_amount
while len(path) != path_segment_start:
path.pop()
return False
_start_notes = _start_notes[:Config.max_notes] if Config.max_notes is not None else _start_notes
self.future_bounces = recurs(
square=self.square.copy(),
notes=remove_too_close_values(
[_sn for _sn in _start_notes],
threshold=Config.bounce_min_spacing
)
)
if self.future_bounces is False:
raise MapLoadingFailureError("The map failed to generate because of the recursion function. " +
"If the MIDI has too many notes too close, it may not generate. " +
"Maybe try changing the \"bounce min spacing\", \"square speed\", or \"change dir chance\" in the config.")
if len(self.future_bounces) == 0:
raise MapLoadingFailureError("Map safe area list is empty. Please report this issue.")
percent_update_callback("Merging overlapping safe areas")
# Merging overlapping safe areas more efficiently
self.safe_areas = fix_overlap(safe_areas, percent_update_callback)
self.rectangles = [_fb.get_collision_rect() for _fb in self.future_bounces]
self.collision_times = [_fb.time for _fb in self.future_bounces]
def gather_colors(num_colors=20):
colors = []
while len(colors) < num_colors:
new_colors = color_hunt()
rgb_colors = [tuple(int(hex_color[i:i+2], 16) for i in (1, 3, 5)) for hex_color in new_colors]
colors.extend(rgb_colors)
return colors[:num_colors]
collected_colors = gather_colors()
# Setting random colors for the generated pegs
self.colors = [random.choice(collected_colors) for _ in self.future_bounces]
# Generate stars in safe areas
self.generate_stars()
return self.safe_areas
class Game:
def __init__(self):
self.active = False
self.notes = []
self.camera = Camera()
self.world = World()
self.safe_areas: list[pygame.Rect] = []
self.music_has_played = False
self.offset_happened = False
def start_song(self, screen: pygame.Surface):
random.seed(Config.seed)
# Load song and notes
with open(Config.current_song.fp, 'rb') as f:
notes = read_midi_file(file=f)
notes = sorted([note for note in notes])
self.notes = notes
# Other settings
self.world = World()
self.music_has_played = False
self.offset_happened = False
self.camera.lock_type = CameraFollow(Config.camera_mode)
screen.fill(get_colors()["background"])
pygame.display.flip()
def update_loading_screen(message: str):
screen.fill(get_colors()["background"], pygame.Rect(0, 0, Config.SCREEN_WIDTH, Config.SCREEN_HEIGHT))
font_size = int(Config.SCREEN_HEIGHT * 0.05)
loading_text = get_font(font_size).render(message, True, get_colors()["hallway"])
text_rect = loading_text.get_rect()
text_rect.center = (Config.SCREEN_WIDTH // 2, Config.SCREEN_HEIGHT // 2)
screen.blit(loading_text, text_rect)
pygame.display.flip()
try:
self.safe_areas = self.world.gen_future_bounces(self.notes, update_loading_screen)
# No need to fix overlap here, as it's handled in gen_future_bounces
except UserCancelsLoadingError:
return True
except MapLoadingFailureError as e:
return e.args[0] if len(e.args) else "Big error... can't load map :("
# Display song title and creator with fade-in and fade-out effects
self.display_song_info(screen)
self.world.start_time = get_current_time()
self.world.square.dir = [0, 0]
if self.world.future_bounces:
self.world.square.pos = self.world.future_bounces[0].square_pos
else:
self.world.square.pos = [Config.SCREEN_WIDTH / 2, Config.SCREEN_HEIGHT / 2]
return None
def display_song_info(self, screen):
# Function to display song title and creator with fade-in and fade-out effects
display_duration = 5000 # milliseconds
fade_duration = 1000 # milliseconds for fade-in and fade-out
start_time = pygame.time.get_ticks()
elapsed = 0
font_size = int(Config.SCREEN_HEIGHT * 0.10)
font_size2 = int(Config.SCREEN_HEIGHT * 0.05)
# Prepare text surfaces with per-pixel alpha
title_surface = get_font(font_size).render(f"{SONG_NAME}", True, color4)
creator_surface = get_font(font_size2).render(f"{SONG_CREATOR}", True, color4)
# Enable alpha blending
title_surface = title_surface.convert_alpha()
creator_surface = creator_surface.convert_alpha()
# Centering the text
title_rect = title_surface.get_rect(center=(Config.SCREEN_WIDTH // 2, Config.SCREEN_HEIGHT // 2 - font_size))
creator_rect = creator_surface.get_rect(center=(Config.SCREEN_WIDTH // 2, Config.SCREEN_HEIGHT // 2 + font_size2))
while elapsed < display_duration:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
self.handle_event(event)
elapsed = pygame.time.get_ticks() - start_time
# Calculate alpha value
if elapsed < fade_duration:
# Fade-in phase
alpha = int(255 * (elapsed / fade_duration))
elif elapsed > display_duration - fade_duration:
# Fade-out phase
alpha = int(255 * ((display_duration - elapsed) / fade_duration))
else:
# Fully visible
alpha = 255
# Clamp alpha value
alpha = max(0, min(255, alpha))
# Set alpha
title_surface.set_alpha(alpha)
creator_surface.set_alpha(alpha)
# Clear screen
screen.fill(get_colors()["background"])
# Draw text
screen.blit(title_surface, title_rect)
screen.blit(creator_surface, creator_rect)
pygame.display.flip()
pygame.time.delay(30) # Slight delay to control the frame rate
def draw(self, screen: pygame.Surface):
if not self.active:
return
if not self.music_has_played:
if not self.offset_happened:
for bnc_change in self.world.future_bounces:
bnc_change.time += Config.start_playing_delay / 1000
self.offset_happened = True
if self.world.time-Config.music_offset/1000 > Config.start_playing_delay/1000:
self.music_has_played = True
song_load_before = get_current_time()
pygame.mixer.music.play()
for bnc_change in self.world.future_bounces:
bnc_change.time += (get_current_time()-song_load_before)/1000
screen_rect = screen.get_rect()
# Set world time
self.world.update_time()
# Move camera
self.camera.attempt_movement()
# Handle square bounces
self.world.handle_bouncing(self.world.square)
# Move square
self.world.square.reg_move()
# Square in center of camera if locked
if self.camera.locked_on_square:
self.camera.follow(self.world.square)
# Bounce animation
sqrect = self.camera.offset(self.world.square.rect)
if (self.world.time - 0.25) + Config.music_offset / 1000 < self.world.square.last_bounce_time:
lerp = abs((self.world.time - 0.25 + Config.music_offset / 1000) - self.world.square.last_bounce_time) * 5
lerp = lerp ** 2 # Square it for better-looking interpolation
if self.world.square.latest_bounce_direction:
sqrect.inflate_ip((lerp * 5, -10 * lerp))
else:
sqrect.inflate_ip((-10 * lerp, lerp * 5))
# Draw background (walls)
screen.fill(get_colors()["background"])
# Create a hallway surface
hallway_surface = pygame.Surface((Config.SCREEN_WIDTH, Config.SCREEN_HEIGHT), pygame.SRCALPHA)
# Safe areas
for safe_area in self.world.safe_areas:
offsetted = self.camera.offset(safe_area)
if screen_rect.colliderect(offsetted):
pygame.draw.rect(hallway_surface, get_colors()["hallway"], offsetted)
# Obtain current time
current_time = pygame.time.get_ticks() / 1000.0 # Time in seconds
# Draw stars onto the hallway surface
for star in self.world.stars:
star.update(current_time) # Update blinking effect
screen_pos = star.get_screen_pos(self.camera)
if 0 <= screen_pos[0] < Config.SCREEN_WIDTH and 0 <= screen_pos[1] < Config.SCREEN_HEIGHT:
# Check if star's position is within the hallway (safe areas)
if hallway_surface.get_at((int(screen_pos[0]), int(screen_pos[1]))).a != 0:
pygame.draw.circle(hallway_surface, star.color, (int(screen_pos[0]), int(screen_pos[1])), int(star.size))
# Blit the hallway surface onto the main screen
screen.blit(hallway_surface, (0, 0))
# Draw pegs
for i, bounce_rect in enumerate(self.world.rectangles):
offsetted = self.camera.offset(bounce_rect)
if offsetted.colliderect(screen_rect):
if Config.do_color_bounce_pegs and self.world.collision_times[i] < (self.world.time * 1000 + Config.music_offset - Config.start_playing_delay)/1000:
pygame.draw.rect(screen, self.world.colors[i], offsetted)
else:
pygame.draw.rect(screen, get_colors()["background"], offsetted)
# Particles
for particle in self.world.particles:
pygame.draw.rect(screen, particle.color, self.camera.offset(particle.rect))
for remove_particle in [particle for particle in self.world.particles if particle.age()]:
self.world.particles.remove(remove_particle)
# Particle trail in game
if Config.particle_trail:
# Tambahkan partikel secara periodik
if not self.world.square.died:
square_color = self.world.square.get_current_color()
new = Particle(self.world.square.pos.copy(), [0, 0], True, color=(255, 255, 255))
new.delta = [random.randint(-2, 2)/20, random.randint(5, 15)/20]
self.world.particles.append(new)
# Draw square
self.world.square.draw(screen, sqrect)
def handle_event(self, event: pygame.event.Event):
if not self.active:
return False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
pygame.quit()
sys.exit()
if event.key == pygame.K_TAB:
self.camera.locked_on_square = not self.camera.locked_on_square
class InvalidSongError(Exception):
"""The song is invalid somehow"""
pass
def make_song_from_midi(fpath: str) -> Any:
new_song = type('Song', (object,), {})()
new_song.fp = fpath
new_song.name = SONG_NAME # Use the song name from the top
new_song.creator = SONG_CREATOR # Use the song creator from the top
new_song.music_offset = 0
return new_song
def main():
pygame.init()
clock = pygame.time.Clock()
screen = pygame.display.set_mode((Config.SCREEN_WIDTH, Config.SCREEN_HEIGHT))
pygame.display.set_caption("Midi Playground")
try:
pygame.display.set_icon(pygame.image.load(Config.assets).convert_alpha())
except Exception as e:
print(e)
# Specify the song file to use
selected_song_filepath = join(Config.song, SONG)
if not isfile(selected_song_filepath):
print(f"Song file {selected_song_filepath} not found.")
return
Config.current_song = make_song_from_midi(selected_song_filepath)
# Load the music file
try:
pygame.mixer.music.load(Config.current_song.fp)
except Exception as e:
print(f"Error loading MIDI file as audio: {e}")
print("Your platform may not support MIDI playback via pygame.mixer.music.")
print("The game will run without audio.")
# Uncomment the line below to exit if audio is essential
# return
pygame.mixer.music.set_volume(Config.volume / 100)
# Initialize the game
game = Game()
# Start the game
if msg := game.start_song(screen):
if isinstance(msg, str):
print(f"Error starting game: {msg}")
return
game.active = True
running = True
while running:
# screen.fill(get_colors()["background"]) # Moved to the draw function
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
game.handle_event(event)
game.draw(screen)
pygame.display.flip()
clock.tick(FRAMERATE)
Config.dt = clock.get_time() / 1000.0 if clock.get_time() > 0 else 0.016 # Avoid division by zero
pygame.quit()
if __name__ == '__main__':
main()ASSISTANT
Certainly! Here's the updated code that implements the requested functionality:
```python
import pygame
import mido
import sys
import random
import requests
import colorsys
import os
from os.path import join, isfile
from time import time as get_current_time
from typing import Union, Optional, Any
from enum import Enum
from math import sin, pi
import math # Ditambahkan untuk fungsi matematika
pygame.mixer.init()
pygame.init()
FRAMERATE = 60
# =========================
# Customizable Configuration
# =========================
# Screen Resolution
SCREEN_WIDTH = 1280 # Ganti sesuai keinginan Anda
SCREEN_HEIGHT = 720 # Ganti sesuai keinginan Anda
# File Selection
SONG = "dashie - ultraphunk.mid" # Ganti dengan nama file MIDI Anda
# Song Information
SONG_NAME = 'SquareVibes' # Set the song name here
SONG_CREATOR = 'SUBSCRIBE :D' # Set the song creator here
# Other Customizable Settings
SQUARE_SPEED = 600 # Kecepatan kotak
SQUARE_SIZE = 50
BOUNCE_MIN_SPACING = 50 # Spasi minimum antar pantulan
DIRECTION_CHANGE = 30 # Peluang perubahan arah
PARTICLE_TRAIL = True # Aktifkan atau nonaktifkan jejak partikel
PARTICLE_SPEED = 5
BOUNCE_PEG = True
# =========================
def color_hunt():
def is_bright(hex_color):
hex_color = hex_color.lstrip('#')
rgb = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
luminance = 0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]
r, g, b = [x / 255.0 for x in rgb]
h, s, v = colorsys.rgb_to_hsv(r, g, b)
return luminance >= 130 and s >= 0.3
while True:
url = 'https://colorhunt.co/php/feed.php'
headers = {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
}
data = {
'step': 0,
'sort': 'random',
'tags': ''
}
response = requests.post(url, headers=headers, data=data)
if response.status_code == 200:
palettes = response.json()
color_pairs = []
for palette in palettes:
code = palette['code']
hex_colors = [f'#{code[i:i+6].upper()}' for i in range(0, len(code), 6)]
if len(hex_colors) == 4 and all(is_bright(hex_color) for hex_color in hex_colors):
color_pairs.append(tuple(hex_colors))
if color_pairs:
selected_color_pair = random.choice(color_pairs)
return selected_color_pair
else:
raise Exception(f"Failed to retrieve data. Status code: {response.status_code}")
selected_colors = color_hunt()
color1, color2, color3, color4 = selected_colors
class Config:
# Constants
SQUARE_SIZE = SQUARE_SIZE
PARTICLE_SPEED = PARTICLE_SPEED
# Colors
color_themes = {
"dark": {
"hallway": pygame.Color('#451952'),
"background": pygame.Color('#0F0F0F'),
"square": [
pygame.Color(color1),
pygame.Color(color2),
pygame.Color(color3),
pygame.Color(color4)
]
}
}
# Configuration
script_dir = os.path.dirname(os.path.abspath(__file__))
assets = os.path.join(script_dir, 'assets/icon.png')
song = os.path.join(script_dir, 'songs')
font = os.path.join(script_dir, 'assets/poppins-regular.ttf')
SCREEN_WIDTH = SCREEN_WIDTH
SCREEN_HEIGHT = SCREEN_HEIGHT
theme: Optional[str] = "dark"
seed: Optional[int] = None
camera_mode: Optional[int] = 2
start_playing_delay = 3000
max_notes: Optional[int] = None
bounce_min_spacing: Optional[float] = BOUNCE_MIN_SPACING
square_speed: Optional[int] = SQUARE_SPEED
volume: Optional[int] = 100
music_offset: Optional[int] = 0
direction_change_chance: Optional[int] = DIRECTION_CHANGE
theatre_mode = True
particle_trail = PARTICLE_TRAIL
do_color_bounce_pegs = BOUNCE_PEG
do_particles_on_bounce = True
# Non-configurable settings
backtrack_chance: Optional[float] = 0.02
backtrack_amount: Optional[int] = 40
square_swipe_anim_speed: Optional[int] = 4
particle_amount = 20
language = "english"
# Other random stuff
current_song = None
dt = 0.01
def get_colors():
return Config.color_themes.get(Config.theme, Config.color_themes["dark"])
def read_midi_file(file):
midi_file = mido.MidiFile(file=file)
notes = []
current_time = 0
for msg in midi_file:
if msg.type == 'note_on' and msg.velocity != 0:
timestamp = current_time + msg.time
notes.append(round(timestamp*1000)/1000)
current_time += msg.time
return notes
def remove_too_close_values(lst: list[float], threshold=30) -> list[float]:
"""Assumes the list is sorted"""
new = []
before = None
for _ in lst:
if before is None:
before = _
new.append(_)
continue
if before+threshold/1000 > _:
continue
before = _
new.append(_)
return new
def fix_overlap(rects: list[pygame.Rect], callback=None):
"""
Optimized function to merge overlapping rectangles efficiently.
"""
if callback is None:
callback = lambda _: None
rects = rects.copy()
# Sort rectangles based on x coordinate
rects.sort(key=lambda r: (r.x, r.y))
merged_rects = []
for rect in rects:
if not merged_rects:
merged_rects.append(rect)
else:
last = merged_rects[-1]
if last.colliderect(rect):
merged_rect = last.union(rect)
merged_rects[-1] = merged_rect
else:
merged_rects.append(rect)
callback("Finished merging rectangles")
return merged_rects
def get_font(size: int = 24) -> pygame.font.Font:
font_path = Config.font
return pygame.font.Font(font_path, size)
class MapLoadingFailureError(Exception):
"""The map fails to load (recurs function fails)"""
pass
class UserCancelsLoadingError(Exception):
"""User cancels the loading screen"""
pass
def interpolate_fn(n):
"""Interpolate sigmoidally from 0-1"""
n = min(max(n, 0), 1)
return sin(pi * (n - 0.5)) / 2 + 0.5
class Particle:
SPEED_VARIATION = 4
SIZE_MIN = 7
SIZE_MAX = 14
AGE_RATE = 20
SLOW_DOWN_RATE = 1.2
def __init__(self, pos: list[float], delta: list[float], invert_color: bool = False, color: Optional[tuple] = None):
self.pos = pos.copy()
self.size = random.randint(Particle.SIZE_MIN, Particle.SIZE_MAX)
self.delta = delta.copy()
self.delta[0] += random.randint(-Particle.SPEED_VARIATION, Particle.SPEED_VARIATION)/8
self.delta[1] += random.randint(-Particle.SPEED_VARIATION, Particle.SPEED_VARIATION)/8
if color is not None:
self.color = color
else:
# Warna default jika warna tidak diberikan
self.color = get_colors()["hallway"] if not invert_color else get_colors()["background"]
def age(self):
self.size -= Particle.AGE_RATE*Config.dt
self.x += self.delta[0] * Config.PARTICLE_SPEED
self.y += self.delta[1] * Config.PARTICLE_SPEED
if Config.dt != 0:
self.delta[0] /= (Particle.SLOW_DOWN_RATE+FRAMERATE) * Config.dt
self.delta[1] /= (Particle.SLOW_DOWN_RATE+FRAMERATE) * Config.dt
return self.size <= 0
@property
def x(self):
return self.pos[0]
@x.setter
def x(self, val: float):
self.pos[0] = val
@property
def y(self):
return self.pos[1]
@y.setter
def y(self, val: float):
self.pos[1] = val
@property
def rect(self):
return pygame.Rect(self.x-self.size/2, self.y-self.size/2, *(2*[self.size]))
class Bounce:
def __init__(self, sq_pos: list[float], sq_dir: list[int], time: float, bounce_dir: int):
self.square_pos = sq_pos # New square position
self.square_dir = sq_dir # New square direction
self.bounce_dir = bounce_dir # Bounce direction for squish effect; 0 or 1
self.time = time # Time during bounce
def get_collision_rect(self):
sx, sy = self.square_pos
if self.bounce_dir == 0:
# Bounce left or right wall
if self.square_dir[0] == -1:
# Right wall
return pygame.Rect(
sx+Config.SQUARE_SIZE/2+1,
sy-10,
10,
20
)
elif self.square_dir[0] == 1:
# Left wall
return pygame.Rect(
sx-10-Config.SQUARE_SIZE/2-1,
sy-10,
10,
20
)
elif self.bounce_dir == 1:
# Bounce top or bottom wall
if self.square_dir[1] == -1:
# Bottom wall
return pygame.Rect(
sx-10,
sy+Config.SQUARE_SIZE/2+1,
20,
10
)
elif self.square_dir[1] == 1:
# Top wall
return pygame.Rect(
sx-10,
sy-10-Config.SQUARE_SIZE/2-1,
20,
10
)
def copy(self) -> "Bounce":
return Bounce(self.square_pos.copy(), self.square_dir.copy(), self.time, self.bounce_dir)
def __repr__(self):
return f"<Bounce(sq_pos={self.square_pos}, sq_dir={self.square_dir}, time={self.time}, dir={self.bounce_dir})"
class Square:
def __init__(self, x: float = 0, y: float = 0, dx: int = 1, dy: int = 1):
self.pos: list[float] = [x, y]
self.dir: list[int] = [dx, dy]
self.last_bounce_time = -100
self.latest_bounce_direction = 0 # 0 = horiz, 1 = vert
self.past_colors = []
self.died = False
def register_past_color(self, col: tuple[int, int, int]):
for _ in range(max(Config.square_swipe_anim_speed, 1)):
self.past_colors.insert(0, col)
while len(self.past_colors) > Config.SQUARE_SIZE * 4 / 5:
self.past_colors.pop()
def get_surface(self, size: tuple[int, int]):
ss = int(Config.SQUARE_SIZE * 4 / 5)
surf = pygame.Surface((ss, ss))
for index, col in enumerate(self.past_colors):
y = index if self.dir_y != 1 else ss - 1 - index
pygame.draw.line(surf, col, (0, y), (ss, y))
return pygame.transform.scale(surf, size)
def copy(self) -> "Square":
new = Square(*self.pos, *self.dir)
new.last_bounce_time = self.last_bounce_time
new.latest_bounce_direction = self.latest_bounce_direction
return new
def get_current_color(self):
square_color_index = round((self.dir_x + 1) / 2 + self.dir_y + 1)
return get_colors()["square"][square_color_index % len(get_colors()["square"])]
@property
def x(self):
return self.pos[0]
@property
def y(self):
return self.pos[1]
def draw(self, screen: pygame.Surface, sqrect: pygame.Rect):
if self.died:
return
square_color_index = round((self.dir_x + 1) / 2 + self.dir_y + 1)
self.register_past_color(get_colors()["square"][square_color_index % len(get_colors()["square"])])
pygame.draw.rect(screen, (0, 0, 0), sqrect)
sq_surf = self.get_surface(
tuple(sqrect.inflate(-int(Config.SQUARE_SIZE / 5), -int(Config.SQUARE_SIZE / 5))[2:]))
screen.blit(sq_surf, sq_surf.get_rect(center=sqrect.center))
@x.setter
def x(self, val: int):
self.pos[0] = val
@y.setter
def y(self, val: int):
self.pos[1] = val
@property
def dir_x(self):
return self.dir[0]
@property
def dir_y(self):
return self.dir[1]
@property
def rect(self):
return pygame.Rect(self.x - Config.SQUARE_SIZE / 2, self.y - Config.SQUARE_SIZE / 2,
*([Config.SQUARE_SIZE] * 2))
def obey_bounce(self, bounce: Bounce):
# Planned bounces
self.pos = bounce.square_pos.copy()
self.dir = bounce.square_dir.copy()
self.latest_bounce_direction = bounce.bounce_dir
self.last_bounce_time = bounce.time
return
def reg_move(self, use_dt: bool = True):
self.x += self.dir_x * Config.square_speed * (Config.dt if use_dt else 1 / FRAMERATE)
self.y += self.dir_y * Config.square_speed * (Config.dt if use_dt else 1 / FRAMERATE)
class Camera:
def __init__(self, x: int = 0, y: int = 0):
self.x = x
self.y = y
self.locked_on_square = True
self.lock_type: CameraFollow = CameraFollow(Config.camera_mode)
def attempt_movement(self):
if not self.locked_on_square:
keys = pygame.key.get_pressed()
shift_modifier = (keys[pygame.K_LSHIFT] | keys[pygame.K_RSHIFT]) + 1
self.x += (keys[pygame.K_d] - keys[pygame.K_a]) * 500 * shift_modifier / FRAMERATE
self.y += (keys[pygame.K_s] - keys[pygame.K_w]) * 500 * shift_modifier / FRAMERATE
@property
def pos(self):
return self.x, self.y
@pos.setter
def pos(self, val: Union[tuple[int, int], list[int]]):
self.x, self.y = val
def offset(self, pos_or_rect: Union[pygame.Rect, tuple[int, int]]) -> Union[pygame.Rect, list[int]]:
if isinstance(pos_or_rect, pygame.Rect):
return pos_or_rect.move(-self.x, -self.y)
else:
return [pos_or_rect[0]-self.x, pos_or_rect[1]-self.y]
def apply_parallax(self, pos: tuple[float, float], depth: float) -> list[float]:
# Parallax factor: nearer stars move faster than distant ones
parallax_factor = 0.2 + depth * 0.8 # From 0.2 to 1.0
screen_x = (pos[0] - self.x) * parallax_factor
screen_y = (pos[1] - self.y) * parallax_factor
return [screen_x, screen_y]
def follow(self, square: Square):
# Square in center
if self.lock_type == CameraFollow.Center:
self.pos = [square.x - Config.SCREEN_WIDTH / 2, square.y - Config.SCREEN_HEIGHT / 2]
# Smooth camera
if self.lock_type == CameraFollow.Smoothed:
easing_rate = 3
self.x = (square.x - Config.SCREEN_WIDTH / 2) * easing_rate * Config.dt + self.x - easing_rate * self.x * Config.dt
self.y = (square.y - Config.SCREEN_HEIGHT / 2) * easing_rate * Config.dt + self.y - easing_rate * self.y * Config.dt
class CameraFollow(Enum):
Center = 0 # Center the square
Smoothed = 2 # Smoothed camera
class Star:
def __init__(self, pos: tuple[float, float], depth: float):
self.initial_pos = list(pos)
self.depth = depth # between 0 (far) and 1 (near)
size_min = 0.5
size_max = 2
self.size = size_min + (1 - self.depth) * (size_max - size_min)
self.phase = random.uniform(0, 2 * math.pi) # Ditambahkan untuk fase acak
self.brightness = 0 # Akan diperbarui dalam metode update
self.color = (255, 255, 255) # Warna default, akan diperbarui
def update(self, time):
# Adjust brightness over time to create blinking effect
blink_speed = 10 # can adjust to get desired speed
brightness_variation = (math.sin(blink_speed * time + self.phase) + 1) / 2 # Normalize between 0 and 1
min_brightness = 100
max_brightness = 255
self.brightness = min_brightness + brightness_variation * (max_brightness - min_brightness)
self.color = (int(self.brightness),) * 3 # Update color based on brightness
def get_screen_pos(self, camera: Camera):
parallax_factor = 0.2 + self.depth * 0.8 # from 0.2 to 1.0
screen_x = (self.initial_pos[0] - camera.x) * parallax_factor
screen_y = (self.initial_pos[1] - camera.y) * parallax_factor
return [screen_x, screen_y]
class World:
"""It's a cruel world out there"""
def __init__(self):
self.future_bounces: list[Bounce] = []
self.past_bounces: list[Bounce] = []
self.start_time = 0
self.time = 0
self.rectangles: list[pygame.Rect] = []
self.collision_times: list[float] = []
self.particles: list[Particle] = []
self.stars: list[Star] = []
self.timestamps = []
self.square = Square()
self.colors = []
self.safe_areas: list[pygame.Rect] = []
def update_time(self) -> None:
self.time = get_current_time() - self.start_time
def get_next_bounce(self) -> Bounce:
"""Also pops the bounce from the future_bounces list"""
self.past_bounces.append(self.future_bounces.pop(0))
return self.past_bounces[-1]
def add_bounce_particles(self, sp: list[float], sd: list[float], color: tuple):
for _ in range(Config.particle_amount):
new = Particle([sp[0] + random.randint(-10, 10), sp[1] + random.randint(-10, 10)], sd, color=color)
self.particles.append(new)
def handle_bouncing(self, square: Square):
if len(self.future_bounces):
if (self.time * 1000 + Config.music_offset)/1000 > self.future_bounces[0].time:
current_bounce = self.get_next_bounce()
before = square.dir.copy()
square.obey_bounce(current_bounce)
changed = square.dir.copy()
for _ in range(2):
if before[_] == changed[_]:
changed[_] = 0
else:
changed[_] = -changed[_]
if Config.do_particles_on_bounce:
square_color = square.get_current_color()
self.add_bounce_particles(square.pos, changed, color=square_color)
# Stop square at end
if len(self.future_bounces) == 0:
square.dir = [0, 0]
square.pos = current_bounce.square_pos
def generate_stars(self):
self.stars = []
# Menghitung total area aman
total_safe_area = sum(area.width * area.height for area in self.safe_areas)
star_density = 0.0001 # Sesuaikan sesuai kebutuhan
total_num_stars = int(total_safe_area * star_density)
min_distance_squared = (20) ** 2 # Menghindari clustering
# Mendapatkan bounding rectangle dari semua area aman
min_x = min(area.left for area in self.safe_areas)
max_x = max(area.right for area in self.safe_areas)
min_y = min(area.top for area in self.safe_areas)
max_y = max(area.bottom for area in self.safe_areas)
bounding_rect = pygame.Rect(min_x, min_y, max_x - min_x, max_y - min_y)
stars = []
attempts = 0
max_attempts = total_num_stars * 10
while len(stars) < total_num_stars and attempts < max_attempts:
x = random.uniform(bounding_rect.left, bounding_rect.right)
y = random.uniform(bounding_rect.top, bounding_rect.bottom)
point = (x, y)
# Memastikan titik berada dalam area aman
inside_safe_area = any(area.collidepoint(point) for area in self.safe_areas)
if inside_safe_area:
# Memastikan jarak minimum antar bintang
if all((star.initial_pos[0] - x) ** 2 + (star.initial_pos[1] - y) ** 2 >= min_distance_squared for star in stars):
depth = random.uniform(0, 1)
star = Star((x, y), depth)
stars.append(star)
attempts += 1
self.stars = stars
def gen_future_bounces(self, _start_notes: list[float], percent_update_callback):
"""Recursive function with optimized overlap checking"""
total_notes = len(_start_notes)
max_percent = 0
path = []
safe_areas = []
force_return = 0
def recurs(
square: Square,
notes: list[float],
bounces_so_far: list[Bounce] = None,
prev_index_priority=None,
t: float = 0,
depth: int = 0
) -> Union[list[Bounce], bool]:
nonlocal force_return, max_percent
if prev_index_priority is None:
prev_index_priority = [0, 1]
if bounces_so_far is None:
bounces_so_far = []
if total_notes:
gone_through_percent = (total_notes-len(notes)) * 100 // total_notes
while gone_through_percent > max_percent:
max_percent += 1
if percent_update_callback(f"{max_percent}% done generating map"):
raise UserCancelsLoadingError()
all_bounce_rects = [_bounc.get_collision_rect() for _bounc in bounces_so_far]
if len(notes) == 0:
return bounces_so_far
path_segment_start = len(path)
start_rect = square.rect.copy()
while True:
t += 1/FRAMERATE
square.reg_move(False)
path.append(square.rect)
if t > notes[0]:
# No collision (we good)
bounce_indexes = prev_index_priority
# Randomly change direction every X% of the time
if random.random() * 100 < Config.direction_change_chance:
bounce_indexes = list(bounce_indexes.__reversed__())
# Add safe area
safe_areas.append(start_rect.union(square.rect))
for direction_to_bounce in bounce_indexes:
square.dir[direction_to_bounce] *= -1
bounces_so_far.append(Bounce(square.pos.copy(), square.dir.copy(), t, direction_to_bounce))
toextend = recurs(
square=square.copy(),
notes=notes[1:],
bounces_so_far=[_b.copy() for _b in bounces_so_far],
t=t,
prev_index_priority=bounce_indexes.copy(),
depth=depth+1
)
if toextend:
return toextend
else:
bounces_so_far = bounces_so_far[:-1]
square.dir[direction_to_bounce] *= -1
# Backtrack if necessary
if force_return:
force_return -= 1
while len(path) != path_segment_start:
path.pop()
return False
continue
while len(path) != path_segment_start:
path.pop()
return False
othercheck = False
if len(bounces_so_far):
othercheck = bounces_so_far[-1].get_collision_rect().collidelist(path[:-10])+1
if square.rect.collidelist(all_bounce_rects) != -1 or othercheck:
if depth > 200:
if random.random() < Config.backtrack_chance:
max_percent -= (Config.backtrack_amount * 100 // total_notes) + 1
force_return = Config.backtrack_amount
while len(path) != path_segment_start:
path.pop()
return False
_start_notes = _start_notes[:Config.max_notes] if Config.max_notes is not None else _start_notes
self.future_bounces = recurs(
square=self.square.copy(),
notes=remove_too_close_values(
[_sn for _sn in _start_notes],
threshold=Config.bounce_min_spacing
)
)
if self.future_bounces is False:
raise MapLoadingFailureError("The map failed to generate because of the recursion function. " +
"If the MIDI has too many notes too close, it may not generate. " +
"Maybe try changing the \"bounce min spacing\", \"square speed\", or \"change dir chance\" in the config.")
if len(self.future_bounces) == 0:
raise MapLoadingFailureError("Map safe area list is empty. Please report this issue.")
percent_update_callback("Merging overlapping safe areas")
# Merging overlapping safe areas more efficiently
self.safe_areas = fix_overlap(safe_areas, percent_update_callback)
self.rectangles = [_fb.get_collision_rect() for _fb in self.future_bounces]
self.collision_times = [_fb.time for _fb in self.future_bounces]
def gather_colors(num_colors=20):
colors = []
while len(colors) < num_colors:
new_colors = color_hunt()
rgb_colors = [tuple(int(hex_color[i:i+2], 16) for i in (1, 3, 5)) for hex_color in new_colors]
colors.extend(rgb_colors)
return colors[:num_colors]
collected_colors = gather_colors()
# Setting random colors for the generated pegs
self.colors = [random.choice(collected_colors) for _ in self.future_bounces]
# Generate stars in safe areas
self.generate_stars()
return self.safe_areas
class Game:
def __init__(self):
self.active = False
self.notes = []
self.camera = Camera()
self.world = World()
self.safe_areas: list[pygame.Rect] = []
self.music_has_played = False
self.offset_happened = False
def start_song(self, screen: pygame.Surface):
random.seed(Config.seed)
# Load song and notes
with open(Config.current_song.fp, 'rb') as f:
notes = read_midi_file(file=f)
notes = sorted([note for note in notes])
self.notes = notes
# Other settings
self.world = World()
self.music_has_played = False
self.offset_happened = False
self.camera.lock_type = CameraFollow(Config.camera_mode)
screen.fill(get_colors()["background"])
pygame.display.flip()
def update_loading_screen(message: str):
screen.fill(get_colors()["background"], pygame.Rect(0, 0, Config.SCREEN_WIDTH, Config.SCREEN_HEIGHT))
font_size = int(Config.SCREEN_HEIGHT * 0.05)
loading_text = get_font(font_size).render(message, True, get_colors()["hallway"])
text_rect = loading_text.get_rect()
text_rect.center = (Config.SCREEN_WIDTH // 2, Config.SCREEN_HEIGHT // 2)
screen.blit(loading_text, text_rect)
pygame.display.flip()
try:
self.safe_areas = self.world.gen_future_bounces(self.notes, update_loading_screen)
# No need to fix overlap here, as it's handled in gen_future_bounces
except UserCancelsLoadingError:
return True
except MapLoadingFailureError as e:
return e.args[0] if len(e.args) else "Big error... can't load map :("
# Display song title and creator with fade-in and fade-out effects
self.display_song_info(screen)
self.world.start_time = get_current_time()
self.world.square.dir = [0, 0]
if self.world.future_bounces:
self.world.square.pos = self.world.future_bounces[0].square_pos
else:
self.world.square.pos = [Config.SCREEN_WIDTH / 2, Config.SCREEN_HEIGHT / 2]
return None
def display_song_info(self, screen):
# Function to display song title and creator with fade-in and fade-out effects
display_duration = 5000 # milliseconds
fade_duration = 1000 # milliseconds for fade-in and fade-out
start_time = pygame.time.get_ticks()
elapsed = 0
font_size = int(Config.SCREEN_HEIGHT * 0.10)
font_size2 = int(Config.SCREEN_HEIGHT * 0.05)
# Prepare text surfaces with per-pixel alpha
title_surface = get_font(font_size).render(f"{SONG_NAME}", True, color4)
creator_surface = get_font(font_size2).render(f"{SONG_CREATOR}", True, color4)
# Enable alpha blending
title_surface = title_surface.convert_alpha()
creator_surface = creator_surface.convert_alpha()
# Centering the text
title_rect = title_surface.get_rect(center=(Config.SCREEN_WIDTH // 2, Config.SCREEN_HEIGHT // 2 - font_size))
creator_rect = creator_surface.get_rect(center=(Config.SCREEN_WIDTH // 2, Config.SCREEN_HEIGHT // 2 + font_size2))
while elapsed < display_duration:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
self.handle_event(event)
elapsed = pygame.time.get_ticks() - start_time
# Calculate alpha value
if elapsed < fade_duration:
# Fade-in phase
alpha = int(255 * (elapsed / fade_duration))
elif elapsed > display_duration - fade_duration:
# Fade-out phase
alpha = int(255 * ((display_duration - elapsed) / fade_duration))
else:
# Fully visible
alpha = 255
# Clamp alpha value
alpha = max(0, min(255, alpha))
# Set alpha
title_surface.set_alpha(alpha)
creator_surface.set_alpha(alpha)
# Clear screen
screen.fill(get_colors()["background"])
# Draw text
screen.blit(title_surface, title_rect)
screen.blit(creator_surface, creator_rect)
pygame.display.flip()
pygame.time.delay(30) # Slight delay to control the frame rate
def draw(self, screen: pygame.Surface):
if not self.active:
return
if not self.music_has_played:
if not self.offset_happened:
for bnc_change in self.world.future_bounces:
bnc_change.time += Config.start_playing_delay / 1000
self.offset_happened = True
if self.world.time-Config.music_offset/1000 > Config.start_playing_delay/1000:
self.music_has_played = True
song_load_before = get_current_time()
pygame.mixer.music.play()
for bnc_change in self.world.future_bounces:
bnc_change.time += (get_current_time()-song_load_before)/1000
screen_rect = screen.get_rect()
# Set world time
self.world.update_time()
# Move camera
self.camera.attempt_movement()
# Handle square bounces
self.world.handle_bouncing(self.world.square)
# Move square
self.world.square.reg_move()
# Square in center of camera if locked
if self.camera.locked_on_square:
self.camera.follow(self.world.square)
# Bounce animation
sqrect = self.camera.offset(self.world.square.rect)
if (self.world.time - 0.25) + Config.music_offset / 1000 < self.world.square.last_bounce_time:
lerp = abs((self.world.time - 0.25 + Config.music_offset / 1000) - self.world.square.last_bounce_time) * 5
lerp = lerp ** 2 # Square it for better-looking interpolation
if self.world.square.latest_bounce_direction:
sqrect.inflate_ip((lerp * 5, -10 * lerp))
else:
sqrect.inflate_ip((-10 * lerp, lerp * 5))
# Draw background (walls)
screen.fill(get_colors()["background"])
# Create a hallway surface
hallway_surface = pygame.Surface((Config.SCREEN_WIDTH, Config.SCREEN_HEIGHT), pygame.SRCALPHA)
# Safe areas
for safe_area in self.world.safe_areas:
offsetted = self.camera.offset(safe_area)
if screen_rect.colliderect(offsetted):
pygame.draw.rect(hallway_surface, get_colors()["hallway"], offsetted)
# Obtain current time
current_time = pygame.time.get_ticks() / 1000.0 # Time in seconds
# Draw stars onto the hallway surface
for star in self.world.stars:
star.update(current_time) # Update blinking effect
screen_pos = star.get_screen_pos(self.camera)
if 0 <= screen_pos[0] < Config.SCREEN_WIDTH and 0 <= screen_pos[1] < Config.SCREEN_HEIGHT:
# Check if star's position is within the hallway (safe areas)
if hallway_surface.get_at((int(screen_pos[0]), int(screen_pos[1]))).a != 0:
pygame.draw.circle(hallway_surface, star.color, (int(screen_pos[0]), int(screen_pos[1])), int(star.size))
# Blit the hallway surface onto the main screen
screen.blit(hallway_surface, (0, 0))
# Draw pegs
for i, bounce_rect in enumerate(self.world.rectangles):
offsetted = self.camera.offset(bounce_rect)
if offsetted.collidescreen_rect):
if Config.do_color_bounce_pegs and self.world.collision_times[i] < (self.world.time * 1000 + Config.music_offset - Config.start_playing_delay)/1000:
pygame.draw.rect(screen, self.world.colors[i], offsetted)
else:
pygame.draw.rect(screen, get_colors()["background"], offsetted)
# Particles
for particle in self.world.particles:
pygame.draw.rect(screen, particle.color, self.camera.offset(particle.rect))
for remove_particle in [particle for particle in self.world.particles if particle.age()]:
self.world.particles.remove(remove_particle)
# Particle trail in game
if Config.particle_trail:
# Tambahkan partikel secara periodik
if not self.world.square.died:
square_color = self.world.square.get_current_color()
new = Particle(self.world.square.pos.copy(), [0, 0], True, color=(255, 255, 255))
new.delta = [random.randint(-2, 2)/20, random.randint(5, 15)/20]
self.world.particles.append(new)
# Draw square
self.world.square.draw(screen, sqrect)
def handle_event(self, event: pygame.event.Event):
if not self.active:
return False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
pygame.quit()
sys.exit()
if event.key == pygame.K_TAB:
self.camera.locked_on_square = not self.camera.locked_on_square
class InvalidSongError(Exception):
"""The song is invalid somehow"""
pass
def make_song_from_midi(fpath: str, audio_fp: Optional[str] = None) -> Any:
new_song = type('Song', (object,), {})()
new_song.fp = fpath # MIDI file path
new_song.audio_fp = audio_fp if audio_fp else fpath # Audio file path, default to fpath
new_song.name = SONG_NAME # Use the song name from the top
new_song.creator = SONG_CREATOR # Use the song creator from the top
new_song.music_offset = 0
return new_song
def main():
pygame.init()
clock = pygame.time.Clock()
screen = pygame.display.set_mode((Config.SCREEN_WIDTH, Config.SCREEN_HEIGHT))
pygame.display.set_caption("Midi Playground")
try:
pygame.display.set_icon(pygame.image.load(Config.assets).convert_alpha())
except Exception as e:
print(e)
# Specify the song file to use
selected_song_filepath = join(Config.song, SONG)
if not isfile(selected_song_filepath):
print(f"Song file {selected_song_filepath} not found.")
return
# Get the base file name without extension
base_name = os.path.splitext(SONG)[0]
# Check for corresponding MP3 file
mp3_song_name = base_name + '.mp3'
mp3_filepath = join(Config.song, mp3_song_name)
if isfile(mp3_filepath):
audio_fp = mp3_filepath
else:
audio_fp = selected_song_filepath
Config.current_song = make_song_from_midi(selected_song_filepath, audio_fp=audio_fp)
# Load the music file
try:
pygame.mixer.music.load(Config.current_song.audio_fp)
except Exception as e:
print(f"Error loading audio file: {e}")
print("The game will run without audio.")
# Uncomment the line below to exit if audio is essential
# return
pygame.mixer.music.set_volume(Config.volume / 100)
# Initialize the game
game = Game()
# Start the game
if msg := game.start_song(screen):
if isinstance(msg, str):
print(f"Error starting game: {msg}")
return
game.active = True
running = True
while running:
# screen.fill(get_colors()["background"]) # Moved to the draw function
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
game.handle_event(event)
game.draw(screen)
pygame.display.flip()
clock.tick(FRAMERATE)
Config.dt = clock.get_time() / 1000.0 if clock.get_time() > 0 else 0.016 # Avoid division by zero
pygame.quit()
if __name__ == '__main__':
main()
```
**Explanation:**
1. **Checking for MP3 File:**
In the `main()` function, after determining the `selected_song_filepath`, we extract the base name of the MIDI file using `os.path.splitext(SONG)[0]`. This gives us the filename without the extension.
```python
base_name = os.path.splitext(SONG)[0]
```
We then construct the MP3 filename by appending '.mp3' to the base name and check if it exists:
```python
mp3_song_name = base_name + '.mp3'
mp3_filepath = join(Config.song, mp3_song_name)
if isfile(mp3_filepath):
audio_fp = mp3_filepath
else:
audio_fp = selected_song_filepath
```
2. **Updating the `make_song_from_midi` Function:**
The `make_song_from_midi` function has been updated to accept an optional `audio_fp` parameter, which represents the path to the audio file to be used for playback.
```python
def make_song_from_midi(fpath: str, audio_fp: Optional[str] = None) -> Any:
new_song = type('Song', (object,), {})()
new_song.fp = fpath # MIDI file path
new_song.audio_fp = audio_fp if audio_fp else fpath # Audio file path, default to fpath
new_song.name = SONG_NAME # Use the song name from the top
new_song.creator = SONG_CREATOR # Use the song creator from the top
new_song.music_offset = 0
return new_song
```
3. **Loading the Correct Audio File:**
In the `main()` function, we instantiate `Config.current_song` with both the MIDI file path and the audio file path (which could be the MP3 or the MIDI file if no MP3 exists).
```python
Config.current_song = make_song_from_midi(selected_song_filepath, audio_fp=audio_fp)
```
When loading the music for playback, we use `Config.current_song.audio_fp`, which ensures that the MP3 file is used if it exists.
```python
try:
pygame.mixer.music.load(Config.current_song.audio_fp)
except Exception as e:
print(f"Error loading audio file: {e}")
print("The game will run without audio.")
```
4. **Using MIDI for Game Mechanics (Bounce):**
The game mechanics, such as bounce calculations, continue to use the MIDI file. The MIDI file is read to extract note information, which is used for game logic.
```python
# Load song and notes
with open(Config.current_song.fp, 'rb') as f:
notes = read_midi_file(file=f)
```
5. **Ensuring Correct Exception Handling:**
The exception messages have been updated to reflect that the audio file could be either an MP3 or MIDI file.
6. **Overall Structure:**
The rest of the code remains the same, ensuring that gameplay is unaffected. The only difference is the check for an MP3 file and using it for audio playback if available.
**Now, the game will:**
- Use the MIDI file for reading notes and game mechanics.
- Check for an MP3 file with the same name as the MIDI file.
- If it exists, it will use the MP3 file for audio playback.
- If it doesn't exist, it will use the MIDI file for audio playback.
- Handle any exceptions appropriately, allowing the game to run even if the audio file cannot be loaded.