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