/** * ExploringState - Chunk-based spiral exploration to discover the world map * * Instead of random walking, this systematically explores in a spiral pattern * outward from the starting position, chunk by chunk (16x16 areas). * Within each chunk, it does a strip-mine pattern to scan all blocks. * * Inspired by runi95/turtle-control-panel's exploration pattern. */ import { BaseState } from './BaseState.js'; export class ExploringState extends BaseState { constructor(turtle, data = {}) { super(turtle, data); this.maxDistance = data.maxDistance || 200; this.minFuel = data.minFuel || 500; this.yLevel = data.yLevel || null; // null = stay at current Y this.blocksDiscovered = 0; this.chunksExplored = 0; // Spiral state this.spiralRing = data.spiralRing || 0; this.spiralSide = data.spiralSide || 0; this.spiralStep = data.spiralStep || 0; this.spiralChunkX = data.spiralChunkX ?? null; this.spiralChunkZ = data.spiralChunkZ ?? null; this.startChunkX = data.startChunkX ?? null; this.startChunkZ = data.startChunkZ ?? null; this.exploredChunks = new Set(data.exploredChunks || []); } get name() { return 'exploring'; } get description() { return `Exploring - ${this.blocksDiscovered} blocks, ${this.chunksExplored} chunks (ring ${this.spiralRing})`; } async *act() { const pos = this.turtle.position; if (!pos) { console.log(`[${this.turtle.id}] No position, trying GPS...`); await this.turtle.gpsLocate(); if (!this.turtle.position) { console.error(`[${this.turtle.id}] Cannot explore without position`); this.turtle.setState('idle'); return; } } // Initialize start chunk from current position if (this.startChunkX === null) { this.startChunkX = Math.floor(this.turtle.position.x / 16); this.startChunkZ = Math.floor(this.turtle.position.z / 16); this.spiralChunkX = this.startChunkX; this.spiralChunkZ = this.startChunkZ; } // Set Y level for exploration if (!this.yLevel) { this.yLevel = this.turtle.position.y; } console.log(`[${this.turtle.id}] Starting chunk-based spiral exploration from chunk (${this.startChunkX}, ${this.startChunkZ}), Y=${this.yLevel}`); while (!this.cancelled) { try { // Safety: check fuel const fuel = await this.checkFuel(); if (fuel !== 'unlimited' && fuel < this.minFuel) { const refueled = await this.tryRefuel(); if (!refueled) { console.log(`[${this.turtle.id}] Low fuel, going home`); this.turtle.setState('goHome', { reason: 'low_fuel', returnState: 'exploring', returnData: this.getRecoveryData().data }); return; } } // Safety: check inventory const isFull = await this.isInventoryFull(); if (isFull) { console.log(`[${this.turtle.id}] Inventory full, going home to dump`); this.turtle.setState('goHome', { reason: 'inventory_full', returnState: 'exploring', returnData: this.getRecoveryData().data }); return; } // Safety: check distance if (this._isTooFar()) { console.log(`[${this.turtle.id}] Too far from home, returning`); this.turtle.setState('goHome', { reason: 'too_far' }); return; } // Get the current target chunk const chunkKey = `${this.spiralChunkX},${this.spiralChunkZ}`; if (!this.exploredChunks.has(chunkKey)) { // Explore this chunk console.log(`[${this.turtle.id}] Exploring chunk (${this.spiralChunkX}, ${this.spiralChunkZ}) [ring ${this.spiralRing}]`); yield* this._exploreChunk(this.spiralChunkX, this.spiralChunkZ); this.exploredChunks.add(chunkKey); this.chunksExplored++; } // Advance spiral to next chunk this._advanceSpiral(); yield; } catch (error) { const isTimeout = error.message?.includes('timed out'); if (isTimeout) { console.warn(`[${this.turtle.id}] Exploration timeout, retrying...`); await this._sleep(3000); } else { throw error; } } } } /** * Explore a single chunk by navigating to it and doing a strip-mine scan pattern. * Scans every 3rd row at the exploration Y level for good coverage. */ async *_exploreChunk(chunkX, chunkZ) { const startX = chunkX * 16; const startZ = chunkZ * 16; // Navigate to the start corner of the chunk const chunkStart = { x: startX, y: this.yLevel, z: startZ }; yield* this._navigateToSafe(chunkStart); // Strip-mine scan pattern across the chunk // Every 3 blocks gives good coverage with turtle.inspect() range let forward = true; for (let row = 0; row < 16; row += 3) { if (this.cancelled) return; const rowZ = startZ + row; const rowStartX = forward ? startX : startX + 15; // Navigate to row start yield* this._navigateToSafe({ x: rowStartX, y: this.yLevel, z: rowZ }); // Walk along the row, scanning as we go const direction = forward ? 1 : 3; // East or West await this.turnToFace(direction); for (let step = 0; step < 15; step++) { if (this.cancelled) return; // 3-direction scan const scanResult = await this.exec(` local results = {} local h, d h, d = turtle.inspect() if h then results.forward = d end h, d = turtle.inspectUp() if h then results.up = d end h, d = turtle.inspectDown() if h then results.down = d end return results `); if (scanResult && typeof scanResult === 'object') { this.turtle.processScanResults(scanResult); this.blocksDiscovered += Object.keys(scanResult).length; } // Mine any valuable ores we find yield* this._checkAndMineOres(); // Move forward (digging through obstacles) const moved = await this.moveForward(true); if (!moved) { // Stuck - try to go over await this.moveUp(true); const movedOver = await this.moveForward(true); if (movedOver) { await this.moveDown(true); } else { // Really stuck, skip rest of row break; } } yield; await this._sleep(100); } // Alternate direction for serpentine pattern forward = !forward; } } /** * Navigate to a target position safely using pathfinding with simple fallback */ async *_navigateToSafe(target) { try { const success = yield* this.navigateTo(target, { canMine: true, maxAttempts: 500 }); if (success) return; } catch (error) { // Pathfinding failed, use simple navigation } // Fallback: simple direct navigation yield* this._simpleNavigate(target); } /** * Simple direct navigation (X -> Z -> Y) */ async *_simpleNavigate(target) { const maxAttempts = 300; let attempts = 0; let stuckCount = 0; while (attempts < maxAttempts && !this.cancelled) { const pos = this.turtle.position; if (!pos) return; const dx = target.x - pos.x; const dy = target.y - pos.y; const dz = target.z - pos.z; // Close enough (within 2 blocks) if (Math.abs(dx) <= 1 && Math.abs(dy) <= 1 && Math.abs(dz) <= 1) return; attempts++; let moved = false; // Move in X direction if (Math.abs(dx) > 1) { const facing = dx > 0 ? 1 : 3; await this.turnToFace(facing); moved = await this.moveForward(true); } // Then Z direction else if (Math.abs(dz) > 1) { const facing = dz > 0 ? 2 : 0; await this.turnToFace(facing); moved = await this.moveForward(true); } // Then Y direction else if (dy > 0) { moved = await this.moveUp(true); } else if (dy < 0) { moved = await this.moveDown(true); } if (!moved) { stuckCount++; if (stuckCount > 5) { // Try going up and over await this.moveUp(true); await this.moveUp(true); stuckCount = 0; } } else { stuckCount = 0; } yield; } } async *_checkAndMineOres() { const valuableOres = new Set([ 'minecraft:diamond_ore', 'minecraft:emerald_ore', 'minecraft:deepslate_diamond_ore', 'minecraft:deepslate_emerald_ore', 'minecraft:gold_ore', 'minecraft:deepslate_gold_ore', 'minecraft:iron_ore', 'minecraft:deepslate_iron_ore', 'minecraft:lapis_ore', 'minecraft:deepslate_lapis_ore', 'minecraft:redstone_ore', 'minecraft:deepslate_redstone_ore', 'minecraft:copper_ore', 'minecraft:deepslate_copper_ore', 'minecraft:coal_ore', 'minecraft:deepslate_coal_ore', 'minecraft:ancient_debris', ]); const directions = [ { check: 'turtle.inspect()', dig: 'turtle.dig()' }, { check: 'turtle.inspectUp()', dig: 'turtle.digUp()' }, { check: 'turtle.inspectDown()', dig: 'turtle.digDown()' }, ]; for (const { check, dig } of directions) { if (this.cancelled) return; const result = await this.exec(` local hasBlock, data = ${check} if hasBlock then return {name = data.name} end return nil `); if (result && valuableOres.has(result.name)) { await this.exec(dig); this.turtle.emit('blockMined', { blockType: result.name }); yield; } } } /** * Advance the spiral to the next chunk position. * Standard clockwise spiral: start at center, go E, then S, W, N with increasing lengths. * Ring 0: just center * Ring 1: side lengths = 1,2,2,1 (total 6 chunks) * Ring R: 8*R chunks around the ring */ _advanceSpiral() { if (this.spiralRing === 0) { // Center done, start ring 1 going East this.spiralRing = 1; this.spiralSide = 0; this.spiralStep = 0; this.spiralChunkX = this.startChunkX + 1; this.spiralChunkZ = this.startChunkZ - (this.spiralRing - 1); return; } // Side directions: S, W, N, E const sideDirs = [ { dx: 0, dz: 1 }, // South { dx: -1, dz: 0 }, // West { dx: 0, dz: -1 }, // North { dx: 1, dz: 0 }, // East ]; this.spiralStep++; const sideLength = this.spiralRing * 2; if (this.spiralStep >= sideLength) { this.spiralStep = 0; this.spiralSide++; if (this.spiralSide >= 4) { // Done with this ring, move to next this.spiralRing++; this.spiralSide = 0; // Jump to start of new ring (one East, one North from current) this.spiralChunkX = this.startChunkX + this.spiralRing; this.spiralChunkZ = this.startChunkZ - (this.spiralRing - 1); return; } } // Move in current side direction const dir = sideDirs[this.spiralSide]; this.spiralChunkX += dir.dx; this.spiralChunkZ += dir.dz; } _isTooFar() { const pos = this.turtle.position; const home = this.turtle.homePosition; if (!pos || !home) return false; const dist = Math.abs(pos.x - home.x) + Math.abs(pos.y - home.y) + Math.abs(pos.z - home.z); return dist > this.maxDistance; } getRecoveryData() { return { ...super.getRecoveryData(), data: { maxDistance: this.maxDistance, minFuel: this.minFuel, yLevel: this.yLevel, spiralRing: this.spiralRing, spiralSide: this.spiralSide, spiralStep: this.spiralStep, spiralChunkX: this.spiralChunkX, spiralChunkZ: this.spiralChunkZ, startChunkX: this.startChunkX, startChunkZ: this.startChunkZ, exploredChunks: [...this.exploredChunks], }, }; } }