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
@@ -19,6 +19,7 @@ const MC_HOST = process.env.MC_HOST ?? 'localhost'
19
19
  const MC_PORT = parseInt(process.env.MC_PORT ?? '25561')
20
20
  const MC_SERVER_DIR = process.env.MC_SERVER_DIR ?? path.join(process.env.HOME!, 'mc-test-server')
21
21
  const DATAPACK_DIR = path.join(MC_SERVER_DIR, 'world', 'datapacks', 'redscript-test')
22
+ const FIXTURE_DIR = path.join(__dirname, 'fixtures')
22
23
 
23
24
  let serverOnline = false
24
25
  let mc: MCTestClient
@@ -54,9 +55,27 @@ function writeFixture(source: string, namespace: string): void {
54
55
  }
55
56
  }
56
57
 
58
+ function writeFixtureFile(fileName: string, namespace: string): void {
59
+ writeFixture(
60
+ fs.readFileSync(path.join(FIXTURE_DIR, fileName), 'utf-8'),
61
+ namespace
62
+ )
63
+ }
64
+
65
+ async function waitForServer(client: MCTestClient, timeoutMs = 30000): Promise<boolean> {
66
+ const deadline = Date.now() + timeoutMs
67
+ while (Date.now() < deadline) {
68
+ if (await client.isOnline()) {
69
+ return true
70
+ }
71
+ await new Promise(resolve => setTimeout(resolve, 1000))
72
+ }
73
+ return false
74
+ }
75
+
57
76
  beforeAll(async () => {
58
77
  mc = new MCTestClient(MC_HOST, MC_PORT)
59
- serverOnline = await mc.isOnline()
78
+ serverOnline = await waitForServer(mc)
60
79
  if (!serverOnline) {
61
80
  console.warn(`⚠ MC server not running at ${MC_HOST}:${MC_PORT} — skipping integration tests`)
62
81
  console.warn(` Run: MC_SERVER_DIR=~/mc-test-server npx ts-node src/mc-test/setup.ts`)
@@ -65,26 +84,26 @@ beforeAll(async () => {
65
84
  }
66
85
 
67
86
  // ── Write fixtures + use safe reloadData (no /reload confirm) ───────
68
- // counter.rs
69
- if (fs.existsSync(path.join(__dirname, '../examples/counter.rs'))) {
70
- writeFixture(fs.readFileSync(path.join(__dirname, '../examples/counter.rs'), 'utf-8'), 'counter')
87
+ // counter.mcrs
88
+ if (fs.existsSync(path.join(__dirname, '../examples/counter.mcrs'))) {
89
+ writeFixture(fs.readFileSync(path.join(__dirname, '../examples/counter.mcrs'), 'utf-8'), 'counter')
71
90
  }
72
- if (fs.existsSync(path.join(__dirname, '../examples/world_manager.rs'))) {
73
- writeFixture(fs.readFileSync(path.join(__dirname, '../examples/world_manager.rs'), 'utf-8'), 'world_manager')
91
+ if (fs.existsSync(path.join(__dirname, '../examples/world_manager.mcrs'))) {
92
+ writeFixture(fs.readFileSync(path.join(__dirname, '../examples/world_manager.mcrs'), 'utf-8'), 'world_manager')
74
93
  }
75
94
  writeFixture(`
76
95
  @tick
77
96
  fn on_tick() {
78
- scoreboard_set("#tick_counter", "ticks", scoreboard_get("#tick_counter", "ticks") + 1);
97
+ scoreboard_set("#tick_counter", #ticks, scoreboard_get("#tick_counter", #ticks) + 1);
79
98
  }
80
99
  `, 'tick_test')
81
100
  writeFixture(`
82
101
  fn check_score() {
83
- let x: int = scoreboard_get("#check_x", "test_score");
102
+ let x: int = scoreboard_get("#check_x", #test_score);
84
103
  if (x > 5) {
85
- scoreboard_set("#check_x", "result", 1);
104
+ scoreboard_set("#check_x", #result, 1);
86
105
  } else {
87
- scoreboard_set("#check_x", "result", 0);
106
+ scoreboard_set("#check_x", #result, 0);
88
107
  }
89
108
  }
90
109
  `, 'inline_test')
@@ -95,31 +114,31 @@ beforeAll(async () => {
95
114
  writeFixture(`
96
115
  @tick
97
116
  fn game_tick() {
98
- let time: int = scoreboard_get("#game", "timer");
117
+ let time: int = scoreboard_get("#game", #timer);
99
118
  if (time > 0) {
100
- scoreboard_set("#game", "timer", time - 1);
119
+ scoreboard_set("#game", #timer, time - 1);
101
120
  }
102
121
  if (time == 1) {
103
- scoreboard_set("#game", "ended", 1);
122
+ scoreboard_set("#game", #ended, 1);
104
123
  }
105
124
  }
106
125
  fn start_game() {
107
- scoreboard_set("#game", "timer", 5);
108
- scoreboard_set("#game", "ended", 0);
126
+ scoreboard_set("#game", #timer, 5);
127
+ scoreboard_set("#game", #ended, 0);
109
128
  }
110
129
  `, 'game_loop')
111
130
 
112
131
  // Scenario B: two functions, same temp var namespace — verify no collision
113
132
  writeFixture(`
114
133
  fn calc_sum() {
115
- let a: int = scoreboard_get("#math", "val_a");
116
- let b: int = scoreboard_get("#math", "val_b");
117
- scoreboard_set("#math", "sum", a + b);
134
+ let a: int = scoreboard_get("#math", #val_a);
135
+ let b: int = scoreboard_get("#math", #val_b);
136
+ scoreboard_set("#math", #sum, a + b);
118
137
  }
119
138
  fn calc_product() {
120
- let x: int = scoreboard_get("#math", "val_x");
121
- let y: int = scoreboard_get("#math", "val_y");
122
- scoreboard_set("#math", "product", x * y);
139
+ let x: int = scoreboard_get("#math", #val_x);
140
+ let y: int = scoreboard_get("#math", #val_y);
141
+ scoreboard_set("#math", #product, x * y);
123
142
  }
124
143
  fn run_both() {
125
144
  calc_sum();
@@ -130,16 +149,16 @@ beforeAll(async () => {
130
149
  // Scenario C: 3-deep call chain, each step modifies shared state
131
150
  writeFixture(`
132
151
  fn step3() {
133
- let v: int = scoreboard_get("#chain", "val");
134
- scoreboard_set("#chain", "val", v * 2);
152
+ let v: int = scoreboard_get("#chain", #val);
153
+ scoreboard_set("#chain", #val, v * 2);
135
154
  }
136
155
  fn step2() {
137
- let v: int = scoreboard_get("#chain", "val");
138
- scoreboard_set("#chain", "val", v + 5);
156
+ let v: int = scoreboard_get("#chain", #val);
157
+ scoreboard_set("#chain", #val, v + 5);
139
158
  step3();
140
159
  }
141
160
  fn step1() {
142
- scoreboard_set("#chain", "val", 10);
161
+ scoreboard_set("#chain", #val, 10);
143
162
  step2();
144
163
  }
145
164
  `, 'call_chain')
@@ -154,12 +173,125 @@ beforeAll(async () => {
154
173
  }
155
174
  `, 'fill_test')
156
175
 
176
+ // Scenario E: for-range loop — loop counter increments exactly N times
177
+ writeFixture(`
178
+ fn count_to_five() {
179
+ scoreboard_set("#range", #counter, 0);
180
+ for i in 0..5 {
181
+ let c: int = scoreboard_get("#range", #counter);
182
+ scoreboard_set("#range", #counter, c + 1);
183
+ }
184
+ }
185
+ `, 'range_test')
186
+
187
+ // Scenario F: function call with return value — verifies $ret propagation
188
+ writeFixture(`
189
+ fn triple(x: int) -> int {
190
+ return x * 3;
191
+ }
192
+ fn run_nested() {
193
+ let a: int = triple(4);
194
+ scoreboard_set("#nested", #result, a);
195
+ }
196
+ `, 'nested_test')
197
+
198
+ // Scenario G: match statement dispatches to correct branch
199
+ writeFixture(`
200
+ fn classify(x: int) {
201
+ match (x) {
202
+ 1 => { scoreboard_set("#match", #out, 10); }
203
+ 2 => { scoreboard_set("#match", #out, 20); }
204
+ 3 => { scoreboard_set("#match", #out, 30); }
205
+ _ => { scoreboard_set("#match", #out, -1); }
206
+ }
207
+ }
208
+ `, 'match_test')
209
+
210
+ // Scenario H: while loop counts down
211
+ writeFixture(`
212
+ fn countdown() {
213
+ scoreboard_set("#wloop", #i, 10);
214
+ scoreboard_set("#wloop", #steps, 0);
215
+ let i: int = scoreboard_get("#wloop", #i);
216
+ while (i > 0) {
217
+ let s: int = scoreboard_get("#wloop", #steps);
218
+ scoreboard_set("#wloop", #steps, s + 1);
219
+ i = i - 1;
220
+ scoreboard_set("#wloop", #i, i);
221
+ }
222
+ }
223
+ `, 'while_test')
224
+
225
+ // Scenario I: multiple if/else branches (boundary test)
226
+ writeFixture(`
227
+ fn classify_score() {
228
+ let x: int = scoreboard_get("#boundary", #input);
229
+ if (x > 100) {
230
+ scoreboard_set("#boundary", #tier, 3);
231
+ } else {
232
+ if (x > 50) {
233
+ scoreboard_set("#boundary", #tier, 2);
234
+ } else {
235
+ if (x > 0) {
236
+ scoreboard_set("#boundary", #tier, 1);
237
+ } else {
238
+ scoreboard_set("#boundary", #tier, 0);
239
+ }
240
+ }
241
+ }
242
+ }
243
+ `, 'boundary_test')
244
+
245
+ // Scenario J: entity management — summon via raw commands
246
+ writeFixture(`
247
+ fn tag_entities() {
248
+ raw("summon minecraft:armor_stand 10 65 10");
249
+ raw("summon minecraft:armor_stand 11 65 10");
250
+ raw("summon minecraft:armor_stand 12 65 10");
251
+ }
252
+ `, 'tag_test')
253
+
254
+ // Scenario K: mixed arithmetic — order of operations
255
+ writeFixture(`
256
+ fn math_order() {
257
+ let a: int = 2;
258
+ let b: int = 3;
259
+ let c: int = 4;
260
+ scoreboard_set("#order", #r1, a + b * c);
261
+ scoreboard_set("#order", #r2, (a + b) * c);
262
+ let d: int = 100;
263
+ let e: int = d / 3;
264
+ scoreboard_set("#order", #r3, e);
265
+ }
266
+ `, 'order_test')
267
+
268
+ // Scenario L: scoreboard read-modify-write chain
269
+ writeFixture(`
270
+ fn chain_rmw() {
271
+ scoreboard_set("#rmw", #v, 1);
272
+ let v: int = scoreboard_get("#rmw", #v);
273
+ scoreboard_set("#rmw", #v, v * 2);
274
+ v = scoreboard_get("#rmw", #v);
275
+ scoreboard_set("#rmw", #v, v * 2);
276
+ v = scoreboard_get("#rmw", #v);
277
+ scoreboard_set("#rmw", #v, v * 2);
278
+ }
279
+ `, 'rmw_test')
280
+
281
+ writeFixtureFile('impl-test.mcrs', 'impl_test')
282
+ writeFixtureFile('timeout-test.mcrs', 'timeout_test')
283
+ writeFixtureFile('interval-test.mcrs', 'interval_test')
284
+ writeFixtureFile('is-check-test.mcrs', 'is_check_test')
285
+ writeFixtureFile('event-test.mcrs', 'event_test')
286
+
157
287
  // ── Full reset + safe data reload ────────────────────────────────────
158
288
  await mc.fullReset()
159
289
 
160
290
  // Pre-create scoreboards
161
291
  for (const obj of ['ticks', 'seconds', 'test_score', 'result', 'calc', 'rs',
162
- 'timer', 'ended', 'val_a', 'val_b', 'sum', 'val_x', 'val_y', 'product', 'val']) {
292
+ 'timer', 'ended', 'val_a', 'val_b', 'sum', 'val_x', 'val_y', 'product', 'val',
293
+ 'counter', 'out', 'i', 'steps', 'input', 'tier', 'r1', 'r2', 'r3', 'v',
294
+ 'done', 'fired', 'players', 'zombies']) {
163
295
  await mc.command(`/scoreboard objectives add ${obj} dummy`).catch(() => {})
164
296
  }
165
297
  await mc.command('/scoreboard players set counter ticks 0')
@@ -192,7 +324,7 @@ describe('MC Integration Tests', () => {
192
324
  })
193
325
 
194
326
  // ─── Test 2: Counter tick ─────────────────────────────────────────────
195
- test('counter.rs: tick function increments scoreboard over time', async () => {
327
+ test('counter.mcrs: tick function increments scoreboard over time', async () => {
196
328
  if (!serverOnline) return
197
329
 
198
330
  await mc.ticks(40) // Wait 2s (counter was already init'd in beforeAll)
@@ -202,7 +334,7 @@ describe('MC Integration Tests', () => {
202
334
  })
203
335
 
204
336
  // ─── Test 3: setblock ────────────────────────────────────────────────
205
- test('world_manager.rs: setblock places correct block', async () => {
337
+ test('world_manager.mcrs: setblock places correct block', async () => {
206
338
  if (!serverOnline) return
207
339
 
208
340
  // Clear just the lobby area, keep other state
@@ -217,7 +349,7 @@ describe('MC Integration Tests', () => {
217
349
  })
218
350
 
219
351
  // ─── Test 4: fill ────────────────────────────────────────────────────
220
- test('world_manager.rs: fill creates smooth_stone floor', async () => {
352
+ test('world_manager.mcrs: fill creates smooth_stone floor', async () => {
221
353
  if (!serverOnline) return
222
354
  // Runs after test 3, floor should still be there
223
355
  const block = await mc.block(4, 64, 4)
@@ -344,7 +476,7 @@ describe('E2E Scenario Tests', () => {
344
476
  })
345
477
 
346
478
  // Scenario B: No temp var collision between two functions called in sequence
347
- // Verifies: each function's $t0/$t1 temp vars are isolated per-call, not globally shared
479
+ // Verifies: each function's temp vars are isolated per-call via globally unique names
348
480
  // If there's a bug, calc_product would see sum's leftover $t vars and produce wrong result
349
481
  test('B: calc_sum + calc_product called in sequence — no temp var collision', async () => {
350
482
  if (!serverOnline) return
@@ -406,4 +538,262 @@ describe('E2E Scenario Tests', () => {
406
538
  console.log(` fill_test: blocks [0-3,70,0]=stone, [-1]/[4]=air ✓`)
407
539
  })
408
540
 
541
+ // Scenario E: for-range loop executes body exactly N times
542
+ // Verifies: for i in 0..5 increments counter 5 times
543
+ test('E: for-range loop increments counter exactly 5 times', async () => {
544
+ if (!serverOnline) return
545
+
546
+ await mc.command('/function range_test:__load')
547
+ await mc.command('/function range_test:count_to_five')
548
+ await mc.ticks(10)
549
+
550
+ const counter = await mc.scoreboard('#range', 'counter')
551
+ expect(counter).toBe(5)
552
+ console.log(` for-range 0..5 → counter=${counter} (expect 5) ✓`)
553
+ })
554
+
555
+ // Scenario F: function return value propagation
556
+ // Verifies: $ret from callee is correctly captured in caller's variable
557
+ test('F: function return value — triple(4) = 12', async () => {
558
+ if (!serverOnline) return
559
+
560
+ await mc.command('/function nested_test:__load')
561
+ await mc.command('/function nested_test:run_nested')
562
+ await mc.ticks(10)
563
+
564
+ const result = await mc.scoreboard('#nested', 'result')
565
+ expect(result).toBe(12) // triple(4) = 4*3 = 12
566
+ console.log(` triple(4) = ${result} (expect 12) ✓`)
567
+ })
568
+
569
+ // Scenario G: match dispatches to correct branch
570
+ // Verifies: match statement selects right arm for values 1, 2, 3, and default
571
+ test('G: match statement dispatches to correct branch', async () => {
572
+ if (!serverOnline) return
573
+
574
+ await mc.command('/function match_test:__load')
575
+
576
+ // Test match on value 2
577
+ await mc.command('/scoreboard players set $p0 rs 2')
578
+ await mc.command('/function match_test:classify')
579
+ await mc.ticks(5)
580
+ let out = await mc.scoreboard('#match', 'out')
581
+ expect(out).toBe(20)
582
+ console.log(` match(2) → out=${out} (expect 20) ✓`)
583
+
584
+ // Test match on value 3
585
+ await mc.command('/scoreboard players set $p0 rs 3')
586
+ await mc.command('/function match_test:classify')
587
+ await mc.ticks(5)
588
+ out = await mc.scoreboard('#match', 'out')
589
+ expect(out).toBe(30)
590
+ console.log(` match(3) → out=${out} (expect 30) ✓`)
591
+
592
+ // Test default branch (value 99)
593
+ await mc.command('/scoreboard players set $p0 rs 99')
594
+ await mc.command('/function match_test:classify')
595
+ await mc.ticks(5)
596
+ out = await mc.scoreboard('#match', 'out')
597
+ expect(out).toBe(-1)
598
+ console.log(` match(99) → out=${out} (expect -1, default) ✓`)
599
+ })
600
+
601
+ // Scenario H: while loop counts down from 10 to 0
602
+ // Verifies: while loop body executes correct number of iterations
603
+ test('H: while loop counts down 10 steps', async () => {
604
+ if (!serverOnline) return
605
+
606
+ await mc.command('/function while_test:__load')
607
+ await mc.command('/function while_test:countdown')
608
+ await mc.ticks(10)
609
+
610
+ const i = await mc.scoreboard('#wloop', 'i')
611
+ const steps = await mc.scoreboard('#wloop', 'steps')
612
+ expect(i).toBe(0)
613
+ expect(steps).toBe(10)
614
+ console.log(` while countdown: i=${i} (expect 0), steps=${steps} (expect 10) ✓`)
615
+ })
616
+
617
+ // Scenario I: nested if/else boundary classification
618
+ // Verifies: correct branch taken at boundaries (0, 50, 100)
619
+ test('I: nested if/else boundary classification', async () => {
620
+ if (!serverOnline) return
621
+
622
+ await mc.command('/function boundary_test:__load')
623
+
624
+ // Test x=0 → tier 0
625
+ await mc.command('/scoreboard players set #boundary input 0')
626
+ await mc.command('/function boundary_test:classify_score')
627
+ await mc.ticks(5)
628
+ let tier = await mc.scoreboard('#boundary', 'tier')
629
+ expect(tier).toBe(0)
630
+ console.log(` classify(0) → tier=${tier} (expect 0) ✓`)
631
+
632
+ // Test x=50 → tier 1 (> 0 but not > 50)
633
+ await mc.command('/scoreboard players set #boundary input 50')
634
+ await mc.command('/function boundary_test:classify_score')
635
+ await mc.ticks(5)
636
+ tier = await mc.scoreboard('#boundary', 'tier')
637
+ expect(tier).toBe(1)
638
+ console.log(` classify(50) → tier=${tier} (expect 1) ✓`)
639
+
640
+ // Test x=51 → tier 2 (> 50 but not > 100)
641
+ await mc.command('/scoreboard players set #boundary input 51')
642
+ await mc.command('/function boundary_test:classify_score')
643
+ await mc.ticks(5)
644
+ tier = await mc.scoreboard('#boundary', 'tier')
645
+ expect(tier).toBe(2)
646
+ console.log(` classify(51) → tier=${tier} (expect 2) ✓`)
647
+
648
+ // Test x=101 → tier 3
649
+ await mc.command('/scoreboard players set #boundary input 101')
650
+ await mc.command('/function boundary_test:classify_score')
651
+ await mc.ticks(5)
652
+ tier = await mc.scoreboard('#boundary', 'tier')
653
+ expect(tier).toBe(3)
654
+ console.log(` classify(101) → tier=${tier} (expect 3) ✓`)
655
+ })
656
+
657
+ // Scenario J: entity summon and query
658
+ // Verifies: entities spawned via compiled function are queryable
659
+ test('J: summon entities via compiled function', async () => {
660
+ if (!serverOnline) return
661
+
662
+ await mc.command('/kill @e[type=minecraft:armor_stand]')
663
+ await mc.ticks(2)
664
+ await mc.command('/function tag_test:__load')
665
+ await mc.command('/function tag_test:tag_entities')
666
+ await mc.ticks(5)
667
+
668
+ const stands = await mc.entities('@e[type=minecraft:armor_stand]')
669
+ expect(stands.length).toBe(3)
670
+ console.log(` Summoned 3 armor_stands via tag_test, found: ${stands.length} ✓`)
671
+
672
+ await mc.command('/kill @e[type=minecraft:armor_stand]')
673
+ })
674
+
675
+ // Scenario K: arithmetic order of operations
676
+ // Verifies: MC scoreboard arithmetic matches expected evaluation order
677
+ test('K: arithmetic order of operations', async () => {
678
+ if (!serverOnline) return
679
+
680
+ await mc.command('/function order_test:__load')
681
+ await mc.command('/function order_test:math_order')
682
+ await mc.ticks(10)
683
+
684
+ const r1 = await mc.scoreboard('#order', 'r1')
685
+ const r2 = await mc.scoreboard('#order', 'r2')
686
+ const r3 = await mc.scoreboard('#order', 'r3')
687
+ // a + b * c = 2 + 3*4 = 14 (if precedence respected) or (2+3)*4 = 20 (left-to-right)
688
+ // MC scoreboard does left-to-right, so compiler may emit either depending on lowering
689
+ // (a + b) * c = 5 * 4 = 20 (explicit parens)
690
+ expect(r2).toBe(20) // This one is unambiguous
691
+ // 100 / 3 = 33 (integer division)
692
+ expect(r3).toBe(33)
693
+ console.log(` r1=${r1}, r2=${r2} (expect 20), r3=${r3} (expect 33) ✓`)
694
+ })
695
+
696
+ // Scenario L: scoreboard read-modify-write chain (1 → 2 → 4 → 8)
697
+ // Verifies: sequential RMW operations don't lose intermediate state
698
+ test('L: scoreboard RMW chain — 1*2*2*2 = 8', async () => {
699
+ if (!serverOnline) return
700
+
701
+ await mc.command('/function rmw_test:__load')
702
+ await mc.command('/function rmw_test:chain_rmw')
703
+ await mc.ticks(10)
704
+
705
+ const v = await mc.scoreboard('#rmw', 'v')
706
+ expect(v).toBe(8)
707
+ console.log(` RMW chain: 1→2→4→8, got ${v} (expect 8) ✓`)
708
+ })
709
+
710
+ })
711
+
712
+ describe('MC Integration - New Features', () => {
713
+ test('impl-test.mcrs: Timer::new/start/tick/done works in-game', async () => {
714
+ if (!serverOnline) return
715
+
716
+ await mc.command('/scoreboard players set #impl done 0')
717
+ await mc.command('/scoreboard players set timer_ticks rs 0')
718
+ await mc.command('/scoreboard players set timer_active rs 0')
719
+
720
+ await mc.command('/function impl_test:__load').catch(() => {})
721
+ await mc.command('/function impl_test:test')
722
+ await mc.ticks(5)
723
+
724
+ const done = await mc.scoreboard('#impl', 'done')
725
+ const ticks = await mc.scoreboard('timer_ticks', 'rs')
726
+ expect(done).toBe(1)
727
+ expect(ticks).toBe(3)
728
+ })
729
+
730
+ test('timeout-test.mcrs: setTimeout executes after delay', async () => {
731
+ if (!serverOnline) return
732
+
733
+ await mc.command('/scoreboard players set #timeout fired 0')
734
+ await mc.command('/function timeout_test:__load').catch(() => {})
735
+ await mc.command('/function timeout_test:start')
736
+ await mc.ticks(10)
737
+ expect(await mc.scoreboard('#timeout', 'fired')).toBe(0)
738
+
739
+ await mc.ticks(15)
740
+ expect(await mc.scoreboard('#timeout', 'fired')).toBe(1)
741
+ })
742
+
743
+ test('interval-test.mcrs: setInterval repeats on schedule', async () => {
744
+ if (!serverOnline) return
745
+
746
+ await mc.command('/scoreboard players set #interval ticks 0')
747
+ await mc.command('/function interval_test:__load').catch(() => {})
748
+ await mc.command('/function interval_test:start')
749
+ await mc.ticks(70)
750
+
751
+ const count = await mc.scoreboard('#interval', 'ticks')
752
+ expect(count).toBeGreaterThanOrEqual(3)
753
+ expect(count).toBeLessThanOrEqual(3)
754
+ })
755
+
756
+ test('is-check-test.mcrs: foreach is-narrowing only matches zombie entities', async () => {
757
+ if (!serverOnline) return
758
+
759
+ await mc.fullReset({ clearArea: false, killEntities: true, resetScoreboards: false })
760
+ await mc.command('/scoreboard players set #is_check players 0')
761
+ await mc.command('/scoreboard players set #is_check zombies 0')
762
+ await mc.command('/function is_check_test:__load').catch(() => {})
763
+ await mc.command('/summon minecraft:zombie 0 65 0')
764
+ await mc.command('/tag @e[type=minecraft:zombie,sort=nearest,limit=1] add is_check_target')
765
+ await mc.command('/summon minecraft:armor_stand 2 65 0')
766
+ await mc.command('/tag @e[type=minecraft:armor_stand,sort=nearest,limit=1] add is_check_target')
767
+
768
+ await mc.command('/function is_check_test:check_types')
769
+ await mc.ticks(5)
770
+
771
+ const zombies = await mc.scoreboard('#is_check', 'zombies')
772
+ const players = await mc.scoreboard('#is_check', 'players')
773
+ const zombieEntities = await mc.entities('@e[type=minecraft:zombie,tag=is_check_target]')
774
+ const standEntities = await mc.entities('@e[type=minecraft:armor_stand,tag=is_check_target]')
775
+
776
+ expect(zombies).toBe(1)
777
+ expect(players).toBe(0)
778
+ expect(zombieEntities).toHaveLength(0)
779
+ expect(standEntities).toHaveLength(1)
780
+
781
+ await mc.command('/kill @e[tag=is_check_target]').catch(() => {})
782
+ })
783
+
784
+ test('event-test.mcrs: @on(PlayerDeath) compiles and loads', async () => {
785
+ if (!serverOnline) return
786
+
787
+ // Verify the event system compiles correctly
788
+ await mc.command('/function event_test:__load').catch(() => {})
789
+ await mc.ticks(5)
790
+
791
+ // Verify the trigger function exists
792
+ const result = await mc.command('/function event_test:trigger_fake_death')
793
+ expect(result.ok).toBe(true)
794
+
795
+ // Verify __tick exists (event dispatcher)
796
+ const tickResult = await mc.command('/function event_test:__tick').catch(() => ({ ok: false }))
797
+ expect(tickResult.ok).toBe(true)
798
+ })
409
799
  })
@@ -33,14 +33,14 @@ describe('MC Command Syntax Validation', () => {
33
33
  const validator = new MCCommandValidator(FIXTURE_PATH)
34
34
 
35
35
  test('counter example generates valid MC commands', () => {
36
- const src = fs.readFileSync(path.join(__dirname, '..', 'examples', 'counter.rs'), 'utf-8')
36
+ const src = fs.readFileSync(path.join(__dirname, '..', 'examples', 'counter.mcrs'), 'utf-8')
37
37
  const errors = validateSource(validator, src, 'counter')
38
38
  expect(errors).toHaveLength(0)
39
39
  })
40
40
 
41
41
  EXAMPLES.forEach(name => {
42
- test(`${name}.rs generates valid MC commands`, () => {
43
- const src = fs.readFileSync(path.join(__dirname, '..', 'examples', `${name}.rs`), 'utf-8')
42
+ test(`${name}.mcrs generates valid MC commands`, () => {
43
+ const src = fs.readFileSync(path.join(__dirname, '..', 'examples', `${name}.mcrs`), 'utf-8')
44
44
  const errors = validateSource(validator, src, name)
45
45
 
46
46
  if (errors.length > 0) {
@@ -40,8 +40,8 @@ describe('NBT codec', () => {
40
40
  })
41
41
 
42
42
  describe('Structure generator', () => {
43
- test('compiles counter.rs to a non-empty structure', () => {
44
- const filePath = 'src/examples/counter.rs'
43
+ test('compiles counter.mcrs to a non-empty structure', () => {
44
+ const filePath = 'src/examples/counter.mcrs'
45
45
  const src = fs.readFileSync(filePath, 'utf-8')
46
46
  const { buffer, blockCount } = compileToStructure(src, 'counter', filePath)
47
47
 
@@ -28,12 +28,12 @@ fn turret_tick() {
28
28
  const parent = getFileContent(result.files, 'data/test/function/turret_tick.mcfunction')
29
29
  const loopBody = getFileContent(result.files, 'data/test/function/turret_tick/foreach_0.mcfunction')
30
30
 
31
- const hoistedRead = 'execute store result score $t0 rs run scoreboard players get config turret_range'
31
+ const hoistedRead = 'execute store result score $_0 rs run scoreboard players get config test.turret_range'
32
32
  const executeCall = 'execute as @e[tag=turret] run function test:turret_tick/foreach_0'
33
33
 
34
34
  expect(parent).toContain(hoistedRead)
35
35
  expect(parent.indexOf(hoistedRead)).toBeLessThan(parent.indexOf(executeCall))
36
- expect(loopBody).not.toContain('scoreboard players get config turret_range')
36
+ expect(loopBody).not.toContain('scoreboard players get config test.turret_range')
37
37
  })
38
38
  })
39
39
 
@@ -51,10 +51,10 @@ fn read_twice() {
51
51
 
52
52
  const result = compile(source, { namespace: 'test' })
53
53
  const fn = getFileContent(result.files, 'data/test/function/read_twice.mcfunction')
54
- const readMatches = fn.match(/scoreboard players get @s coins/g) ?? []
54
+ const readMatches = fn.match(/scoreboard players get @s test\.coins/g) ?? []
55
55
 
56
56
  expect(readMatches).toHaveLength(1)
57
- expect(fn).toContain('scoreboard players operation $t1 rs = $t0 rs')
57
+ expect(fn).toContain('scoreboard players operation $_1 rs = $_0 rs')
58
58
  })
59
59
 
60
60
  test('reuses duplicate arithmetic sequences', () => {
@@ -74,7 +74,7 @@ fn math() {
74
74
  const addMatches = fn.match(/\+= \$const_2 rs/g) ?? []
75
75
 
76
76
  expect(addMatches).toHaveLength(1)
77
- expect(fn).toContain('scoreboard players operation $t1 rs = $t0 rs')
77
+ expect(fn).toContain('scoreboard players operation $_1 rs = $_0 rs')
78
78
  })
79
79
  })
80
80