/** * BaseState - Abstract base class for turtle state machine * * Uses async generator pattern inspired by runi95/turtle-control-panel. * Each state implements *act() which yields control back periodically, * allowing the state machine to be cooperative and interruptible. */ export class BaseState { /** * @param {import('../Turtle.js').Turtle} turtle - The turtle instance * @param {Object} data - State-specific data for initialization/recovery */ constructor(turtle, data = {}) { this.turtle = turtle; this.data = data; this.cancelled = false; this.startedAt = Date.now(); } /** * The name of this state (used for logging and UI) */ get name() { return 'base'; } /** * Get a human-readable description of what the state is doing */ get description() { return 'Idle'; } /** * The async generator that drives the state's behavior. * Subclasses MUST override this method. * * Yield to give up control temporarily (cooperative multitasking). * Return to signal that the state is complete. * Throw to signal an error. * * @yields {void} */ async *act() { // Base state does nothing - just idles while (!this.cancelled) { yield; await this._sleep(1000); } } /** * Cancel this state. The act() generator should check this.cancelled * and clean up gracefully. */ cancel() { this.cancelled = true; } /** * Get recovery data for persisting state across reconnections */ getRecoveryData() { return { stateName: this.name, data: this.data, startedAt: this.startedAt, }; } // ========== Helper Methods for Subclasses ========== /** * Execute a Lua command on the turtle and wait for the result */ async exec(luaCode) { return this.turtle.exec(luaCode); } /** * Move the turtle forward, digging if necessary */ async moveForward(dig = true) { if (dig) { const result = await this.exec('turtle.dig(); return turtle.forward()'); if (result) this.turtle.updatePositionForward(); return result; } const result = await this.exec('return turtle.forward()'); if (result) this.turtle.updatePositionForward(); return result; } /** * Move the turtle up, digging if necessary */ async moveUp(dig = true) { if (dig) { const result = await this.exec('turtle.digUp(); return turtle.up()'); if (result) this.turtle.updatePositionUp(); return result; } const result = await this.exec('return turtle.up()'); if (result) this.turtle.updatePositionUp(); return result; } /** * Move the turtle down, digging if necessary */ async moveDown(dig = true) { if (dig) { const result = await this.exec('turtle.digDown(); return turtle.down()'); if (result) this.turtle.updatePositionDown(); return result; } const result = await this.exec('return turtle.down()'); if (result) this.turtle.updatePositionDown(); return result; } /** * Turn the turtle to face a specific direction * @param {number} targetFacing - 0=North, 1=East, 2=South, 3=West */ async turnToFace(targetFacing) { const currentFacing = this.turtle.facing; if (currentFacing === targetFacing) return true; const diff = (targetFacing - currentFacing + 4) % 4; if (diff === 1) { await this.exec(`turtle.turnRight(); _G._turtleFacing = ${targetFacing}`); this.turtle.facing = targetFacing; } else if (diff === 2) { await this.exec(`turtle.turnRight(); turtle.turnRight(); _G._turtleFacing = ${targetFacing}`); this.turtle.facing = targetFacing; } else if (diff === 3) { await this.exec(`turtle.turnLeft(); _G._turtleFacing = ${targetFacing}`); this.turtle.facing = targetFacing; } return true; } /** * Move to an adjacent point (one of the 6 neighbors) * Handles turning and digging automatically */ async moveToAdjacent(targetPoint) { const pos = this.turtle.position; if (!pos) return false; const dx = targetPoint.x - pos.x; const dy = targetPoint.y - pos.y; const dz = targetPoint.z - pos.z; if (dy === 1) return this.moveUp(); if (dy === -1) return this.moveDown(); // Horizontal movement - determine facing let targetFacing; if (dx === 1) targetFacing = 1; // East else if (dx === -1) targetFacing = 3; // West else if (dz === 1) targetFacing = 2; // South else if (dz === -1) targetFacing = 0; // North else return false; // Not adjacent await this.turnToFace(targetFacing); return this.moveForward(); } /** * Navigate to a target point using the D* Lite pathfinder * This is a generator that yields after each step */ async *navigateTo(targetPoint, options = {}) { const { Point, DStarLite } = await import('../pathfinding/index.js'); const start = Point.from(this.turtle.position); const goal = Point.from(targetPoint); if (!start || !goal) { console.error('Cannot navigate: missing position'); return false; } if (start.equals(goal)) return true; const pathfinder = new DStarLite(start, goal, this.turtle.server.worldBlocks, { canMine: options.canMine !== false, maxSteps: options.maxSteps || 50000, }); let currentPos = start; let attempts = 0; const maxAttempts = options.maxAttempts || 5000; while (!currentPos.equals(goal) && attempts < maxAttempts && !this.cancelled) { attempts++; const nextStep = pathfinder.getNextStep(); if (!nextStep) { console.log(`[${this.turtle.id}] No path to ${goal} from ${currentPos}`); return false; } // Scan surroundings before moving const scanResult = await this.exec(` local results = {} local hasBlock, data hasBlock, data = turtle.inspect() if hasBlock then results.forward = data end hasBlock, data = turtle.inspectUp() if hasBlock then results.up = data end hasBlock, data = turtle.inspectDown() if hasBlock then results.down = data end return results `); // Update pathfinder with discovered blocks if (scanResult && typeof scanResult === 'object') { this.turtle.processScanResults(scanResult); // Update pathfinder with new information for (const [dir, blockData] of Object.entries(scanResult)) { const blockPos = this.turtle.getBlockPositionInDirection(dir); if (blockPos) { pathfinder.updateBlock(Point.from(blockPos), blockData); } } } // Try to move to the next step const success = await this.moveToAdjacent(nextStep); if (success) { currentPos = Point.from(this.turtle.position); pathfinder.updateStart(currentPos); } else { // Movement failed - update the blocked node and replan pathfinder.updateBlock(nextStep, { name: 'minecraft:bedrock' }); // Treat as blocked if (!pathfinder.replan()) { console.log(`[${this.turtle.id}] Replanning failed - no path exists`); return false; } } yield; // Cooperative yield } return currentPos.equals(goal); } /** * Scan all 6 directions around the turtle */ async scanSurroundings() { const result = await this.exec(` local results = {} local facing = _G._turtleFacing or 0 -- Scan up/down local hasBlock, data hasBlock, data = turtle.inspectUp() if hasBlock then results.up = {name=data.name, metadata=data.metadata or 0} end hasBlock, data = turtle.inspectDown() if hasBlock then results.down = {name=data.name, metadata=data.metadata or 0} end -- Scan all 4 horizontal directions for i = 0, 3 do hasBlock, data = turtle.inspect() if hasBlock then local dir = (facing + i) % 4 results["h" .. dir] = {name=data.name, metadata=data.metadata or 0} end if i < 3 then turtle.turnRight(); facing = (facing + 1) % 4 end end -- Turn back to original facing turtle.turnRight() facing = (facing + 1) % 4 _G._turtleFacing = facing return results `); if (result) { this.turtle.processScanResults(result); } return result; } /** * Check fuel level */ async checkFuel() { const fuel = await this.exec('return turtle.getFuelLevel()'); if (fuel !== null) this.turtle.fuel = fuel; return fuel; } /** * Get inventory contents */ async getInventory() { const inv = await this.exec(` local items = {} for slot = 1, 16 do local item = turtle.getItemDetail(slot) if item then items[slot] = {name=item.name, count=item.count, damage=item.damage} end end return items `); if (inv) this.turtle.inventory = inv; return inv; } /** * Check if inventory is full */ async isInventoryFull() { const result = await this.exec(` for slot = 1, 16 do if turtle.getItemCount(slot) == 0 then return false end end return true `); return result === true; } /** * Try to refuel from inventory */ async tryRefuel() { return this.exec(` for slot = 1, 16 do turtle.select(slot) if turtle.refuel(0) then turtle.refuel(1) turtle.select(1) return true end end turtle.select(1) return false `); } /** * Sleep for a duration (non-blocking) */ _sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } }