redscript-mc 1.0.0 → 1.2.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 (136) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.yml +72 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.yml +57 -0
  3. package/.github/PULL_REQUEST_TEMPLATE.md +17 -25
  4. package/CHANGELOG.md +112 -0
  5. package/CONTRIBUTING.md +140 -0
  6. package/README.md +28 -19
  7. package/README.zh.md +28 -19
  8. package/dist/__tests__/cli.test.js +148 -10
  9. package/dist/__tests__/codegen.test.js +26 -1
  10. package/dist/__tests__/diagnostics.test.js +5 -5
  11. package/dist/__tests__/e2e.test.js +336 -17
  12. package/dist/__tests__/formatter.test.d.ts +1 -0
  13. package/dist/__tests__/formatter.test.js +40 -0
  14. package/dist/__tests__/lexer.test.js +12 -2
  15. package/dist/__tests__/lowering.test.js +200 -12
  16. package/dist/__tests__/mc-integration.test.js +370 -31
  17. package/dist/__tests__/mc-syntax.test.js +3 -3
  18. package/dist/__tests__/nbt.test.js +2 -2
  19. package/dist/__tests__/optimizer-advanced.test.js +5 -5
  20. package/dist/__tests__/parser.test.js +80 -0
  21. package/dist/__tests__/runtime.test.js +9 -9
  22. package/dist/__tests__/typechecker.test.js +158 -0
  23. package/dist/ast/types.d.ts +40 -3
  24. package/dist/cli.js +25 -7
  25. package/dist/codegen/mcfunction/index.d.ts +1 -1
  26. package/dist/codegen/mcfunction/index.js +38 -3
  27. package/dist/codegen/structure/index.js +32 -1
  28. package/dist/compile.d.ts +10 -0
  29. package/dist/compile.js +36 -5
  30. package/dist/events/types.d.ts +35 -0
  31. package/dist/events/types.js +59 -0
  32. package/dist/formatter/index.d.ts +1 -0
  33. package/dist/formatter/index.js +26 -0
  34. package/dist/index.js +3 -2
  35. package/dist/ir/builder.d.ts +2 -1
  36. package/dist/ir/types.d.ts +11 -2
  37. package/dist/ir/types.js +1 -1
  38. package/dist/lexer/index.d.ts +1 -1
  39. package/dist/lexer/index.js +2 -0
  40. package/dist/lowering/index.d.ts +34 -1
  41. package/dist/lowering/index.js +622 -23
  42. package/dist/mc-test/runner.d.ts +2 -2
  43. package/dist/mc-test/runner.js +3 -3
  44. package/dist/mc-test/setup.js +2 -2
  45. package/dist/parser/index.d.ts +4 -0
  46. package/dist/parser/index.js +153 -16
  47. package/dist/typechecker/index.d.ts +17 -0
  48. package/dist/typechecker/index.js +343 -17
  49. package/docs/COMPILATION_STATS.md +24 -24
  50. package/docs/ENTITY_TYPE_SYSTEM.md +242 -0
  51. package/docs/IMPLEMENTATION_GUIDE.md +1 -1
  52. package/docs/STRUCTURE_TARGET.md +1 -1
  53. package/editors/vscode/.vscodeignore +1 -0
  54. package/editors/vscode/CHANGELOG.md +9 -0
  55. package/editors/vscode/icons/mcrs.svg +7 -0
  56. package/editors/vscode/icons/redscript-icons.json +10 -0
  57. package/editors/vscode/out/extension.js +1295 -80
  58. package/editors/vscode/package-lock.json +2 -2
  59. package/editors/vscode/package.json +10 -3
  60. package/editors/vscode/src/hover.ts +55 -2
  61. package/editors/vscode/src/symbols.ts +42 -0
  62. package/package.json +1 -1
  63. package/src/__tests__/cli.test.ts +176 -10
  64. package/src/__tests__/codegen.test.ts +28 -1
  65. package/src/__tests__/diagnostics.test.ts +5 -5
  66. package/src/__tests__/e2e.test.ts +335 -17
  67. package/src/__tests__/fixtures/event-test.mcrs +13 -0
  68. package/src/__tests__/fixtures/impl-test.mcrs +46 -0
  69. package/src/__tests__/fixtures/interval-test.mcrs +11 -0
  70. package/src/__tests__/fixtures/is-check-test.mcrs +20 -0
  71. package/src/__tests__/fixtures/timeout-test.mcrs +7 -0
  72. package/src/__tests__/lexer.test.ts +14 -2
  73. package/src/__tests__/lowering.test.ts +226 -12
  74. package/src/__tests__/mc-integration.test.ts +421 -31
  75. package/src/__tests__/mc-syntax.test.ts +3 -3
  76. package/src/__tests__/nbt.test.ts +2 -2
  77. package/src/__tests__/optimizer-advanced.test.ts +5 -5
  78. package/src/__tests__/parser.test.ts +91 -5
  79. package/src/__tests__/runtime.test.ts +9 -9
  80. package/src/__tests__/typechecker.test.ts +171 -0
  81. package/src/ast/types.ts +44 -3
  82. package/src/cli.ts +10 -10
  83. package/src/codegen/mcfunction/index.ts +40 -3
  84. package/src/codegen/structure/index.ts +35 -1
  85. package/src/compile.ts +54 -6
  86. package/src/events/types.ts +69 -0
  87. package/src/examples/capture_the_flag.mcrs +208 -0
  88. package/src/examples/{counter.rs → counter.mcrs} +1 -1
  89. package/src/examples/hunger_games.mcrs +301 -0
  90. package/src/examples/new_features_demo.mcrs +193 -0
  91. package/src/examples/parkour_race.mcrs +233 -0
  92. package/src/examples/rpg.mcrs +13 -0
  93. package/src/examples/{shop.rs → shop.mcrs} +1 -1
  94. package/src/examples/{showcase_game.rs → showcase_game.mcrs} +3 -3
  95. package/src/examples/{turret.rs → turret.mcrs} +1 -1
  96. package/src/examples/zombie_survival.mcrs +314 -0
  97. package/src/index.ts +4 -3
  98. package/src/ir/builder.ts +3 -1
  99. package/src/ir/types.ts +12 -2
  100. package/src/lexer/index.ts +3 -1
  101. package/src/lowering/index.ts +684 -24
  102. package/src/mc-test/runner.ts +3 -3
  103. package/src/mc-test/setup.ts +2 -2
  104. package/src/parser/index.ts +170 -19
  105. package/src/stdlib/README.md +178 -140
  106. package/src/stdlib/bossbar.mcrs +68 -0
  107. package/src/stdlib/{cooldown.rs → cooldown.mcrs} +1 -1
  108. package/src/stdlib/effects.mcrs +64 -0
  109. package/src/stdlib/interactions.mcrs +195 -0
  110. package/src/stdlib/inventory.mcrs +38 -0
  111. package/src/stdlib/mobs.mcrs +99 -0
  112. package/src/stdlib/particles.mcrs +52 -0
  113. package/src/stdlib/sets.mcrs +20 -0
  114. package/src/stdlib/spawn.mcrs +41 -0
  115. package/src/stdlib/tags.mcrs +951 -0
  116. package/src/stdlib/teams.mcrs +68 -0
  117. package/src/stdlib/timer.mcrs +72 -0
  118. package/src/stdlib/world.mcrs +92 -0
  119. package/src/typechecker/index.ts +404 -18
  120. package/src/examples/rpg.rs +0 -13
  121. package/src/stdlib/mobs.rs +0 -99
  122. package/src/stdlib/timer.rs +0 -51
  123. /package/src/examples/{arena.rs → arena.mcrs} +0 -0
  124. /package/src/examples/{pvp_arena.rs → pvp_arena.mcrs} +0 -0
  125. /package/src/examples/{quiz.rs → quiz.mcrs} +0 -0
  126. /package/src/examples/{stdlib_demo.rs → stdlib_demo.mcrs} +0 -0
  127. /package/src/examples/{world_manager.rs → world_manager.mcrs} +0 -0
  128. /package/src/stdlib/{combat.rs → combat.mcrs} +0 -0
  129. /package/src/stdlib/{math.rs → math.mcrs} +0 -0
  130. /package/src/stdlib/{player.rs → player.mcrs} +0 -0
  131. /package/src/stdlib/{strings.rs → strings.mcrs} +0 -0
  132. /package/src/templates/{combat.rs → combat.mcrs} +0 -0
  133. /package/src/templates/{economy.rs → economy.mcrs} +0 -0
  134. /package/src/templates/{mini-game-framework.rs → mini-game-framework.mcrs} +0 -0
  135. /package/src/templates/{quest.rs → quest.mcrs} +0 -0
  136. /package/src/test_programs/{zombie_game.rs → zombie_game.mcrs} +0 -0
@@ -52,6 +52,7 @@ const MC_HOST = process.env.MC_HOST ?? 'localhost';
52
52
  const MC_PORT = parseInt(process.env.MC_PORT ?? '25561');
53
53
  const MC_SERVER_DIR = process.env.MC_SERVER_DIR ?? path.join(process.env.HOME, 'mc-test-server');
54
54
  const DATAPACK_DIR = path.join(MC_SERVER_DIR, 'world', 'datapacks', 'redscript-test');
55
+ const FIXTURE_DIR = path.join(__dirname, 'fixtures');
55
56
  let serverOnline = false;
56
57
  let mc;
57
58
  /** Write compiled RedScript source into the shared test datapack directory.
@@ -84,9 +85,22 @@ function writeFixture(source, namespace) {
84
85
  }
85
86
  }
86
87
  }
88
+ function writeFixtureFile(fileName, namespace) {
89
+ writeFixture(fs.readFileSync(path.join(FIXTURE_DIR, fileName), 'utf-8'), namespace);
90
+ }
91
+ async function waitForServer(client, timeoutMs = 30000) {
92
+ const deadline = Date.now() + timeoutMs;
93
+ while (Date.now() < deadline) {
94
+ if (await client.isOnline()) {
95
+ return true;
96
+ }
97
+ await new Promise(resolve => setTimeout(resolve, 1000));
98
+ }
99
+ return false;
100
+ }
87
101
  beforeAll(async () => {
88
102
  mc = new client_1.MCTestClient(MC_HOST, MC_PORT);
89
- serverOnline = await mc.isOnline();
103
+ serverOnline = await waitForServer(mc);
90
104
  if (!serverOnline) {
91
105
  console.warn(`⚠ MC server not running at ${MC_HOST}:${MC_PORT} — skipping integration tests`);
92
106
  console.warn(` Run: MC_SERVER_DIR=~/mc-test-server npx ts-node src/mc-test/setup.ts`);
@@ -94,26 +108,26 @@ beforeAll(async () => {
94
108
  return;
95
109
  }
96
110
  // ── Write fixtures + use safe reloadData (no /reload confirm) ───────
97
- // counter.rs
98
- if (fs.existsSync(path.join(__dirname, '../examples/counter.rs'))) {
99
- writeFixture(fs.readFileSync(path.join(__dirname, '../examples/counter.rs'), 'utf-8'), 'counter');
111
+ // counter.mcrs
112
+ if (fs.existsSync(path.join(__dirname, '../examples/counter.mcrs'))) {
113
+ writeFixture(fs.readFileSync(path.join(__dirname, '../examples/counter.mcrs'), 'utf-8'), 'counter');
100
114
  }
101
- if (fs.existsSync(path.join(__dirname, '../examples/world_manager.rs'))) {
102
- writeFixture(fs.readFileSync(path.join(__dirname, '../examples/world_manager.rs'), 'utf-8'), 'world_manager');
115
+ if (fs.existsSync(path.join(__dirname, '../examples/world_manager.mcrs'))) {
116
+ writeFixture(fs.readFileSync(path.join(__dirname, '../examples/world_manager.mcrs'), 'utf-8'), 'world_manager');
103
117
  }
104
118
  writeFixture(`
105
119
  @tick
106
120
  fn on_tick() {
107
- scoreboard_set("#tick_counter", "ticks", scoreboard_get("#tick_counter", "ticks") + 1);
121
+ scoreboard_set("#tick_counter", #ticks, scoreboard_get("#tick_counter", #ticks) + 1);
108
122
  }
109
123
  `, 'tick_test');
110
124
  writeFixture(`
111
125
  fn check_score() {
112
- let x: int = scoreboard_get("#check_x", "test_score");
126
+ let x: int = scoreboard_get("#check_x", #test_score);
113
127
  if (x > 5) {
114
- scoreboard_set("#check_x", "result", 1);
128
+ scoreboard_set("#check_x", #result, 1);
115
129
  } else {
116
- scoreboard_set("#check_x", "result", 0);
130
+ scoreboard_set("#check_x", #result, 0);
117
131
  }
118
132
  }
119
133
  `, 'inline_test');
@@ -122,30 +136,30 @@ beforeAll(async () => {
122
136
  writeFixture(`
123
137
  @tick
124
138
  fn game_tick() {
125
- let time: int = scoreboard_get("#game", "timer");
139
+ let time: int = scoreboard_get("#game", #timer);
126
140
  if (time > 0) {
127
- scoreboard_set("#game", "timer", time - 1);
141
+ scoreboard_set("#game", #timer, time - 1);
128
142
  }
129
143
  if (time == 1) {
130
- scoreboard_set("#game", "ended", 1);
144
+ scoreboard_set("#game", #ended, 1);
131
145
  }
132
146
  }
133
147
  fn start_game() {
134
- scoreboard_set("#game", "timer", 5);
135
- scoreboard_set("#game", "ended", 0);
148
+ scoreboard_set("#game", #timer, 5);
149
+ scoreboard_set("#game", #ended, 0);
136
150
  }
137
151
  `, 'game_loop');
138
152
  // Scenario B: two functions, same temp var namespace — verify no collision
139
153
  writeFixture(`
140
154
  fn calc_sum() {
141
- let a: int = scoreboard_get("#math", "val_a");
142
- let b: int = scoreboard_get("#math", "val_b");
143
- scoreboard_set("#math", "sum", a + b);
155
+ let a: int = scoreboard_get("#math", #val_a);
156
+ let b: int = scoreboard_get("#math", #val_b);
157
+ scoreboard_set("#math", #sum, a + b);
144
158
  }
145
159
  fn calc_product() {
146
- let x: int = scoreboard_get("#math", "val_x");
147
- let y: int = scoreboard_get("#math", "val_y");
148
- scoreboard_set("#math", "product", x * y);
160
+ let x: int = scoreboard_get("#math", #val_x);
161
+ let y: int = scoreboard_get("#math", #val_y);
162
+ scoreboard_set("#math", #product, x * y);
149
163
  }
150
164
  fn run_both() {
151
165
  calc_sum();
@@ -155,16 +169,16 @@ beforeAll(async () => {
155
169
  // Scenario C: 3-deep call chain, each step modifies shared state
156
170
  writeFixture(`
157
171
  fn step3() {
158
- let v: int = scoreboard_get("#chain", "val");
159
- scoreboard_set("#chain", "val", v * 2);
172
+ let v: int = scoreboard_get("#chain", #val);
173
+ scoreboard_set("#chain", #val, v * 2);
160
174
  }
161
175
  fn step2() {
162
- let v: int = scoreboard_get("#chain", "val");
163
- scoreboard_set("#chain", "val", v + 5);
176
+ let v: int = scoreboard_get("#chain", #val);
177
+ scoreboard_set("#chain", #val, v + 5);
164
178
  step3();
165
179
  }
166
180
  fn step1() {
167
- scoreboard_set("#chain", "val", 10);
181
+ scoreboard_set("#chain", #val, 10);
168
182
  step2();
169
183
  }
170
184
  `, 'call_chain');
@@ -177,11 +191,115 @@ beforeAll(async () => {
177
191
  setblock((3, 70, 0), "minecraft:stone");
178
192
  }
179
193
  `, 'fill_test');
194
+ // Scenario E: for-range loop — loop counter increments exactly N times
195
+ writeFixture(`
196
+ fn count_to_five() {
197
+ scoreboard_set("#range", #counter, 0);
198
+ for i in 0..5 {
199
+ let c: int = scoreboard_get("#range", #counter);
200
+ scoreboard_set("#range", #counter, c + 1);
201
+ }
202
+ }
203
+ `, 'range_test');
204
+ // Scenario F: function call with return value — verifies $ret propagation
205
+ writeFixture(`
206
+ fn triple(x: int) -> int {
207
+ return x * 3;
208
+ }
209
+ fn run_nested() {
210
+ let a: int = triple(4);
211
+ scoreboard_set("#nested", #result, a);
212
+ }
213
+ `, 'nested_test');
214
+ // Scenario G: match statement dispatches to correct branch
215
+ writeFixture(`
216
+ fn classify(x: int) {
217
+ match (x) {
218
+ 1 => { scoreboard_set("#match", #out, 10); }
219
+ 2 => { scoreboard_set("#match", #out, 20); }
220
+ 3 => { scoreboard_set("#match", #out, 30); }
221
+ _ => { scoreboard_set("#match", #out, -1); }
222
+ }
223
+ }
224
+ `, 'match_test');
225
+ // Scenario H: while loop counts down
226
+ writeFixture(`
227
+ fn countdown() {
228
+ scoreboard_set("#wloop", #i, 10);
229
+ scoreboard_set("#wloop", #steps, 0);
230
+ let i: int = scoreboard_get("#wloop", #i);
231
+ while (i > 0) {
232
+ let s: int = scoreboard_get("#wloop", #steps);
233
+ scoreboard_set("#wloop", #steps, s + 1);
234
+ i = i - 1;
235
+ scoreboard_set("#wloop", #i, i);
236
+ }
237
+ }
238
+ `, 'while_test');
239
+ // Scenario I: multiple if/else branches (boundary test)
240
+ writeFixture(`
241
+ fn classify_score() {
242
+ let x: int = scoreboard_get("#boundary", #input);
243
+ if (x > 100) {
244
+ scoreboard_set("#boundary", #tier, 3);
245
+ } else {
246
+ if (x > 50) {
247
+ scoreboard_set("#boundary", #tier, 2);
248
+ } else {
249
+ if (x > 0) {
250
+ scoreboard_set("#boundary", #tier, 1);
251
+ } else {
252
+ scoreboard_set("#boundary", #tier, 0);
253
+ }
254
+ }
255
+ }
256
+ }
257
+ `, 'boundary_test');
258
+ // Scenario J: entity management — summon via raw commands
259
+ writeFixture(`
260
+ fn tag_entities() {
261
+ raw("summon minecraft:armor_stand 10 65 10");
262
+ raw("summon minecraft:armor_stand 11 65 10");
263
+ raw("summon minecraft:armor_stand 12 65 10");
264
+ }
265
+ `, 'tag_test');
266
+ // Scenario K: mixed arithmetic — order of operations
267
+ writeFixture(`
268
+ fn math_order() {
269
+ let a: int = 2;
270
+ let b: int = 3;
271
+ let c: int = 4;
272
+ scoreboard_set("#order", #r1, a + b * c);
273
+ scoreboard_set("#order", #r2, (a + b) * c);
274
+ let d: int = 100;
275
+ let e: int = d / 3;
276
+ scoreboard_set("#order", #r3, e);
277
+ }
278
+ `, 'order_test');
279
+ // Scenario L: scoreboard read-modify-write chain
280
+ writeFixture(`
281
+ fn chain_rmw() {
282
+ scoreboard_set("#rmw", #v, 1);
283
+ let v: int = scoreboard_get("#rmw", #v);
284
+ scoreboard_set("#rmw", #v, v * 2);
285
+ v = scoreboard_get("#rmw", #v);
286
+ scoreboard_set("#rmw", #v, v * 2);
287
+ v = scoreboard_get("#rmw", #v);
288
+ scoreboard_set("#rmw", #v, v * 2);
289
+ }
290
+ `, 'rmw_test');
291
+ writeFixtureFile('impl-test.mcrs', 'impl_test');
292
+ writeFixtureFile('timeout-test.mcrs', 'timeout_test');
293
+ writeFixtureFile('interval-test.mcrs', 'interval_test');
294
+ writeFixtureFile('is-check-test.mcrs', 'is_check_test');
295
+ writeFixtureFile('event-test.mcrs', 'event_test');
180
296
  // ── Full reset + safe data reload ────────────────────────────────────
181
297
  await mc.fullReset();
182
298
  // Pre-create scoreboards
183
299
  for (const obj of ['ticks', 'seconds', 'test_score', 'result', 'calc', 'rs',
184
- 'timer', 'ended', 'val_a', 'val_b', 'sum', 'val_x', 'val_y', 'product', 'val']) {
300
+ 'timer', 'ended', 'val_a', 'val_b', 'sum', 'val_x', 'val_y', 'product', 'val',
301
+ 'counter', 'out', 'i', 'steps', 'input', 'tier', 'r1', 'r2', 'r3', 'v',
302
+ 'done', 'fired', 'players', 'zombies']) {
185
303
  await mc.command(`/scoreboard objectives add ${obj} dummy`).catch(() => { });
186
304
  }
187
305
  await mc.command('/scoreboard players set counter ticks 0');
@@ -209,7 +327,7 @@ describe('MC Integration Tests', () => {
209
327
  console.log(` Server: ${status.version}, TPS: ${status.tps_1m.toFixed(1)}`);
210
328
  });
211
329
  // ─── Test 2: Counter tick ─────────────────────────────────────────────
212
- test('counter.rs: tick function increments scoreboard over time', async () => {
330
+ test('counter.mcrs: tick function increments scoreboard over time', async () => {
213
331
  if (!serverOnline)
214
332
  return;
215
333
  await mc.ticks(40); // Wait 2s (counter was already init'd in beforeAll)
@@ -218,7 +336,7 @@ describe('MC Integration Tests', () => {
218
336
  console.log(` counter/ticks after setup+40 ticks: ${count}`);
219
337
  });
220
338
  // ─── Test 3: setblock ────────────────────────────────────────────────
221
- test('world_manager.rs: setblock places correct block', async () => {
339
+ test('world_manager.mcrs: setblock places correct block', async () => {
222
340
  if (!serverOnline)
223
341
  return;
224
342
  // Clear just the lobby area, keep other state
@@ -231,7 +349,7 @@ describe('MC Integration Tests', () => {
231
349
  console.log(` Block at (4,65,4): ${block.type}`);
232
350
  });
233
351
  // ─── Test 4: fill ────────────────────────────────────────────────────
234
- test('world_manager.rs: fill creates smooth_stone floor', async () => {
352
+ test('world_manager.mcrs: fill creates smooth_stone floor', async () => {
235
353
  if (!serverOnline)
236
354
  return;
237
355
  // Runs after test 3, floor should still be there
@@ -337,7 +455,7 @@ describe('E2E Scenario Tests', () => {
337
455
  console.log(` timer hit 0 (final=${finalTimer}), ended=${ended} ✓`);
338
456
  });
339
457
  // Scenario B: No temp var collision between two functions called in sequence
340
- // Verifies: each function's $t0/$t1 temp vars are isolated per-call, not globally shared
458
+ // Verifies: each function's temp vars are isolated per-call via globally unique names
341
459
  // If there's a bug, calc_product would see sum's leftover $t vars and produce wrong result
342
460
  test('B: calc_sum + calc_product called in sequence — no temp var collision', async () => {
343
461
  if (!serverOnline)
@@ -391,5 +509,226 @@ describe('E2E Scenario Tests', () => {
391
509
  expect(after.type).toBe('minecraft:air');
392
510
  console.log(` fill_test: blocks [0-3,70,0]=stone, [-1]/[4]=air ✓`);
393
511
  });
512
+ // Scenario E: for-range loop executes body exactly N times
513
+ // Verifies: for i in 0..5 increments counter 5 times
514
+ test('E: for-range loop increments counter exactly 5 times', async () => {
515
+ if (!serverOnline)
516
+ return;
517
+ await mc.command('/function range_test:__load');
518
+ await mc.command('/function range_test:count_to_five');
519
+ await mc.ticks(10);
520
+ const counter = await mc.scoreboard('#range', 'counter');
521
+ expect(counter).toBe(5);
522
+ console.log(` for-range 0..5 → counter=${counter} (expect 5) ✓`);
523
+ });
524
+ // Scenario F: function return value propagation
525
+ // Verifies: $ret from callee is correctly captured in caller's variable
526
+ test('F: function return value — triple(4) = 12', async () => {
527
+ if (!serverOnline)
528
+ return;
529
+ await mc.command('/function nested_test:__load');
530
+ await mc.command('/function nested_test:run_nested');
531
+ await mc.ticks(10);
532
+ const result = await mc.scoreboard('#nested', 'result');
533
+ expect(result).toBe(12); // triple(4) = 4*3 = 12
534
+ console.log(` triple(4) = ${result} (expect 12) ✓`);
535
+ });
536
+ // Scenario G: match dispatches to correct branch
537
+ // Verifies: match statement selects right arm for values 1, 2, 3, and default
538
+ test('G: match statement dispatches to correct branch', async () => {
539
+ if (!serverOnline)
540
+ return;
541
+ await mc.command('/function match_test:__load');
542
+ // Test match on value 2
543
+ await mc.command('/scoreboard players set $p0 rs 2');
544
+ await mc.command('/function match_test:classify');
545
+ await mc.ticks(5);
546
+ let out = await mc.scoreboard('#match', 'out');
547
+ expect(out).toBe(20);
548
+ console.log(` match(2) → out=${out} (expect 20) ✓`);
549
+ // Test match on value 3
550
+ await mc.command('/scoreboard players set $p0 rs 3');
551
+ await mc.command('/function match_test:classify');
552
+ await mc.ticks(5);
553
+ out = await mc.scoreboard('#match', 'out');
554
+ expect(out).toBe(30);
555
+ console.log(` match(3) → out=${out} (expect 30) ✓`);
556
+ // Test default branch (value 99)
557
+ await mc.command('/scoreboard players set $p0 rs 99');
558
+ await mc.command('/function match_test:classify');
559
+ await mc.ticks(5);
560
+ out = await mc.scoreboard('#match', 'out');
561
+ expect(out).toBe(-1);
562
+ console.log(` match(99) → out=${out} (expect -1, default) ✓`);
563
+ });
564
+ // Scenario H: while loop counts down from 10 to 0
565
+ // Verifies: while loop body executes correct number of iterations
566
+ test('H: while loop counts down 10 steps', async () => {
567
+ if (!serverOnline)
568
+ return;
569
+ await mc.command('/function while_test:__load');
570
+ await mc.command('/function while_test:countdown');
571
+ await mc.ticks(10);
572
+ const i = await mc.scoreboard('#wloop', 'i');
573
+ const steps = await mc.scoreboard('#wloop', 'steps');
574
+ expect(i).toBe(0);
575
+ expect(steps).toBe(10);
576
+ console.log(` while countdown: i=${i} (expect 0), steps=${steps} (expect 10) ✓`);
577
+ });
578
+ // Scenario I: nested if/else boundary classification
579
+ // Verifies: correct branch taken at boundaries (0, 50, 100)
580
+ test('I: nested if/else boundary classification', async () => {
581
+ if (!serverOnline)
582
+ return;
583
+ await mc.command('/function boundary_test:__load');
584
+ // Test x=0 → tier 0
585
+ await mc.command('/scoreboard players set #boundary input 0');
586
+ await mc.command('/function boundary_test:classify_score');
587
+ await mc.ticks(5);
588
+ let tier = await mc.scoreboard('#boundary', 'tier');
589
+ expect(tier).toBe(0);
590
+ console.log(` classify(0) → tier=${tier} (expect 0) ✓`);
591
+ // Test x=50 → tier 1 (> 0 but not > 50)
592
+ await mc.command('/scoreboard players set #boundary input 50');
593
+ await mc.command('/function boundary_test:classify_score');
594
+ await mc.ticks(5);
595
+ tier = await mc.scoreboard('#boundary', 'tier');
596
+ expect(tier).toBe(1);
597
+ console.log(` classify(50) → tier=${tier} (expect 1) ✓`);
598
+ // Test x=51 → tier 2 (> 50 but not > 100)
599
+ await mc.command('/scoreboard players set #boundary input 51');
600
+ await mc.command('/function boundary_test:classify_score');
601
+ await mc.ticks(5);
602
+ tier = await mc.scoreboard('#boundary', 'tier');
603
+ expect(tier).toBe(2);
604
+ console.log(` classify(51) → tier=${tier} (expect 2) ✓`);
605
+ // Test x=101 → tier 3
606
+ await mc.command('/scoreboard players set #boundary input 101');
607
+ await mc.command('/function boundary_test:classify_score');
608
+ await mc.ticks(5);
609
+ tier = await mc.scoreboard('#boundary', 'tier');
610
+ expect(tier).toBe(3);
611
+ console.log(` classify(101) → tier=${tier} (expect 3) ✓`);
612
+ });
613
+ // Scenario J: entity summon and query
614
+ // Verifies: entities spawned via compiled function are queryable
615
+ test('J: summon entities via compiled function', async () => {
616
+ if (!serverOnline)
617
+ return;
618
+ await mc.command('/kill @e[type=minecraft:armor_stand]');
619
+ await mc.ticks(2);
620
+ await mc.command('/function tag_test:__load');
621
+ await mc.command('/function tag_test:tag_entities');
622
+ await mc.ticks(5);
623
+ const stands = await mc.entities('@e[type=minecraft:armor_stand]');
624
+ expect(stands.length).toBe(3);
625
+ console.log(` Summoned 3 armor_stands via tag_test, found: ${stands.length} ✓`);
626
+ await mc.command('/kill @e[type=minecraft:armor_stand]');
627
+ });
628
+ // Scenario K: arithmetic order of operations
629
+ // Verifies: MC scoreboard arithmetic matches expected evaluation order
630
+ test('K: arithmetic order of operations', async () => {
631
+ if (!serverOnline)
632
+ return;
633
+ await mc.command('/function order_test:__load');
634
+ await mc.command('/function order_test:math_order');
635
+ await mc.ticks(10);
636
+ const r1 = await mc.scoreboard('#order', 'r1');
637
+ const r2 = await mc.scoreboard('#order', 'r2');
638
+ const r3 = await mc.scoreboard('#order', 'r3');
639
+ // a + b * c = 2 + 3*4 = 14 (if precedence respected) or (2+3)*4 = 20 (left-to-right)
640
+ // MC scoreboard does left-to-right, so compiler may emit either depending on lowering
641
+ // (a + b) * c = 5 * 4 = 20 (explicit parens)
642
+ expect(r2).toBe(20); // This one is unambiguous
643
+ // 100 / 3 = 33 (integer division)
644
+ expect(r3).toBe(33);
645
+ console.log(` r1=${r1}, r2=${r2} (expect 20), r3=${r3} (expect 33) ✓`);
646
+ });
647
+ // Scenario L: scoreboard read-modify-write chain (1 → 2 → 4 → 8)
648
+ // Verifies: sequential RMW operations don't lose intermediate state
649
+ test('L: scoreboard RMW chain — 1*2*2*2 = 8', async () => {
650
+ if (!serverOnline)
651
+ return;
652
+ await mc.command('/function rmw_test:__load');
653
+ await mc.command('/function rmw_test:chain_rmw');
654
+ await mc.ticks(10);
655
+ const v = await mc.scoreboard('#rmw', 'v');
656
+ expect(v).toBe(8);
657
+ console.log(` RMW chain: 1→2→4→8, got ${v} (expect 8) ✓`);
658
+ });
659
+ });
660
+ describe('MC Integration - New Features', () => {
661
+ test('impl-test.mcrs: Timer::new/start/tick/done works in-game', async () => {
662
+ if (!serverOnline)
663
+ return;
664
+ await mc.command('/scoreboard players set #impl done 0');
665
+ await mc.command('/scoreboard players set timer_ticks rs 0');
666
+ await mc.command('/scoreboard players set timer_active rs 0');
667
+ await mc.command('/function impl_test:__load').catch(() => { });
668
+ await mc.command('/function impl_test:test');
669
+ await mc.ticks(5);
670
+ const done = await mc.scoreboard('#impl', 'done');
671
+ const ticks = await mc.scoreboard('timer_ticks', 'rs');
672
+ expect(done).toBe(1);
673
+ expect(ticks).toBe(3);
674
+ });
675
+ test('timeout-test.mcrs: setTimeout executes after delay', async () => {
676
+ if (!serverOnline)
677
+ return;
678
+ await mc.command('/scoreboard players set #timeout fired 0');
679
+ await mc.command('/function timeout_test:__load').catch(() => { });
680
+ await mc.command('/function timeout_test:start');
681
+ await mc.ticks(10);
682
+ expect(await mc.scoreboard('#timeout', 'fired')).toBe(0);
683
+ await mc.ticks(15);
684
+ expect(await mc.scoreboard('#timeout', 'fired')).toBe(1);
685
+ });
686
+ test('interval-test.mcrs: setInterval repeats on schedule', async () => {
687
+ if (!serverOnline)
688
+ return;
689
+ await mc.command('/scoreboard players set #interval ticks 0');
690
+ await mc.command('/function interval_test:__load').catch(() => { });
691
+ await mc.command('/function interval_test:start');
692
+ await mc.ticks(70);
693
+ const count = await mc.scoreboard('#interval', 'ticks');
694
+ expect(count).toBeGreaterThanOrEqual(3);
695
+ expect(count).toBeLessThanOrEqual(3);
696
+ });
697
+ test('is-check-test.mcrs: foreach is-narrowing only matches zombie entities', async () => {
698
+ if (!serverOnline)
699
+ return;
700
+ await mc.fullReset({ clearArea: false, killEntities: true, resetScoreboards: false });
701
+ await mc.command('/scoreboard players set #is_check players 0');
702
+ await mc.command('/scoreboard players set #is_check zombies 0');
703
+ await mc.command('/function is_check_test:__load').catch(() => { });
704
+ await mc.command('/summon minecraft:zombie 0 65 0');
705
+ await mc.command('/tag @e[type=minecraft:zombie,sort=nearest,limit=1] add is_check_target');
706
+ await mc.command('/summon minecraft:armor_stand 2 65 0');
707
+ await mc.command('/tag @e[type=minecraft:armor_stand,sort=nearest,limit=1] add is_check_target');
708
+ await mc.command('/function is_check_test:check_types');
709
+ await mc.ticks(5);
710
+ const zombies = await mc.scoreboard('#is_check', 'zombies');
711
+ const players = await mc.scoreboard('#is_check', 'players');
712
+ const zombieEntities = await mc.entities('@e[type=minecraft:zombie,tag=is_check_target]');
713
+ const standEntities = await mc.entities('@e[type=minecraft:armor_stand,tag=is_check_target]');
714
+ expect(zombies).toBe(1);
715
+ expect(players).toBe(0);
716
+ expect(zombieEntities).toHaveLength(0);
717
+ expect(standEntities).toHaveLength(1);
718
+ await mc.command('/kill @e[tag=is_check_target]').catch(() => { });
719
+ });
720
+ test('event-test.mcrs: @on(PlayerDeath) compiles and loads', async () => {
721
+ if (!serverOnline)
722
+ return;
723
+ // Verify the event system compiles correctly
724
+ await mc.command('/function event_test:__load').catch(() => { });
725
+ await mc.ticks(5);
726
+ // Verify the trigger function exists
727
+ const result = await mc.command('/function event_test:trigger_fake_death');
728
+ expect(result.ok).toBe(true);
729
+ // Verify __tick exists (event dispatcher)
730
+ const tickResult = await mc.command('/function event_test:__tick').catch(() => ({ ok: false }));
731
+ expect(tickResult.ok).toBe(true);
732
+ });
394
733
  });
395
734
  //# sourceMappingURL=mc-integration.test.js.map
@@ -57,13 +57,13 @@ function validateSource(validator, source, namespace) {
57
57
  describe('MC Command Syntax Validation', () => {
58
58
  const validator = new mc_validator_1.MCCommandValidator(FIXTURE_PATH);
59
59
  test('counter example generates valid MC commands', () => {
60
- const src = fs.readFileSync(path.join(__dirname, '..', 'examples', 'counter.rs'), 'utf-8');
60
+ const src = fs.readFileSync(path.join(__dirname, '..', 'examples', 'counter.mcrs'), 'utf-8');
61
61
  const errors = validateSource(validator, src, 'counter');
62
62
  expect(errors).toHaveLength(0);
63
63
  });
64
64
  EXAMPLES.forEach(name => {
65
- test(`${name}.rs generates valid MC commands`, () => {
66
- const src = fs.readFileSync(path.join(__dirname, '..', 'examples', `${name}.rs`), 'utf-8');
65
+ test(`${name}.mcrs generates valid MC commands`, () => {
66
+ const src = fs.readFileSync(path.join(__dirname, '..', 'examples', `${name}.mcrs`), 'utf-8');
67
67
  const errors = validateSource(validator, src, name);
68
68
  if (errors.length > 0) {
69
69
  console.log('Invalid commands:', errors);
@@ -66,8 +66,8 @@ describe('NBT codec', () => {
66
66
  });
67
67
  });
68
68
  describe('Structure generator', () => {
69
- test('compiles counter.rs to a non-empty structure', () => {
70
- const filePath = 'src/examples/counter.rs';
69
+ test('compiles counter.mcrs to a non-empty structure', () => {
70
+ const filePath = 'src/examples/counter.mcrs';
71
71
  const src = fs.readFileSync(filePath, 'utf-8');
72
72
  const { buffer, blockCount } = (0, structure_1.compileToStructure)(src, 'counter', filePath);
73
73
  expect(buffer.length).toBeGreaterThan(100);
@@ -26,11 +26,11 @@ fn turret_tick() {
26
26
  const result = (0, index_1.compile)(source, { namespace: 'test' });
27
27
  const parent = getFileContent(result.files, 'data/test/function/turret_tick.mcfunction');
28
28
  const loopBody = getFileContent(result.files, 'data/test/function/turret_tick/foreach_0.mcfunction');
29
- const hoistedRead = 'execute store result score $t0 rs run scoreboard players get config turret_range';
29
+ const hoistedRead = 'execute store result score $_0 rs run scoreboard players get config test.turret_range';
30
30
  const executeCall = 'execute as @e[tag=turret] run function test:turret_tick/foreach_0';
31
31
  expect(parent).toContain(hoistedRead);
32
32
  expect(parent.indexOf(hoistedRead)).toBeLessThan(parent.indexOf(executeCall));
33
- expect(loopBody).not.toContain('scoreboard players get config turret_range');
33
+ expect(loopBody).not.toContain('scoreboard players get config test.turret_range');
34
34
  });
35
35
  });
36
36
  describe('CSE', () => {
@@ -46,9 +46,9 @@ fn read_twice() {
46
46
  `;
47
47
  const result = (0, index_1.compile)(source, { namespace: 'test' });
48
48
  const fn = getFileContent(result.files, 'data/test/function/read_twice.mcfunction');
49
- const readMatches = fn.match(/scoreboard players get @s coins/g) ?? [];
49
+ const readMatches = fn.match(/scoreboard players get @s test\.coins/g) ?? [];
50
50
  expect(readMatches).toHaveLength(1);
51
- expect(fn).toContain('scoreboard players operation $t1 rs = $t0 rs');
51
+ expect(fn).toContain('scoreboard players operation $_1 rs = $_0 rs');
52
52
  });
53
53
  test('reuses duplicate arithmetic sequences', () => {
54
54
  const source = `
@@ -65,7 +65,7 @@ fn math() {
65
65
  const fn = getFileContent(result.files, 'data/test/function/math.mcfunction');
66
66
  const addMatches = fn.match(/\+= \$const_2 rs/g) ?? [];
67
67
  expect(addMatches).toHaveLength(1);
68
- expect(fn).toContain('scoreboard players operation $t1 rs = $t0 rs');
68
+ expect(fn).toContain('scoreboard players operation $_1 rs = $_0 rs');
69
69
  });
70
70
  });
71
71
  describe('setblock batching', () => {