Zwei Instanzen im Gleichschritt
Ein Test startet einen zweiten Godot-Prozess als Gast. Der Gast tritt über das Netzwerk bei und schlägt Stein, beide Welten enden im selben Zustand, ohne Abgleich unterwegs. Damit ist die Prüffrage von Meilenstein 1 Teil der automatischen Prüfungen. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,65 @@
|
|||||||
|
extends TestCase
|
||||||
|
## What milestone 1 promises: two instances of the game, each in a process of
|
||||||
|
## its own, stay in lockstep. This process is the host; a second Godot
|
||||||
|
## process plays a guest with tools/guest_bot.gd.
|
||||||
|
|
||||||
|
## The tick at which the host stops and both compare their worlds.
|
||||||
|
const UNTIL := 150
|
||||||
|
## Milliseconds the test waits for the second instance.
|
||||||
|
const PATIENCE_MSEC := 30000
|
||||||
|
|
||||||
|
|
||||||
|
func test_a_guest_in_another_process_stays_in_lockstep() -> void:
|
||||||
|
var directory := TestFiles.temporary_dir("two-instances")
|
||||||
|
var host: Node = load("res://main.tscn").instantiate()
|
||||||
|
host.storage_dir = directory.path_join("host")
|
||||||
|
host.command_line = PackedStringArray()
|
||||||
|
tree.root.add_child(host)
|
||||||
|
host.options.port = 0
|
||||||
|
host.listen_address = "127.0.0.1"
|
||||||
|
check(host.play(true), "the world opens")
|
||||||
|
|
||||||
|
var report_path := directory.path_join("guest.json")
|
||||||
|
var arguments := PackedStringArray([
|
||||||
|
"--headless",
|
||||||
|
"--path", ProjectSettings.globalize_path("res://"),
|
||||||
|
"--log-file", ProjectSettings.globalize_path(directory.path_join("guest.log")),
|
||||||
|
"-s", "res://tools/guest_bot.gd",
|
||||||
|
"--",
|
||||||
|
"--join=127.0.0.1:%d" % host.link.link.local_port(),
|
||||||
|
"--storage=" + directory.path_join("guest"),
|
||||||
|
"--until=%d" % UNTIL,
|
||||||
|
"--out=" + report_path,
|
||||||
|
])
|
||||||
|
var process := OS.create_process(OS.get_executable_path(), arguments)
|
||||||
|
check(process > 0, "the second instance starts")
|
||||||
|
var started := Time.get_ticks_msec()
|
||||||
|
while OS.is_process_running(process) and Time.get_ticks_msec() - started < PATIENCE_MSEC:
|
||||||
|
if host.session.guest_count() > 0:
|
||||||
|
# Once the guest is there, the test runs the host's ticks itself,
|
||||||
|
# one per frame, instead of twenty a second, and stops at UNTIL.
|
||||||
|
# A guest's arrival ends a pause, so the clock is stopped here.
|
||||||
|
host.clock.paused = true
|
||||||
|
if host.session.tick() < UNTIL:
|
||||||
|
host._run_tick()
|
||||||
|
await tree.process_frame
|
||||||
|
if OS.is_process_running(process):
|
||||||
|
OS.kill(process)
|
||||||
|
check(false, "the second instance did not finish in time")
|
||||||
|
|
||||||
|
var report: Variant = JSON.parse_string(FileAccess.get_file_as_string(report_path))
|
||||||
|
check(report is Dictionary and not report.has("error"), "the guest reports: %s" % [report])
|
||||||
|
if report is Dictionary and not report.has("error"):
|
||||||
|
check_eq(int(report["tick"]), UNTIL, "the guest stopped where the host stopped")
|
||||||
|
check_eq(report["hash"], host.session.state_hash_hex(), "both worlds are the same")
|
||||||
|
check_eq(int(report["player"]), 1, "the guest is player 1")
|
||||||
|
check_eq(int(report["stones"]), 1, "the guest broke the rock")
|
||||||
|
check_eq(int(report["resyncs"]), 0, "without a divergence on the way")
|
||||||
|
var rock: Vector2i = host.session.spawn_point() + Vector2i(7, -2)
|
||||||
|
check_eq(host.session.terrain_at(rock.x, rock.y), GwSession.TERRAIN_GROUND, "the rock is gone in the host's world too")
|
||||||
|
check_eq(host.session.player_name(1), "Bot", "the host knows the guest")
|
||||||
|
|
||||||
|
tree.root.remove_child(host)
|
||||||
|
host.link.link.close()
|
||||||
|
host.free()
|
||||||
|
TestFiles.remove_dir(directory)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c2vju7aa5pgqm
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
extends SceneTree
|
||||||
|
## A second instance of the game for the test of two instances. It starts the
|
||||||
|
## real main scene as a guest, hits the rock beside the spawn clearing until
|
||||||
|
## it has a stone, follows the host until the host stops at a tick, and
|
||||||
|
## writes what its world looks like then as JSON.
|
||||||
|
##
|
||||||
|
## The test of two instances starts it; by hand:
|
||||||
|
## godot --headless --path game -s res://tools/guest_bot.gd -- --join=ADDRESS
|
||||||
|
## --storage=FOLDER --until=TICK --out=FILE
|
||||||
|
|
||||||
|
## Milliseconds the bot waits for everything before it gives up.
|
||||||
|
const PATIENCE_MSEC := 20000
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
_run.call_deferred()
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
var settings := {}
|
||||||
|
for argument in OS.get_cmdline_user_args():
|
||||||
|
settings[argument.get_slice("=", 0)] = argument.get_slice("=", 1)
|
||||||
|
var until := int(settings.get("--until", "0"))
|
||||||
|
var report := {"error": "the host did not answer"}
|
||||||
|
|
||||||
|
var main: Node = load("res://main.tscn").instantiate()
|
||||||
|
main.storage_dir = settings.get("--storage", "user://bot")
|
||||||
|
main.command_line = PackedStringArray(["--join=" + settings.get("--join", ""), "--name=Bot"])
|
||||||
|
root.add_child(main)
|
||||||
|
|
||||||
|
var started := Time.get_ticks_msec()
|
||||||
|
while Time.get_ticks_msec() - started < PATIENCE_MSEC:
|
||||||
|
await process_frame
|
||||||
|
var session: GwSession = main.session
|
||||||
|
if session == null:
|
||||||
|
report = {"error": "the session ended: %s" % main.menu.status()}
|
||||||
|
break
|
||||||
|
if session.local_player() < 0:
|
||||||
|
continue
|
||||||
|
var rock := session.spawn_point() + Vector2i(7, -2)
|
||||||
|
if session.item_count(GwSession.ITEM_STONE) == 0:
|
||||||
|
main.dig_at(rock)
|
||||||
|
if session.tick() == until and session.pending_ticks() == 0:
|
||||||
|
report = {
|
||||||
|
"tick": session.tick(),
|
||||||
|
"hash": session.state_hash_hex(),
|
||||||
|
"player": session.local_player(),
|
||||||
|
"stones": session.item_count(GwSession.ITEM_STONE),
|
||||||
|
"resyncs": main.resyncs,
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
var file := FileAccess.open(settings.get("--out", "user://bot.json"), FileAccess.WRITE)
|
||||||
|
file.store_string(JSON.stringify(report))
|
||||||
|
file.close()
|
||||||
|
main.notification(Node.NOTIFICATION_WM_CLOSE_REQUEST)
|
||||||
|
root.remove_child(main)
|
||||||
|
main.free()
|
||||||
|
quit(0 if not report.has("error") else 1)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://hvhqt2viwbjx
|
||||||
Reference in New Issue
Block a user