| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107 |
- import { describe, expect, it } from 'vitest';
- import { listMapIds, loadCharacterStats, loadMap } from '../src/server/assets';
- import { Game } from '../src/server/game';
- import { isModifierId } from '../src/server/modifiers';
- import { defaultMap, lastPlaced, rotationPool, sanitizeRotation } from '../src/server/rotation';
- import { TICK_RATE } from '../src/shared/constants';
- import { Rng } from '../src/shared/rng';
- import type { MapSummary } from '../src/shared/protocol';
- import { ASSETS, realMaterials } from './helpers';
- const materials = realMaterials();
- const catalog: MapSummary[] = [
- { id: 'a', name: 'A' },
- { id: 'b', name: 'B' },
- { id: 'c', name: 'C' },
- ];
- describe('built-in maps', () => {
- it('all load, validate, and only use known modifiers', () => {
- const ids = listMapIds(ASSETS);
- expect(ids).toEqual(expect.arrayContaining(['gateau-etages', 'mille-feuille', 'piece-montee']));
- for (const id of ids) {
- const { definition, terrain } = loadMap(ASSETS, id, materials);
- expect(terrain.width).toBe(definition.size[0]);
- for (const m of definition.modifiers) expect(isModifierId(m), `${id}: ${m}`).toBe(true);
- }
- });
- it('la pièce montée: removing a chou drops the column above it', () => {
- const { definition, terrain } = loadMap(ASSETS, 'piece-montee', materials);
- const game = new Game({ map: definition, terrain, materials, seed: 1, loadCharacter: (id) => loadCharacterStats(ASSETS, id) });
- // Bottom row, centre chou: centre (1024, 905 - 42).
- game.queueOp({ kind: 'carve', x: 1024, y: 863, r: 44, power: 99 });
- const res = game.step();
- const heights = res.terrain.flatMap((e) => (e.ev.kind === 'detach' && !e.ev.debris ? [e.ev.h] : []));
- expect(heights.length).toBeGreaterThanOrEqual(1);
- expect(Math.max(...heights)).toBeGreaterThan(300); // the whole column, not just one chou
- });
- });
- describe('rotation', () => {
- it('pool: the selection when valid, every map otherwise', () => {
- expect(rotationPool({ mode: 'all', selection: ['b'] }, catalog)).toEqual(catalog);
- expect(rotationPool({ mode: 'selection', selection: ['c', 'a'] }, catalog).map((m) => m.id)).toEqual(['a', 'c']);
- expect(rotationPool({ mode: 'selection', selection: ['zzz'] }, catalog)).toEqual(catalog);
- expect(sanitizeRotation({ mode: 'selection', selection: ['a', 'zzz', 'a'] }, catalog)).toEqual({ mode: 'selection', selection: ['a'] });
- });
- it('default map: in order, or seeded random', () => {
- const rng = new Rng(1);
- expect([1, 2, 3, 4].map((r) => defaultMap({ mode: 'all', selection: [] }, catalog, r, rng))).toEqual(['a', 'b', 'c', 'a']);
- const pick = (): string[] => [1, 2, 3, 4, 5].map((r) => defaultMap({ mode: 'random', selection: [] }, catalog, r, new Rng(9 + r)));
- expect(pick()).toEqual(pick());
- });
- it('the last-placed player picks', () => {
- expect(
- lastPlaced([
- { id: 1, name: '', characterId: '', roundScore: 5, total: 20 },
- { id: 2, name: '', characterId: '', roundScore: 1, total: 3 },
- { id: 3, name: '', characterId: '', roundScore: 9, total: 3 },
- ]),
- ).toBe(2);
- expect(lastPlaced([])).toBeNull();
- });
- });
- describe('modifiers', () => {
- function round(mapId: string, seed = 3): Game {
- const { definition, terrain } = loadMap(ASSETS, mapId, materials);
- return new Game({ map: definition, terrain, materials, seed, rules: { duration: 180 }, loadCharacter: (id) => loadCharacterStats(ASSETS, id) });
- }
- it('wind blows players sideways (piece-montee)', () => {
- const game = round('piece-montee');
- const { id } = game.addPlayer('Plume', 'generic');
- game.teleport(id, 300, 300);
- let maxWind = 0;
- const x0 = game.snapshot()[0].x;
- let drift = 0;
- for (let i = 0; i < 12 * TICK_RATE; i++) {
- game.step();
- maxWind = Math.max(maxWind, Math.abs(game.roundState().wind));
- if (i === 30) drift = game.snapshot().find((p) => p.id === id)!.x - x0;
- }
- expect(maxWind).toBeGreaterThan(50);
- expect(Math.abs(drift)).toBeGreaterThan(0);
- });
- it('random cracks open in the brittle pastry over time (mille-feuille), deterministically', () => {
- const a = round('mille-feuille', 4);
- const b = round('mille-feuille', 4);
- for (let i = 0; i < 10 * TICK_RATE; i++) {
- a.step();
- b.step();
- }
- expect(a.events.length).toBeGreaterThan(10);
- expect(a.terrain.hash()).toBe(b.terrain.hash());
- });
- it('modifiers are off during the warm-up', () => {
- const { definition, terrain } = loadMap(ASSETS, 'mille-feuille', materials);
- const warm = new Game({ map: definition, terrain, materials, seed: 4, loadCharacter: (id) => loadCharacterStats(ASSETS, id) });
- for (let i = 0; i < 10 * TICK_RATE; i++) warm.step();
- expect(warm.events).toHaveLength(0);
- });
- });
|