Lab8: Snake
Forberedelser
- Les kursnotater om interaktiv grafikk. Det kan være greit å ha kursnotatene grafikk 1 og grafikk 2 i bakhodet også.
Innlevering og retting
- Innlevering skjer via mitt.uib.
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å laben bestått, må du levere et fullt ut funksjonelt spill. Laben rettes manuelt av gruppeleder etter innleveringsfristen.
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 lab8-kanalen på Discord også.
- Steg 0: kom i gang
- Steg 1: tegn et rutenett
- Steg 2: tegn en modell
- Steg 3: slå av og på debug-modus
- Steg 4: flytt på slangen
- Steg 5: endre retning
- Steg 6: spis epler og bli stor
- Steg 7: game over
- Steg 8: gå automatisk
- Steg 9: idéer til frivillige forbedringer
Steg 0: kom i gang
Opprett en fil uke_08_snake.py, og ta utgangspunkt i koden under. Husk å legge en kopi av uib_inf100_graphics.py i samme mappe.
from uib_inf100_graphics import *
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.
...
run_app(width=500, height=400, title="Snake")
Steg 1: tegn et rutenett
I dette steget skal vi skrive en funksjon som tegner et rutenett
Opprett en funksjon draw_board(canvas, x1, y1, x2, y2, board, debug_mode)
med følgende parametere:
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 tall som representerer et brettet vi skal tegne. Vi kan anta at alle radene har like mange kolonner.debug_mode
, en boolsk verdi (True eller False) som indikerer hvorvidt vi er i debug-modus eller ikke.
Ta gjerne utgangspunkt i koden din for å tegne et rutenett fra lab7. Den vil trenge noen små justeringer:
Her er et løsningsforslag til oppgave 1 i lab7.
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_x1 = x1 + col * cell_width
cell_y1 = y1 + row * cell_height
cell_x2 = cell_x1 + cell_width
cell_y2 = cell_y1 + cell_height
canvas.create_rectangle(cell_x1, cell_y1, cell_x2, cell_y2,
fill=color_grid[row][col])
- I stedet for at rutenettet som blir gitt som input inneholder strenger som representerer farger, så skal rutenettet nå inneholde tall - hver rute inneholder et heltall.
- 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.
- En mulighet for å oppnå dette er å opprette en hjelpefunksjon
get_color(value)
som regner ut fargen basert på reglene over (bruk if-setninger). 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.
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.
- Dersom
debug_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.
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 1 for eksempel på bruk). 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_x1 + cell_x2) / 2
). Gjør det samme for y-koordinatet, slik at teksten blir sentrert midt i ruten
Test deg selv: bytt ut redraw_all -funksjonen med denne. Du skal da se bildet vist under når du kjører programmet.
def redraw_all(app, canvas):
board = [
[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],
]
draw_board(canvas, 25, 25, app.width-25, app.height-25, board, True)
Dersom du endrer argumentet True
til False
i funksjonskallet, skal samme bilde tegnes, men uten teksten i rutene.
Steg 2: tegn en modell
I dette steget skal vi opprette en modell for snake-spillet vårt, og få visningen til å tegne denne modellen.
Vi vil representere et spill med snake som 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 med variabler som befinner seg i app
-objektet. I forrige steg testet vi draw_board ved å kalle funksjonen med en 2D-liste board
som ble opprettet direkte i redraw_all. Vi skal nå endre dette til at argumentene til draw_board i stedet bli hentet fra app
.
- 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
app_started
, opprett en variabelapp.debug_mode
og initialiser den med verdienTrue
.I funksjonen
redraw_all
, gjør et kall tildraw_board
medapp.board
ogapp.debug_mode
som argumenter til parametrene board og debug_mode.- For koordinat-parametrene kan du bruke samme verdier som test-koden fra steg 1.
- Dersom du fremdeles har test-koden liggende fra steg 1, fjern variabelen board som var opprettet for test-formål, og modifiser kallet til draw_board slik at argumentene blir app.board og app.debug_mode istedet for board og True.
Når du er ferdig med dette steget, skal følgende vises når du kjører programmet:
Steg 3: slå av og på debug-modus
I dette steget skal vi la spilleren slå av og på debug-modus ved å trykke på d
på tastaturet.
- I funksjonen
key_pressed
, legg til følgende oppførsel:- Dersom
event.key
er lik"d"
, endre variabelenapp.debug_mode
til motsatt verdi av det den var fra før.
- Dersom
For å endre en boolsk variabel til det motsatte av det den er, kan du bruke not
-operatoren. Sjekk ut eksempelet:
x = True
x = not x
print(x)
Når du er ferdig med dette steget kan du slå av og på debug-informasjon ved å trykke på d
på tastaturet.
Steg 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.
Steg 4a: utvidelse av modellen
Først må vi utvide modellen vår slik at den inneholder informasjon om hvilken retning slangen skal gå i, slangens lengde, samt informasjon om slagehodet sin posisjon. I funksjonen app_started
:
- 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
- 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.direction
og initialiser den med strengen"east"
. Denne verdien indikerer at slangen sitt hode skal bevege seg mot høyre når brukeren trykker på mellomromstasten.
Når vi er i debug-modus, er det fint om vi kan se verdiene til disse variablene.
- I
redraw_all
, legg til følgende oppførsel: hvis app.debug_mode er aktivt, benytt create_text -metoden på lerretet for å skrive ut f-strengenf'{app.head_pos=} {app.snake_size=} {app.direction=}'
øverst i skjermbildet. 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(app)
for å utføre flyttingen. Denne funksjonen skal utføre følgende:
- Regn ut hva som er neste posisjon for slangen sitt hode. Det er klokt å skille dette ut som en egen hjelpefunksjon
get_next_head_position(head_row, head_col, direction)
som returnerer en tuple med rad og kolonne for neste posisjon. Parameteren direction er en streng som er enten"north"
,"south"
,"east"
eller"west"
.- I move_snake, gjør et kall til denne hjelpefunksjonen med app.head_pos[0], app.head_pos[1] 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 å kopiere denne testen inn i uke_08_snake.py (legg testen inn før kallet til run_app, men etter at funksjonen get_next_head_position er definert)
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")
Benytt if-setninger og sjekk hva direction
er. Legg til eller trekk fra 1
på row eller col alt ettersom. Se «test deg selv underveis» over.
- For alle positive verdier i
app.board
, trekk fra 1. Det er klokt å skille også dette ut som en egen hjelpefunksjonsubtract_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 å kopiere denne testen inn i uke_08_snake.py (legg testen inn før kallet til run_app, men etter at funksjonen subtract_one_from_all_positives er definert)
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)
print("OK")
- Oppdater
app.board
slik at posisjonen til slangen sitt hode får verdi lik slangen sin størrelse (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
.
Til slutt gjenstår det å gjøre et kall til move_snake når brukeren trykker på mellomrom
- I
key_pressed
, legg til følgende oppførsel:- Dersom
event.key
er lik"Space"
, utfør et funksjonskall tilmove_snake
.
- Dersom
Steg 5: endre retning
I dette steget skal vi gjøre det mulig å endre retningen slangen beveger seg ved at spilleren trykker på piltastene på tastaturet.
- I
key_pressed
, legg til følgende oppførsel:- Dersom
event.key
er lik"Up"
, endre variabelenapp.direction
til å ha verdien"north"
. - På samme måte skal
"Down"
endre til"south"
,"Left"
til"west"
og"Right"
til"east"
.
- Dersom
Du skal nå kunne endre retning slangehodet beveger seg ved hjelp av piltastene. Selve flyttingen krever fremdeles at spilleren trykker på mellomromstasten.
Steg 6: 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.
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.
- 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 å kopiere denne testen inn i uke_08_snake.py (legg testen inn før kallet til run_app, men etter at funksjonen add_apple_at_random_location er definert)
print("Tester add_apple_at_random_location...", end="")
for _ in range(100):
a = [[2, 3, -1, 0], [1, 0, 0, 0]]
add_apple_at_random_location(a)
legal_results = [
[[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]],
]
assert(a in legal_results)
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.
Steg 7: 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 debug-utskriften i redraw_all.Opprett en funksjon
is_legal_move(row, col, board)
som returnererTrue
dersom både posisjonen(row, col)
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).
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))
assert(not is_legal_move(1, 3, board)) # Utenfor brettet
assert(not is_legal_move(1, 1, board)) # Krasjer med seg selv
assert(not is_legal_move(0, 2, board)) # Krasjer med seg selv
assert(is_legal_move(0, 0, board))
assert(is_legal_move(3, 0, board)) # Eplets posisjon er lovlig
assert(is_legal_move(3, 2, board))
assert(not is_legal_move(-1, 0, board)) # Utenfor brettet
assert(not is_legal_move(0, -1, board)) # Utenfor brettet
assert(not is_legal_move(3, -1, board)) # Utenfor brettet
assert(not is_legal_move(3, 3, board)) # Utenfor brettet
assert(not is_legal_move(4, 2, board)) # 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 frad
for debug-modus, 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.
Steg 8: gå automatisk
I funksjonen timer_fired
, legg til følgende funksjonalitet:
- Dersom både
app.debug_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 debug-modus.
Vi ber om at du i koden som leveres inn har debug-modus 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
.
Steg 9: idéer til frivillige forberdringer
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 debug-modus.
La spilleren starte på nytt ved å trykke
"Return"
i game-over -bildet.Sett spillet på pause ved å trykke på
p
(lignende debug-modus, men uten å vise debug-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"
.Modus med to epler om gangen.
To spillere på samme maskin: La wasd styre den ene slangen og og la piltastene styre den andre (debug-tasten må flyttes til f. eks.
v
(for “verbose”)).- 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.
Din egen idé.
Guide til snake av Torstein Strømme er lisensiert under CC-NC-SA 4.0.