npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@kradle/challenges-sdk

v0.4.0

Published

Kradle's challenges SDK - Create Minecraft datapack-based challenges

Readme

@kradle/challenges-sdk

A TypeScript framework for creating Minecraft datapack-based challenges with event-driven game logic, score tracking, and player role management. Built on top of Sandstone.

Getting started

We strongly recommend using Kradle's CLI to create challenges with the CLI. Make sure to read the CLI's README

If you still want to perform a manual installation, you also need to install the Sandstone peer-dependency:

npm install @kradle/challenges-sdk [email protected]

Quick Start

Like for installation, we strongly recommend using Kradle's CLI to bootstrap a challenge:

# Will create a challenge in challenges/my-challenge/
kradle challenge create my-challenge

API Overview

createChallenge(config)

Creates a new challenge with the specified configuration.

createChallenge({
  name: string;                    // Challenge name (used for datapack)
  kradle_challenge_path: string;   // Output directory for generated datapack
  roles: readonly string[];        // Player roles (e.g., ["attacker", "defender"])
  GAME_DURATION?: number;          // Duration in ticks (default: 6000 = 5 minutes)
  custom_variables: Record<string, VariableDefinition>;
})

Fluent Builder Methods

The challenge builder uses a fluent API:

createChallenge(config)
  .events(callback)           // Define lifecycle event handlers
  .custom_events(callback)    // Define score/advancement-triggered events
  .end_condition(callback)    // Define when the game ends
  .win_conditions(callback)   // Define win conditions per role

Variables

Variables are the core mechanism for tracking game state. All variables are automatically updated each tick.

Built-in Variables

These variables are always available:

| Variable | Type | Description | |----------|------|-------------| | death_count | individual | Number of times the player has died | | has_never_died | individual | 1 if player hasn't died, 0 otherwise | | alive_players | global | Count of living participants | | main_score | individual | Primary score displayed on sidebar | | game_timer | global | Ticks elapsed since game start | | game_state | global | Current state (0=CREATED, 1=OFF, 2=ON) | | player_count | global | Total number of participants | | player_number | individual | Unique player ID (1 to N) |

Custom Variable Types

Individual Variables (per-player)

Track values for each player separately.

Objective-based (uses Minecraft statistics):

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

Dummy (computed values):

current_height: {
  type: "individual",
  objective_type: "dummy",
  updater: (value) => {
    value.set(Actions.getPlayerPosition().y)
  },
}

Global Variables (shared)

Track values across all players.

max_score: {
  type: "global",
  hidden: false,  // optional: hide from scoreboard
  default: 0,
  updater: (value, { main_score }) => {
    value.set(0);
    forEveryPlayer(() => {
      _.if(main_score.greaterThan(value), () => {
        value.set(main_score);
      });
    });
  },
}

Updater Functions

Updaters run every tick and receive (currentValue, allVariables):

updater: (value, { main_score, death_count, game_timer }) => {
  // Set value based on other variables
  value.set(0);
  _.if(main_score.greaterThan(10), () => {
    value.set(1);
  });
}

If you track a Minecraft objective, you do not need an updater!

Events

Lifecycle Events

Lifecycle events are triggered once on specific occasions.

.events((variables, roles) => ({
  start_challenge: () => {
    // Runs once when the challenge starts
    Actions.setTime({ time: "day" });
    Actions.announce({ message: "Game starting!" });
  },

  init_participants: () => {
    // Runs 1s after the challenge starts
    Actions.give({ target: "all", item: "minecraft:diamond_sword", count: 1 });
    Actions.setAttribute({ target: "all", attribute_: "generic.max_health", value: 40 });
  },

  on_tick: () => {
    // Runs once every tick
  },

  end_challenge: () => {
    // Runs when the challenge ends
    Actions.announce({ message: "Game over!" });
  },
}))

Custom Events

Custom events trigger actions based on score thresholds or Minecraft advancements.

Score-based events

Score events watch a variable and trigger when it reaches a target value.

  • score: The variable to watch.
  • target (optional): The target value. If omitted, triggers on any score change
  • mode:
    • "fire_once": Triggers once when the score reaches the target (per player for individual variables)
    • "repeatable": Triggers every tick while the score is at target

For individual variables, events fire per-player; for global variables, events fire globally.

.custom_events((variables, roles) => [
  {
    score: variables.diamonds,
    target: 5,
    mode: "fire_once",
    actions: () => {
      Actions.announce({ message: "Someone collected 5 diamonds!" });
    },
  },
])

If target is omitted, the event triggers whenever the score changes (useful for reacting to any increment).

Advancement-based events

Advancement events trigger when a Minecraft advancement criterion is met (e.g., player attacks, item picked up). The criteria follow the Minecraft Advancement JSON format.

Advancement-based events always fire per-player.

  • criteria: Array of advancement triggers with optional conditions
  • mode:
    • "fire_once": Triggers once per player when the advancement is granted
    • "repeatable": Triggers every time the advancement criterion is met (advancement is auto-revoked to allow re-triggering)
.custom_events((variables, roles) => [
  {
    criteria: [
      {
        trigger: "minecraft:player_hurt_entity",
        conditions: {
          entity: { type: "minecraft:player" }  // Only PvP hits
        }
      }
    ],
    mode: "repeatable",
    actions: () => {
      Actions.increment({ variable: variables.pvp_hits });
    },
  },
])

End Conditions

Define when the game ends:

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

Multiple conditions can be combined:

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

Ending conditions are evaluated once per tick, at the global level (not individually). It means you should not check for individual variables here - instead, you should aggregate these individual variables into a global custom variable, that you then check in the ending condition.

Win Conditions

Define how winners are determined per role:

.win_conditions((variables, { attacker, defender }) => ({
  [attacker]: variables.kills.greaterOrEqualThan(5),
  [defender]: variables.survived.equalTo(1),
}))

Actions

Actions are higher-level functions that wrap common Minecraft operations, designed to work seamlessly with Kradle's challenge system. They handle target mapping, formatting, and integration with Kradle's interface automatically.

For more advanced use cases, you can always fall back to Sandstone's lower-level functions directly (e.g., give, tellraw, effect, kill, execute). See the Sandstone Integration section below.

Communication

// Simple string message
Actions.announce({ message: "Hello everyone!" });

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

// Send to specific target (only visible in-game, not in Kradle's interface)
Actions.tellraw({ target: "all", message: ["Hello ", { text: "world", color: "gold" }] });
Actions.tellraw({ target: "self", message: { text: "You win!", color: "green", bold: true } });

Items & Inventory

Actions.give({ target: "self", item: "minecraft:diamond_sword", count: 1 });
Actions.giveLoot({
  target: "self",
  items: [
    { name: "minecraft:diamond", count: 5, weight: 1 },
    { name: "minecraft:iron_ingot", count: 10, weight: 3 }
  ]
});
Actions.clear({ target: "self" });

// Count items - returns a Score variable
const count = Actions.countItems({ target: "self", item: "minecraft:diamond" });
// Use in conditions or set to custom variables
_.if(count.greaterThan(5), () => { /* ... */ });

// Get player position - returns { x, y, z } Score variables
const pos = Actions.getCurrentPlayerPosition();
_.if(pos.y.greaterThan(100), () => { /* player is high up */ });

Entities

Actions.summonMultiple({ entity: "minecraft:zombie", count: 5, x: 0, y: 64, z: 0, absolute: true });
Actions.kill({ selector: Selector("@e", { type: "minecraft:zombie" }) });
Actions.teleport({ target: "self", x: 0, y: 100, z: 0, absolute: true });

World

Actions.setBlock({ block: "minecraft:diamond_block", x: 0, y: 64, z: 0, absolute: true });
Actions.fill({
  block: "minecraft:stone",
  x1: 0, y1: 64, z1: 0,
  x2: 10, y2: 64, z2: 10,
  absolute: true,
  mode: "fill"  // "fill", "line", or "pyramid"
});
Actions.setTime({ time: "day" });  // or "night" or specific tick value
Actions.gamerule({ rule: "doDaylightCycle", value: false });

Scores

Actions.set({ variable: variables.main_score, value: 10 });
Actions.set({ variable: variables.main_score, value: variables.diamonds });
Actions.increment({ variable: variables.counter });
Actions.decrement({ variable: variables.counter });

Player Attributes

Actions.setAttribute({ target: "self", attribute_: "generic.max_health", value: 40 });
Actions.setAttribute({ target: "self", attribute_: "generic.movement_speed", value: 0.2 });

Custom Commands

Actions.custom(() => {
  // Any Sandstone code here
  execute.as("@a").run.effect.give("@s", "speed", 10, 1);
});

Utilities

forEveryPlayer(callback)

Execute code for each participant at their location:

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

forEveryPlayer(() => {
  // Runs as each player, at their position
  execute.run.particle("minecraft:flame", rel(0, 1, 0));
});

Example: Battle Royale

import { createChallenge, Actions, forEveryPlayer } from "@kradle/challenges-sdk";
import { _, Selector } 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,
    },
    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.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 });
    },
  }))
  .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: Capture the Flag

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_enemy_flag: {
      type: "individual",
      updater: (value: Score) => {
        value.set(0);
        // Check if player holds the enemy team's banner
        _.if(Selector("@s", {
          nbt: { Inventory: [{ id: "minecraft:red_banner" }] }
        }), () => {
          value.set(1);
        });
      },
    },
    at_home_base: {
      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_enemy_flag, at_home_base }) => {
        value.set(0);
        _.if(_.and(
          holds_enemy_flag.equalTo(1),
          at_home_base.equalTo(1)
        ), () => {
          value.set(1);
        });
      },
    },
  },
})
  .events(() => ({
    start_challenge: () => {
      Actions.announce({ message: "Capture the enemy flag!" });
    },
  }))
  .custom_events(({ captured_flag }) => [
    {
      score: captured_flag,
      target: 1,
      mode: "fire_once",
      actions: () => {
        Actions.announce({ message: "Flag captured!" });
      },
    },
  ])
  .end_condition(({ captured_flag }) => captured_flag.equalTo(1))
  .win_conditions(({ captured_flag }, { red_team, blue_team }) => ({
    [red_team]: captured_flag.equalTo(1),
    [blue_team]: captured_flag.equalTo(0),
  }));