From d4e6b469df5360501558e21e5cabe6664336d9c7 Mon Sep 17 00:00:00 2001 From: MayaTheShy Date: Fri, 20 Feb 2026 02:11:45 -0500 Subject: [PATCH] feat: Add BuildingState for autonomous block placement with material management --- server/states/BuildingState.js | 311 +++++++++++++++++++++++++++++++++ 1 file changed, 311 insertions(+) create mode 100644 server/states/BuildingState.js diff --git a/server/states/BuildingState.js b/server/states/BuildingState.js new file mode 100644 index 0000000..5d1419b --- /dev/null +++ b/server/states/BuildingState.js @@ -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; + } +}