feat(runtime): village progression — crafting, tech-tree reflex, LLM planner (#11)
Closes the loop "стой и кидай proposals" → "копит ресурсы, строит,
работает к глобальной цели". Three pieces:
1. Crafting primitives (runtime/actions.js).
craftPlanks (4 per log, any wood type), craftSticks (4 per 2 planks),
placeCraftingTable (crafts a table from planks if needed + places at
reference block + reuses an existing table within 4 m), craftWoodenAxe,
craftWoodenPickaxe, craftWoodenSword. Each uses bot.recipesFor()
+ bot.craft() with a 15s timeout. Returns the same {ok, detail}
contract as the other actions.
inv.{getItemCount, getAnyPlanksCount, getAnyLogCount} helpers
exported so the reflex layer can read inventory cheaply without
pulling mineflayer state through every reducer.
2. Tech-tree reflex (runtime/reflex.js).
New techTreeReflex between sleep and autonomous. Inventory-driven
progression: log+0 planks → planks; planks+0 sticks → sticks;
planks+sticks+no axe → wooden_axe; +no pickaxe → wooden_pickaxe;
+no sword → wooden_sword. 5 s cooldown so we don't fire on every
tick.
Pure script, no LLM. The progression is exactly what a player
does in the first 10 min on a new world; making it scripted means
the bot never burns tokens on it.
3. LLM planner (runtime/planner.js).
Background timer (every 15 min, with a 30 s warm-up after start).
Reads goal.md + plan.md + a slim snapshot, prompts Pi to output a
fresh plan.md to stdout. Stripped of code fences and written
verbatim to state/<host>/plan.md. Capped at 16 KB.
The plan is markdown the operator can read or edit by hand. Numbered
milestones, ✓ prefix for completed ones, kept short. The reflex
layer doesn't auto-execute LLM text — but the planner sets the
long-horizon shape that future reflexes (build house, plant farm)
can read.
5 min timeout on the pi subprocess. If it crashes or times out, the
next 15-min tick just retries — no propagation to the reflex loop.
The progression now looks like, roughly:
chop log (autonomous) →
craft planks → craft sticks → wooden_axe (tech-tree) →
chop faster (autonomous, has axe now) →
wooden_pickaxe + wooden_sword (tech-tree) →
mine stone … (next PR: stone tools, farm site selection,
house frame)
Smoke-tested: all three modules import cleanly, exports check out.
Co-authored-by: Yuriy Mayatnikov <mayatnikov@me.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit was merged in pull request #11.
This commit is contained in:
+91
-1
@@ -7,7 +7,22 @@
|
||||
// escalates to Pi after a threshold (see ESCALATE_AFTER_NOOPS).
|
||||
|
||||
import { info, warn } from "./log.js";
|
||||
import { attackNearest, fleeFrom, eatBestFood, sleepInBed, goTo, chopNearestTree, wander } from "./actions.js";
|
||||
import {
|
||||
attackNearest,
|
||||
fleeFrom,
|
||||
eatBestFood,
|
||||
sleepInBed,
|
||||
goTo,
|
||||
chopNearestTree,
|
||||
wander,
|
||||
craftPlanks,
|
||||
craftSticks,
|
||||
placeCraftingTable,
|
||||
craftWoodenAxe,
|
||||
craftWoodenPickaxe,
|
||||
craftWoodenSword,
|
||||
inv,
|
||||
} from "./actions.js";
|
||||
|
||||
const REFLEX_LOG = "reflex";
|
||||
|
||||
@@ -194,6 +209,80 @@ function autonomousReflex(ctx) {
|
||||
return { action: "dispatched", kind: "autonomous-wander", label: "wander" };
|
||||
}
|
||||
|
||||
// ---- tech-tree progression -------------------------------------------------
|
||||
//
|
||||
// Scripted progression toward the long-term goal (small farm + village). Runs
|
||||
// between the autonomous wood-gathering reflex and idle. Order:
|
||||
// 1. have ≥4 logs but 0 planks → craft planks
|
||||
// 2. have ≥2 planks but 0 sticks → craft sticks
|
||||
// 3. have planks+sticks but no axe → craft wooden_axe (places a table)
|
||||
// 4. have axe but no pickaxe → craft wooden_pickaxe
|
||||
// 5. have pickaxe but no sword → craft wooden_sword
|
||||
// 6. tools done — fall through to autonomous (chop more, then mine stone)
|
||||
//
|
||||
// Each step is cheap and idempotent: if it can't act it returns noop.
|
||||
|
||||
const TECH_TREE_COOLDOWN_MS = 5_000;
|
||||
|
||||
function techTreeReflex(ctx) {
|
||||
const s = ctx.snapshot;
|
||||
if (!s.connected) return { action: "noop" };
|
||||
if (!ctx.bot) return { action: "noop" };
|
||||
|
||||
const since = Date.now() - (ctx.lastTechTreeAt ?? 0);
|
||||
if (since < TECH_TREE_COOLDOWN_MS) return { action: "noop" };
|
||||
|
||||
const logs = inv.getAnyLogCount(ctx.bot);
|
||||
const planks = inv.getAnyPlanksCount(ctx.bot);
|
||||
const sticks = inv.getItemCount(ctx.bot, "stick");
|
||||
const hasAxe = ["wooden_axe", "stone_axe", "iron_axe", "diamond_axe", "netherite_axe"].some(
|
||||
(n) => inv.getItemCount(ctx.bot, n) > 0,
|
||||
);
|
||||
const hasPickaxe = [
|
||||
"wooden_pickaxe",
|
||||
"stone_pickaxe",
|
||||
"iron_pickaxe",
|
||||
"diamond_pickaxe",
|
||||
"netherite_pickaxe",
|
||||
].some((n) => inv.getItemCount(ctx.bot, n) > 0);
|
||||
const hasSword = ["wooden_sword", "stone_sword", "iron_sword", "diamond_sword", "netherite_sword"].some(
|
||||
(n) => inv.getItemCount(ctx.bot, n) > 0,
|
||||
);
|
||||
|
||||
// Step 1: planks
|
||||
if (logs >= 1 && planks < 4) {
|
||||
ctx.lastTechTreeAt = Date.now();
|
||||
ctx.dispatch(() => craftPlanks(ctx.bot, 4), "craft planks");
|
||||
return { action: "dispatched", kind: "tech-planks", label: `planks (have ${planks}/4)` };
|
||||
}
|
||||
// Step 2: sticks
|
||||
if (planks >= 2 && sticks < 4) {
|
||||
ctx.lastTechTreeAt = Date.now();
|
||||
ctx.dispatch(() => craftSticks(ctx.bot, 4), "craft sticks");
|
||||
return { action: "dispatched", kind: "tech-sticks", label: `sticks (have ${sticks}/4)` };
|
||||
}
|
||||
// Step 3: axe
|
||||
if (planks >= 3 && sticks >= 2 && !hasAxe) {
|
||||
ctx.lastTechTreeAt = Date.now();
|
||||
ctx.dispatch(() => craftWoodenAxe(ctx.bot), "craft wooden_axe");
|
||||
return { action: "dispatched", kind: "tech-axe", label: "wooden_axe" };
|
||||
}
|
||||
// Step 4: pickaxe
|
||||
if (planks >= 3 && sticks >= 2 && hasAxe && !hasPickaxe) {
|
||||
ctx.lastTechTreeAt = Date.now();
|
||||
ctx.dispatch(() => craftWoodenPickaxe(ctx.bot), "craft wooden_pickaxe");
|
||||
return { action: "dispatched", kind: "tech-pickaxe", label: "wooden_pickaxe" };
|
||||
}
|
||||
// Step 5: sword
|
||||
if (planks >= 2 && sticks >= 1 && hasAxe && hasPickaxe && !hasSword) {
|
||||
ctx.lastTechTreeAt = Date.now();
|
||||
ctx.dispatch(() => craftWoodenSword(ctx.bot), "craft wooden_sword");
|
||||
return { action: "dispatched", kind: "tech-sword", label: "wooden_sword" };
|
||||
}
|
||||
|
||||
return { action: "noop" };
|
||||
}
|
||||
|
||||
// ---- idle ------------------------------------------------------------------
|
||||
|
||||
function idleReflex(ctx) {
|
||||
@@ -213,6 +302,7 @@ const REFLEXES = [
|
||||
{ name: "defend", fn: defendReflex },
|
||||
{ name: "eat", fn: eatReflex },
|
||||
{ name: "sleep", fn: sleepReflex },
|
||||
{ name: "tech-tree", fn: techTreeReflex },
|
||||
{ name: "autonomous", fn: autonomousReflex },
|
||||
{ name: "idle", fn: idleReflex },
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user