Sprint 6 Project · Duo · ±4 uur samen

Arcade Dodge — Player, Enemy & Game

Je bouwt dit spel met zijn tweeën, elk aan een eigen kant met een eigen contract. Het speler-team bouwt Player en Bullet; het wereld-team bouwt Enemy en Game. Jullie werken grotendeels onafhankelijk — en komen samen bij de integratiestap, waar de twee kanten via de meegeleverde game-engine voor het eerst echt op elkaar moeten passen. Aan het eind zie je je eigen arcade-spel draaien: een speler die vijanden ontwijkt en neerschiet.

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

classes & objecten constructor status-attributen object-in-object pygame duo-project contract/interface integratie

Wat ga je bouwen?

±30 min · samen

Jullie bouwen samen een arcade dodge/shooter met drie classes, verdeeld over twee kanten. Een Player beweegt onderin en schiet; Enemy-objecten vallen van boven; Bullet-objecten vliegen omhoog; en één Game-object bundelt de hele spelstaat. De meegeleverde game_engine.py tekent alles en draait de game-loop — jullie schrijven alleen de classes.

RolBouwtKernvraag
Speler-team Player + Bullet + spawn_bullet() Hoe lever je precies de attributen die de engine tekent, en hoe maakt de factory een kogel op de juiste plek?
Wereld-team Enemy + Game Hoe bundel je speler, vijanden en score in één object dat je aan de engine geeft?
Samen het __main__-integratieblok Passen jullie twee kanten via het engine-contract op elkaar?

Verdeel deze twee rollen onderling. Installeer eenmalig pygame in je venv uit sprint 1 (pip install pygame) en zet game_engine.py naast je script.

Benodigd bestand: game_engine.py

De engine krijg je kant-en-klaar mee — die pas je niet aan. Download game_engine.py en zet hem in dezelfde map als je script. Kun je niet downloaden? Klap dan de code hieronder uit en kopieer hem naar een bestand game_engine.py.

Toon de code van game_engine.py
"""
game_engine.py - Arcade-engine voor Sprint 6 (v1, ATTRIBUUT-gebaseerd). NIET AANPASSEN.

Jij schrijft de classes; deze engine opent het venster, draait de game-loop,
leest de ATTRIBUTEN van jouw objecten en tekent ze als rechthoeken. Jouw classes
zijn dus pure data; de engine verzorgt alle beweging en het tekenwerk.

--------------------------------------------------------------------------
HET CONTRACT - wat de engine van jouw objecten verwacht:

  player  - een object met:
      x, y      (int)    positie van de linkerbovenhoek
      width     (int)    breedte in pixels
      height    (int)    hoogte in pixels
      color     (tuple)  RGB, bijvoorbeeld (0, 200, 255)
      speed     (int)    pixels die de speler per toetsstap opschuift
      health    (int)    OPTIONEEL - levens; bij 0 stopt het spel

  enemies - een LIJST objecten, elk met:
      x, y, width, height, color   (zoals hierboven)
      speed     (int)    pixels die deze enemy per frame ZAKT

  world   - OPTIONEEL (US4 + project) - een object met:
      player, enemies, bullets, score, width, height

  bullet_factory(player) -> bullet   - OPTIONEEL, een gewone functie die een
      nieuw bullet-object teruggeeft met: x, y, width, height, color, speed
      (speed = pixels die de kogel per frame OMHOOG gaat)

  y telt naar BENEDEN: y groter = lager op het scherm. Vijanden vallen dus met
  een POSITIEVE speed; kogels gaan omhoog. Kleuren zijn RGB-tuples, geen strings.
--------------------------------------------------------------------------
"""

import random
import pygame


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 run_game(player, enemies, width=480, height=640, fps=60,
             world=None, bullet_factory=None):
    pygame.init()
    screen = pygame.display.set_mode((width, height))
    pygame.display.set_caption("Sprint 6 - Arcade Dodge")
    clock = pygame.time.Clock()
    font = pygame.font.SysFont(None, 28)

    bullets = getattr(world, "bullets", []) if world is not None else []

    running = True
    while running:
        # 1) Vensterknop / afsluiten en schieten
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
            if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
                if bullet_factory is not None:
                    bullets.append(bullet_factory(player))

        # 2) Toetsen lezen -> speler-attribuut aanpassen
        keys = pygame.key.get_pressed()
        if keys[pygame.K_LEFT]:
            player.x -= player.speed
        if keys[pygame.K_RIGHT]:
            player.x += player.speed
        player.x = max(0, min(player.x, width - player.width))

        # 3) Vijanden laten vallen; onderaan -> herspawnen bovenaan
        for enemy in enemies:
            enemy.y += enemy.speed
            if enemy.y > height:
                enemy.y = -enemy.height
                enemy.x = random.randint(0, width - enemy.width)
                if world is not None:
                    world.score += 1

        # 4) Kogels omhoog; buiten beeld -> weg
        for bullet in bullets:
            bullet.y -= bullet.speed
        bullets[:] = [b for b in bullets if b.y + b.height > 0]

        # 5) Botsing - kogel raakt vijand
        for bullet in bullets:
            for enemy in enemies:
                if _overlap(bullet, enemy):
                    enemy.y = -enemy.height
                    enemy.x = random.randint(0, width - enemy.width)
                    bullet.y = -100
                    if world is not None:
                        world.score += 5

        # 6) Botsing - vijand raakt speler
        for enemy in enemies:
            if _overlap(player, enemy):
                enemy.y = -enemy.height
                enemy.x = random.randint(0, width - enemy.width)
                if hasattr(player, "health"):
                    player.health -= 1
                    if player.health <= 0:
                        running = False

        # 7) Tekenen (alles als rechthoek, op basis van attributen)
        screen.fill((15, 15, 22))
        pygame.draw.rect(screen, player.color,
                         (player.x, player.y, player.width, player.height))
        for enemy in enemies:
            pygame.draw.rect(screen, enemy.color,
                             (enemy.x, enemy.y, enemy.width, enemy.height))
        for bullet in bullets:
            pygame.draw.rect(screen, bullet.color,
                             (bullet.x, bullet.y, bullet.width, bullet.height))

        score = getattr(world, "score", 0) if world is not None else 0
        hp = getattr(player, "health", None)
        text = f"Score: {score}" + (f"    HP: {hp}" if hp is not None else "")
        screen.blit(font.render(text, True, (240, 240, 240)), (10, 10))

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

    pygame.quit()


def run_world(world, bullet_factory=None):
    """Voor US4 + project: geef je hele Game/World-object mee."""
    run_game(world.player, world.enemies,
             width=world.width, height=world.height,
             world=world, bullet_factory=bullet_factory)

User stories

  • Als speler wil ik mijn schip met links/rechts kunnen besturen, zodat ik vallende vijanden kan ontwijken.
  • Als speler wil ik dat mijn schip verplicht op een startpositie ontstaat, zodat het nooit halfleeg in het spel verschijnt.
  • Als ontwikkelaar wil ik elke vijand als eigen object met eigen x/y/speed, zodat een golf vijanden niet uit parallelle lijsten hoeft te komen.
  • Als ontwikkelaar wil ik een hele golf vijanden in één lijst beheren, zodat ik er met één for doorheen kan.
  • Als speler wil ik kogels kunnen afvuren die omhoog vliegen, zodat ik vijanden kan raken.
  • Als ontwikkelaar wil ik speler, vijanden, kogels en score in één Game-object, zodat mijn spel niet op globals draait.
  • Als team wil ik één vaste engine-afspraak (de attribuutnamen), zodat onze objecten meteen door de engine getekend worden.
  • Als team wil ik één onderlinge afspraak (constructor-signaturen en game.bullets), zodat we parallel kunnen bouwen en de code aan het eind gewoon samenpast.
Bouw tegen het contract, niet tegen elkaars code.
Zodra jullie in het tabblad Ontwerpen de contracttabel hebben ingevuld, hoeft het wereld-team nooit de broncode van Player te lezen — en hoeft het speler-team Game niet te kennen. Alleen de namen en retourwaarden uit die tabel tellen.
Waar dit project om draait. Je werkt met classes, attributen, een goed gevulde constructor en losse functies. De engine verzorgt alle actie op het scherm (bewegen, vallen, schieten, botsen); jouw classes beschrijven de data waarmee de engine werkt. spawn_bullet is een gewone functie — precies wat je uit sprint 5 kent.

De drie classes op hoofdlijnen

Naast de attributen die de engine tekent, laat je de constructor ook attributen zélf berekenen uit de andere waarden. En de Game-constructor doet echt werk: hij bouwt de vijandengolf op uit een data-lijst WAVE — een lijst met (x, speed) per vijand — en berekent daaruit overzichtswaarden.

ClassEigenaarAttributen uit parameters / vastBerekend in de constructor
Player Speler-team x, y, width, height, color, speed, health center_x, start_x/start_y (beginpositie onthouden)
Bullet Speler-team x, y, width, height, color, speed (+ losse functie spawn_bullet(player) die een Bullet teruggeeft)
Enemy Wereld-team x, y, width, height, color, speed points (uit speed), valid (snelheid speelbaar?)
Game Wereld-team player, bullets, score, width, height enemies (uit WAVE met een for-lus, gestaffeld in y), total_enemies, hardest_speed

Planning maken

Maak vóór jullie beginnen sámen een bestand project_plan.md. Beschrijf daarin:

  • Wie bouwt welke class(es)?
  • Op welk moment spreken jullie het contract definitief af, en wanneer plannen jullie de integratiestap in?
  • Welke losse globals uit een game-script (denk aan US4) worden nu een attribuut, en welke een methode — en bij welke class hoort dat?
  • Hoelang schat elke rol zijn eigen bouwstappen in, en welke fase lijkt jullie het lastigst?

Dit bestand lever je samen in met je code.