SoulFire LogoSoulFire
AutomationSDKGuidesWorkflows

Beat-game runner

Save and resume progress for one bot or many bots with the TypeScript SDK.

@soulfiremc/beat-game is a separate first-party package. It uses SoulFire observations, actions, pathfinding, control leases, tasks, and plugin APIs. SoulFire does not contain a beat-game planner.

SoulFire handles Minecraft mechanics close to the connection. Your TypeScript process owns goals, recovery policy, checkpoints, and team coordination.

Run one bot

Install the packages

bun add @soulfiremc/sdk@2.10.2 @soulfiremc/beat-game@2.10.2 effect@^4

Create a ready bot and start the runner

beat-game.ts
import { SoulFire } from "@soulfiremc/sdk/bun";
import { beatGame } from "@soulfiremc/beat-game";
import { Effect, Stream } from "effect";

const program = Effect.scoped(
  Effect.gen(function* () {
    const bot = yield* SoulFire.createBot({
      server: "localhost:25565",
      username: "Survivor",
      installation: { version: "2.10.2" },
    });
    const run = yield* beatGame(bot);

    yield* Effect.forkScoped(
      run.events.pipe(
        Stream.runForEach((event) =>
          Effect.logInfo(event.type, {
            phase: event.phase,
            sequence: event.sequence,
          })
        ),
      ),
    );

    return yield* run.awaitCompletion;
  }),
);

const result = await Effect.runPromise(program);
console.log(result);

Run an offline-mode survival Minecraft server at localhost:25565, save the file, and run bun run beat-game.ts. For Node.js, use @soulfiremc/sdk/node and the Node runtime installation. For an existing backend, use named provisioning and attach the bot in the same scope. The SDK and runner both require Effect v4. The former /promise entry point is removed.

Keep the worker alive

awaitCompletion ends after confirmed dragon defeat and the configured completion objective. The default objective returns the bot to the Overworld through the End exit. Keep the Effect scope alive for the whole run. Closing it interrupts tasks, releases control, and ends subscriptions.

Add restart persistence

Every run has a checkpoint store. The default store is in memory. Use the Node-only JSON store to keep one worker state across restarts:

import { JsonFileBeatGameCheckpointStore } from
  "@soulfiremc/beat-game/node";

const checkpointStore =
  new JsonFileBeatGameCheckpointStore("./soulfire-runs");

const run = yield* beatGame(bot, {
  runId: "survival-01",
  checkpointStore,
});

Restart with the same runId, instance, bot, team ID, and directory. The store validates the complete checkpoint, locks each run file, and uses compare-and-set revisions to reject a second writer.

The runner also saves the current action identity and last stable action evidence. Durable tasks use stable idempotency keys and deadlines. After a restart, the worker attaches to existing server work instead of duplicating it.

For a shared database, implement BeatGameCheckpointStore with load, save, and remove. save must enforce expectedRevision.

Run a coordinated team

import {
  InMemoryBeatGameCoordinator,
  beatGameTeam,
} from "@soulfiremc/beat-game";

const bots = botIds.map((botId) =>
  soulfire.instance(instanceId).bot(botId)
);

const team = yield* beatGameTeam(bots, {
  teamId: "release-run",
  checkpointStore,
  coordinator: new InMemoryBeatGameCoordinator(),
});

const results = yield* team.awaitCompletion;

The default assignment is deterministic. Team state includes roles, shared requirement counts, discoveries, expiring claims, leader fencing, and an End-entry quota. The in-memory coordinator is for one process. Implement BeatGameCoordinator with Redis, Postgres, or another shared system before running workers in separate processes.

Pause, resume, stop, and inspect

Run handles expose Effect values:

yield* run.pause;
const current = yield* run.snapshot;
yield* run.resume;
yield* run.stop;

Stopping interrupts an active observation, task, or action. A stopped run fails awaitCompletion with BeatGameCancelled and retains its checkpoint.

What the server still does

The runner deliberately uses SoulFire's durable generic tasks for work such as collecting blocks, attacking, crafting, building, smelting, exploring, and automatic equipment management. It uses direct actions and observations for game-specific workflows such as throwing an eye, casting a portal, and filling End frames.

This boundary lets another application use the same generic tasks without inheriting the beat-game plan.

Next steps

How is this page?

Last updated on

On this page