أفضل ليلة
الدرس 19 من 20 في دورة مشروع Pygame: الصمود في القلعة على Coddy.
تطرح لعبة البقاء سؤالًا: كم من الوقت صمدت؟ احتفظ بأطول ليلة في ملف، لتظل موجودة عند فتح اللعبة في المرة التالية:
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، فاجعله الأفضل واحفظه. - أسفل رسالة النهاية، اعرض
f"You lasted {clock_text(night.time)}"عند (360, 260)، وf"Best night: {clock_text(best)}"عند (360, 300).
جرّب بنفسك
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: الصمود في القلعة
تدرّب بنفسك: Playground لـ Pygame