# LLM_README.md - @kradle/challenges-sdk API Reference

This document provides exhaustive API documentation for AI agents using the `@kradle/challenges-sdk` package. This is the complete reference for creating Minecraft datapack-based challenges.

## Table of Contents

1. [Overview](#overview)
2. [Installation](#installation)
3. [Helpers — start here](#helpers--start-here)
4. [Core Concepts](#core-concepts)
5. [createChallenge API](#createchallenge-api)
6. [Variables](#variables)
7. [Events](#events)
8. [Actions](#actions)
9. [Utilities](#utilities)
10. [Sandstone Integration](#sandstone-integration)
11. [Complete Examples](#complete-examples)
12. [Common Patterns](#common-patterns)
13. [Common Gotchas](#common-gotchas)

---

## Helpers — start here

These compress arena geometry / detection patterns that show up in nearly every challenge. Reach for them before writing raw `Actions.fill` loops or `execute if block` chains.

| Helper | What it does | Use instead of |
|--------|--------------|----------------|
| **`Actions.box(...)`** | Stone floor + 4 walls (or full hollow box, with optional roof / interior clearing) — single call. Full reference below in [World](#world). | Five separate `Actions.fill` calls per arena |
| **`Actions.flatArena({ block, center, radius, headroom? })`** | Single-block-thick floor + cleared airspace above, no walls. The "open ground for things to move on" pattern from boss-brawl, sumo-arena, wave-survival, stalker. | Two manual `Actions.fill` calls (one stone, one air) per arena |
| **`Actions.countEntities({ entity?, tag?, volume? }): Score`** | Counts entities matching any combination of `entity` type, `tag`, and/or `volume`. Returns an anonymous `Score` — the caller assigns it into their custom_variable (`value.set(Actions.countEntities({ entity: "minecraft:zombie" }))`). At least one filter must be provided (counting "every entity in the world" throws at build). Drop-in replacement for the `value.set(0); execute.as(Selector("@e", { type })).run(() => value.add(1))` pattern that shows up in 8+ challenges' custom_variable updaters. | Hand-rolled `execute.as(Selector(...)).run(...)` chain |
| **`Actions.countBlocksInRegion(...)` / `Actions.countBlocksRelativeTo(...)`** | Counts a block ID across a 3D region; returns a `Score` you can drive end-states off. | `_.if(_.block(abs(x,y,z), ...))` chains across many cells |
| **`Actions.maxPlayerScore({ score, min? }): Score`** | Reads a per-player `Score` and returns the maximum across all players as an anonymous `Score`. `min` (default 0) seeds the result before the sweep — pass a negative value if your per-player score can go below 0 (e.g. climb's `current_height` defaulting to -1000), otherwise the default 0 would mask negative actual maxes. Caller writes into a global custom_variable: `value.set(Actions.maxPlayerScore({ score: diamonds }))`. Lets `Actions.setEndStates` / `.end_condition` reference "any player has X" semantics correctly. | Manual `execute as @a if score @s …` loops |
| **`Actions.randomInt({ min, max }): Score`** | Uniform random integer in `[min, max]` returned as an anonymous `Score`. Wraps Minecraft's `random value` (which Sandstone has no typed wrapper for; this helper exists so user code never reaches for `raw`). | `raw("execute store result score … run random value …")` |
| **`Actions.detectBlockMissing({ at, expected }): Score`** | Returns 1 if the block at `at` is NOT `expected`, 0 if it is. Non-latching — wrap in `_.if(missing, () => value.set(1))` for the latching "once broken, stays broken" pattern (castle-ctf, castle-siege, beanstalk). | Manual `_.if(_.not(_.block(abs(...), expected)), () => value.set(1))` |
| **`Actions.knockbackWithLevitation({ target, mode, durationSeconds, amplifier })`** | Levitation (or levitation + slow-fall) effect for non-lethal displacement. | Hand-rolled `effect give … levitation` |

Worked example using `Actions.box`:

```typescript
import { Actions } from "@kradle/challenges-sdk";

// 17×17 stone arena, 4-block walls, no roof — what boss-solo / melee-arena need.
Actions.box({
  block: "minecraft:stone",
  from: { x: -8, y: -60, z: -8 },
  to:   { x:  8, y: -56, z:  8 },
  absolute: true,
  hollow: true,
  floor: "minecraft:stone",
});
```

---

## Overview

`@kradle/challenges-sdk` is a TypeScript framework for creating Minecraft challenges that compile to datapacks. It provides:

- **Variable System**: Track per-player and global game state with automatic tick updates
- **Event System**: Lifecycle hooks and custom event triggers based on scores/advancements
- **Actions Library**: Pre-built game operations (give items, teleport, announce, etc.)
- **Role Management**: Assign players to teams with role-specific win conditions
- **Sandstone Integration**: Full access to Sandstone's Minecraft command generation

**Key Principle**: Challenges are defined declaratively. You specify variables, events, and conditions - the framework generates the datapack.

---

## Installation

```bash
npm install @kradle/challenges-sdk sandstone@0.14.0-alpha.13
```

**Requirements:**
- Node.js >= 22.18.0
- Sandstone 0.14.0-alpha.13 (peer dependency)

---

## Core Concepts

### Ticks

Minecraft runs at 20 ticks per second. Time values in this API are in ticks:
- 1 second = 20 ticks
- 1 minute = 1200 ticks (60 * 20)
- 5 minutes = 6000 ticks

### Namespace & Item IDs

**IMPORTANT:** Most Actions require the full `minecraft:` prefix for items, blocks, and entities:
- ✅ Correct: `item: "minecraft:diamond_sword"`
- ❌ Wrong: `item: "diamond_sword"`

The only exceptions are when the API explicitly accepts an item without the namespace (check each Action's documentation).

**Examples:**
- Items: `"minecraft:diamond"`, `"minecraft:iron_sword"`, `"minecraft:cooked_beef"`
- Blocks: `"minecraft:stone"`, `"minecraft:diamond_block"`
- Entities: `"minecraft:zombie"`, `"minecraft:pig"`, `"minecraft:creeper"`

### Scores

All variables are backed by Minecraft scoreboards. The `Score` type from Sandstone represents a scoreboard value. Scores support comparison methods:
- `score.equalTo(value)` / `score.equalTo(otherScore)`
- `score.greaterThan(value)`
- `score.greaterOrEqualThan(value)`
- `score.lowerThan(value)`
- `score.lowerOrEqualThan(value)`

### Roles

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.

---

## createChallenge API

### Required Imports

```typescript
import { createChallenge, Actions, forEveryPlayer } from "@kradle/challenges-sdk";
import { _, execute, Selector, rel, abs } from "sandstone";
import type { Score } from "sandstone";
```

### Signature

```typescript
function createChallenge<
  ROLES extends readonly string[],
  VARIABLES extends Record<string, _InputVariableType>
>(config: _BaseConfig<ROLES, VARIABLES>): ChallengeBuilder
```

### Configuration Object

```typescript
interface _BaseConfig<ROLES, VARIABLES> {
  // Required: Challenge name (used for datapack namespace)
  name: string;

  // Required: Output path for generated datapack
  kradle_challenge_path: string;

  // Required: Player roles as readonly tuple
  // Example: ["attacker", "defender"] as const
  roles: ROLES;

  // Required: Custom variable definitions
  custom_variables: VARIABLES;

  // Optional: Game duration in ticks. If not set (or 0), no time limit.
  GAME_DURATION?: number;

  // Optional: World spawn point for the challenge
  // Can be explicit coordinates or an entity selector (e.g., from Actions.getLocation)
  spawnPoint?: { x: number; y: number; z: number } | SelectorClass;

  // Optional: Per-rule overrides for the implicit setup the SDK applies
  // inside start_challenge / init_participants. See "Implicit setup" below.
  defaults?: {
    time?: "day" | "noon" | "night" | "midnight" | false;  // default: "day"
    daylightCycle?: boolean;                                // default: false
    mobSpawning?: boolean;                                  // default: false
    naturalRegeneration?: boolean;                          // default: true
    clearInventory?: boolean;                               // default: true
    maxHealth?: number | false;                             // default: 20
  } | false;
}
```

### Implicit setup (`defaults`)

`createChallenge` applies a small, predictable setup at the top of `start_challenge` and `init_participants` so user code doesn't have to:

- **`start_challenge`** — `time set day`, `gamerule doDaylightCycle false`, `gamerule doMobSpawning false`, `gamerule naturalRegeneration true`.
- **`init_participants`** — `clear @a[participant]`, `attribute generic.max_health base set 20`.

Override per-rule when needed:

```typescript
// Wave-defense: keep mob spawns on
createChallenge({ defaults: { mobSpawning: true }, ... })

// PvP arena that pre-stocks inventory in start_challenge: keep what was given
createChallenge({ defaults: { clearInventory: false }, ... })

// Boss fight: 30 HP per agent
createChallenge({ defaults: { maxHealth: 30 }, ... })

// Tight combat tuning: no idle healing
createChallenge({ defaults: { naturalRegeneration: false }, ... })
```

Set `defaults: false` to disable implicit setup entirely (you're then responsible for time, gamerules, and per-player init):

```typescript
createChallenge({ defaults: false, ... })
```

**Role Validation:** When building a challenge via `kradle challenge build`, the roles in `createChallenge()` are validated against the roles defined in `config.ts`. If a role in `challenge.ts` doesn't match a role in `config.ts`, the build will fail with an error.

**Spawn Point:** The `spawnPoint` option sets the world spawn point and teleports new players to it. It accepts either:
- Explicit coordinates: `{ x: 0, y: 64, z: 0 }`
- An entity selector (e.g., from `Actions.getLocation`): `Actions.getLocation({ name: "spawn" })`

When using an entity selector, the spawn point will be set at the entity's location once it exists in the world. New players are automatically teleported to the spawn point and have their individual spawn point set there.

```typescript
// Using explicit coordinates
createChallenge({
  name: "my-challenge",
  kradle_challenge_path: "./output",
  roles: ["player"] as const,
  spawnPoint: { x: 0, y: 100, z: 0 },
  custom_variables: {},
});

// Using a location marker (preferred for map-based challenges)
createChallenge({
  name: "my-challenge",
  kradle_challenge_path: "./output",
  roles: ["player"] as const,
  spawnPoint: Actions.getLocation({ name: "spawn" }),
  custom_variables: {},
});
```

### Builder Methods

The builder uses a fluent API. Methods must be called in order:

```typescript
createChallenge(config)
  .events(eventCallback)               // Lifecycle events; call Actions.setEndStates from on_tick
  .custom_events(customEventCallback)  // OPTIONAL — score-threshold side effects (announce, sound, etc.)
  .end_condition(endConditionCallback) // OPTIONAL — when the game should end (auto-includes GAME_DURATION timer)
  .win_conditions(winConditionsCallback) // Per-role win conditions (triggers build)
```

**Use `Actions.setEndStates({ ... })` from `events.on_tick` for the standard "win condition X / loss Y / timeout Z" pattern.** It collapses what would otherwise be three `custom_events` entries with manual `Actions.setEndState` calls. Hand its return value (the OR of all conditions) to `.end_condition()` if you want the game to end as soon as any branch fires.

#### `.events(callback)`

```typescript
.events((variables: Variables, roles: Roles) => {
  return {
    start_challenge?: () => void;    // Runs once when game starts
    init_participants?: () => void;  // Runs once per player after start
    on_tick?: () => void;            // Runs every tick for each player
    end_challenge?: () => void;      // Runs once when game ends
  };
})
```

**Worked example — solo speedrun "break the gold block":**

```typescript
let endReached: ConditionType | undefined;
challenge
  .events(({ goal_broken, alive_players, game_timer }) => ({
    on_tick: () => {
      endReached = Actions.setEndStates({
        goal_reached: goal_broken.equalTo(1),
        time_up:      _.or(alive_players.equalTo(0), game_timer.greaterOrEqualThan(GAME_DURATION)),
      });
    },
  }))
  .end_condition(() => endReached!);
```

End-state names are validated against `config.endStates` at build time (same as direct `Actions.setEndState` calls).

**IMPORTANT — server context:** when called from `events.on_tick`, the conditions passed to `Actions.setEndStates` are evaluated as the server entity (not a participant). Reference **globals only** (e.g. `alive_players`, `game_timer`, your global custom_variables). For "any player has X" semantics, derive a global via a per-player updater first (see `Actions.maxPlayerScore`).

#### `.custom_events(callback)` (optional — for score-threshold side effects)

For triggered side effects keyed off score thresholds — an `announce` when score crosses N, a sound effect, a `repeatable` event. For end-states, prefer `Actions.setEndStates` from `events.on_tick`.

```typescript
.custom_events((variables: Variables, roles: Roles) => {
  return Array<ScoreEvent | AdvancementEvent>;
})
```

**Score Event:**
```typescript
{
  score: Score;           // Variable to watch
  target: number;         // Threshold value
  mode: "fire_once" | "repeatable";
  actions: () => void;    // Actions to execute
}
```

**Advancement Event:**
```typescript
{
  criteria: Array<{ trigger: string; conditions?: object }>;
  mode: "fire_once" | "repeatable";
  actions: () => void;
}
```

**Execution context:** for **individual** scores/advancements the `actions` callback runs `as` *and* `at` each matching player — `@s` is the player and relative coords (`~ ~ ~`) / `distance=` selectors resolve from the player's position. **Global** scores run once in the server context (no `@s`, no position); wrap per-player work in `forEveryPlayer(...)`.

#### `.end_condition(callback)`

Defines when the game should end. The challenge already auto-ends after `GAME_DURATION` ticks if specified — `.end_condition()` adds an extra termination criterion that's OR'd with the timer. The most common pattern is to capture the return of `Actions.setEndStates(...)` from `on_tick` and feed it back here, so the game ends as soon as any end-state has been applied.

```typescript
.end_condition((variables: Variables, roles: Roles) => {
  return Condition;  // Returns a Sandstone condition
})
```

#### `.win_conditions(callback)`

```typescript
.win_conditions((variables: Variables, roles: Roles) => {
  return {
    [roleName: string]: Condition;  // One condition per role
  };
})
```

---

## Variables

### Built-in Variables

These are always available in every challenge:

| Variable | Type | Description |
|----------|------|-------------|
| `death_count` | individual | Player's death count (Minecraft `deathCount` objective) |
| `has_never_died` | individual | 1 if player has never died, 0 otherwise |
| `alive_players` | global | Count of living participants |
| `main_score` | individual | Primary score shown on sidebar |
| `game_timer` | global | Ticks since game start (increments each tick) |
| `game_state` | global | 0=CREATED, 1=OFF, 2=ON |
| `player_count` | global | Total participant count |
| `player_number` | individual | Unique player ID (1 to N) |

### Custom Variable Definition

```typescript
custom_variables: {
  variable_name: {
    type: "individual" | "global";
    objective_type?: string;  // Minecraft objective criterion
    hidden?: boolean;         // Hide from scoreboard (global only)
    default?: number;         // Initial value
    updater?: UpdaterFunction;
  }
}
```

### Variable Types

#### Individual Objective-Based

Automatically tracks Minecraft statistics:

```typescript
pigs_killed: {
  type: "individual",
  objective_type: "minecraft.killed:minecraft.pig",
  default: 0,
}
```

Common objective types:
- `"minecraft.killed:minecraft.<entity>"` - Entities killed
- `"minecraft.killed_by:minecraft.<entity>"` - Killed by entity
- `"minecraft.picked_up:minecraft.<item>"` - Items picked up
- `"minecraft.mined:minecraft.<block>"` - Blocks mined
- `"minecraft.used:minecraft.<item>"` - Items used
- `"minecraft.crafted:minecraft.<item>"` - Items crafted
- `"playerKillCount"` - PvP kills
- `"deathCount"` - Deaths
- `"dummy"` - Manual/computed value

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.

> **⚠️ `minecraft.picked_up` only fires on item-entity pickup, NOT on `Actions.give`.**
>
> If you award items via `Actions.give({ item, target, count })` and your `score` uses `objective_type: "minecraft.picked_up:minecraft.<item>"`, the score will stay at `0` because `/give` adds straight to the player's inventory and never spawns the item entity that triggers the statistic.
>
> Only one of these patterns will register on `picked_up`:
> 1. Drop the item with `summon item ... ItemTags ...` so the player walks over it (slow + visible).
> 2. Track the score yourself in a `dummy` objective and increment it at every payout site.
>
> Pattern 2 is almost always what you want for "emeralds awarded so far":
> ```typescript
> custom_variables: {
>   score: {
>     type: "individual",
>     objective_type: "dummy",
>     default: 0,
>     updater: (value, { main_score }) => main_score.set(value),
>   },
>   ticks_in_a: {
>     type: "individual",
>     objective_type: "dummy",
>     default: 0,
>     // The second argument to `updater` exposes every other custom_variable —
>     // including `score` — so you can bump it yourself wherever Actions.give fires.
>     updater: (value, { score }) => {
>       forEveryPlayer(() => {
>         _.if(<on_platform>, () => {
>           value.add(1);
>           _.if(value.modulo(20).equalTo(0), () => {
>             Actions.give({ target: "self", item: "minecraft:emerald", count: 1 });
>             score.add(1);
>           });
>         });
>       });
>     },
>   },
> }
> ```
>
> The same applies to "deduction" via `clear @s emerald N` — `picked_up` does NOT decrement when an item leaves the inventory. If you need a *current-balance* score (e.g. "ended with > 50 emeralds"), pair every `give`/`clear` with explicit `score.add(N)` / `score.remove(N)`.

#### Individual Dummy

Computed per-player values:

```typescript
current_y_position: {
  type: "individual",
  objective_type: "dummy",
  updater: (value) => {
    value.set(Actions.getCurrentPlayerPosition().y)
  },
}
```

#### Global Variables

Shared across all players:

```typescript
max_score_global: {
  type: "global",
  hidden: false,
  default: 0,
  updater: (value, { main_score }) => {
    value.set(0);
    forEveryPlayer(() => {
      _.if(main_score.greaterThan(value), () => {
        value.set(main_score);
      });
    });
  },
}
```

### Updater Function Signature

```typescript
type UpdaterFunction = (
  value: Score,                    // Current variable's score
  variables: Record<string, Score> // All variables including built-ins
) => void;
```

Updaters run every tick. They should be idempotent. They are not necessary if the variable is individual and tracks a Minecraft objective.

### Score Methods

All variables are `Score` objects with these methods:

```typescript
// Set value
score.set(number | Score);

// Arithmetic
score.add(number | Score);
score.remove(number | Score);

// Comparison (return Condition)
score.equalTo(number | Score);
score.greaterThan(number | Score);
score.greaterOrEqualThan(number | Score);
score.lowerThan(number | Score);
score.lowerOrEqualThan(number | Score);
```

A `Score` is itself a Condition that compiles to `unless score @s OBJ matches 0` — i.e. **truthy = non-zero**. So for 0/1 latches, `_.if(flag, ...)` is the short form of `_.if(flag.equalTo(1), ...)`. **But not** for counters that can hit ≥2 — `_.if(alive_players)` matches "≥1 alive", `_.if(alive_players.equalTo(1))` matches "exactly one alive" (the last-player-standing semantic). Use the truthy form only when you're sure the score is bounded to 0/1.

---

## Events

### Lifecycle Events

All lifecycle events run globally (not per-player).

#### `start_challenge`

Runs once when the challenge starts. Use for global setup.

```typescript
start_challenge: () => {
  Actions.setTime({ time: "day" });
  Actions.gamerule({ rule: "doDaylightCycle", value: false });
  Actions.announce({ message: "Game starting!" });
}
```

#### `init_participants`

Runs 1 second after start_challenge. Use for player setup. It still runs globally.

```typescript
init_participants: () => {
  Actions.give({ target: "all", item: "minecraft:diamond_sword", count: 1 });
  Actions.setAttribute({ target: "all", attribute_: "generic.max_health", value: 40 });
  Actions.teleport({ target: "all", x: 0, y: 100, z: 0, absolute: true });
}
```

#### `on_tick`

Runs every tick. Use sparingly for performance.

```typescript
on_tick: () => {
  // Check something every tick
}
```

#### `end_challenge`

Runs once when the game ends. Use for cleanup and announcements.

```typescript
end_challenge: () => {
  Actions.announce({ message: "Game over!" });
}
```

### Custom Events

Custom events allow you to trigger actions based on score thresholds or Minecraft advancement criteria.

#### Score-Based Events

Score events watch a variable and trigger when it reaches a specified target value. They are evaluated every tick.

**Parameters:**
- `score` (required): The `Score` variable to watch
- `target` (optional): The threshold value. If omitted, triggers on any score change
- `mode` (required): `"fire_once"` or `"repeatable"`
- `actions` (required): Function containing actions to execute

**How it works:**
1. Every tick, the system compares the current score to the target
2. For `"fire_once"`: triggers once when `score == target` (tracks previous value to detect the 1st time threshold is met)
3. For `"repeatable"`: triggers every tick while `score == target`
4. For individual variables, events fire per-player; for global variables, events fire once globally

```typescript
{
  score: variables.diamonds,
  target: 10,
  mode: "fire_once",
  actions: () => {
    Actions.announce({ message: "10 diamonds collected!" });
  },
}
```

**Without target (triggers on any change):**
```typescript
{
  score: variables.death_count,
  mode: "fire_once",  // Triggers once when death_count changes away from initial value
  actions: () => {
    Actions.announce({ message: "First death!" });
  },
}
```

**Modes:**
- `"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.
- `"repeatable"`: Triggers every tick while `score >= target`. Useful for continuous effects.

#### Advancement-Based Events

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).

Advancement-based events always fire per-player.

**Parameters:**
- `criteria` (required): Array of advancement trigger objects with optional conditions
- `mode` (required): `"fire_once"` or `"repeatable"`
- `actions` (required): Function containing actions to execute

**How it works:**
1. An advancement is generated with your specified criteria
2. When Minecraft grants the advancement (criterion met), the event fires
3. For `"repeatable"`: the advancement is automatically revoked so it can trigger again
4. For `"fire_once"`: the advancement stays granted, preventing re-triggering

**Simple example - Track when a player hits another player:**
```typescript
{
  criteria: [
    {
      trigger: "minecraft:player_hurt_entity",
      conditions: {
        entity: { type: "minecraft:player" }
      }
    }
  ],
  mode: "repeatable",
  actions: () => {
    Actions.increment({ variable: variables.pvp_hits });
    Actions.announce({ message: "PvP hit!" });
  },
}
```

**Multiple triggers example:**
```typescript
{
  criteria: [
    { trigger: "minecraft:player_hurt_entity" },
    { trigger: "minecraft:entity_hurt_player" },
  ],
  mode: "repeatable",
  actions: () => {
    Actions.increment({ variable: variables.combat_actions });
  },
}
```

Common triggers:
- `"minecraft:player_hurt_entity"` - Player attacks entity
- `"minecraft:entity_hurt_player"` - Entity attacks player
- `"minecraft:player_killed_entity"` - Player kills entity
- `"minecraft:consume_item"` - Item consumed
- `"minecraft:inventory_changed"` - Inventory changes
- `"minecraft:location"` - Player at location
- `"minecraft:enter_block"` - Player enters block

See the [Minecraft Wiki](https://minecraft.fandom.com/wiki/Advancement/JSON_format#List_of_triggers) for the full list of triggers and their conditions.

---

## Actions

Actions are higher-level functions that wrap common Minecraft operations. They provide:
- Automatic target mapping (`"all"`, `"self"`, role names → proper selectors)
- Integration with Kradle's interface (e.g., `Actions.announce` messages appear in Kradle)
- Consistent API for common operations

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).

All actions are called via the `Actions` object:

```typescript
import { Actions } from "@kradle/challenges-sdk";
```

### Target Parameter

Many actions accept a `target` parameter of type `TargetNames`, which can be:
- `"all"` - Targets all participants (maps to `@a[tag=kradle_participant]`)
- `"self"` - Targets the current player (maps to `@s`)
- A role name (e.g. `"red"`) - Targets all players in that role (maps to `@a[tag=red]` — roles are stored as player tags, not scoreboard teams)
- An entity ID (e.g. `"minecraft:zombie"`) - Targets all entities of that type (maps to `@e[type=minecraft:zombie]`)
- Any `Selector` instance - Custom selector for fine-grained targeting (e.g., `Selector("@a", { tag: "red" })`)

### Communication

#### `Actions.announce(params)`

Broadcast message to all players with KRADLE tag.

```typescript
Actions.announce({
  message: JSONTextComponent;  // Message (string or formatted object)
});

// Simple string:
Actions.announce({ message: "Game starting!" });

// Formatted JSONTextComponent:
Actions.announce({
  message: [
    { text: "Player ", color: "white" },
    { selector: "@s", color: "gold", bold: true },
    { text: " won the game!", color: "green" }
  ]
});
```

#### `Actions.tellraw(params)`

Send formatted message to specific target with KRADLE display tag.

```typescript
Actions.tellraw({
  target: TargetNames;       // "all", "self", or any selector
  message: JSONTextComponent; // Message (string or formatted object)
});

// Examples:
Actions.tellraw({
  target: "all",
  message: ["Hello, ", { text: "world!", color: "gold", bold: true }]
});
Actions.tellraw({
  target: "self",
  message: "You won!"
});
Actions.tellraw({
  target: "self",
  message: { text: "Critical hit!", color: "red", bold: true }
});

// Score values embed directly into the message — Sandstone serializes them
// as `{ score: { name, objective } }` and Minecraft renders each recipient's
// own value. So a single tellraw to "all" shows each player THEIR number,
// no `forEveryPlayer` + `_.if(player_number.equalTo(N))` branching needed:
Actions.tellraw({
  target: "all",
  message: [{ text: "Your player number is ", color: "yellow" }, player_number],
});
```

### Items & Inventory

#### `Actions.give(params)`

Give items to a target.

```typescript
Actions.give({
  target: TargetNames;  // "all", "self", or any selector
  item: string;         // Item ID (with "minecraft:" prefix)
  count?: number;       // Amount (default: 1)
});

// Examples:
Actions.give({ target: "self", item: "minecraft:diamond_sword", count: 1 });
Actions.give({ target: "all", item: "minecraft:diamond", count: 10 });
Actions.give({ target: "red", item: "minecraft:iron_sword", count: 1 });  // Targets the "red" role (resolves to @a[tag=red])
```

**Note:** The `target` parameter can be:
- `"all"` - All participants
- `"self"` - Current player (`@s`)
- A role name - Targets all players in that role (roles are stored as tags, not scoreboard teams)
- Any `Selector` instance for custom targeting

#### `Actions.giveLoot(params)`

Give random items from weighted loot table.

```typescript
Actions.giveLoot({
  target: TargetNames;  // "all", "self", or any selector
  items: [{ name: ITEMS; count: number; weight: number }];  // Weighted item list
});

// Example:
Actions.giveLoot({
  target: "self",
  items: [
    { name: "minecraft:diamond", count: 5, weight: 1 },
    { name: "minecraft:iron_ingot", count: 10, weight: 3 },
    { name: "minecraft:gold_ingot", count: 7, weight: 2 }
  ]
});
```

**Note:** This creates a weighted loot table. Items with higher weights are more likely to be selected.

#### `Actions.clear(params)`

Clear all items from a target's inventory.

```typescript
Actions.clear({
  target: TargetNames;  // "all", "self", or any selector
});

// Examples:
Actions.clear({ target: "self" });  // Clear current player's inventory
Actions.clear({ target: "all" });   // Clear all participants' inventories
```

#### `Actions.countItems(params)`

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.

```typescript
Actions.countItems({
  target: TargetNames;  // The target to count items for
  item: ITEMS;          // The item to count
}): Score;              // Returns a new variable containing the count

// Example - Use directly in conditions:
_.if(Actions.countItems({ target: "self", item: "minecraft:diamond" }).greaterThan(5), () => {
  Actions.announce({ message: "You have more than 5 diamonds!" });
});

// Example - Store in a variable for later use:
const diamondCount = Actions.countItems({ target: "self", item: "minecraft:diamond" });
_.if(diamondCount.greaterThan(10), () => {
  Actions.announce({ message: "You have more than 10 diamonds!" });
});

// Example - Set a custom variable from the count:
const count = Actions.countItems({ target: "self", item: "minecraft:diamond" });
variables.my_diamond_count.set(count);
```

**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.

#### `Actions.getCurrentPlayerPosition()`

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.

```typescript
Actions.getCurrentPlayerPosition(): { x: Score; y: Score; z: Score }

// Example - Check if player is above Y=100:
const pos = Actions.getCurrentPlayerPosition();
_.if(pos.y.greaterThan(100), () => {
  Actions.announce({ message: "You reached the sky!" });
});

// Example - Store position in custom variables:
const { x, y, z } = Actions.getCurrentPlayerPosition();
variables.player_x.set(x);
variables.player_y.set(y);
variables.player_z.set(z);

// Example - Check if player is in a specific area:
const pos = Actions.getCurrentPlayerPosition();
_.if(_.and(
  pos.x.greaterThan(0),
  pos.x.lowerThan(100),
  pos.z.greaterThan(0),
  pos.z.lowerThan(100)
), () => {
  Actions.announce({ message: "You're in the zone!" });
});
```

**Note:** This returns integer coordinates (block position). The values are truncated from the player's exact floating-point position.

### Entities

#### `Actions.summonMultiple(params)`

Summon multiple entities at location.

```typescript
Actions.summonMultiple({
  entity: string;    // Entity ID (with "minecraft:" prefix)
  count: number;     // How many entities to summon
  x: number;         // X coordinate
  y: number;         // Y coordinate
  z: number;         // Z coordinate
  absolute: boolean; // true for absolute coords, false for relative
});

// Example:
Actions.summonMultiple({
  entity: "minecraft:zombie",
  count: 5,
  x: 0,
  y: 64,
  z: 0,
  absolute: true
});
```

#### `Actions.summonItem(params)`

Summon item entity at location.

```typescript
Actions.summonItem({
  item: string;      // Item ID (with "minecraft:" prefix)
  x: number;         // X coordinate
  y: number;         // Y coordinate
  z: number;         // Z coordinate
  absolute: boolean; // true for absolute coords, false for relative
});

// Example:
Actions.summonItem({
  item: "minecraft:diamond",
  x: 0,
  y: 64,
  z: 0,
  absolute: true
});
```

#### `Actions.kill(params)`

Kill entities matching selector.

```typescript
Actions.kill({
  selector: TargetNames;  // "all", "self", or any selector
});

// Examples:
Actions.kill({ selector: Selector("@e", { type: "minecraft:zombie" }) });
Actions.kill({ selector: Selector("@e", { type: "!minecraft:player" }) });
Actions.kill({ selector: "all" });  // Kill all participants
```

#### `Actions.teleport(params)`

Teleport entities to a location or to another entity.

```typescript
// Teleport to coordinates
Actions.teleport({
  target: TargetNames;  // "all", "self", or any selector
  x: number;            // X coordinate
  y: number;            // Y coordinate
  z: number;            // Z coordinate
  absolute: boolean;    // true for absolute coords, false for relative
});

// Teleport to entity
Actions.teleport({
  target: TargetNames;          // "all", "self", or any selector
  toEntity: SingleEntityArgument; // Entity to teleport to
});

// Examples:
Actions.teleport({ target: "self", x: 0, y: 100, z: 0, absolute: true });
Actions.teleport({ target: "all", x: 0, y: 64, z: 0, absolute: true });
Actions.teleport({ target: "self", x: 10, y: 0, z: 5, absolute: false });  // Relative position

// Teleport to a location marker entity
const spawn = Actions.getLocation({ name: "spawn" });
Actions.teleport({ target: "all", toEntity: spawn });
```

#### `Actions.forceMoveTo(params)`

Force the target player(s) to immediately interrupt whatever code they are running and **walk** to absolute coordinates. Server-initiated — the challenge forces the move, unlike the agent's own movement skills. The agents always walk and are **never teleported, even in cheat mode**. Only the interrupt plus a single pathfind are guaranteed; an agent may decide to walk away again on its next action.

```typescript
Actions.forceMoveTo({
  target: TargetNames;  // "self", "all", a role name, or any Selector. "self" resolves only inside forEveryPlayer; raw "@..." strings are rejected.
  x: number;            // Absolute X coordinate
  y: number;            // Absolute Y coordinate
  z: number;            // Absolute Z coordinate
});

// Examples:
Actions.forceMoveTo({ target: "all", x: 0, y: -60, z: 0 });
Actions.forceMoveTo({ target: "red", x: 10, y: -60, z: 12 });   // everyone tagged "red"
Actions.forceMoveTo({ target: Selector("@a", { tag: "winner" }), x: 0, y: -60, z: 0 });
// "self" works only inside forEveryPlayer (per-player executor); a contextless
// forceMoveTo("self") fails the build's context-safety check.
forEveryPlayer(() => Actions.forceMoveTo({ target: "self", x: 0, y: -60, z: 0 }));
```

Coordinates are always absolute. Unlike `Actions.teleport`, this is a real pathfind (walk / jump / swim), so the destination must be reachable on foot. Each target's in-flight code is interrupted, and the agent is told it was force-moved. Use it to herd players into a phase-2 room, pull a winner onto a podium, reset positions between rounds, etc.

#### `Actions.getLocation(params)`

Get a location marker entity by name. Returns a selector targeting the marker entity with the specified location tag. Locations must be defined in `config.ts` under `challengeConfig.locations`.

```typescript
Actions.getLocation({
  name: string;  // The location name (must match a key in config.locations)
}): SelectorClass;  // Returns a selector targeting the location marker

// Example - Get a location and teleport to it:
const spawn = Actions.getLocation({ name: "spawn" });
Actions.teleport({ target: "all", toEntity: spawn });

// Example - Use as spawnPoint in config:
createChallenge({
  name: "my-challenge",
  // ...
  spawnPoint: Actions.getLocation({ name: "spawn" }),
});
```

**Important:** When building via `kradle challenge build`, locations are validated against the `locations` defined in `config.ts`. If a location name doesn't exist in the config, the build will fail with an error listing valid locations.

#### `Actions.box(params)`

Build a box (4 walls + optional floor/roof) in a single call. Replaces the typical 5+ `Actions.fill` calls per arena.

```typescript
Actions.box({
  block: BLOCKS;          // Wall block
  from: { x, y, z };      // First corner (inclusive)
  to: { x, y, z };        // Opposite corner (inclusive)
  absolute: boolean;
  hollow?: boolean;       // Default true: only walls placed
  floor?: BLOCKS;         // Optional override block for floor
  roof?: BLOCKS;          // Optional roof
  clearInside?: BLOCKS | boolean; // true = air; supply a BLOCKS to fill interior
});

// Example - 13×13×5 stone arena with stone-bricks floor, no roof, interior cleared
Actions.box({
  block: "minecraft:stone",
  from: { x: -6, y: -60, z: -6 },
  to: { x: 6, y: -55, z: 6 },
  absolute: true,
  hollow: true,
  floor: "minecraft:stone_bricks",
  clearInside: true,
});
```

#### `Actions.countBlocksInRegion(params)`

Count instances of a block within an axis-aligned region. Returns a `Score`. Useful for detecting agent-built structures without pinning detection to a single coordinate (which breaks the moment the agent moves before building).

```typescript
Actions.countBlocksInRegion({
  block: BLOCKS;        // Block ID to count (e.g. "minecraft:cobblestone")
  from: { x, y, z };    // First corner (inclusive)
  to: { x, y, z };      // Opposite corner (inclusive)
  absolute: boolean;
}): Score;

// Detect any 5+ cobblestone block tower built within a 12×6×12 zone:
const count = Actions.countBlocksInRegion({
  block: "minecraft:cobblestone",
  from: { x: -6, y: -60, z: -6 },
  to: { x: 6, y: -55, z: 6 },
  absolute: true,
});
_.if(count.greaterOrEqualThan(5), () => Actions.announce({ message: "Tower built!" }));
```

The region is hard-capped at 4096 cells (~64×8×8) — emits one `execute if block` check per cell at codegen time, so larger regions would bloat the generated datapack. If you need to scan a bigger area, do it in slices.

#### `Actions.countBlocksRelativeTo(params)`

Same as `countBlocksInRegion`, but the bounds are *relative to a target's current position*. Finds structures the agent built nearby regardless of where they wandered after spawning. Returns a `Score`.

```typescript
Actions.countBlocksRelativeTo({
  target: TargetNames;          // "self", "all", or any selector
  block: BLOCKS;
  dxRange: [number, number];    // x offsets relative to target (e.g. [-8, 8])
  dyRange: [number, number];    // y offsets (e.g. [0, 5] for "this block + 5 above")
  dzRange: [number, number];
}): Score;

// Detect a 5-tall pillar within 8 blocks of the agent (in either direction):
pillar_built: {
  type: "individual",
  default: 0,
  updater: (value) => {
    value.set(0);
    const count = Actions.countBlocksRelativeTo({
      target: "self",
      block: "minecraft:cobblestone",
      dxRange: [-8, 8],
      dyRange: [0, 5],
      dzRange: [-8, 8],
    });
    _.if(count.greaterOrEqualThan(5), () => value.set(1));
  },
}
```

Use this instead of `countBlocksInRegion` when the build site moves with the agent. Same 4096-cell cap.

#### `Actions.knockbackWithLevitation(params)`

Apply a non-lethal knockback / displacement via Minecraft's `effect` command. Default is a brief levitation (lifts straight up without dealing damage). Pass `mode: "slowfall"` to combine with slow-falling for a soft landing.

```typescript
Actions.knockbackWithLevitation({
  target: TargetNames;
  durationSeconds?: number;  // Default 1
  amplifier?: number;        // Default 5
  mode?: "levitation" | "slowfall";
});

// Periodic knockback in a King-of-the-Hill challenge:
Actions.knockbackWithLevitation({ target: "all", durationSeconds: 2, amplifier: 6 });

// Knock + slow-fall for safe landing:
Actions.knockbackWithLevitation({ target: "self", mode: "slowfall", durationSeconds: 3 });
```

### World

#### `Actions.setBlock(params)`

Set a single block.

```typescript
Actions.setBlock({
  block: string;     // Block ID (with "minecraft:" prefix)
  x: number;         // X coordinate
  y: number;         // Y coordinate
  z: number;         // Z coordinate
  absolute: boolean; // true for absolute coords, false for relative
});

// Example:
Actions.setBlock({
  block: "minecraft:diamond_block",
  x: 0,
  y: 64,
  z: 0,
  absolute: true
});
```

#### `Actions.fill(params)`

Fill region with blocks.

```typescript
Actions.fill({
  block: string;     // Block ID (with "minecraft:" prefix)
  x1: number;        // Start X coordinate
  y1: number;        // Start Y coordinate
  z1: number;        // Start Z coordinate
  x2: number;        // End X coordinate
  y2: number;        // End Y coordinate
  z2: number;        // End Z coordinate
  absolute: boolean; // true for absolute coords, false for relative
  mode: "fill" | "line" | "pyramid";  // Fill mode
});

// Examples:
Actions.fill({
  block: "minecraft:stone",
  x1: 0, y1: 64, z1: 0,
  x2: 10, y2: 64, z2: 10,
  absolute: true,
  mode: "fill"
});

Actions.fill({
  block: "minecraft:gold_block",
  x1: 0, y1: 64, z1: 0,
  x2: 0, y2: 10, z2: 0,
  absolute: true,
  mode: "pyramid"  // Builds a pyramid
});
```

#### `Actions.setTime(params)`

Set world time.

```typescript
Actions.setTime({
  time: "day" | "night" | number;  // Named or tick value
});

// Examples:
Actions.setTime({ time: "day" });
Actions.setTime({ time: 6000 });  // Noon
```

#### `Actions.gamerule(params)`

Set a gamerule.

```typescript
Actions.gamerule({
  rule: string;
  value: boolean | number;
});

// Examples:
Actions.gamerule({ rule: "doDaylightCycle", value: false });
Actions.gamerule({ rule: "mobGriefing", value: false });
Actions.gamerule({ rule: "randomTickSpeed", value: 0 });
```

### Scores

#### `Actions.set(params)`

Set score to value or copy from another score.

```typescript
// Set to number
Actions.set({
  variable: Score;
  value: number | Score;
});

// Examples:
Actions.set({ variable: variables.main_score, value: 0 });
Actions.set({ variable: variables.main_score, value: variables.diamonds });
```

#### `Actions.increment(params)`

Add 1 to score.

```typescript
Actions.increment({
  variable: Score;
});

// Example:
Actions.increment({ variable: variables.counter });
```

#### `Actions.decrement(params)`

Subtract 1 from score.

```typescript
Actions.decrement({
  variable: Score;
});

// Example:
Actions.decrement({ variable: variables.counter });
```

#### `Actions.standingOnBlock(params)` → `Selector`

Returns a Sandstone selector that matches `@s` ONLY when standing on the block at `(x, y, z)`. Use this anywhere you need a "is the player on this pad / tile / pressure plate?" check — the bare `Selector("@s", { x, y, z })` form does NOT constrain position (Minecraft treats x/y/z as the selector ORIGIN for distance math, not as a position filter), so the condition silently passes for everyone. This helper bakes in `dx: 0, dy: 1, dz: 0` so the selector matches the 1×1×2 column above the floor block.

```typescript
Actions.standingOnBlock({
  x: number;        // X of the floor block
  y: number;        // Y of the floor block (player's feet are at y+1)
  z: number;        // Z of the floor block
  role?: string;    // Optional role tag the player must also have
});
```

```typescript
// Trigger alice_shared when Alice steps on her pad block.
alice_shared: {
  type: "global",
  default: 0,
  updater: (value) => {
    _.if(value.equalTo(0), () => {
      forEveryPlayer(() => {
        _.if(
          Actions.standingOnBlock({ x: ALICE_PAD.x, y: ALICE_PAD.y, z: ALICE_PAD.z, role: "alice" }),
          () => { value.set(1); },
        );
      });
    });
  },
},
```

### Voting

Challenges can offer named votes, declared in `config.ts` as `challengeConfig.votingOptions` (e.g. `{ selectroom: ["red", "green", "blue"] }`). Agents cast a vote with `skills.voteForOption(voteId, option)`; the arena validates it (rejecting unknown options and second votes) and records the choice as the player tags `kradle.voteOptions.<voteId>` (voted-at-all) and `kradle.voteOptions.<voteId>.<option>` (the choice). The actions below read and drive those tags, so you can build a full "discuss → vote → act on the result" phase (see Pattern 13). Vote ids and options must be tag-safe and dot-free (`[A-Za-z0-9_-]`). Like `getLocation`, the vote actions validate at build time against the declared `votingOptions`: an undeclared vote id (or an option not in that vote) fails `kradle challenge build`, catching typos early.

#### `Actions.votedFor(voteId, option?)` → `Selector`

Per-player condition matching `@s` when the player has voted in `voteId` (any option), or — when `option` is given — only when they voted for that specific option. Use it in `_.if` (inside `forEveryPlayer`), `setEndStates`, or win conditions. It matches `@s`, so **do not** pass it to `execute.as(...)` from a tick/load context — `@s` resolves to no one there and the command runs for nobody (the build's context-safety check flags this).

```typescript
Actions.votedFor("selectroom");          // voted in selectroom at all
Actions.votedFor("selectroom", "red");   // voted specifically for red
```

```typescript
// Move everyone who picked the red room — votedFor is a per-player condition,
// so check it inside forEveryPlayer (which makes @s each participant).
forEveryPlayer(() => {
  _.if(Actions.votedFor("selectroom", "red"), () => {
    Actions.teleport({ target: "self", x: 10, y: -60, z: 10, absolute: true });
  });
});
```

#### `Actions.promptVote(params)`

Announce a vote to all participants over the `***KRADLE***` channel agents read, naming the vote and its options.

```typescript
Actions.promptVote({
  voteId: string;     // matches a key in config votingOptions
  prompt: string;     // human-readable instruction
  options: string[];  // the valid options to surface
});

// Example:
Actions.promptVote({ voteId: "selectroom", prompt: "Pick a room!", options: ["red", "green", "blue"] });
```

#### `Actions.allVotesIn(voteId)` → `ConditionType`

True once every participant has voted in `voteId` (no participant is still missing the `kradle.voteOptions.<voteId>` tag). Combine with a timer to end a vote phase as soon as everyone is in.

```typescript
_.if(_.or(Actions.allVotesIn("selectroom"), game_timer.greaterOrEqualThan(600)), () => {
  // resolve the vote (600 ticks = 30s)
});
```

#### `Actions.assignDefaultVotes(params)`

Record `default` for every participant who has not voted in `voteId` yet — call it on timeout so non-voters are still moved by `votedFor`. Writes the same tags the arena would.

```typescript
Actions.assignDefaultVotes({
  voteId: string;   // the vote
  default: string;  // option assigned to non-voters
});

// Example:
Actions.assignDefaultVotes({ voteId: "selectroom", default: "red" });
```

**Full vote phase** — prompt once, resolve when all are in or 30s elapse, default non-voters, then move each player by their choice. Gate the resolve block behind a global `resolved` flag so it fires once:

```typescript
// config.ts: challengeConfig.votingOptions = { selectroom: ["red", "green", "blue"] }
const ROOMS = { red: { x: 10, y: -60, z: 10 }, green: { x: 20, y: -60, z: 10 }, blue: { x: 30, y: -60, z: 10 } };

challenge.events(({ game_timer, resolved }) => ({
  start_challenge: () => {
    Actions.promptVote({ voteId: "selectroom", prompt: "Pick a room!", options: Object.keys(ROOMS) });
  },
  on_tick: () => {
    _.if(resolved.equalTo(0), () => {
      _.if(_.or(Actions.allVotesIn("selectroom"), game_timer.greaterOrEqualThan(600)), () => {
        resolved.set(1);
        Actions.assignDefaultVotes({ voteId: "selectroom", default: "red" });
        forEveryPlayer(() => {
          for (const [option, pos] of Object.entries(ROOMS)) {
            _.if(Actions.votedFor("selectroom", option), () => {
              Actions.teleport({ target: "self", x: pos.x, y: pos.y, z: pos.z, absolute: true });
            });
          }
        });
      });
    });
  },
}));
```

### Player Attributes

#### `Actions.setAttribute(params)`

Set entity attribute for a target.

```typescript
Actions.setAttribute({
  target: TargetNames;  // "all", "self", or any selector
  attribute_: string;   // Attribute name
  value: number;        // Attribute value
});

// Examples:
Actions.setAttribute({ target: "self", attribute_: "generic.max_health", value: 40 });
Actions.setAttribute({ target: "all", attribute_: "generic.movement_speed", value: 0.2 });
Actions.setAttribute({ target: "self", attribute_: "generic.attack_damage", value: 10 });
```

Common attributes (with `generic.` prefix):
- `"generic.max_health"` - Maximum HP (default 20)
- `"generic.movement_speed"` - Walk speed (default 0.1)
- `"generic.attack_damage"` - Base attack damage
- `"generic.armor"` - Armor points
- `"generic.knockback_resistance"` - Knockback resistance (0-1)


### Logging

#### `Actions.log_variable(params)`

Log variable to watcher system (debugging).

```typescript
Actions.log_variable({
  message: string;   // Log message
  variable: Score;   // Variable to log
  store: boolean;    // Whether to store in backend
});

// Example:
Actions.log_variable({
  message: "Player score",
  variable: variables.main_score,
  store: true
});
```

#### `Actions.setEndState(params)`

Set the end state of the challenge. End states are custom final states (e.g., "victory", "defeat", "timeout") that get recorded when the game ends.

```typescript
Actions.setEndState({
  endState: string;  // The end state identifier
});

// Example:
Actions.setEndState({ endState: "victory" });
Actions.setEndState({ endState: "timeout" });
```

**Important:** If `endStates` is defined in your `config.ts`, the end state will be validated at build time. Only end states listed in `config.endStates` are allowed.

```typescript
// config.ts
export const config = {
  // ...
  endStates: {
    victory: "Player achieved the goal",
    defeat: "Player failed the challenge",
    timeout: "Time ran out",
  },
};

// challenge.ts - These are valid:
Actions.setEndState({ endState: "victory" });
Actions.setEndState({ endState: "defeat" });

// This would throw an error at build time:
Actions.setEndState({ endState: "unknown" }); // Error: Invalid end state "unknown"
```

#### `Actions.setEndStates(map): ConditionType`

For each `(name, condition)` pair, emit `_.if(condition, () => setEndState(name))` at the current codegen location. Where (and how often) those checks run is up to the caller — `events.on_tick` for a per-tick check, `custom_events.actions` for a single fire, etc.

Returns a `ConditionType` that's true when at least one of the input conditions is true (i.e. an end-state was applied). Hand it to `.end_condition()` if you want the game to end as soon as any end-state has been set; ignore it if the game's end is gated on something else (timer, separate condition).

```typescript
Actions.setEndStates({
  [endStateName: string]: ConditionType,
}): ConditionType;
```

```typescript
let endReached: ConditionType | undefined;
challenge
  .events(({ goal_broken, alive_players, game_timer }) => ({
    on_tick: () => {
      endReached = Actions.setEndStates({
        goal_reached: goal_broken.equalTo(1),
        time_up:      _.or(alive_players.equalTo(0), game_timer.greaterOrEqualThan(GAME_DURATION)),
      });
    },
  }))
  .end_condition(() => endReached!);
```

End-state names are validated against `config.endStates` at build time, same as direct `Actions.setEndState` calls.

**Note: server context** — when called from `events.on_tick` (the most common spot), conditions are evaluated as the server entity, where per-player score reads silently fail. Reference globals only; for "any player has X" semantics use `Actions.maxPlayerScore` to materialize a global first.

---

## Utilities

### `forEveryPlayer(callback)`

Execute code for each participant at their location.

```typescript
import { forEveryPlayer } from "@kradle/challenges-sdk";

forEveryPlayer(() => {
  // Runs as(@s) at(@s) for each participant
  // @s is the current player
  // All individual variables reference the current player within this context
});
```

### `forEachOtherPlayer(callback)`

Inside a `forEveryPlayer` (or any "@s is an outer participant") context, run `callback` for every OTHER participant. Inside the callback `@s` is the inner participant; the outer participant is no longer `@s` but is tagged `kradle_pair_self` for the duration of the inner loop so you can refer to them.

Visits ordered pairs (each unordered pair appears twice, once with each direction). For each-pair-once semantics, gate the callback on `other.player_number > self.player_number`.

```typescript
import { forEveryPlayer, forEachOtherPlayer } from "@kradle/challenges-sdk";

forEveryPlayer(() => {
  forEachOtherPlayer(() => {
    // @s = the inner player; @a[tag=kradle_pair_self] = the outer player.
    // Useful for matchup-style logic: prisoner's dilemma payoffs, alliance
    // detection, "did I bump into another player" events, etc.
  });
});
```

**Important Notes:**
- Individual variables automatically reference the current player (`@s`) within the loop
- Global variables remain global and are the same across all iterations
- Each iteration executes at the player's position (`at(@s)`)

**Example - Find maximum score:**
```typescript
max_score: {
  type: "global",
  updater: (value, { main_score }) => {
    value.set(0);
    forEveryPlayer(() => {
      // main_score here refers to the current player's main_score
      _.if(main_score.greaterThan(value), () => {
        value.set(main_score);
      });
    });
  },
}
```

### Constants

```typescript
import { ALL, KRADLE_PARTICIPANT_TAG, WINNER_TAG } from "@kradle/challenges-sdk";

// ALL - Selector for all participants: @a[tag=kradle_participant]
// KRADLE_PARTICIPANT_TAG - Tag name: "kradle_participant"
// WINNER_TAG - Tag name: "kradle_winner"
```

---

## Sandstone Integration

This package is built on Sandstone. You can use Sandstone APIs directly:

```typescript
import { _, execute, Selector, rel, abs, MCFunction } from "sandstone";
```

### Conditions with `_`

```typescript
// Single condition
_.if(score.greaterThan(10), () => {
  // actions
});

// Combined conditions
_.if(_.and(
  score1.greaterThan(5),
  score2.equalTo(1)
), () => {
  // actions
});

_.if(_.or(
  condition1,
  condition2
), () => {
  // actions
});

// Block check
_.if(_.block(rel(0, -1, 0), "minecraft:diamond_block"), () => {
  // Player standing on diamond block
});
```

### Execute Commands

```typescript
// Store result in score
execute.as("@s").store.result.score(myScore).run.data.get.entity("@s", "Pos[1]");

// Run at location
execute.at("@s").run.particle("minecraft:flame", rel(0, 1, 0));

// Conditional execution
execute.if.score(myScore, ">=", 10).run.say("High score!");
```

### Selectors

```typescript
import { Selector } from "sandstone";

// With arguments
Selector("@a", { tag: "my_tag" });
Selector("@e", { type: "zombie", limit: 1, sort: "nearest" });

// NBT check
Selector("@s", {
  nbt: { Inventory: [{ id: "minecraft:diamond" }] }
});
```

### Relative/Absolute Coordinates

```typescript
import { rel, abs } from "sandstone";

rel(0, 1, 0);   // ~ ~1 ~
abs(0, 64, 0);  // 0 64 0
```

---

## Complete Examples

### Example 1: Speed Challenge - First to Kill 2 Pigs

```typescript
import { createChallenge, Actions, forEveryPlayer } from "@kradle/challenges-sdk";
import { _ } from "sandstone";

createChallenge({
  name: "pig-farming",
  kradle_challenge_path: "./output",
  roles: ["farmer"] as const,
  GAME_DURATION: 2 * 60 * 20,  // 2 minutes
  custom_variables: {
    pigs_farmed: {
      type: "individual",
      objective_type: "minecraft.killed:minecraft.pig",
      default: 0,
      updater: (value, { main_score }) => {
        main_score.set(value);
      },
    },
    game_over: {
      type: "global",
      updater: (value, { pigs_farmed }) => {
        value.set(0);
        forEveryPlayer(() => {
          _.if(pigs_farmed.greaterOrEqualThan(2), () => {
            value.set(1);
          });
        });
      },
    },
  },
})
  .events(() => ({
    start_challenge: () => {
      Actions.setTime({ time: "day" });
      Actions.announce({ message: "First to kill 2 pigs wins!" });
    },
    init_participants: () => {
      Actions.give({ target: "all", item: "minecraft:iron_sword", count: 1 });
    },
  }))
  .custom_events(({ pigs_farmed }) => [
    {
      score: pigs_farmed,
      target: 1,
      mode: "fire_once",
      actions: () => {
        Actions.announce({ message: "First pig down!" });
      },
    },
  ])
  .end_condition(({ game_over }) => game_over.equalTo(1))
  .win_conditions(({ pigs_farmed }, { farmer }) => ({
    [farmer]: pigs_farmed.greaterOrEqualThan(2),
  }));
```

### Example 2: Climb Challenge - Reach Highest Point

```typescript
import { createChallenge, Actions, forEveryPlayer } from "@kradle/challenges-sdk";
import { _, execute } from "sandstone";

createChallenge({
  name: "climb",
  kradle_challenge_path: "./output",
  roles: ["climber"] as const,
  GAME_DURATION: 3 * 60 * 20,
  custom_variables: {
    current_height: {
      type: "individual",
      objective_type: "dummy",
      updater: (value) => {
        value.set(Actions.getCurrentPlayerPosition().y)
      },
    },
    max_height: {
      type: "individual",
      updater: (value, { current_height, main_score }) => {
        _.if(current_height.greaterThan(value), () => {
          value.set(current_height);
        });
        main_score.set(value);
      },
    },
    max_height_global: {
      type: "global",
      updater: (value, { max_height }) => {
        // Pass `min: -1000` because climber Y can go below 0 (lava drops, void).
        value.set(Actions.maxPlayerScore({ score: max_height, min: -1000 }));
      },
    },
    is_winner: {
      type: "individual",
      updater: (value, { max_height, max_height_global, has_never_died }) => {
        value.set(0);
        _.if(_.and(
          max_height.equalTo(max_height_global),
          max_height.greaterThan(0),
          has_never_died.equalTo(1)
        ), () => {
          value.set(1);
        });
      },
    },
  },
})
  .events(() => ({
    start_challenge: () => {
      Actions.setTime({ time: "day" });
      Actions.announce({ message: "Climb as high as you can!" });
    },
    init_participants: () => {
      Actions.give({ target: "all", item: "minecraft:cobblestone", count: 64 });
      Actions.give({ target: "all", item: "minecraft:cobblestone", count: 64 });
    },
  }))
  .custom_events(() => [])
  .end_condition(({ game_timer }) => game_timer.greaterThan(3 * 60 * 20))
  .win_conditions(({ is_winner }, { climber }) => ({
    [climber]: is_winner.equalTo(1),
  }));
```

### Example 3: Battle Royale - Last Player Standing

```typescript
import { createChallenge, Actions } from "@kradle/challenges-sdk";
import { _ } from "sandstone";

createChallenge({
  name: "battle-royale",
  kradle_challenge_path: "./output",
  roles: ["fighter"] as const,
  GAME_DURATION: 5 * 60 * 20,
  custom_variables: {
    kills: {
      type: "individual",
      objective_type: "playerKillCount",
      default: 0,
      updater: (value, { main_score }) => {
        main_score.set(value);
      },
    },
    sole_survivor: {
      type: "individual",
      updater: (value, { alive_players, has_never_died }) => {
        value.set(0);
        _.if(_.and(
          alive_players.equalTo(1),
          has_never_died.equalTo(1)
        ), () => {
          value.set(1);
        });
      },
    },
  },
})
  .events(() => ({
    start_challenge: () => {
      Actions.setTime({ time: "day" });
      Actions.gamerule({ rule: "naturalRegeneration", value: false });
      Actions.announce({ message: "Last player standing wins!" });
    },
    init_participants: () => {
      Actions.give({ target: "all", item: "minecraft:stone_sword", count: 1 });
      Actions.give({ target: "all", item: "minecraft:leather_chestplate", count: 1 });
      Actions.give({ target: "all", item: "minecraft:cooked_beef", count: 10 });
    },
  }))
  .custom_events(({ kills }) => [
    {
      score: kills,
      target: 1,
      mode: "fire_once",
      actions: () => {
        Actions.announce({ message: "First blood!" });
      },
    },
  ])
  .end_condition(({ alive_players }) => alive_players.equalTo(1))
  .win_conditions(({ sole_survivor }, { fighter }) => ({
    [fighter]: sole_survivor.equalTo(1),
  }));
```

### Example 4: Team-Based - Hunters vs Protectors

```typescript
import { createChallenge, Actions, forEveryPlayer } from "@kradle/challenges-sdk";
import { _ } from "sandstone";

createChallenge({
  name: "pig-farming-v2",
  kradle_challenge_path: "./output",
  roles: ["pighunter", "pigsaver"] as const,
  GAME_DURATION: 2 * 60 * 20,
  custom_variables: {
    pigs_killed: {
      type: "individual",
      objective_type: "minecraft.killed:minecraft.pig",
      default: 0,
      updater: (value, { main_score }) => {
        main_score.set(value);
      },
    },
    pig_killed_max: {
      type: "global",
      updater: (value, { pigs_killed }) => {
        value.set(Actions.maxPlayerScore({ score: pigs_killed }));
      },
    },
  },
})
  .events((vars, { pighunter, pigsaver }) => ({
    start_challenge: () => {
      Actions.setTime({ time: "day" });
      Actions.announce({ message: "Hunters: Kill 2 pigs! Protectors: Stop them!" });
    },
    init_participants: () => {
      // Different items based on role would be set via role-specific logic
      Actions.give({ target: "all", item: "minecraft:wooden_sword", count: 1 });
    },
  }))
  .custom_events(() => [])
  .end_condition(({ pig_killed_max }) => pig_killed_max.greaterOrEqualThan(2))
  .win_conditions(({ pig_killed_max }, { pighunter, pigsaver }) => ({
    [pighunter]: pig_killed_max.greaterOrEqualThan(2),
    [pigsaver]: pig_killed_max.lowerThan(2),
  }));
```

### Example 5: Capture the Flag

```typescript
import { createChallenge, Actions } from "@kradle/challenges-sdk";
import { _, Selector, rel } from "sandstone";
import type { Score } from "sandstone";

createChallenge({
  name: "capture-the-flag",
  kradle_challenge_path: "./output",
  roles: ["red_team", "blue_team"] as const,
  GAME_DURATION: 5 * 60 * 20,
  custom_variables: {
    holds_red_banner: {
      type: "individual",
      updater: (value: Score) => {
        value.set(0);
        _.if(Selector("@s", {
          nbt: { Inventory: [{ id: "minecraft:red_banner" }] }
        }), () => {
          value.set(1);
        });
      },
    },
    stands_blue_wool: {
      type: "individual",
      updater: (value: Score) => {
        value.set(0);
        _.if(_.block(rel(0, -1, 0), "minecraft:blue_wool"), () => {
          value.set(1);
        });
      },
    },
    captured_flag: {
      type: "individual",
      updater: (value, { holds_red_banner, stands_blue_wool, main_score }) => {
        value.set(0);
        _.if(_.and(
          holds_red_banner.equalTo(1),
          stands_blue_wool.equalTo(1)
        ), () => {
          value.set(1);
          main_score.set(1);
        });
      },
    },
  },
})
  .events(() => ({
    start_challenge: () => {
      Actions.announce({ message: "Capture the enemy flag and return to base!" });
    },
    init_participants: () => {
      Actions.give({ target: "all", item: "minecraft:iron_sword", count: 1 });
    },
  }))
  .custom_events(({ captured_flag }) => [
    {
      score: captured_flag,
      target: 1,
      mode: "fire_once",
      actions: () => {
        Actions.announce({ message: "Flag captured! Game over!" });
      },
    },
  ])
  .end_condition(({ captured_flag }) => captured_flag.equalTo(1))
  .win_conditions(({ captured_flag }, { red_team, blue_team }) => ({
    [red_team]: captured_flag.equalTo(0),
    [blue_team]: captured_flag.equalTo(1),
  }));
```

---

## Common Patterns

| # | Pattern | Use when | Canonical challenge |
|---|---------|----------|---------------------|
| 1 | Sync variable to main_score | Score should display on the watcher leaderboard | sprint, patience |
| 2 | Find global maximum | "Highest score wins" comparisons | crafters-race, splat-rush |
| 3 | Winner detection (has max + alive) | Latched is_winner that requires surviving | prisoners-dilemma, king-of-the-hill |
| 4 | Track player position | Per-player coordinate snapshot for distance/region checks | sprint, lighthouse |
| 5 | Check inventory for item | "Has the agent crafted X yet?" | crafters-race, biome-bazaar |
| 6 | Check block below player | "Is the agent on a specific tile?" — use `Actions.standingOnBlock` | sacrifice, reputation |
| 7 | Count entities | Wave / mob accounting | wave-survivor, zombie-survival |
| 8 | Simple end condition | Single-condition timer or score gate | sprint |
| 9 | Multi-condition end | OR of timer / score / event gates | crafters-race |
| 10 | Opposing-team win conditions | Capture-the-flag style 0/1 flag state | castle-ctf, capture-the-flag |
| 11 | Per-participant win attribution inside a single role | Same role, but only some win (saboteur vs miners) | saboteur |
| 12 | Sequential turn-taking | "One player acts at a time" — current_voter gate | cascade, adaptive |
| 13 | Two-phase game (sequential broadcast → simultaneous decision) | Vote after public discussion | common-knowledge |

### Pattern 1: Sync Variable to Main Score

```typescript
my_variable: {
  type: "individual",
  objective_type: "some_criterion",
  updater: (value, { main_score }) => {
    main_score.set(value);
  },
}
```

### Pattern 2: Find Global Maximum

```typescript
max_global: {
  type: "global",
  updater: (value, { individual_score }) => {
    value.set(Actions.maxPlayerScore({ score: individual_score }));
  },
}
```

Pass `min: -1000` (or any sufficiently-negative bound) when the per-player score can go below 0 — the default `min: 0` would otherwise mask negative actual maxes.

### Pattern 3: Winner Detection (Has Max Score + Alive)

```typescript
is_winner: {
  type: "individual",
  updater: (value, { main_score, max_global, has_never_died }) => {
    value.set(0);
    _.if(_.and(
      main_score.equalTo(max_global),
      main_score.greaterThan(0),
      has_never_died.equalTo(1)
    ), () => {
      value.set(1);
    });
  },
}
```

### Pattern 4: Track Player Position

```typescript
current_y: {
  type: "individual",
  objective_type: "dummy",
  updater: (value) => {
    // Prefered way: using the dedicated Action
    value.set(Actions.getCurrentPlayerPosition().y)

    // Alternative way: using execute.store + data.get
    execute.as("@s").store.result.score(value).run.data.get.entity("@s", "Pos[1]");
  },
}
```

### Pattern 5: Check Inventory for Item

```typescript
has_diamond: {
  type: "individual",
  updater: (value: Score) => {
    value.set(0);
    _.if(Selector("@s", {
      nbt: { Inventory: [{ id: "minecraft:diamond" }] }
    }), () => {
      value.set(1);
    });
  },
}
```

### Pattern 6: Check Block Below Player

```typescript
on_gold_block: {
  type: "individual",
  updater: (value: Score) => {
    value.set(0);
    _.if(_.block(rel(0, -1, 0), "minecraft:gold_block"), () => {
      value.set(1);
    });
  },
}
```

### Pattern 7: Count Entities

```typescript
zombie_count: {
  type: "global",
  updater: (value) => {
    value.set(Actions.countEntities({ entity: "minecraft:zombie" }));
  },
}
```

Combine filters with `tag` and/or `volume` for "zombies in the arena" or "tagged hunt targets only". At least one filter must be provided (counting "every entity in the world" throws at build time).

### Pattern 8: Simple End Condition

```typescript
.end_condition(({ objective_complete }) => objective_complete.equalTo(1))
```

### Pattern 9: Multi-Condition End

```typescript
.end_condition(({ alive_players, objective_complete }) =>
  _.or(
    alive_players.equalTo(1),
    objective_complete.equalTo(1)
  )
)
```

### Pattern 10: Opposing Team Win Conditions

```typescript
.win_conditions(({ team_a_score, team_b_score }, { team_a, team_b }) => ({
  [team_a]: team_a_score.greaterThan(team_b_score),
  [team_b]: team_b_score.greaterThan(team_a_score),
}))
```

### Pattern 11: Per-participant Win Attribution Inside a Single Role

For asymmetric games where one role hides another (e.g. social deduction: 1 secret saboteur + 3 miners share the same `player` role), `.win_conditions` is keyed by role but its body runs `as @a[tag=<role>]` — so `@s` is each participant. Filter further inside the condition with `Selector("@s", { tag })`:

```typescript
.win_conditions(({ saboteur_alive, diamonds_mined }, { player }) => ({
  [player]: _.or(
    _.and(
      Selector("@s", { tag: "saboteur" } as any),
      saboteur_alive.equalTo(1),
      diamonds_mined.lowerThan(4),
    ),
    _.and(
      Selector("@s", { tag: "!saboteur" } as any),
      _.or(diamonds_mined.greaterOrEqualThan(4), saboteur_alive.equalTo(0)),
    ),
  ),
}))
```

This is how saboteur ends up correctly attributing the win to the saboteur participant when miners run out of time, and to all miners when they catch the impostor — without needing a separate `saboteur` role. The `as any` cast on the negated tag is a Sandstone selector-typing limitation; `tag: "!saboteur"` is a valid Minecraft selector token but the type narrowing doesn't model it.

### Pattern 12: Sequential turn-taking (one player acts at a time)

For games where only one participant should act per tick — like cascade's information-cascade voting where each voter must wait their turn — gate the action updater on `player_number == current_voter`. The SDK's built-in `player_number` is per-player (1, 2, 3, … assigned in init_participants); maintain a global `current_voter` that you bump when a player completes their turn:

```typescript
custom_variables: {
  // ... your other variables ...
  current_voter: { type: "global", default: 1 },
  has_voted: { type: "individual", objective_type: "dummy", default: 0 },
  vote: {
    type: "individual",
    objective_type: "dummy",
    default: -1,
    updater: (value, { current_voter, has_voted, player_number }) => {
      forEveryPlayer(() => {
        _.if(
          _.and(has_voted.equalTo(0), player_number.equalTo(current_voter)),
          () => {
            _.if(Actions.standingOnBlock(RED_PAD), () => {
              value.set(0);
              has_voted.set(1);
              current_voter.add(1);
            });
            _.if(Actions.standingOnBlock(BLUE_PAD), () => {
              value.set(1);
              has_voted.set(1);
              current_voter.add(1);
            });
          },
        );
      });
    },
  },
},
// Announce each turn boundary via custom_events keyed on current_voter:
.custom_events(({ current_voter }) => [
  { score: current_voter, target: 2, mode: "fire_once",
    actions: () => Actions.announce({ message: [{ text: "Voter 2's turn.", color: "yellow" }] }) },
  { score: current_voter, target: 3, mode: "fire_once",
    actions: () => Actions.announce({ message: [{ text: "Voter 3's turn.", color: "yellow" }] }) },
  // ... one per voter beyond the first ...
])
```

The compiled mcfunction reads `execute if score @s has_voted matches 0 if score @s player_number = current_voter kradle.board run …` — the player_number-vs-current_voter equality is what enforces serialization. Used by cascade and adaptive.

### Pattern 13: Two-phase game (sequential broadcast, then simultaneous decision)

Common-knowledge formation games and similar designs need a sequential phase 1 (one act per turn) followed by a simultaneous phase 2 (all act independently in private). Phase 1 reuses Pattern 12; phase 2 gates on a global "phase 1 complete" condition AND uses **per-player rooms** so each player's decision stays private:

```typescript
custom_variables: {
  // ... phase 1: same as Pattern 12 ...
  broadcasts_done: {
    type: "global",
    default: 0,
    updater: (value, { has_broadcast }) => {
      value.set(0);
      forEveryPlayer(() => { value.add(has_broadcast); });
    },
  },
  has_voted_final: { type: "individual", objective_type: "dummy", default: 0 },
  final_vote: {
    type: "individual",
    objective_type: "dummy",
    default: -1,
    updater: (value, { broadcasts_done, has_voted_final, player_number }) => {
      // Per-player room layout — keeps each player's vote private until all cast.
      const FINAL_PADS = [
        { red: FINAL_RED_P1, blue: FINAL_BLUE_P1 },
        { red: FINAL_RED_P2, blue: FINAL_BLUE_P2 },
        { red: FINAL_RED_P3, blue: FINAL_BLUE_P3 },
      ];
      forEveryPlayer(() => {
        _.if(
          _.and(broadcasts_done.equalTo(3), has_voted_final.equalTo(0)),
          () => {
            FINAL_PADS.forEach((pads, idx) => {
              _.if(player_number.equalTo(idx + 1), () => {
                _.if(Actions.standingOnBlock(pads.red), () => {
                  value.set(0);
                  has_voted_final.set(1);
                });
                _.if(Actions.standingOnBlock(pads.blue), () => {
                  value.set(1);
                  has_voted_final.set(1);
                });
              });
            });
          },
        );
      });
    },
  },
},
// In init_participants, tell each player their player_number — without it, agents can't tell
// which "Voter 2's turn" announcement is theirs and can't navigate to the right phase-2 room.
// One tellraw to "all" with player_number embedded does it (each recipient sees their own value):
//   Actions.tellraw({ target: "all", message: ["Your player number is ", player_number] });
```

**Collective-win tip:** for "all-or-nothing" reward (everyone wins iff every vote is correct AND unanimous), add a global `correct_votes` updater that sums `(has_voted_final == 1 AND final_vote == truth)` per player; then `.win_conditions` can gate `[role]: correct_votes.equalTo(N)`. Used by common-knowledge.

**Skill-based voting (alternative to pads):** when the decision is a choice among named options rather than a physical location, skip the per-player rooms entirely. Declare `votingOptions` in `config.ts`, have agents call `skills.voteForOption(voteId, option)` (votes are private — each agent whispers the watcher), and read the result with `Actions.votedFor(voteId, option?)`. Use `Actions.promptVote` to announce the vote, `Actions.allVotesIn(voteId)` (paired with a `game_timer` deadline) to detect when everyone's cast, and `Actions.assignDefaultVotes` to default non-voters on timeout — see the **Voting** section under Actions. The pad-based approach above is still the right choice when the vote *is* a spatial commitment (which room you walk into) or you want votes visible via player position.

---

## Common Gotchas

These are real bugs that bit AI authors building the existing challenge corpus. Read before authoring; reach for the SDK helper called out where one exists.

### Relative coords in global custom_variable updaters resolve at world origin (0, 0, 0)

`_.block(rel(...))` and any sandstone command using `rel(...)` inside a **global** variable updater evaluates relative to the server entity at `(0, 0, 0)`, not at any player position. A "is the player standing on the trigger pad" check that uses `rel(0, -1, 0)` will silently always look at one block below world origin.

**Fix:** wrap the updater body in `forEveryPlayer(() => { ... })` so the relative coords resolve at `@s` (each player's position):

```ts
trigger_pressed: {
  type: "global",
  default: 0,
  updater: (value) => {
    forEveryPlayer(() => {
      _.if(_.block(rel(0, -1, 0), "minecraft:gold_block"), () => {
        value.set(1);
      });
    });
  },
}
```

This bit sequence-pad's wool-pad detection when the updater ran at world origin instead of per-player.

### Sandstone NBT serialization: `true` → `{}`, `false` → `0b`

Both are invalid NBT tokens. Any time you'd reach for sandstone's typed `summon(entity, abs(...), { CustomNameVisible: true, … })`, the resulting datapack command will be malformed and the summon will fail silently in-game.

**Fix:** use **`Actions.summonEntityWithStats`** for boss-style configured mobs (HP / attack damage / movement speed / tags). It builds the NBT command string directly via sandstone's `raw()` and dodges the gotcha. For un-configured spawns, plain `Actions.summonMultiple` works fine.

### `Actions.tagEntity?.(...)` / `Actions.tagPlayer` don't exist

These look plausible but aren't on the Actions object. Optional-chaining (`Actions.tagEntity?.(...)`) silently no-ops, which means your "mark hunt targets" call does nothing and your tagged-entity selectors find zero matches.

**Fix:** import `tag` from sandstone directly: `tag("@e[type=chicken,distance=..1]").add("hunt_target")`. Or design around tags entirely — chicken-chase ended up counting all chickens since flat-world has no naturally-spawned ones to confuse the count.

### `score.lessThan` doesn't exist; use `lowerThan`

Sandstone's `Score` API uses `lowerThan` and `greaterThan`. `lessThan` and `moreThan` look like reasonable guesses but TypeScript won't catch them at build time without `tsc --noEmit` (the kradle CLI build doesn't run strict tsc).

### `scheduled_events` / `setInterval`-style helpers don't exist

For "spawn a wave every N seconds", use `.custom_events` with one `fire_once` score event per wave keyed off `game_timer` at the right tick value. wave-survival uses 3 explicit entries; golem-ally maps an array of `WAVE_TICKS` to `custom_events` entries with `.map(...)`.

### `gamerule "pvp"` is not a real gamerule in 1.20.4

PvP is on by default in flat-world. Spyglass validation rejects this rule. Just don't set it.

### End-state names are validated at build time against `config.ts → endStates`

`Actions.setEndState({ endState: "x" })` and `Actions.setEndStates({ x: ... })` both check each name against the `endStates` map declared in your `config.ts`. A typo throws at build time, not at runtime — useful to lean on, but means you must keep `config.ts` in sync when adding/renaming an end state.

### `events.on_tick` and `.custom_events` run as the server entity, not as participants

Same context issue as the global-updater gotcha above. End-state conditions that reference a per-player score directly will silently fail (server has no entry on a per-player objective; `if score @s X matches Y` short-circuits the whole compound). For "any player has X" semantics in the conditions you pass to `Actions.setEndStates` from `on_tick`, derive a global via a per-player updater first (e.g. `Actions.maxPlayerScore({ score })`), then reference that global in the condition.

### `Selector("@s", { x, y, z })` does NOT constrain position

This is the trap that bit `sacrifice` — the alice/bob "stepped on the pad" check fired the moment the player existed because the selector didn't actually require position. In Minecraft selectors, `x/y/z` without `dx/dy/dz` defines the *origin* used for distance/volume math, not a position filter. Without a volume the entity check passes for everyone.

**Fix:** use `Actions.standingOnBlock({ x, y, z, role? })` (recommended) — it bakes in the right `dx: 0, dy: 1, dz: 0` so the selector matches a 1×1×2 column above the floor block. If you must hand-roll the selector, include `dx`/`dy`/`dz` explicitly:

```typescript
// ❌ Silently passes for every entity:
Selector("@s", { tag: "alice", x: PAD.x, y: PAD.y + 1, z: PAD.z })

// ✅ Matches only when feet are at (PAD.x, PAD.y+1, PAD.z):
Selector("@s", { tag: "alice", x: PAD.x, y: PAD.y + 1, z: PAD.z, dx: 0, dy: 1, dz: 0 })
// or — preferred:
Actions.standingOnBlock({ x: PAD.x, y: PAD.y, z: PAD.z, role: "alice" })
```

### `minecraft.picked_up:<item>` does NOT fire on `Actions.give` (also: does NOT decrement on `clear`)

Already covered in the "Common objective types" section earlier — short version: `objective_type: "minecraft.picked_up:..."` only counts ITEM-ENTITY pickups. `/give` writes straight to inventory and bypasses the statistic. Same for `clear` — the counter doesn't go down. Use `objective_type: "dummy"` and bump the score yourself next to every `Actions.give` / `clear` call (the `updater` second arg exposes other custom_variables, so you can grab `{ score }` and call `score.add(N)` / `score.remove(N)` inline).

### Don't reach for `raw("scoreboard players add ...")`

Sandstone exports `scoreboard`, `give`, `clear`, `execute`, `Selector`, `_.if` etc. as typed builders — use them. The `raw()` string skips type-checking, hard-codes objective names so a `score` rename silently breaks the command, and is harder to spot in code review. For per-target score updates from a global context, use `execute.as(Selector("@a", { tag: alice })).run(() => score.add(10))` — `@s` resolves correctly inside the `run` body.

### Roles get tagged with the bare role name, not `kradle_<role>`

If your challenge has `roles: ["alice", "bob"] as const`, the kradle runtime adds `kradle_participant` plus the bare role name (`alice`, `bob`) as tags. Selectors filtering by `tag: "kradle_alice"` match nobody. Use `tag: "alice"`.

---

## Tips for LLMs

1. **Always use `as const`** for roles array to get proper type inference
2. **Updaters should be idempotent** - they run every tick
3. **Reach for the helper before hand-rolling an aggregation** — `Actions.maxPlayerScore` for cross-player max, `Actions.countEntities` for entity counts. Drop to `forEveryPlayer` only when no helper covers the shape.
4. **Import `_` from sandstone** for conditions (`_.if`, `_.and`, `_.or`)
5. **Variables are Scores** - use `.set()`, `.add()`, comparison methods
6. **Main score is displayed** - sync your primary metric to `main_score`
7. **Time is in ticks** - multiply seconds by 20
8. **Win conditions are per-role** - each role needs its own condition
9. **Custom events fire per-player** for individual variables
