최고의 밤
Coddy의 Pygame 프로젝트: 성채를 지켜라 코스 레슨. 20개 중 19번째.
생존 게임에서는 얼마나 오래 버텼는지 묻습니다. 가장 오래 버틴 밤을 파일에 저장해 두면 다음에 게임을 열 때도 기록이 남아 있습니다.
def load_best():
try:
with open("best_night.txt") as file:
return int(file.read())
except (FileNotFoundError, ValueError):
return 0게임을 처음 실행하면 파일이 없습니다. open은 FileNotFoundError를 발생시키고, except는 프로그램을 멈추는 대신 0을 반환합니다. 파일에 숫자가 아닌 값이 들어 있으면 int는 ValueError를 발생시키며, 이때도 같은 값을 반환합니다.
밤이 끝나면 기록과 한 번 비교합니다. night.saved는 이 밤의 기록을 저장했는지 기억합니다. 밤이 끝난 뒤에도 루프는 계속 실행되므로, 이 표시가 없으면 매 프레임마다 저장하게 됩니다.
브라우저에서는 페이지가 열려 있는 동안 파일이 유지되고, 자신의 컴퓨터에서는 main.py 옆에 파일이 남아 있습니다.
챌린지
중급load_best()를 작성하고,save_best(frames)는str(frames)를best_night.txt에 씁니다.night = Night()앞에best = load_best()를 추가합니다.Night에saved = False를 지정합니다.- 업데이트 후 상태가
"won"또는"lost"이고 아직 밤을 저장하지 않았다면 저장된 것으로 표시합니다. 그리고night.time이best보다 크면 이를 최고 기록으로 설정하고 저장합니다. - 종료 메시지 아래에 (360, 260)에
f"You lasted {clock_text(night.time)}"를, (360, 300)에f"Best night: {clock_text(best)}"를 표시합니다.
직접 해보기
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()
Pygame 프로젝트: 성채를 지켜라의 모든 레슨
직접 연습해 보세요: Pygame 플레이그라운드