Add a second dungeon: the Proving Grounds, a test harness you walk into
ci / verify (push) Successful in 47s
ci / verify (push) Successful in 47s
Two labelled portals now stand side by side in the hub. The Proving Grounds runs the same generator, the same rooms, the same enemies and the same four-phase Warden -- enemies at a fifth health, the boss at 288 instead of 3600, and trash dropping potions 80% of the time instead of 8%. A manual pass over loot, the inventory, dropping and every boss phase takes a couple of minutes rather than a quarter of an hour. It is multipliers over the shared content rather than a parallel copy: a duplicated Content would drift the first time anything was tuned, and "identical but easier" would quietly stop being true. And it is a portal rather than a launch flag, so the two can be compared back to back without restarting the server -- which is most of the point. Which dungeon you enter is resolved from the player's server-side position, and PORTAL_USED carries the answer. There is deliberately no client message that names a dungeon: one would let any client ask for the generous loot table and bring the results back to the hub. Instance matching compares dungeon ids too, so walking into one entrance can never drop you into the other's run on timing alone. SimWorld.portals replaces portal_pos/portal_enabled, enter_instance carries the portal list and the dungeon id (the client needs the latter to scale the boss bar's ceiling the way the server scaled the boss), and Protocol.VERSION goes to 7. Also pins what happens when two players reach for one item on the same tick: exactly one gets it -- the loop is sequential and the pickup erases the entity before the next player looks. The tie-break is join order rather than distance, which is arbitrary rather than designed, so it is recorded as such. Stale doc fixed while here: MapGen.build() still claimed the client rebuilds the map from the seed, which has not been true since map streaming landed and is the opposite of the rule. check.sh clean, 288 tests, SMOKE PASS (18 assertions, both dungeon kinds opened over a real socket), all three diagnostics green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+65
-2
@@ -95,8 +95,7 @@ func test_a_full_inventory_leaves_the_item_on_the_floor() -> void:
|
||||
## The portal and pickup share the interact key. A full bag must not leave a
|
||||
## player standing on the portal unable to use it.
|
||||
func test_a_failed_pickup_does_not_block_the_portal() -> void:
|
||||
world.portal_enabled = true
|
||||
world.portal_pos = Vector2.ZERO
|
||||
world.portals = [SimPortal.make(Vector2.ZERO, Dungeons.default_id())]
|
||||
var me := world.players[ME]
|
||||
for _i in SimConfig.INVENTORY_SLOTS:
|
||||
me.add_item(Items.WARDENS_RATION)
|
||||
@@ -355,3 +354,67 @@ func test_a_replica_never_creates_loot_of_its_own() -> void:
|
||||
replica.step()
|
||||
assert_eq(replica.loot.size(), 0,
|
||||
"a client must never invent an item for itself to pick up")
|
||||
|
||||
|
||||
# --- Two players, one item ---------------------------------------------------
|
||||
|
||||
## There is no race here to lose. The simulation is single-threaded and
|
||||
## `_step_players` walks the player list one at a time within a tick, and
|
||||
## `_try_pickup` erases the item from `world.loot` the instant it succeeds --
|
||||
## so the second player's search that same tick finds nothing.
|
||||
func test_two_players_reaching_for_the_same_item_on_one_tick() -> void:
|
||||
var them := world.add_player(THEM, "them")
|
||||
them.pos = Vector2(6.0, 0.0)
|
||||
them.spawn_grace = 0
|
||||
world.players[ME].pos = Vector2(-6.0, 0.0)
|
||||
world.spawn_loot(Items.HEALTH_POTION, Vector2.ZERO)
|
||||
|
||||
var at := world.tick + 1
|
||||
var mine: Array[InputFrame] = [
|
||||
InputFrame.make(at, Vector2.ZERO, 0.0, InputFrame.BTN_INTERACT)]
|
||||
var theirs: Array[InputFrame] = [
|
||||
InputFrame.make(at, Vector2.ZERO, 0.0, InputFrame.BTN_INTERACT)]
|
||||
world.queue_input(ME, mine)
|
||||
world.queue_input(THEM, theirs)
|
||||
world.step()
|
||||
|
||||
var holders := 0
|
||||
for p in world.players.values():
|
||||
if p.inventory[0] == Items.HEALTH_POTION:
|
||||
holders += 1
|
||||
assert_eq(holders, 1, "exactly one player ends up with it")
|
||||
assert_eq(world.loot.size(), 0, "and it is gone from the floor")
|
||||
assert_eq(_events_of(SimEvent.Type.ITEM_PICKED_UP).size(), 1,
|
||||
"one pickup happened, so one pickup is announced")
|
||||
|
||||
|
||||
## The tie-break, recorded because it is arbitrary rather than designed: the
|
||||
## player list is a Dictionary, Godot dictionaries iterate in insertion order,
|
||||
## and players are inserted as they join. So on a genuine same-tick tie the
|
||||
## player who has been in the instance longer wins, every time -- distance to
|
||||
## the item does not enter into it.
|
||||
##
|
||||
## Worth knowing before it is mistaken for a fairness rule. If it ever needs to
|
||||
## be one, resolving contested pickups by distance after the player loop is the
|
||||
## natural change.
|
||||
func test_the_same_tick_tie_break_is_join_order_not_distance() -> void:
|
||||
var them := world.add_player(THEM, "them")
|
||||
them.spawn_grace = 0
|
||||
# The later joiner stands almost on top of it; the earlier one is further
|
||||
# away but still in reach.
|
||||
them.pos = Vector2(1.0, 0.0)
|
||||
world.players[ME].pos = Vector2(SimConfig.LOOT_PICKUP_RADIUS - 2.0, 0.0)
|
||||
world.spawn_loot(Items.HEALTH_POTION, Vector2.ZERO)
|
||||
|
||||
var at := world.tick + 1
|
||||
var mine: Array[InputFrame] = [
|
||||
InputFrame.make(at, Vector2.ZERO, 0.0, InputFrame.BTN_INTERACT)]
|
||||
var theirs: Array[InputFrame] = [
|
||||
InputFrame.make(at, Vector2.ZERO, 0.0, InputFrame.BTN_INTERACT)]
|
||||
world.queue_input(ME, mine)
|
||||
world.queue_input(THEM, theirs)
|
||||
world.step()
|
||||
|
||||
assert_eq(world.players[ME].inventory[0], Items.HEALTH_POTION,
|
||||
"the player who joined first wins, despite standing further away")
|
||||
assert_eq(them.inventory[0], Items.NONE)
|
||||
|
||||
Reference in New Issue
Block a user