extends Node ## Fährt das echte Spiel headless und prüft die Implementierungen. var fehler := 0 var main: Node3D const ZEITRAFFER := 8.0 # die Engine läuft im Test achtmal schneller ## Abschnitte, die ganze Tage oder viele Läufe simulieren -- im Schnelllauf übersprungen. const LANGSAM := ["test_tagesablauf", "test_verfolgung", "test_erster_traum_schaffbar", "test_huegel", "test_dorf_organisch", "test_welt_waechst", "test_zufallslauf"] var schnell := false var _uhr_abschnitt := 0 var _dauern: Array = [] var _uebersprungen := 0 func p(ok: bool, text: String) -> void: print((" OK " if ok else " FEHLT ") + text) if not ok: fehler += 1 func abschnitt(t: String) -> void: _abschnitt_ende() print("\n== " + t) _dauern.append([t, 0.0]) _uhr_abschnitt = Time.get_ticks_msec() func _abschnitt_ende() -> void: if _dauern.is_empty(): return _dauern[_dauern.size() - 1][1] = float(Time.get_ticks_msec() - _uhr_abschnitt) / 1000.0 ## Wer den Traum mit reinen Game-Aufrufen vorantreibt, tickt keine Momente: ## ein laufender Moment hielte den Fortschritt sonst für immer an. func _ohne_momente() -> void: Game.situation = {} Game.situation_pause = 1.0e6 ## Ruft einen Testabschnitt auf -- oder überspringt ihn im Schnelllauf. func lauf(name: String) -> void: if schnell and LANGSAM.has(name): _uebersprungen += 1 return # Ausgangslage für jeden Abschnitt: Oberfläche zu, niemand verfolgt, kein # Moment, keine Taste gedrückt -- sonst erbt ein Abschnitt die Fehler des vorigen ui().schliessen() main.player.folgt = null main.player.look_enabled = true main.player.velocity = Vector3.ZERO Game.situation = {} Game.notiz_sperre = 0.0 for t in ["vor", "zurueck", "links", "rechts", "steigen", "drehen"]: Input.action_release(t) await Callable(self, name).call() func _ready() -> void: schnell = OS.get_cmdline_user_args().has("schnell") # Zeitraffer: die Uhr läuft achtmal schneller, der Physikschritt bleibt 1/60 s -- # sonst fiele der Spieler bei großen Schritten durch den Boden. Engine.time_scale = ZEITRAFFER Engine.physics_ticks_per_second = int(60.0 * ZEITRAFFER) Engine.max_physics_steps_per_frame = 64 seed(20240) main = load("res://scenes/main.tscn").instantiate() add_child(main) await get_tree().process_frame await get_tree().process_frame await lauf("test_start") await lauf("test_journal") await lauf("test_traumdorf") await lauf("test_dorf_organisch") await lauf("test_huegel") await lauf("test_haus_betreten") await lauf("test_hoersaal") await lauf("test_mission_annehmen") await lauf("test_reise_zur_erde") await lauf("test_welten_bleiben") await lauf("test_spuren") await lauf("test_ankunft") await lauf("test_akte") await lauf("test_passanten") await lauf("test_wesen_helfen") await lauf("test_bezirk") await lauf("test_tagesablauf") await lauf("test_wege_machbar") await lauf("test_geist") await lauf("test_verfolgung") await lauf("test_charakter") await lauf("test_kamera_und_etagen") await lauf("test_zufallslauf") await lauf("test_gehen_bei_tempo") await lauf("test_diagnose") await lauf("test_notieren_im_spiel") await lauf("test_tagesablauf_anzeige") await lauf("test_ablauf_im_hud") await lauf("test_probleme_und_momente") await lauf("test_eigene_menschen") await lauf("test_systeme") await lauf("test_tempo_des_traums") await lauf("test_cooldown") await lauf("test_chance_verdeckt") await lauf("test_mission_abschliessen") await lauf("test_welt_waechst") await lauf("test_techniken_verbessern_sich") await lauf("test_journal_ueberall") await lauf("test_steuerung") await lauf("test_erster_traum_schaffbar") await lauf("test_brett_nie_leer") await lauf("test_lesen_lohnt_sich") await lauf("test_tempo") _abschnitt_ende() _dauern.sort_custom(func(x, y): return x[1] > y[1]) var langsam: Array = [] for i in range(mini(5, _dauern.size())): langsam.append("%s %.0f s" % [_dauern[i][0], _dauern[i][1]]) print("\nLangsamste Abschnitte: " + " · ".join(langsam)) if schnell: print("Schnelllauf: %d Abschnitte übersprungen" % _uebersprungen) 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 _alle_kinder(ui()._content): if c is RichTextLabel: out.append((c as RichTextLabel).get_parsed_text()) return "\n".join(out) func _journal_text() -> String: return _panel_text() func _lehre_text() -> String: return _panel_text() ## Hebt alle Technik-Sperren auf. Nur für Messschleifen, die viele Anwendungen ## in Folge brauchen — der Cooldown selbst wird eigens geprüft. func _frei() -> void: Game.cooldown.clear() ## 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 _alle_kinder(ui()._content): if c is Button: out.append((c as Button).text) return "\n".join(out) func _knopf_da(anfang: String) -> bool: for c in _alle_kinder(ui()._content): if c is Button and (c as Button).text.begins_with(anfang): return true return false func _druecke(anfang: String) -> bool: for c in _alle_kinder(ui()._content): 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 if welt() is DreamWorld: von.y = maxf(von.y, DreamDB.hoehe_bei(von.x, von.z) + 1.0) # nie im Hügel starten 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.known.size() == 1, "Ein Weg ist von Anfang an gelernt (%d)" % Game.known.size()) p(Game.knows(str(_basiswege()[0])), "Und zwar der des Hörsaals: %s" % TechDB.by_id(str(_basiswege()[0]))["name"]) p(Game.skill_of(str(_basiswege()[0])) == 1, "Auf Stufe 1, wie frisch gelernt") p(not Game.offene_missionen().is_empty(), "Damit hängt vom ersten Moment an eine Mission aus (%d)" % Game.offene_missionen().size()) 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"] != "" \ and t["woran"] != "" if not vollstaendig: p(false, "Technik %s hat alle vier Hinweise" % t["id"]) p(true, "Alle %d Techniken tragen Was/Wann/Warum/Woran" % TechDB.LIST.size()) # Der Woran-Hinweis muss die Tätigkeiten benennen, gegen die die Technik # hilft — sonst ist im Spiel nicht herauszulesen, wohin sie gehört. var stamm := { "putzflucht": "aufräumt", "recherche": "recherchiert", "liste": "später-liste", "jasagen": "ja sagt", "scrollen": "scrollt", "serie": "serie", "wach": "wach", "kuehlschrank": "kühlschrank", "spielen": "spielt", "umschalten": "umschalt", "klagen": "klagt", "vergleichen": "vergleicht", "mails": "postfach", "kaffee": "kaffee", "perfekt": "detail", "plan_neu": "plan", "snooze": "schlummer", "doesen": "döst", "bestellen": "bestellt", "nachrichten": "nachrichten", "ueberstunden": "länger", "absage": "absag", "waelzen": "wälzt", } var luecken: Array = [] var zuviel: Array = [] for t in TechDB.LIST: var text: String = str(t["woran"]).to_lower() var eigene := Ursachen.alle_blocker(str(t["ursache"])) var trifft := 0 for b in eigene: if text.contains(str(stamm[b])): trifft += 1 if trifft == 0: luecken.append("%s nennt keinen eigenen Blocker" % t["id"]) for b in stamm: if eigene.has(b): continue if text.contains(str(stamm[b])): zuviel.append("%s nennt fremdes »%s«" % [t["id"], b]) p(luecken.is_empty(), "Jeder Woran-Hinweis nennt eine Tätigkeit seiner Ursache%s" % ("" if luecken.is_empty() else " — " + ", ".join(luecken))) p(zuviel.is_empty(), "Und keine, gegen die die Technik gar nicht hilft%s" % ("" if zuviel.is_empty() else " — " + ", ".join(zuviel))) ui().journal_panel("techniken") p(ui().offen and ui()._kind == "journal", "Journal öffnet sich") p(_knopf_texte().contains(str(TechDB.by_id(str(_basiswege()[0]))["name"])), "Das Journal führt den mitgebrachten Weg") p(not _knopf_da("Diesen Weg"), "Das Journal lehrt nicht selbst") ui().journal_panel("missionen") p(_journal_text().contains("Schwarze"), "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.knows(erster), "Er ist schon gelernt: %s" % TechDB.by_id(erster)["name"]) p(not Game.learn(erster), "Zweimal lernen geht nicht") p(not Game.learn(str(TechDB.LIST[1]["id"])), "Der zweite Weg ist nirgends zu lernen — er ist noch nicht entdeckt") 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 var zeilen_buch := _knopf_texte() for id in Game.known: if not zeilen_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.kennt(str(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) var ohne_netz := 0 for w in welt().bewohner: ohne_netz += int(w.nav_fehl) p(ohne_netz == 0, "Auch sie laufen auf dem Navigationsnetz der Insel (%d Rückfälle)" % ohne_netz) # Ausgangslage wiederherstellen: das Dorf ist wieder leer Game.completed.clear() Game.welt_erzeugen() main._traumwelt() await get_tree().process_frame ## Die Insel ist ein Hügel: innen eben, außen ansteigend, jedes Gebäude auf ## einer Terrasse. Spieler und Bewohner laufen darauf, statt hindurchzufallen. func test_huegel() -> void: abschnitt("Die Insel ist ein Hügel") var uhr_vorher := Game.time_h var tag_vorher := Game.day p(is_equal_approx(DreamDB.hang(0.0), 0.0) and is_equal_approx(DreamDB.hang(DreamDB.FLACH), 0.0), "Bis %.0f Einheiten ist es eben" % DreamDB.FLACH) p(DreamDB.hang(DreamDB.FLACH + 20.0) > 3.0 and DreamDB.hang(200.0) <= DreamDB.GIPFEL, "Dahinter steigt es an, bis zum Gipfel bei %.0f (bei +20: %.1f)" % [DreamDB.GIPFEL, DreamDB.hang(DreamDB.FLACH + 20.0)]) p(DreamDB.hoehe_bei(0.0, 2.0) == 0.0, "Der Dorfplatz liegt eben") var tor_terr := DreamDB.terrasse("tor") p(is_equal_approx(DreamDB.hoehe_bei(0.0, -34.0), tor_terr) and is_equal_approx(DreamDB.hoehe_bei(0.0, -34.0 + DreamDB.TOR_FLAECHE * 0.5 + 2.0), tor_terr), "Das Tor steht auf einer Terrasse, eben mit seiner Dorfseite (%.1f)" % tor_terr) var stufe := absf(DreamDB.hoehe_bei(0.0, 25.4) - DreamDB.terrasse("hoersaal")) p(stufe < 0.1, "Vor der Hörsaaltür ist keine Stufe (%.2f)" % stufe) # Ein Dorf mit vielen Häusern: manche stehen am Hang, jedes auf einer Terrasse var merker: Array = Game.completed.duplicate() Game.completed.clear() for i in range(14): Game.completed.append("huegel_%d" % i) Game.welt_erzeugen() var am_hang := 0 var schief: Array = [] for key in DreamDB.PLACES: var s: Array = DreamDB.PLACES[key]["size"] if float(s[0]) <= 0.0: continue var c := Orte.center(DreamDB.PLACES, key) var terr := DreamDB.terrasse(key) if terr > 0.5: am_hang += 1 for dx in [-1.0, 1.0]: for dz in [-1.0, 1.0]: var h := DreamDB.hoehe_bei(c.x + dx * float(s[0]) * 0.45, c.z + dz * float(s[1]) * 0.45) if absf(h - terr) > 0.01: schief.append(key) p(am_hang >= 3, "Mit 14 Häusern stehen welche am Hang (%d)" % am_hang) p(schief.is_empty(), "Jedes Gebäude steht innen eben auf seiner Terrasse%s" % ("" if schief.is_empty() else " — nicht: " + ", ".join(schief))) main._traumwelt() await get_tree().process_frame await get_tree().process_frame # Bewohner laufen einen Tag lang auf dem Boden, nicht darunter oder darüber var abweichung := 0.0 Game.time_h = 0.0 for i in range(3000): schritte(1) for w in welt().bewohner: var soll := DreamDB.hoehe_bei(w.global_position.x, w.global_position.z) abweichung = maxf(abweichung, absf(w.global_position.y - soll)) p(abweichung < 0.05, "%d Bewohner bleiben auf dem Boden (Abweichung %.2f)" % [welt().bewohner.size(), abweichung]) # Der Spieler kommt den Hang hinauf und fällt nicht hindurch. Dafür eine # Richtung, in der kein Gebäude im Weg steht. var richtung := Vector3.ZERO var wenigste := 999 for k in range(24): var a := TAU * float(k) / 24.0 var d := Vector3(cos(a), 0, sin(a)) var treffer := 0 for r in range(int(DreamDB.FLACH), int(DreamDB.FLACH) + 24, 2): var q := d * float(r) for key in DreamDB.PLACES: var s: Array = DreamDB.PLACES[key]["size"] if float(s[0]) <= 0.0: continue var c := Orte.center(DreamDB.PLACES, key) if absf(q.x - c.x) < float(s[0]) * 0.5 + 2.5 and absf(q.z - c.z) < float(s[1]) * 0.5 + 2.5: treffer += 1 if treffer < wenigste: wenigste = treffer richtung = d p(wenigste == 0, "Eine freie Richtung den Hang hinauf gibt es (beste: %d Treffer)" % wenigste) var start := richtung * (DreamDB.FLACH + 1.0) + Vector3(0, 1.0, 0) var yaw := atan2(-richtung.x, -richtung.z) var ende: Vector3 = await laufe(start, yaw, 140) var boden_dort := DreamDB.hoehe_bei(ende.x, ende.z) var weit := Vector2(ende.x, ende.z).length() - Vector2(start.x, start.z).length() p(weit > 6.0, "Er läuft hangaufwärts (%.0f Einheiten weiter außen)" % weit) p(ende.y > start.y + 1.0, "Und kommt dabei höher (y %.1f -> %.1f)" % [start.y, ende.y]) p(ende.y >= boden_dort - 0.2 and ende.y < boden_dort + 2.5, "Er steht auf dem Boden, nicht darunter (y %.1f, Boden %.1f)" % [ende.y, boden_dort]) Game.completed.assign(merker) Game.time_h = uhr_vorher Game.day = tag_vorher 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 _alle_kinder(ui()._content): if c is Button and (c as Button).text.begins_with("Diesen Weg"): 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.known.clear() ui().lehre_panel() var vorher := Game.known.size() p(_druecke("Diesen Weg"), "Ein Lern-Knopf steht bereit") p(Game.known.size() == vorher + 1, "Im Hörsaal gelernt (%d -> %d)" % [vorher, Game.known.size()]) # Gelernt heißt fertig: der Lehrort bietet danach nichts mehr an ui().lehre_panel() var xp_summe := 0 for id in Game.known: xp_summe += int(Game.known[id]) p(not _knopf_da("Vertiefen") and not _knopf_da("Üben") and not _knopf_da("Trainieren"), "Kein Knopf trainiert eine gelernte Technik") p(not _druecke("Diesen Weg"), "Auch der Lern-Knopf ist verschwunden") var xp_neu := 0 for id in Game.known: xp_neu += int(Game.known[id]) p(xp_neu == xp_summe, "Der Besuch im Hörsaal bringt keine Erfahrung (%d)" % xp_neu) ui().schliessen() # Ausgangslage für die folgenden Prüfungen wiederherstellen Game.known = {"pomodoro": 0, "zerlegen": 0, "umgebung": 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") # Nur wer mit dem Gelernten zu schaffen ist, hängt aus. var alle_wege: Dictionary = Game.known.duplicate() Game.known = {str(_basiswege()[0]): 0} ui().brett_panel() var aushang := _knopf_texte() var gezeigt := 0 var fehlend := 0 for h in HumanDB.LIST: var id: String = h["id"] if aushang.contains(str(h["name"])): gezeigt += 1 if not Game.machbar(id): p(false, "%s hängt aus, obwohl kein Weg greift" % h["name"]) elif Game.machbar(id): p(false, "%s wäre lösbar, hängt aber nicht aus" % h["name"]) else: fehlend += 1 p(gezeigt > 0, "Mit einem Weg hängt mindestens ein Aushang (%d von %d)" % [gezeigt, HumanDB.LIST.size()]) p(fehlend > 0, "Und mindestens einer bleibt verdeckt (%d)" % fehlend) p(_panel_text().contains("verdeckt"), "Das Brett sagt, dass etwas verdeckt bleibt") p(Game.offene_missionen().size() == gezeigt, "Brett und offene_missionen() sind sich einig (%d)" % gezeigt) # Ohne jede Technik bleibt das Brett leer, mit Hinweis auf den Hörsaal Game.known.clear() ui().brett_panel() p(Game.offene_missionen().is_empty(), "Ohne Technik ist nichts zu holen") p(_panel_text().contains("Hörsaal"), "Und das Brett schickt in den Hörsaal") Game.known = alle_wege ui().brett_panel() p(Game.offene_missionen().size() >= gezeigt, "Mehr Wege heißt mehr Aushänge (%d)" % Game.offene_missionen().size()) p(Game.active_mission == "", "Vorher keine Mission") ui().aushang_panel("mensch_0") p(_panel_text().contains(str(HumanDB.by_id("mensch_0")["problem"])), "Der Aushang zeigt aufgeschlagen, was im Weg steht") 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(_knopf_texte().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()) ## Beide Welten bleiben bestehen: wer zurückkehrt, findet dieselbe Stadt, und ## die Menschen haben derweil ihren Tag weitergelebt. func test_welten_bleiben() -> void: abschnitt("Die Welten bleiben bestehen") var erde_vorher: Node3D = welt() var mensch: Bewohner = welt().humans[0] Game.time_h = 9.5 schritte(20, 0.05, 9.5) var stand: Vector3 = mensch.global_position main._traumwelt() await get_tree().process_frame p(welt() is DreamWorld and is_instance_valid(erde_vorher) and not erde_vorher.visible, "Im Traum bleibt die Erde bestehen, nur unsichtbar") var himmel := 0 for we in get_tree().root.find_children("*", "WorldEnvironment", true, false): if (we as WorldEnvironment).environment != null: himmel += 1 p(himmel == 1, "Genau ein Himmel ist aktiv (%d)" % himmel) for i in range(400): Game.time_h = 12.5 for m in erde_vorher.humans: m._process(0.05) main._erde() await get_tree().process_frame p(welt() == erde_vorher, "Zurück auf der Erde ist es dieselbe Stadt") p(mensch.global_position.distance_to(stand) > 3.0, "%s hat derweil ihren Tag weitergelebt (%.0f m weiter)" % [mensch.data["name"], mensch.global_position.distance_to(stand)]) p(main.player.collision_mask == 2, "Der Geist stößt nur an die Erde, nicht an die unsichtbare Insel") ## Spuren in der Wohnung: wer hineingeht und hinsieht, kennt die Ursache und ## weiß, welche Stunden im Tag sich lohnen. func test_spuren() -> void: abschnitt("Spuren in der Wohnung") if not main.auf_erde: main._erde() await get_tree().process_frame ui().schliessen() var sp: Player = main.player sp.folgt = null var mensch: Bewohner = welt().humans[0] var id := str(mensch.data["id"]) var heim := str(mensch.data["heim"]) var ursachen: Array = mensch.data["ursachen"] Game.active_mission = id Game.spuren.erase(id) var spuren: Array = [] for n in get_tree().get_nodes_in_group("interactable"): if n.get_meta("kind", "") == "spur" and str(n.get_meta("human")) == id: spuren.append(n) p(spuren.size() >= 1 and spuren.size() <= ursachen.size(), "In %s liegen %d Spuren, je Ursache höchstens eine (%d Ursachen)" % [HumanDB.PLACES[heim]["name"], spuren.size(), ursachen.size()]) var c := Orte.center(HumanDB.PLACES, heim) var g: Array = HumanDB.PLACES[heim]["size"] var drinnen := 0 for n in spuren: var q: Vector3 = (n as Node3D).global_position if absf(q.x - c.x) < float(g[0]) * 0.5 and absf(q.z - c.z) < float(g[1]) * 0.5 \ and absf(q.y) < 2.0: drinnen += 1 p(drinnen == spuren.size(), "Alle liegen in seiner Wohnung") if spuren.is_empty(): Game.active_mission = "" return var spur: Node3D = spuren[0] var u := str(spur.get_meta("arg")) p(not Game.ursache_bekannt(id, u), "Vorher ist die Ursache '%s' unbekannt" % Ursachen.LISTE[u]["name"]) var verdaechtig_vorher := 0 for nr in range(Game.slots(id).size()): if Game.slot_zustand(id, nr) == Game.Stunde.VERDAECHTIG: verdaechtig_vorher += 1 sp.global_position = spur.global_position + Vector3(0.8, 0.3, 0.8) sp.velocity = Vector3.ZERO main._process(0.05) p(main._fokus == spur and ui()._hinweis.text.contains("Spur"), "Vor der Spur zeigt der Hinweis sie an: %s" % ui()._hinweis.text) main._interagieren() p(Game.spur_bekannt(id, u) and Game.ursache_bekannt(id, u), "E liest die Spur: die Ursache '%s' steht im Journal" % Ursachen.LISTE[u]["name"]) var verdaechtig := 0 for nr in range(Game.slots(id).size()): if Game.slot_zustand(id, nr) == Game.Stunde.VERDAECHTIG: verdaechtig += 1 p(verdaechtig_vorher == 0 and verdaechtig >= 1, "Die Stunden dieser Ursache sind jetzt verdächtig (%d), gesehen sind sie nicht" % verdaechtig) main._process(0.05) ui()._ablauf_aktualisieren() p(ui()._ablauf_titel.text.contains("verdächtig"), "Die Zeitleiste sagt es: %s" % ui()._ablauf_titel.text) p(not Game.spur_lesen(id, u), "Noch einmal lesen bringt nichts Neues") Game.spuren.erase(id) Game.active_mission = "" ## Wer mit Mission zur Erde reist, landet vor der Tür seines Ziels; ein Knopf ## bringt ihn jederzeit dorthin. func test_ankunft() -> void: abschnitt("Ankunft beim Ziel") var ziel: Bewohner = welt().humans[0] var id := str(ziel.data["id"]) Game.active_mission = id main._traumwelt() await get_tree().process_frame main._erde() await get_tree().process_frame var nah: float = _xz(main.player.global_position).distance_to(_xz(ziel.global_position)) var tuer := Orte.door(HumanDB.PLACES, ziel.place) if HumanDB.PLACES.has(ziel.place) \ and float(HumanDB.PLACES[ziel.place]["size"][0]) > 0.0 else ziel.global_position var vor_tuer: float = _xz(main.player.global_position).distance_to(_xz(tuer)) p(minf(nah, vor_tuer) < 4.0 and main.player.global_position.y > -0.5, "Mit Mission landet man vor der Tür von %s (%.1f m)" % [ziel.data["name"], minf(nah, vor_tuer)]) var andere: Bewohner = welt().humans[3] main.hinfahren(str(andere.data["id"])) var d2: float = _xz(main.player.global_position).distance_to(_xz(andere.global_position)) p(d2 < 12.0 and main.auf_erde, "„Hinfahren“ setzt einen zu %s (%.1f m)" % [andere.data["name"], d2]) main._traumwelt() await get_tree().process_frame main.hinfahren(id) p(main.auf_erde, "Aus dem Traum heraus geht es ebenfalls, über das Tor") Game.active_mission = "" ## Die Akte: ein Blatt je Mensch mit Person, Traum, Befund, Tag und Notizen. func test_akte() -> void: abschnitt("Die Akte") var ziel: Bewohner = welt().humans[0] var id := str(ziel.data["id"]) var h := HumanDB.by_id(id) Game.active_mission = id Game.progress[id] = Game.progress.get(id, 0.0) Game.spuren.erase(id) Game.notizen.erase(id) ui().journal_panel("missionen") p(_knopf_da(str(h["name"])), "Im Journal liegt eine Akte je Mensch mit Mission") p(_druecke(str(h["name"])) and ui()._kind == "akte", "Sie lässt sich aufschlagen") var text := _panel_text() p(text.contains(str(h["name"])) and text.contains(str(h["traum"])) and text.contains(str(h["problem"])), "Oben stehen Person, Traum und was im Weg steht") p(text.contains("BEFUND") and text.contains("TAG") and text.contains("NOTIZEN"), "Darunter Befund, Tag und Notizen") p(text.contains("noch keine") and text.contains("noch nichts gefunden"), "Ohne Spuren und Beobachtungen ist der Befund leer") p(_knopf_da("Hinfahren"), "Ein Knopf bringt einen vor die Tür") await get_tree().process_frame await get_tree().process_frame var breit := 0.0 for c in _alle_kinder(ui()._content): if c is Control: breit = maxf(breit, (c as Control).size.x) p(ui()._drawer.size.x <= UI.DRAWER_BREITE + 1.0 and breit <= ui()._content.size.x + 1.0, "Nichts ragt aus der Leiste (Leiste %.0f, breitestes Element %.0f)" % [ui()._drawer.size.x, breit]) ui().journal_panel("techniken") p(not ui()._drawer.has_theme_stylebox_override("panel"), "Andere Seiten sind wieder dunkel") # keine Seite der Leiste darf breiter sein als die Leiste -- und die Leiste nicht als das Fenster var zu_breit: Array = [] for seite in ["techniken", "missionen"]: ui().journal_panel(seite) await get_tree().process_frame var mind := ui()._content.get_combined_minimum_size().x if mind > UI.DRAWER_BREITE - 30.0 or ui()._drawer.size.x > UI.DRAWER_BREITE + 1.0: zu_breit.append("%s (%.0f)" % [seite, mind]) ui().akte_panel(id) await get_tree().process_frame if ui()._content.get_combined_minimum_size().x > UI.DRAWER_BREITE - 30.0 \ or ui()._drawer.size.x > UI.DRAWER_BREITE + 1.0: zu_breit.append("akte (%.0f)" % ui()._content.get_combined_minimum_size().x) p(zu_breit.is_empty(), "Journal und Akte bleiben in der Leistenbreite%s" % ("" if zu_breit.is_empty() else " — zu breit: " + ", ".join(zu_breit))) var rand: float = ui()._drawer.position.x + ui()._drawer.size.x p(rand <= ui()._root.size.x, "Die Leiste endet im Fenster (%.0f von %.0f)" % [rand, ui()._root.size.x]) var hinweis_rechts := 0.0 for c in ui()._root.get_children(): if c is Label and (c as Label).text.begins_with("J Journal"): hinweis_rechts = (c as Control).position.x + (c as Control).size.x p(hinweis_rechts > 0.0 and hinweis_rechts <= ui()._root.size.x, "Der Steuerungshinweis endet im Fenster (%.0f von %.0f)" % [hinweis_rechts, ui()._root.size.x]) var u := str((h["ursachen"] as Array)[0]) Game.spur_lesen(id, u) ui().akte_panel(id) text = _panel_text() p(text.contains(str(Ursachen.LISTE[u]["name"])) and text.contains("Spur gelesen"), "Eine gelesene Spur steht als Befund: %s" % Ursachen.LISTE[u]["name"]) p(text.contains("Uhr —") and text.contains("Spur:"), "Und als Notiz mit Uhrzeit") ui().schliessen() main._traumwelt() await get_tree().process_frame ui().akte_panel(id) p(_druecke("Hinfahren") and main.auf_erde and not ui().offen, "„Hinfahren“ führt aus dem Traum vor die Tür von %s" % h["name"]) Game.active_mission = "" Game.spuren.erase(id) Game.notizen.erase(id) ## Passanten beleben die Straßen: ohne Namen, ohne Schild, ohne Mission. func test_passanten() -> void: abschnitt("Passanten beleben die Straßen") if not main.auf_erde: main._erde() await get_tree().process_frame p(welt().passanten.size() == HumanDB.PASSANTEN and welt().humans.size() == HumanDB.LIST.size(), "%d Passanten neben den %d Menschen mit Geschichte" % [welt().passanten.size(), welt().humans.size()]) var unterwegs := 0 var proben := 0 for i in range(1200): schritte(1, 0.05, 9.0 + float(i) * 0.004) if i % 100 == 99: proben += 1 for pz in welt().passanten: if not pz._path.is_empty(): unterwegs += 1 p(unterwegs >= proben, "Zu jeder Zeit ist im Schnitt mindestens einer unterwegs (%d in %d Proben)" % [unterwegs, proben]) var schilder := 0 for pz in welt().passanten: if (pz._label as Label3D).visible: schilder += 1 p(schilder == 0, "Passanten tragen kein Schild") var pz0: Bewohner = welt().passanten[0] main.player.global_position = pz0.global_position + Vector3(0, 0.3, 0) main._process(0.05) p(main._fokus_mensch != pz0, "Und kommen nicht in den Fokus") ## Die Traumwesen helfen: ein Bewohner flüstert, was in der Wohnung des ## Menschen liegt, um den es gerade geht. func test_wesen_helfen() -> void: abschnitt("Die Traumwesen helfen") var merker: Array = Game.completed.duplicate() Game.completed.clear() Game.completed.append("mensch_1") Game.welt_erzeugen() main._traumwelt() await get_tree().process_frame p(welt().bewohner.size() == 1, "Mit einem erfüllten Traum wohnt ein Wesen hier") var wesen: Bewohner = welt().bewohner[0] p(str(wesen.data["rolle"]).contains("Traum"), "Es trägt, aus wessen Traum es kam: %s" % wesen.data["rolle"]) var marke: Node3D = null for n in get_tree().get_nodes_in_group("interactable"): if n.get_meta("kind", "") == "wesen": marke = n p(marke != null, "Man kann es ansprechen") Game.active_mission = "mensch_0" Game.spuren.erase("mensch_0") main.player.set_mode(Player.Mode.TRAUM) main.player.global_position = wesen.global_position + Vector3(0.5, 0.3, 0.5) main.player.velocity = Vector3.ZERO main._process(0.05) p(main._fokus == marke and ui()._hinweis.text.contains("zuhören"), "Davor steht: zuhören") main._interagieren() var bekannt := 0 for u in Game.ursachen_von("mensch_0"): if Game.spur_bekannt("mensch_0", str(u)): bekannt += 1 p(bekannt == 1, "Es flüstert eine Spur des Menschen, um den es geht (%d bekannt)" % bekannt) Game.spuren.erase("mensch_0") Game.active_mission = "" Game.completed.assign(merker) Game.welt_erzeugen() main._erde() # die folgenden Abschnitte spielen auf der Erde await get_tree().process_frame 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"]) for i in range(3000): # die Welt bleibt bestehen: er kann weit weg sein schritte(1, 0.05, 2.0) if mensch.place == str(mensch.data["heim"]) and mensch._path.is_empty(): break p(mensch.activity == "schlaeft", "Um 02:00 schläft er (%s)" % mensch.activity) p(_xz(mensch.global_position).distance_to( _xz(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.kennt(str(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, main.erde.nav_map) _wege_pruefen("Traumwelt", DreamDB.PLACES, DreamDB.LIST, main.traum.nav_map) func _wege_pruefen(welt_name: String, places: Dictionary, liste: Array, karte: RID) -> 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, karte) / 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]) ## Weglänge über das Navigationsnetz der Welt -- so, wie die Menschen wirklich laufen. func _weg_laenge(places: Dictionary, von: String, nach: String, karte: RID) -> float: var a := Orte.center(places, von) var b := Orte.center(places, nach) # freie Plätze: neben der Mitte stehen, wie die Bewohner auch (in der Mitte steht der Traumbaum) if float(places[von]["size"][0]) <= 0.0: a += Vector3(2.2, 0, 0) if float(places[nach]["size"][0]) <= 0.0: b += Vector3(2.2, 0, 0) var pts := NavigationServer3D.map_get_path(karte, a, b, true) if pts.size() < 2: return 9999.0 # kein Weg: fällt als "passt nicht" auf var l := 0.0 for i in range(1, pts.size()): l += pts[i - 1].distance_to(pts[i]) 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_mask != 0, "Der Geist stößt an Wände wie ein Mensch") main.player.global_position = Vector3(0, 8.0, 0) main.player.velocity = Vector3(6, 0, 0) for i in range(60): await get_tree().physics_frame p(main.player.global_position.x > 0.5, "Der Geist bewegt sich durch den Park") p(main.player.global_position.y < 0.6 and main.player.is_on_floor(), "Die Schwerkraft holt ihn auf den Boden (y = %.2f)" % main.player.global_position.y) var lena: Bewohner = welt().humans[0] for i in range(1500): # erst, wenn sie irgendwo angekommen ist if lena._path.is_empty(): break schritte(1) var davor: Vector3 = lena.global_position var uhr_geist := Game.time_h main.player.global_position = lena.global_position + Vector3(0, 1, 0.5) schritte(10, 0.05, uhr_geist) p(lena.global_position.distance_to(davor) < 2.0, "Menschen nehmen den Geist nicht wahr") func _farbabstand(a: Color, b: Color) -> float: return Vector3(a.r, a.g, a.b).distance_to(Vector3(b.r, b.g, b.b)) func test_charakter() -> void: abschnitt("Der Geist und die Menschen sind am Körper lesbar") ui().schliessen() var sp: Player = main.player sp.set_mode(Player.Mode.GEIST) sp.look_enabled = true sp.folgt = null Game.situation = {} # ein laufender Moment ließe die Farbe flackern p(sp._augen.size() == 2 and sp._augen[0].position.z < 0.0, "Zwei Augen, vorn am Kopf") p(sp._funken.size() == Player.FUNKEN_MAX, "%d Funken stehen bereit" % Player.FUNKEN_MAX) for i in range(60): await get_tree().physics_frame var sichtbar := 0 for f in sp._funken: if f.visible: sichtbar += 1 var soll := clampi(Game.known.size(), 1, Player.FUNKEN_MAX) p(sichtbar == soll, "Je gelernter Technik kreist ein Funke (%d bei %d Techniken)" % [sichtbar, Game.known.size()]) p(_farbabstand(sp._farbe, Player.FARBE_GEIST) < 0.1, "Beim Beobachten leuchtet er kühl") var mensch: Bewohner = welt().humans[0] sp.global_position = mensch.global_position + Vector3(0, 1.5, 2.0) sp.folgt = mensch for i in range(90): await get_tree().physics_frame p(_farbabstand(sp._farbe, Player.FARBE_FOLGE) < 0.1, "Beim Verfolgen leuchtet er warm") sp.folgt = null sp.global_position = Vector3(0, 0.5, 9.0) # freier Rasen im Park sp.velocity = Vector3.ZERO sp.yaw = 0.0 Input.action_press("vor") for i in range(40): await get_tree().physics_frame Input.action_release("vor") var kopf: Vector3 = sp.body.to_global(Vector3(0, 1.1, 0)) var hinten := 0 for k in sp._schleppe: if k.visible and (k as Node3D).global_position.distance_to(kopf) > 0.3: hinten += 1 p(hinten >= 2, "Beim Fliegen zieht er einen Schweif hinter sich her (%d von 3)" % hinten) for i in range(40): await get_tree().physics_frame var ruht := true for k in sp._schleppe: if k.visible: ruht = false p(ruht, "Im Stand verschwindet der Schweif") # Haltung der Menschen: Angst duckt, Erschöpfung beugt -- bis überwunden var merker := Game.ueberwunden.duplicate(true) var muede: Bewohner = null var bang: Bewohner = null var frei: Bewohner = null for m in welt().humans: var pr: Array = m.data.get("probleme", []) if muede == null and pr.has("erschoepfung"): muede = m elif bang == null and pr.has("angst"): bang = m elif frei == null and not pr.has("angst") and not pr.has("erschoepfung"): frei = m p(muede != null and bang != null and frei != null, "Müde, ängstliche und freie Menschen sind da") if muede != null: Game.ueberwunden.erase(str(muede.data["id"])) if bang != null: Game.ueberwunden.erase(str(bang.data["id"])) schritte(80) p(muede != null and muede._ober.rotation.x < -0.25, "Erschöpfung beugt den Oberkörper (%.2f)" % (muede._ober.rotation.x if muede else 0.0)) p(bang != null and bang._ober.scale.x < 0.9, "Angst macht klein (%.2f)" % (bang._ober.scale.x if bang else 0.0)) p(frei != null and absf(frei._ober.rotation.x) < 0.05 and frei._ober.scale.x > 0.98, "Ohne diese Klemmen steht man aufrecht") if muede != null: Game.ueberwunden[str(muede.data["id"])] = {"erschoepfung": true} schritte(200) p(muede._ober.rotation.x > -0.05, "Überwunden richtet er sich auf (%.2f)" % muede._ober.rotation.x) Game.ueberwunden = merker # Hüte nach Beruf var kappen: Dictionary = {} for m in welt().humans: var typ := str(HumanDB.BERUF_TAGESTYP.get(str(m.data.get("beruf", "")), "")) kappen[typ] = m._ober.get_child_count() p(kappen.has("heim") and kappen.has("frueh") and kappen.has("buero") and int(kappen["frueh"]) > int(kappen["heim"]) and int(kappen["buero"]) > int(kappen["heim"]), "Berufe tragen verschiedene Kopfbedeckungen (%s)" % str(kappen)) p(mensch._arme.size() == 2, "Jeder hat zwei Arme") sp.velocity = Vector3.ZERO func _xz(v: Vector3) -> Vector2: return Vector2(v.x, v.z) func test_kamera_und_etagen() -> void: abschnitt("Feste Kamera, kleine Menschen, geschlossene Häuser") ui().schliessen() var sp: Player = main.player sp.set_mode(Player.Mode.GEIST) sp.look_enabled = true sp.folgt = null p(Input.mouse_mode == Input.MOUSE_MODE_VISIBLE, "Die Maus ist frei") p(is_equal_approx(sp.arm.rotation.x, Player.PITCH) and sp.cam.get_parent() == sp.arm, "Die Kamera schaut fest von schräg oben (%.0f°)" % rad_to_deg(-Player.PITCH)) sp.yaw = Player.CAM_YAW var yaw_vorher: float = sp.yaw var dreh := InputEventAction.new() dreh.action = "drehen" dreh.pressed = true Input.parse_input_event(dreh) for i in range(80): await get_tree().physics_frame p(absf(angle_difference(yaw_vorher + Player.DREHSCHRITT, sp.yaw)) < 0.05, "Q dreht die Kamera um 90° (%.0f°)" % rad_to_deg(sp.yaw - yaw_vorher)) var rad := InputEventMouseButton.new() rad.button_index = MOUSE_BUTTON_WHEEL_DOWN rad.pressed = true Input.parse_input_event(rad) for i in range(150): await get_tree().physics_frame p(absf(sp.cam.position.z - float(Player.ZOOM[1])) < 0.5, "Mausrad zoomt heraus (%.1f)" % sp.cam.position.z) rad = InputEventMouseButton.new() rad.button_index = MOUSE_BUTTON_WHEEL_UP rad.pressed = true Input.parse_input_event(rad) for i in range(150): await get_tree().physics_frame p(absf(sp.cam.position.z - float(Player.ZOOM[0])) < 0.5, "Und wieder heran (%.1f)" % sp.cam.position.z) var mensch: Bewohner = welt().humans[0] p(mensch._visual.scale.x < 0.85 and sp.body.scale.x < 0.7, "Menschen und Geist sind kleiner (%.2f / %.2f)" % [mensch._visual.scale.x, sp.body.scale.x]) # Geschlossene Häuser: einstöckig, ein Raum mit Tür var eng := 0 for key in HumanDB.PLACES: var sz: Array = HumanDB.PLACES[key]["size"] if float(sz[0]) > 0.0 and minf(float(sz[0]), float(sz[1])) < 6.0: eng += 1 p(eng == 0, "Jedes Gebäude ist breit genug für Tür und Möbel (%d zu schmal)" % eng) for b in welt()._bauten: for mi in Prims._meshes_von(b): var bm := (mi as MeshInstance3D).mesh as BoxMesh if bm != null and (bm.size.x <= 0.0 or bm.size.y <= 0.0 or bm.size.z <= 0.0): eng += 1 p(eng == 0, "Keine Platte mit negativer Größe") var oben: Bewohner = welt().humans[0] var heim := str(oben.data["heim"]) var c := Orte.center(HumanDB.PLACES, heim) var g: Array = HumanDB.PLACES[heim]["size"] var uhr := Game.time_h var zuhause := false for i in range(3000): schritte(1, 0.05, 2.0) if oben.place == heim and oben._path.is_empty(): zuhause = true break p(zuhause and absf(oben.global_position.y) < 0.3, "Nachts liegt %s zu Hause, ebenerdig (y = %.1f)" % [oben.data["name"], oben.global_position.y]) # Schnitt wie bei den Sims: drinnen verschwindet das Dach, alle Wände werden Stümpfe var bau: Dictionary = {} for b in welt()._bauten: if (b["c"] as Vector3).distance_to(c) < 0.1: bau = b p(not bau.is_empty() and (bau["teile"] as Array).size() >= 1 and (bau["waende"] as Array).size() >= 4, "Sein Haus steht mit Dach und Wänden da") sp.global_position = c + Vector3(0, 0.3, 0) sp.velocity = Vector3.ZERO sp.yaw = Player.CAM_YAW await get_tree().physics_frame welt()._process(0.05) var stuempfe := 0 var hoch := 0 for w in bau["waende"]: var bm := (w as MeshInstance3D).mesh as BoxMesh if (w as Node3D).visible and bm.size.y <= Prims.STUMPF + 0.01: stuempfe += 1 elif (w as Node3D).visible: hoch += 1 p(not (bau["teile"][0] as Node3D).visible, "Steht der Geist im Haus, verschwindet das Dach") p(stuempfe >= 4 and hoch == 0, "Alle Wände sind kniehohe Stümpfe, keine bleibt hoch, nichts wird durchsichtig (%d Stümpfe)" % stuempfe) var durchsichtig := 0 for b in welt()._bauten: for mi in Prims._meshes_von(b): if (mi as GeometryInstance3D).transparency > 0.0: durchsichtig += 1 p(durchsichtig == 0, "Kein Bau ist durchsichtig") # nichts über Kniehöhe verdeckt drinnen den Blick von oben: kein Band, keine Decke var hoch_sichtbar: Array = [] var knoten: Array = (bau["teile"] as Array).duplicate() knoten.append(bau["raum"]) for k in knoten: for mi in (k as Node).find_children("*", "MeshInstance3D", true, false): var m := mi as MeshInstance3D if not m.is_visible_in_tree(): continue var kasten := m.get_aabb() var kante: float = m.global_position.y + kasten.end.y * m.scale.y var ist_wand := (bau["waende"] as Array).has(m) if kante > 1.9 and not ist_wand: hoch_sichtbar.append("%s (bis %.1f m)" % [m.name, kante]) p(hoch_sichtbar.is_empty(), "Drinnen verdeckt nichts über Kniehöhe die Sicht%s" % ("" if hoch_sichtbar.is_empty() else " — " + ", ".join(hoch_sichtbar))) sp.global_position = c + Vector3(float(g[0]) + 4.0, 0.3, 0) await get_tree().physics_frame welt()._process(0.05) var alles := (bau["teile"][0] as Node3D).visible for w in bau["waende"]: var bm := (w as MeshInstance3D).mesh as BoxMesh if not (w as Node3D).visible or bm.size.y <= Prims.STUMPF + 0.01: alles = false # der Türsturz ist niedrig, aber kein Stumpf p(alles, "Draußen steht das Haus wieder ganz, Wände in voller Höhe") var hoechster_bau := 0.0 for b in welt()._bauten: hoechster_bau = maxf(hoechster_bau, float(b["hoch"])) p(hoechster_bau <= 3.5, "Kein Bau ist höher als ein Erdgeschoss (%.1f m)" % hoechster_bau) # Klick auf einen Menschen sp.global_position = oben.global_position + Vector3(2.0, 1.4, 2.0) sp.velocity = Vector3.ZERO await get_tree().physics_frame var schirm := sp.cam.unproject_position(oben.global_position + Vector3(0, 0.8, 0)) main._klick(schirm) p(sp.folgt == oben, "Ein Klick auf einen Menschen hängt den Geist an ihn") main._klick(schirm) p(sp.folgt == null, "Noch ein Klick löst ihn wieder") # Anlauf: von weit weg zu Fuß über die Straße bis in seine Wohnung for m in welt().humans: m.nav_fehl = 0 # frühere Tests haben Leute versetzt var daheim := false for i in range(3000): schritte(1, 0.05, 2.0) if oben.place == heim and oben._path.is_empty(): daheim = true break sp.global_position = Vector3(0, 0.3, 12.0) # im Park, weit weg sp.velocity = Vector3.ZERO sp.spruenge = 0 sp.anhaengen(oben) await get_tree().physics_frame await get_tree().physics_frame p(daheim and sp._leine_pfad.size() >= 4, "Von weit weg plant das Navigationsnetz den Weg bis in seine Wohnung (%d Punkte)" % sp._leine_pfad.size()) var angekommen := false for i in range(2400): await get_tree().physics_frame if sp.global_position.distance_to(oben.global_position) < 4.0: angekommen = true break p(angekommen and sp.spruenge == 0, "Und kommt zu Fuß bei ihm an, ohne dass die Leine nachziehen muss (%d Sprünge, Abstand %.1f)" % [sp.spruenge, sp.global_position.distance_to(oben.global_position)]) # Schilder: nur wer markiert ist schritte(2) var fremde_schilder := 0 for m in welt().humans: if m != oben and (m._label as Label3D).visible and not Game.markiert.has(str(m.data["id"])): fremde_schilder += 1 p((oben._label as Label3D).visible and fremde_schilder == 0, "Nur der Verfolgte trägt ein Schild, die anderen nicht (%d fremde)" % fremde_schilder) sp.folgt = null var ohne_netz := 0 for m in welt().humans: ohne_netz += int(m.nav_fehl) p(ohne_netz == 0, "Alle Wege der Menschen kamen vom Navigationsnetz (%d Rückfälle)" % ohne_netz) Game.time_h = uhr ## Wie ein Spieler, der einfach losläuft: 90 Sekunden kreuz und quer durch die ## Stadt, bei Wand Richtung wechseln. Dabei darf er nie hängen bleiben, nie ## unter den Boden und nie über die Dächer geraten. func test_zufallslauf() -> void: abschnitt("Freies Herumlaufen: nie eingeklemmt, nie unter dem Boden") if not main.auf_erde: main._erde() await get_tree().process_frame ui().schliessen() var sp: Player = main.player sp.set_mode(Player.Mode.GEIST) sp.look_enabled = true sp.folgt = null sp.spruenge = 0 sp.global_position = Vector3(0, 0.3, 12.0) sp.velocity = Vector3.ZERO var rng := RandomNumberGenerator.new() rng.seed = 4711 var richtungen := [["vor"], ["vor", "rechts"], ["rechts"], ["zurueck", "rechts"], ["zurueck"], ["zurueck", "links"], ["links"], ["vor", "links"]] var kurs := 0 var letzte := sp.global_position var tiefster := 0.0 var hoechster := 0.0 var still := 0 var laengste_still := 0 var gesamt := 0.0 for runde in range(150): for t in richtungen[kurs]: Input.action_press(t) var gemeldet := false for i in range(36): await get_tree().physics_frame if sp.global_position.y < -0.2 and not gemeldet: gemeldet = true var k := sp.get_last_slide_collision() var txt := "-" if k != null and k.get_collider() != null: txt = str((k.get_collider() as Node).get_parent().global_position) print(" [unter dem Boden] Runde %d, %s, Kollision bei %s" % [runde, str(sp.global_position), txt]) for t in richtungen[kurs]: Input.action_release(t) var bewegt := _xz(sp.global_position).distance_to(_xz(letzte)) if bewegt < 0.3 and still >= 2: print(" [steht] Runde %d bei %s, Kurs %d" % [runde, str(sp.global_position), kurs]) gesamt += bewegt if bewegt < 0.3: still += 1 laengste_still = maxi(laengste_still, still) kurs = (kurs + 3 + rng.randi_range(0, 2)) % 8 # umdrehen, leicht versetzt else: still = 0 if rng.randf() < 0.2: kurs = rng.randi_range(0, 7) letzte = sp.global_position tiefster = minf(tiefster, sp.global_position.y) hoechster = maxf(hoechster, sp.global_position.y) p(gesamt > 200.0, "Er kommt herum (%.0f m in 90 s)" % gesamt) p(laengste_still <= 3, "Nie länger als drei Züge ohne Bewegung (längste Pause %d)" % laengste_still) p(tiefster > -0.5, "Nie unter dem Boden (tiefster y %.2f)" % tiefster) p(hoechster < 6.0, "Nie über den Dächern (höchster y %.1f)" % hoechster) p(sp.spruenge == 0, "Nie eingeklemmt -- die Stadt musste ihn nicht befreien (%d)" % sp.spruenge) var schwebend := 0 for m in welt().humans: if m._path.is_empty() and m.global_position.y > 0.4: schwebend += 1 p(schwebend == 0, "Kein stehender Mensch schwebt über dem Boden (%d)" % schwebend) ## Bei hohem Tempo wird der Schritt je Bild größer als die Ankunftsschwelle. ## Wer dann über den Wegpunkt hinausläuft, pendelt um ihn herum statt anzukommen. func test_gehen_bei_tempo() -> void: abschnitt("Gehen bleibt auch im Schnelllauf ruhig") ui().schliessen() var mensch: Bewohner = welt().humans[0] var stufe_kehrt: Dictionary = {} for stufe in [1, 3]: Game.tempo_setzen(stufe) Game.time_h = 12.9 for i in range(400): schritte(1, 1.0 / 60.0) if not mensch._path.is_empty(): break var kehrt := 0 var bewegt := 0 var vorher: Vector3 = mensch.global_position var richtung := Vector3.ZERO for i in range(600): schritte(1, 1.0 / 60.0) if mensch._path.is_empty(): continue var d: Vector3 = mensch.global_position - vorher d.y = 0.0 if d.length() > 0.001: bewegt += 1 if richtung != Vector3.ZERO and d.normalized().dot(richtung) < -0.5: kehrt += 1 richtung = d.normalized() vorher = mensch.global_position stufe_kehrt[stufe] = kehrt var weite := Bewohner.GEHTEMPO * (1.0 / 60.0) * Game.faktor_von(stufe) p(kehrt <= 4, "Tempo %d (%d×, %.2f je Bild): %d Kehrtwenden in %d Bildern" % [stufe, int(Game.faktor_von(stufe)), weite, kehrt, bewegt]) p(int(stufe_kehrt[3]) <= int(stufe_kehrt[1]) + 4, "Schnelllauf zappelt nicht mehr als Normaltempo (%d gegen %d)" % [stufe_kehrt[3], stufe_kehrt[1]]) Game.tempo_setzen(1) ## Sich an einen Menschen hängen: der Geist begleitet ihn durch seinen Tag, ## statt ihm hinterherzulaufen. Damit bleibt er die ganze Zeit beobachtbar. func test_verfolgung() -> void: abschnitt("Als Geist einen Menschen begleiten") p(InputMap.has_action("folgen") and not InputMap.action_get_events("folgen").is_empty(), "Eigene Taste fürs Folgen") ui().schliessen() main.player.set_mode(Player.Mode.GEIST) main.player.look_enabled = true main.player.yaw = 0.0 var mensch: Bewohner = welt().humans[0] Game.active_mission = str(mensch.data["id"]) # kurz vor seinem nächsten Ortswechsel einsteigen, damit er auch losgeht var routine: Array = mensch.data["routine"] for k in range(routine.size()): if str(routine[k][1]) != mensch.place and float(routine[k][0]) > 0.5: Game.time_h = float(routine[k][0]) - 0.4 break schritte(5) main.player.global_position = mensch.global_position + Vector3(0, 0.3, 0) main.player.velocity = Vector3.ZERO main._process(0.05) p(main._fokus_mensch == mensch, "Der Mensch ist im Fokus") main._verfolgen() p(main.player.folgt == mensch, "F hängt den Geist an ihn") # Er lebt seinen Tag weiter, der Geist bleibt dran var weiteste := 0.0 var gelaufen := false var im_fokus := 0 for i in range(2400): await get_tree().physics_frame # nur die Engine steppt: sonst laufen die Menschen doppelt if not mensch._path.is_empty(): gelaufen = true if main._fokus_mensch == mensch: im_fokus += 1 weiteste = maxf(weiteste, main.player.global_position.distance_to(mensch.global_position)) p(gelaufen, "Er wechselt in dieser Zeit den Ort") p(weiteste < 8.0, "Der Geist bleibt die ganze Zeit dicht dabei (höchstens %.1f)" % weiteste) p(im_fokus > 2000, "Und behält ihn im Fokus (%d von 2400 Schritten)" % im_fokus) p(main._fokus_mensch == mensch or main.player.folgt == mensch, "Unterwegs bleibt er notierbar") # Eigene Bewegung löst die Verfolgung Input.action_press("vor") for i in range(10): await get_tree().physics_frame Input.action_release("vor") p(main.player.folgt == null, "Wer selbst losgeht, lässt ihn los") # Die Taste löst ebenfalls main.player.global_position = mensch.global_position + Vector3(0, 0.3, 0) main.player.velocity = Vector3.ZERO main._process(0.05) main._verfolgen() p(main.player.folgt == mensch, "Wieder angehängt") main._verfolgen() p(main.player.folgt == null, "F lässt wieder los") # In der Traumwelt gibt es niemanden zu verfolgen main._traumwelt() await get_tree().process_frame main._verfolgen() p(main.player.folgt == null, "In der Traumwelt hängt sich der Läufer an niemanden") main._erde() await get_tree().process_frame ## Woran der Traum scheitert, steht nirgends. Man findet es, indem man ## dabeisteht -- awareness training, bevor irgendetwas ersetzt wird. func test_diagnose() -> void: abschnitt("Diagnose — die Ursache zeigt sich im Tag") var id := "mensch_0" Game.active_mission = id Game.systeme.clear() Game.erkannt_slots.clear() Game.notiz_sperre = 0.0 var urs: Array = Game.ursachen_von(id) p(urs.size() >= 2 and urs.size() <= 3, "%s scheitert an %d Ursachen: %s" % [HumanDB.by_id(id)["name"], urs.size(), str(urs)]) var alle_bekannt := true for u in urs: if not Ursachen.LISTE.has(u): alle_bekannt = false p(alle_bekannt, "Alle davon stehen im Katalog") # Zu jeder Ursache eine Tätigkeit im Tag var luecken: Array = [] for u in urs: var da := false for nr in range(Game.slots(id).size()): if Ursachen.ursache_von(Game.slot_grund(id, nr)) == str(u): da = true if not da: luecken.append(str(u)) p(luecken.is_empty(), "Jede Ursache zeigt sich in einer Tätigkeit%s" % ("" if luecken.is_empty() else " — fehlt: " + ", ".join(luecken))) # Ein Blocker-Slot ist zunächst verdeckt, ein neutraler nicht var blocker_nr := -1 var neutral_nr := -1 for nr in range(Game.slots(id).size()): var g := Game.slot_grund(id, nr) if Ursachen.ist_blocker(g) and blocker_nr < 0: blocker_nr = nr elif g != "schlaeft" and not Ursachen.ist_blocker(g) and neutral_nr < 0: neutral_nr = nr p(blocker_nr >= 0 and neutral_nr >= 0, "Ihr Tag hat Blocker und freie Slots") p(not Game.erkannt(id, blocker_nr), "Was im Blocker-Slot läuft, ist verdeckt") p(Game.erkannt(id, neutral_nr), "Ein freier Slot ist offen sichtbar") var ursache := Ursachen.ursache_von(Game.slot_grund(id, blocker_nr)) p(not Game.ursache_bekannt(id, ursache), "Und die Ursache ist noch unbekannt") # Solange man nicht lange genug dabeisteht, passiert nichts # Zusehen allein deckt nichts auf -- der Spieler muss den Moment benennen for i in range(60): schritte(1) p(not Game.erkannt(id, blocker_nr), "Bloßes Dabeistehen deckt nichts auf") p(Game.notieren(id, neutral_nr) == "daneben", "Im freien Slot geht die Notiz daneben") p(Game.notiz_sperre > 0.0, "Ein Fehlgriff sperrt kurz (%.0f s)" % Game.notiz_sperre) p(Game.notieren(id, blocker_nr) == "gesperrt", "Solange nimmt er keine Notiz an") Game.notiz_sperre = 0.0 p(Game.notieren(id, blocker_nr) == "treffer", "Im Blocker-Slot sitzt sie") p(Game.erkannt(id, blocker_nr), "Und deckt auf, was dort läuft") p(is_equal_approx(Game.notiz_sperre, 0.0), "Ein Treffer sperrt nicht") p(Game.notieren(id, blocker_nr) == "bekannt", "Zweimal notieren bringt nichts Neues") p(Game.ursache_bekannt(id, ursache), "Und damit die Ursache: %s" % Ursachen.LISTE[ursache]["name"]) # Ohne Diagnose lässt sich nichts einrichten Game.known = {"pomodoro": 0, "zerlegen": 0, "box_atmung": 0, "nein_sagen": 0, "habit_stack": 0, "reframing": 0} var zweiter := -1 for nr in range(Game.slots(id).size()): if Ursachen.ist_blocker(Game.slot_grund(id, nr)) and nr != blocker_nr: zweiter = nr if zweiter >= 0: var pr := Game.system_pruefen(id, zweiter, "pomodoro") p(not pr["geht"] and str(pr["grund"]).contains("gesehen"), "In einen unerkannten Slot lässt sich nichts bauen") ## Gegen eine Ursache baut man ein System. Passt die Belohnung, hält es besser. ## Die zweite Ebene: Probleme hängen nicht an Stunden, sondern an Momenten. ## Gewohnheiten laufen ohne dich weiter -- hier musst du dabei sein. func test_probleme_und_momente() -> void: abschnitt("Probleme zeigen sich in Momenten") var id := "mensch_0" Game.active_mission = id Game.situation = {} Game.situation_pause = 0.0 Game.probleme_bekannt.clear() Game.problem_stand.clear() Game.ueberwunden.clear() Game.cooldown.clear() ui().schliessen() var prob: Array = Game.probleme_von(id) p(prob.size() >= 1 and prob.size() <= 2, "%s trägt %d Problem(e): %s" % [HumanDB.by_id(id)["name"], prob.size(), str(prob)]) var alle_bekannt := true for x in prob: if not Probleme.problem_da(str(x)): alle_bekannt = false p(alle_bekannt, "Alle stehen im Katalog") var ohne_situation: Array = [] for x in Probleme.LISTE: if Probleme.situationen_von(str(x)).is_empty(): ohne_situation.append(str(x)) p(ohne_situation.is_empty(), "Jedes Problem zeigt sich in Situationen%s" % ("" if ohne_situation.is_empty() else " — außer " + ", ".join(ohne_situation))) var ohne_technik: Array = [] for x in Probleme.LISTE: if Probleme.techniken_gegen(str(x)).is_empty(): ohne_technik.append(str(x)) p(ohne_technik.is_empty(), "Und gegen jedes gibt es Techniken%s" % ("" if ohne_technik.is_empty() else " — außer " + ", ".join(ohne_technik))) # Sie kommt von allein, aber nicht sofort p(not Game.situation_laeuft(), "Erst läuft kein Moment") Game.situationen_ticken(1.0, false) p(not Game.situation_laeuft(), "Bei einem Schlafenden kommt keiner") Game.situationen_ticken(1.0, true) p(Game.situation_laeuft(), "Bei einem wachen Menschen bricht einer auf") var problem := Game.situation_problem() p(prob.has(problem), "Und gehört zu einem seiner Probleme: %s" % problem) # Solange sie läuft, steht der Traum still Game.progress[id] = 10.0 Game.traum_voranbringen(6.0) p(is_equal_approx(Game.mission_progress(id), 10.0), "Im Moment kommt der Traum nicht voran (%.1f %%)" % Game.mission_progress(id)) # Dabeisein zeigt, was dahintersteckt p(not Game.problem_bekannt(id, problem), "Was dahintersteckt, ist noch unbekannt") Game.situation_sehen() p(Game.problem_bekannt(id, problem), "Dabeisein zeigt es: %s" % Probleme.name_von(problem)) # Eine unpassende Technik greift nicht var passend := "" var unpassend := "" for tid in TechDB.ids(): if Probleme.hilft(tid, problem) and passend == "": passend = tid elif not Probleme.hilft(tid, problem) and unpassend == "": unpassend = tid Game.known[passend] = 0 Game.known[unpassend] = 0 var res_falsch := Game.situation_anwenden(unpassend) p(res_falsch["ok"] and not res_falsch["treffer"], "%s greift bei %s nicht" % [TechDB.by_id(unpassend)["name"], Probleme.name_von(problem)]) p(Game.situation_laeuft(), "Der Moment läuft weiter") # Die passende beendet ihn var vorher: float = Game.mission_progress(id) var res := Game.situation_anwenden(passend) p(res["treffer"], "%s trägt hindurch" % TechDB.by_id(passend)["name"]) p(not Game.situation_laeuft(), "Der Moment ist vorbei") p(Game.mission_progress(id) > vorher, "Und bringt den Traum voran (%.0f → %.0f %%)" % [vorher, Game.mission_progress(id)]) p(Game.problem_fortschritt(id, problem) == 1, "Einmal geschafft") # Oft genug, und das Problem kommt nicht wieder for i in range(Game.PROBLEM_UEBERWINDEN): Game.situation = {"mensch": id, "key": Probleme.situationen_von(problem)[0], "problem": problem, "rest": Game.SITUATION_DAUER} Game.cooldown.clear() Game.situation_anwenden(passend) p(Game.problem_ueberwunden(id, problem), "Nach %d Malen ist %s überwunden" % [Game.PROBLEM_UEBERWINDEN, Probleme.name_von(problem)]) Game.situation = {} Game.situation_pause = 0.0 var kommt_wieder := false for i in range(200): Game.situationen_ticken(1.0, true) if Game.situation_laeuft() and Game.situation_problem() == problem: kommt_wieder = true break p(not kommt_wieder, "Dieser Moment kommt nicht wieder") # Beide Ebenen stehen nebeneinander im HUD Game.situation = {} ui()._hud_aktualisieren() p(ui()._ablauf.visible and ui()._probleme.visible, "Tagesablauf und Momente stehen beide im HUD") p(ui()._probleme.text.contains(Probleme.name_von(problem)), "Die Momente nennen das Problem: %s" % ui()._probleme.text.replace("\n", " / ")) Game.situation = {"mensch": id, "key": Probleme.situationen_von(problem)[0], "problem": problem, "rest": 9.0} ui()._hud_aktualisieren() p(ui()._probleme.text.contains("Jetzt:"), "Der laufende Moment steht oben") # Und er kündigt sich am Menschen selbst an, nicht nur im HUD var wer: Bewohner = null for mm in welt().humans: if mm.data["id"] == id: wer = mm p(wer.beschriftung().contains(str(Probleme.SITUATIONEN[Game.situation["key"]]["name"])), "Am Menschen steht, was gerade aufbricht: %s" % wer.beschriftung().replace("\n", " / ")) Game.situation = {} p(not wer.beschriftung().contains("»"), "Ohne Moment steht dort nichts davon") # E führt in den Moment, sonst in den Tagesablauf -- die zwei Ebenen # teilen sich eine Taste, ohne sich in die Quere zu kommen var mensch: Bewohner = null for mm in welt().humans: if mm.data["id"] == id: mensch = mm main.player.set_mode(Player.Mode.GEIST) main.player.folgt = null main.player.global_position = mensch.global_position + Vector3(0, 1.5, 1.5) Game.situation = {} main._process(0.05) main._interagieren() p(ui()._kind == "anwenden", "Ohne Moment führt E in den Tagesablauf (%s)" % ui()._kind) ui().schliessen() Game.situation = {"mensch": id, "key": Probleme.situationen_von(problem)[0], "problem": problem, "rest": 12.0} main._process(0.05) main._interagieren() p(ui()._kind == "situation", "Im Moment führt dieselbe Taste dorthin (%s)" % ui()._kind) p(_panel_text().contains(str(Probleme.SITUATIONEN[Game.situation["key"]]["was"])), "Das Panel zeigt, was gerade passiert") ui().schliessen() # Die Gewohnheitsebene läuft davon unberührt weiter Game.situation = {} Game.progress[id] = 0.0 Game.traum_voranbringen(24.0) p(Game.mission_progress(id) > 0.0, "Ohne Moment liefert der Tagesablauf wieder sein Tempo (%.0f %%)" % Game.mission_progress(id)) Game.situation = {} Game.ueberwunden.clear() Game.probleme_bekannt.clear() Game.problem_stand.clear() ## Eigene Menschen: aus Wörtern wird eine Psyche, aus dem Beruf ein Alltag, ## und beides lässt sich am Brett schreiben, speichern und wieder abnehmen. func test_eigene_menschen() -> void: abschnitt("Eigene Menschen aus Wörtern") Aushaenge.PFAD = "user://aushaenge_test.cfg" Aushaenge.alle_loeschen() # --- Der Content ist gekoppelt: nichts ist unlösbar var ohne_technik: Array = [] for b in Ursachen.BLOCKER: if TechDB.fuer_ursache(Ursachen.ursache_von(str(b))).is_empty(): ohne_technik.append(str(b)) p(ohne_technik.is_empty(), "Jeder der %d Blocker hat Techniken gegen seine Ursache%s" % [Ursachen.BLOCKER.size(), "" if ohne_technik.is_empty() else " — außer " + ", ".join(ohne_technik)]) var ohne: Array = [] for pr in Probleme.LISTE: if Probleme.techniken_gegen(str(pr)).is_empty() or Probleme.situationen_von(str(pr)).is_empty(): ohne.append(str(pr)) p(ohne.is_empty(), "Jede der %d Klemmen hat Techniken und Situationen%s" % [Probleme.LISTE.size(), "" if ohne.is_empty() else " — außer " + ", ".join(ohne)]) var tage: Dictionary = {} for tid in TechDB.ids(): var t := TechDB.by_id(tid) if str(t["was"]) == "" or str(t["wann"]) == "" or str(t["warum"]) == "" or str(t["woran"]) == "": tage[tid] = true p(tage.is_empty(), "Alle %d Techniken tragen Was, Wann, Warum, Woran" % TechDB.LIST.size()) # --- Die Wörter erreichen jeden Eintrag des Modells var erreicht := {"ursachen": {}, "blocker": {}, "probleme": {}, "verhalten": {}} var leer: Array = [] for w in Woerter.ids(): var e: Dictionary = Woerter.LISTE[w] var wirkt := false for u in e.get("ursachen", {}): erreicht["ursachen"][u] = true wirkt = true for b in e.get("blocker", []): erreicht["blocker"][b] = true wirkt = true for pr in e.get("probleme", {}): erreicht["probleme"][pr] = true wirkt = true for v in e.get("verhalten", {}): erreicht["verhalten"][v] = true wirkt = true if not wirkt: leer.append(w) p(Woerter.ids().size() >= 50, "%d Wörter stehen zur Wahl" % Woerter.ids().size()) p(leer.is_empty(), "Jedes Wort wirkt irgendwo%s" % ("" if leer.is_empty() else " — außer " + ", ".join(leer))) p(erreicht["ursachen"].size() == Ursachen.LISTE.size(), "Die Wörter erreichen alle %d Ursachen" % Ursachen.LISTE.size()) p(erreicht["blocker"].size() == Ursachen.BLOCKER.size(), "Alle %d Blocker (%d)" % [Ursachen.BLOCKER.size(), erreicht["blocker"].size()]) p(erreicht["probleme"].size() == Probleme.LISTE.size(), "Alle %d Klemmen" % Probleme.LISTE.size()) p(erreicht["verhalten"].size() == Woerter.VERHALTEN.size(), "Und alle %d Verhaltenswerte" % Woerter.VERHALTEN.size()) var unbekannt: Array = [] for w in Woerter.ids(): var e: Dictionary = Woerter.LISTE[w] for b in e.get("blocker", []): if not Ursachen.BLOCKER.has(b): unbekannt.append("%s->%s" % [w, b]) for pr in e.get("probleme", {}): if not Probleme.LISTE.has(pr): unbekannt.append("%s->%s" % [w, pr]) for u in e.get("ursachen", {}): if not Ursachen.LISTE.has(u): unbekannt.append("%s->%s" % [w, u]) p(unbekannt.is_empty(), "Kein Wort zeigt ins Leere%s" % ("" if unbekannt.is_empty() else ": " + ", ".join(unbekannt))) # --- Aus Wörtern wird ein Bild var bild := Woerter.profil(["aufschiebend", "perfektionistisch", "stur", "aengstlich", "nachtaktiv", "zurueckgezogen"]) p(bild["ursachen"].size() >= 2 and bild["ursachen"].size() <= 3, "Zwei bis drei Ursachen: %s" % str(bild["ursachen"])) p(bild["ursachen"].has("anfang"), "Aufschiebend und perfektionistisch ergeben »kein Anfang«") p(bild["probleme"].size() >= 1 and bild["probleme"].size() <= 2, "Ein bis zwei Klemmen: %s" % str(bild["probleme"])) p(bild["probleme"].has("angst") or bild["probleme"].has("perfektionismus"), "Ängstlich und perfektionistisch schlagen durch") p(bild["blocker"].size() >= 2, "Blocker zu den Ursachen: %s" % str(bild["blocker"])) var fremd := false for b in bild["blocker"]: if not bild["ursachen"].has(Ursachen.ursache_von(str(b))): fremd = true p(not fremd, "Kein Blocker ohne passende Ursache") p(int(bild["verhalten"]["abwehr"]) >= 2, "Stur und perfektionistisch: Abwehr %d" % int(bild["verhalten"]["abwehr"])) p(int(bild["verhalten"]["sichtbar"]) < 0, "Zurückgezogen: schwer zu durchschauen (%d)" % int(bild["verhalten"]["sichtbar"])) var gegen := Woerter.profil(["ordentlich", "chaotisch", "geduldig", "ungeduldig", "gelassen", "reizbar"]) p(int(gegen["verhalten"]["guete"]) == 0 and int(gegen["verhalten"]["momente"]) == 0, "Widersprüche heben sich auf (Güte %d, Momente %d)" % [int(gegen["verhalten"]["guete"]), int(gegen["verhalten"]["momente"])]) var wenig := Woerter.profil(["heiter"]) p(wenig["ursachen"].size() >= 2 and wenig["probleme"].size() >= 1, "Auch ein Wort ergibt einen ganzen Menschen (%s / %s)" % [str(wenig["ursachen"]), str(wenig["probleme"])]) # --- Ein Mensch aus einem Aushang, mit eigenem Alltag var pr := {"name": "Mo Test", "alter": 33, "beruf": "Bäcker", "traum": "Wieder Gitarre spielen.", "geschichte": "Nach der Schicht ist nichts übrig.", "woerter": ["erschoepft", "bequem", "konfliktscheu", "gruendlich", "stur", "aengstlich"]} p(Aushaenge.pruefen(pr).is_empty(), "Der Aushang ist vollständig") p(not Aushaenge.pruefen({"name": "X", "woerter": ["stur"]}).is_empty(), "Ein lückenhafter wird abgewiesen: %s" % ", ".join(Aushaenge.pruefen({"name": "X", "woerter": ["stur"]}))) var id := Aushaenge.speichern(pr) p(id.begins_with("eigen_"), "Gespeichert als %s" % id) p(Aushaenge.laden().size() == 1 and Aushaenge.laden()[0]["name"] == "Mo Test", "Und aus der Datei zurückgelesen") Game.completed.clear() Game.welt_erzeugen() var m := HumanDB.by_id(id) p(not m.is_empty(), "Er lebt in der Stadt") p(HumanDB.LIST.size() == HumanDB.EINWOHNER, "Die Stadt bleibt bei %d Einwohnern" % HumanDB.EINWOHNER) p(HumanDB.PLACES.has(str(m["heim"])), "Er hat ein Zuhause: %s" % m["heim"]) var frueh := false for e in m["routine"]: if int(e[0]) == 4 and str(e[1]) == "baeckerei": frueh = true p(frueh, "Als Bäcker steht er um vier in der Backstube") var luecken := 0 for stunde in range(24): if Game.slot_bei_stunde(id, stunde) < 0: luecken += 1 p(luecken == 0, "Sein Tag ist lückenlos") var blocker_im_tag: Array = [] for e in m["routine"]: if Ursachen.ist_blocker(str(e[2])): blocker_im_tag.append(str(e[2])) p(blocker_im_tag.size() >= 2 and blocker_im_tag.size() <= HumanDB.BLOCKER_MAX, "Zwei bis %d Blocker stehen im Tag: %s" % [HumanDB.BLOCKER_MAX, ", ".join(blocker_im_tag)]) var passt_alle := true for b in blocker_im_tag: if not (m["ursachen"] as Array).has(Ursachen.ursache_von(b)): passt_alle = false p(passt_alle, "Jeder davon gehört zu einer seiner Ursachen") var ohne_blocker: Array = [] for u in m["ursachen"]: var da := false for b in blocker_im_tag: if Ursachen.ursache_von(b) == str(u): da = true if not da: ohne_blocker.append(str(u)) p(ohne_blocker.is_empty(), "Und jede seiner Ursachen zeigt sich im Tag%s" % ("" if ohne_blocker.is_empty() else " — nicht: " + ", ".join(ohne_blocker))) # Das gilt für jeden Menschen der Stadt, sonst bremst eine Ursache für immer var stumm: Array = [] for hh in HumanDB.LIST: for u in hh["ursachen"]: var zeigt := false for e in hh["routine"]: if Ursachen.ursache_von(str(e[2])) == str(u): zeigt = true if not zeigt: stumm.append("%s/%s" % [hh["name"], u]) p(stumm.is_empty(), "Bei allen %d Menschen zeigt sich jede Ursache im Tag%s" % [HumanDB.LIST.size(), "" if stumm.is_empty() else " — nicht: " + ", ".join(stumm)]) p(Game.machbar(id) or true, "Bereit fürs Brett") main._erde() await get_tree().process_frame _wege_pruefen("Stadt mit eigenem Menschen", HumanDB.PLACES, HumanDB.LIST, main.erde.nav_map) # --- Die Wörter verändern, wie er sich verhält p(Game.verhalten(id, "abwehr") >= 1, "Stur: Abwehr %d" % Game.verhalten(id, "abwehr")) var std_id := "mensch_0" p(Game.verhalten(std_id, "abwehr") == 0, "Ein erzeugter Mensch steht auf null") Game.situation = {} var pause_normal := Game._pause_fuer(std_id) var reiz := Woerter.profil(["reizbar", "aengstlich", "rastlos", "spontan", "ehrgeizig", "stur"]) var pr2 := {"name": "Ria Test", "alter": 28, "beruf": "Studentin", "traum": "T", "geschichte": "G", "woerter": ["reizbar", "aengstlich", "rastlos", "spontan", "ehrgeizig", "stur"]} var id2 := Aushaenge.speichern(pr2) Game.welt_erzeugen() p(int(reiz["verhalten"]["momente"]) >= 2, "Reizbar, ängstlich, rastlos: Momente %d" % int(reiz["verhalten"]["momente"])) p(Game._pause_fuer(id2) < pause_normal, "Bei ihr kommt der nächste Moment schneller (%.0f s statt %.0f s)" % [Game._pause_fuer(id2), pause_normal]) var tempo_std := Game.traum_tempo(std_id) var pr3 := {"name": "Zed Test", "alter": 40, "beruf": "Lehrer", "traum": "T", "geschichte": "G", "woerter": ["diszipliniert", "zielstrebig", "optimistisch", "sportlich", "verbissen", "ehrgeizig"]} var id3 := Aushaenge.speichern(pr3) Game.welt_erzeugen() Game.systeme.clear() p(Game.traum_tempo(id3) > Game.traum_tempo(id) or Game.verhalten(id3, "tempo") > Game.verhalten(id, "tempo"), "Zielstrebig treibt den Traum von sich aus (Tempo %d gegen %d)" % [Game.verhalten(id3, "tempo"), Game.verhalten(id, "tempo")]) p(HumanDB.LIST.size() == HumanDB.EINWOHNER, "Drei eigene, immer noch %d Einwohner" % HumanDB.LIST.size()) # --- Am Brett: schreiben, Zufall, speichern, abnehmen main._traumwelt() await get_tree().process_frame ui().brett_panel() p(_knopf_da("Aushang schreiben"), "Das Brett bietet an, einen Aushang zu schreiben") p(_knopf_texte().contains("Mo Test"), "Der eigene Mensch hängt aus") ui().schreiben_panel() p(ui()._kind == "schreiben", "Das Formular öffnet sich") var haken := 0 for c in _alle_kinder(ui()._content): if c is CheckBox: haken += 1 p(haken == Woerter.ids().size(), "Alle %d Wörter sind ankreuzbar (%d)" % [Woerter.ids().size(), haken]) ui().schreiben_panel(HumanDB.zufallsprofil()) p(str(ui()._entwurf["name"]) != "" and (ui()._entwurf["woerter"] as Array).size() >= Woerter.MINDESTENS, "Zufall füllt alles: %s mit %d Wörtern" % [ui()._entwurf["name"], (ui()._entwurf["woerter"] as Array).size()]) var vorher := Aushaenge.laden().size() ui()._aushang_speichern() p(Aushaenge.laden().size() == vorher + 1, "Speichern hängt ihn ans Brett (%d)" % Aushaenge.laden().size()) p(ui()._kind == "aushang", "Und zeigt den Aushang") p(_knopf_da("Aushang abnehmen"), "Ein eigener lässt sich abnehmen") _druecke("Aushang abnehmen") p(Aushaenge.laden().size() == vorher, "Abgenommen (%d)" % Aushaenge.laden().size()) ui().schliessen() Aushaenge.alle_loeschen() Aushaenge.PFAD = "user://aushaenge.cfg" Game.welt_erzeugen() main._erde() await get_tree().process_frame func _alle_kinder(n: Node) -> Array: var out: Array = [] for c in n.get_children(): out.append(c) out.append_array(_alle_kinder(c)) return out ## Der Ablauf steht auch im HUD, ohne dass man ein Panel öffnen muss. func test_ablauf_im_hud() -> void: abschnitt("Der Ablauf steht im HUD") var id := "mensch_0" Game.gesehen.clear() Game.erkannt_slots.clear() Game.systeme.clear() Game.active_mission = "" ui().schliessen() ui()._hud_aktualisieren() p(not ui()._ablauf.visible, "Ohne Mission zeigt das HUD keinen Ablauf") Game.active_mission = id Game.time_h = 9.5 ui()._hud_aktualisieren() p(ui()._ablauf.visible, "Mit Mission schon") p(ui()._zellen.size() == 24, "Ein Feld je Stunde (%d)" % ui()._zellen.size()) p(ui()._ablauf_titel.text.contains("0 – 23"), "Beschriftet von 0 bis 23 Uhr") p(ui()._ablauf_titel.text.contains("24 Stunden unbekannt"), "Am Anfang ist alles unbekannt: %s" % ui()._ablauf_titel.text) var unbekannt: Color = ui()._ablauf_farbe(id, Game.slot_bei_stunde(id, 9)) var nr := Game.slot_bei_stunde(id, 9) Game.sehen(id, nr) ui()._hud_aktualisieren() p(ui()._ablauf_farbe(id, nr) != unbekannt, "Eine gesehene Stunde färbt sich um") p(not ui()._ablauf_titel.text.contains("24 Stunden"), "Und der Zähler geht runter: %s" % ui()._ablauf_titel.text) p(ui()._ablauf_jetzt.text.begins_with("09 Uhr"), "Darunter steht die laufende Stunde: %s" % ui()._ablauf_jetzt.text) # Die laufende Stunde ist hervorgehoben var jetzt: ColorRect = ui()._zellen[9] var andere: ColorRect = ui()._zellen[8] p(jetzt.custom_minimum_size.y > andere.custom_minimum_size.y, "Das Feld der laufenden Stunde steht heraus") # Ein benannter Blocker sieht anders aus als eine harmlose Stunde var blocker_nr := -1 var frei_nr := -1 for n in range(Game.slots(id).size()): var g := Game.slot_grund(id, n) if Ursachen.ist_blocker(g) and blocker_nr < 0: blocker_nr = n elif g != "schlaeft" and not Ursachen.ist_blocker(g) and frei_nr < 0: frei_nr = n Game.sehen(id, blocker_nr) Game.sehen(id, frei_nr) var vor_notiz: Color = ui()._ablauf_farbe(id, blocker_nr) Game.notieren(id, blocker_nr) p(ui()._ablauf_farbe(id, blocker_nr) != vor_notiz, "Notiert wechselt der Blocker noch einmal die Farbe") p(ui()._ablauf_farbe(id, blocker_nr) != ui()._ablauf_farbe(id, frei_nr), "Und hebt sich von einer harmlosen Stunde ab") Game.notiz_sperre = 0.0 ## Der Ablauf 0-23 Uhr: am Anfang leer, füllt sich durchs Dabeisein. func test_tagesablauf_anzeige() -> void: abschnitt("Der Tag steht als Ablauf von 0 bis 23 Uhr") var id := "mensch_0" Game.active_mission = id Game.gesehen.clear() Game.erkannt_slots.clear() Game.systeme.clear() Game.notiz_sperre = 0.0 # Jede Stunde des Tages hat einen Slot, und jeder Tag ist derselbe var luecken := 0 for stunde in range(24): if Game.slot_bei_stunde(id, stunde) < 0: luecken += 1 p(luecken == 0, "Alle 24 Stunden sind belegt") Game.day = 1 var tag_eins: Array = [] for stunde in range(24): tag_eins.append(Game.slot_bei_stunde(id, stunde)) Game.day = 9 var tag_neun: Array = [] for stunde in range(24): tag_neun.append(Game.slot_bei_stunde(id, stunde)) p(tag_eins == tag_neun, "Tag 9 sieht aus wie Tag 1 — es gibt keine Wochentage") Game.day = 1 ui().systeme_panel(id) var zeilen := _knopf_texte() p(zeilen.contains("00 – "), "Der Ablauf beginnt um 00 Uhr") p(zeilen.contains(" – 24"), "Und reicht bis 24 Uhr") var leer := 0 for stunde in range(24): if not Game.gesehen_slot(id, Game.slot_bei_stunde(id, stunde)): leer += 1 p(leer == 24, "Am Anfang ist jede der 24 Stunden unbekannt (%d)" % leer) p(zeilen.contains("—") and not zeilen.contains("schläft"), "Und im Ablauf steht nur ein Strich") ui().schliessen() # Dabeistehen füllt genau die Stunde, in der man steht var mensch: Bewohner = null for m in welt().humans: if m.data["id"] == id: mensch = m var frei_nr := -1 for nr in range(Game.slots(id).size()): var g := Game.slot_grund(id, nr) if g != "schlaeft" and not Ursachen.ist_blocker(g): frei_nr = nr break await _hin_zu(mensch, float(Game.slots(id)[frei_nr][0]) + 0.5) main._process(0.05) p(Game.gesehen_slot(id, frei_nr), "Wer dabeisteht, füllt die Stunde") ui().systeme_panel(id) p(_knopf_texte().contains(Orte.text(Game.slot_grund(id, frei_nr))), "Sie steht jetzt im Ablauf: %s" % Orte.text(Game.slot_grund(id, frei_nr))) ui().schliessen() # Ein Blocker bleibt ein Fragezeichen, bis er benannt ist var blocker_nr := -1 for nr in range(Game.slots(id).size()): if Ursachen.ist_blocker(Game.slot_grund(id, nr)): blocker_nr = nr break Game.sehen(id, blocker_nr) ui().systeme_panel(id) p(_knopf_texte().contains("?"), "Was schiefläuft, steht erst als Fragezeichen da") ui().schliessen() Game.notieren(id, blocker_nr) ui().systeme_panel(id) var b: Dictionary = Ursachen.BLOCKER[Game.slot_grund(id, blocker_nr)] p(_knopf_texte().contains(str(b["name"])), "Nach dem Notieren steht dort, was sie tut") p(_knopf_texte().contains(str(Ursachen.LISTE[b["ursache"]]["name"])), "Und warum: %s" % Ursachen.LISTE[b["ursache"]]["name"]) ui().schliessen() ## Der ganze Weg: dabeistehen, den Moment einschätzen, R drücken. func test_notieren_im_spiel() -> void: abschnitt("Notieren ist eine Entscheidung, kein Timer") var id := "mensch_0" Game.active_mission = id Game.erkannt_slots.clear() Game.notiz_sperre = 0.0 ui().schliessen() main.player.set_mode(Player.Mode.GEIST) main.player.look_enabled = true var blocker_nr := -1 var frei_nr := -1 for nr in range(Game.slots(id).size()): var g := Game.slot_grund(id, nr) if Ursachen.ist_blocker(g) and blocker_nr < 0: blocker_nr = nr elif g != "schlaeft" and not Ursachen.ist_blocker(g) and frei_nr < 0: frei_nr = nr p(blocker_nr >= 0 and frei_nr >= 0, "Ihr Tag hat einen Blocker und einen freien Slot") var mensch: Bewohner = null for m in welt().humans: if m.data["id"] == id: mensch = m # Im freien Slot danebengegriffen await _hin_zu(mensch, float(Game.slots(id)[frei_nr][0]) + 0.5) p(mensch.slot == frei_nr, "Sie steht im freien Slot (%d)" % mensch.slot) main._notieren() p(not Game.erkannt(id, blocker_nr), "Hier gibt es nichts zu notieren") p(Game.notiz_sperre > 0.0, "Der Fehlgriff kostet eine Sperre") # Sperre läuft in Echtzeit ab for i in range(200): main._process(0.05) p(is_equal_approx(Game.notiz_sperre, 0.0), "Sie läuft von allein ab") # Im Blocker-Slot getroffen await _hin_zu(mensch, float(Game.slots(id)[blocker_nr][0]) + 0.5) p(mensch.slot == blocker_nr, "Jetzt steht sie im Blocker-Slot (%d)" % mensch.slot) p(main._fokus_mensch == mensch, "Und ist in Reichweite") main._notieren() p(Game.erkannt(id, blocker_nr), "R notiert, was hier schiefläuft: %s" % Orte.text(Game.slot_grund(id, blocker_nr))) # Ein fremder Mensch geht niemanden etwas an Game.erkannt_slots.clear() Game.notiz_sperre = 0.0 var fremd: Bewohner = null for m in welt().humans: if m.data["id"] != id: fremd = m main.player.folgt = null main.player.global_position = fremd.global_position + Vector3(0, 1.5, 1.5) main._process(0.05) main._notieren() p(Game.erkannt_slots.is_empty(), "Ohne angenommene Mission notiert er bei %s nichts" % fremd.data["name"]) p(is_equal_approx(Game.notiz_sperre, 0.0), "Und das kostet auch keine Sperre") # Aus der Verfolgung heraus geht es ebenfalls Game.erkannt_slots.clear() await _hin_zu(mensch, float(Game.slots(id)[blocker_nr][0]) + 0.5) main._verfolgen() p(main.player.folgt == mensch, "An sie gehängt") main._notieren() p(Game.erkannt(id, blocker_nr), "Auch im Vorbeischweben lässt sich notieren") main.player.folgt = null ## Stellt den Geist zur genannten Stunde neben den Menschen. func _hin_zu(mensch: Bewohner, stunde: float) -> void: for i in range(600): schritte(1, 0.05, stunde) if mensch._path.is_empty(): break main.player.folgt = null main.player.global_position = mensch.global_position + Vector3(0, 1.5, 1.5) main.player.velocity = Vector3.ZERO await get_tree().process_frame main._process(0.05) func test_systeme() -> void: abschnitt("Systeme — gegen die Ursache, nicht gegen die Tätigkeit") var id := "mensch_0" Game.active_mission = id Game.systeme.clear() Game.cooldown.clear() Game.known.clear() for t in TechDB.LIST: Game.known[str(t["id"])] = 28 Game.erkannt_slots[id] = {} for nr in range(Game.slots(id).size()): Game.erkannt_slots[id][nr] = true var blocker_nr := -1 for nr in range(Game.slots(id).size()): if Ursachen.ist_blocker(Game.slot_grund(id, nr)): blocker_nr = nr break var b: Dictionary = Ursachen.BLOCKER[Game.slot_grund(id, blocker_nr)] p(blocker_nr >= 0, "Ein Blocker im Tag: %s" % b["name"]) # falsche Ursache geht nicht var falsch := "" for tid in TechDB.ids(): if str(TechDB.by_id(tid)["ursache"]) != str(b["ursache"]): falsch = tid break var pr_falsch := Game.system_pruefen(id, blocker_nr, falsch) p(not pr_falsch["geht"], "Eine Technik gegen eine andere Ursache passt nicht hier") # richtige Ursache, gleiche Belohnung -> bessere Güte var gleich := "" var anders := "" for tid in TechDB.fuer_ursache(str(b["ursache"])): var t := TechDB.by_id(str(tid)) if str(t["belohnung"]) == str(b["belohnung"]) or TechDB.entfernt_ausloeser(str(tid)): gleich = str(tid) else: anders = str(tid) p(gleich != "", "Es gibt eine Technik mit derselben Belohnung: %s" % TechDB.by_id(gleich)["name"]) var g1: float = float(Game.system_pruefen(id, blocker_nr, gleich)["guete"]) p(g1 > Game.GUETE_NEU, "Die hält besser (Güte %.2f)" % g1) if anders != "": var g2: float = float(Game.system_pruefen(id, blocker_nr, anders)["guete"]) p(g2 < g1, "Eine mit anderer Belohnung hält schlechter (%.2f)" % g2) # einbauen Game.cooldown.clear() var vorher := Game.slot_aktivitaet(id, blocker_nr) var versuche := 0 var res: Dictionary = {} while versuche < 30: Game.cooldown.clear() res = Game.system_setzen(id, blocker_nr, gleich, false) versuche += 1 if res.get("erfolg", false): break p(res.get("erfolg", false), "Nach höchstens %d Versuchen steht das System" % versuche) p(Game.slot_aktivitaet(id, blocker_nr) != vorher, "Der Slot zeigt jetzt die Ersatzhandlung: %s" % Game.slot_aktivitaet(id, blocker_nr)) p(Game.ursache_geloest(id, str(b["ursache"])) > 0.0, "Und die Ursache gilt als gelöst") Game.system_entfernen(id, blocker_nr) p(Game.ursache_geloest(id, str(b["ursache"])) == 0.0, "Herausnehmen macht es rückgängig") p(Game.slot_aktivitaet(id, blocker_nr) == vorher, "Der Blocker ist wieder da") # Schlaf bleibt tabu var schlaf := -1 for nr in range(Game.slots(id).size()): if Game.slot_grund(id, nr) == "schlaeft": schlaf = nr p(schlaf >= 0 and not Game.system_pruefen(id, schlaf, gleich)["geht"], "Im Schlaf lässt sich nichts einrichten") # In einen freien Slot passt jede Technik, aber nur mit einfacher Güte var frei := -1 for nr in range(Game.slots(id).size()): var g := Game.slot_grund(id, nr) if g != "schlaeft" and not Ursachen.ist_blocker(g): frei = nr break Game.cooldown.clear() var pr_frei := Game.system_pruefen(id, frei, falsch) p(pr_frei["geht"], "In einen freien Slot passt auch eine andere Technik") p(is_equal_approx(float(pr_frei["guete"]), Game.GUETE_NEU), "Aber nur mit einfacher Güte (%.2f)" % pr_frei["guete"]) ## Je mehr Ursachen abgedeckt sind, desto schneller kommt der Traum voran. func test_tempo_des_traums() -> void: abschnitt("Tempo — je besser die Systeme, desto schneller") _ohne_momente() var id := "mensch_0" Game.active_mission = id Game.systeme.clear() Game.cooldown.clear() Game.known.clear() for t in TechDB.LIST: Game.known[str(t["id"])] = 28 Game.erkannt_slots[id] = {} for nr in range(Game.slots(id).size()): Game.erkannt_slots[id][nr] = true var urs: Array = Game.ursachen_von(id) var leer := Game.traum_tempo(id) p(leer < Game.BASIS_TEMPO * 0.25, "Ohne Systeme kriecht der Traum: %.0f %% je Tag" % leer) var gesetzt: Array = [] for u in urs: for nr in range(Game.slots(id).size()): if Ursachen.ursache_von(Game.slot_grund(id, nr)) != str(u): continue for tid in TechDB.fuer_ursache(str(u)): var pr := Game.system_pruefen(id, nr, str(tid)) if pr["geht"]: Game.systeme[id] = Game.systeme.get(id, {}) Game.systeme[id][nr] = {"tech": str(tid), "guete": pr["guete"]} gesetzt.append(Game.traum_tempo(id)) break break p(gesetzt.size() == urs.size(), "Gegen jede Ursache steht ein System") var steigt := true var vor := leer for w in gesetzt: if float(w) <= vor: steigt = false vor = float(w) p(steigt, "Jedes weitere System hebt das Tempo: %.0f -> %s" % [leer, str(gesetzt.map(func(x): return "%.0f" % float(x)))]) p(Game.traum_tempo(id) > leer * 8.0, "Alles gelöst ist mehr als achtmal so schnell (%.0f statt %.0f)" % [Game.traum_tempo(id), leer]) # Die Bremsen wirken multiplikativ Game.systeme.clear() var keine := Game.traum_tempo(id) var erwartet := Game.BASIS_TEMPO for u in urs: erwartet *= float(Ursachen.LISTE[u]["faktor"]) p(is_equal_approx(keine, erwartet), "Zwei Bremsen bremsen doppelt: %.1f wie gerechnet" % keine) # Fortschritt läuft über die Zeit, nicht über Einzeltreffer Game.progress[id] = 0.0 Game.traum_voranbringen(24.0) p(is_equal_approx(Game.mission_progress(id), keine), "Ein ganzer Tag bringt genau ein Tagestempo (%.1f %%)" % Game.mission_progress(id)) func test_mission_abschliessen() -> void: abschnitt("Mission abschließen") _ohne_momente() var id := "mensch_0" Game.active_mission = id Game.progress[id] = 0.0 Game.completed.clear() var tage := 0 while Game.mission_progress(id) < Game.MISSION_ZIEL and tage < 200: Game.traum_voranbringen(24.0) tage += 1 p(Game.completed.has(id), "Der Traum erfüllt sich nach %d Tagen" % tage) p(Game.active_mission == "", "Aktive Mission wurde freigegeben") 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") _ohne_momente() 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()) var neu_id := str(TechDB.LIST[1]["id"]) Game.known = {str(_basiswege()[0]): 0} 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("Diesen Weg"), "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.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() main._traumwelt() await get_tree().process_frame _wege_pruefen("Traumdorf mit %d Häusern" % stufen, DreamDB.PLACES, DreamDB.LIST, main.traum.nav_map) # Jede Haustür muss zu Fuß erreichbar sein -- keine Klippe vor der Tür var platz := Orte.center(DreamDB.PLACES, "lichtung") + Vector3(2.2, 0, 0) var unerreichbar: Array = [] for key in DreamDB.PLACES: if not str(key).begins_with("haus_"): continue var t := Orte.door(DreamDB.PLACES, key) t.y = DreamDB.hoehe_bei(t.x, t.z) var pf := NavigationServer3D.map_get_path(main.traum.nav_map, platz, t, true) if pf.is_empty() or pf[pf.size() - 1].distance_to(t) > 1.5: unerreichbar.append("%s (Terrasse %.1f)" % [key, DreamDB.terrasse(key)]) p(unerreichbar.is_empty(), "Bei %d Häusern ist jede Haustür vom Dorfplatz aus zu Fuß erreichbar%s" % [stufen, "" if unerreichbar.is_empty() else " — nicht: " + ", ".join(unerreichbar)]) main._erde() await get_tree().process_frame _wege_pruefen("Stadt mit %d Menschen" % HumanDB.LIST.size(), HumanDB.PLACES, HumanDB.LIST, main.erde.nav_map) Game.completed.assign(merker) Game.welt_erzeugen() func test_techniken_verbessern_sich() -> void: abschnitt("Techniken verbessern sich nur durch Anwendung") _ohne_momente() Game.known.clear() Game.systeme.clear() Game.cooldown.clear() p(Game.learn("zwei_minuten"), "Technik gelernt") p(Game.skill_of("zwei_minuten") == 1, "Frisch gelernt heißt Stufe 1") var id := "mensch_0" Game.active_mission = id Game.erkannt_slots[id] = {} for nr in range(Game.slots(id).size()): Game.erkannt_slots[id][nr] = true var frei := -1 for nr in range(Game.slots(id).size()): var g := Game.slot_grund(id, nr) if g != "schlaeft" and not Ursachen.ist_blocker(g): frei = nr break var versuche := 0 while Game.skill_of("zwei_minuten") < 3 and versuche < 80: Game.cooldown.clear() Game.system_entfernen(id, frei) Game.system_setzen(id, frei, "zwei_minuten", false) versuche += 1 p(Game.skill_of("zwei_minuten") >= 3, "Nach %d Einsätzen von allein auf Stufe %d" % [versuche, Game.skill_of("zwei_minuten")]) # Es gibt keinen zweiten Weg nach oben p(not Game.has_method("deepen"), "Kein gezieltes Vertiefen mehr im Spiel") p(not ("einsicht" in Game), "Und keine Währung, die man dafür ausgeben könnte") var stand: int = Game.known["zwei_minuten"] Game.advance_time(Game.DAY_SECONDS) p(Game.known["zwei_minuten"] == stand, "Ein ganzer Tag bringt keine Erfahrung") ui().lehre_panel(DreamDB.ort_von_technik("zwei_minuten")) var geknoepft := _druecke("Diesen Weg") or _druecke("Vertiefen") or _druecke("Üben") ui().schliessen() p(not geknoepft and Game.known["zwei_minuten"] == stand, "Ein zweiter Besuch am Lehrort ändert nichts (%d EP)" % stand) 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", "drehen", "interagieren", "folgen", "notieren", "journal", "tempo_1", "tempo_2", "tempo_3"] 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 %d Steuerungsaktionen sind mit Tasten belegt" % aktionen.size()) 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) # Ein geschlossener Klotz hält ihn auf: von der Straße aus gegen die Wand var war_erde: bool = main.auf_erde if not war_erde: main._erde() await get_tree().process_frame main.player.look_enabled = true var klotz: Dictionary = {} var richtung := Vector3.ZERO for b in welt()._bauten: if not (b["waende"] as Array).is_empty(): continue # ein Haus mit Wänden ist kein Klotz for pz in HumanDB.PARZELLEN: if (pz["pos"] as Vector3).distance_to(b["c"]) < 0.1: klotz = b richtung = Orte.DIR_VEC[pz["dir"]] if not klotz.is_empty(): break p(not klotz.is_empty(), "Es gibt geschlossene Klötze") var kc: Vector3 = klotz["c"] var kg: Vector2 = klotz["size"] var halb: float = (kg.y if absf(richtung.z) > 0.0 else kg.x) * 0.5 main.player.global_position = kc + richtung * (halb + 2.0) + Vector3(0, 0.3, 0) main.player.velocity = Vector3.ZERO main.player.yaw = atan2(richtung.x, richtung.z) # "vor" zeigt zum Klotz Input.action_press("vor") for i in range(60): await get_tree().physics_frame Input.action_release("vor") var tiefe_im_klotz: float = halb - (main.player.global_position - kc).dot(richtung) p(tiefe_im_klotz < 0.6, "Wände halten den Geist auf (%.1f m in der Wand)" % maxf(0.0, tiefe_im_klotz)) # Eingeklemmt zwischen zwei Klötzen: nach zwei Sekunden Drücken steht er wieder auf der Straße var quer_r := Vector3(-richtung.z, 0, richtung.x) var breit: float = (kg.x if absf(richtung.z) > 0.0 else kg.y) * 0.5 main.player.global_position = kc + quer_r * (breit + 0.4) + Vector3(0, 0.3, 0) main.player.velocity = Vector3.ZERO main.player.spruenge = 0 main.player.yaw = atan2(richtung.x, richtung.z) Input.action_press("vor") for i in range(200): await get_tree().physics_frame Input.action_release("vor") var fuge: Vector3 = kc + quer_r * (breit + 0.4) var weg_von_fuge: float = _xz(main.player.global_position).distance_to(_xz(fuge)) p(main.player.spruenge >= 1 or weg_von_fuge > 3.0, "In der Fuge zwischen zwei Klötzen bleibt er nicht stecken (%.1f m weg, %d Befreiungen)" % [weg_von_fuge, main.player.spruenge]) if not war_erde: main._traumwelt() # die folgenden Messungen erwarten die Traumwelt await get_tree().process_frame ## Nach jedem Einsatz braucht eine Technik Ruhe. Ein Weg allein deckt daher ## keinen ganzen Tag ab — das ist der Grund, mehrere zu lernen. ## Nach jedem Einsatz braucht eine Technik Ruhe. Ein Weg allein baut daher ## nicht den ganzen Tag um. func test_cooldown() -> void: abschnitt("Techniken haben einen Cooldown") _ohne_momente() var id := "mensch_0" Game.active_mission = id Game.cooldown.clear() Game.systeme.clear() Game.known.clear() for t in TechDB.LIST: Game.known[str(t["id"])] = 28 Game.erkannt_slots[id] = {} for nr in range(Game.slots(id).size()): Game.erkannt_slots[id][nr] = true var frei: Array = [] for nr in range(Game.slots(id).size()): var g := Game.slot_grund(id, nr) if g != "schlaeft" and not Ursachen.ist_blocker(g): frei.append(nr) p(Game.bereit("pomodoro"), "Frisch ist eine Technik einsatzbereit") var erste := Game.system_setzen(id, int(frei[0]), "pomodoro", false) p(erste["ok"], "Der erste Einsatz geht durch") p(not Game.bereit("pomodoro"), "Danach ist sie gesperrt") p(is_equal_approx(Game.cooldown_rest("pomodoro"), Game.TECHNIK_COOLDOWN), "Und zwar %.0f Sekunden lang" % Game.TECHNIK_COOLDOWN) var zweite := Game.system_setzen(id, int(frei[1]), "pomodoro", false) p(not zweite["ok"], "Ein zweiter Einsatz prallt an der Sperre ab") p(str(zweite["grund"]).contains("einsatzbereit"), "Mit klarer Begründung") p(Game.bereit("zerlegen"), "Die Sperre gilt nur für diese eine Technik") p(Game.system_setzen(id, int(frei[1]), "zerlegen", false)["ok"], "Eine andere Technik greift weiter") Game.advance_time(Game.TECHNIK_COOLDOWN * 0.5) p(not Game.bereit("pomodoro"), "Nach der halben Zeit noch immer gesperrt") Game.advance_time(Game.TECHNIK_COOLDOWN * 0.6) p(Game.bereit("pomodoro"), "Nach Ablauf wieder frei") p(Game.cooldown_rest("pomodoro") == 0.0, "Und der Zähler steht auf null") ## Wie gut etwas steht, sagt das Spiel nicht. Es zeigt Auslöser und Belohnung ## -- entscheiden muss der Spieler. func test_chance_verdeckt() -> void: abschnitt("Die Erfolgschance bleibt verdeckt") var id := "mensch_0" Game.active_mission = id Game.cooldown.clear() Game.systeme.clear() Game.known.clear() for t in TechDB.LIST: Game.known[str(t["id"])] = 28 Game.erkannt_slots[id] = {} for nr in range(Game.slots(id).size()): Game.erkannt_slots[id][nr] = true var blocker_nr := -1 for nr in range(Game.slots(id).size()): if Ursachen.ist_blocker(Game.slot_grund(id, nr)): blocker_nr = nr break ui().slot_panel(id, blocker_nr) var wahl := _panel_text() var knoepfe := _knopf_texte() p(not knoepfe.contains("%"), "Kein Knopf nennt eine Prozentzahl") p(not wahl.contains("Erfolgschance"), "Und das Wort Erfolgschance kommt nicht vor") p(not wahl.contains("Wann:") and not wahl.contains("Zu sehen an:"), "Die Auswahl zeigt nur Namen, keine Beschreibung") p(wahl.contains("Auslöser") and wahl.contains("Belohnung"), "Dafür stehen Auslöser und Belohnung des Blockers da") ui().schliessen() ui().systeme_panel(id) p(not _knopf_texte().contains("%"), "Auch die Tageszeilen nennen keine Chance") p(_panel_text().contains("je Tag"), "Das Panel nennt das Tempo des Traums") ui().schliessen() ui().journal_panel("techniken") p(_knopf_texte().contains(str(TechDB.LIST[0]["name"])), "Das Journal listet die Technik als eine Zeile") ui().technik_panel(str(TechDB.LIST[0]["id"])) var buch := _panel_text() p(buch.contains("Wann:") and buch.contains("Zu sehen an:"), "Aufgeschlagen stehen Was, Wann, Warum und Zu sehen an da") ui().schliessen() ## Wenn nur lösbare Missionen aushängen, darf das Brett nie ganz leer werden — ## sonst hinge Loop 2 fest: ohne Traum kein neuer Weg, ohne Weg kein Aushang. func test_brett_nie_leer() -> void: abschnitt("Das Brett läuft nie leer") _ohne_momente() var merk_c: Array = Game.completed.duplicate() var merk_k: Dictionary = Game.known.duplicate() var leer_bei := -1 var kleinste := 99 var stufen := 14 for stufe in range(stufen): # Ein Spieler, der alles lernt, was das Dorf hergibt for t in TechDB.LIST: if Game.technik_offen(str(t["id"])) and not Game.knows(str(t["id"])): Game.known[str(t["id"])] = 0 var offen: Array = Game.offene_missionen() kleinste = mini(kleinste, offen.size()) if offen.is_empty(): leer_bei = stufe break Game.completed.append(str(offen[0])) Game.welt_erzeugen() p(leer_bei < 0, "Über %d Ausbaustufen hängt immer etwas aus%s" % [stufen, "" if leer_bei < 0 else " — leer bei Stufe %d" % leer_bei]) p(kleinste >= 1, "Im knappsten Fall bleibt %d Aushang übrig" % kleinste) # Und der jeweils neueste Mensch ist mit dem neuesten Weg zu schaffen Game.completed.clear() Game.known.clear() Game.welt_erzeugen() var passt := 0 for i in range(TechDB.LIST.size()): Game.completed.clear() for k in range(i): Game.completed.append("stufe_%d" % k) Game.welt_erzeugen() Game.known = {str(TechDB.LIST[i]["id"]): 0} if not Game.offene_missionen().is_empty(): passt += 1 p(passt == TechDB.LIST.size(), "Zu jedem der %d Wege gibt es einen Menschen, dem er hilft (%d)" % [TechDB.LIST.size(), passt]) Game.completed.assign(merk_c) Game.known = merk_k Game.welt_erzeugen() ## Warten soll nicht wehtun: 1, 2, 3 stellen das Tempo. Über der Nacht des ## Menschen springt es von allein auf drei — dort ist ohnehin nichts zu holen. func test_tempo() -> void: abschnitt("Tempo lässt sich stellen") Game.completed.clear() Game.known = {"pomodoro": 0} Game.welt_erzeugen() Game.active_mission = "" Game.tempo = 1 p(is_equal_approx(Game.zeit_faktor(), 1.0), "Ohne Zutun läuft die Welt einfach") # Wie in Die Sims: die Stufe ist nicht der Faktor var erwartet := {1: 1.0, 2: 3.0, 3: 7.0} for stufe in [2, 3, 1]: Game.tempo_setzen(stufe) p(Game.tempo == stufe and is_equal_approx(Game.zeit_faktor(), float(erwartet[stufe])), "Stufe %d läuft %.0f-fach" % [stufe, Game.zeit_faktor()]) Game.tempo_setzen(3) p(not is_equal_approx(Game.zeit_faktor(), 3.0), "Stufe 3 heißt gerade nicht dreifach (%.0f×)" % Game.zeit_faktor()) Game.tempo_setzen(1) Game.tempo_setzen(9) p(Game.tempo == 3, "Höher als drei geht nicht (%d)" % Game.tempo) Game.tempo_setzen(0) p(Game.tempo == 1, "Und tiefer als eins auch nicht (%d)" % Game.tempo) # Über der Nacht des Menschen springt es von allein Game.accept_mission("mensch_0") var wach := -1.0 var nacht := -1.0 for stunde in range(24): Game.time_h = float(stunde) + 0.5 if Game.schlaeft_ziel(): nacht = Game.zeit_faktor() else: wach = Game.zeit_faktor() p(nacht > 0.0 and wach > 0.0, "Lenas Tag hat wache und schlafende Stunden") p(is_equal_approx(nacht, Game.faktor_von(3)), "Im Schlaf läuft die Welt auf der höchsten Stufe (%.0f×)" % nacht) p(is_equal_approx(wach, 1.0), "Wach wieder auf der gewählten Stufe (%.0f)" % wach) Game.tempo_setzen(1) Game.time_h = 2.0 p(Game.schlaeft_ziel(), "Um 02:00 schläft sie") p(is_equal_approx(Game.zeit_faktor(), Game.faktor_von(3)), "Die eigene Wahl Stufe 1 gilt dann nicht — die Nacht springt") # Ohne Mission gibt es nichts zu überspringen Game.active_mission = "" p(not Game.schlaeft_ziel(), "Ohne Mission kein Nachtsprung") p(is_equal_approx(Game.zeit_faktor(), 1.0), "Dann zählt nur die Wahl") # Der Faktor beschleunigt die Uhr wirklich Game.tempo_setzen(3) Game.time_h = 8.0 Game.day = 1 var vorher := Game.time_h Game.advance_time(1.0 * Game.zeit_faktor()) var schnell := Game.time_h - vorher Game.tempo_setzen(1) Game.time_h = 8.0 vorher = Game.time_h Game.advance_time(1.0 * Game.zeit_faktor()) var langsam := Game.time_h - vorher p(is_equal_approx(schnell, langsam * Game.faktor_von(3)), "Auf Stufe 3 vergeht %.0f-mal so viel Zeit je Sekunde (%.4f statt %.4f h)" % [Game.faktor_von(3), schnell, langsam]) # Das HUD sagt, woran man ist Game.tempo_setzen(2) ui().journal_panel() ui().schliessen() ui()._hud_aktualisieren() p(ui()._hud.text.contains("Tempo 2 (3×)"), "Das HUD nennt Stufe und Faktor getrennt") Game.accept_mission("mensch_0") Game.time_h = 2.0 ui()._hud_aktualisieren() p(ui()._hud.text.contains("Nacht"), "Und weist den Nachtsprung aus") Game.tempo_setzen(1) Game.active_mission = "" ## Das Dorf wird nicht gesetzt, sondern wächst. Also muss über viele ## Ausbaustufen gelten: nichts überlappt, kein Weg führt durch ein Haus, alles ## hängt zusammen, und was einmal steht, bleibt stehen. func test_dorf_organisch() -> void: abschnitt("Das Dorf wächst organisch") var merker: Array = Game.completed.duplicate() var stufen := 24 var ueberlappung := "" var wegtreffer := "" var lose := "" var ohne_anker := "" var unerreichbar := "" var sektoren_12 := 0 var dichte := "" for stufe in range(stufen + 1): Game.completed.clear() for i in range(stufe): Game.completed.append("stufe_%d" % i) Game.welt_erzeugen() var netz: Netz = DreamDB.NETZ var keys: Array = [] for key in DreamDB.PLACES: if float(DreamDB.PLACES[key]["size"][0]) > 0.0: keys.append(key) # nichts steht ineinander for a in range(keys.size()): for b in range(a + 1, keys.size()): var ca := Orte.center(DreamDB.PLACES, keys[a]) var cb := Orte.center(DreamDB.PLACES, keys[b]) var sa: Array = DreamDB.PLACES[keys[a]]["size"] var sb: Array = DreamDB.PLACES[keys[b]]["size"] if absf(ca.x - cb.x) < (float(sa[0]) + float(sb[0])) * 0.5 \ and absf(ca.z - cb.z) < (float(sa[1]) + float(sb[1])) * 0.5 \ and ueberlappung == "": ueberlappung = ("%s und %s bei %d Häusern" % [keys[a], keys[b], stufe]) # kein Weg führt durch ein Gebäude for k in netz.kanten: for key in keys: var s2: Array = DreamDB.PLACES[key]["size"] if Netz.trifft_rechteck(netz.knoten[k[0]], netz.knoten[k[1]], Orte.center(DreamDB.PLACES, key), Vector2(float(s2[0]), float(s2[1])), 0.0) and wegtreffer == "": wegtreffer = "%s bei %d Häusern" % [key, stufe] if not netz.zusammenhaengend() and lose == "": lose = "bei %d Häusern" % stufe # jedes Haus hängt am Netz und ist vom Dorfplatz aus zu erreichen for i in range(stufe): var key3 := DreamDB.haus_key(i) if not DreamDB.ANKER.has(key3): if ohne_anker == "": ohne_anker = "%s bei %d Häusern" % [key3, stufe] continue if netz.weglaenge(int(DreamDB.ANKER["lichtung"]), int(DreamDB.ANKER[key3])) == INF \ and unerreichbar == "": unerreichbar = "%s bei %d Häusern" % [key3, stufe] if stufe == 12: var sekt: Dictionary = {} for i in range(stufe): var c := Orte.center(DreamDB.PLACES, DreamDB.haus_key(i)) sekt[int(floor((atan2(c.z, c.x) + PI) / (TAU / 8.0))) % 8] = true sektoren_12 = sekt.size() if stufe == stufens_probe(): var flaeche := PI * pow(Game.insel_radius(), 2.0) # Terrassen verschiedener Höhe brauchen Böschungen dazwischen: das kostet Fläche if flaeche / float(stufe) > 3300.0: dichte = "%.0f m² je Haus" % (flaeche / float(stufe)) p(ueberlappung == "", "Über %d Ausbaustufen steht kein Gebäude im anderen%s" % [stufen, "" if ueberlappung == "" else " — " + ueberlappung]) p(wegtreffer == "", "Und kein Weg führt durch ein Gebäude%s" % ["" if wegtreffer == "" else " — " + wegtreffer]) p(lose == "", "Das Wegenetz hängt immer zusammen%s" % ["" if lose == "" else " — " + lose]) p(ohne_anker == "", "Jedes Haus hängt am Netz%s" % ["" if ohne_anker == "" else " — " + ohne_anker]) p(unerreichbar == "", "Und ist vom Dorfplatz aus erreichbar%s" % ["" if unerreichbar == "" else " — " + unerreichbar]) p(sektoren_12 >= 6, "Bei 12 Häusern ist das Dorf rundum gewachsen (%d von 8 Richtungen)" % sektoren_12) p(dichte == "", "Die Insel bleibt dicht besiedelt%s" % ["" if dichte == "" else " — " + dichte]) # Verwinkelt heißt: Gabeln und Schleifen, nicht Speichen zur Mitte Game.completed.clear() for i in range(18): Game.completed.append("stufe_%d" % i) Game.welt_erzeugen() var netz2: Netz = DreamDB.NETZ var gabeln := 0 for i in range(netz2.knoten.size()): if netz2.nachbarn(i).size() >= 3: gabeln += 1 var schleifen: int = netz2.kanten.size() - netz2.knoten.size() + 1 p(gabeln >= 6, "Bei 18 Häusern gabelt sich das Netz %d-mal" % gabeln) p(schleifen >= 2, "Und trägt %d Schleifen statt lauter Sackgassen" % schleifen) # Was einmal steht, bleibt stehen var vorher: Array = [] for i in range(18): vorher.append(Orte.center(DreamDB.PLACES, DreamDB.haus_key(i))) Game.completed.append("stufe_18") Game.welt_erzeugen() var verrutscht := 0 for i in range(18): if vorher[i].distance_to(Orte.center(DreamDB.PLACES, DreamDB.haus_key(i))) > 0.01: verrutscht += 1 p(verrutscht == 0, "Das 19. Haus verschiebt keines der 18 alten (%d)" % verrutscht) Game.completed.assign(merker) Game.welt_erzeugen() func stufens_probe() -> int: return 24 ## Die Erde ist ein erzeugter Bezirk. Also muss gelten: Parzellen liegen ## nebeneinander statt ineinander, jede hat Straßenanschluss, das Netz hängt ## zusammen -- und anders als im Dorf wächst hier nichts. func test_bezirk() -> void: abschnitt("Die Erde ist ein Bezirk") var netz: Netz = HumanDB.NETZ p(HumanDB.PARZELLEN.size() >= 60, "Der Bezirk ist in %d Parzellen geteilt" % HumanDB.PARZELLEN.size()) var ueber := 0 for i in range(HumanDB.PARZELLEN.size()): for j in range(i + 1, HumanDB.PARZELLEN.size()): var a: Dictionary = HumanDB.PARZELLEN[i] var b: Dictionary = HumanDB.PARZELLEN[j] if absf(a["pos"].x - b["pos"].x) < (a["size"].x + b["size"].x) * 0.5 - 0.05 \ and absf(a["pos"].z - b["pos"].z) < (a["size"].y + b["size"].y) * 0.5 - 0.05: ueber += 1 p(ueber == 0, "Keine Parzelle steht in einer anderen (%d)" % ueber) var ohne := 0 var weiteste := 0.0 for pz in HumanDB.PARZELLEN: var d := INF for st in HumanDB.STRASSEN: var f := Geometry3D.get_closest_point_to_segment(pz["pos"], st["a"], st["b"]) d = minf(d, f.distance_to(pz["pos"]) - maxf(pz["size"].x, pz["size"].y) * 0.5 - float(st["breite"]) * 0.5) weiteste = maxf(weiteste, d) if d > 2.0: ohne += 1 p(ohne == 0, "Jede Parzelle liegt an einer Straße (weiteste Lücke %.1f m)" % weiteste) p(netz.zusammenhaengend(), "Das Straßennetz hängt zusammen (%d Knoten, %d Kanten)" % [netz.knoten.size(), netz.kanten.size()]) var unerreichbar := 0 var keys: Array = HumanDB.PLACES.keys() for i in range(keys.size()): for j in range(i + 1, keys.size()): if netz.weglaenge(int(HumanDB.ANKER[keys[i]]), int(HumanDB.ANKER[keys[j]])) == INF: unerreichbar += 1 p(unerreichbar == 0, "Jeder Ort ist von jedem anderen aus zu erreichen (%d Lücken)" % unerreichbar) # Arterien im Raster, roji schmal und als Sackgasse ins Blockinnere var arterien := 0 var gassen := 0 for st in HumanDB.STRASSEN: if float(st["breite"]) >= HumanDB.ARTERIE: arterien += 1 else: gassen += 1 p(arterien == HumanDB.ACHSEN.size() * 2, "%d Arterien spannen das Raster auf" % arterien) p(gassen >= 4, "Und %d roji führen ins Blockinnere" % gassen) p(HumanDB.ROJI < HumanDB.ARTERIE * 0.6, "Eine roji ist mit %.1f m deutlich schmaler als eine Arterie (%.1f m)" % [HumanDB.ROJI, HumanDB.ARTERIE]) var sackgassen := 0 for st in HumanDB.STRASSEN: if float(st["breite"]) < HumanDB.ARTERIE: var ende: Vector3 = st["b"] if netz.nachbarn(netz.fuege_knoten(ende)).size() <= 1: sackgassen += 1 p(sackgassen >= gassen / 2, "Die meisten roji enden blind (%d von %d)" % [sackgassen, gassen]) # Alle Menschen sind da, und sie werden nicht mehr var vorher := HumanDB.LIST.size() p(vorher == HumanDB.EINWOHNER, "Alle %d Menschen sind von Anfang an da" % vorher) var merker: Array = Game.completed.duplicate() for i in range(6): Game.completed.append("stufe_%d" % i) Game.welt_erzeugen() p(HumanDB.LIST.size() == vorher, "Sechs erfüllte Träume ändern daran nichts (%d)" % HumanDB.LIST.size()) Game.completed.assign(merker) Game.welt_erzeugen() # Jeder wohnt in einer eigenen Wohnung var heime: Dictionary = {} for m in HumanDB.LIST: heime[str(m["heim"])] = true p(heime.size() == HumanDB.LIST.size(), "Jeder Mensch hat seine eigene Wohnung (%d)" % heime.size()) # Der mittlere Block bleibt frei var im_park := 0 var halb: float = (float(HumanDB.ACHSEN[2]) - float(HumanDB.ACHSEN[1])) * 0.5 \ - HumanDB.ARTERIE for pz in HumanDB.PARZELLEN: var q: Vector3 = pz["pos"] if absf(q.x) < halb and absf(q.z) < halb: im_park += 1 p(im_park == 0, "Im mittleren Block steht kein Haus — er ist der Park (%d)" % im_park) ## 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. func _erster_traum_tage(saat: int) -> int: seed(saat) Game.cooldown.clear() Game.known.clear() Game.systeme.clear() Game.erkannt_slots.clear() Game.notiz_sperre = 0.0 Game.progress.clear() Game.completed.clear() Game.active_mission = "" _ohne_momente() Game.welt_erzeugen() for id in _basiswege(): Game.learn(str(id)) Game.accept_mission("mensch_0") _alles_erkennen("mensch_0") _bestes_system("mensch_0") Game.progress["mensch_0"] = 0.0 Game.time_h = 7.0 Game.day = 1 for i in range(3000): Game.advance_time(1.0) Game.traum_voranbringen(24.0 / Game.DAY_SECONDS) if Game.completed.has("mensch_0") or Game.day > 30: break return Game.day ## Deckt alle Slots eines Menschen auf -- für Messungen, nicht fürs Spiel. func _alles_erkennen(id: String) -> void: Game.erkannt_slots[id] = {} for nr in range(Game.slots(id).size()): Game.erkannt_slots[id][nr] = true ## Setzt gegen jede Ursache das beste verfügbare System. func _bestes_system(id: String) -> int: var n := 0 for u in Game.ursachen_von(id): var bester := "" var beste := 0.0 var beste_nr := -1 for nr in range(Game.slots(id).size()): for tid in TechDB.fuer_ursache(str(u)): if not Game.knows(str(tid)): continue Game.cooldown.clear() var pr := Game.system_pruefen(id, nr, str(tid)) if pr["geht"] and float(pr["guete"]) > beste: beste = float(pr["guete"]) bester = str(tid) beste_nr = nr if bester != "": Game.systeme[id] = Game.systeme.get(id, {}) Game.systeme[id][beste_nr] = {"tech": bester, "guete": beste} n += 1 return n func test_erster_traum_schaffbar() -> void: abschnitt("Der erste Traum ist mit dem Startkapital zu schaffen") _ohne_momente() 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.system_anzahl("mensch_0") == 1, "Damit steht genau ein System (%d)" % Game.system_anzahl("mensch_0")) var schlimmster: int = tage.max() var bester: int = tage.min() p(schlimmster <= 12, "Der Traum erfüllt sich in allen %d Läufen (%s)" % [tage.size(), str(tage)]) p(bester >= 2, "Nie geschenkt — mindestens %d Tage" % bester) p(schlimmster <= 8, "Nie zäh — höchstens %d Tage" % schlimmster) p(Game.offene_techniken() > 1, "Und öffnet neue Wege: %d entdeckt" % Game.offene_techniken()) ## Die Probe auf den Kern: wer die Ursache trifft und die Belohnung hält, ## kommt schneller ans Ziel als wer irgendwo irgendwas einbaut. func test_lesen_lohnt_sich() -> void: abschnitt("Die Technik zu lesen zahlt sich aus") _ohne_momente() var id := "mensch_0" Game.known.clear() for t in TechDB.LIST: Game.known[str(t["id"])] = 0 Game.active_mission = id _alles_erkennen(id) Game.systeme.clear() var n := _bestes_system(id) var gezielt := Game.traum_tempo(id) # blind: dieselbe Zahl Systeme, aber in freie Slots gesetzt Game.systeme.clear() var frei: Array = [] for nr in range(Game.slots(id).size()): var g := Game.slot_grund(id, nr) if g != "schlaeft" and not Ursachen.ist_blocker(g): frei.append(nr) var gesetzt := 0 for tid in TechDB.ids(): if gesetzt >= n or gesetzt >= frei.size(): break Game.cooldown.clear() var pr := Game.system_pruefen(id, int(frei[gesetzt]), str(tid)) if pr["geht"]: Game.systeme[id] = Game.systeme.get(id, {}) Game.systeme[id][int(frei[gesetzt])] = {"tech": str(tid), "guete": pr["guete"]} gesetzt += 1 var blind := Game.traum_tempo(id) p(gezielt > blind, "Gezielt gesetzt läuft der Traum schneller: %.0f statt %.0f %% je Tag" % [gezielt, blind]) # Der Grund: die Belohnung entscheidet über die Güte Game.systeme.clear() var blocker_nr := -1 for nr in range(Game.slots(id).size()): if Ursachen.ist_blocker(Game.slot_grund(id, nr)): blocker_nr = nr break var b: Dictionary = Ursachen.BLOCKER[Game.slot_grund(id, blocker_nr)] var gleich := "" var anders := "" for tid in TechDB.fuer_ursache(str(b["ursache"])): if str(TechDB.by_id(str(tid))["belohnung"]) == str(b["belohnung"]): gleich = str(tid) elif not TechDB.entfernt_ausloeser(str(tid)): anders = str(tid) if gleich != "" and anders != "": var g1: float = float(Game.system_pruefen(id, blocker_nr, gleich)["guete"]) var g2: float = float(Game.system_pruefen(id, blocker_nr, anders)["guete"]) p(g1 > g2, "Gleiche Belohnung hält besser: Güte %.2f statt %.2f" % [g1, g2]) p(Game.system_pruefen(id, blocker_nr, "schlaffenster")["geht"] == (str(TechDB.by_id("schlaffenster")["ursache"]) == str(b["ursache"])), "Eine Technik gegen eine andere Ursache greift hier gar nicht")