Häufige Fragen zur LunarLander API — aktualisiert für v0.9.9. | v6.3
Mit setCustomDrawer(drawFunction). Deine Funktion zeichnet den Lander relativ zu seinem Mittelpunkt — setPos(0, 0) ist die Mitte des Landers.
from lunarlander import *
def meinLander():
# Empfohlen: absolut mit moveTo() — (0,0) = Lander-Mitte
# +Y = oben, –Y = unten, +X = rechts, –X = links
setPenColor("cyan")
setPenWidth(2)
penUp()
setPos(-12, 14) # oben-links
penDown()
moveTo(12, 14) # → oben-rechts
moveTo(12, -14) # → unten-rechts
moveTo(-12, -14) # → unten-links
moveTo(-12, 14) # → schließt Rechteck
penUp()
setPenColor("white")
setPos(0, 2)
dot(8) # Cockpit-Fenster
setCustomDrawer(meinLander) # vor init() anmelden
init(handleKey)
start()
setPos() teleportiert immer ohne Linie — auch bei penDown(). Für Linien immer moveTo() verwenden.Das Terrain ist eine Liste mit Höhenwerten. Jeder Eintrag steht für einen Abschnitt von links nach rechts. Mindestens 8 Werte sind erforderlich.
setTerrain([30, 60, 90, 70, 20, 20, 20, 20, 55, 100, 120, 80, 40])
setLandingZone(4, 7) # flacher Bereich (Index 4–7) = Landeplatz
Größere Werte = höheres Gelände. Die Werte sind relative Höhen, keine absoluten Koordinaten.
Mit setLandingZones([...]) kannst du mehrere Landezonen auf einmal setzen. Der Lander reagiert dann automatisch auf jede dieser Zonen.
setLandingZones([(4, 7), (10, 12)])
# Landeplatz 1: Terrain-Index 4 bis 7
# Landeplatz 2: Terrain-Index 10 bis 12
setScale(faktor) vergrößert oder verkleinert die Zeichnung und die Kollisionsprobe. setRotation(winkel) dreht sie.
setScale(1.5) # 50 % größer als Standard (Standard: 1.0)
setRotation(30) # 30° gedreht
Winkelkonvention: 0° = Ausgangsposition, positive Werte = gegen den Uhrzeigersinn.
Rotation und Scale lassen sich auch in der updateFunction animieren:
winkel = 0
def update():
global winkel
winkel = (winkel + 2) % 360
setRotation(winkel)
setUpdateFunction(update)
HUD-Werte anzeigen: setHudItems(["rotation", "scale"])
Die CollisionProbe ist der Auftreffpunkt für die Terrain-Kollision. Sie wird relativ zur Lander-Mitte definiert:
setCollisionProbe(north, east, south, west)
# Beispiel: Probe am Fuß eines Landers mit y=-14
setCollisionProbe(0, 0, 14, 0)
Die Berechnung: collisionY = landerY + north − south
Zum Debuggen sichtbar machen:
setShowCollisionProbe(True)
setCollisionProbeStyle("red", 10)
Standardwert: south = landerSize / 2 (= 15 bei landerSize=30)
Animationen brauchen immer drei Teile: eine Zustandsvariable, setUpdateFunction zum Ändern des Zustands, und setCustomDrawer zum Zeichnen.
schubAktiv = False
def update():
global schubAktiv
# Zustand pro Frame aktualisieren
# (hier wird schubAktiv anderswo gesetzt — z.B. im keyHandler)
def meinLander():
# Rumpf immer zeichnen — absolut mit moveTo()
setPenColor("cyan"); setPenWidth(2)
penUp(); setPos(-10, 10); penDown()
moveTo(10, 10); moveTo(10, -10)
moveTo(-10, -10); moveTo(-10, 10); penUp()
# Flamme nur wenn Schub aktiv
if schubAktiv:
setPenColor("orange"); setPenWidth(3)
penUp(); setPos(-4, -10); penDown()
moveTo(-4, -22) # Flamme nach unten
penUp()
setUpdateFunction(update)
setCustomDrawer(meinLander)
setRotation() und setScale() lassen sich direkt in update() aufrufen — das reicht oft für einfache Animationen ohne eigenen Drawer.Dafür ist setUpdateFunction(fn) gedacht. Deine Funktion wird jeden Frame aufgerufen — vor Physik, Zeichnen und Kollisionsprüfung.
def meinUpdate():
if getFuel() < 30:
print("Treibstoff fast leer!")
if isOverLandingZone() and getHeight() < 60:
print("Landeanflug!")
setUpdateFunction(meinUpdate) # vor init() anmelden
Dort kannst du z.B. Punkte vergeben, Warnmeldungen ausgeben oder eigene Spielbedingungen prüfen.
Die Bibliothek vergibt keine Punkte automatisch. Du verwaltest sie mit einer eigenen Variable in der updateFunction.
punkte = 1000 # Startpunkte
def update():
global punkte
verbrauch = 200 - getFuel()
punkte = 1000 - verbrauch # Abzug für Treibstoffverbrauch
setUpdateFunction(update)
# Nach start(): Endstand ausgeben
start()
print("Punkte:", punkte)
Mit setGameEndFunction(fn). Gibt deine Funktion True zurück, endet das Spiel sofort.
zeitLimit = 300 # 300 Frames ≈ 21 Sekunden
def pruefeEnde():
global zeitLimit
zeitLimit -= 1
if zeitLimit <= 0:
print("Zeit abgelaufen!")
return True
return False
setGameEndFunction(pruefeEnde) # vor init() anmelden
Die Spielschleife endet durch Landung, Absturz, ESC oder eine eigene Endbedingung. Das Fenster selbst bleibt normalerweise offen. In TigerJython wird es beim Programmende automatisch geschlossen.
Die ESC-Taste beendet das Spiel standardmäßig. Eine andere Taste kann mit setAbortKeyCode(code) gesetzt werden.
Ja — durch unterschiedliche Konfiguration vor init():
level = 2 # 1 = leicht, 2 = mittel, 3 = schwer
if level == 1:
setGravity(0.08)
setFuel(300)
setMaxVerticalLandingSpeed(4.0)
elif level == 2:
setGravity(0.15)
setFuel(200)
setMaxVerticalLandingSpeed(3.0)
elif level == 3:
setGravity(0.25)
setFuel(120)
setMaxVerticalLandingSpeed(2.0)
init(handleKey)
start()
Ja, mit restart() in Kombination mit einer eigenen Levelstruktur:
terrains = [
[20, 20, 60, 80, 5, 5, 5, 70, 40, 20],
[80, 60, 30, 10, 10, 10, 40, 70, 90, 50],
]
aktuellesLevel = 0
def ladeLevel(n):
setTerrain(terrains[n])
setLandingZone(3, 5)
restart()
ladeLevel(0)
init(handleKey)
start()
if hasLanded() and aktuellesLevel < len(terrains) - 1:
aktuellesLevel += 1
ladeLevel(aktuellesLevel)
start()
Mit der LunarLander-API direkt ist das schwierig, weil es nur einen Lander gibt. Es gibt aber kreative Ansätze:
restart() für Spieler 2 mit anderem Startpunkt.setCustomDrawer selbst zeichnen und verwalten — wie bei Pong.getFrameTime() gibt die Dauer des letzten kompletten Frames in Millisekunden zurück. Das ist nützlich um zu erkennen, ob das Spiel ruckelt oder ob die Zeichenfunktion zu aufwendig ist.
def update():
if getFrameTime() > 100:
print("Langsamer Frame:", round(getFrameTime(), 1), "ms")
setUpdateFunction(update)
Faustregel:
Auch als HUD-Wert anzeigbar: setHudItems(["fuel", "vSpeed", "frameTime"])
delay ist die Wartezeit am Ende jedes Frames. Größeres delay → langsameres Spiel insgesamt.smoothFactor bestimmt, wie stark sich etwas pro Frame ändert — nicht das Gesamttempo.Beispiele:
setDelay(30) statt setDelay(70)setSmoothFactor(0.3) statt 0.45setSmoothFactor(0.7)smoothFactor = 0 ist keine gute Idee — dann ist fast keine Bewegung sichtbar.In v0.9 werden zu kleine Werte automatisch auf ein sinnvolles Minimum angehoben:
delay mindestens 5 mssmoothFactor mindestens 0.05setDelay(0)
print(getDelay()) # 5
setSmoothFactor(0)
print(getSmoothFactor()) # 0.05
pauseGame() # Spielschleife anhalten
resumeGame() # Spielschleife fortsetzen
togglePause() # Pause ein/aus wechseln
print(isPaused())
Typischer Einsatz: eine Taste schaltet Pause ein und aus.
def handleKey(keyCode):
if keyCode == 80: # P-Taste
togglePause()
elif keyCode == 38:
thrust()
"paused"Nein — die Bibliothek bleibt bei keyPressed. Das OS sendet beim Halten einer Taste automatisch wiederholte Druckereignisse (Auto-Repeat).
Workaround für sanfte Bewegung (z.B. in einem Pong-Spiel): Velocity + Damping.
paddleY = 0.0
paddleVY = 0.0
MAX_SPEED = 12
DAMPING = 0.5 # klingt in ca. 3 Frames auf null ab
def update():
global paddleY, paddleVY
paddleY = paddleY + paddleVY
paddleVY = paddleVY * DAMPING # ohne Tastendruck: Auslaufen
def handleKey(keyCode):
global paddleVY
if keyCode == 87: # W-Taste
paddleVY = MAX_SPEED
elif keyCode == 83: # S-Taste
paddleVY = -MAX_SPEED
setLandingZone(start, ende) setzt genau einen Landeplatz.
setLandingZones([[start, ende], ...]) setzt mehrere Landeplätze auf einmal.
# Ein Landeplatz:
setLandingZone(3, 5)
# Mehrere Landeplätze:
setLandingZones([(2, 4), (8, 10)]) # zwei Landeplätze
Im keyHandler kannst du beliebige Keycodes abfragen. Häufig verwendete Codes:
getKeyCode() liefert immer den Code der
physischen Taste — unabhängig davon ob Shift gedrückt ist. Buchstabentasten haben deshalb immer
die Großbuchstaben-Codes (A=65, W=87 …), auch wenn man ohne Shift tippt.def handleKey(keyCode):
# print(keyCode) # zum Herausfinden unbekannter Codes
if keyCode == 87: # W-Taste (Code 87 — unabhängig von Shift)
thrust()
elif keyCode == 65: # A-Taste
moveLeft()
elif keyCode == 68: # D-Taste
moveRight()
elif keyCode == 80: # P-Taste
togglePause()
Die ESC-Taste (27) beendet das Spiel standardmäßig. Ändern mit setAbortKeyCode(code).
global — und wie rufe ich eigene Funktionen aus einem Hook auf?Die Regel: In Python 2.7 (TigerJython) musst du eine Variable mit global deklarieren, wenn du sie innerhalb einer Funktion verändern willst. Nur lesen geht ohne global.
zaehler = 0
warnungGezeigt = False
def update():
global zaehler, warnungGezeigt # beide werden verändert
zaehler = zaehler + 1
if getFuel() < 30 and not warnungGezeigt:
print("Treibstoff niedrig!")
warnungGezeigt = True # ohne global: lokale Kopie, Original bleibt 0
setUpdateFunction(update)
Eigene Hilfsfunktionen aus Hooks aufrufen: Du kannst beliebige eigene Funktionen definieren und sie aus update() oder handleKey() aufrufen — einfach den Namen schreiben. Die global-Regel gilt auch dort.
aktionAusgefuehrt = False
def zeigeWarnung(): # eigene Hilfsfunktion
global aktionAusgefuehrt # wird verändert → global nötig
if not aktionAusgefuehrt:
print("Achtung!")
aktionAusgefuehrt = True
def update():
if getFuel() < 40:
zeigeWarnung() # Hilfsfunktion aufrufen — kein global nötig hier
setUpdateFunction(update)
global ·
Verändern → global erforderlich ·
Hilfsfunktion aufrufen → einfach den Namen schreiben
updateFunction — Werte jeden Frame ausgebensetShowCollisionProbe(True)setShowInnerPlayfieldBorder(True)setHudItems(["height", "vSpeed", "collisionY", "frameTime"])def meinUpdate():
print("h:", round(getHeight(),1),
"vS:", round(getVSpeed(),2),
"ft:", round(getFrameTime(),0))
setUpdateFunction(meinUpdate)
Mit setWindowSize(breite, hoehe) vor init(). Das schwarze Fenster ist die gesamte Spielfläche — es gibt keine feste Standardgröße, die du berücksichtigen musst.
setWindowSize(500, 700) # Hochformat — vor init()
init(handleKey)
start()
Bei Änderung der Höhe wird groundLevel automatisch neu berechnet (relativ-Modus)
— oder proportional skaliert wenn es vorher manuell gesetzt wurde. Mehr dazu in der
API-Dokumentation → setWindowSize().
Aktuelle Größe abfragen: getWindowSize() → gibt (breite, hoehe) zurück.
Beim Aufruf von init() gibt die API automatisch eine Meldung in der Konsole aus —
z.B. LunarLander API v0.9.9 ready. Das hilft SuS zu sehen, welche API-Version aktiv ist.
Abschalten mit setStartupMessage(False) vor init():
setStartupMessage(False) # Meldung unterdrücken
init(handleKey)
start()
Mit setHudPosition(abstandLinks, abstandOben).
setHudPosition(10, 30) # Standard: 10 px von links, 30 px von oben
setHudPosition(200, 30) # HUD in der Mitte des Fensters
Schriftgröße anpassen: setHudFontSize(18) — Standard ist 16.
Alle verfügbaren HUD-Werte: fuel, vSpeed, hSpeed, height, x, gravity, collisionX, collisionY, frameTime, rotation, scale, paused, abortKey
Die Standard-HUD-Anzeige ist immer vertikal (eine Zeile pro Element). Eine echte horizontale Anordnung ist mit dem eingebauten HUD nicht möglich.
Workaround: HUD ausblenden und die Werte im setCustomDrawer selbst zeichnen — z.B. mit setPos() und dot() als Balkenanzeige, oder durch Aufrufe direkt im Drawer.
setHudItems([]) # eingebautes HUD ausblenden
def meinDrawer():
# eigene Anzeige — z.B. Treibstoffbalken
setPenColor("lime")
setPenWidth(6)
penUp()
setPos(-80, -160) # untere linke Ecke
penDown()
# Balken proportional zu getFuel() (0–200)
moveTo(-80 + getFuel() * 0.8, -160)
penUp()
setCustomDrawer(meinDrawer)
Ab v0.9.5 ist das schwarze Fenster die gesamte Spielfläche — es gibt keinen erzwungenen Randabstand mehr. Wenn du noch Abstand siehst, liegt das an deinen eigenen Koordinaten.
Das optionale Playfield hat standardmäßig Margin 0 und deckt sich damit exakt mit dem Fenster. Du kannst es zum Debuggen sichtbar machen:
setShowInnerPlayfieldBorder(True)
setPlayfieldMargin(10, 10, 10, 10) # optionaler Abstand
setCustomDrawer(meinLander) vergessen — oder nach init() aufgerufensetCustomDrawer()penDown() vergessen — die Turtle zeichnet nur bei penDown()setPos() teleportiert immer ohne Linie. Für Linien: penDown() + moveTo() oder forward().print(keyCode) in den Handler einfügen um den richtigen Code zu ermittelnhandleKey nicht an init() übergeben: init(handleKey)start() nicht aufgerufensetStartPosition(x, y)setGroundLevel zu hoch gesetztsetGameEndFunction gibt sofort True zurücksetFuel(0) versehentlich aufgerufen)Die Probe wird relativ zur Lander-Mitte berechnet: collisionY = landerY + north − south
setShowCollisionProbe(True)south-Wert anpassensetLanderSize() wird die Probe automatisch auf south = landerSize/2 zurückgesetztsetCollisionProbe(0, 0, 14, 0) # Probe 14 Einheiten unterhalb der Mitte
setShowCollisionProbe(True) # zum Testen sichtbar machen
groundLevel (Standard: relativ — 95% der halben Fensterhöhe, bei 400px = –190, bei 800px = –380). Terrain-Werte werden addiert. Manuell setzen mit setGroundLevel().setTerrain() muss vor init() aufgerufen werdensetHudItems([]) blendet das HUD ausfuel, vSpeed, hSpeed, height, x, gravity, collisionX, collisionY, frameTime, rotation, scale, paused, abortKeysetHudPosition(10, 30) prüfensetScale() mit zu großem oder kleinem Wert aufgerufen — Standard ist 1.0setLanderSize() zu groß gesetztsetScale(1.0) # zurücksetzen auf Standard
setLanderSize(30) # Standard-Kantenlänge
Positive Winkel drehen gegen den Uhrzeigersinn, negative im Uhrzeigersinn. Die Winkelkonvention von gturtle weicht von mathematischen Standards ab.
Einfacher Test: setRotation(90) — dreht den Lander nach links. setRotation(-90) — nach rechts.
Das passiert meistens wenn forward(), left() und right()
mit falschen Winkelwerten kombiniert werden. Die Winkelkonvention in gturtle ist:
setHeading(0) = nach oben (nicht rechts wie in der Schulmathematik!)setHeading(90) = nach rechts · 180 = unten · 270 = linksright(90) dreht im UhrzeigersinnEinfachste Lösung: Verwende stattdessen setPos() und moveTo()
mit direkten Koordinaten — das ist weniger fehleranfällig:
# Statt: setHeading(0); forward(20); right(90); ...
# Besser: direkte Koordinaten — (0,0) = Lander-Mitte, +Y = oben
penUp()
setPos(0, 15) # Spitze oben
penDown()
moveTo(-18, -15) # unten links
moveTo(18, -15) # unten rechts
moveTo(0, 15) # schließt Dreieck
penUp()
setTerrain() und setLandingZone() vor restart() aufrufen — nicht danachrestart() setzt Lander-Position, Treibstoff und Spielstatus zurück, aber nicht die Terrain-Konfigurationstart() nochmal start() aufrufen für das nächste LevelDas ist ein gturtle/TigerJython-internes Problem mit dem Repaint-Mechanismus. Die Bibliothek versucht es durch manuelles Repaint-Management zu verhindern.
setDelay() leicht erhöhengetFrameTime() sehr niedrig ist — zu kurze Frames können Flackern verursachendelay()-Aufrufe in der updateFunctionstart() nicht aufgerufen — das Fenster ist offen, aber die Schleife läuft nichtinit() — Konsole prüfensetCustomDrawer mit leerer Funktion registriertupdateFunction oder dem setCustomDrawer — die genaue Fehlermeldung steht in der TigerJython-Konsoleglobal verändertlunarlander.py — Python importiert dann sich selbst statt der APIsetCustomDrawer() übergeben, die Parameter erwartet# Falsch — update erwartet keinen Parameter:
def update():
...
setUpdateFunction(update) # korrekt — keine Klammern beim Übergeben
global verändertsetUpdateFunction oder setCustomDrawer vergessen — oder nach init() aufgerufenprint() zum PrüfensetCustomDrawer istfrom lunarlander import * einen Fehler?lunarlander.py liegt nicht im selben Ordner wie dein Programmlunarlander.py.init() und alle anderen Funktionen fehlen und es kommt sofort ein Fehler.mein_lander.py.
Die LunarLander-API lässt sich für sehr verschiedene Projekte nutzen:
Erweiterungen des LunarLanders:
Völlig andere Spiele mit der Engine:
Kür-Ideen für Fortgeschrittene:
setGameEndFunction()