@kradle/cli 0.2.1 → 0.2.3

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.
@@ -0,0 +1,1468 @@
1
+ # LLM_README.md - @kradle/challenges API Reference
2
+
3
+ This document provides exhaustive API documentation for AI agents using the `@kradle/challenges` package. This is the complete reference for creating Minecraft datapack-based challenges.
4
+
5
+ ## Table of Contents
6
+
7
+ 1. [Overview](#overview)
8
+ 2. [Installation](#installation)
9
+ 3. [Core Concepts](#core-concepts)
10
+ 4. [createChallenge API](#createchallenge-api)
11
+ 5. [Variables](#variables)
12
+ 6. [Events](#events)
13
+ 7. [Actions](#actions)
14
+ 8. [Utilities](#utilities)
15
+ 9. [Sandstone Integration](#sandstone-integration)
16
+ 10. [Complete Examples](#complete-examples)
17
+ 11. [Common Patterns](#common-patterns)
18
+
19
+ ---
20
+
21
+ ## Overview
22
+
23
+ `@kradle/challenges` is a TypeScript framework for creating Minecraft challenges that compile to datapacks. It provides:
24
+
25
+ - **Variable System**: Track per-player and global game state with automatic tick updates
26
+ - **Event System**: Lifecycle hooks and custom event triggers based on scores/advancements
27
+ - **Actions Library**: Pre-built game operations (give items, teleport, announce, etc.)
28
+ - **Role Management**: Assign players to teams with role-specific win conditions
29
+ - **Sandstone Integration**: Full access to Sandstone's Minecraft command generation
30
+
31
+ **Key Principle**: Challenges are defined declaratively. You specify variables, events, and conditions - the framework generates the datapack.
32
+
33
+ ---
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ npm install @kradle/challenges sandstone@0.14.0-alpha.13
39
+ ```
40
+
41
+ **Requirements:**
42
+ - Node.js >= 22.18.0
43
+ - Sandstone 0.14.0-alpha.13 (peer dependency)
44
+
45
+ ---
46
+
47
+ ## Core Concepts
48
+
49
+ ### Ticks
50
+
51
+ Minecraft runs at 20 ticks per second. Time values in this API are in ticks:
52
+ - 1 second = 20 ticks
53
+ - 1 minute = 1200 ticks (60 * 20)
54
+ - 5 minutes = 6000 ticks
55
+
56
+ ### Namespace & Item IDs
57
+
58
+ **IMPORTANT:** Most Actions require the full `minecraft:` prefix for items, blocks, and entities:
59
+ - ✅ Correct: `item: "minecraft:diamond_sword"`
60
+ - ❌ Wrong: `item: "diamond_sword"`
61
+
62
+ The only exceptions are when the API explicitly accepts an item without the namespace (check each Action's documentation).
63
+
64
+ **Examples:**
65
+ - Items: `"minecraft:diamond"`, `"minecraft:iron_sword"`, `"minecraft:cooked_beef"`
66
+ - Blocks: `"minecraft:stone"`, `"minecraft:diamond_block"`
67
+ - Entities: `"minecraft:zombie"`, `"minecraft:pig"`, `"minecraft:creeper"`
68
+
69
+ ### Scores
70
+
71
+ All variables are backed by Minecraft scoreboards. The `Score` type from Sandstone represents a scoreboard value. Scores support comparison methods:
72
+ - `score.equalTo(value)` / `score.equalTo(otherScore)`
73
+ - `score.greaterThan(value)`
74
+ - `score.greaterOrEqualThan(value)`
75
+ - `score.lowerThan(value)`
76
+ - `score.lowerOrEqualThan(value)`
77
+
78
+ ### Roles
79
+
80
+ Roles define player groups (e.g., teams). Each role can have different win conditions. All players assigned to a role share that role's win condition.
81
+
82
+ ---
83
+
84
+ ## createChallenge API
85
+
86
+ ### Required Imports
87
+
88
+ ```typescript
89
+ import { createChallenge, Actions, forEveryPlayer } from "@kradle/challenges";
90
+ import { _, execute, Selector, rel, abs } from "sandstone";
91
+ import type { Score } from "sandstone";
92
+ ```
93
+
94
+ ### Signature
95
+
96
+ ```typescript
97
+ function createChallenge<
98
+ ROLES extends readonly string[],
99
+ VARIABLES extends Record<string, _InputVariableType>
100
+ >(config: _BaseConfig<ROLES, VARIABLES>): ChallengeBuilder
101
+ ```
102
+
103
+ ### Configuration Object
104
+
105
+ ```typescript
106
+ interface _BaseConfig<ROLES, VARIABLES> {
107
+ // Required: Challenge name (used for datapack namespace)
108
+ name: string;
109
+
110
+ // Required: Output path for generated datapack
111
+ kradle_challenge_path: string;
112
+
113
+ // Required: Player roles as readonly tuple
114
+ // Example: ["attacker", "defender"] as const
115
+ roles: ROLES;
116
+
117
+ // Required: Custom variable definitions
118
+ custom_variables: VARIABLES;
119
+
120
+ // Optional: Game duration in ticks (default: 6000 = 5 minutes)
121
+ GAME_DURATION?: number;
122
+ }
123
+ ```
124
+
125
+ ### Builder Methods
126
+
127
+ The builder uses a fluent API. Methods must be called in order:
128
+
129
+ ```typescript
130
+ createChallenge(config)
131
+ .events(eventCallback) // Define lifecycle events
132
+ .custom_events(customEventCallback) // Define triggered events
133
+ .end_condition(endConditionCallback) // Define game end condition
134
+ .win_conditions(winConditionsCallback) // Define win conditions (triggers build)
135
+ ```
136
+
137
+ #### `.events(callback)`
138
+
139
+ ```typescript
140
+ .events((variables: Variables, roles: Roles) => {
141
+ return {
142
+ start_challenge?: () => void; // Runs once when game starts
143
+ init_participants?: () => void; // Runs once per player after start
144
+ on_tick?: () => void; // Runs every tick for each player
145
+ end_challenge?: () => void; // Runs once when game ends
146
+ };
147
+ })
148
+ ```
149
+
150
+ #### `.custom_events(callback)`
151
+
152
+ ```typescript
153
+ .custom_events((variables: Variables, roles: Roles) => {
154
+ return Array<ScoreEvent | AdvancementEvent>;
155
+ })
156
+ ```
157
+
158
+ **Score Event:**
159
+ ```typescript
160
+ {
161
+ score: Score; // Variable to watch
162
+ target: number; // Threshold value
163
+ mode: "fire_once" | "repeatable";
164
+ actions: () => void; // Actions to execute
165
+ }
166
+ ```
167
+
168
+ **Advancement Event:**
169
+ ```typescript
170
+ {
171
+ criteria: Array<{ trigger: string; conditions?: object }>;
172
+ mode: "fire_once" | "repeatable";
173
+ actions: () => void;
174
+ }
175
+ ```
176
+
177
+ #### `.end_condition(callback)`
178
+
179
+ ```typescript
180
+ .end_condition((variables: Variables, roles: Roles) => {
181
+ return Condition; // Returns a Sandstone condition
182
+ })
183
+ ```
184
+
185
+ #### `.win_conditions(callback)`
186
+
187
+ ```typescript
188
+ .win_conditions((variables: Variables, roles: Roles) => {
189
+ return {
190
+ [roleName: string]: Condition; // One condition per role
191
+ };
192
+ })
193
+ ```
194
+
195
+ ---
196
+
197
+ ## Variables
198
+
199
+ ### Built-in Variables
200
+
201
+ These are always available in every challenge:
202
+
203
+ | Variable | Type | Description |
204
+ |----------|------|-------------|
205
+ | `death_count` | individual | Player's death count (Minecraft `deathCount` objective) |
206
+ | `has_never_died` | individual | 1 if player has never died, 0 otherwise |
207
+ | `alive_players` | global | Count of living participants |
208
+ | `main_score` | individual | Primary score shown on sidebar |
209
+ | `game_timer` | global | Ticks since game start (increments each tick) |
210
+ | `game_state` | global | 0=CREATED, 1=OFF, 2=ON |
211
+ | `player_count` | global | Total participant count |
212
+ | `player_number` | individual | Unique player ID (1 to N) |
213
+
214
+ ### Custom Variable Definition
215
+
216
+ ```typescript
217
+ custom_variables: {
218
+ variable_name: {
219
+ type: "individual" | "global";
220
+ objective_type?: string; // Minecraft objective criterion
221
+ hidden?: boolean; // Hide from scoreboard (global only)
222
+ default?: number; // Initial value
223
+ updater?: UpdaterFunction;
224
+ }
225
+ }
226
+ ```
227
+
228
+ ### Variable Types
229
+
230
+ #### Individual Objective-Based
231
+
232
+ Automatically tracks Minecraft statistics:
233
+
234
+ ```typescript
235
+ pigs_killed: {
236
+ type: "individual",
237
+ objective_type: "minecraft.killed:minecraft.pig",
238
+ default: 0,
239
+ }
240
+ ```
241
+
242
+ Common objective types:
243
+ - `"minecraft.killed:minecraft.<entity>"` - Entities killed
244
+ - `"minecraft.killed_by:minecraft.<entity>"` - Killed by entity
245
+ - `"minecraft.picked_up:minecraft.<item>"` - Items picked up
246
+ - `"minecraft.mined:minecraft.<block>"` - Blocks mined
247
+ - `"minecraft.used:minecraft.<item>"` - Items used
248
+ - `"minecraft.crafted:minecraft.<item>"` - Items crafted
249
+ - `"playerKillCount"` - PvP kills
250
+ - `"deathCount"` - Deaths
251
+ - `"dummy"` - Manual/computed value
252
+
253
+ You can find objectives definition on the [Scoreboard](https://minecraft.fandom.com/wiki/Scoreboard) wiki pagem as well as the [Statistics](https://minecraft.fandom.com/wiki/Statistics) page for more in-depth explanation.
254
+
255
+ #### Individual Dummy
256
+
257
+ Computed per-player values:
258
+
259
+ ```typescript
260
+ current_y_position: {
261
+ type: "individual",
262
+ objective_type: "dummy",
263
+ updater: (value) => {
264
+ execute.as("@s").store.result.score(value).run.data.get.entity("@s", "Pos[1]");
265
+ },
266
+ }
267
+ ```
268
+
269
+ #### Global Variables
270
+
271
+ Shared across all players:
272
+
273
+ ```typescript
274
+ max_score_global: {
275
+ type: "global",
276
+ hidden: false,
277
+ default: 0,
278
+ updater: (value, { main_score }) => {
279
+ value.set(0);
280
+ forEveryPlayer(() => {
281
+ _.if(main_score.greaterThan(value), () => {
282
+ value.set(main_score);
283
+ });
284
+ });
285
+ },
286
+ }
287
+ ```
288
+
289
+ ### Updater Function Signature
290
+
291
+ ```typescript
292
+ type UpdaterFunction = (
293
+ value: Score, // Current variable's score
294
+ variables: Record<string, Score> // All variables including built-ins
295
+ ) => void;
296
+ ```
297
+
298
+ Updaters run every tick. They should be idempotent. They are not necessary if the variable is individual and tracks a Minecraft objective.
299
+
300
+ ### Score Methods
301
+
302
+ All variables are `Score` objects with these methods:
303
+
304
+ ```typescript
305
+ // Set value
306
+ score.set(number | Score);
307
+
308
+ // Arithmetic
309
+ score.add(number | Score);
310
+ score.remove(number | Score);
311
+
312
+ // Comparison (return Condition)
313
+ score.equalTo(number | Score);
314
+ score.greaterThan(number | Score);
315
+ score.greaterOrEqualThan(number | Score);
316
+ score.lowerThan(number | Score);
317
+ score.lowerOrEqualThan(number | Score);
318
+ ```
319
+
320
+ ---
321
+
322
+ ## Events
323
+
324
+ ### Lifecycle Events
325
+
326
+ #### `start_challenge`
327
+
328
+ Runs once when the challenge starts. Use for global setup.
329
+
330
+ ```typescript
331
+ start_challenge: () => {
332
+ Actions.setTime({ time: "day" });
333
+ Actions.gamerule({ rule: "doDaylightCycle", value: false });
334
+ Actions.announce({ message: "Game starting!" });
335
+ }
336
+ ```
337
+
338
+ #### `init_participants`
339
+
340
+ Runs once per player after start (delayed 1 second). Use for player setup.
341
+
342
+ ```typescript
343
+ init_participants: () => {
344
+ Actions.give({ target: "self", item: "minecraft:diamond_sword", count: 1 });
345
+ Actions.setAttribute({ target: "self", attribute_: "generic.max_health", value: 40 });
346
+ Actions.teleport({ target: "self", x: 0, y: 100, z: 0, absolute: true });
347
+ }
348
+ ```
349
+
350
+ #### `on_tick`
351
+
352
+ Runs every tick for each player. Use sparingly for performance.
353
+
354
+ ```typescript
355
+ on_tick: () => {
356
+ // Check something every tick
357
+ }
358
+ ```
359
+
360
+ #### `end_challenge`
361
+
362
+ Runs once when the game ends. Use for cleanup and announcements.
363
+
364
+ ```typescript
365
+ end_challenge: () => {
366
+ Actions.announce({ message: "Game over!" });
367
+ }
368
+ ```
369
+
370
+ ### Custom Events
371
+
372
+ Custom events allow you to trigger actions based on score thresholds or Minecraft advancement criteria.
373
+
374
+ #### Score-Based Events
375
+
376
+ Score events watch a variable and trigger when it reaches a specified target value. They are evaluated every tick.
377
+
378
+ **Parameters:**
379
+ - `score` (required): The `Score` variable to watch
380
+ - `target` (optional): The threshold value. If omitted, triggers on any score change
381
+ - `mode` (required): `"fire_once"` or `"repeatable"`
382
+ - `actions` (required): Function containing actions to execute
383
+
384
+ **How it works:**
385
+ 1. Every tick, the system compares the current score to the target
386
+ 2. For `"fire_once"`: triggers once when `score == target` (tracks previous value to detect the 1st time threshold is met)
387
+ 3. For `"repeatable"`: triggers every tick while `score == target`
388
+ 4. For individual variables, events fire per-player; for global variables, events fire once globally
389
+
390
+ ```typescript
391
+ {
392
+ score: variables.diamonds,
393
+ target: 10,
394
+ mode: "fire_once",
395
+ actions: () => {
396
+ Actions.announce({ message: "10 diamonds collected!" });
397
+ },
398
+ }
399
+ ```
400
+
401
+ **Without target (triggers on any change):**
402
+ ```typescript
403
+ {
404
+ score: variables.death_count,
405
+ mode: "fire_once", // Triggers once when death_count changes from 0
406
+ actions: () => {
407
+ Actions.announce({ message: "First death!" });
408
+ },
409
+ }
410
+ ```
411
+
412
+ **Modes:**
413
+ - `"fire_once"`: Triggers once when threshold is reached (per player for individual variables). Uses previous tick comparison to detect when the score crosses the target.
414
+ - `"repeatable"`: Triggers every tick while `score >= target`. Useful for continuous effects.
415
+
416
+ #### Advancement-Based Events
417
+
418
+ Advancement events trigger when a Minecraft advancement criterion is met. Internally, an advancement is created that grants when the criterion triggers, which then fires the event. The `criteria` array follows the [Minecraft Advancement JSON format](https://minecraft.fandom.com/wiki/Advancement/JSON_format).
419
+
420
+ **Parameters:**
421
+ - `criteria` (required): Array of advancement trigger objects with optional conditions
422
+ - `mode` (required): `"fire_once"` or `"repeatable"`
423
+ - `actions` (required): Function containing actions to execute
424
+
425
+ **How it works:**
426
+ 1. An advancement is generated with your specified criteria
427
+ 2. When Minecraft grants the advancement (criterion met), the event fires
428
+ 3. For `"repeatable"`: the advancement is automatically revoked so it can trigger again
429
+ 4. For `"fire_once"`: the advancement stays granted, preventing re-triggering
430
+
431
+ **Simple example - Track when a player hits another player:**
432
+ ```typescript
433
+ {
434
+ criteria: [
435
+ {
436
+ trigger: "minecraft:player_hurt_entity",
437
+ conditions: {
438
+ entity: { type: "minecraft:player" }
439
+ }
440
+ }
441
+ ],
442
+ mode: "repeatable",
443
+ actions: () => {
444
+ Actions.increment({ variable: variables.pvp_hits });
445
+ Actions.announce({ message: "PvP hit!" });
446
+ },
447
+ }
448
+ ```
449
+
450
+ **Multiple triggers example:**
451
+ ```typescript
452
+ {
453
+ criteria: [
454
+ { trigger: "minecraft:player_hurt_entity" },
455
+ { trigger: "minecraft:entity_hurt_player" },
456
+ ],
457
+ mode: "repeatable",
458
+ actions: () => {
459
+ Actions.increment({ variable: variables.combat_actions });
460
+ },
461
+ }
462
+ ```
463
+
464
+ Common triggers:
465
+ - `"minecraft:player_hurt_entity"` - Player attacks entity
466
+ - `"minecraft:entity_hurt_player"` - Entity attacks player
467
+ - `"minecraft:player_killed_entity"` - Player kills entity
468
+ - `"minecraft:consume_item"` - Item consumed
469
+ - `"minecraft:inventory_changed"` - Inventory changes
470
+ - `"minecraft:location"` - Player at location
471
+ - `"minecraft:enter_block"` - Player enters block
472
+
473
+ See the [Minecraft Wiki](https://minecraft.fandom.com/wiki/Advancement/JSON_format#List_of_triggers) for the full list of triggers and their conditions.
474
+
475
+ ---
476
+
477
+ ## Actions
478
+
479
+ All actions are called via the `Actions` object:
480
+
481
+ ```typescript
482
+ import { Actions } from "@kradle/challenges";
483
+ ```
484
+
485
+ ### Target Parameter
486
+
487
+ Many actions accept a `target` parameter of type `TargetNames`, which can be:
488
+ - `"all"` - Targets all participants (maps to `@a[tag=kradle_participant]`)
489
+ - `"self"` - Targets the current player (maps to `@s`)
490
+ - Any `Selector` instance - Custom selector for fine-grained targeting (e.g., `Selector("@a", { team: "red" })`)
491
+
492
+ ### Communication
493
+
494
+ #### `Actions.announce(params)`
495
+
496
+ Broadcast message to all players with KRADLE tag.
497
+
498
+ ```typescript
499
+ Actions.announce({
500
+ message: string; // Message text
501
+ });
502
+ ```
503
+
504
+ #### `Actions.tellraw(params)`
505
+
506
+ Send formatted message to specific target.
507
+
508
+ ```typescript
509
+ Actions.tellraw({
510
+ target: TargetNames; // "all", "self", or any selector
511
+ message: string[]; // Message array
512
+ });
513
+
514
+ // Example:
515
+ Actions.tellraw({
516
+ target: "all",
517
+ message: ["Hello, ", { text: "world!", color: "gold", bold: true }]
518
+ });
519
+ Actions.tellraw({
520
+ target: "self",
521
+ message: ["You won!"]
522
+ });
523
+ ```
524
+
525
+ ### Items & Inventory
526
+
527
+ #### `Actions.give(params)`
528
+
529
+ Give items to a target.
530
+
531
+ ```typescript
532
+ Actions.give({
533
+ target: TargetNames; // "all", "self", or any selector
534
+ item: string; // Item ID (with "minecraft:" prefix)
535
+ count?: number; // Amount (default: 1)
536
+ });
537
+
538
+ // Examples:
539
+ Actions.give({ target: "self", item: "minecraft:diamond_sword", count: 1 });
540
+ Actions.give({ target: "all", item: "minecraft:diamond", count: 10 });
541
+ Actions.give({ target: Selector("@a", { team: "red" }), item: "minecraft:iron_sword", count: 1 });
542
+ ```
543
+
544
+ **Note:** The `target` parameter can be:
545
+ - `"all"` - All participants
546
+ - `"self"` - Current player (`@s`)
547
+ - Any `Selector` instance for custom targeting
548
+
549
+ #### `Actions.giveLoot(params)`
550
+
551
+ Give random items from weighted loot table.
552
+
553
+ ```typescript
554
+ Actions.giveLoot({
555
+ target: TargetNames; // "all", "self", or any selector
556
+ items: [{ name: ITEMS; count: number; weight: number }]; // Weighted item list
557
+ });
558
+
559
+ // Example:
560
+ Actions.giveLoot({
561
+ target: "self",
562
+ items: [
563
+ { name: "minecraft:diamond", count: 5, weight: 1 },
564
+ { name: "minecraft:iron_ingot", count: 10, weight: 3 },
565
+ { name: "minecraft:gold_ingot", count: 7, weight: 2 }
566
+ ]
567
+ });
568
+ ```
569
+
570
+ **Note:** This creates a weighted loot table. Items with higher weights are more likely to be selected.
571
+
572
+ #### `Actions.clear(params)`
573
+
574
+ Clear all items from a target's inventory.
575
+
576
+ ```typescript
577
+ Actions.clear({
578
+ target: TargetNames; // "all", "self", or any selector
579
+ });
580
+
581
+ // Examples:
582
+ Actions.clear({ target: "self" }); // Clear current player's inventory
583
+ Actions.clear({ target: "all" }); // Clear all participants' inventories
584
+ ```
585
+
586
+ #### `Actions.countItems(params)`
587
+
588
+ Count the number of a specific item in a target's inventory. Creates and returns a temporary variable with the count. This is the prefered way of counting items.
589
+
590
+ ```typescript
591
+ Actions.countItems({
592
+ target: TargetNames; // The target to count items for
593
+ item: ITEMS; // The item to count
594
+ }): Score; // Returns a new variable containing the count
595
+
596
+ // Example - Use directly in conditions:
597
+ _.if(Actions.countItems({ target: "self", item: "minecraft:diamond" }).greaterThan(5), () => {
598
+ Actions.announce({ message: "You have more than 5 diamonds!" });
599
+ });
600
+
601
+ // Example - Store in a variable for later use:
602
+ const diamondCount = Actions.countItems({ target: "self", item: "minecraft:diamond" });
603
+ _.if(diamondCount.greaterThan(10), () => {
604
+ Actions.announce({ message: "You have more than 10 diamonds!" });
605
+ });
606
+
607
+ // Example - Set a custom variable from the count:
608
+ const count = Actions.countItems({ target: "self", item: "minecraft:diamond" });
609
+ variables.my_diamond_count.set(count);
610
+ ```
611
+
612
+ **Note:** This action creates a temporary variable internally using `Variable()` and uses `execute.store.result.score` with `clear` command (count 0) to count items without removing them from the inventory.
613
+
614
+ ### Entities
615
+
616
+ #### `Actions.summonMultiple(params)`
617
+
618
+ Summon multiple entities at location.
619
+
620
+ ```typescript
621
+ Actions.summonMultiple({
622
+ entity: string; // Entity ID (with "minecraft:" prefix)
623
+ count: number; // How many entities to summon
624
+ x: number; // X coordinate
625
+ y: number; // Y coordinate
626
+ z: number; // Z coordinate
627
+ absolute: boolean; // true for absolute coords, false for relative
628
+ });
629
+
630
+ // Example:
631
+ Actions.summonMultiple({
632
+ entity: "minecraft:zombie",
633
+ count: 5,
634
+ x: 0,
635
+ y: 64,
636
+ z: 0,
637
+ absolute: true
638
+ });
639
+ ```
640
+
641
+ #### `Actions.summonItem(params)`
642
+
643
+ Summon item entity at location.
644
+
645
+ ```typescript
646
+ Actions.summonItem({
647
+ item: string; // Item ID (with "minecraft:" prefix)
648
+ x: number; // X coordinate
649
+ y: number; // Y coordinate
650
+ z: number; // Z coordinate
651
+ absolute: boolean; // true for absolute coords, false for relative
652
+ });
653
+
654
+ // Example:
655
+ Actions.summonItem({
656
+ item: "minecraft:diamond",
657
+ x: 0,
658
+ y: 64,
659
+ z: 0,
660
+ absolute: true
661
+ });
662
+ ```
663
+
664
+ #### `Actions.kill(params)`
665
+
666
+ Kill entities matching selector.
667
+
668
+ ```typescript
669
+ Actions.kill({
670
+ selector: TargetNames; // "all", "self", or any selector
671
+ });
672
+
673
+ // Examples:
674
+ Actions.kill({ selector: Selector("@e", { type: "minecraft:zombie" }) });
675
+ Actions.kill({ selector: Selector("@e", { type: "!minecraft:player" }) });
676
+ Actions.kill({ selector: "all" }); // Kill all participants
677
+ ```
678
+
679
+ #### `Actions.teleport(params)`
680
+
681
+ Teleport entities to a location.
682
+
683
+ ```typescript
684
+ Actions.teleport({
685
+ target: TargetNames; // "all", "self", or any selector
686
+ x: number; // X coordinate
687
+ y: number; // Y coordinate
688
+ z: number; // Z coordinate
689
+ absolute: boolean; // true for absolute coords, false for relative
690
+ });
691
+
692
+ // Examples:
693
+ Actions.teleport({ target: "self", x: 0, y: 100, z: 0, absolute: true });
694
+ Actions.teleport({ target: "all", x: 0, y: 64, z: 0, absolute: true });
695
+ Actions.teleport({ target: "self", x: 10, y: 0, z: 5, absolute: false }); // Relative position
696
+ ```
697
+
698
+ ### World
699
+
700
+ #### `Actions.setBlock(params)`
701
+
702
+ Set a single block.
703
+
704
+ ```typescript
705
+ Actions.setBlock({
706
+ block: string; // Block ID (with "minecraft:" prefix)
707
+ x: number; // X coordinate
708
+ y: number; // Y coordinate
709
+ z: number; // Z coordinate
710
+ absolute: boolean; // true for absolute coords, false for relative
711
+ });
712
+
713
+ // Example:
714
+ Actions.setBlock({
715
+ block: "minecraft:diamond_block",
716
+ x: 0,
717
+ y: 64,
718
+ z: 0,
719
+ absolute: true
720
+ });
721
+ ```
722
+
723
+ #### `Actions.fill(params)`
724
+
725
+ Fill region with blocks.
726
+
727
+ ```typescript
728
+ Actions.fill({
729
+ block: string; // Block ID (with "minecraft:" prefix)
730
+ x1: number; // Start X coordinate
731
+ y1: number; // Start Y coordinate
732
+ z1: number; // Start Z coordinate
733
+ x2: number; // End X coordinate
734
+ y2: number; // End Y coordinate
735
+ z2: number; // End Z coordinate
736
+ absolute: boolean; // true for absolute coords, false for relative
737
+ mode: "fill" | "line" | "pyramid"; // Fill mode
738
+ });
739
+
740
+ // Examples:
741
+ Actions.fill({
742
+ block: "minecraft:stone",
743
+ x1: 0, y1: 64, z1: 0,
744
+ x2: 10, y2: 64, z2: 10,
745
+ absolute: true,
746
+ mode: "fill"
747
+ });
748
+
749
+ Actions.fill({
750
+ block: "minecraft:gold_block",
751
+ x1: 0, y1: 64, z1: 0,
752
+ x2: 0, y2: 10, z2: 0,
753
+ absolute: true,
754
+ mode: "pyramid" // Builds a pyramid
755
+ });
756
+ ```
757
+
758
+ #### `Actions.setTime(params)`
759
+
760
+ Set world time.
761
+
762
+ ```typescript
763
+ Actions.setTime({
764
+ time: "day" | "night" | number; // Named or tick value
765
+ });
766
+
767
+ // Examples:
768
+ Actions.setTime({ time: "day" });
769
+ Actions.setTime({ time: 6000 }); // Noon
770
+ ```
771
+
772
+ #### `Actions.gamerule(params)`
773
+
774
+ Set a gamerule.
775
+
776
+ ```typescript
777
+ Actions.gamerule({
778
+ rule: string;
779
+ value: boolean | number;
780
+ });
781
+
782
+ // Examples:
783
+ Actions.gamerule({ rule: "doDaylightCycle", value: false });
784
+ Actions.gamerule({ rule: "mobGriefing", value: false });
785
+ Actions.gamerule({ rule: "randomTickSpeed", value: 0 });
786
+ ```
787
+
788
+ ### Scores
789
+
790
+ #### `Actions.set(params)`
791
+
792
+ Set score to value or copy from another score.
793
+
794
+ ```typescript
795
+ // Set to number
796
+ Actions.set({
797
+ variable: Score;
798
+ value: number | Score;
799
+ });
800
+
801
+ // Examples:
802
+ Actions.set({ variable: variables.main_score, value: 0 });
803
+ Actions.set({ variable: variables.main_score, value: variables.diamonds });
804
+ ```
805
+
806
+ #### `Actions.increment(params)`
807
+
808
+ Add 1 to score.
809
+
810
+ ```typescript
811
+ Actions.increment({
812
+ variable: Score;
813
+ });
814
+
815
+ // Example:
816
+ Actions.increment({ variable: variables.counter });
817
+ ```
818
+
819
+ #### `Actions.decrement(params)`
820
+
821
+ Subtract 1 from score.
822
+
823
+ ```typescript
824
+ Actions.decrement({
825
+ variable: Score;
826
+ });
827
+
828
+ // Example:
829
+ Actions.decrement({ variable: variables.counter });
830
+ ```
831
+
832
+ ### Player Attributes
833
+
834
+ #### `Actions.setAttribute(params)`
835
+
836
+ Set entity attribute for a target.
837
+
838
+ ```typescript
839
+ Actions.setAttribute({
840
+ target: TargetNames; // "all", "self", or any selector
841
+ attribute_: string; // Attribute name
842
+ value: number; // Attribute value
843
+ });
844
+
845
+ // Examples:
846
+ Actions.setAttribute({ target: "self", attribute_: "generic.max_health", value: 40 });
847
+ Actions.setAttribute({ target: "all", attribute_: "generic.movement_speed", value: 0.2 });
848
+ Actions.setAttribute({ target: "self", attribute_: "generic.attack_damage", value: 10 });
849
+ ```
850
+
851
+ Common attributes (with `generic.` prefix):
852
+ - `"generic.max_health"` - Maximum HP (default 20)
853
+ - `"generic.movement_speed"` - Walk speed (default 0.1)
854
+ - `"generic.attack_damage"` - Base attack damage
855
+ - `"generic.armor"` - Armor points
856
+ - `"generic.knockback_resistance"` - Knockback resistance (0-1)
857
+
858
+
859
+ ### Logging
860
+
861
+ #### `Actions.log_variable(params)`
862
+
863
+ Log variable to watcher system (debugging).
864
+
865
+ ```typescript
866
+ Actions.log_variable({
867
+ message: string; // Log message
868
+ variable: Score; // Variable to log
869
+ store: boolean; // Whether to store in backend
870
+ });
871
+
872
+ // Example:
873
+ Actions.log_variable({
874
+ message: "Player score",
875
+ variable: variables.main_score,
876
+ store: true
877
+ });
878
+ ```
879
+
880
+ ---
881
+
882
+ ## Utilities
883
+
884
+ ### `forEveryPlayer(callback)`
885
+
886
+ Execute code for each participant at their location.
887
+
888
+ ```typescript
889
+ import { forEveryPlayer } from "@kradle/challenges";
890
+
891
+ forEveryPlayer(() => {
892
+ // Runs as(@s) at(@s) for each participant
893
+ // @s is the current player
894
+ // All individual variables reference the current player within this context
895
+ });
896
+ ```
897
+
898
+ **Important Notes:**
899
+ - Individual variables automatically reference the current player (`@s`) within the loop
900
+ - Global variables remain global and are the same across all iterations
901
+ - Each iteration executes at the player's position (`at(@s)`)
902
+
903
+ **Example - Find maximum score:**
904
+ ```typescript
905
+ max_score: {
906
+ type: "global",
907
+ updater: (value, { main_score }) => {
908
+ value.set(0);
909
+ forEveryPlayer(() => {
910
+ // main_score here refers to the current player's main_score
911
+ _.if(main_score.greaterThan(value), () => {
912
+ value.set(main_score);
913
+ });
914
+ });
915
+ },
916
+ }
917
+ ```
918
+
919
+ ### Constants
920
+
921
+ ```typescript
922
+ import { ALL, KRADLE_PARTICIPANT_TAG, WINNER_TAG } from "@kradle/challenges";
923
+
924
+ // ALL - Selector for all participants: @a[tag=kradle_participant]
925
+ // KRADLE_PARTICIPANT_TAG - Tag name: "kradle_participant"
926
+ // WINNER_TAG - Tag name: "kradle_winner"
927
+ ```
928
+
929
+ ---
930
+
931
+ ## Sandstone Integration
932
+
933
+ This package is built on Sandstone. You can use Sandstone APIs directly:
934
+
935
+ ```typescript
936
+ import { _, execute, Selector, rel, abs, MCFunction } from "sandstone";
937
+ ```
938
+
939
+ ### Conditions with `_`
940
+
941
+ ```typescript
942
+ // Single condition
943
+ _.if(score.greaterThan(10), () => {
944
+ // actions
945
+ });
946
+
947
+ // Combined conditions
948
+ _.if(_.and(
949
+ score1.greaterThan(5),
950
+ score2.equalTo(1)
951
+ ), () => {
952
+ // actions
953
+ });
954
+
955
+ _.if(_.or(
956
+ condition1,
957
+ condition2
958
+ ), () => {
959
+ // actions
960
+ });
961
+
962
+ // Block check
963
+ _.if(_.block(rel(0, -1, 0), "minecraft:diamond_block"), () => {
964
+ // Player standing on diamond block
965
+ });
966
+ ```
967
+
968
+ ### Execute Commands
969
+
970
+ ```typescript
971
+ // Store result in score
972
+ execute.as("@s").store.result.score(myScore).run.data.get.entity("@s", "Pos[1]");
973
+
974
+ // Run at location
975
+ execute.at("@s").run.particle("minecraft:flame", rel(0, 1, 0));
976
+
977
+ // Conditional execution
978
+ execute.if.score(myScore, ">=", 10).run.say("High score!");
979
+ ```
980
+
981
+ ### Selectors
982
+
983
+ ```typescript
984
+ import { Selector } from "sandstone";
985
+
986
+ // With arguments
987
+ Selector("@a", { tag: "my_tag" });
988
+ Selector("@e", { type: "zombie", limit: 1, sort: "nearest" });
989
+
990
+ // NBT check
991
+ Selector("@s", {
992
+ nbt: { Inventory: [{ id: "minecraft:diamond" }] }
993
+ });
994
+ ```
995
+
996
+ ### Relative/Absolute Coordinates
997
+
998
+ ```typescript
999
+ import { rel, abs } from "sandstone";
1000
+
1001
+ rel(0, 1, 0); // ~ ~1 ~
1002
+ abs(0, 64, 0); // 0 64 0
1003
+ ```
1004
+
1005
+ ---
1006
+
1007
+ ## Complete Examples
1008
+
1009
+ ### Example 1: Speed Challenge - First to Kill 2 Pigs
1010
+
1011
+ ```typescript
1012
+ import { createChallenge, Actions, forEveryPlayer } from "@kradle/challenges";
1013
+ import { _ } from "sandstone";
1014
+
1015
+ createChallenge({
1016
+ name: "pig-farming",
1017
+ kradle_challenge_path: "./output",
1018
+ roles: ["farmer"] as const,
1019
+ GAME_DURATION: 2 * 60 * 20, // 2 minutes
1020
+ custom_variables: {
1021
+ pigs_farmed: {
1022
+ type: "individual",
1023
+ objective_type: "minecraft.killed:minecraft.pig",
1024
+ default: 0,
1025
+ updater: (value, { main_score }) => {
1026
+ main_score.set(value);
1027
+ },
1028
+ },
1029
+ game_over: {
1030
+ type: "global",
1031
+ updater: (value, { pigs_farmed }) => {
1032
+ value.set(0);
1033
+ forEveryPlayer(() => {
1034
+ _.if(pigs_farmed.greaterOrEqualThan(2), () => {
1035
+ value.set(1);
1036
+ });
1037
+ });
1038
+ },
1039
+ },
1040
+ },
1041
+ })
1042
+ .events(() => ({
1043
+ start_challenge: () => {
1044
+ Actions.setTime({ time: "day" });
1045
+ Actions.announce({ message: "First to kill 2 pigs wins!" });
1046
+ },
1047
+ init_participants: () => {
1048
+ Actions.give({ target: "self", item: "minecraft:iron_sword", count: 1 });
1049
+ },
1050
+ }))
1051
+ .custom_events(({ pigs_farmed }) => [
1052
+ {
1053
+ score: pigs_farmed,
1054
+ target: 1,
1055
+ mode: "fire_once",
1056
+ actions: () => {
1057
+ Actions.announce({ message: "First pig down!" });
1058
+ },
1059
+ },
1060
+ ])
1061
+ .end_condition(({ game_over }) => game_over.equalTo(1))
1062
+ .win_conditions(({ pigs_farmed }, { farmer }) => ({
1063
+ [farmer]: pigs_farmed.greaterOrEqualThan(2),
1064
+ }));
1065
+ ```
1066
+
1067
+ ### Example 2: Climb Challenge - Reach Highest Point
1068
+
1069
+ ```typescript
1070
+ import { createChallenge, Actions, forEveryPlayer } from "@kradle/challenges";
1071
+ import { _, execute } from "sandstone";
1072
+
1073
+ createChallenge({
1074
+ name: "climb",
1075
+ kradle_challenge_path: "./output",
1076
+ roles: ["climber"] as const,
1077
+ GAME_DURATION: 3 * 60 * 20,
1078
+ custom_variables: {
1079
+ current_height: {
1080
+ type: "individual",
1081
+ objective_type: "dummy",
1082
+ updater: (value) => {
1083
+ execute.as("@s").store.result.score(value).run.data.get.entity("@s", "Pos[1]");
1084
+ },
1085
+ },
1086
+ max_height: {
1087
+ type: "individual",
1088
+ updater: (value, { current_height, main_score }) => {
1089
+ _.if(current_height.greaterThan(value), () => {
1090
+ value.set(current_height);
1091
+ });
1092
+ main_score.set(value);
1093
+ },
1094
+ },
1095
+ max_height_global: {
1096
+ type: "global",
1097
+ updater: (value, { max_height }) => {
1098
+ value.set(0);
1099
+ forEveryPlayer(() => {
1100
+ _.if(max_height.greaterThan(value), () => {
1101
+ value.set(max_height);
1102
+ });
1103
+ });
1104
+ },
1105
+ },
1106
+ is_winner: {
1107
+ type: "individual",
1108
+ updater: (value, { max_height, max_height_global, has_never_died }) => {
1109
+ value.set(0);
1110
+ _.if(_.and(
1111
+ max_height.equalTo(max_height_global),
1112
+ max_height.greaterThan(0),
1113
+ has_never_died.equalTo(1)
1114
+ ), () => {
1115
+ value.set(1);
1116
+ });
1117
+ },
1118
+ },
1119
+ },
1120
+ })
1121
+ .events(() => ({
1122
+ start_challenge: () => {
1123
+ Actions.setTime({ time: "day" });
1124
+ Actions.announce({ message: "Climb as high as you can!" });
1125
+ },
1126
+ init_participants: () => {
1127
+ Actions.give({ target: "self", item: "minecraft:cobblestone", count: 64 });
1128
+ Actions.give({ target: "self", item: "minecraft:cobblestone", count: 64 });
1129
+ },
1130
+ }))
1131
+ .custom_events(() => [])
1132
+ .end_condition(({ game_timer }) => game_timer.greaterThan(3 * 60 * 20))
1133
+ .win_conditions(({ is_winner }, { climber }) => ({
1134
+ [climber]: is_winner.equalTo(1),
1135
+ }));
1136
+ ```
1137
+
1138
+ ### Example 3: Battle Royale - Last Player Standing
1139
+
1140
+ ```typescript
1141
+ import { createChallenge, Actions } from "@kradle/challenges";
1142
+ import { _ } from "sandstone";
1143
+
1144
+ createChallenge({
1145
+ name: "battle-royale",
1146
+ kradle_challenge_path: "./output",
1147
+ roles: ["fighter"] as const,
1148
+ GAME_DURATION: 5 * 60 * 20,
1149
+ custom_variables: {
1150
+ kills: {
1151
+ type: "individual",
1152
+ objective_type: "playerKillCount",
1153
+ default: 0,
1154
+ updater: (value, { main_score }) => {
1155
+ main_score.set(value);
1156
+ },
1157
+ },
1158
+ sole_survivor: {
1159
+ type: "individual",
1160
+ updater: (value, { alive_players, has_never_died }) => {
1161
+ value.set(0);
1162
+ _.if(_.and(
1163
+ alive_players.equalTo(1),
1164
+ has_never_died.equalTo(1)
1165
+ ), () => {
1166
+ value.set(1);
1167
+ });
1168
+ },
1169
+ },
1170
+ },
1171
+ })
1172
+ .events(() => ({
1173
+ start_challenge: () => {
1174
+ Actions.setTime({ time: "day" });
1175
+ Actions.gamerule({ rule: "naturalRegeneration", value: false });
1176
+ Actions.announce({ message: "Last player standing wins!" });
1177
+ },
1178
+ init_participants: () => {
1179
+ Actions.give({ target: "self", item: "minecraft:stone_sword", count: 1 });
1180
+ Actions.give({ target: "self", item: "minecraft:leather_chestplate", count: 1 });
1181
+ Actions.give({ target: "self", item: "minecraft:cooked_beef", count: 10 });
1182
+ },
1183
+ }))
1184
+ .custom_events(({ kills }) => [
1185
+ {
1186
+ score: kills,
1187
+ target: 1,
1188
+ mode: "fire_once",
1189
+ actions: () => {
1190
+ Actions.announce({ message: "First blood!" });
1191
+ },
1192
+ },
1193
+ ])
1194
+ .end_condition(({ alive_players }) => alive_players.equalTo(1))
1195
+ .win_conditions(({ sole_survivor }, { fighter }) => ({
1196
+ [fighter]: sole_survivor.equalTo(1),
1197
+ }));
1198
+ ```
1199
+
1200
+ ### Example 4: Team-Based - Hunters vs Protectors
1201
+
1202
+ ```typescript
1203
+ import { createChallenge, Actions, forEveryPlayer } from "@kradle/challenges";
1204
+ import { _ } from "sandstone";
1205
+
1206
+ createChallenge({
1207
+ name: "pig-farming-v2",
1208
+ kradle_challenge_path: "./output",
1209
+ roles: ["pighunter", "pigsaver"] as const,
1210
+ GAME_DURATION: 2 * 60 * 20,
1211
+ custom_variables: {
1212
+ pigs_killed: {
1213
+ type: "individual",
1214
+ objective_type: "minecraft.killed:minecraft.pig",
1215
+ default: 0,
1216
+ updater: (value, { main_score }) => {
1217
+ main_score.set(value);
1218
+ },
1219
+ },
1220
+ pig_killed_max: {
1221
+ type: "global",
1222
+ updater: (value, { pigs_killed }) => {
1223
+ value.set(0);
1224
+ forEveryPlayer(() => {
1225
+ _.if(pigs_killed.greaterThan(value), () => {
1226
+ value.set(pigs_killed);
1227
+ });
1228
+ });
1229
+ },
1230
+ },
1231
+ },
1232
+ })
1233
+ .events((vars, { pighunter, pigsaver }) => ({
1234
+ start_challenge: () => {
1235
+ Actions.setTime({ time: "day" });
1236
+ Actions.announce({ message: "Hunters: Kill 2 pigs! Protectors: Stop them!" });
1237
+ },
1238
+ init_participants: () => {
1239
+ // Different items based on role would be set via role-specific logic
1240
+ Actions.give({ target: "self", item: "minecraft:wooden_sword", count: 1 });
1241
+ },
1242
+ }))
1243
+ .custom_events(() => [])
1244
+ .end_condition(({ pig_killed_max }) => pig_killed_max.greaterOrEqualThan(2))
1245
+ .win_conditions(({ pig_killed_max }, { pighunter, pigsaver }) => ({
1246
+ [pighunter]: pig_killed_max.greaterOrEqualThan(2),
1247
+ [pigsaver]: pig_killed_max.lowerThan(2),
1248
+ }));
1249
+ ```
1250
+
1251
+ ### Example 5: Capture the Flag
1252
+
1253
+ ```typescript
1254
+ import { createChallenge, Actions } from "@kradle/challenges";
1255
+ import { _, Selector, rel } from "sandstone";
1256
+ import type { Score } from "sandstone";
1257
+
1258
+ createChallenge({
1259
+ name: "capture-the-flag",
1260
+ kradle_challenge_path: "./output",
1261
+ roles: ["red_team", "blue_team"] as const,
1262
+ GAME_DURATION: 5 * 60 * 20,
1263
+ custom_variables: {
1264
+ holds_red_banner: {
1265
+ type: "individual",
1266
+ updater: (value: Score) => {
1267
+ value.set(0);
1268
+ _.if(Selector("@s", {
1269
+ nbt: { Inventory: [{ id: "minecraft:red_banner" }] }
1270
+ }), () => {
1271
+ value.set(1);
1272
+ });
1273
+ },
1274
+ },
1275
+ stands_blue_wool: {
1276
+ type: "individual",
1277
+ updater: (value: Score) => {
1278
+ value.set(0);
1279
+ _.if(_.block(rel(0, -1, 0), "minecraft:blue_wool"), () => {
1280
+ value.set(1);
1281
+ });
1282
+ },
1283
+ },
1284
+ captured_flag: {
1285
+ type: "individual",
1286
+ updater: (value, { holds_red_banner, stands_blue_wool, main_score }) => {
1287
+ value.set(0);
1288
+ _.if(_.and(
1289
+ holds_red_banner.equalTo(1),
1290
+ stands_blue_wool.equalTo(1)
1291
+ ), () => {
1292
+ value.set(1);
1293
+ main_score.set(1);
1294
+ });
1295
+ },
1296
+ },
1297
+ },
1298
+ })
1299
+ .events(() => ({
1300
+ start_challenge: () => {
1301
+ Actions.announce({ message: "Capture the enemy flag and return to base!" });
1302
+ },
1303
+ init_participants: () => {
1304
+ Actions.give({ target: "self", item: "minecraft:iron_sword", count: 1 });
1305
+ },
1306
+ }))
1307
+ .custom_events(({ captured_flag }) => [
1308
+ {
1309
+ score: captured_flag,
1310
+ target: 1,
1311
+ mode: "fire_once",
1312
+ actions: () => {
1313
+ Actions.announce({ message: "Flag captured! Game over!" });
1314
+ },
1315
+ },
1316
+ ])
1317
+ .end_condition(({ captured_flag }) => captured_flag.equalTo(1))
1318
+ .win_conditions(({ captured_flag }, { red_team, blue_team }) => ({
1319
+ [red_team]: captured_flag.equalTo(0),
1320
+ [blue_team]: captured_flag.equalTo(1),
1321
+ }));
1322
+ ```
1323
+
1324
+ ---
1325
+
1326
+ ## Common Patterns
1327
+
1328
+ ### Pattern 1: Sync Variable to Main Score
1329
+
1330
+ ```typescript
1331
+ my_variable: {
1332
+ type: "individual",
1333
+ objective_type: "some_criterion",
1334
+ updater: (value, { main_score }) => {
1335
+ main_score.set(value);
1336
+ },
1337
+ }
1338
+ ```
1339
+
1340
+ ### Pattern 2: Find Global Maximum
1341
+
1342
+ ```typescript
1343
+ max_global: {
1344
+ type: "global",
1345
+ updater: (value, { individual_score }) => {
1346
+ value.set(0);
1347
+ forEveryPlayer(() => {
1348
+ _.if(individual_score.greaterThan(value), () => {
1349
+ value.set(individual_score);
1350
+ });
1351
+ });
1352
+ },
1353
+ }
1354
+ ```
1355
+
1356
+ ### Pattern 3: Winner Detection (Has Max Score + Alive)
1357
+
1358
+ ```typescript
1359
+ is_winner: {
1360
+ type: "individual",
1361
+ updater: (value, { main_score, max_global, has_never_died }) => {
1362
+ value.set(0);
1363
+ _.if(_.and(
1364
+ main_score.equalTo(max_global),
1365
+ main_score.greaterThan(0),
1366
+ has_never_died.equalTo(1)
1367
+ ), () => {
1368
+ value.set(1);
1369
+ });
1370
+ },
1371
+ }
1372
+ ```
1373
+
1374
+ ### Pattern 4: Track Player Position
1375
+
1376
+ ```typescript
1377
+ current_y: {
1378
+ type: "individual",
1379
+ objective_type: "dummy",
1380
+ updater: (value) => {
1381
+ execute.as("@s").store.result.score(value).run.data.get.entity("@s", "Pos[1]");
1382
+ },
1383
+ }
1384
+ ```
1385
+
1386
+ ### Pattern 5: Check Inventory for Item
1387
+
1388
+ ```typescript
1389
+ has_diamond: {
1390
+ type: "individual",
1391
+ updater: (value: Score) => {
1392
+ value.set(0);
1393
+ _.if(Selector("@s", {
1394
+ nbt: { Inventory: [{ id: "minecraft:diamond" }] }
1395
+ }), () => {
1396
+ value.set(1);
1397
+ });
1398
+ },
1399
+ }
1400
+ ```
1401
+
1402
+ ### Pattern 6: Check Block Below Player
1403
+
1404
+ ```typescript
1405
+ on_gold_block: {
1406
+ type: "individual",
1407
+ updater: (value: Score) => {
1408
+ value.set(0);
1409
+ _.if(_.block(rel(0, -1, 0), "minecraft:gold_block"), () => {
1410
+ value.set(1);
1411
+ });
1412
+ },
1413
+ }
1414
+ ```
1415
+
1416
+ ### Pattern 7: Count Entities
1417
+
1418
+ ```typescript
1419
+ zombie_count: {
1420
+ type: "global",
1421
+ updater: (value) => {
1422
+ value.set(0);
1423
+ execute.as(Selector("@e", { type: "zombie" })).run(() => {
1424
+ value.add(1);
1425
+ });
1426
+ },
1427
+ }
1428
+ ```
1429
+
1430
+ ### Pattern 8: Simple End Condition
1431
+
1432
+ ```typescript
1433
+ .end_condition(({ objective_complete }) => objective_complete.equalTo(1))
1434
+ ```
1435
+
1436
+ ### Pattern 9: Multi-Condition End
1437
+
1438
+ ```typescript
1439
+ .end_condition(({ alive_players, objective_complete }) =>
1440
+ _.or(
1441
+ alive_players.equalTo(1),
1442
+ objective_complete.equalTo(1)
1443
+ )
1444
+ )
1445
+ ```
1446
+
1447
+ ### Pattern 10: Opposing Team Win Conditions
1448
+
1449
+ ```typescript
1450
+ .win_conditions(({ team_a_score, team_b_score }, { team_a, team_b }) => ({
1451
+ [team_a]: team_a_score.greaterThan(team_b_score),
1452
+ [team_b]: team_b_score.greaterThan(team_a_score),
1453
+ }))
1454
+ ```
1455
+
1456
+ ---
1457
+
1458
+ ## Tips for LLMs
1459
+
1460
+ 1. **Always use `as const`** for roles array to get proper type inference
1461
+ 2. **Updaters should be idempotent** - they run every tick
1462
+ 3. **Use `forEveryPlayer` for global aggregations** (max, count, any/all checks)
1463
+ 4. **Import `_` from sandstone** for conditions (`_.if`, `_.and`, `_.or`)
1464
+ 5. **Variables are Scores** - use `.set()`, `.add()`, comparison methods
1465
+ 6. **Main score is displayed** - sync your primary metric to `main_score`
1466
+ 7. **Time is in ticks** - multiply seconds by 20
1467
+ 8. **Win conditions are per-role** - each role needs its own condition
1468
+ 9. **Custom events fire per-player** for individual variables