refactor: enhance ExploringState for chunk-based spiral exploration and improve navigation logic
This commit is contained in:
@@ -1,6 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* ExploringState - Autonomous exploration to discover the world map
|
* ExploringState - Chunk-based spiral exploration to discover the world map
|
||||||
* Similar to mining but focused on discovering blocks rather than mining them
|
*
|
||||||
|
* 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';
|
import { BaseState } from './BaseState.js';
|
||||||
|
|
||||||
@@ -9,9 +14,19 @@ export class ExploringState extends BaseState {
|
|||||||
super(turtle, data);
|
super(turtle, data);
|
||||||
this.maxDistance = data.maxDistance || 200;
|
this.maxDistance = data.maxDistance || 200;
|
||||||
this.minFuel = data.minFuel || 500;
|
this.minFuel = data.minFuel || 500;
|
||||||
|
this.yLevel = data.yLevel || null; // null = stay at current Y
|
||||||
this.blocksDiscovered = 0;
|
this.blocksDiscovered = 0;
|
||||||
this.stuckCounter = 0;
|
this.chunksExplored = 0;
|
||||||
this.visitedPositions = new Set();
|
|
||||||
|
// 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() {
|
get name() {
|
||||||
@@ -19,69 +34,235 @@ export class ExploringState extends BaseState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get description() {
|
get description() {
|
||||||
return `Exploring - ${this.blocksDiscovered} blocks discovered`;
|
return `Exploring - ${this.blocksDiscovered} blocks, ${this.chunksExplored} chunks (ring ${this.spiralRing})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async *act() {
|
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) {
|
while (!this.cancelled) {
|
||||||
try {
|
try {
|
||||||
// Safety checks
|
// Safety: check fuel
|
||||||
const fuel = await this.checkFuel();
|
const fuel = await this.checkFuel();
|
||||||
if (fuel !== 'unlimited' && fuel < this.minFuel) {
|
if (fuel !== 'unlimited' && fuel < this.minFuel) {
|
||||||
const refueled = await this.tryRefuel();
|
const refueled = await this.tryRefuel();
|
||||||
if (!refueled) {
|
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;
|
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()) {
|
if (this._isTooFar()) {
|
||||||
|
console.log(`[${this.turtle.id}] Too far from home, returning`);
|
||||||
this.turtle.setState('goHome', { reason: 'too_far' });
|
this.turtle.setState('goHome', { reason: 'too_far' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check inventory
|
// Get the current target chunk
|
||||||
const isFull = await this.isInventoryFull();
|
const chunkKey = `${this.spiralChunkX},${this.spiralChunkZ}`;
|
||||||
if (isFull) {
|
|
||||||
this.turtle.setState('goHome', { reason: 'inventory_full', returnState: 'exploring' });
|
if (!this.exploredChunks.has(chunkKey)) {
|
||||||
return;
|
// 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
|
// Advance spiral to next chunk
|
||||||
const pos = this.turtle.position;
|
this._advanceSpiral();
|
||||||
if (pos) {
|
|
||||||
this.visitedPositions.add(`${pos.x},${pos.y},${pos.z}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Comprehensive scan
|
yield;
|
||||||
const scanResult = await this.scanSurroundings();
|
} catch (error) {
|
||||||
if (scanResult) {
|
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;
|
this.blocksDiscovered += Object.keys(scanResult).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mine any valuable ores we find
|
// Mine any valuable ores we find
|
||||||
yield* this._checkAndMineOres();
|
yield* this._checkAndMineOres();
|
||||||
|
|
||||||
// Exploration movement
|
// Move forward (digging through obstacles)
|
||||||
yield* this._exploreStep();
|
const moved = await this.moveForward(true);
|
||||||
|
if (!moved) {
|
||||||
} catch (error) {
|
// Stuck - try to go over
|
||||||
const isTimeout = error.message?.includes('timed out');
|
await this.moveUp(true);
|
||||||
if (isTimeout) {
|
const movedOver = await this.moveForward(true);
|
||||||
console.warn(`[${this.turtle.id}] Exploration exec timeout, will retry next iteration`);
|
if (movedOver) {
|
||||||
// Wait longer before retrying after a timeout
|
await this.moveDown(true);
|
||||||
await this._sleep(3000);
|
} else {
|
||||||
} else {
|
// Really stuck, skip rest of row
|
||||||
// Non-timeout errors still propagate
|
break;
|
||||||
throw error;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
yield;
|
||||||
await this._sleep(300);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +270,13 @@ export class ExploringState extends BaseState {
|
|||||||
const valuableOres = new Set([
|
const valuableOres = new Set([
|
||||||
'minecraft:diamond_ore', 'minecraft:emerald_ore',
|
'minecraft:diamond_ore', 'minecraft:emerald_ore',
|
||||||
'minecraft:deepslate_diamond_ore', 'minecraft:deepslate_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 = [
|
const directions = [
|
||||||
@@ -113,61 +301,54 @@ export class ExploringState extends BaseState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async *_exploreStep() {
|
/**
|
||||||
const pos = this.turtle.position;
|
* Advance the spiral to the next chunk position.
|
||||||
if (!pos) return;
|
* Standard clockwise spiral: start at center, go E, then S, W, N with increasing lengths.
|
||||||
|
* Ring 0: just center
|
||||||
// Favor horizontal movement heavily (85%)
|
* Ring 1: side lengths = 1,2,2,1 (total 6 chunks)
|
||||||
const r = Math.random() * 100;
|
* Ring R: 8*R chunks around the ring
|
||||||
|
*/
|
||||||
if (r < 85) {
|
_advanceSpiral() {
|
||||||
// Try to find unvisited direction
|
if (this.spiralRing === 0) {
|
||||||
let moved = false;
|
// Center done, start ring 1 going East
|
||||||
|
this.spiralRing = 1;
|
||||||
for (let i = 0; i < 4; i++) {
|
this.spiralSide = 0;
|
||||||
const fwdPos = this.turtle.getBlockPositionInDirection('forward');
|
this.spiralStep = 0;
|
||||||
if (fwdPos && !this.visitedPositions.has(`${fwdPos.x},${fwdPos.y},${fwdPos.z}`)) {
|
this.spiralChunkX = this.startChunkX + 1;
|
||||||
const canMove = await this.exec('local h = turtle.inspect(); return not h');
|
this.spiralChunkZ = this.startChunkZ - (this.spiralRing - 1);
|
||||||
if (canMove) {
|
return;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.stuckCounter > 6) {
|
// Side directions: S, W, N, E
|
||||||
await this.moveUp(true);
|
const sideDirs = [
|
||||||
this.stuckCounter = 0;
|
{ 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() {
|
_isTooFar() {
|
||||||
@@ -184,6 +365,15 @@ export class ExploringState extends BaseState {
|
|||||||
data: {
|
data: {
|
||||||
maxDistance: this.maxDistance,
|
maxDistance: this.maxDistance,
|
||||||
minFuel: this.minFuel,
|
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],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user