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
- Sørg for å ha installert . Om du har Python med PiP, kan det installered ved å kjøre
python3 -m pip install pgzero
i terminalen. Ta kontakt så tidlig som mulig om du har problemer med å installere.- Test at du kan kjøre kommandoen
pgzrun
i terminalen og at du får følgende output:
pygame 1.9.6 Hello from the pygame community. https://www.pygame.org/contribute.html Usage: pgzrun [options] pgzrun: error: You must specify which module to run.
- Test at du kan kjøre kommandoen
- Lag en mappe
images
i mappen du skriver denne innleveringen. I mappenimages
legger du følgende bilder: sau.png, sau2.png, ulv.png, gress.png, stein.png. Om du senere vil bytte ut bildene med dine egne bilder, kan du gjøre det, men pass på at de er like store som originalbildene.
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:
posisjon_venstre
: Antall pixler sauen befinner seg fra venstre side av skjermen)posisjon_topp
: Antall pixler sauen befinner seg fra toppen av skjermen)bilde
: Navnet på et bilde (Pygame Zero vil automatisk lete etter bilder i mappen images, så strengen "sau" vil gi bildet "images/sau.png")
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:
- En metode
sett_posisjon
som setter posisjonen (self._posisjon_venstre
ogself._posisjon_topp
) til en ny posisjon. Denne metoden må altså ta to parametere (i tillegg tilself
). - En metode
hent_posisjon_venstre
og en metodehent_posisjon_topp
som returnerer de to posisjonene. - En metode
hent_fra_fra_venstre
og en metodehent_fart_fra_topp
som returnerer farten (fra venstre og fra toppen). - En metode
sett_fart
som setter farten (self._fart_fra_venstre
ogself._fart_fra_topp
) til en ny fart. - 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å skalself._posisjon_topp
endres ved atself._fart_fra_topp
plusses på, og tilsvarende forself._posisjon_venstre
. - 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 atfart_fra_venstre = 1
ogfart_fra_topp=1
), så skal den nye farten blifart_fra_venstre=-1
ogfart_fra_topp=-1
. Vi snur sauen 180 grader ved å gangefart_fra_venstre
ogfart_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. - En metode
tegn
som tar et parameterskjerm
som representerer skjermen vi skal tegne sauen på når spillet kjører. Ikke implementer denne metoden nå, bare brukpass
ellerreturn
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):
- Opprett et nytt sau-objekt med bildestrengen "sau" og posisjonen
50, 50
(altså 50 pixler fra venstre og 50 pixler fra toppen). - Sett deretter posisjonen til å være
0, 0
ved å kallesett_posisjon
- Sett farten til å være
10, 20
(dvs. 10 i venstre-til-høyre-retning og 20 i topp-til-bunn-retning) - Beveg sauen 2 ganger ved å kalle
beveg
to ganger - Sjekk at posisisjonen nå er
20, 40
(enten ved å printe posisjonen eller brukeassert
) - Snu retningen sauen beveger seg
- Sjekk at farten nå er
-10, -20
- Beveg sauen én gang
- Sjekk at posisjonen nå er
10, 20
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.
-
Lag en ny fil
spillbrett.py
med en klasseSpillbrett
som har en konstruktør. Konstruktøren tar ingen parametere nå, men initierer en instansvariabel_sauer
som peker på en tom liste. Denne listen skal holde på alle sauene på spillbrettet. -
Klassen skal ha en metode
opprett_sau(bilde, posisjon_venstre, posisjon_topp)
som lager en ny sau og legger den til i listen over sauer . Husk å importere Sau-klassen ved å hafrom sau import Sau
øverst ispillbrett.py
-
Lag en metode
oppdater
. Metoden tar ingen parametere. Denne metoden kommer til å bli kalt hver gang spillbrettet skal oppdateres (mange ganger i sekundet), og etter hvert vil denne metoden holde styr på en del objekter. Nå ønsker vi bare at denne metoden beveger alle sauene én gang. Dette gjør du ved å kalle beveg-metoden på alle sau-objektene på spillbrettet (ved hjelp av en for-løkke). -
Lag en metode
tegn
. Denne metoden kalles hver gang spillbrettet skal tegnes på skjermen (noe som skjer mange ganger i sekundet). Her ønsker vi å tegne alle sauene våre, noe vi gjør ved å kalle en tegn-metode på sau-objektet (denne metoden finnes ikke nå, men vi legger den til i neste oppgave):def tegn(self, skjerm): for sau in self._sauer: sau.tegn(skjerm)
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 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:
-
Installer hvis du ikke har gjort det enda.
-
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 sinoppdater
-metode hver gang noe skal oppdateres (som i praksis skjer mange ganger i sekundet). -
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 tarskjerm
som parameter.skjerm
er et objekt i Pygame Zero som har ulike metoder for interagere med bildet som vises på skjermen. Vi skal kalle metodenblit
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:
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:
Oppgave 6: Flere objekter (ulv, gress, stein)
Lag en klasse Gress i en fil gress.py
:
- Klassen skal ha samme signatur som Sau (dvs. at init-metoden skal ta de samme parameterene)
- I tillegg skal den ha en instansvariabel
_er_spist
som er satt til False - Lag en metode
blir_spist
som setter_er_spist
til True og en metodeer_spist()
som returnerer True hvis gresset er spist og False hvis ikke. - Importer Gress-klassen i spillbrett.py og main.py (på samme måte som du importerer Sau)
- Lag en metode
oppett_gress
i Spillbrett som oppretter et nytt gress-objekt og legger objektet til i en liste_gress
på samme måte som med sauene. - Utvid tegn-metoden i
Spillbrett
slik at den også kallertegn
på alle gress-objektene. Du trenger ikke å gjøre noe i oppdater-metoden, fordi gress skal ikke bevege på seg. - Endre hovedprogram.py slik at du oppretter to gress-objekter (bilde skal være "gress", velg posisjon selv).
- Sjekk at du ser de to gress-objektene på spillbrettet
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.
- Utvid Ulv-klassen slik at konstruktøren også tar et spillbrett som lagres i en instansvariabel.
Du må også endre
opprett_ulv
-metoden i Spillbrett slik at spillbrettet sendes med som det 4. parameteret. Det kan du gjøre ved å sende medself
, som peker på spillbrettet.- Alternativt kan du endre Ulv-klassen til å ta inn en liste over sauer og en liste over steiner, ettersom det er alt Ulven trenger å se. Fordelen er at Ulven da slipper å måtte hente ut sauer og steiner fra spillbrettet, men init-metoden vil få to ekstra parametere i stedet for ett. Du kan velge selv hvilken løsning du vil gå for.
- Lag en metode
finn_naermeste_sau
i Ulv-klassen. Denne metoden skal gå gjennom alle sauene i spillbrettet og finne og returnere den sauen som er nærmest ulven. For å få til dette må den gå gjennom alle sauene, regne ut avstanden fra sauen til Ulven og returnere den sauen som har kortest avstand. Her vil du måtte bruke Pytagaros-formelen (som vi alle har lært enn gang på skolen), men det er lov å spørre om hjelp her hvis du ikke får til å regne ut avstanden til nærmeste sau. - Lag en prosedyre
test_finn_naermeste_sau
i test.py der du oppretter et spillbrett, legger inn en Ulv og noen sauer og kallerfinn_naermeste_sau
på ulv-objektet. Print koordinatene til den sauen som returneres, og sjekk at det faktisk er det nærmeste sauen:
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:
- Først finner den nærmeste sau
- Hvis sauen sin venstre-koordinat er lavere enn ulven sin, endrer den fart fra venstre til -1. Hvis sauens venstre-koordinat er høyere enn ulvens venstre-koordinat, endrer den fart fra venstre til 1.
- Samme regel følges for bevegelse i topp-mot-bunn-retning
- Implementer også den samme sjekken som du har i Sau for å unngå at Ulven går utenfor brettet. Du trenger da også en snu-metode, som du kan kopiere fra Sau-klassen.
- Husk at beveg-metoden må kalles fra
oppdater
i Spillbrett på samme måte som med sauer.
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:
- et objekt sin venstre-posisjon er større enn det andre objektets venstre-posisjon minus 50 pixler og lavere enn det andre objektets venstre-posisjon pluss 50 pixler ...
- ... og objektet sin topp-posisjon er større enn det andre objekts topp-posisjon minus 50 pixler og lavere enn det andre objekts topp-posisjon pluss 50 pixler
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_spist
og 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
:
- For hver ulv, gå gjennom alle sauene som ikke er spist og sjekk om ulven overlapper med den sauen. Hvis den gjør det, kall
blir_spist
på sau-objektet. - For hver sau som ikke er spist, gå gjennom alle gress, og gjør tilsvarende.
- For hver sau, sjekk om sauen krasjer med en stein. Hvis den gjør det, kall snu-metoden slik at sauen går tilbake der den kom ifra. Sauer skal ikke kunne gå på stein, men Ulver skal det (enn så lenge).
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:
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
- Kun .py-filene skal leveres inn (du trenger ikke å levere mappen "images")
- Spørsmålene til innleveringen (nedenfor) skal besvares i kommentarfeltet.
- Koden skal inneholde gode kommentarer som forklarer hva programmet gjør.
- Programmet skal inneholde gode utskriftssetninger som gjør det enkelt for bruker å forstå.
Hvordan levere oppgaven
Kommenter på følgende spørsmål i kommentarfeltet i Devilry. Spørsmålene skal besvares.
- Hvordan synes du innleveringen var? Hva var enkelt og hva var vanskelig?
- Hvor lang tid (ca) brukte du på innleveringen?
- Var det noen oppgaver du ikke fikk til? Hvis ja: i. Hvilke(n) oppave(r) er det som ikke fungerer i innleveringen? ii. Hvorfor tror du at oppgaven ikke fungerer? iii. Hva ville du gjort for å få oppgaven til å fungere hvis du hadde mer tid?
For å levere:
- Logg inn på Devilry.
- Lever alle .py-filene , og husk å svare på spørsmålene i kommentarfeltet.
- 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.
- 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.