Skip to content

Commit 82591fd

Browse files
committed
feat(stdlib): ecs.mcrs — Entity Component System (health + velocity + registry)
1 parent 25a929c commit 82591fd

2 files changed

Lines changed: 609 additions & 0 deletions

File tree

Lines changed: 371 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,371 @@
1+
/**
2+
* End-to-end tests for ecs.mcrs — Entity Component System stdlib.
3+
*
4+
* Tests health component, velocity component, and registry operations by
5+
* compiling with librarySources, loading into MCRuntime, and asserting values.
6+
*/
7+
8+
import * as fs from 'fs'
9+
import * as path from 'path'
10+
import { compile } from '../../emit/compile'
11+
import { MCRuntime } from '../../runtime'
12+
13+
// ---------------------------------------------------------------------------
14+
// Constants
15+
// ---------------------------------------------------------------------------
16+
17+
const NS = 'test'
18+
const OBJ = `__${NS}`
19+
20+
const MATH_SRC = fs.readFileSync(
21+
path.join(__dirname, '../../stdlib/math.mcrs'),
22+
'utf-8',
23+
)
24+
const ECS_SRC = fs.readFileSync(
25+
path.join(__dirname, '../../stdlib/ecs.mcrs'),
26+
'utf-8',
27+
)
28+
29+
// ---------------------------------------------------------------------------
30+
// Helpers
31+
// ---------------------------------------------------------------------------
32+
33+
function makeRuntime(source: string, libs: string[] = [MATH_SRC, ECS_SRC]): MCRuntime {
34+
const result = compile(source, { namespace: NS, librarySources: libs })
35+
const rt = new MCRuntime(NS)
36+
for (const file of result.files) {
37+
if (!file.path.endsWith('.mcfunction')) continue
38+
const m = file.path.match(/data\/([^/]+)\/function\/(.+)\.mcfunction$/)
39+
if (!m) continue
40+
rt.loadFunction(`${m[1]}:${m[2]}`, file.content.split('\n'))
41+
}
42+
rt.execFunction(`${NS}:load`)
43+
return rt
44+
}
45+
46+
function callAndGetRet(rt: MCRuntime, fnName: string): number {
47+
rt.execFunction(`${NS}:${fnName}`)
48+
return rt.getScore('$ret', OBJ)
49+
}
50+
51+
// ---------------------------------------------------------------------------
52+
// Health Component Tests
53+
// ---------------------------------------------------------------------------
54+
55+
describe('ecs.mcrs — Health Component', () => {
56+
const rt = makeRuntime(`
57+
fn test_health_init_entity(): int {
58+
let s: int[] = ecs_health_init(42, 100)
59+
return s[0]
60+
}
61+
62+
fn test_health_init_current(): int {
63+
let s: int[] = ecs_health_init(42, 100)
64+
return s[1]
65+
}
66+
67+
fn test_health_init_max(): int {
68+
let s: int[] = ecs_health_init(42, 100)
69+
return s[2]
70+
}
71+
72+
fn test_health_get(): int {
73+
let s: int[] = ecs_health_init(1, 80)
74+
return ecs_health_get(s)
75+
}
76+
77+
fn test_health_max(): int {
78+
let s: int[] = ecs_health_init(1, 80)
79+
return ecs_health_max(s)
80+
}
81+
82+
fn test_health_damage(): int {
83+
let s: int[] = ecs_health_init(1, 100)
84+
s = ecs_health_damage(s, 30)
85+
return ecs_health_get(s)
86+
}
87+
88+
fn test_health_damage_clamp(): int {
89+
let s: int[] = ecs_health_init(1, 100)
90+
s = ecs_health_damage(s, 200)
91+
return ecs_health_get(s)
92+
}
93+
94+
fn test_health_heal(): int {
95+
let s: int[] = ecs_health_init(1, 100)
96+
s = ecs_health_damage(s, 50)
97+
s = ecs_health_heal(s, 20)
98+
return ecs_health_get(s)
99+
}
100+
101+
fn test_health_heal_clamp(): int {
102+
let s: int[] = ecs_health_init(1, 100)
103+
s = ecs_health_damage(s, 10)
104+
s = ecs_health_heal(s, 50)
105+
return ecs_health_get(s)
106+
}
107+
108+
fn test_health_is_dead_alive(): int {
109+
let s: int[] = ecs_health_init(1, 100)
110+
return ecs_health_is_dead(s)
111+
}
112+
113+
fn test_health_is_dead_dead(): int {
114+
let s: int[] = ecs_health_init(1, 100)
115+
s = ecs_health_damage(s, 100)
116+
return ecs_health_is_dead(s)
117+
}
118+
119+
fn test_health_is_dead_overdamage(): int {
120+
let s: int[] = ecs_health_init(1, 100)
121+
s = ecs_health_damage(s, 500)
122+
return ecs_health_is_dead(s)
123+
}
124+
125+
fn test_health_pct_full(): int {
126+
let s: int[] = ecs_health_init(1, 100)
127+
return ecs_health_pct(s)
128+
}
129+
130+
fn test_health_pct_half(): int {
131+
let s: int[] = ecs_health_init(1, 100)
132+
s = ecs_health_damage(s, 50)
133+
return ecs_health_pct(s)
134+
}
135+
136+
fn test_health_pct_zero(): int {
137+
let s: int[] = ecs_health_init(1, 100)
138+
s = ecs_health_damage(s, 100)
139+
return ecs_health_pct(s)
140+
}
141+
142+
fn test_health_set(): int {
143+
let s: int[] = ecs_health_init(1, 100)
144+
s = ecs_health_set(s, 75)
145+
return ecs_health_get(s)
146+
}
147+
148+
fn test_health_lifecycle(): int {
149+
// init(42, 100) -> damage(30) -> heal(10) -> HP should be 80
150+
let s: int[] = ecs_health_init(42, 100)
151+
s = ecs_health_damage(s, 30)
152+
s = ecs_health_heal(s, 10)
153+
return ecs_health_get(s)
154+
}
155+
`)
156+
157+
// --- init ---
158+
test('init: state[0] == entity_score', () =>
159+
expect(callAndGetRet(rt, 'test_health_init_entity')).toBe(42))
160+
161+
test('init: state[1] == max_hp (starts full)', () =>
162+
expect(callAndGetRet(rt, 'test_health_init_current')).toBe(100))
163+
164+
test('init: state[2] == max_hp', () =>
165+
expect(callAndGetRet(rt, 'test_health_init_max')).toBe(100))
166+
167+
// --- getters ---
168+
test('ecs_health_get returns current HP', () =>
169+
expect(callAndGetRet(rt, 'test_health_get')).toBe(80))
170+
171+
test('ecs_health_max returns max HP', () =>
172+
expect(callAndGetRet(rt, 'test_health_max')).toBe(80))
173+
174+
// --- damage ---
175+
test('damage: HP decreases correctly (100 - 30 = 70)', () =>
176+
expect(callAndGetRet(rt, 'test_health_damage')).toBe(70))
177+
178+
test('damage: clamps to 0 on overkill', () =>
179+
expect(callAndGetRet(rt, 'test_health_damage_clamp')).toBe(0))
180+
181+
// --- heal ---
182+
test('heal: HP increases correctly (50 dmg then +20 = 70)', () =>
183+
expect(callAndGetRet(rt, 'test_health_heal')).toBe(70))
184+
185+
test('heal: clamps to max HP', () =>
186+
expect(callAndGetRet(rt, 'test_health_heal_clamp')).toBe(100))
187+
188+
// --- is_dead ---
189+
test('is_dead: 0 when HP > 0', () =>
190+
expect(callAndGetRet(rt, 'test_health_is_dead_alive')).toBe(0))
191+
192+
test('is_dead: 1 when HP == 0', () =>
193+
expect(callAndGetRet(rt, 'test_health_is_dead_dead')).toBe(1))
194+
195+
test('is_dead: 1 when overdamaged (clamped to 0)', () =>
196+
expect(callAndGetRet(rt, 'test_health_is_dead_overdamage')).toBe(1))
197+
198+
// --- pct ---
199+
test('pct: 100/100 HP -> 10000', () =>
200+
expect(callAndGetRet(rt, 'test_health_pct_full')).toBe(10000))
201+
202+
test('pct: 50/100 HP -> 5000', () =>
203+
expect(callAndGetRet(rt, 'test_health_pct_half')).toBe(5000))
204+
205+
test('pct: 0/100 HP -> 0', () =>
206+
expect(callAndGetRet(rt, 'test_health_pct_zero')).toBe(0))
207+
208+
// --- set ---
209+
test('ecs_health_set: sets HP to value', () =>
210+
expect(callAndGetRet(rt, 'test_health_set')).toBe(75))
211+
212+
// --- lifecycle ---
213+
test('lifecycle: init(42,100) -> damage(30) -> heal(10) -> HP == 80', () =>
214+
expect(callAndGetRet(rt, 'test_health_lifecycle')).toBe(80))
215+
})
216+
217+
// ---------------------------------------------------------------------------
218+
// Velocity Component Tests
219+
// ---------------------------------------------------------------------------
220+
221+
describe('ecs.mcrs — Velocity Component', () => {
222+
const rt = makeRuntime(`
223+
fn test_vel_init_x(): int {
224+
let s: int[] = ecs_vel_init(1000, 2000, 3000)
225+
return ecs_vel_get_x(s)
226+
}
227+
228+
fn test_vel_init_y(): int {
229+
let s: int[] = ecs_vel_init(1000, 2000, 3000)
230+
return ecs_vel_get_y(s)
231+
}
232+
233+
fn test_vel_init_z(): int {
234+
let s: int[] = ecs_vel_init(1000, 2000, 3000)
235+
return ecs_vel_get_z(s)
236+
}
237+
238+
fn test_vel_set(): int {
239+
let s: int[] = ecs_vel_init(0, 0, 0)
240+
s = ecs_vel_set(s, 500, 1500, 2500)
241+
return ecs_vel_get_y(s)
242+
}
243+
244+
fn test_vel_gravity(): int {
245+
// Apply gravity (980) to vy=2000 -> 2000 - 980 = 1020
246+
let s: int[] = ecs_vel_init(0, 2000, 0)
247+
s = ecs_vel_apply_gravity(s, 980)
248+
return ecs_vel_get_y(s)
249+
}
250+
251+
fn test_vel_gravity_negative(): int {
252+
// vy=0, apply gravity 980 -> -980
253+
let s: int[] = ecs_vel_init(0, 0, 0)
254+
s = ecs_vel_apply_gravity(s, 980)
255+
return ecs_vel_get_y(s)
256+
}
257+
258+
fn test_vel_speed_3_4_0(): int {
259+
// (3000, 4000, 0) -> speed == 5000
260+
let s: int[] = ecs_vel_init(3000, 4000, 0)
261+
return ecs_vel_speed(s)
262+
}
263+
264+
fn test_vel_damp(): int {
265+
// vx=10000, damp factor=5000 (0.5) -> 5000
266+
let s: int[] = ecs_vel_init(10000, 0, 0)
267+
s = ecs_vel_damp(s, 5000)
268+
return ecs_vel_get_x(s)
269+
}
270+
`)
271+
272+
test('vel_init: get_x correct', () =>
273+
expect(callAndGetRet(rt, 'test_vel_init_x')).toBe(1000))
274+
275+
test('vel_init: get_y correct', () =>
276+
expect(callAndGetRet(rt, 'test_vel_init_y')).toBe(2000))
277+
278+
test('vel_init: get_z correct', () =>
279+
expect(callAndGetRet(rt, 'test_vel_init_z')).toBe(3000))
280+
281+
test('vel_set: updates velocity correctly', () =>
282+
expect(callAndGetRet(rt, 'test_vel_set')).toBe(1500))
283+
284+
test('apply_gravity: vy decreases by gravity_fx', () =>
285+
expect(callAndGetRet(rt, 'test_vel_gravity')).toBe(1020))
286+
287+
test('apply_gravity: vy goes negative when at 0', () =>
288+
expect(callAndGetRet(rt, 'test_vel_gravity_negative')).toBe(-980))
289+
290+
test('vel_speed: (3000, 4000, 0) -> 5000', () =>
291+
expect(callAndGetRet(rt, 'test_vel_speed_3_4_0')).toBe(5000))
292+
293+
test('vel_damp: 10000 × 0.5 = 5000', () =>
294+
expect(callAndGetRet(rt, 'test_vel_damp')).toBe(5000))
295+
})
296+
297+
// ---------------------------------------------------------------------------
298+
// ECS Registry Tests
299+
// ---------------------------------------------------------------------------
300+
301+
describe('ecs.mcrs — Registry', () => {
302+
const rt = makeRuntime(`
303+
fn test_registry_empty(): int {
304+
let reg: int[] = ecs_registry_new()
305+
return ecs_is_registered(reg, 1)
306+
}
307+
308+
fn test_registry_register_health(): int {
309+
let reg: int[] = ecs_registry_new()
310+
reg = ecs_register(reg, 1)
311+
return ecs_is_registered(reg, 1)
312+
}
313+
314+
fn test_registry_register_velocity(): int {
315+
let reg: int[] = ecs_registry_new()
316+
reg = ecs_register(reg, 2)
317+
return ecs_is_registered(reg, 2)
318+
}
319+
320+
fn test_registry_no_cross_contamination(): int {
321+
// Registering comp 1 should NOT affect comp 2
322+
let reg: int[] = ecs_registry_new()
323+
reg = ecs_register(reg, 1)
324+
return ecs_is_registered(reg, 2)
325+
}
326+
327+
fn test_registry_multi(): int {
328+
let reg: int[] = ecs_registry_new()
329+
reg = ecs_register(reg, 1)
330+
reg = ecs_register(reg, 2)
331+
reg = ecs_register(reg, 3)
332+
// All three registered
333+
let a: int = ecs_is_registered(reg, 1)
334+
let b: int = ecs_is_registered(reg, 2)
335+
let c: int = ecs_is_registered(reg, 3)
336+
return a + b + c
337+
}
338+
339+
fn test_registry_predefined_ids(): int {
340+
// ECS_COMP_HEALTH=1, ECS_COMP_VELOCITY=2, ECS_COMP_DAMAGE=3
341+
// Constants are defined in ecs.mcrs; register all three and verify
342+
let reg: int[] = ecs_registry_new()
343+
reg = ecs_register(reg, 1)
344+
reg = ecs_register(reg, 2)
345+
reg = ecs_register(reg, 3)
346+
let a: int = ecs_is_registered(reg, 1)
347+
let b: int = ecs_is_registered(reg, 2)
348+
let c: int = ecs_is_registered(reg, 3)
349+
// Also verify IDs are distinct (registering 1 does not register 2 or 3)
350+
return a * 100 + b * 10 + c
351+
}
352+
`)
353+
354+
test('registry: new registry has nothing registered', () =>
355+
expect(callAndGetRet(rt, 'test_registry_empty')).toBe(0))
356+
357+
test('registry: register health comp -> is_registered returns 1', () =>
358+
expect(callAndGetRet(rt, 'test_registry_register_health')).toBe(1))
359+
360+
test('registry: register velocity comp -> is_registered returns 1', () =>
361+
expect(callAndGetRet(rt, 'test_registry_register_velocity')).toBe(1))
362+
363+
test('registry: no cross-contamination between comp IDs', () =>
364+
expect(callAndGetRet(rt, 'test_registry_no_cross_contamination')).toBe(0))
365+
366+
test('registry: register multiple comps, all found', () =>
367+
expect(callAndGetRet(rt, 'test_registry_multi')).toBe(3))
368+
369+
test('registry: predefined component IDs 1,2,3 are independently registerable', () =>
370+
expect(callAndGetRet(rt, 'test_registry_predefined_ids')).toBe(111))
371+
})

0 commit comments

Comments
 (0)