sapbot

Robot Vacuum

Feb 6th, 2026
50
0
Never
2
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. const rl = require('raylib');
  2. const readline = require('readline');
  3. const romai = require('romai');
  4.  
  5. // --- Configuration ---
  6. const ROOM_SIZE = 10;
  7. const SCALE = 60;
  8. const WIN_SIZE = ROOM_SIZE * SCALE;
  9. const ROBOT_SIZE = 0.3;
  10. const MOVE_STEP = 0.5;
  11. const GRID_RES = 0.5;
  12. const GRID_CELLS = ROOM_SIZE / GRID_RES;
  13.  
  14. // --- CLI Parsing ---
  15. const args = process.argv.slice(2);
  16. const isBot = args.includes('--bot');
  17. const aiIndex = args.indexOf('--ai');
  18. let aiModel = null;
  19.  
  20. if (aiIndex !== -1) {
  21.   if (aiIndex + 1 < args.length && !args[aiIndex + 1].startsWith('-')) {
  22.     aiModel = args[aiIndex + 1];
  23.   } else {
  24.     aiModel = undefined;
  25.   }
  26. }
  27.  
  28. if (isBot) {
  29.   console.log(`πŸ€– BOT MODE${aiModel !== null ? ` (AI: ${aiModel || 'default'})` : ` (Algorithm: Perimeterβ†’Snake)`}`);
  30.   console.log('Close window to stop.\n');
  31. }
  32.  
  33. // --- State ---
  34. const state = {
  35.   robot: { x: 2, y: 2, angle: 0 },
  36.   chair: { x: 6, y: 6, width: 0.6, height: 0.6 },
  37.   distances: { Forward: 0, Left: 0, Right: 0 },
  38.   moveCount: 0,
  39.   cleaned: Array(GRID_CELLS).fill().map(() => Array(GRID_CELLS).fill(false)),
  40.   completed: false,
  41.   phase: 'PERIMETER',
  42.   perimeterStart: null,
  43.   snakeRow: 0,
  44.   snakeDirection: 1,
  45.   targetY: 0.5
  46. };
  47.  
  48. // Randomize chair
  49. state.chair.x = 4 + Math.random() * 4;
  50. state.chair.y = 4 + Math.random() * 4;
  51.  
  52. // --- Math Helpers ---
  53. function toRad(deg) { return deg * Math.PI / 180; }
  54. function getVector(angleDeg) {
  55.   const rad = toRad(angleDeg);
  56.   return { x: Math.cos(rad), y: Math.sin(rad) };
  57. }
  58.  
  59. function getGridCoord(x, y) {
  60.   return {
  61.     x: Math.floor(x / GRID_RES),
  62.     y: Math.floor(y / GRID_RES)
  63.   };
  64. }
  65.  
  66. // --- Sensors & Physics ---
  67. function rayIntersect(origin, dir, rect) {
  68.   const minX = rect.x - rect.width/2;
  69.   const maxX = rect.x + rect.width/2;
  70.   const minY = rect.y - rect.height/2;
  71.   const maxY = rect.y + rect.height/2;
  72.   let tMin = -Infinity, tMax = Infinity;
  73.  
  74.   if (dir.x !== 0) {
  75.     const tx1 = (minX - origin.x) / dir.x;
  76.     const tx2 = (maxX - origin.x) / dir.x;
  77.     tMin = Math.max(tMin, Math.min(tx1, tx2));
  78.     tMax = Math.min(tMax, Math.max(tx1, tx2));
  79.   } else if (origin.x < minX || origin.x > maxX) return Infinity;
  80.  
  81.   if (dir.y !== 0) {
  82.     const ty1 = (minY - origin.y) / dir.y;
  83.     const ty2 = (maxY - origin.y) / dir.y;
  84.     tMin = Math.max(tMin, Math.min(ty1, ty2));
  85.     tMax = Math.min(tMax, Math.max(ty1, ty2));
  86.   } else if (origin.y < minY || origin.y > maxY) return Infinity;
  87.  
  88.   if (tMax < 0 || tMin > tMax) return Infinity;
  89.   return tMin > 0 ? tMin : Infinity;
  90. }
  91.  
  92. function wallDistance(origin, dir) {
  93.   let dist = Infinity;
  94.   if (dir.x > 0) dist = Math.min(dist, (ROOM_SIZE - origin.x) / dir.x);
  95.   else if (dir.x < 0) dist = Math.min(dist, -origin.x / dir.x);
  96.   if (dir.y > 0) dist = Math.min(dist, (ROOM_SIZE - origin.y) / dir.y);
  97.   else if (dir.y < 0) dist = Math.min(dist, -origin.y / dir.y);
  98.   return dist;
  99. }
  100.  
  101. function updateSensors() {
  102.   [
  103.     { key: 'Forward', angle: state.robot.angle },
  104.     { key: 'Left', angle: state.robot.angle - 90 },
  105.     { key: 'Right', angle: state.robot.angle + 90 }
  106.   ].forEach(d => {
  107.     const vec = getVector(d.angle);
  108.     const dChair = rayIntersect(state.robot, vec, state.chair);
  109.     const dWall = wallDistance(state.robot, vec);
  110.     state.distances[d.key] = Math.min(dChair, dWall);
  111.   });
  112. }
  113.  
  114. function checkCollision(x, y) {
  115.   const r = ROBOT_SIZE / 2;
  116.   if (x - r < 0 || x + r > ROOM_SIZE || y - r < 0 || y + r > ROOM_SIZE) return true;
  117.   const cx = state.chair.x - state.chair.width/2 - r;
  118.   const cy = state.chair.y - state.chair.height/2 - r;
  119.   const cw = state.chair.width + r*2;
  120.   const ch = state.chair.height + r*2;
  121.   if (x > cx && x < cx + cw && y > cy && y < cy + ch) return true;
  122.   return false;
  123. }
  124.  
  125. function markCleaned() {
  126.   const r = ROBOT_SIZE / 2;
  127.   // Mark all grid cells that the robot physically overlaps
  128.   const minX = Math.floor((state.robot.x - r) / GRID_RES);
  129.   const maxX = Math.floor((state.robot.x + r) / GRID_RES);
  130.   const minY = Math.floor((state.robot.y - r) / GRID_RES);
  131.   const maxY = Math.floor((state.robot.y + r) / GRID_RES);
  132.  
  133.   for (let y = minY; y <= maxY; y++) {
  134.     for (let x = minX; x <= maxX; x++) {
  135.       if (y >= 0 && y < GRID_CELLS && x >= 0 && x < GRID_CELLS) {
  136.         state.cleaned[y][x] = true;
  137.       }
  138.     }
  139.   }
  140. }
  141.  
  142. function getCoverageStats() {
  143.   let cleaned = 0;
  144.   for (let row of state.cleaned) {
  145.     for (let cell of row) if (cell) cleaned++;
  146.   }
  147.   const total = GRID_CELLS * GRID_CELLS;
  148.   return { cleaned, total, percent: (cleaned / total * 100).toFixed(1) };
  149. }
  150.  
  151. // --- Command Execution ---
  152. function executeCommand(cmd) {
  153.   if (cmd === 'right') {
  154.     state.robot.angle = (state.robot.angle + 90) % 360;
  155.     return true;
  156.   } else if (cmd === 'left') {
  157.     state.robot.angle = (state.robot.angle - 90 + 360) % 360;
  158.     return true;
  159.   } else if (cmd === 'forward') {
  160.     const vec = getVector(state.robot.angle);
  161.     const newX = state.robot.x + vec.x * MOVE_STEP;
  162.     const newY = state.robot.y + vec.y * MOVE_STEP;
  163.    
  164.     if (!checkCollision(newX, newY)) {
  165.       state.robot.x = newX;
  166.       state.robot.y = newY;
  167.       state.moveCount++;
  168.       markCleaned();
  169.       return true;
  170.     } else {
  171.       console.log('*** CLUNK! ***');
  172.       return false;
  173.     }
  174.   } else if (cmd === 'pass') {
  175.     return true;
  176.   }
  177.   return false;
  178. }
  179.  
  180. // --- Standard Vacuum Algorithm ---
  181. function getAlgorithmicCommand() {
  182.   const canMove = (angle) => {
  183.     const vec = getVector(angle);
  184.     const newX = state.robot.x + vec.x * MOVE_STEP;
  185.     const newY = state.robot.y + vec.y * MOVE_STEP;
  186.     return !checkCollision(newX, newY);
  187.   };
  188.  
  189.   const getDist = (angle) => {
  190.     const rel = (angle - state.robot.angle + 360) % 360;
  191.     if (rel === 0) return state.distances.Forward;
  192.     if (rel === 90 || rel === -270) return state.distances.Left;
  193.     if (rel === 270 || rel === -90) return state.distances.Right;
  194.     if (rel === 180) {
  195.       const vec = getVector(angle);
  196.       const dChair = rayIntersect(state.robot, vec, state.chair);
  197.       const dWall = wallDistance(state.robot, vec);
  198.       return Math.min(dChair, dWall);
  199.     }
  200.     return 0;
  201.   };  // PHASE 1: PERIMETER FOLLOWING (Fixed)
  202.   if (state.phase === 'PERIMETER') {
  203.     // Initialize with rotation tracking
  204.     if (!state.perimeterStart) {
  205.       state.perimeterStart = {
  206.         x: state.robot.x,
  207.         y: state.robot.y,
  208.         startAngle: state.robot.angle,
  209.         totalRotation: 0,
  210.         lastAngle: state.robot.angle,
  211.         hugging: false,
  212.         moves: 0
  213.       };
  214.       state.hadWallOnRight = false;
  215.     }
  216.    
  217.     // Track net rotation (handles 0/360 wrap)
  218.     let angleDelta = state.robot.angle - state.perimeterStart.lastAngle;
  219.     if (angleDelta > 180) angleDelta -= 360;
  220.     if (angleDelta < -180) angleDelta += 360;
  221.     state.perimeterStart.totalRotation += angleDelta;
  222.     state.perimeterStart.lastAngle = state.robot.angle;
  223.     state.perimeterStart.moves++;
  224.    
  225.     // Completion: Near start + made full rotation (~360Β°) + minimum moves
  226.     const distToStart = Math.hypot(state.robot.x - state.perimeterStart.x,
  227.                                    state.robot.y - state.perimeterStart.y);
  228.     const madeFullCircle = Math.abs(state.perimeterStart.totalRotation) > 300;
  229.    
  230.     if (state.perimeterStart.hugging && state.perimeterStart.moves > 40 &&
  231.         distToStart < 1.2 && madeFullCircle) {
  232.       console.log('βœ“ Perimeter complete. Starting snake...');
  233.       state.phase = 'SNAKE';
  234.       // Align to East/West for snake
  235.       if (state.robot.angle > 45 && state.robot.angle <= 135) return 'left';  // Face North->West
  236.       if (state.robot.angle > 135 && state.robot.angle <= 225) return 'right'; // Face West->North then left?
  237.       if (state.robot.angle > 225 && state.robot.angle <= 315) return 'right'; // Face South->West
  238.       // Already roughly East/West
  239.       state.snakeDirection = (state.robot.angle < 90 || state.robot.angle > 270) ? 1 : -1;
  240.       return 'forward';
  241.     }
  242.  
  243.     // Step 1: Find a wall to hug
  244.     if (!state.perimeterStart.hugging) {
  245.       if (state.distances.Forward > 0.6 && state.distances.Right > 0.6) {
  246.         return 'forward';
  247.       }
  248.       state.perimeterStart.hugging = true;
  249.       return (state.distances.Forward <= 0.6) ? 'right' : 'forward';
  250.     }
  251.  
  252.     // Step 2: Follow wall on right
  253.     const wallRight = state.distances.Right <= 0.6;
  254.     const wallFront = state.distances.Forward <= 0.6;
  255.    
  256.     // Outside corner: had wall, now gone -> turn right to follow new wall
  257.     if (state.hadWallOnRight && !wallRight) {
  258.       state.hadWallOnRight = wallRight;
  259.       return 'right';
  260.     }
  261.    
  262.     // Standard follow: wall on right, clear ahead -> forward
  263.     if (wallRight && !wallFront) {
  264.       state.hadWallOnRight = wallRight;
  265.       return 'forward';
  266.     }
  267.    
  268.     // Inside corner: wall ahead (and wall on right) -> turn left
  269.     if (wallFront && wallRight) {
  270.       state.hadWallOnRight = wallRight;
  271.       return 'left';
  272.     }
  273.    
  274.     // Lost wall (drifted away) -> turn right to find it again
  275.     if (!wallRight) {
  276.       state.hadWallOnRight = wallRight;
  277.       return 'right';
  278.     }
  279.    
  280.     // Dead end
  281.     state.hadWallOnRight = wallRight;
  282.     return 'left';
  283.   }
  284.     // PHASE 2: SNAKE (inside getAlgorithmicCommand)
  285.   if (state.phase === 'SNAKE') {
  286.     const hitWall = state.distances.Forward < 0.6;
  287.    
  288.     if (hitWall) {
  289.       // Determine which way to turn based on current heading
  290.       const goingEast = state.robot.angle === 0;
  291.       const goingWest = state.robot.angle === 180;
  292.      
  293.       if (goingEast || goingWest) {
  294.         // Try to move down (or up) to next row
  295.         // Check both down (90) and up (270) to find an open path
  296.         const canGoDown = !checkCollision(
  297.           state.robot.x + Math.cos(toRad(90)) * MOVE_STEP,
  298.           state.robot.y + Math.sin(toRad(90)) * MOVE_STEP
  299.         );
  300.         const canGoUp = !checkCollision(
  301.           state.robot.x + Math.cos(toRad(270)) * MOVE_STEP,
  302.           state.robot.y + Math.sin(toRad(270)) * MOVE_STEP
  303.         );
  304.        
  305.         if (canGoDown || canGoUp) {
  306.           state.snakeDirection *= -1;
  307.           // Turn toward the open direction
  308.           if (goingEast) return canGoDown ? 'right' : 'left';
  309.           else return canGoDown ? 'left' : 'right';
  310.         } else {
  311.           // Completely stuck - coverage complete or blocked
  312.           state.completed = true;
  313.           return 'pass';
  314.         }
  315.       }
  316.     }
  317.    
  318.     // Continue in current snake direction...
  319.     const targetAngle = state.snakeDirection === 1 ? 0 : 180;
  320.     if (state.robot.angle === targetAngle) return 'forward';
  321.    
  322.     const diff = (targetAngle - state.robot.angle + 360) % 360;
  323.     if (diff === 90) return 'right';
  324.     if (diff === 270) return 'left';
  325.     return 'right';
  326.   }
  327.  
  328.   return 'pass';
  329. }
  330.  
  331. // --- AI Logic ---
  332. async function getAICommand() {
  333.   await new Promise(r => setTimeout(r, 800));
  334.  
  335.   const available = ['right', 'left', 'pass'];
  336.   if (state.distances.Forward > 0.6) available.push('forward');
  337.  
  338.   const prompt = `You are a robot vacuum. Telemetry:
  339. - Position: (${state.robot.x.toFixed(1)}, ${state.robot.y.toFixed(1)})
  340. - Facing: ${state.robot.angle}Β°
  341. - Forward: ${state.distances.Forward.toFixed(2)}m${state.distances.Forward <= 0.6 ? ' [BLOCKED]' : ''}
  342. - Left: ${state.distances.Left.toFixed(2)}m
  343. - Right: ${state.distances.Right.toFixed(2)}m
  344. - Coverage: ${getCoverageStats().percent}%
  345. - Phase: ${state.phase}
  346.  
  347. Available commands: ${available.join(', ')}
  348. Strategy: Circle room perimeter first, then fill interior with back-forth pattern.
  349. Output exactly one command.`;
  350.  
  351.   try {
  352.     const response = romai.ask(prompt, aiModel, "Robot controller. Output one word.", false);
  353.     const clean = response.toLowerCase().trim();
  354.    
  355.     for (let cmd of available) {
  356.       if (clean.includes(cmd)) return cmd;
  357.     }
  358.     return available[0];
  359.   } catch (e) {
  360.     return 'pass';
  361.   }
  362. }
  363.  
  364. // --- Rendering ---
  365. function draw() {
  366.   rl.BeginDrawing();
  367.   rl.ClearBackground(rl.Color(240, 240, 240, 255));
  368.  
  369.   for (let y = 0; y < GRID_CELLS; y++) {
  370.     for (let x = 0; x < GRID_CELLS; x++) {
  371.       if (state.cleaned[y][x]) {
  372.         rl.DrawRectangle(
  373.           x * GRID_RES * SCALE,
  374.           y * GRID_RES * SCALE,
  375.           GRID_RES * SCALE - 1,
  376.           GRID_RES * SCALE - 1,
  377.           rl.Color(100, 255, 100, 80)
  378.         );
  379.       }
  380.     }
  381.   }
  382.  
  383.   for (let i = 0; i <= ROOM_SIZE; i++) {
  384.     const pos = i * SCALE;
  385.     rl.DrawLine(pos, 0, pos, WIN_SIZE, rl.Color(200, 200, 200, 255));
  386.     rl.DrawLine(0, pos, WIN_SIZE, pos, rl.Color(200, 200, 200, 255));
  387.   }
  388.  
  389.   const cx = (state.chair.x - state.chair.width/2) * SCALE;
  390.   const cy = (state.chair.y - state.chair.height/2) * SCALE;
  391.   rl.DrawRectangle(cx, cy, state.chair.width*SCALE, state.chair.height*SCALE, rl.BROWN);
  392.   rl.DrawRectangleLines(cx, cy, state.chair.width*SCALE, state.chair.height*SCALE, rl.DARKBROWN);
  393.  
  394.   const rx = state.robot.x * SCALE;
  395.   const ry = state.robot.y * SCALE;
  396.   const r = (ROBOT_SIZE/2) * SCALE;
  397.  
  398.   rl.DrawCircle(rx, ry, r, isBot ? (aiModel ? rl.PURPLE : rl.Color(0, 150, 255, 255)) : rl.BLUE);
  399.   rl.DrawCircleLines(rx, ry, r, rl.DARKBLUE);
  400.  
  401.   const vec = getVector(state.robot.angle);
  402.   rl.DrawLine(rx, ry, rx + vec.x*r*2, ry + vec.y*r*2, rl.RED);
  403.  
  404.   ['Forward', 'Left', 'Right'].forEach(name => {
  405.     const ang = name === 'Forward' ? state.robot.angle :
  406.                 name === 'Left' ? state.robot.angle - 90 : state.robot.angle + 90;
  407.     const v = getVector(ang);
  408.     const dist = state.distances[name];
  409.     const blocked = name === 'Forward' && dist < 0.6;
  410.     const color = blocked ? rl.RED : (name === 'Forward' ? rl.GREEN : rl.ORANGE);
  411.     const endX = rx + v.x * dist * SCALE;
  412.     const endY = ry + v.y * dist * SCALE;
  413.    
  414.     rl.DrawLine(rx, ry, endX, endY, color);
  415.     rl.DrawCircle(endX, endY, 3, rl.RED);
  416.   });
  417.  
  418.   const stats = getCoverageStats();
  419.   const modeText = isBot ? (aiModel ? `AI:${aiModel}` : `${state.phase}`) : 'MANUAL';
  420.   rl.DrawText(`Mode: ${modeText}`, 10, 10, 20, isBot ? rl.Color(0, 200, 255, 255) : rl.BLACK);
  421.   rl.DrawText(`Moves: ${state.moveCount} | Cleaned: ${stats.percent}%`, 10, 35, 20, rl.DARKGRAY);
  422.  
  423.   if (state.completed) {
  424.     rl.DrawText("100% COVERAGE!", WIN_SIZE/2 - 100, WIN_SIZE/2 - 20, 30, rl.GREEN);
  425.   }
  426.  
  427.   rl.EndDrawing();
  428. }
  429.  
  430. // --- Terminal UI ---
  431. function printTelemetry() {
  432.   const d = state.distances;
  433.   const stats = getCoverageStats();
  434.   const mode = isBot ? (aiModel ? `AI (${aiModel})` : `Algorithm [${state.phase}]`) : 'Human';
  435.  
  436.   const output = `
  437. ╔══════════════════════════════════════════╗
  438. β•‘        ROBOT VACUUM TELEMETRY            β•‘
  439. β•‘        Mode: ${mode.padEnd(26)} β•‘
  440. ╠══════════════════════════════════════════╣
  441. β•‘  Distance to objects:                    β•‘
  442. β•‘    Forward - ${d.Forward.toFixed(2).padStart(5)}m ${d.Forward < 0.6 ? '[BLOCKED]' : '         '}       β•‘
  443. β•‘    Left    - ${d.Left.toFixed(2).padStart(5)}m                  β•‘
  444. β•‘    Right   - ${d.Right.toFixed(2).padStart(5)}m                  β•‘
  445. ╠══════════════════════════════════════════╣
  446. β•‘  Coverage: ${stats.percent}% (${stats.cleaned}/${stats.total} cells)        β•‘
  447. β•‘  Position: ${state.robot.x.toFixed(1)},${state.robot.y.toFixed(1)}m  Angle: ${state.robot.angle.toString().padStart(3)}Β°      β•‘
  448. ╠══════════════════════════════════════════╣
  449. ${isBot ?
  450. `β•‘  πŸ€– Phase: ${state.phase.padEnd(24)} β•‘` :
  451. 'β•‘  Commands: right | left | forward | pass β•‘'}
  452. ${state.completed ? 'β•‘  βœ… COMPLETE                             β•‘' : ''}
  453. β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
  454. `;
  455.   console.log(output);
  456. }
  457.  
  458. // --- Input Handling ---
  459. const rlInterface = readline.createInterface({
  460.   input: process.stdin,
  461.   output: process.stdout
  462. });
  463.  
  464. function waitForHumanCommand() {
  465.   return new Promise((resolve) => {
  466.     const interval = setInterval(() => {
  467.       if (rl.WindowShouldClose()) {
  468.         clearInterval(interval);
  469.         resolve('quit');
  470.       }
  471.       draw();
  472.     }, 16);
  473.    
  474.     rlInterface.question('> ', (answer) => {
  475.       clearInterval(interval);
  476.       resolve(answer.trim().toLowerCase());
  477.     });
  478.   });
  479. }
  480.  
  481. async function getBotCommand() {
  482.   if (aiModel !== null) {
  483.     return await getAICommand();
  484.   } else {
  485.     await new Promise(r => setTimeout(r, 200));
  486.     return getAlgorithmicCommand();
  487.   }
  488. }
  489.  
  490. // --- Main Loop ---
  491. async function main() {
  492.   rl.InitWindow(WIN_SIZE, WIN_SIZE, `Robot Vacuum - ${isBot ? 'Auto' : 'Manual'}`);
  493.   rl.SetTargetFPS(60);
  494.  
  495.   markCleaned();
  496.   updateSensors();
  497.   draw();
  498.   printTelemetry();
  499.  
  500.   while (!rl.WindowShouldClose()) {
  501.     let cmd;
  502.    
  503.     if (isBot) {
  504.       cmd = await getBotCommand();
  505.       if (cmd !== 'pass' || !state.completed) {
  506.         console.log(`> ${cmd}`);
  507.       }
  508.     } else {
  509.       cmd = await waitForHumanCommand();
  510.       if (cmd === 'quit' || cmd === 'exit') break;
  511.     }
  512.    
  513.     if (['right', 'left', 'forward', 'pass'].includes(cmd)) {
  514.       if (!isBot && cmd === 'forward' && state.distances.Forward < 0.6) {
  515.         console.log('*** Forward blocked! Turn first. ***');
  516.         continue;
  517.       }
  518.      
  519.       executeCommand(cmd);
  520.       updateSensors();
  521.       draw();
  522.       printTelemetry();
  523.     } else if (!isBot) {
  524.       console.log(`? Unknown: "${cmd}"`);
  525.     }
  526.    
  527.     if (!state.completed && isBot && !aiModel) {
  528.       const stats = getCoverageStats();
  529.       if (stats.percent >= 99.5) {
  530.         state.completed = true;
  531.         console.log('\n╔════════════════════════════════╗');
  532.         console.log('β•‘   πŸŽ‰ 100% COVERAGE ACHIEVED!   β•‘');
  533.         console.log(`β•‘   Total moves: ${state.moveCount.toString().padStart(16)} β•‘`);
  534.         console.log('β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•\n');
  535.       }
  536.     }
  537.   }
  538.  
  539.   rl.CloseWindow();
  540.   rlInterface.close();
  541.   process.exit(0);
  542. }
  543.  
  544. main().catch(err => {
  545.   console.error('Fatal error:', err);
  546.   process.exit(1);
  547. });
Advertisement
Comments
  • User was banned
  • User was banned
Add Comment
Please, Sign In to add comment