Files
remoteturtle/server/states/ExploringState.js

381 lines
12 KiB
JavaScript

/**
* 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],
},
};
}
}