/** * 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; } }