Menu
CoddyTech

Die beste Nacht

Lektion 19 von 20 im Kurs Pygame-Projekt: Verteidige den Bergfried von Coddy.

Ein Überlebensspiel fragt: Wie lange hast du durchgehalten? Speichere die längste Nacht in einer Datei, damit sie beim nächsten Öffnen des Spiels noch vorhanden ist:

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

Beim ersten Start des Spiels gibt es noch keine Datei: open löst FileNotFoundError aus, und except liefert stattdessen 0 zurück, damit das Programm nicht angehalten wird. Enthält die Datei etwas, das keine Zahl ist, löst int ValueError aus und liefert ebenfalls denselben Wert zurück.

Vergleiche das Ende einer Nacht einmal mit dem Rekord. night.saved merkt sich, dass diese Nacht gezählt wurde, denn die Schleife läuft nach dem Ende weiter und würde sonst in jedem Frame speichern.

Im Browser bleibt die Datei bestehen, solange die Seite geöffnet ist; auf deinem eigenen Computer bleibt sie neben main.py.

challenge icon

Aufgabe

Mittel
  1. Schreibe load_best() oberhalb von save_best(frames), das str(frames) in best_night.txt schreibt.
  2. Vor night = Night(): best = load_best(). Gib Night den Wert saved = False.
  3. Nach dem Update: Wenn der Status "won" oder "lost" ist und die Nacht noch nicht gespeichert wurde, markiere sie als gespeichert. Falls night.time größer als best ist, setze den Bestwert darauf und speichere ihn.
  4. Zeige unter der Endnachricht f"You lasted {clock_text(night.time)}" bei (360, 260) und f"Best night: {clock_text(best)}" bei (360, 300) an.

Probier es selbst

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))


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.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"


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())

    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)))
    pygame.display.flip()
    clock.tick(60)

pygame.quit()

Alle Lektionen in Pygame-Projekt: Verteidige den Bergfried

Übe selbstständig: Pygame-Playground