From 836e7b61c7ab1ebf4436dd934d0bc3187b3312ec Mon Sep 17 00:00:00 2001 From: MayaTheShy Date: Fri, 20 Feb 2026 01:54:15 -0500 Subject: [PATCH] feat: Enhance turtle instance management with improved state handling and event forwarding --- server/server.js | 103 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 92 insertions(+), 11 deletions(-) diff --git a/server/server.js b/server/server.js index 1c98c75..89361df 100644 --- a/server/server.js +++ b/server/server.js @@ -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