import pygame import random from pygame import mixer import sys # ----------------- Fighter Class Definition ----------------- class Fighter(): def __init__(self, player, x, y, flip, data, sprite_sheet, animation_steps, sound): self.player = player self.size = data[0] self.image_scale = data[1] self.offset = data[2] self.flip = flip self.animation_list = self.load_images(sprite_sheet, animation_steps) self.action = 0 # 0: idle, 1: run, 2: jump, 3: attack1, 4: attack2, 5: hit, 6: death self.frame_index = 0 self.image = self.animation_list[self.action][self.frame_index] self.update_time = pygame.time.get_ticks() self.rect = pygame.Rect((x, y, 80, 180)) self.vel_y = 0 self.running = False self.jump = False self.attacking = False self.attack_type = 0 self.attack_cooldown = 0 self.attack_sound = sound self.hit = False self.health = 100 self.alive = True def load_images(self, sprite_sheet, animation_steps): animation_list = [] for y, animation in enumerate(animation_steps): temp_img_list = [] for x in range(animation): temp_img = sprite_sheet.subsurface(x * self.size, y * self.size, self.size, self.size) temp_img_list.append(pygame.transform.scale( temp_img, (self.size * self.image_scale, self.size * self.image_scale))) animation_list.append(temp_img_list) return animation_list def move(self, screen_width, screen_height, surface, target, round_over): SPEED = 10 GRAVITY = 2 dx = 0 dy = 0 self.running = False self.attack_type = 0 # Only allow movement if not attacking and round is active key = pygame.key.get_pressed() if not self.attacking and self.alive and not round_over: # Player 1 controls if self.player == 1: if key[pygame.K_a]: dx = -SPEED self.running = True if key[pygame.K_d]: dx = SPEED self.running = True if key[pygame.K_w] and not self.jump: self.vel_y = -30 self.jump = True if key[pygame.K_r] or key[pygame.K_t]: self.attack(target) if key[pygame.K_r]: self.attack_type = 1 if key[pygame.K_t]: self.attack_type = 2 # Player 2 controls (used in 1v1 mode) if self.player == 2: if key[pygame.K_LEFT]: dx = -SPEED self.running = True if key[pygame.K_RIGHT]: dx = SPEED self.running = True if key[pygame.K_UP] and not self.jump: self.vel_y = -30 self.jump = True if key[pygame.K_KP1] or key[pygame.K_KP2]: self.attack(target) if key[pygame.K_KP1]: self.attack_type = 1 if key[pygame.K_KP2]: self.attack_type = 2 self.vel_y += GRAVITY dy += self.vel_y # Ensure fighter stays on screen horizontally if self.rect.left + dx < 0: dx = -self.rect.left if self.rect.right + dx > screen_width: dx = screen_width - self.rect.right # Ground collision if self.rect.bottom + dy > screen_height - 110: self.vel_y = 0 self.jump = False dy = screen_height - 110 - self.rect.bottom # Ensure fighters face each other if target.rect.centerx > self.rect.centerx: self.flip = False else: self.flip = True if self.attack_cooldown > 0: self.attack_cooldown -= 1 self.rect.x += dx self.rect.y += dy def update(self): # Determine current action if self.health <= 0: self.health = 0 self.alive = False self.update_action(6) # Death elif self.hit: self.update_action(5) # Hit elif self.attacking: if self.attack_type == 1: self.update_action(3) # Attack1 elif self.attack_type == 2: self.update_action(4) # Attack2 elif self.jump: self.update_action(2) # Jump elif self.running: self.update_action(1) # Run else: self.update_action(0) # Idle animation_cooldown = 50 self.image = self.animation_list[self.action][self.frame_index] if pygame.time.get_ticks() - self.update_time > animation_cooldown: self.frame_index += 1 self.update_time = pygame.time.get_ticks() if self.frame_index >= len(self.animation_list[self.action]): if not self.alive: self.frame_index = len(self.animation_list[self.action]) - 1 else: self.frame_index = 0 if self.action in [3, 4]: self.attacking = False self.attack_cooldown = 20 if self.action == 5: self.hit = False self.attacking = False self.attack_cooldown = 20 def attack(self, target): if self.attack_cooldown == 0: self.attacking = True self.attack_sound.play() attacking_rect = pygame.Rect( self.rect.centerx - (2 * self.rect.width * self.flip), self.rect.y, 2 * self.rect.width, self.rect.height ) if attacking_rect.colliderect(target.rect): target.health -= 10 target.hit = True def update_action(self, new_action): if new_action != self.action: self.action = new_action self.frame_index = 0 self.update_time = pygame.time.get_ticks() def draw(self, surface): img = pygame.transform.flip(self.image, self.flip, False) surface.blit(img, (self.rect.x - (self.offset[0] * self.image_scale), self.rect.y - (self.offset[1] * self.image_scale))) # ----------------- End Fighter Class Definition ----------------- # ----------------- Initialization ----------------- mixer.init() pygame.init() SCREEN_WIDTH = 1000 SCREEN_HEIGHT = 600 screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption("Brawler") clock = pygame.time.Clock() FPS = 60 # Define colours RED = (255, 0, 0) YELLOW = (255, 255, 0) WHITE = (255, 255, 255) BLACK = (0, 0, 0) # Game variables intro_count = 3 last_count_update = pygame.time.get_ticks() score = [0, 0] # [P1, P2] round_over = False ROUND_OVER_COOLDOWN = 2000 # Fighter variables WARRIOR_SIZE = 162 WARRIOR_SCALE = 4 WARRIOR_OFFSET = [72, 56] WARRIOR_DATA = [WARRIOR_SIZE, WARRIOR_SCALE, WARRIOR_OFFSET] WIZARD_SIZE = 250 WIZARD_SCALE = 3 WIZARD_OFFSET = [112, 107] WIZARD_DATA = [WIZARD_SIZE, WIZARD_SCALE, WIZARD_OFFSET] # Load music and sounds pygame.mixer.music.load("assets/audio/music.mp3") pygame.mixer.music.set_volume(0.5) pygame.mixer.music.play(-1, 0.0, 5000) sword_fx = pygame.mixer.Sound("assets/audio/sword.wav") sword_fx.set_volume(0.5) magic_fx = pygame.mixer.Sound("assets/audio/magic.wav") magic_fx.set_volume(0.75) # Load images bg_image = pygame.image.load("assets/images/background/background.jpg").convert_alpha() warrior_sheet = pygame.image.load("assets/images/warrior/Sprites/warrior.png").convert_alpha() wizard_sheet = pygame.image.load("assets/images/wizard/Sprites/wizard.png").convert_alpha() victory_img = pygame.image.load("assets/images/icons/victory.png").convert_alpha() # Animation steps WARRIOR_ANIMATION_STEPS = [10, 8, 1, 7, 7, 3, 7] WIZARD_ANIMATION_STEPS = [8, 8, 1, 8, 8, 3, 7] # Fonts count_font = pygame.font.Font("assets/fonts/turok.ttf", 80) score_font = pygame.font.Font("assets/fonts/turok.ttf", 30) menu_font = pygame.font.Font("assets/fonts/turok.ttf", 50) title_font = pygame.font.Font("assets/fonts/turok.ttf", 80) def draw_text(text, font, text_col, x, y): img = font.render(text, True, text_col) screen.blit(img, (x, y)) return img def draw_bg(): scaled_bg = pygame.transform.scale(bg_image, (SCREEN_WIDTH, SCREEN_HEIGHT)) screen.blit(scaled_bg, (0, 0)) def draw_health_bar(health, x, y): ratio = health / 100 pygame.draw.rect(screen, WHITE, (x - 2, y - 2, 404, 34)) pygame.draw.rect(screen, RED, (x, y, 400, 30)) pygame.draw.rect(screen, YELLOW, (x, y, 400 * ratio, 30)) # ----------------- End Initialization ----------------- # ----------------- CPU Movement Function ----------------- def cpu_move(fighter, target, screen_width, screen_height): SPEED = 7 # Base movement speed CLOSE_SPEED = SPEED * 0.5 # Slower speed when too close GRAVITY = 2 dx = 0 dy = 0 # Decrement attack cooldown for repeated attacks if fighter.attack_cooldown > 0: fighter.attack_cooldown -= 1 # Calculate horizontal distance and update facing direction distance = target.rect.centerx - fighter.rect.centerx distance_abs = abs(distance) fighter.flip = False if distance > 0 else True # Define optimal distance boundaries optimal_min = 100 # Minimum optimal distance optimal_max = 200 # Maximum optimal distance # Decision making based on distance: if distance_abs > optimal_max: # Too far: approach the target dx = SPEED if distance > 0 else -SPEED fighter.running = True elif distance_abs < optimal_min: # Too close: either attack or retreat slowly if fighter.attack_cooldown == 0 and not fighter.attacking and random.random() < 0.6: fighter.attack_type = random.choice([1, 2]) fighter.attack(target) else: # Use a slower speed for smoother retreat dx = -CLOSE_SPEED if distance > 0 else CLOSE_SPEED fighter.running = True # Optionally add a jump for evasiveness if not fighter.jump and random.random() < 0.3: fighter.vel_y = -30 fighter.jump = True else: # In the optimal range: attack or reposition with lateral movement if fighter.attack_cooldown == 0 and not fighter.attacking and random.random() < 0.5: fighter.attack_type = random.choice([1, 2]) fighter.attack(target) else: dx = random.choice([-SPEED, 0, SPEED]) fighter.running = True # Additional reaction: if the target is attacking nearby, back off slightly if target.attacking and distance_abs < optimal_max + 50: dx = -CLOSE_SPEED * 1.2 if distance > 0 else CLOSE_SPEED * 1.2 fighter.running = True # Occasionally add a spontaneous jump for unpredictability if random.random() < 0.02 and not fighter.jump: fighter.vel_y = -30 fighter.jump = True # Apply gravity for smooth vertical motion fighter.vel_y += GRAVITY dy += fighter.vel_y # Keep the fighter within horizontal screen bounds if fighter.rect.left + dx < 0: dx = -fighter.rect.left if fighter.rect.right + dx > screen_width: dx = screen_width - fighter.rect.right # Ground collision: reset jump when on the ground if fighter.rect.bottom + dy > screen_height - 110: fighter.vel_y = 0 fighter.jump = False dy = screen_height - 110 - fighter.rect.bottom # Update fighter's position fighter.rect.x += dx fighter.rect.y += dy # ----------------- End CPU Movement Function ----------------- # ----------------- Game Loop for 1v1 Mode ----------------- def game_loop(): global intro_count, last_count_update, score, round_over fighter_1 = Fighter(1, 200, 310, False, WARRIOR_DATA, warrior_sheet, WARRIOR_ANIMATION_STEPS, sword_fx) fighter_2 = Fighter(2, 700, 310, True, WIZARD_DATA, wizard_sheet, WIZARD_ANIMATION_STEPS, magic_fx) intro_count = 3 last_count_update = pygame.time.get_ticks() score = [0, 0] round_over = False run = True while run: clock.tick(FPS) draw_bg() draw_health_bar(fighter_1.health, 20, 20) draw_health_bar(fighter_2.health, 580, 20) draw_text("P1: " + str(score[0]), score_font, RED, 20, 60) draw_text("P2: " + str(score[1]), score_font, RED, 580, 60) if intro_count <= 0: fighter_1.move(SCREEN_WIDTH, SCREEN_HEIGHT, screen, fighter_2, round_over) fighter_2.move(SCREEN_WIDTH, SCREEN_HEIGHT, screen, fighter_1, round_over) else: draw_text(str(intro_count), count_font, RED, SCREEN_WIDTH / 2, SCREEN_HEIGHT / 3) if (pygame.time.get_ticks() - last_count_update) >= 1000: intro_count -= 1 last_count_update = pygame.time.get_ticks() fighter_1.update() fighter_2.update() fighter_1.draw(screen) fighter_2.draw(screen) if not round_over: if not fighter_1.alive: score[1] += 1 round_over = True round_over_time = pygame.time.get_ticks() elif not fighter_2.alive: score[0] += 1 round_over = True round_over_time = pygame.time.get_ticks() else: screen.blit(victory_img, (360, 150)) if pygame.time.get_ticks() - round_over_time > ROUND_OVER_COOLDOWN: round_over = False intro_count = 3 fighter_1 = Fighter(1, 200, 310, False, WARRIOR_DATA, warrior_sheet, WARRIOR_ANIMATION_STEPS, sword_fx) fighter_2 = Fighter(2, 700, 310, True, WIZARD_DATA, wizard_sheet, WIZARD_ANIMATION_STEPS, magic_fx) for event in pygame.event.get(): if event.type == pygame.QUIT: run = False pygame.quit() sys.exit() pygame.display.update() # ----------------- End Game Loop for 1v1 Mode ----------------- # ----------------- Game Loop for 1vCPU Mode ----------------- def game_loop_cpu(): global intro_count, last_count_update, score, round_over fighter_1 = Fighter(1, 200, 310, False, WARRIOR_DATA, warrior_sheet, WARRIOR_ANIMATION_STEPS, sword_fx) # The CPU fighter is created with player value 2 even though controls are handled by CPU logic fighter_2 = Fighter(2, 700, 310, True, WIZARD_DATA, wizard_sheet, WIZARD_ANIMATION_STEPS, magic_fx) intro_count = 3 last_count_update = pygame.time.get_ticks() score = [0, 0] round_over = False run = True while run: clock.tick(FPS) draw_bg() draw_health_bar(fighter_1.health, 20, 20) draw_health_bar(fighter_2.health, 580, 20) draw_text("P1: " + str(score[0]), score_font, RED, 20, 60) draw_text("CPU: " + str(score[1]), score_font, RED, 580, 60) if intro_count <= 0: # Human player uses normal controls fighter_1.move(SCREEN_WIDTH, SCREEN_HEIGHT, screen, fighter_2, round_over) # CPU fighter uses the custom AI movement cpu_move(fighter_2, fighter_1, SCREEN_WIDTH, SCREEN_HEIGHT) else: draw_text(str(intro_count), count_font, RED, SCREEN_WIDTH / 2, SCREEN_HEIGHT / 3) if (pygame.time.get_ticks() - last_count_update) >= 1000: intro_count -= 1 last_count_update = pygame.time.get_ticks() fighter_1.update() fighter_2.update() fighter_1.draw(screen) fighter_2.draw(screen) if not round_over: if not fighter_1.alive: score[1] += 1 round_over = True round_over_time = pygame.time.get_ticks() elif not fighter_2.alive: score[0] += 1 round_over = True round_over_time = pygame.time.get_ticks() else: screen.blit(victory_img, (360, 150)) if pygame.time.get_ticks() - round_over_time > ROUND_OVER_COOLDOWN: round_over = False intro_count = 3 fighter_1 = Fighter(1, 200, 310, False, WARRIOR_DATA, warrior_sheet, WARRIOR_ANIMATION_STEPS, sword_fx) fighter_2 = Fighter(2, 700, 310, True, WIZARD_DATA, wizard_sheet, WIZARD_ANIMATION_STEPS, magic_fx) for event in pygame.event.get(): if event.type == pygame.QUIT: run = False pygame.quit() sys.exit() pygame.display.update() # ----------------- End Game Loop for 1vCPU Mode ----------------- # ----------------- Options Menu Function ----------------- def options_menu(): running = True while running: screen.fill(BLACK) draw_text("Options Menu", title_font, WHITE, SCREEN_WIDTH / 2 - 200, 100) draw_text("Press any key to return", menu_font, WHITE, SCREEN_WIDTH / 2 - 250, 300) for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() sys.exit() if event.type == pygame.KEYDOWN or event.type == pygame.MOUSEBUTTONDOWN: running = False pygame.display.update() clock.tick(60) # ----------------- End Options Menu Function ----------------- # ----------------- Mode Menu Function ----------------- def mode_menu(): """This menu appears after the main menu 'Start' option. It allows the player to choose between 1v1 and 1vCPU game modes.""" while True: screen.fill(BLACK) title_surface = title_font.render("Select Mode", True, WHITE) title_rect = title_surface.get_rect(center=(SCREEN_WIDTH / 2, 100)) screen.blit(title_surface, title_rect) onevone_surface = menu_font.render("1v1", True, WHITE) onevcpu_surface = menu_font.render("1vCPU", True, WHITE) onevone_rect = onevone_surface.get_rect(center=(SCREEN_WIDTH / 2, 250)) onevcpu_rect = onevcpu_surface.get_rect(center=(SCREEN_WIDTH / 2, 350)) mouse_pos = pygame.mouse.get_pos() if onevone_rect.collidepoint(mouse_pos): onevone_surface = menu_font.render("1v1", True, YELLOW) if onevcpu_rect.collidepoint(mouse_pos): onevcpu_surface = menu_font.render("1vCPU", True, YELLOW) screen.blit(onevone_surface, onevone_rect) screen.blit(onevcpu_surface, onevcpu_rect) for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() sys.exit() if event.type == pygame.MOUSEBUTTONDOWN: if onevone_rect.collidepoint(event.pos): game_loop() if onevcpu_rect.collidepoint(event.pos): game_loop_cpu() pygame.display.update() clock.tick(60) # ----------------- End Mode Menu Function ----------------- # ----------------- Main Menu Function ----------------- def main_menu(): while True: screen.fill(BLACK) title_surface = title_font.render("Brawler", True, WHITE) title_rect = title_surface.get_rect(center=(SCREEN_WIDTH / 2, 100)) screen.blit(title_surface, title_rect) start_surface = menu_font.render("Start", True, WHITE) options_surface = menu_font.render("Options", True, WHITE) end_surface = menu_font.render("End Game", True, WHITE) start_rect = start_surface.get_rect(center=(SCREEN_WIDTH / 2, 250)) options_rect = options_surface.get_rect(center=(SCREEN_WIDTH / 2, 350)) end_rect = end_surface.get_rect(center=(SCREEN_WIDTH / 2, 450)) mouse_pos = pygame.mouse.get_pos() if start_rect.collidepoint(mouse_pos): start_surface = menu_font.render("Start", True, YELLOW) if options_rect.collidepoint(mouse_pos): options_surface = menu_font.render("Options", True, YELLOW) if end_rect.collidepoint(mouse_pos): end_surface = menu_font.render("End Game", True, YELLOW) screen.blit(start_surface, start_rect) screen.blit(options_surface, options_rect) screen.blit(end_surface, end_rect) for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() sys.exit() if event.type == pygame.MOUSEBUTTONDOWN: if start_rect.collidepoint(event.pos): mode_menu() # Jump to mode selection menu if options_rect.collidepoint(event.pos): options_menu() if end_rect.collidepoint(event.pos): pygame.quit() sys.exit() pygame.display.update() clock.tick(60) # ----------------- End Main Menu Function ----------------- # Start with the main menu main_menu()