5180 fichiers 38.5 Mo 0 téléchargements 206 vues mis à jour il y a 5 heures ui
nitro-v36-with-renderer-v36 / src / api / snowwar / SnowWarSimulation.test.ts
SnowWarSimulation.test.ts 15.8 Ko Télécharger le fichier
import { beforeEach, describe, expect, it } from 'vitest';
import { SUBTURN_MS, SUBTURNS_PER_TICK, tileToWorld } from './SnowWarMath';
import {
    SNOWWAR_EVENT_CREATE_SNOWBALL,
    SNOWWAR_EVENT_DELETE_OBJECT,
    SNOWWAR_EVENT_HIT,
    SNOWWAR_EVENT_LAUNCH_SNOWBALL,
    SNOWWAR_EVENT_MACHINE_TRANSFER,
    SNOWWAR_EVENT_MOVE,
    SNOWWAR_EVENT_RAY_GUN_BURST,
    SNOWWAR_EVENT_STUN,
    SNOWWAR_EVENT_TREE_HIT,
    SNOWWAR_OBJECT_AVATAR,
    SNOWWAR_OBJECT_MACHINE,
    SNOWWAR_OBJECT_PILE,
    SNOWWAR_OBJECT_TREE,
    SNOWWAR_STATE_CREATING,
    SNOWWAR_STATE_INVINCIBLE,
    SNOWWAR_STATE_NORMAL,
    SNOWWAR_STATE_STUNNED,
    SnowWarSimEvent,
    SnowWarSimulation,
} from './SnowWarSimulation';

const avatarObject = (objectId: number, userId: number, tileX: number, tileY: number) => ({
    objectType: SNOWWAR_OBJECT_AVATAR,
    objectId,
    userId,
    teamId: objectId % 2,
    name: `user${userId}`,
    figure: 'hd-180-1',
    gender: 'M',
    worldX: tileToWorld(tileX),
    worldY: tileToWorld(tileY),
    rotation: 0,
    health: 5,
    snowballCount: 5,
    activityTimer: 0,
    activityState: SNOWWAR_STATE_NORMAL,
    score: 0,
});

const event = (type: number, p1 = 0, p2 = 0, p3 = 0, p4 = 0, p5 = 0): SnowWarSimEvent =>
    ({ type, p1, p2, p3, p4, p5 });

describe('SnowWarSimulation', () =>
{
    let sim: SnowWarSimulation;
    let clock: number;

    /** Queue one AIR server turn and pump real time until its three subturns have played. */
    const applyTick = (events: SnowWarSimEvent[] = []) =>
    {
        const lists = Array.from({ length: SUBTURNS_PER_TICK }, (_, index) => index === 0 ? events : []);
        sim.queueGameStatus(lists);
        const target = sim.subturnCount + SUBTURNS_PER_TICK;
        let guard = 0;
        while (sim.subturnCount < target && guard++ < 100)
        {
            clock += SUBTURN_MS;
            sim.update(clock);
        }
    };

    beforeEach(() =>
    {
        sim = new SnowWarSimulation();
        clock = 0;
        sim.applyFullStatus([
            avatarObject(1, 100, 5, 5),
            avatarObject(2, 200, 10, 5),
            {
                objectType: SNOWWAR_OBJECT_MACHINE,
                objectId: 50,
                worldX: tileToWorld(8),
                worldY: tileToWorld(2),
                snowballCount: 3,
            },
            {
                objectType: SNOWWAR_OBJECT_TREE,
                objectId: 60,
                worldX: tileToWorld(9),
                worldY: tileToWorld(9),
                maximumHits: 3,
                hits: 0,
            },
            {
                objectType: SNOWWAR_OBJECT_PILE,
                objectId: 61,
                worldX: tileToWorld(4),
                worldY: tileToWorld(6),
                maxSnowballs: 12,
                snowballCount: 12,
            },
        ]);
        sim.update(clock); // prime the clock
    });

    it('applies a full status snapshot', () =>
    {
        expect(sim.avatars.size).toBe(2);
        expect(sim.machines.get(50)?.tileX).toBe(8);
        expect(sim.getAvatarByUserId(200)?.objectId).toBe(2);
        expect(sim.trees.get('9,9')?.hits).toBe(0);
        expect(sim.piles.get('4,6')?.snowballCount).toBe(12);
    });

    it('moves an avatar toward its move target at the AIR 534 units per subturn', () =>
    {
        const startX = tileToWorld(5);
        applyTick([event(SNOWWAR_EVENT_MOVE, 1, tileToWorld(7), tileToWorld(5))]);

        const avatar = sim.avatars.get(1);
        // One AIR turn advances exactly three 534-unit subturns.
        expect(avatar.worldX).toBe(startX + SUBTURNS_PER_TICK * 534);
        expect(avatar.prevWorldX).toBe(startX + (SUBTURNS_PER_TICK - 1) * 534);
        expect(avatar.worldY).toBe(tileToWorld(5));

        // 2 tiles = 6400 units = 12 subturns; three more turns complete it.
        applyTick();
        applyTick();
        applyTick();
        expect(avatar.worldX).toBe(tileToWorld(7));
        expect(avatar.walkGoalX).toBeNull();
    });

    it('paths around blocked tiles instead of walking through them', () =>
    {
        // 11x11 open map with a single wall tile at (6,5), directly between
        // the avatar (5,5) and its goal (7,5).
        const rows: string[] = [];
        for (let y = 0; y < 11; y++)
        {
            rows.push(y === 5 ? '000000x0000' : '00000000000');
        }
        sim.setLevel(rows, [], []);

        applyTick([event(SNOWWAR_EVENT_MOVE, 1, tileToWorld(7), tileToWorld(5))]);

        const avatar = sim.avatars.get(1);
        let steppedOnWall = false;
        for (let i = 0; i < 10; i++)
        {
            applyTick();
            if (avatar.tileX === 6 && avatar.tileY === 5) steppedOnWall = true;
        }

        expect(steppedOnWall).toBe(false);
        expect(avatar.worldX).toBe(tileToWorld(7));
        expect(avatar.worldY).toBe(tileToWorld(5));
        expect(avatar.walkGoalX).toBeNull();
    });

    it('never cuts diagonally across void ice corners', () =>
    {
        sim.setLevel(['0x0', 'x00', '000'], [], []);
        const avatar = sim.avatars.get(1);
        avatar.worldX = tileToWorld(0);
        avatar.worldY = tileToWorld(0);
        avatar.tileX = 0;
        avatar.tileY = 0;

        applyTick([event(SNOWWAR_EVENT_MOVE, 1, tileToWorld(2), tileToWorld(2))]);

        expect(avatar.worldX).toBe(tileToWorld(0));
        expect(avatar.worldY).toBe(tileToWorld(0));
    });

    it('stops at the closest reachable tile instead of oscillating at an unreachable goal', () =>
    {
        // Goal tile (7,5) is fully walled in; the avatar at (5,5) cannot
        // get closer than its own tile and must stop instead of bouncing
        // between equally-distant neighbours.
        const rows: string[] = [];
        for (let y = 0; y < 11; y++)
        {
            if (y >= 4 && y <= 6) rows.push('000000xxx00');
            else rows.push('00000000000');
        }
        sim.setLevel(rows, [], []);

        applyTick([event(SNOWWAR_EVENT_MOVE, 1, tileToWorld(7), tileToWorld(5))]);

        const avatar = sim.avatars.get(1);
        for (let i = 0; i < 6; i++) applyTick();

        expect(avatar.worldX).toBe(tileToWorld(5));
        expect(avatar.worldY).toBe(tileToWorld(5));
        expect(avatar.walkGoalX).toBeNull();
    });

    it('handles the create-snowball state machine', () =>
    {
        const avatar = sim.avatars.get(1);
        avatar.snowballCount = 0;

        applyTick([event(SNOWWAR_EVENT_CREATE_SNOWBALL, 1)]);
        expect(avatar.activityState).toBe(SNOWWAR_STATE_CREATING);
        expect(avatar.activityTimer).toBe(17); // 20 frames minus 3 elapsed

        // 20-frame creation timer expires within 6 more turns.
        for (let i = 0; i < 6; i++) applyTick();
        expect(avatar.activityState).toBe(SNOWWAR_STATE_NORMAL);
        expect(avatar.snowballCount).toBe(1);
    });

    it('cancels snowball creation when a new AIR movement target arrives', () =>
    {
        const avatar = sim.avatars.get(1);
        avatar.snowballCount = 0;

        applyTick([event(SNOWWAR_EVENT_CREATE_SNOWBALL, 1)]);
        expect(avatar.activityState).toBe(SNOWWAR_STATE_CREATING);

        applyTick([event(SNOWWAR_EVENT_MOVE, 1, tileToWorld(7), tileToWorld(5))]);
        expect(avatar.activityState).toBe(SNOWWAR_STATE_NORMAL);
        expect(avatar.activityTimer).toBe(0);
        expect(avatar.snowballCount).toBe(0);
        expect(avatar.worldX).toBeGreaterThan(tileToWorld(5));
    });

    it('spawns, flies and deletes snowballs', () =>
    {
        applyTick([event(SNOWWAR_EVENT_LAUNCH_SNOWBALL, 99, 1, tileToWorld(10), tileToWorld(5), 2)]);

        const ball = sim.snowballs.get(99);
        expect(ball).toBeDefined();
        expect(sim.avatars.get(1).snowballCount).toBe(4);

        applyTick([event(SNOWWAR_EVENT_DELETE_OBJECT, 99, tileToWorld(8), tileToWorld(5), 2300, 1)]);
        expect(sim.snowballs.has(99)).toBe(false);
        expect(sim.drainImpacts()).toEqual([{
            id: 1,
            worldX: tileToWorld(8),
            worldY: tileToWorld(5),
            height: 2300,
            trajectory: 1,
        }]);
    });

    it('applies authoritative tree damage states', () =>
    {
        applyTick([event(SNOWWAR_EVENT_TREE_HIT, 9, 9, 1)]);
        expect(sim.trees.get('9,9')?.hits).toBe(1);

        applyTick([event(SNOWWAR_EVENT_TREE_HIT, 9, 9, 3)]);
        expect(sim.trees.get('9,9')?.hits).toBe(3);
    });

    it('replays the ray gun as a seven-ball burst without spending carried snowballs', () =>
    {
        applyTick([event(SNOWWAR_EVENT_RAY_GUN_BURST, 99, 1, tileToWorld(20), tileToWorld(5), 3)]);

        expect([...sim.snowballs.keys()]).toEqual([99, 100, 101, 102, 103, 104, 105]);
        // The server never charges ammo for gun bursts; the replica has to
        // agree or the HUD count drifts out of sync.
        expect(sim.avatars.get(1).snowballCount).toBe(5);
    });

    it('snowballs expire on their own when TTL runs out', () =>
    {
        // AIR derives a short-lob TTL from distance: 10 tiles resolves to 17
        // subturns. The arc keeps descending after zero and is removed only
        // once it passes below ground.
        applyTick([event(SNOWWAR_EVENT_LAUNCH_SNOWBALL, 99, 1, tileToWorld(15), tileToWorld(5), 1)]);
        expect(sim.snowballs.has(99)).toBe(true);

        applyTick();
        applyTick();
        expect(sim.snowballs.has(99)).toBe(true);

        applyTick();
        expect(sim.snowballs.has(99)).toBe(true);

        for (let i = 0; i < 4; i++) applyTick();
        expect(sim.snowballs.has(99)).toBe(false);
    });

    it('presents every projectile subturn instead of consuming a whole throw in one browser frame', () =>
    {
        const subturns = Array.from({ length: 24 }, () => [] as SnowWarSimEvent[]);
        subturns[0] = [event(SNOWWAR_EVENT_LAUNCH_SNOWBALL, 99, 1, tileToWorld(10), tileToWorld(5), 0)];
        subturns[10] = [event(SNOWWAR_EVENT_DELETE_OBJECT, 99, tileToWorld(10), tileToWorld(5), 0, 0)];

        sim.queueGameStatus(subturns);
        expect(sim.subturnCount).toBe(0);

        // Even a half-second event-loop stall may present only one projectile
        // subturn per update. The quick throw therefore survives each paint
        // instead of launching and deleting synchronously.
        clock += 500;
        sim.update(clock);
        expect(sim.subturnCount).toBe(1);
        expect(sim.snowballs.has(99)).toBe(true);
        const presentedPositions = [sim.snowballs.get(99)!.locH];

        sim.queueGameStatus(Array.from({ length: 30 }, () => []));
        expect(sim.subturnCount).toBe(1);
        expect(sim.snowballs.has(99)).toBe(true);

        clock += 500;
        sim.update(clock);
        expect(sim.subturnCount).toBe(2);
        expect(sim.snowballs.has(99)).toBe(true);
        presentedPositions.push(sim.snowballs.get(99)!.locH);

        // Subturns 0..9 each leave a projectile state for React to paint.
        // Subturn 10 is the authoritative impact/delete event.
        while (sim.subturnCount < 10)
        {
            clock += SUBTURN_MS;
            sim.update(clock);
            const current = sim.snowballs.get(99);
            expect(current).toBeDefined();
            presentedPositions.push(current.locH);
        }
        expect(presentedPositions).toHaveLength(10);
        expect(new Set(presentedPositions).size).toBe(presentedPositions.length);
        expect(sim.snowballs.has(99)).toBe(true);

        clock += SUBTURN_MS;
        sim.update(clock);
        expect(sim.subturnCount).toBe(11);
        expect(sim.snowballs.has(99)).toBe(false);
    });

    it('applies hits and stuns with scores', () =>
    {
        applyTick([event(SNOWWAR_EVENT_HIT, 1, 2, 90)]);
        expect(sim.avatars.get(2).health).toBe(4);
        expect(sim.avatars.get(1).score).toBe(1);

        applyTick([event(SNOWWAR_EVENT_STUN, 2, 1, 90)]);
        const stunned = sim.avatars.get(2);
        expect(stunned.activityState).toBe(SNOWWAR_STATE_STUNNED);
        expect(stunned.snowballCount).toBe(5);
        expect(stunned.rotation).toBe(6);
        expect(sim.avatars.get(1).score).toBe(6);

        // 100 AIR stun frames -> invincible with restored health.
        for (let i = 0; i < 34; i++) applyTick();
        expect(stunned.activityState).toBe(SNOWWAR_STATE_INVINCIBLE);
        expect(stunned.health).toBe(5);

        // 60 invincibility frames -> normal.
        for (let i = 0; i < 20; i++) applyTick();
        expect(stunned.activityState).toBe(SNOWWAR_STATE_NORMAL);
    });

    it('transfers snowballs from machines', () =>
    {
        const avatar = sim.avatars.get(1);
        avatar.snowballCount = 0;

        applyTick([event(SNOWWAR_EVENT_MACHINE_TRANSFER, 1, 50)]);
        expect(avatar.snowballCount).toBe(1);
        expect(sim.machines.get(50).snowballCount).toBe(2);
    });

    it('depletes finite snowball piles through the shared pickup event', () =>
    {
        const avatar = sim.avatars.get(1);
        avatar.snowballCount = 0;

        applyTick([event(SNOWWAR_EVENT_MACHINE_TRANSFER, 1, 61)]);
        expect(avatar.snowballCount).toBe(1);
        expect(sim.piles.get('4,6').snowballCount).toBe(11);
    });

    it('does not instantly fast-forward a normal jitter backlog (no teleport)', () =>
    {
        // Four AIR turns bunched together remain below the memory backstop and
        // drain smoothly instead of teleporting a walking avatar.
        for (let i = 0; i < 4; i++)
        {
            sim.queueGameStatus(Array.from({ length: SUBTURNS_PER_TICK }, () => []));
        }
        expect(sim.subturnCount).toBe(0);
    });

    it('hard fast-forwards a runaway backlog, bounding memory and standing delay', () =>
    {
        // 21 AIR turns = 63 subturns; the backstop caps the buffer at 18
        // (0.9s of standing delay), so the 45 oldest drain synchronously.
        for (let i = 0; i < 21; i++)
        {
            sim.queueGameStatus(Array.from({ length: SUBTURNS_PER_TICK }, () => []));
        }
        expect(sim.subturnCount).toBe(45);
    });

    const emptyTurn = () => Array.from({ length: SUBTURNS_PER_TICK }, (): SnowWarSimEvent[] => []);

    it('collapses the backlog in a hidden tab even with a snowball in flight', () =>
    {
        sim.isPageHidden = () => true;
        sim.queueGameStatus([[event(SNOWWAR_EVENT_LAUNCH_SNOWBALL, 99, 1, tileToWorld(10), tileToWorld(5), 2)], [], []]);
        for (let i = 0; i < 20; i++) sim.queueGameStatus(emptyTurn());

        // 63 subturns queued, the buffer is capped at 18.
        expect(sim.subturnCount).toBe(45);
    });

    it('keeps a projectile backlog while visible and skips it on catchUp', () =>
    {
        sim.isPageHidden = () => false;
        sim.queueGameStatus([[event(SNOWWAR_EVENT_LAUNCH_SNOWBALL, 99, 1, tileToWorld(10), tileToWorld(5), 2)], [], []]);
        for (let i = 0; i < 20; i++) sim.queueGameStatus(emptyTurn());
        const before = sim.subturnCount;
        expect(before).toBeLessThan(45);

        sim.catchUp();

        expect(sim.subturnCount).toBe(63 - 2);
        expect(sim.drainImpacts()).toEqual([]);
    });

    it('removes a leaver only after the subturns buffered before the exit', () =>
    {
        sim.queueGameStatus([[event(SNOWWAR_EVENT_MOVE, 1, tileToWorld(6), tileToWorld(5))], [], []]);
        sim.removeAvatarAfterBuffer(1);
        expect(sim.avatars.has(1)).toBe(true);

        sim.queueGameStatus(emptyTurn());
        let guard = 0;
        while (sim.subturnCount < SUBTURNS_PER_TICK - 1 && guard++ < 100)
        {
            clock += SUBTURN_MS;
            sim.update(clock);
        }
        expect(sim.avatars.has(1)).toBe(true);

        while (sim.subturnCount < SUBTURNS_PER_TICK && guard++ < 100)
        {
            clock += SUBTURN_MS;
            sim.update(clock);
        }
        expect(sim.avatars.has(1)).toBe(false);
        expect(sim.avatars.has(2)).toBe(true);
    });

    it('removes a leaver at once when nothing is buffered', () =>
    {
        sim.removeAvatarAfterBuffer(2);
        expect(sim.avatars.has(2)).toBe(false);
    });
});
15Sujets
1Dépôts
20Membres
2En ligne
Profile
Inscrit
Dernière visite
Messages
0
Votes positifs
0
Votes négatifs
0

Nous utilisons des cookies pour assurer le bon fonctionnement du site et améliorer votre expérience. en savoir plus

Autoriser