Menu
CoddyTech

Bình minh

Bài học 23 trên 25 trong khóa học Dự án Pygame: Trấn giữ thành trì của Coddy.

Giờ đây, người chơi có thể chiến thắng màn đêm. Khi night.time đạt đến NIGHT, mặt trời mọc, trạng thái chuyển thành "won" và vòng lặp ngừng cập nhật, giống như khi thua.

Hãy để người chơi thấy bình minh đang đến gần. Phủ một lớp màu xanh đậm trong suốt lên sân trong, với độ tối 120 trên 255 suốt đêm, rồi làm lớp màu nhạt dần trong phút cuối. Tạo Surface shade một lần trước vòng lặp, giống như lớp phủ, rồi tô màu cho nó ở mỗi khung hình:

dark = min(120, max(0, NIGHT - night.time) * 120 // (60 * 60))
shade.fill((10, 10, 40, dark))
screen.blit(shade, (0, 0))

Khi còn 30 giây, 1800 * 120 // 3600 bằng 60: độ tối giảm một nửa. Vẽ lớp phủ sau hiệp sĩ và các rìu nhưng trước các tia phép và thanh trên cùng, để phép thuật và trái tim tỏa sáng xuyên qua màn đêm.

challenge icon

Thử thách

Trung bình

Chiến thắng vào lúc bình minh.

  1. Ở cuối update: khi đang chơi và self.time >= NIGHT, đặt self.state = "won".
  2. Tạo shade giống cover, nhưng không tô màu; vẽ nó như trên, sau các rìu.
  3. Tạo dark_cover giống cover, tô màu bằng (0, 0, 0, 210). Khi thắng hoặc thua, hãy blit nó; nếu thắng, hiển thị Dawn! The keep holds bằng big_font màu vàng tại (360, 200).

Nhấn Chơi rồi nhấn T ba lần: bóng tối tan đi. Nhấn bốn lần: bình minh. Sau đó nhấn Kiểm tra.

Tự làm thử

import random
import pygame

WIDTH, HEIGHT = 720, 480
TILE = 48
NIGHT = 60 * 60 * 4
BOSS_AT = NIGHT - 60 * 60
# Các loại quái vật xuất hiện trong từng đợt, từ đợt đầu đến đợt cuối
WAVES = [
    ["slime"],
    ["slime", "bat"],
    ["slime", "bat", "spider"],
    ["bat", "spider"],
    ["slime", "spider", "ghost"],
    ["bat", "spider", "ghost"],
    ["spider", "ghost"],
    ["bat", "ghost"],
]
# Dòng chữ trên thẻ của mỗi nâng cấp và tác dụng của nó với hiệp sĩ
UPGRADES = {
    "Quick bolts": "Bolts fire more often",  # bolt_wait giảm 8, không thấp hơn 8
    "Sharp bolts": "Bolts hit harder",  # bolt_damage tăng 1
    "Spinning axe": "One more axe circles you",  # axes tăng 1
    "Swift boots": "You walk faster",  # speed tăng 1
    "Gem magnet": "Gems come from further",  # magnet tăng 40
    "Hearty meal": "Get back a heart",  # hearts tăng 1, không vượt quá max_hearts
}
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))


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


night = Night()

running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        elif event.type == pygame.KEYDOWN:
            # Phím thử nghiệm: T bỏ qua một phút trong đêm để thấy trùm và bình minh sớm hơn
            if event.key == pygame.K_t and night.state == "playing":
                night.time += 60 * 60
            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)
    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 == "lost":
        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()

Tất cả bài học trong Dự án Pygame: Trấn giữ thành trì

Tự luyện tập: Playground Pygame