Menu
CoddyTech

Titolo e gioca ancora

Lezione 20 di 20 del corso Progetto Pygame: Difendi il castello di Coddy.

Gli ultimi due elementi fanno sì che chiunque possa prendere in mano il gioco: una schermata del titolo che spiega cosa fare e un modo per giocare di nuovo.

Una nuova notte ora inizia in un terzo stato, "title". Non si muove nulla finché non premi Spazio, così il giocatore può prima leggere lo schermo. Dopo una vittoria o una sconfitta, premere Spazio crea una nuova Night: ogni mostro, gemma e timer riparte da zero, perché fanno tutti parte della notte. best rimane al di fuori, quindi il record resta.

elif night.state in ("won", "lost") and event.key == pygame.K_SPACE:
    night = Night()
    night.state = "playing"

La schermata del titolo elenca le sue righe e le disegna una sotto l'altra, con la prima in grandi lettere dorate. Poi gioca: una notte dura quattro minuti. Scegli i potenziamenti più adatti al tuo stile di gioco e scopri se riesci a difendere la rocca fino all'alba.

challenge icon

Sfida

Medio
  1. Una nuova Night inizia con state = "title".
  2. Su KEYDOWN: in "title", Space imposta "playing"; in "won" o "lost", Space crea una nuova notte e la imposta su playing.
  3. In "title", esegui il blit di dark_cover e disegna, centrati a x 360 a partire da y 150, a 50 pixel di distanza: "Hold the Keep" (in oro, grande), "Survive until dawn. Your weapons fire on their own.", "Arrows to move", "Press Space to start" e f"Best night: {clock_text(best)}".
  4. Sotto i messaggi finali, disegna "Press Space to play again" in (360, 350).

Provalo tu

import random
import pygame

WIDTH, HEIGHT = 720, 480
TILE = 48
NIGHT = 60 * 60 * 4
BOSS_AT = NIGHT - 60 * 60
WAVES = [
    ["slime"],
    ["slime", "bat"],
    ["slime", "bat", "spider"],
    ["bat", "spider"],
    ["slime", "spider", "ghost"],
    ["bat", "spider", "ghost"],
    ["spider", "ghost"],
    ["bat", "ghost"],
]
UPGRADES = {
    "Quick bolts": "Bolts fire more often",
    "Sharp bolts": "Bolts hit harder",
    "Spinning axe": "One more axe circles you",
    "Swift boots": "You walk faster",
    "Gem magnet": "Gems come from further",
    "Hearty meal": "Get back a heart",
}
WHITE = (240, 240, 240)
BOLT = (120, 200, 255)
GOLD = (250, 200, 60)

pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Hold the Keep")
clock = pygame.time.Clock()
font = pygame.font.Font("assets/fonts/pixel.ttf", 24)
small_font = pygame.font.Font("assets/fonts/pixel.ttf", 16)
big_font = pygame.font.Font("assets/fonts/pixel.ttf", 48)


def load(name):
    return pygame.image.load("assets/images/" + name).convert_alpha()


def sound(name):
    return pygame.mixer.Sound("assets/sounds/" + name)


def whiten(image):
    white = image.copy()
    white.fill((255, 255, 255), special_flags=pygame.BLEND_RGB_MAX)
    return white


floor_img = load("floor.png")
wall_img = load("wall.png")
knight_img = load("knight.png")
axe_img = load("axe.png")
gem_img = load("gem.png")
heart_img = load("heart.png")
empty_img = load("heart_empty.png")
MONSTERS = {
    "slime": {"image": load("slime.png"), "speed": 1.0, "hp": 1, "size": 28},
    "bat": {"image": load("dungeon_bat.png"), "speed": 1.6, "hp": 1, "size": 28},
    "spider": {"image": load("spider.png"), "speed": 1.2, "hp": 3, "size": 28},
    "ghost": {"image": load("ghost.png"), "speed": 0.8, "hp": 4, "size": 28},
    "boss": {"image": pygame.transform.scale(load("ghost.png"), (96, 96)), "speed": 0.7, "hp": 120, "size": 64},
}
hit_sound = sound("hit.ogg")
hurt_sound = sound("thud.ogg")
gem_sound = sound("coin.ogg")
level_sound = sound("powerup.ogg")

courtyard = pygame.Rect(TILE, TILE, WIDTH - 2 * TILE, HEIGHT - 2 * TILE)
background = pygame.Surface((WIDTH, HEIGHT))
for row in range(HEIGHT // TILE):
    for col in range(WIDTH // TILE):
        x = col * TILE
        y = row * TILE
        if courtyard.collidepoint(x, y):
            background.blit(floor_img, (x, y))
        else:
            background.blit(wall_img, (x, y))
cover = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)
cover.fill((0, 0, 0, 150))
shade = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)
dark_cover = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)
dark_cover.fill((0, 0, 0, 210))


def draw(image, center):
    screen.blit(image, image.get_rect(center=center))


def clock_text(frames):
    seconds = frames // 60
    return f"{seconds // 60}:{seconds % 60:02d}"


def edge_point():
    side = random.randint(0, 3)
    if side == 0:
        return (random.randint(0, WIDTH), -24)
    if side == 1:
        return (random.randint(0, WIDTH), HEIGHT + 24)
    if side == 2:
        return (-24, random.randint(0, HEIGHT))
    return (WIDTH + 24, random.randint(0, HEIGHT))


def load_best():
    try:
        with open("best_night.txt") as file:
            return int(file.read())
    except (FileNotFoundError, ValueError):
        return 0


def save_best(frames):
    with open("best_night.txt", "w") as file:
        file.write(str(frames))


class Knight(pygame.sprite.Sprite):
    def __init__(self):
        super().__init__()
        self.image = knight_img
        self.rect = pygame.Rect(0, 0, 24, 24)
        self.rect.center = courtyard.center
        self.speed = 3
        self.hearts = 5
        self.max_hearts = 5
        self.hurt = 0
        self.bolt_wait = 30
        self.bolt_damage = 1
        self.axes = 1
        self.magnet = 80

    def move(self, keys):
        if keys[pygame.K_LEFT]:
            self.rect.x -= self.speed
        if keys[pygame.K_RIGHT]:
            self.rect.x += self.speed
        if keys[pygame.K_UP]:
            self.rect.y -= self.speed
        if keys[pygame.K_DOWN]:
            self.rect.y += self.speed
        self.rect.clamp_ip(courtyard)


class Monster(pygame.sprite.Sprite):
    def __init__(self, kind, pos):
        super().__init__()
        self.kind = kind
        self.image = MONSTERS[kind]["image"]
        self.speed = MONSTERS[kind]["speed"]
        self.hp = MONSTERS[kind]["hp"]
        self.axe_wait = 0
        self.flash = 0
        self.pos = pygame.Vector2(pos)
        self.rect = pygame.Rect(0, 0, MONSTERS[kind]["size"], MONSTERS[kind]["size"])
        self.rect.center = self.pos

    def update(self, target):
        step = pygame.Vector2(target) - self.pos
        if step.length() > 0:
            self.pos += step.normalize() * self.speed
        self.rect.center = self.pos
        if self.axe_wait > 0:
            self.axe_wait -= 1
        if self.flash > 0:
            self.flash -= 1

    def hit(self, damage):
        self.hp -= damage
        self.flash = 6
        hit_sound.play()


class Bolt(pygame.sprite.Sprite):
    def __init__(self, start, target, damage):
        super().__init__()
        self.pos = pygame.Vector2(start)
        step = pygame.Vector2(target) - self.pos
        if step.length() == 0:
            step = pygame.Vector2(1, 0)
        self.velocity = step.normalize() * 7
        self.damage = damage
        self.rect = pygame.Rect(0, 0, 10, 10)
        self.rect.center = self.pos

    def update(self):
        self.pos += self.velocity
        self.rect.center = self.pos
        if not screen.get_rect().colliderect(self.rect):
            self.kill()


class Gem(pygame.sprite.Sprite):
    def __init__(self, pos):
        super().__init__()
        self.rect = pygame.Rect(0, 0, 24, 24)
        self.rect.center = pos
        self.rect.clamp_ip(courtyard)
        self.pos = pygame.Vector2(self.rect.center)

    def update(self, knight):
        target = pygame.Vector2(knight.rect.center)
        if self.pos.distance_to(target) < knight.magnet:
            self.pos.move_towards_ip(target, 6)
        self.rect.center = self.pos


class Night:
    def __init__(self):
        self.knight = Knight()
        self.time = 0
        self.monsters = pygame.sprite.Group()
        self.bolts = pygame.sprite.Group()
        self.bolt_timer = 0
        self.spin = 0
        self.gems = pygame.sprite.Group()
        self.xp = 0
        self.level = 1
        self.need = 5
        self.choices = []
        self.pick = 0
        self.boss = None
        self.saved = False
        self.state = "playing"

    def wave(self):
        return min(len(WAVES), self.time * len(WAVES) // NIGHT + 1)

    def spawn(self):
        wave = self.wave()
        if self.time % max(16, 46 - wave * 5) == 0:
            kind = random.choice(WAVES[wave - 1])
            self.monsters.add(Monster(kind, edge_point()))
        if self.boss is None and self.time >= BOSS_AT:
            self.boss = Monster("boss", (WIDTH // 2, -40))
            self.monsters.add(self.boss)
        boss = self.boss
        if boss and boss.alive() and boss.hp < MONSTERS["boss"]["hp"] // 2:
            if self.time % 120 == 0:
                for _ in range(3):
                    offset = (random.randint(-50, 50), random.randint(-50, 50))
                    self.monsters.add(Monster("bat", boss.pos + offset))

    def nearest(self):
        point = pygame.Vector2(self.knight.rect.center)
        best = None
        for monster in self.monsters:
            if best is None or point.distance_to(monster.pos) < point.distance_to(best.pos):
                best = monster
        return best

    def shoot(self):
        self.bolt_timer += 1
        target = self.nearest()
        if self.bolt_timer >= self.knight.bolt_wait and target is not None:
            self.bolt_timer = 0
            bolt = Bolt(self.knight.rect.center, target.pos, self.knight.bolt_damage)
            self.bolts.add(bolt)

    def axe_spots(self):
        spots = []
        for i in range(self.knight.axes):
            angle = self.spin + i * 360 / self.knight.axes
            offset = pygame.Vector2(70, 0).rotate(angle)
            spots.append(pygame.Vector2(self.knight.rect.center) + offset)
        return spots

    def hurt_monsters(self):
        for bolt in self.bolts:
            target = pygame.sprite.spritecollideany(bolt, self.monsters)
            if target is not None:
                target.hit(bolt.damage)
                bolt.kill()
        for spot in self.axe_spots():
            blade = pygame.Rect(0, 0, 36, 36)
            blade.center = spot
            for monster in self.monsters:
                if monster.axe_wait == 0 and blade.colliderect(monster.rect):
                    monster.hit(1)
                    monster.axe_wait = 20
        for monster in list(self.monsters):
            if monster.hp <= 0:
                monster.kill()
                drops = 8 if monster.kind == "boss" else 1
                for _ in range(drops):
                    self.gems.add(Gem(monster.pos))

    def collect(self):
        for gem in pygame.sprite.spritecollide(self.knight, self.gems, True):
            self.xp += 1
            gem_sound.play()
        if self.xp >= self.need:
            self.xp -= self.need
            self.level += 1
            self.need += 4
            self.choices = random.sample(list(UPGRADES), 3)
            self.pick = 1
            self.state = "levelup"
            level_sound.play()

    def upgrade(self, name):
        knight = self.knight
        if name == "Quick bolts":
            knight.bolt_wait = max(8, knight.bolt_wait - 8)
        elif name == "Sharp bolts":
            knight.bolt_damage += 1
        elif name == "Spinning axe":
            knight.axes += 1
        elif name == "Swift boots":
            knight.speed += 1
        elif name == "Gem magnet":
            knight.magnet += 40
        elif name == "Hearty meal":
            knight.hearts = min(knight.max_hearts, knight.hearts + 1)
        self.state = "playing"

    def take_hits(self):
        knight = self.knight
        if knight.hurt > 0:
            knight.hurt -= 1
        elif pygame.sprite.spritecollideany(knight, self.monsters):
            knight.hearts -= 1
            knight.hurt = 75
            hurt_sound.play()
            if knight.hearts <= 0:
                self.state = "lost"

    def update(self, keys):
        self.time += 1
        self.spin = (self.spin + 4) % 360
        self.knight.move(keys)
        self.spawn()
        self.monsters.update(self.knight.rect.center)
        self.bolts.update()
        self.gems.update(self.knight)
        self.shoot()
        self.hurt_monsters()
        self.collect()
        self.take_hits()
        if self.time >= NIGHT and self.state == "playing":
            self.state = "won"


best = load_best()
night = Night()

running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        elif event.type == pygame.KEYDOWN:
            if night.state == "levelup":
                if event.key == pygame.K_LEFT:
                    night.pick = (night.pick - 1) % 3
                elif event.key == pygame.K_RIGHT:
                    night.pick = (night.pick + 1) % 3
                elif event.key == pygame.K_SPACE:
                    night.upgrade(night.choices[night.pick])

    if night.state == "playing":
        night.update(pygame.key.get_pressed())
    if night.state in ("won", "lost") and not night.saved:
        night.saved = True
        if night.time > best:
            best = night.time
            save_best(best)

    screen.blit(background, (0, 0))
    knight = night.knight
    for gem in night.gems:
        draw(gem_img, gem.rect.center)
    for monster in night.monsters:
        if monster.flash > 0:
            draw(whiten(monster.image), monster.rect.center)
        else:
            draw(monster.image, monster.rect.center)
    if knight.hurt % 10 < 5:
        draw(knight.image, knight.rect.center)
    for spot in night.axe_spots():
        draw(pygame.transform.rotate(axe_img, -night.spin * 3), spot)
    dark = min(120, max(0, NIGHT - night.time) * 120 // (60 * 60))
    shade.fill((10, 10, 40, dark))
    screen.blit(shade, (0, 0))
    for bolt in night.bolts:
        pygame.draw.circle(screen, BOLT, bolt.rect.center, 5)
    for i in range(knight.max_hearts):
        if i < knight.hearts:
            screen.blit(heart_img, (8 + i * 34, 6))
        else:
            screen.blit(empty_img, (8 + i * 34, 6))
    left = font.render(clock_text(max(0, NIGHT - night.time)) + " until dawn", True, WHITE)
    screen.blit(left, left.get_rect(center=(WIDTH // 2, 24)))
    label = font.render(f"Level {night.level}", True, WHITE)
    screen.blit(label, label.get_rect(topright=(WIDTH - 10, 4)))
    pygame.draw.rect(screen, (40, 40, 60), (WIDTH - 160, 30, 150, 10))
    pygame.draw.rect(screen, GOLD, (WIDTH - 160, 30, 150 * night.xp // night.need, 10))
    if night.state == "levelup":
        screen.blit(cover, (0, 0))
        title = big_font.render("Level up! Pick one", True, GOLD)
        screen.blit(title, title.get_rect(center=(WIDTH // 2, 130)))
        for i, name in enumerate(night.choices):
            card = pygame.Rect(60 + i * 210, 190, 180, 120)
            pygame.draw.rect(screen, (40, 40, 60), card)
            if i == night.pick:
                pygame.draw.rect(screen, GOLD, card, 4)
            else:
                pygame.draw.rect(screen, WHITE, card, 2)
            text = font.render(name, True, WHITE)
            screen.blit(text, text.get_rect(center=(card.centerx, card.y + 40)))
            about = small_font.render(UPGRADES[name], True, WHITE)
            screen.blit(about, about.get_rect(center=(card.centerx, card.y + 80)))
        hint = font.render("Left and Right to choose, Space to take it", True, WHITE)
        screen.blit(hint, hint.get_rect(center=(WIDTH // 2, 360)))
    if night.state in ("title", "won", "lost"):
        screen.blit(dark_cover, (0, 0))
    if night.state in ("won", "lost"):
        if night.state == "won":
            message = big_font.render("Dawn! The keep holds", True, GOLD)
        else:
            message = big_font.render("The keep has fallen", True, WHITE)
        screen.blit(message, message.get_rect(center=(WIDTH // 2, 200)))
        lasted = font.render(f"You lasted {clock_text(night.time)}", True, WHITE)
        screen.blit(lasted, lasted.get_rect(center=(WIDTH // 2, 260)))
        record = font.render(f"Best night: {clock_text(best)}", True, WHITE)
        screen.blit(record, record.get_rect(center=(WIDTH // 2, 300)))
    pygame.display.flip()
    clock.tick(60)

pygame.quit()
quiz iconMettiti alla prova

Questa lezione include un breve quiz. Inizia la lezione per rispondere e tenere traccia dei tuoi progressi.

Tutte le lezioni di Progetto Pygame: Difendi il castello

Esercitati da solo: Playground Pygame