extends Node ## Fährt das echte Spiel headless und prüft die Implementierungen. var fehler := 0 var main: Node3D func p(ok: bool, text: String) -> void: print((" OK " if ok else " FEHLT ") + text) if not ok: fehler += 1 func abschnitt(t: String) -> void: print("\n== " + t) func _ready() -> void: seed(20240) main = load("res://scenes/main.tscn").instantiate() add_child(main) await get_tree().process_frame await get_tree().process_frame await test_start() await test_journal() await test_traumdorf() await test_dorf_organisch() await test_haus_betreten() await test_hoersaal() await test_mission_annehmen() await test_reise_zur_erde() await test_bezirk() await test_tagesablauf() await test_wege_machbar() await test_geist() await test_verfolgung() await test_gehen_bei_tempo() await test_diagnose() await test_notieren_im_spiel() await test_tagesablauf_anzeige() await test_ablauf_im_hud() await test_probleme_und_momente() await test_eigene_menschen() await test_systeme() await test_tempo_des_traums() await test_cooldown() await test_chance_verdeckt() await test_mission_abschliessen() await test_welt_waechst() await test_techniken_verbessern_sich() await test_journal_ueberall() await test_steuerung() await test_erster_traum_schaffbar() await test_brett_nie_leer() await test_lesen_lohnt_sich() await test_tempo() print("\n=========================================") print("Fehlende Prüfungen: %d" % fehler) print("=========================================") get_tree().quit(1 if fehler > 0 else 0) func ui() -> UI: return main.ui func welt() -> Node3D: return main.welt func schritte(n: int, dt: float = 0.05, zeit: float = -1.0) -> void: for i in range(n): if zeit >= 0.0: Game.time_h = zeit main._process(dt) if welt().has_method("_process"): welt()._process(dt) for kind in welt().get_children(): if kind is Bewohner: kind._process(dt) func _panel_text() -> String: var out: Array = [] for c in ui()._content.get_children(): if c is RichTextLabel: out.append((c as RichTextLabel).get_parsed_text()) return "\n".join(out) func _journal_text() -> String: return _panel_text() 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 ui()._content.get_children(): if c is Button: out.append((c as Button).text) return "\n".join(out) func _knopf_da(anfang: String) -> bool: for c in ui()._content.get_children(): if c is Button and (c as Button).text.begins_with(anfang): return true return false func _druecke(anfang: String) -> bool: for c in ui()._content.get_children(): if c is Button and not (c as Button).disabled and (c as Button).text.begins_with(anfang): (c as Button).pressed.emit() return true return false func laufe(von: Vector3, yaw: float, frames: int) -> Vector3: main.player.set_mode(Player.Mode.TRAUM) main.player.look_enabled = true main.player.global_position = von main.player.velocity = Vector3.ZERO main.player.yaw = yaw Input.action_press("vor") for i in range(frames): await get_tree().physics_frame Input.action_release("vor") return main.player.global_position # -------------------------------------------------------------------- func test_start() -> void: abschnitt("Start") p(not ui().offen, "Kein Startbildschirm — das Spiel läuft sofort") p(welt() is DreamWorld, "Startwelt ist die Traumwelt") p(main.player != null and main.player.mode == Player.Mode.TRAUM, "Spieler läuft (kein Geist)") var arten: Dictionary = {} for n in get_tree().get_nodes_in_group("interactable"): arten[n.get_meta("kind")] = true p(arten.has("erde") and arten.has("lehre") and arten.has("brett"), "Traumwelt bietet Tor, Hörsaal und Schwarzes Brett (%s)" % ", ".join(arten.keys())) p(Game.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) # Ausgangslage wiederherstellen: das Dorf ist wieder leer Game.completed.clear() Game.welt_erzeugen() main._traumwelt() await get_tree().process_frame func test_haus_betreten() -> void: abschnitt("Orte betreten") p(not Game.ort_offen(DreamDB.haus_key(0)), "Ohne Bewohner steht kein Haus") Game.completed.append("probe_haus") Game.welt_erzeugen() main._traumwelt() await get_tree().process_frame var haus_name := str(DreamDB.PLACES[DreamDB.haus_key(0)]["name"]) p(_gebaute_orte().has(haus_name), "Mit dem ersten Bewohner steht sein Haus: %s" % haus_name) var haus := Orte.center(DreamDB.PLACES, DreamDB.haus_key(0)) var tuer := Orte.door(DreamDB.PLACES, DreamDB.haus_key(0)) var hinein: Vector3 = (haus - tuer).normalized() var yaw_hinein := atan2(-hinein.x, -hinein.z) var ziel: Vector3 = await laufe(tuer - hinein * 1.5 + Vector3(0, 1.0, 0), yaw_hinein, 80) var drin: bool = absf(ziel.x - haus.x) < 6.0 and absf(ziel.z - haus.z) < 5.0 p(drin, "Durch die Tür läuft man ins Haus (Ziel %.1f / %.1f, Haus %.1f / %.1f)" % [ziel.x, ziel.z, haus.x, haus.z]) # Die Wand hält: von der türabgewandten Seite kommt niemand hinein var rueck := haus + hinein * 11.0 + Vector3(0, 1.0, 0) var gestoppt: Vector3 = await laufe(rueck, atan2(hinein.x, hinein.z), 80) var drin2: bool = absf(gestoppt.x - haus.x) < 6.0 and absf(gestoppt.z - haus.z) < 5.0 p(not drin2, "Die Rückwand hält den Spieler auf (%.1f / %.1f)" % [gestoppt.x, gestoppt.z]) main.player.set_mode(Player.Mode.TRAUM) main.player.global_position = (welt() as DreamWorld).spawn Game.completed.clear() Game.welt_erzeugen() main._traumwelt() await get_tree().process_frame func test_hoersaal() -> void: abschnitt("Techniken werden im Hörsaal gelehrt") var pult := _marke("lehre") p(pult != null, "Der Hörsaal hat ein Lehrpult") var raum := Orte.center(DreamDB.PLACES, "hoersaal") p(absf(pult.global_position.x - raum.x) < 8.0 and absf(pult.global_position.z - raum.z) < 6.0, "Das Pult steht im Hörsaal (%.1f / %.1f)" % [pult.global_position.x, pult.global_position.z]) var im_saal: Vector3 = await laufe(Vector3(0, 1.0, 24.0), PI, 150) p(im_saal.z > 33.0, "Vom Eingang führt ein freier Mittelgang zum Pult (z = %.1f, Pult bei %.1f)" % [im_saal.z, raum.z + 3.4]) p(absf(im_saal.x) < 2.0, "Der Gang hält den Spieler in der Mitte (x = %.1f)" % im_saal.x) _hingehen(pult) p(main._fokus == pult, "Das Pult wird als Fokus erkannt") main._interagieren() p(ui().offen and ui()._kind == "lehre", "Der Hörsaal öffnet sich") var seite := _panel_text() var basis_da := true for id in _basiswege(): if not seite.contains(str(TechDB.by_id(id)["name"])): basis_da = false p(basis_da, "Die %d Basiswege werden gelehrt" % 1) var lernbar := 0 for c in ui()._content.get_children(): if c is Button and (c as Button).text.begins_with("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()) func test_tagesablauf() -> void: abschnitt("Tagesablauf der Menschen") var mensch: Bewohner = null for m in welt().humans: if m.data["id"] == "mensch_0": mensch = m p(mensch != null, "Der erste Mensch lebt auf der Erde: %s" % mensch.data["name"]) schritte(30, 0.05, 2.0) p(mensch.activity == "schlaeft", "Um 02:00 schläft er (%s)" % mensch.activity) p(mensch.global_position.distance_to( Orte.center(HumanDB.PLACES, str(mensch.data["heim"]))) < 5.0, "Und zwar zu Hause") # Jede Station des eigenen Tagesablaufs wird pünktlich erreicht var verfehlt: Array = [] for e in mensch.data["routine"]: var stunde: float = float(e[0]) + 0.5 var da := false for i in range(1200): schritte(1, 0.05, stunde) if mensch.place == e[1] and mensch._path.is_empty(): da = true break if not da or mensch.activity != e[2]: verfehlt.append("%s um %d Uhr" % [e[1], int(e[0])]) p(verfehlt.is_empty(), "Alle %d Stationen seines Tages werden erreicht (%s)" % [(mensch.data["routine"] as Array).size(), "lückenlos" if verfehlt.is_empty() else ", ".join(verfehlt)]) var luecken := 0 for m in welt().humans: for h in range(24): var r: Array = m._routine_at(float(h) + 0.5) if not HumanDB.PLACES.has(r[0]) or not Orte.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, HumanDB.NETZ, HumanDB.ANKER) _wege_pruefen("Traumwelt", DreamDB.PLACES, DreamDB.LIST, DreamDB.NETZ, DreamDB.ANKER) func _wege_pruefen(welt_name: String, places: Dictionary, liste: Array, netz: Netz = null, anker: Dictionary = {}) -> 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, netz, anker) / Bewohner.GEHTEMPO var slot_h: float = (float(r[i + 1][0]) - float(r[i][0])) if i + 1 < r.size() \ else (24.0 - float(r[i][0])) var fenster := slot_h * sek_pro_stunde if dauer >= fenster: alles_ok = false p(false, "%s: %s -> %s braucht %.0f s, Fenster nur %.0f s" % [m["name"], von, nach, dauer, fenster]) elif fenster - dauer < engste: engste = fenster - dauer engste_text = ("%s: %s -> %s (%.0f s Weg, %.0f s Fenster)" % [m["name"], von, nach, dauer, fenster]) p(alles_ok, "%s — jeder Weg passt, engster Fall: %s" % [welt_name, engste_text]) func _weg_laenge(places: Dictionary, von: String, nach: String, netz: Netz = null, anker: Dictionary = {}) -> float: var pts: Array = Orte.route(places, von, nach, Vector3.ZERO, netz, anker) var l := 0.0 var pos := Orte.center(places, von) for pt in pts: l += pos.distance_to(pt) pos = pt return l func test_geist() -> void: abschnitt("Geist-Bewegung") p(main.player.mode == Player.Mode.GEIST, "Spieler ist auf der Erde ein Geist") p(main.player.collision_layer == 0 and main.player.collision_mask == 0, "Der Geist kollidiert mit nichts — er schwebt durch Wände") var vor: Vector3 = main.player.global_position main.player.global_position = Vector3(0, 8.0, 0) main.player.velocity = Vector3(6, 0, 0) for i in range(30): await get_tree().physics_frame p(main.player.global_position.x > 0.5, "Der Geist bewegt sich frei durch den Raum") p(absf(main.player.global_position.y - 8.0) < 0.5, "Ohne Schwerkraft fällt er nicht") var lena: Bewohner = welt().humans[0] var davor: Vector3 = lena.global_position main.player.global_position = lena.global_position + Vector3(0, 1, 0.5) schritte(10) p(lena.global_position.distance_to(davor) < 2.0, "Menschen nehmen den Geist nicht wahr") ## 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"]) main.player.global_position = mensch.global_position + Vector3(0, 1.5, 2.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(900): schritte(1) await get_tree().physics_frame 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 < 6.0, "Der Geist bleibt die ganze Zeit dicht dabei (höchstens %.1f)" % weiteste) p(im_fokus > 850, "Und behält ihn im Fokus (%d von 900 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, 1.5, 2.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") _wege_pruefen("Stadt mit eigenem Menschen", HumanDB.PLACES, HumanDB.LIST) # --- 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") 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") 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") 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() _wege_pruefen("Traumdorf mit %d Häusern" % stufen, DreamDB.PLACES, DreamDB.LIST, DreamDB.NETZ, DreamDB.ANKER) _wege_pruefen("Stadt mit %d Menschen" % HumanDB.LIST.size(), HumanDB.PLACES, HumanDB.LIST, HumanDB.NETZ, HumanDB.ANKER) Game.completed.assign(merker) Game.welt_erzeugen() func test_techniken_verbessern_sich() -> void: abschnitt("Techniken verbessern sich nur durch Anwendung") 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", "sinken", "interagieren", "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) main.player.global_position = Vector3(0, 8.0, 0) main.player.velocity = Vector3.ZERO Input.action_press("steigen") for i in range(40): await get_tree().physics_frame Input.action_release("steigen") p(main.player.global_position.y > 9.0, "Leertaste lässt den Geist steigen (y = %.1f)" % main.player.global_position.y) main.player.velocity = Vector3.ZERO var hoch: float = main.player.global_position.y Input.action_press("sinken") for i in range(40): await get_tree().physics_frame Input.action_release("sinken") p(main.player.global_position.y < hoch - 1.0, "Umschalt lässt den Geist sinken (y = %.1f)" % main.player.global_position.y) ## 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") 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") 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) if flaeche / float(stufe) > 2200.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 = "" 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") 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") 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")