@codehz/ecs 0.7.2 → 0.7.4

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 (81) hide show
  1. package/examples/advanced-scheduling.ts +96 -0
  2. package/examples/collision-detection.ts +229 -0
  3. package/examples/inventory-system-relations.ts +108 -0
  4. package/examples/parent-child-hierarchy.ts +206 -0
  5. package/examples/serialization.ts +337 -0
  6. package/examples/simple.ts +96 -0
  7. package/examples/spatial-grid.ts +276 -0
  8. package/examples/state-machine.ts +273 -0
  9. package/examples/tag-filtering.ts +266 -0
  10. package/package.json +60 -12
  11. package/src/__tests__/commands/buffer-limits.test.ts +72 -0
  12. package/src/__tests__/commands/buffer.test.ts +195 -0
  13. package/src/__tests__/component/singleton.test.ts +148 -0
  14. package/src/__tests__/core/archetype.test.ts +247 -0
  15. package/src/__tests__/core/bitset.test.ts +171 -0
  16. package/src/__tests__/core/changeset.test.ts +254 -0
  17. package/src/__tests__/core/multi-map.test.ts +74 -0
  18. package/src/__tests__/entity/component-registry.test.ts +66 -0
  19. package/src/__tests__/entity/entity.test.ts +520 -0
  20. package/src/__tests__/entity/id-manager.test.ts +157 -0
  21. package/src/__tests__/entity/id-system.test.ts +260 -0
  22. package/src/__tests__/perf/comprehensive.perf.test.ts +300 -0
  23. package/src/__tests__/perf/sync-hotpath.perf.test.ts +79 -0
  24. package/src/__tests__/query/basic.test.ts +341 -0
  25. package/src/__tests__/query/caching.test.ts +112 -0
  26. package/src/__tests__/query/filter.test.ts +111 -0
  27. package/src/__tests__/query/optional.test.ts +231 -0
  28. package/src/__tests__/query/perf.test.ts +99 -0
  29. package/src/__tests__/relations/dont-fragment/basic.test.ts +496 -0
  30. package/src/__tests__/relations/dont-fragment/query-notification.test.ts +125 -0
  31. package/src/__tests__/relations/wildcard.test.ts +179 -0
  32. package/src/__tests__/serialization/bounds.test.ts +237 -0
  33. package/src/__tests__/testing/assertions.test.ts +224 -0
  34. package/src/__tests__/testing/entity-builder.test.ts +84 -0
  35. package/src/__tests__/testing/snapshot.test.ts +150 -0
  36. package/src/__tests__/testing/world-fixture.test.ts +73 -0
  37. package/src/__tests__/world/component-hooks.test.ts +185 -0
  38. package/src/__tests__/world/component-management.test.ts +447 -0
  39. package/src/__tests__/world/entity-management.test.ts +86 -0
  40. package/src/__tests__/world/get-optional.test.ts +96 -0
  41. package/src/__tests__/world/multi-component-hooks.test.ts +502 -0
  42. package/src/__tests__/world/perf.test.ts +93 -0
  43. package/src/__tests__/world/query.test.ts +223 -0
  44. package/src/__tests__/world/serialize.test.ts +83 -0
  45. package/src/__tests__/world/wildcard-relation-hooks.test.ts +332 -0
  46. package/src/archetype/archetype.ts +472 -0
  47. package/src/archetype/helpers.ts +186 -0
  48. package/src/archetype/store.ts +33 -0
  49. package/src/commands/buffer.ts +110 -0
  50. package/src/commands/changeset.ts +104 -0
  51. package/src/component/entity-store.ts +223 -0
  52. package/src/component/registry.ts +657 -0
  53. package/src/component/type-utils.ts +9 -0
  54. package/src/entity/index.ts +63 -0
  55. package/src/entity/manager.ts +115 -0
  56. package/src/entity/relation.ts +319 -0
  57. package/src/entity/types.ts +135 -0
  58. package/src/index.ts +41 -0
  59. package/src/query/filter.ts +75 -0
  60. package/src/query/query.ts +313 -0
  61. package/src/query/registry.ts +101 -0
  62. package/src/storage/serialization.ts +130 -0
  63. package/src/testing/index.ts +634 -0
  64. package/src/types/index.ts +99 -0
  65. package/src/utils/bit-set.ts +133 -0
  66. package/src/utils/multi-map.ts +96 -0
  67. package/src/utils/utils.ts +19 -0
  68. package/src/world/builder.ts +100 -0
  69. package/src/world/commands.ts +378 -0
  70. package/src/world/hooks.ts +358 -0
  71. package/src/world/references.ts +38 -0
  72. package/src/world/serialization.ts +122 -0
  73. package/src/world/world.ts +1201 -0
  74. /package/{builder.d.mts → dist/builder.d.mts} +0 -0
  75. /package/{index.d.mts → dist/index.d.mts} +0 -0
  76. /package/{index.mjs → dist/index.mjs} +0 -0
  77. /package/{testing.d.mts → dist/testing.d.mts} +0 -0
  78. /package/{testing.mjs → dist/testing.mjs} +0 -0
  79. /package/{testing.mjs.map → dist/testing.mjs.map} +0 -0
  80. /package/{world.mjs → dist/world.mjs} +0 -0
  81. /package/{world.mjs.map → dist/world.mjs.map} +0 -0
@@ -0,0 +1,171 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { BitSet } from "../../utils/bit-set";
3
+
4
+ describe("BitSet word boundary tests", () => {
5
+ it("should setRange across word boundaries", () => {
6
+ const bitset = new BitSet(100);
7
+
8
+ // Set range crossing 32-bit boundary (30-35)
9
+ bitset.setRange(30, 35);
10
+
11
+ // Check that all bits in range are set
12
+ for (let i = 30; i <= 35; i++) {
13
+ expect(bitset.has(i)).toBe(true);
14
+ }
15
+
16
+ // Check boundary bits
17
+ expect(bitset.has(29)).toBe(false);
18
+ expect(bitset.has(36)).toBe(false);
19
+ });
20
+
21
+ it("should handle setRange at word boundary exactly", () => {
22
+ const bitset = new BitSet(100);
23
+
24
+ // Set range [0..31] = first word
25
+ bitset.setRange(0, 31);
26
+
27
+ for (let i = 0; i <= 31; i++) {
28
+ expect(bitset.has(i)).toBe(true);
29
+ }
30
+ expect(bitset.has(32)).toBe(false);
31
+ });
32
+
33
+ it("should handle setRange spanning multiple words", () => {
34
+ const bitset = new BitSet(200);
35
+
36
+ // Set range [20..80] spanning parts of 3 words
37
+ bitset.setRange(20, 80);
38
+
39
+ for (let i = 20; i <= 80; i++) {
40
+ expect(bitset.has(i)).toBe(true);
41
+ }
42
+ expect(bitset.has(19)).toBe(false);
43
+ expect(bitset.has(81)).toBe(false);
44
+ });
45
+
46
+ it("should anyClearInRange work across word boundaries", () => {
47
+ const bitset = new BitSet(100);
48
+
49
+ // Set all bits
50
+ bitset.setRange(0, 99);
51
+
52
+ // All bits are set, so anyClearInRange should return false
53
+ expect(bitset.anyClearInRange(30, 35)).toBe(false);
54
+ expect(bitset.anyClearInRange(0, 99)).toBe(false);
55
+
56
+ // Clear some bits crossing boundary
57
+ bitset.clear(31);
58
+ bitset.clear(32);
59
+
60
+ expect(bitset.anyClearInRange(30, 35)).toBe(true);
61
+ expect(bitset.anyClearInRange(0, 30)).toBe(false);
62
+ expect(bitset.anyClearInRange(33, 99)).toBe(false);
63
+ });
64
+
65
+ it("should handle anyClearInRange with single word", () => {
66
+ const bitset = new BitSet(100);
67
+
68
+ bitset.setRange(10, 20);
69
+
70
+ // Range entirely in one word and all bits set
71
+ expect(bitset.anyClearInRange(10, 20)).toBe(false);
72
+
73
+ // Clear one bit in range
74
+ bitset.clear(15);
75
+ expect(bitset.anyClearInRange(10, 20)).toBe(true);
76
+
77
+ // Check range before and after
78
+ expect(bitset.anyClearInRange(0, 9)).toBe(true);
79
+ expect(bitset.anyClearInRange(21, 99)).toBe(true);
80
+ });
81
+
82
+ it("should handle large ranges", () => {
83
+ const bitset = new BitSet(1000);
84
+
85
+ // Set all bits
86
+ bitset.setRange(0, 999);
87
+
88
+ expect(bitset.anyClearInRange(0, 999)).toBe(false);
89
+
90
+ // Clear one bit in the middle
91
+ bitset.clear(500);
92
+ expect(bitset.anyClearInRange(0, 999)).toBe(true);
93
+ expect(bitset.anyClearInRange(0, 499)).toBe(false);
94
+ expect(bitset.anyClearInRange(501, 999)).toBe(false);
95
+ });
96
+
97
+ it("should handle range with no overlap", () => {
98
+ const bitset = new BitSet(100);
99
+
100
+ bitset.setRange(10, 20);
101
+
102
+ expect(bitset.anyClearInRange(0, 9)).toBe(true);
103
+ expect(bitset.anyClearInRange(21, 99)).toBe(true);
104
+ });
105
+
106
+ it("should iterate over set bits correctly", () => {
107
+ const bitset = new BitSet(100);
108
+
109
+ bitset.set(5);
110
+ bitset.set(35);
111
+ bitset.set(65);
112
+ bitset.set(99);
113
+
114
+ const setBits: number[] = [];
115
+ for (const bit of bitset) {
116
+ setBits.push(bit);
117
+ }
118
+
119
+ expect(setBits).toEqual([5, 35, 65, 99]);
120
+ });
121
+
122
+ it("should iterate over setRange result", () => {
123
+ const bitset = new BitSet(100);
124
+
125
+ bitset.setRange(10, 15);
126
+
127
+ const setBits: number[] = [];
128
+ for (const bit of bitset) {
129
+ setBits.push(bit);
130
+ }
131
+
132
+ expect(setBits).toEqual([10, 11, 12, 13, 14, 15]);
133
+ });
134
+
135
+ it("should handle reset correctly", () => {
136
+ const bitset = new BitSet(100);
137
+
138
+ bitset.setRange(0, 99);
139
+ bitset.reset();
140
+
141
+ expect(bitset.anyClearInRange(0, 99)).toBe(true);
142
+
143
+ const setBits: number[] = [];
144
+ for (const bit of bitset) {
145
+ setBits.push(bit);
146
+ }
147
+
148
+ expect(setBits).toHaveLength(0);
149
+ });
150
+
151
+ it("should handle edge case: setRange with reversed bounds", () => {
152
+ const bitset = new BitSet(100);
153
+
154
+ // setRange(hi, lo) where hi > lo should do nothing
155
+ bitset.setRange(50, 30);
156
+
157
+ for (let i = 0; i < 100; i++) {
158
+ expect(bitset.has(i)).toBe(false);
159
+ }
160
+ });
161
+
162
+ it("should handle single bit in range", () => {
163
+ const bitset = new BitSet(100);
164
+
165
+ bitset.setRange(25, 25);
166
+
167
+ expect(bitset.has(25)).toBe(true);
168
+ expect(bitset.has(24)).toBe(false);
169
+ expect(bitset.has(26)).toBe(false);
170
+ });
171
+ });
@@ -0,0 +1,254 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { ComponentChangeset } from "../../commands/changeset";
3
+ import { component } from "../../entity";
4
+
5
+ describe("ComponentChangeset", () => {
6
+ const PositionId = component<{ x: number; y: number }>();
7
+ const VelocityId = component<{ x: number; y: number }>();
8
+ const HealthId = component<number>();
9
+
10
+ describe("Basic Operations", () => {
11
+ it("should start with no changes", () => {
12
+ const changeset = new ComponentChangeset();
13
+ expect(changeset.hasChanges()).toBe(false);
14
+ expect(changeset.adds.size).toBe(0);
15
+ expect(changeset.removes.size).toBe(0);
16
+ });
17
+
18
+ it("should add components", () => {
19
+ const changeset = new ComponentChangeset();
20
+ changeset.set(PositionId, { x: 10, y: 20 });
21
+
22
+ expect(changeset.hasChanges()).toBe(true);
23
+ expect(changeset.adds.get(PositionId)).toEqual({ x: 10, y: 20 });
24
+ expect(changeset.removes.size).toBe(0);
25
+ });
26
+
27
+ it("should remove components", () => {
28
+ const changeset = new ComponentChangeset();
29
+ changeset.delete(PositionId);
30
+
31
+ expect(changeset.hasChanges()).toBe(true);
32
+ expect(changeset.adds.size).toBe(0);
33
+ expect(changeset.removes.has(PositionId)).toBe(true);
34
+ });
35
+
36
+ it("should clear all changes", () => {
37
+ const changeset = new ComponentChangeset();
38
+ changeset.set(PositionId, { x: 10, y: 20 });
39
+ changeset.delete(VelocityId);
40
+
41
+ expect(changeset.hasChanges()).toBe(true);
42
+
43
+ changeset.clear();
44
+
45
+ expect(changeset.hasChanges()).toBe(false);
46
+ expect(changeset.adds.size).toBe(0);
47
+ expect(changeset.removes.size).toBe(0);
48
+ });
49
+ });
50
+
51
+ describe("Conflict Resolution", () => {
52
+ it("should remove from removes when adding a component that was going to be removed", () => {
53
+ const changeset = new ComponentChangeset();
54
+ changeset.delete(PositionId);
55
+ changeset.set(PositionId, { x: 10, y: 20 });
56
+
57
+ expect(changeset.adds.get(PositionId)).toEqual({ x: 10, y: 20 });
58
+ expect(changeset.removes.has(PositionId)).toBe(false);
59
+ });
60
+
61
+ it("should remove from adds when removing a component that was going to be added", () => {
62
+ const changeset = new ComponentChangeset();
63
+ changeset.set(PositionId, { x: 10, y: 20 });
64
+ changeset.delete(PositionId);
65
+
66
+ expect(changeset.adds.has(PositionId)).toBe(false);
67
+ expect(changeset.removes.has(PositionId)).toBe(true);
68
+ });
69
+ });
70
+
71
+ describe("Apply Changes", () => {
72
+ it("should apply additions to existing components", () => {
73
+ const changeset = new ComponentChangeset();
74
+ changeset.set(PositionId, { x: 10, y: 20 });
75
+ changeset.set(VelocityId, { x: 1, y: 2 });
76
+
77
+ const existing = new Map();
78
+ existing.set(HealthId, 100);
79
+
80
+ const result = changeset.applyTo(existing);
81
+
82
+ expect(result.get(PositionId)).toEqual({ x: 10, y: 20 });
83
+ expect(result.get(VelocityId)).toEqual({ x: 1, y: 2 });
84
+ expect(result.get(HealthId)).toBe(100);
85
+ });
86
+
87
+ it("should apply removals to existing components", () => {
88
+ const changeset = new ComponentChangeset();
89
+ changeset.delete(PositionId);
90
+
91
+ const existing = new Map();
92
+ existing.set(PositionId, { x: 10, y: 20 });
93
+ existing.set(VelocityId, { x: 1, y: 2 });
94
+
95
+ const result = changeset.applyTo(existing);
96
+
97
+ expect(result.has(PositionId)).toBe(false);
98
+ expect(result.get(VelocityId)).toEqual({ x: 1, y: 2 });
99
+ });
100
+
101
+ it("should apply both additions and removals", () => {
102
+ const changeset = new ComponentChangeset();
103
+ changeset.set(PositionId, { x: 50, y: 60 });
104
+ changeset.delete(VelocityId);
105
+
106
+ const existing = new Map();
107
+ existing.set(PositionId, { x: 10, y: 20 });
108
+ existing.set(VelocityId, { x: 1, y: 2 });
109
+ existing.set(HealthId, 100);
110
+
111
+ const result = changeset.applyTo(existing);
112
+
113
+ expect(result.get(PositionId)).toEqual({ x: 50, y: 60 }); // Updated
114
+ expect(result.has(VelocityId)).toBe(false); // Removed
115
+ expect(result.get(HealthId)).toBe(100); // Unchanged
116
+ });
117
+
118
+ it("should get final component types correctly", () => {
119
+ const changeset = new ComponentChangeset();
120
+ changeset.set(PositionId, { x: 10, y: 20 });
121
+ changeset.delete(VelocityId);
122
+
123
+ const existing = new Map();
124
+ existing.set(VelocityId, { x: 1, y: 2 });
125
+ existing.set(HealthId, 100);
126
+
127
+ const finalTypes = changeset.applyTo(existing);
128
+
129
+ expect([...finalTypes.keys()]).toEqual([HealthId, PositionId]);
130
+ });
131
+ });
132
+
133
+ describe("Direct Access", () => {
134
+ it("should return direct references to internal maps for performance", () => {
135
+ const changeset = new ComponentChangeset();
136
+ changeset.set(PositionId, { x: 10, y: 20 });
137
+
138
+ const adds = changeset.adds;
139
+ const removes = changeset.removes;
140
+
141
+ // Since this is internal API, direct modification is allowed
142
+ adds.clear();
143
+ removes.add(VelocityId);
144
+
145
+ expect(changeset.adds.size).toBe(0);
146
+ expect(changeset.removes.size).toBe(1);
147
+ });
148
+ });
149
+
150
+ describe("Merge Changesets", () => {
151
+ it("should merge additions into an empty changeset", () => {
152
+ const changeset1 = new ComponentChangeset();
153
+ const changeset2 = new ComponentChangeset();
154
+ changeset2.set(PositionId, { x: 10, y: 20 });
155
+ changeset2.set(VelocityId, { x: 1, y: 2 });
156
+
157
+ changeset1.merge(changeset2);
158
+
159
+ expect(changeset1.adds.get(PositionId)).toEqual({ x: 10, y: 20 });
160
+ expect(changeset1.adds.get(VelocityId)).toEqual({ x: 1, y: 2 });
161
+ expect(changeset1.removes.size).toBe(0);
162
+ expect(changeset1.hasChanges()).toBe(true);
163
+ });
164
+
165
+ it("should merge removals into an empty changeset", () => {
166
+ const changeset1 = new ComponentChangeset();
167
+ const changeset2 = new ComponentChangeset();
168
+ changeset2.delete(PositionId);
169
+ changeset2.delete(VelocityId);
170
+
171
+ changeset1.merge(changeset2);
172
+
173
+ expect(changeset1.removes.has(PositionId)).toBe(true);
174
+ expect(changeset1.removes.has(VelocityId)).toBe(true);
175
+ expect(changeset1.adds.size).toBe(0);
176
+ expect(changeset1.hasChanges()).toBe(true);
177
+ });
178
+
179
+ it("should merge additions and removals together", () => {
180
+ const changeset1 = new ComponentChangeset();
181
+ const changeset2 = new ComponentChangeset();
182
+ changeset2.set(PositionId, { x: 10, y: 20 });
183
+ changeset2.delete(VelocityId);
184
+
185
+ changeset1.merge(changeset2);
186
+
187
+ expect(changeset1.adds.get(PositionId)).toEqual({ x: 10, y: 20 });
188
+ expect(changeset1.removes.has(VelocityId)).toBe(true);
189
+ expect(changeset1.hasChanges()).toBe(true);
190
+ });
191
+
192
+ it("should override removal with addition when merging", () => {
193
+ const changeset1 = new ComponentChangeset();
194
+ changeset1.delete(PositionId); // Initially removing
195
+
196
+ const changeset2 = new ComponentChangeset();
197
+ changeset2.set(PositionId, { x: 10, y: 20 }); // Adding the same component
198
+
199
+ changeset1.merge(changeset2);
200
+
201
+ expect(changeset1.adds.get(PositionId)).toEqual({ x: 10, y: 20 });
202
+ expect(changeset1.removes.has(PositionId)).toBe(false);
203
+ });
204
+
205
+ it("should override addition with removal when merging", () => {
206
+ const changeset1 = new ComponentChangeset();
207
+ changeset1.set(PositionId, { x: 5, y: 5 }); // Initially adding
208
+
209
+ const changeset2 = new ComponentChangeset();
210
+ changeset2.delete(PositionId); // Removing the same component
211
+
212
+ changeset1.merge(changeset2);
213
+
214
+ expect(changeset1.adds.has(PositionId)).toBe(false);
215
+ expect(changeset1.removes.has(PositionId)).toBe(true);
216
+ });
217
+
218
+ it("should merge multiple changesets sequentially", () => {
219
+ const changeset1 = new ComponentChangeset();
220
+ changeset1.set(PositionId, { x: 10, y: 20 });
221
+
222
+ const changeset2 = new ComponentChangeset();
223
+ changeset2.delete(PositionId);
224
+ changeset2.set(VelocityId, { x: 1, y: 2 });
225
+
226
+ const changeset3 = new ComponentChangeset();
227
+ changeset3.delete(VelocityId);
228
+ changeset3.set(HealthId, 100);
229
+
230
+ changeset1.merge(changeset2);
231
+ changeset1.merge(changeset3);
232
+
233
+ expect(changeset1.adds.has(PositionId)).toBe(false); // Removed by changeset2
234
+ expect(changeset1.removes.has(PositionId)).toBe(true);
235
+ expect(changeset1.adds.has(VelocityId)).toBe(false); // Removed by changeset3
236
+ expect(changeset1.removes.has(VelocityId)).toBe(true);
237
+ expect(changeset1.adds.get(HealthId)).toBe(100);
238
+ expect(changeset1.hasChanges()).toBe(true);
239
+ });
240
+
241
+ it("should handle merging empty changeset", () => {
242
+ const changeset1 = new ComponentChangeset();
243
+ changeset1.set(PositionId, { x: 10, y: 20 });
244
+
245
+ const changeset2 = new ComponentChangeset(); // Empty
246
+
247
+ changeset1.merge(changeset2);
248
+
249
+ expect(changeset1.adds.get(PositionId)).toEqual({ x: 10, y: 20 });
250
+ expect(changeset1.removes.size).toBe(0);
251
+ expect(changeset1.hasChanges()).toBe(true);
252
+ });
253
+ });
254
+ });
@@ -0,0 +1,74 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { MultiMap } from "../../utils/multi-map";
3
+
4
+ describe("MultiMap", () => {
5
+ test("add and get values", () => {
6
+ const mm = new MultiMap<string, number>();
7
+ mm.add("a", 1);
8
+ mm.add("a", 2);
9
+ mm.add("b", 3);
10
+
11
+ expect(mm.keyCount).toBe(2);
12
+ expect(mm.valueCount).toBe(3);
13
+
14
+ const aValues = mm.get("a");
15
+ expect(Array.from(aValues).sort()).toEqual([1, 2]);
16
+ expect(Array.from(mm.get("c"))).toEqual([]);
17
+ });
18
+
19
+ test("remove value and deleteKey behavior", () => {
20
+ const mm = new MultiMap<string, string>();
21
+ mm.add("x", "one");
22
+ mm.add("x", "two");
23
+ mm.add("y", "three");
24
+
25
+ expect(mm.valueCount).toBe(3);
26
+
27
+ const removed = mm.remove("x", "one");
28
+ expect(removed).toBe(true);
29
+ expect(mm.valueCount).toBe(2);
30
+ expect(mm.has("x", "one")).toBe(false);
31
+
32
+ // remove remaining value for x -> key removed
33
+ expect(mm.remove("x", "two")).toBe(true);
34
+ expect(mm.hasKey("x")).toBe(false);
35
+ expect(mm.keyCount).toBe(1);
36
+ expect(mm.valueCount).toBe(1);
37
+
38
+ // delete whole key y
39
+ expect(mm.deleteKey("y")).toBe(true);
40
+ expect(mm.keyCount).toBe(0);
41
+ expect(mm.valueCount).toBe(0);
42
+ });
43
+
44
+ test("keys, values and entries iteration", () => {
45
+ const mm = new MultiMap<number, string>();
46
+ mm.add(1, "a");
47
+ mm.add(1, "b");
48
+ mm.add(2, "c");
49
+
50
+ const keys = Array.from(mm.keys()).sort((a, b) => a - b);
51
+ expect(keys).toEqual([1, 2]);
52
+
53
+ const values = Array.from(mm.values()).sort();
54
+ expect(values).toEqual(["a", "b", "c"]);
55
+
56
+ const entries = Array.from(mm.entries());
57
+ // entries should be pairs of key and a Set copy
58
+ expect(entries.length).toBe(3);
59
+ const entryMap = new Map(entries);
60
+ expect(entryMap.get(1)!).toEqual("b");
61
+ });
62
+
63
+ test("clear resets state", () => {
64
+ const mm = new MultiMap<string, number>();
65
+ mm.add("k", 1);
66
+ mm.add("k", 2);
67
+ expect(mm.keyCount).toBe(1);
68
+ expect(mm.valueCount).toBe(2);
69
+ mm.clear();
70
+ expect(mm.keyCount).toBe(0);
71
+ expect(mm.valueCount).toBe(0);
72
+ expect(Array.from(mm.keys()).length).toBe(0);
73
+ });
74
+ });
@@ -0,0 +1,66 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { ComponentIdAllocator } from "../../entity/manager";
3
+ import { COMPONENT_ID_MAX, createComponentId } from "../../entity/types";
4
+
5
+ describe("ComponentIdAllocator", () => {
6
+ it("should allocate component IDs sequentially", () => {
7
+ const allocator = new ComponentIdAllocator();
8
+
9
+ const id1 = allocator.allocate();
10
+ const id2 = allocator.allocate();
11
+ const id3 = allocator.allocate();
12
+
13
+ expect(id1).toBe(createComponentId(1));
14
+ expect(id2).toBe(createComponentId(2));
15
+ expect(id3).toBe(createComponentId(3));
16
+ });
17
+
18
+ it("should throw when exceeding COMPONENT_ID_MAX", () => {
19
+ const allocator = new ComponentIdAllocator();
20
+
21
+ // Allocate up to the limit
22
+ for (let i = 1; i <= COMPONENT_ID_MAX; i++) {
23
+ allocator.allocate();
24
+ }
25
+
26
+ // The next allocation should throw
27
+ expect(() => allocator.allocate()).toThrow(/out of component IDs|overflow/i);
28
+ });
29
+
30
+ it("should check availability correctly", () => {
31
+ const allocator = new ComponentIdAllocator();
32
+
33
+ expect(allocator.hasAvailableIds()).toBe(true);
34
+
35
+ // Allocate up to the limit
36
+ for (let i = 1; i <= COMPONENT_ID_MAX; i++) {
37
+ allocator.allocate();
38
+ }
39
+
40
+ expect(allocator.hasAvailableIds()).toBe(false);
41
+ });
42
+
43
+ it("should track next ID correctly", () => {
44
+ const allocator = new ComponentIdAllocator();
45
+
46
+ expect(allocator.getNextId()).toBe(1);
47
+
48
+ allocator.allocate();
49
+ expect(allocator.getNextId()).toBe(2);
50
+
51
+ allocator.allocate();
52
+ expect(allocator.getNextId()).toBe(3);
53
+ });
54
+
55
+ it("should allocate exactly COMPONENT_ID_MAX IDs", () => {
56
+ const allocator = new ComponentIdAllocator();
57
+
58
+ for (let i = 1; i <= COMPONENT_ID_MAX; i++) {
59
+ const id = allocator.allocate();
60
+ expect(id).toBe(createComponentId(i));
61
+ }
62
+
63
+ // Should be exhausted now
64
+ expect(allocator.hasAvailableIds()).toBe(false);
65
+ });
66
+ });