vimp-engine 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/bin/vimp-sim.js +137 -0
  2. package/package.json +10 -3
  3. package/src/config/gameCodes.js +8 -0
  4. package/src/config/lobby.js +23 -1
  5. package/src/config/master.js +8 -0
  6. package/src/config/opcodes.js +3 -1
  7. package/src/devtools/RecordingSocketManager.js +118 -0
  8. package/src/devtools/ScenarioRunner.js +529 -0
  9. package/src/devtools/VirtualClient.js +363 -0
  10. package/src/devtools/VirtualClock.js +169 -0
  11. package/src/devtools/inspectHost.js +31 -0
  12. package/src/devtools/invariants.js +672 -0
  13. package/src/devtools/pluginLoader.js +101 -0
  14. package/src/devtools/report.js +202 -0
  15. package/src/devtools/resetHostSingletons.js +23 -0
  16. package/src/host/DebugRecorder.js +224 -0
  17. package/src/host/GameCoreAdapter.js +17 -0
  18. package/src/host/HostGame.js +129 -7
  19. package/src/host/host.worker.js +46 -47
  20. package/src/host/meta/SocketManager.js +15 -7
  21. package/src/host/meta/modules/Panel.js +7 -0
  22. package/src/host/meta/modules/PlayerDataSync.js +29 -5
  23. package/src/host/meta/modules/RTTManager.js +4 -2
  24. package/src/host/meta/modules/Stat.js +7 -0
  25. package/src/host/meta/modules/TimerManager.js +15 -7
  26. package/src/host/meta/modules/Vote.js +10 -1
  27. package/src/host/meta/modules/chat/Chat.js +7 -0
  28. package/src/host/meta/modules/chat/index.js +1 -1
  29. package/src/host/meta/player/HumanParticipant.js +2 -1
  30. package/src/lib/AbstractTimer.js +6 -4
  31. package/src/lib/applyRoomOverrides.js +6 -0
  32. package/src/lib/clientCoreConfig.js +4 -0
  33. package/src/lib/clock.js +53 -0
  34. package/src/lib/createHostRuntime.js +108 -0
  35. package/src/lib/reconstructHot.js +78 -0
  36. package/src/lib/validators.js +10 -0
  37. package/tests/fixtures/miniGame/client/fakeClientCore.js +304 -0
  38. package/tests/fixtures/miniGame/client/index.js +37 -0
  39. package/tests/fixtures/miniGame/client/parts/Actor.js +18 -0
  40. package/tests/fixtures/miniGame/client/parts/ActorRadar.js +7 -0
  41. package/tests/fixtures/miniGame/config/auth.js +41 -0
  42. package/tests/fixtures/miniGame/config/client.js +139 -0
  43. package/tests/fixtures/miniGame/config/game.js +148 -0
  44. package/tests/fixtures/miniGame/host/ScriptedManager.js +129 -0
  45. package/tests/fixtures/miniGame/host/createModules.js +6 -0
  46. package/tests/fixtures/miniGame/host/fakeCore.js +233 -0
  47. package/tests/fixtures/miniGame/host/index.js +29 -0
  48. package/tests/fixtures/miniGame/host/spawnCommand.js +14 -0
  49. package/tests/fixtures/miniGame/host/systemMessages.js +6 -0
  50. package/tests/fixtures/miniGame.contract.test.js +76 -0
@@ -0,0 +1,233 @@
1
+ // Фейковое JS-ядро миниигры-фикстуры (Этап 7 плана отделения движка,
2
+ // PLAN.md §4/Wasm Host ABI §3.4): реализует полную поверхность методов,
3
+ // которую вызывает GameCoreAdapter, без Rust/WASM — доказательство, что
4
+ // движок не завязан на конкретную реализацию ядра. Актёры — плоские
5
+ // объекты { x, y, angle, team, alive }; тик — тривиальная линейная
6
+ // интеграция скорости (та же идея, что у Rust-фикстуры TestClient,
7
+ // core/src/client/game.rs: форма ABI важнее физики).
8
+ export default class FakeGameCore {
9
+ constructor(configJson) {
10
+ this._config = JSON.parse(configJson);
11
+ this._actors = new Map(); // gameId -> { x, y, angle, team, alive, vx, vy, lastInputSeq }
12
+ this._map = null;
13
+ this._events = [];
14
+ this._lastBody = [];
15
+ this._lastFrame = null;
16
+ }
17
+
18
+ // ***** карта ***** //
19
+
20
+ load_map(mapJson) {
21
+ this._map = JSON.parse(mapJson);
22
+ }
23
+
24
+ map_info() {
25
+ if (!this._map) {
26
+ return 'null';
27
+ }
28
+
29
+ return JSON.stringify({
30
+ setId: this._map.setId,
31
+ respawns: this._map.respawns || {},
32
+ });
33
+ }
34
+
35
+ clear() {
36
+ this._actors.clear();
37
+ this._events.length = 0;
38
+ this._lastBody = [];
39
+ }
40
+
41
+ // ***** участники ***** //
42
+
43
+ spawn_actor(gameId, model, teamId, x, y, angle) {
44
+ this._actors.set(gameId, {
45
+ x,
46
+ y,
47
+ angle,
48
+ team: teamId,
49
+ alive: true,
50
+ vx: 0,
51
+ vy: 0,
52
+ lastInputSeq: 0,
53
+ });
54
+
55
+ this._events.push({ type: 'panelSet', id: gameId, field: 'energy', value: 100 });
56
+ }
57
+
58
+ remove_actor(gameId) {
59
+ this._actors.delete(gameId);
60
+ }
61
+
62
+ reset_actor(gameId, teamId, x, y, angle) {
63
+ const actor = this._actors.get(gameId);
64
+
65
+ if (actor) {
66
+ Object.assign(actor, { x, y, angle, team: teamId, alive: true, vx: 0, vy: 0 });
67
+ }
68
+ }
69
+
70
+ reset_all_vitals() {
71
+ for (const actor of this._actors.values()) {
72
+ actor.alive = true;
73
+ }
74
+ }
75
+
76
+ spawn_scripted_actor(gameId, model, teamId, x, y, angle) {
77
+ this.spawn_actor(gameId, model, teamId, x, y, angle);
78
+ this._actors.get(gameId).scripted = true;
79
+ }
80
+
81
+ remove_scripted_actor(gameId) {
82
+ this.remove_actor(gameId);
83
+ }
84
+
85
+ remove_players_and_shots() {
86
+ const names = [...this._actors.keys()].map(String);
87
+
88
+ this._actors.clear();
89
+
90
+ return JSON.stringify(names);
91
+ }
92
+
93
+ // ***** ввод ***** //
94
+
95
+ apply_input(gameId, seq, action, name) {
96
+ const actor = this._actors.get(gameId);
97
+
98
+ if (!actor) {
99
+ return;
100
+ }
101
+
102
+ actor.lastInputSeq = seq;
103
+
104
+ const magnitude = action === 'down' ? 40 : 0;
105
+
106
+ if (name === 'forward') {
107
+ actor.vy = -magnitude;
108
+ } else if (name === 'back') {
109
+ actor.vy = magnitude;
110
+ } else if (name === 'fire' && action === 'down') {
111
+ this._events.push({ type: 'custom', data: { kind: 'fire', id: gameId } });
112
+ }
113
+ }
114
+
115
+ last_input_seq(gameId) {
116
+ return this._actors.get(gameId)?.lastInputSeq ?? 0;
117
+ }
118
+
119
+ // ***** запросы состояния ***** //
120
+
121
+ is_alive(gameId) {
122
+ return Boolean(this._actors.get(gameId)?.alive);
123
+ }
124
+
125
+ position_of(gameId) {
126
+ const actor = this._actors.get(gameId);
127
+
128
+ return actor ? [actor.x, actor.y] : [];
129
+ }
130
+
131
+ players_data() {
132
+ return JSON.stringify(
133
+ [...this._actors.entries()].map(([id, a]) => ({
134
+ id: Number(id),
135
+ x: a.x,
136
+ y: a.y,
137
+ team: a.team,
138
+ })),
139
+ );
140
+ }
141
+
142
+ // ***** игровой тик ***** //
143
+
144
+ step(dt) {
145
+ for (const actor of this._actors.values()) {
146
+ actor.x += actor.vx * dt;
147
+ actor.y += actor.vy * dt;
148
+ }
149
+ }
150
+
151
+ take_events() {
152
+ const events = this._events;
153
+
154
+ this._events = [];
155
+
156
+ return JSON.stringify(events);
157
+ }
158
+
159
+ // ***** упаковка снапшота (фикстурный формат — не бинарный кодек ядра;
160
+ // достаточно для тестов меты, которые не декодируют реальный фрейминг) ***** //
161
+
162
+ // поля записи — по снапшот-схеме фикстуры (config/game.js, ключ a1):
163
+ // x, y, angle, team; клиентская половина фикстуры раскладывает их в
164
+ // hot-буфер той же ширины, что заявлена схемой
165
+ pack_body() {
166
+ this._lastBody = [...this._actors.entries()].map(([id, a]) => ({
167
+ id: Number(id),
168
+ x: a.x,
169
+ y: a.y,
170
+ angle: a.angle,
171
+ team: a.team,
172
+ }));
173
+ }
174
+
175
+ body_has_events() {
176
+ return false;
177
+ }
178
+
179
+ pack_frame(serverTime, seq, hasCamera, camX, camY, forceReset, shake, playerId) {
180
+ this._lastFrame = {
181
+ serverTime,
182
+ seq,
183
+ camera: hasCamera ? [camX, camY, Boolean(forceReset), shake ?? null] : null,
184
+ playerId,
185
+ body: this._lastBody,
186
+ };
187
+ }
188
+
189
+ frame_bytes() {
190
+ return new TextEncoder().encode(JSON.stringify(this._lastFrame));
191
+ }
192
+
193
+ // ***** отладка ***** //
194
+
195
+ // форма дампа — та же, что у движкового crate::debug (тела/карта/rng),
196
+ // с поправкой на фикстуру без физики: актёры и есть тела
197
+ debug_json() {
198
+ return JSON.stringify({
199
+ bodies: [...this._actors.entries()].map(([id, a]) => ({
200
+ handle: Number(id),
201
+ tag: 2,
202
+ userData: String(id),
203
+ translation: [a.x, a.y],
204
+ rotation: a.angle,
205
+ linvel: [a.vx, a.vy],
206
+ angvel: 0,
207
+ mass: 1,
208
+ bodyType: 'Dynamic',
209
+ ccd: false,
210
+ })),
211
+ colliders: [],
212
+ map: this._map
213
+ ? { setId: this._map.setId, step: this._map.step ?? null }
214
+ : null,
215
+ nav: null,
216
+ spatial: { cells: 0, entities: 0, cellCounts: [] },
217
+ rng: { state: '0' },
218
+ step: { timeStep: this._config.timeStep ?? null, accumulator: 0 },
219
+ });
220
+ }
221
+
222
+ // ***** handoff (задел, PLAN.md §6 «открытые вопросы») ***** //
223
+
224
+ serialize_state() {
225
+ return new TextEncoder().encode(JSON.stringify([...this._actors.entries()]));
226
+ }
227
+
228
+ deserialize_state(bytes) {
229
+ const entries = JSON.parse(new TextDecoder().decode(bytes));
230
+
231
+ this._actors = new Map(entries);
232
+ }
233
+ }
@@ -0,0 +1,29 @@
1
+ import { ENGINE_API_VERSION } from '../../../../src/config/opcodes.js';
2
+ import FakeGameCore from './fakeCore.js';
3
+ import gameConfig from '../config/game.js';
4
+ import authSchema from '../config/auth.js';
5
+ import clientConfig from '../config/client.js';
6
+ import systemMessages from './systemMessages.js';
7
+ import spawnCommand from './spawnCommand.js';
8
+ import createModules from './createModules.js';
9
+
10
+ // HostPlugin миниигры-фикстуры (Этап 7 плана отделения движка, PLAN.md
11
+ // §3.2): доказывает, что HostGame и движковая мета работают с любым
12
+ // HostPlugin, реализующим контракт — не только с @vimp-games/tanks. createCore
13
+ // не грузит WASM (fake-core — обычный JS-класс), поэтому фикстура не
14
+ // требует собранного Rust-ядра игры.
15
+ export default {
16
+ id: 'miniGame',
17
+ engineApi: ENGINE_API_VERSION,
18
+
19
+ async createCore(coreConfigJson) {
20
+ return new FakeGameCore(coreConfigJson);
21
+ },
22
+
23
+ gameConfig,
24
+ authSchema,
25
+ chatCommands: [spawnCommand],
26
+ systemMessages,
27
+ createModules,
28
+ buildClientGameConfig: () => clientConfig,
29
+ };
@@ -0,0 +1,14 @@
1
+ // Игровая чат-команда фикстуры: '/spawn <count>' — создаёт scripted-
2
+ // участников без голосования (зеркало vimp-tanks/src/host/botCommand.js,
3
+ // упрощённое — доказывает только регистрацию HostPlugin.chatCommands).
4
+ // Регистрируется в движковом CommandProcessor.
5
+ export default {
6
+ name: '/spawn',
7
+ handler(ctx, gameId, args) {
8
+ const count = Number(args[0]) || 1;
9
+ const created = ctx.scripted.createScripted(count);
10
+
11
+ ctx.chat.pushSystem('SCRIPTED_SPAWNED', [created]);
12
+ ctx.roundManager.initiateNewRound();
13
+ },
14
+ };
@@ -0,0 +1,6 @@
1
+ // Игровые коды системных сообщений фикстуры (группа g:*) — зеркало
2
+ // vimp-tanks/src/host/systemMessages.js (группа b:*). Merge в движковый
3
+ // реестр через registerCodes (HostGame конструктор).
4
+ export default {
5
+ SCRIPTED_SPAWNED: 'g:0', // {0} scripted participant(s) spawned
6
+ };
@@ -0,0 +1,76 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import hostPlugin from './miniGame/host/index.js';
3
+ import clientPlugin from './miniGame/client/index.js';
4
+ import { assertGameConfigShape } from '../../src/lib/gamePlugin.js';
5
+ import { buildCoreConfig } from '../../src/lib/coreConfig.js';
6
+ import { ENGINE_API_VERSION } from '../../src/config/opcodes.js';
7
+
8
+ // Доказательство «второй игры» (Этап 7 плана отделения движка): миниигра-
9
+ // фикстура удовлетворяет тем же контрактам (§3.2/§3.3 PLAN.md), что и
10
+ // @vimp-games/tanks, — без единого общего файла с игрой.
11
+ describe('miniGame fixture: HostPlugin/ClientPlugin contract', () => {
12
+ it('HostPlugin.engineApi совпадает с движковым', () => {
13
+ expect(hostPlugin.engineApi).toBe(ENGINE_API_VERSION);
14
+ });
15
+
16
+ it('gameConfig проходит движковую валидацию формы (§HostPlugin API)', () => {
17
+ expect(() => assertGameConfigShape(hostPlugin)).not.toThrow();
18
+ });
19
+
20
+ it('createCore возвращает объект с полной поверхностью Wasm Host ABI', async () => {
21
+ const core = await hostPlugin.createCore(JSON.stringify({ seed: 1 }));
22
+
23
+ const abiMethods = [
24
+ 'load_map',
25
+ 'map_info',
26
+ 'clear',
27
+ 'spawn_actor',
28
+ 'remove_actor',
29
+ 'reset_actor',
30
+ 'spawn_scripted_actor',
31
+ 'remove_scripted_actor',
32
+ 'remove_players_and_shots',
33
+ 'apply_input',
34
+ 'last_input_seq',
35
+ 'is_alive',
36
+ 'position_of',
37
+ 'players_data',
38
+ 'step',
39
+ 'take_events',
40
+ 'pack_body',
41
+ 'body_has_events',
42
+ 'pack_frame',
43
+ 'frame_bytes',
44
+ 'reset_all_vitals',
45
+ 'serialize_state',
46
+ 'deserialize_state',
47
+ ];
48
+
49
+ for (const method of abiMethods) {
50
+ expect(typeof core[method]).toBe('function');
51
+ }
52
+ });
53
+
54
+ it('gameConfig совместим с общим коллектором конфига ядра (buildCoreConfig)', () => {
55
+ const config = buildCoreConfig(hostPlugin.gameConfig, { seed: 1 });
56
+
57
+ expect(config.engine.mapScale).toBe(hostPlugin.gameConfig.mapScale);
58
+ expect(config.game.models).toBe(hostPlugin.gameConfig.parts.models);
59
+ // схема снапшота — игровая (Д1): ядро получает раскладку фикстуры
60
+ expect(config.engine.snapshot.keys).toBe(hostPlugin.gameConfig.snapshot);
61
+ });
62
+
63
+ it('ClientPlugin несёт минимум 1-2 заглушечных part и совпадающий engineApi', () => {
64
+ expect(clientPlugin.engineApi).toBe(hostPlugin.engineApi);
65
+ expect(Object.keys(clientPlugin.parts).length).toBeGreaterThanOrEqual(1);
66
+ expect(Object.keys(clientPlugin.parts).length).toBeLessThanOrEqual(2);
67
+ });
68
+
69
+ it('stat/panel схемы фикстуры отличаются от танков (одна играющая команда)', () => {
70
+ const teams = Object.keys(hostPlugin.gameConfig.teams).filter(
71
+ name => name !== hostPlugin.gameConfig.spectatorTeam,
72
+ );
73
+
74
+ expect(teams).toHaveLength(1);
75
+ });
76
+ });