diff --git a/server/states/ExploringState.js b/server/states/ExploringState.js index 9a69daf..79f1b07 100644 --- a/server/states/ExploringState.js +++ b/server/states/ExploringState.js @@ -1,6 +1,11 @@ /** - * ExploringState - Autonomous exploration to discover the world map - * Similar to mining but focused on discovering blocks rather than mining them + * 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'; @@ -9,9 +14,19 @@ export class ExploringState extends BaseState { 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.stuckCounter = 0; - this.visitedPositions = new Set(); + 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() { @@ -19,69 +34,235 @@ export class ExploringState extends BaseState { } get description() { - return `Exploring - ${this.blocksDiscovered} blocks discovered`; + return `Exploring - ${this.blocksDiscovered} blocks, ${this.chunksExplored} chunks (ring ${this.spiralRing})`; } async *act() { - console.log(`[${this.turtle.id}] Starting exploration`); + 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 checks + // Safety: check fuel const fuel = await this.checkFuel(); if (fuel !== 'unlimited' && fuel < this.minFuel) { const refueled = await this.tryRefuel(); if (!refueled) { - this.turtle.setState('goHome', { reason: 'low_fuel' }); + console.log(`[${this.turtle.id}] Low fuel, going home`); + this.turtle.setState('goHome', { reason: 'low_fuel', returnState: 'exploring', returnData: this.getRecoveryData().data }); return; } } - // Check distance + // 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; } - // Check inventory - const isFull = await this.isInventoryFull(); - if (isFull) { - this.turtle.setState('goHome', { reason: 'inventory_full', returnState: 'exploring' }); - 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++; } - // Mark position - const pos = this.turtle.position; - if (pos) { - this.visitedPositions.add(`${pos.x},${pos.y},${pos.z}`); - } + // Advance spiral to next chunk + this._advanceSpiral(); - // Comprehensive scan - const scanResult = await this.scanSurroundings(); - if (scanResult) { + 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(); - // Exploration movement - yield* this._exploreStep(); - - } catch (error) { - const isTimeout = error.message?.includes('timed out'); - if (isTimeout) { - console.warn(`[${this.turtle.id}] Exploration exec timeout, will retry next iteration`); - // Wait longer before retrying after a timeout - await this._sleep(3000); - } else { - // Non-timeout errors still propagate - throw error; + // 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; - await this._sleep(300); } } @@ -89,6 +270,13 @@ export class ExploringState extends BaseState { 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 = [ @@ -113,61 +301,54 @@ export class ExploringState extends BaseState { } } - async *_exploreStep() { - const pos = this.turtle.position; - if (!pos) return; - - // Favor horizontal movement heavily (85%) - const r = Math.random() * 100; - - if (r < 85) { - // Try to find unvisited direction - let moved = false; - - for (let i = 0; i < 4; i++) { - const fwdPos = this.turtle.getBlockPositionInDirection('forward'); - if (fwdPos && !this.visitedPositions.has(`${fwdPos.x},${fwdPos.y},${fwdPos.z}`)) { - const canMove = await this.exec('local h = turtle.inspect(); return not h'); - if (canMove) { - await this.moveForward(false); - moved = true; - this.stuckCounter = 0; - break; - } else { - // Block in the way - dig through if exploring - const success = await this.moveForward(true); - if (success) { - moved = true; - this.stuckCounter = 0; - break; - } - } - } - - await this.exec('turtle.turnRight()'); - this.turtle.facing = (this.turtle.facing + 1) % 4; - } - - if (!moved) { - this.stuckCounter++; - const success = await this.moveForward(true); - if (!success) { - await this.exec('turtle.turnRight()'); - this.turtle.facing = (this.turtle.facing + 1) % 4; - } - } - } else if (r < 93 && pos.y > 10) { - await this.moveDown(true); - } else { - await this.moveUp(true); + /** + * 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; } - if (this.stuckCounter > 6) { - await this.moveUp(true); - this.stuckCounter = 0; + // 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; + } } - yield; + // Move in current side direction + const dir = sideDirs[this.spiralSide]; + this.spiralChunkX += dir.dx; + this.spiralChunkZ += dir.dz; } _isTooFar() { @@ -184,6 +365,15 @@ export class ExploringState extends BaseState { 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], }, }; }