Sprint 7 Project · Duo · ±4 uur samen

Arcade Dodge v2 — Player, Enemy & Game leren bewegen

Je bouwt de arcade dodge/shooter uit sprint 6 om: het gedrag dat vroeger in de engine zat — bewegen, vallen, schieten, schade, score — verhuist nu naar methodes in je eigen classes. De nieuwe engine (v2) roept die methodes aan. Het speler-team geeft Player en Bullet hun methodes; het wereld-team doet dat voor Enemy en Game. Nieuw in het spel: een startscherm met uitleg, een schietpauze, levels met snellere vijanden en een game-over-scherm met een rapport dat jouw Game maakt.

⏱ ±4 uur samen (Opdracht t/m Opleveren; Extra niet inbegrepen)

methodes parameters returnwaarden guards & queries self pygame duo-project contract/interface aparte bestanden

Wat ga je bouwen?

±30 min · samen

Jullie bouwen samen je sprint-6-game om tot een methode-gebaseerde versie. In sprint 6 deed de engine alle actie op basis van attributen; nu bepalen jouw classes zelf hoe ze bewegen, schieten, schade oplopen, sneller worden en herspawnen — via methodes. De nieuwe game_engine.py (v2) roept die methodes aan en laat de uitkomsten zien.

De kern: gedrag verhuist naar je classes. De regel die vroeger in de engine stond, staat nu in jouw class als methode. player.x -= player.speed wordt Player.move(...); enemy.y += enemy.speed wordt Enemy.update(); de kogel-fabriek wordt Player.shoot(); de score wordt Game.add_score(n). Start je spel met run_world(game) — de speler maakt zijn kogels nu zelf.

Zo speel je het spel

Bij het starten verschijnt eerst een startscherm met deze uitleg. Het spel begint pas als je op SPATIE drukt. Lees het scherm dus eerst, anders ben je zó game over.

WatHoe
BewegenPijltje links en pijltje rechts. Je blijft binnen het scherm.
SchietenHoud de spatiebalk ingedrukt. Na elk schot moet je even wachten: bovenin staat Kanon: laden… of Kanon: klaar.
DoelOntwijk de rode blokken, of schiet ze neer.
Punten+1 voor elk blok dat je mist (het valt onderaan weg). +5 voor elk blok dat je raakt met een kogel.
LevensJe begint met 5 levens (HP). Raakt een blok jou, dan verlies je er één. Bij 0 is het game over.
LevelsElke 20 punten gaat het level omhoog. Dan verschijnt LEVEL n in beeld en worden alle blokken sneller.
Game overJe ziet een scherm met jouw rapport: score, level, aantal schoten, treffers en nauwkeurigheid. Het scherm blijft staan tot je op SPATIE, ENTER of ESC drukt (of het venster sluit).
Tip voor de eerste keer. Beweeg eerst een paar seconden en schiet pas daarna. Zie je een fout als AttributeError? Dan mist er een methode of klopt een naam niet — kijk dan in het contract hieronder.

Wat is nieuw ten opzichte van sprint 6?

Je ziet in het spelDat komt van jouw methode
Kanon: klaar / laden…, en je kunt niet sneller schieten dan de pauzePlayer.can_shoot(), Player.update(), Player.shoot()
LEVEL 2, LEVEL 3… en snellere vijandenGame.level() en Enemy.speed_up()
Het game-over-scherm met score, schoten, treffers en percentageGame.record_shot(), Game.record_hit(), Game.summary()
Het startscherm met uitleggeen methode: dat doet de engine
RolBouwtKernvraag
Speler-team bullet.py + player.py: Bullet- en Player-methodes Hoe beweegt, schiet (met pauze) en incasseert de speler via methodes die de engine aanroept?
Wereld-team enemy.py + game.py: Enemy-methodes en Game met score, level en rapport Hoe laat elke vijand zichzelf vallen, melden, herspawnen en versnellen, en houdt Game score, level en statistieken bij?
Samen settings.py, main.py, test_game.py + overstap naar engine v2 Passen jullie methodes op het engine-contract?

Bestandsindeling

Elke class krijgt een eigen bestand. Zo kan ieder team aan eigen bestanden werken zonder dat jullie elkaars code overschrijven. Alle bestanden staan in dezelfde map, samen met game_engine.py.

BestandVan wieBevat
settings.pySamenWIDTH, HEIGHT en de WAVE-data
bullet.pySpeler-teamde class Bullet
player.pySpeler-teamde class Player
enemy.pyWereld-teamde class Enemy
game.pyWereld-teamde class Game
main.pySamenstart het spel met run_world(game)
test_game.pySamende assert-tests van beide teams

Een bestand gebruikt een class uit een ander bestand met een import, bijvoorbeeld from bullet import Bullet bovenaan player.py. Je kent dit uit sprint 5 en 6 en deed het al in een aantal opdrachten van deze sprint. Start het spel met python main.py en de tests met python test_game.py.

Benodigd bestand: game_engine.py (v2)

Voor sprint 7 gebruik je de nieuwe engine die methodes aanroept. Download game_engine.py (v2) en zet hem in dezelfde map als main.py (vervang de v1 uit sprint 6). Kun je niet downloaden? Klap de code hieronder uit en kopieer hem naar een bestand game_engine.py.

Toon de code van game_engine.py (v2)
"""
game_engine.py - Arcade-engine voor Sprint 7 (v2, METHODE-gebaseerd). NIET AANPASSEN.

In sprint 6 muteerde deze engine ZELF de attributen van je objecten (player.x -= ...).
Nu roept de engine METHODES aan op jouw objecten. Jouw classes bepalen zelf hoe ze
bewegen, schieten, schade oplopen, sneller worden en hun eigen rapport maken. De engine
leest alleen nog attributen om te TEKENEN (x, y, width, height, color) en om botsingen
te bepalen. De engine toont ook een startscherm met uitleg en een game-over-scherm.

--------------------------------------------------------------------------
HET CONTRACT - welke METHODES de engine aanroept:

  game.player  - naast de teken-attributen x, y, width, height, color, health:
      move(direction, width)  direction is -1 (links), 0, of +1 (rechts); de speler
                              verschuift zichzelf EN houdt zich binnen [0, width-breedte]
      update()                wordt elk frame aangeroepen (bijv. om de schietpauze te laten aflopen)
      can_shoot()  -> bool    True als de speler nu mag schieten
      shoot()      -> bullet  geeft een NIEUW bullet-object terug, boven de speler,
                              en start de schietpauze
      take_damage(n)          verlaagt de eigen health met n
      is_dead()    -> bool    True als de speler geen leven meer heeft

  game.enemies - een LIJST; elke enemy heeft de teken-attributen EN:
      update()                laat zichzelf een stap vallen
      off_screen(height) -> bool   True als de vijand onder het scherm is
      respawn(width)          zet zichzelf weer bovenaan, op een nieuwe x
      speed_up()              wordt bij elk nieuw level aangeroepen: de vijand wordt sneller

  game.bullets - een LIJST; elke bullet heeft de teken-attributen EN:
      update()                beweegt zichzelf omhoog
      off_screen() -> bool    True als de kogel boven het scherm verdwenen is

  game         - het spelobject met:
      player, enemies, bullets, score, width, height   (attributen)
      add_score(n)            telt n bij de score op
      level()      -> int     het huidige level (begint op 1)
      record_shot()           de engine meldt: er is een kogel afgevuurd
      record_hit()            de engine meldt: een kogel heeft een vijand geraakt
      summary()    -> str     tekst voor het game-over-scherm (mag uit meerdere regels bestaan)

  y telt naar BENEDEN. Kleuren zijn RGB-tuples. De engine tekent, leest toetsen,
  detecteert botsingen en beheert de kogellijst; jouw METHODES doen de rest.
--------------------------------------------------------------------------
"""

import pygame

BACKGROUND = (15, 15, 22)
WHITE = (240, 240, 240)
GRAY = (150, 150, 160)
CYAN = (0, 200, 255)
YELLOW = (255, 220, 0)
RED = (255, 80, 80)
GREEN = (80, 220, 120)


def _overlap(a, b):
    """True als twee rechthoeken (objecten met x/y/width/height) elkaar raken."""
    return (a.x < b.x + b.width and a.x + a.width > b.x and
            a.y < b.y + b.height and a.y + a.height > b.y)


def _text(screen, font, text, color, y):
    """Tekent een regel horizontaal gecentreerd op hoogte y."""
    image = font.render(text, True, color)
    screen.blit(image, ((screen.get_width() - image.get_width()) // 2, y))


def _wait_screen(screen, clock, fonts, title, title_color, lines, footer, keys, ignore_frames=0):
    """Toont een scherm en wacht op een toets uit keys. Toetsen in de eerste ignore_frames
    frames tellen niet mee, zodat een speler die nog toetsen vasthoudt het scherm niet per
    ongeluk wegdrukt. Geeft False terug als het venster wordt gesloten."""
    big, font, small = fonts
    pygame.event.clear()
    frames = 0
    while True:
        frames += 1
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                return False
            if event.type == pygame.KEYDOWN and frames > ignore_frames and event.key in keys:
                return True
        screen.fill(BACKGROUND)
        _text(screen, big, title, title_color, 70)
        y = 160
        for line in lines:
            _text(screen, font, line, WHITE, y)
            y += 30
        _text(screen, small, footer, YELLOW, screen.get_height() - 70)
        pygame.display.flip()
        clock.tick(30)


START_LINES = [
    "Pijltjes links / rechts: bewegen",
    "Spatiebalk (ingedrukt houden): schieten",
    "",
    "Rode blokken vallen naar beneden.",
    "Ontwijk ze: +1 punt voor elk blok dat je mist.",
    "Schiet ze neer: +5 punten.",
    "Raakt een blok jou? Dan verlies je een leven.",
    "",
    "Elke 20 punten ga je een level omhoog.",
    "Hoger level = snellere vijanden.",
]


def run_world(game):
    """Start het spel. Geef je hele Game-object mee."""
    pygame.init()
    screen = pygame.display.set_mode((game.width, game.height))
    pygame.display.set_caption("Sprint 7 - Arcade Dodge (methode-gebaseerd)")
    clock = pygame.time.Clock()
    fonts = (pygame.font.SysFont(None, 56), pygame.font.SysFont(None, 26), pygame.font.SysFont(None, 22))
    big, font, small = fonts

    # Startscherm: uitleg, en wachten tot de speler klaar is.
    if not _wait_screen(screen, clock, fonts, "ARCADE DODGE", CYAN, START_LINES,
                        "Druk op SPATIE om te starten", {pygame.K_SPACE, pygame.K_RETURN}):
        pygame.quit()
        return

    last_level = game.level()
    banner = 0            # aantal frames dat de LEVEL-melding nog zichtbaar is
    window_closed = False
    running = True
    while running:
        # 1) Vensterknop
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
                window_closed = True

        keys = pygame.key.get_pressed()

        # 2) De speler ververst zichzelf; ingedrukte spatie -> de SPELER maakt zelf een kogel
        game.player.update()
        if keys[pygame.K_SPACE] and game.player.can_shoot():
            game.bullets.append(game.player.shoot())
            game.record_shot()

        # 3) Toetsen -> direction; de SPELER beweegt en klemt zichzelf
        direction = 0
        if keys[pygame.K_LEFT]:
            direction -= 1
        if keys[pygame.K_RIGHT]:
            direction += 1
        game.player.move(direction, game.width)

        # 4) Elke vijand valt zichzelf; onderaan -> zichzelf herspawnen
        for enemy in game.enemies:
            enemy.update()
            if enemy.off_screen(game.height):
                enemy.respawn(game.width)
                game.add_score(1)

        # 5) Elke kogel beweegt zichzelf; buiten beeld -> uit de lijst
        for bullet in game.bullets:
            bullet.update()
        game.bullets[:] = [b for b in game.bullets if not b.off_screen()]

        # 6) Botsing kogel x vijand (engine detecteert; objecten reageren)
        for bullet in game.bullets:
            for enemy in game.enemies:
                if _overlap(bullet, enemy):
                    enemy.respawn(game.width)
                    bullet.y = -100          # markeer voor opruimen (volgende frame)
                    game.add_score(5)
                    game.record_hit()

        # 7) Botsing vijand x speler
        for enemy in game.enemies:
            if _overlap(game.player, enemy):
                enemy.respawn(game.width)
                game.player.take_damage(1)
                if game.player.is_dead():
                    running = False

        # 8) Nieuw level? Dan worden alle vijanden sneller.
        level = game.level()
        if level > last_level:
            last_level = level
            banner = 90
            for enemy in game.enemies:
                enemy.speed_up()

        # 9) Tekenen op basis van attributen (net als in sprint 6)
        screen.fill(BACKGROUND)
        p = game.player
        pygame.draw.rect(screen, p.color, (p.x, p.y, p.width, p.height))
        for enemy in game.enemies:
            pygame.draw.rect(screen, enemy.color,
                             (enemy.x, enemy.y, enemy.width, enemy.height))
        for bullet in game.bullets:
            pygame.draw.rect(screen, bullet.color,
                             (bullet.x, bullet.y, bullet.width, bullet.height))

        hud = f"Score: {game.score}    Level: {level}    HP: {game.player.health}"
        screen.blit(font.render(hud, True, WHITE), (10, 10))
        ready = game.player.can_shoot()
        screen.blit(small.render("Kanon: klaar" if ready else "Kanon: laden...", True,
                                 GREEN if ready else GRAY), (10, 38))
        if banner > 0:
            _text(screen, big, f"LEVEL {level}", YELLOW, game.height // 2 - 30)
            banner -= 1
        _text(screen, small, "Pijltjes: bewegen    Spatie: schieten", GRAY, game.height - 18)

        pygame.display.flip()
        clock.tick(60)

    # Game-over-scherm met het rapport van jouw Game-object.
    if not window_closed:
        # SPATIE telt hier niet mee: die is al "schieten" en wordt vaak nog ingedrukt.
        _wait_screen(screen, clock, fonts, "GAME OVER", RED, str(game.summary()).splitlines(),
                     "Druk op ENTER of ESC om af te sluiten",
                     {pygame.K_RETURN, pygame.K_ESCAPE}, ignore_frames=45)

    pygame.quit()

User stories

  • Als speler wil ik mijn schip met links/rechts besturen via move(direction, width), zodat ik zelf binnen het scherm blijf.
  • Als speler wil ik met de spatiebalk een kogel afvuren via shoot(), met een schietpauze die can_shoot() bewaakt, zodat mijn speler zelf beslist wanneer hij mag schieten.
  • Als speler wil ik dat mijn schip schade oploopt via take_damage(n) en met is_dead() game-over aangeeft, zodat de engine niet zelf aan mijn health zit.
  • Als ontwikkelaar wil ik dat elke vijand zichzelf laat vallen via update(), zodat de beweging bij de vijand hoort.
  • Als ontwikkelaar wil ik dat een vijand met off_screen(height) zelf meldt of hij eruit is, zodat de engine die beslissing niet hoeft te kennen.
  • Als ontwikkelaar wil ik dat een vijand zichzelf herstart via respawn(width) en sneller wordt via speed_up(), zodat dit gedrag van de vijand is.
  • Als ontwikkelaar wil ik dat het Game-object score, level en statistieken bijhoudt via add_score(n), level(), record_shot() en record_hit(), zodat er geen zwevende variabelen meer zijn.
  • Als speler wil ik na afloop een rapport zien via summary(), zodat ik weet hoe ik heb gespeeld.
  • Als team wil ik dat elke methode die iets moet melden een waarde teruggeeft, zodat de engine erop kan reageren.

Planning maken

Maak vóór jullie beginnen sámen een bestand project_plan.md. Beschrijf: wie bouwt welke methodes (en in welk bestand); wanneer leggen jullie het contract v2 vast en wanneer integreren jullie; welke engine-regel uit sprint 6 wordt welke methode. Dit lever je samen in met je code.