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