From 6051be2f61c077b584c21a307949cbf07e342231 Mon Sep 17 00:00:00 2001 From: Jake Date: Sun, 30 Mar 2025 21:31:04 +0800 Subject: [PATCH] robot sensor values now available in simulation, line sensors return distance, await is automatically inserted before sleep commands --- game.js | 11 ++++--- gameworld.js | 48 ++++++++++++++++------------- pyodide-worker.js | 78 ++++++++++++++++++++++++++++++++++++++++++++--- readme.md | 12 +++++--- robot.js | 6 ++-- sensor.js | 22 +++++++++---- todo.md | 10 +++--- 7 files changed, 140 insertions(+), 47 deletions(-) diff --git a/game.js b/game.js index 845c8fe..5cfdd96 100644 --- a/game.js +++ b/game.js @@ -21,7 +21,7 @@ let startX, startY; // Mouse start positions // ✅ Function to create the Pyodide Worker function startPyodideWorker() { - const worker = new Worker("pyodide-worker.js"); + const worker = new Worker("pyodide-worker.js?v=" + Date.now()); // ✅ Reattach the event listener when a new worker is created worker.onmessage = (event) => { @@ -60,7 +60,7 @@ function createInitialRobots() { // ✅ Function to log messages to console function logToConsole(text) { - console.log(text.replace("", "").replace("", "")); + //console.log(text.replace("", "").replace("", "")); consoleElement.innerHTML += text.replace(/\n/g, "
") + "
"; consoleElement.scrollTop = consoleElement.scrollHeight; } @@ -155,13 +155,16 @@ function updateSensorData() { //logToConsole(`📡 Sensor Update - Distance: ${distance.toFixed(2)}, Speed: ${speed.toFixed(2)}`); } -setInterval(updateSensorData, 2000); // Call every 2 seconds +//setInterval(updateSensorData, 2000); // Call every 2 seconds // ✅ Button Event Listeners document.getElementById("compile-button").addEventListener("click", () => { if (paused) return; - const code = document.getElementById("python-code").value; + let code = document.getElementById("python-code").value; + console.log(code); + code = code.replace(/time\.sleep\(/g, "await time.sleep("); + console.log(code); consoleElement.innerHTML = ""; pyodideWorker.postMessage({ type: "execute", diff --git a/gameworld.js b/gameworld.js index ff8f590..8a81ec8 100644 --- a/gameworld.js +++ b/gameworld.js @@ -16,13 +16,13 @@ export class GameWorld { this.robots = []; - + } - reset(){ + reset() { this.robots = [] this.obstacles = [] Matter.World.clear(this.world); // Clear the world without resetting the engine @@ -40,7 +40,7 @@ export class GameWorld { { x: 420, y: 380 }, // Vertex 2 { x: 350, y: 550 }, // Vertex 3 { x: 280, y: 420 } // Vertex 4 - ], { x: 400, y: 0 }); + ], { x: 400, y: -50 }); } @@ -123,43 +123,47 @@ export class GameWorld { } - rayCast(startX, startY, endX, endY) { + rayCast(startX, startY, endX, endY, ignoreBodies = []) { let closestIntersection = null; let startPoint = { x: startX, y: startY }; let endPoint = { x: endX, y: endY }; - // // Loop through all obstacles - // for (let i = 0; i < this.obstacles.length; i++) { - // let obstacle = this.obstacles[i]; - - // return this.lineIntersectsPolygon(startX, startY, endX, endY, obstacle); - // } // Get all bodies in the world let allBodies = Matter.Composite.allBodies(this.engine.world); - let hit = this.getRayHitPoint(startPoint, endPoint, allBodies); + // ✅ Filter out ignored bodies instead of removing them + let filteredBodies = allBodies.filter(body => !ignoreBodies.includes(body)); + + // Debugging + //console.log("Ignoring bodies: ", ignoreBodies.map(b => b.id)); + //console.log("Filtered bodies: ", filteredBodies.map(b => b.id)); + + // Perform raycast only on the filtered bodies + let hit = this.getRayHitPoint(startPoint, endPoint, filteredBodies); if (hit) { closestIntersection = hit.point; } + return closestIntersection; } + getRayHitPoint(start, end, bodies) { let closestHit = null; let minDistance = Infinity; - + bodies.forEach(body => { let vertices = body.vertices; - + // Loop through each edge of the polygon body for (let i = 0; i < vertices.length; i++) { let v1 = vertices[i]; let v2 = vertices[(i + 1) % vertices.length]; // Wrap around to close the shape - + let intersection = this.getLineIntersection(start, end, v1, v2); if (intersection) { let distance = Matter.Vector.magnitude(Matter.Vector.sub(intersection, start)); - + // Keep track of the closest intersection if (distance < minDistance) { minDistance = distance; @@ -168,29 +172,29 @@ export class GameWorld { } } }); - + return closestHit; } - + // Line intersection helper function getLineIntersection(p1, p2, p3, p4) { let denom = (p4.y - p3.y) * (p2.x - p1.x) - (p4.x - p3.x) * (p2.y - p1.y); - + if (denom === 0) return null; // Parallel lines, no intersection - + let ua = ((p4.x - p3.x) * (p1.y - p3.y) - (p4.y - p3.y) * (p1.x - p3.x)) / denom; let ub = ((p2.x - p1.x) * (p1.y - p3.y) - (p2.y - p1.y) * (p1.x - p3.x)) / denom; - + if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1) { return { x: p1.x + ua * (p2.x - p1.x), y: p1.y + ua * (p2.y - p1.y) }; } - + return null; // No valid intersection } - + diff --git a/pyodide-worker.js b/pyodide-worker.js index e937564..6581d2e 100644 --- a/pyodide-worker.js +++ b/pyodide-worker.js @@ -1,6 +1,7 @@ importScripts("https://cdn.jsdelivr.net/pyodide/v0.23.4/full/pyodide.js"); let sensorData = {}; // ✅ Store sensor values +let gameWorld = null; async function initializePyodide() { self.pyodide = await loadPyodide({ @@ -11,9 +12,44 @@ async function initializePyodide() { self.postMessage({ type: event, data: data }); }); + + // ✅ Expose sensor data to Python self.pyodide.globals.set("get_sensor_data", (name) => { - return sensorData[name] ?? null; + if (gameWorld == null){ + return null; + } + + + //console.log(gameWorld.robots); + let robot = gameWorld.robots[0]; + //console.log(robot); + let sensorArray = robot.sensors || []; // Assuming `sensors` is an array inside the robot object + sensorData = sensorArray.map(sensor => ({ + "type": sensor.type, // Individual sensor's type + "angle": sensor.angle, // Individual sensor's angle + "distance": sensor.distance, // Individual sensor's distance + "hitpoint": sensor.hitpoint // Whatever other attributes you need + })); + //console.log(sensorData["x"]); + sensorData = JSON.stringify(sensorData); + return sensorData; + }); + + + self.pyodide.globals.set("get_robot_data", () => { + if (gameWorld == null){ + return null; + } + let robot = gameWorld.robots[0]; + + let robotData = { + "position": robot.body.position, + "angle": robot.body.angle, + "velocity": robot.body.velocity + }; + robotData = JSON.stringify(robotData); + return robotData; }); // ✅ Run Python initialization @@ -22,10 +58,44 @@ import sys import pyodide import asyncio import time +import json +import math class RobotModule: def get_sensor(self, name): - return get_sensor_data(name) + sensor_data = json.loads(get_sensor_data(name)) + return sensor_data + + def get_sensors(self): + return json.loads(get_sensor_data("sensors")) # Returns list of sensor dicts + + def get_pos(self): + robot_data = json.loads(get_robot_data()) + position = robot_data["position"] + return position + + def get_x(self): + robot_data = json.loads(get_robot_data()) + position = robot_data["position"]["x"] + return position + + def get_y(self): + robot_data = json.loads(get_robot_data()) + position = robot_data["position"]["y"] + return position + + def get_velocity(self): + robot_data = json.loads(get_robot_data()) + velocity = robot_data["velocity"] + return velocity + + def get_velocity_magnitude(self): + velXY = self.get_velocity() + velX = velXY["x"] + velY = velXY["y"] + magnitude = math.sqrt(velX**2 + velY**2) + + return magnitude def move(self, speed): send_to_main("move", speed) @@ -65,7 +135,6 @@ time.sleep = async_sleep # ✅ Monkey-patch time.sleep() } initializePyodide(); -self.postMessage({ type: "console", message: ":-(" }); self.onmessage = async (event) => { if (!self.pyodide) { @@ -77,7 +146,8 @@ self.onmessage = async (event) => { // ✅ Update sensor data Object.assign(sensorData, event.data.data); } else if (event.data.type === "game_state") { - console.log("Game state updated in"); + gameWorld = event.data.state; + //console.log(event.data); } else if (event.data.type === "execute") { try { diff --git a/readme.md b/readme.md index 2963302..f97d255 100644 --- a/readme.md +++ b/readme.md @@ -1,8 +1,12 @@ -import robot +import robot import time -robot.move(0.5) -await time.sleep(1) +robot.move(0.00006) while True: - robot.turn(5) + if robot.get_sensors()[0]["hitpoint"]["x"] is not None: + robot.turn(0.001) + elif robot.get_sensors()[1]["hitpoint"]["x"] is not None: + robot.turn(-0.001) + else: + robot.turn(0) await time.sleep(0.1) \ No newline at end of file diff --git a/robot.js b/robot.js index 630f034..bf19a8f 100644 --- a/robot.js +++ b/robot.js @@ -22,8 +22,8 @@ export class Robot { { x: -this.width / 2, y: this.height / 2 } // Vertex 4 ]; - this.addSensor(new RaycastSensor(this, -40, 13, -45, 60)); - this.addSensor(new RaycastSensor(this, 40, 13, 45, 60)); + this.addSensor(new RaycastSensor(this, -40, 12, -45, 60)); + this.addSensor(new RaycastSensor(this, 40, 12, 45, 60)); this.addSensor(new FloorColorSensor(this, 0, 0)); @@ -72,7 +72,7 @@ export class Robot { } turn(degrees) { - this.angularThrust += degrees; + this.angularThrust = degrees; } draw(ctx) { diff --git a/sensor.js b/sensor.js index 083c9cc..fd13bfd 100644 --- a/sensor.js +++ b/sensor.js @@ -1,5 +1,6 @@ export class Sensor { constructor(robot, offsetAngle, offsetDistance) { + this.type = "base"; this.robot = robot this.value = null; // Last recorded value this.offsetAngle = offsetAngle; @@ -7,6 +8,8 @@ export class Sensor { this.hitX = null; this.hitY = null; this.hitObject = null; + this.angle = 0; + this.hitpoint = {x: null, y: null}; } @@ -28,11 +31,12 @@ export class Sensor { } export class RaycastSensor extends Sensor { - constructor(robot, offsetAngle, offsetDistance, angle, distance) { + constructor(robot, offsetAngle, offsetDistance, angle, range) { super(robot, offsetAngle, offsetDistance); // Call the parent class constructor + this.type = "line"; this.angle = angle; - this.distance = distance; - this.range = 100; + this.distance = null; + this.range = range; } updatePosition() { @@ -51,19 +55,24 @@ export class RaycastSensor extends Sensor { this.updatePosition(); const angleInRadians = (this.robot.direction + this.angle) * Math.PI / 180; - const x = this.robot.x + Math.cos(angleInRadians) * this.distance; - const y = this.robot.y + Math.sin(angleInRadians) * this.distance; + const x = this.robot.x + Math.cos(angleInRadians) * this.range; + const y = this.robot.y + Math.sin(angleInRadians) * this.range; // Ensure gameWorld is available and properly passed to the sensor - let hitPos = gameWorld.rayCast(this.startX, this.startY, this.endX, this.endY); + let hitPos = gameWorld.rayCast(this.startX, this.startY, this.endX, this.endY, [this.robot.body]); if (hitPos != null) { this.hitX = hitPos.x; this.hitY = hitPos.y; this.endX = this.hitX; this.endY = this.hitY; + this.hitpoint = {x: this.hitX, y: this.hitY}; + this.distance = Math.sqrt(Math.pow(this.hitX - this.startX, 2) + Math.pow(this.hitY - this.startY, 2)); } else { this.hitX = null; this.hitY = null; + + this.hitpoint = {x: this.hitX, y: this.hitY}; + this.distance = Math.sqrt(Math.pow(this.endX - this.startX, 2) + Math.pow(this.endY - this.startY, 2)); } // console.log("Obstacle detected!"); // } else { @@ -106,6 +115,7 @@ export class RaycastSensor extends Sensor { export class FloorColorSensor extends Sensor { constructor(robot, offsetAngle, offsetDistance) { super(robot, offsetAngle, offsetDistance); // No angle offset, directly below robot + this.type = "color"; } read(robot, gameWorld) { diff --git a/todo.md b/todo.md index acaa6f1..3e27120 100644 --- a/todo.md +++ b/todo.md @@ -1,12 +1,14 @@ DO +Create test level of a track with obstacles all around +Improve the text editor to be larger, and maybe have some colour coding and allow tabs + +IN PROGRESS + +DONE Add robot.angle and other state variables Add robot sensor data connections to detect and change parameters Add pan and zoom to canvas - -IN PROGRESS Collision between robots and objects - -DONE Add line sensor object collisions Add robot sensors that detect things in game world, color sense and distance sense for start Add Pause Button