Skip to content

Instantly share code, notes, and snippets.

@srpiroliro
Last active November 28, 2024 21:52
Show Gist options
  • Save srpiroliro/fdbf0afdf02172786773b81c89006975 to your computer and use it in GitHub Desktop.
Save srpiroliro/fdbf0afdf02172786773b81c89006975 to your computer and use it in GitHub Desktop.
const SPEED = 0.1;
const DELAY_BETWEEN_PASSENGERS = 2000;
const log = (category, message) => {
const timestamp = new Date().toISOString().split("T")[1].slice(0, -1);
console.log(`[${timestamp}] [${category}] ${message}`);
};
const logState = (elevator) => {
const state = {
floor: elevator.getCurrentFloor(),
direction: elevator.getCurrentDirection(),
doorsOpen: elevator.doorsOpen,
isMoving: elevator.isMoving,
passengers: elevator.currentPassengers.size,
waitingUp: [...elevator.pickUpQueue.up].join(","),
waitingDown: [...elevator.pickUpQueue.down].join(","),
destinations: [...elevator.destinationQueue].join(","),
requestOrder: elevator.requestOrder
.map((r) => `Passenger #${r.id} from ${r.startFloor} to ${r.destinationFloor}`)
.join(","),
};
log(
"STATE",
`Floor: ${state.floor}, Direction: ${state.direction || "idle"}, ` +
`Doors: ${state.doorsOpen ? "open" : "closed"}, Moving: ${state.isMoving}, ` +
`Passengers: ${state.passengers}/${HardwareElevator.MAX_CAPACITY}\n` +
` Waiting Up: [${state.waitingUp}], Waiting Down: [${state.waitingDown}]\n` +
` Destinations: [${state.destinations}]\n` +
` Request Order: [${state.requestOrder}]`,
);
};
class HardwareElevator {
static MIN_FLOOR = 1;
static MAX_FLOOR = 10;
static FLOOR_TRAVEL_TIME = SPEED * 2000;
static DOOR_OPERATION_TIME = SPEED * 3000;
static MAX_CAPACITY = 4;
constructor() {
this.currentFloor = HardwareElevator.MIN_FLOOR;
this.currentDirection = null;
this.isMoving = false;
this.doorsOpen = false;
this.isOperating = false;
}
async moveUp() {
this.isMoving = true;
this.currentDirection = "up";
log("MOVEMENT", `Moving up from floor ${this.currentFloor}`);
await new Promise((resolve) =>
setTimeout(resolve, HardwareElevator.FLOOR_TRAVEL_TIME),
);
this.currentFloor++;
this.isMoving = false;
log("MOVEMENT", `Arrived at floor ${this.currentFloor}`);
}
async moveDown() {
this.isMoving = true;
this.currentDirection = "down";
log("MOVEMENT", `Moving down from floor ${this.currentFloor}`);
await new Promise((resolve) =>
setTimeout(resolve, HardwareElevator.FLOOR_TRAVEL_TIME),
);
this.currentFloor--;
this.isMoving = false;
log("MOVEMENT", `Arrived at floor ${this.currentFloor}`);
}
async stopAndOpenDoors() {
if (this.isOperating) {
log("DOORS", "Cannot operate doors - elevator is busy");
return;
}
this.isOperating = true;
log("DOORS", `Opening doors at floor ${this.currentFloor}`);
await new Promise((resolve) =>
setTimeout(resolve, HardwareElevator.DOOR_OPERATION_TIME / 2),
);
this.doorsOpen = true;
log("DOORS", "Doors fully open - waiting");
await new Promise((resolve) =>
setTimeout(resolve, HardwareElevator.DOOR_OPERATION_TIME),
);
log("DOORS", "Closing doors");
this.doorsOpen = false;
await new Promise((resolve) =>
setTimeout(resolve, HardwareElevator.DOOR_OPERATION_TIME / 2),
);
log("DOORS", "Doors fully closed");
this.isOperating = false;
}
getCurrentFloor() {
return this.currentFloor;
}
getCurrentDirection() {
return this.currentDirection;
}
hasCapacity() {
return this.currentPassengers.size < HardwareElevator.MAX_CAPACITY;
}
}
class Passenger {
constructor(id, startFloor, destinationFloor) {
if (
startFloor < HardwareElevator.MIN_FLOOR ||
startFloor > HardwareElevator.MAX_FLOOR ||
destinationFloor < HardwareElevator.MIN_FLOOR ||
destinationFloor > HardwareElevator.MAX_FLOOR
) {
throw new Error("Invalid floor number");
}
this.id = id;
this.startFloor = startFloor;
this.destinationFloor = destinationFloor;
this.state = "waiting";
this.direction = startFloor < destinationFloor ? "up" : "down";
}
}
class Elevator extends HardwareElevator {
constructor() {
super();
this.pickUpQueue = {
up: new Set(),
down: new Set(),
};
this.destinationQueue = [];
this.waitingPassengers = new Map();
this.currentPassengers = new Set();
this.requestOrder = [];
log("INIT", "Elevator system initialized");
}
async run() {
log("INIT", "Elevator system running");
while(!this._shouldWeMove()) {
await new Promise(resolve => setTimeout(resolve, 100));
}
while (this._shouldWeMove()) {
await this._move();
}
log("FINISHED", "Elevator finished");
}
async onDoorsClosed() {
// ...
}
async onBeforeFloor() {
const floor = this.getCurrentFloor();
logState(this);
if (this.isOperating) {
log("OPERATION", "Skipping floor check - elevator is busy");
return;
}
log(
"FLOOR",
`Approaching floor ${floor} going ${this.getCurrentDirection() || "idle"}`,
);
const passengersToRemove = this._identifyExitingPassengers(floor);
const shouldStop = this._shouldStopAtFloor(floor, passengersToRemove);
if (shouldStop) {
await this._handleFloorStop(floor, passengersToRemove);
}
}
async floorButtonPressed(passenger) {
if (!this.waitingPassengers.has(passenger.startFloor)) {
this.waitingPassengers.set(passenger.startFloor, new Set());
}
this.waitingPassengers.get(passenger.startFloor).add(passenger);
this.requestOrder.push(passenger);
if (!this.pickUpQueue[passenger.direction].has(passenger.startFloor)) {
this.pickUpQueue[passenger.direction].add(passenger.startFloor);
log(
"BUTTON",
`Floor ${passenger.startFloor} ${passenger.direction} button pressed`,
);
}
}
cabinButtonPressed(passenger) {
const floor = this.getCurrentFloor();
const destinationFloor = passenger.destinationFloor;
passenger.state = "inside";
this.currentPassengers.add(passenger);
this.pickUpQueue[passenger.direction].delete(floor);
this.waitingPassengers.get(floor).delete(passenger);
if (this.destinationQueue.includes(destinationFloor)) {
log("BUTTON", `Floor ${floor} already in destination queue`);
return;
}
this.destinationQueue.push(destinationFloor);
log(
"BUTTON",
`Passenger #${passenger.id} pressed cabin button for floor ${destinationFloor}`,
);
}
_identifyExitingPassengers(floor) {
const passengersToRemove = new Set();
for (const passenger of this.currentPassengers) {
if (passenger.destinationFloor === floor) {
passenger.state = "exiting";
passengersToRemove.add(passenger);
this.destinationQueue = this.destinationQueue.filter(
(f) => f !== floor,
);
log(
"PASSENGER",
`Passenger #${passenger.id} exiting at floor ${floor}`,
);
}
}
return passengersToRemove;
}
_shouldWeMove() {
const shouldMove = !!(
this.destinationQueue.length ||
this.pickUpQueue.up.size ||
this.pickUpQueue.down.size
);
log(
"DECISION",
`Should move: ${shouldMove} - ` +
`Destinations: ${this.destinationQueue.length}, ` +
`Waiting up: ${this.pickUpQueue.up.size}, ` +
`Waiting down: ${this.pickUpQueue.down.size}`,
);
return shouldMove;
}
_shouldWeMoveTo(floor) {
if (this.destinationQueue.some((f) => f === floor)) {
log("DECISION", "Destination floor already in destination queue");
return "same";
}
if (this.destinationQueue.length > 0) {
const isUp = this.destinationQueue[0] > floor;
const isDown = this.destinationQueue[0] < floor;
if (isUp && isDown) {
log(
"DECISION",
"Found both upward and downward destinations - moving up",
);
return "up";
}
if (isUp) {
log("DECISION", "Found upward destinations - moving up");
return "up";
}
if (isDown) {
log("DECISION", "Found downward destinations - moving down");
return "down";
}
log("DECISION", "Current floor");
return null;
}
if (this.currentPassengers.size === 0 && this.requestOrder.length > 0) {
const nextRequest = this.requestOrder[0];
const shouldMoveUp = nextRequest.startFloor > floor;
log(
"DECISION",
`Empty elevator following request order: going ${shouldMoveUp ? "up" : "down"} to floor ${nextRequest.startFloor}`,
);
return shouldMoveUp ? "up" : "down";
}
const hasUpwardPickups =
[...this.pickUpQueue.up].some((f) => f > floor) ||
[...this.pickUpQueue.down].some((f) => f > floor);
const hasDownwardPickups =
[...this.pickUpQueue.down].some((f) => f < floor) ||
[...this.pickUpQueue.up].some((f) => f < floor);
if (!hasUpwardPickups && !hasDownwardPickups) {
log("DECISION", "No pickups - resetting direction");
return null;
}
if (this.getCurrentDirection() === "up" && !hasUpwardPickups) {
log("DIRECTION", "No more upward requests - allowing direction change");
this.currentDirection = null;
return "down";
}
const shouldMoveUp =
(!this.getCurrentDirection() && hasUpwardPickups) ||
(this.getCurrentFloor() === HardwareElevator.MIN_FLOOR &&
hasUpwardPickups);
return shouldMoveUp ? "up" : "down";
}
async _move() {
if (this.isMoving || this.isOperating) {
log("MOVEMENT", "Cannot move - elevator is busy");
return;
}
const floor = this.getCurrentFloor();
const shouldWeMoveTo = this._shouldWeMoveTo(floor);
log("MOVEMENT", `Evaluating movement from floor ${floor}`);
switch (shouldWeMoveTo) {
case "up":
await this.moveUp();
break;
case "down":
await this.moveDown();
break;
case "same":
await this._handleFloorStop(
floor,
this._identifyExitingPassengers(floor),
);
break;
default:
this.currentDirection = null;
log(
"DIRECTION",
"No requests in current direction - resetting direction",
);
break;
}
if (shouldWeMoveTo !== null)
await this.onBeforeFloor();
}
_shouldStopAtFloor(floor, passengersToRemove) {
if (passengersToRemove.size > 0) return true;
if (this.currentPassengers.size === 0)
return this.requestOrder[0]?.startFloor === floor;
const direction = this.getCurrentDirection();
return (
this.pickUpQueue[direction]?.size &&
this.pickUpQueue[direction].has(floor)
);
}
async _handleFloorStop(floor, passengersToRemove) {
log("DECISION", `Stopping at floor ${floor}`);
await this.stopAndOpenDoors();
if (passengersToRemove.size > 0) {
for (const passenger of passengersToRemove) {
this.currentPassengers.delete(passenger);
passenger.state = "arrived";
log(
"PASSENGER",
`Passenger #${passenger.id} has arrived at destination ${floor}`,
);
}
this.requestOrder = this.requestOrder.filter(
(r) => !passengersToRemove.has(r),
);
}
const waitingAtFloor = this.waitingPassengers.get(floor) || new Set();
const eligiblePassengers = new Set();
const direction = this.getCurrentDirection();
if (this.currentPassengers.size === 0) {
const nextRequest = this.requestOrder[0];
if (nextRequest?.startFloor === floor) {
const firstRequestDirection = nextRequest.direction;
for (const passenger of waitingAtFloor)
if (passenger.direction === firstRequestDirection)
eligiblePassengers.add(passenger);
if (eligiblePassengers.size > 0) {
log(
"PASSENGER",
`${eligiblePassengers.size} passengers eligible to board at floor ${floor}, all going ${firstRequestDirection}`,
);
}
}
} else {
for (const passenger of waitingAtFloor)
if (passenger.direction === direction)
eligiblePassengers.add(passenger);
if (eligiblePassengers.size > 0) {
log(
"PASSENGER",
`${eligiblePassengers.size} passengers waiting to go ${direction} at floor ${floor}`,
);
}
}
let boardedCount = 0;
for (const passenger of eligiblePassengers) {
if (this.hasCapacity()) {
this.cabinButtonPressed(passenger);
boardedCount++;
} else {
log(
"CAPACITY",
`Cannot pick up more passengers - elevator full at ${this.currentPassengers.size}/${HardwareElevator.MAX_CAPACITY}`,
);
break;
}
}
if (boardedCount > 0) {
log("PASSENGER", `${boardedCount} passengers boarded at floor ${floor}`);
}
if (waitingAtFloor.size === 0) {
this.waitingPassengers.delete(floor);
}
await this.onDoorsClosed();
}
}
async function runElevator() {
const elevator = new Elevator();
const passengers = [
{ id: 1, startFloor: 4, destinationFloor: 6 },
{ id: 2, startFloor: 4, destinationFloor: 3 },
{ id: 3, startFloor: 3, destinationFloor: 9 },
{ id: 4, startFloor: 6, destinationFloor: 1 },
];
passengers.forEach((passengerData, index) => {
setTimeout(
() => {
const passenger = new Passenger(
passengerData.id,
passengerData.startFloor,
passengerData.destinationFloor,
);
elevator.floorButtonPressed(passenger);
},
(index+1) * DELAY_BETWEEN_PASSENGERS * SPEED,
);
});
await elevator.run();
}
runElevator();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment