Files
adminandClaude Opus 5.5 70402aa19a
feuerwerk / gate (push) Successful in 5m31s
feuerwerk / suite (push) Successful in 19m9s
Let the agent hold the game at a given game time (#435)
The agent could not look at a short moment of a running game. Its
calls took longer than the moment lasted, so an intro window from
0.5 to 2 s slipped past twice in the 5d test. Now the editor itself
stops the clock at the time the agent names.

- play takes until: play, resume or pause let the game run on to
  that game time and hold it there. The call answers once the game
  is paused, so get_runtime and screenshot show that moment.
- The frame that would step past the time is cut short
  (frameWithin in src/core/time.ts, used in PlaySession). The game
  stands at 0.5 s, not one frame later.
- play_input takes a last step {"pause": true}. After a wait it
  holds the game at the time of the wait, after a key or click at
  once. Its answer names the game time and whether it is paused.
- A pause that comes first (toolbar or a Break node), a Load scene
  or a Restart game call the until off, and the answer says so.
- When the clock stands still, the call gives up after 25 s with
  the time it is at. A tab in the background is refused at once.
- A recording that does not run in real time is not cut. There the
  game stops in the first frame that reaches the time.
- get_runtime and play_input give times to the millisecond, not to
  the hundredth, so 0.505 no longer reads as 0.51.
- The relay's instructions name both ways.

Tests:
- Unit agent-play (6 new) and time (2 new): 19/19, related editor
  and relay tests 1158/1158.
- 6 counter-checks, all compile, all red. One stayed green at
  first: 0.3 s is exactly 18 frames. The test now takes 0.31 s.
- e2e 71 (atx): ALL PASSED, 27 checks. New 71h3 holds at 0.505 s
  and 0.8 s and after a wait of 0.31 s. Without the cut in
  PlaySession it went red at 0.5166 s.
- Gate (atx): CHECK EXIT 0, 327 files, 5033 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 21:26:53 +02:00

93 lines
3.0 KiB
TypeScript

/** The global clock: one scaled step a frame, which is what the world, the particles and the shaders share. */
import { beforeEach, describe, expect, it } from 'vitest';
import { effectiveScale, frameWithin, GameClock, MAX_FRAME, timeStep } from '../../src/core/time';
describe('effectiveScale', () => {
it('is the scale itself while running', () => {
expect(effectiveScale(1, false)).toBe(1);
expect(effectiveScale(2.5, false)).toBe(2.5);
});
it('is zero while paused, whatever the scale says', () => {
expect(effectiveScale(1, true)).toBe(0);
expect(effectiveScale(4, true)).toBe(0);
});
it('is zero for anything that is not a positive number', () => {
expect(effectiveScale(0, false)).toBe(0);
expect(effectiveScale(-1, false)).toBe(0);
expect(effectiveScale(Number.NaN, false)).toBe(0);
expect(effectiveScale(Number.POSITIVE_INFINITY, false)).toBe(0);
});
});
describe('timeStep', () => {
it('is the frame itself at scale 1', () => {
expect(timeStep(0.016, 1, false)).toBeCloseTo(0.016, 9);
});
it('caps the frame before scaling it', () => {
// A stalled frame must not jump the game, and at scale 2 the cap is still on the frame, so
// twice speed really is twice the step.
expect(timeStep(5, 1, false)).toBe(MAX_FRAME);
expect(timeStep(5, 2, false)).toBe(MAX_FRAME * 2);
});
it('never runs backwards', () => {
expect(timeStep(-0.5, 1, false)).toBe(0);
});
});
describe('frameWithin (#435)', () => {
it('cuts the frame that would go past the time left, at the clock scale', () => {
expect(frameWithin(1 / 60, 1, 0.005)).toBeCloseTo(0.005, 12);
expect(timeStep(frameWithin(1 / 60, 2, 0.005), 2, false)).toBeCloseTo(0.005, 12);
});
it('leaves a frame that stays short of it, and one the clock does not move by', () => {
expect(frameWithin(1 / 60, 1, 1)).toBe(1 / 60);
expect(frameWithin(1 / 60, 0, 0.005)).toBe(1 / 60);
expect(frameWithin(1 / 60, 1, -1)).toBe(0);
});
});
describe('GameClock', () => {
let clock: GameClock;
beforeEach(() => {
clock = new GameClock();
});
it('starts at zero and runs in real time', () => {
expect(clock.now).toBe(0);
expect(clock.scale).toBe(1);
expect(clock.advance(0.02)).toBeCloseTo(0.02, 9);
clock.advance(0.02);
expect(clock.now).toBeCloseTo(0.04, 9);
});
it('stands still while paused and picks up where it stopped', () => {
clock.advance(0.05);
expect(clock.advance(0.05, true)).toBe(0);
expect(clock.advance(0.05, true)).toBe(0);
expect(clock.now).toBeCloseTo(0.05, 9);
clock.advance(0.05);
expect(clock.now).toBeCloseTo(0.1, 9);
});
it('runs at the scale it is set to', () => {
clock.scale = 2;
clock.advance(0.03);
expect(clock.now).toBeCloseTo(0.06, 9);
clock.scale = 0;
clock.advance(0.03);
expect(clock.now).toBeCloseTo(0.06, 9);
});
it('goes back to zero on reset', () => {
clock.advance(0.08);
clock.reset();
expect(clock.now).toBe(0);
});
});