Files
dreamer-game/tests/verify.gd
2026-08-30 19:56:47 +02:00

1094 lines
40 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
extends Node
## Fährt das echte Spiel headless und prüft die Implementierungen.
var fehler := 0
var main: Node3D
func p(ok: bool, text: String) -> void:
print((" OK " if ok else " FEHLT ") + text)
if not ok:
fehler += 1
func abschnitt(t: String) -> void:
print("\n== " + t)
func _ready() -> void:
seed(20240)
main = load("res://scenes/main.tscn").instantiate()
add_child(main)
await get_tree().process_frame
await get_tree().process_frame
await test_start()
await test_journal()
await test_traumdorf()
await test_haus_betreten()
await test_hoersaal()
await test_mission_annehmen()
await test_reise_zur_erde()
await test_tagesablauf()
await test_wege_machbar()
await test_geist()
await test_technik_anwenden()
await test_pipeline()
await test_mission_abschliessen()
await test_welt_waechst()
await test_techniken_verbessern_sich()
await test_journal_ueberall()
await test_steuerung()
await test_erster_traum_schaffbar()
print("\n=========================================")
print("Fehlende Prüfungen: %d" % fehler)
print("=========================================")
get_tree().quit(1 if fehler > 0 else 0)
func ui() -> UI:
return main.ui
func welt() -> Node3D:
return main.welt
func schritte(n: int, dt: float = 0.05, zeit: float = -1.0) -> void:
for i in range(n):
if zeit >= 0.0:
Game.time_h = zeit
main._process(dt)
if welt().has_method("_process"):
welt()._process(dt)
for kind in welt().get_children():
if kind is Bewohner:
kind._process(dt)
func _panel_text() -> String:
var out: Array = []
for c in ui()._content.get_children():
if c is RichTextLabel:
out.append((c as RichTextLabel).get_parsed_text())
return "\n".join(out)
func _journal_text() -> String:
return _panel_text()
## Welche Gebäude stehen wirklich in der Traumwelt? Erkennbar an ihren Schildern.
## Was der Hörsaal von Anfang an lehrt.
func _basiswege() -> Array:
return DreamDB.techniken_von("hoersaal")
func _gebaute_orte() -> Array:
var namen: Array = []
for key in DreamDB.PLACES:
if float(DreamDB.PLACES[key]["size"][0]) <= 0.0:
continue
namen.append(str(DreamDB.PLACES[key]["name"]))
var da: Array = []
for n in welt().get_children():
if n is Label3D and namen.has((n as Label3D).text):
da.append((n as Label3D).text)
return da
func _marke(kind: String) -> Node3D:
for n in get_tree().get_nodes_in_group("interactable"):
if n.get_meta("kind") == kind:
return n
return null
## Stellt den Spieler vor eine Marke und lässt den Fokus neu suchen.
func _hingehen(ziel: Node3D) -> void:
main.player.set_mode(Player.Mode.TRAUM)
main.player.global_position = ziel.global_position + Vector3(0, 0, 1.2)
main.player.velocity = Vector3.ZERO
ui().schliessen()
main._process(0.05)
## Drückt den ersten aktiven Knopf, dessen Beschriftung so beginnt.
func _knopf_texte() -> String:
var out: Array = []
for c in ui()._content.get_children():
if c is Button:
out.append((c as Button).text)
return "\n".join(out)
func _knopf_da(anfang: String) -> bool:
for c in ui()._content.get_children():
if c is Button and (c as Button).text.begins_with(anfang):
return true
return false
func _druecke(anfang: String) -> bool:
for c in ui()._content.get_children():
if c is Button and not (c as Button).disabled and (c as Button).text.begins_with(anfang):
(c as Button).pressed.emit()
return true
return false
func laufe(von: Vector3, yaw: float, frames: int) -> Vector3:
main.player.set_mode(Player.Mode.TRAUM)
main.player.look_enabled = true
main.player.global_position = von
main.player.velocity = Vector3.ZERO
main.player.yaw = yaw
Input.action_press("vor")
for i in range(frames):
await get_tree().physics_frame
Input.action_release("vor")
return main.player.global_position
# --------------------------------------------------------------------
func test_start() -> void:
abschnitt("Start")
p(not ui().offen, "Kein Startbildschirm — das Spiel läuft sofort")
p(welt() is DreamWorld, "Startwelt ist die Traumwelt")
p(main.player != null and main.player.mode == Player.Mode.TRAUM, "Spieler läuft (kein Geist)")
var arten: Dictionary = {}
for n in get_tree().get_nodes_in_group("interactable"):
arten[n.get_meta("kind")] = true
p(arten.has("erde") and arten.has("lehre") and arten.has("brett"),
"Traumwelt bietet Tor, Hörsaal und Schwarzes Brett (%s)" % ", ".join(arten.keys()))
p(Game.einsicht == 3, "Startkapital 3 Einsicht")
p(Game.known.is_empty(), "Noch keine Technik gelernt")
p((welt() as DreamWorld).bewohner.is_empty(),
"Das Traumdorf steht am Anfang leer (%d Wesen)" %
(welt() as DreamWorld).bewohner.size())
var lebt := false
for n in welt().get_children():
if n is Bewohner:
lebt = true
p(not lebt, "Auch kein Wesen im Szenenbaum")
var gebaut := _gebaute_orte()
p(gebaut.size() == 1 and gebaut.has("Hörsaal der Wege"),
"Nur der Hörsaal steht als Gebäude (%s)" % ", ".join(gebaut))
func test_journal() -> void:
abschnitt("Journal")
for t in TechDB.LIST:
var vollstaendig: bool = t["was"] != "" and t["wann"] != "" and t["warum"] != ""
if not vollstaendig:
p(false, "Technik %s hat alle drei Hinweise" % t["id"])
p(true, "Alle %d Techniken tragen Was/Wann/Warum" % TechDB.LIST.size())
ui().journal_panel("techniken")
p(ui().offen and ui()._kind == "journal", "Journal öffnet sich")
p(_journal_text().contains("Hörsaal"),
"Ohne gelernte Technik verweist das Journal auf den Hörsaal")
p(not _knopf_da("Lernen") and not _knopf_da("Vertiefen"),
"Das Journal lehrt und vertieft nicht selbst")
ui().journal_panel("missionen")
p(_journal_text().contains("Schwarzen Brett"),
"Ohne Mission verweist das Journal auf das Schwarze Brett")
p(not _knopf_da("Mission annehmen"), "Das Journal vergibt keine Missionen")
ui().schliessen()
var erster := str(_basiswege()[0])
p(Game.offene_techniken() == 1,
"Am Anfang ist genau ein Weg entdeckt (%d)" % Game.offene_techniken())
p(Game.learn(erster), "Der erste Weg ist gelernt: %s" % TechDB.by_id(erster)["name"])
p(Game.einsicht == 2, "Das kostet 1 von 3 Einsicht (%d)" % Game.einsicht)
p(not Game.learn(str(TechDB.LIST[1]["id"])),
"Der zweite Weg ist trotz Einsicht nirgends zu lernen")
p(Game.skill_of(erster) == 1, "Frisch gelernte Technik ist Stufe 1")
Game.add_xp(erster, 4)
p(Game.skill_of(erster) == 2, "4 EP heben auf Stufe 2")
Game.add_xp(erster, 24)
p(Game.skill_of(erster) == 5, "28 EP erreichen Stufe 5")
Game.known[erster] = 0
ui().journal_panel("techniken")
var buch := _journal_text()
var alle_gelernten := true
for id in Game.known:
if not buch.contains(str(TechDB.by_id(id)["name"])):
alle_gelernten = false
p(alle_gelernten, "Das Journal führt alle %d gelernten Techniken" % Game.known.size())
var fremde := 0
for t in TechDB.LIST:
if not Game.knows(t["id"]) and buch.contains(str(t["name"])):
fremde += 1
p(fremde == 0, "Ungelernte Techniken stehen nicht im Journal (%d)" % fremde)
ui().schliessen()
func test_traumdorf() -> void:
abschnitt("Traumwesen ziehen ein und leben ihren Tag")
p(welt().bewohner.is_empty(), "Vorher wohnt niemand hier")
for i in range(4):
Game.completed.append("probe_%d" % i)
Game.welt_erzeugen()
main._traumwelt()
await get_tree().process_frame
if welt().bewohner.size() != i + 1:
p(false, "Nach %d Träumen wohnen %d Wesen hier" %
[i + 1, welt().bewohner.size()])
p(welt().bewohner.size() == 4,
"Je erfülltem Traum zieht eines ein, hier sind es vier (%d)" %
welt().bewohner.size())
var wesen: Array = welt().bewohner
var haeuser := 0
for key in DreamDB.PLACES:
if key.begins_with("haus_"):
haeuser += 1
p(haeuser == 4, "Vier Häuser sind erzeugt (%d)" % haeuser)
p(float(DreamDB.PLACES["hoersaal"]["size"][0]) > 0.0, "Dazu steht der Hörsaal")
var eigene: Dictionary = {}
for w in wesen:
var haus: String = str(w.data["haus"])
p(DreamDB.PLACES.has(haus), "%s hat ein eigenes Haus (%s)" % [w.data["name"], haus])
eigene[haus] = true
var kommt_vor := false
for e in w.data["routine"]:
if e[1] == haus:
kommt_vor = true
p(kommt_vor, "Die Routine von %s führt ins eigene Haus" % w.data["name"])
p(eigene.size() == 4, "Jedes Wesen hat ein anderes Haus")
var luecken := 0
for w in wesen:
for h in range(24):
var r: Array = w._routine_at(float(h) + 0.5)
if not DreamDB.PLACES.has(r[0]) or not Orte.ACTIVITY_TEXT.has(r[1]):
luecken += 1
p(luecken == 0, "Alle vier haben rund um die Uhr Ort und Tätigkeit")
# Ein voller Tag am Stück: niemand bleibt hängen
Game.time_h = 0.0
Game.day = 1
var besucht: Dictionary = {}
for w in wesen:
besucht[w.data["id"]] = {}
for i in range(9000):
schritte(1)
for w in wesen:
if w._path.is_empty():
besucht[w.data["id"]][w.place] = true
if Game.time_h >= 23.5:
break
var alle_ok := true
for w in wesen:
var soll: Dictionary = {}
for e in w.data["routine"]:
soll[e[1]] = true
var ist: int = besucht[w.data["id"]].size()
if ist < soll.size():
alle_ok = false
p(false, "%s erreichte nur %d von %d Stationen" % [w.data["name"], ist, soll.size()])
p(alle_ok, "Über einen ganzen Tag erreicht jedes Wesen jede Station seiner Routine")
var zuhause := 0
schritte(20, 0.05, 1.0)
for w in wesen:
var r: Array = w._routine_at(1.0)
if w.place == r[0]:
zuhause += 1
p(zuhause == 4, "Um 01:00 sind alle vier an ihrem Nachtplatz (%d)" % zuhause)
# Ausgangslage wiederherstellen: das Dorf ist wieder leer
Game.completed.clear()
Game.welt_erzeugen()
main._traumwelt()
await get_tree().process_frame
func test_haus_betreten() -> void:
abschnitt("Orte betreten")
p(not Game.ort_offen(DreamDB.haus_key(0)), "Ohne Bewohner steht kein Haus")
Game.completed.append("probe_haus")
Game.welt_erzeugen()
main._traumwelt()
await get_tree().process_frame
var haus_name := str(DreamDB.PLACES[DreamDB.haus_key(0)]["name"])
p(_gebaute_orte().has(haus_name), "Mit dem ersten Bewohner steht sein Haus: %s" % haus_name)
var haus := Orte.center(DreamDB.PLACES, DreamDB.haus_key(0))
var tuer := Orte.door(DreamDB.PLACES, DreamDB.haus_key(0))
var hinein: Vector3 = (haus - tuer).normalized()
var yaw_hinein := atan2(-hinein.x, -hinein.z)
var ziel: Vector3 = await laufe(tuer - hinein * 1.5 + Vector3(0, 1.0, 0), yaw_hinein, 80)
var drin: bool = absf(ziel.x - haus.x) < 6.0 and absf(ziel.z - haus.z) < 5.0
p(drin, "Durch die Tür läuft man ins Haus (Ziel %.1f / %.1f, Haus %.1f / %.1f)" %
[ziel.x, ziel.z, haus.x, haus.z])
# Die Wand hält: von der türabgewandten Seite kommt niemand hinein
var rueck := haus + hinein * 11.0 + Vector3(0, 1.0, 0)
var gestoppt: Vector3 = await laufe(rueck, atan2(hinein.x, hinein.z), 80)
var drin2: bool = absf(gestoppt.x - haus.x) < 6.0 and absf(gestoppt.z - haus.z) < 5.0
p(not drin2, "Die Rückwand hält den Spieler auf (%.1f / %.1f)" % [gestoppt.x, gestoppt.z])
main.player.set_mode(Player.Mode.TRAUM)
main.player.global_position = (welt() as DreamWorld).spawn
Game.completed.clear()
Game.welt_erzeugen()
main._traumwelt()
await get_tree().process_frame
func test_hoersaal() -> void:
abschnitt("Techniken werden im Hörsaal gelehrt")
var pult := _marke("lehre")
p(pult != null, "Der Hörsaal hat ein Lehrpult")
var raum := Orte.center(DreamDB.PLACES, "hoersaal")
p(absf(pult.global_position.x - raum.x) < 8.0 and absf(pult.global_position.z - raum.z) < 6.0,
"Das Pult steht im Hörsaal (%.1f / %.1f)" % [pult.global_position.x, pult.global_position.z])
var im_saal: Vector3 = await laufe(Vector3(0, 1.0, 24.0), PI, 150)
p(im_saal.z > 33.0,
"Vom Eingang führt ein freier Mittelgang zum Pult (z = %.1f, Pult bei %.1f)" %
[im_saal.z, raum.z + 3.4])
p(absf(im_saal.x) < 2.0, "Der Gang hält den Spieler in der Mitte (x = %.1f)" % im_saal.x)
_hingehen(pult)
p(main._fokus == pult, "Das Pult wird als Fokus erkannt")
main._interagieren()
p(ui().offen and ui()._kind == "lehre", "Der Hörsaal öffnet sich")
var seite := _panel_text()
var basis_da := true
for id in _basiswege():
if not seite.contains(str(TechDB.by_id(id)["name"])):
basis_da = false
p(basis_da, "Die %d Basiswege werden gelehrt" % 1)
var lernbar := 0
for c in ui()._content.get_children():
if c is Button and (c as Button).text.begins_with("Lernen"):
lernbar += 1
p(lernbar <= 1, "Und höchstens einer davon ist zu lernen (%d)" % lernbar)
p(seite.contains(str(TechDB.LIST[1]["name"])),
"Der Hörsaal nennt den nächsten noch unentdeckten Weg")
p(not Game.learn("box_atmung"), "Eine unentdeckte Technik lässt sich nicht lernen")
Game.einsicht = 1
Game.known.clear()
ui().lehre_panel()
var vorher := Game.known.size()
p(_druecke("Lernen"), "Ein Lernen-Knopf steht bereit")
p(Game.known.size() == vorher + 1, "Im Hörsaal gelernt (%d -> %d)" % [vorher, Game.known.size()])
p(Game.einsicht == 0, "Lernen kostet 1 Einsicht")
Game.einsicht = 1
ui().lehre_panel()
var xp_summe := 0
for id in Game.known:
xp_summe += int(Game.known[id])
p(_druecke("Vertiefen"), "Ein Vertiefen-Knopf steht bereit")
var xp_neu := 0
for id in Game.known:
xp_neu += int(Game.known[id])
p(xp_neu == xp_summe + 3, "Vertiefen bringt 3 EP (%d -> %d)" % [xp_summe, xp_neu])
p(Game.einsicht == 0, "Vertiefen kostet 1 Einsicht")
ui().schliessen()
# Ausgangslage für die folgenden Prüfungen wiederherstellen
Game.known = {"pomodoro": 0, "zerlegen": 0, "umgebung": 0}
Game.einsicht = 0
func test_mission_annehmen() -> void:
abschnitt("Mission am Schwarzen Brett annehmen")
var brett := _marke("brett")
p(brett != null, "Das Schwarze Brett steht in der Traumwelt")
var spawn: Vector3 = (welt() as DreamWorld).spawn
p(absf(brett.global_position.x) > 2.5,
"Es steht neben der Achse Start–Tor, versperrt sie also nicht (x = %.1f)" %
brett.global_position.x)
p(brett.global_position.distance_to(spawn) < 14.0,
"Vom Start aus in Sichtweite (%.1f)" % brett.global_position.distance_to(spawn))
var hin: Vector3 = await laufe(Vector3(brett.global_position.x, 1.0, spawn.z), 0.0, 150)
p(hin.z < 14.0, "Man läuft vom Start ungehindert bis davor (z = %.1f)" % hin.z)
main._process(0.05)
p(main._fokus == brett, "Davor steht man in Reichweite")
ui().schliessen()
main._interagieren()
p(ui().offen and ui()._kind == "brett", "Das Brett öffnet sich")
var alle_aus := true
for h in HumanDB.LIST:
if not _panel_text().contains(str(h["name"])):
alle_aus = false
p(alle_aus, "Alle %d Menschen hängen aus" % HumanDB.LIST.size())
p(Game.active_mission == "", "Vorher keine Mission")
var gedrueckt := _druecke("Mission annehmen")
p(gedrueckt, "Ein Aushang lässt sich annehmen")
p(Game.active_mission == "mensch_0", "Mission Lena ist aktiv (%s)" % Game.active_mission)
p(Game.angenommen("mensch_0"), "Lena steht als angenommen im Buch")
ui().schliessen()
ui().journal_panel("missionen")
var buch := _panel_text()
p(buch.contains(str(HumanDB.by_id("mensch_0")["name"])),
"Das Journal führt die angenommene Mission")
p(not buch.contains(str(HumanDB.by_id("mensch_1")["name"])),
"Nicht angenommene Menschen fehlen im Journal")
p(not _knopf_da("Mission annehmen"), "Im Journal lässt sich keine Mission annehmen")
ui().schliessen()
func test_reise_zur_erde() -> void:
abschnitt("Reise zur Erde")
var tor: Node3D = null
for n in get_tree().get_nodes_in_group("interactable"):
if n.get_meta("kind") == "erde":
tor = n
p(tor != null, "Tor zur Erde existiert")
# Zu Fuß hin: die Torplattform darf keine unüberwindliche Stufe sein
ui().schliessen()
main.player.set_mode(Player.Mode.TRAUM)
main.player.look_enabled = true
main.player.global_position = Vector3(0, 1.0, -20.0)
main.player.velocity = Vector3.ZERO
main.player.yaw = 0.0
Input.action_press("vor")
var erreicht := false
var weit: float = main.player.global_position.z
for i in range(200):
await get_tree().physics_frame
main._process(0.02)
weit = main.player.global_position.z
if main._fokus == tor:
erreicht = true
break
Input.action_release("vor")
p(erreicht, "Zu Fuß kommt man ans Tor — keine Stufe im Weg (z = %.1f, Tor bei %.1f)" %
[weit, tor.global_position.z])
Game.teleport_lock = 1.0
main.player.global_position = tor.global_position + Vector3(0, 0, 1.5)
main._process(0.05)
p(main._fokus == tor, "Tor wird als Fokus erkannt")
main._interagieren()
await get_tree().process_frame
p(main.auf_erde and welt() is EarthWorld, "Spieler ist auf der Erde")
p(welt().humans.size() == HumanDB.LIST.size() and welt().humans.size() >= 3,
"%d Menschen leben in der Stadt" % welt().humans.size())
func test_tagesablauf() -> void:
abschnitt("Tagesablauf der Menschen")
var mensch: Bewohner = null
for m in welt().humans:
if m.data["id"] == "mensch_0":
mensch = m
p(mensch != null, "Der erste Mensch lebt auf der Erde: %s" % mensch.data["name"])
schritte(30, 0.05, 2.0)
p(mensch.activity == "schlaeft", "Um 02:00 schläft er (%s)" % mensch.activity)
p(mensch.global_position.distance_to(
Orte.center(HumanDB.PLACES, str(mensch.data["heim"]))) < 5.0,
"Und zwar zu Hause")
# Jede Station des eigenen Tagesablaufs wird pünktlich erreicht
var verfehlt: Array = []
for e in mensch.data["routine"]:
var stunde: float = float(e[0]) + 0.5
var da := false
for i in range(1200):
schritte(1, 0.05, stunde)
if mensch.place == e[1] and mensch._path.is_empty():
da = true
break
if not da or mensch.activity != e[2]:
verfehlt.append("%s um %d Uhr" % [e[1], int(e[0])])
p(verfehlt.is_empty(), "Alle %d Stationen seines Tages werden erreicht (%s)" %
[(mensch.data["routine"] as Array).size(),
"lückenlos" if verfehlt.is_empty() else ", ".join(verfehlt)])
var luecken := 0
for m in welt().humans:
for h in range(24):
var r: Array = m._routine_at(float(h) + 0.5)
if not HumanDB.PLACES.has(r[0]) or not Orte.ACTIVITY_TEXT.has(r[1]):
luecken += 1
p(luecken == 0, "Alle %d Menschen haben rund um die Uhr Ort und Tätigkeit" %
welt().humans.size())
Game.time_h = 0.0
Game.day = 1
var besucht: Dictionary = {}
for m in welt().humans:
besucht[m.data["id"]] = {}
for i in range(9000):
schritte(1)
for m in welt().humans:
if m._path.is_empty():
besucht[m.data["id"]][m.place] = true
if Game.time_h >= 23.5:
break
var alle_ok := true
for m in welt().humans:
var soll: Dictionary = {}
for e in m.data["routine"]:
soll[e[1]] = true
if besucht[m.data["id"]].size() < soll.size():
alle_ok = false
p(false, "%s erreichte nur %d von %d Stationen" %
[m.data["name"], besucht[m.data["id"]].size(), soll.size()])
p(alle_ok, "Über einen ganzen Tag erreicht jeder Mensch jede Station seiner Routine")
func test_wege_machbar() -> void:
abschnitt("Wege passen in die Zeitfenster")
_wege_pruefen("Erde", HumanDB.PLACES, HumanDB.LIST)
_wege_pruefen("Traumwelt", DreamDB.PLACES, DreamDB.LIST)
func _wege_pruefen(welt_name: String, places: Dictionary, liste: Array) -> void:
var sek_pro_stunde := Game.DAY_SECONDS / 24.0
var alles_ok := true
var engste := 999.0
var engste_text := ""
for m in liste:
var r: Array = m["routine"]
for i in range(r.size()):
var von: String = String(r[i - 1][1]) if i > 0 else String(r[r.size() - 1][1])
var nach: String = String(r[i][1])
if von == nach:
continue
var dauer := _weg_laenge(places, von, nach) / Bewohner.GEHTEMPO
var slot_h: float = (float(r[i + 1][0]) - float(r[i][0])) if i + 1 < r.size() \
else (24.0 - float(r[i][0]))
var fenster := slot_h * sek_pro_stunde
if dauer >= fenster:
alles_ok = false
p(false, "%s: %s -> %s braucht %.0f s, Fenster nur %.0f s" %
[m["name"], von, nach, dauer, fenster])
elif fenster - dauer < engste:
engste = fenster - dauer
engste_text = ("%s: %s -> %s (%.0f s Weg, %.0f s Fenster)"
% [m["name"], von, nach, dauer, fenster])
p(alles_ok, "%s — jeder Weg passt, engster Fall: %s" % [welt_name, engste_text])
func _weg_laenge(places: Dictionary, von: String, nach: String) -> float:
var pts: Array = Orte.route(places, von, nach, Vector3.ZERO)
var l := 0.0
var pos := Orte.center(places, von)
for pt in pts:
l += pos.distance_to(pt)
pos = pt
return l
func test_geist() -> void:
abschnitt("Geist-Bewegung")
p(main.player.mode == Player.Mode.GEIST, "Spieler ist auf der Erde ein Geist")
p(main.player.collision_layer == 0 and main.player.collision_mask == 0,
"Der Geist kollidiert mit nichts — er schwebt durch Wände")
var vor: Vector3 = main.player.global_position
main.player.global_position = Vector3(0, 8.0, 0)
main.player.velocity = Vector3(6, 0, 0)
for i in range(30):
await get_tree().physics_frame
p(main.player.global_position.x > 0.5, "Der Geist bewegt sich frei durch den Raum")
p(absf(main.player.global_position.y - 8.0) < 0.5, "Ohne Schwerkraft fällt er nicht")
var lena: Bewohner = welt().humans[0]
var davor: Vector3 = lena.global_position
main.player.global_position = lena.global_position + Vector3(0, 1, 0.5)
schritte(10)
p(lena.global_position.distance_to(davor) < 2.0, "Menschen nehmen den Geist nicht wahr")
func test_technik_anwenden() -> void:
abschnitt("Techniken anwenden — Erfolgschance")
var lena: Bewohner = null
for m in welt().humans:
if m.data["id"] == "mensch_0":
lena = m
# Die Stunde suchen, in der dieser Mensch zögert — sie steht in seiner Routine
var zoegert_um := -1.0
for e in lena.data["routine"]:
if e[2] == "zoegert":
zoegert_um = float(e[0]) + 0.5
for i in range(1200):
schritte(1, 0.05, zoegert_um)
if lena.activity == "zoegert" and lena._path.is_empty():
break
p(lena.activity == "zoegert", "%s zögert um %.0f Uhr (%s)" %
[lena.data["name"], zoegert_um, lena.activity])
main.player.set_mode(Player.Mode.GEIST)
main.player.global_position = lena.global_position + Vector3(0, 1.0, 1.5)
main._process(0.05)
p(main._fokus_mensch == lena, "Der Geist erfasst Lena in Reichweite")
main._interagieren()
p(ui().offen and ui()._kind == "anwenden", "Anwenden-Panel öffnet sich")
ui().schliessen()
Game.known["pomodoro"] = 0
var s1: float = Game.evaluate("pomodoro", "mensch_0", "zoegert", false)["chance"]
Game.known["pomodoro"] = 28
var s5: float = Game.evaluate("pomodoro", "mensch_0", "zoegert", false)["chance"]
p(s5 > s1, "Höherer Skill = höhere Erfolgschance (%.0f%% -> %.0f%%)" % [s1 * 100, s5 * 100])
Game.known["pomodoro"] = 0
var passend: Dictionary = Game.evaluate("pomodoro", "mensch_0", "zoegert", false)
var unpassend: Dictionary = Game.evaluate("pomodoro", "mensch_0", "essen", false)
p(passend["moment"] and not unpassend["moment"], "Moment-Erkennung: zögert ja, isst nein")
p(passend["chance"] > unpassend["chance"], "Passender Moment erhöht die Chance")
p(passend["tag"], "Pomodoro trifft Lenas Thema Aufschieben")
var falsches_thema: Dictionary = Game.evaluate("pomodoro", "mensch_2", "zoegert", false)
p(not falsches_thema["tag"], "Bei Mira trifft Pomodoro das Thema nicht")
p(passend["chance"] > falsches_thema["chance"], "Passendes Thema erhöht die Chance")
p(Game.evaluate("pomodoro", "mensch_0", "schlaeft", false)["blocked"],
"Schlafende Menschen sind nicht erreichbar")
p(Game.evaluate("pomodoro", "mensch_0", "zoegert", true)["chance"] < passend["chance"],
"Wer abblockt, ist schwerer zu erreichen")
p(Game.evaluate("nein_sagen", "mensch_0", "zoegert", false)["blocked"],
"Ungelernte Techniken lassen sich nicht anwenden")
var vorher_p: float = Game.mission_progress("mensch_0")
var res := Game.apply_technique("pomodoro", "mensch_0", "zoegert", false)
p(res["ok"], "Anwendung wird ausgeführt")
if res["success"]:
p(Game.mission_progress("mensch_0") > vorher_p, "Erfolg bringt Missionsfortschritt")
else:
p(Game.mission_progress("mensch_0") == vorher_p, "Misserfolg bringt keinen Fortschritt")
p(Game.known["pomodoro"] > 0, "Jede Anwendung bringt Erfahrung")
Game.progress["mensch_0"] = 0.0
Game.used.clear()
Game.known["pomodoro"] = 28
var ertraege: Array = []
for i in range(6):
var r := Game.apply_technique("pomodoro", "mensch_0", "zoegert", false)
if r["success"]:
ertraege.append(r["gain"])
p(ertraege.size() >= 2 and ertraege[ertraege.size() - 1] < ertraege[0],
"Dieselbe Technik immer wieder wirkt schwächer")
func test_pipeline() -> void:
abschnitt("Regelwerk — Missionsfortschritt automatisieren")
Game.active_mission = "mensch_0"
Game.progress["mensch_0"] = 0.0
Game.completed.clear()
Game.used.clear()
Game.pipeline.clear()
Game.known = {"pomodoro": 28, "zerlegen": 28, "umgebung": 28}
var taten := HumanDB.aktivitaeten("mensch_0")
p(taten.size() >= 5, "Lenas Tag hat %d verschiedene Tätigkeiten" % taten.size())
p(taten.has("zoegert") and taten.has("schlaeft"),
"Darunter zögert und schläft: %s" % ", ".join(taten))
# Regelwerk über das Journal aufbauen
ui().journal_panel("missionen")
p(_knopf_da("Regelwerk"), "Die angenommene Mission trägt ein Regelwerk")
p(_druecke("Regelwerk"), "Das Regelwerk öffnet sich")
p(ui()._kind == "pipeline", "Panel »pipeline« ist offen")
var zeilen := _knopf_texte()
var fehlt: Array = []
for a in taten:
if not zeilen.contains(Orte.text(a)):
fehlt.append(a)
p(fehlt.is_empty(), "Jede der %d Tätigkeiten hat eine Zeile (fehlt: %s)" %
[taten.size(), ", ".join(fehlt)])
p(zeilen.contains("schläft → unerreichbar"),
"Schlaf ist als unerreichbar ausgewiesen, statt eine Regel anzubieten")
var schlaf_aktiv := false
for c in ui()._content.get_children():
if c is Button and (c as Button).text.begins_with("schläft") and not (c as Button).disabled:
schlaf_aktiv = true
p(not schlaf_aktiv, "Und lässt sich gar nicht erst anklicken")
ui().regel_panel("mensch_0", "zoegert")
p(ui()._kind == "regel", "Die Technikwahl für »zögert« öffnet sich")
p(_panel_text().contains("Pomodoro-Technik"), "Gelernte Techniken stehen zur Wahl")
p(_druecke("Diese Regel"), "Eine Technik lässt sich zuordnen")
p(Game.regel_fuer("mensch_0", "zoegert") != "",
"Regel steht: zögert -> %s" % Game.regel_fuer("mensch_0", "zoegert"))
p(ui()._kind == "pipeline", "Danach ist wieder das Regelwerk offen")
ui().schliessen()
Game.regel_setzen("mensch_0", "gruebelt", "zerlegen")
Game.regel_setzen("mensch_0", "aufraeumen", "umgebung")
p(Game.regel_anzahl("mensch_0") == 3, "Drei Regeln im Werk (%d)" % Game.regel_anzahl("mensch_0"))
# Eine Technik hält nur einen Posten
Game.regel_setzen("mensch_0", "arbeitet", "zerlegen")
p(Game.regel_anzahl("mensch_0") == 3,
"Dieselbe Technik anderswo einzusetzen fügt keine Regel hinzu (%d)" %
Game.regel_anzahl("mensch_0"))
p(Game.regel_fuer("mensch_0", "gruebelt") == "" and Game.regel_fuer("mensch_0", "arbeitet") == "zerlegen",
"Sie ist umgezogen statt sich zu verdoppeln")
Game.regel_setzen("mensch_0", "arbeitet", "")
Game.regel_setzen("mensch_0", "gruebelt", "zerlegen")
# Ohne Zutun einen Tag laufen lassen
var lena: Bewohner = null
for m in welt().humans:
if m.data["id"] == "mensch_0":
lena = m
p(lena != null, "Lena lebt in der Stadt")
var vorher: float = Game.mission_progress("mensch_0")
var vor_xp: int = int(Game.known["pomodoro"]) + int(Game.known["zerlegen"])
Game.time_h = 0.0
main.player.global_position = Vector3(0, 30.0, 60.0) # weit weg, kein Zutun
for i in range(9000):
schritte(1)
if Game.time_h >= 23.5:
break
var nachher: float = Game.mission_progress("mensch_0")
p(nachher > vorher, "Der Traum wächst von allein: %d %% -> %d %%" % [int(vorher), int(nachher)])
p(int(Game.known["pomodoro"]) + int(Game.known["zerlegen"]) > vor_xp,
"Die Regeln sammeln nebenbei Erfahrung")
# Ohne Regeln passiert nichts
Game.pipeline.clear()
Game.progress["mensch_0"] = 0.0
var still: float = Game.mission_progress("mensch_0")
Game.time_h = 0.0
for i in range(9000):
schritte(1)
if Game.time_h >= 23.5:
break
p(is_equal_approx(Game.mission_progress("mensch_0"), still),
"Ohne Regeln bewegt sich nichts (%d %%)" % int(Game.mission_progress("mensch_0")))
# Regeln greifen nur für die aktive Mission
Game.regel_setzen("mensch_1", "zoegert", "zerlegen")
Game.progress["mensch_1"] = 0.0
Game.time_h = 0.0
for i in range(9000):
schritte(1)
if Game.time_h >= 23.5:
break
p(is_equal_approx(Game.mission_progress("mensch_1"), 0.0),
"Regeln fremder Missionen bleiben still (%d %%)" % int(Game.mission_progress("mensch_1")))
# Ausgangslage für die folgenden Prüfungen
Game.pipeline.clear()
Game.progress["mensch_0"] = 0.0
Game.used.clear()
Game.known = {"pomodoro": 0, "zerlegen": 0, "umgebung": 0}
func test_mission_abschliessen() -> void:
abschnitt("Mission abschließen")
Game.progress["mensch_0"] = 0.0
Game.used.clear()
var einsicht_vorher := Game.einsicht
var runden := 0
while Game.mission_progress("mensch_0") < Game.MISSION_ZIEL and runden < 400:
Game.apply_technique("pomodoro", "mensch_0", "zoegert", false)
Game.apply_technique("zerlegen", "mensch_0", "gruebelt", false)
Game.apply_technique("umgebung", "mensch_0", "aufraeumen", false)
runden += 1
p(Game.mission_progress("mensch_0") >= Game.MISSION_ZIEL,
"Lenas Traum erreicht 100 %% (nach %d Runden)" % runden)
p(Game.completed.has("mensch_0"), "Mission gilt als abgeschlossen")
p(Game.active_mission == "", "Aktive Mission wurde freigegeben")
p(Game.einsicht == einsicht_vorher + 3, "Abschluss bringt 3 Einsicht")
p(DreamDB.LIST.size() == Game.completed.size(),
"Der Abschluss lässt das Dorf wachsen (%d Wesen)" % DreamDB.LIST.size())
schritte(3)
var lena: Bewohner = welt().humans[0]
p(lena.beschriftung().contains("erfüllt"), "Lena zeigt sichtbar den erfüllten Traum")
func test_welt_waechst() -> void:
abschnitt("Erfüllte Träume lassen das Dorf wachsen")
var zugeordnet: Dictionary = {}
var doppelt: Array = []
for i in range(TechDB.LIST.size()):
var ort := DreamDB.ort_von_technik(str(TechDB.LIST[i]["id"]))
if zugeordnet.has(ort):
doppelt.append(ort)
zugeordnet[ort] = TechDB.LIST[i]["id"]
p(zugeordnet.size() == TechDB.LIST.size() and doppelt.is_empty(),
"Jede der %d Techniken hat einen eigenen Lehrort (%d Orte, %d doppelt)" %
[TechDB.LIST.size(), zugeordnet.size(), doppelt.size()])
p(DreamDB.ort_von_technik(str(TechDB.LIST[0]["id"])) == "hoersaal",
"Den ersten Weg lehrt der Hörsaal")
p(DreamDB.ort_von_technik(str(TechDB.LIST[1]["id"])) == DreamDB.haus_key(0),
"Den zweiten das erste Haus")
p(Game.completed.size() == 1, "Ein Traum ist erfüllt (%d)" % Game.completed.size())
main._traumwelt()
await get_tree().process_frame
var lehrorte: Dictionary = {}
for n in get_tree().get_nodes_in_group("interactable"):
if n.get_meta("kind") == "lehre":
lehrorte[n.get_meta("arg")] = n
p(lehrorte.has("hoersaal") and lehrorte.has(DreamDB.haus_key(0)),
"Hörsaal und das erste Haus lehren (%s)" % ", ".join(lehrorte.keys()))
p(not lehrorte.has(DreamDB.haus_key(1)), "Ein zweites Haus gibt es noch nicht")
p(Game.insel_radius() >= Game.INSEL_GRUND,
"Die Insel trägt das Dorf (%.0f)" % Game.insel_radius())
Game.einsicht = 3
var neu_id := str(TechDB.LIST[1]["id"])
ui().lehre_panel(DreamDB.haus_key(0))
p(ui()._kind == "lehre", "Das neue Haus lehrt")
p(_panel_text().contains(str(TechDB.by_id(neu_id)["name"])),
"Und zwar den Weg, den sein Bewohner mitbrachte")
p(_druecke("Lernen"), "Er lässt sich lernen")
p(Game.knows(neu_id), "Gelernt: %s" % TechDB.by_id(neu_id)["name"])
ui().schliessen()
p(not Game.technik_offen(str(TechDB.LIST[2]["id"])),
"%s bleibt unentdeckt" % TechDB.by_id(str(TechDB.LIST[2]["id"]))["name"])
p(Game.offene_techniken() == 2, "Entdeckt sind zwei Wege (%d)" % Game.offene_techniken())
await _dorf_platz_pruefen()
# Ausgangslage für die folgenden Prüfungen
Game.known.erase(neu_id)
Game.einsicht = 2
Game.welt_erzeugen()
main._erde()
await get_tree().process_frame
## Das Dorf wird erzeugt — also muss über viele Ausbaustufen gelten:
## nichts überlappt, nichts ragt über den Rand, jeder Weg passt ins Zeitfenster.
func _dorf_platz_pruefen() -> void:
var merker: Array = Game.completed.duplicate()
var stufen := 24
var engster := 999.0
var engster_text := ""
var ueberlappung := ""
var rand_fehler := ""
for stufe in range(stufen + 1):
Game.completed.clear()
for i in range(stufe):
Game.completed.append("stufe_%d" % i)
Game.welt_erzeugen()
var radius := Game.insel_radius()
var keys: Array = []
for key in DreamDB.PLACES:
if float(DreamDB.PLACES[key]["size"][0]) > 0.0:
keys.append(key)
for a in range(keys.size()):
var ka: String = keys[a]
var ca := Orte.center(DreamDB.PLACES, ka)
var sa: Array = DreamDB.PLACES[ka]["size"]
var ecke: float = Vector3(absf(ca.x) + float(sa[0]) * 0.5, 0,
absf(ca.z) + float(sa[1]) * 0.5).length()
if radius - ecke < engster:
engster = radius - ecke
engster_text = ("%s bei %d Häusern (%.1f von %.0f)"
% [DreamDB.PLACES[ka]["name"], stufe, ecke, radius])
if ecke >= radius and rand_fehler == "":
rand_fehler = "%s ragt bei %d Häusern über den Rand (%.1f >= %.0f)" % \
[DreamDB.PLACES[ka]["name"], stufe, ecke, radius]
for b in range(a + 1, keys.size()):
var kb: String = keys[b]
var cb := Orte.center(DreamDB.PLACES, kb)
var sb: Array = DreamDB.PLACES[kb]["size"]
var dx: float = absf(ca.x - cb.x) - (float(sa[0]) + float(sb[0])) * 0.5
var dz: float = absf(ca.z - cb.z) - (float(sa[1]) + float(sb[1])) * 0.5
if dx < 2.0 and dz < 2.0 and ueberlappung == "":
ueberlappung = "%s und %s stehen ineinander (%.1f / %.1f)" % \
[DreamDB.PLACES[ka]["name"], DreamDB.PLACES[kb]["name"], dx, dz]
p(ueberlappung == "", "Über %d Ausbaustufen berührt sich kein Gebäude (%s)" %
[stufen, "keins" if ueberlappung == "" else ueberlappung])
p(rand_fehler == "" and engster >= 4.0,
"Und keins ragt über den Rand — enger Fall: %s%s" %
[engster_text, "" if rand_fehler == "" else " — " + rand_fehler])
Game.completed.clear()
for i in range(stufen):
Game.completed.append("stufe_%d" % i)
Game.welt_erzeugen()
_wege_pruefen("Traumdorf mit %d Häusern" % stufen, DreamDB.PLACES, DreamDB.LIST)
_wege_pruefen("Stadt mit %d Menschen" % HumanDB.LIST.size(), HumanDB.PLACES, HumanDB.LIST)
Game.completed.assign(merker)
Game.welt_erzeugen()
func test_techniken_verbessern_sich() -> void:
abschnitt("Techniken verbessern sich durch Anwendung")
Game.einsicht = 2
p(Game.learn("zwei_minuten"), "Technik gelernt")
p(Game.skill_of("zwei_minuten") == 1, "Start auf Stufe 1")
var einsicht_vorher := Game.einsicht
var anwendungen := 0
while Game.skill_of("zwei_minuten") < 3 and anwendungen < 60:
Game.apply_technique("zwei_minuten", "mensch_2", "gruebelt", false)
anwendungen += 1
p(Game.skill_of("zwei_minuten") >= 3,
"Nach %d Anwendungen von allein auf Stufe %d" % [anwendungen, Game.skill_of("zwei_minuten")])
p(Game.einsicht == einsicht_vorher, "Das kostet keine Einsicht")
var vor_xp: int = Game.known["zwei_minuten"]
var res := Game.apply_technique("zwei_minuten", "mensch_2", "essen", false)
p(Game.known["zwei_minuten"] > vor_xp,
"Auch ein %s bringt Erfahrung" % ("Erfolg" if res["success"] else "Fehlversuch"))
var xp_vor: int = Game.known["zwei_minuten"]
p(Game.deepen("zwei_minuten"), "Vertiefen funktioniert")
p(Game.known["zwei_minuten"] == xp_vor + 3, "Vertiefen bringt 3 EP")
p(Game.einsicht == einsicht_vorher - 1, "Vertiefen kostet 1 Einsicht")
func test_journal_ueberall() -> void:
abschnitt("Journal ist immer dabei")
p(InputMap.has_action("journal"), "Eigene Aktion für das Journal")
var tasten: Array = []
for ev in InputMap.action_get_events("journal"):
tasten.append(ev.physical_keycode)
p(tasten.has(KEY_J) and tasten.has(KEY_TAB), "Auf J und Tab gelegt")
p(main.auf_erde, "Test läuft auf der Erde")
ui().journal_panel()
p(ui().offen, "Journal öffnet auf der Erde")
ui().schliessen()
main._traumwelt()
await get_tree().process_frame
ui().journal_panel()
p(ui().offen, "Journal öffnet auch in der Traumwelt")
ui().schliessen()
# Traumwesen sind keine Missionsziele
var wesen: Bewohner = welt().bewohner[0]
main.player.set_mode(Player.Mode.TRAUM)
main.player.global_position = wesen.global_position + Vector3(0, 1.0, 1.2)
main._process(0.05)
p(main._fokus_mensch == null, "Traumwesen lassen sich nicht mit Techniken bearbeiten")
p(not wesen.mit_mission, "Sie tragen keine Missionsmarkierung")
p(wesen.beschriftung().contains(str(wesen.data["rolle"])),
"Ihre Beschriftung nennt Name und Rolle: %s" % wesen.beschriftung().replace("\n", " / "))
func test_steuerung() -> void:
abschnitt("Steuerung")
var aktionen := ["vor", "zurueck", "links", "rechts", "steigen", "sinken",
"interagieren", "journal"]
var alle := true
for a in aktionen:
if not InputMap.has_action(a) or InputMap.action_get_events(a).is_empty():
alle = false
p(false, "Aktion '%s' ist belegt" % a)
p(alle, "Alle acht Steuerungsaktionen sind mit Tasten belegt")
ui().schliessen()
main.player.look_enabled = true
main.player.set_mode(Player.Mode.GEIST)
main.player.global_position = Vector3(0, 8.0, 0)
main.player.velocity = Vector3.ZERO
main.player.yaw = 0.0
var start: Vector3 = main.player.global_position
Input.action_press("vor")
for i in range(40):
await get_tree().physics_frame
Input.action_release("vor")
p(main.player.global_position.z < start.z - 1.0,
"W schiebt den Spieler nach vorn (dz = %.1f)" % (main.player.global_position.z - start.z))
main.player.global_position = Vector3(0, 8.0, 0)
main.player.velocity = Vector3.ZERO
Input.action_press("rechts")
for i in range(40):
await get_tree().physics_frame
Input.action_release("rechts")
p(main.player.global_position.x > 1.0,
"D schiebt den Spieler nach rechts (x = %.1f)" % main.player.global_position.x)
main.player.global_position = Vector3(0, 8.0, 0)
main.player.velocity = Vector3.ZERO
Input.action_press("steigen")
for i in range(40):
await get_tree().physics_frame
Input.action_release("steigen")
p(main.player.global_position.y > 9.0,
"Leertaste lässt den Geist steigen (y = %.1f)" % main.player.global_position.y)
main.player.velocity = Vector3.ZERO
var hoch: float = main.player.global_position.y
Input.action_press("sinken")
for i in range(40):
await get_tree().physics_frame
Input.action_release("sinken")
p(main.player.global_position.y < hoch - 1.0,
"Umschalt lässt den Geist sinken (y = %.1f)" % main.player.global_position.y)
## Die Probe auf Loop 2: ohne einen erfüllten Traum gibt es keine neuen Wege.
## Also muss der erste Traum mit dem Startkapital allein zu schaffen sein —
## und zwar zuverlässig, nicht nur bei gutem Würfelglück.
func _erster_traum_tage(saat: int) -> int:
seed(saat)
Game.einsicht = 3
Game.known.clear()
Game.used.clear()
Game.pipeline.clear()
Game.progress.clear()
Game.completed.clear()
Game.active_mission = ""
Game.welt_erzeugen()
for id in _basiswege():
Game.learn(id)
Game.accept_mission("mensch_0")
# So ordnet ein Spieler zu: jede Technik an den Moment, wo sie am besten wirkt.
for id in Game.known:
var bester := ""
var beste_chance := 0.0
for activity in HumanDB.aktivitaeten("mensch_0"):
if not Game.erreichbar(activity) or Game.regel_fuer("mensch_0", activity) != "":
continue
var ev := Game.evaluate(id, "mensch_0", activity, false)
if float(ev["chance"]) > beste_chance:
beste_chance = float(ev["chance"])
bester = activity
if bester != "":
Game.regel_setzen("mensch_0", bester, id)
main._erde()
await get_tree().process_frame
main.player.global_position = Vector3(0, 30.0, 60.0) # der Spieler tut nichts
Game.time_h = 7.0
Game.day = 1
for i in range(140000):
schritte(1)
if Game.completed.has("mensch_0") or Game.day > 14:
break
return Game.day
func test_erster_traum_schaffbar() -> void:
abschnitt("Der erste Traum ist mit dem Startkapital zu schaffen")
Game.completed.clear()
Game.welt_erzeugen()
p(Game.offene_techniken() == 1,
"Am Anfang ist genau ein Weg entdeckt (%d)" % Game.offene_techniken())
var tage: Array = []
for saat in [1, 7, 555, 20240, 90210]:
tage.append(await _erster_traum_tage(saat))
p(Game.known.size() == 1, "Man startet mit einem Weg (%d)" % Game.known.size())
p(Game.regel_anzahl("mensch_0") == 1,
"Der trägt genau eine Regel (%d)" % Game.regel_anzahl("mensch_0"))
var schlimmster: int = tage.max()
var bester: int = tage.min()
p(schlimmster <= 14, "Der Traum erfüllt sich in allen %d Läufen ohne Zutun (%s)" %
[tage.size(), str(tage)])
p(bester >= 2, "Nie geschenkt — mindestens %d Tage" % bester)
p(schlimmster <= 8, "Nie zäh — höchstens %d Tage (von Hand geht es schneller)" %
schlimmster)
p(Game.einsicht >= 3, "Der erfüllte Traum bringt 3 Einsicht (%d)" % Game.einsicht)
p(Game.offene_techniken() > 1,
"Und öffnet neue Wege: %d entdeckt" % Game.offene_techniken())