// main.js
import DeliverooClient from "./api/deliverooClient.js";
import { Me } from "./models/me.js";
import { ParcelsStore } from "./models/parcelsStore.js";
import { MapStore } from "./models/mapStore.js";
import { AgentStore } from "./models/agentStore.js";
import { ServerConfig } from "./models/serverConfig.js";
import { getPlan, executePlan } from "./PDDL/planner.js";
import { moveAndWait, moveToNearestBase, easyExplore } from "./actions/movement.js";
import { direction } from "./utils/astar.js";
import { TILE_TYPES } from "./utils/tile.js";
import { coord2Key, key2Coord } from "./utils/hashMap.js";
/**
* Handles unhandled rejections and uncaught exceptions globally.
*/
process.on('unhandledRejection', (reason, promise) => {
console.log('Unhandled Rejection at:', promise, 'reason:', reason);
});
/**
* Handles uncaught exceptions globally.
* This is a last resort to catch errors that were not handled anywhere else.
*/
process.on('uncaughtException', (err) => {
console.log('Uncaught Exception:', err);
});
/**
* Initializes the application by creating instances of necessary classes.
* @returns {Promise<Object>} - An object containing initialized instances.
* @throws {Error} - Throws an error if initialization fails.
* @description
* This function initializes the main components of the application:
* - DeliverooClient: The client to interact with the Deliveroo API.
* - Me: Represents the current player.
* - ParcelsStore: Manages the parcels in the game.
* - MapStore: Manages the game map
* - AgentStore: Manages the agents in the game.
* - ServerConfig: Holds the server configuration.
*/
async function initApp() {
const client = new DeliverooClient(true);
const me = new Me();
const parcels = new ParcelsStore();
const mapStore = new MapStore();
const agentStore = new AgentStore();
const serverConfig = new ServerConfig();
return { client, me, parcels, mapStore, agentStore, serverConfig };
}
/**
* Registers event handlers for the Deliveroo client.
* @param {Object} context - The context containing initialized instances.
* @param {DeliverooClient} context.client - The Deliveroo client instance.
* @param {Me} context.me - The current player instance.
* @param {ParcelsStore} context.parcels - The parcels store instance.
* @param {MapStore} context.mapStore - The map store instance.
* @param {AgentStore} context.agentStore - The agent store instance.
* @param {ServerConfig} context.serverConfig - The server configuration instance.
* @description
* This function sets up event listeners for various events emitted by the Deliveroo client.
* It updates the player state, map tiles, configuration, parcels sensing, and agents sensing.
* It ensures that the application reacts to changes in the game state and updates the relevant models accordingly.
* @throws {Error} - Throws an error if the client fails to register event handlers.
*/
function registerEventHandlers({ client, me, parcels, mapStore, agentStore, serverConfig }) {
client.onYou((payload, time) => {
me.update(payload, time);
});
client.onTile(({ x, y, type }) => {
mapStore.addTile({ x, y, type: parseInt(type) });
});
client.onConfig((cfg) => {
serverConfig.updateConfig(cfg);
});
client.onMap((w, h, tiles) => {
mapStore.mapSize = w;
tiles.forEach((t) => mapStore.addTile({ x: t.x, y: t.y, type: t.type }));
mapStore.calculateDistances();
mapStore.calculateSparseness(serverConfig);
});
client.onParcelsSensing((pp) => {
if (mapStore.mapSize > 0) {
parcels.updateAll(me, pp, mapStore, serverConfig);
}
});
client.onAgentsSensing((agents) => {
agents.forEach((a) => agentStore.addAgent(a, me.ms));
});
}
/**
* Waits for the application to be ready by checking if the player ID is set and the map size is greater than zero.
* @param {Object} context - The context containing initialized instances.
* @param {Me} context.me - The current player instance.
* @param {MapStore} context.mapStore - The map store instance.
* @returns {Promise<void>} - A promise that resolves when the application is ready.
* @description
* This function continuously checks if the player ID is set and the map size is greater than zero.
* It uses a polling mechanism to wait until these conditions are met before resolving the promise.
* This is useful to ensure that the application has all necessary data before proceeding with the main logic.
* @throws {Error} - Throws an error if the application is not ready within a reasonable time.
*/
async function waitForAppReady({ me, mapStore }) {
while (!me.id || mapStore.mapSize === 0) {
await new Promise((r) => setTimeout(r, 50));
}
}
/**
* Creates an onMove handler that moves the agent to a target position.
* @param {DeliverooClient} client - The Deliveroo client instance.
* @param {Me} me - The current player instance.
* @returns {function} - A function that takes target coordinates and moves the agent.
* @description
* This function returns a handler that can be used to move the agent to a specified target position.
* It calculates the direction from the current position to the target position and uses the client to emit a move action.
* It also waits for the move to complete and checks if the agent has reached the target position.
* If the move fails or the agent does not reach the target position, it logs an error and rejects the promise.
* @throws {Error} - Throws an error if the direction cannot be determined or if the move fails.
*/
function makeOnMove(client, me) {
return async (targetX, targetY) => {
const dir = direction(me, { x: targetX, y: targetY });
if (!dir) {
const msg = `Cannot find direction from ${me.x},${me.y} to ${targetX},${targetY}`;
console.warn(msg);
return Promise.reject(new Error(msg));
}
const oldX = me.x, oldY = me.y;
try {
await moveAndWait(client, me, dir);
// Wait for update
const timeout = 1000;
const startTime = Date.now();
while ((me.x === oldX && me.y === oldY) && (Date.now() - startTime < timeout)) {
await new Promise(r => setTimeout(r, 10));
}
if (me.x === oldX && me.y === oldY) {
const errMsg = `Move to ${targetX},${targetY} may have failed: still at ${me.x},${me.y}`;
console.warn(errMsg);
return Promise.reject(new Error(errMsg));
}
if (me.x !== targetX || me.y !== targetY) {
const errMsg = `Expected position ${targetX},${targetY} but at ${me.x},${me.y}`;
console.warn(errMsg);
return Promise.reject(new Error(errMsg));
}
} catch (err) {
console.log("Movement error:", err);
return Promise.reject(err);
}
};
}
/**
* Creates an onPickup handler that emits a pickup action.
* @param {DeliverooClient} client - The Deliveroo client instance.
* @param {Me} me - The current player instance.
* @returns {function} - A function that emits a pickup action when called.
* @description
* This function returns a handler that can be used to emit a pickup action.
* It uses the client to emit a pickup event, which will trigger the server to handle the pickup logic.
*/
function makeOnPickup(client, me) {
return async () => {
await client.emitPickup();
};
}
/**
* Creates an onDeposit handler that emits a putdown action with the current parcels.
* @param {DeliverooClient} client - The Deliveroo client instance.
* @param {ParcelsStore} parcels - The parcels store instance.
* @param {Me} me - The current player instance.
* @returns {function} - A function that emits a putdown action with the current parcels when called.
* @description
* This function returns a handler that can be used to emit a putdown action.
* It uses the client to emit a putdown event with the current parcels and the player's ID.
* This will trigger the server to handle the deposit logic for the parcels.
* */
function makeOnDeposit(client, parcels, me) {
return async () => {
await client.emitPutdown(parcels, me.id);
};
}
/**
* Plans the next actions using dynamic agents.
* @param {MapStore} mapStore - The MapStore instance containing the current map state.
* @param {AgentStore} agentStore - The AgentStore instance containing the current agents state.
* @param {Me} me - The current player instance.
* @param {ParcelsStore} parcels - The list of parcels to be processed.
* @param {ServerConfig} serverConfig - The server configuration instance.
* @returns {Promise<Array>} - A promise that resolves to the raw plan generated by the planner.
* @description
* This function generates a plan for the current player based on the current map state, visible agents, and parcels.
* It temporarily removes agents from the map to avoid interference during planning.
* After generating the plan, it restores the agents back to their original state on the map.
* If an error occurs during planning, it logs the error and returns an empty plan.
* @throws {Error} - Throws an error if the planning process fails.
*/
async function planWithDynamicAgents(mapStore, agentStore, me, parcels, serverConfig) {
let tileMapTemp = new Map();
// Remove agents from map
for (const a of agentStore.visible(me, serverConfig)) {
let type = mapStore.setType(a, TILE_TYPES.EMPTY);
tileMapTemp.set(coord2Key(a), type);
}
let rawPlan;
try {
rawPlan = await getPlan(mapStore, parcels, me, serverConfig);
} catch (err) {
console.error("Planning error:", err);
}
// Restore tiles
for (const [key, value] of tileMapTemp) {
let tile = key2Coord(key);
mapStore.setType(tile, value);
}
return rawPlan;
}
/**
* Checks if the current player is at a base location.
* @param {Me} me - The current player instance.
* @param {MapStore} mapStore - The MapStore instance containing the current map state.
* @returns {boolean} - Returns true if the player is at a base, false otherwise.
* @description
* This function checks if the current player's coordinates match any of the base locations in the map store.
* It uses the coord2Key function to convert the player's coordinates into a key that can be checked against the map store's bases.
* @throws {Error} - Throws an error if the map store is not initialized or if the player's coordinates are invalid.
*/
function checkIfIamAtBase(me, mapStore) {
const currentKey = coord2Key({ x: me.x, y: me.y });
return mapStore.bases.has(currentKey);
}
/**
* Runs the main planning loop for the application.
* @param {Object} context - The context containing initialized instances.
* @param {DeliverooClient} context.client - The Deliveroo client instance.
* @param {Me} context.me - The current player instance.
* @param {ParcelsStore} context.parcels - The parcels store instance.
* @param {MapStore} context.mapStore - The map store instance.
* @param {AgentStore} context.agentStore - The agent store instance.
* @param {ServerConfig} context.serverConfig - The server configuration instance.
* @returns {Promise<void>} - A promise that resolves when the planning loop is complete.
* @description
* This function runs the main planning loop for the application.
* It continuously checks the game state, computes a plan using dynamic agents, and executes the plan.
* If no plan is found, it attempts to move towards the nearest base.
* It handles errors during planning and execution, ensuring that the application remains responsive and can recover from failures.
* @throws {Error} - Throws an error if the planning or execution fails.
*/
async function runPlanningLoop(context) {
const { client, me, parcels, mapStore, agentStore, serverConfig } = context;
const onMove = makeOnMove(client, me);
const onPickup = makeOnPickup(client, me);
const onDeposit = makeOnDeposit(client, parcels, me);
let isExecutingPlan = false;
while (true) {
try {
await new Promise((r) => setTimeout(r, serverConfig.clock));
if (isExecutingPlan) {
console.log("Still executing previous plan, skipping...");
continue;
}
if (!me.id || mapStore.mapSize === 0) {
continue;
}
// Compute plan
let rawPlan;
try {
rawPlan = await planWithDynamicAgents(mapStore, agentStore, me, parcels, serverConfig);
} catch (err) {
console.error("Planning error:", err);
continue;
}
if (!rawPlan || rawPlan.length === 0) {
console.log("No plan found, moving toward nearest base");
try {
if (checkIfIamAtBase(me, mapStore)) {
await easyExplore(client, me, mapStore);
} else {
await moveToNearestBase(client, me, mapStore);
}
} catch (baseErr) {
console.error("Error moving to base:", baseErr.message);
}
continue;
}
isExecutingPlan = true;
try {
console.log(`Executing plan with ${rawPlan.length} actions`);
await executePlan(rawPlan, onMove, onPickup, onDeposit);
console.log("Plan execution completed successfully");
} catch (err) {
console.error("Plan execution failed:", err.message);
console.log("Will generate new plan in next iteration");
} finally {
isExecutingPlan = false;
}
} catch (mainLoopErr) {
console.error("Main loop error:", mainLoopErr.message);
isExecutingPlan = false;
}
}
}
/**
* Main function to initialize the application and start the planning loop.
* @returns {Promise<void>} - A promise that resolves when the application is fully initialized and the planning loop is running.
* @function mainSinglePDDL
* @async
* @description
* This function initializes the application by creating instances of necessary classes, registers event handlers,
* waits for the application to be ready, and then starts the main planning loop.
* It serves as the entry point for the application, ensuring that all components are set up correctly before starting the main logic.
* @throws {Error} - Throws an error if the application initialization or planning loop fails.
*/
async function main() {
console.log("Initializing application…");
const context = await initApp();
registerEventHandlers(context);
await waitForAppReady(context);
await runPlanningLoop(context);
}
main();