498 lines
16 KiB
Python
498 lines
16 KiB
Python
import random
|
|
import pygame
|
|
import json
|
|
from pathlib import Path
|
|
|
|
pygame.init()
|
|
|
|
WIDTH, HEIGHT = 640, 480
|
|
screen = pygame.display.set_mode((WIDTH, HEIGHT))
|
|
pygame.display.set_caption("Sugarmania")
|
|
clock = pygame.time.Clock()
|
|
font = pygame.font.SysFont(None, 36)
|
|
big_font = pygame.font.SysFont(None, 64)
|
|
small_font = pygame.font.SysFont(None, 26)
|
|
|
|
tiny_font = pygame.font.SysFont(None, 20)
|
|
|
|
MENU_OPTIONS = ["Play", "Quit"]
|
|
|
|
MODE_NAMES = ["easy", "medium", "hard"]
|
|
|
|
MODES = {
|
|
"easy": {"lives": 5, "speed": 7, "spawn_every": 10, "enemy_pool": ["Bouncer"], "desc": "Only Bouncers. One new enemy per 10 sugar"},
|
|
"medium": {"lives": 3, "speed": 6, "spawn_every": 7, "enemy_pool": ["Bouncer", "Chaser"], "desc": "Bouncers and Chasers. One new enemy per 7 sugar"},
|
|
"hard": {"lives": 2, "speed": 5, "spawn_every": 5, "enemy_pool": ["Bouncer", "Chaser", "Dasher"], "desc": "Bouncers, Chasers and Dashers. One new enemy per 5 sugar"}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class ChaserEnemy:
|
|
def __init__(self, avoid_rect):
|
|
self.rect = pygame.Rect(0, 0, 36, 36)
|
|
while True:
|
|
self.rect.x = random.randint(0, WIDTH - 36)
|
|
self.rect.y = random.randint(0, HEIGHT - 36)
|
|
if abs(self.rect.x - avoid_rect.x) > 150 or abs(self.rect.y - avoid_rect.y) > 150:
|
|
break
|
|
self.speed = 1
|
|
self.spawn_timer = 60
|
|
|
|
def update(self, target):
|
|
if self.rect.centerx < target.centerx:
|
|
self.rect.x += self.speed
|
|
elif self.rect.centerx > target.centerx:
|
|
self.rect.x -= self.speed
|
|
if self.rect.y < target.centery:
|
|
self.rect.y += self.speed
|
|
elif self.rect.centery > target.centery:
|
|
self.rect.y -= self.speed
|
|
class DasherEnemy:
|
|
WAIT_BEFORE_FIRST_DASH = 90
|
|
DASH_FRAMES = 30
|
|
WAIT_AFTER_DASH = 120
|
|
DASH_SPEED = 6
|
|
def __init__(self, avoid_rect):
|
|
self.rect = pygame.Rect(0, 0, 36, 36)
|
|
while True:
|
|
self.rect.x = random.randint(0, WIDTH - 36)
|
|
self.rect.y = random.randint(0, HEIGHT - 36)
|
|
if abs(self.rect.x - avoid_rect.x) > 150 or abs(self.rect.y - avoid_rect.y) > 150:
|
|
break
|
|
self.x = float(self.rect.x)
|
|
self.y = float(self.rect.y)
|
|
self.vx = 0
|
|
self.vy = 0
|
|
self.state = "wait"
|
|
self.timer = self.WAIT_BEFORE_FIRST_DASH
|
|
self.spawn_timer = 60
|
|
|
|
def update(self, target):
|
|
self.timer -= 1
|
|
|
|
if self.state == "wait":
|
|
if self.timer > 0:
|
|
return
|
|
dx = target.centerx - self.rect.centerx
|
|
dy = target.centery - self.rect.centery
|
|
dist = max(1, (dx ** 2 + dy ** 2) ** 0.5)
|
|
self.vx = dx / dist * self.DASH_SPEED
|
|
self.vy = dy / dist * self.DASH_SPEED
|
|
self.state = "dash"
|
|
self.timer = self.DASH_FRAMES
|
|
return
|
|
|
|
self.x += self.vx
|
|
self.y += self.vy
|
|
self.rect.x = int(self.x)
|
|
self.rect.y = int(self.y)
|
|
self.rect.clamp_ip(screen.get_rect())
|
|
self.x, self.y = float(self.rect.x), float(self.rect.y)
|
|
|
|
if self.timer <= 0:
|
|
self.state = "wait"
|
|
self.vx = 0
|
|
self.vy = 0
|
|
self.timer = self.WAIT_AFTER_DASH
|
|
class BouncerEnemy:
|
|
def __init__(self, avoid_rect):
|
|
self.rect = pygame.Rect(0, 0, 36, 36)
|
|
while True:
|
|
self.rect.x = random.randint(0, WIDTH - 36)
|
|
self.rect.y = random.randint(0, HEIGHT - 36)
|
|
if abs(self.rect.x - avoid_rect.x) > 150 or abs(self.rect.y - avoid_rect.y) > 150:
|
|
break
|
|
self.vx = random.choice([-3, -2, 2, 3])
|
|
self.vy = random.choice([-3, -2, 2, 3])
|
|
self.spawn_timer = 60
|
|
|
|
def update(self, target=None):
|
|
self.rect.x += self.vx
|
|
self.rect.y += self.vy
|
|
if self.rect.left < 0 or self.rect.right > WIDTH:
|
|
self.vx = -self.vx
|
|
if self.rect.top < 0 or self.rect.bottom > HEIGHT:
|
|
self.vy = -self.vy
|
|
|
|
|
|
ENEMY_CLASSES = {"Bouncer": BouncerEnemy, "Chaser": ChaserEnemy, "Dasher": DasherEnemy}
|
|
|
|
|
|
|
|
player_img = pygame.image.load("player.png").convert_alpha()
|
|
player_img = pygame.transform.scale(player_img, (40, 60))
|
|
|
|
|
|
Bouncer_img = pygame.image.load("Bouncer.png").convert_alpha()
|
|
Bouncer_img = pygame.transform.scale(Bouncer_img, (30, 45))
|
|
|
|
Chaser_img = pygame.image.load("Chaser.png").convert_alpha()
|
|
Chaser_img = pygame.transform.scale(Chaser_img, (30, 45))
|
|
|
|
Dasher_img = pygame.image.load("Dasher.png").convert_alpha()
|
|
Dasher_img = pygame.transform.scale(Dasher_img, (30, 45))
|
|
|
|
ENEMY_IMAGES = {
|
|
"Bouncer": Bouncer_img,
|
|
"Dasher": Dasher_img,
|
|
"Chaser": Chaser_img
|
|
}
|
|
|
|
|
|
sugar_img = pygame.image.load("honey.png").convert_alpha()
|
|
sugar_img = pygame.transform.scale(sugar_img, (50, 50))
|
|
|
|
|
|
heart_img = pygame.image.load("Heart.png").convert_alpha()
|
|
heart_img = pygame.transform.scale(heart_img,(40, 40))
|
|
|
|
background_img = pygame.image.load("background.png").convert_alpha()
|
|
background_img = pygame.transform.scale(background_img, (WIDTH, HEIGHT))
|
|
|
|
title_img = pygame.image.load("titles.png").convert_alpha()
|
|
|
|
button_img = pygame.image.load("button.png").convert_alpha()
|
|
button_hover_img = pygame.image.load("button_hover.png").convert_alpha()
|
|
|
|
quit_img = pygame.image.load("quit.png").convert_alpha()
|
|
quit_hover_img = pygame.image.load("quit_hover.png").convert_alpha()
|
|
|
|
panel_img = pygame.image.load("panel.png").convert_alpha()
|
|
|
|
|
|
def new_sugar():
|
|
return pygame.Rect(random.randint(0, WIDTH - 20),
|
|
random.randint(0, HEIGHT - 20), 50, 50) #20,s
|
|
|
|
|
|
SAVE_PATH = Path(__file__).with_name("highscores.json")
|
|
|
|
def load_best(mode):
|
|
try:
|
|
scores = json.loads(SAVE_PATH.read_text())
|
|
except (FileNotFoundError, json.JSONDecodeError):
|
|
return 0
|
|
return scores.get(mode, 0)
|
|
|
|
def save_best(mode, score):
|
|
try:
|
|
scores = json.loads(SAVE_PATH.read_text())
|
|
except (FileNotFoundError, json.JSONDecodeError):
|
|
scores = {}
|
|
scores[mode] = score
|
|
SAVE_PATH.write_text(json.dumps(scores))
|
|
|
|
|
|
|
|
|
|
|
|
#game loop
|
|
def game_screen(mode):
|
|
cfg = MODES[mode]
|
|
player = pygame.Rect(300, 220, 40, 60)
|
|
speed = cfg["speed"]
|
|
score = 0
|
|
Enemies = []
|
|
sugar = new_sugar()
|
|
lives = cfg["lives"]
|
|
hit_timer = 0
|
|
start_ticks = pygame.time.get_ticks()
|
|
|
|
while True:
|
|
for event in pygame.event.get():
|
|
if event.type == pygame.QUIT:
|
|
return "quit", {}
|
|
|
|
if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
|
|
return "menu", {}
|
|
|
|
|
|
|
|
#movement
|
|
keys = pygame.key.get_pressed()
|
|
if keys[pygame.K_LEFT] or keys[pygame.K_a]:
|
|
player.x -= speed
|
|
if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
|
|
player.x += speed
|
|
if keys[pygame.K_UP] or keys[pygame.K_w]:
|
|
player.y -= speed
|
|
if keys[pygame.K_DOWN] or keys[pygame.K_s]:
|
|
player.y += speed
|
|
|
|
for enemy in Enemies:
|
|
if enemy.spawn_timer > 0:
|
|
enemy.spawn_timer -= 1
|
|
else:
|
|
enemy.update(player)
|
|
if hit_timer > 0:
|
|
hit_timer -= 1
|
|
else:
|
|
for enemy in Enemies:
|
|
if enemy.spawn_timer <= 0 and player.colliderect(enemy.rect):
|
|
lives -= 1
|
|
hit_timer = 90
|
|
break
|
|
if lives <= 0:
|
|
return "gameover", {
|
|
"score": score,
|
|
"snapshot": screen.copy(),
|
|
"seconds": (pygame.time.get_ticks() - start_ticks) // 1000,
|
|
}
|
|
|
|
#keep player in
|
|
player.clamp_ip(screen.get_rect())
|
|
|
|
# score
|
|
if player.colliderect(sugar):
|
|
score += 1
|
|
sugar = new_sugar()
|
|
if score % cfg["spawn_every"] == 0 and cfg["enemy_pool"]:
|
|
enemy_name = random.choice(cfg["enemy_pool"])
|
|
enemy = ENEMY_CLASSES[enemy_name](player)
|
|
enemy.image = ENEMY_IMAGES[enemy_name]
|
|
Enemies.append(enemy)
|
|
|
|
|
|
|
|
screen.fill((255, 228, 240)) # screen
|
|
screen.blit(sugar_img, sugar) #sugar
|
|
text = font.render(f"score: {score}", True, (90, 30, 60))
|
|
screen.blit(text, (10, 10))
|
|
for enemy in Enemies:
|
|
if enemy.spawn_timer % 10 < 5:
|
|
screen.blit(enemy.image, enemy.image.get_rect(center=enemy.rect.center))
|
|
screen.blit(player_img, player)
|
|
if hit_timer > 0 and hit_timer % 10 < 5:
|
|
red = player_img.copy()
|
|
red.fill((255, 40, 40, 0), special_flags=pygame.BLEND_RGBA_ADD)
|
|
screen.blit(red, player)
|
|
for i in range(lives):
|
|
screen.blit(heart_img, (10 + i * 35, 40))
|
|
pygame.display.flip()
|
|
clock.tick(60)
|
|
|
|
|
|
|
|
class MenuButton:
|
|
def __init__(self, center, action, image, hover_image):
|
|
self.action = action
|
|
self.image = image
|
|
self.hover_image = hover_image
|
|
self.rect = self.image.get_rect(center = center)
|
|
|
|
def hovered(self, mouse_pos):
|
|
return self.rect.collidepoint(mouse_pos)
|
|
|
|
def draw(self, surface, highlighted):
|
|
picture = self.hover_image if highlighted else self.image
|
|
surface.blit(picture, self.rect)
|
|
|
|
def menu_screen():
|
|
selected = 0
|
|
|
|
buttons = [
|
|
MenuButton((WIDTH // 2, 250), "mode", button_img, button_hover_img),
|
|
MenuButton((WIDTH // 2, 340), "quit", quit_img, quit_hover_img),
|
|
]
|
|
|
|
|
|
while True:
|
|
for event in pygame.event.get():
|
|
if event.type == pygame.QUIT:
|
|
return "quit"
|
|
|
|
if event.type == pygame.KEYDOWN:
|
|
if event.key in (pygame.K_UP, pygame.K_w):
|
|
selected = (selected - 1) % len(buttons)
|
|
elif event.key in (pygame.K_DOWN, pygame.K_s):
|
|
selected = (selected + 1) % len(buttons)
|
|
elif event.key == pygame.K_RETURN:
|
|
if selected == -1:
|
|
selected = 0
|
|
return buttons [selected].action
|
|
elif event.key == pygame.K_ESCAPE:
|
|
return "quit"
|
|
|
|
if event.type == pygame.MOUSEMOTION:
|
|
selected = -1
|
|
for i, button in enumerate(buttons):
|
|
if button.hovered(event.pos):
|
|
selected = i
|
|
|
|
if event.type == pygame.WINDOWLEAVE:
|
|
selected = -1
|
|
|
|
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
|
|
for button in buttons:
|
|
if button.hovered(event.pos):
|
|
return button.action
|
|
|
|
screen.blit(background_img, (0, 0))
|
|
screen.blit(title_img, title_img.get_rect(center=(WIDTH // 2, 80)))
|
|
|
|
for i, button in enumerate(buttons):
|
|
button.draw(screen, i == selected)
|
|
pygame.display.flip()
|
|
clock.tick(60)
|
|
|
|
def mode_screen():
|
|
selected = 0
|
|
hovered = -1
|
|
|
|
play_button = MenuButton((210, 405), "game", button_img, button_hover_img)
|
|
back_button = MenuButton((430, 405), "menu", quit_img, quit_hover_img)
|
|
buttons = [play_button, back_button]
|
|
|
|
panel_rect = panel_img.get_rect(center=(WIDTH // 2, 225))
|
|
slot_rects = [
|
|
pygame.Rect(panel_rect.x + 22, panel_rect.y + 22, 78, 68),
|
|
pygame.Rect(panel_rect.x + 133, panel_rect.y + 22, 77, 68),
|
|
pygame.Rect(panel_rect.x + 234, panel_rect.y + 28, 73, 66),
|
|
]
|
|
|
|
while True:
|
|
for event in pygame.event.get():
|
|
if event.type == pygame.QUIT:
|
|
return "quit", None
|
|
|
|
if event.type == pygame.KEYDOWN:
|
|
if event.key in (pygame.K_LEFT, pygame.K_a):
|
|
selected = (selected - 1) % len(MODE_NAMES)
|
|
elif event.key in (pygame.K_RIGHT, pygame.K_d):
|
|
selected = (selected + 1) % len(MODE_NAMES)
|
|
elif event.key == pygame.K_RETURN:
|
|
return "game", MODE_NAMES[selected]
|
|
elif event.key == pygame.K_ESCAPE:
|
|
return "menu", None
|
|
|
|
if event.type == pygame.MOUSEMOTION:
|
|
hovered = -1
|
|
for i, button in enumerate(buttons):
|
|
if button.hovered(event.pos):
|
|
hovered = i
|
|
|
|
if event.type == pygame.WINDOWLEAVE:
|
|
hovered = -1
|
|
|
|
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
|
|
for i, slot in enumerate(slot_rects):
|
|
if slot.collidepoint(event.pos):
|
|
selected = i
|
|
for button in buttons:
|
|
if button.hovered(event.pos):
|
|
if button.action == "game":
|
|
return "game", MODE_NAMES[selected]
|
|
return "menu", None
|
|
|
|
screen.blit(background_img, (0, 0))
|
|
screen.blit(title_img, title_img.get_rect(center=(WIDTH // 2, 70)))
|
|
|
|
pygame.draw.rect(screen, (255, 129, 195), panel_rect)
|
|
screen.blit(panel_img, panel_rect)
|
|
|
|
for i, slot in enumerate(slot_rects):
|
|
fill = (255, 200, 225) if i == selected else (227, 89, 161)
|
|
pygame.draw.rect(screen, fill, slot.inflate (-4, -4), border_radius=6)
|
|
|
|
for i, slot in enumerate(slot_rects):
|
|
name = small_font.render(MODE_NAMES[i].upper(), True, (255, 240, 8))
|
|
screen.blit(name, name.get_rect(center=slot.center))
|
|
|
|
cfg = MODES[MODE_NAMES[selected]]
|
|
first_sentence, second_sentence = cfg["desc"].split(". ", 1)
|
|
line1 = tiny_font.render(first_sentence + ".", True, (255, 240, 8))
|
|
line2 = tiny_font.render(second_sentence, True, (255, 240, 8))
|
|
screen.blit(line1, line1.get_rect(center=(panel_rect.centerx, panel_rect.top + 108)))
|
|
screen.blit(line2, line2.get_rect(center=(panel_rect.centerx, panel_rect.top + 132)))
|
|
|
|
for i, button in enumerate(buttons):
|
|
button.draw(screen, i == hovered)
|
|
|
|
pygame.display.flip()
|
|
clock.tick(60)
|
|
|
|
def game_over_screen(mode, result):
|
|
snapshot = result["snapshot"]
|
|
score = result["score"]
|
|
seconds = result.get("seconds", 0)
|
|
|
|
best = load_best(mode)
|
|
new_best = score > best
|
|
if new_best:
|
|
save_best(mode, score)
|
|
best = score
|
|
|
|
overlay = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)
|
|
fade = 0
|
|
|
|
while True:
|
|
for event in pygame.event.get():
|
|
if event.type == pygame.QUIT:
|
|
return "quit", {}
|
|
|
|
if event.type == pygame.KEYDOWN:
|
|
if event.key in (pygame.K_RETURN, pygame.K_r):
|
|
return "game", {}
|
|
elif event.key == pygame.K_m:
|
|
return "mode", {}
|
|
elif event.key == pygame.K_ESCAPE:
|
|
return "menu", {}
|
|
elif event.key == pygame.K_q:
|
|
return "quit", {}
|
|
|
|
if fade < 180:
|
|
fade = min(180, fade + 12)
|
|
|
|
screen.blit(snapshot, (0, 0))
|
|
overlay.fill((0, 0, 0, fade))
|
|
screen.blit(overlay, (0, 0))
|
|
|
|
title = big_font.render("GAME OVER", True, (255, 240, 8))
|
|
screen.blit(title, title.get_rect(center=(WIDTH // 2, 130)))
|
|
|
|
score_text = font.render(f"Score: {score}", True, (255, 255, 255))
|
|
screen.blit(score_text, score_text.get_rect(center=(WIDTH // 2, 205)))
|
|
|
|
if new_best:
|
|
best_text = small_font.render("NEW BEST", True, (255, 240, 8))
|
|
else:
|
|
best_text = small_font.render(f"best {best}", True, (255, 255, 255))
|
|
screen.blit(best_text, best_text.get_rect(center=(WIDTH // 2, 245)))
|
|
|
|
stats = small_font.render(
|
|
f"{mode} · {seconds}s survived", True, (255, 255, 255)
|
|
)
|
|
screen.blit(stats, stats.get_rect(center=(WIDTH // 2, 285)))
|
|
|
|
prompts = [
|
|
"ENTER-RETRY",
|
|
"M-change mode",
|
|
"esc-main menu",
|
|
"Q-quit",
|
|
]
|
|
for i, text in enumerate(prompts):
|
|
prompt = font.render(text, True, (255, 255, 255))
|
|
screen.blit(prompt, prompt.get_rect(center=(WIDTH // 2, 345 + i * 36)))
|
|
|
|
pygame.display.flip()
|
|
clock.tick(60)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
state = "menu"
|
|
mode = None
|
|
payload = {}
|
|
|
|
while state != "quit":
|
|
if state == "menu":
|
|
state = menu_screen()
|
|
elif state == "mode":
|
|
state, mode = mode_screen()
|
|
elif state == "game":
|
|
state, payload = game_screen(mode)
|
|
elif state == "gameover":
|
|
state, payload = game_over_screen(mode, payload)
|
|
|
|
pygame.quit()
|