Skip to content

Instantly share code, notes, and snippets.

@ivanfioravanti
Last active July 22, 2025 20:58
Show Gist options
  • Save ivanfioravanti/1d47ad939dd8300e8ba5bb20c93c6c38 to your computer and use it in GitHub Desktop.
Save ivanfioravanti/1d47ad939dd8300e8ba5bb20c93c6c38 to your computer and use it in GitHub Desktop.
Triangle-Square-Pentagon Rotation Game
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