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_mission_annehmen() await test_reise_zur_erde() await test_tagesablauf() await test_wege_machbar() await test_geist() await test_technik_anwenden() await test_mission_abschliessen() await test_techniken_verbessern_sich() await test_journal_ueberall() await test_steuerung() 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 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(ui().offen, "Intro-Panel ist offen") ui().schliessen() p(not ui().offen, "Intro lässt sich schließen") 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 inter := get_tree().get_nodes_in_group("interactable") p(inter.size() == 1 and inter[0].get_meta("kind") == "erde", "Einzige Interaktion in der Traumwelt ist das Tor zur Erde (%d)" % inter.size()) p(Game.einsicht == 3, "Startkapital 3 Einsicht") p(Game.known.is_empty(), "Noch keine Technik gelernt") 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") var knoepfe := 0 for c in ui()._content.get_children(): if c is Button: knoepfe += 1 p(knoepfe >= TechDB.LIST.size(), "Jede der %d Techniken hat einen Knopf (%d)" % [TechDB.LIST.size(), knoepfe]) ui().journal_panel("missionen") p(ui()._journal_seite == "missionen" and ui()._content.get_child_count() > 0, "Missionsseite baut auf") ui().schliessen() p(Game.learn("pomodoro"), "Pomodoro gelernt") p(Game.learn("zerlegen"), "Aufgabe zerlegen gelernt") p(Game.learn("umgebung"), "Umgebung gestalten gelernt") p(Game.einsicht == 0, "Einsicht auf 0 verbraucht") p(not Game.learn("reframing"), "Ohne Einsicht keine weitere Technik") p(Game.skill_of("pomodoro") == 1, "Frisch gelernte Technik ist Stufe 1") Game.add_xp("pomodoro", 4) p(Game.skill_of("pomodoro") == 2, "4 EP heben auf Stufe 2") Game.add_xp("pomodoro", 24) p(Game.skill_of("pomodoro") == 5, "28 EP erreichen Stufe 5") Game.known["pomodoro"] = 0 func test_traumdorf() -> void: abschnitt("Traumwesen leben ihren Tag") var wesen: Array = welt().bewohner p(wesen.size() == 4, "Vier Wesen wohnen in der Traumwelt (%d)" % wesen.size()) var haeuser := 0 for key in DreamDB.PLACES: var s: Array = DreamDB.PLACES[key]["size"] if s[0] > 0.0: haeuser += 1 p(haeuser == 4, "Es gibt vier Häuser (%d)" % haeuser) var eigene: Dictionary = {} for w in wesen: var haus := "haus_" + str(w.data["id"]) 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) func test_haus_betreten() -> void: abschnitt("Orte betreten") var haus := Orte.center(DreamDB.PLACES, "haus_ailen") var tuer := Orte.door(DreamDB.PLACES, "haus_ailen") var ziel: Vector3 = await laufe(tuer + Vector3(0, 1.0, 1.5), 0.0, 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 var vor_wand := haus + Vector3(-11.0, 1.0, 0) var gestoppt: Vector3 = await laufe(vor_wand, -PI * 0.5, 80) p(gestoppt.x < haus.x - 5.5, "Die Seitenwand hält den Spieler auf (x = %.1f, Wand bei %.1f)" % [gestoppt.x, haus.x - 6.0]) main.player.set_mode(Player.Mode.TRAUM) main.player.global_position = (welt() as DreamWorld).spawn func test_mission_annehmen() -> void: abschnitt("Mission annehmen") ui().journal_panel("missionen") p(ui().offen and ui()._journal_seite == "missionen", "Missionsseite des Journals offen") ui().schliessen() p(Game.accept_mission("lena"), "Mission Lena angenommen") p(Game.active_mission == "lena", "Mission ist aktiv") 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") 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() == 4, "Vier Menschen leben in der Stadt") func test_tagesablauf() -> void: abschnitt("Tagesablauf der Menschen") var lena: Bewohner = null for m in welt().humans: if m.data["id"] == "lena": lena = m p(lena != null, "Lena existiert auf der Erde") schritte(30, 0.05, 2.0) p(lena.activity == "schlaeft", "Um 02:00 schläft Lena (%s)" % lena.activity) p(lena.global_position.distance_to(Orte.center(HumanDB.PLACES, "haus_lena")) < 4.0, "Sie schläft in ihrem Atelier") var gewandert := false for i in range(600): schritte(1, 0.05, 8.1) if lena.place == "cafe" and lena._path.is_empty(): gewandert = true break p(gewandert, "Um 08:00 geht Lena ins Café und kommt dort an") p(lena.activity == "essen", "Im Café isst sie (%s)" % lena.activity) var daheim := false for i in range(600): schritte(1, 0.05, 10.2) if lena.place == "haus_lena" and lena._path.is_empty(): daheim = true break p(daheim, "Um 10:00 ist sie zurück im Atelier") p(lena.activity == "zoegert", "Dort zögert sie vor der Arbeit (%s)" % lena.activity) 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 4 Menschen haben rund um die Uhr Ort und Tätigkeit") 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 var ist: int = besucht[m.data["id"]].size() if ist < soll.size(): alle_ok = false p(false, "%s erreichte nur %d von %d Stationen" % [m.data["name"], ist, 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"] == "lena": lena = m for i in range(900): schritte(1, 0.05, 10.2) if lena.place == "haus_lena" and lena._path.is_empty(): break p(lena.activity == "zoegert", "Lena zögert gerade") 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", "lena", "zoegert", false)["chance"] Game.known["pomodoro"] = 28 var s5: float = Game.evaluate("pomodoro", "lena", "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", "lena", "zoegert", false) var unpassend: Dictionary = Game.evaluate("pomodoro", "lena", "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", "mira", "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", "lena", "schlaeft", false)["blocked"], "Schlafende Menschen sind nicht erreichbar") p(Game.evaluate("pomodoro", "lena", "zoegert", true)["chance"] < passend["chance"], "Wer abblockt, ist schwerer zu erreichen") p(Game.evaluate("nein_sagen", "lena", "zoegert", false)["blocked"], "Ungelernte Techniken lassen sich nicht anwenden") var vorher_p: float = Game.mission_progress("lena") var res := Game.apply_technique("pomodoro", "lena", "zoegert", false) p(res["ok"], "Anwendung wird ausgeführt") if res["success"]: p(Game.mission_progress("lena") > vorher_p, "Erfolg bringt Missionsfortschritt") else: p(Game.mission_progress("lena") == vorher_p, "Misserfolg bringt keinen Fortschritt") p(Game.known["pomodoro"] > 0, "Jede Anwendung bringt Erfahrung") Game.progress["lena"] = 0.0 Game.used.clear() Game.known["pomodoro"] = 28 var ertraege: Array = [] for i in range(6): var r := Game.apply_technique("pomodoro", "lena", "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_mission_abschliessen() -> void: abschnitt("Mission abschließen") Game.progress["lena"] = 0.0 Game.used.clear() var einsicht_vorher := Game.einsicht var runden := 0 while Game.mission_progress("lena") < Game.MISSION_ZIEL and runden < 400: Game.apply_technique("pomodoro", "lena", "zoegert", false) Game.apply_technique("zerlegen", "lena", "gruebelt", false) Game.apply_technique("umgebung", "lena", "aufraeumen", false) runden += 1 p(Game.mission_progress("lena") >= Game.MISSION_ZIEL, "Lenas Traum erreicht 100 %% (nach %d Runden)" % runden) p(Game.completed.has("lena"), "Mission gilt als abgeschlossen") p(Game.active_mission == "", "Aktive Mission wurde freigegeben") p(Game.einsicht == einsicht_vorher + 3, "Abschluss bringt 3 Einsicht") schritte(3) var lena: Bewohner = welt().humans[0] p(lena.beschriftung().contains("erfüllt"), "Lena zeigt sichtbar den erfüllten Traum") func test_techniken_verbessern_sich() -> void: abschnitt("Techniken verbessern sich durch Anwendung") Game.einsicht = 2 p(Game.learn("reframing"), "Technik im Journal gelernt") p(Game.skill_of("reframing") == 1, "Start auf Stufe 1") var einsicht_vorher := Game.einsicht var anwendungen := 0 while Game.skill_of("reframing") < 3 and anwendungen < 60: Game.apply_technique("reframing", "mira", "gruebelt", false) anwendungen += 1 p(Game.skill_of("reframing") >= 3, "Nach %d Anwendungen von allein auf Stufe %d" % [anwendungen, Game.skill_of("reframing")]) p(Game.einsicht == einsicht_vorher, "Das kostet keine Einsicht") var vor_xp: int = Game.known["reframing"] var res := Game.apply_technique("reframing", "mira", "essen", false) p(Game.known["reframing"] > vor_xp, "Auch ein %s bringt Erfahrung" % ("Erfolg" if res["success"] else "Fehlversuch")) var xp_vor: int = Game.known["reframing"] p(Game.deepen("reframing"), "Vertiefen im Journal funktioniert") p(Game.known["reframing"] == 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)