Last active
July 22, 2025 20:58
-
-
Save ivanfioravanti/1d47ad939dd8300e8ba5bb20c93c6c38 to your computer and use it in GitHub Desktop.
Triangle-Square-Pentagon Rotation Game
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import pygame | |
import math | |
import random | |
# Initialize pygame | |
pygame.init() | |
# Screen dimensions | |
WIDTH, HEIGHT = 800, 800 | |
screen = pygame.display.set_mode((WIDTH, HEIGHT)) | |
pygame.display.set_caption("Triangle-Square-Pentagon Rotation Game") | |
# Colors | |
BACKGROUND = (10, 10, 30) | |
TRIANGLE_COLOR = (255, 100, 100) | |
SQUARE_COLOR = (100, 255, 100) | |
PENTAGON_COLOR = (100, 100, 255) | |
BALL_COLOR = (255, 255, 100) | |
TEXT_COLOR = (220, 220, 220) | |
WIN_COLOR = (100, 255, 200) | |
IMPACT_COLOR = (255, 200, 100) | |
BROKEN_COLOR = (200, 50, 50) | |
# Center of the screen | |
CENTER = (WIDTH // 2, HEIGHT // 2) | |
# Rotation speeds (radians per frame) - 3x faster | |
TRIANGLE_SPEED = 0.03 | |
SQUARE_SPEED = -0.024 | |
PENTAGON_SPEED = 0.015 | |
# Game state | |
game_won = False | |
class Ball: | |
def __init__(self, x, y): | |
self.x = x | |
self.y = y | |
self.radius = 10 | |
self.speed = 9 # 3x faster | |
self.angle = random.uniform(0, 2 * math.pi) | |
self.trail = [] | |
self.max_trail = 20 | |
self.vx = math.cos(self.angle) * self.speed | |
self.vy = math.sin(self.angle) * self.speed | |
def move(self): | |
# Move in current direction | |
self.x += self.vx | |
self.y += self.vy | |
# Add current position to trail | |
self.trail.append((self.x, self.y)) | |
if len(self.trail) > self.max_trail: | |
self.trail.pop(0) | |
def draw(self, surface): | |
# Draw trail | |
for i, pos in enumerate(self.trail): | |
alpha = int(255 * i / len(self.trail)) | |
radius = int(self.radius * i / len(self.trail)) | |
pygame.draw.circle(surface, (*BALL_COLOR[:3], alpha), | |
(int(pos[0]), int(pos[1])), radius) | |
# Draw ball | |
pygame.draw.circle(surface, BALL_COLOR, (int(self.x), int(self.y)), self.radius) | |
pygame.draw.circle(surface, (255, 255, 255), (int(self.x), int(self.y)), self.radius, 2) | |
class Polygon: | |
def __init__(self, center, radius, sides, color, rotation_speed): | |
self.center = center | |
self.radius = radius | |
self.sides = sides | |
self.color = color | |
self.rotation_speed = rotation_speed | |
self.angle = 0 | |
self.vertices = [] | |
# Dictionary to track impact counts for each segment | |
self.segment_impacts = {} # key: segment index, value: impact count | |
self.update_vertices() | |
def update_vertices(self): | |
self.vertices = [] | |
for i in range(self.sides): | |
angle = self.angle + i * (2 * math.pi / self.sides) | |
x = self.center[0] + self.radius * math.cos(angle) | |
y = self.center[1] + self.radius * math.sin(angle) | |
self.vertices.append((x, y)) | |
def rotate(self): | |
self.angle += self.rotation_speed | |
self.update_vertices() | |
def draw(self, surface): | |
if len(self.vertices) > 1: | |
# Draw all segments | |
for i in range(self.sides): | |
start = self.vertices[i] | |
end = self.vertices[(i + 1) % self.sides] | |
# Draw segment with different color based on impact count | |
if i in self.segment_impacts: | |
impact_count = self.segment_impacts[i] | |
if impact_count >= 4: | |
# Broken segment - draw in red | |
pygame.draw.line(surface, BROKEN_COLOR, start, end, 3) | |
else: | |
# Partially damaged segment - draw with impact indicator | |
pygame.draw.line(surface, self.color, start, end, 3) | |
# Draw impact markers | |
for j in range(impact_count): | |
t = (j + 1) / 5 | |
marker_x = start[0] + t * (end[0] - start[0]) | |
marker_y = start[1] + t * (end[1] - start[1]) | |
pygame.draw.circle(surface, IMPACT_COLOR, (int(marker_x), int(marker_y)), 5) | |
else: | |
# Undamaged segment | |
pygame.draw.line(surface, self.color, start, end, 3) | |
def check_collision(self, ball): | |
global game_won | |
collision_occurred = False | |
for i in range(self.sides): | |
start = self.vertices[i] | |
end = self.vertices[(i + 1) % self.sides] | |
# Calculate distance from ball to line segment | |
distance = self.point_to_line_distance(ball.x, ball.y, start, end) | |
# Check if ball is close enough to the line segment | |
if distance < ball.radius: | |
# Only process collision if the segment is not broken | |
if i not in self.segment_impacts or self.segment_impacts[i] < 4: | |
# Increment impact count for this segment | |
if i not in self.segment_impacts: | |
self.segment_impacts[i] = 0 | |
self.segment_impacts[i] += 1 | |
# Calculate reflection if segment is not broken yet | |
if self.segment_impacts[i] < 4: | |
# Calculate the normal vector of the line segment | |
line_dx = end[0] - start[0] | |
line_dy = end[1] - start[1] | |
line_length = math.sqrt(line_dx**2 + line_dy**2) | |
# Normal vector (perpendicular to the line segment) | |
normal_x = -line_dy / line_length | |
normal_y = line_dx / line_length | |
# Make sure the normal points toward the ball | |
dot_product = (ball.x - start[0]) * normal_x + (ball.y - start[1]) * normal_y | |
if dot_product < 0: | |
normal_x = -normal_x | |
normal_y = -normal_y | |
# Reflect the ball's velocity vector | |
dot = ball.vx * normal_x + ball.vy * normal_y | |
ball.vx = ball.vx - 2 * dot * normal_x | |
ball.vy = ball.vy - 2 * dot * normal_y | |
# Move ball outside the collision to prevent sticking | |
penetration = ball.radius - distance | |
ball.x += normal_x * penetration * 1.1 | |
ball.y += normal_y * penetration * 1.1 | |
# Check if this segment has 4 impacts and is the pentagon | |
if self.segment_impacts[i] >= 4 and self.sides == 5: | |
# Check if any segment is broken (has 4 impacts) | |
broken_count = sum(1 for count in self.segment_impacts.values() if count >= 4) | |
if broken_count >= 1: # At least 1/4 of pentagon (1 out of 5) | |
game_won = True | |
collision_occurred = True | |
break # Only handle one collision per frame | |
return collision_occurred | |
def point_to_line_distance(self, px, py, line_start, line_end): | |
x1, y1 = line_start | |
x2, y2 = line_end | |
# Vector from line_start to line_end | |
line_vec = (x2 - x1, y2 - y1) | |
# Vector from line_start to point | |
point_vec = (px - x1, py - y1) | |
# Length of line segment squared | |
line_len_sq = line_vec[0]**2 + line_vec[1]**2 | |
# Calculate projection of point_vec onto line_vec | |
if line_len_sq == 0: | |
return math.sqrt((px - x1)**2 + (py - y1)**2) | |
t = max(0, min(1, (point_vec[0]*line_vec[0] + point_vec[1]*line_vec[1]) / line_len_sq)) | |
# Projection point on the line segment | |
projection = (x1 + t * line_vec[0], y1 + t * line_vec[1]) | |
# Distance from point to projection | |
return math.sqrt((px - projection[0])**2 + (py - projection[1])**2) | |
# Create game objects | |
triangle = Polygon(CENTER, 150, 3, TRIANGLE_COLOR, TRIANGLE_SPEED) | |
square = Polygon(CENTER, 250, 4, SQUARE_COLOR, SQUARE_SPEED) | |
pentagon = Polygon(CENTER, 350, 5, PENTAGON_COLOR, PENTAGON_SPEED) | |
# Create ball inside the triangle | |
ball = Ball(CENTER[0], CENTER[1]) | |
# Font for text | |
font = pygame.font.SysFont(None, 36) | |
small_font = pygame.font.SysFont(None, 28) | |
# Main game loop | |
clock = pygame.time.Clock() | |
running = True | |
while running: | |
for event in pygame.event.get(): | |
if event.type == pygame.QUIT: | |
running = False | |
elif event.type == pygame.KEYDOWN: | |
if event.key == pygame.K_ESCAPE: | |
running = False | |
elif event.key == pygame.K_r and game_won: | |
# Reset game | |
game_won = False | |
triangle.segment_impacts.clear() | |
square.segment_impacts.clear() | |
pentagon.segment_impacts.clear() | |
ball.x, ball.y = CENTER | |
ball.angle = random.uniform(0, 2 * math.pi) | |
ball.vx = math.cos(ball.angle) * ball.speed | |
ball.vy = math.sin(ball.angle) * ball.speed | |
ball.trail = [] | |
if not game_won: | |
# Update rotations | |
triangle.rotate() | |
square.rotate() | |
pentagon.rotate() | |
# Move ball | |
ball.move() | |
# Check collisions with polygons (inner to outer) | |
triangle.check_collision(ball) | |
square.check_collision(ball) | |
pentagon.check_collision(ball) | |
# Drawing | |
screen.fill(BACKGROUND) | |
# Draw polygons | |
pentagon.draw(screen) | |
square.draw(screen) | |
triangle.draw(screen) | |
# Draw ball | |
ball.draw(screen) | |
# Draw instructions | |
if not game_won: | |
instructions = [ | |
"Ball is trapped inside rotating shapes!", | |
"It takes 4 impacts to break a side.", | |
"Game is won when it breaks 1/4 of pentagon sides.", | |
"Press ESC to quit" | |
] | |
for i, text in enumerate(instructions): | |
text_surface = small_font.render(text, True, TEXT_COLOR) | |
screen.blit(text_surface, (20, 20 + i * 30)) | |
else: | |
# Draw win message | |
win_text = font.render("YOU WIN! Ball has escaped!", True, WIN_COLOR) | |
screen.blit(win_text, (WIDTH//2 - win_text.get_width()//2, HEIGHT//2 - 50)) | |
restart_text = font.render("Press R to restart", True, TEXT_COLOR) | |
screen.blit(restart_text, (WIDTH//2 - restart_text.get_width()//2, HEIGHT//2 + 20)) | |
# Draw impact counters | |
tri_text = small_font.render(f"Triangle impacts: {sum(triangle.segment_impacts.values())}", True, TRIANGLE_COLOR) | |
screen.blit(tri_text, (20, HEIGHT - 100)) | |
sq_text = small_font.render(f"Square impacts: {sum(square.segment_impacts.values())}", True, SQUARE_COLOR) | |
screen.blit(sq_text, (20, HEIGHT - 70)) | |
pen_text = small_font.render(f"Pentagon impacts: {sum(pentagon.segment_impacts.values())}", True, PENTAGON_COLOR) | |
screen.blit(pen_text, (20, HEIGHT - 40)) | |
pygame.display.flip() | |
clock.tick(60) | |
pygame.quit() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment