Skip to content

step_simulation() can return without advancing t, and the caller cannot tell #20

Description

@thc1006

Flight.step_simulation() can return without advancing t, and a caller driving its own clock has no way to tell.

Two of its exits do bookkeeping and return rather than integrating:

  • flight.py:926, return # Move to next phase on next call, when the current phase has no time nodes left;
  • flight.py:884, which sets finished and returns once the phases run out.

Both are reasonable internally. The problem is for a caller stepping the flight alongside something else: it has no signal that the call was a no-op, so it advances its own clock while flight.t stands still, and the two drift apart.

What I measured

BalloonPoppingChallenge steps the flight once per environment step and advances its balloon positions on its own counter, so it is a direct case of this. Scenario 0 with the shipped example agent, 0.01 s steps:

environment steps 6012
calls that returned without advancing t 2
where steps 6011 and 6012, the last two
final world_time - flight.t 0.0232 s, or 2.3 steps
popped count 10, matching the stored baseline

The important part is the second row against the third: nothing stalled mid-flight. The out-of-rail transition, which I expected to consume a call, did not. Both stalls landed at the end, after the flight had already reached the ground, so the collision sweeps that decide the score were correctly aligned throughout. A run with a rocket that never leaves the pad shows the same shape, 2 stalls out of 43 steps, both at the end.

So this is not currently costing anyone a score. I am raising it as a contract question rather than a bug report.

The check

while not flight._step_state["finished"]:
    previous_t = flight.t
    flight.step_simulation()
    assert flight._step_state["finished"] or flight.t > previous_t

Two ways to close it

Document that a call may return without advancing time, so callers know they have to read flight.t rather than count their own steps. That costs nothing and puts the burden where the caller can act on it.

Or have step_simulation() loop until it has either advanced or finished, which makes one call mean one step of simulated time and leaves existing callers correct without changes. This is the stronger guarantee but it changes how much work a single call can do.

I am happy to send either one. The first is a docstring, the second is a small loop plus a test that no call is a no-op unless the flight is finished. My preference is the second, since the whole point of a stepping API is that the caller drives the clock, but it is your call which contract you want to commit to.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions