From 2c0c13bc9b0a2523bb2043711f75f779c92b3565 Mon Sep 17 00:00:00 2001 From: Jake Date: Thu, 17 Jul 2025 14:10:54 +0800 Subject: [PATCH] added basic line following, need to work it into lesson system --- game.js | 11 +++- gameworld.js | 128 ++++++++++++++++++++++++++++++++++++++++------ index.html | 4 +- pyodide-worker.js | 10 +++- readme.md | 19 ++++++- robot.js | 7 +-- sensor.js | 26 +++++++--- style.css | 4 ++ todo.md | 7 +++ 9 files changed, 183 insertions(+), 33 deletions(-) diff --git a/game.js b/game.js index 20f57a6..8aae3ed 100644 --- a/game.js +++ b/game.js @@ -525,6 +525,12 @@ gameCanvas.addEventListener("wheel", (event) => { offsetY = mouseY - worldY * scale; }); +const canvas = document.getElementById("gameCanvas"); +window.addEventListener("resize", () => { + // console.log("RESIZE"); + // gameWorld.resizeCanvas(canvas) +}); + gameCanvas.addEventListener("mousedown", (event) => { isPanning = true; @@ -574,6 +580,7 @@ function setupCanvas() { const canvas = document.getElementById("gameCanvas"); const context = canvas.getContext("2d"); + // Get the device pixel ratio const dpr = window.devicePixelRatio || 1; @@ -590,7 +597,7 @@ function setupCanvas() { } // Call this function when the page loads -fetch('/data/levels.json') +fetch('./data/levels.json') .then(response => response.json()) .then(data => { gameWorld.levelData = data; @@ -601,7 +608,7 @@ fetch('/data/levels.json') // Start game loop gameLoop(); - showLesson(0); + showLesson(10); }); diff --git a/gameworld.js b/gameworld.js index bacf487..b866ad9 100644 --- a/gameworld.js +++ b/gameworld.js @@ -19,6 +19,21 @@ export class GameWorld { this.obstacles = []; this.robots = []; + this.lineColor = `rgba(0,0,0,1)`; + this.floorLines = [ + { x: 170, y: 75 }, + { x: 300, y: 75 }, + { x: 500, y: 300 }, + { x: 500, y: 400 }, + { x: 450, y: 450 }, + { x: 400, y: 450 }, + { x: 100, y: 350 }, + { x: 50, y: 250 }, + { x: 80, y: 150 }, + { x: 170, y: 75 }, + // add more points as needed + ]; + this.lineWidth = 3; @@ -171,24 +186,24 @@ export class GameWorld { getCentroid(vertices) { let area = 0, cx = 0, cy = 0; - for (let i = 0; i < vertices.length; i++) { - const p1 = vertices[i]; - const p2 = vertices[(i + 1) % vertices.length]; - const cross = p1.x * p2.y - p2.x * p1.y; + for (let i = 0; i < vertices.length; i++) { + const p1 = vertices[i]; + const p2 = vertices[(i + 1) % vertices.length]; + const cross = p1.x * p2.y - p2.x * p1.y; - area += cross; - cx += (p1.x + p2.x) * cross; - cy += (p1.y + p2.y) * cross; - } + area += cross; + cx += (p1.x + p2.x) * cross; + cy += (p1.y + p2.y) * cross; + } - area *= 0.5; - if (area === 0) return vertices[0]; // fallback to first point + area *= 0.5; + if (area === 0) return vertices[0]; // fallback to first point - cx /= (6 * area); - cy /= (6 * area); + cx /= (6 * area); + cy /= (6 * area); - return { x: cx, y: cy }; -} + return { x: cx, y: cy }; + } addObstacle(vertices, strokeColor = "black", fillColor = "gray") { // Sort vertices clockwise (Matter requires this) @@ -247,15 +262,24 @@ export class GameWorld { // Return the floor color based on (x, y) coordinates getFloorColor(x, y) { - return (x + y) % 50 < 25 ? "black" : "white"; // Example pattern + const isUnderLine = this.isPointNearLine(x, y, this.floorLines, this.lineWidth); + return isUnderLine; + // if (isUnderLine) { + // console.log("Robot is over a floor line"); + // } } // Draw the game world (e.g., obstacles, background) draw(ctx) { + //this.render(ctx); + + + this.drawFloorLines(ctx); // Draw obstacles ctx.strokeStyle = "gray"; // Obstacle outline color ctx.lineWidth = 2; // Optional: to make the outline thicker + // Draw obstacles this.obstacles.forEach(body => { ctx.beginPath(); @@ -313,8 +337,45 @@ export class GameWorld { robot.draw(ctx); // Draw the robot's hull and sensors }); + + } + drawFloorLines(ctx) { + if (this.floorLines.length < 2) return; + + ctx.strokeStyle = this.lineColor; + ctx.lineWidth = this.lineWidth; + ctx.beginPath(); + ctx.moveTo(this.floorLines[0].x, this.floorLines[0].y); + for (let i = 1; i < this.floorLines.length; i++) { + ctx.lineTo(this.floorLines[i].x, this.floorLines[i].y); + } + ctx.stroke(); + } + + + resizeCanvasToDisplaySize(ctx) { + let canvas = ctx.canvas; + const width = canvas.clientWidth; + const height = canvas.clientHeight; + + if (canvas.width !== width || canvas.height !== height) { + canvas.width = width; + canvas.height = height; + } + } + + render(ctx) { + let canvas = ctx.canvas; + this.resizeCanvasToDisplaySize(canvas); // 👈 ensures drawing resolution matches display size + ctx.clearRect(0, 0, canvas.width, canvas.height); + game.draw(ctx); + requestAnimationFrame(render); + } + + + rayCast(startX, startY, endX, endY, ignoreBodies = []) { let closestIntersection = null; let startPoint = { x: startX, y: startY }; @@ -389,8 +450,45 @@ export class GameWorld { return null; // No valid intersection } + isPointNearLine(x, y, linePoints, width) { + const threshold = width / 2; + for (let i = 0; i < linePoints.length - 1; i++) { + const p1 = linePoints[i]; + const p2 = linePoints[i + 1]; + const dist = this.pointToSegmentDistance(x, y, p1.x, p1.y, p2.x, p2.y); + if (dist <= threshold) { + return true; + } + } + return false; + } + + pointToSegmentDistance(px, py, x1, y1, x2, y2) { + const dx = x2 - x1; + const dy = y2 - y1; + const lengthSquared = dx * dx + dy * dy; + + if (lengthSquared === 0) { + // p1 == p2 + const dxp = px - x1; + const dyp = py - y1; + return Math.sqrt(dxp * dxp + dyp * dyp); + } + + // Project point onto the segment + let t = ((px - x1) * dx + (py - y1) * dy) / lengthSquared; + t = Math.max(0, Math.min(1, t)); // clamp to segment + + const projX = x1 + t * dx; + const projY = y1 + t * dy; + + const dxp = px - projX; + const dyp = py - projY; + + return Math.sqrt(dxp * dxp + dyp * dyp); + } } diff --git a/index.html b/index.html index 4c66d7a..2af1f75 100644 --- a/index.html +++ b/index.html @@ -17,12 +17,12 @@
-
+
diff --git a/pyodide-worker.js b/pyodide-worker.js index 7385022..7934067 100644 --- a/pyodide-worker.js +++ b/pyodide-worker.js @@ -32,8 +32,10 @@ async function initializePyodide() { "type": sensor.type, // Individual sensor's type "angle": sensor.angle, // Individual sensor's angle "distance": Math.round(sensor.distance * 100) / 100, // Individual sensor's distance - "hitpoint": sensor.hitpoint // Whatever other attributes you need + "hitpoint": sensor.hitpoint, // Whatever other attributes you need + "canSeeLine": sensor.hasLine })); + //console.log(robot.sensors); //console.log(sensorData["x"]); sensorData = JSON.stringify(sensorData); return sensorData; @@ -75,6 +77,12 @@ class RobotModule: def get_distance_right(self): return self.get_sensors()[1]["distance"] + def get_line_left(self): + return self.get_sensors()[2]["canSeeLine"] + + def get_line_right(self): + return self.get_sensors()[3]["canSeeLine"] + def get_sensors(self): return json.loads(get_sensor_data("sensors")) # Returns list of sensor dicts diff --git a/readme.md b/readme.md index 12db980..9b1b8f6 100644 --- a/readme.md +++ b/readme.md @@ -79,4 +79,21 @@ while True: else: robot.move(1) robot.turn(0) - time.sleep(0.01) \ No newline at end of file + time.sleep(0.01) + + + +# FOLLOW LINE +import robot +import time + +while True: + robot.move(0.1) + if robot.get_line_left(): + robot.turn(-1) + elif robot.get_line_right(): + robot.turn(1) + else: + robot.turn(0) + + time.sleep(0.05) \ No newline at end of file diff --git a/robot.js b/robot.js index b31d35a..a5e7ffe 100644 --- a/robot.js +++ b/robot.js @@ -24,7 +24,8 @@ export class Robot { 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)); + this.addSensor(new FloorColorSensor(this, -20, 12)); + this.addSensor(new FloorColorSensor(this, 20, 12)); } @@ -43,10 +44,6 @@ export class Robot { this.update_sensors(gameWorld); } - draw(ctx){ - this.draw(ctx); - } - update_sensors(gameWorld) { this.sensors.forEach(sensor => sensor.read(this, gameWorld)); } diff --git a/sensor.js b/sensor.js index abce3a6..94e5572 100644 --- a/sensor.js +++ b/sensor.js @@ -9,7 +9,7 @@ export class Sensor { this.hitY = null; this.hitObject = null; this.angle = 0; - this.hitpoint = {x: null, y: null}; + this.hitpoint = { x: null, y: null }; } @@ -65,13 +65,13 @@ export class RaycastSensor extends Sensor { this.hitY = hitPos.y; this.endX = this.hitX; this.endY = this.hitY; - this.hitpoint = {x: this.hitX, y: 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.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!"); @@ -115,22 +115,34 @@ 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.hasLine = false; this.type = "color"; + this.fill = "red"; } read(robot, gameWorld) { - this.value = gameWorld.getFloorColor(robot.x, robot.y); + this.updatePosition(); + + this.value = gameWorld.getFloorColor(this.startX, this.startY); + this.hasLine = this.value; + if (this.value) { + this.fill = "green"; + } else { + this.fill = "red"; + } + //console.log(this.value); return this.value; } draw(ctx) { this.updatePosition(); - ctx.strokeStyle = "purple"; + ctx.lineWidth = 1; + ctx.strokeStyle = this.fill; ctx.fillStyle = "green" ctx.beginPath(); ctx.arc(this.startX, this.startY, 2, 0, 2 * Math.PI); - ctx.fillStyle = "red"; + ctx.fillStyle = this.fill; ctx.fill(); ctx.stroke(); diff --git a/style.css b/style.css index d254510..5494a9e 100644 --- a/style.css +++ b/style.css @@ -137,6 +137,7 @@ button { /* ===== Main Content ===== */ main { + height: 100%; display: flex; justify-content: center; padding: 16px; @@ -246,6 +247,9 @@ main { border: 1px solid #d1d5db; /* gray-300 */ height: 100%; + display: flex; + flex-direction: column; + min-width: 200px; } /* Right side: canvas + console */ diff --git a/todo.md b/todo.md index c5289f3..cee61d3 100644 --- a/todo.md +++ b/todo.md @@ -1,3 +1,10 @@ +BUGS? + Allow different caps in "Hello World" in lesson1? + Disallow copy/paste of objectives? + Require printing of arithmetic in lesson2? + Hide hints behind button? Maybe require a few fails first? + Variables persist past reset + DO Create test level of a track with obstacles all around