added basic line following, need to work it into lesson system

master
Jake 2025-07-17 14:10:54 +08:00
parent 5766cc7ab3
commit 2c0c13bc9b
9 changed files with 183 additions and 33 deletions

11
game.js
View File

@ -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);
});

View File

@ -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;
@ -188,7 +203,7 @@ export class GameWorld {
cy /= (6 * area);
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);
}
}

View File

@ -17,12 +17,12 @@
<body>
<!-- Top Bar with Heading and Buttons -->
<header>
<div class="header-inner">
<!-- <div class="header-inner">
<div>
<h1>Learn CircuitPython</h1>
<p>Write code, simulate physics, and see instant output</p>
</div>
</div>
</div> -->
</header>

View File

@ -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

View File

@ -80,3 +80,20 @@ while True:
robot.move(1)
robot.turn(0)
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)

View File

@ -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));
}

View File

@ -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();

View File

@ -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 */

View File

@ -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