diff --git a/data/lessons.js b/data/lessons.js
index 0cdbdf7..153411b 100644
--- a/data/lessons.js
+++ b/data/lessons.js
@@ -992,9 +992,9 @@ robot.move(0) # Stop the robot
};
- if (!codeRanGood) {
- return { done: false, hint: "" };
- }
+ // if (!codeRanGood) {
+ // return { done: false, progressArray: Object.values(progress), hint: "" };
+ // }
@@ -1022,7 +1022,7 @@ robot.move(0) # Stop the robot
})()
},
{
- id: 'robot1',
+ id: 'robot2',
title: '2. Steering the Robot',
tabtitle: 'Importing Modules',
level: 'robot',
@@ -1061,9 +1061,9 @@ robot.turn(0)
};
- if (!codeRanGood) {
- return { done: false, hint: "" };
- }
+ // if (!codeRanGood) {
+ // return { done: false, progressArray: Object.values(progress), hint: "" };
+ // }
@@ -1093,5 +1093,74 @@ robot.turn(0)
+ },
+ {
+ id: 'robot3',
+ title: '3. Using the Distance Sensors',
+ tabtitle: 'Importing Modules',
+ level: 'robot',
+ map: 'Level 3',
+ content: `
+
Turning is very similar to moving, we use the robot.turn(amount) function.
+ The amount parameter is a number between -1 and 1, where -1 is full left, 0 is no turn, and 1 is full right.
+
+
+import robot
+import time
+
+robot.turn(1)
+time.sleep(2)
+robot.turn(0)
+
+
+This code causes the robot to turn right at max speed for 2 seconds, then stop.
+
+You'll need to combine moving, turning, and waiting to reach all the checkpoints.
+Note: The values for move, turn, and sleep can all be decimal numbers (floats). ie time.sleep(0.5) or robot.move(0.8)
+ `,
+ objectives: [
+ "Reach the first checkpoint",
+
+ "Code should complete without errors"
+ ],
+
+ doneCondition: (() => {
+ return ({ code, consoleText, codeRanGood, gameWorld }) => {
+ const progress = {
+ firstCheckpoint: gameWorld.waypointsReached[0],
+ codeRanGood: codeRanGood,
+
+ };
+
+ // if (!codeRanGood) {
+ // return { done: false, progressArray: Object.values(progress), hint: "" };
+ // }
+
+
+
+ // 5. Build hint
+ const missing = [];
+ if (!progress.firstCheckpoint) missing.push("reach the first checkpoint");
+
+ let hint = "";
+ if (missing.length === 1) {
+ hint = `I still need you to ${missing[0]}`;
+ } else if (missing.length > 1) {
+ hint = `I still need you to ${missing.slice(0, -1).join(", ")} and ${missing.at(-1)}`;
+ }
+
+ return {
+ done:
+ progress.firstCheckpoint &&
+ progress.codeRanGood,
+ progressArray: Object.values(progress),
+ hint,
+ };
+ };
+ })()
+
+
+
+
},
];
diff --git a/data/levels.json b/data/levels.json
index 6d3ed60..e44465e 100644
--- a/data/levels.json
+++ b/data/levels.json
@@ -5,58 +5,50 @@
"player": {
"position": {
"x": 200,
- "y": 200
+ "y": 75
}
}
},
"waypoints": [
{
- "position": {
- "x": 420,
- "y": 200
- },
"vertices": [
{
- "x": -50,
- "y": -50
+ "x": 300,
+ "y": 40
},
{
- "x": 50,
- "y": -50
+ "x": 300,
+ "y": 110
},
{
- "x": 50,
- "y": 50
+ "x": 320,
+ "y": 110
},
{
- "x": -50,
- "y": 50
+ "x": 320,
+ "y": 40
}
],
"strokeColor": "#0000FF",
"fillColor": "#0000CC"
},
{
- "position": {
- "x": 50,
- "y": 200
- },
"vertices": [
{
- "x": -50,
- "y": -50
+ "x": 100,
+ "y": 40
},
{
- "x": 50,
- "y": -50
+ "x": 100,
+ "y": 110
},
{
- "x": 50,
- "y": 50
+ "x": 120,
+ "y": 110
},
{
- "x": -50,
- "y": 50
+ "x": 120,
+ "y": 40
}
],
"strokeColor": "#0000FF",
@@ -67,130 +59,88 @@
{
"vertices": [
{
- "x": -100,
- "y": -100
+ "x": 0,
+ "y": 0
},
{
- "x": 100,
- "y": -100
+ "x": 850,
+ "y": 0
},
{
- "x": 100,
- "y": 100
+ "x": 850,
+ "y": 20
},
{
- "x": -100,
- "y": 100
+ "x": 0,
+ "y": 20
}
],
- "position": {
- "x": 800,
- "y": 300
- },
"strokeColor": "#999999",
"fillColor": "#CCCCCC"
},
{
"vertices": [
{
- "x": -10,
- "y": -10
+ "x": 830,
+ "y": 20
},
{
- "x": 1010,
- "y": -10
+ "x": 850,
+ "y": 20
},
{
- "x": 1010,
- "y": 10
+ "x": 850,
+ "y": 600
},
{
- "x": -10,
- "y": 10
+ "x": 830,
+ "y": 600
}
],
- "position": {
- "x": 500,
- "y": 0
- },
"strokeColor": "#999999",
"fillColor": "#CCCCCC"
},
{
"vertices": [
{
- "x": -10,
- "y": -10
+ "x": 0,
+ "y": 600
},
{
- "x": 1010,
- "y": -10
+ "x": 850,
+ "y": 600
},
{
- "x": 1010,
- "y": 10
+ "x": 850,
+ "y": 620
},
{
- "x": -10,
- "y": 10
+ "x": 0,
+ "y": 620
}
],
- "position": {
- "x": 500,
- "y": 620
- },
"strokeColor": "#999999",
"fillColor": "#CCCCCC"
},
{
"vertices": [
{
- "x": -10,
- "y": 10
+ "x": 0,
+ "y": 20
},
{
- "x": 10,
- "y": 10
+ "x": 20,
+ "y": 20
},
{
- "x": 10,
- "y": 610
+ "x": 20,
+ "y": 600
},
{
- "x": -10,
- "y": 610
+ "x": 0,
+ "y": 600
}
],
- "position": {
- "x": 0,
- "y": 310
- },
- "strokeColor": "#999999",
- "fillColor": "#CCCCCC"
- },
- {
- "vertices": [
- {
- "x": -10,
- "y": 10
- },
- {
- "x": 10,
- "y": 10
- },
- {
- "x": 10,
- "y": 610
- },
- {
- "x": -10,
- "y": 610
- }
- ],
- "position": {
- "x": 1000,
- "y": 310
- },
"strokeColor": "#999999",
"fillColor": "#CCCCCC"
}
@@ -202,58 +152,50 @@
"player": {
"position": {
"x": 200,
- "y": 200
+ "y": 75
}
}
},
"waypoints": [
{
- "position": {
- "x": 420,
- "y": 200
- },
"vertices": [
{
- "x": -50,
- "y": -50
+ "x": 300,
+ "y": 40
},
{
- "x": 50,
- "y": -50
+ "x": 300,
+ "y": 110
},
{
- "x": 50,
- "y": 50
+ "x": 320,
+ "y": 110
},
{
- "x": -50,
- "y": 50
+ "x": 320,
+ "y": 40
}
],
"strokeColor": "#0000FF",
"fillColor": "#0000CC"
},
{
- "position": {
- "x": 420,
- "y": 500
- },
"vertices": [
{
- "x": -50,
- "y": -50
+ "x": 300,
+ "y": 200
},
{
- "x": 50,
- "y": -50
+ "x": 320,
+ "y": 200
},
{
- "x": 50,
- "y": 50
+ "x": 320,
+ "y": 280
},
{
- "x": -50,
- "y": 50
+ "x": 300,
+ "y": 280
}
],
"strokeColor": "#0000FF",
@@ -264,130 +206,487 @@
{
"vertices": [
{
- "x": -100,
- "y": -100
+ "x": 0,
+ "y": 0
},
{
- "x": 100,
- "y": -100
+ "x": 850,
+ "y": 0
},
{
- "x": 100,
+ "x": 850,
+ "y": 20
+ },
+ {
+ "x": 0,
+ "y": 20
+ }
+ ],
+ "strokeColor": "#999999",
+ "fillColor": "#CCCCCC"
+ },
+ {
+ "vertices": [
+ {
+ "x": 830,
+ "y": 20
+ },
+ {
+ "x": 850,
+ "y": 20
+ },
+ {
+ "x": 850,
+ "y": 600
+ },
+ {
+ "x": 830,
+ "y": 600
+ }
+ ],
+ "strokeColor": "#999999",
+ "fillColor": "#CCCCCC"
+ },
+ {
+ "vertices": [
+ {
+ "x": 0,
+ "y": 600
+ },
+ {
+ "x": 850,
+ "y": 600
+ },
+ {
+ "x": 850,
+ "y": 620
+ },
+ {
+ "x": 0,
+ "y": 620
+ }
+ ],
+ "strokeColor": "#999999",
+ "fillColor": "#CCCCCC"
+ },
+ {
+ "vertices": [
+ {
+ "x": 0,
+ "y": 20
+ },
+ {
+ "x": 20,
+ "y": 20
+ },
+ {
+ "x": 20,
+ "y": 600
+ },
+ {
+ "x": 0,
+ "y": 600
+ }
+ ],
+ "strokeColor": "#999999",
+ "fillColor": "#CCCCCC"
+ }
+ ]
+ },
+ {
+ "name": "Level 3",
+ "robots": {
+ "player": {
+ "position": {
+ "x": 50,
+ "y": 70
+ }
+ }
+ },
+ "waypoints": [
+ {
+ "vertices": [
+ {
+ "x": 340,
+ "y": 270
+ },
+ {
+ "x": 340,
+ "y": 360
+ },
+ {
+ "x": 360,
+ "y": 360
+ },
+ {
+ "x": 360,
+ "y": 270
+ }
+ ],
+ "strokeColor": "#0000FF",
+ "fillColor": "#0000CC"
+ }
+ ],
+ "obstacles": [
+ {
+ "vertices": [
+ {
+ "x": 0,
+ "y": 0
+ },
+ {
+ "x": 850,
+ "y": 0
+ },
+ {
+ "x": 850,
+ "y": 20
+ },
+ {
+ "x": 0,
+ "y": 20
+ }
+ ],
+ "strokeColor": "#999999",
+ "fillColor": "#CCCCCC"
+ },
+ {
+ "vertices": [
+ {
+ "x": 830,
+ "y": 20
+ },
+ {
+ "x": 850,
+ "y": 20
+ },
+ {
+ "x": 850,
+ "y": 600
+ },
+ {
+ "x": 830,
+ "y": 600
+ }
+ ],
+ "strokeColor": "#999999",
+ "fillColor": "#CCCCCC"
+ },
+ {
+ "vertices": [
+ {
+ "x": 0,
+ "y": 600
+ },
+ {
+ "x": 850,
+ "y": 600
+ },
+ {
+ "x": 850,
+ "y": 620
+ },
+ {
+ "x": 0,
+ "y": 620
+ }
+ ],
+ "strokeColor": "#999999",
+ "fillColor": "#CCCCCC"
+ },
+ {
+ "vertices": [
+ {
+ "x": 0,
+ "y": 20
+ },
+ {
+ "x": 20,
+ "y": 20
+ },
+ {
+ "x": 20,
+ "y": 600
+ },
+ {
+ "x": 0,
+ "y": 600
+ }
+ ],
+ "strokeColor": "#999999",
+ "fillColor": "#CCCCCC"
+ },
+ {
+ "vertices": [
+ {
+ "x": 330,
+ "y": 20
+ },
+ {
+ "x": 410,
"y": 100
},
{
- "x": -100,
+ "x": 430,
+ "y": 100
+ },
+ {
+ "x": 430,
+ "y": 20
+ }
+ ],
+ "strokeColor": "#999999",
+ "fillColor": "#CCCCCC"
+ },
+ {
+ "vertices": [
+ {
+ "x": 410,
+ "y": 100
+ },
+ {
+ "x": 410,
+ "y": 160
+ },
+ {
+ "x": 430,
+ "y": 160
+ },
+ {
+ "x": 430,
"y": 100
}
],
- "position": {
- "x": 800,
- "y": 300
- },
"strokeColor": "#999999",
"fillColor": "#CCCCCC"
},
{
"vertices": [
{
- "x": -10,
- "y": -10
+ "x": 410,
+ "y": 160
},
{
- "x": 1010,
- "y": -10
+ "x": 330,
+ "y": 240
},
{
- "x": 1010,
- "y": 10
+ "x": 330,
+ "y": 260
},
{
- "x": -10,
- "y": 10
+ "x": 430,
+ "y": 260
+ },
+ {
+ "x": 430,
+ "y": 160
}
],
- "position": {
- "x": 500,
- "y": 0
- },
"strokeColor": "#999999",
"fillColor": "#CCCCCC"
},
{
"vertices": [
{
- "x": -10,
- "y": -10
+ "x": 330,
+ "y": 240
},
{
- "x": 1010,
- "y": -10
+ "x": 130,
+ "y": 240
},
{
- "x": 1010,
- "y": 10
+ "x": 130,
+ "y": 260
},
{
- "x": -10,
- "y": 10
+ "x": 330,
+ "y": 260
}
],
- "position": {
- "x": 500,
- "y": 620
- },
"strokeColor": "#999999",
"fillColor": "#CCCCCC"
},
{
"vertices": [
{
- "x": -10,
- "y": 10
+ "x": 300,
+ "y": 120
},
{
- "x": 10,
- "y": 10
+ "x": 300,
+ "y": 140
},
{
- "x": 10,
- "y": 610
+ "x": 20,
+ "y": 140
},
{
- "x": -10,
- "y": 610
+ "x": 20,
+ "y": 120
}
],
- "position": {
- "x": 0,
- "y": 310
- },
"strokeColor": "#999999",
"fillColor": "#CCCCCC"
},
{
"vertices": [
{
- "x": -10,
- "y": 10
+ "x": 20,
+ "y": 140
},
{
- "x": 10,
- "y": 10
+ "x": 90,
+ "y": 140
},
{
- "x": 10,
- "y": 610
- },
- {
- "x": -10,
- "y": 610
+ "x": 20,
+ "y": 210
+ }
+ ],
+ "strokeColor": "#999999",
+ "fillColor": "#CCCCCC"
+ },
+ {
+ "vertices": [
+ {
+ "x": 130,
+ "y": 260
+ },
+ {
+ "x": 130,
+ "y": 480
+ },
+ {
+ "x": 150,
+ "y": 480
+ },
+ {
+ "x": 150,
+ "y": 260
+ }
+ ],
+ "strokeColor": "#999999",
+ "fillColor": "#CCCCCC"
+ },
+ {
+ "vertices": [
+ {
+ "x": 20,
+ "y": 530
+ },
+ {
+ "x": 90,
+ "y": 600
+ },
+ {
+ "x": 20,
+ "y": 600
+ }
+ ],
+ "strokeColor": "#999999",
+ "fillColor": "#CCCCCC"
+ },
+ {
+ "vertices": [
+ {
+ "x": 260,
+ "y": 600
+ },
+ {
+ "x": 260,
+ "y": 370
+ },
+ {
+ "x": 280,
+ "y": 370
+ },
+ {
+ "x": 280,
+ "y": 600
+ }
+ ],
+ "strokeColor": "#999999",
+ "fillColor": "#CCCCCC"
+ },
+ {
+ "vertices": [
+ {
+ "x": 150,
+ "y": 350
+ },
+ {
+ "x": 240,
+ "y": 260
+ },
+ {
+ "x": 150,
+ "y": 260
+ }
+ ],
+ "strokeColor": "#999999",
+ "fillColor": "#CCCCCC"
+ },
+ {
+ "vertices": [
+ {
+ "x": 280,
+ "y": 370
+ },
+ {
+ "x": 430,
+ "y": 370
+ },
+ {
+ "x": 430,
+ "y": 390
+ },
+ {
+ "x": 280,
+ "y": 390
+ }
+ ],
+ "strokeColor": "#999999",
+ "fillColor": "#CCCCCC"
+ },
+ {
+ "vertices": [
+ {
+ "x": 430,
+ "y": 260
+ },
+ {
+ "x": 430,
+ "y": 370
+ },
+ {
+ "x": 410,
+ "y": 370
+ },
+ {
+ "x": 410,
+ "y": 260
+ }
+ ],
+ "strokeColor": "#999999",
+ "fillColor": "#CCCCCC"
+ },
+ {
+ "vertices": [
+ {
+ "x": 170,
+ "y": 600
+ },
+ {
+ "x": 260,
+ "y": 510
+ },
+ {
+ "x": 260,
+ "y": 600
}
],
- "position": {
- "x": 1000,
- "y": 310
- },
"strokeColor": "#999999",
"fillColor": "#CCCCCC"
}
diff --git a/editor.html b/editor.html
new file mode 100644
index 0000000..4af44fd
--- /dev/null
+++ b/editor.html
@@ -0,0 +1,39 @@
+
+
+
+
+ Level Editor with Absolute Vertices and Level Load
+
+
+
+ Level Editor
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/editor.js b/editor.js
new file mode 100644
index 0000000..06c0537
--- /dev/null
+++ b/editor.js
@@ -0,0 +1,279 @@
+const canvas = document.getElementById("canvas");
+ const ctx = canvas.getContext("2d");
+
+ const gridSize = 10;
+ let currentPolygon = [];
+ let shapes = {
+ robots: {
+ player: { position: { x: 200, y: 200 } }
+ },
+ waypoints: [],
+ obstacles: []
+ };
+
+ let view = {
+ zoom: 1,
+ offsetX: 0,
+ offsetY: 0
+ };
+
+ let loadedLevels = []; // store loaded levels here
+
+ // Coordinate conversions
+ function worldToScreen(x, y) {
+ return {
+ x: (x - view.offsetX) * view.zoom,
+ y: (y - view.offsetY) * view.zoom
+ };
+ }
+
+ function screenToWorld(x, y) {
+ return {
+ x: x / view.zoom + view.offsetX,
+ y: y / view.zoom + view.offsetY
+ };
+ }
+
+ // Draw grid
+ function drawGrid() {
+ const step = gridSize;
+ const bounds = {
+ left: view.offsetX,
+ right: view.offsetX + canvas.width / view.zoom,
+ top: view.offsetY,
+ bottom: view.offsetY + canvas.height / view.zoom
+ };
+
+ ctx.strokeStyle = "#eee";
+ ctx.lineWidth = 1;
+ ctx.beginPath();
+ for (let x = Math.floor(bounds.left / step) * step; x < bounds.right; x += step) {
+ const sx = worldToScreen(x, 0).x;
+ ctx.moveTo(sx, 0);
+ ctx.lineTo(sx, canvas.height);
+ }
+ for (let y = Math.floor(bounds.top / step) * step; y < bounds.bottom; y += step) {
+ const sy = worldToScreen(0, y).y;
+ ctx.moveTo(0, sy);
+ ctx.lineTo(canvas.width, sy);
+ }
+ ctx.stroke();
+ }
+
+ // Draw a circle (for vertices)
+ function drawCircle(x, y, radius = 4) {
+ const screen = worldToScreen(x, y);
+ ctx.beginPath();
+ ctx.arc(screen.x, screen.y, radius, 0, Math.PI * 2);
+ ctx.fill();
+ }
+
+ // Draw polygon from absolute vertices
+ function drawPolygon(vertices, stroke = "#000", fill = "#ccc") {
+ if (vertices.length === 0) return;
+ ctx.beginPath();
+ const first = worldToScreen(vertices[0].x, vertices[0].y);
+ ctx.moveTo(first.x, first.y);
+ for (let i = 1; i < vertices.length; i++) {
+ const p = worldToScreen(vertices[i].x, vertices[i].y);
+ ctx.lineTo(p.x, p.y);
+ }
+ ctx.closePath();
+ ctx.fillStyle = fill;
+ ctx.fill();
+ ctx.strokeStyle = stroke;
+ ctx.stroke();
+ }
+
+ // Redraw entire canvas
+ function redrawAll() {
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ drawGrid();
+
+ for (let w of shapes.waypoints) {
+ drawPolygon(w.vertices, w.strokeColor, w.fillColor);
+ }
+ for (let o of shapes.obstacles) {
+ drawPolygon(o.vertices, o.strokeColor, o.fillColor);
+ }
+
+ if (currentPolygon.length > 0) {
+ ctx.strokeStyle = "#000";
+ ctx.fillStyle = "black";
+
+ for (let p of currentPolygon) {
+ drawCircle(p.x, p.y);
+ }
+
+ ctx.beginPath();
+ const start = worldToScreen(currentPolygon[0].x, currentPolygon[0].y);
+ ctx.moveTo(start.x, start.y);
+ for (let i = 1; i < currentPolygon.length; i++) {
+ const p = worldToScreen(currentPolygon[i].x, currentPolygon[i].y);
+ ctx.lineTo(p.x, p.y);
+ }
+ ctx.stroke();
+ }
+ }
+
+ // Snap to grid if ctrl pressed
+ function snapToGrid(x, y) {
+ return {
+ x: Math.round(x / gridSize) * gridSize,
+ y: Math.round(y / gridSize) * gridSize
+ };
+ }
+
+ // Canvas click event to add vertices
+ canvas.addEventListener("click", (e) => {
+ const rect = canvas.getBoundingClientRect();
+ let sx = e.clientX - rect.left;
+ let sy = e.clientY - rect.top;
+ let { x, y } = screenToWorld(sx, sy);
+
+ if (e.ctrlKey) {
+ ({ x, y } = snapToGrid(x, y));
+ }
+
+ if (currentPolygon.length > 0) {
+ const dx = x - currentPolygon[0].x;
+ const dy = y - currentPolygon[0].y;
+ if (Math.sqrt(dx * dx + dy * dy) < 10 / view.zoom) {
+ finishPolygon();
+ return;
+ }
+ }
+
+ currentPolygon.push({ x, y });
+ redrawAll();
+ });
+
+ // Finish polygon and add to shapes
+ function finishPolygon() {
+ if (currentPolygon.length < 3) return;
+
+ const type = document.getElementById("drawMode").value;
+ const shape = {
+ vertices: [...currentPolygon],
+ strokeColor: type === "waypoints" ? "#0000FF" : "#999999",
+ fillColor: type === "waypoints" ? "#0000CC" : "#CCCCCC"
+ };
+
+ shapes[type].push(shape);
+ currentPolygon = [];
+ redrawAll();
+ }
+ //document.getElementById("finishPolygon").addEventListener("click", finishPolygon);
+
+ // Save JSON to file
+ document.getElementById("saveJSON").addEventListener("click", () => {
+ const levelName = document.getElementById("levelName").value;
+ const exportData = [{
+ name: levelName,
+ ...shapes
+ }];
+ const blob = new Blob([JSON.stringify(exportData, null, 4)], { type: "application/json" });
+ const a = document.createElement("a");
+ a.href = URL.createObjectURL(blob);
+ a.download = levelName.replace(/\s+/g, "_") + ".json";
+ a.click();
+ });
+
+ // Zoom with mouse wheel, zoom toward cursor
+ canvas.addEventListener("wheel", (e) => {
+ e.preventDefault();
+ const delta = -e.deltaY;
+ const zoomFactor = 1.1;
+ const mouse = screenToWorld(e.offsetX, e.offsetY);
+
+ if (delta > 0) {
+ view.zoom *= zoomFactor;
+ } else {
+ view.zoom /= zoomFactor;
+ }
+
+ view.offsetX = mouse.x - (e.offsetX / view.zoom);
+ view.offsetY = mouse.y - (e.offsetY / view.zoom);
+
+ redrawAll();
+ }, { passive: false });
+
+ // Undo with Ctrl+Z
+ document.addEventListener("keydown", (e) => {
+ if (e.ctrlKey && e.key === "z") {
+ if (currentPolygon.length > 0) {
+ currentPolygon.pop();
+ } else {
+ const type = document.getElementById("drawMode").value;
+ if (shapes[type].length > 0) {
+ shapes[type].pop();
+ }
+ }
+ redrawAll();
+ }
+ });
+
+ // --------- NEW: Load Levels from data/levels.json ---------
+ async function loadLevels() {
+ try {
+ const response = await fetch('data/levels.json');
+ if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
+ loadedLevels = await response.json();
+
+ const levelSelect = document.getElementById('levelSelect');
+ levelSelect.innerHTML = ''; // Clear loading message
+
+ loadedLevels.forEach((level, idx) => {
+ const option = document.createElement('option');
+ option.value = idx;
+ option.textContent = level.name || `Level ${idx + 1}`;
+ levelSelect.appendChild(option);
+ });
+ } catch (err) {
+ alert('Failed to load levels.json: ' + err.message);
+ const levelSelect = document.getElementById('levelSelect');
+ levelSelect.innerHTML = '';
+ }
+ }
+
+ function loadSelectedLevel() {
+ const levelSelect = document.getElementById('levelSelect');
+ const idx = parseInt(levelSelect.value);
+ if (isNaN(idx) || !loadedLevels[idx]) {
+ alert('Please select a valid level');
+ return;
+ }
+
+ const level = loadedLevels[idx];
+ document.getElementById('levelName').value = level.name || '';
+
+ if (level.robots && level.robots.player && level.robots.player.position) {
+ shapes.robots.player.position = { ...level.robots.player.position };
+ } else {
+ shapes.robots.player.position = { x: 200, y: 200 };
+ }
+
+ shapes.waypoints = (level.waypoints || []).map(wp => ({
+ vertices: wp.vertices.map(v => ({ x: v.x, y: v.y })),
+ strokeColor: wp.strokeColor || "#0000FF",
+ fillColor: wp.fillColor || "#0000CC"
+ }));
+
+ shapes.obstacles = (level.obstacles || []).map(ob => ({
+ vertices: ob.vertices.map(v => ({ x: v.x, y: v.y })),
+ strokeColor: ob.strokeColor || "#999999",
+ fillColor: ob.fillColor || "#CCCCCC"
+ }));
+
+ currentPolygon = [];
+ redrawAll();
+ }
+
+
+ document.getElementById('loadLevelBtn').addEventListener('click', loadSelectedLevel);
+
+ // Load levels.json on page load
+ loadLevels();
+
+ // Initial draw
+ redrawAll();
\ No newline at end of file
diff --git a/game.js b/game.js
index 0820d9d..d4ee26e 100644
--- a/game.js
+++ b/game.js
@@ -32,7 +32,7 @@ function showLesson(index) {
console.log("Setting current level to:", i);
break;
}
- }
+ }
resetGameWorld();
} else {
document.getElementById('gameCanvas').style.display = 'none';
@@ -95,7 +95,8 @@ document.getElementById('next-lesson').addEventListener('click', () => {
function checkLessonDone() {
//consoleText = outputText; // Update console text
- if (consoleText.includes("Error") || consoleText.includes("Exception")) {
+ if (consoleText.includes("Error") || consoleText.includes("Exception") || consoleText.includes("PythonError")) {
+ console.log(consoleText);
codeRanGood = false;
} else {
codeRanGood = true;
@@ -114,17 +115,18 @@ function checkLessonDone() {
if (result.done) {
markLessonDone(lesson.id);
}
- if (result.progressArray) {
- console.log("Progress: ", result.progressArray);
- for (let i = 0; i < result.progressArray.length; i++) {
- const objective = result.progressArray[i];
- if (objective) {
- toggleObjective(i, true); // Mark as completed
- } else {
- toggleObjective(i, false); // Mark as not completed
- }
+ console.log(result);
+ //if (result.progressArray) {
+ console.log("Progress: ", result.progressArray);
+ for (let i = 0; i < result.progressArray.length; i++) {
+ const objective = result.progressArray[i];
+ if (objective) {
+ toggleObjective(i, true); // Mark as completed
+ } else {
+ toggleObjective(i, false); // Mark as not completed
}
}
+ //}
if (result.hint) {
logToConsole("Hint: " + result.hint, false);
//console.log("Hint:", result.hint); // Or show it in your console UI
@@ -352,6 +354,7 @@ function logToConsole(text, checkLesson = true) {
function clearConsole() {
logLines.length = 0; // Empty the logLines array
consoleElement.innerHTML = ""; // Clear the console DOM element
+ consoleText = "";
}
// ✅ Game Control Functions
@@ -423,10 +426,15 @@ function gameLoop(timestamp) {
ctx.translate(offsetX, offsetY); // Apply panning
ctx.scale(scale, scale); // Apply zooming
-
+
gameWorld.update();
gameWorld.draw(ctx);
-console.log(gameWorld.waypointsReached);
+
+ if (gameWorld.waypointsReachedChanged()) {
+ console.log("Waypoints reached:", gameWorld.waypointsReached);
+ checkLessonDone();
+ }
+
// if (gameWorld.checkPlayerCompletedTask()) {
// logToConsole("✅ Task Completed! ✅");
// togglePause();
@@ -463,13 +471,19 @@ function updateSensorData() {
document.getElementById("compile-button").addEventListener("click", () => {
if (paused) return;
document.getElementById('compile-button').disabled = true;
+ clearConsole();
resetGameWorld();
// Use the Monaco Editor instance to get the code
let code = monacoEditor.getValue(); // Get text from the editor
//console.log(code);
code = code.replace(/time\.sleep\(/g, "await time.sleep(");
- //console.log(code);
+
+ // code = code.replace(
+ // /^([ \t]*)while True:\s*$/gm,
+ // (_, indent) => `${indent}while True:\n${indent} time.sleep(0.0001)`
+ // );
+ console.log(code);
consoleElement.innerHTML = "";
pyodideWorker.postMessage({
type: "execute",
@@ -586,7 +600,7 @@ fetch('/data/levels.json')
// Start game loop
gameLoop();
- showLesson(11);
+ showLesson(12);
});
diff --git a/gameworld.js b/gameworld.js
index 5a480b1..bacf487 100644
--- a/gameworld.js
+++ b/gameworld.js
@@ -14,6 +14,7 @@ export class GameWorld {
this.currentLevel = 0;
this.waypoints = [];
+ this.prevWaypointsReached = [];
this.waypointsReached = [];
this.obstacles = [];
this.robots = [];
@@ -30,6 +31,7 @@ export class GameWorld {
this.obstacles = []
this.waypoints = [];
this.waypointsReached = [];
+ this.prevWaypointsReached = [];
Matter.World.clear(this.world); // Clear the world without resetting the engine
let level = this.levelData[this.currentLevel];
@@ -55,6 +57,7 @@ export class GameWorld {
this.addWaypoint(obstacle.vertices, obstacle.position, obstacle.strokeColor, obstacle.fillColor);
this.waypointsReached.push(false); // Initialize as not reached
}
+ this.prevWaypointsReached = [...this.waypointsReached];
console.log(level.waypointsReached)
// this.addObstacle([
@@ -106,6 +109,20 @@ export class GameWorld {
return this.waypointsReached;
}
+ waypointsReachedChanged() {
+ // Check if waypointsReached has changed since the last update
+ let changed = false;
+ for (let i = 0; i < this.waypointsReached.length; i++) {
+ if (this.waypointsReached[i] !== this.prevWaypointsReached[i]) {
+ changed = true;
+ break;
+ }
+ }
+ // Update prevWaypointsReached to the current state
+ this.prevWaypointsReached = [...this.waypointsReached];
+ return changed;
+ }
+
// Needs to be updated to handle different win conditions
// checkPlayerCompletedTask() {
// let playerPos = this.robots[0].body.position;
@@ -119,31 +136,100 @@ export class GameWorld {
addWaypoint(vertices, position = { x: 0, y: 0 }, strokeColor = "yellow", fillColor = "yellow") {
// Convert the polygon points into a Matter.js body
- let body = Matter.Bodies.fromVertices(position.x, position.y, [vertices], {
- isSensor: true,
- isStatic: true,
- label: "zone"
- });
- body.strokeColor = strokeColor;
- body.fillColor = fillColor;
- // Add body to world and store it
+ const sortedVertices = Matter.Vertices.clockwiseSort(vertices);
+
+ // Compute centroid
+ const centroid = this.getCentroid(sortedVertices);
+
+ // Create relative vertices centered around centroid
+ const relativeVertices = sortedVertices.map(v => ({
+ x: v.x - centroid.x,
+ y: v.y - centroid.y
+ }));
+
+
+ const body = Matter.Bodies.fromVertices(
+ centroid.x,
+ centroid.y,
+ [relativeVertices],
+ {
+ isSensor: true,
+ isStatic: true,
+ label: "zone",
+ },
+ true
+ );
+
+ body.strokeColor = 'rgba(0, 0, 255, 0.1)';
+ body.fillColor = 'rgba(0, 0, 255, 0.1)';
+
Matter.World.add(this.world, body);
this.waypoints.push(body);
}
- addObstacle(vertices, position = { x: 0, y: 0 }, strokeColor = "black", fillColor = "gray") {
- // Convert the polygon points into a Matter.js body
- let body = Matter.Bodies.fromVertices(position.x, position.y, [vertices], {
- isStatic: true, // Obstacles shouldn't move
- });
+ 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;
+
+ 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
+
+ cx /= (6 * area);
+ cy /= (6 * area);
+
+ return { x: cx, y: cy };
+}
+
+ addObstacle(vertices, strokeColor = "black", fillColor = "gray") {
+ // Sort vertices clockwise (Matter requires this)
+ const sortedVertices = Matter.Vertices.clockwiseSort(vertices);
+
+ // Compute centroid
+ const centroid = this.getCentroid(sortedVertices);
+
+ // Create relative vertices centered around centroid
+ const relativeVertices = sortedVertices.map(v => ({
+ x: v.x - centroid.x,
+ y: v.y - centroid.y
+ }));
+
+ // Create the body with options to reduce auto corrections
+ const body = Matter.Bodies.fromVertices(
+ centroid.x,
+ centroid.y,
+ [relativeVertices],
+ {
+ isStatic: true,
+ // If your Matter.js supports these:
+ // removeCollinear: 0.01,
+ // minimumArea: 10
+ },
+ true // autoHull (convex decomposition) enabled
+ );
+
body.strokeColor = strokeColor;
body.fillColor = fillColor;
- // Add body to world and store it
+
Matter.World.add(this.world, body);
this.obstacles.push(body);
}
+
+
+
+
+
+
addRobot(robot) {
console.log("added robot");
// Create the robot's Matter.js body
diff --git a/readme.md b/readme.md
index 99842aa..12db980 100644
--- a/readme.md
+++ b/readme.md
@@ -38,8 +38,7 @@ while True:
-
-
+#Robot 1
import time
import robot
@@ -49,4 +48,35 @@ robot.move(0)
robot.turn(1)
time.sleep(2.2)
robot.turn(0)
-robot.move(1)
\ No newline at end of file
+robot.move(1)
+
+
+#Robot 2
+import robot
+import time
+
+robot.move(1)
+time.sleep(0.8)
+robot.move(0.4)
+robot.turn(1)
+time.sleep(4.7)
+robot.move(0)
+robot.turn(0)
+
+
+#Robot 3
+import time
+import robot
+
+robot.move(1)
+while True:
+ if robot.get_distance_left() < 58:
+ robot.turn(1)
+ robot.move(0.2)
+ elif robot.get_distance_right() < 58:
+ robot.turn(-1)
+ robot.move(0.2)
+ else:
+ robot.move(1)
+ robot.turn(0)
+ time.sleep(0.01)
\ No newline at end of file