Was du hier lernst
- Klassen und Objekte
- Level als Textdateien lesen
- Listen als Warteschlange
- Kollisionserkennung mit in
- Konstanten als Rückgabewerte
- Spielschleife mit Takt
So funktioniert Snake
Snake besteht aus erstaunlich wenigen Teilen: einem Spielfeld mit Mauern, Äpfeln und einem Ausgang, einer Schlange, die sich Feld für Feld bewegt, und einer Schleife, die beides immer wieder zeichnet. Das Programm teilt die Aufgaben auf zwei Klassen auf: Playground kennt das Spielfeld, Snake kennt die Bewegung.
1. Ein Level ist eine Textdatei
Statt Mauern und Äpfel im Code festzulegen, liest das Programm das Level aus einer Datei. Jedes Zeichen steht für ein Feld:
HHHHHHHHHHHHHHHHHHHH
H A A H
H S HHHHH H
H S E H
HHHHHHHHHHHHHHHHHHHH
H ist eine Mauer (Hindernis), A ein Apfel, S ein Stück Schlange und E der Ausgang. loadLevel geht die Datei Zeile für Zeile und Zeichen für Zeichen durch und sammelt die Positionen als Tupel (x, y) in Listen:
for zeile in datei:
x = -1
y += 1
for zeichen in zeile:
if zeichen != '\n':
x += 1
if zeichen == 'A':
self.applePositions.append((x, y))
Der große Vorteil: Neue Level entstehen in jedem Texteditor, ohne eine Zeile Python zu ändern. Das Spielfeld übernimmt Breite und Höhe einfach aus der Datei.
2. Die Schlange ist eine Liste
Die Schlange ist nichts weiter als eine Liste von Feldern, der Kopf steht vorn. Bewegen heißt: vorn ein neues Feld einfügen, hinten eines entfernen.
self.playground.snakePosition.insert(0, newHeadPosition)
if self.expansion == 0:
self.playground.snakePosition.pop()
else:
self.expansion -= 1
Frisst die Schlange einen Apfel, erhöht sich expansion um 4. Solange der Zähler größer als 0 ist, fällt das Entfernen am Ende weg – die Schlange wächst also über die nächsten vier Schritte.
3. Kollisionen mit in
Bevor die Schlange weiterzieht, prüft collisionDetection, was auf dem Zielfeld liegt. Pythons in fragt, ob ein Tupel in einer Liste vorkommt:
if newHeadPosition in self.barPositions:
print("bar collision")
return Snake.COLLISION
Die Antwort ist eine von vier Konstanten der Klasse Snake: NOCOLLISION, COLLISION, EATAPPLE oder PASSEXIT. Solche benannten Werte machen den Code lesbarer als nackte Zahlen. Die print-Aufrufe landen in der Konsole unter dem Spielfeld – dort siehst du bei jedem Apfel »apple collision«.
Warum die Schlange sich selbst beißen kann
Die erste Prüfung schaut, ob der neue Kopf auf einem Feld der Schlange landet. Weil das Programm den Kopf erst danach einfügt und das Schwanzende entfernt, zählt auch das Feld mit, das im selben Schritt frei würde. Ein kleines Detail, das das Spiel etwas strenger macht als manche andere Snake-Version.
4. Steuerung über eine Warteschlange
Jeder Pfeiltastendruck landet als Richtung in der Liste commands. Bei jedem Schritt nimmt das Programm den ältesten Befehl heraus – commands.pop(0) – und prüft ihn:
if not ((abs(new_x) == abs(old_x)) and (abs(new_y) == abs(old_y))):
validCommand = True
Die Bedingung verwirft Befehle auf derselben Achse. Wer nach rechts fährt, kann nicht direkt nach links umdrehen, das wäre sofort tödlich. Die Warteschlange hat noch einen Vorteil: Drückst du schnell »hoch« und »links« hintereinander, geht keiner der beiden Befehle verloren – sie werden in zwei Schritten nacheinander ausgeführt.
5. Ein Takt für das Spiel
Die Hauptschleife läuft 20-mal pro Sekunde, die Schlange soll aber gemächlicher ziehen. Dafür zählt gameCycle von 0 bis 3 und springt dann zurück:
gameCycle += 1
gameCycle %= 4
if gameCycle == 0:
...
Nur bei 0 bewegt sich die Schlange – also fünfmal pro Sekunde. Tastendrücke werden trotzdem bei jedem Durchlauf eingesammelt, damit sich das Spiel direkt anfühlt.
6. Game Over und das nächste Level
Stößt die Schlange an, zeigt showASCIIArt den Schriftzug aus GameOver.txt – dieselbe Idee wie beim Level: Ein X in der Textdatei wird zu einem roten Kästchen. Hat die Schlange alle Äpfel gefressen, erscheint der Ausgang in Blau. Wer hindurchfährt, sieht »You won« und landet im nächsten Level.
Schneller mit Mengen
in muss bei einer Liste im schlimmsten Fall jedes Element ansehen. Bei ein paar hundert Mauerfeldern ist das kein Problem. Für riesige Level wäre eine Menge (set) besser: Sie beantwortet in sofort, egal wie groß sie ist.
Probier mal
Jede Variante ändert den Code oben. Ein Klick auf »Ausprobieren« übernimmt die Änderung in den Editor und startet das Programm. »Zurücksetzen« holt das Original zurück.
-
01Doppeltes Tempo
Die Schlange bewegt sich nur bei jedem vierten Durchlauf der Hauptschleife. Mit jedem zweiten wird sie doppelt so schnell – und das Spiel deutlich schwieriger.
- gameCycle %= 4 + gameCycle %= 2 -
02Gefräßige Schlange
Jeder Apfel lässt die Schlange um vier Felder wachsen. Mit zehn Feldern pro Apfel wird es im zweiten Level richtig eng.
- self.expansion += 4 + self.expansion += 10 -
03Dein eigenes Level
Ein Level ist nur Text:
Hist eine Mauer,Aein Apfel,Sdie Schlange,Eder Ausgang. Diese Variante schreibt vor dem Start ein eigenes, kleines Level in die Dateilevel1.txt. Zeichne es um – das Spielfeld passt seine Größe automatisch an.- playground.loadLevel("level1.txt") - screen = Screen( + myLevel = """HHHHHHHHHHHHHHHHHHHHHHHHHHHHHH + H H + H A HH A H + H HH H + H S HHHHHHHHHHHH H + H S HH A H + H S H + H A HHHH E H + H H + HHHHHHHHHHHHHHHHHHHHHHHHHHHHHH""" + with open("level1.txt", "w") as datei: + datei.write(myLevel) + playground.loadLevel("level1.txt") + screen = Screen( -
04Retro-Handy
Graugrüner Hintergrund, dunkle Pixel: So sah Snake auf den ersten Handys aus. Es ändern sich nur die Farben beim Zeichnen.
- background="black", title="Snake" + background=(155, 188, 15), title="Snake" - drawElement(playground.barPositions, (128, 128, 128)) + drawElement(playground.barPositions, (48, 98, 48)) - drawElement(playground.applePositions, (255, 0, 0)) + drawElement(playground.applePositions, (139, 60, 20)) - drawElement(playground.snakePosition, (0, 255, 0)) + drawElement(playground.snakePosition, (15, 56, 15))
Vom Buch in den Browser
Im Buch verteilt sich Snake auf drei Dateien: snake.py mit der Klasse Snake, playground.py mit Playground und snakeGame_v3.py mit der Hauptschleife. Hier stehen alle drei in einer Datei, damit du alles in einem Editor hast. Die Klassen sind unverändert, die Level- und ASCII-Art-Dateien liegen im virtuellen Dateisystem des Browsers und werden wie im Original mit open() gelesen.
| Im Buch (pygame + OpenGL) | Hier (c4f) |
|---|---|
Viereck aus vier glVertex3f, Feldgröße 10 |
screen.rect(x * 16, y * 16, 15, 15, farbe) |
glColor3f(0.5, 0.5, 0.5) |
Farbe (128, 128, 128) |
pygame.event.poll() / K_RIGHT usw. |
screen.events() / K_RIGHT usw. aus c4f |
pygame.time.delay(50) |
await screen.frame(20) |
pygame.time.delay(3000) in showASCIIArt |
await sleep(3) |
pygame.event.clear() |
screen.events() (holt alte Ereignisse ab) |
Weil showASCIIArt jetzt mit await sleep(...) wartet, sind auch showGameOver und showYouWon async-Funktionen und werden mit await aufgerufen. Das ist die einzige strukturelle Änderung.
Python 3: Aus print self.width, " -> ", self.height wird print(self.width, " -> ", self.height). Die Dateien werden nach dem Lesen geschlossen.
Zwei kleine Fehler aus dem Original sind behoben – beide fallen erst nach dem letzten Level auf:
- In
restartGame()fehlteglobal currentLevel. Die ZuweisungcurrentLevel = 1erzeugte deshalb nur eine lokale Variable, der Levelzähler lief nach einem Game Over weiter. - Nach Level 2 wollte das Spiel
level3.txtladen, die es nicht gibt, und brach mit einem Fehler ab. Jetzt prüftloadNextLevelmitos.path.exists, ob die Datei existiert, und beginnt sonst wieder mit Level 1.
Neu sind außerdem die Touch-Tasten unter dem Spielfeld (screen.controls), damit Snake auch auf dem Handy spielbar ist.
Original aus dem Buch ansehen snakeGame_v3.py · Python 2
'''
Created on 07.11.2009
@author: Lars Heppert
'''
from OpenGL.GL import *
from OpenGL.GLU import *
import pygame, math, datetime
from pygame.locals import *
from coding4fun.kapitel04.snake import Snake
from coding4fun.kapitel04.playground import Playground
gameCycle = 0
currentLevel = 1
direction = (0, 0)
commands = []
playground = Playground()
snake = Snake(playground)
def resize((width, height)):
if height == 0:
height = 1
glViewport(0, 0, width, height)
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
glOrtho(-10.0, playground.width * 10.0 + 20.0, playground.height * 10.0 + 20.0, -10.0, -6.0, 0.0)
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
def init():
glClearColor(0.0, 0.0, 0.0, 0.0)
def clearScreen():
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
glLoadIdentity()
glTranslatef(0.0, 0.0, 3.0)
def drawElement(element, color):
r, g, b = color
glColor3f(r, g, b)
glBegin(GL_QUADS)
for part in element:
x, y = part
x = x * 10.0
y = y * 10.0
glVertex3f(x, y, 0.0)
glVertex3f(9.0 + x, y, 0.0)
glVertex3f(9.0 + x, 9.0 + y, 0.0)
glVertex3f(x, 9.0 + y, 0.0)
glEnd()
def handleEvent(event):
if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE):
return False
global commands
if event.type == KEYDOWN:
if event.key == K_RIGHT:
commands.append(( +1, 0))
if event.key == K_LEFT:
commands.append(( -1, 0))
if event.key == K_UP:
commands.append(( 0, -1))
if event.key == K_DOWN:
commands.append(( 0, +1))
return True
def showASCIIArt(fileName, delay):
datei = open(fileName, "r")
asciiArt = []
y = -1
for zeile in datei:
x = -1
y += 1
for zeichen in zeile:
if zeichen != '\n':
x += 1
if zeichen == 'X':
asciiArt.append((x, y))
clearScreen()
drawElement(asciiArt, (1.0, 0.0, 0.0))
pygame.display.flip()
pygame.time.delay(delay)
def showGameOver():
showASCIIArt("GameOver.txt", 3000)
restartGame()
def showYouWon():
global currentLevel
currentLevel += 1
showASCIIArt("YouWon.txt", 5000)
loadNextLevel(currentLevel)
def resetGameState():
# reinitialize the game (restart)
global direction, commands, playground, snake
playground = Playground()
snake = Snake(playground)
direction = (0, 0)
commands = []
pygame.event.clear()
def loadNextLevel(nextLevel):
resetGameState()
playground.loadLevel("level%d.txt" % nextLevel)
def restartGame():
currentLevel = 1
loadNextLevel(currentLevel)
def main():
pygame.init()
video_flags = OPENGL | HWSURFACE | DOUBLEBUF
playground.loadLevel("level1.txt")
screenSize = (playground.width * 20, playground.height * 20)
pygame.display.set_mode(screenSize, video_flags)
resize(screenSize)
init()
while True:
if not handleEvent(pygame.event.poll()):
break
global gameCycle
gameCycle += 1
gameCycle %= 4
if gameCycle == 0:
global direction
if commands:
validCommand = False
while not validCommand and commands:
new_x, new_y = commands.pop(0)
old_x, old_y = direction
if not ((abs(new_x) == abs(old_x)) and (abs(new_y) == abs(old_y))):
validCommand = True
if validCommand:
direction = (new_x, new_y)
if direction != (0, 0):
collisionEvent = snake.move(direction)
if collisionEvent == Snake.COLLISION:
showGameOver()
if collisionEvent == Snake.PASSEXIT:
if not playground.applePositions:
showYouWon()
clearScreen()
drawElement(playground.barPositions, (0.5, 0.5, 0.5))
drawElement(playground.applePositions, (1.0, 0.0, 0.0))
drawElement(playground.snakePosition, (0.0, 1.0, 0.0))
if not playground.applePositions:
drawElement(playground.exitPosition, (0.0, 0.0, 1.0))
pygame.display.flip()
pygame.time.delay(50)
if __name__ == '__main__':
main()
Original aus dem Buch ansehen snake.py · Python 2
'''
Created on 17.11.2009
@author: Lars Heppert
'''
class Snake(object):
'''
represents a snake in our snake game
'''
NOCOLLISION = 0
COLLISION = -1
EATAPPLE = -2
PASSEXIT = -3
def __init__(self, playground):
self.playground = playground
self.expansion = 0
def move(self, direction):
x, y = self.playground.snakePosition[0]
dx, dy = direction
newHeadPosition = (x + dx, y + dy)
collisionEvent = self.playground.collisionDetection(newHeadPosition)
if collisionEvent == Snake.EATAPPLE:
self.expansion += 4
self.playground.snakePosition.insert(0, newHeadPosition)
if self.expansion == 0:
self.playground.snakePosition.pop()
else:
self.expansion -= 1
return collisionEvent
Original aus dem Buch ansehen playground.py · Python 2
'''
Created on 17.11.2009
@author: sourcer
'''
from coding4fun.kapitel04.snake import Snake
class Playground(object):
'''
represents the playground in our snake game
'''
def __init__(self):
self.width = 0
self.height = 0
self.applePositions = []
self.barPositions = []
self.exitPosition = []
self.snakePosition = []
def loadLevel(self, fileName):
datei = open(fileName, "r")
y = -1
for zeile in datei:
x = -1
y += 1
for zeichen in zeile:
if zeichen != '\n':
x += 1
if zeichen == 'A':
self.applePositions.append((x, y))
elif zeichen == 'S':
self.snakePosition.append((x, y))
elif zeichen == 's':
self.snakePosition.insert(0, (x, y))
elif zeichen == 'H':
self.barPositions.append((x,y))
elif zeichen == 'E':
self.exitPosition.append((x,y))
self.width = x
self.height = y
print self.width, " -> ", self.height
def collisionDetection(self, newHeadPosition):
if newHeadPosition in self.snakePosition:
print "self collision"
return Snake.COLLISION
if newHeadPosition in self.barPositions:
print "bar collision"
return Snake.COLLISION
if newHeadPosition in self.applePositions:
print "apple collision"
self.applePositions.remove(newHeadPosition)
return Snake.EATAPPLE
if newHeadPosition in self.exitPosition:
print "exit collision"
return Snake.PASSEXIT
return Snake.NOCOLLISION