added level editor, added third robot level (unfinished)

master
Jake 2025-07-06 17:10:06 +08:00
parent 3551ec747d
commit 4f8c2ba774
7 changed files with 1039 additions and 223 deletions

View File

@ -992,9 +992,9 @@ robot.move(0) # Stop the robot
}; };
if (!codeRanGood) { // if (!codeRanGood) {
return { done: false, hint: "" }; // 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', title: '2. Steering the Robot',
tabtitle: 'Importing Modules', tabtitle: 'Importing Modules',
level: 'robot', level: 'robot',
@ -1061,9 +1061,9 @@ robot.turn(0)
}; };
if (!codeRanGood) { // if (!codeRanGood) {
return { done: false, hint: "" }; // 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: `
<p>Turning is very similar to moving, we use the <code>robot.turn(amount)</code> function.</p>
<p>The <code>amount</code> parameter is a number between -1 and 1, where -1 is full left, 0 is no turn, and 1 is full right.</p>
<pre><code>
import robot
import time
robot.turn(1)
time.sleep(2)
robot.turn(0)
</code></pre>
</br>
<p>This code causes the robot to turn right at max speed for 2 seconds, then stop.</p>
<p>You'll need to combine moving, turning, and waiting to reach all the checkpoints.</p>
<p><strong>Note:</strong> The values for move, turn, and sleep can all be decimal numbers (floats). ie <code>time.sleep(0.5)</code> or <code>robot.move(0.8)</code></p>
`,
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,
};
};
})()
}, },
]; ];

View File

@ -5,58 +5,50 @@
"player": { "player": {
"position": { "position": {
"x": 200, "x": 200,
"y": 200 "y": 75
} }
} }
}, },
"waypoints": [ "waypoints": [
{ {
"position": {
"x": 420,
"y": 200
},
"vertices": [ "vertices": [
{ {
"x": -50, "x": 300,
"y": -50 "y": 40
}, },
{ {
"x": 50, "x": 300,
"y": -50 "y": 110
}, },
{ {
"x": 50, "x": 320,
"y": 50 "y": 110
}, },
{ {
"x": -50, "x": 320,
"y": 50 "y": 40
} }
], ],
"strokeColor": "#0000FF", "strokeColor": "#0000FF",
"fillColor": "#0000CC" "fillColor": "#0000CC"
}, },
{ {
"position": {
"x": 50,
"y": 200
},
"vertices": [ "vertices": [
{ {
"x": -50, "x": 100,
"y": -50 "y": 40
}, },
{ {
"x": 50, "x": 100,
"y": -50 "y": 110
}, },
{ {
"x": 50, "x": 120,
"y": 50 "y": 110
}, },
{ {
"x": -50, "x": 120,
"y": 50 "y": 40
} }
], ],
"strokeColor": "#0000FF", "strokeColor": "#0000FF",
@ -67,130 +59,88 @@
{ {
"vertices": [ "vertices": [
{ {
"x": -100, "x": 0,
"y": -100 "y": 0
}, },
{ {
"x": 100, "x": 850,
"y": -100 "y": 0
}, },
{ {
"x": 100, "x": 850,
"y": 100 "y": 20
}, },
{ {
"x": -100, "x": 0,
"y": 100 "y": 20
} }
], ],
"position": {
"x": 800,
"y": 300
},
"strokeColor": "#999999", "strokeColor": "#999999",
"fillColor": "#CCCCCC" "fillColor": "#CCCCCC"
}, },
{ {
"vertices": [ "vertices": [
{ {
"x": -10, "x": 830,
"y": -10 "y": 20
}, },
{ {
"x": 1010, "x": 850,
"y": -10 "y": 20
}, },
{ {
"x": 1010, "x": 850,
"y": 10 "y": 600
}, },
{ {
"x": -10, "x": 830,
"y": 10 "y": 600
} }
], ],
"position": {
"x": 500,
"y": 0
},
"strokeColor": "#999999", "strokeColor": "#999999",
"fillColor": "#CCCCCC" "fillColor": "#CCCCCC"
}, },
{ {
"vertices": [ "vertices": [
{ {
"x": -10, "x": 0,
"y": -10 "y": 600
}, },
{ {
"x": 1010, "x": 850,
"y": -10 "y": 600
}, },
{ {
"x": 1010, "x": 850,
"y": 10 "y": 620
}, },
{ {
"x": -10, "x": 0,
"y": 10 "y": 620
} }
], ],
"position": {
"x": 500,
"y": 620
},
"strokeColor": "#999999", "strokeColor": "#999999",
"fillColor": "#CCCCCC" "fillColor": "#CCCCCC"
}, },
{ {
"vertices": [ "vertices": [
{ {
"x": -10, "x": 0,
"y": 10 "y": 20
}, },
{ {
"x": 10, "x": 20,
"y": 10 "y": 20
}, },
{ {
"x": 10, "x": 20,
"y": 610 "y": 600
}, },
{ {
"x": -10, "x": 0,
"y": 610 "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", "strokeColor": "#999999",
"fillColor": "#CCCCCC" "fillColor": "#CCCCCC"
} }
@ -202,58 +152,50 @@
"player": { "player": {
"position": { "position": {
"x": 200, "x": 200,
"y": 200 "y": 75
} }
} }
}, },
"waypoints": [ "waypoints": [
{ {
"position": {
"x": 420,
"y": 200
},
"vertices": [ "vertices": [
{ {
"x": -50, "x": 300,
"y": -50 "y": 40
}, },
{ {
"x": 50, "x": 300,
"y": -50 "y": 110
}, },
{ {
"x": 50, "x": 320,
"y": 50 "y": 110
}, },
{ {
"x": -50, "x": 320,
"y": 50 "y": 40
} }
], ],
"strokeColor": "#0000FF", "strokeColor": "#0000FF",
"fillColor": "#0000CC" "fillColor": "#0000CC"
}, },
{ {
"position": {
"x": 420,
"y": 500
},
"vertices": [ "vertices": [
{ {
"x": -50, "x": 300,
"y": -50 "y": 200
}, },
{ {
"x": 50, "x": 320,
"y": -50 "y": 200
}, },
{ {
"x": 50, "x": 320,
"y": 50 "y": 280
}, },
{ {
"x": -50, "x": 300,
"y": 50 "y": 280
} }
], ],
"strokeColor": "#0000FF", "strokeColor": "#0000FF",
@ -264,130 +206,487 @@
{ {
"vertices": [ "vertices": [
{ {
"x": -100, "x": 0,
"y": -100 "y": 0
}, },
{ {
"x": 100, "x": 850,
"y": -100 "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 "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 "y": 100
} }
], ],
"position": {
"x": 800,
"y": 300
},
"strokeColor": "#999999", "strokeColor": "#999999",
"fillColor": "#CCCCCC" "fillColor": "#CCCCCC"
}, },
{ {
"vertices": [ "vertices": [
{ {
"x": -10, "x": 410,
"y": -10 "y": 160
}, },
{ {
"x": 1010, "x": 330,
"y": -10 "y": 240
}, },
{ {
"x": 1010, "x": 330,
"y": 10 "y": 260
}, },
{ {
"x": -10, "x": 430,
"y": 10 "y": 260
},
{
"x": 430,
"y": 160
} }
], ],
"position": {
"x": 500,
"y": 0
},
"strokeColor": "#999999", "strokeColor": "#999999",
"fillColor": "#CCCCCC" "fillColor": "#CCCCCC"
}, },
{ {
"vertices": [ "vertices": [
{ {
"x": -10, "x": 330,
"y": -10 "y": 240
}, },
{ {
"x": 1010, "x": 130,
"y": -10 "y": 240
}, },
{ {
"x": 1010, "x": 130,
"y": 10 "y": 260
}, },
{ {
"x": -10, "x": 330,
"y": 10 "y": 260
} }
], ],
"position": {
"x": 500,
"y": 620
},
"strokeColor": "#999999", "strokeColor": "#999999",
"fillColor": "#CCCCCC" "fillColor": "#CCCCCC"
}, },
{ {
"vertices": [ "vertices": [
{ {
"x": -10, "x": 300,
"y": 10 "y": 120
}, },
{ {
"x": 10, "x": 300,
"y": 10 "y": 140
}, },
{ {
"x": 10, "x": 20,
"y": 610 "y": 140
}, },
{ {
"x": -10, "x": 20,
"y": 610 "y": 120
} }
], ],
"position": {
"x": 0,
"y": 310
},
"strokeColor": "#999999", "strokeColor": "#999999",
"fillColor": "#CCCCCC" "fillColor": "#CCCCCC"
}, },
{ {
"vertices": [ "vertices": [
{ {
"x": -10, "x": 20,
"y": 10 "y": 140
}, },
{ {
"x": 10, "x": 90,
"y": 10 "y": 140
}, },
{ {
"x": 10, "x": 20,
"y": 610 "y": 210
}, }
{ ],
"x": -10, "strokeColor": "#999999",
"y": 610 "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", "strokeColor": "#999999",
"fillColor": "#CCCCCC" "fillColor": "#CCCCCC"
} }

39
editor.html Normal file
View File

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Level Editor with Absolute Vertices and Level Load</title>
<style>
canvas {
border: 1px solid black;
cursor: crosshair;
}
body {
font-family: sans-serif;
margin: 20px;
}
</style>
</head>
<body>
<h2>Level Editor</h2>
<label>Level Name: <input type="text" id="levelName" value="Level 1" /></label><br /><br />
<label>Mode:
<select id="drawMode">
<option value="waypoints">Waypoint</option>
<option value="obstacles">Obstacle</option>
</select>
</label><br /><br />
<!-- Load Level UI -->
<label>Load Level:
<select id="levelSelect"><option>Loading...</option></select>
<button id="loadLevelBtn">Load</button>
</label><br /><br />
<canvas id="canvas" width="1000" height="640"></canvas><br />
<button id="saveJSON">Download JSON</button>
<script src="editor.js" defer></script>
</body>
</html>

279
editor.js Normal file
View File

@ -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 = '<option>Error loading levels</option>';
}
}
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();

44
game.js
View File

@ -32,7 +32,7 @@ function showLesson(index) {
console.log("Setting current level to:", i); console.log("Setting current level to:", i);
break; break;
} }
} }
resetGameWorld(); resetGameWorld();
} else { } else {
document.getElementById('gameCanvas').style.display = 'none'; document.getElementById('gameCanvas').style.display = 'none';
@ -95,7 +95,8 @@ document.getElementById('next-lesson').addEventListener('click', () => {
function checkLessonDone() { function checkLessonDone() {
//consoleText = outputText; // Update console text //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; codeRanGood = false;
} else { } else {
codeRanGood = true; codeRanGood = true;
@ -114,17 +115,18 @@ function checkLessonDone() {
if (result.done) { if (result.done) {
markLessonDone(lesson.id); markLessonDone(lesson.id);
} }
if (result.progressArray) { console.log(result);
console.log("Progress: ", result.progressArray); //if (result.progressArray) {
for (let i = 0; i < result.progressArray.length; i++) { console.log("Progress: ", result.progressArray);
const objective = result.progressArray[i]; for (let i = 0; i < result.progressArray.length; i++) {
if (objective) { const objective = result.progressArray[i];
toggleObjective(i, true); // Mark as completed if (objective) {
} else { toggleObjective(i, true); // Mark as completed
toggleObjective(i, false); // Mark as not completed } else {
} toggleObjective(i, false); // Mark as not completed
} }
} }
//}
if (result.hint) { if (result.hint) {
logToConsole("Hint: " + result.hint, false); logToConsole("Hint: " + result.hint, false);
//console.log("Hint:", result.hint); // Or show it in your console UI //console.log("Hint:", result.hint); // Or show it in your console UI
@ -352,6 +354,7 @@ function logToConsole(text, checkLesson = true) {
function clearConsole() { function clearConsole() {
logLines.length = 0; // Empty the logLines array logLines.length = 0; // Empty the logLines array
consoleElement.innerHTML = ""; // Clear the console DOM element consoleElement.innerHTML = ""; // Clear the console DOM element
consoleText = "";
} }
// ✅ Game Control Functions // ✅ Game Control Functions
@ -423,10 +426,15 @@ function gameLoop(timestamp) {
ctx.translate(offsetX, offsetY); // Apply panning ctx.translate(offsetX, offsetY); // Apply panning
ctx.scale(scale, scale); // Apply zooming ctx.scale(scale, scale); // Apply zooming
gameWorld.update(); gameWorld.update();
gameWorld.draw(ctx); gameWorld.draw(ctx);
console.log(gameWorld.waypointsReached);
if (gameWorld.waypointsReachedChanged()) {
console.log("Waypoints reached:", gameWorld.waypointsReached);
checkLessonDone();
}
// if (gameWorld.checkPlayerCompletedTask()) { // if (gameWorld.checkPlayerCompletedTask()) {
// logToConsole("✅ Task Completed! ✅"); // logToConsole("✅ Task Completed! ✅");
// togglePause(); // togglePause();
@ -463,13 +471,19 @@ function updateSensorData() {
document.getElementById("compile-button").addEventListener("click", () => { document.getElementById("compile-button").addEventListener("click", () => {
if (paused) return; if (paused) return;
document.getElementById('compile-button').disabled = true; document.getElementById('compile-button').disabled = true;
clearConsole();
resetGameWorld(); resetGameWorld();
// Use the Monaco Editor instance to get the code // Use the Monaco Editor instance to get the code
let code = monacoEditor.getValue(); // Get text from the editor let code = monacoEditor.getValue(); // Get text from the editor
//console.log(code); //console.log(code);
code = code.replace(/time\.sleep\(/g, "await time.sleep("); 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 = ""; consoleElement.innerHTML = "";
pyodideWorker.postMessage({ pyodideWorker.postMessage({
type: "execute", type: "execute",
@ -586,7 +600,7 @@ fetch('/data/levels.json')
// Start game loop // Start game loop
gameLoop(); gameLoop();
showLesson(11); showLesson(12);
}); });

View File

@ -14,6 +14,7 @@ export class GameWorld {
this.currentLevel = 0; this.currentLevel = 0;
this.waypoints = []; this.waypoints = [];
this.prevWaypointsReached = [];
this.waypointsReached = []; this.waypointsReached = [];
this.obstacles = []; this.obstacles = [];
this.robots = []; this.robots = [];
@ -30,6 +31,7 @@ export class GameWorld {
this.obstacles = [] this.obstacles = []
this.waypoints = []; this.waypoints = [];
this.waypointsReached = []; this.waypointsReached = [];
this.prevWaypointsReached = [];
Matter.World.clear(this.world); // Clear the world without resetting the engine Matter.World.clear(this.world); // Clear the world without resetting the engine
let level = this.levelData[this.currentLevel]; let level = this.levelData[this.currentLevel];
@ -55,6 +57,7 @@ export class GameWorld {
this.addWaypoint(obstacle.vertices, obstacle.position, obstacle.strokeColor, obstacle.fillColor); this.addWaypoint(obstacle.vertices, obstacle.position, obstacle.strokeColor, obstacle.fillColor);
this.waypointsReached.push(false); // Initialize as not reached this.waypointsReached.push(false); // Initialize as not reached
} }
this.prevWaypointsReached = [...this.waypointsReached];
console.log(level.waypointsReached) console.log(level.waypointsReached)
// this.addObstacle([ // this.addObstacle([
@ -106,6 +109,20 @@ export class GameWorld {
return this.waypointsReached; 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 // Needs to be updated to handle different win conditions
// checkPlayerCompletedTask() { // checkPlayerCompletedTask() {
// let playerPos = this.robots[0].body.position; // 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") { addWaypoint(vertices, position = { x: 0, y: 0 }, strokeColor = "yellow", fillColor = "yellow") {
// Convert the polygon points into a Matter.js body // Convert the polygon points into a Matter.js body
let body = Matter.Bodies.fromVertices(position.x, position.y, [vertices], { const sortedVertices = Matter.Vertices.clockwiseSort(vertices);
isSensor: true,
isStatic: true, // Compute centroid
label: "zone" const centroid = this.getCentroid(sortedVertices);
});
body.strokeColor = strokeColor; // Create relative vertices centered around centroid
body.fillColor = fillColor; const relativeVertices = sortedVertices.map(v => ({
// Add body to world and store it 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); Matter.World.add(this.world, body);
this.waypoints.push(body); this.waypoints.push(body);
} }
addObstacle(vertices, position = { x: 0, y: 0 }, strokeColor = "black", fillColor = "gray") { getCentroid(vertices) {
// Convert the polygon points into a Matter.js body let area = 0, cx = 0, cy = 0;
let body = Matter.Bodies.fromVertices(position.x, position.y, [vertices], {
isStatic: true, // Obstacles shouldn't move 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.strokeColor = strokeColor;
body.fillColor = fillColor; body.fillColor = fillColor;
// Add body to world and store it
Matter.World.add(this.world, body); Matter.World.add(this.world, body);
this.obstacles.push(body); this.obstacles.push(body);
} }
addRobot(robot) { addRobot(robot) {
console.log("added robot"); console.log("added robot");
// Create the robot's Matter.js body // Create the robot's Matter.js body

View File

@ -38,8 +38,7 @@ while True:
#Robot 1
import time import time
import robot import robot
@ -49,4 +48,35 @@ robot.move(0)
robot.turn(1) robot.turn(1)
time.sleep(2.2) time.sleep(2.2)
robot.turn(0) robot.turn(0)
robot.move(1) 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)