Aussehen
Eine Wiese simulieren
Wie viele Hasen können gemütlich auf einer Wiese leben, ohne zu verhungern? Wir könnten jetzt ins Zoogeschäft fahren und es praktisch ausprobieren, oder wir simulieren die Fragestellung einfach.
Für eine Simulation benötigen wir ein Modell unseres Problemfalls: Gras wächst, Hasen laufen darüber, fressen, bekommen Junge und werden auch einmal hungrig.
Simulation
Eine Simulation rechnet nach, wie sich ein System im Lauf der Zeit verhält. Sie arbeitet in Schritten: In jedem Schritt wenden alle Beteiligten ihre Regeln einmal an, dann ist die Welt einen Moment weiter.
Wo Zufall im Spiel ist, laufen zwei Simulationen mit denselben Regeln nie ganz gleich ab. Interessant ist deshalb nicht der einzelne Lauf, sondern das, was in vielen Läufen gleich bleibt.
Die Wiese ist ein Gitter aus Feldern, so wie die Welt von Kara. Auf jedem Feld wächst Gras oder nicht.
Die Regeln
Das Modell kennt nur Gras und Hasen und folgt diesen Regeln:
| Wer | Regel |
|---|---|
| Gras | Auf einem kahlen Feld wächst in jedem Schritt mit kleiner Wahrscheinlichkeit wieder Gras. |
| Hase | Er geht einen Schritt in eine zufällige Richtung. Das kostet 1 Energie. |
| Hase | Steht er auf Gras, frisst er es. Das Feld wird kahl, und er bekommt 6 Energie. |
| Hase | Hat er 24 Energie oder mehr, bekommt er ein Junges. Beide haben danach die Hälfte. |
| Hase | Hat er keine Energie mehr, scheidet er aus der Simulation aus. |
Was du brauchst
Das Modul Pygame Zero (pgzero). Wie man Module installiert, steht in der Projektübersicht. Ab Stufe 5 brauchst du die Klassen aus dem Objekte-Kapitel.
Das Projekt hat sieben Stufen und am Ende eine Liste mit Erweiterungen. Nach jeder Stufe läuft dein Programm. Ab Stufe 2 steht im Text nur, was sich ändert. Das ganze Programm der Stufe klappt darunter auf.
Das Bild vom Hasen
Ab Stufe 3 bekommt der Hase ein Bild. Rechtsklick auf das Bild → „Bild speichern unter", und zwar als hase.png in einen Ordner namens images direkt neben deinem Programm. Der Ordner muss genau so heißen, sonst findet Pygame Zero das Bild nicht.
Das Bild ist 14 × 14 Pixel groß, so groß wie ein Feld der Wiese. Es ist für dieses Buch gezeichnet worden. Du darfst es behalten, verändern und in deinen eigenen Programmen benutzen.
Stufe 1: Das Gitter
python
import random
import pgzrun
# Die Wiese hat 40 Spalten und 30 Zeilen. Jedes Feld ist 14 Pixel groß.
SPALTEN = 40
ZEILEN = 30
FELD = 14
# Pygame Zero erwartet diese beiden Namen.
WIDTH = SPALTEN * FELD
HEIGHT = ZEILEN * FELD
FARBE_ERDE = (150, 120, 85)
FARBE_GRAS = (96, 170, 80)
# Mit einer Zahl läuft jedes Mal dasselbe ab. Mit None entscheidet der Zufall neu.
STARTWERT = None
if STARTWERT is not None:
random.seed(STARTWERT)
# gras[y][x] ist True, wo Gras wächst.
gras = []
for y in range(ZEILEN):
zeile = []
for x in range(SPALTEN):
zeile.append(random.random() < 0.5)
gras.append(zeile)
def draw():
screen.fill(FARBE_ERDE)
for y in range(ZEILEN):
for x in range(SPALTEN):
if gras[y][x]:
screen.draw.filled_rect(Rect(x * FELD, y * FELD, FELD - 1, FELD - 1), FARBE_GRAS)
pgzrun.go()Das ist ein Gitter mit zufälligem Gras. Zwei Dinge sind neu.
gras ist eine Liste von Listen. Jede innere Liste ist eine Zeile der Wiese, und gras[y][x] ist das Feld in Zeile y und Spalte x. Erst die Zeile, dann die Spalte — so, wie man in einer Tabelle zuerst die Zeile sucht.
random.random() < 0.5 ergibt True oder False. random.random() liefert eine Zufallszahl von 0 bis knapp unter 1. Ist sie kleiner als 0.5, wird das Feld grün. Das passiert bei der Hälfte aller Felder.
Gezeichnet wird in zwei Schleifen. x * FELD rechnet die Spalte in Pixel um, und das FELD - 1 lässt zwischen den Feldern einen schmalen Streifen frei, damit man sie auseinanderhält.
Zufall, den man wiederholen kann
random.seed(7) setzt den Zufallsgenerator auf einen festen Startwert. Danach liefert er immer dieselbe Folge von Zahlen: Sie sieht zufällig aus, ist aber wiederholbar. So lassen sich Versuche wiederholen und vergleichen.
Test
Es muss sich ein braunes Fenster mit etwa halb so vielen grünen Feldern öffnen. Starte das Programm mehrmals: Jedes Mal sieht die Wiese anders aus.
Probier es aus
- Ersetze
0.5inrandom.random() < 0.5durch0.1und danach durch0.9. Was bedeutet die Zahl? - Setze
STARTWERT = 7und starte das Programm zweimal. DannSTARTWERT = 8. Was fällt dir auf? - Ändere
FELDauf10. Warum ändert sich auch die Größe des Fensters?
Stufe 2: Das Gras wächst
Jetzt bekommt die Wiese Zeit. Die Welt rechnet in Schritten, und ein Schritt soll nicht sechzigmal pro Sekunde laufen wie update() im Spiel, sonst wäre die Wiese in einer Sekunde zugewachsen.
Unter den Farben kommen zwei neue Zahlen dazu:
python
START_GRAS = 0.1 # So viele Felder sind am Anfang grün.
WACHSTUM = 0.02 # So wahrscheinlich wird ein kahles Feld pro Schritt grün.Beim Anlegen der Wiese ersetzt START_GRAS die 0.5:
python
gras = []
for y in range(ZEILEN):
zeile = []
for x in range(SPALTEN):
zeile.append(random.random() < START_GRAS)
gras.append(zeile)Vor draw() kommen zwei neue Funktionen:
python
def wachsen():
for y in range(ZEILEN):
for x in range(SPALTEN):
if not gras[y][x] and random.random() < WACHSTUM:
gras[y][x] = True
def schritt():
wachsen()Und vor pgzrun.go() steht:
python
# Zehnmal pro Sekunde ein Schritt.
clock.schedule_interval(schritt, 0.1)clock.schedule_interval(schritt, 0.1) sagt Pygame Zero: Rufe die Funktion schritt alle 0,1 Sekunden auf. Das sind zehn Schritte pro Sekunde. draw() zeichnet weiter nur und verändert nichts. schritt() ruft vorerst nur wachsen() auf.
Das ganze Programm dieser Stufe
python
import random
import pgzrun
# Die Wiese hat 40 Spalten und 30 Zeilen. Jedes Feld ist 14 Pixel groß.
SPALTEN = 40
ZEILEN = 30
FELD = 14
# Pygame Zero erwartet diese beiden Namen.
WIDTH = SPALTEN * FELD
HEIGHT = ZEILEN * FELD
FARBE_ERDE = (150, 120, 85)
FARBE_GRAS = (96, 170, 80)
START_GRAS = 0.1 # So viele Felder sind am Anfang grün.
WACHSTUM = 0.02 # So wahrscheinlich wird ein kahles Feld pro Schritt grün.
# Mit einer Zahl läuft jedes Mal dasselbe ab. Mit None entscheidet der Zufall neu.
STARTWERT = None
if STARTWERT is not None:
random.seed(STARTWERT)
# gras[y][x] ist True, wo Gras wächst.
gras = []
for y in range(ZEILEN):
zeile = []
for x in range(SPALTEN):
zeile.append(random.random() < START_GRAS)
gras.append(zeile)
def wachsen():
for y in range(ZEILEN):
for x in range(SPALTEN):
if not gras[y][x] and random.random() < WACHSTUM:
gras[y][x] = True
def schritt():
wachsen()
def draw():
screen.fill(FARBE_ERDE)
for y in range(ZEILEN):
for x in range(SPALTEN):
if gras[y][x]:
screen.draw.filled_rect(Rect(x * FELD, y * FELD, FELD - 1, FELD - 1), FARBE_GRAS)
# Zehnmal pro Sekunde ein Schritt.
clock.schedule_interval(schritt, 0.1)
pgzrun.go()Test
Am Anfang sind nur wenige Felder grün. Dann füllt sich die Wiese, bis fast alles grün ist.
Probier es aus
- Miss mit der Uhr, wie lange es dauert, bis fast die ganze Wiese grün ist.
- Setze
WACHSTUMauf0.2und danach auf0.002. Wie ändert sich die Zeit? - Setze
START_GRASauf0. Wächst trotzdem etwas? Warum?
Stufe 3: Ein Hase läuft und frisst
Ab jetzt ist die Wiese bewohnt. Ein Hase ist erst einmal nur ein Feld, auf dem er steht: hase_x und hase_y.
Er bekommt vier mögliche Richtungen. START_GRAS steigt auf 0.5, damit er etwas zu fressen findet:
python
START_GRAS = 0.5 # So viele Felder sind am Anfang grün.
WACHSTUM = 0.02 # So wahrscheinlich wird ein kahles Feld pro Schritt grün.
# Die vier Richtungen als (dx, dy): rechts, links, runter, hoch.
RICHTUNGEN = [(1, 0), (-1, 0), (0, 1), (0, -1)]Nach dem Anlegen der Wiese bekommt er seinen Platz in der Mitte:
python
hase_x = SPALTEN // 2
hase_y = ZEILEN // 2schritt() lässt ihn laufen und fressen:
python
def schritt():
global hase_x, hase_y
wachsen()
dx, dy = random.choice(RICHTUNGEN)
# Das % lässt die Wiese an den Rändern herumlaufen: rechts raus, links wieder rein.
hase_x = (hase_x + dx) % SPALTEN
hase_y = (hase_y + dy) % ZEILEN
if gras[hase_y][hase_x]:
gras[hase_y][hase_x] = FalseAm Ende von draw() wird er gezeichnet:
python
screen.blit("hase", (hase_x * FELD, hase_y * FELD))screen.blit("hase", (x, y)) kopiert das Bild images/hase.png auf den Bildschirm, mit seiner oberen linken Ecke an die Stelle (x, y). Der Name ist der Dateiname ohne Ordner und Endung. Mit hase_x * FELD und hase_y * FELD liegt das Bild genau auf dem Feld des Hasen, wie das Rechteck beim Gras.
random.choice(RICHTUNGEN) zieht eine der vier Richtungen zufällig aus der Liste. Jede Richtung ist ein Paar (dx, dy): um wie viel sich x und y ändern. Mit dx, dy = ... bekommen beide Werte auf einmal einen Namen.
Das Prozentzeichen
a % b ist der Rest der Division von a durch b. 7 % 3 ist 1, und 40 % 40 ist 0.
Genau das braucht der Hase am Rand: Läuft er rechts aus der Spalte 39 hinaus, wird aus der 40 durch % SPALTEN wieder eine 0. Er kommt links herein. Die Wiese hat dadurch keinen Rand, sie ist wie eine Walze.
Das ganze Programm dieser Stufe
python
import random
import pgzrun
# Die Wiese hat 40 Spalten und 30 Zeilen. Jedes Feld ist 14 Pixel groß.
SPALTEN = 40
ZEILEN = 30
FELD = 14
# Pygame Zero erwartet diese beiden Namen.
WIDTH = SPALTEN * FELD
HEIGHT = ZEILEN * FELD
FARBE_ERDE = (150, 120, 85)
FARBE_GRAS = (96, 170, 80)
START_GRAS = 0.5 # So viele Felder sind am Anfang grün.
WACHSTUM = 0.02 # So wahrscheinlich wird ein kahles Feld pro Schritt grün.
# Die vier Richtungen als (dx, dy): rechts, links, runter, hoch.
RICHTUNGEN = [(1, 0), (-1, 0), (0, 1), (0, -1)]
# Mit einer Zahl läuft jedes Mal dasselbe ab. Mit None entscheidet der Zufall neu.
STARTWERT = None
if STARTWERT is not None:
random.seed(STARTWERT)
# gras[y][x] ist True, wo Gras wächst.
gras = []
for y in range(ZEILEN):
zeile = []
for x in range(SPALTEN):
zeile.append(random.random() < START_GRAS)
gras.append(zeile)
hase_x = SPALTEN // 2
hase_y = ZEILEN // 2
def wachsen():
for y in range(ZEILEN):
for x in range(SPALTEN):
if not gras[y][x] and random.random() < WACHSTUM:
gras[y][x] = True
def schritt():
global hase_x, hase_y
wachsen()
dx, dy = random.choice(RICHTUNGEN)
# Das % lässt die Wiese an den Rändern herumlaufen: rechts raus, links wieder rein.
hase_x = (hase_x + dx) % SPALTEN
hase_y = (hase_y + dy) % ZEILEN
if gras[hase_y][hase_x]:
gras[hase_y][hase_x] = False
def draw():
screen.fill(FARBE_ERDE)
for y in range(ZEILEN):
for x in range(SPALTEN):
if gras[y][x]:
screen.draw.filled_rect(Rect(x * FELD, y * FELD, FELD - 1, FELD - 1), FARBE_GRAS)
screen.blit("hase", (hase_x * FELD, hase_y * FELD))
# Zehnmal pro Sekunde ein Schritt.
clock.schedule_interval(schritt, 0.1)
pgzrun.go()Test
Ein Hase läuft zufällig über die Wiese, und hinter ihm werden die grünen Felder kahl. Kommt stattdessen eine Fehlermeldung mit 'hase', heißt der Ordner oder die Datei falsch, oder sie liegen an der falschen Stelle.
Probier es aus
- Lass dir in einem eigenen kleinen Programm
-1 % 40ausgeben. Was kommt heraus, und was bedeutet das für den Hasen am linken Rand? - Hänge an
RICHTUNGENein fünftes Paar(0, 0)an. Wie bewegt sich der Hase jetzt? - Setze
WACHSTUMauf0. Was bleibt nach einer Weile von der Wiese übrig?
Ein eigener Hase?
Ein eigenes PNG mit durchsichtigem Hintergrund, 14 × 14 Pixel groß, ersetzt den Hasen, wenn es als hase.png im images-Ordner liegt. Ein Frosch tut es auch.
Stufe 4: Energie
Ein Hase, der ewig läuft und nie hungrig wird, ist kein Hase. Jetzt bekommt er Energie.
Unter WACHSTUM kommen zwei neue Zahlen dazu:
python
START_ENERGIE = 12 # Damit kommt ein Hase ohne Futter 12 Schritte weit.
GRAS_ENERGIE = 6 # Ein Büschel Gras gibt so viel Energie.Der Hase merkt sich seine Energie und ob er noch lebt:
python
hase_x = SPALTEN // 2
hase_y = ZEILEN // 2
hase_energie = START_ENERGIE
hase_lebt = Trueschritt() wird länger:
python
def schritt():
global hase_x, hase_y, hase_energie, hase_lebt
wachsen()
if not hase_lebt:
return
dx, dy = random.choice(RICHTUNGEN)
# Das % lässt die Wiese an den Rändern herumlaufen: rechts raus, links wieder rein.
hase_x = (hase_x + dx) % SPALTEN
hase_y = (hase_y + dy) % ZEILEN
# Jeder Schritt kostet Energie.
hase_energie = hase_energie - 1
if gras[hase_y][hase_x]:
gras[hase_y][hase_x] = False
hase_energie = hase_energie + GRAS_ENERGIE
if hase_energie <= 0:
hase_lebt = False
print("Die Energie des Hasen ist aufgebraucht. Er scheidet aus der Simulation aus.")Am Ende von draw() wird der Hase nur noch gezeichnet, solange er lebt, und oben links steht seine Energie:
python
if hase_lebt:
screen.blit("hase", (hase_x * FELD, hase_y * FELD))
screen.draw.text("Energie: " + str(hase_energie), (6, 6), color="white")Solange hase_lebt False ist, tut schritt() für den Hasen nichts mehr: Das return beendet die Funktion früher. Das Gras wächst aber weiter, denn wachsen() steht davor.
Das ganze Programm dieser Stufe
python
import random
import pgzrun
# Die Wiese hat 40 Spalten und 30 Zeilen. Jedes Feld ist 14 Pixel groß.
SPALTEN = 40
ZEILEN = 30
FELD = 14
# Pygame Zero erwartet diese beiden Namen.
WIDTH = SPALTEN * FELD
HEIGHT = ZEILEN * FELD
FARBE_ERDE = (150, 120, 85)
FARBE_GRAS = (96, 170, 80)
START_GRAS = 0.5 # So viele Felder sind am Anfang grün.
WACHSTUM = 0.02 # So wahrscheinlich wird ein kahles Feld pro Schritt grün.
START_ENERGIE = 12 # Damit kommt ein Hase ohne Futter 12 Schritte weit.
GRAS_ENERGIE = 6 # Ein Büschel Gras gibt so viel Energie.
# Die vier Richtungen als (dx, dy): rechts, links, runter, hoch.
RICHTUNGEN = [(1, 0), (-1, 0), (0, 1), (0, -1)]
# Mit einer Zahl läuft jedes Mal dasselbe ab. Mit None entscheidet der Zufall neu.
STARTWERT = None
if STARTWERT is not None:
random.seed(STARTWERT)
# gras[y][x] ist True, wo Gras wächst.
gras = []
for y in range(ZEILEN):
zeile = []
for x in range(SPALTEN):
zeile.append(random.random() < START_GRAS)
gras.append(zeile)
hase_x = SPALTEN // 2
hase_y = ZEILEN // 2
hase_energie = START_ENERGIE
hase_lebt = True
def wachsen():
for y in range(ZEILEN):
for x in range(SPALTEN):
if not gras[y][x] and random.random() < WACHSTUM:
gras[y][x] = True
def schritt():
global hase_x, hase_y, hase_energie, hase_lebt
wachsen()
if not hase_lebt:
return
dx, dy = random.choice(RICHTUNGEN)
# Das % lässt die Wiese an den Rändern herumlaufen: rechts raus, links wieder rein.
hase_x = (hase_x + dx) % SPALTEN
hase_y = (hase_y + dy) % ZEILEN
# Jeder Schritt kostet Energie.
hase_energie = hase_energie - 1
if gras[hase_y][hase_x]:
gras[hase_y][hase_x] = False
hase_energie = hase_energie + GRAS_ENERGIE
if hase_energie <= 0:
hase_lebt = False
print("Die Energie des Hasen ist aufgebraucht. Er scheidet aus der Simulation aus.")
def draw():
screen.fill(FARBE_ERDE)
for y in range(ZEILEN):
for x in range(SPALTEN):
if gras[y][x]:
screen.draw.filled_rect(Rect(x * FELD, y * FELD, FELD - 1, FELD - 1), FARBE_GRAS)
if hase_lebt:
screen.blit("hase", (hase_x * FELD, hase_y * FELD))
screen.draw.text("Energie: " + str(hase_energie), (6, 6), color="white")
# Zehnmal pro Sekunde ein Schritt.
clock.schedule_interval(schritt, 0.1)
pgzrun.go()Test
Oben links steht eine Zahl, die sinkt, wenn der Hase auf kahlen Feldern geht, und steigt, wenn er Gras frisst.
Probier es aus
- Setze
START_GRASundWACHSTUMauf0. Nach wie vielen Schritten verhungert der Hase? Rechne es zuerst aus und prüfe es dann mit der Uhr. - Setze beides wieder zurück. Beobachte die Energie eine Minute lang. Ist es auf dieser Wiese schwer, satt zu werden?
Stufe 5: Viele Hasen
Ein Hase hat drei Werte: x, y und die Energie. Für zehn Hasen bräuchtest du dreißig Variablen. Das ist der Moment für eine Klasse: einmal den Bauplan aufschreiben und dann beliebig viele Hasen daraus machen. Wie das geht, weißt du aus dem Objekte-Kapitel.
Bei den Zahlen kommen START_HASEN und NACHWUCHS_ENERGIE dazu:
python
START_HASEN = 10 # So viele Hasen gibt es am Anfang.
START_ENERGIE = 12 # Damit kommt ein Hase ohne Futter 12 Schritte weit.
GRAS_ENERGIE = 6 # Ein Büschel Gras gibt so viel Energie.
NACHWUCHS_ENERGIE = 24 # Ab dieser Energie bekommt ein Hase ein Junges.Die vier Zeilen mit hase_ entfallen. An ihre Stelle kommen die Klasse und die Liste der Hasen:
python
class Hase:
def __init__(self, x, y, energie):
self.x = x
self.y = y
self.energie = energie
def laufen(self):
dx, dy = random.choice(RICHTUNGEN)
self.x = (self.x + dx) % SPALTEN
self.y = (self.y + dy) % ZEILEN
self.energie = self.energie - 1
def fressen(self):
if gras[self.y][self.x]:
gras[self.y][self.x] = False
self.energie = self.energie + GRAS_ENERGIE
def zeichnen(self):
screen.blit("hase", (self.x * FELD, self.y * FELD))
hasen = []
for i in range(START_HASEN):
hasen.append(Hase(random.randrange(SPALTEN), random.randrange(ZEILEN), START_ENERGIE))Nach wachsen() kommt eine Funktion, die das Gras zählt:
python
def gras_zaehlen():
summe = 0
for zeile in gras:
for gewachsen in zeile:
if gewachsen:
summe = summe + 1
return summeschritt() geht jetzt alle Hasen durch:
python
def schritt():
global hasen
wachsen()
# Die Liste hasen wird nicht verändert, während die Schleife sie durchläuft.
# Wer bleibt und wer neu dazukommt, sammeln wir in zwei neuen Listen.
lebende = []
junge = []
for hase in hasen:
hase.laufen()
hase.fressen()
if hase.energie >= NACHWUCHS_ENERGIE:
# Das Junge bekommt die Hälfte der Energie, die Mutter behält die andere Hälfte.
hase.energie = hase.energie // 2
junge.append(Hase(hase.x, hase.y, hase.energie))
if hase.energie > 0:
lebende.append(hase)
hasen = lebende + jungeAm Ende von draw() zeichnet jeder Hase sich selbst, und oben links stehen die beiden Zahlen:
python
for hase in hasen:
hase.zeichnen()
screen.draw.text("Gras: " + str(gras_zaehlen()) + " Hasen: " + str(len(hasen)), (6, 6), color="white")Alles, was vorher mit hase_x und hase_energie im Programm verstreut war, steckt jetzt in der Klasse Hase: Sie kann laufen, fressen und sich zeichnen. In schritt() steht nur noch, in welcher Reihenfolge das geschieht.
Vermehrung: hase.energie // 2 teilt die Energie ganzzahlig durch zwei. Mutter und Junges haben danach gleich viel.
Zwei neue Listen: lebende und junge. Die Liste hasen wird nicht verändert, während die Schleife sie durchläuft. Am Ende ersetzt lebende + junge sie.
Das ganze Programm dieser Stufe
python
import random
import pgzrun
# Die Wiese hat 40 Spalten und 30 Zeilen. Jedes Feld ist 14 Pixel groß.
SPALTEN = 40
ZEILEN = 30
FELD = 14
# Pygame Zero erwartet diese beiden Namen.
WIDTH = SPALTEN * FELD
HEIGHT = ZEILEN * FELD
FARBE_ERDE = (150, 120, 85)
FARBE_GRAS = (96, 170, 80)
START_GRAS = 0.5 # So viele Felder sind am Anfang grün.
WACHSTUM = 0.02 # So wahrscheinlich wird ein kahles Feld pro Schritt grün.
START_HASEN = 10 # So viele Hasen gibt es am Anfang.
START_ENERGIE = 12 # Damit kommt ein Hase ohne Futter 12 Schritte weit.
GRAS_ENERGIE = 6 # Ein Büschel Gras gibt so viel Energie.
NACHWUCHS_ENERGIE = 24 # Ab dieser Energie bekommt ein Hase ein Junges.
# Die vier Richtungen als (dx, dy): rechts, links, runter, hoch.
RICHTUNGEN = [(1, 0), (-1, 0), (0, 1), (0, -1)]
# Mit einer Zahl läuft jedes Mal dasselbe ab. Mit None entscheidet der Zufall neu.
STARTWERT = None
if STARTWERT is not None:
random.seed(STARTWERT)
# gras[y][x] ist True, wo Gras wächst.
gras = []
for y in range(ZEILEN):
zeile = []
for x in range(SPALTEN):
zeile.append(random.random() < START_GRAS)
gras.append(zeile)
class Hase:
def __init__(self, x, y, energie):
self.x = x
self.y = y
self.energie = energie
def laufen(self):
dx, dy = random.choice(RICHTUNGEN)
self.x = (self.x + dx) % SPALTEN
self.y = (self.y + dy) % ZEILEN
self.energie = self.energie - 1
def fressen(self):
if gras[self.y][self.x]:
gras[self.y][self.x] = False
self.energie = self.energie + GRAS_ENERGIE
def zeichnen(self):
screen.blit("hase", (self.x * FELD, self.y * FELD))
hasen = []
for i in range(START_HASEN):
hasen.append(Hase(random.randrange(SPALTEN), random.randrange(ZEILEN), START_ENERGIE))
def wachsen():
for y in range(ZEILEN):
for x in range(SPALTEN):
if not gras[y][x] and random.random() < WACHSTUM:
gras[y][x] = True
def gras_zaehlen():
summe = 0
for zeile in gras:
for gewachsen in zeile:
if gewachsen:
summe = summe + 1
return summe
def schritt():
global hasen
wachsen()
# Die Liste hasen wird nicht verändert, während die Schleife sie durchläuft.
# Wer bleibt und wer neu dazukommt, sammeln wir in zwei neuen Listen.
lebende = []
junge = []
for hase in hasen:
hase.laufen()
hase.fressen()
if hase.energie >= NACHWUCHS_ENERGIE:
# Das Junge bekommt die Hälfte der Energie, die Mutter behält die andere Hälfte.
hase.energie = hase.energie // 2
junge.append(Hase(hase.x, hase.y, hase.energie))
if hase.energie > 0:
lebende.append(hase)
hasen = lebende + junge
def draw():
screen.fill(FARBE_ERDE)
for y in range(ZEILEN):
for x in range(SPALTEN):
if gras[y][x]:
screen.draw.filled_rect(Rect(x * FELD, y * FELD, FELD - 1, FELD - 1), FARBE_GRAS)
for hase in hasen:
hase.zeichnen()
screen.draw.text("Gras: " + str(gras_zaehlen()) + " Hasen: " + str(len(hasen)), (6, 6), color="white")
# Zehnmal pro Sekunde ein Schritt.
clock.schedule_interval(schritt, 0.1)
pgzrun.go()Test
Zehn Hasen laufen über die Wiese. Oben links stehen die Zahl der Grasfelder und die Zahl der Hasen. Nach wenigen Sekunden werden es mehr Hasen.
Probier es aus
- Beobachte die beiden Zahlen oben links eine Minute lang. Schreib auf, was du siehst.
- Warum steigt die Zahl der Hasen nicht ewig weiter? Begründe mit den Regeln.
Stufe 6: Das Diagramm
Zahlen oben links verraten dir nicht, wie sich die Wiese verändert. Dazu brauchst du eine Kurve. Du zeichnest sie selbst, unter der Wiese, im selben Fenster.
Das Fenster wird höher, damit unter dem Gitter Platz ist. Ersetze die Zeilen mit WIDTH und HEIGHT durch:
python
# Unter der Wiese ist Platz für das Diagramm.
DIAGRAMM_OBEN = ZEILEN * FELD
DIAGRAMM_HOEHE = 110
# Pygame Zero erwartet diese beiden Namen.
WIDTH = SPALTEN * FELD
HEIGHT = DIAGRAMM_OBEN + DIAGRAMM_HOEHEDazu kommen drei Farben bei den anderen Farben:
python
FARBE_DIAGRAMM = (30, 40, 30)
FARBE_GRASLINIE = (120, 215, 100)
FARBE_HASENLINIE = (240, 240, 230)Dann merkt sich das Programm in jedem Schritt, wie viel Gras und wie viele Hasen es gibt. Dazu kommen zwei Listen vor dem Start:
python
# Für jeden Schritt wird gemerkt, wie viel Gras und wie viele Hasen es gab.
gras_verlauf = []
hasen_verlauf = []und am Ende von schritt() kommen diese Zeilen hinzu:
python
gras_verlauf.append(gras_zaehlen())
hasen_verlauf.append(len(hasen))
# Das Diagramm zeigt die letzten WIDTH // 2 Schritte, jeder ist zwei Pixel breit.
if len(hasen_verlauf) > WIDTH // 2:
gras_verlauf.pop(0)
hasen_verlauf.pop(0)Die Funktionen zum Zeichnen stehen zwischen schritt() und draw():
python
def diagramm_y(wert, maximum):
# Null liegt unten, maximum ganz oben. y wächst nach unten, deshalb wird von unten abgezogen.
return int(DIAGRAMM_OBEN + DIAGRAMM_HOEHE - 6 - wert / maximum * (DIAGRAMM_HOEHE - 30))
def kurve_zeichnen(verlauf, maximum, farbe):
for i in range(1, len(verlauf)):
von = ((i - 1) * 2, diagramm_y(verlauf[i - 1], maximum))
bis = (i * 2, diagramm_y(verlauf[i], maximum))
screen.draw.line(von, bis, farbe)
def diagramm_zeichnen():
screen.draw.filled_rect(Rect(0, DIAGRAMM_OBEN, WIDTH, DIAGRAMM_HOEHE), FARBE_DIAGRAMM)
if len(hasen_verlauf) < 2:
return
gras_max = SPALTEN * ZEILEN
hasen_max = max(100, max(hasen_verlauf))
kurve_zeichnen(gras_verlauf, gras_max, FARBE_GRASLINIE)
kurve_zeichnen(hasen_verlauf, hasen_max, FARBE_HASENLINIE)
screen.draw.text("Gras", (6, DIAGRAMM_OBEN + 4), color=FARBE_GRASLINIE)
screen.draw.text("Hasen", (50, DIAGRAMM_OBEN + 4), color=FARBE_HASENLINIE)In draw() kommt vor dem Text ein neuer Aufruf dazu:
python
for hase in hasen:
hase.zeichnen()
diagramm_zeichnen()pop(0) wirft den ältesten Wert weg. So behält das Diagramm immer die letzten 280 Schritte, jeder ist zwei Pixel breit.
diagramm_y rechnet einen Wert in eine Pixelhöhe um. Der größte Wert liegt oben, null unten. Weil y im Fenster nach unten wächst, wird von unten abgezogen — wie schon bei der Ente in Ein Spiel bauen. kurve_zeichnen verbindet die Werte eines Verlaufs mit Strichen.
Die zwei Kurven haben verschiedene Maßstäbe: Das Gras wird gegen die Zahl aller Felder gezeichnet, die Hasen gegen ihren bisherigen Höchstwert, mindestens aber 100. Vergleichen kannst du also die Form der Kurven, nicht ihre Höhe.
Das ganze Programm dieser Stufe
python
import random
import pgzrun
# Die Wiese hat 40 Spalten und 30 Zeilen. Jedes Feld ist 14 Pixel groß.
SPALTEN = 40
ZEILEN = 30
FELD = 14
# Unter der Wiese ist Platz für das Diagramm.
DIAGRAMM_OBEN = ZEILEN * FELD
DIAGRAMM_HOEHE = 110
# Pygame Zero erwartet diese beiden Namen.
WIDTH = SPALTEN * FELD
HEIGHT = DIAGRAMM_OBEN + DIAGRAMM_HOEHE
FARBE_ERDE = (150, 120, 85)
FARBE_GRAS = (96, 170, 80)
FARBE_DIAGRAMM = (30, 40, 30)
FARBE_GRASLINIE = (120, 215, 100)
FARBE_HASENLINIE = (240, 240, 230)
START_GRAS = 0.5 # So viele Felder sind am Anfang grün.
WACHSTUM = 0.02 # So wahrscheinlich wird ein kahles Feld pro Schritt grün.
START_HASEN = 10 # So viele Hasen gibt es am Anfang.
START_ENERGIE = 12 # Damit kommt ein Hase ohne Futter 12 Schritte weit.
GRAS_ENERGIE = 6 # Ein Büschel Gras gibt so viel Energie.
NACHWUCHS_ENERGIE = 24 # Ab dieser Energie bekommt ein Hase ein Junges.
# Die vier Richtungen als (dx, dy): rechts, links, runter, hoch.
RICHTUNGEN = [(1, 0), (-1, 0), (0, 1), (0, -1)]
# Mit einer Zahl läuft jedes Mal dasselbe ab. Mit None entscheidet der Zufall neu.
STARTWERT = None
if STARTWERT is not None:
random.seed(STARTWERT)
# gras[y][x] ist True, wo Gras wächst.
gras = []
for y in range(ZEILEN):
zeile = []
for x in range(SPALTEN):
zeile.append(random.random() < START_GRAS)
gras.append(zeile)
class Hase:
def __init__(self, x, y, energie):
self.x = x
self.y = y
self.energie = energie
def laufen(self):
dx, dy = random.choice(RICHTUNGEN)
self.x = (self.x + dx) % SPALTEN
self.y = (self.y + dy) % ZEILEN
self.energie = self.energie - 1
def fressen(self):
if gras[self.y][self.x]:
gras[self.y][self.x] = False
self.energie = self.energie + GRAS_ENERGIE
def zeichnen(self):
screen.blit("hase", (self.x * FELD, self.y * FELD))
# Für jeden Schritt wird gemerkt, wie viel Gras und wie viele Hasen es gab.
gras_verlauf = []
hasen_verlauf = []
hasen = []
for i in range(START_HASEN):
hasen.append(Hase(random.randrange(SPALTEN), random.randrange(ZEILEN), START_ENERGIE))
def wachsen():
for y in range(ZEILEN):
for x in range(SPALTEN):
if not gras[y][x] and random.random() < WACHSTUM:
gras[y][x] = True
def gras_zaehlen():
summe = 0
for zeile in gras:
for gewachsen in zeile:
if gewachsen:
summe = summe + 1
return summe
def schritt():
global hasen
wachsen()
# Die Liste hasen wird nicht verändert, während die Schleife sie durchläuft.
# Wer bleibt und wer neu dazukommt, sammeln wir in zwei neuen Listen.
lebende = []
junge = []
for hase in hasen:
hase.laufen()
hase.fressen()
if hase.energie >= NACHWUCHS_ENERGIE:
# Das Junge bekommt die Hälfte der Energie, die Mutter behält die andere Hälfte.
hase.energie = hase.energie // 2
junge.append(Hase(hase.x, hase.y, hase.energie))
if hase.energie > 0:
lebende.append(hase)
hasen = lebende + junge
gras_verlauf.append(gras_zaehlen())
hasen_verlauf.append(len(hasen))
# Das Diagramm zeigt die letzten WIDTH // 2 Schritte, jeder ist zwei Pixel breit.
if len(hasen_verlauf) > WIDTH // 2:
gras_verlauf.pop(0)
hasen_verlauf.pop(0)
def diagramm_y(wert, maximum):
# Null liegt unten, maximum ganz oben. y wächst nach unten, deshalb wird von unten abgezogen.
return int(DIAGRAMM_OBEN + DIAGRAMM_HOEHE - 6 - wert / maximum * (DIAGRAMM_HOEHE - 30))
def kurve_zeichnen(verlauf, maximum, farbe):
for i in range(1, len(verlauf)):
von = ((i - 1) * 2, diagramm_y(verlauf[i - 1], maximum))
bis = (i * 2, diagramm_y(verlauf[i], maximum))
screen.draw.line(von, bis, farbe)
def diagramm_zeichnen():
screen.draw.filled_rect(Rect(0, DIAGRAMM_OBEN, WIDTH, DIAGRAMM_HOEHE), FARBE_DIAGRAMM)
if len(hasen_verlauf) < 2:
return
gras_max = SPALTEN * ZEILEN
hasen_max = max(100, max(hasen_verlauf))
kurve_zeichnen(gras_verlauf, gras_max, FARBE_GRASLINIE)
kurve_zeichnen(hasen_verlauf, hasen_max, FARBE_HASENLINIE)
screen.draw.text("Gras", (6, DIAGRAMM_OBEN + 4), color=FARBE_GRASLINIE)
screen.draw.text("Hasen", (50, DIAGRAMM_OBEN + 4), color=FARBE_HASENLINIE)
def draw():
screen.fill(FARBE_ERDE)
for y in range(ZEILEN):
for x in range(SPALTEN):
if gras[y][x]:
screen.draw.filled_rect(Rect(x * FELD, y * FELD, FELD - 1, FELD - 1), FARBE_GRAS)
for hase in hasen:
hase.zeichnen()
diagramm_zeichnen()
screen.draw.text("Gras: " + str(gras_zaehlen()) + " Hasen: " + str(len(hasen)), (6, 6), color="white")
# Zehnmal pro Sekunde ein Schritt.
clock.schedule_interval(schritt, 0.1)
pgzrun.go()Test
Unter der Wiese wächst von links nach rechts ein Diagramm mit zwei Kurven: weiß für die Hasen, grün für das Gras.
Probier es aus
- Lass die Simulation zwei Minuten laufen. Skizziere die beiden Kurven und beschreibe sie in zwei Sätzen.
- Wann steigt die Hasenkurve? Was macht in diesem Moment die Graskurve? Und wenn die Hasen weniger werden?
- Ersetze in
diagramm_zeichnendie Zeile mithasen_maxdurchhasen_max = gras_max, damit beide Kurven denselben Maßstab haben. Was siehst du jetzt, und warum nimmt man verschiedene Maßstäbe? - Setze
STARTWERT = 1und starte das Programm zweimal. Sehen die Kurven beide Male gleich aus?
Stufe 7: Was passiert, wenn …
Bisher hast du zugeschaut. Jetzt fragst du. Alle Zahlen, die du verändern kannst, stehen oben im Programm:
| Zahl | Bedeutung |
|---|---|
WACHSTUM | wie schnell das Gras nachwächst |
GRAS_ENERGIE | wie viel Energie ein Büschel Gras gibt |
NACHWUCHS_ENERGIE | ab wann ein Hase ein Junges bekommt |
START_HASEN | mit wie vielen Hasen es losgeht |
START_ENERGIE | mit wie viel Energie sie starten |
STARTWERT | welcher Zufall es ist |
Eine Untersuchung hat eine feste Reihenfolge: Erst vermuten, dann ausprobieren, dann aufschreiben.
Probier es aus
Wähle drei der Fragen. Schreibe jeweils deine Vermutung auf, bevor du das Programm startest, und danach, was passiert ist.
- Was geschieht, wenn das Gras sehr langsam nachwächst (
WACHSTUM = 0.002)? Und was beiWACHSTUM = 0.1? - Was ändert sich, wenn ein Büschel Gras nur 3 Energie gibt? Und wenn es 10 gibt?
- Hängt es vom Start ab? Starte mit 2 Hasen, dann mit 100. Wo landet die Hasenzahl nach zwei Minuten?
- Ein Hase bekommt schon bei 12 Energie ein Junges. Was ändert das?
- Läuft es mit
STARTWERT = 1anders als mitSTARTWERT = 2? Was ist gleich, was nicht?
Probier es aus
Denke dir eine eigene Frage an die Wiese aus. Schreibe sie, deine Vermutung, den Versuch und das Ergebnis auf — so, dass jemand anderes den Versuch wiederholen kann. Dazu gehört der STARTWERT.
Die Grenzen des Modells
Deine Ergebnisse gelten für dieses Modell. Ob echte Wiesen sich so verhalten, lässt sich nur auf echten Wiesen nachprüfen.
Probier es aus
Nenne zwei Dinge, die echte Hasen tun, das Modell aber nicht. Welches davon würde das Ergebnis deiner Meinung nach am stärksten verändern? Wie könntest du es einbauen?
Erweiterungen
Wenn du noch Lust hast, such dir etwas aus. Hier gibt es keinen fertigen Quelltext mehr.
- Gras nur aus Gras. Bisher wächst Gras aus dem Nichts. Ändere
wachsen()so, dass ein kahles Feld nur grün wird, wenn mindestens ein Nachbarfeld grün ist. Ändert sich das Bild der Wiese? Passiert etwas mit den Kurven? Achte auf die Ränder. - Jahreszeiten. Lass
WACHSTUMmit der Zeit schwanken, zum Beispiel mitmath.sin. Wie reagieren die Hasen? - Genügsame und hungrige Hasen. Gib jedem Hasen einen eigenen Energieverbrauch pro Schritt. Ein Junges bekommt den Wert der Mutter, mal um 1 größer, mal um 1 kleiner. Lass die Simulation lange laufen und gib dann die Werte aller Hasen aus. Welcher Wert hat sich durchgesetzt?
- Eine zweite Art. Es gibt Modelle, in denen eine zweite Tierart von der ersten lebt. Such nach Räuber-Beute-Modell und Lotka-Volterra und überlege, was du dafür in der Klasse
Haseund inschritt()verändern müsstest.