Compare commits

...

6 Commits

6 changed files with 1245 additions and 4 deletions

View File

@@ -53,8 +53,13 @@ export class Turtle extends EventEmitter {
this._homePosition = null;
this._facing = 0;
this._fuel = 0;
this._fuelLimit = 100000;
this._inventory = {};
this._selectedSlot = 1;
this._label = null;
this._peripherals = {}; // { side: { types: [...], data: {...} } }
this._error = null;
this._warning = null;
// State machine
this._state = null;
@@ -127,6 +132,49 @@ export class Turtle extends EventEmitter {
this._emitUpdate();
}
get peripherals() {
return this._peripherals;
}
set peripherals(value) {
this._peripherals = value;
this._emitUpdate();
}
get selectedSlot() {
return this._selectedSlot;
}
set selectedSlot(value) {
this._selectedSlot = value;
}
get fuelLimit() {
return this._fuelLimit;
}
set fuelLimit(value) {
this._fuelLimit = value;
}
get error() {
return this._error;
}
set error(value) {
this._error = value;
if (value) this._emitUpdate();
}
get warning() {
return this._warning;
}
set warning(value) {
this._warning = value;
if (value) this._emitUpdate();
}
get stateName() {
return this._state?.name || 'idle';
}
@@ -245,6 +293,53 @@ export class Turtle extends EventEmitter {
this.emit('sendCommand', command);
}
// ========== Real-time Event Handling ==========
/**
* Handle real-time events from the turtle (inventory, peripherals, etc.)
* These come via the webbridge as separate event messages.
*/
handleEvent(eventType, eventData) {
switch (eventType) {
case 'INVENTORY_UPDATE':
this._inventory = eventData;
this._emitUpdate();
break;
case 'PERIPHERAL_ATTACHED':
// Merge new peripherals
this._peripherals = { ...this._peripherals, ...eventData };
this._emitUpdate();
break;
case 'PERIPHERAL_DETACHED':
// eventData is the side name string
if (typeof eventData === 'string' && this._peripherals[eventData]) {
const { [eventData]: _, ...rest } = this._peripherals;
this._peripherals = rest;
this._emitUpdate();
}
break;
default:
console.log(`[Turtle ${this.id}] Unknown event type: ${eventType}`);
}
}
/**
* Check if turtle has a peripheral with the given type name
*/
hasPeripheral(typeName) {
return Object.values(this._peripherals).some(p =>
p.types && p.types.includes(typeName)
);
}
/**
* Get the chunk coordinates for this turtle's position
*/
get chunk() {
if (!this._position) return null;
return [Math.floor(this._position.x / 16), Math.floor(this._position.z / 16)];
}
/**
* Handle a response from the turtle (matched by UUID)
*/

View File

@@ -145,6 +145,25 @@ export function initializeDatabase() {
)
`);
// Chunk analysis table (ore density per chunk)
db.exec(`
CREATE TABLE IF NOT EXISTS chunks (
x INTEGER NOT NULL,
z INTEGER NOT NULL,
analysis TEXT DEFAULT '{}',
scanned_at INTEGER NOT NULL,
PRIMARY KEY (x, z)
)
`);
// Add block_state and block_tags columns to world_blocks if not present
try {
db.exec(`ALTER TABLE world_blocks ADD COLUMN block_state TEXT DEFAULT '{}'`);
} catch (e) { /* column already exists */ }
try {
db.exec(`ALTER TABLE world_blocks ADD COLUMN block_tags TEXT DEFAULT '{}'`);
} catch (e) { /* column already exists */ }
// Create indexes for better performance
db.exec(`
CREATE INDEX IF NOT EXISTS idx_world_blocks_discovered
@@ -156,6 +175,16 @@ export function initializeDatabase() {
ON task_queue(status, priority DESC);
`);
db.exec(`
CREATE INDEX IF NOT EXISTS idx_world_blocks_name
ON world_blocks(block_name);
`);
db.exec(`
CREATE INDEX IF NOT EXISTS idx_chunks_coords
ON chunks(x, z);
`);
console.log('✅ Database initialized');
}
@@ -203,12 +232,14 @@ export function getTurtleConfig(turtleId) {
}
// World Blocks
export function saveWorldBlock(x, y, z, blockName, metadata, discoveredBy) {
export function saveWorldBlock(x, y, z, blockName, metadata, discoveredBy, blockState = null, blockTags = null) {
const stmt = db.prepare(`
INSERT OR REPLACE INTO world_blocks (x, y, z, block_name, metadata, discovered_by, discovered_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
INSERT OR REPLACE INTO world_blocks (x, y, z, block_name, metadata, discovered_by, discovered_at, block_state, block_tags)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
stmt.run(x, y, z, blockName, metadata || 0, discoveredBy, Date.now());
stmt.run(x, y, z, blockName, metadata || 0, discoveredBy, Date.now(),
blockState ? JSON.stringify(blockState) : '{}',
blockTags ? JSON.stringify(blockTags) : '{}');
}
export function getWorldBlocks(limit = 10000) {
@@ -545,5 +576,82 @@ export function closeDatabase() {
db.close();
}
// ========== CHUNK ANALYSIS ==========
export function saveChunkAnalysis(x, z, analysis) {
const stmt = db.prepare(`
INSERT OR REPLACE INTO chunks (x, z, analysis, scanned_at)
VALUES (?, ?, ?, ?)
`);
stmt.run(x, z, JSON.stringify(analysis), Date.now());
}
export function getChunkAnalysis(x, z) {
const stmt = db.prepare('SELECT * FROM chunks WHERE x = ? AND z = ?');
const row = stmt.get(x, z);
if (row) {
return { ...row, analysis: JSON.parse(row.analysis) };
}
return null;
}
export function getAllChunkAnalyses() {
const stmt = db.prepare('SELECT * FROM chunks');
return stmt.all().map(row => ({
...row,
analysis: JSON.parse(row.analysis)
}));
}
// ========== BLOCK SEARCH ==========
export function getBlocksWithNameLike(fromX, fromY, fromZ, toX, toY, toZ, namePattern) {
const stmt = db.prepare(`
SELECT x, y, z, block_name as name, metadata, block_state, block_tags
FROM world_blocks
WHERE x BETWEEN ? AND ?
AND y BETWEEN ? AND ?
AND z BETWEEN ? AND ?
AND block_name LIKE ?
`);
return stmt.all(
Math.min(fromX, toX), Math.max(fromX, toX),
Math.min(fromY, toY), Math.max(fromY, toY),
Math.min(fromZ, toZ), Math.max(fromZ, toZ),
namePattern
).map(row => ({
...row,
state: row.block_state ? JSON.parse(row.block_state) : {},
tags: row.block_tags ? JSON.parse(row.block_tags) : {},
}));
}
export function getBlock(x, y, z) {
const stmt = db.prepare('SELECT block_name as name, metadata, block_state, block_tags FROM world_blocks WHERE x = ? AND y = ? AND z = ?');
const row = stmt.get(x, y, z);
if (row) {
return {
...row,
state: row.block_state ? JSON.parse(row.block_state) : {},
tags: row.block_tags ? JSON.parse(row.block_tags) : {},
};
}
return null;
}
export function deleteBlocksInArea(fromX, fromY, fromZ, toX, toY, toZ) {
const stmt = db.prepare(`
DELETE FROM world_blocks
WHERE x BETWEEN ? AND ?
AND y BETWEEN ? AND ?
AND z BETWEEN ? AND ?
`);
return stmt.run(
Math.min(fromX, toX), Math.max(fromX, toX),
Math.min(fromY, toY), Math.max(fromY, toY),
Math.min(fromZ, toZ), Math.max(fromZ, toZ)
);
}
// Export database instance for custom queries if needed
export { db };

View File

@@ -0,0 +1,222 @@
/**
* AutocraftState - Automated crafting using a workbench peripheral
*
* The turtle must have a crafting table equipped as a peripheral.
* Given a recipe (16-slot inventory layout), it will:
* 1. Verify workbench peripheral is present
* 2. Arrange items in the correct slots
* 3. Pull materials from adjacent inventories
* 4. Craft continuously
* 5. Push crafted items into adjacent inventories
*/
import { BaseState } from './BaseState.js';
export class AutocraftState extends BaseState {
constructor(turtle, data = {}) {
super(turtle, data);
// recipe: { 1: {name, count}, 2: null, 3: {name, count}, ... } (1-16 slots)
this.recipe = data.recipe || {};
this.craftCount = 0;
this.totalCrafted = 0;
this.maxCrafts = data.maxCrafts || Infinity;
this.warning = null;
}
get name() {
return 'autocrafting';
}
get description() {
if (this.warning) return `Crafting - ${this.warning}`;
return `Crafting - ${this.totalCrafted} items crafted`;
}
async *act() {
console.log(`[${this.turtle.id}] Starting autocraft`);
// Verify workbench peripheral
const hasWorkbench = await this.exec(`
local names = peripheral.getNames()
for _, name in ipairs(names) do
local types = {peripheral.getType(name)}
for _, t in ipairs(types) do
if t == "workbench" then return true end
end
end
return false
`);
if (!hasWorkbench) {
console.log(`[${this.turtle.id}] No workbench peripheral found`);
this.warning = 'No crafting table equipped';
await this._sleep(3000);
this.turtle.setState('idle');
return;
}
yield;
// Initial craft to verify recipe works
const initialCraft = await this.exec(`
local workbench = peripheral.find("workbench")
if workbench then
local ok, msg = workbench.craft(0)
return ok
end
return false
`);
if (!initialCraft) {
console.log(`[${this.turtle.id}] Recipe validation failed`);
this.warning = 'Invalid recipe';
await this._sleep(3000);
this.turtle.setState('idle');
return;
}
yield;
// Main crafting loop
while (!this.cancelled && this.totalCrafted < this.maxCrafts) {
this.warning = null;
let recipeReady = true;
// Arrange items for recipe
for (let slot = 1; slot <= 16; slot++) {
if (this.cancelled) return;
const recipeItem = this.recipe[slot];
// Get current item in slot
const currentItem = await this.exec(`
local item = turtle.getItemDetail(${slot})
if item then return {name=item.name, count=item.count} end
return nil
`);
yield;
if (!recipeItem) {
// Slot should be empty - move item out if present
if (currentItem) {
await this._transferSlotOut(slot);
yield;
}
continue;
}
// Slot needs a specific item
if (currentItem) {
if (currentItem.name === recipeItem.name) {
continue; // Already has correct item
}
// Wrong item - move it out
await this._transferSlotOut(slot);
yield;
}
// Pull the required item into this slot
const pulled = await this._pullItemToSlot(recipeItem.name, slot);
yield;
if (!pulled) {
this.warning = `Missing: ${recipeItem.name.replace('minecraft:', '')}`;
recipeReady = false;
break;
}
}
if (!recipeReady) {
yield;
await this._sleep(5000);
yield;
continue;
}
// Craft!
const craftResult = await this.exec(`
local workbench = peripheral.find("workbench")
if workbench then
local ok, msg = workbench.craft()
if ok then return true
else return false
end
end
return false
`);
if (craftResult) {
this.totalCrafted++;
this.craftCount++;
console.log(`[${this.turtle.id}] Crafted item #${this.totalCrafted}`);
// Push crafted items out to adjacent inventory
await this._pushCraftedItems();
yield;
} else {
this.warning = 'Craft failed';
await this._sleep(2000);
}
yield;
await this._sleep(500);
}
console.log(`[${this.turtle.id}] Autocraft finished: ${this.totalCrafted} total`);
this.turtle.setState('idle');
}
async _transferSlotOut(slot) {
// Try to drop the item in the given slot into an adjacent inventory
await this.exec(`
turtle.select(${slot})
if not turtle.dropDown() then
if not turtle.dropUp() then
turtle.drop()
end
end
turtle.select(1)
`);
}
async _pullItemToSlot(itemName, targetSlot) {
// Try to pull item from adjacent inventories
// First try suckDown, then suckUp, then suck
const result = await this.exec(`
turtle.select(${targetSlot})
-- Try each direction
for _, suckFn in ipairs({turtle.suckDown, turtle.suckUp, turtle.suck}) do
suckFn(1)
local item = turtle.getItemDetail(${targetSlot})
if item and item.name == "${itemName}" then
return true
elseif item then
-- Wrong item, put it back
for _, dropFn in ipairs({turtle.dropDown, turtle.dropUp, turtle.drop}) do
if dropFn() then break end
end
end
end
turtle.select(1)
return false
`);
return result === true;
}
async _pushCraftedItems() {
// Push all items out of inventory
await this.exec(`
for slot = 1, 16 do
local item = turtle.getItemDetail(slot)
if item then
turtle.select(slot)
if not turtle.dropDown() then
if not turtle.dropUp() then
turtle.drop()
end
end
end
end
turtle.select(1)
`);
}
}

View File

@@ -0,0 +1,311 @@
/**
* BuildingState - Blueprint-based autonomous block placement
*
* Given a list of blocks (x, y, z, name, state), the turtle will:
* 1. Calculate required materials
* 2. Go home to collect materials from nearby inventories
* 3. Navigate to each block position and place it
* 4. Handle facing-aware placement (stairs, logs, etc.)
* 5. Track failed placements and retry later
*/
import { BaseState } from './BaseState.js';
export class BuildingState extends BaseState {
constructor(turtle, data = {}) {
super(turtle, data);
// blocks: [{x, y, z, name, state?}]
this.blocks = data.blocks || [];
this.placedCount = 0;
this.failedPlacements = new Map(); // "x,y,z" -> retry count
this.maxRetries = 3;
// Group blocks by column (x,z) for efficient placement
this.columns = new Map(); // "x,z" -> [{x,y,z,name,state}] sorted by y ascending
this._organizeBlocks();
}
get name() {
return 'building';
}
get description() {
return `Building - ${this.placedCount}/${this.blocks.length} placed`;
}
_organizeBlocks() {
// Filter out blocks that might already be placed (we can't check DB here,
// but we organize for efficient traversal)
for (const block of this.blocks) {
const key = `${block.x},${block.z}`;
if (!this.columns.has(key)) {
this.columns.set(key, []);
}
this.columns.get(key).push(block);
}
// Sort each column by Y ascending (place from bottom up)
for (const [key, column] of this.columns) {
column.sort((a, b) => a.y - b.y);
}
}
async *act() {
console.log(`[${this.turtle.id}] Starting build: ${this.blocks.length} blocks to place`);
while (!this.cancelled) {
// Check if we have any blocks left to place
const remainingBlocks = this._getRemainingBlocks();
if (remainingBlocks.length === 0) {
console.log(`[${this.turtle.id}] Build complete: ${this.placedCount} blocks placed`);
this.turtle.setState('idle');
return;
}
// Safety checks
const fuel = await this.checkFuel();
if (fuel !== 'unlimited' && fuel < 500) {
const refueled = await this.tryRefuel();
if (!refueled) {
console.log(`[${this.turtle.id}] Low fuel during build, going home`);
this.turtle.setState('goHome', { reason: 'low_fuel', returnState: 'building', returnData: this.data });
return;
}
}
yield;
// Calculate required materials
const requiredMaterials = this._calculateMaterials(remainingBlocks);
// Check if we have the materials
const inventory = await this.getInventory();
const hasMaterials = this._hasRequiredMaterials(requiredMaterials, inventory);
yield;
if (!hasMaterials) {
// Go home to collect materials
const home = this.turtle.homePosition;
if (!home) {
console.log(`[${this.turtle.id}] No home set, cannot collect materials`);
this.turtle.setState('idle');
return;
}
console.log(`[${this.turtle.id}] Going home to collect building materials`);
// Navigate home
const reachedHome = yield* this.navigateTo(home);
if (!reachedHome) {
console.log(`[${this.turtle.id}] Cannot reach home`);
await this._sleep(5000);
yield;
continue;
}
yield;
// Dump current inventory into nearby chests
await this._dumpInventory();
yield;
// Try to pull required materials
const gotMaterials = await this._pullMaterials(requiredMaterials);
yield;
if (!gotMaterials) {
console.log(`[${this.turtle.id}] Cannot get required materials, waiting...`);
await this._sleep(10000);
yield;
continue;
}
}
// Find the next block to place (closest to turtle)
const pos = this.turtle.position;
if (!pos) {
await this._sleep(1000);
yield;
continue;
}
const nextBlock = this._findClosestBlock(remainingBlocks, pos);
if (!nextBlock) {
await this._sleep(1000);
yield;
continue;
}
// Navigate to one block above the target (to place down)
const placePos = { x: nextBlock.x, y: nextBlock.y + 1, z: nextBlock.z };
const reached = yield* this.navigateTo(placePos);
yield;
if (!reached) {
// Mark as failed
const key = `${nextBlock.x},${nextBlock.y},${nextBlock.z}`;
const retries = (this.failedPlacements.get(key) || 0) + 1;
this.failedPlacements.set(key, retries);
if (retries >= this.maxRetries) {
console.log(`[${this.turtle.id}] Cannot reach ${key}, skipping permanently`);
}
yield;
continue;
}
// Handle facing - turn turtle to face correct direction before placing
if (nextBlock.state && nextBlock.state.facing) {
await this._turnForPlacement(nextBlock.state.facing);
yield;
}
// Select the correct item
const selected = await this._selectItem(nextBlock.name);
if (!selected) {
console.log(`[${this.turtle.id}] Don't have ${nextBlock.name} to place`);
yield;
continue;
}
// Place the block
const placed = await this.exec('return turtle.placeDown()');
yield;
if (placed) {
this.placedCount++;
// Remove from our list
this._markBlockPlaced(nextBlock);
console.log(`[${this.turtle.id}] Placed ${nextBlock.name} at ${nextBlock.x},${nextBlock.y},${nextBlock.z} (${this.placedCount}/${this.blocks.length})`);
} else {
const key = `${nextBlock.x},${nextBlock.y},${nextBlock.z}`;
const retries = (this.failedPlacements.get(key) || 0) + 1;
this.failedPlacements.set(key, retries);
}
yield;
await this._sleep(100);
}
}
_getRemainingBlocks() {
const remaining = [];
for (const [key, column] of this.columns) {
for (const block of column) {
const blockKey = `${block.x},${block.y},${block.z}`;
const retries = this.failedPlacements.get(blockKey) || 0;
if (retries < this.maxRetries) {
remaining.push(block);
}
}
}
return remaining;
}
_calculateMaterials(blocks) {
const materials = new Map(); // name -> count
for (const block of blocks) {
materials.set(block.name, (materials.get(block.name) || 0) + 1);
}
return materials;
}
_hasRequiredMaterials(required, inventory) {
if (!inventory) return false;
const available = new Map();
for (const [slot, item] of Object.entries(inventory)) {
if (item && item.name) {
available.set(item.name, (available.get(item.name) || 0) + item.count);
}
}
for (const [name, count] of required) {
if ((available.get(name) || 0) < Math.min(count, 1)) {
return false; // Need at least 1 of each material
}
}
return true;
}
_findClosestBlock(blocks, from) {
let closest = null;
let minDist = Infinity;
for (const block of blocks) {
const dist = Math.abs(block.x - from.x) + Math.abs(block.y - from.y) + Math.abs(block.z - from.z);
if (dist < minDist) {
minDist = dist;
closest = block;
}
}
return closest;
}
_markBlockPlaced(block) {
const key = `${block.x},${block.z}`;
const column = this.columns.get(key);
if (column) {
const idx = column.findIndex(b => b.x === block.x && b.y === block.y && b.z === block.z);
if (idx >= 0) column.splice(idx, 1);
if (column.length === 0) this.columns.delete(key);
}
}
async _turnForPlacement(facing) {
// When placing blocks with facing, the turtle should face the opposite direction
// because placed blocks face toward the placer
const facingMap = { north: 2, south: 0, east: 3, west: 1 };
const targetFacing = facingMap[facing];
if (targetFacing !== undefined) {
await this.turnToFace(targetFacing);
}
}
async _selectItem(blockName) {
const result = await this.exec(`
for slot = 1, 16 do
local item = turtle.getItemDetail(slot)
if item and item.name == "${blockName}" then
turtle.select(slot)
return true
end
end
return false
`);
return result === true;
}
async _dumpInventory() {
await this.exec(`
for slot = 1, 16 do
local item = turtle.getItemDetail(slot)
if item then
turtle.select(slot)
turtle.dropDown()
end
end
turtle.select(1)
`);
}
async _pullMaterials(required) {
// Try to suck items from chests below (at home position)
let gotAny = false;
for (const [name, count] of required) {
const result = await this.exec(`
local needed = "${name}"
for slot = 1, 16 do
if turtle.getItemCount(slot) == 0 then
turtle.select(slot)
turtle.suckDown(64)
local item = turtle.getItemDetail(slot)
if item and item.name == needed then
return true
elseif item then
turtle.dropDown()
end
end
end
turtle.select(1)
return false
`);
if (result) gotAny = true;
}
return gotAny;
}
}

View File

@@ -0,0 +1,321 @@
/**
* ExtractionState - Smart ore extraction in a defined area
*
* Uses scanner peripherals to find ores, navigates to them,
* mines them, and returns home when full or low on fuel.
* Much more targeted than basic MiningState.
*/
import { BaseState } from './BaseState.js';
const ORE_BLOCKS = new Set([
'minecraft:coal_ore', 'minecraft:iron_ore', 'minecraft:gold_ore',
'minecraft:diamond_ore', 'minecraft:emerald_ore', 'minecraft:redstone_ore',
'minecraft:lapis_ore', 'minecraft:copper_ore',
'minecraft:deepslate_coal_ore', 'minecraft:deepslate_iron_ore',
'minecraft:deepslate_gold_ore', 'minecraft:deepslate_diamond_ore',
'minecraft:deepslate_emerald_ore', 'minecraft:deepslate_redstone_ore',
'minecraft:deepslate_lapis_ore', 'minecraft:deepslate_copper_ore',
'minecraft:nether_gold_ore', 'minecraft:nether_quartz_ore',
'minecraft:ancient_debris',
]);
export class ExtractionState extends BaseState {
constructor(turtle, data = {}) {
super(turtle, data);
// Area bounds for extraction
this.area = data.area || []; // [{x,y,z}, ...] coordinates to scan/mine
this.minY = data.minY ?? -64;
this.maxY = data.maxY ?? 320;
this.oreTargets = []; // Known ore positions to mine
this.scanPositions = []; // Positions still needing scanning
this.remainingPositions = []; // Ore positions still to mine
this.blocksMined = 0;
this.oresExtracted = 0;
this.scannerType = null;
this.hasInitialized = false;
// If area not specified, generate from bounds
if (this.area.length === 0 && data.bounds) {
this._generateAreaFromBounds(data.bounds);
}
}
get name() {
return 'extracting';
}
get description() {
return `Extracting - ${this.oresExtracted} ores, ${this.remainingPositions.length} remaining`;
}
_generateAreaFromBounds(bounds) {
const { minX, minY, minZ, maxX, maxY, maxZ } = bounds;
for (let x = minX; x <= maxX; x++) {
for (let y = minY; y <= maxY; y++) {
for (let z = minZ; z <= maxZ; z++) {
this.area.push({ x, y, z });
}
}
}
}
async *act() {
console.log(`[${this.turtle.id}] Starting extraction (${this.area.length} area blocks)`);
// Detect scanner
this.scannerType = await this._detectScanner();
yield;
// Initialize: query DB for known ores in area, compute scan positions
if (!this.hasInitialized) {
await this._initializeScanPlan();
this.hasInitialized = true;
}
yield;
while (!this.cancelled) {
// Done if no more scan positions and no remaining ore positions
if (this.scanPositions.length === 0 && this.remainingPositions.length === 0) {
console.log(`[${this.turtle.id}] Extraction complete: ${this.oresExtracted} ores extracted`);
this.turtle.setState('idle');
return;
}
// Safety: check fuel
const fuel = await this.checkFuel();
if (fuel !== 'unlimited' && fuel < 500) {
const refueled = await this.tryRefuel();
if (!refueled) {
console.log(`[${this.turtle.id}] Low fuel, going home`);
this.turtle.setState('goHome', { reason: 'low_fuel', returnState: 'extracting', returnData: this.data });
return;
}
}
yield;
// Check inventory fullness
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: 'extracting', returnData: this.data });
return;
}
yield;
const pos = this.turtle.position;
if (!pos) {
await this._sleep(1000);
yield;
continue;
}
// Priority 1: Navigate to scan positions and scan
if (this.scanPositions.length > 0) {
const scanTarget = this._findClosest(this.scanPositions, pos);
if (scanTarget) {
// Navigate to scan position
const success = yield* this.navigateTo(scanTarget);
if (success) {
// Perform scan
const scannedBlocks = await this._performScan();
yield;
// Process results - find ores
if (scannedBlocks) {
for (const block of scannedBlocks) {
if (this._isOre(block.name)) {
this.remainingPositions.push({ x: block.x, y: block.y, z: block.z });
}
}
}
// Remove this scan position
const idx = this.scanPositions.findIndex(p => p.x === scanTarget.x && p.y === scanTarget.y && p.z === scanTarget.z);
if (idx >= 0) this.scanPositions.splice(idx, 1);
}
}
yield;
continue;
}
// Priority 2: Navigate to and mine ore positions
if (this.remainingPositions.length > 0) {
const oreTarget = this._findClosest(this.remainingPositions, pos);
if (oreTarget) {
// Navigate adjacent to ore
const success = yield* this.navigateTo(oreTarget);
yield;
if (success) {
// Mine the ore (we should be at or adjacent to it)
await this._mineAt(oreTarget);
this.oresExtracted++;
}
// Remove from remaining
const idx = this.remainingPositions.findIndex(p => p.x === oreTarget.x && p.y === oreTarget.y && p.z === oreTarget.z);
if (idx >= 0) this.remainingPositions.splice(idx, 1);
}
yield;
}
await this._sleep(200);
yield;
}
}
async _detectScanner() {
const result = await this.exec(`
local names = peripheral.getNames()
for _, name in ipairs(names) do
local types = {peripheral.getType(name)}
for _, t in ipairs(types) do
if t == "geoScanner" then return "geoScanner"
elseif t == "universal_scanner" then return "universal_scanner"
elseif t == "plethora:scanner" then return "plethora:scanner"
end
end
end
return nil
`);
return result || 'native';
}
async _initializeScanPlan() {
// Query the DB for known ores in the area
if (this.area.length > 0) {
const bounds = this._getBoundingBox();
try {
const knownOres = this.turtle.server.db.getBlocksWithNameLike(
bounds.minX, bounds.minY, bounds.minZ,
bounds.maxX, bounds.maxY, bounds.maxZ,
'%_ore'
);
for (const ore of knownOres) {
this.remainingPositions.push({ x: ore.x, y: ore.y, z: ore.z });
}
} catch (e) {
console.log(`[${this.turtle.id}] DB ore query failed:`, e.message);
}
}
// Compute scan positions (evenly spaced grid across area)
if (this.scannerType !== 'native' && this.area.length > 0) {
const bounds = this._getBoundingBox();
const scanSpacing = this.scannerType === 'plethora:scanner' ? 16 : 32;
for (let x = bounds.minX; x <= bounds.maxX; x += scanSpacing) {
for (let z = bounds.minZ; z <= bounds.maxZ; z += scanSpacing) {
const y = Math.floor((bounds.minY + bounds.maxY) / 2);
this.scanPositions.push({ x, y, z });
}
}
}
}
_getBoundingBox() {
let minX = Infinity, minY = Infinity, minZ = Infinity;
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
for (const p of this.area) {
minX = Math.min(minX, p.x); maxX = Math.max(maxX, p.x);
minY = Math.min(minY, p.y); maxY = Math.max(maxY, p.y);
minZ = Math.min(minZ, p.z); maxZ = Math.max(maxZ, p.z);
}
return { minX, minY, minZ, maxX, maxY, maxZ };
}
_findClosest(positions, from) {
let closest = null;
let minDist = Infinity;
for (const pos of positions) {
const dist = Math.abs(pos.x - from.x) + Math.abs(pos.y - from.y) + Math.abs(pos.z - from.z);
if (dist < minDist) {
minDist = dist;
closest = pos;
}
}
return closest;
}
_isOre(name) {
return ORE_BLOCKS.has(name) || (name && name.endsWith('_ore'));
}
async _performScan() {
const pos = this.turtle.position;
if (!pos) return null;
let rawBlocks = null;
if (this.scannerType === 'geoScanner') {
rawBlocks = await this.exec(`
local scanner = peripheral.find("geoScanner")
if scanner then
local ok, blocks = pcall(scanner.scan, 16)
if ok then return blocks end
end
return nil
`);
} else if (this.scannerType === 'universal_scanner') {
rawBlocks = await this.exec(`
local scanner = peripheral.find("universal_scanner")
if scanner then
local ok, blocks = pcall(scanner.scan, "block", 16)
if ok then return blocks end
end
return nil
`);
} else if (this.scannerType === 'plethora:scanner') {
rawBlocks = await this.exec(`
local scanner = peripheral.find("plethora:scanner")
if scanner then
local ok, blocks = pcall(scanner.scan)
if ok then return blocks end
end
return nil
`);
}
if (!rawBlocks || !Array.isArray(rawBlocks)) return null;
// Convert relative positions to absolute
return rawBlocks
.filter(b => b && b.name && b.name !== 'minecraft:air')
.map(b => ({
x: pos.x + (b.x || 0),
y: pos.y + (b.y || 0),
z: pos.z + (b.z || 0),
name: b.name,
}));
}
async _mineAt(target) {
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;
// We should be at or adjacent - mine in appropriate direction
if (dy === 1 || (dy === 0 && dx === 0 && dz === 0)) {
await this.exec('turtle.digUp()');
} else if (dy === -1) {
await this.exec('turtle.digDown()');
} else {
// Face the block and dig
let targetFacing;
if (dx === 1) targetFacing = 1;
else if (dx === -1) targetFacing = 3;
else if (dz === 1) targetFacing = 2;
else if (dz === -1) targetFacing = 0;
else targetFacing = this.turtle.facing;
await this.turnToFace(targetFacing);
await this.exec('turtle.dig()');
}
this.blocksMined++;
this.turtle.emit('blockMined', { blockType: 'ore' });
}
}

184
server/states/ScanState.js Normal file
View File

@@ -0,0 +1,184 @@
/**
* ScanState - Systematic scanning of surroundings using peripheral scanners
*
* Supports multiple scanner types:
* - geoScanner (Advanced Peripherals)
* - universal_scanner
* - plethora:scanner (Plethora mod)
* - Native turtle.inspect() fallback (6-directional scan)
*/
import { BaseState } from './BaseState.js';
export class ScanState extends BaseState {
constructor(turtle, data = {}) {
super(turtle, data);
this.scanRadius = data.scanRadius || 16;
this.blocksScanned = 0;
this.scannerType = null;
}
get name() {
return 'scanning';
}
get description() {
return `Scanning (${this.scannerType || 'detecting'}) - ${this.blocksScanned} blocks`;
}
async *act() {
console.log(`[${this.turtle.id}] Starting scan`);
// Detect available scanner peripheral
this.scannerType = await this._detectScanner();
yield;
if (this.scannerType === 'geoScanner') {
yield* this._geoScan();
} else if (this.scannerType === 'universal_scanner') {
yield* this._universalScan();
} else if (this.scannerType === 'plethora:scanner') {
yield* this._plethuraScan();
} else {
// Fallback to native inspect-based scanning
yield* this._nativeScan();
}
console.log(`[${this.turtle.id}] Scan complete: ${this.blocksScanned} blocks`);
this.turtle.setState('idle');
}
async _detectScanner() {
// Check for geoScanner
const geoResult = await this.exec(`
local names = peripheral.getNames()
for _, name in ipairs(names) do
local types = {peripheral.getType(name)}
for _, t in ipairs(types) do
if t == "geoScanner" then return "geoScanner" end
end
end
return nil
`);
if (geoResult) return geoResult;
// Check for universal_scanner
const uniResult = await this.exec(`
local names = peripheral.getNames()
for _, name in ipairs(names) do
local types = {peripheral.getType(name)}
for _, t in ipairs(types) do
if t == "universal_scanner" then return "universal_scanner" end
end
end
return nil
`);
if (uniResult) return uniResult;
// Check for plethora scanner
const plResult = await this.exec(`
local names = peripheral.getNames()
for _, name in ipairs(names) do
local types = {peripheral.getType(name)}
for _, t in ipairs(types) do
if t == "plethora:scanner" then return "plethora:scanner" end
end
end
return nil
`);
if (plResult) return plResult;
return 'native';
}
async *_geoScan() {
// Wait for cooldown
yield;
await this._sleep(500);
const result = await this.exec(`
local scanner = peripheral.find("geoScanner")
if not scanner then return nil end
local ok, blocks = pcall(scanner.scan, ${this.scanRadius})
if ok then return blocks end
return nil
`);
if (result && Array.isArray(result)) {
this._processScannedBlocks(result);
}
yield;
}
async *_universalScan() {
yield;
await this._sleep(500);
const result = await this.exec(`
local scanner = peripheral.find("universal_scanner")
if not scanner then return nil end
local ok, blocks = pcall(scanner.scan, "block", ${this.scanRadius})
if ok then return blocks end
return nil
`);
if (result && Array.isArray(result)) {
this._processScannedBlocks(result);
}
yield;
}
async *_plethuraScan() {
yield;
const result = await this.exec(`
local scanner = peripheral.find("plethora:scanner")
if not scanner then return nil end
local ok, blocks = pcall(scanner.scan)
if ok then return blocks end
return nil
`);
if (result && Array.isArray(result)) {
this._processScannedBlocks(result);
}
yield;
}
async *_nativeScan() {
// Fallback: scan all 6 directions using turtle.inspect
const scanResult = await this.scanSurroundings();
if (scanResult) {
this.blocksScanned += Object.keys(scanResult).length;
}
yield;
}
_processScannedBlocks(blocks) {
const pos = this.turtle.position;
if (!pos) return;
const discoveredBlocks = [];
for (const block of blocks) {
if (!block || !block.name) continue;
if (block.name === 'minecraft:air') continue;
if (block.x === 0 && block.y === 0 && block.z === 0) continue;
discoveredBlocks.push({
x: pos.x + (block.x || 0),
y: pos.y + (block.y || 0),
z: pos.z + (block.z || 0),
name: block.name,
metadata: block.metadata || 0,
state: block.state || {},
tags: block.tags || {},
discoveredBy: this.turtle.id,
});
}
this.blocksScanned = discoveredBlocks.length;
if (discoveredBlocks.length > 0) {
this.turtle.emit('blocksDiscovered', discoveredBlocks);
}
}
}