Files
transcience/tools/bench.gd
T
claude c4beeae38f Initial commit: Transcience MVP
Top-down twin-stick bullet-hell, Godot 4.7, server-authoritative dedicated
server with client-side prediction. Clients send input only; the server
resolves every hit for both players and enemies (no PvP).

- SimWorld: whole simulation as plain RefCounted objects (no nodes, no
  physics server), ~0.24ms/tick at peak load -- runs headless for free and
  drives 78 tests in under a second
- BulletPool: struct-of-arrays bullet storage, replicated as spawn/despawn
  events rather than per-tick state
- Emitter framework (Ring/AimedSpread/WallGap/ArcSweep) shared by trash
  enemies and bosses -- a new boss is data in src/content/content.gd, no
  simulation changes
- The Warden of the Fold: stationary 4-phase boss built entirely on that
  format
- Lobby hub with a portal into on-demand dungeon instances; one process
  hosts the hub plus every concurrent dungeon
- Emergency escape: 3s server-owned channel, cancelled by damage
- tools/check.sh, test.sh (GUT), smoke.sh (real server + bot clients over
  ENet), bench.gd; git hooks wired to the same scripts
- docs/ARCHITECTURE.md, NETCODE.md, WORKFLOW.md, ROADMAP.md
2026-09-03 16:03:57 +02:00

84 lines
2.5 KiB
GDScript

extends SceneTree
## Headless performance probe for the simulation alone -- no transport, no
## rendering. Answers the only question that decides whether the design holds:
## how many ticks per second can one dedicated server sustain per instance, and
## how big does the bullet field get?
##
## godot --headless --path . --script tools/bench.gd
## godot --headless --path . --script tools/bench.gd -- --players 4 --ticks 5400
var players: int = 4
var ticks: int = 3600
func _init() -> void:
_parse()
print("bench: %d players, %d ticks (%.1f s of play)"
% [players, ticks, float(ticks) / float(SimConfig.TICK_RATE)])
_run("boss fight", _make_boss_world())
_run("trash wave", _make_wave_world())
quit(0)
func _parse() -> void:
var argv := OS.get_cmdline_user_args()
var i := 0
while i < argv.size():
match argv[i]:
"--players":
i += 1
players = int(argv[i])
"--ticks":
i += 1
ticks = int(argv[i])
i += 1
func _add_players(world: SimWorld) -> void:
for n in players:
var p := world.add_player(n + 2, "bench%d" % n)
p.pos = Vector2(-200.0 + 100.0 * float(n), 180.0)
func _make_boss_world() -> SimWorld:
var world := SimWorld.new(1234)
_add_players(world)
world.spawn_boss(Content.warden())
return world
func _make_wave_world() -> SimWorld:
var world := SimWorld.new(4321)
_add_players(world)
for i in 6:
world.spawn_enemy(Content.turret(), Vector2(-400.0 + 160.0 * float(i), -220.0), i * 20)
for i in 4:
world.spawn_enemy(Content.drifter(), Vector2(-240.0 + 160.0 * float(i), -80.0), i * 25)
return world
func _run(label: String, world: SimWorld) -> void:
var peak := 0
var total := 0
var start := Time.get_ticks_usec()
for t in ticks:
for peer in world.players:
var frames: Array[InputFrame] = [InputFrame.make(world.tick + 1,
Vector2(cos(float(t) * 0.02), sin(float(t) * 0.031)),
float(t) * 0.05, InputFrame.BTN_FIRE)]
world.queue_input(peer, frames)
world.step()
world.drain_events()
if world.boss != null:
# Sweep hp downwards instead of letting the boss die, so the run
# covers every phase including the heaviest one.
world.boss.hp = int(float(world.boss.def.max_hp)
* (0.95 - 0.9 * float(t) / float(ticks)))
peak = maxi(peak, world.pool.live_count)
total += world.pool.live_count
var elapsed := float(Time.get_ticks_usec() - start) / 1_000_000.0
var per_tick_ms := elapsed / float(ticks) * 1000.0
print(" %-12s %7.3f ms/tick peak %4d bullets avg %4d headroom x%.1f"
% [label, per_tick_ms, peak, total / ticks,
(1000.0 / float(SimConfig.TICK_RATE)) / maxf(per_tick_ms, 0.0001)])