Files
dreamer-game/tests/verify.gd
2026-09-16 20:06:38 +02:00

3354 lines
128 KiB
GDScript
Raw Blame History

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