feat: Add BuildingState for autonomous block placement with material management
This commit is contained in:
311
server/states/BuildingState.js
Normal file
311
server/states/BuildingState.js
Normal file
@@ -0,0 +1,311 @@
|
||||
/**
|
||||
* BuildingState - Blueprint-based autonomous block placement
|
||||
*
|
||||
* Given a list of blocks (x, y, z, name, state), the turtle will:
|
||||
* 1. Calculate required materials
|
||||
* 2. Go home to collect materials from nearby inventories
|
||||
* 3. Navigate to each block position and place it
|
||||
* 4. Handle facing-aware placement (stairs, logs, etc.)
|
||||
* 5. Track failed placements and retry later
|
||||
*/
|
||||
import { BaseState } from './BaseState.js';
|
||||
|
||||
export class BuildingState extends BaseState {
|
||||
constructor(turtle, data = {}) {
|
||||
super(turtle, data);
|
||||
// blocks: [{x, y, z, name, state?}]
|
||||
this.blocks = data.blocks || [];
|
||||
this.placedCount = 0;
|
||||
this.failedPlacements = new Map(); // "x,y,z" -> retry count
|
||||
this.maxRetries = 3;
|
||||
|
||||
// Group blocks by column (x,z) for efficient placement
|
||||
this.columns = new Map(); // "x,z" -> [{x,y,z,name,state}] sorted by y ascending
|
||||
this._organizeBlocks();
|
||||
}
|
||||
|
||||
get name() {
|
||||
return 'building';
|
||||
}
|
||||
|
||||
get description() {
|
||||
return `Building - ${this.placedCount}/${this.blocks.length} placed`;
|
||||
}
|
||||
|
||||
_organizeBlocks() {
|
||||
// Filter out blocks that might already be placed (we can't check DB here,
|
||||
// but we organize for efficient traversal)
|
||||
for (const block of this.blocks) {
|
||||
const key = `${block.x},${block.z}`;
|
||||
if (!this.columns.has(key)) {
|
||||
this.columns.set(key, []);
|
||||
}
|
||||
this.columns.get(key).push(block);
|
||||
}
|
||||
|
||||
// Sort each column by Y ascending (place from bottom up)
|
||||
for (const [key, column] of this.columns) {
|
||||
column.sort((a, b) => a.y - b.y);
|
||||
}
|
||||
}
|
||||
|
||||
async *act() {
|
||||
console.log(`[${this.turtle.id}] Starting build: ${this.blocks.length} blocks to place`);
|
||||
|
||||
while (!this.cancelled) {
|
||||
// Check if we have any blocks left to place
|
||||
const remainingBlocks = this._getRemainingBlocks();
|
||||
if (remainingBlocks.length === 0) {
|
||||
console.log(`[${this.turtle.id}] Build complete: ${this.placedCount} blocks placed`);
|
||||
this.turtle.setState('idle');
|
||||
return;
|
||||
}
|
||||
|
||||
// Safety checks
|
||||
const fuel = await this.checkFuel();
|
||||
if (fuel !== 'unlimited' && fuel < 500) {
|
||||
const refueled = await this.tryRefuel();
|
||||
if (!refueled) {
|
||||
console.log(`[${this.turtle.id}] Low fuel during build, going home`);
|
||||
this.turtle.setState('goHome', { reason: 'low_fuel', returnState: 'building', returnData: this.data });
|
||||
return;
|
||||
}
|
||||
}
|
||||
yield;
|
||||
|
||||
// Calculate required materials
|
||||
const requiredMaterials = this._calculateMaterials(remainingBlocks);
|
||||
|
||||
// Check if we have the materials
|
||||
const inventory = await this.getInventory();
|
||||
const hasMaterials = this._hasRequiredMaterials(requiredMaterials, inventory);
|
||||
yield;
|
||||
|
||||
if (!hasMaterials) {
|
||||
// Go home to collect materials
|
||||
const home = this.turtle.homePosition;
|
||||
if (!home) {
|
||||
console.log(`[${this.turtle.id}] No home set, cannot collect materials`);
|
||||
this.turtle.setState('idle');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[${this.turtle.id}] Going home to collect building materials`);
|
||||
|
||||
// Navigate home
|
||||
const reachedHome = yield* this.navigateTo(home);
|
||||
if (!reachedHome) {
|
||||
console.log(`[${this.turtle.id}] Cannot reach home`);
|
||||
await this._sleep(5000);
|
||||
yield;
|
||||
continue;
|
||||
}
|
||||
yield;
|
||||
|
||||
// Dump current inventory into nearby chests
|
||||
await this._dumpInventory();
|
||||
yield;
|
||||
|
||||
// Try to pull required materials
|
||||
const gotMaterials = await this._pullMaterials(requiredMaterials);
|
||||
yield;
|
||||
|
||||
if (!gotMaterials) {
|
||||
console.log(`[${this.turtle.id}] Cannot get required materials, waiting...`);
|
||||
await this._sleep(10000);
|
||||
yield;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Find the next block to place (closest to turtle)
|
||||
const pos = this.turtle.position;
|
||||
if (!pos) {
|
||||
await this._sleep(1000);
|
||||
yield;
|
||||
continue;
|
||||
}
|
||||
|
||||
const nextBlock = this._findClosestBlock(remainingBlocks, pos);
|
||||
if (!nextBlock) {
|
||||
await this._sleep(1000);
|
||||
yield;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Navigate to one block above the target (to place down)
|
||||
const placePos = { x: nextBlock.x, y: nextBlock.y + 1, z: nextBlock.z };
|
||||
const reached = yield* this.navigateTo(placePos);
|
||||
yield;
|
||||
|
||||
if (!reached) {
|
||||
// Mark as failed
|
||||
const key = `${nextBlock.x},${nextBlock.y},${nextBlock.z}`;
|
||||
const retries = (this.failedPlacements.get(key) || 0) + 1;
|
||||
this.failedPlacements.set(key, retries);
|
||||
|
||||
if (retries >= this.maxRetries) {
|
||||
console.log(`[${this.turtle.id}] Cannot reach ${key}, skipping permanently`);
|
||||
}
|
||||
yield;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle facing - turn turtle to face correct direction before placing
|
||||
if (nextBlock.state && nextBlock.state.facing) {
|
||||
await this._turnForPlacement(nextBlock.state.facing);
|
||||
yield;
|
||||
}
|
||||
|
||||
// Select the correct item
|
||||
const selected = await this._selectItem(nextBlock.name);
|
||||
if (!selected) {
|
||||
console.log(`[${this.turtle.id}] Don't have ${nextBlock.name} to place`);
|
||||
yield;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Place the block
|
||||
const placed = await this.exec('return turtle.placeDown()');
|
||||
yield;
|
||||
|
||||
if (placed) {
|
||||
this.placedCount++;
|
||||
// Remove from our list
|
||||
this._markBlockPlaced(nextBlock);
|
||||
console.log(`[${this.turtle.id}] Placed ${nextBlock.name} at ${nextBlock.x},${nextBlock.y},${nextBlock.z} (${this.placedCount}/${this.blocks.length})`);
|
||||
} else {
|
||||
const key = `${nextBlock.x},${nextBlock.y},${nextBlock.z}`;
|
||||
const retries = (this.failedPlacements.get(key) || 0) + 1;
|
||||
this.failedPlacements.set(key, retries);
|
||||
}
|
||||
|
||||
yield;
|
||||
await this._sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
_getRemainingBlocks() {
|
||||
const remaining = [];
|
||||
for (const [key, column] of this.columns) {
|
||||
for (const block of column) {
|
||||
const blockKey = `${block.x},${block.y},${block.z}`;
|
||||
const retries = this.failedPlacements.get(blockKey) || 0;
|
||||
if (retries < this.maxRetries) {
|
||||
remaining.push(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
return remaining;
|
||||
}
|
||||
|
||||
_calculateMaterials(blocks) {
|
||||
const materials = new Map(); // name -> count
|
||||
for (const block of blocks) {
|
||||
materials.set(block.name, (materials.get(block.name) || 0) + 1);
|
||||
}
|
||||
return materials;
|
||||
}
|
||||
|
||||
_hasRequiredMaterials(required, inventory) {
|
||||
if (!inventory) return false;
|
||||
const available = new Map();
|
||||
for (const [slot, item] of Object.entries(inventory)) {
|
||||
if (item && item.name) {
|
||||
available.set(item.name, (available.get(item.name) || 0) + item.count);
|
||||
}
|
||||
}
|
||||
for (const [name, count] of required) {
|
||||
if ((available.get(name) || 0) < Math.min(count, 1)) {
|
||||
return false; // Need at least 1 of each material
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
_findClosestBlock(blocks, from) {
|
||||
let closest = null;
|
||||
let minDist = Infinity;
|
||||
for (const block of blocks) {
|
||||
const dist = Math.abs(block.x - from.x) + Math.abs(block.y - from.y) + Math.abs(block.z - from.z);
|
||||
if (dist < minDist) {
|
||||
minDist = dist;
|
||||
closest = block;
|
||||
}
|
||||
}
|
||||
return closest;
|
||||
}
|
||||
|
||||
_markBlockPlaced(block) {
|
||||
const key = `${block.x},${block.z}`;
|
||||
const column = this.columns.get(key);
|
||||
if (column) {
|
||||
const idx = column.findIndex(b => b.x === block.x && b.y === block.y && b.z === block.z);
|
||||
if (idx >= 0) column.splice(idx, 1);
|
||||
if (column.length === 0) this.columns.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
async _turnForPlacement(facing) {
|
||||
// When placing blocks with facing, the turtle should face the opposite direction
|
||||
// because placed blocks face toward the placer
|
||||
const facingMap = { north: 2, south: 0, east: 3, west: 1 };
|
||||
const targetFacing = facingMap[facing];
|
||||
if (targetFacing !== undefined) {
|
||||
await this.turnToFace(targetFacing);
|
||||
}
|
||||
}
|
||||
|
||||
async _selectItem(blockName) {
|
||||
const result = await this.exec(`
|
||||
for slot = 1, 16 do
|
||||
local item = turtle.getItemDetail(slot)
|
||||
if item and item.name == "${blockName}" then
|
||||
turtle.select(slot)
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
`);
|
||||
return result === true;
|
||||
}
|
||||
|
||||
async _dumpInventory() {
|
||||
await this.exec(`
|
||||
for slot = 1, 16 do
|
||||
local item = turtle.getItemDetail(slot)
|
||||
if item then
|
||||
turtle.select(slot)
|
||||
turtle.dropDown()
|
||||
end
|
||||
end
|
||||
turtle.select(1)
|
||||
`);
|
||||
}
|
||||
|
||||
async _pullMaterials(required) {
|
||||
// Try to suck items from chests below (at home position)
|
||||
let gotAny = false;
|
||||
for (const [name, count] of required) {
|
||||
const result = await this.exec(`
|
||||
local needed = "${name}"
|
||||
for slot = 1, 16 do
|
||||
if turtle.getItemCount(slot) == 0 then
|
||||
turtle.select(slot)
|
||||
turtle.suckDown(64)
|
||||
local item = turtle.getItemDetail(slot)
|
||||
if item and item.name == needed then
|
||||
return true
|
||||
elseif item then
|
||||
turtle.dropDown()
|
||||
end
|
||||
end
|
||||
end
|
||||
turtle.select(1)
|
||||
return false
|
||||
`);
|
||||
if (result) gotAny = true;
|
||||
}
|
||||
return gotAny;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user