feat: Enhance turtle instance management with improved state handling and event forwarding

This commit is contained in:
MayaTheShy
2026-02-20 01:54:15 -05:00
parent f8baadce3a
commit 836e7b61c7

View File

@@ -3,13 +3,14 @@ import { WebSocketServer } from 'ws';
import cors from 'cors';
import { createServer } from 'http';
import * as db from './database.js';
import { Turtle } from './Turtle.js';
const app = express();
const PORT = 3001;
const WS_PORT = 3002;
app.use(cors());
app.use(express.json());
app.use(express.json({ limit: '5mb' }));
// Initialize database
db.initializeDatabase();
@@ -23,11 +24,23 @@ console.log(` Loaded ${savedBlocks.length} world blocks`);
// Store connected web clients and turtle data
const webClients = new Set();
const turtleData = new Map(); // turtleID -> turtle state
const turtles = new Map(); // turtleID -> Turtle instance
const worldBlocks = new Map(); // "x,y,z" -> {name, metadata, discoveredBy, timestamp}
const turtleHomes = new Map(); // turtleID -> {x, y, z} home position
const turtleConfig = new Map(); // turtleID -> {maxDistance, facing, etc}
// Legacy compat: turtleData getter (returns serialized Turtle data)
const turtleData = {
has(id) { return turtles.has(id); },
get(id) { const t = turtles.get(id); return t ? t.toJSON() : undefined; },
set(id, _val) { /* no-op, use getOrCreateTurtle */ },
delete(id) { return turtles.delete(id); },
get size() { return turtles.size; },
entries() { return Array.from(turtles.entries()).map(([id, t]) => [id, t.toJSON()])[Symbol.iterator](); },
values() { return Array.from(turtles.values()).map(t => t.toJSON())[Symbol.iterator](); },
keys() { return turtles.keys(); },
};
// Load saved homes into memory
for (const home of savedHomes) {
turtleHomes.set(home.turtle_id, { x: home.x, y: home.y, z: home.z });
@@ -57,22 +70,91 @@ function broadcastToClients(data) {
});
}
// ========== Turtle Instance Management ==========
/**
* Get or create a Turtle instance for the given ID.
* Wires up event handlers for the state machine and command forwarding.
*/
function getOrCreateTurtle(turtleID) {
if (turtles.has(turtleID)) {
return turtles.get(turtleID);
}
console.log(`✨ Creating Turtle instance for #${turtleID}`);
const turtle = new Turtle(turtleID, {
worldBlocks,
broadcastToClients,
storeBlock,
db,
});
// Restore home position from DB
const savedHome = turtleHomes.get(turtleID);
if (savedHome) {
turtle.homePosition = savedHome;
}
// ---- Event handlers ----
// Forward eval commands to webbridge via pending command queue
turtle.on('sendCommand', (command) => {
// Queue eval command for webbridge to poll
turtle.pendingLegacyCommands.push({
...command,
timestamp: Date.now(),
});
});
// Store discovered blocks
turtle.on('blocksDiscovered', (blocks) => {
for (const block of blocks) {
storeBlock(block.x, block.y, block.z, { name: block.name, metadata: block.metadata }, block.discoveredBy);
}
// Broadcast blocks to web clients
broadcastToClients({
type: 'blocks_discovered',
blocks: blocks.map(b => ({
x: b.x, y: b.y, z: b.z,
name: b.name,
metadata: b.metadata,
discoveredBy: b.discoveredBy,
timestamp: Date.now(),
})),
});
});
// Broadcast turtle updates to web clients
turtle.on('update', (data) => {
broadcastToClients({
type: 'turtle_update',
turtle: data,
});
});
// Handle turtle disconnect
turtle.on('disconnect', (id) => {
broadcastToClients({
type: 'turtle_removed',
turtleID: id,
});
});
turtles.set(turtleID, turtle);
return turtle;
}
// Cleanup stale turtles periodically
setInterval(() => {
const now = Date.now();
let removedCount = 0;
for (const [turtleID, turtle] of turtleData.entries()) {
for (const [turtleID, turtle] of turtles.entries()) {
if (now - turtle.lastUpdate > TURTLE_TIMEOUT) {
console.log(`🔌 Turtle ${turtleID} timed out (last seen ${Math.floor((now - turtle.lastUpdate) / 1000)}s ago)`);
turtleData.delete(turtleID);
turtle.disconnect();
turtles.delete(turtleID);
removedCount++;
// Notify web clients
broadcastToClients({
type: 'turtle_removed',
turtleID: turtleID
});
}
}
@@ -105,7 +187,6 @@ function getBlockPosition(turtlePos, facing, direction) {
} else if (direction === 'down') {
pos.y -= 1;
} else if (direction === 'forward') {
// Calculate based on facing direction
if (facing === 0) pos.z -= 1; // North
else if (facing === 1) pos.x += 1; // East
else if (facing === 2) pos.z += 1; // South