@kradle/challenges 0.1.0 → 0.2.1

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.
package/LLM_README.md ADDED
@@ -0,0 +1,1532 @@
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
+ value.set(Actions.getCurrentPlayerPosition().y)
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
+ All lifecycle events run globally (not per-player).
327
+
328
+ #### `start_challenge`
329
+
330
+ Runs once when the challenge starts. Use for global setup.
331
+
332
+ ```typescript
333
+ start_challenge: () => {
334
+ Actions.setTime({ time: "day" });
335
+ Actions.gamerule({ rule: "doDaylightCycle", value: false });
336
+ Actions.announce({ message: "Game starting!" });
337
+ }
338
+ ```
339
+
340
+ #### `init_participants`
341
+
342
+ Runs 1 second after start_challenge. Use for player setup. It still runs globally.
343
+
344
+ ```typescript
345
+ init_participants: () => {
346
+ Actions.give({ target: "all", item: "minecraft:diamond_sword", count: 1 });
347
+ Actions.setAttribute({ target: "all", attribute_: "generic.max_health", value: 40 });
348
+ Actions.teleport({ target: "all", x: 0, y: 100, z: 0, absolute: true });
349
+ }
350
+ ```
351
+
352
+ #### `on_tick`
353
+
354
+ Runs every tick. Use sparingly for performance.
355
+
356
+ ```typescript
357
+ on_tick: () => {
358
+ // Check something every tick
359
+ }
360
+ ```
361
+
362
+ #### `end_challenge`
363
+
364
+ Runs once when the game ends. Use for cleanup and announcements.
365
+
366
+ ```typescript
367
+ end_challenge: () => {
368
+ Actions.announce({ message: "Game over!" });
369
+ }
370
+ ```
371
+
372
+ ### Custom Events
373
+
374
+ Custom events allow you to trigger actions based on score thresholds or Minecraft advancement criteria.
375
+
376
+ #### Score-Based Events
377
+
378
+ Score events watch a variable and trigger when it reaches a specified target value. They are evaluated every tick.
379
+
380
+ **Parameters:**
381
+ - `score` (required): The `Score` variable to watch
382
+ - `target` (optional): The threshold value. If omitted, triggers on any score change
383
+ - `mode` (required): `"fire_once"` or `"repeatable"`
384
+ - `actions` (required): Function containing actions to execute
385
+
386
+ **How it works:**
387
+ 1. Every tick, the system compares the current score to the target
388
+ 2. For `"fire_once"`: triggers once when `score == target` (tracks previous value to detect the 1st time threshold is met)
389
+ 3. For `"repeatable"`: triggers every tick while `score == target`
390
+ 4. For individual variables, events fire per-player; for global variables, events fire once globally
391
+
392
+ ```typescript
393
+ {
394
+ score: variables.diamonds,
395
+ target: 10,
396
+ mode: "fire_once",
397
+ actions: () => {
398
+ Actions.announce({ message: "10 diamonds collected!" });
399
+ },
400
+ }
401
+ ```
402
+
403
+ **Without target (triggers on any change):**
404
+ ```typescript
405
+ {
406
+ score: variables.death_count,
407
+ mode: "fire_once", // Triggers once when death_count changes away from initial value
408
+ actions: () => {
409
+ Actions.announce({ message: "First death!" });
410
+ },
411
+ }
412
+ ```
413
+
414
+ **Modes:**
415
+ - `"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.
416
+ - `"repeatable"`: Triggers every tick while `score >= target`. Useful for continuous effects.
417
+
418
+ #### Advancement-Based Events
419
+
420
+ 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).
421
+
422
+ Advancement-based events always fire per-player.
423
+
424
+ **Parameters:**
425
+ - `criteria` (required): Array of advancement trigger objects with optional conditions
426
+ - `mode` (required): `"fire_once"` or `"repeatable"`
427
+ - `actions` (required): Function containing actions to execute
428
+
429
+ **How it works:**
430
+ 1. An advancement is generated with your specified criteria
431
+ 2. When Minecraft grants the advancement (criterion met), the event fires
432
+ 3. For `"repeatable"`: the advancement is automatically revoked so it can trigger again
433
+ 4. For `"fire_once"`: the advancement stays granted, preventing re-triggering
434
+
435
+ **Simple example - Track when a player hits another player:**
436
+ ```typescript
437
+ {
438
+ criteria: [
439
+ {
440
+ trigger: "minecraft:player_hurt_entity",
441
+ conditions: {
442
+ entity: { type: "minecraft:player" }
443
+ }
444
+ }
445
+ ],
446
+ mode: "repeatable",
447
+ actions: () => {
448
+ Actions.increment({ variable: variables.pvp_hits });
449
+ Actions.announce({ message: "PvP hit!" });
450
+ },
451
+ }
452
+ ```
453
+
454
+ **Multiple triggers example:**
455
+ ```typescript
456
+ {
457
+ criteria: [
458
+ { trigger: "minecraft:player_hurt_entity" },
459
+ { trigger: "minecraft:entity_hurt_player" },
460
+ ],
461
+ mode: "repeatable",
462
+ actions: () => {
463
+ Actions.increment({ variable: variables.combat_actions });
464
+ },
465
+ }
466
+ ```
467
+
468
+ Common triggers:
469
+ - `"minecraft:player_hurt_entity"` - Player attacks entity
470
+ - `"minecraft:entity_hurt_player"` - Entity attacks player
471
+ - `"minecraft:player_killed_entity"` - Player kills entity
472
+ - `"minecraft:consume_item"` - Item consumed
473
+ - `"minecraft:inventory_changed"` - Inventory changes
474
+ - `"minecraft:location"` - Player at location
475
+ - `"minecraft:enter_block"` - Player enters block
476
+
477
+ See the [Minecraft Wiki](https://minecraft.fandom.com/wiki/Advancement/JSON_format#List_of_triggers) for the full list of triggers and their conditions.
478
+
479
+ ---
480
+
481
+ ## Actions
482
+
483
+ Actions are higher-level functions that wrap common Minecraft operations. They provide:
484
+ - Automatic target mapping (`"all"`, `"self"`, team names → proper selectors)
485
+ - Integration with Kradle's interface (e.g., `Actions.announce` messages appear in Kradle)
486
+ - Consistent API for common operations
487
+
488
+ For advanced use cases not covered by Actions, you can fall back to Sandstone's lower-level functions directly (`give`, `tellraw`, `effect`, `kill`, `execute`, etc.). See [Sandstone Integration](#sandstone-integration).
489
+
490
+ All actions are called via the `Actions` object:
491
+
492
+ ```typescript
493
+ import { Actions } from "@kradle/challenges";
494
+ ```
495
+
496
+ ### Target Parameter
497
+
498
+ Many actions accept a `target` parameter of type `TargetNames`, which can be:
499
+ - `"all"` - Targets all participants (maps to `@a[tag=kradle_participant]`)
500
+ - `"self"` - Targets the current player (maps to `@s`)
501
+ - Any `Selector` instance - Custom selector for fine-grained targeting (e.g., `Selector("@a", { team: "red" })`)
502
+
503
+ ### Communication
504
+
505
+ #### `Actions.announce(params)`
506
+
507
+ Broadcast message to all players with KRADLE tag.
508
+
509
+ ```typescript
510
+ Actions.announce({
511
+ message: JSONTextComponent; // Message (string or formatted object)
512
+ });
513
+
514
+ // Simple string:
515
+ Actions.announce({ message: "Game starting!" });
516
+
517
+ // Formatted JSONTextComponent:
518
+ Actions.announce({
519
+ message: [
520
+ { text: "Player ", color: "white" },
521
+ { selector: "@s", color: "gold", bold: true },
522
+ { text: " won the game!", color: "green" }
523
+ ]
524
+ });
525
+ ```
526
+
527
+ #### `Actions.tellraw(params)`
528
+
529
+ Send formatted message to specific target. **Note:** These messages are only visible to players in-game and will not appear in Kradle's interface. Use `Actions.announce` for messages that should be visible in Kradle.
530
+
531
+ ```typescript
532
+ Actions.tellraw({
533
+ target: TargetNames; // "all", "self", or any selector
534
+ message: JSONTextComponent; // Message (string or formatted object)
535
+ });
536
+
537
+ // Examples:
538
+ Actions.tellraw({
539
+ target: "all",
540
+ message: ["Hello, ", { text: "world!", color: "gold", bold: true }]
541
+ });
542
+ Actions.tellraw({
543
+ target: "self",
544
+ message: "You won!"
545
+ });
546
+ Actions.tellraw({
547
+ target: "self",
548
+ message: { text: "Critical hit!", color: "red", bold: true }
549
+ });
550
+ ```
551
+
552
+ ### Items & Inventory
553
+
554
+ #### `Actions.give(params)`
555
+
556
+ Give items to a target.
557
+
558
+ ```typescript
559
+ Actions.give({
560
+ target: TargetNames; // "all", "self", or any selector
561
+ item: string; // Item ID (with "minecraft:" prefix)
562
+ count?: number; // Amount (default: 1)
563
+ });
564
+
565
+ // Examples:
566
+ Actions.give({ target: "self", item: "minecraft:diamond_sword", count: 1 });
567
+ Actions.give({ target: "all", item: "minecraft:diamond", count: 10 });
568
+ Actions.give({ target: Selector("@a", { team: "red" }), item: "minecraft:iron_sword", count: 1 });
569
+ ```
570
+
571
+ **Note:** The `target` parameter can be:
572
+ - `"all"` - All participants
573
+ - `"self"` - Current player (`@s`)
574
+ - Any `Selector` instance for custom targeting
575
+
576
+ #### `Actions.giveLoot(params)`
577
+
578
+ Give random items from weighted loot table.
579
+
580
+ ```typescript
581
+ Actions.giveLoot({
582
+ target: TargetNames; // "all", "self", or any selector
583
+ items: [{ name: ITEMS; count: number; weight: number }]; // Weighted item list
584
+ });
585
+
586
+ // Example:
587
+ Actions.giveLoot({
588
+ target: "self",
589
+ items: [
590
+ { name: "minecraft:diamond", count: 5, weight: 1 },
591
+ { name: "minecraft:iron_ingot", count: 10, weight: 3 },
592
+ { name: "minecraft:gold_ingot", count: 7, weight: 2 }
593
+ ]
594
+ });
595
+ ```
596
+
597
+ **Note:** This creates a weighted loot table. Items with higher weights are more likely to be selected.
598
+
599
+ #### `Actions.clear(params)`
600
+
601
+ Clear all items from a target's inventory.
602
+
603
+ ```typescript
604
+ Actions.clear({
605
+ target: TargetNames; // "all", "self", or any selector
606
+ });
607
+
608
+ // Examples:
609
+ Actions.clear({ target: "self" }); // Clear current player's inventory
610
+ Actions.clear({ target: "all" }); // Clear all participants' inventories
611
+ ```
612
+
613
+ #### `Actions.countItems(params)`
614
+
615
+ 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.
616
+
617
+ ```typescript
618
+ Actions.countItems({
619
+ target: TargetNames; // The target to count items for
620
+ item: ITEMS; // The item to count
621
+ }): Score; // Returns a new variable containing the count
622
+
623
+ // Example - Use directly in conditions:
624
+ _.if(Actions.countItems({ target: "self", item: "minecraft:diamond" }).greaterThan(5), () => {
625
+ Actions.announce({ message: "You have more than 5 diamonds!" });
626
+ });
627
+
628
+ // Example - Store in a variable for later use:
629
+ const diamondCount = Actions.countItems({ target: "self", item: "minecraft:diamond" });
630
+ _.if(diamondCount.greaterThan(10), () => {
631
+ Actions.announce({ message: "You have more than 10 diamonds!" });
632
+ });
633
+
634
+ // Example - Set a custom variable from the count:
635
+ const count = Actions.countItems({ target: "self", item: "minecraft:diamond" });
636
+ variables.my_diamond_count.set(count);
637
+ ```
638
+
639
+ **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.
640
+
641
+ #### `Actions.getCurrentPlayerPosition()`
642
+
643
+ Get the current player's position as x, y, z Score variables. Must be called in a player context (e.g., inside `forEveryPlayer`, individual variables updaters, or when `@s` is a player). This is the prefered way of checking a player's position.
644
+
645
+ ```typescript
646
+ Actions.getCurrentPlayerPosition(): { x: Score; y: Score; z: Score }
647
+
648
+ // Example - Check if player is above Y=100:
649
+ const pos = Actions.getCurrentPlayerPosition();
650
+ _.if(pos.y.greaterThan(100), () => {
651
+ Actions.announce({ message: "You reached the sky!" });
652
+ });
653
+
654
+ // Example - Store position in custom variables:
655
+ const { x, y, z } = Actions.getCurrentPlayerPosition();
656
+ variables.player_x.set(x);
657
+ variables.player_y.set(y);
658
+ variables.player_z.set(z);
659
+
660
+ // Example - Check if player is in a specific area:
661
+ const pos = Actions.getCurrentPlayerPosition();
662
+ _.if(_.and(
663
+ pos.x.greaterThan(0),
664
+ pos.x.lowerThan(100),
665
+ pos.z.greaterThan(0),
666
+ pos.z.lowerThan(100)
667
+ ), () => {
668
+ Actions.announce({ message: "You're in the zone!" });
669
+ });
670
+ ```
671
+
672
+ **Note:** This returns integer coordinates (block position). The values are truncated from the player's exact floating-point position.
673
+
674
+ ### Entities
675
+
676
+ #### `Actions.summonMultiple(params)`
677
+
678
+ Summon multiple entities at location.
679
+
680
+ ```typescript
681
+ Actions.summonMultiple({
682
+ entity: string; // Entity ID (with "minecraft:" prefix)
683
+ count: number; // How many entities to summon
684
+ x: number; // X coordinate
685
+ y: number; // Y coordinate
686
+ z: number; // Z coordinate
687
+ absolute: boolean; // true for absolute coords, false for relative
688
+ });
689
+
690
+ // Example:
691
+ Actions.summonMultiple({
692
+ entity: "minecraft:zombie",
693
+ count: 5,
694
+ x: 0,
695
+ y: 64,
696
+ z: 0,
697
+ absolute: true
698
+ });
699
+ ```
700
+
701
+ #### `Actions.summonItem(params)`
702
+
703
+ Summon item entity at location.
704
+
705
+ ```typescript
706
+ Actions.summonItem({
707
+ item: string; // Item ID (with "minecraft:" prefix)
708
+ x: number; // X coordinate
709
+ y: number; // Y coordinate
710
+ z: number; // Z coordinate
711
+ absolute: boolean; // true for absolute coords, false for relative
712
+ });
713
+
714
+ // Example:
715
+ Actions.summonItem({
716
+ item: "minecraft:diamond",
717
+ x: 0,
718
+ y: 64,
719
+ z: 0,
720
+ absolute: true
721
+ });
722
+ ```
723
+
724
+ #### `Actions.kill(params)`
725
+
726
+ Kill entities matching selector.
727
+
728
+ ```typescript
729
+ Actions.kill({
730
+ selector: TargetNames; // "all", "self", or any selector
731
+ });
732
+
733
+ // Examples:
734
+ Actions.kill({ selector: Selector("@e", { type: "minecraft:zombie" }) });
735
+ Actions.kill({ selector: Selector("@e", { type: "!minecraft:player" }) });
736
+ Actions.kill({ selector: "all" }); // Kill all participants
737
+ ```
738
+
739
+ #### `Actions.teleport(params)`
740
+
741
+ Teleport entities to a location.
742
+
743
+ ```typescript
744
+ Actions.teleport({
745
+ target: TargetNames; // "all", "self", or any selector
746
+ x: number; // X coordinate
747
+ y: number; // Y coordinate
748
+ z: number; // Z coordinate
749
+ absolute: boolean; // true for absolute coords, false for relative
750
+ });
751
+
752
+ // Examples:
753
+ Actions.teleport({ target: "self", x: 0, y: 100, z: 0, absolute: true });
754
+ Actions.teleport({ target: "all", x: 0, y: 64, z: 0, absolute: true });
755
+ Actions.teleport({ target: "self", x: 10, y: 0, z: 5, absolute: false }); // Relative position
756
+ ```
757
+
758
+ ### World
759
+
760
+ #### `Actions.setBlock(params)`
761
+
762
+ Set a single block.
763
+
764
+ ```typescript
765
+ Actions.setBlock({
766
+ block: string; // Block ID (with "minecraft:" prefix)
767
+ x: number; // X coordinate
768
+ y: number; // Y coordinate
769
+ z: number; // Z coordinate
770
+ absolute: boolean; // true for absolute coords, false for relative
771
+ });
772
+
773
+ // Example:
774
+ Actions.setBlock({
775
+ block: "minecraft:diamond_block",
776
+ x: 0,
777
+ y: 64,
778
+ z: 0,
779
+ absolute: true
780
+ });
781
+ ```
782
+
783
+ #### `Actions.fill(params)`
784
+
785
+ Fill region with blocks.
786
+
787
+ ```typescript
788
+ Actions.fill({
789
+ block: string; // Block ID (with "minecraft:" prefix)
790
+ x1: number; // Start X coordinate
791
+ y1: number; // Start Y coordinate
792
+ z1: number; // Start Z coordinate
793
+ x2: number; // End X coordinate
794
+ y2: number; // End Y coordinate
795
+ z2: number; // End Z coordinate
796
+ absolute: boolean; // true for absolute coords, false for relative
797
+ mode: "fill" | "line" | "pyramid"; // Fill mode
798
+ });
799
+
800
+ // Examples:
801
+ Actions.fill({
802
+ block: "minecraft:stone",
803
+ x1: 0, y1: 64, z1: 0,
804
+ x2: 10, y2: 64, z2: 10,
805
+ absolute: true,
806
+ mode: "fill"
807
+ });
808
+
809
+ Actions.fill({
810
+ block: "minecraft:gold_block",
811
+ x1: 0, y1: 64, z1: 0,
812
+ x2: 0, y2: 10, z2: 0,
813
+ absolute: true,
814
+ mode: "pyramid" // Builds a pyramid
815
+ });
816
+ ```
817
+
818
+ #### `Actions.setTime(params)`
819
+
820
+ Set world time.
821
+
822
+ ```typescript
823
+ Actions.setTime({
824
+ time: "day" | "night" | number; // Named or tick value
825
+ });
826
+
827
+ // Examples:
828
+ Actions.setTime({ time: "day" });
829
+ Actions.setTime({ time: 6000 }); // Noon
830
+ ```
831
+
832
+ #### `Actions.gamerule(params)`
833
+
834
+ Set a gamerule.
835
+
836
+ ```typescript
837
+ Actions.gamerule({
838
+ rule: string;
839
+ value: boolean | number;
840
+ });
841
+
842
+ // Examples:
843
+ Actions.gamerule({ rule: "doDaylightCycle", value: false });
844
+ Actions.gamerule({ rule: "mobGriefing", value: false });
845
+ Actions.gamerule({ rule: "randomTickSpeed", value: 0 });
846
+ ```
847
+
848
+ ### Scores
849
+
850
+ #### `Actions.set(params)`
851
+
852
+ Set score to value or copy from another score.
853
+
854
+ ```typescript
855
+ // Set to number
856
+ Actions.set({
857
+ variable: Score;
858
+ value: number | Score;
859
+ });
860
+
861
+ // Examples:
862
+ Actions.set({ variable: variables.main_score, value: 0 });
863
+ Actions.set({ variable: variables.main_score, value: variables.diamonds });
864
+ ```
865
+
866
+ #### `Actions.increment(params)`
867
+
868
+ Add 1 to score.
869
+
870
+ ```typescript
871
+ Actions.increment({
872
+ variable: Score;
873
+ });
874
+
875
+ // Example:
876
+ Actions.increment({ variable: variables.counter });
877
+ ```
878
+
879
+ #### `Actions.decrement(params)`
880
+
881
+ Subtract 1 from score.
882
+
883
+ ```typescript
884
+ Actions.decrement({
885
+ variable: Score;
886
+ });
887
+
888
+ // Example:
889
+ Actions.decrement({ variable: variables.counter });
890
+ ```
891
+
892
+ ### Player Attributes
893
+
894
+ #### `Actions.setAttribute(params)`
895
+
896
+ Set entity attribute for a target.
897
+
898
+ ```typescript
899
+ Actions.setAttribute({
900
+ target: TargetNames; // "all", "self", or any selector
901
+ attribute_: string; // Attribute name
902
+ value: number; // Attribute value
903
+ });
904
+
905
+ // Examples:
906
+ Actions.setAttribute({ target: "self", attribute_: "generic.max_health", value: 40 });
907
+ Actions.setAttribute({ target: "all", attribute_: "generic.movement_speed", value: 0.2 });
908
+ Actions.setAttribute({ target: "self", attribute_: "generic.attack_damage", value: 10 });
909
+ ```
910
+
911
+ Common attributes (with `generic.` prefix):
912
+ - `"generic.max_health"` - Maximum HP (default 20)
913
+ - `"generic.movement_speed"` - Walk speed (default 0.1)
914
+ - `"generic.attack_damage"` - Base attack damage
915
+ - `"generic.armor"` - Armor points
916
+ - `"generic.knockback_resistance"` - Knockback resistance (0-1)
917
+
918
+
919
+ ### Logging
920
+
921
+ #### `Actions.log_variable(params)`
922
+
923
+ Log variable to watcher system (debugging).
924
+
925
+ ```typescript
926
+ Actions.log_variable({
927
+ message: string; // Log message
928
+ variable: Score; // Variable to log
929
+ store: boolean; // Whether to store in backend
930
+ });
931
+
932
+ // Example:
933
+ Actions.log_variable({
934
+ message: "Player score",
935
+ variable: variables.main_score,
936
+ store: true
937
+ });
938
+ ```
939
+
940
+ ---
941
+
942
+ ## Utilities
943
+
944
+ ### `forEveryPlayer(callback)`
945
+
946
+ Execute code for each participant at their location.
947
+
948
+ ```typescript
949
+ import { forEveryPlayer } from "@kradle/challenges";
950
+
951
+ forEveryPlayer(() => {
952
+ // Runs as(@s) at(@s) for each participant
953
+ // @s is the current player
954
+ // All individual variables reference the current player within this context
955
+ });
956
+ ```
957
+
958
+ **Important Notes:**
959
+ - Individual variables automatically reference the current player (`@s`) within the loop
960
+ - Global variables remain global and are the same across all iterations
961
+ - Each iteration executes at the player's position (`at(@s)`)
962
+
963
+ **Example - Find maximum score:**
964
+ ```typescript
965
+ max_score: {
966
+ type: "global",
967
+ updater: (value, { main_score }) => {
968
+ value.set(0);
969
+ forEveryPlayer(() => {
970
+ // main_score here refers to the current player's main_score
971
+ _.if(main_score.greaterThan(value), () => {
972
+ value.set(main_score);
973
+ });
974
+ });
975
+ },
976
+ }
977
+ ```
978
+
979
+ ### Constants
980
+
981
+ ```typescript
982
+ import { ALL, KRADLE_PARTICIPANT_TAG, WINNER_TAG } from "@kradle/challenges";
983
+
984
+ // ALL - Selector for all participants: @a[tag=kradle_participant]
985
+ // KRADLE_PARTICIPANT_TAG - Tag name: "kradle_participant"
986
+ // WINNER_TAG - Tag name: "kradle_winner"
987
+ ```
988
+
989
+ ---
990
+
991
+ ## Sandstone Integration
992
+
993
+ This package is built on Sandstone. You can use Sandstone APIs directly:
994
+
995
+ ```typescript
996
+ import { _, execute, Selector, rel, abs, MCFunction } from "sandstone";
997
+ ```
998
+
999
+ ### Conditions with `_`
1000
+
1001
+ ```typescript
1002
+ // Single condition
1003
+ _.if(score.greaterThan(10), () => {
1004
+ // actions
1005
+ });
1006
+
1007
+ // Combined conditions
1008
+ _.if(_.and(
1009
+ score1.greaterThan(5),
1010
+ score2.equalTo(1)
1011
+ ), () => {
1012
+ // actions
1013
+ });
1014
+
1015
+ _.if(_.or(
1016
+ condition1,
1017
+ condition2
1018
+ ), () => {
1019
+ // actions
1020
+ });
1021
+
1022
+ // Block check
1023
+ _.if(_.block(rel(0, -1, 0), "minecraft:diamond_block"), () => {
1024
+ // Player standing on diamond block
1025
+ });
1026
+ ```
1027
+
1028
+ ### Execute Commands
1029
+
1030
+ ```typescript
1031
+ // Store result in score
1032
+ execute.as("@s").store.result.score(myScore).run.data.get.entity("@s", "Pos[1]");
1033
+
1034
+ // Run at location
1035
+ execute.at("@s").run.particle("minecraft:flame", rel(0, 1, 0));
1036
+
1037
+ // Conditional execution
1038
+ execute.if.score(myScore, ">=", 10).run.say("High score!");
1039
+ ```
1040
+
1041
+ ### Selectors
1042
+
1043
+ ```typescript
1044
+ import { Selector } from "sandstone";
1045
+
1046
+ // With arguments
1047
+ Selector("@a", { tag: "my_tag" });
1048
+ Selector("@e", { type: "zombie", limit: 1, sort: "nearest" });
1049
+
1050
+ // NBT check
1051
+ Selector("@s", {
1052
+ nbt: { Inventory: [{ id: "minecraft:diamond" }] }
1053
+ });
1054
+ ```
1055
+
1056
+ ### Relative/Absolute Coordinates
1057
+
1058
+ ```typescript
1059
+ import { rel, abs } from "sandstone";
1060
+
1061
+ rel(0, 1, 0); // ~ ~1 ~
1062
+ abs(0, 64, 0); // 0 64 0
1063
+ ```
1064
+
1065
+ ---
1066
+
1067
+ ## Complete Examples
1068
+
1069
+ ### Example 1: Speed Challenge - First to Kill 2 Pigs
1070
+
1071
+ ```typescript
1072
+ import { createChallenge, Actions, forEveryPlayer } from "@kradle/challenges";
1073
+ import { _ } from "sandstone";
1074
+
1075
+ createChallenge({
1076
+ name: "pig-farming",
1077
+ kradle_challenge_path: "./output",
1078
+ roles: ["farmer"] as const,
1079
+ GAME_DURATION: 2 * 60 * 20, // 2 minutes
1080
+ custom_variables: {
1081
+ pigs_farmed: {
1082
+ type: "individual",
1083
+ objective_type: "minecraft.killed:minecraft.pig",
1084
+ default: 0,
1085
+ updater: (value, { main_score }) => {
1086
+ main_score.set(value);
1087
+ },
1088
+ },
1089
+ game_over: {
1090
+ type: "global",
1091
+ updater: (value, { pigs_farmed }) => {
1092
+ value.set(0);
1093
+ forEveryPlayer(() => {
1094
+ _.if(pigs_farmed.greaterOrEqualThan(2), () => {
1095
+ value.set(1);
1096
+ });
1097
+ });
1098
+ },
1099
+ },
1100
+ },
1101
+ })
1102
+ .events(() => ({
1103
+ start_challenge: () => {
1104
+ Actions.setTime({ time: "day" });
1105
+ Actions.announce({ message: "First to kill 2 pigs wins!" });
1106
+ },
1107
+ init_participants: () => {
1108
+ Actions.give({ target: "all", item: "minecraft:iron_sword", count: 1 });
1109
+ },
1110
+ }))
1111
+ .custom_events(({ pigs_farmed }) => [
1112
+ {
1113
+ score: pigs_farmed,
1114
+ target: 1,
1115
+ mode: "fire_once",
1116
+ actions: () => {
1117
+ Actions.announce({ message: "First pig down!" });
1118
+ },
1119
+ },
1120
+ ])
1121
+ .end_condition(({ game_over }) => game_over.equalTo(1))
1122
+ .win_conditions(({ pigs_farmed }, { farmer }) => ({
1123
+ [farmer]: pigs_farmed.greaterOrEqualThan(2),
1124
+ }));
1125
+ ```
1126
+
1127
+ ### Example 2: Climb Challenge - Reach Highest Point
1128
+
1129
+ ```typescript
1130
+ import { createChallenge, Actions, forEveryPlayer } from "@kradle/challenges";
1131
+ import { _, execute } from "sandstone";
1132
+
1133
+ createChallenge({
1134
+ name: "climb",
1135
+ kradle_challenge_path: "./output",
1136
+ roles: ["climber"] as const,
1137
+ GAME_DURATION: 3 * 60 * 20,
1138
+ custom_variables: {
1139
+ current_height: {
1140
+ type: "individual",
1141
+ objective_type: "dummy",
1142
+ updater: (value) => {
1143
+ value.set(Actions.getCurrentPlayerPosition().y)
1144
+ },
1145
+ },
1146
+ max_height: {
1147
+ type: "individual",
1148
+ updater: (value, { current_height, main_score }) => {
1149
+ _.if(current_height.greaterThan(value), () => {
1150
+ value.set(current_height);
1151
+ });
1152
+ main_score.set(value);
1153
+ },
1154
+ },
1155
+ max_height_global: {
1156
+ type: "global",
1157
+ updater: (value, { max_height }) => {
1158
+ value.set(0);
1159
+ forEveryPlayer(() => {
1160
+ _.if(max_height.greaterThan(value), () => {
1161
+ value.set(max_height);
1162
+ });
1163
+ });
1164
+ },
1165
+ },
1166
+ is_winner: {
1167
+ type: "individual",
1168
+ updater: (value, { max_height, max_height_global, has_never_died }) => {
1169
+ value.set(0);
1170
+ _.if(_.and(
1171
+ max_height.equalTo(max_height_global),
1172
+ max_height.greaterThan(0),
1173
+ has_never_died.equalTo(1)
1174
+ ), () => {
1175
+ value.set(1);
1176
+ });
1177
+ },
1178
+ },
1179
+ },
1180
+ })
1181
+ .events(() => ({
1182
+ start_challenge: () => {
1183
+ Actions.setTime({ time: "day" });
1184
+ Actions.announce({ message: "Climb as high as you can!" });
1185
+ },
1186
+ init_participants: () => {
1187
+ Actions.give({ target: "all", item: "minecraft:cobblestone", count: 64 });
1188
+ Actions.give({ target: "all", item: "minecraft:cobblestone", count: 64 });
1189
+ },
1190
+ }))
1191
+ .custom_events(() => [])
1192
+ .end_condition(({ game_timer }) => game_timer.greaterThan(3 * 60 * 20))
1193
+ .win_conditions(({ is_winner }, { climber }) => ({
1194
+ [climber]: is_winner.equalTo(1),
1195
+ }));
1196
+ ```
1197
+
1198
+ ### Example 3: Battle Royale - Last Player Standing
1199
+
1200
+ ```typescript
1201
+ import { createChallenge, Actions } from "@kradle/challenges";
1202
+ import { _ } from "sandstone";
1203
+
1204
+ createChallenge({
1205
+ name: "battle-royale",
1206
+ kradle_challenge_path: "./output",
1207
+ roles: ["fighter"] as const,
1208
+ GAME_DURATION: 5 * 60 * 20,
1209
+ custom_variables: {
1210
+ kills: {
1211
+ type: "individual",
1212
+ objective_type: "playerKillCount",
1213
+ default: 0,
1214
+ updater: (value, { main_score }) => {
1215
+ main_score.set(value);
1216
+ },
1217
+ },
1218
+ sole_survivor: {
1219
+ type: "individual",
1220
+ updater: (value, { alive_players, has_never_died }) => {
1221
+ value.set(0);
1222
+ _.if(_.and(
1223
+ alive_players.equalTo(1),
1224
+ has_never_died.equalTo(1)
1225
+ ), () => {
1226
+ value.set(1);
1227
+ });
1228
+ },
1229
+ },
1230
+ },
1231
+ })
1232
+ .events(() => ({
1233
+ start_challenge: () => {
1234
+ Actions.setTime({ time: "day" });
1235
+ Actions.gamerule({ rule: "naturalRegeneration", value: false });
1236
+ Actions.announce({ message: "Last player standing wins!" });
1237
+ },
1238
+ init_participants: () => {
1239
+ Actions.give({ target: "all", item: "minecraft:stone_sword", count: 1 });
1240
+ Actions.give({ target: "all", item: "minecraft:leather_chestplate", count: 1 });
1241
+ Actions.give({ target: "all", item: "minecraft:cooked_beef", count: 10 });
1242
+ },
1243
+ }))
1244
+ .custom_events(({ kills }) => [
1245
+ {
1246
+ score: kills,
1247
+ target: 1,
1248
+ mode: "fire_once",
1249
+ actions: () => {
1250
+ Actions.announce({ message: "First blood!" });
1251
+ },
1252
+ },
1253
+ ])
1254
+ .end_condition(({ alive_players }) => alive_players.equalTo(1))
1255
+ .win_conditions(({ sole_survivor }, { fighter }) => ({
1256
+ [fighter]: sole_survivor.equalTo(1),
1257
+ }));
1258
+ ```
1259
+
1260
+ ### Example 4: Team-Based - Hunters vs Protectors
1261
+
1262
+ ```typescript
1263
+ import { createChallenge, Actions, forEveryPlayer } from "@kradle/challenges";
1264
+ import { _ } from "sandstone";
1265
+
1266
+ createChallenge({
1267
+ name: "pig-farming-v2",
1268
+ kradle_challenge_path: "./output",
1269
+ roles: ["pighunter", "pigsaver"] as const,
1270
+ GAME_DURATION: 2 * 60 * 20,
1271
+ custom_variables: {
1272
+ pigs_killed: {
1273
+ type: "individual",
1274
+ objective_type: "minecraft.killed:minecraft.pig",
1275
+ default: 0,
1276
+ updater: (value, { main_score }) => {
1277
+ main_score.set(value);
1278
+ },
1279
+ },
1280
+ pig_killed_max: {
1281
+ type: "global",
1282
+ updater: (value, { pigs_killed }) => {
1283
+ value.set(0);
1284
+ forEveryPlayer(() => {
1285
+ _.if(pigs_killed.greaterThan(value), () => {
1286
+ value.set(pigs_killed);
1287
+ });
1288
+ });
1289
+ },
1290
+ },
1291
+ },
1292
+ })
1293
+ .events((vars, { pighunter, pigsaver }) => ({
1294
+ start_challenge: () => {
1295
+ Actions.setTime({ time: "day" });
1296
+ Actions.announce({ message: "Hunters: Kill 2 pigs! Protectors: Stop them!" });
1297
+ },
1298
+ init_participants: () => {
1299
+ // Different items based on role would be set via role-specific logic
1300
+ Actions.give({ target: "all", item: "minecraft:wooden_sword", count: 1 });
1301
+ },
1302
+ }))
1303
+ .custom_events(() => [])
1304
+ .end_condition(({ pig_killed_max }) => pig_killed_max.greaterOrEqualThan(2))
1305
+ .win_conditions(({ pig_killed_max }, { pighunter, pigsaver }) => ({
1306
+ [pighunter]: pig_killed_max.greaterOrEqualThan(2),
1307
+ [pigsaver]: pig_killed_max.lowerThan(2),
1308
+ }));
1309
+ ```
1310
+
1311
+ ### Example 5: Capture the Flag
1312
+
1313
+ ```typescript
1314
+ import { createChallenge, Actions } from "@kradle/challenges";
1315
+ import { _, Selector, rel } from "sandstone";
1316
+ import type { Score } from "sandstone";
1317
+
1318
+ createChallenge({
1319
+ name: "capture-the-flag",
1320
+ kradle_challenge_path: "./output",
1321
+ roles: ["red_team", "blue_team"] as const,
1322
+ GAME_DURATION: 5 * 60 * 20,
1323
+ custom_variables: {
1324
+ holds_red_banner: {
1325
+ type: "individual",
1326
+ updater: (value: Score) => {
1327
+ value.set(0);
1328
+ _.if(Selector("@s", {
1329
+ nbt: { Inventory: [{ id: "minecraft:red_banner" }] }
1330
+ }), () => {
1331
+ value.set(1);
1332
+ });
1333
+ },
1334
+ },
1335
+ stands_blue_wool: {
1336
+ type: "individual",
1337
+ updater: (value: Score) => {
1338
+ value.set(0);
1339
+ _.if(_.block(rel(0, -1, 0), "minecraft:blue_wool"), () => {
1340
+ value.set(1);
1341
+ });
1342
+ },
1343
+ },
1344
+ captured_flag: {
1345
+ type: "individual",
1346
+ updater: (value, { holds_red_banner, stands_blue_wool, main_score }) => {
1347
+ value.set(0);
1348
+ _.if(_.and(
1349
+ holds_red_banner.equalTo(1),
1350
+ stands_blue_wool.equalTo(1)
1351
+ ), () => {
1352
+ value.set(1);
1353
+ main_score.set(1);
1354
+ });
1355
+ },
1356
+ },
1357
+ },
1358
+ })
1359
+ .events(() => ({
1360
+ start_challenge: () => {
1361
+ Actions.announce({ message: "Capture the enemy flag and return to base!" });
1362
+ },
1363
+ init_participants: () => {
1364
+ Actions.give({ target: "all", item: "minecraft:iron_sword", count: 1 });
1365
+ },
1366
+ }))
1367
+ .custom_events(({ captured_flag }) => [
1368
+ {
1369
+ score: captured_flag,
1370
+ target: 1,
1371
+ mode: "fire_once",
1372
+ actions: () => {
1373
+ Actions.announce({ message: "Flag captured! Game over!" });
1374
+ },
1375
+ },
1376
+ ])
1377
+ .end_condition(({ captured_flag }) => captured_flag.equalTo(1))
1378
+ .win_conditions(({ captured_flag }, { red_team, blue_team }) => ({
1379
+ [red_team]: captured_flag.equalTo(0),
1380
+ [blue_team]: captured_flag.equalTo(1),
1381
+ }));
1382
+ ```
1383
+
1384
+ ---
1385
+
1386
+ ## Common Patterns
1387
+
1388
+ ### Pattern 1: Sync Variable to Main Score
1389
+
1390
+ ```typescript
1391
+ my_variable: {
1392
+ type: "individual",
1393
+ objective_type: "some_criterion",
1394
+ updater: (value, { main_score }) => {
1395
+ main_score.set(value);
1396
+ },
1397
+ }
1398
+ ```
1399
+
1400
+ ### Pattern 2: Find Global Maximum
1401
+
1402
+ ```typescript
1403
+ max_global: {
1404
+ type: "global",
1405
+ updater: (value, { individual_score }) => {
1406
+ value.set(0);
1407
+ forEveryPlayer(() => {
1408
+ _.if(individual_score.greaterThan(value), () => {
1409
+ value.set(individual_score);
1410
+ });
1411
+ });
1412
+ },
1413
+ }
1414
+ ```
1415
+
1416
+ ### Pattern 3: Winner Detection (Has Max Score + Alive)
1417
+
1418
+ ```typescript
1419
+ is_winner: {
1420
+ type: "individual",
1421
+ updater: (value, { main_score, max_global, has_never_died }) => {
1422
+ value.set(0);
1423
+ _.if(_.and(
1424
+ main_score.equalTo(max_global),
1425
+ main_score.greaterThan(0),
1426
+ has_never_died.equalTo(1)
1427
+ ), () => {
1428
+ value.set(1);
1429
+ });
1430
+ },
1431
+ }
1432
+ ```
1433
+
1434
+ ### Pattern 4: Track Player Position
1435
+
1436
+ ```typescript
1437
+ current_y: {
1438
+ type: "individual",
1439
+ objective_type: "dummy",
1440
+ updater: (value) => {
1441
+ // Prefered way: using the dedicated Action
1442
+ value.set(Actions.getCurrentPlayerPosition().y)
1443
+
1444
+ // Alternative way: using execute.store + data.get
1445
+ execute.as("@s").store.result.score(value).run.data.get.entity("@s", "Pos[1]");
1446
+ },
1447
+ }
1448
+ ```
1449
+
1450
+ ### Pattern 5: Check Inventory for Item
1451
+
1452
+ ```typescript
1453
+ has_diamond: {
1454
+ type: "individual",
1455
+ updater: (value: Score) => {
1456
+ value.set(0);
1457
+ _.if(Selector("@s", {
1458
+ nbt: { Inventory: [{ id: "minecraft:diamond" }] }
1459
+ }), () => {
1460
+ value.set(1);
1461
+ });
1462
+ },
1463
+ }
1464
+ ```
1465
+
1466
+ ### Pattern 6: Check Block Below Player
1467
+
1468
+ ```typescript
1469
+ on_gold_block: {
1470
+ type: "individual",
1471
+ updater: (value: Score) => {
1472
+ value.set(0);
1473
+ _.if(_.block(rel(0, -1, 0), "minecraft:gold_block"), () => {
1474
+ value.set(1);
1475
+ });
1476
+ },
1477
+ }
1478
+ ```
1479
+
1480
+ ### Pattern 7: Count Entities
1481
+
1482
+ ```typescript
1483
+ zombie_count: {
1484
+ type: "global",
1485
+ updater: (value) => {
1486
+ value.set(0);
1487
+ execute.as(Selector("@e", { type: "zombie" })).run(() => {
1488
+ value.add(1);
1489
+ });
1490
+ },
1491
+ }
1492
+ ```
1493
+
1494
+ ### Pattern 8: Simple End Condition
1495
+
1496
+ ```typescript
1497
+ .end_condition(({ objective_complete }) => objective_complete.equalTo(1))
1498
+ ```
1499
+
1500
+ ### Pattern 9: Multi-Condition End
1501
+
1502
+ ```typescript
1503
+ .end_condition(({ alive_players, objective_complete }) =>
1504
+ _.or(
1505
+ alive_players.equalTo(1),
1506
+ objective_complete.equalTo(1)
1507
+ )
1508
+ )
1509
+ ```
1510
+
1511
+ ### Pattern 10: Opposing Team Win Conditions
1512
+
1513
+ ```typescript
1514
+ .win_conditions(({ team_a_score, team_b_score }, { team_a, team_b }) => ({
1515
+ [team_a]: team_a_score.greaterThan(team_b_score),
1516
+ [team_b]: team_b_score.greaterThan(team_a_score),
1517
+ }))
1518
+ ```
1519
+
1520
+ ---
1521
+
1522
+ ## Tips for LLMs
1523
+
1524
+ 1. **Always use `as const`** for roles array to get proper type inference
1525
+ 2. **Updaters should be idempotent** - they run every tick
1526
+ 3. **Use `forEveryPlayer` for global aggregations** (max, count, any/all checks)
1527
+ 4. **Import `_` from sandstone** for conditions (`_.if`, `_.and`, `_.or`)
1528
+ 5. **Variables are Scores** - use `.set()`, `.add()`, comparison methods
1529
+ 6. **Main score is displayed** - sync your primary metric to `main_score`
1530
+ 7. **Time is in ticks** - multiply seconds by 20
1531
+ 8. **Win conditions are per-role** - each role needs its own condition
1532
+ 9. **Custom events fire per-player** for individual variables