#!/usr/bin/python3 # Description: circlewars - A simple game written in Python utilizing Pygame. # Usage: python3 circlewars-game.py # Author: Justin Oros # Source: https://github.com/JustinOros import pygame import random import time import os # Initialize pygame pygame.init() # Set up full-screen mode SCREEN_WIDTH = pygame.display.Info().current_w SCREEN_HEIGHT = pygame.display.Info().current_h screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.FULLSCREEN) pygame.display.set_caption("Circle Wars") # <-- Renamed here # Initially hide mouse cursor; will toggle visible when mouse active pygame.mouse.set_visible(False) # Define colors NEON_BLUE = (0, 200, 255) # Used for UI text BLUE = (0, 200, 255) # Player glow blue (same as NEON_BLUE) RED = (255, 0, 0) DARK_GRAY = (50, 50, 50) WHITE = (255, 255, 255) # Define constants PLAYER_SIZE = 20 ENEMY_SIZE = 20 PROJECTILE_SIZE = 5 # Function to load sound safely def load_sound(filename): if os.path.exists(filename): return pygame.mixer.Sound(filename) else: print(f"Warning: {filename} not found. Sound will be disabled.") return None # Load sounds GO_SOUND = load_sound("audio_go.wav") FIRE_SOUND = load_sound("audio_playerfire.wav") DIE_SOUND = load_sound("audio_playerdeath.wav") LEVEL_UP_SOUND = load_sound("audio_levelup.wav") ENEMY_DEATH_SOUND = load_sound("audio_enemydeath.wav") EXPLOSION_SOUND = load_sound("audio_playerdeath.wav") # Load background music (pygame.mixer.music) if os.path.exists("audio_music.mp3"): try: pygame.mixer.music.load("audio_music.mp3") except Exception as e: print(f"Error loading background music: {e}") else: print("Warning: audio_music.mp3 not found. Background music disabled.") # Set font for score font = pygame.font.SysFont(None, 30) # Initialize the joystick (controller) pygame.joystick.init() controller = None if pygame.joystick.get_count() > 0: controller = pygame.joystick.Joystick(0) controller.init() # Draw glowing circle ONLY (no solid fill) def draw_glow_circle(surf, center, radius, glow_color, glow_radius): glow_surface = pygame.Surface((radius*2 + glow_radius*2, radius*2 + glow_radius*2), pygame.SRCALPHA) max_glow = int(glow_radius * 0.6) for i in range(max_glow, 0, -1): alpha = int(255 * (i / max_glow) ** 2) pygame.draw.circle( glow_surface, glow_color + (alpha,), (radius + glow_radius, radius + glow_radius), radius + i, width=2 ) surf.blit(glow_surface, (center[0] - radius - glow_radius, center[1] - radius - glow_radius)) # Helper function for circle collision def circles_collide(x1, y1, r1, x2, y2, r2): dist_sq = (x1 - x2)**2 + (y1 - y2)**2 radius_sum_sq = (r1 + r2)**2 return dist_sq <= radius_sum_sq # Draw small white crosshair at given position def draw_crosshair(surf, pos): size = 10 color = WHITE x, y = pos pygame.draw.line(surf, color, (x - size // 2, y), (x + size // 2, y), 2) pygame.draw.line(surf, color, (x, y - size // 2), (x, y + size // 2), 2) # Player class class Player: def __init__(self): self.x = SCREEN_WIDTH // 2 self.y = SCREEN_HEIGHT // 2 self.size = PLAYER_SIZE self.speed = 3 # Will be set in game() self.direction = None # Current movement direction vector (dx, dy) self.last_direction = (0, -1) # Default facing up def move(self, keys, joystick_axes): move_x, move_y = 0, 0 # Joystick input with deadzone axis_x = joystick_axes[0] axis_y = joystick_axes[1] deadzone = 0.3 if abs(axis_x) > deadzone: move_x = self.speed * (1 if axis_x > 0 else -1) if abs(axis_y) > deadzone: move_y = self.speed * (1 if axis_y > 0 else -1) # Keyboard input only if joystick inactive this frame if move_x == 0 and move_y == 0: if keys[pygame.K_a]: move_x = -self.speed elif keys[pygame.K_d]: move_x = self.speed if keys[pygame.K_w]: move_y = -self.speed elif keys[pygame.K_s]: move_y = self.speed self.x += move_x self.y += move_y # Screen wrapping if self.x < 0: self.x = SCREEN_WIDTH - self.size elif self.x > SCREEN_WIDTH - self.size: self.x = 0 if self.y < 0: self.y = SCREEN_HEIGHT - self.size elif self.y > SCREEN_HEIGHT - self.size: self.y = 0 # Set current direction vector normalized to -1, 0, or 1 on each axis norm_dx = 0 norm_dy = 0 if move_x > 0: norm_dx = 1 elif move_x < 0: norm_dx = -1 if move_y > 0: norm_dy = 1 elif move_y < 0: norm_dy = -1 if norm_dx == 0 and norm_dy == 0: self.direction = None else: self.direction = (norm_dx, norm_dy) self.last_direction = self.direction # Update last known direction def draw(self): center = (int(self.x + self.size // 2), int(self.y + self.size // 2)) radius = self.size // 2 draw_glow_circle(screen, center, radius, BLUE, glow_radius=10) # Projectile class class Projectile: def __init__(self, x, y, direction_vector): self.x = x self.y = y self.size = PROJECTILE_SIZE self.color = BLUE self.speed = 15 self.dir_x, self.dir_y = direction_vector def move(self): # Normalize direction so diagonal speed isn't faster length = (self.dir_x ** 2 + self.dir_y ** 2) ** 0.5 if length == 0: return self.x += self.speed * self.dir_x / length self.y += self.speed * self.dir_y / length def draw(self): center = (int(self.x + self.size // 2), int(self.y + self.size // 2)) radius = self.size // 2 pygame.draw.circle(screen, self.color, center, radius) # Explosion class class Explosion: def __init__(self, x, y, color=RED): self.x = x self.y = y self.size = 50 self.lifetime = 10 self.color = color def draw(self): pygame.draw.circle(screen, self.color, (self.x, self.y), self.size) self.lifetime -= 1 def is_alive(self): return self.lifetime > 0 # Enemy class class Enemy: def __init__(self, speed): edge = random.choice(["top", "bottom", "left", "right"]) if edge == "top": self.x = random.randint(0, SCREEN_WIDTH - ENEMY_SIZE) self.y = 0 elif edge == "bottom": self.x = random.randint(0, SCREEN_WIDTH - ENEMY_SIZE) self.y = SCREEN_HEIGHT - ENEMY_SIZE elif edge == "left": self.x = 0 self.y = random.randint(0, SCREEN_HEIGHT - ENEMY_SIZE) else: self.x = SCREEN_WIDTH - ENEMY_SIZE self.y = random.randint(0, SCREEN_HEIGHT - ENEMY_SIZE) self.size = ENEMY_SIZE self.speed = speed def move(self, player_x, player_y): dx = player_x - self.x dy = player_y - self.y dist = max((dx ** 2 + dy ** 2) ** 0.5, 0.001) # Avoid zero division self.x += self.speed * dx / dist self.y += self.speed * dy / dist def draw(self): center = (int(self.x + self.size // 2), int(self.y + self.size // 2)) radius = self.size // 2 draw_glow_circle(screen, center, radius, RED, glow_radius=10) # Pause screen def draw_pause_screen(paused_by_controller): overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT)) overlay.set_alpha(180) overlay.fill((0, 0, 0)) screen.blit(overlay, (0, 0)) pause_font = pygame.font.SysFont(None, 100) instruction_font = pygame.font.SysFont(None, 40) pause_text = pause_font.render("PAUSED", True, NEON_BLUE) if paused_by_controller: instruction = "Press START to resume or X to Exit." else: instruction = "Press ESC to resume or Q to Quit." instruction_text = instruction_font.render(instruction, True, NEON_BLUE) screen.blit(pause_text, ( SCREEN_WIDTH // 2 - pause_text.get_width() // 2, SCREEN_HEIGHT // 2 - pause_text.get_height() // 2 - 30 )) screen.blit(instruction_text, ( SCREEN_WIDTH // 2 - instruction_text.get_width() // 2, SCREEN_HEIGHT // 2 + 30 )) # Helper function to fire projectile toward target_pos or fallback to last direction def fire_projectile(player, projectiles, target_pos=None): px = player.x + player.size // 2 py = player.y + player.size // 2 if target_pos is None: # fallback: use player.last_direction direction = player.direction if player.direction is not None else player.last_direction dir_x, dir_y = direction else: # Calculate normalized direction vector from player center to target_pos dir_x = target_pos[0] - px dir_y = target_pos[1] - py length = (dir_x ** 2 + dir_y ** 2) ** 0.5 if length == 0: return # no shooting if zero length vector dir_x /= length dir_y /= length projectile_start_x = px - PROJECTILE_SIZE // 2 projectile_start_y = py - PROJECTILE_SIZE // 2 projectiles.append(Projectile(projectile_start_x, projectile_start_y, (dir_x, dir_y))) if FIRE_SOUND: FIRE_SOUND.play() # Intro screen def intro_screen(): # Start background music looping indefinitely if not already playing if not pygame.mixer.music.get_busy(): try: pygame.mixer.music.play(-1) # loop forever except Exception as e: print(f"Failed to play background music: {e}") title_font = pygame.font.SysFont(None, 150, bold=True) subtitle_font = pygame.font.SysFont(None, 50) waiting = True while waiting: screen.fill(DARK_GRAY) # Render the title text "CIRCLE WARS" title_text = title_font.render("CIRCLE WARS", True, NEON_BLUE) title_rect = title_text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2 - 50)) screen.blit(title_text, title_rect) # Render the subtitle "Press any key to begin..." subtitle_text = subtitle_font.render("Press any key to begin...", True, NEON_BLUE) subtitle_rect = subtitle_text.get_rect(center=(SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2 + 40)) screen.blit(subtitle_text, subtitle_rect) pygame.display.flip() for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() exit() elif event.type == pygame.KEYDOWN or event.type == pygame.MOUSEBUTTONDOWN or event.type == pygame.JOYBUTTONDOWN: waiting = False pygame.time.Clock().tick(60) # Main game loop def game(): clock = pygame.time.Clock() player = Player() projectiles = [] explosions = [] level = 1 score = 0 enemy_speed = 2 enemies = [Enemy(enemy_speed)] player.speed = enemy_speed + 1 # Player slightly faster than enemies running = True paused = False paused_by_controller = False player_dead = False controller_active = False # Start assuming keyboard usage # Mouse activity tracking for cursor visibility and crosshair MOUSE_INACTIVE_TIMEOUT = 2000 # milliseconds last_mouse_move_time = 0 mouse_active = False def rumble(duration=300, strength=1.0): if controller_active and controller and hasattr(controller, "rumble"): try: controller.rumble(strength, strength, duration) except Exception as e: print(f"Rumble error: {e}") if GO_SOUND: GO_SOUND.play() while running: screen.fill(DARK_GRAY) keys = pygame.key.get_pressed() joystick_axes = [controller.get_axis(0), controller.get_axis(1)] if controller else [0, 0] for event in pygame.event.get(): if event.type == pygame.QUIT: running = False # Track input method for rumble control if event.type in [pygame.JOYAXISMOTION, pygame.JOYBUTTONDOWN]: controller_active = True elif event.type in [pygame.KEYDOWN, pygame.MOUSEBUTTONDOWN]: controller_active = False # Track mouse movement for cursor visibility if event.type == pygame.MOUSEMOTION: last_mouse_move_time = pygame.time.get_ticks() mouse_active = True if event.type == pygame.KEYDOWN: if event.key == pygame.K_ESCAPE: paused = not paused paused_by_controller = False elif paused and event.key == pygame.K_q: running = False elif not paused and event.key == pygame.K_SPACE: fire_projectile(player, projectiles) if event.type == pygame.JOYBUTTONDOWN: # Button mapping notes: # 7 or 6 = START buttons (pause/resume) # 2 = X button (quit when paused) # 0 = A button (fire projectile) if event.button == 7 or event.button == 6: paused = not paused paused_by_controller = True elif paused and event.button == 2: running = False elif not paused and event.button == 0: fire_projectile(player, projectiles) if event.type == pygame.MOUSEBUTTONDOWN: if not paused and event.button == 1: # Left mouse button if mouse_active: fire_projectile(player, projectiles, pygame.mouse.get_pos()) else: fire_projectile(player, projectiles) # Also check if keyboard movement keys are pressed, set controller_active to False if keys[pygame.K_w] or keys[pygame.K_a] or keys[pygame.K_s] or keys[pygame.K_d]: controller_active = False # Update mouse cursor visibility based on inactivity timeout current_time = pygame.time.get_ticks() if current_time - last_mouse_move_time > MOUSE_INACTIVE_TIMEOUT: mouse_active = False # HIDE the OS cursor ALWAYS to avoid overlap with crosshair pygame.mouse.set_visible(False) if paused: draw_pause_screen(paused_by_controller) pygame.display.flip() clock.tick(60) continue if player_dead: for explosion in explosions[:]: explosion.draw() if not explosion.is_alive(): explosions.remove(explosion) pygame.display.flip() clock.tick(60) if not explosions: show_final_score(score) running = False continue player.move(keys, joystick_axes) for projectile in projectiles[:]: projectile.move() projectile.draw() # Remove projectile if out of screen if (projectile.x < 0 or projectile.x > SCREEN_WIDTH or projectile.y < 0 or projectile.y > SCREEN_HEIGHT): projectiles.remove(projectile) for enemy in enemies[:]: enemy.move(player.x, player.y) enemy.draw() # Circle collision for player and enemy player_center = (player.x + player.size // 2, player.y + player.size // 2) enemy_center = (enemy.x + enemy.size // 2, enemy.y + enemy.size // 2) if circles_collide(player_center[0], player_center[1], player.size // 2, enemy_center[0], enemy_center[1], enemy.size // 2): if not player_dead: if DIE_SOUND: DIE_SOUND.play() rumble(duration=1000, strength=1.0) player_dead = True explosions.append(Explosion(player_center[0], player_center[1], color=BLUE)) for projectile in projectiles[:]: projectile_center = (projectile.x + projectile.size // 2, projectile.y + projectile.size // 2) if circles_collide(enemy_center[0], enemy_center[1], enemy.size // 2, projectile_center[0], projectile_center[1], projectile.size // 2): explosions.append(Explosion(enemy_center[0], enemy_center[1])) if ENEMY_DEATH_SOUND: ENEMY_DEATH_SOUND.play() rumble(duration=150, strength=0.5) enemies.remove(enemy) projectiles.remove(projectile) score += 100 break for explosion in explosions[:]: explosion.draw() if not explosion.is_alive(): explosions.remove(explosion) if not enemies: level += 1 if LEVEL_UP_SOUND: LEVEL_UP_SOUND.play() # Increase enemy speed every 2 levels, cap at 8 if level % 2 == 0 and enemy_speed < 8: enemy_speed += 1 player.speed = enemy_speed + 1 # Player stays slightly faster enemies = [Enemy(enemy_speed) for _ in range(level)] player.draw() # Draw crosshair if mouse active if mouse_active: draw_crosshair(screen, pygame.mouse.get_pos()) score_text = font.render(f"Score: {score}", True, NEON_BLUE) level_text = font.render(f"Level: {level}", True, NEON_BLUE) screen.blit(score_text, (10, 10)) screen.blit(level_text, (SCREEN_WIDTH - level_text.get_width() - 10, 10)) pygame.display.flip() clock.tick(60) # Show final score on death def show_final_score(score): font_large = pygame.font.SysFont(None, 80) if score == 0: text = "GAME OVER" else: text = f"Final Score: {score}" # Show it centered screen.fill(DARK_GRAY) text_surface = font_large.render(text, True, RED) screen.blit(text_surface, ((SCREEN_WIDTH - text_surface.get_width()) // 2, (SCREEN_HEIGHT - text_surface.get_height()) // 2)) pygame.display.flip() pygame.time.wait(4000) if __name__ == "__main__": intro_screen() game() pygame.quit()