Menu
CoddyTech

업그레이드 선택

Coddy의 Pygame 프로젝트: 성채를 지켜라 코스 레슨. 20개 중 15번째.

레벨이 오르면 기사가 더 강해져야 하고, 플레이어가 어떤 방식으로 강해질지 선택해야 합니다. 각 업그레이드와 그 효과를 딕셔너리에 저장하고, 그중 세 가지를 무작위로 제시하세요.

UPGRADES = {
    "Quick bolts": "Bolts fire more often",
    "Sharp bolts": "Bolts hit harder",
    ...
}

self.choices = random.sample(list(UPGRADES), 3)

random.sample(items, 3)은 서로 다른 항목 세 개를 고릅니다. self.pick은 플레이어가 선택 중인 카드입니다. 왼쪽과 오른쪽 키로 카드를 이동하며, % 3을 사용하면 순환합니다. 마지막 카드의 오른쪽은 첫 번째 카드입니다.

업그레이드 이름을 받아 적용하는 메서드를 하나 만들면 카드, 키, 효과를 서로 분리할 수 있습니다.

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
    ...
    self.state = "playing"
challenge icon

챌린지

중급
  1. UPGRADES를 추가합니다: "Quick bolts", "Sharp bolts", "Spinning axe"(도끼가 하나 더 주위를 돕니다), "Swift boots"(더 빠르게 걷습니다), "Gem magnet"(보석을 더 먼 곳에서 끌어옵니다), "Hearty meal"(하트를 하나 회복합니다). 각 항목에 짧은 설명을 붙입니다.
  2. Night: choices = []와 pick = 0을 설정합니다. 레벨이 오르면 무작위 choices 세 개를 설정하고 pick = 1로 설정합니다.
  3. upgrade(name)을 작성합니다. 빠른 볼트는 bolt_wait를 8 줄이되 8보다 낮아지지 않게 합니다. 강력한 볼트는 피해량을 1 늘립니다. 도끼는 도끼를 1개 추가합니다. 장화는 속도를 1 늘립니다. 자석은 40을 더합니다. 식사는 하트를 max_hearts까지 1개 회복합니다. 그런 다음 "playing"으로 돌아갑니다.
  4. "levelup" 상태에서는 왼쪽 및 오른쪽 키로 세 카드 사이에서 night.pick을 변경하고, 스페이스 키를 누르면 night.upgrade(night.choices[night.pick])를 호출합니다.
  5. x 좌표가 60 + i * 210, y 좌표가 190인 180 x 120 크기의 카드를 세 장 그립니다. 각 카드에는 이름과 설명을 표시합니다. 선택한 카드에는 두께가 4인 금색 테두리를, 나머지 카드에는 두께가 2인 흰색 테두리를 그립니다. 카드 위에는 "Level up! Pick one"을 표시합니다.

직접 해보기

import random
import pygame

WIDTH, HEIGHT = 720, 480
TILE = 48
NIGHT = 60 * 60 * 4
WAVES = [
    ["slime"],
    ["slime", "bat"],
    ["slime", "bat", "spider"],
    ["bat", "spider"],
    ["slime", "spider", "ghost"],
    ["bat", "spider", "ghost"],
    ["spider", "ghost"],
    ["bat", "ghost"],
]
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)
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},
    "bat": {"image": load("dungeon_bat.png"), "speed": 1.6, "hp": 1},
    "spider": {"image": load("spider.png"), "speed": 1.2, "hp": 3},
    "ghost": {"image": load("ghost.png"), "speed": 0.8, "hp": 4},
}
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, 28, 28)
        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.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()))

    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()
                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.state = "levelup"
            level_sound.play()

    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:
            if night.state == "levelup":
                if event.key == pygame.K_SPACE:
                    night.state = "playing"

    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!", True, GOLD)
        screen.blit(title, title.get_rect(center=(WIDTH // 2, 200)))
        hint = font.render("Press Space to keep going", True, WHITE)
        screen.blit(hint, hint.get_rect(center=(WIDTH // 2, 260)))
    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()
quiz icon실력 점검

이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.

Pygame 프로젝트: 성채를 지켜라의 모든 레슨

직접 연습해 보세요: Pygame 플레이그라운드