Lab5: Snake
I denne lab’en skal vi lage et spill hvor spilleren styrer en sulten python-slange på jakt etter epler. Vi kan kalle spillet for «Snake». For å få full pott i hovedfeltet, må du levere et fullt ut funksjonelt spill. Laben rettes manuelt av gruppeleder etter innleveringsfristen. Hovedfeltet rettes etter følgende kategorier:
- 20 poeng: programmet fungerer uten åpenbare feil
- 10 poeng: programmet fungerer, men har bugs
- 5 poeng: det er gjort en god innsats, men programmet er ikke funksjonelt
- 0 poeng: god innsats er ikke sannsynligggjort i innlevert materiale
Det er i tillegg mulig å få opp til 5 poeng i ekstrafeltet. Dette forutsetter at hovedfeltet er gjennomført.
Vi ønsker at du tar utganspunkt i denne guiden, og spillet du leverer kan ikke være skrevet i andre rammeverk enn uib_inf100_graphics. Det er viktig at du starter tidlig og ber om hjelp dersom du blir sittende fast. Samarbeid gjerne med andre (men skriv opp hvem du har jobbet med). Om noe er uklart, spør gjerne i lab5-kanalen på Discord også.
Forberedelser.
- Gjør lab pre5.
Kursnotater for tema som er nye i denne laben:
Det er nok nyttig å friske opp på disse også:
Guide til snake av Torstein Strømme er lisensiert under CC-NC-SA 4.0.
0: Kom i gang
Opprett en fil snake_main.py, og kopier inn koden under. Vi skal bruke denne filen som utgangspunkt for å lage applikasjonen.
def app_started(app):
# Modellen.
# Denne funksjonen kalles én gang ved programmets oppstart.
# Her skal vi __opprette__ variabler i som behøves i app.
...
def timer_fired(app):
# En kontroller.
# Denne funksjonen kalles ca 10 ganger per sekund som standard.
# Funksjonen kan __endre på__ eksisterende variabler i app.
...
def key_pressed(app, event):
# En kontroller.
# Denne funksjonen kalles hver gang brukeren trykker på tastaturet.
# Funksjonen kan __endre på__ eksisterende variabler i app.
...
def redraw_all(app, canvas):
# Visningen.
# Denne funksjonen tegner vinduet. Funksjonen kalles hver gang
# modellen har endret seg, eller vinduet har forandret størrelse.
# Funksjonen kan __lese__ variabler fra app, men har ikke lov til
# å endre på dem.
...
if __name__ == '__main__':
from uib_inf100_graphics.event_app import run_app
run_app(width=500, height=400, title='Snake')
Når du kjører programmet, skal det vises et tomt vindu med tittelen Snake.
Guide til snake av Torstein Strømme er lisensiert under CC-NC-SA 4.0.
1: Retningsendring
En viktig del av Snake innebærer at slangen beveger seg. Vi trenger derfor å vite hvilken retning slangen beveger seg i, og brukeren må kunne endre denne retningen ved å trykke på piltastene. I dette steget skal vi legge til denne funksjonaliteten.
I tillegg skal vi legge til en funksjon som gjør det mulig å slå av og på vårt eget informasjonsmodus ved å trykke på i
på tastaturet. I informasjonsmodus er tanken at vi viser en mengde ekstra informasjon om spillets tilstand som er nyttig under selve utviklingen av spillet.
- I funksjonen app_started: opprett og initier en variabel i modellen for slangens bevegelsesretning og en variabel som indikerer om vi er i informasjonsmodus eller ikke:
- Opprett en variabel
app.direction
og gi den verdien'east'
. - Opprett en variabel
app.info_mode
og gi den verdienTrue
.
- Opprett en variabel
- I funksjonen redraw_all, legg til følgende oppførsel:
- Hvis
app.info_mode
er True, tegn en tekst øverst på skjermen som viser hvilken retning slangen skal gå (hvis ikke, skal ingen tekst tegnes).
- Hvis
- Benytt
create_text
-metoden (se grafikk-notatene for eksempel på bruk). - Du kan benytte en f-streng (se notater om strenger) for å konvertere
app.direction
til en streng:
f'{app.direction=}'
- I funksjonen key_pressed, legg til følgende oppførsel:
- Dersom
event.key
er lik'i'
, endre variabelenapp.info_mode
til motsatt verdi av det den var fra før. - Dersom
event.key
er lik'Up'
, endre variabelenapp.direction
til å ha verdien'north'
. Tilsvarende skal'Down'
endre til'south'
,'Left'
til'west'
og'Right'
til'east'
.
- Dersom
Når du er ferdig, skal du kunne slå av og på informasjon ved å trykke på i
på tastaturet. Du skal også kunne endre på teksten som vises til north, south, east og west ved hjelp av piltastene.
Guide til snake av Torstein Strømme er lisensiert under CC-NC-SA 4.0.
2: Tegn et rutenett
I dette steget skal vi skrive en funksjon som tegner et rutenett. Denne funksjonen kan vi opprette i en egen fil snake_view.py som ligger i samme mappe som snake_main.py. Senere skal vi importere og bruke funksjonen i hovedprogrammet.
- I en egen fil snake_view.py, opprett en funksjon
draw_board
med følgende parametre:canvas
, lerretet vi skal tegne på.x1
ogy1
, koordinatene for punktet til venstre øverst for rutenettet som skal tegnes.x2
ogy2
, koordinatene for punktet til høyre nederst for rutenettet som skal tegnes.board
, en 2D-liste med heltall som representerer brettet vi skal tegne. Vi kan anta at alle radene har like mange kolonner.info_mode
, en boolsk verdi (True eller False) som indikerer hvorvidt vi er i informasjonsmodus eller ikke.
- Når du tegner rutenettet, la fargen til rektanglene være
'lightgray'
dersom tallet i ruten er 0,'orange'
dersom tallet er større enn 0, og'cyan'
dersom tallet er mindre enn 0 (du står fritt til å velge dine egne farger også, bare du bruker tre forskjellige farger.) - Dersom
info_mode
er True: skriv ut en tekst i hver rute som inneholder rutens posisjon (rad, kolonne) og tallverdien som finnes på denne posisjonen i board.
Ta gjerne utgangspunkt i koden din for å tegne et rutenett fra lab4.
Her er et løsningsforslag til rutenett fra lab4.
def draw_grid(canvas, x1, y1, x2, y2, color_grid):
rows = len(color_grid)
cols = len(color_grid[0])
cell_width = (x2 - x1) / cols
cell_height = (y2 - y1) / rows
for row in range(rows):
for col in range(cols):
cell_left = x1 + col * cell_width
cell_top = y1 + row * cell_height
cell_right = cell_left + cell_width
cell_bottom = cell_top + cell_height
color = color_grid[row][col]
canvas.create_rectangle(
cell_left, cell_top, cell_right, cell_bottom,
fill=color
)
Det er lurt å benytte en egen funksjon som «oversetter» fra tallverdi til fargeverdi (en slik kort funksjon kaller vi for en «hjelpefunksjon», siden den bare har ansvar for en liten del av en større helhet). Ved å bruke en hjelpefunksjon holder du fargevalget separat fra resten av koden som tegner rutenettet. Dette gjør det enklere å endre fargevalget senere.
- Opprett en hjelpefunksjon
get_color(value)
som regner ut fargen basert på reglene over (bruk en if-setning). For eksempel skalget_color(-1)
returnere strengen «cyan»,get_color(0)
skal returnere strengen «lightgray», ogget_color(1)
skal returnere strengen «orange». Du kan teste en slik hjelpefunksjon med koden under om du ønsker. PS, hvis du bestemmer deg for å endre fargene, må du også endre fargene i testen. - Hjelpefunksjonen kan ligge i snake_view.py ved siden av draw_board-funksjonen.
def test_get_color():
print('Tester get_color...', end='')
assert 'cyan' == get_color(-1)
assert 'lightgray' == get_color(0)
assert 'orange' == get_color(1)
assert 'orange' == get_color(42)
print('OK')
-
I
draw_board
-funksjonen kan vi kalle påget_color
-funksjonen når vi trenger å regne ut en farge. Opprett en variabelcolor
inne i de nøstede for-løkkene, og gi den en verdi ved å kalle påget_color
-funksjonen medboard[row][col]
som argument. Bruk så variabelencolor
som argument for fill-parameteren til create_rectangle når en rute tegnes.
I denne laben vil du etter hvert ha lyst til å teste flere hjelpefunksjoner. Det kan fort bli rotete om du blander alle testene med selve programkoden. Derfor er det vanlig å ha testene i en egen fil.
- Opprett filen snake_tests.py og legg den i samme mappe som snake_main.py og snake_view.py. Kopiér inn koden under og studér hva den gjør:
# snake_tests.py
from snake_view import get_color
def run_all():
''' Run all tests for the snake program '''
# As you add more test functions to this file, call them here
test_get_color()
def test_get_color():
print('Tester get_color...', end='')
assert 'cyan' == get_color(-1)
assert 'lightgray' == get_color(0)
assert 'orange' == get_color(1)
assert 'orange' == get_color(42)
print('OK')
if __name__ == '__main__':
print('Starting snake_test.py')
run_all()
print('Finished snake_test.py')
Når du senere i utviklingen legger til flere tester, er det lurt å legge dem til her i denne filen. Legg selve test-funksjonene et sted i hovedprogrammet, og legg til et kall til funksjonen i run_all.
For eksempel, bruk f-strengen f'{row},{col}\n{board[row][col]}'
(her antas det at iterandene i de nøstede for-løkkene som tegner rutenettet kalles row
og col
)
-
Bruk
create_text
-metoden (se grafikk). La x-koordinatet du gir som input til create_text være midt mellom x-koordinatene du gir som input til create_rectangle (cell_mid_x = (cell_left_x + cell_right_x) / 2
). Gjør det samme for y-koordinatet, slik at teksten blir sentrert midt i ruten
Test deg selv: kopier denne koden inn nederst i snake_view.py og kjør.
if __name__ == '__main__':
from uib_inf100_graphics.simple import canvas, display
test_board = [
[1, 2, 3, 0, 5, 4,-1,-1, 1, 2, 3],
[0, 4, 0, 7, 0, 3,-1, 0, 0, 4, 0],
[0, 5, 0, 8, 1, 2,-1,-1, 0, 5, 0],
[0, 6, 0, 9, 0, 0, 0,-1, 0, 6, 0],
[0, 7, 0,10,11,12,-1,-1, 0, 7, 0],
]
draw_board(canvas, 25, 80, 375, 320, test_board, True)
display(canvas)
Du skal da se:
PS: Pass på at du kun importerer
uib_inf100_graphics.simple
inne iif __name__ == '__main__'
-blokken og ikke i selve hoveprogrammet i snake_view.py; ellers får du kluss når du senere skal importere draw_board -funksjonen i snake_main.py.
I forrige steg la vi til et eksempelprogram som benyttet seg av draw_board-funksjonen inne i if __name__ == '__main__'
-blokken i selve snake_view.py. Dette er helt greit som en sanity check underveis i utviklingen. For en grundigere sjekk, er det også mulig å ha flere eksempelprogrammer som bruker draw_board -funksjonen. Disse eksempelprogrammene kan være i andre filer.
I filen snake_view_extra_demo.py: kopier inn koden under og kjør den. Du skal da se sekvensen vist under når du kjører programmet. Denne sekvensen viser funksjonskall med ulike størrelser på brettene og ulike verdier for info_mode.
# snake_view_extra_demo.py
from uib_inf100_graphics.simple import canvas, display
from snake_view import draw_board
test_boards = [
[
[0,-1, 0],
[1, 2, 0]
],
[
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 9,10,11, 0,-1, 0],
[0, 0, 0, 8, 0, 0, 0, 0, 0],
[0, 0, 0, 7, 6, 5, 0, 0, 0],
[0, 0, 0, 0, 0, 4, 0, 0, 0],
[0, 0, 0, 1, 2, 3, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
]
]
info_mode = True
while True:
for test_board in test_boards:
draw_board(canvas, 25, 25, 375, 375, test_board, info_mode)
display(canvas, min_duration_sec=2)
info_mode = not info_mode
Guide til snake av Torstein Strømme er lisensiert under CC-NC-SA 4.0.
3: Rutenett i modellen
Vi flytter oss nå tilbake til snake_main.py. I dette steget skal vi bygge hovedelementet i modellen til snake-spillet vårt, nemlig et rutenett med heltall. Vi skal så få visningen til å tegne denne.
Vi representerer et spill med snake som et rutenett (en 2D-liste) med heltall, hvor tallet 0 betyr at en rute er tom, tallet -1 betyr at det er et eple på en gitt posisjon, mens et tall høyere enn 0 betyr at slangen befinner seg på dette området. Slangen sitt hode er på den posisjonen på brettet med det høyeste tallet, og resten av kroppen til slangen følger deretter med synkende tall.
Modellen består av en rekke variabler som befinner seg i app
-objektet. Vi har tidligere opprettet en variabel app.direction; nå skal vi opprette en variabel app.board som representerer selve spillbrettet.
- I funksjonen
app_started
, opprett en variabelapp.board
og initialiser den med verdien
[
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0,-1, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 2, 3, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
]
- I funksjonen
redraw_all
, gjør et kall tildraw_board
medapp.board
ogapp.info_mode
som argumenter til parametrene board og info_mode.- Du må importere draw_board -funksjonen fra snake_view -modulen for å få dette til å fungere.
- For parametrene x1, y1, x2 og y2, velg argumenter slik at rutenettet vises midt i vinduet med en margin på 25 piksler på alle sider til rammen
Variablene
app.width
ogapp.height
er definert for oss automatisk, og inneholder henholdsvis bredden og høyden til vinduet. Du kan bruke disse verdiene som utgangspunkt for å finne x2 og y2. Hvis du har gjort det riktig, vil størrelsen på rutenettet tilpasse seg automatisk når du endrer størrelsen på vinduet.
Når du er ferdig med dette steget, skal følgende vises når du kjører programmet (og trykker på ‘i’):
Guide til snake av Torstein Strømme er lisensiert under CC-NC-SA 4.0.
4: Flytt på slangen
I dette steget skal vi la slangen gå ett steg hver gang brukeren trykker på mellomromstasten. Senere skal vi få programmet til å flytte slangen av seg selv; men i utviklingsfasen er det fint å kunne jobbe i «sakte film» og da er det fint å bruke mellomromstasten til å kontrollere tiden.
Steg 4a: utvidelse av modellen
Først må vi utvide modellen vår slik at den inneholder informasjon om slangens lengde og slagehodet sin posisjon. I funksjonen app_started
:
- Opprett en variabel
app.snake_size
og initialiser den med verdien3
(dette er slangen sin initielle størrelse slik den er i app.board). - Opprett en variabel
app.head_pos
og initialiser den med verdien(3, 4)
(dette er den initielle posisjonen (rad, kolonne) til slangen sitt hode slik slangen ligger i app.board).
En tuple er en liste som ikke kan muteres. De aller fleste ikke-destruktive operasjoner på lister fungerer helt likt for tupler. Les også om tupler i kursnotater om lister.
# Opprett en tuple
pos = (3, 4)
print(pos) # (3, 4)
# "Pakk ut" en tuple i variabler
row, col = pos
print(f'{row} {col}') # 3 4
Når vi er i informasjonsmodus, er det fint om vi kan se verdiene til disse variablene.
- I
redraw_all
, endre strengen som skriver ut informasjon slik at den også inkluderer de nye variablene i app vi nettopp opprettet. Se bilder under.
Steg 4b: selve forflyttningen
før forflyttning | etter forflytning |
Nå kommer vi til selve forflyttningen; bildene over viser tilstanden før og etter en forflytning i retning «east». Vi oppretter en egen funksjon move_snake
med en parameter app
for å utføre flyttingen. I første omgang ønsker vi at denne funksjonen kalles hver gang vi trykker på mellomromstasten (senere skal denne flyttingen foregår periodisk av seg selv).
- I
key_pressed
: hvisevent.key
er lik'Space'
, utfør et funksjonskall tilmove_snake
medapp
som argument.
Funksjonen move_snake
skal utføre følgende:
- Oppdater
app.head_pos
: finn hva som er neste posisjon for slangen sitt hode. - Oppdater
app.board
: for alle positive verdier, trekk fra 1. - Oppdater
app.board
: la den nye posisjonen til slangen sitt hode få verdi lik slangen sin størrelse
Opprett egnede hjelpefunksjoner for de to første stegene. Du må kalle på disse hjelpefunksjonene i move_snake. Det tredje steget er såpass kort at du kan utføre det direkte i move_snake uten egen hjelpemetode.
Det er klokt å skille dette ut som en egen hjelpefunksjon get_next_head_position(head_pos, direction)
som returnerer en tuple med rad og kolonne for neste posisjon.
- I move_snake, gjør et kall til denne hjelpefunksjonen med app.head_pos og app.direction som argumenter.
- La retur-verdien fra dette funksjonskallet bli den nye verdien til app.head_pos.
I før/etter -bildene over ser vi at verdien
app.head_pos
endres fra (3, 4) til (3, 5). Den endringen gjør vi her.
Test hjelpefunksjonen din ved å legge denne testen til i snake_tests.py:
def test_get_next_head_position():
print('Tester get_next_head_position...', end='')
assert (3, 9) == get_next_head_position((3, 8), 'east')
assert (3, 7) == get_next_head_position((3, 8), 'west')
assert (2, 8) == get_next_head_position((3, 8), 'north')
assert (4, 8) == get_next_head_position((3, 8), 'south')
assert (1, 6) == get_next_head_position((1, 5), 'east')
assert (1, 4) == get_next_head_position((1, 5), 'west')
assert (0, 5) == get_next_head_position((1, 5), 'north')
assert (2, 5) == get_next_head_position((1, 5), 'south')
print('OK')
- Husk at en tuple (akkurat som en liste) kan «pakkes opp» til individuelle variabler (se kursnotater om lister). For eksempel:
foo = (3, 4) # foo er en tuple med to elementer
x, y = foo # nå er x lik 3 og y lik 4
- Benytt if-setninger og sjekk hva
direction
er. Legg til eller trekk fra1
på row eller col alt ettersom. Se «test deg selv underveis» over.
Det er klokt å skille også dette ut som en egen hjelpefunksjon subtract_one_from_all_positives(grid)
som muterer en 2D-liste med tall gitt som input.
- I move_snake, gjør et kall til denne hjelpefunksjonen med app.board som argument.
I før/etter -bildene over ser vi at verdien i rute (3, 2) endres fra 1 til 0, verdien i rute (3, 3) endres fra 2 til 1 og verdien i rute (3, 4) endres fra 3 til 2. Disse endringene gjør vi her.
PS: Vi sier at en verdi er positiv dersom den er strengt større enn 0; altså er 0 ikke en positiv verdi.
Test hjelpefunksjonen din ved å legge denne testen til i snake_tests.py:
def test_subtract_one_from_all_positives():
print('Tester subtract_one_from_all_positives...', end='')
a = [[2, 3, 0], [1, -1, 2]]
subtract_one_from_all_positives(a)
assert [[1, 2, 0], [0, -1, 1]] == a
b = [[2, 0], [0, -1]]
subtract_one_from_all_positives(b)
assert [[1, 0], [0, -1]] == b
print('OK')
Husk at slangen sin størrelse befinner seg i variabelen app.snake_size
.
I før/etter -bildene over ser vi at verdien i ruten (3, 5) endres fra 0 til 3. Den endringen gjør vi her.
- Har du husket å oppdatere
app.head_pos
med returverdien fra kallet tilget_next_head_position
? - Slangehodet sin posisjon som rad og kolonne er
row, col = app.head_pos
. - Muter
app.board[row][col]
slik at verdien blirapp.snake_size
.
Du skal nå kunne flytte slangen ved å trykke på mellomrom, og du skal også kunne endre hvilken retning slangen flytter seg ved å benytte piltastene.
Guide til snake av Torstein Strømme er lisensiert under CC-NC-SA 4.0.
5: Spis epler og bli stor
I dette steget skal vi implementere funksjonalitet som gjør at slangen vokser når den spiser et eple.
før spising | etter spising |
I før/etter -bildene over, legg merke til at når slangen spiser:
- økes verdien
app.snake_size
fra 3 til 4,- posisjonen hvor hodet flytter til (1, 6) endrer verdi fra -1 til 4, som er den nye verdien til
app.snake_size
, og- ingen verdier tilhørende slangen (positive verdier) i
app.board
synker.I tillegg vil det opprettes et nytt eple et tilfeldig sted på brettet. Dette tilfeldige stedet skal ikke kunne være en del av selve slangen.
For å gjennomføre dette steget, må vi modifisere koden vi skrev i move_snake
tidligere:
- Etter at app.head_pos er oppdatert men før det er gjort noen endringer i app.board, sjekk om det er et eple på posisjonen hvor slangens hode skal flytte seg.
- Dersom det er et eple der (app.board har -1 i den gitte posisjonen): øk app.snake_size med 1, og opprett et nytt eple på et tilfeldig sted som er ledig.
- For å opprette et eple på et tilfeldig ledig sted, er det klokt å utføre dette i en hjelpefunksjon
add_apple_at_random_location(grid)
som muterer brettet, og så kalle på denne funksjonen med app.board som argument.
- For å opprette et eple på et tilfeldig ledig sted, er det klokt å utføre dette i en hjelpefunksjon
- Hvis det ikke er et eple der hodet skal flytte seg: utfør kallet til
subtract_one_from_all_positives
i stedet (som før).
- Dersom det er et eple der (app.board har -1 i den gitte posisjonen): øk app.snake_size med 1, og opprett et nytt eple på et tilfeldig sted som er ledig.
import random
# Et tilfeldig tall mellom 0 og 10 (ikke inkludert 10)
print(random.choice(range(10)))
# Et tilfeldig element i en liste
print(random.choice(['a', 'b', 'c']))
Test hjelpefunksjonen din ved å legge denne testen til i snake_tests.py:
def test_add_apple_at_random_location():
print('Tester add_apple_at_random_location...', end='')
NUMBER_OF_RUNS = 1000
legal_states = [
[[2, 3, -1, -1], [1, 0, 0, 0]],
[[2, 3, -1, 0], [1, -1, 0, 0]],
[[2, 3, -1, 0], [1, 0, -1, 0]],
[[2, 3, -1, 0], [1, 0, 0, -1]],
]
counters = [0] * len(legal_states)
for _ in range(NUMBER_OF_RUNS):
a = [[2, 3, -1, 0], [1, 0, 0, 0]]
add_apple_at_random_location(a)
assert a in legal_states
print('OK')
Legg merke til at funksjonen kun skal opprette epler på steder som er ledige, altså har verdien 0. Siden vi her tester en funksjon som produserer tilfeldige resultater, sjekker vi i assert-setningen at resultatet er ett av de fire lovlige resultatene for test-casen vår. Vi kjører også testen mange ganger slik at vi ikke bare passerer testen på grunn av flaks.
Det finnes i hovedsak to måter å løse add_apple_at_random_location
på. Velg selv hvilken strategi du ønsker å bruke.
Alternativ A. Denne tilnærmingen er som regel rask og effektiv så lenge slangen er relativt kort, men man har ingen øvre grense for hvor lang tid som kreves i verste fall.
- Velg en tilfeldig rad mellom 0 og antall rader i
grid
- Velg en tilfeldig kolonne mellom 0 og antall kolonner i
grid
- Sjekk om det er ledig plass (altså verdien 0) i
grid
på den gitte posisjonen:- Hvis ja, legg inn et eple (verdien -1) i posisjonen og avslutt så funksjonen med
return
- Hvis ikke, begynn på nytt (f. eks. ha all koden inn i en
while True
-løkke)
- Hvis ja, legg inn et eple (verdien -1) i posisjonen og avslutt så funksjonen med
Alternativ B. Denne tilnærmingen er ikke spesielt effektiv i gjennomsnitt, men har en øvre grense for hvor lang tid den tar. Dersom det ikke er plass til et nytt eple i det hele tatt vil denne algoritmen kunne avsløre det (f. eks. ved å krasje, eller håndtere det på annen måte), mens alternativ A aldri vil terminere, og programmet vil fryse (som tross alt er et dårligere alternativ).
- Opprett først en liste med alle posisjonene hvor det er mulig å opprette et eple.
- Velg en tilfeldig posisjon fra listen over muligheter.
- Legg inn et eple i den valgte posisjonen
For de ambisiøse: det er mulig å kombinere alternativene A og B ved å prøve alternativ A noen få iterasjoner først, og deretter hoppe over til alternativ B dersom ingen gode alternativer ble funnet. Da får vi det beste fra begge verdener.
Guide til snake av Torstein Strømme er lisensiert under CC-NC-SA 4.0.
6: Game over
Foreløpig krasjer spillet dersom slangen går utenfor brettet. Slangen er også i stand til å krysse seg selv. Vi vil at begge disse hendelsene fører oss inn i en «game over» -tilstand.
-
Opprett en variabel
app.state
i modellen, og initialiser variabelen til strengen'active'
. Inkluder variabelen i info-utskriften i redraw_all. -
Opprett en funksjon
is_legal_move(pos, board)
som returnererTrue
dersom både posisjonenpos
er innenfor brettets rammer og det også er lovlig for slangehodet å flytte seg til denne posisjonen uten at den krasjer med seg selv (det er ikke nødvendig å sjekke at posisjonen faktisk er ved siden av slangehodet). En posisjon er en tuple med to verdier (rad og kolonne).
Test hjelpefunksjonen din ved å legge denne testen til i snake_tests.py:
def test_is_legal_move()
print('Tester is_legal_move...', end='')
board = [
[0, 3, 4],
[0, 2, 5],
[0, 1, 0],
[-1, 0, 0],
]
assert is_legal_move((2, 2), board) is True
assert is_legal_move((1, 3), board) is False # Utenfor brettet
assert is_legal_move((1, 1), board) is False # Krasjer med seg selv
assert is_legal_move((0, 2), board) is False # Krasjer med seg selv
assert is_legal_move((0, 0), board) is True
assert is_legal_move((3, 0), board) is True # Eplets posisjon er lovlig
assert is_legal_move((3, 2), board) is True
assert is_legal_move((-1, 0), board) is False # Utenfor brettet
assert is_legal_move((0, -1), board) is False # Utenfor brettet
assert is_legal_move((3, -1), board) is False # Utenfor brettet
assert is_legal_move((3, 3), board) is False # Utenfor brettet
assert is_legal_move((4, 2), board) is False # Utenfor brettet
print('OK')
- Endre
move_snake
-funksjonen. Umiddelbart etter at slangehodets nye posisjon er regnet ut men før brettet oppdateres eller slangen vokser, sjekk om slangehodets nye posisjon er lovlig ved å gjøre et kall til is_legal_move.- Hvis den nye posisjonen ikke er lovlig (returverdien fra funksjonskallet er false), endre
app.state
til'gameover'
. Ikke gjør noe mer i move_snake-funksjonen om dette skjer (du kan avbryte resten av funskjonskallet ved å bruke enreturn
-setning).
- Hvis den nye posisjonen ikke er lovlig (returverdien fra funksjonskallet er false), endre
I key_pressed
, endre oppførselen slik at:
- hvis
app.state
er lik'active'
, fungerer tastetrykkene som før, men - hvis
app.state
er lik'gameover'
, har tastetrykk ingen betydning (bortsett frai
for informasjonsmodus, som skal virke uansett).
I redraw_all
, endre oppførselen slik at:
- hvis
app.state
er lik'active'
, tegnes brettet som før, men - hvis
app.state
er lik'gameover'
, skrives det kun ut'Game Over'
midt på lerretet.
Guide til snake av Torstein Strømme er lisensiert under CC-NC-SA 4.0.
7: Tiden går
I funksjonen timer_fired
, legg til følgende funksjonalitet:
- Dersom både
app.info_mode
er slått av ogapp.state
er'active'
utfør et funksjonskall tilmove_snake
.
Du skal nå kunne spille spillet i praksis ved å slå av informasjonsmodus.
Såfremt du ikke gjør oppgaver i steg 8 som gjør at du har en velkomst-skjerm, ber vi om at du i koden som leveres inn har informasjonsmodus slått på som standard, slik at det blir lettere for oss å rette.
For å øke hvor lang tid det går mellom hvert kall til
timer_fired
, kan du legge inn linjenapp.timer_delay = 200 # milliseconds
(eller en annen verdi) i funksjonenapp_started
.
Guide til snake av Torstein Strømme er lisensiert under CC-NC-SA 4.0.
8: Ekstrafeltet
I dette steget skal du gjøre egne forbedringer i spillet. Du kan få opp til 5 poeng på innleveringen ved å gjøre slike egne forbedringer. Gi en kort beskrivelse av alle forbedringene dine i filen ekstrafeltet.txt som legges ved innleveringen, slik at sensor ikke går glipp av noe. Sensor gir disse poengene som en skjønnsmessig vurdering. Her er noen eksempler:
- 1 poeng: du har en eller to ekstra småting, som for eksempel enkle visuelle endringer for å gjøre spillet penere, eller du gir spilleren muligheten for å starte spillet på nytt etter game over.
- 2 poeng: du har gjort flere småting som til sammen utgjør en betydelig forbedring.
- 3 poeng: du har en pen start-skjerm som vises før spillet starter, og kan starte spillet på nytt. Spillet «føles» ferdig, og vi trenger ikke å ha informasjonsmodus påslått i begynnelsen for å hindre at spillet starter før vi er klare.
- 4 poeng: spilleren kan velge ulike vanskelighetsgrader fra en pen start-skjerm, og brettet opprettes dynamisk.
- 5 poeng: du har virkelig gjort noe ekstra ut av oppgaven. For eksempel en startskjerm du kan navigere i med piltastene eller en highscore -liste hvor du oppgir navnet ditt når du har gjort det bra. Eller kanskje du har gjort spiller-opplevelsen ekstra smooth ved at du ikke kan krasje i deg selv ved å være for rask med fingrene. Eller kanskje du har gjort spesielt imponerende visuelle forbedringer. Uansett, wow!
De aller flotteste innleveringene blir nominert til slangespillprisen og får et fint diplom å henge på veggen eller legge ved CV’en. Skjermbilder av de nominerte vil også vises på hjemmesiden til emnet (se for eksempel forrige semester). Dersom du av en eller annen grunn ikke ønsker at vi bruker navnet ditt på oversikten over nominerte, oppgi dette i ekstrafeltet.txt.
Å gjøre egne forberdringer er det er det beste beviset for deg selv (og andre) på at du har forstått hvordan programmet fungerer.
Noen idéer:
-
Gjør brettet større.
-
Opprett brettet dynamisk basert på ønsket antall rader og ønsket antall kolonner, slik at det holder å endre på én variabel for å endre størrelsen på hele brettet (dette blir litt enklere hvis vi starter med en slange på størrelse
1
). -
Penere design, finere farger, bedre layout.
-
Deaktiver mellomroms-tasten hvis man ikke er i informasjonsmodus.
-
La spilleren starte på nytt ved å trykke
'Return'
i game-over -bildet. -
Sett spillet på pause ved å trykke på
p
(lignende informasjonsmodus, men uten å vise informasjon). -
Ha en velkomst-tilstand (i tillegg til ‘active’ og ‘gameover’) som forklarer reglene og hvilke taster du kan bruke.
-
gradvis og mørkere farger jo lengre bak på slangen man kommer.
-
Spiller kan velge ulike størrelser på brettet fra velkomst-skjermen ved å trykke f. eks.
'1'
,'2'
eller'3'
. Eller enda bedre, spilleren kan navigere i startmenyen med piltastene. -
Modus med to epler om gangen.
-
High score. Enda bedre: high score som lagres mellom ulike kjøringer (du kan f. eks. lagre i en tekstfil).
-
High score der tidsbruk er tie-breaker hvis to spillere ble like lange.
-
To spillere på samme maskin: La wasd styre den ene slangen og og la piltastene styre den andre.
- For å få ulike farger på de ulike slangene kan man lage et nytt brett app.last_visited_by_player i tillegg til app.board. Dette nye rutenettet har samme dimensjoner som app.board, og oppdateres med informasjon om hvilken slange som var her sist. Det er tilstrekkelig å oppdatere de rutene som tilsvarer hodene til slangene sine posisjoner.
- For å unngå tvetydighet om hvem som krasjer i hvem, kan vi la de to slangene flytte annenhver gang og heller la timer gå dobbelt så fort. (For å doble timer-hastigheten, legg til linjen
app.timer_delay = 50 # milliseconds
i funksjonenapp_started
.)
-
Modus med massevis av epler, men hvor slangens lengde krymper periodisk (raskere og raskere?) og det er om å gjøre å overleve flest mulig steg.
-
Legg til noen spesielle epler som raskt blir borte, men som halverer slangens lengde (eller reduserer hastigheten en liten stund) hvis de blir spist.
-
Din egen idé.
Guide til snake av Torstein Strømme er lisensiert under CC-NC-SA 4.0.