Skip to content

Place finishers by when they crossed ​

Goal ​

Two racers who cross the finish line on the same fixed step are placed by which of them crossed first within that step, not by their seat number. A step is a sixtieth of a second, and at running speed that is long enough for a close finish to go to the wrong racer every time it happens.

Files you will edit ​

  • src/finish.ts

Steps ​

  1. Keep where each racer was on the previous step, and room for this step's finishers. Both are seat lanes, allocated once.

    ts
    // src/finish.ts
    import { Transform, type GameContext } from 'gameable';
    import { CONTACT, FINISH_Z, TOP, MAX_PLAYERS } from './course';
    import { race } from './state';
    import { emit } from './events';
    
    /** Each racer's z on the previous step: where they came from when they cross. */
    const lastZ = new Float32Array(MAX_PLAYERS);
    /** When, within this step, each finisher crossed. */
    const crossedAt = new Float64Array(MAX_PLAYERS);
    /** This step's finishers, earliest first. */
    const order = new Int8Array(MAX_PLAYERS);
  2. Work out when within the step each finisher crossed. The course runs along -Z, so a racer who was at from and is now past FINISH_Z crossed after (from - FINISH_Z) / (from - z) of the step. Keep lastZ up to date in every phase, so the first racing step already knows where everyone came from.

    ts
    // src/finish.ts
    export function finish(ctx: GameContext): void {
      let count = 0;
      for (let seat = 0; seat < MAX_PLAYERS; seat += 1) {
        const e = race.entity[seat];
        if (e === 0) continue;
        const from = lastZ[seat];
        const z = Transform.z[e];
        lastZ[seat] = z;
        if (
          race.phase !== 'racing' ||
          race.spectator[seat] !== 0 ||
          race.place[seat] !== 0 ||
          z > FINISH_Z ||
          Math.abs(Transform.x[e]) > CONTACT.finishWidth ||
          Transform.y[e] < TOP ||
          Transform.y[e] > CONTACT.finishMaxY
        )
          continue;
        const frac = from > FINISH_Z && from > z ? (from - FINISH_Z) / (from - z) : 1;
        crossedAt[seat] = ctx.elapsed - ctx.dt + frac * ctx.dt;
        // insert, earliest first; equal times keep seat order
        let i = count;
        while (i > 0 && crossedAt[order[i - 1]] > crossedAt[seat]) {
          order[i] = order[i - 1];
          i -= 1;
        }
        order[i] = seat;
        count += 1;
      }
  3. Give the places in that order.

    ts
    // src/finish.ts
      for (let i = 0; i < count; i += 1) {
        const seat = order[i];
        race.place[seat] = race.next++;
        emit(seat, 'finish');
        if (race.first < 0) race.first = ctx.elapsed;
        race.revision += 1;
      }
    }

Verify ​

sh
npm test

Add a test beside "awards places in order": on the same step seat 0 crosses from z = -66 to -68.5, which is late in the step, and seat 1 crosses from -67.9 to -69, which is early in it. Seat 1 is placed first:

ts
expect([...race.place.slice(0, 2)]).toEqual([2, 1]);

See also ​