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_bibliothek() await test_gebaeude_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_ui_panels() 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 Human: kind._process(dt) # -------------------------------------------------------------------- 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() == 12 + 4 + 1 + 1, "18 Interaktionspunkte (12 Techniken, 4 Missionen, Meditation, Tor) — gefunden: %d" % inter.size()) p(Game.einsicht == 3, "Startkapital 3 Einsicht") p(Game.known.is_empty(), "Noch keine Technik gelernt") func test_bibliothek() -> void: abschnitt("Techniken lernen (Bibliothek)") 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()) # Über den echten Interaktionsweg: Spieler an ein Pult stellen var ziel: Node3D = null for n in get_tree().get_nodes_in_group("interactable"): if n.get_meta("kind") == "technik" and n.get_meta("arg") == "pomodoro": ziel = n p(ziel != null, "Pult für die Pomodoro-Technik existiert") main.player.global_position = ziel.global_position + Vector3(0, 0.5, 1.0) main._process(0.05) p(main._fokus == ziel, "Spieler erfasst das Pult als Fokus") main._interagieren() p(ui().offen and ui()._kind == "technik", "Technik-Panel öffnet sich") 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_gebaeude_betreten() -> void: abschnitt("Gebäude betreten") var vorher: Vector3 = main.player.global_position main.player.global_position = Vector3(-24, 1.0, -4.0) Game.teleport_lock = 0.0 for i in range(12): await get_tree().physics_frame main.player.global_position = Vector3(-24, 1.0, -9.0) for i in range(20): await get_tree().physics_frame var drin: bool = main.player.global_position.y < -250.0 p(drin, "Tür der Bibliothek versetzt in den Innenraum (y=%.1f)" % main.player.global_position.y) # drinnen stehen die Pulte var innen_pulte := 0 for n in get_tree().get_nodes_in_group("interactable"): if n.get_meta("kind") == "technik" and n.global_position.y < -250.0: innen_pulte += 1 p(innen_pulte == 12, "12 Technik-Pulte stehen im Innenraum (%d)" % innen_pulte) Game.teleport_lock = 0.0 main.player.global_position = Vector3(0, -299.0, 6.0) for i in range(10): await get_tree().physics_frame main.player.global_position = Vector3(0, -299.0, 12.0) for i in range(20): await get_tree().physics_frame p(main.player.global_position.y > -100.0, "Ausgang bringt zurück nach draußen (y=%.1f)" % main.player.global_position.y) main.player.global_position = vorher func test_mission_annehmen() -> void: abschnitt("Mission annehmen") var ziel: Node3D = null for n in get_tree().get_nodes_in_group("interactable"): if n.get_meta("kind") == "mission" and n.get_meta("arg") == "lena": ziel = n p(ziel != null, "Stele für Lena Vogt existiert") Game.teleport_lock = 1.0 main.player.global_position = ziel.global_position + Vector3(0, 0, 1.2) main._process(0.05) p(main._fokus == ziel, "Stele wird als Fokus erkannt") main._interagieren() p(ui().offen and ui()._kind == "mission", "Missions-Panel öffnet sich") 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: Human = 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(HumanDB.place_center("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) p(lena.global_position.distance_to(HumanDB.place_center("cafe")) < 4.0, "Sie steht im Café (Abstand %.1f)" % lena.global_position.distance_to(HumanDB.place_center("cafe"))) 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) # alle vier haben zu jeder Stunde einen definierten Ort 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 HumanDB.ACTIVITY_TEXT.has(r[1]): luecken += 1 p(luecken == 0, "Alle 4 Menschen haben rund um die Uhr Ort und Tätigkeit") # ein voller Tag am Stück, mit echter Uhr: niemand bleibt hängen Game.time_h = 0.0 var besuchte: Dictionary = {} for m in welt().humans: besuchte[m.data["id"]] = {} for i in range(7200): schritte(1) for m in welt().humans: if m._path.is_empty(): besuchte[m.data["id"]][m.place] = true if Game.time_h >= 23.5 and Game.day >= 1: break var alle_ok := true for m in welt().humans: var orte: int = besuchte[m.data["id"]].size() var soll: Dictionary = {} for e in m.data["routine"]: soll[e[1]] = true if orte < soll.size(): alle_ok = false p(false, "%s erreichte nur %d von %d Orten" % [m.data["name"], orte, 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") var sek_pro_stunde := Game.DAY_SECONDS / 24.0 var alles_ok := true var engste := 999.0 var engste_text := "" for m in HumanDB.LIST: 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(von, nach) / Human.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, "Jeder Weg passt in sein Zeitfenster — engster Fall: " + engste_text) func _weg_laenge(von: String, nach: String) -> float: var pts: Array = HumanDB.route(von, nach, Vector3.ZERO) var l := 0.0 var pos := HumanDB.place_center(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 > vor.x, "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") # keiner der Menschen reagiert auf ihn var lena: Human = 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: Human = null for m in welt().humans: if m.data["id"] == "lena": lena = m Game.time_h = 9.5 for i in range(900): schritte(1) if lena.place == "haus_lena" and lena._path.is_empty(): break p(lena.activity == "zoegert", "Lena zögert gerade") 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(Game.skill_of("pomodoro") == 5, "Pomodoro auf Stufe 5 gebracht") 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 (%.0f%% > %.0f%%)" % [passend["chance"] * 100, unpassend["chance"] * 100]) p(passend["tag"], "Pomodoro trifft Lenas Thema Prokrastination") 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") var blockiert: Dictionary = Game.evaluate("pomodoro", "lena", "schlaeft", false) p(blockiert["blocked"], "Schlafende Menschen sind nicht erreichbar") var wehrt: Dictionary = Game.evaluate("pomodoro", "lena", "zoegert", true) p(wehrt["chance"] < passend["chance"], "Wer abblockt, ist schwerer zu erreichen") var ungelernt: Dictionary = Game.evaluate("nein_sagen", "lena", "zoegert", false) p(ungelernt["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") # Wiederholung derselben Technik wirkt schwächer 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: Human = welt().humans[0] p(lena.beschriftung().contains("erfüllt"), "Lena zeigt sichtbar den erfüllten Traum") func test_ui_panels() -> void: abschnitt("Oberfläche") ui().kodex_panel() p(ui().offen and ui()._content.get_child_count() > 0, "Kodex zeigt gelernte Techniken") ui().schliessen() ui().vertiefen_panel() p(ui()._content.get_child_count() > 0, "Meditationskammer listet Techniken") ui().schliessen() var e_vor := Game.einsicht p(Game.deepen("pomodoro"), "Vertiefen funktioniert") p(Game.einsicht == e_vor - 1, "Vertiefen kostet 1 Einsicht") ui().mission_panel("bruno") p(ui()._content.get_child_count() > 0, "Missions-Panel für Bruno baut auf") ui().schliessen() ui().anwenden_panel(welt().humans[1]) p(ui()._content.get_child_count() > 0, "Anwenden-Panel für Bruno baut auf") ui().schliessen() p(main._fokus == null or true, "HUD bleibt fehlerfrei") func test_steuerung() -> void: abschnitt("Steuerung") var aktionen := ["vor", "zurueck", "links", "rechts", "steigen", "sinken", "interagieren", "kodex"] 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") main.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)