Compare commits
No commits in common. "2986d89fc5a622ad300f48268549e79dd17ce2f6" and "ba1adee770c39ff9072331e0a70175b2b477211f" have entirely different histories.
2986d89fc5
...
ba1adee770
11
game.js
11
game.js
|
|
@ -525,12 +525,6 @@ gameCanvas.addEventListener("wheel", (event) => {
|
||||||
offsetY = mouseY - worldY * scale;
|
offsetY = mouseY - worldY * scale;
|
||||||
});
|
});
|
||||||
|
|
||||||
const canvas = document.getElementById("gameCanvas");
|
|
||||||
window.addEventListener("resize", () => {
|
|
||||||
// console.log("RESIZE");
|
|
||||||
// gameWorld.resizeCanvas(canvas)
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
gameCanvas.addEventListener("mousedown", (event) => {
|
gameCanvas.addEventListener("mousedown", (event) => {
|
||||||
isPanning = true;
|
isPanning = true;
|
||||||
|
|
@ -580,7 +574,6 @@ function setupCanvas() {
|
||||||
const canvas = document.getElementById("gameCanvas");
|
const canvas = document.getElementById("gameCanvas");
|
||||||
const context = canvas.getContext("2d");
|
const context = canvas.getContext("2d");
|
||||||
|
|
||||||
|
|
||||||
// Get the device pixel ratio
|
// Get the device pixel ratio
|
||||||
const dpr = window.devicePixelRatio || 1;
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
|
||||||
|
|
@ -597,7 +590,7 @@ function setupCanvas() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call this function when the page loads
|
// Call this function when the page loads
|
||||||
fetch('./data/levels.json')
|
fetch('/data/levels.json')
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
gameWorld.levelData = data;
|
gameWorld.levelData = data;
|
||||||
|
|
@ -608,7 +601,7 @@ fetch('./data/levels.json')
|
||||||
// Start game loop
|
// Start game loop
|
||||||
gameLoop();
|
gameLoop();
|
||||||
|
|
||||||
showLesson(10);
|
showLesson(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
128
gameworld.js
128
gameworld.js
|
|
@ -19,21 +19,6 @@ export class GameWorld {
|
||||||
this.obstacles = [];
|
this.obstacles = [];
|
||||||
this.robots = [];
|
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;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -186,24 +171,24 @@ export class GameWorld {
|
||||||
getCentroid(vertices) {
|
getCentroid(vertices) {
|
||||||
let area = 0, cx = 0, cy = 0;
|
let area = 0, cx = 0, cy = 0;
|
||||||
|
|
||||||
for (let i = 0; i < vertices.length; i++) {
|
for (let i = 0; i < vertices.length; i++) {
|
||||||
const p1 = vertices[i];
|
const p1 = vertices[i];
|
||||||
const p2 = vertices[(i + 1) % vertices.length];
|
const p2 = vertices[(i + 1) % vertices.length];
|
||||||
const cross = p1.x * p2.y - p2.x * p1.y;
|
const cross = p1.x * p2.y - p2.x * p1.y;
|
||||||
|
|
||||||
area += cross;
|
area += cross;
|
||||||
cx += (p1.x + p2.x) * cross;
|
cx += (p1.x + p2.x) * cross;
|
||||||
cy += (p1.y + p2.y) * cross;
|
cy += (p1.y + p2.y) * cross;
|
||||||
}
|
}
|
||||||
|
|
||||||
area *= 0.5;
|
area *= 0.5;
|
||||||
if (area === 0) return vertices[0]; // fallback to first point
|
if (area === 0) return vertices[0]; // fallback to first point
|
||||||
|
|
||||||
cx /= (6 * area);
|
cx /= (6 * area);
|
||||||
cy /= (6 * area);
|
cy /= (6 * area);
|
||||||
|
|
||||||
return { x: cx, y: cy };
|
return { x: cx, y: cy };
|
||||||
}
|
}
|
||||||
|
|
||||||
addObstacle(vertices, strokeColor = "black", fillColor = "gray") {
|
addObstacle(vertices, strokeColor = "black", fillColor = "gray") {
|
||||||
// Sort vertices clockwise (Matter requires this)
|
// Sort vertices clockwise (Matter requires this)
|
||||||
|
|
@ -262,24 +247,15 @@ export class GameWorld {
|
||||||
|
|
||||||
// Return the floor color based on (x, y) coordinates
|
// Return the floor color based on (x, y) coordinates
|
||||||
getFloorColor(x, y) {
|
getFloorColor(x, y) {
|
||||||
const isUnderLine = this.isPointNearLine(x, y, this.floorLines, this.lineWidth);
|
return (x + y) % 50 < 25 ? "black" : "white"; // Example pattern
|
||||||
return isUnderLine;
|
|
||||||
// if (isUnderLine) {
|
|
||||||
// console.log("Robot is over a floor line");
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw the game world (e.g., obstacles, background)
|
// Draw the game world (e.g., obstacles, background)
|
||||||
draw(ctx) {
|
draw(ctx) {
|
||||||
//this.render(ctx);
|
|
||||||
|
|
||||||
|
|
||||||
this.drawFloorLines(ctx);
|
|
||||||
// Draw obstacles
|
// Draw obstacles
|
||||||
ctx.strokeStyle = "gray"; // Obstacle outline color
|
ctx.strokeStyle = "gray"; // Obstacle outline color
|
||||||
ctx.lineWidth = 2; // Optional: to make the outline thicker
|
ctx.lineWidth = 2; // Optional: to make the outline thicker
|
||||||
|
|
||||||
|
|
||||||
// Draw obstacles
|
// Draw obstacles
|
||||||
this.obstacles.forEach(body => {
|
this.obstacles.forEach(body => {
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
|
|
@ -337,45 +313,8 @@ export class GameWorld {
|
||||||
robot.draw(ctx); // Draw the robot's hull and sensors
|
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 = []) {
|
rayCast(startX, startY, endX, endY, ignoreBodies = []) {
|
||||||
let closestIntersection = null;
|
let closestIntersection = null;
|
||||||
let startPoint = { x: startX, y: startY };
|
let startPoint = { x: startX, y: startY };
|
||||||
|
|
@ -450,45 +389,8 @@ export class GameWorld {
|
||||||
return null; // No valid intersection
|
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,12 @@
|
||||||
<body>
|
<body>
|
||||||
<!-- Top Bar with Heading and Buttons -->
|
<!-- Top Bar with Heading and Buttons -->
|
||||||
<header>
|
<header>
|
||||||
<!-- <div class="header-inner">
|
<div class="header-inner">
|
||||||
<div>
|
<div>
|
||||||
<h1>Learn CircuitPython</h1>
|
<h1>Learn CircuitPython</h1>
|
||||||
<p>Write code, simulate physics, and see instant output</p>
|
<p>Write code, simulate physics, and see instant output</p>
|
||||||
</div>
|
</div>
|
||||||
</div> -->
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,10 +32,8 @@ async function initializePyodide() {
|
||||||
"type": sensor.type, // Individual sensor's type
|
"type": sensor.type, // Individual sensor's type
|
||||||
"angle": sensor.angle, // Individual sensor's angle
|
"angle": sensor.angle, // Individual sensor's angle
|
||||||
"distance": Math.round(sensor.distance * 100) / 100, // Individual sensor's distance
|
"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"]);
|
//console.log(sensorData["x"]);
|
||||||
sensorData = JSON.stringify(sensorData);
|
sensorData = JSON.stringify(sensorData);
|
||||||
return sensorData;
|
return sensorData;
|
||||||
|
|
@ -77,12 +75,6 @@ class RobotModule:
|
||||||
def get_distance_right(self):
|
def get_distance_right(self):
|
||||||
return self.get_sensors()[1]["distance"]
|
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):
|
def get_sensors(self):
|
||||||
return json.loads(get_sensor_data("sensors")) # Returns list of sensor dicts
|
return json.loads(get_sensor_data("sensors")) # Returns list of sensor dicts
|
||||||
|
|
||||||
|
|
|
||||||
17
readme.md
17
readme.md
|
|
@ -80,20 +80,3 @@ while True:
|
||||||
robot.move(1)
|
robot.move(1)
|
||||||
robot.turn(0)
|
robot.turn(0)
|
||||||
time.sleep(0.01)
|
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)
|
|
||||||
7
robot.js
7
robot.js
|
|
@ -24,8 +24,7 @@ export class Robot {
|
||||||
|
|
||||||
this.addSensor(new RaycastSensor(this, -40, 12, -45, 60));
|
this.addSensor(new RaycastSensor(this, -40, 12, -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, -20, 12));
|
this.addSensor(new FloorColorSensor(this, 0, 0));
|
||||||
this.addSensor(new FloorColorSensor(this, 20, 12));
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -44,6 +43,10 @@ export class Robot {
|
||||||
this.update_sensors(gameWorld);
|
this.update_sensors(gameWorld);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
draw(ctx){
|
||||||
|
this.draw(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
update_sensors(gameWorld) {
|
update_sensors(gameWorld) {
|
||||||
this.sensors.forEach(sensor => sensor.read(this, gameWorld));
|
this.sensors.forEach(sensor => sensor.read(this, gameWorld));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
24
sensor.js
24
sensor.js
|
|
@ -9,7 +9,7 @@ export class Sensor {
|
||||||
this.hitY = null;
|
this.hitY = null;
|
||||||
this.hitObject = null;
|
this.hitObject = null;
|
||||||
this.angle = 0;
|
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.hitY = hitPos.y;
|
||||||
this.endX = this.hitX;
|
this.endX = this.hitX;
|
||||||
this.endY = this.hitY;
|
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));
|
this.distance = Math.sqrt(Math.pow(this.hitX - this.startX, 2) + Math.pow(this.hitY - this.startY, 2));
|
||||||
} else {
|
} else {
|
||||||
this.hitX = null;
|
this.hitX = null;
|
||||||
this.hitY = 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));
|
this.distance = Math.sqrt(Math.pow(this.endX - this.startX, 2) + Math.pow(this.endY - this.startY, 2));
|
||||||
}
|
}
|
||||||
// console.log("Obstacle detected!");
|
// console.log("Obstacle detected!");
|
||||||
|
|
@ -115,34 +115,22 @@ export class RaycastSensor extends Sensor {
|
||||||
export class FloorColorSensor extends Sensor {
|
export class FloorColorSensor extends Sensor {
|
||||||
constructor(robot, offsetAngle, offsetDistance) {
|
constructor(robot, offsetAngle, offsetDistance) {
|
||||||
super(robot, offsetAngle, offsetDistance); // No angle offset, directly below robot
|
super(robot, offsetAngle, offsetDistance); // No angle offset, directly below robot
|
||||||
this.hasLine = false;
|
|
||||||
this.type = "color";
|
this.type = "color";
|
||||||
this.fill = "red";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
read(robot, gameWorld) {
|
read(robot, gameWorld) {
|
||||||
this.updatePosition();
|
this.value = gameWorld.getFloorColor(robot.x, robot.y);
|
||||||
|
|
||||||
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;
|
return this.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
draw(ctx) {
|
draw(ctx) {
|
||||||
this.updatePosition();
|
this.updatePosition();
|
||||||
|
|
||||||
ctx.lineWidth = 1;
|
ctx.strokeStyle = "purple";
|
||||||
ctx.strokeStyle = this.fill;
|
|
||||||
ctx.fillStyle = "green"
|
ctx.fillStyle = "green"
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(this.startX, this.startY, 2, 0, 2 * Math.PI);
|
ctx.arc(this.startX, this.startY, 2, 0, 2 * Math.PI);
|
||||||
ctx.fillStyle = this.fill;
|
ctx.fillStyle = "red";
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,6 @@ button {
|
||||||
|
|
||||||
/* ===== Main Content ===== */
|
/* ===== Main Content ===== */
|
||||||
main {
|
main {
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
|
@ -247,9 +246,6 @@ main {
|
||||||
border: 1px solid #d1d5db;
|
border: 1px solid #d1d5db;
|
||||||
/* gray-300 */
|
/* gray-300 */
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-width: 200px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Right side: canvas + console */
|
/* Right side: canvas + console */
|
||||||
|
|
|
||||||
7
todo.md
7
todo.md
|
|
@ -1,10 +1,3 @@
|
||||||
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
|
DO
|
||||||
Create test level of a track with obstacles all around
|
Create test level of a track with obstacles all around
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue