324 lines
12 KiB
GDScript
324 lines
12 KiB
GDScript
extends SceneTree
|
|
## P5 online: the Old Stag on a server, fought by three players. Alice (this
|
|
## test, with the real world scene) and two bots, Bob and Cara
|
|
## (tests/net_world_bot.gd, each its own process with its own world), on a
|
|
## real headless server with --debug-cheats (to set the clock, give items,
|
|
## move players and hold the stag still).
|
|
##
|
|
## Checks: each of them can set a key piece in the shared altar; the stag is
|
|
## tougher for three; everyone sees the same stag; all three players' hits
|
|
## count; when it falls, all three are named, on every player's game; the
|
|
## antler goes to whoever picks it up first.
|
|
## Run: godot --headless --path . --script res://tests/test_net_boss.gd
|
|
|
|
const PORT := 24352
|
|
const DIR := "user://test_net"
|
|
const SRV := DIR + "/server_boss"
|
|
const BOTS := {"Bob": DIR + "/bob_boss", "Cara": DIR + "/cara_boss"}
|
|
const SEED := 777
|
|
|
|
var _fails := 0
|
|
var _finished := false
|
|
var _pids: Array[int] = []
|
|
var Net: Node
|
|
var WN: Node
|
|
var PN: Node
|
|
var CN: Node
|
|
var A: Node
|
|
var world: Node3D
|
|
var player: CharacterBody3D
|
|
var _cmd := {"Bob": 0, "Cara": 0}
|
|
var _toasts: Array[String] = []
|
|
|
|
func check(cond: bool, what: String) -> void:
|
|
print((" ok " if cond else " FAIL ") + what)
|
|
if not cond:
|
|
_fails += 1
|
|
|
|
func _initialize() -> void:
|
|
_run.call_deferred()
|
|
|
|
func _run() -> void:
|
|
Engine.max_fps = 60
|
|
Net = root.get_node("Net")
|
|
WN = root.get_node("WorldNet")
|
|
PN = root.get_node("PlayerNet")
|
|
CN = root.get_node("CreatureNet")
|
|
A = root.get_node("Actions")
|
|
A.notify.connect(func(t): _toasts.append(t)) # (the server's messages arrive as toasts too)
|
|
_rm_tree(SRV)
|
|
for n in BOTS:
|
|
_rm_tree(BOTS[n])
|
|
DirAccess.make_dir_recursive_absolute(BOTS[n])
|
|
# (No wildlife: nothing else should join the fight. Day 2: the wolves
|
|
# haven't come yet.)
|
|
_spawn(["--server", "--port", str(PORT), "--config", SRV + "/server.json",
|
|
"--server-name", "Boss Test", "--seed", str(SEED), "--debug-cheats", "--no-creatures"])
|
|
await _sleep(4.0)
|
|
await _body()
|
|
Net.stop()
|
|
for pid in _pids:
|
|
if OS.is_process_running(pid):
|
|
OS.kill(pid)
|
|
check(_finished, "test ran to the end (no script errors)")
|
|
_rm_tree(SRV)
|
|
for n in BOTS:
|
|
_rm_tree(BOTS[n])
|
|
print("RESULT: %s (%d failures)" % ["PASS" if _fails == 0 else "FAIL", _fails])
|
|
quit(1 if _fails > 0 else 0)
|
|
|
|
func _body() -> void:
|
|
var me = load("res://scripts/net/identity.gd").open(DIR + "/a_boss")
|
|
me.name = "Alice"
|
|
Net.identity = me
|
|
var r := await _join()
|
|
check(r == "", "Alice joins (%s)" % r)
|
|
if r != "":
|
|
return
|
|
world = load("res://scenes/world.tscn").instantiate()
|
|
root.add_child(world)
|
|
for _i in range(8):
|
|
await physics_frame
|
|
player = world.player
|
|
player.set_physics_process(false)
|
|
await _until(func(): return WN.received >= 1, 10.0)
|
|
for n in BOTS:
|
|
_spawn(["--identity", DIR + "/" + n.to_lower() + "_boss_id", "--name", n, "--address", "127.0.0.1:%d" % PORT, "--dir", BOTS[n]],
|
|
"res://tests/net_world_bot.gd")
|
|
for n in BOTS:
|
|
var b0 := await _bot_result(n, 0, 40.0)
|
|
check(bool(b0.get("ok", false)), "%s joins (%s)" % [n, b0.get("why", "ok")])
|
|
if not bool(b0.get("ok", false)):
|
|
return
|
|
await _keys()
|
|
await _call()
|
|
await _fight()
|
|
await _prize()
|
|
world.queue_free()
|
|
await process_frame
|
|
Net.leave()
|
|
_finished = true
|
|
|
|
var c := Vector3.ZERO ## the stones' centre
|
|
|
|
func _ground(p: Vector3) -> Vector3:
|
|
return Vector3(p.x, world.terrain.sample_height(p.x, p.z) + 0.05, p.z)
|
|
|
|
func _keys() -> void:
|
|
print("three players, three key pieces")
|
|
check(world.stones.found, "Alice's game has the same standing stones")
|
|
c = world.stones.center()
|
|
PN.ask("debug_time", [2, 22.0])
|
|
await _tp(_ground(c + Vector3(2.2, 0, 0)))
|
|
await _bot("Bob", {"do": "tp", "to": _v(_ground(c + Vector3(-2.2, 0, 0)))})
|
|
await _bot("Cara", {"do": "tp", "to": _v(_ground(c + Vector3(0, 0, 2.2)))})
|
|
PN.ask("debug_clear", [])
|
|
await _give("barrow_key", 1)
|
|
_toasts.clear()
|
|
check(A.use_stones(player, world.stones), "Alice sets hers")
|
|
await _until(func(): return world.stones.keys == 1, 3.0)
|
|
check(world.stones.keys == 1 and _toast_has("1 of 3"), "the altar has one (%d; %s)" % [world.stones.keys, _toasts])
|
|
for n in BOTS:
|
|
await _bot(n, {"do": "ask", "what": "debug_clear", "args": []})
|
|
await _bot(n, {"do": "ask", "what": "debug_give", "args": ["barrow_key", 1]})
|
|
await _bot(n, {"do": "ask", "what": "stones", "args": []})
|
|
await _until(func(): return world.stones.keys == 3, 4.0)
|
|
check(world.stones.keys == 3, "then Bob and Cara set theirs: Alice sees all three")
|
|
var bs := await _bot("Bob", {"do": "stones"})
|
|
check(int(bs.get("keys", -1)) == 3, "and so does Bob")
|
|
|
|
func _call() -> void:
|
|
print("calling it for three")
|
|
_toasts.clear()
|
|
check(A.use_stones(player, world.stones), "Alice calls the Old Stag")
|
|
var found := {"s": null}
|
|
await _until(func():
|
|
for cid in CN.puppets:
|
|
if CN.puppets[cid].kind == "old_stag" and not CN.puppets[cid].is_dead():
|
|
found.s = CN.puppets[cid]
|
|
return found.s != null, 6.0)
|
|
var s: Node3D = found.s
|
|
check(s != null, "it appears on Alice's screen (from the server)")
|
|
if s == null:
|
|
return
|
|
stag_cid = int(s.get_meta("cid"))
|
|
check(absf(s.hp - 1980.0) < 0.5, "with 1980 health: 60%% more for each of the two extra players (%.0f)" % s.hp)
|
|
check(_toast_has("Old Stag has come"), "everyone's told (%s)" % [_toasts])
|
|
var life := await _bot("Cara", {"do": "life"})
|
|
var seen: Dictionary = (life.get("puppets", {}) as Dictionary).get(str(stag_cid), {})
|
|
check(not seen.is_empty() and seen.kind == "old_stag" and _p(seen.pos).distance_to(s.global_position) < 4.0,
|
|
"Cara sees the same stag, in the same place")
|
|
|
|
var stag_cid := -1
|
|
|
|
func _stag() -> Node3D:
|
|
var s: Variant = CN.puppets.get(stag_cid)
|
|
return s if is_instance_valid(s) else null
|
|
|
|
func _fight() -> void:
|
|
print("three players hit it")
|
|
var s := _stag()
|
|
if s == null:
|
|
return
|
|
PN.ask("debug_boss", ["hold", 60.0])
|
|
await _sleep(0.5)
|
|
s = _stag()
|
|
var at: Vector3 = s.global_position
|
|
await _tp(_ground(at + Vector3(2.4, 0, 0)))
|
|
await _bot("Bob", {"do": "tp", "to": _v(_ground(at + Vector3(-2.4, 0, 0)))})
|
|
await _bot("Cara", {"do": "tp", "to": _v(_ground(at + Vector3(0, 0, 2.4)))})
|
|
PN.ask("debug_clear", [])
|
|
var slot := await _give("iron_spear", 1)
|
|
player.select(slot)
|
|
PN.ask("debug_stats", [{"health": 100.0, "stamina": 100.0}])
|
|
await _sleep(0.4)
|
|
var hp0: float = s.hp
|
|
check(A.melee(player, s), "Alice strikes with an iron spear")
|
|
await _until(func(): return _stag() != null and _stag().hp < hp0 - 1.0, 3.0)
|
|
check(_stag() != null and absf(hp0 - _stag().hp - 39.0) < 1.0, "39 (26, half as much again while it's stunned): %.0f" % (hp0 - _stag().hp))
|
|
for n in BOTS:
|
|
await _bot(n, {"do": "ask", "what": "debug_clear", "args": []})
|
|
await _bot(n, {"do": "ask", "what": "debug_give", "args": ["iron_spear", 1]})
|
|
await _bot(n, {"do": "ask", "what": "debug_stats", "args": [{"stamina": 100.0}]})
|
|
await _bot(n, {"do": "ask", "what": "melee", "args": [stag_cid]})
|
|
await _until(func(): return _stag() != null and _stag().hp < hp0 - 116.0, 3.0)
|
|
check(_stag() != null and absf(hp0 - _stag().hp - 117.0) < 1.5, "Bob's and Cara's blows count too: 117 in all (%.0f)" % (hp0 - _stag().hp))
|
|
|
|
func _prize() -> void:
|
|
print("it falls")
|
|
var s := _stag()
|
|
if s == null:
|
|
return
|
|
var at: Vector3 = s.global_position
|
|
_toasts.clear()
|
|
PN.ask("debug_boss", ["hp", 5.0])
|
|
await _sleep(0.3)
|
|
await _bot("Cara", {"do": "ask", "what": "debug_stats", "args": [{"stamina": 100.0}]})
|
|
await _bot("Cara", {"do": "ask", "what": "melee", "args": [stag_cid]})
|
|
await _until(func(): return _toast_has("has fallen"), 4.0)
|
|
check(_toast_has("Beaten by Alice, Bob and Cara"), "everyone who fought it is named: %s" % [_toasts])
|
|
await _until(func(): return world.stones.beaten, 3.0)
|
|
check(world.stones.beaten and world.stones.beaten_by == "Alice, Bob and Cara" and world.stones.fighters.size() == 3,
|
|
"Alice's game remembers it: beaten by \"%s\"" % world.stones.beaten_by)
|
|
var bs := await _bot("Bob", {"do": "stones"})
|
|
check(bool(bs.get("beaten", false)) and String(bs.get("beaten_by", "")) == "Alice, Bob and Cara" and int(bs.get("keys", -1)) == 0,
|
|
"so does Bob's, and the key pieces are spent")
|
|
var drops := {"antler": "", "trophy": ""}
|
|
await _until(func():
|
|
for nid in WN.dropped:
|
|
var d: Node3D = WN.dropped[nid]
|
|
if is_instance_valid(d) and d.global_position.distance_to(at) < 5.0:
|
|
if d.item_id == "stag_antler":
|
|
drops.antler = "#%d" % nid
|
|
elif d.item_id == "stag_trophy":
|
|
drops.trophy = "#%d" % nid
|
|
return not drops.antler.is_empty() and not drops.trophy.is_empty(), 4.0)
|
|
check(not drops.antler.is_empty() and not drops.trophy.is_empty(), "its antler and trophy lie where it fell, for everyone to see")
|
|
if drops.antler.is_empty():
|
|
return
|
|
var took := await _bot("Bob", {"do": "take", "key": drops.antler})
|
|
var has := false
|
|
for st in took.get("inv", []):
|
|
if st != null and String(st.get("id", "")) == "stag_antler":
|
|
has = true
|
|
check(has, "Bob gets there first: the antler is his")
|
|
await _until(func(): return not WN.dropped.has(int(drops.antler.substr(1))), 3.0)
|
|
check(not WN.dropped.has(int(drops.antler.substr(1))), "and it's gone from the ground for Alice")
|
|
|
|
# --- helpers -----------------------------------------------------------------------
|
|
|
|
func _toast_has(s: String) -> bool:
|
|
for t in _toasts:
|
|
if t.contains(s):
|
|
return true
|
|
return false
|
|
|
|
func _tp(at: Vector3) -> void:
|
|
PN.ask("debug_tp", [at + Vector3(0, 0.05, 0)])
|
|
await _until(func(): return player.global_position.distance_to(at) < 1.0, 2.0)
|
|
player.set_physics_process(false)
|
|
await _sleep(0.3)
|
|
|
|
func _give(id: String, n: int) -> int:
|
|
var before: int = player.inventory.count(id)
|
|
PN.ask("debug_give", [id, n])
|
|
await _until(func(): return player.inventory.count(id) >= before + n, 3.0)
|
|
return _slot_of(id)
|
|
|
|
func _slot_of(id: String) -> int:
|
|
for i in range(player.inventory.size()):
|
|
var s = player.inventory.get_slot(i)
|
|
if s != null and s.id == id:
|
|
return i
|
|
return -1
|
|
|
|
func _until(cond: Callable, limit: float) -> bool:
|
|
var t := 0.0
|
|
while not cond.call() and t < limit:
|
|
await _sleep(0.05)
|
|
t += 0.05
|
|
return cond.call()
|
|
|
|
static func _v(p: Vector3) -> Array:
|
|
return [p.x, p.y, p.z]
|
|
|
|
static func _p(a: Array) -> Vector3:
|
|
return Vector3(a[0], a[1], a[2])
|
|
|
|
func _bot(n: String, cmd: Dictionary) -> Dictionary:
|
|
_cmd[n] += 1
|
|
var f := FileAccess.open("%s/cmd_%d.json" % [BOTS[n], _cmd[n]], FileAccess.WRITE)
|
|
f.store_string(JSON.stringify(cmd))
|
|
f.close()
|
|
return await _bot_result(n, _cmd[n], 20.0)
|
|
|
|
func _bot_result(n: String, i: int, limit: float) -> Dictionary:
|
|
var path := "%s/res_%d.json" % [BOTS[n], i]
|
|
var t := 0.0
|
|
while not FileAccess.file_exists(path) and t < limit:
|
|
await _sleep(0.05)
|
|
t += 0.05
|
|
await _sleep(0.05)
|
|
var d: Variant = JSON.parse_string(FileAccess.get_file_as_string(path)) if FileAccess.file_exists(path) else null
|
|
return d if d is Dictionary else {"error": "%s gave no answer to command %d" % [n, i], "puppets": {}}
|
|
|
|
func _join() -> String:
|
|
var state := {"done": false, "reason": ""}
|
|
var ok := func(): state.done = true
|
|
var bad := func(why): state.done = true; state.reason = why
|
|
Net.joined.connect(ok)
|
|
Net.join_failed.connect(bad)
|
|
Net.join("127.0.0.1:%d" % PORT)
|
|
var t := 0.0
|
|
while not state.done and t < 25.0:
|
|
await _sleep(0.05)
|
|
t += 0.05
|
|
Net.joined.disconnect(ok)
|
|
Net.join_failed.disconnect(bad)
|
|
return state.reason if state.done else "test timed out"
|
|
|
|
func _spawn(user_args: Array, script: String = "") -> int:
|
|
var args := PackedStringArray(["--headless", "--path", ProjectSettings.globalize_path("res://")])
|
|
if not script.is_empty():
|
|
args.append_array(["--script", script])
|
|
args.append("--")
|
|
for x in user_args:
|
|
args.append(str(x))
|
|
args.append_array(["--exit-with", str(OS.get_process_id())])
|
|
var pid := OS.create_process(OS.get_executable_path(), args)
|
|
_pids.append(pid)
|
|
return pid
|
|
|
|
func _sleep(s: float) -> void:
|
|
await create_timer(s).timeout
|
|
|
|
static func _rm_tree(d: String) -> void:
|
|
var da := DirAccess.open(d)
|
|
if da == null:
|
|
return
|
|
for sub in da.get_directories():
|
|
_rm_tree(d.path_join(sub))
|
|
for f in da.get_files():
|
|
da.remove(f)
|
|
DirAccess.remove_absolute(d)
|