Obligatorisk innlevering uke 8-9 (innlevering nr. 7)

Frist for innlevering: 20.10. kl 23:59

OBS: Denne innleveringen er obligatorisk og må være godkjent for at man skal kunne ta eksamen i faget.

Det er mulig å levere innleveringen før fristen og få den rettet tidligere ved å skrive "Klar til retting" i kommentarfeltet i Devilry. Man vil da ikke kunne få ny retting senere.

Spørsmål eller kommentarer til oppgaveteksten sendes til ivargry@ifi.uio.no.

Introduksjon

Oppgavene i denne innleveringen går ut på å bruke objektorientert programmering til å lage en "spill-motor" til en verden der sauer og ulver (objekter) beveger seg rundt. Denne motoren skal også brukes i neste obligatoriske innlevering, hvor vi skal implementere intelligens til objektene i spillet. Vi skal jobbe med et 2-dimensjonalt spillbrett, der ulike dyr beveger seg rundt på skjermen. Objektorientert programmering er et nyttig verktøy i en slik setting, fordi vi kan representere hvert dyr ved hjelp av et objekt, og enkelt legge til eller fjerne dyr. Hvert objekt vil kjenne til sin egen posisjon, bevegelsesretning og lignende.

Før du begynner

Oppgave 1: Implementere en Sau-klasse

Filnavn: sau.py

Før vi starter med å visualisere objektene våre i spillbrettet, skal vi først implementere en Sau-klasse og sjekke at den fungerer. Denne klassen ligner litt på klassen Sau fra forrige innlevering, men har en del endringer og utvidelser, så det er lurt å skrive en ny klasse fra starten av.

Lag en fil sau.py hvor du definerer en klasse Sau. En sau har et bilde (en streng som peker til en bildefil) og en posisjon (antall pixler fra venstre side og toppen av skjermen). Konstruktøren skal ta denne informasjonen ved hjelp av tre parametere og lagre informasjonen i tilhørende instansvariable:

Informasjonen skal lagres i tilhørende instansvariabler (f. eks skal bilde lagres i self._bilde).

Sau skal i tillegg ha to instansvariabler _fart_fra_venstre og _fart_fra_topp som forteller oss nåværende fart til sauen (hvor mange pixler per tidsenhet den beveger seg fra venstre mot høyre side og fra toppen mot bunnen av skjermen). Disse kan begges settes til å være 1.

Videre ønsker vi at klassen vår skal ha disse metodene:

  1. En metode sett_posisjon som setter posisjonen (self._posisjon_venstre og self._posisjon_topp) til en ny posisjon. Denne metoden må altså ta to parametere (i tillegg til self).
  2. En metode hent_posisjon_venstre og en metode hent_posisjon_topp som returnerer de to posisjonene.
  3. En metode hent_fra_fra_venstre og en metode hent_fart_fra_topp som returnerer farten (fra venstre og fra toppen).
  4. En metode sett_fart som setter farten (self._fart_fra_venstre og self._fart_fra_topp) til en ny fart.
  5. En metode beveg som endrer posisjonen. Metoden tar ingen parametere, men endrer posisjonen i forhold til nåværende fart, ved å legge farten til posisjonen. Altså skal self._posisjon_topp endres ved at self._fart_fra_topp plusses på, og tilsvarende for self._posisjon_venstre.
  6. En metode snu som gjør at sauen endrer bevegelesesretning 180 grader. Hvis sauen for eksempel beveger seg skrått nedover fra øverste venstre hjørne til nederste høyre hjørne (dvs at fart_fra_venstre = 1 og fart_fra_topp=1), så skal den nye farten bli fart_fra_venstre=-1 og fart_fra_topp=-1. Vi snur sauen 180 grader ved å gange fart_fra_venstre og fart_fra_topp med -1 (dette reverserer retningen). Hvis du er usikker på hvorfor dette fungerer, er det ikke viktig å forstå dette nå. Etter å ha jobbet mer med spillbrettet senere vil man få mer forståelse for koordinater og retninger.
  7. En metode tegn som tar et parameter skjerm som representerer skjermen vi skal tegne sauen på når spillet kjører. Ikke implementer denne metoden nå, bare bruk pass eller return slik at programmet ikke krasjer hvis metoden blir kalt.

Det kan være lurt å tegne et spillbrett på et ark for å gjøre ting enklere å forstå.

Oppgave 2: Test Sau

Filnavn: test.py

Lag en ny fil test.py hvor du importerer klassen Sau: from sau import Sau.

Lag en prosedyre test_sau() hvor du gjør følgende (der metode ikke er spesifisert, prøv å tenke selv hvilke metoder du må kalle):

Kall prosedyren test_sau() nederst i filen test.py for å kjøre testene.

Oppgave 3: Spillbrett

Filnavn: spillbrett.py

Vi kommer til å ha en del ulike objekter på spillbrettet vårt, og ønsker derfor å ha en klasse Spillbrett for å holde orden på objektene.

I første omgang består spillbrettet kun av en mengde sauer (sau-objekter), men vi kommer senere til å utvide denne klassen.

I neste oppgave skal vi implementere metoden tegn til klassen Sau.

Oppgave 4: Vis sauen på spillbrettet

Vi ønsker nå å bruke et veldig enkelt spillrammeverk PyGame Zero til å visualisere sauen vår på spillbrettet. Vi skal bruke lite funksjonalitet fra Pygame Zero, og du trenger ikke å sette deg inn i Pygame Zero. Målet er bare å visualisere objektene våre. Følg disse stegene:

  1. Installer Pygame Zero hvis du ikke har gjort det enda.

  2. Lage en fil hovedprogram.py som inneholder følgende kode:

    from spillbrett import Spillbrett
    
    # Her lager vi et nytt spillbrett og oppretter to sauer med ulike bilder og ulike start-posisjoner
    spillbrett = Spillbrett()
    spillbrett.opprett_sau("sau", 100, 100)
    spillbrett.opprett_sau("sau2", 400, 400)
    
    
    # Dette er prekode som gjør at pygame zero fungerer. Ikke endre dette:
    WIDTH = 900
    HEIGHT = 700
    def draw():
        screen.fill((128, 81, 9))
        spillbrett.tegn(screen)
    
    def update():
        spillbrett.oppdater()
    
    

    I koden over importerer vi Spillbrett klassen, lager et Spillbrett-objekt og oppretter to sauer. Resten av koden er kun der for å fortelle PyGame Zero at den skal tegne et rektangel og kalle spillbrettet sin tegn-metode hver gang den skal tegne noe, og kalle spillbrettet sin oppdater-metode hver gang noe skal oppdateres (som i praksis skjer mange ganger i sekundet).

  3. For å få sauene våre til å vises på spillbrettet må vi tegne bildet til sauen på skjermen. Lag en metode tegn til klassen sau som tar skjerm som parameter. skjerm er et objekt i Pygame Zero som har ulike metoder for interagere med bildet som vises på skjermen. Vi skal kalle metoden blit som lar oss tegne et bilde på skjermen:

    def tegn(self, skjerm):
        skjerm.blit(self._bilde, (self._posisjon_venstre, self._posisjon_topp))

Dette er alt som trengs for å få Pygame Zero til å vise et spillbrett og visualisere sauene på dette brettet. Vi ønsker nå å kjøre hovedprogram.py, men i stedet for å kalle main.py ved å kjøre python3 hovedprogram.py vil vi kjøre hovedprogram.py gjennoom Pygame Zero. Programmet vil fortsatt kjøres som et python-program, men får noen ekstra egenskaper som gjør at vi får vist spillet på skjermen.

Du gjør dette ved å kjøre pgzrun hovedprogram.py i terminalen. Hvis alt virker som det skal bør du se noe slikt:

screenshot

Hvis du vil kan du før du går videre leke deg litt ved å for eksempel sette en annen fart på sauene og se hva som skjer.

Oppgave 5: Bedre bevegelse av sauene

Nå går sauene bare i en helt rett linje og forsvinner ut av skjermen. Vi ønsker å gjøre noen forbedringer.

Sjekk kollisjon mot kanten av brettet

Utvid beveg-metoden til sauen slik at sauen først beveger seg og deretter sjekker om den er utenfor bildet. Sauen er 50 px bred og 50 px høy, og bildet er 900 px bredt og 700 px høyt. Hvis sauen er utenfor bildet, snu sauen ved å kalle snu-metoden sånn at at den går tilbake der den kom fra.

La sauen bevege seg mer tilfeldig rundt

Importer randint ved å legge til from random import randint øverst i filen. Vi ønsker å gi sauen en mer tilfeldig bevegelse rundt på skjermen.

Utvid beveg-metoden slik at sauen av og til (basert på tilfeldige tall) endrer retning og/eller fart. Bruk fantasien, vær gjerne kreativ og prøv deg fram med ulike måter å trekke tilfeldige tall på og la de tallene påvirke bevegelsen til sauen. Prøv å ende opp med noe som gjør at sauen får en noenlunde "naturlig" bevegelse. Husk at beveg-metoden kalles mange ganger i sekundet, så bevegelsen kan bli veldig hakkete om sauen endrer bevegelse ofte.

Her er et eksempel på bruk av tilfeldige tall for å få sauen til å gå rett til venstre med 0.5% sannsynlighet:

if randint(1, 1000) <= 5:
    self.sett_fart(-1, 0)

Her er eksempel på noen sauer som beveger seg tilfeldig rundt:

screenshot

Oppgave 6: Flere objekter (ulv, gress, stein)

Lag en klasse Gress i en fil gress.py:

Gjenta punktene over for Ulv og Stein (Ulv-klassen skal ligge i en fil ulv.py og Stein-klassen i stein.py). Ulv har bildet "ulv" og stein har bildet "stein". Lag et par ulv- og stein-objekter på samme måte som med gress, og sjekk at de vises på brettet.

Du trenger ikke å implementere noen andre metoder i disse klassene nå. I neste oppgave skal vi få ulvene til å bevege seg.

Oppgave 8: Gi Ulvene bevegelse

Til nå har ulvene stått stille, men vi ønsker å gi dem litt bevegelse. Gjøre enten oppgave a eller b under. Oppgave b er mer krevende, og gir ikke noe mer poeng på obligen.

a)

Implementer at ulvene beveger seg tilfeldig rundt på samme måte som sauene, altså ved å implementere en beveg-metode i klassen Ulv og kall den metoden i oppdater-metoden til Spillbrett.

b)

Implementer smartere bevegelse til ulvene ved at hver ulv beveger seg mot den sauen som er nærmest. For å få til dette ønsker vi at ulver skal ha tilgang til spillbrettet slik at de kan se hva som er på brettet og bevege seg etter det.

def test_finn_naermeste_sau():
    brett = Spillbrett()
    brett.opprett_sau("sau", 0, 0)
    brett.opprett_sau("sau", 100, 100)
    ulv = brett.opprett_ulv("ulv", 90, 80)
    naermeste_sau = ulv.finn_naermeste_sau()
    print(naermeste_sau.hent_posisjon_venstre(), naermeste_sau.hent_posisjon_topp())  # Det bør printes 100, 100, ettersom denne sauen er nærmest ulven

OBS: For testingen vår sin skyld, ønsker vi å ha tilgang til ulven som blir opprettet når vi kaller opprett_ulv på spillbrettet. Derfor er det greit å utvide opprett_ulv til å returnere ulven som blir laget. Da fungerer ulv = brett.opprett_ulv("Ulv", 90, 80) som tenkt i koden over.

Når du har fått finn_narmeste_sau til å fungere, kan du implementere beveg-metoden. Ulven skal bevege seg slik:

Oppgave 9: Sjekk kollisjoner

Vi ønsker snart å implementere at ulver spiser sauer de kommer over, og at sauer spise gress de kommer over.

Vi trenger da en måte for å sjekke om to objekter har kollidert (om de overlapper på skjermen). Vi har nå fire forskjellig type objekter (sau, stein, gress og ulv), og alle har en posisjon og størrelse (alle er 50px brede og høye). For å slippe å implementere en metode i hver av disse klassene for å sjekke kollisjon mot et annet objekt, kan vi i stedet lage en funksjon som tar to objekter og sjekker om de har kollidert.

Lag en funksjon (ikke en klasse-metode) har_kollidert(objekt1, objekt2) i filen spillbrett.py. Denne funksjonen tar to ulike objekter som parametere, og den trenger ikke å vite hvilke objekter det er, så lenge de har metodene hent_posisjon_venstre og hent_posisjon_topp. Implementer funksjonen slik at den returnerer True hvis to objekter overlapper og False hvis ikke (se reglene lenger nede om du sliter med dette).

Lag en prosedyre test_har_kollidert i test.py (husk å importere har_kollidert øverst i filen: from spillbrett import har_kollidert). I denne prosedyren kan du enkelt teste om du har implementert har_kollidert riktig, og det er mye enklere å finne feil slik enn å kjøre hele spillet. Her er noen eksempler på test-caser. Implementer 2 til test-caser på samme måte:

def test_har_kollidert():
    # Test-case 1: Disse to objektene har kollidert, fordi ulven ligger delvis oppå sauen
    sau = Sau("sau", 50, 50)
    ulv = Ulv("ulv", 60, 60)
    assert har_kollidert(sau, ulv)
    # Rekkefølgen skal ikke ha noe å si
    assert har_kollidert(ulv, sau)
    
    # Test-case 2: Disse to objektene ligger rett ved siden av hverandre 
    # og har ikke kollidert (husk at de er 50px brede/høye):
    gress = Gress("gress", 100, 100)
    sau = Sau("sau", 150, 150)
    assert not har_kollidert(gress, sau)
    
    # Implementer to test-caser til her: 
    # ...

Hvis du sliter med å implementere har_kollidert kan du prøve å følge disse reglene: En kollisjon skjer hvis:

Disse regelen fungerer fordi vi antar at alle objektene er 50 pixler brede/høye.

Oppgave 10: La ulvene spise sauer og sauene spise gress

Utvid klassen Sau slik at den har en instansvariabel _er_spist, en metode blir_spistog en metode er_spist (som fungerer akkurat likt som i klassen Gress).

Endre tegn-metoden i klassen spillbrett slik at den bare tegner sauer og gress som ikke er spist.

Utvid oppdater-metoden i klassen Spillbrett:

Legg gjerne til mer gress, flere steiner og flere sauer/ulver for å teste dette.

Her er et eksempel på hvordan det kan se ut når du har implementert alt:

screeenshot

Ekstra valgfrie utvidelser

I neste innlevering skal vi prøve å gjøre sauene og ulvene mye smartere. Målet for sauene er å overleve lengst mulig og spise mest mulig gress før de blir spist av en ulv. Det kommer til å være en konkurranse der målet er å lage de smarteste sauene, og det vil bli en turnering der sauer etter tur konkurrerer om høyest mulig score på samme baner. Sauene vil bli testet på et utvalg av (hemmelige) baner og målet er å overleve lengst og spise mest mulig gress.

Legg til en variabel _score i klassen Sau. Hver gang en Sau spiser gress, skal scoren øke med 1. Print scoren til terminalen når en Sau blir spist av en Ulv.

Utvid init-metoden til Sau slik at spillbrettet også sendes inn (på samme måte som i Ulv). Prøv å forbedre bevegelsen til sauen slik at den forsøker å se hvor det er ulver og beveger seg trygt i forhold til det. Dette skal vi se mer på i neste innlevering.

PS: Merk at det nå ikke er noen begrensning på farten til sauen. Man kan altså lage en sau som er veldig flink til å unngå å bli spist ved å bare la den bevege seg veldig fort. I neste innlevering kommer vi til å ha begresninger på hvor fort ulver og sauer kan bevege seg.

Krav til innlevering

Hvordan levere oppgaven

Kommenter på følgende spørsmål i kommentarfeltet i Devilry. Spørsmålene skal besvares.

For å levere:

  1. Logg inn på Devilry.
  2. Lever alle .py-filene , og husk å svare på spørsmålene i kommentarfeltet.
  3. Husk å trykke lever/add delivery og sjekk deretter at innleveringen din er komplett. Du kan levere flere ganger, men alle filer må være med i hver innlevering.
  4. Den obligatoriske innleveringen er minimum av hva du bør ha programmert i løpet av en uke. Du finner flere oppgaver for denne uken på semestersiden.