first commit

This commit is contained in:
team3
2026-08-29 11:44:31 +02:00
commit c9416ed3c6
66 changed files with 4821 additions and 0 deletions

245
tests/audio_test.gd Normal file
View File

@@ -0,0 +1,245 @@
extends Node
## Prüft Klangerzeugung und Wiedergabe.
var main
var step := 0
var wait := 0.0
var ok := 0
var fail := 0
var worst := 0.0
var elapsed := 0.0
func chk(n: String, c: bool, i := "") -> void:
if c: ok += 1; print(" OK %s %s" % [n, i])
else: fail += 1; print(" FAIL %s %s" % [n, i])
func _key(c: int, d: bool) -> void:
var e := InputEventKey.new(); e.physical_keycode = c; e.keycode = c; e.pressed = d
Input.parse_input_event(e)
func _process(d):
if main == null:
main = get_tree().current_scene
return
if not main.started:
main._start_solo()
return
if not main.audio.loaded:
elapsed += d
worst = maxf(worst, d)
return
if step == 0:
chk("Kein Ruckler beim Erzeugen", worst < 0.2,
"längster Frame %.0f ms in %.1f s Ladezeit" % [worst * 1000.0, elapsed])
if wait > 0.0:
wait -= d
return
var a = main.audio
match step:
0:
_check_harmony()
_check_shapes()
_check_loops()
var names := ["clack", "clack2", "rumble", "squeal", "whistle", "hiss",
"bell", "coin", "pour", "clink", "thud", "step", "click", "deny"]
var missing: Array[String] = []
for n in names:
if not a.sounds.has(n):
missing.append(n)
chk("Alle Klänge da", missing.is_empty(), "fehlt: %s" % [missing])
var silent: Array[String] = []
for n in a.sounds.keys():
var w: AudioStreamWAV = a.sounds[n]
if w.data.size() < 200 or _peak(w) < 1000:
silent.append(String(n))
chk("Kein Klang ist stumm", silent.is_empty(), "still: %s" % [silent])
chk("Musik erzeugt", a._music.stream != null and a._music.playing)
chk("Thread beendet", a._thread == null)
var m: AudioStreamWAV = a._music.stream
chk("Musik ist eine Schleife", m.loop_mode == AudioStreamWAV.LOOP_FORWARD)
chk("Musik lang genug", m.data.size() / 2 > 20 * 22050,
"%.1f s" % (m.data.size() / 2.0 / 22050.0))
chk("Musik nicht stumm", _peak(m) > 3000, "peak=%d" % _peak(m))
chk("Rumble läuft leise", a._rumble.playing and a._rumble.volume_db < -40.0,
"db=%.0f" % a._rumble.volume_db)
wait = 0.3
1:
# Losfahren: Rumble muss lauter werden, Radschläge kommen
main._srv_toggle_drive(1)
a.clacks = 0
_key(KEY_W, true)
wait = 9.0
2:
chk("Rumble folgt dem Tempo", a._rumble.volume_db > -30.0,
"v=%.1f db=%.0f" % [main.train.speed, a._rumble.volume_db])
chk("Radschläge kommen", a.clacks >= 3, "clacks=%d" % a.clacks)
_key(KEY_W, false)
_key(KEY_S, true)
wait = 0.5
3:
chk("Bremsen quietscht", a._squeal.volume_db > -60.0,
"db=%.0f" % a._squeal.volume_db)
# Pfeife
_key(KEY_H, true); _key(KEY_H, false)
wait = 0.2
4:
var whistling := false
for p in a._pool3d:
if p.playing and p.stream == a.sounds["whistle"]:
whistling = true
chk("H pfeift", whistling)
wait = 3.0
5:
_key(KEY_S, false)
chk("Zug steht", main.train.speed < 0.2, "v=%.2f" % main.train.speed)
chk("Rumble wieder still", a._rumble.volume_db < -40.0,
"db=%.0f" % a._rumble.volume_db)
_key(KEY_M, true); _key(KEY_M, false)
wait = 0.2
6:
chk("M schaltet Musik aus", not a.music_on and a._music.volume_db < -60.0)
_key(KEY_M, true); _key(KEY_M, false)
wait = 0.2
7:
chk("M schaltet Musik wieder an", a.music_on and a._music.volume_db > -30.0)
# Ankunftsglocke und Auszischen gezielt auslösen
main._last_dock = null
main._park_at(main.stations[1])
a.last_played = ""
main._update_audio(0.016)
chk("Glocke bei Ankunft", a.last_played == "bell", "zuletzt=%s" % a.last_played)
main._last_speed = 6.0
main.train.speed = 0.0
a.last_played = ""
main._update_audio(0.016)
chk("Zischen beim Halten", a.last_played == "hiss", "zuletzt=%s" % a.last_played)
wait = 0.2
8:
print("== %d ok, %d fail ==" % [ok, fail])
get_tree().quit(1 if fail > 0 else 0)
step += 1
func _peak(w: AudioStreamWAV) -> int:
var d := w.data
var n: int = mini(d.size() / 2, 300000)
var peak := 0
for i in range(0, n, 7):
var v: int = absi(d.decode_s16(i * 2))
if v > peak:
peak = v
return peak
## Kein Ton darf eine kleine Sekunde oder kleine None gegen eine andere
## Stimme bilden. Große Septimen sind erlaubt - die gehören zum Klang.
## An genau dieser Regel ist das C über G6 aufgefallen.
func _check_harmony() -> void:
var Sfx = load("res://scripts/sfx.gd")
var bad: Array[String] = []
for stage in Sfx.PROGRESSION:
var voices: Array[float] = []
for f in stage["pad"]:
voices.append(float(f))
voices.append(float(stage["bass"]))
# Flächen und Bass untereinander
for i in voices.size():
for j in range(i + 1, voices.size()):
_flag(bad, String(stage["name"]), voices[i], voices[j])
# Melodie gegen jede Stimme
for f in stage["melody"]:
for v in voices:
_flag(bad, String(stage["name"]), float(f), v)
chk("Keine Halbton- oder None-Reibung", bad.is_empty(), "%s" % [bad])
func _flag(bad: Array[String], chord: String, a: float, b: float) -> void:
var d: int = absi(int(round(12.0 * (log(a / b) / log(2.0)))))
if d == 1 or d == 13:
bad.append("%s: %s gegen %s (%d HT)" % [chord, _name(_pc(a)), _name(_pc(b)), d])
func _pc(f: float) -> int:
return int(round(69.0 + 12.0 * (log(f / 440.0) / log(2.0)))) % 12
func _name(pc: int) -> String:
return ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"][pc]
## Form der Schlaggeräusche: lang genug und mit Tiefe, nicht nur ein Tick.
func _check_shapes() -> void:
for n in ["clack", "clack2", "step", "thud"]:
var x := _samples(n)
chk("%s klingt aus" % n, _tail(x) > 0.18, "%.2f s hörbar" % _tail(x))
chk("%s hat Tiefe" % n, _zcr(x, 0, x.size()) < 0.22,
"Nulldurchgänge %.3f je Sample" % _zcr(x, 0, x.size()))
for n in ["clack", "clack2"]:
var r := _double_hit(_samples(n))
chk("%s ist ein Doppelschlag" % n, bool(r["ok"]),
"zweiter Schlag bei %.3f s, Einbruch davor auf %.0f %%"
% [r["at"], float(r["dip"]) * 100.0])
## Schleifen dürfen an der Naht nicht knicken, und das Quietschen darf
## seine Tonhöhe nicht weglaufen lassen.
func _check_loops() -> void:
for n in ["squeal", "rumble"]:
var x := _samples(n)
var kink: float = absf(x[0] - (2.0 * x[x.size() - 1] - x[x.size() - 2]))
var worst := 0.0
for i in range(1, x.size()):
worst = maxf(worst, absf(x[i] - x[i - 1]))
chk("%s läuft nahtlos" % n, kink < worst * 1.5,
"Knick %.4f, größter Schritt %.4f" % [kink, worst])
var sq := _samples("squeal")
var h: int = sq.size() / 2
var a := _zcr(sq, 0, h)
var b := _zcr(sq, h, sq.size())
chk("Quietschen bleibt auf Tonhöhe", absf(a - b) / maxf(a, 0.0001) < 0.15,
"Nulldurchgänge %.3f vorne, %.3f hinten" % [a, b])
func _samples(name: String) -> PackedFloat32Array:
var d: PackedByteArray = (main.audio.sounds[name] as AudioStreamWAV).data
var out := PackedFloat32Array()
out.resize(d.size() / 2)
for i in out.size():
out[i] = d.decode_s16(i * 2) / 32768.0
return out
## Sekunde, bis der Klang unter 4 % seines Höchstwerts fällt.
func _tail(x: PackedFloat32Array) -> float:
var peak := 0.0
for v in x:
peak = maxf(peak, absf(v))
var last := 0
for i in x.size():
if absf(x[i]) > peak * 0.04:
last = i
return float(last) / 22050.0
func _zcr(x: PackedFloat32Array, a: int, b: int) -> float:
var c := 0
for i in range(a + 1, b):
if (x[i] >= 0.0) != (x[i - 1] >= 0.0):
c += 1
return float(c) / float(maxi(b - a, 1))
## Sucht einen zweiten, deutlich getrennten Schlag: eine Spitze im
## Bereich 0,10-0,25 s, davor ein echter Einbruch der Hüllkurve.
func _double_hit(x: PackedFloat32Array) -> Dictionary:
var win := 66
var env := PackedFloat32Array()
env.resize(x.size() / win)
var peak := 0.0
for i in env.size():
var m := 0.0
for k in win:
m = maxf(m, absf(x[i * win + k]))
env[i] = m
peak = maxf(peak, m)
var a: int = int(0.10 * 22050.0 / win)
var b: int = mini(int(0.25 * 22050.0 / win), env.size() - 1)
var best := 0.0
var best_i := a
for i in range(a, b + 1):
if env[i] > best:
best = env[i]
best_i = i
var dip := peak
for i in range(int(0.03 * 22050.0 / win), best_i):
dip = minf(dip, env[i])
var ok: bool = best > peak * 0.22 and dip < best * 0.6
return {"ok": ok, "at": float(best_i * win) / 22050.0, "dip": dip / maxf(best, 0.0001)}

97
tests/direction_test.gd Normal file
View File

@@ -0,0 +1,97 @@
extends Node
## Prüft Bewegungs- und Blickrichtung gegen die Kamera-Achsen.
var main
var step := 0
var wait := 0.0
var ok := 0
var fail := 0
var pos0 := Vector3.ZERO
var fwd0 := Vector3.ZERO
var right0 := Vector3.ZERO
func chk(n: String, c: bool, i := "") -> void:
if c: ok += 1; print(" OK %s %s" % [n, i])
else: fail += 1; print(" FAIL %s %s" % [n, i])
func _key(c: int, d: bool) -> void:
var e := InputEventKey.new(); e.physical_keycode = c; e.keycode = c; e.pressed = d
Input.parse_input_event(e)
func _mouse(dx: float, dy: float) -> void:
var e := InputEventMouseMotion.new()
e.relative = Vector2(dx, dy)
e.position = get_viewport().get_visible_rect().size * 0.5
e.global_position = e.position
Input.parse_input_event(e)
func snap() -> void:
var c = main.player.camera
pos0 = main.player.global_position
fwd0 = -c.global_basis.z # Blickrichtung
right0 = c.global_basis.x # Bildschirm rechts
func moved() -> Vector3:
return main.player.global_position - pos0
func _process(d):
if main == null:
main = get_tree().current_scene
return
if not main.started:
main._start_solo()
return
if wait > 0.0:
wait -= d
return
var p = main.player
match step:
0:
# Freie Fläche im Personenwagen, quer zur Fahrtrichtung schauen
var car = main.train.cars[1]
p.s = car.center_s
p.x = 0.0
p.yaw = 0.0
p.pitch = 0.0
wait = 0.3
1:
snap(); _key(KEY_W, true); wait = 0.5
2:
_key(KEY_W, false)
chk("W läuft nach vorn", moved().dot(fwd0) > 0.2,
"%.2f" % moved().dot(fwd0))
wait = 0.3
3:
snap(); _key(KEY_S, true); wait = 0.5
4:
_key(KEY_S, false)
chk("S läuft rückwärts", moved().dot(fwd0) < -0.2,
"%.2f" % moved().dot(fwd0))
wait = 0.3
5:
snap(); _key(KEY_D, true); wait = 0.4
6:
_key(KEY_D, false)
chk("D läuft nach rechts", moved().dot(right0) > 0.15,
"rechts=%.2f (negativ = falsche Seite)" % moved().dot(right0))
wait = 0.3
7:
snap(); _key(KEY_A, true); wait = 0.4
8:
_key(KEY_A, false)
chk("A läuft nach links", moved().dot(right0) < -0.15,
"rechts=%.2f" % moved().dot(right0))
wait = 0.3
9:
snap(); _mouse(150.0, 0.0); wait = 0.3
10:
var f = -p.camera.global_basis.z
chk("Maus rechts dreht nach rechts", f.dot(right0) > 0.1,
"%.2f" % f.dot(right0))
snap(); _mouse(0.0, 150.0); wait = 0.3
11:
var f2 = -p.camera.global_basis.z
chk("Maus runter schaut runter", f2.y < fwd0.y - 0.05,
"y %.2f -> %.2f" % [fwd0.y, f2.y])
print("== %d ok, %d fail ==" % [ok, fail])
get_tree().quit(1 if fail > 0 else 0)
step += 1

View File

@@ -0,0 +1 @@
uid://bnbo2gtpe5mtw

289
tests/input_test.gd Normal file
View File

@@ -0,0 +1,289 @@
extends Node
## Prüft die tatsächliche Eingabekette: Events -> GUI -> _unhandled_input.
var main
var step := 0
var wait := 0.0
var ok := 0
var fail := 0
var y0 := 0.0
var p0 := 0.0
var s0 := 0.0
var x0 := 0.0
var m0 := 0
var n0 := 0
func chk(name: String, cond: bool, info := "") -> void:
if cond:
ok += 1
print(" OK %s %s" % [name, info])
else:
fail += 1
print(" FAIL %s %s" % [name, info])
func _key(c: int, d: bool) -> void:
var e := InputEventKey.new()
e.physical_keycode = c
e.keycode = c
e.pressed = d
Input.parse_input_event(e)
func _mouse(dx: float, dy: float) -> void:
var e := InputEventMouseMotion.new()
e.relative = Vector2(dx, dy)
e.position = get_viewport().get_visible_rect().size * 0.5
e.global_position = e.position
Input.parse_input_event(e)
## Stellt den Spieler vor einen Hotspot und laesst ihn hinsehen.
func goto(car_index: int, kind: String) -> bool:
var car = main.train.cars[car_index]
for h in car.hotspots:
if String(h["kind"]) != kind:
continue
var pos: Vector3 = h["pos"]
var hs: float = car.center_s + pos.z
var p = main.player
for dz in [-1.0, 1.0, -1.4, 1.4, -0.7, 0.7]:
for px in [pos.x * 0.35, 0.0, pos.x * 0.6, -pos.x * 0.35]:
if p._blocked(px, hs + dz):
continue
p.x = px
p.s = hs + dz
p.yaw = atan2(pos.x - px, hs - p.s)
p.pitch = clampf(atan2(pos.y - (2.72), Vector2(pos.x - px, hs - p.s).length()), -1.2, 1.2)
return true
return false
func _process(d):
if main == null:
main = get_tree().current_scene
return
if not main.started:
main._start_solo()
return
if wait > 0.0:
wait -= d
return
var p = main.player
match step:
0:
chk("Maus gefangen", Input.mouse_mode == Input.MOUSE_MODE_CAPTURED,
"mode=%d" % Input.mouse_mode)
y0 = p.yaw
p0 = p.pitch
_mouse(120.0, 60.0)
wait = 0.3
1:
chk("Mausblick horizontal", absf(p.yaw - y0) > 0.05,
"yaw %.3f -> %.3f" % [y0, p.yaw])
chk("Mausblick vertikal", absf(p.pitch - p0) > 0.05,
"pitch %.3f -> %.3f" % [p0, p.pitch])
p.yaw = 0.0
p.pitch = 0.0
s0 = p.s
_key(KEY_W, true)
wait = 1.0
2:
_key(KEY_W, false)
chk("Vorwärts laufen", p.s - s0 > 0.5, "ds=%.2f" % (p.s - s0))
x0 = p.x
_key(KEY_D, true)
wait = 0.6
3:
_key(KEY_D, false)
chk("Seitwärts laufen", absf(p.x - x0) > 0.2, "dx=%.2f" % (p.x - x0))
# Ans Fahrpult stellen und E drücken
var loco = main.train.cars[0]
p.s = loco.center_s + loco.inner_z1 - 1.7
p.x = 0.0
p.yaw = 0.0
p.pitch = 0.0
wait = 0.3
4:
chk("Hotspot erkannt", String(p.hotspot.get("kind", "")) == "drive",
"kind=%s" % String(p.hotspot.get("kind", "-")))
_key(KEY_E, true)
wait = 0.3
5:
_key(KEY_E, false)
chk("E übernimmt Steuerung", main.driver_id == 1, "driver=%d" % main.driver_id)
chk("Spieler im Fahrmodus", p.mode == 1, "mode=%d" % p.mode)
_key(KEY_W, true)
wait = 1.5
6:
_key(KEY_W, false)
chk("W gibt Gas", main.train.speed > 1.0, "v=%.1f" % main.train.speed)
_key(KEY_S, true)
wait = 3.0
7:
_key(KEY_S, false)
chk("S bremst", main.train.speed < 0.05, "v=%.2f" % main.train.speed)
_key(KEY_E, true)
wait = 0.3
8:
_key(KEY_E, false)
chk("E gibt Steuerung frei", main.driver_id == 0)
_key(KEY_V, true)
_key(KEY_V, false)
wait = 0.3
9:
chk("V schaltet auf Außenkamera", main.cam_rig.is_active())
_key(KEY_V, true)
_key(KEY_V, false)
wait = 0.3
10:
chk("V schaltet zurück", p.camera.current)
_key(KEY_ESCAPE, true)
_key(KEY_ESCAPE, false)
wait = 0.3
11:
chk("Esc gibt Maus frei", Input.mouse_mode != Input.MOUSE_MODE_CAPTURED)
p.capture_mouse(true)
# Zweiter Spieler darf nicht dazwischenfunken
main._srv_toggle_drive(1)
main._srv_toggle_drive(2)
chk("Führerstand exklusiv", main.driver_id == 1, "driver=%d" % main.driver_id)
main._srv_toggle_drive(1)
# Zug mit Speisewagen und Gästen ausstatten
main.train.consist = ["coach", "dining", "freight"] as Array[String]
main._rebuild_train()
main._park_at(main.stations[0])
for i in 4:
main.spawn_passenger(0, 2)
wait = 7.0
12:
chk("Gäste eingestiegen", _seated() >= 4, "seated=%d" % _seated())
chk("Zum Depot-Terminal gelaufen", goto(0, "shop"))
wait = 0.3
13:
chk("Terminal im Blick", String(p.hotspot.get("kind", "")) == "shop",
"kind=%s" % String(p.hotspot.get("kind", "-")))
_key(KEY_E, true); _key(KEY_E, false)
wait = 0.4
14:
chk("Terminal offen", main.shop.is_open)
main.shop._tabs.current_tab = 1
wait = 0.3
15:
var btn := _find_button(main.shop, "Kaufen")
chk("Kaufen-Knopf sichtbar", btn != null)
m0 = main.money
n0 = main.train.consist.size()
if btn != null:
_click(btn.get_global_rect().get_center())
wait = 0.5
16:
chk("Klick im Laden kauft", main.train.consist.size() == n0 + 1 and main.money < m0,
"consist %d->%d, geld %d->%d" % [n0, main.train.consist.size(), m0, main.money])
_key(KEY_ESCAPE, true); _key(KEY_ESCAPE, false)
wait = 0.4
17:
chk("Terminal geschlossen", not main.shop.is_open)
chk("Gäste überleben den Umbau", _seated() >= 4, "seated=%d" % _seated())
chk("Maus wieder gefangen", Input.mouse_mode == Input.MOUSE_MODE_CAPTURED)
chk("Zur Theke gelaufen", goto(_car_of("tap"), "tap"))
wait = 0.3
18:
chk("Theke im Blick", String(p.hotspot.get("kind", "")) == "tap",
"kind=%s" % String(p.hotspot.get("kind", "-")))
_key(KEY_E, true); _key(KEY_E, false)
wait = 0.3
19:
chk("Getränk aufgenommen", p.carrying > 0, "carrying=%d" % p.carrying)
chk("Zum Gast gelaufen", _goto_occupied_seat())
wait = 0.3
20:
chk("Gast im Blick", String(p.hotspot.get("kind", "")) == "seat",
"kind=%s" % String(p.hotspot.get("kind", "-")))
m0 = main.money
_key(KEY_E, true); _key(KEY_E, false)
wait = 0.3
21:
chk("Ticket bringt Geld", main.money > m0, "+%d €" % (main.money - m0))
var occ = _seat_occupant()
chk("Gast erreichbar", occ != null)
if occ != null:
occ.wants_drink = true
occ.served = false
m0 = main.money
_key(KEY_E, true); _key(KEY_E, false)
wait = 0.3
22:
var occ2 = _seat_occupant()
chk("Serviert", occ2 != null and occ2.served)
chk("Trinkgeld", main.money > m0, "+%d €" % (main.money - m0))
chk("Getränk verbraucht", p.carrying == 0, "carrying=%d" % p.carrying)
chk("Zur Ladekonsole gelaufen", goto(_car_of("load"), "load"))
wait = 0.3
23:
chk("Konsole im Blick", String(p.hotspot.get("kind", "")) == "load",
"kind=%s" % String(p.hotspot.get("kind", "-")))
main.stations[0].stock = 10.0
_key(KEY_E, true)
wait = 2.5
24:
_key(KEY_E, false)
chk("E halten lädt Ware", main.train.total_cargo() > 0,
"cargo=%d" % main.train.total_cargo())
print("== %d ok, %d fail ==" % [ok, fail])
get_tree().quit(1 if fail > 0 else 0)
step += 1
func _seated() -> int:
var n := 0
for pa in main.passengers:
if pa.state == 2:
n += 1
return n
func _seat_occupant():
var h = main.player.hotspot
if String(h.get("kind", "")) != "seat":
return null
return main.train.cars[int(h["car"])].seats[int(h["seat"])]["occupant"]
func _click(pos: Vector2) -> void:
for pressed in [true, false]:
var e := InputEventMouseButton.new()
e.button_index = MOUSE_BUTTON_LEFT
e.pressed = pressed
e.position = pos
e.global_position = pos
Input.parse_input_event(e)
func _find_button(n: Node, text: String) -> Button:
if n is Button and (n as Button).text == text and not (n as Button).disabled:
return n
for c in n.get_children():
var r := _find_button(c, text)
if r != null:
return r
return null
func _car_of(kind: String) -> int:
for ci in main.train.cars.size():
for h in main.train.cars[ci].hotspots:
if String(h["kind"]) == kind:
return ci
return -1
func _goto_occupied_seat() -> bool:
var pl = main.player
for ci in main.train.cars.size():
var car = main.train.cars[ci]
for h in car.hotspots:
if String(h["kind"]) != "seat":
continue
if car.seats[int(h["seat"])]["occupant"] == null:
continue
var pos: Vector3 = h["pos"]
var hs: float = car.center_s + pos.z
for dz in [0.0, -0.8, 0.8, -1.2, 1.2]:
if pl._blocked(0.0, hs + dz):
continue
pl.x = 0.0
pl.s = hs + dz
pl.yaw = atan2(pos.x, hs - pl.s)
pl.pitch = 0.0
return true
return false

1
tests/input_test.gd.uid Normal file
View File

@@ -0,0 +1 @@
uid://bip2svfktigjy

101
tests/net_test.gd Normal file
View File

@@ -0,0 +1,101 @@
extends Node
## Netzwerktest. Rolle über: -- --host bzw. -- --client
var main
var role := ""
var step := 0
var wait := 0.0
var ok := 0
var fail := 0
var m0 := 0
func _ready() -> void:
for a in OS.get_cmdline_user_args():
if a == "--host":
role = "host"
elif a == "--client":
role = "client"
func chk(name: String, cond: bool, info := "") -> void:
if cond:
ok += 1
print("[%s] OK %s %s" % [role, name, info])
else:
fail += 1
print("[%s] FAIL %s %s" % [role, name, info])
func _process(d):
if main == null:
main = get_tree().current_scene
return
if not main.started:
if role == "host":
main._start_host(7788)
elif role == "client" and step == 0:
main._start_join("127.0.0.1", 7788)
step = 1
return
if wait > 0.0:
wait -= d
return
if role == "host":
_host_step()
else:
_client_step()
func _host_step() -> void:
match step:
0:
print("[host] Sitzung gestartet")
for i in 4:
main.spawn_passenger(0, 2)
wait = 30.0
1:
print("[host] Ende. money=%d driver=%d" % [main.money, main.driver_id])
get_tree().quit()
step += 1
func _client_step() -> void:
match step:
1:
wait = 6.0
2:
chk("Welt gebaut", main.stations.size() == 5)
chk("Zug übernommen", main.train.cars.size() == 3, "cars=%d" % main.train.cars.size())
chk("Eigene Peer-ID", main.my_id > 1)
chk("Fahrgäste sichtbar", main.passengers.size() >= 4, "n=%d" % main.passengers.size())
var seated := 0
for p in main.passengers:
if p.state == 2:
seated += 1
chk("Gäste sitzen", seated >= 4, "seated=%d" % seated)
chk("Geld synchron", main.money == 1500)
main.request("drive")
wait = 1.5
3:
chk("Steuerung erhalten", main.driver_id == main.my_id)
m0 = main.money
var done := false
for ci in main.train.cars.size():
for si in main.train.cars[ci].seats.size():
var occ = main.train.cars[ci].seats[si]["occupant"]
if occ != null and not occ.ticket_checked:
main.request("seat", "", ci, si)
done = true
break
if done:
break
chk("Ticket-Anfrage gesendet", done)
wait = 1.5
4:
chk("Geld gestiegen", main.money > m0, "%d -> %d" % [m0, main.money])
main.request("buy_car", "coach")
wait = 1.5
5:
chk("Kauf synchron", main.train.consist.size() == 3)
chk("Zug neu gebaut", main.train.cars.size() == 4)
wait = 2.0
6:
chk("Kein Selbstlauf ohne Gas", main.train.speed < 0.01)
print("[client] == %d ok, %d fail ==" % [ok, fail])
get_tree().quit(1 if fail > 0 else 0)
step += 1

1
tests/net_test.gd.uid Normal file
View File

@@ -0,0 +1 @@
uid://dodlor1js5f2c

54
tests/run.sh Executable file
View File

@@ -0,0 +1,54 @@
#!/bin/bash
# Führt die Testskripte aus. Sie werden als Autoload eingehängt und danach
# wird das Projekt wieder in den Ausgangszustand gebracht.
set -u
GODOT="${GODOT:-$HOME/bin/godot}"
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
restore() { git checkout project.godot 2>/dev/null || cp /tmp/_pg.bak project.godot; rm -f scripts/_test.gd /tmp/_pg.bak; }
cp project.godot /tmp/_pg.bak
trap restore EXIT
install_test() {
cp "$1" scripts/_test.gd
cp /tmp/_pg.bak project.godot
printf '\n[autoload]\n\nTest="*res://scripts/_test.gd"\n' >> project.godot
}
echo "=== Einzelspieler-Tests ==="
install_test tests/solo_test.gd
"$GODOT" --headless --path . --fixed-fps 60 2>&1 | grep -E "OK|FAIL|=="
SOLO=${PIPESTATUS[0]}
echo
echo "=== Eingabe-Tests (braucht ein Fenster) ==="
install_test tests/input_test.gd
"$GODOT" --path . --resolution 1280x720 2>&1 | grep -E "OK|FAIL|=="
INPUT=${PIPESTATUS[0]}
echo
echo "=== Klang-Tests ==="
install_test tests/audio_test.gd
"$GODOT" --path . --resolution 1280x720 2>&1 | grep -E "OK|FAIL|=="
AUDIO=${PIPESTATUS[0]}
echo
echo "=== Richtungs-Tests ==="
install_test tests/direction_test.gd
"$GODOT" --path . --resolution 1280x720 2>&1 | grep -E "OK|FAIL|=="
DIR=${PIPESTATUS[0]}
echo
echo "=== Netzwerk-Tests ==="
install_test tests/net_test.gd
"$GODOT" --headless --path . -- --host > /tmp/_host.log 2>&1 &
HOST=$!
sleep 6
"$GODOT" --headless --path . -- --client 2>&1 | grep -E "OK|FAIL|=="
NET=${PIPESTATUS[0]}
wait $HOST 2>/dev/null
echo
[ "$SOLO" -eq 0 ] && [ "$INPUT" -eq 0 ] && [ "$DIR" -eq 0 ] && [ "$AUDIO" -eq 0 ] && [ "$NET" -eq 0 ] && echo "Alle Tests bestanden." || echo "Es sind Tests fehlgeschlagen."
exit $(( SOLO || INPUT || DIR || AUDIO || NET ))

158
tests/solo_test.gd Normal file
View File

@@ -0,0 +1,158 @@
extends Node
var main
var step := 0
var wait := 0.0
var ok := 0
var fail := 0
var m0 := 0
func chk(name: String, cond: bool, info := "") -> void:
if cond:
ok += 1
print(" OK %s %s" % [name, info])
else:
fail += 1
print(" FAIL %s %s" % [name, info])
func seated() -> int:
var n := 0
for p in main.passengers:
if p.state == 2:
n += 1
return n
func _unchecked() -> int:
var n := 0
for p in main.passengers:
if p.state == 2 and not p.ticket_checked:
n += 1
return n
func _key(code: int, down: bool) -> void:
var e := InputEventKey.new()
e.physical_keycode = code
e.keycode = code
e.pressed = down
Input.parse_input_event(e)
func _process(d):
if main == null:
main = get_tree().current_scene
return
if not main.started:
main._start_solo()
return
if wait > 0.0:
wait -= d
return
match step:
0:
print("== Test start ==")
chk("Zug gebaut", main.train.cars.size() == 3, "cars=%d" % main.train.cars.size())
chk("Sitze", main.train.seat_count() == 8)
chk("Ladekapazität", main.train.cargo_capacity() == 10)
chk("Am Bahnhof", main.train.docked_at == main.stations[0])
var wr = main.train.walk_range()
chk("Begehbarer Bereich", wr.y - wr.x > 25.0, "%.1f m" % (wr.y - wr.x))
chk("Spieler startet im Führerstand", main.train.locate(main.player.s)["index"] == 0)
for i in 6:
main.spawn_passenger(0, 2)
chk("Wartende", main.stations[0].waiting.size() == 6)
wait = 8.0
1:
chk("Eingestiegen", seated() == 6, "seated=%d" % seated())
m0 = main.money
for p in main.passengers:
if p.state == 2:
main._check_ticket(p, 1)
chk("Ticketgeld", main.money > m0, "+%d €" % (main.money - m0))
chk("Alle kontrolliert", _unchecked() == 0)
2:
m0 = main.money
var st = main.stations[0]
st.stock = 12.0
for i in 10:
main._srv_transfer_one(1)
chk("Ladung aufgenommen", main.train.total_cargo() == 10)
chk("Lager geleert", st.stock < 3.0, "stock=%.1f" % st.stock)
3:
main._park_at(main.stations[2])
wait = 0.3
4:
chk("Am Eisenwerk", main.train.docked_at == main.stations[2])
m0 = main.money
for i in 12:
main._srv_transfer_one(1)
chk("Ware verkauft", main.train.total_cargo() == 0)
chk("Erlös", main.money - m0 >= 100, "+%d €" % (main.money - m0))
wait = 8.0
5:
var still := 0
for p in main.passengers:
if p.state == 2 and p.dest_station == 2:
still += 1
chk("Ausgestiegen", still == 0)
chk("Ansehen gestiegen", main.reputation > 0, "rep=%d" % main.reputation)
main.money = 20000
main.reputation = 50
var n0 = main.train.consist.size()
main.buy_car("dining")
chk("Speisewagen gekauft", main.train.consist.size() == n0 + 1)
chk("Speisewagen erkannt", main.train.has_dining())
main.buy_car("saloon")
chk("Salonwagen gekauft", main.train.seat_count() == 22)
wait = 0.2
6:
var v0 = main.train.max_speed()
main.buy_loco("express")
chk("Lok getauscht", main.train.loco_id == "express")
chk("Schneller", main.train.max_speed() > v0)
chk("Ausbau", main.buy_upgrade("scanner"))
chk("Ausbau nicht doppelt", not main.buy_upgrade("scanner"))
var before = main.train.consist.size()
main.sell_car(0)
chk("Wagen verkauft", main.train.consist.size() == before - 1)
wait = 0.3
7:
chk("Spieler im Zug", main.train.locate(main.player.s)["index"] == 0)
main._srv_toggle_drive(1)
chk("Steuerung übernommen", main.driver_id == 1)
_key(KEY_W, true)
wait = 3.0
8:
chk("Zug fährt", main.train.speed > 3.0, "v=%.1f m/s" % main.train.speed)
_key(KEY_W, false)
_key(KEY_S, true)
wait = 4.0
9:
chk("Zug bremst", main.train.speed < 0.01, "v=%.2f" % main.train.speed)
_key(KEY_S, false)
# Durch den ganzen Zug laufen: hinten anfangen, nach vorn gehen.
main._srv_toggle_drive(1)
chk("Steuerung abgegeben", main.driver_id == 0)
var span = main.train.walk_range()
main.player.s = span.x + 0.2
main.player.x = 0.0
main.player.yaw = 0.0
_key(KEY_W, true)
_key(KEY_SHIFT, true)
wait = 25.0
10:
_key(KEY_W, false)
_key(KEY_SHIFT, false)
var span2 = main.train.walk_range()
chk("Ganzer Zug begehbar", main.player.s > span2.y - 1.0,
"s=%.1f von %.1f" % [main.player.s, span2.y])
chk("Im Führerstand angekommen", main.train.locate(main.player.s)["index"] == 0)
main.player.x = 0.0
_key(KEY_D, true)
wait = 3.0
11:
_key(KEY_D, false)
chk("Wand hält den Spieler", absf(main.player.x) < 1.1, "x=%.2f" % main.player.x)
main.money = 200
chk("Kein Kauf ohne Geld", not main.buy_car("saloon"))
chk("Kein Kauf ohne Ansehen", not main.buy_upgrade("trolley") or main.reputation >= 20)
print("== %d ok, %d fail ==" % [ok, fail])
get_tree().quit(1 if fail > 0 else 0)
step += 1

1
tests/solo_test.gd.uid Normal file
View File

@@ -0,0 +1 @@
uid://bvosfi7jnndvs