@kradle/challenges-sdk 0.5.3 → 0.6.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/dist/actions.js CHANGED
@@ -25,6 +25,35 @@ function mapTarget(target) {
25
25
  }
26
26
  return (0, sandstone_1.Selector)("@a", { tag: target }); // Treat unknown string targets as a role name (roles are stored as player tags)
27
27
  }
28
+ /**
29
+ * Throws if `voteId` — or, when given, `option` — isn't declared in config.ts's
30
+ * `challengeConfig.votingOptions` (passed at build time as
31
+ * `KRADLE_CHALLENGE_VOTING_OPTIONS`), mirroring `getLocation`/`setEndState`.
32
+ * No-op when the env var is unset (e.g. running `challenge.ts` outside the CLI).
33
+ */
34
+ function assertDeclaredVote(voteId, option) {
35
+ const declared = process.env.KRADLE_CHALLENGE_VOTING_OPTIONS;
36
+ if (!declared)
37
+ return;
38
+ const votes = JSON.parse(declared);
39
+ const options = votes[voteId];
40
+ if (!options) {
41
+ throw new Error(`Invalid vote "${voteId}". Valid votes are: ${Object.keys(votes).join(", ")}. ` +
42
+ `Make sure the vote is defined in config.ts under "challengeConfig.votingOptions".`);
43
+ }
44
+ if (option !== undefined && !options.includes(option)) {
45
+ throw new Error(`Invalid option "${option}" for vote "${voteId}". Valid options are: ${options.join(", ")}.`);
46
+ }
47
+ }
48
+ /**
49
+ * Selects participants who haven't voted in `voteId` yet (missing the
50
+ * `kradle.voteOptions.<voteId>` tag). The multi-tag, `!`-negated form needs
51
+ * `as never` because Sandstone's Selector `tag` type doesn't accept a string[]
52
+ * (same workaround as api_utils.ts).
53
+ */
54
+ function participantsMissingVote(voteId) {
55
+ return (0, sandstone_1.Selector)("@a", { tag: [constants_1.KRADLE_PARTICIPANT_TAG, `!kradle.voteOptions.${voteId}`] });
56
+ }
28
57
  exports.Actions = {
29
58
  /**
30
59
  * Send a chat message to everyone.
@@ -33,6 +62,65 @@ exports.Actions = {
33
62
  announce: ({ message }) => {
34
63
  (0, sandstone_1.tellraw)("@a", ["\n", { text: constants_1.DISPLAY_TAG, color: "aqua" }, " | ", message, "\n"]);
35
64
  },
65
+ /**
66
+ * Pick `count` random participants and tag them. Used to assign hidden
67
+ * roles in social-deduction challenges — combine with `Actions.tellraw` to
68
+ * privately notify each tagged participant of their assignment.
69
+ *
70
+ * Uses Minecraft's `sort=random,limit=N` selector behavior. The selection
71
+ * happens at command time (typically `start_challenge`), so it's stable
72
+ * for the rest of the run.
73
+ *
74
+ * @example Saboteur assignment in a 4-player game:
75
+ * ```ts
76
+ * start_challenge: () => {
77
+ * Actions.assignRandomTag({ tag: "saboteur", count: 1 });
78
+ * // Then DM each tagged participant their secret role:
79
+ * Actions.tellraw({
80
+ * target: Selector("@a", { tag: "saboteur" }),
81
+ * message: [{ text: "🔪 You're the saboteur. Stop the miners.", color: "red" }],
82
+ * });
83
+ * }
84
+ * ```
85
+ *
86
+ * `count` is capped to the number of participants — over-asking just tags
87
+ * everyone available.
88
+ */
89
+ assignRandomTag: ({ tag: tagName, count }) => {
90
+ sandstone_1.execute.as((0, sandstone_1.Selector)("@a", { tag: constants_1.KRADLE_PARTICIPANT_TAG, sort: "random", limit: count })).run(() => {
91
+ (0, sandstone_1.tag)("@s").add(tagName);
92
+ });
93
+ },
94
+ /**
95
+ * Pick a uniform random integer in `[min, max]` (both inclusive) and
96
+ * return it as an anonymous `Score`. Wraps Minecraft's `random value`
97
+ * command, which Sandstone does not expose as a typed primitive — this
98
+ * helper is the abstraction so that user code never has to write `raw`
99
+ * for randomness.
100
+ *
101
+ * Use to randomize challenge state at `start_challenge` (which goal block
102
+ * is "the" goal, which player a saboteur is paired with for a side-quest,
103
+ * etc.) so behavior isn't gameable across runs.
104
+ *
105
+ * @example Pick one of 4 goal directions per run:
106
+ * ```ts
107
+ * goal_index: { type: "global", default: 0 },
108
+ *
109
+ * start_challenge: () => {
110
+ * variables.goal_index.score.set(Actions.randomInt({ min: 0, max: 3 }));
111
+ * // Then branch: _.if(goal_index.equalTo(0), () => place at N), etc.
112
+ * }
113
+ * ```
114
+ */
115
+ randomInt: ({ min, max }) => {
116
+ const result = (0, sandstone_1.Variable)(0);
117
+ // Sandstone has no typed wrapper for `random value <min>..<max>`.
118
+ // This is the single legitimate `raw` in the SDK — encapsulating it
119
+ // here means user code never has to. Once Sandstone gains the
120
+ // command, swap the body for `execute.store.result.score(result).run.random.value(...)`.
121
+ (0, sandstone_1.raw)(`execute store result score ${result.target} ${result.objective.name} run random value ${min}..${max}`);
122
+ return result;
123
+ },
36
124
  /**
37
125
  * Clear the inventory of a target.
38
126
  * @param {TargetNames} target - The target to clear the inventory of.
@@ -58,6 +146,7 @@ exports.Actions = {
58
146
  * @param {number} y2 - The y coordinate of the region.
59
147
  * @param {number} z2 - The z coordinate of the region.
60
148
  * @param {boolean} absolute - Whether the coordinates are absolute or relative.
149
+ * @param {"fill" | "line" | "pyramid"} mode - Required. How to fill the region: `"fill"` (solid box between the two points), `"line"` (1-wide line from point 1 to point 2), or `"pyramid"` (pyramid `|y2|` blocks tall, stepping in by 2 per layer). Omitting it throws `Invalid fill mode` at build time.
61
150
  */
62
151
  fill: ({ block, x1, y1, z1, x2, y2, z2, absolute, mode, }) => {
63
152
  // fill the region with the block
@@ -165,6 +254,34 @@ exports.Actions = {
165
254
  kill: ({ selector }) => {
166
255
  (0, sandstone_1.kill)(mapTarget(selector));
167
256
  },
257
+ /**
258
+ * Set the gamemode of a target. Useful for moving an eliminated player
259
+ * to spectator without killing them (preserves the entity, lets them
260
+ * watch the rest of the run).
261
+ *
262
+ * @example
263
+ * Actions.setGamemode({ target: "self", mode: "spectator" });
264
+ * Actions.setGamemode({ target: "all", mode: "adventure" });
265
+ */
266
+ setGamemode: ({ target, mode, }) => {
267
+ (0, sandstone_1.gamemode)(mode, mapTarget(target));
268
+ },
269
+ /**
270
+ * Eliminate a participant from the active round: switch them to
271
+ * spectator mode (so they can still observe but can't act). Sister
272
+ * helper to `kill`, intended for vote-out / king-of-the-hill / battle
273
+ * royale style games where elimination shouldn't end the agent's
274
+ * connection.
275
+ *
276
+ * Note: this only flips gamemode. The challenge author is responsible
277
+ * for any per-player tracking variable (e.g. `is_alive`).
278
+ *
279
+ * @example
280
+ * Actions.eliminate({ target: "self" });
281
+ */
282
+ eliminate: ({ target }) => {
283
+ (0, sandstone_1.gamemode)("spectator", mapTarget(target));
284
+ },
168
285
  /**
169
286
  * Set an attribute for a target.
170
287
  * @param {ATTRIBUTES} attribute_ - The attribute to set.
@@ -207,6 +324,50 @@ exports.Actions = {
207
324
  }
208
325
  sandstone_1.data.modify.storage(constants_1.KRADLE_STORAGE, constants_1.END_STATE_KEY).set.value(endState);
209
326
  },
327
+ /**
328
+ * For each `(name, condition)` pair, emit `_.if(condition, () =>
329
+ * setEndState(name))` at the current codegen location. Where (and how
330
+ * often) those checks run is up to the caller — `events.on_tick` for a
331
+ * per-tick check, `custom_events.actions` for a single fire, etc.
332
+ *
333
+ * Returns a `ConditionType` that is true when at least one of the input
334
+ * conditions is true (i.e. an end-state was applied). Hand it to
335
+ * `.end_condition()` if you want the game to end as soon as any end-state
336
+ * has been set; ignore it if the game's end is gated on something else
337
+ * (timer, separate condition).
338
+ *
339
+ * Note: conditions are evaluated in whatever execution context the caller
340
+ * is in. From `on_tick` that's the server entity, where per-player score
341
+ * reads silently fail — see `Actions.maxPlayerScore` for the per-player →
342
+ * global bridge.
343
+ *
344
+ * @example
345
+ * let endReached: ConditionType | undefined;
346
+ * const challenge = createChallenge({...});
347
+ * challenge
348
+ * .events((vars) => ({
349
+ * on_tick: () => {
350
+ * endReached = Actions.setEndStates({
351
+ * winner_found: vars.top_kills.greaterOrEqualThan(20),
352
+ * all_dead: vars.alive_players.equalTo(0),
353
+ * });
354
+ * },
355
+ * }))
356
+ * .end_condition(() => endReached!); // game ends as soon as any branch fires
357
+ */
358
+ setEndStates: (endStates) => {
359
+ const entries = Object.entries(endStates);
360
+ if (entries.length === 0) {
361
+ throw new Error("Actions.setEndStates: at least one end-state must be provided.");
362
+ }
363
+ for (const [name, condition] of entries) {
364
+ sandstone_1._.if(condition, () => {
365
+ exports.Actions.setEndState({ endState: name });
366
+ });
367
+ }
368
+ const conditions = entries.map(([, condition]) => condition);
369
+ return conditions.length === 1 ? conditions[0] : sandstone_1._.or(...conditions);
370
+ },
210
371
  /**
211
372
  * Set a block at a specific location.
212
373
  */
@@ -265,6 +426,137 @@ exports.Actions = {
265
426
  set: ({ variable, value }) => {
266
427
  variable.set(value);
267
428
  },
429
+ /**
430
+ * Selector that matches `@s` only when standing on the block at the
431
+ * given absolute coordinate. Use this anywhere you'd otherwise have
432
+ * written `Selector("@s", { x, y, z })` — that bare form does NOT
433
+ * constrain position (Minecraft treats x/y/z as the selector origin
434
+ * for distance math, not as a position filter), so the condition
435
+ * silently passes for everyone. This helper bakes in the right
436
+ * `dx: 0, dy: 1, dz: 0` volume covering the block above and the
437
+ * head space, so the selector matches only when the player's feet
438
+ * are inside the 1×1×1 column at (x, y+1, z).
439
+ *
440
+ * @param {number} x - X of the block being stood on.
441
+ * @param {number} y - Y of the block being stood on (the player's feet are at y+1).
442
+ * @param {number} z - Z of the block being stood on.
443
+ * @param {string} [role] - Optional role tag the player must also have.
444
+ *
445
+ * @example
446
+ * // Trigger alice_shared when Alice steps on her pad block.
447
+ * alice_shared: {
448
+ * type: "global",
449
+ * default: 0,
450
+ * updater: (value) => {
451
+ * _.if(value.equalTo(0), () => {
452
+ * forEveryPlayer(() => {
453
+ * _.if(Actions.standingOnBlock({ x: ALICE_PAD.x, y: ALICE_PAD.y, z: ALICE_PAD.z, role: "alice" }), () => {
454
+ * value.set(1);
455
+ * });
456
+ * });
457
+ * });
458
+ * },
459
+ * },
460
+ */
461
+ standingOnBlock: ({ x, y, z, role }) => (0, sandstone_1.Selector)("@s", {
462
+ ...(role ? { tag: role } : {}),
463
+ x,
464
+ y: y + 1,
465
+ z,
466
+ dx: 0,
467
+ dy: 1,
468
+ dz: 0,
469
+ }),
470
+ /**
471
+ * Condition that matches `@s` when the player has voted in `voteId` (any
472
+ * option), or — when `option` is given — only when they voted for that
473
+ * specific option. Agents cast votes with skills.voteForOption; the arena
474
+ * records each pick as the player tags kradle.voteOptions.<voteId> and
475
+ * kradle.voteOptions.<voteId>.<option>.
476
+ *
477
+ * It matches `@s`, so use it as a CONDITION in a player context — inside
478
+ * `forEveryPlayer(...)`, or in `setEndStates` / win conditions. Don't hand it
479
+ * to `execute.as(...)` from a tick/load context: `@s` resolves to no one
480
+ * there, so the command runs for nobody (the context-safety check flags this).
481
+ *
482
+ * @param {string} voteId - The vote's id, e.g. "selectroom".
483
+ * @param {string} [option] - Optional specific option, e.g. "red".
484
+ * @returns {ConditionType} a per-player condition for `_.if` / win conditions.
485
+ * @throws {Error} If the vote or option isn't declared in config.ts's votingOptions.
486
+ *
487
+ * @example
488
+ * // Teleport everyone who picked the red room.
489
+ * forEveryPlayer(() => {
490
+ * _.if(Actions.votedFor("selectroom", "red"), () => {
491
+ * Actions.teleport({ target: "self", x: 10, y: -60, z: 10, absolute: true });
492
+ * });
493
+ * });
494
+ */
495
+ votedFor: (voteId, option) => {
496
+ assertDeclaredVote(voteId, option);
497
+ return (0, sandstone_1.Selector)("@s", {
498
+ tag: `kradle.voteOptions.${voteId}${option ? `.${option}` : ""}`,
499
+ });
500
+ },
501
+ /**
502
+ * Announces a vote to all participants over the `***KRADLE***` channel agents
503
+ * read, naming the vote and its options. Agents respond with
504
+ * skills.voteForOption(voteId, option). Compose with `allVotesIn`,
505
+ * `assignDefaultVotes`, and `votedFor` to build a full vote phase.
506
+ *
507
+ * @param {string} voteId - The vote's id, e.g. "selectroom".
508
+ * @param {string} prompt - Human-readable instruction, e.g. "Pick a room!".
509
+ * @param {string[]} options - Valid options, e.g. ["red", "green", "blue"].
510
+ * @throws {Error} If the vote or any option isn't declared in config.ts's votingOptions.
511
+ *
512
+ * @example
513
+ * Actions.promptVote({ voteId: "selectroom", prompt: "Pick a room!", options: ["red", "green"] });
514
+ */
515
+ promptVote: ({ voteId, prompt, options }) => {
516
+ assertDeclaredVote(voteId);
517
+ for (const option of options)
518
+ assertDeclaredVote(voteId, option);
519
+ exports.Actions.announce({
520
+ message: `${prompt} Vote with skills.voteForOption("${voteId}", option) — options: ${options.join(", ")}.`,
521
+ });
522
+ },
523
+ /**
524
+ * Condition that is true once every participant has voted in `voteId` — i.e.
525
+ * no participant is still missing the `kradle.voteOptions.<voteId>` tag the
526
+ * arena writes on each vote. Combine with a timer to end a vote phase as soon
527
+ * as everyone is in.
528
+ *
529
+ * @param {string} voteId - The vote's id, e.g. "selectroom".
530
+ * @returns {ConditionType} true when all participants have voted.
531
+ * @throws {Error} If the vote isn't declared in config.ts's votingOptions.
532
+ *
533
+ * @example
534
+ * _.if(_.or(Actions.allVotesIn("selectroom"), vars.voteTimer.greaterOrEqualThan(600)), () => { ... });
535
+ */
536
+ allVotesIn: (voteId) => {
537
+ assertDeclaredVote(voteId);
538
+ return sandstone_1._.not(participantsMissingVote(voteId));
539
+ },
540
+ /**
541
+ * Records `default` for every participant who has not voted in `voteId` yet —
542
+ * call this when a vote phase times out so non-voters are still moved by
543
+ * `votedFor`. Writes the same tags the arena would (kradle.voteOptions.<voteId>
544
+ * and kradle.voteOptions.<voteId>.<option>).
545
+ *
546
+ * @param {string} voteId - The vote's id, e.g. "selectroom".
547
+ * @param {string} default - Option assigned to non-voters, e.g. "red".
548
+ * @throws {Error} If the vote or default option isn't declared in config.ts's votingOptions.
549
+ *
550
+ * @example
551
+ * Actions.assignDefaultVotes({ voteId: "selectroom", default: "red" });
552
+ */
553
+ assignDefaultVotes: ({ voteId, default: defaultOption }) => {
554
+ assertDeclaredVote(voteId, defaultOption);
555
+ sandstone_1.execute.as(participantsMissingVote(voteId)).run(() => {
556
+ (0, sandstone_1.tag)("@s").add(`kradle.voteOptions.${voteId}`);
557
+ (0, sandstone_1.tag)("@s").add(`kradle.voteOptions.${voteId}.${defaultOption}`);
558
+ });
559
+ },
268
560
  /**
269
561
  * log a message with the watcher
270
562
  * @param {string} message - The message to send.
@@ -284,8 +576,8 @@ exports.Actions = {
284
576
  * @param {boolean} absolute - Whether the coordinates are absolute or relative.
285
577
  */
286
578
  summonItem: ({ item, x, y, z, absolute }) => {
287
- const abs = absolute ? "" : "~";
288
- (0, sandstone_1.raw)(`summon item ${abs}${x} ${abs}${y} ${abs}${z} {Item:{id:"${item}",Count:1b}}`);
579
+ const pos = absolute ? (0, sandstone_1.abs)(x, y, z) : (0, sandstone_1.rel)(x, y, z);
580
+ (0, sandstone_1.summon)("minecraft:item", pos, { Item: { id: item, Count: sandstone_1.NBT.byte(1) } });
289
581
  },
290
582
  /**
291
583
  * Count the number of a specific item in a target's inventory.
@@ -299,6 +591,219 @@ exports.Actions = {
299
591
  sandstone_1.execute.store.result.score(variable).run.clear(mapTarget(target), item, 0);
300
592
  return variable;
301
593
  },
594
+ /**
595
+ * Compute the maximum value of an individual score across all
596
+ * participants and store it in a global Variable. Returns the variable.
597
+ *
598
+ * Why this helper exists: end-state and end-condition checks evaluated
599
+ * from `events.on_tick` run in server context (the caller is a non-player
600
+ * entity), so an `if score @s X matches Y` check on a per-player score
601
+ * silently fails because @s has no entry on the per-player objective.
602
+ * Wrapping the comparison in this helper materializes a global "max
603
+ * across players" value that IS safe to reference from those checks.
604
+ *
605
+ * @param {Score} score - The per-player score to aggregate.
606
+ * @param {number} [min=0] - Lower bound to seed the result with. Pass a
607
+ * negative value if your per-player score can go below 0 (e.g. climb's
608
+ * `current_height` defaulting to -1000); otherwise the default `0`
609
+ * would mask negative actual maxes.
610
+ * @returns {Score} An anonymous Score containing the max.
611
+ *
612
+ * @example
613
+ * // In a custom_variables global updater:
614
+ * max_diamonds: {
615
+ * type: "global",
616
+ * default: 0,
617
+ * updater: (value, { diamonds }) => {
618
+ * value.set(Actions.maxPlayerScore({ score: diamonds }));
619
+ * },
620
+ * },
621
+ *
622
+ * // Then end-states can reference the global:
623
+ * .events(({ max_diamonds }) => ({
624
+ * on_tick: () => Actions.setEndStates({
625
+ * winner_found: max_diamonds.greaterOrEqualThan(5),
626
+ * }),
627
+ * }))
628
+ *
629
+ * @example
630
+ * // Climb's max_height — y can go below 0:
631
+ * max_height_global: {
632
+ * type: "global",
633
+ * default: -1000,
634
+ * updater: (value, { max_height }) => {
635
+ * value.set(Actions.maxPlayerScore({ score: max_height, min: -1000 }));
636
+ * },
637
+ * },
638
+ */
639
+ maxPlayerScore: ({ score, min = 0 }) => {
640
+ const result = (0, sandstone_1.Variable)(min);
641
+ // Iterate over participants; for each, if their `score` exceeds the
642
+ // running max, copy it in. Same pattern an author would write by
643
+ // hand with forEveryPlayer + _.if(score.greaterThan(result), ...).
644
+ sandstone_1.execute.as(constants_1.ALL).run(() => {
645
+ sandstone_1._.if(score.greaterThan(result), () => {
646
+ result.set(score);
647
+ });
648
+ });
649
+ return result;
650
+ },
651
+ /**
652
+ * Returns a Sandstone condition that's true when the executing player
653
+ * (in `as @s` context) is standing on `block`. The default is a
654
+ * single-cell check (block at `~ ~-1 ~`), but `slop` widens the check
655
+ * to a `(2*slop+1)×(2*slop+1)` patch around the player's foot position
656
+ * — useful when the agent's pathfinder leaves them slightly off-center
657
+ * from a target block (e.g. mineflayer's `goToNearestBlock` reports
658
+ * success once within ~1.5 blocks of the target, so the agent is
659
+ * commonly off by a fractional block in x/z).
660
+ *
661
+ * Compiles to: `block ~ ~-1 ~ <block>` for slop=0, or an `_.or(...)` of
662
+ * 9 / 25 / etc. cell checks for slop=1 / 2 / etc.
663
+ *
664
+ * @example
665
+ * // PD-style "did the agent step on the cooperate pad?" check
666
+ * choice: {
667
+ * type: "individual",
668
+ * default: 0,
669
+ * updater: (value) => {
670
+ * _.if(Actions.isStandingOn({ block: "minecraft:lime_wool", slop: 1 }), () => {
671
+ * value.set(COOPERATE);
672
+ * });
673
+ * },
674
+ * },
675
+ */
676
+ isStandingOn: ({ block, slop = 0 }) => {
677
+ if (slop < 0 || !Number.isInteger(slop)) {
678
+ throw new Error(`Actions.isStandingOn: slop must be a non-negative integer, got ${slop}`);
679
+ }
680
+ if (slop === 0) {
681
+ return sandstone_1._.block((0, sandstone_1.rel)(0, -1, 0), block);
682
+ }
683
+ const conditions = [];
684
+ for (let dx = -slop; dx <= slop; dx++) {
685
+ for (let dz = -slop; dz <= slop; dz++) {
686
+ conditions.push(sandstone_1._.block((0, sandstone_1.rel)(dx, -1, dz), block));
687
+ }
688
+ }
689
+ return sandstone_1._.or(...conditions);
690
+ },
691
+ /**
692
+ * Count instances of a block within an axis-aligned region. Useful for
693
+ * detecting agent-built structures without pinning detection to a single
694
+ * coordinate (which breaks the moment the agent moves before building).
695
+ *
696
+ * Implementation note: emits one `execute if block` check per cell in the
697
+ * region at codegen time, so the region is hard-capped at 4096 cells
698
+ * (~64×8×8) to keep the generated datapack reasonable.
699
+ *
700
+ * @param block The block ID to count (e.g. "minecraft:cobblestone").
701
+ * @param from First corner of the region (inclusive).
702
+ * @param to Opposite corner (inclusive).
703
+ * @param absolute Whether `from` / `to` are absolute world coordinates.
704
+ * @returns A Sandstone `Score` containing the count.
705
+ *
706
+ * @example
707
+ * // Detect any 5+ cobblestone block tower built within a 12×6×12 zone:
708
+ * const count = Actions.countBlocksInRegion({
709
+ * block: "minecraft:cobblestone",
710
+ * from: { x: -6, y: -60, z: -6 },
711
+ * to: { x: 6, y: -55, z: 6 },
712
+ * absolute: true,
713
+ * });
714
+ * _.if(count.greaterOrEqualThan(5), () => Actions.announce({ message: "Tower built!" }));
715
+ */
716
+ countBlocksInRegion: ({ block, from, to, absolute, }) => {
717
+ const x1 = Math.min(from.x, to.x);
718
+ const x2 = Math.max(from.x, to.x);
719
+ const y1 = Math.min(from.y, to.y);
720
+ const y2 = Math.max(from.y, to.y);
721
+ const z1 = Math.min(from.z, to.z);
722
+ const z2 = Math.max(from.z, to.z);
723
+ const cellCount = (x2 - x1 + 1) * (y2 - y1 + 1) * (z2 - z1 + 1);
724
+ const MAX_CELLS = 4096;
725
+ if (cellCount > MAX_CELLS) {
726
+ throw new Error(`Actions.countBlocksInRegion: region has ${cellCount} cells, exceeding the safety cap of ${MAX_CELLS}. ` +
727
+ `Reduce the bounding box or use a smaller scan area.`);
728
+ }
729
+ const counter = (0, sandstone_1.Variable)();
730
+ counter.set(0);
731
+ for (let x = x1; x <= x2; x++) {
732
+ for (let y = y1; y <= y2; y++) {
733
+ for (let z = z1; z <= z2; z++) {
734
+ const coords = absolute ? (0, sandstone_1.abs)(x, y, z) : (0, sandstone_1.rel)(x, y, z);
735
+ sandstone_1.execute.if.block(coords, block).run(() => {
736
+ counter.add(1);
737
+ });
738
+ }
739
+ }
740
+ }
741
+ return counter;
742
+ },
743
+ /**
744
+ * Count instances of a block in a region centered on a target. Same idea
745
+ * as `countBlocksInRegion` but the bounds are *relative to the target's
746
+ * current position* — useful when the region of interest moves with a
747
+ * player (e.g. "how many X blocks are around the agent right now").
748
+ *
749
+ * What this is NOT: it does not verify a structure (the matches don't
750
+ * have to be contiguous), and it does not prove the target placed them.
751
+ * Treat the result as "presence count in a volume." Turn it into "the
752
+ * agent placed N of these" only when the block type doesn't spawn
753
+ * naturally in your world (and your arena init didn't pre-place any), or
754
+ * by clearing the volume first and counting the delta.
755
+ *
756
+ * Implementation note: emits one `execute as <target> at @s if block`
757
+ * check per cell at codegen time, so the region is hard-capped at 4096
758
+ * cells just like `countBlocksInRegion`.
759
+ *
760
+ * @example
761
+ * // Count diamond_block instances within 6 blocks of the agent. Diamond
762
+ * // blocks don't generate naturally, so in a flat-world challenge whose
763
+ * // arena init doesn't pre-place any, this is equivalently the count of
764
+ * // diamond blocks the agent has placed within reach.
765
+ * diamond_blocks_nearby: {
766
+ * type: "individual",
767
+ * default: 0,
768
+ * updater: (value) => {
769
+ * value.set(Actions.countBlocksRelativeTo({
770
+ * target: "self",
771
+ * block: "minecraft:diamond_block",
772
+ * dxRange: [-6, 6],
773
+ * dyRange: [-2, 4],
774
+ * dzRange: [-6, 6],
775
+ * }));
776
+ * },
777
+ * }
778
+ */
779
+ countBlocksRelativeTo: ({ target, block, dxRange, dyRange, dzRange, }) => {
780
+ const [dx1, dx2] = [Math.min(...dxRange), Math.max(...dxRange)];
781
+ const [dy1, dy2] = [Math.min(...dyRange), Math.max(...dyRange)];
782
+ const [dz1, dz2] = [Math.min(...dzRange), Math.max(...dzRange)];
783
+ const cellCount = (dx2 - dx1 + 1) * (dy2 - dy1 + 1) * (dz2 - dz1 + 1);
784
+ const MAX_CELLS = 4096;
785
+ if (cellCount > MAX_CELLS) {
786
+ throw new Error(`Actions.countBlocksRelativeTo: region has ${cellCount} cells, exceeding the safety cap of ${MAX_CELLS}. ` +
787
+ `Reduce dx/dy/dzRange or scan in slices.`);
788
+ }
789
+ const counter = (0, sandstone_1.Variable)();
790
+ counter.set(0);
791
+ sandstone_1.execute
792
+ .as(mapTarget(target))
793
+ .at("@s")
794
+ .run(() => {
795
+ for (let dx = dx1; dx <= dx2; dx++) {
796
+ for (let dy = dy1; dy <= dy2; dy++) {
797
+ for (let dz = dz1; dz <= dz2; dz++) {
798
+ sandstone_1.execute.if.block((0, sandstone_1.rel)(dx, dy, dz), block).run(() => {
799
+ counter.add(1);
800
+ });
801
+ }
802
+ }
803
+ }
804
+ });
805
+ return counter;
806
+ },
302
807
  /**
303
808
  * Get the current player's position as x, y, z Score variables.
304
809
  * Must be called in a player context (e.g., inside forEveryPlayer or when @s is a player).
@@ -333,5 +838,296 @@ exports.Actions = {
333
838
  }
334
839
  return (0, sandstone_1.Selector)("@e", { type: "minecraft:marker", tag: `kradle.location.${name}`, limit: 1 });
335
840
  },
841
+ /**
842
+ * Build a box (4 walls + optional floor/roof) in a single call. Replaces
843
+ * the typical 5+ `Actions.fill` calls per arena.
844
+ *
845
+ * @param block The wall block (e.g. "minecraft:stone").
846
+ * @param from First corner (inclusive).
847
+ * @param to Opposite corner (inclusive).
848
+ * @param absolute Whether coordinates are absolute or player-relative.
849
+ * @param hollow When true (default), only walls are placed; the interior is left untouched.
850
+ * When false, the entire volume is filled with `block`.
851
+ * @param floor Optional block to fill the floor with (overrides `block` for floor only).
852
+ * @param roof Optional block to fill the roof with.
853
+ * @param clearInside When true and `hollow=true`, the interior volume is set to air
854
+ * (useful for clearing existing terrain inside the new box).
855
+ *
856
+ * @example
857
+ * // 13x13x5 stone arena with a stone-bricks floor, no roof, interior cleared
858
+ * Actions.box({
859
+ * block: "minecraft:stone",
860
+ * from: { x: -6, y: -60, z: -6 },
861
+ * to: { x: 6, y: -55, z: 6 },
862
+ * absolute: true,
863
+ * hollow: true,
864
+ * floor: "minecraft:stone_bricks",
865
+ * clearInside: true,
866
+ * });
867
+ */
868
+ box: ({ block, from, to, absolute, hollow = true, floor, roof, clearInside = false, }) => {
869
+ const x1 = Math.min(from.x, to.x);
870
+ const x2 = Math.max(from.x, to.x);
871
+ const y1 = Math.min(from.y, to.y);
872
+ const y2 = Math.max(from.y, to.y);
873
+ const z1 = Math.min(from.z, to.z);
874
+ const z2 = Math.max(from.z, to.z);
875
+ const fillRegion = (b, ax, ay, az, bx, by, bz) => {
876
+ const c1 = absolute ? (0, sandstone_1.abs)(ax, ay, az) : (0, sandstone_1.rel)(ax, ay, az);
877
+ const c2 = absolute ? (0, sandstone_1.abs)(bx, by, bz) : (0, sandstone_1.rel)(bx, by, bz);
878
+ (0, sandstone_1.fill)(c1, c2, b);
879
+ };
880
+ if (!hollow) {
881
+ fillRegion(block, x1, y1, z1, x2, y2, z2);
882
+ if (floor)
883
+ fillRegion(floor, x1, y1, z1, x2, y1, z2);
884
+ if (roof)
885
+ fillRegion(roof, x1, y2, z1, x2, y2, z2);
886
+ return;
887
+ }
888
+ // Walls: two pairs of opposite faces.
889
+ // North/south
890
+ fillRegion(block, x1, y1, z1, x2, y2, z1);
891
+ fillRegion(block, x1, y1, z2, x2, y2, z2);
892
+ // East/west
893
+ fillRegion(block, x1, y1, z1, x1, y2, z2);
894
+ fillRegion(block, x2, y1, z1, x2, y2, z2);
895
+ if (floor)
896
+ fillRegion(floor, x1, y1, z1, x2, y1, z2);
897
+ if (roof)
898
+ fillRegion(roof, x1, y2, z1, x2, y2, z2);
899
+ if (clearInside) {
900
+ const interiorBlock = clearInside === true ? "minecraft:air" : clearInside;
901
+ // Only fill the interior if there's actually room (>= 3 in each axis).
902
+ if (x2 - x1 >= 2 && y2 - y1 >= 2 && z2 - z1 >= 2) {
903
+ fillRegion(interiorBlock, x1 + 1, y1 + 1, z1 + 1, x2 - 1, y2 - 1, z2 - 1);
904
+ }
905
+ }
906
+ },
907
+ /**
908
+ * Give every participant a list of items in one call. Replaces the
909
+ * sequence of `Actions.give({ target: "all", item: …, count: … })` calls
910
+ * that opens nearly every multi-player init_participants (castle-ctf has
911
+ * six of them for the iron-sword + 4 armor pieces + food loadout).
912
+ *
913
+ * @example PvP standard kit:
914
+ * ```ts
915
+ * Actions.giveAll([
916
+ * { item: "minecraft:iron_sword" },
917
+ * { item: "minecraft:iron_helmet" },
918
+ * { item: "minecraft:iron_chestplate" },
919
+ * { item: "minecraft:iron_leggings" },
920
+ * { item: "minecraft:iron_boots" },
921
+ * { item: "minecraft:cooked_beef", count: 5 },
922
+ * ]);
923
+ * ```
924
+ *
925
+ * `count` defaults to 1. For role-specific equipment, use plain
926
+ * `Actions.give({ target: roleName, … })`.
927
+ */
928
+ giveAll: (items) => {
929
+ for (const it of items) {
930
+ (0, sandstone_1.give)(mapTarget("all"), it.item, it.count ?? 1);
931
+ }
932
+ },
933
+ /**
934
+ * "When the block at this absolute coordinate stops being `expected`, set
935
+ * `into` to 1." The standard goal_broken / flag_broken global-variable
936
+ * updater idiom. Latches at 1 — once tripped, stays tripped (so the
937
+ * end_state fires consistently even if the block is later replaced).
938
+ *
939
+ * Replaces the 4-line `_.if(_.not(_.block(abs(...), ...)), () => value.set(1))`
940
+ * pattern that appears in ~10 challenges' custom_variable updaters
941
+ * (castle-ctf×2, castle-siege×4, defender-striker, beanstalk, tower-trio,
942
+ * tunnel-rush, sky-walker, …). Removes the need to import `abs` from
943
+ * sandstone for this purpose.
944
+ *
945
+ * For multi-block checks ("all N landmarks broken" — diamond-hunt,
946
+ * return-home), keep using the manual `_.if(_.and(...conditions), …)`
947
+ * pattern — this helper is single-block only.
948
+ *
949
+ * @example castle-ctf red flag (latching — once broken, stays broken):
950
+ * ```ts
951
+ * red_flag_broken: {
952
+ * type: "global",
953
+ * default: 0,
954
+ * updater: (value) => {
955
+ * // Latch: only flip 0→1, never reset to 0. The returned Score is
956
+ * // itself a Condition (truthy = non-zero), so no `.equalTo(1)` needed.
957
+ * _.if(Actions.detectBlockMissing({
958
+ * at: { x: RED_BASE.x, y: RED_BASE.y + 1, z: RED_BASE.z },
959
+ * expected: "minecraft:red_wool",
960
+ * }), () => value.set(1));
961
+ * },
962
+ * }
963
+ * ```
964
+ *
965
+ * @returns {Score} An anonymous Score: 1 if the block at `at` is NOT
966
+ * `expected`, 0 if it is. Non-latching — recomputed every call. Wrap in
967
+ * `_.if(missing, () => value.set(1))` (the Score is itself a Condition,
968
+ * truthy = non-zero) for the latching "once broken, stays broken" pattern.
969
+ */
970
+ detectBlockMissing: ({ at, expected }) => {
971
+ const result = (0, sandstone_1.Variable)(0);
972
+ sandstone_1._.if(sandstone_1._.not(sandstone_1._.block((0, sandstone_1.abs)(at.x, at.y, at.z), expected)), () => {
973
+ result.set(1);
974
+ });
975
+ return result;
976
+ },
977
+ /**
978
+ * Summon a single configurable entity with optional health, attack damage,
979
+ * movement speed, and tags. Uses Sandstone's typed `summon` + NBT helpers
980
+ * (`NBT.byte`, `NBT.float`) so that booleans and NBT primitives serialize
981
+ * correctly — sandstone otherwise emits JS `true` as `{}` (an invalid NBT
982
+ * token), which broke boss-brawl's `CustomNameVisible: true` summon early on.
983
+ *
984
+ * For boss-style configured mobs (boss-solo, boss-brawl). Use
985
+ * `Actions.summonMultiple` for plain entities without stat overrides.
986
+ *
987
+ * @example A 60-HP fast-moving boss zombie:
988
+ * ```ts
989
+ * Actions.summonEntityWithStats({
990
+ * entity: "minecraft:zombie",
991
+ * x: 0, y: -60, z: 0, absolute: true,
992
+ * health: 60,
993
+ * attackDamage: 6,
994
+ * movementSpeed: 0.18,
995
+ * tags: ["boss"], // for `Actions.countEntities({ tag: "boss", … })`
996
+ * });
997
+ * ```
998
+ */
999
+ summonEntityWithStats: ({ entity, x, y, z, absolute, health, attackDamage, movementSpeed, tags, }) => {
1000
+ const attributes = [];
1001
+ if (health !== undefined)
1002
+ attributes.push({ Name: "generic.max_health", Base: health });
1003
+ if (attackDamage !== undefined)
1004
+ attributes.push({ Name: "generic.attack_damage", Base: attackDamage });
1005
+ if (movementSpeed !== undefined)
1006
+ attributes.push({ Name: "generic.movement_speed", Base: movementSpeed });
1007
+ const nbt = {};
1008
+ if (health !== undefined)
1009
+ nbt.Health = sandstone_1.NBT.float(health);
1010
+ if (attributes.length > 0)
1011
+ nbt.Attributes = attributes;
1012
+ if (tags !== undefined && tags.length > 0)
1013
+ nbt.Tags = tags;
1014
+ const pos = absolute ? (0, sandstone_1.abs)(x, y, z) : (0, sandstone_1.rel)(x, y, z);
1015
+ (0, sandstone_1.summon)(entity, pos, nbt);
1016
+ },
1017
+ /**
1018
+ * Count entities matching any combination of `entity` type, `tag`, and/or
1019
+ * `volume`, returning the count as an anonymous `Score`. At least one
1020
+ * filter must be provided (counting "every entity in the world" is almost
1021
+ * always a bug — guarded at runtime).
1022
+ *
1023
+ * Use this in a global custom_variable updater whose value tracks "how
1024
+ * many Xs are currently in the world", e.g. boss alive (zombie count),
1025
+ * chickens left, sheep in a pen.
1026
+ *
1027
+ * @param entity Optional entity type (e.g. `"minecraft:zombie"`).
1028
+ * @param tag Optional tag filter (Minecraft `@e[tag=…]`). Used by e.g.
1029
+ * boss-brawl, which counts zombies tagged `"boss"` to ignore wave-spawned mooks.
1030
+ * @param volume Optional bounding box (Minecraft-style: corner + dx/dy/dz
1031
+ * extents, absolute coords). Omit to count anywhere in the world.
1032
+ * @returns {Score} An anonymous Score holding the count.
1033
+ *
1034
+ * @example boss_alive — counts zombies anywhere
1035
+ * ```ts
1036
+ * boss_alive: {
1037
+ * type: "global",
1038
+ * default: 1,
1039
+ * updater: (value) => {
1040
+ * value.set(Actions.countEntities({ entity: "minecraft:zombie" }));
1041
+ * },
1042
+ * }
1043
+ * ```
1044
+ *
1045
+ * @example boss-brawl — count only the tagged boss zombies
1046
+ * ```ts
1047
+ * value.set(Actions.countEntities({ entity: "minecraft:zombie", tag: "boss" }));
1048
+ * ```
1049
+ *
1050
+ * @example sheep_in_pen — counts sheep inside a 3×1×3 box
1051
+ * ```ts
1052
+ * value.set(Actions.countEntities({
1053
+ * entity: "minecraft:sheep",
1054
+ * volume: { x: 14, y: -59, z: -1, dx: 2, dy: 1, dz: 2 },
1055
+ * }));
1056
+ * ```
1057
+ */
1058
+ countEntities: ({ entity, tag, volume, }) => {
1059
+ if (!entity && !tag && !volume) {
1060
+ throw new Error("Actions.countEntities requires at least one of `entity`, `tag`, or `volume` — counting every entity in the world is almost always a bug.");
1061
+ }
1062
+ const result = (0, sandstone_1.Variable)(0);
1063
+ const selectorParams = {};
1064
+ if (entity)
1065
+ selectorParams.type = entity;
1066
+ if (tag)
1067
+ selectorParams.tag = tag;
1068
+ if (volume) {
1069
+ selectorParams.x = volume.x;
1070
+ selectorParams.y = volume.y;
1071
+ selectorParams.z = volume.z;
1072
+ selectorParams.dx = volume.dx;
1073
+ selectorParams.dy = volume.dy;
1074
+ selectorParams.dz = volume.dz;
1075
+ }
1076
+ sandstone_1.execute.as((0, sandstone_1.Selector)("@e", selectorParams)).run(() => {
1077
+ result.add(1);
1078
+ });
1079
+ return result;
1080
+ },
1081
+ /**
1082
+ * Build a flat open arena: a single-block-thick floor at `center.y` with
1083
+ * cleared airspace above up to `headroom` blocks tall. The "no walls,
1084
+ * just open ground for things to move on" pattern shared by sumo-arena,
1085
+ * boss-brawl, wave-survival, stalker, and one half of defender-striker.
1086
+ *
1087
+ * For arenas with walls, use `Actions.box({ hollow: true, floor: … })`.
1088
+ *
1089
+ * @example
1090
+ * Actions.flatArena({
1091
+ * block: "minecraft:stone",
1092
+ * center: { x: 0, y: -60, z: 0 },
1093
+ * radius: 10, // 21×21 footprint
1094
+ * headroom: 4, // 4 blocks of air above the floor (default 4)
1095
+ * });
1096
+ */
1097
+ flatArena: ({ block, center, radius, headroom = 4, }) => {
1098
+ const x_min = center.x - radius;
1099
+ const x_max = center.x + radius;
1100
+ const z_min = center.z - radius;
1101
+ const z_max = center.z + radius;
1102
+ // Floor at center.y.
1103
+ (0, sandstone_1.fill)((0, sandstone_1.abs)(x_min, center.y, z_min), (0, sandstone_1.abs)(x_max, center.y, z_max), block);
1104
+ // Clear air above (skip if headroom <= 0).
1105
+ if (headroom > 0) {
1106
+ (0, sandstone_1.fill)((0, sandstone_1.abs)(x_min, center.y + 1, z_min), (0, sandstone_1.abs)(x_max, center.y + headroom, z_max), "minecraft:air");
1107
+ }
1108
+ },
1109
+ /**
1110
+ * Apply a non-lethal knockback / displacement to a target via Minecraft's
1111
+ * `effect` command. The default effect is a brief levitation (lifts the
1112
+ * target straight up without dealing damage).
1113
+ *
1114
+ * Use `mode: "slowfall"` to combine levitation with slow falling so the
1115
+ * target doesn't take fall damage on landing.
1116
+ *
1117
+ * @example
1118
+ * // Periodic knockback in King-of-the-Hill style:
1119
+ * Actions.knockbackWithLevitation({ target: "all", durationSeconds: 2, amplifier: 6 });
1120
+ *
1121
+ * // Knock with slow-fall for safe landing:
1122
+ * Actions.knockbackWithLevitation({ target: "self", mode: "slowfall", durationSeconds: 3 });
1123
+ */
1124
+ knockbackWithLevitation: ({ target, durationSeconds = 1, amplifier = 5, mode = "levitation", }) => {
1125
+ const t = mapTarget(target);
1126
+ sandstone_1.effect.give(t, "minecraft:levitation", durationSeconds, amplifier, true);
1127
+ if (mode === "slowfall") {
1128
+ // 8s of slow-falling — covers most levitation falls without damage.
1129
+ sandstone_1.effect.give(t, "minecraft:slow_falling", durationSeconds + 8, 0, true);
1130
+ }
1131
+ },
336
1132
  };
337
1133
  //# sourceMappingURL=actions.js.map