From 8c7da42e642cc552157e2df5aafa6692a082aef8 Mon Sep 17 00:00:00 2001 From: team3 Date: Tue, 1 Sep 2026 13:07:02 +0200 Subject: [PATCH] update --- .godot/editor/filesystem_cache10 | 29 +- .godot/global_script_class_cache.cfg | 8 + .godot/uid_cache.bin | Bin 565 -> 570 bytes README.md | 68 +- anforderungen.md | 17 +- scripts/bewohner.gd | 40 +- scripts/game.gd | 340 +++++----- scripts/human_db.gd | 110 ++-- scripts/main.gd | 24 +- scripts/orte.gd | 9 +- scripts/technique_db.gd | 75 ++- scripts/ui.gd | 236 ++++--- scripts/ursachen.gd | 136 ++++ scripts/ursachen.gd.uid | 1 + tests/shots.gd | 41 +- tests/verify.gd | 922 ++++++++++++++------------- 16 files changed, 1178 insertions(+), 878 deletions(-) create mode 100644 scripts/ursachen.gd create mode 100644 scripts/ursachen.gd.uid diff --git a/.godot/editor/filesystem_cache10 b/.godot/editor/filesystem_cache10 index ce2aa2f..d520441 100644 --- a/.godot/editor/filesystem_cache10 +++ b/.godot/editor/filesystem_cache10 @@ -1,27 +1,26 @@ 78098b89c88a74ebe8c20117b8754620 ::res://::1788113016 -anforderungen.md::TextFile::-1::1788165575::0::1::::<><><>0<>0<><>:: -README.md::TextFile::-1::1788165599::0::1::::<><><>0<>0<><>:: +anforderungen.md::TextFile::-1::1788204851::0::1::::<><><>0<>0<><>:: +README.md::TextFile::-1::1788165734::0::1::::<><><>0<>0<><>:: ::res://scenes/::1787989294 main.tscn::PackedScene::-1::1787989294::0::1::::<><><>0<>0<><>::res://scripts/main.gd -::res://scripts/::1788163668 -bewohner.gd::GDScript::2125278938812678811::1788165374::0::1::::Bewohner<>Node3D<><>0<>0<><>:: +::res://scripts/::1788209300 +bewohner.gd::GDScript::2125278938812678811::1788209097::0::1::::Bewohner<>Node3D<><>0<>0<><>:: dream_db.gd::GDScript::4217518896355437099::1788164505::0::1::::DreamDB<>RefCounted<><>0<>0<><>:: dream_world.gd::GDScript::8279158278942867258::1788164873::0::1::::DreamWorld<>Node3D<><>0<>0<><>:: earth_world.gd::GDScript::6086818627272428934::1788165412::0::1::::EarthWorld<>Node3D<><>0<>0<><>:: -game.gd::GDScript::3600922541500868814::1788165125::0::1::::<>Node<><>0<>0<><>:: -human_db.gd::GDScript::412604892032291296::1788165374::0::1::::HumanDB<>RefCounted<><>0<>0<><>:: -main.gd::GDScript::1504009331368977934::1788121650::0::1::::<>Node3D<><>0<>0<><>:: +game.gd::GDScript::3600922541500868814::1788209816::0::1::::<>Node<><>0<>0<><>:: +human_db.gd::GDScript::412604892032291296::1788208999::0::1::::HumanDB<>RefCounted<><>0<>0<><>:: +main.gd::GDScript::1504009331368977934::1788209245::0::1::::<>Node3D<><>0<>0<><>:: netz.gd::GDScript::8422943321868648339::1788163667::0::1::::Netz<>RefCounted<><>0<>0<><>:: -orte.gd::GDScript::5536340784294498671::1788163839::0::1::::Orte<>RefCounted<><>0<>0<><>:: +orte.gd::GDScript::5536340784294498671::1788209635::0::1::::Orte<>RefCounted<><>0<>0<><>:: player.gd::GDScript::8724108548430371398::1788039718::0::1::::Player<>CharacterBody3D<><>0<>0<><>:: prims.gd::GDScript::322632462553361584::1788039797::0::1::::Prims<>RefCounted<><>0<>0<><>:: -technique_db.gd::GDScript::4543286561661639701::1788120975::0::1::::TechDB<>RefCounted<><>0<>0<><>:: -ui.gd::GDScript::843471304695966286::1788122559::0::1::::UI<>CanvasLayer<><>0<>0<><>:: -::res://tests/::1788164088 -probe.gd::GDScript::5788011518062194748::1788165350::0::1::::<>Node<><>0<>0<><>:: -probe.tscn::PackedScene::-1::1788163874::0::1::::<><><>0<>0<><>::res://tests/probe.gd -shots.gd::GDScript::5999651346126871724::1788118441::0::1::::<>Node<><>0<>0<><>:: +technique_db.gd::GDScript::4543286561661639701::1788209343::0::1::::TechDB<>RefCounted<><>0<>0<><>:: +ui.gd::GDScript::843471304695966286::1788209227::0::1::::UI<>CanvasLayer<><>0<>0<><>:: +ursachen.gd::GDScript::7371116734724270900::1788208917::0::1::::Ursachen<>RefCounted<><>0<>0<><>:: +::res://tests/::1788209571 +shots.gd::GDScript::5999651346126871724::1788209816::0::1::::<>Node<><>0<>0<><>:: shots.tscn::PackedScene::-1::1787990528::0::1::::<><><>0<>0<><>::res://tests/shots.gd -verify.gd::GDScript::4392606218392155269::1788165669::0::1::::<>Node<><>0<>0<><>:: +verify.gd::GDScript::4392606218392155269::1788209816::0::1::::<>Node<><>0<>0<><>:: verify.tscn::PackedScene::-1::1787989983::0::1::::<><><>0<>0<><>::res://tests/verify.gd diff --git a/.godot/global_script_class_cache.cfg b/.godot/global_script_class_cache.cfg index 896b4a7..16e42c8 100644 --- a/.godot/global_script_class_cache.cfg +++ b/.godot/global_script_class_cache.cfg @@ -86,4 +86,12 @@ list=[{ "is_tool": false, "language": &"GDScript", "path": "res://scripts/ui.gd" +}, { +"base": &"RefCounted", +"class": &"Ursachen", +"icon": "", +"is_abstract": false, +"is_tool": false, +"language": &"GDScript", +"path": "res://scripts/ursachen.gd" }] diff --git a/.godot/uid_cache.bin b/.godot/uid_cache.bin index 9d2cea1f8e182a5228c26694262be4df432378df..6952fad12f8fe12fa824cfc93f052875043aeb4e 100644 GIT binary patch delta 46 zcmdnWvWsQI6-FhKY~9my%e~Vi85kIfQj4wh^^22>G7CzI^-GJ26O%Ji^Co{}bO8W? CqY%#k delta 41 xcmdnRvXy1S6-HT`g722+Rf7XW7#J9eQj4wh^-EHVON#Xiit>|ECx2sf0RS~U4$A-l diff --git a/README.md b/README.md index 9c7fc8f..bcb5087 100644 --- a/README.md +++ b/README.md @@ -24,41 +24,50 @@ Das Journal führt Buch — es lehrt und vergibt nichts. * **Techniken** — was du gelernt hast, mit Stufe und Erfahrung. Besser werden sie nur draußen. * **Missionen** — die Menschen, für die du dich entschieden hast, jeweils mit - ihrem **Regelwerk**. + ihrem **Tagesablauf**. -**Regelwerk** — der Kern des Spiels +**Diagnose und Systeme** — der Kern des Spiels -Jede Tätigkeit im Tag eines Menschen kann eine Technik auslösen: *wenn Lena -zögert, dann Pomodoro*. Wechselt sie in die Tätigkeit, greift die Regel von -allein, solange du auf der Erde bist. +Ein Traum scheitert an Ursachen: *kein Anfang*, *keine Zeit*, *keine Kraft*, +*kein Halt*, *kein Grund*. Jeder Mensch hat zwei bis drei davon, nie alle. Die +stehen nirgends — du findest sie nur, indem du dabeistehst. -**Ob eine Regel taugt, sagt dir niemand.** Keine Prozentzahl, kein „passt zum -Thema", keine Liste guter Momente. Du bekommst nur die Technik selbst: was sie -tut, wann sie passt, warum sie hilft. Und den Menschen: seinen Traum und das, -was im Weg steht. Beides zusammenzubringen ist das Spiel. +Sichtbar wird eine Ursache an den Tätigkeiten, die sie hervorbringt. Wer nie in +Gang kommt, räumt auf statt anzufangen, recherchiert oder schreibt Später-Listen. +Wer keine Kraft hat, schaut abends eine Serie und bleibt danach wach. Jede +dieser Tätigkeiten hat einen **Auslöser** und eine **Belohnung**. -Eine Technik darf an beliebig vielen Tätigkeiten hängen. Sie überall -hinzuhängen ist trotzdem falsch: im falschen Moment greift sie kaum und wirft -den Traum um 14 % zurück. Nach jedem Einsatz braucht sie außerdem 30 Sekunden -Ruhe — ein Fehlgriff verbrennt also den Platz, den ein guter Moment gehabt -hätte. Mehr Momente sauber abzudecken heißt, mehr Techniken zu kennen, und die -gibt es nur auf der gewachsenen Insel. +Gegen eine erkannte Ursache baust du ein System in den Tag: + + Einführen Ein System, das fehlt — in einen freien Slot. + Ändern Eine Tätigkeit, die schadet — mit gleicher Belohnung ersetzt. + +Liefert die Ersatzhandlung dieselbe Belohnung, hält sie besser. Das ist Duhiggs +goldene Regel: Auslöser und Belohnung bleiben, nur die Handlung dazwischen wird +getauscht. Manche Techniken ersetzen nichts, sie nehmen dem Verhalten den +Auslöser. + +Der Fortschritt ist keine Kette von Einzeltreffern, sondern ein **Tempo**: + + Tempo je Tag = Grundrate × Faktor je Ursache + + nichts gelöst 4 % je Tag → der Traum steht praktisch still + eine gelöst 18 % je Tag → er kommt voran + alle gelöst 61 % je Tag → in gut zwei Tagen erfüllt + +Ungelöste Ursachen bremsen multiplikativ. Zwei Bremsen sind viel schlimmer als +eine — deshalb lohnt es, alle zu finden. So laufen die zwei Schleifen: - Techniken lernen → Regelwerk anpassen → Probleme lösen - Träume erfüllen → Dorf wächst → neue Wege entdecken - -Eine Regel greift immer wieder, solange der Moment anhält — im Takt des -Cooldowns. Wie viel ein Tag einbringt, hängt also daran, wie lange ein Mensch -in der passenden Tätigkeit bleibt. + Techniken lernen → Systeme bauen → Traum läuft schneller + Träume erfüllen → Dorf wächst → neue Wege entdecken Ein erfüllter Traum bringt ein Wesen mit seinem Haus ins Dorf, und in diesem Haus wird ein neuer Weg gelehrt. Zu Beginn kennt das Dorf nur einen. -Schlafende sind unerreichbar, dieselbe Technik wirkt bei Wiederholung -schwächer, ein Fehlversuch macht kurz dicht. Von Hand geht es weiter: nah -herangehen und **E** wirkt sofort. +Nach jedem Einsatz braucht eine Technik 30 Sekunden Ruhe. Ein Einbau gelingt +nicht immer — Übung entscheidet. **Traumwelt** — eine schwebende Insel mit einem Dorf, das wächst @@ -133,18 +142,19 @@ Menschen einsetzen — auch Fehlversuche zählen. project.godot Projekt, Autoload `Game` scenes/main.tscn Einstieg - scripts/game.gd Zustand, Regelwerk, wachsende Welt, Uhr (Autoload) + scripts/game.gd Zustand, Diagnose, Systeme, Tempo, Uhr (Autoload) scripts/technique_db.gd die zwölf Techniken mit ihren Hinweisen scripts/human_db.gd Bezirk: Arterien, Blöcke, roji, Parzellen, Menschen scripts/dream_db.gd Traumdorf: wachsende Häuser, Wege, Bewohner, Lehrorte scripts/netz.gd Wegenetz: Knoten, Kanten, Andocken, kürzester Weg scripts/orte.gd Orte, Türen, Wege, Tätigkeiten (beide Welten) - scripts/bewohner.gd Bewohner mit Tagesablauf, löst Regeln aus + scripts/bewohner.gd Bewohner mit Tagesablauf und Slots scripts/main.gd Weltenwechsel, Fokus, Interaktion scripts/player.gd Läufer in der Traumwelt, Geist auf der Erde scripts/dream_world.gd Insel mit Dorf scripts/earth_world.gd Stadt mit Tag-Nacht-Wechsel - scripts/ui.gd HUD, Journal, Regelwerk, Hörsaal, Brett, Panels + scripts/ursachen.gd Ursachen, blockierende Tätigkeiten, Belohnungen + scripts/ui.gd HUD, Journal, Tagesablauf, Hörsaal, Brett, Panels scripts/prims.gd Bausteine aus Primitiven ## Prüfen @@ -156,9 +166,9 @@ Menschen einsetzen — auch Fehlversuche zählen. Tagesabläufen, gewachsenes Dorf und erzeugter Bezirk, Häuser betreten, Journal in beiden Welten, im Hörsaal lernen, Verbessern allein durchs Anwenden, Mission am Schwarzen Brett annehmen, -Regelwerk bauen und einen Tag unbeaufsichtigt laufen lassen, zur Erde reisen, +Ursachen aufdecken, Systeme bauen, Tempo messen, zur Erde reisen, Tagesabläufe über einen ganzen Tag, Wirkung der Technikwahl, Cooldown, -verdeckte Erfolgschance, keine Passungshinweise, Mehrfachbelegung von Regeln, +verdeckte Erfolgschance, keine Passungshinweise, Tempo und Nachtsprung, nur lösbare Aushänge, Mission abschließen, Insel wachsen lassen, Tastensteuerung. Beendet sich mit Code 1, wenn etwas diff --git a/anforderungen.md b/anforderungen.md index afdbf19..412fd05 100644 --- a/anforderungen.md +++ b/anforderungen.md @@ -9,15 +9,8 @@ Loops Funktionen - Bewegung: Bewegen, Springen, Geist - Journal: Gelernte Techniken, Akzetierte Missionen -- Techniken: Lernen am Lehrort, Verbessern durch Anwenden -- Missionen: Pipeline, nur lösbare werden angeboten -- Techniken: Cooldown, Fehlschlag kostet Fortschritt -- Erfolgschance und Passung bleiben verdeckt -- Eine Technik darf an mehreren Tätigkeiten hängen -- Menschen: Tagesablauf -- Tempo: Stufe 1, 2, 3 wie in Die Sims (1×, 3×, 7×) -- Im Schlaf des Menschen automatisch Stufe 3 -- Prozedurale Generierung - - Dorf: wächst Haus für Haus, verwinkelte Wege, grün und gemütlich - - Erde: Stadtbezirk nach japanischem Vorbild, Arterien und roji - - Auf der Erde sind alle Menschen von Anfang an da +- Techniken: Lernen, Verbessern +- Missionen: Pipeline +- Techniken: Cooldown, Fehlschlag +- Menschen: Tagesablauf, Tempo +- Prozedurale Generierung: Dorf, Stadt diff --git a/scripts/bewohner.gd b/scripts/bewohner.gd index 34149be..948e502 100644 --- a/scripts/bewohner.gd +++ b/scripts/bewohner.gd @@ -23,7 +23,7 @@ var _aura: MeshInstance3D var _strahl: MeshInstance3D var _path: Array = [] var _letzte_aktivitaet: String = "" -var _seit_regel: float = 0.0 +var slot: int = 0 var _bob: float = 0.0 var _offset: Vector3 @@ -70,13 +70,18 @@ func setup(d: Dictionary, index: int, orte: Dictionary, global_position = Orte.center(places, place) + _offset +## [Ort, Tätigkeit, Slotnummer] zur Stunde h. Steht in dem Slot ein System, +## ist die Tätigkeit die Ersatzhandlung. func _routine_at(h: float) -> Array: var r: Array = data["routine"] - var cur: Array = r[r.size() - 1] - for eintrag in r: - if h >= float(eintrag[0]): - cur = eintrag - return [cur[1], cur[2]] + var nr := r.size() - 1 + for i in range(r.size()): + if h >= float(r[i][0]): + nr = i + var akt: String = str(r[nr][2]) + if mit_mission: + akt = Game.slot_aktivitaet(str(data["id"]), nr) + return [str(r[nr][1]), akt, nr] func _process(roh: float) -> void: @@ -84,6 +89,7 @@ func _process(roh: float) -> void: resist_left = maxf(0.0, resist_left - delta) var soll := _routine_at(Game.time_h) + slot = int(soll[2]) if soll[0] != place: var war_unterwegs := not _path.is_empty() _path = Orte.route(places, place, soll[0], _offset, netz, anker) @@ -117,16 +123,8 @@ func _process(roh: float) -> void: _visual.rotation.z = lerp_angle(_visual.rotation.z, PI * 0.5 if activity == "schlaeft" else 0.0, 1.0 - pow(0.01, delta)) - # Solange der Moment anhält, greift die Regel erneut, sobald ihre Technik - # wieder frei ist. Der Cooldown gibt damit den Takt vor. - _seit_regel += delta if activity != _letzte_aktivitaet: _letzte_aktivitaet = activity - _seit_regel = 0.0 - _regel_pruefen() - elif _seit_regel >= 1.0: - _seit_regel = 0.0 - _regel_pruefen() var erledigt: bool = mit_mission and Game.completed.has(data["id"]) var aktiv: bool = mit_mission and Game.active_mission == data["id"] @@ -158,20 +156,6 @@ func beschriftung() -> String: return s -## Wechselt der Mensch die Tätigkeit, greift das Regelwerk der Mission von allein. -func _regel_pruefen() -> void: - if not mit_mission: - return - var res := Game.regel_ausloesen(data["id"], activity, resist_left > 0.0) - if res.is_empty(): - return - var t := TechDB.by_id(str(res["tech"])) - if res["success"]: - reagiere(true, "%s\n+%d %%" % [t["name"], int(round(float(res["gain"])))]) - else: - reagiere(false, "%s prallt ab" % t["name"]) - - ## Sichtbare Reaktion auf eine angewandte Technik. func reagiere(success: bool, text: String) -> void: var l := Prims.label(self, Vector3(0, 3.5, 0), text, 0.34, diff --git a/scripts/game.gd b/scripts/game.gd index b2d4a1a..b6f2ef6 100644 --- a/scripts/game.gd +++ b/scripts/game.gd @@ -21,9 +21,10 @@ const FEHLVERSUCH_VERLUST := 14.0 # ein Fehlschlag wirft den Traum zurück var known: Dictionary = {} # tech_id -> xp var active_mission: String = "" # human_id oder "" var progress: Dictionary = {} # human_id -> 0..100 -var used: Dictionary = {} # "human:tech" -> Anzahl var cooldown: Dictionary = {} # tech_id -> Restsekunden -var pipeline: Dictionary = {} # human_id -> {activity -> tech_id} +var systeme: Dictionary = {} # human_id -> {slot -> {tech, guete}} +var erkannt_slots: Dictionary = {} # human_id -> {slot -> true} +var beobachtung: Dictionary = {} # "human:slot" -> Sekunden var completed: Array = [] var time_h: float = 7.0 var day: int = 1 @@ -113,15 +114,6 @@ func advance_time(delta: float) -> void: while time_h >= 24.0: time_h -= 24.0 day += 1 - _gewoehnung_klingt_ab() - - -## Abstand macht eine Technik wieder wirksam: über Nacht verfällt die Hälfte -## der Gewöhnung. Wer täglich denselben Weg geht, hält ihn brauchbar — wer ihn -## an einem Tag immer wieder auffährt, stumpft ihn ab. -func _gewoehnung_klingt_ab() -> void: - for key in used: - used[key] = int(used[key]) / 2 func time_string() -> String: @@ -261,25 +253,12 @@ func angenommen(human_id: String) -> bool: return progress.has(human_id) -## Eine Mission ist machbar, wenn mindestens eine gelernte Technik das Thema -## des Menschen trifft und in seinem Tag auch einen Moment dafür findet. -## Nur solche werden am Schwarzen Brett überhaupt ausgehängt. +## Eine Mission ist machbar, wenn mindestens eine ihrer Ursachen mit einer +## gelernten Technik anzugehen ist. Die übrigen bremsen dann eben noch. func machbar(human_id: String) -> bool: - var h := HumanDB.by_id(human_id) - if h.is_empty(): - return false - var tag_liste: Array = HumanDB.aktivitaeten(human_id) - for id in known: - var t := TechDB.by_id(id) - var thema := false - for tag in t["tags"]: - if (h["tags"] as Array).has(tag): - thema = true - break - if not thema: - continue - for a in t["momente"]: - if tag_liste.has(a) and erreichbar(a): + for u in ursachen_von(human_id): + for id in known: + if str(TechDB.by_id(id)["ursache"]) == str(u): return true return false @@ -317,161 +296,188 @@ func _insel_waechst() -> void: Color(0.75, 0.95, 1)) -# ------------------------------------------------------------------- Regelwerk -# Eine Regel ist ein Wenn-Dann: tut der Mensch etwas Bestimmtes, greift die -# zugeordnete Technik von allein. Damit automatisiert der Spieler den Fortschritt. +# --------------------------------------------------------------------- Systeme +# Ein Traum scheitert an Ursachen. Die sind verdeckt und zeigen sich nur im +# Tagesablauf. Wer sie gefunden hat, baut Systeme dagegen: in einen freien +# Slot ein neues, oder auf einen Blocker ein besseres. Je mehr Ursachen +# abgedeckt sind, desto schneller kommt der Traum voran. -func regeln(human_id: String) -> Dictionary: - return pipeline.get(human_id, {}) +const BASIS_TEMPO := 45.0 # Prozent je Tag, wenn nichts mehr bremst +const GUETE_ERSATZ := 1.35 # Blocker mit gleicher Belohnung ersetzt +const GUETE_NEU := 1.0 # System in einen freien Slot gesetzt +const ERKENNEN_SEK := 3.0 # so lange muss man dabeistehen -func regel_fuer(human_id: String, activity: String) -> String: - return regeln(human_id).get(activity, "") +func slots(human_id: String) -> Array: + var h := HumanDB.by_id(human_id) + return h["routine"] if h.has("routine") else [] -func regel_anzahl(human_id: String) -> int: - return regeln(human_id).size() +func slot_grund(human_id: String, nr: int) -> String: + var r := slots(human_id) + return str(r[nr][2]) if nr >= 0 and nr < r.size() else "" -## An welchen Tätigkeiten hängt diese Technik gerade? -func regel_momente(human_id: String, tech_id: String) -> Array: - var out: Array = [] - for activity in regeln(human_id): - if regeln(human_id)[activity] == tech_id: - out.append(activity) - return out +## Was in diesem Slot wirklich passiert: die Ersatzhandlung, wenn ein System +## steht -- sonst das, was von Haus aus dort steht. +func slot_aktivitaet(human_id: String, nr: int) -> String: + var sys := system_in(human_id, nr) + if not sys.is_empty(): + return str(TechDB.by_id(str(sys["tech"]))["ersatz"]) + return slot_grund(human_id, nr) -## Leere tech_id löscht die Regel. Eine Technik darf an beliebig vielen -## Tätigkeiten hängen — ob das klug ist, muss der Spieler aus ihr herauslesen. -func regel_setzen(human_id: String, activity: String, tech_id: String) -> void: - if not pipeline.has(human_id): - pipeline[human_id] = {} - var r: Dictionary = pipeline[human_id] - if tech_id == "": - r.erase(activity) - else: - r[activity] = tech_id +func system_in(human_id: String, nr: int) -> Dictionary: + return systeme.get(human_id, {}).get(nr, {}) + + +# ------------------------------------------------------------------- Erkennen + +func erkannt(human_id: String, nr: int) -> bool: + if not Ursachen.ist_blocker(slot_grund(human_id, nr)): + return true + return erkannt_slots.get(human_id, {}).has(nr) + + +## Dabeistehen deckt auf, was dort wirklich läuft -- und damit die Ursache. +func beobachten(human_id: String, nr: int, delta: float) -> bool: + if erkannt(human_id, nr): + return false + var schluessel := "%s:%d" % [human_id, nr] + var stand: float = float(beobachtung.get(schluessel, 0.0)) + delta + beobachtung[schluessel] = stand + if stand < ERKENNEN_SEK: + return false + if not erkannt_slots.has(human_id): + erkannt_slots[human_id] = {} + erkannt_slots[human_id][nr] = true + var b := slot_grund(human_id, nr) + notified.emit("Erkannt: %s — %s" % [Ursachen.BLOCKER[b]["name"], + Ursachen.LISTE[Ursachen.ursache_von(b)]["name"]], Color(0.8, 0.95, 1)) state_changed.emit() + return true -## Greift für diese Tätigkeit eine Regel? Wird nicht nur beim Wechsel gefragt, -## sondern immer wieder — solange der Moment anhält und die Technik frei ist. -func regel_ausloesen(human_id: String, activity: String, resisting: bool) -> Dictionary: - if human_id != active_mission: - return {} - var tech_id := regel_fuer(human_id, activity) - if tech_id == "" or not known.has(tech_id): - return {} +## Eine Ursache gilt als erkannt, sobald eine ihrer Tätigkeiten aufgedeckt ist. +## Nur eine Quelle -- sonst driften Slot und Ursache auseinander. +func ursache_bekannt(human_id: String, ursache: String) -> bool: + for nr in erkannt_slots.get(human_id, {}): + if Ursachen.ursache_von(slot_grund(human_id, int(nr))) == ursache: + return true + return false + + +func ursachen_von(human_id: String) -> Array: + var h := HumanDB.by_id(human_id) + return h["ursachen"] if h.has("ursachen") else [] + + +# -------------------------------------------------------------------- Tempo + +## Steht gegen diese Ursache ein System? Gibt die Güte zurück, sonst 0. +func ursache_geloest(human_id: String, ursache: String) -> float: + var beste := 0.0 + for nr in systeme.get(human_id, {}): + var sys: Dictionary = systeme[human_id][nr] + if str(TechDB.by_id(str(sys["tech"]))["ursache"]) == ursache: + beste = maxf(beste, float(sys["guete"])) + return beste + + +## Wie schnell kommt der Traum voran? Jede ungelöste Ursache bremst. +func traum_tempo(human_id: String) -> float: + var t := BASIS_TEMPO + for u in ursachen_von(human_id): + var g := ursache_geloest(human_id, str(u)) + t *= g if g > 0.0 else float(Ursachen.LISTE[u]["faktor"]) + return t + + +# ------------------------------------------------------------- System setzen + +## Passt diese Technik in diesen Slot? Gibt Güte und Grund zurück. +func system_pruefen(human_id: String, nr: int, tech_id: String) -> Dictionary: + var res := {"geht": false, "guete": 0.0, "grund": ""} + if not known.has(tech_id): + res["grund"] = "Diese Technik kennst du nicht." + return res if not bereit(tech_id): - return {} # still: die Technik greift wieder, sobald ihre Sperre fällt - var res := apply_technique(tech_id, human_id, activity, resisting) - if not res["ok"]: - return {} - res["tech"] = tech_id + res["grund"] = "%s ist noch nicht wieder einsatzbereit." % TechDB.by_id(tech_id)["name"] + return res + var grund_akt := slot_grund(human_id, nr) + if grund_akt == "schlaeft": + res["grund"] = "Im Schlaf lässt sich nichts einrichten." + return res + if not erkannt(human_id, nr): + res["grund"] = "Was hier läuft, hast du noch nicht gesehen." + return res var t := TechDB.by_id(tech_id) - if res["success"]: - notified.emit("Regel greift — %s bei »%s« (+%d %%)" % - [t["name"], Orte.text(activity), int(round(float(res["gain"])))], - Color(0.6, 1, 0.7)) - else: - notified.emit("Regel läuft ins Leere — %s bei »%s« (−%d %%)" % - [t["name"], Orte.text(activity), int(round(float(res["verlust"])))], - Color(1, 0.7, 0.5)) + if Ursachen.ist_blocker(grund_akt): + var b: Dictionary = Ursachen.BLOCKER[grund_akt] + if str(t["ursache"]) != str(b["ursache"]): + res["grund"] = "Das hier hat einen anderen Grund." + return res + res["geht"] = true + var gleich: bool = str(t["belohnung"]) == str(b["belohnung"]) + res["guete"] = GUETE_ERSATZ if gleich or TechDB.entfernt_ausloeser(tech_id) else GUETE_NEU + return res + res["geht"] = true + res["guete"] = GUETE_NEU return res -# ----------------------------------------------------- Technik auf Mensch anwenden +## Richtet das System ein. Gelingt nicht immer -- Übung entscheidet. +func system_setzen(human_id: String, nr: int, tech_id: String, widerstand: bool) -> Dictionary: + var pruef := system_pruefen(human_id, nr, tech_id) + if not pruef["geht"]: + return {"ok": false, "grund": pruef["grund"]} + cooldown[tech_id] = TECHNIK_COOLDOWN + var chance := clampf(0.20 + 0.13 * float(skill_of(tech_id)) - (0.25 if widerstand else 0.0), + 0.05, 0.95) + var erfolg := randf() < chance + if not erfolg: + add_xp(tech_id, 1) + state_changed.emit() + return {"ok": true, "erfolg": false, "guete": 0.0} + if not systeme.has(human_id): + systeme[human_id] = {} + systeme[human_id][nr] = {"tech": tech_id, "guete": float(pruef["guete"])} + add_xp(tech_id, 2) + notified.emit("System steht: %s um %02d:00" % + [TechDB.by_id(tech_id)["name"], int(slots(human_id)[nr][0])], Color(0.6, 1, 0.7)) + state_changed.emit() + return {"ok": true, "erfolg": true, "guete": float(pruef["guete"])} -## Schlafende erreicht keine Technik — weder von Hand noch über eine Regel. + +func system_entfernen(human_id: String, nr: int) -> void: + if systeme.has(human_id): + systeme[human_id].erase(nr) + state_changed.emit() + + +func system_anzahl(human_id: String) -> int: + return systeme.get(human_id, {}).size() + + +## Der Traum kommt voran, solange Systeme stehen. +func traum_voranbringen(stunden: float) -> void: + if active_mission == "": + return + var p: float = progress.get(active_mission, 0.0) \ + + traum_tempo(active_mission) * stunden / 24.0 + progress[active_mission] = minf(p, MISSION_ZIEL) + if progress[active_mission] >= MISSION_ZIEL and not completed.has(active_mission): + var wer := active_mission + completed.append(wer) + active_mission = "" + mission_done.emit(wer) + notified.emit("%s hat es geschafft." % HumanDB.by_id(wer)["name"], Color(1, 0.85, 0.4)) + _insel_waechst() + state_changed.emit() + + +# --------------------------------------------------- Erreichbarkeit + +## Schlafende erreicht niemand. func erreichbar(activity: String) -> bool: return activity != "schlaeft" - - -## Bewertet eine Technik in genau dieser Situation, ohne sie anzuwenden. -func evaluate(tech_id: String, human_id: String, activity: String, resisting: bool) -> Dictionary: - var t := TechDB.by_id(tech_id) - var h := HumanDB.by_id(human_id) - var skill := skill_of(tech_id) - var res := { - "chance": 0.0, "tag": false, "moment": false, "blocked": false, - "grund": "", "skill": skill, - } - if not erreichbar(activity): - res["blocked"] = true - res["grund"] = "Sie oder er schläft. Techniken erreichen nur wache Menschen." - return res - if skill == 0: - res["blocked"] = true - res["grund"] = "Technik nicht gelernt." - return res - - for tag in t["tags"]: - if h["tags"].has(tag): - res["tag"] = true - break - res["moment"] = t["momente"].has(activity) - - # Der Moment wiegt am schwersten: eine Technik zur falschen Zeit greift kaum, - # egal wie gut sie zum Menschen passt. Genau das steht in ihrem "Wann". - var c := 0.05 + 0.09 * float(skill) - if res["tag"]: - c += 0.20 - if res["moment"]: - c += 0.35 - if resisting: - c -= 0.30 - res["grund"] = "Der Mensch macht gerade dicht." - res["chance"] = clampf(c, 0.05, 0.95) - return res - - -## Wendet die Technik wirklich an: würfelt, verteilt EP und Fortschritt. -func apply_technique(tech_id: String, human_id: String, activity: String, resisting: bool) -> Dictionary: - if not bereit(tech_id): - return {"ok": false, "grund": "%s ist noch nicht wieder einsatzbereit." % - TechDB.by_id(tech_id)["name"]} - var ev := evaluate(tech_id, human_id, activity, resisting) - if ev["blocked"]: - return {"ok": false, "grund": ev["grund"]} - cooldown[tech_id] = TECHNIK_COOLDOWN - - var roll := randf() - var success: bool = roll < float(ev["chance"]) - var key := "%s:%s" % [human_id, tech_id] - var wiederholung: int = used.get(key, 0) - used[key] = wiederholung + 1 - - var gain := 0.0 - var verlust := 0.0 - if success: - var base := 8.0 + 2.0 * float(ev["skill"]) - if ev["tag"]: - base += 5.0 - if ev["moment"]: - base += 5.0 - gain = base / (1.0 + 0.6 * float(wiederholung)) - add_xp(tech_id, 2) - else: - verlust = FEHLVERSUCH_VERLUST - add_xp(tech_id, 1) - - var done := false - if human_id == active_mission: - var p: float = progress.get(human_id, 0.0) + gain - verlust - progress[human_id] = clampf(p, 0.0, MISSION_ZIEL) - if progress[human_id] >= MISSION_ZIEL and not completed.has(human_id): - completed.append(human_id) - active_mission = "" - done = true - mission_done.emit(human_id) - notified.emit("%s hat es geschafft." % HumanDB.by_id(human_id)["name"], - Color(1, 0.85, 0.4)) - _insel_waechst() - state_changed.emit() - - return { - "ok": true, "success": success, "chance": ev["chance"], "gain": gain, - "verlust": verlust, "wiederholung": wiederholung, "mission_done": done, - "tag": ev["tag"], "moment": ev["moment"], - } diff --git a/scripts/human_db.gd b/scripts/human_db.gd index b1d9879..8f6377e 100644 --- a/scripts/human_db.gd +++ b/scripts/human_db.gd @@ -101,32 +101,20 @@ static var ANLIEGEN: Array = [ }, ] -## Tagesabläufe: Platzhalter werden durch Wohnung und Arbeitsplatz ersetzt. -## Tagesabläufe: Platzhalter werden durch Wohnung und Arbeitsplatz ersetzt. -## Die Reihenfolge passt zu den Anliegen — wer aufschiebt, arbeitet zu Hause. -static var ABLAEUFE: Array = [ - [[0, "heim", "schlaeft"], [7, "heim", "zoegert"], [10, "heim", "arbeitet"], - [13, "park", "pause"], [15, "heim", "arbeitet"], [18, "cafe", "wartet"], - [20, "heim", "zoegert"], [22, "heim", "gruebelt"], [23, "heim", "abendroutine"]], - [[0, "heim", "schlaeft"], [4, "arbeit", "arbeitet"], [10, "cafe", "pause"], - [12, "heim", "essen"], [14, "heim", "ruht"], [16, "heim", "zoegert"], - [18, "park", "unterwegs"], [20, "heim", "muede"], [22, "heim", "abendroutine"]], - [[0, "heim", "schlaeft"], [8, "heim", "morgenroutine"], [10, "arbeit", "arbeitet"], - [13, "cafe", "angespannt"], [15, "arbeit", "wartet"], [18, "heim", "gruebelt"], - [20, "park", "unterwegs"], [22, "heim", "abendroutine"]], - [[0, "heim", "schlaeft"], [6, "heim", "morgenroutine"], [8, "arbeit", "arbeitet"], - [12, "cafe", "essen"], [14, "arbeit", "arbeitet"], [19, "arbeit", "angespannt"], - [21, "heim", "essen"], [23, "heim", "muede"]], - [[0, "heim", "schlaeft"], [7, "heim", "morgenroutine"], [9, "arbeit", "arbeitet"], - [12, "cafe", "essen"], [14, "arbeit", "arbeitet"], [17, "park", "unterwegs"], - [19, "heim", "aufraeumen"], [21, "heim", "gruebelt"], [23, "heim", "abendroutine"]], +## Der Grundtag. Blocker werden später in passende Fenster gesetzt. +## [Stunde, Ort, Tätigkeit, Fenster] +static var GRUNDTAG: Array = [ + [0, "heim", "schlaeft", "nacht"], + [7, "heim", "morgenroutine", "morgen"], + [9, "arbeit", "arbeitet", "tag"], + [12, "cafe", "essen", "tag"], + [14, "arbeit", "arbeitet", "tag"], + [17, "park", "unterwegs", "abend"], + [19, "heim", "ruht", "abend"], + [21, "heim", "ruht", "abend"], + [23, "heim", "abendroutine", "nacht"], ] - -## Zu jeder Technik ein Anliegen und ein Tagesablauf, bei dem sie greift: -## [Anliegen, Ablauf]. Mensch i bekommt den Eintrag zu Technik i — und Technik i -## ist genau dann entdeckt, wenn i Träume erfüllt sind. So hängt am Schwarzen -## Brett immer mindestens ein Mensch, dem mit dem Gelernten zu helfen ist. static var PASSEND: Array = [ [0, 0], # Pomodoro -> aufschieben, zögert [6, 4], # Zwei-Minuten -> Werkstatt, räumt auf @@ -403,25 +391,68 @@ static func _anker_setzen() -> void: ANKER[key] = int(NETZ.andocken(tuer)["knoten"]) -static func _mensch(i: int) -> Dictionary: - # Bis alle Wege entdeckt sind, folgt jeder Mensch dem Weg gleicher Nummer. - # Danach mischt sich das Feld wieder. - var a_nr: int = i % ANLIEGEN.size() - var b_nr: int = i % ABLAEUFE.size() - if i < PASSEND.size(): - a_nr = int(PASSEND[i][0]) - b_nr = int(PASSEND[i][1]) - var anliegen: Dictionary = ANLIEGEN[a_nr] - var heim := heim_key(i) - var arbeit := _naechster_arbeitsplatz(heim, i) - var ablauf: Array = [] - for e in ABLAEUFE[b_nr]: +## Woran der Traum dieses Menschen scheitert. Zwei bis drei Ursachen, nie alle. +## Die erste hängt am Weg gleicher Nummer -- so ist der neueste Mensch immer +## mit dem neuesten Weg anzugehen. +static func ursachen_von(i: int) -> Array: + var alle: Array = Ursachen.LISTE.keys() + var erste: String = str(alle[i % alle.size()]) + if i < TechDB.LIST.size(): + erste = str(TechDB.LIST[i]["ursache"]) + var out: Array = [erste] + var zweite: String = str(alle[(alle.find(erste) + 1 + i % 3) % alle.size()]) + if zweite != erste: + out.append(zweite) + if i % 3 == 2: + var dritte: String = str(alle[(alle.find(erste) + 3) % alle.size()]) + if not out.has(dritte): + out.append(dritte) + return out + + +## Der Tagesablauf: Grundtag, in den je Ursache ein Blocker gesetzt wird. +static func _tagesablauf(i: int, heim: String, arbeit: String) -> Array: + var slots: Array = [] + for e in GRUNDTAG: var ort: String = str(e[1]) if ort == "heim": ort = heim elif ort == "arbeit": ort = arbeit - ablauf.append([e[0], ort, e[2]]) + slots.append([int(e[0]), ort, str(e[2]), str(e[3])]) + var belegt: Dictionary = {} + var nr := 0 + for u in ursachen_von(i): + for fenster in ["tag", "abend", "morgen", "nacht"]: + var kandidaten: Array = Ursachen.blocker_fuer(str(u), fenster) + if kandidaten.is_empty(): + continue + var blocker: String = str(kandidaten[(i + nr) % kandidaten.size()]) + var gesetzt := false + for k in range(slots.size()): + if belegt.has(k) or str(slots[k][2]) == "schlaeft": + continue + if str(slots[k][3]) != fenster: + continue + slots[k][2] = blocker + belegt[k] = true + gesetzt = true + break + if gesetzt: + break + nr += 1 + var out: Array = [] + for e in slots: + out.append([e[0], e[1], e[2]]) + return out + + +static func _mensch(i: int) -> Dictionary: + var anliegen: Dictionary = ANLIEGEN[i % ANLIEGEN.size()] + if i < PASSEND.size(): + anliegen = ANLIEGEN[int(PASSEND[i][0])] + var heim := heim_key(i) + var arbeit := _naechster_arbeitsplatz(heim, i) return { "id": "mensch_%d" % i, "name": "%s %s" % [_vorname(i), _nachname(i)], @@ -430,10 +461,11 @@ static func _mensch(i: int) -> Dictionary: "traum": str(anliegen["traum"]), "problem": str(anliegen["problem"]), "tags": (anliegen["tags"] as Array).duplicate(), + "ursachen": ursachen_von(i), "mission": true, "heim": heim, "farbe": Color.from_hsv(fmod(0.02 + float(i) * 0.191, 1.0), 0.5, 0.8), - "routine": ablauf, + "routine": _tagesablauf(i, heim, arbeit), } diff --git a/scripts/main.gd b/scripts/main.gd index 6894550..e2bd149 100644 --- a/scripts/main.gd +++ b/scripts/main.gd @@ -65,15 +65,28 @@ func _setze_welt(neu: Node3D) -> void: func _process(delta: float) -> void: - Game.advance_time(delta * Game.zeit_faktor()) + var dt := delta * Game.zeit_faktor() + Game.advance_time(dt) + Game.traum_voranbringen(dt * 24.0 / Game.DAY_SECONDS) Game.teleport_lock = maxf(0.0, Game.teleport_lock - delta) if ui.offen: ui.setze_hinweis("") return _fokus_suchen() + _beobachten(dt) _hinweis_setzen() +## Dabeistehen deckt auf, was der Mensch gerade wirklich tut. +func _beobachten(dt: float) -> void: + if _fokus_mensch == null or not auf_erde: + return + var id: String = str(_fokus_mensch.data["id"]) + if id != Game.active_mission: + return + Game.beobachten(id, int(_fokus_mensch.slot), dt) + + func _fokus_suchen() -> void: _fokus = null _fokus_mensch = null @@ -99,8 +112,13 @@ func _fokus_suchen() -> void: func _hinweis_setzen() -> void: if _fokus_mensch != null: - ui.setze_hinweis("[E] %s ansprechen — %s" % - [_fokus_mensch.data["name"], Orte.text(_fokus_mensch.activity)]) + var mid: String = str(_fokus_mensch.data["name"]) + var mnr: int = int(_fokus_mensch.slot) + var mkey: String = str(_fokus_mensch.data["id"]) + if mkey == Game.active_mission and not Game.erkannt(mkey, mnr): + ui.setze_hinweis("%s — schau zu, was sie hier tut …" % mid) + else: + ui.setze_hinweis("[E] %s — %s" % [mid, Orte.text(_fokus_mensch.activity)]) elif _fokus != null: var kind: String = _fokus.get_meta("kind") var titel: String = _fokus.get_meta("titel") diff --git a/scripts/orte.gd b/scripts/orte.gd index c14001f..f56aa07 100644 --- a/scripts/orte.gd +++ b/scripts/orte.gd @@ -37,8 +37,15 @@ static var ACTIVITY_TEXT: Dictionary = { } +## Blocker tragen ihren Namen im Ursachen-Katalog, alles andere hier. static func text(a: String) -> String: - return ACTIVITY_TEXT.get(a, a) + if Ursachen.ist_blocker(a): + return str(Ursachen.BLOCKER[a]["name"]) + return str(ACTIVITY_TEXT.get(a, a)) + + +static func kennt(a: String) -> bool: + return ACTIVITY_TEXT.has(a) or Ursachen.ist_blocker(a) static func center(places: Dictionary, key: String) -> Vector3: diff --git a/scripts/technique_db.gd b/scripts/technique_db.gd index 7cf85d4..28bfdf0 100644 --- a/scripts/technique_db.gd +++ b/scripts/technique_db.gd @@ -9,7 +9,10 @@ static var LIST: Array = [ "was": "25 Minuten am Stück an einer Sache arbeiten, dann 5 Minuten Pause. Nach vier Runden eine lange Pause.", "wann": "Wenn eine Aufgabe zu groß wirkt und der Anfang immer wieder verschoben wird.", "warum": "Ein klar begrenzter Zeitraum senkt die Einstiegshürde. Der Kopf muss nur 25 Minuten durchhalten, nicht den ganzen Tag.", - "woran": "jemandem, der zögert — oder der arbeitet und trotzdem nicht vorankommt.", + "woran": "jemandem, der aufräumt oder recherchiert, statt endlich anzufangen.", + "ursache": "anfang", + "belohnung": "erfolg", + "ersatz": "arbeitet in Blöcken", "tags": ["prokrastination", "fokus"], "momente": ["arbeitet", "zoegert"], "farbe": Color(0.95, 0.45, 0.30), @@ -20,7 +23,10 @@ static var LIST: Array = [ "was": "Was unter zwei Minuten dauert, sofort erledigen. Große Vorhaben auf einen Zwei-Minuten-Anfang schrumpfen.", "wann": "Bei einem Berg von Kleinkram oder wenn schon der erste Schritt blockiert.", "warum": "Der Start kostet die meiste Energie. Zwei Minuten sind zu klein zum Ablehnen und ziehen meist die ganze Aufgabe nach sich.", - "woran": "jemandem, der zögert, lieber aufräumt als anzufangen, oder beim Arbeiten im Kleinkram hängt.", + "woran": "jemandem, der aufräumt statt anzufangen oder eine Später-Liste schreibt.", + "ursache": "anfang", + "belohnung": "erfolg", + "ersatz": "fängt zwei Minuten an", "tags": ["prokrastination", "ueberforderung"], "momente": ["zoegert", "aufraeumen", "arbeitet"], "farbe": Color(0.98, 0.72, 0.25), @@ -31,7 +37,10 @@ static var LIST: Array = [ "was": "Den Vorsatz an einen konkreten Auslöser binden: \"Wenn ich X tue, dann mache ich Y.\"", "wann": "Wenn ein Vorhaben im Alltag jedes Mal untergeht.", "warum": "Der Plan hängt an einer Situation statt an Willenskraft. Der Auslöser erinnert von allein.", - "woran": "der Morgenroutine und der Pause — den festen Punkten, an die sich ein Vorsatz hängen lässt.", + "woran": "jemandem, der eine Später-Liste schreibt und dann doch nichts tut.", + "ursache": "anfang", + "belohnung": "ordnung", + "ersatz": "folgt dem Wenn-Dann-Plan", "tags": ["gewohnheit", "prokrastination"], "momente": ["morgenroutine", "pause"], "farbe": Color(0.45, 0.80, 0.55), @@ -42,7 +51,10 @@ static var LIST: Array = [ "was": "Eine neue, kleine Gewohnheit direkt an eine bestehende anhängen.", "wann": "Wenn eine neue Routine nie einen Platz im Tag findet.", "warum": "Die alte Gewohnheit ist der Anker. Es muss kein neuer Zeitpunkt erfunden werden.", - "woran": "den Ankern des Tages: Morgenroutine, Essen, Abendroutine.", + "woran": "jemandem, der auf etwas anderes umschaltet, sobald es zäh wird.", + "ursache": "halt", + "belohnung": "ordnung", + "ersatz": "hängt es an die Routine", "tags": ["gewohnheit"], "momente": ["morgenroutine", "essen", "abendroutine"], "farbe": Color(0.40, 0.70, 0.85), @@ -53,7 +65,10 @@ static var LIST: Array = [ "was": "Ein großes Ziel in benennbare Einzelschritte teilen, bis der nächste Schritt trivial ist.", "wann": "Wenn jemand vor einem Vorhaben erstarrt und nicht weiß, wo er anfangen soll.", "warum": "Unklarheit lähmt stärker als Aufwand. Ein sichtbarer nächster Schritt macht Handeln wieder möglich.", - "woran": "jemandem, der grübelt statt anzufangen, oder der arbeitet und sich im Vorhaben verliert.", + "woran": "jemandem, der recherchiert statt anzufangen oder eine Später-Liste schreibt.", + "ursache": "anfang", + "belohnung": "ordnung", + "ersatz": "zerlegt die Aufgabe", "tags": ["ueberforderung", "prokrastination", "ziel"], "momente": ["gruebelt", "arbeitet"], "farbe": Color(0.75, 0.55, 0.95), @@ -64,7 +79,10 @@ static var LIST: Array = [ "was": "Das gewünschte Verhalten leichter machen, das störende schwerer. Handy in einen anderen Raum, Instrument sichtbar aufstellen.", "wann": "Wenn gute Vorsätze regelmäßig an Ablenkung scheitern.", "warum": "Verhalten folgt dem geringsten Widerstand. Die Umgebung wirkt zuverlässiger als Disziplin.", - "woran": "jemandem, der aufräumt, arbeitet oder sich ausruht — dann ist der Raum um ihn herum greifbar.", + "woran": "jemandem, der noch eine Runde spielt oder ständig umschaltet.", + "ursache": "halt", + "belohnung": "", + "ersatz": "hat es sich leicht gemacht", "tags": ["ablenkung", "gewohnheit"], "momente": ["aufraeumen", "arbeitet", "ruht"], "farbe": Color(0.55, 0.85, 0.40), @@ -75,7 +93,10 @@ static var LIST: Array = [ "was": "Einen belastenden Gedanken prüfen und durch eine ebenso wahre, aber tragfähigere Sicht ersetzen.", "wann": "Wenn jemand in einem Gedanken feststeckt, der ihn klein hält.", "warum": "Nicht das Ereignis belastet, sondern seine Deutung. Eine andere Deutung verändert Gefühl und Handlung.", - "woran": "jemandem, der grübelt, angespannt ist oder sich ausruht und den Gedanken doch nicht loswird.", + "woran": "jemandem, der sich mit anderen vergleicht und dabei klein wird.", + "ursache": "grund", + "belohnung": "einordnung", + "ersatz": "deutet den Gedanken um", "tags": ["selbstzweifel", "angst"], "momente": ["gruebelt", "ruht", "angespannt"], "farbe": Color(0.95, 0.55, 0.75), @@ -86,7 +107,10 @@ static var LIST: Array = [ "was": "Vier Sekunden einatmen, vier halten, vier ausatmen, vier halten. Mehrere Runden lang.", "wann": "Bei akuter Anspannung, kurz vor einem gefürchteten Gespräch oder Auftritt.", "warum": "Langes Ausatmen bremst den Alarmzustand des Körpers. Der Puls sinkt, der Kopf wird wieder klar.", - "woran": "jemandem, der angespannt ist, wartet oder in der Pause nicht runterkommt.", + "woran": "jemandem, der abends eine Serie schaut, um überhaupt runterzukommen.", + "ursache": "kraft", + "belohnung": "abschalten", + "ersatz": "atmet sich ruhig", "tags": ["angst", "stress"], "momente": ["angespannt", "wartet", "pause"], "farbe": Color(0.40, 0.85, 0.95), @@ -97,7 +121,10 @@ static var LIST: Array = [ "was": "Mit sich selbst so sprechen wie mit einem guten Freund in derselben Lage.", "wann": "Nach einem Fehler oder Rückschlag, wenn die innere Kritik lauter wird als der Vorsatz.", "warum": "Harte Selbstkritik erhöht die Vermeidung. Freundlichkeit hält handlungsfähig und senkt die Abbruchquote.", - "woran": "jemandem, der nach einem Fehler grübelt, angespannt ist oder sich ausruht, ohne zur Ruhe zu kommen.", + "woran": "jemandem, der am Telefon klagt, statt weiterzumachen.", + "ursache": "grund", + "belohnung": "verbundenheit", + "ersatz": "ist freundlich mit sich", "tags": ["selbstzweifel", "rueckschlag"], "momente": ["gruebelt", "ruht", "angespannt"], "farbe": Color(1.0, 0.80, 0.60), @@ -108,7 +135,10 @@ static var LIST: Array = [ "was": "Feste Aufsteh- und Schlafenszeit, die letzte Stunde ohne Bildschirm, Zimmer dunkel und kühl.", "wann": "Bei Dauermüdigkeit, Reizbarkeit und wegbrechender Konzentration.", "warum": "Der innere Rhythmus braucht feste Anker. Erholter Schlaf ist die Grundlage für jede andere Veränderung.", - "woran": "der Abendroutine, an jemandem, der müde ist oder sich mitten am Tag ausruht.", + "woran": "jemandem, der wach bleibt oder abends am Kühlschrank steht.", + "ursache": "kraft", + "belohnung": "zeit_fuer_sich", + "ersatz": "hält das Schlaffenster", "tags": ["erschoepfung", "gesundheit"], "momente": ["abendroutine", "muede", "ruht"], "farbe": Color(0.45, 0.50, 0.90), @@ -119,7 +149,10 @@ static var LIST: Array = [ "was": "Eine klare, kleine Bitte an eine konkrete Person richten, statt allgemein zu hoffen.", "wann": "Wenn jemand allein an etwas hängen bleibt, das andere leicht lösen könnten.", "warum": "Vage Bitten werden überhört. Menschen helfen gern, wenn sie genau wissen, was zu tun ist.", - "woran": "jemandem, der wartet, Pause macht oder unterwegs ist — da ist Luft für eine Bitte.", + "woran": "jemandem, der zu allem ja sagt und alles allein stemmt.", + "ursache": "zeit", + "belohnung": "anschluss", + "ersatz": "bittet um Hilfe", "tags": ["einsamkeit", "ueberforderung"], "momente": ["wartet", "pause", "unterwegs"], "farbe": Color(0.90, 0.90, 0.45), @@ -130,7 +163,10 @@ static var LIST: Array = [ "was": "Absagen und zugleich benennen, was ginge: \"Heute nicht, am Donnerstag gern.\"", "wann": "Wenn der eigene Kalender von fremden Anliegen aufgebraucht wird.", "warum": "Ein nacktes Nein fühlt sich riskant an. Mit Alternative bleibt die Beziehung heil und die Zeit geschützt.", - "woran": "jemandem, der arbeitet, angespannt ist oder unterwegs zwischen Terminen hängt.", + "woran": "jemandem, der zu allem ja sagt, bis für ihn nichts übrig bleibt.", + "ursache": "zeit", + "belohnung": "anerkennung", + "ersatz": "sagt Nein mit Alternative", "tags": ["stress", "grenzen"], "momente": ["arbeitet", "angespannt", "unterwegs"], "farbe": Color(0.85, 0.35, 0.45), @@ -138,6 +174,21 @@ static var LIST: Array = [ ] +## Techniken, die gegen diese Ursache helfen. +static func fuer_ursache(ursache: String) -> Array: + var out: Array = [] + for t in LIST: + if t["ursache"] == ursache: + out.append(t["id"]) + return out + + +## Manche Techniken ersetzen die Handlung nicht, sie nehmen ihr den Auslöser. +## Die passen überall, wo ihre Ursache stimmt. +static func entfernt_ausloeser(id: String) -> bool: + return str(by_id(id).get("belohnung", "")) == "" + + static func by_id(id: String) -> Dictionary: for t in LIST: if t["id"] == id: diff --git a/scripts/ui.gd b/scripts/ui.gd index 5a18220..1b54200 100644 --- a/scripts/ui.gd +++ b/scripts/ui.gd @@ -154,9 +154,9 @@ func _hud_aktualisieren() -> void: [Game.known.size(), Game.offene_techniken(), TechDB.LIST.size()]) if Game.active_mission != "": var h := HumanDB.by_id(Game.active_mission) - zeilen.append("Mission: [color=#ffdd77]%s[/color] — %d %% · %d Regeln" % + zeilen.append("Mission: [color=#ffdd77]%s[/color] — %d %% · %d %% je Tag" % [h["name"], int(Game.mission_progress(Game.active_mission)), - Game.regel_anzahl(Game.active_mission)]) + int(round(Game.traum_tempo(Game.active_mission)))]) zeilen.append("[color=#aaaaaa]%s[/color]" % h["traum"]) elif Game.completed.size() >= HumanDB.LIST.size(): zeilen.append("[color=#88ff99]Alle Träume erfüllt.[/color]") @@ -347,84 +347,126 @@ func _journal_missionen() -> void: _text("[color=#aaaaaa]Zurückgestellt — %d %%[/color]" % int(Game.mission_progress(id)), 15) if not Game.completed.has(id): - _knopf("Regelwerk — %d Regeln" % Game.regel_anzahl(id), - func() -> void: pipeline_panel(id)) + _knopf("Tagesablauf — %d Systeme" % Game.system_anzahl(id), + func() -> void: systeme_panel(id)) _trenner() -# -------------------------------------------------------- Regelwerk einer Mission +# ---------------------------------------------- Diagnose und Systeme -## Wenn-Dann-Regeln: tut der Mensch etwas Bestimmtes, greift die zugeordnete -## Technik von allein. So automatisiert der Spieler den Fortschritt. -func pipeline_panel(human_id: String) -> void: - var scroll_alt := _scroll.scroll_vertical if offen and _kind == "pipeline" else 0 +## Der Traum scheitert an Ursachen. Die stehen nicht im Panel -- man findet sie, +## indem man dabeisteht. Gegen jede gefundene Ursache baut man ein System in +## den Tag. Je mehr abgedeckt ist, desto schneller kommt der Traum voran. +func systeme_panel(human_id: String) -> void: + var scroll_alt := _scroll.scroll_vertical if offen and _kind == "systeme" else 0 var h := HumanDB.by_id(human_id) - _leeren("Regelwerk — %s" % h["name"]) - _text("Jede Tätigkeit im Tag von %s kann eine Technik auslösen. " % h["name"] + - "Wechselt sie hinein, greift die Regel von allein — solange du auf der Erde bist.\n" + - "Nach jedem Einsatz braucht eine Technik Ruhe, bevor sie wieder anschlägt.", 15) - _text("[color=#aaaaaa]Fortschritt: [b]%d %%[/b] · Regeln: [b]%d[/b][/color]" % - [int(Game.mission_progress(human_id)), Game.regel_anzahl(human_id)], 15) + _leeren("%s — Tagesablauf" % h["name"]) + _text("[b]Traum:[/b] %s" % h["traum"], 16) + _text("[b]Fortschritt:[/b] %d %% · [b]Tempo:[/b] %d %% je Tag" % + [int(Game.mission_progress(human_id)), int(round(Game.traum_tempo(human_id)))], 16) _trenner() - for activity in HumanDB.aktivitaeten(human_id): - var tat := Orte.text(activity) - if not Game.erreichbar(activity): - _knopf("%s → unerreichbar" % tat, func() -> void: pass, false) + + _text("[b]Woran es scheitert[/b]", 17) + var offene := 0 + for u in Game.ursachen_von(human_id): + var name: String = str(u) + if not Game.ursache_bekannt(human_id, name): + _text("[color=#8e93a8]? — noch nicht erkannt[/color]", 15) + offene += 1 continue - var tech: String = Game.regel_fuer(human_id, activity) - var beschriftung := "%s → keine Regel" % tat - if tech != "": - beschriftung = "%s → %s" % [tat, TechDB.by_id(tech)["name"]] - if not Game.bereit(tech): - beschriftung += " (noch gesperrt)" - _knopf(beschriftung, func() -> void: regel_panel(human_id, activity)) + var g := Game.ursache_geloest(human_id, name) + var stand := "[color=#88ff99]gelöst[/color]" if g > 0.0 else \ + "[color=#ff9977]offen · bremst auf %d %%[/color]" % \ + int(float(Ursachen.LISTE[name]["faktor"]) * 100.0) + _text("[b]%s[/b] — %s %s" % [Ursachen.LISTE[name]["name"], + Ursachen.LISTE[name]["kurz"], stand], 15) + if offene > 0: + _text("[color=#aaaaaa]Steh dabei, wenn sie etwas tut, das du noch nicht " + + "kennst. Dann zeigt sich, warum.[/color]", 14) + _trenner() + + _text("[b]Ihr Tag[/b]", 17) + for nr in range(Game.slots(human_id).size()): + _slot_zeile(human_id, nr) _trenner() _knopf("Zurück zum Journal", func() -> void: journal_panel("missionen")) - _zeigen("pipeline") + _zeigen("systeme") _scroll_wunsch = scroll_alt _scroll_frames = 4 -## Welche Technik soll bei dieser Tätigkeit greifen? -func regel_panel(human_id: String, activity: String) -> void: - var h := HumanDB.by_id(human_id) - _leeren("Regel für »%s«" % Orte.text(activity)) - if Game.known.is_empty(): - _text("[color=#ffcc66]Du kennst keine Technik. Im Hörsaal der Wege " + - "werden sie gelehrt.[/color]", 16) - _knopf("Zurück", func() -> void: pipeline_panel(human_id)) - _zeigen("regel") +func _slot_zeile(human_id: String, nr: int) -> void: + var slot: Array = Game.slots(human_id)[nr] + var stunde := "%02d:00" % int(slot[0]) + var grund := Game.slot_grund(human_id, nr) + if grund == "schlaeft": + _knopf("%s schläft" % stunde, func() -> void: pass, false) return - _text("Was soll greifen, wenn %s %s? Ob eine Technik hier taugt, steht in ihr " % - [h["name"], Orte.text(activity)] + - "selbst — nachzulesen im Journal. Ein Fehlgriff wirft den Traum zurück.", 15) + var sys := Game.system_in(human_id, nr) + if not sys.is_empty(): + _knopf("%s %s ← %s" % [stunde, Game.slot_aktivitaet(human_id, nr), + TechDB.by_id(str(sys["tech"]))["name"]], + func() -> void: slot_panel(human_id, nr)) + return + if not Game.erkannt(human_id, nr): + _knopf("%s ?" % stunde, func() -> void: pass, false) + return + var text := Orte.text(grund) + if Ursachen.ist_blocker(grund): + text = "%s (%s)" % [Ursachen.BLOCKER[grund]["name"], + Ursachen.LISTE[Ursachen.BLOCKER[grund]["ursache"]]["name"]] + _knopf("%s %s" % [stunde, text], func() -> void: slot_panel(human_id, nr)) + + +## Was soll in diesem Slot stehen? +func slot_panel(human_id: String, nr: int) -> void: + var h := HumanDB.by_id(human_id) + var grund := Game.slot_grund(human_id, nr) + var stunde := "%02d:00" % int(Game.slots(human_id)[nr][0]) + _leeren("%s um %s" % [h["name"], stunde]) + if Ursachen.ist_blocker(grund): + var b: Dictionary = Ursachen.BLOCKER[grund] + _text("Sie %s.\n[b]Auslöser:[/b] %s\n[b]Belohnung:[/b] %s" % + [b["name"], b["ausloeser"], Ursachen.belohnung_text(str(b["belohnung"]))], 16) + _text("[color=#aaaaaa]Eine Ersatzhandlung muss dieselbe Belohnung liefern, " + + "sonst hält sie nicht.[/color]", 15) + else: + _text("Hier ist Platz. Ein neues System passt in diesen Slot.", 16) _trenner() + var sys := Game.system_in(human_id, nr) + if not sys.is_empty(): + _text("[color=#88ff99]Hier steht: %s[/color]" % + TechDB.by_id(str(sys["tech"]))["name"], 16) + _knopf("System wieder herausnehmen", func() -> void: + Game.system_entfernen(human_id, nr) + systeme_panel(human_id)) + _trenner() for id in TechDB.ids(): if not Game.knows(id): continue + var pruef := Game.system_pruefen(human_id, nr, id) var t := TechDB.by_id(id) - var ev := Game.evaluate(id, human_id, activity, false) - var wo: Array = Game.regel_momente(human_id, id) - var gewaehlt := "" - if wo.has(activity): - gewaehlt = " — hier eingestellt" - elif not wo.is_empty(): - var namen: Array = [] - for a in wo: - namen.append("»%s«" % Orte.text(a)) - gewaehlt = " — hängt an %s" % ", ".join(namen) var beschriftung := "%s · Stufe %d" % [t["name"], Game.skill_of(id)] - if ev["blocked"]: - beschriftung += " — " + str(ev["grund"]) - elif gewaehlt != "": - beschriftung += gewaehlt - _knopf(beschriftung, func() -> void: - Game.regel_setzen(human_id, activity, id) - pipeline_panel(human_id), not ev["blocked"]) - _knopf("Keine Regel für diese Tätigkeit", func() -> void: - Game.regel_setzen(human_id, activity, "") - pipeline_panel(human_id)) - _zeigen("regel") + if not pruef["geht"]: + beschriftung += " — " + str(pruef["grund"]) + elif float(pruef["guete"]) > Game.GUETE_NEU: + beschriftung += " — passt genau" + _knopf(beschriftung, func() -> void: _system_setzen(human_id, nr, id), + bool(pruef["geht"])) + _trenner() + _knopf("Zurück", func() -> void: systeme_panel(human_id)) + _zeigen("slot") + + +func _system_setzen(human_id: String, nr: int, tech_id: String) -> void: + var res := Game.system_setzen(human_id, nr, tech_id, false) + if not res["ok"]: + Game.notified.emit(str(res["grund"]), Color(1, 0.6, 0.4)) + return + if not res["erfolg"]: + Game.notified.emit("%s greift diesmal nicht. Später noch einmal." % + TechDB.by_id(tech_id)["name"], Color(1, 0.6, 0.5)) + systeme_panel(human_id) # -------------------------------------------------------- Lehrorte @@ -522,70 +564,66 @@ func brett_panel() -> void: _scroll_frames = 4 -# -------------------------------------------------------- Anwenden auf der Erde +# -------------------------------------------------------- Vor Ort +## Draußen am Menschen: hier greift man in den Slot ein, der gerade läuft. func anwenden_panel(mensch: Node3D) -> void: _apply_human = mensch var h: Dictionary = mensch.data - var aktiv: String = mensch.activity - _leeren("%s — %s" % [h["name"], Orte.text(aktiv)]) + var id: String = str(h["id"]) + var nr: int = int(mensch.slot) + _leeren("%s — %s" % [h["name"], Orte.text(mensch.activity)]) _text("[b]Traum:[/b] %s" % h["traum"], 16) - if Game.active_mission == h["id"]: - _text("Fortschritt: [b]%d %%[/b]" % int(Game.mission_progress(h["id"])), 16) - elif Game.completed.has(h["id"]): - _text("[color=#88ff99]★ Traum erfüllt.[/color]", 16) - else: - _text("[color=#ffcc66]Keine aktive Mission für diesen Menschen. " + - "Fortschritt zählt nur mit angenommener Mission.[/color]", 15) + if Game.active_mission != id: + _text("[color=#ffcc66]Keine aktive Mission für diesen Menschen.[/color]", 15) + _zeigen("anwenden") + return + var grund := Game.slot_grund(id, nr) + if not Game.erkannt(id, nr): + _text("[color=#ffcc66]Was hier läuft, hast du noch nicht gesehen. " + + "Bleib stehen und schau zu.[/color]", 16) + _zeigen("anwenden") + return + if Ursachen.ist_blocker(grund): + var b: Dictionary = Ursachen.BLOCKER[grund] + _text("Sie %s. [b]Auslöser:[/b] %s · [b]Belohnung:[/b] %s" % + [b["name"], b["ausloeser"], Ursachen.belohnung_text(str(b["belohnung"]))], 15) _trenner() - if Game.known.is_empty(): _text("[color=#ffcc66]Du kennst keine Technik.[/color]") _zeigen("anwenden") return _text("[color=#aaaaaa]Was eine Technik tut, steht im Journal.[/color]", 15) - - for id in TechDB.ids(): - if not Game.knows(id): + for tid in TechDB.ids(): + if not Game.knows(tid): continue - var t := TechDB.by_id(id) - var ev := Game.evaluate(id, h["id"], aktiv, mensch.resist_left > 0.0) - var gesperrt := not Game.bereit(id) - var beschriftung := "%s · Stufe %d" % [t["name"], Game.skill_of(id)] - if gesperrt: - beschriftung += " — noch %d s gesperrt" % int(ceil(Game.cooldown_rest(id))) - elif ev["blocked"]: - beschriftung += " — " + str(ev["grund"]) + var pruef := Game.system_pruefen(id, nr, tid) + var t := TechDB.by_id(tid) + var beschriftung := "%s · Stufe %d" % [t["name"], Game.skill_of(tid)] + if not pruef["geht"]: + beschriftung += " — " + str(pruef["grund"]) elif mensch.resist_left > 0.0: beschriftung += " — macht gerade dicht" - _knopf(beschriftung, func() -> void: _anwenden(id), not ev["blocked"] and not gesperrt) + _knopf(beschriftung, func() -> void: _vor_ort_setzen(tid), bool(pruef["geht"])) _zeigen("anwenden") -func _anwenden(id: String) -> void: +func _vor_ort_setzen(tech_id: String) -> void: if _apply_human == null or not is_instance_valid(_apply_human): schliessen() return var mensch := _apply_human - var h: Dictionary = mensch.data - var res := Game.apply_technique(id, h["id"], mensch.activity, mensch.resist_left > 0.0) + var id: String = str(mensch.data["id"]) + var res := Game.system_setzen(id, int(mensch.slot), tech_id, mensch.resist_left > 0.0) schliessen() if not res["ok"]: - Game.notified.emit(res["grund"], Color(1, 0.6, 0.4)) + Game.notified.emit(str(res["grund"]), Color(1, 0.6, 0.4)) return - var t := TechDB.by_id(id) - if res["success"]: - mensch.reagiere(true, "%s wirkt\n+%d %%" % [t["name"], int(round(float(res["gain"])))]) - Game.notified.emit("%s erreicht %s. (+%d %%)" % - [t["name"], h["name"], int(round(float(res["gain"])))], Color(0.6, 1, 0.7)) - if res["wiederholung"] >= 2: - Game.notified.emit("Immer dieselbe Technik wirkt schwächer. Probier eine andere.", - Color(0.85, 0.85, 0.6)) + var t := TechDB.by_id(tech_id) + if res["erfolg"]: + mensch.reagiere(true, "%s\nsteht" % t["name"]) else: - mensch.reagiere(false, "%s prallt ab\n−%d %%" % [t["name"], - int(round(float(res["verlust"])))]) - Game.notified.emit("%s dringt nicht durch. (−%d %%)" % - [t["name"], int(round(float(res["verlust"])))], Color(1, 0.6, 0.5)) + mensch.reagiere(false, "%s greift nicht" % t["name"]) func _unhandled_input(event: InputEvent) -> void: diff --git a/scripts/ursachen.gd b/scripts/ursachen.gd new file mode 100644 index 0000000..67287a1 --- /dev/null +++ b/scripts/ursachen.gd @@ -0,0 +1,136 @@ +class_name Ursachen +## Woran ein Traum scheitert, und was im Tag davon zu sehen ist. +## +## Ein Traum hat eine Grundrate. Jede ungelöste Ursache bremst sie. Zwei +## ungelöste Ursachen bremsen doppelt -- deshalb lohnt es, alle zu finden. +## Sichtbar wird eine Ursache nur an den Tätigkeiten, die sie hervorbringt: +## wer nie in Gang kommt, räumt auf, recherchiert und schreibt Listen. +## +## Grundlage: Habit Reversal Training beginnt mit awareness training -- erst +## bemerken, dann ersetzen. Und Duhiggs goldene Regel: Auslöser und Belohnung +## bleiben, nur die Handlung dazwischen wird getauscht. + +static var LISTE: Dictionary = { + "anfang": { + "name": "Kein Anfang", "kurz": "kommt nie in Gang", + "symptom": "Sie setzt sich hin und tut dann etwas anderes.", + "faktor": 0.30, + }, + "zeit": { + "name": "Keine Zeit", "kurz": "der Tag ist voll", + "symptom": "Der Tag ist voll mit dem, was andere wollen.", + "faktor": 0.30, + }, + "kraft": { + "name": "Keine Kraft", "kurz": "abends ist nichts mehr übrig", + "symptom": "Am Abend reicht es nur noch für Berieselung.", + "faktor": 0.40, + }, + "halt": { + "name": "Kein Halt", "kurz": "fängt an und hört wieder auf", + "symptom": "Angefangen wird oft, drangeblieben selten.", + "faktor": 0.50, + }, + "grund": { + "name": "Kein Grund", "kurz": "glaubt nicht mehr daran", + "symptom": "Sie redet sich das Vorhaben selbst klein.", + "faktor": 0.40, + }, +} + +## Tätigkeiten, die einen Traum aufhalten. Jede hat einen Auslöser und eine +## Belohnung -- genau die muss eine Ersatzhandlung wieder liefern. +## zeit: wann im Tag die Tätigkeit auftaucht. +static var BLOCKER: Dictionary = { + "putzflucht": { + "name": "räumt auf statt anzufangen", "ausloeser": "eine große Aufgabe", + "belohnung": "erfolg", "ursache": "anfang", "zeit": "tag", + }, + "recherche": { + "name": "recherchiert statt anzufangen", "ausloeser": "Unsicherheit", + "belohnung": "erfolg", "ursache": "anfang", "zeit": "tag", + }, + "liste": { + "name": "schreibt eine Später-Liste", "ausloeser": "Überforderung", + "belohnung": "ordnung", "ursache": "anfang", "zeit": "morgen", + }, + "jasagen": { + "name": "sagt zu allem ja", "ausloeser": "eine fremde Bitte", + "belohnung": "anerkennung", "ursache": "zeit", "zeit": "tag", + }, + "scrollen": { + "name": "scrollt endlos", "ausloeser": "Leerlauf", + "belohnung": "ablenkung", "ursache": "zeit", "zeit": "tag", + }, + "serie": { + "name": "schaut eine Serie", "ausloeser": "Erschöpfung", + "belohnung": "abschalten", "ursache": "kraft", "zeit": "abend", + }, + "wach": { + "name": "bleibt wach", "ausloeser": "ein fremdbestimmter Tag", + "belohnung": "zeit_fuer_sich", "ursache": "kraft", "zeit": "nacht", + }, + "kuehlschrank": { + "name": "steht am Kühlschrank", "ausloeser": "Langeweile", + "belohnung": "unterbrechung", "ursache": "kraft", "zeit": "abend", + }, + "spielen": { + "name": "spielt noch eine Runde", "ausloeser": "Anspannung", + "belohnung": "erfolg", "ursache": "halt", "zeit": "abend", + }, + "umschalten": { + "name": "schaltet auf etwas anderes um", "ausloeser": "Widerstand", + "belohnung": "neuheit", "ursache": "halt", "zeit": "tag", + }, + "klagen": { + "name": "klagt am Telefon", "ausloeser": "Frust", + "belohnung": "verbundenheit", "ursache": "grund", "zeit": "abend", + }, + "vergleichen": { + "name": "vergleicht sich mit anderen", "ausloeser": "ein fremder Erfolg", + "belohnung": "einordnung", "ursache": "grund", "zeit": "morgen", + }, +} + +static var BELOHNUNG_TEXT: Dictionary = { + "erfolg": "ein Erfolgserlebnis", + "ordnung": "Ordnung", + "anerkennung": "Anerkennung", + "anschluss": "Anschluss", + "abschalten": "Abschalten", + "zeit_fuer_sich": "Zeit für sich", + "unterbrechung": "eine Unterbrechung", + "neuheit": "etwas Neues", + "verbundenheit": "Verbundenheit", + "einordnung": "Einordnung", + "ablenkung": "Ablenkung", +} + + +static func ist_blocker(activity: String) -> bool: + return BLOCKER.has(activity) + + +static func ursache_von(activity: String) -> String: + return str(BLOCKER[activity]["ursache"]) if BLOCKER.has(activity) else "" + + +static func belohnung_text(schluessel: String) -> String: + return str(BELOHNUNG_TEXT.get(schluessel, schluessel)) + + +## Blocker einer Ursache, die in dieses Zeitfenster passen. +static func blocker_fuer(ursache: String, zeit: String) -> Array: + var out: Array = [] + for id in BLOCKER: + if BLOCKER[id]["ursache"] == ursache and BLOCKER[id]["zeit"] == zeit: + out.append(id) + return out + + +static func alle_blocker(ursache: String) -> Array: + var out: Array = [] + for id in BLOCKER: + if BLOCKER[id]["ursache"] == ursache: + out.append(id) + return out diff --git a/scripts/ursachen.gd.uid b/scripts/ursachen.gd.uid new file mode 100644 index 0000000..8a31763 --- /dev/null +++ b/scripts/ursachen.gd.uid @@ -0,0 +1 @@ +uid://ddavhllmw3nlk diff --git a/tests/shots.gd b/tests/shots.gd index bfeed22..c3ca83f 100644 --- a/tests/shots.gd +++ b/tests/shots.gd @@ -87,15 +87,24 @@ func _ready() -> void: await warte(6) await knipse("11_journal_missionen") - # Regelwerk: Wenn-Dann-Ketten, die den Fortschritt automatisieren - Game.regel_setzen("mensch_0", "zoegert", str(DreamDB.techniken_von("hoersaal")[0])) - main.ui.pipeline_panel("mensch_0") + # Diagnose: erst aufdecken, dann Systeme bauen + Game.erkannt_slots["mensch_0"] = {} + main.ui.systeme_panel("mensch_0") await warte(6) - await knipse("12_regelwerk") + await knipse("12_tagesablauf_verdeckt") - main.ui.regel_panel("mensch_0", "arbeitet") + var blocker := -1 + for nr in range(Game.slots("mensch_0").size()): + Game.beobachten("mensch_0", nr, Game.ERKENNEN_SEK * 2.0) + if Ursachen.ist_blocker(Game.slot_grund("mensch_0", nr)) and blocker < 0: + blocker = nr + main.ui.systeme_panel("mensch_0") await warte(6) - await knipse("13_regel_waehlen") + await knipse("12b_tagesablauf_erkannt") + + main.ui.slot_panel("mensch_0", blocker) + await warte(6) + await knipse("13_system_waehlen") main.ui.schliessen() # Lichtung, wenn sich die Wesen dort treffen @@ -145,26 +154,30 @@ func _ready() -> void: await warte(20) await knipse("17_lena_zoegert") - # Das Regelwerk greift ohne Zutun, sobald Lena die Tätigkeit wechselt - var vor := Game.mission_progress("mensch_0") - for i in range(20000): + # Der Traum läuft von allein weiter, solange Systeme stehen + for i in range(4000): Game.time_h += 0.004 main._process(0.02) for k in main.welt.get_children(): if k is Bewohner: k._process(0.05) - if Game.mission_progress("mensch_0") > vor: - break main.player.global_position = lena.global_position + Vector3(0, 1.6, -5.5) main.player.yaw = PI main.player.pitch = -0.10 await warte(4) - await knipse("18_regel_greift") + await knipse("18_traum_laeuft") main.ui.anwenden_panel(lena) await warte(6) - await knipse("19_anwenden") - main.ui._anwenden(str(DreamDB.techniken_von("hoersaal")[0])) + await knipse("19_vor_ort") + main.ui.schliessen() + Game.cooldown.clear() + var wege: Array = [] + for tid in Game.known: + wege.append(str(tid)) + if not wege.is_empty(): + main.ui._apply_human = lena + main.ui._vor_ort_setzen(str(wege[0])) await warte(25) await knipse("20_reaktion") diff --git a/tests/verify.gd b/tests/verify.gd index efc28f3..7bf2879 100644 --- a/tests/verify.gd +++ b/tests/verify.gd @@ -34,10 +34,11 @@ func _ready() -> void: await test_tagesablauf() await test_wege_machbar() await test_geist() - await test_technik_anwenden() + await test_diagnose() + await test_systeme() + await test_tempo_des_traums() await test_cooldown() await test_chance_verdeckt() - await test_pipeline() await test_mission_abschliessen() await test_welt_waechst() await test_techniken_verbessern_sich() @@ -170,6 +171,7 @@ func laufe(von: Vector3, yaw: float, frames: int) -> Vector3: # -------------------------------------------------------------------- + func test_start() -> void: abschnitt("Start") p(not ui().offen, "Kein Startbildschirm — das Spiel läuft sofort") @@ -210,36 +212,33 @@ func test_journal() -> void: 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 jeden Moment der Technik wirklich benennen — - # sonst ist die Technik im Spiel nicht lesbar. + # 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 := { - "schlaeft": "schläf", "morgenroutine": "morgenroutine", "arbeitet": "arbeit", - "pause": "pause", "essen": "essen", "gruebelt": "grübel", "ruht": "ausruh", - "angespannt": "angespannt", "abendroutine": "abendroutine", - "aufraeumen": "aufräum", "wartet": "wartet", "muede": "müde", - "unterwegs": "unterwegs", "zoegert": "zöger", + "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", } var luecken: Array = [] - for t in TechDB.LIST: - var text: String = str(t["woran"]).to_lower() - for m in t["momente"]: - if not stamm.has(m): - luecken.append("%s: %s unbekannt" % [t["id"], m]) - elif not text.contains(str(stamm[m])): - luecken.append("%s nennt »%s« nicht" % [t["id"], m]) - p(luecken.is_empty(), "Jeder Woran-Hinweis benennt alle Momente seiner Technik%s" % - ("" if luecken.is_empty() else " — " + ", ".join(luecken))) - - # Aber er verrät nicht mehr als die Momente var zuviel: Array = [] for t in TechDB.LIST: var text: String = str(t["woran"]).to_lower() - for m in stamm: - if (t["momente"] as Array).has(m): + 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[m])): - zuviel.append("%s nennt fremdes »%s«" % [t["id"], m]) - p(zuviel.is_empty(), "Und keinen Moment, der nicht dazugehört%s" % + 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") @@ -321,7 +320,7 @@ func test_traumdorf() -> void: for w in wesen: for h in range(24): var r: Array = w._routine_at(float(h) + 0.5) - if not DreamDB.PLACES.has(r[0]) or not Orte.ACTIVITY_TEXT.has(r[1]): + 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") @@ -598,7 +597,7 @@ func test_tagesablauf() -> void: for m in welt().humans: for h in range(24): var r: Array = m._routine_at(float(h) + 0.5) - if not HumanDB.PLACES.has(r[0]) or not Orte.ACTIVITY_TEXT.has(r[1]): + 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()) @@ -692,208 +691,238 @@ func test_geist() -> void: p(lena.global_position.distance_to(davor) < 2.0, "Menschen nehmen den Geist nicht wahr") -func test_technik_anwenden() -> void: - abschnitt("Techniken anwenden — Erfolgschance") - var lena: Bewohner = null - for m in welt().humans: - if m.data["id"] == "mensch_0": - lena = m +## 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.beobachtung.clear() - # Die Stunde suchen, in der dieser Mensch zögert — sie steht in seiner Routine - var zoegert_um := -1.0 - for e in lena.data["routine"]: - if e[2] == "zoegert": - zoegert_um = float(e[0]) + 0.5 - for i in range(1200): - schritte(1, 0.05, zoegert_um) - if lena.activity == "zoegert" and lena._path.is_empty(): + 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 + Game.beobachten(id, blocker_nr, Game.ERKENNEN_SEK * 0.4) + p(not Game.erkannt(id, blocker_nr), "Kurz hinschauen reicht nicht") + Game.beobachten(id, blocker_nr, Game.ERKENNEN_SEK) + p(Game.erkannt(id, blocker_nr), "Nach %.0f Sekunden dabeistehen zeigt es sich" % + Game.ERKENNEN_SEK) + 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. +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 - p(lena.activity == "zoegert", "%s zögert um %.0f Uhr (%s)" % - [lena.data["name"], zoegert_um, lena.activity]) + var b: Dictionary = Ursachen.BLOCKER[Game.slot_grund(id, blocker_nr)] + p(blocker_nr >= 0, "Ein Blocker im Tag: %s" % b["name"]) - main.player.set_mode(Player.Mode.GEIST) - main.player.global_position = lena.global_position + Vector3(0, 1.0, 1.5) - main._process(0.05) - p(main._fokus_mensch == lena, "Der Geist erfasst Lena in Reichweite") - main._interagieren() - p(ui().offen and ui()._kind == "anwenden", "Anwenden-Panel öffnet sich") - ui().schliessen() - - Game.known["pomodoro"] = 0 - var s1: float = Game.evaluate("pomodoro", "mensch_0", "zoegert", false)["chance"] - Game.known["pomodoro"] = 28 - var s5: float = Game.evaluate("pomodoro", "mensch_0", "zoegert", false)["chance"] - p(s5 > s1, "Höherer Skill = höhere Erfolgschance (%.0f%% -> %.0f%%)" % [s1 * 100, s5 * 100]) - Game.known["pomodoro"] = 0 - - var passend: Dictionary = Game.evaluate("pomodoro", "mensch_0", "zoegert", false) - var unpassend: Dictionary = Game.evaluate("pomodoro", "mensch_0", "essen", false) - p(passend["moment"] and not unpassend["moment"], "Moment-Erkennung: zögert ja, isst nein") - p(passend["chance"] > unpassend["chance"], "Passender Moment erhöht die Chance") - p(passend["tag"], "Pomodoro trifft Lenas Thema Aufschieben") - var falsches_thema: Dictionary = Game.evaluate("pomodoro", "mensch_2", "zoegert", false) - p(not falsches_thema["tag"], "Bei Mira trifft Pomodoro das Thema nicht") - p(passend["chance"] > falsches_thema["chance"], "Passendes Thema erhöht die Chance") - p(Game.evaluate("pomodoro", "mensch_0", "schlaeft", false)["blocked"], - "Schlafende Menschen sind nicht erreichbar") - p(Game.evaluate("pomodoro", "mensch_0", "zoegert", true)["chance"] < passend["chance"], - "Wer abblockt, ist schwerer zu erreichen") - p(Game.evaluate("nein_sagen", "mensch_0", "zoegert", false)["blocked"], - "Ungelernte Techniken lassen sich nicht anwenden") - - _frei() - Game.progress["mensch_0"] = 50.0 - var vorher_p: float = Game.mission_progress("mensch_0") - var res := Game.apply_technique("pomodoro", "mensch_0", "zoegert", false) - p(res["ok"], "Anwendung wird ausgeführt") - if res["success"]: - p(Game.mission_progress("mensch_0") > vorher_p, "Erfolg bringt Missionsfortschritt") - else: - p(Game.mission_progress("mensch_0") < vorher_p, - "Fehlversuch kostet Fortschritt (%.0f -> %.0f)" % - [vorher_p, Game.mission_progress("mensch_0")]) - p(Game.known["pomodoro"] > 0, "Jede Anwendung bringt Erfahrung") - - _frei() - Game.progress["mensch_0"] = 0.0 - Game.used.clear() - Game.known["pomodoro"] = 28 - var ertraege: Array = [] - for i in range(6): - _frei() - var r := Game.apply_technique("pomodoro", "mensch_0", "zoegert", false) - if r["success"]: - ertraege.append(r["gain"]) - p(ertraege.size() >= 2 and ertraege[ertraege.size() - 1] < ertraege[0], - "Dieselbe Technik immer wieder wirkt schwächer") - - -func test_pipeline() -> void: - abschnitt("Regelwerk — Missionsfortschritt automatisieren") - Game.active_mission = "mensch_0" - Game.progress["mensch_0"] = 0.0 - Game.completed.clear() - Game.used.clear() - Game.pipeline.clear() - Game.known = {"pomodoro": 28, "zerlegen": 28, "umgebung": 28} - - var taten := HumanDB.aktivitaeten("mensch_0") - p(taten.size() >= 5, "Lenas Tag hat %d verschiedene Tätigkeiten" % taten.size()) - p(taten.has("zoegert") and taten.has("schlaeft"), - "Darunter zögert und schläft: %s" % ", ".join(taten)) - - # Regelwerk über das Journal aufbauen - ui().journal_panel("missionen") - p(_knopf_da("Regelwerk"), "Die angenommene Mission trägt ein Regelwerk") - p(_druecke("Regelwerk"), "Das Regelwerk öffnet sich") - p(ui()._kind == "pipeline", "Panel »pipeline« ist offen") - var zeilen := _knopf_texte() - var fehlt: Array = [] - for a in taten: - if not zeilen.contains(Orte.text(a)): - fehlt.append(a) - p(fehlt.is_empty(), "Jede der %d Tätigkeiten hat eine Zeile (fehlt: %s)" % - [taten.size(), ", ".join(fehlt)]) - p(zeilen.contains("schläft → unerreichbar"), - "Schlaf ist als unerreichbar ausgewiesen, statt eine Regel anzubieten") - var schlaf_aktiv := false - for c in ui()._content.get_children(): - if c is Button and (c as Button).text.begins_with("schläft") and not (c as Button).disabled: - schlaf_aktiv = true - p(not schlaf_aktiv, "Und lässt sich gar nicht erst anklicken") - ui().regel_panel("mensch_0", "zoegert") - p(ui()._kind == "regel", "Die Technikwahl für »zögert« öffnet sich") - p(_knopf_texte().contains("Pomodoro-Technik"), "Gelernte Techniken stehen zur Wahl") - p(_druecke("Pomodoro-Technik"), "Eine Technik lässt sich zuordnen") - p(Game.regel_fuer("mensch_0", "zoegert") != "", - "Regel steht: zögert -> %s" % Game.regel_fuer("mensch_0", "zoegert")) - p(ui()._kind == "pipeline", "Danach ist wieder das Regelwerk offen") - ui().schliessen() - - Game.regel_setzen("mensch_0", "gruebelt", "zerlegen") - Game.regel_setzen("mensch_0", "aufraeumen", "umgebung") - p(Game.regel_anzahl("mensch_0") == 3, "Drei Regeln im Werk (%d)" % Game.regel_anzahl("mensch_0")) - - # Eine Technik darf an mehreren Tätigkeiten hängen - Game.regel_setzen("mensch_0", "arbeitet", "zerlegen") - p(Game.regel_anzahl("mensch_0") == 4, - "Dieselbe Technik anderswo einzusetzen kommt hinzu (%d)" % - Game.regel_anzahl("mensch_0")) - p(Game.regel_fuer("mensch_0", "gruebelt") == "zerlegen" - and Game.regel_fuer("mensch_0", "arbeitet") == "zerlegen", - "Sie hängt jetzt an beiden Tätigkeiten") - p(Game.regel_momente("mensch_0", "zerlegen").size() == 2, - "Und das Spiel weiß von beiden Posten (%d)" % - Game.regel_momente("mensch_0", "zerlegen").size()) - Game.regel_setzen("mensch_0", "arbeitet", "") - p(Game.regel_momente("mensch_0", "zerlegen").size() == 1, - "Einzeln löschen lässt den anderen Posten stehen") - - # Ohne Zutun einen Tag laufen lassen - var lena: Bewohner = null - for m in welt().humans: - if m.data["id"] == "mensch_0": - lena = m - p(lena != null, "Lena lebt in der Stadt") - var vorher: float = Game.mission_progress("mensch_0") - var vor_xp: int = int(Game.known["pomodoro"]) + int(Game.known["zerlegen"]) - Game.time_h = 0.0 - main.player.global_position = Vector3(0, 30.0, 60.0) # weit weg, kein Zutun - for i in range(9000): - schritte(1) - if Game.time_h >= 23.5: + # 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 nachher: float = Game.mission_progress("mensch_0") - p(nachher > vorher, "Der Traum wächst von allein: %d %% -> %d %%" % [int(vorher), int(nachher)]) - p(int(Game.known["pomodoro"]) + int(Game.known["zerlegen"]) > vor_xp, - "Die Regeln sammeln nebenbei Erfahrung") + var pr_falsch := Game.system_pruefen(id, blocker_nr, falsch) + p(not pr_falsch["geht"], "Eine Technik gegen eine andere Ursache passt nicht hier") - # Ohne Regeln passiert nichts - Game.pipeline.clear() - Game.progress["mensch_0"] = 0.0 - var still: float = Game.mission_progress("mensch_0") - Game.time_h = 0.0 - for i in range(9000): - schritte(1) - if Game.time_h >= 23.5: + # 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(is_equal_approx(Game.mission_progress("mensch_0"), still), - "Ohne Regeln bewegt sich nichts (%d %%)" % int(Game.mission_progress("mensch_0"))) + 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") - # Regeln greifen nur für die aktive Mission - Game.regel_setzen("mensch_1", "zoegert", "zerlegen") - Game.progress["mensch_1"] = 0.0 - Game.time_h = 0.0 - for i in range(9000): - schritte(1) - if Game.time_h >= 23.5: + # 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 - p(is_equal_approx(Game.mission_progress("mensch_1"), 0.0), - "Regeln fremder Missionen bleiben still (%d %%)" % int(Game.mission_progress("mensch_1"))) + 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"]) - # Ausgangslage für die folgenden Prüfungen - Game.pipeline.clear() - Game.progress["mensch_0"] = 0.0 - Game.used.clear() - Game.known = {"pomodoro": 0, "zerlegen": 0, "umgebung": 0} + +## 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") - Game.progress["mensch_0"] = 0.0 - Game.used.clear() - var runden := 0 - while Game.mission_progress("mensch_0") < Game.MISSION_ZIEL and runden < 400: - _frei() - Game.apply_technique("pomodoro", "mensch_0", "zoegert", false) - _frei() - Game.apply_technique("zerlegen", "mensch_0", "gruebelt", false) - _frei() - Game.apply_technique("umgebung", "mensch_0", "aufraeumen", false) - runden += 1 - p(Game.mission_progress("mensch_0") >= Game.MISSION_ZIEL, - "Lenas Traum erreicht 100 %% (nach %d Runden)" % runden) - p(Game.completed.has("mensch_0"), "Mission gilt als abgeschlossen") + 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()) @@ -933,6 +962,7 @@ func test_welt_waechst() -> void: "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"])), @@ -1014,40 +1044,46 @@ func _dorf_platz_pruefen() -> void: 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 anwendungen := 0 - while Game.skill_of("zwei_minuten") < 3 and anwendungen < 60: - _frei() - Game.apply_technique("zwei_minuten", "mensch_2", "gruebelt", false) - anwendungen += 1 - p(Game.skill_of("zwei_minuten") >= 3, - "Nach %d Anwendungen von allein auf Stufe %d" % [anwendungen, Game.skill_of("zwei_minuten")]) + 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 vor_xp: int = Game.known["zwei_minuten"] - _frei() - var res := Game.apply_technique("zwei_minuten", "mensch_2", "essen", false) - p(Game.known["zwei_minuten"] > vor_xp, - "Auch ein %s bringt Erfahrung" % ("Erfolg" if res["success"] else "Fehlversuch")) + var 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.learn("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) - # Zeitablauf allein bringt auch nichts - var vorher: int = Game.known["zwei_minuten"] - Game.advance_time(Game.DAY_SECONDS) - p(Game.known["zwei_minuten"] == vorher, "Und auch ein ganzer Tag bringt keine Erfahrung") - func test_journal_ueberall() -> void: abschnitt("Journal ist immer dabei") @@ -1133,193 +1169,91 @@ func test_steuerung() -> void: "Umschalt lässt den Geist sinken (y = %.1f)" % main.player.global_position.y) -## Die Probe auf Loop 2: ohne einen erfüllten Traum gibt es keine neuen Wege. -## Also muss der erste Traum mit dem Startkapital allein zu schaffen sein — -## und zwar zuverlässig, nicht nur bei gutem Würfelglück. -func _erster_traum_tage(saat: int) -> int: - seed(saat) - Game.cooldown.clear() - Game.known.clear() - Game.used.clear() - Game.pipeline.clear() - Game.progress.clear() - Game.completed.clear() - Game.active_mission = "" - Game.welt_erzeugen() - for id in _basiswege(): - Game.learn(id) - Game.accept_mission("mensch_0") - # So ordnet ein Spieler zu: jede Technik an den Moment, wo sie am besten wirkt. - for id in Game.known: - var bester := "" - var beste_chance := 0.0 - for activity in HumanDB.aktivitaeten("mensch_0"): - if not Game.erreichbar(activity) or Game.regel_fuer("mensch_0", activity) != "": - continue - var ev := Game.evaluate(id, "mensch_0", activity, false) - if float(ev["chance"]) > beste_chance: - beste_chance = float(ev["chance"]) - bester = activity - if bester != "": - Game.regel_setzen("mensch_0", bester, id) - - main._erde() - await get_tree().process_frame - main.player.global_position = Vector3(0, 30.0, 60.0) # der Spieler tut nichts - Game.time_h = 7.0 - Game.day = 1 - for i in range(140000): - schritte(1) - if Game.completed.has("mensch_0") or Game.day > 14: - break - return Game.day - - -func test_erster_traum_schaffbar() -> void: - abschnitt("Der erste Traum ist mit dem Startkapital zu schaffen") - Game.completed.clear() - Game.welt_erzeugen() - p(Game.offene_techniken() == 1, - "Am Anfang ist genau ein Weg entdeckt (%d)" % Game.offene_techniken()) - - var tage: Array = [] - for saat in [1, 7, 555, 20240, 90210]: - tage.append(await _erster_traum_tage(saat)) - p(Game.known.size() == 1, "Man startet mit einem Weg (%d)" % Game.known.size()) - p(Game.regel_anzahl("mensch_0") == 1, - "Der trägt genau eine Regel (%d)" % Game.regel_anzahl("mensch_0")) - - var schlimmster: int = tage.max() - var bester: int = tage.min() - p(schlimmster <= 14, "Der Traum erfüllt sich in allen %d Läufen ohne Zutun (%s)" % - [tage.size(), str(tage)]) - p(bester >= 2, "Nie geschenkt — mindestens %d Tage" % bester) - p(schlimmster <= 8, "Nie zäh — höchstens %d Tage (von Hand geht es schneller)" % - schlimmster) - p(Game.offene_techniken() > 1, - "Und öffnet neue Wege: %d entdeckt" % Game.offene_techniken()) - - ## 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") - _frei() - Game.known = {"pomodoro": 28, "zerlegen": 28} - Game.active_mission = "mensch_0" - Game.progress["mensch_0"] = 50.0 + 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.apply_technique("pomodoro", "mensch_0", "zoegert", false) + 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.apply_technique("pomodoro", "mensch_0", "zoegert", false) + 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") - var andere := Game.apply_technique("zerlegen", "mensch_0", "gruebelt", false) - p(andere["ok"], "Eine andere Technik greift weiter") - - # Eine Regel bleibt still, solange die Technik gesperrt ist - Game.pipeline.clear() - Game.regel_setzen("mensch_0", "zoegert", "pomodoro") - var still := Game.regel_ausloesen("mensch_0", "zoegert", false) - p(still.is_empty(), "Auch eine Regel läuft nicht gegen die Sperre an") + 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") - var wieder := Game.regel_ausloesen("mensch_0", "zoegert", false) - p(not wieder.is_empty(), "Jetzt greift die Regel wieder") - - # Im Anwenden-Panel ist die Sperre sichtbar und der Knopf tot - var lena: Bewohner = null - for m in welt().humans: - if m.data["id"] == "mensch_0": - lena = m - ui().anwenden_panel(lena) - var gesperrt_sichtbar := _knopf_texte().contains("gesperrt") - var knopf_tot := true - for c in ui()._content.get_children(): - if c is Button and (c as Button).text.contains("gesperrt") \ - and not (c as Button).disabled: - knopf_tot = false - p(gesperrt_sichtbar, "Der Anwenden-Knopf weist die Sperre aus") - p(knopf_tot, "Und lässt sich nicht drücken") - ui().schliessen() - Game.pipeline.clear() - _frei() -## Wie gut eine Technik im Moment steht, sagt das Spiel nicht. Es nennt nur, -## was zum Thema und was zum Moment passt — entscheiden muss der Spieler. +## 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") - _frei() - Game.known = {"pomodoro": 28, "zerlegen": 28} - Game.active_mission = "mensch_0" - Game.pipeline.clear() - Game.regel_setzen("mensch_0", "zoegert", "pomodoro") + 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().regel_panel("mensch_0", "zoegert") - var knoepfe := _knopf_texte() - p(not knoepfe.contains("%"), "Kein Knopf der Technikwahl nennt eine Prozentzahl") - p(not _panel_text().contains("Erfolgschance"), - "Und das Wort Erfolgschance kommt nicht vor") + ui().slot_panel(id, blocker_nr) var wahl := _panel_text() - p(not wahl.contains("passt zum Thema") and not wahl.contains("passender Moment") - and not wahl.contains("weder Thema"), - "Auch kein Hinweis, ob Thema oder Moment passen") + 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 die Namen, keine Beschreibung") - p(_knopf_texte().contains("Pomodoro-Technik · Stufe"), - "Jede Wahl steht als Name mit Stufe da") + "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().pipeline_panel("mensch_0") - var zeilen := _knopf_texte() - p(not zeilen.contains("%"), "Auch die Regelzeilen nennen keine Chance") - p(zeilen.contains("Pomodoro-Technik"), "Sie nennen nur die eingehängte Technik") - ui().schliessen() - - var lena: Bewohner = null - for m in welt().humans: - if m.data["id"] == "mensch_0": - lena = m - ui().anwenden_panel(lena) - var anwenden := _panel_text() - p(not _knopf_texte().contains("%"), "Und der Anwenden-Knopf nennt keine Chance") - p(_knopf_texte().contains("Pomodoro-Technik · Stufe"), - "Er trägt nur Name und Stufe") - p(not anwenden.contains("passt zum Thema") and not anwenden.contains("passender Moment"), - "Auch beim Anwenden steht nicht da, ob es passt") - p(not anwenden.contains("Hilft bei") and not anwenden.contains("Momente:"), - "Und keine Liste der passenden Themen oder Momente") - p(not anwenden.contains("Wann:") and not anwenden.contains("Zu sehen an:"), - "Auch dort nur Namen — die Beschreibung steht im Journal") - p(anwenden.contains("Journal"), "Und das Panel sagt, wo sie steht") + ui().systeme_panel(id) + p(not _knopf_texte().contains("%"), "Auch die Tageszeilen nennen keine Chance") + p(_panel_text().contains("Tempo"), "Das Panel nennt das Tempo des Traums") ui().schliessen() ui().journal_panel("techniken") var buch := _panel_text() - p(not buch.contains("Hilft bei") and not buch.contains("Momente:"), - "Auch das Journal verrät die Passung nicht") p(buch.contains("Wann:") and buch.contains("Zu sehen an:"), - "Das Journal dagegen führt Was, Wann, Warum und Zu sehen an") + "Das Journal führt Was, Wann, Warum und Zu sehen an") ui().schliessen() - ui().brett_panel() - p(not _panel_text().contains("Themen:"), - "Und das Brett listet die Themen des Menschen nicht auf") - ui().schliessen() - Game.pipeline.clear() - ## 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. @@ -1368,79 +1302,6 @@ func test_brett_nie_leer() -> void: Game.welt_erzeugen() -## Die Probe auf den Kern: wer die Technik liest und sie nur dort einhängt, wo -## ihr "Wann" trifft, kommt schneller ans Ziel als wer sie überall hinhängt. -## Ohne diesen Abstand wäre das Lesen umsonst. -func _traum_tage(saat: int, alles: bool) -> int: - seed(saat) - Game.cooldown.clear() - Game.known.clear() - Game.used.clear() - Game.pipeline.clear() - Game.progress.clear() - Game.completed.clear() - Game.active_mission = "" - Game.welt_erzeugen() - var weg := str(_basiswege()[0]) - Game.learn(weg) - Game.accept_mission("mensch_0") - var t := TechDB.by_id(weg) - for a in HumanDB.aktivitaeten("mensch_0"): - if not Game.erreichbar(a): - continue - if alles or (t["momente"] as Array).has(a): - Game.regel_setzen("mensch_0", a, weg) - main._erde() - await get_tree().process_frame - main.player.global_position = Vector3(0, 30.0, 60.0) - Game.time_h = 7.0 - Game.day = 1 - for i in range(200000): - schritte(1) - if Game.completed.has("mensch_0") or Game.day > 14: - break - return Game.day if Game.completed.has("mensch_0") else 99 - - -func test_lesen_lohnt_sich() -> void: - abschnitt("Die Technik zu lesen zahlt sich aus") - var gezielt: Array = [] - var blind: Array = [] - for saat in [7, 555, 90210]: - gezielt.append(await _traum_tage(saat, false)) - blind.append(await _traum_tage(saat, true)) - var g := 0.0 - var b := 0.0 - for i in range(gezielt.size()): - g += float(gezielt[i]) - b += float(blind[i]) - g /= float(gezielt.size()) - b /= float(blind.size()) - p(b >= g * 1.25, - "Gezielt gesetzt geht es deutlich schneller: %.1f statt %.1f Tage (gezielt %s, blind %s)" % - [g, b, str(gezielt), str(blind)]) - var schlechter := 0 - for i in range(gezielt.size()): - if int(blind[i]) < int(gezielt[i]): - schlechter += 1 - p(schlechter == 0, - "Und blind gesetzt ist in keinem der %d Läufe besser (%d)" % [gezielt.size(), schlechter]) - - # Der Grund: ein falscher Moment ist unterm Strich ein Verlustgeschäft - Game.known = {"pomodoro": 0} - var gut := Game.evaluate("pomodoro", "mensch_0", "zoegert", false) - var falsch := Game.evaluate("pomodoro", "mensch_0", "gruebelt", false) - p(gut["moment"] and not falsch["moment"], "»zögert« ist Pomodoros Moment, »grübelt« nicht") - var ertrag_gut := (8.0 + 2.0) + 5.0 + 5.0 - var ertrag_falsch := (8.0 + 2.0) + 5.0 - var ev_gut: float = float(gut["chance"]) * ertrag_gut \ - - (1.0 - float(gut["chance"])) * Game.FEHLVERSUCH_VERLUST - var ev_falsch: float = float(falsch["chance"]) * ertrag_falsch \ - - (1.0 - float(falsch["chance"])) * Game.FEHLVERSUCH_VERLUST - p(ev_gut > 0.0, "Im richtigen Moment trägt sie (%.1f je Einsatz)" % ev_gut) - p(ev_falsch < 0.0, "Im falschen kostet sie (%.1f je Einsatz)" % ev_falsch) - - ## 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: @@ -1741,3 +1602,146 @@ func test_bezirk() -> void: 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.beobachtung.clear() + 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")