maps.test.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. import { describe, expect, it } from 'vitest';
  2. import { listMapIds, loadCharacterStats, loadMap } from '../src/server/assets';
  3. import { Game } from '../src/server/game';
  4. import { isModifierId } from '../src/server/modifiers';
  5. import { defaultMap, lastPlaced, rotationPool, sanitizeRotation } from '../src/server/rotation';
  6. import { TICK_RATE } from '../src/shared/constants';
  7. import { Rng } from '../src/shared/rng';
  8. import type { MapSummary } from '../src/shared/protocol';
  9. import { ASSETS, realMaterials } from './helpers';
  10. const materials = realMaterials();
  11. const catalog: MapSummary[] = [
  12. { id: 'a', name: 'A' },
  13. { id: 'b', name: 'B' },
  14. { id: 'c', name: 'C' },
  15. ];
  16. describe('built-in maps', () => {
  17. it('all load, validate, and only use known modifiers', () => {
  18. const ids = listMapIds(ASSETS);
  19. expect(ids).toEqual(expect.arrayContaining(['gateau-etages', 'mille-feuille', 'piece-montee', 'roule', 'tarte-fruits', 'crepes']));
  20. for (const id of ids) {
  21. const { definition, terrain } = loadMap(ASSETS, id, materials);
  22. expect(terrain.width).toBe(definition.size[0]);
  23. for (const m of definition.modifiers) expect(isModifierId(m), `${id}: ${m}`).toBe(true);
  24. }
  25. });
  26. it('la pièce montée: removing a chou drops the column above it', () => {
  27. const { definition, terrain } = loadMap(ASSETS, 'piece-montee', materials);
  28. const game = new Game({ map: definition, terrain, materials, seed: 1, loadCharacter: (id) => loadCharacterStats(ASSETS, id) });
  29. // Bottom row, centre chou: centre (1024, 905 - 42).
  30. game.queueOp({ kind: 'carve', x: 1024, y: 863, r: 44, power: 99 });
  31. const res = game.step();
  32. const heights = res.terrain.flatMap((e) => (e.ev.kind === 'detach' && !e.ev.debris ? [e.ev.h] : []));
  33. expect(heights.length).toBeGreaterThanOrEqual(1);
  34. expect(Math.max(...heights)).toBeGreaterThan(300); // the whole column, not just one chou
  35. });
  36. });
  37. describe('le roulé', () => {
  38. it('only its biscuit stand holds it: saw through the stand and the whole roll comes loose', () => {
  39. const { definition, terrain } = loadMap(ASSETS, 'roule', materials);
  40. const game = new Game({ map: definition, terrain, materials, seed: 1, loadCharacter: (id) => loadCharacterStats(ASSETS, id) });
  41. for (let x = 836; x <= 1214; x += 10) game.queueOp({ kind: 'carve', x, y: 930, r: 32, power: 99 });
  42. const res = game.step();
  43. const freed = res.terrain.flatMap((e) => (e.ev.kind === 'detach' && !e.ev.debris ? [e.ev.w] : []));
  44. expect(Math.max(0, ...freed)).toBeGreaterThan(600); // the whole roll, ~660 px wide
  45. expect(game.chunkStates().length).toBeGreaterThanOrEqual(1);
  46. });
  47. });
  48. describe('rotation', () => {
  49. it('pool: the selection when valid, every map otherwise', () => {
  50. expect(rotationPool({ mode: 'all', selection: ['b'] }, catalog)).toEqual(catalog);
  51. expect(rotationPool({ mode: 'selection', selection: ['c', 'a'] }, catalog).map((m) => m.id)).toEqual(['a', 'c']);
  52. expect(rotationPool({ mode: 'selection', selection: ['zzz'] }, catalog)).toEqual(catalog);
  53. expect(sanitizeRotation({ mode: 'selection', selection: ['a', 'zzz', 'a'] }, catalog)).toEqual({ mode: 'selection', selection: ['a'] });
  54. });
  55. it('default map: in order, or seeded random', () => {
  56. const rng = new Rng(1);
  57. expect([1, 2, 3, 4].map((r) => defaultMap({ mode: 'all', selection: [] }, catalog, r, rng))).toEqual(['a', 'b', 'c', 'a']);
  58. const pick = (): string[] => [1, 2, 3, 4, 5].map((r) => defaultMap({ mode: 'random', selection: [] }, catalog, r, new Rng(9 + r)));
  59. expect(pick()).toEqual(pick());
  60. });
  61. it('the last-placed player picks', () => {
  62. expect(
  63. lastPlaced([
  64. { id: 1, name: '', characterId: '', roundScore: 5, total: 20 },
  65. { id: 2, name: '', characterId: '', roundScore: 1, total: 3 },
  66. { id: 3, name: '', characterId: '', roundScore: 9, total: 3 },
  67. ]),
  68. ).toBe(2);
  69. expect(lastPlaced([])).toBeNull();
  70. });
  71. });
  72. describe('modifiers', () => {
  73. function round(mapId: string, seed = 3): Game {
  74. const { definition, terrain } = loadMap(ASSETS, mapId, materials);
  75. return new Game({ map: definition, terrain, materials, seed, rules: { duration: 180 }, loadCharacter: (id) => loadCharacterStats(ASSETS, id) });
  76. }
  77. it('wind blows players sideways (crepes)', () => {
  78. const game = round('crepes');
  79. const { id } = game.addPlayer('Plume', 'generic');
  80. game.teleport(id, 300, 300);
  81. let maxWind = 0;
  82. const x0 = game.snapshot()[0].x;
  83. let drift = 0;
  84. for (let i = 0; i < 12 * TICK_RATE; i++) {
  85. game.step();
  86. maxWind = Math.max(maxWind, Math.abs(game.roundState().wind));
  87. if (i === 30) drift = game.snapshot().find((p) => p.id === id)!.x - x0;
  88. }
  89. expect(maxWind).toBeGreaterThan(50);
  90. expect(Math.abs(drift)).toBeGreaterThan(0);
  91. });
  92. it('random cracks open in the brittle shortcrust over time (tarte-fruits), deterministically', () => {
  93. const a = round('tarte-fruits', 4);
  94. const b = round('tarte-fruits', 4);
  95. for (let i = 0; i < 10 * TICK_RATE; i++) {
  96. a.step();
  97. b.step();
  98. }
  99. expect(a.events.length).toBeGreaterThan(10);
  100. expect(a.terrain.hash()).toBe(b.terrain.hash());
  101. });
  102. it('modifiers are off during the warm-up', () => {
  103. const { definition, terrain } = loadMap(ASSETS, 'tarte-fruits', materials);
  104. const warm = new Game({ map: definition, terrain, materials, seed: 4, loadCharacter: (id) => loadCharacterStats(ASSETS, id) });
  105. for (let i = 0; i < 10 * TICK_RATE; i++) warm.step();
  106. expect(warm.events).toHaveLength(0);
  107. });
  108. });