357 lines
9.6 KiB
JavaScript
357 lines
9.6 KiB
JavaScript
/**
|
|
* 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));
|
|
}
|
|
}
|