From 8a0219500d19bf2f841095f04a3126f8a76ecd13 Mon Sep 17 00:00:00 2001 From: Jake Date: Fri, 4 Jul 2025 15:50:06 +0800 Subject: [PATCH] added objectives to first robot lesson, locked compile button while executing, game world resets on compile & run --- data/lessons.js | 163 +++++++++++++++++++--------------------------- data/levels.json | 28 +++++++- game.js | 26 +++++--- gameworld.js | 55 ++++++++++++---- pyodide-worker.js | 20 ++++-- style.css | 4 ++ 6 files changed, 172 insertions(+), 124 deletions(-) diff --git a/data/lessons.js b/data/lessons.js index f7f8ac3..b432f17 100644 --- a/data/lessons.js +++ b/data/lessons.js @@ -77,71 +77,71 @@ export const lessons = [ "Print a boolean (True/False) to the console" ], doneCondition: (() => { - // Matches print("something") or print('something') - const stringPrintRegex = /print\s*\(\s*(['"]).*?\1\s*\)/; + // Matches print("something") or print('something') + const stringPrintRegex = /print\s*\(\s*(['"]).*?\1\s*\)/; - // Matches print of float literal like 3.14, .5, -2.0, 1e-3 - const floatPrintRegex = /print\s*\(\s*[-+]?(?:\d+\.\d*|\.\d+|\d+[eE][-+]?\d+)\s*\)/; + // Matches print of float literal like 3.14, .5, -2.0, 1e-3 + const floatPrintRegex = /print\s*\(\s*[-+]?(?:\d+\.\d*|\.\d+|\d+[eE][-+]?\d+)\s*\)/; - // Matches print of int literal like 3, -42 (excluding floats) - const intPrintRegex = /print\s*\(\s*[-+]?\d+\s*\)/; + // Matches print of int literal like 3, -42 (excluding floats) + const intPrintRegex = /print\s*\(\s*[-+]?\d+\s*\)/; - // Matches print(True) or print(False), case-insensitive - const boolPrintRegex = /print\s*\(\s*(True|False)\s*\)/i; + // Matches print(True) or print(False), case-insensitive + const boolPrintRegex = /print\s*\(\s*(True|False)\s*\)/i; - return ({ code, codeRanGood }) => { - if (!codeRanGood) { - return { - done: false, - hint: "Your code had an error — try fixing it and run again." - }; - } + return ({ code, codeRanGood }) => { + if (!codeRanGood) { + return { + done: false, + hint: "Your code had an error — try fixing it and run again." + }; + } - const progress = { - stringDone: stringPrintRegex.test(code), - intDone: intPrintRegex.test(code), - floatDone: floatPrintRegex.test(code), - boolDone: boolPrintRegex.test(code), - }; + const progress = { + stringDone: stringPrintRegex.test(code), + intDone: intPrintRegex.test(code), + floatDone: floatPrintRegex.test(code), + boolDone: boolPrintRegex.test(code), + }; - // Fix false positives where float also matches int - if (progress.floatDone && progress.intDone) { - // Check if the float number has a dot or exponent; if not, it was a false int match - const matches = code.match(floatPrintRegex); - if (matches) { - const numbers = matches.map(m => m.match(/[-+]?(?:\d+\.\d*|\.\d+|\d+[eE][-+]?\d+)/)?.[0]); - for (const num of numbers) { - if (num && !num.includes('.') && !num.toLowerCase().includes('e')) { - // It's not a float, remove the float flag - progress.floatDone = false; + // Fix false positives where float also matches int + if (progress.floatDone && progress.intDone) { + // Check if the float number has a dot or exponent; if not, it was a false int match + const matches = code.match(floatPrintRegex); + if (matches) { + const numbers = matches.map(m => m.match(/[-+]?(?:\d+\.\d*|\.\d+|\d+[eE][-+]?\d+)/)?.[0]); + for (const num of numbers) { + if (num && !num.includes('.') && !num.toLowerCase().includes('e')) { + // It's not a float, remove the float flag + progress.floatDone = false; + } + } } } - } - } - const missing = []; - if (!progress.stringDone) missing.push("string"); - if (!progress.floatDone) missing.push("float"); - if (!progress.intDone) missing.push("int"); - if (!progress.boolDone) missing.push("boolean"); + const missing = []; + if (!progress.stringDone) missing.push("string"); + if (!progress.floatDone) missing.push("float"); + if (!progress.intDone) missing.push("int"); + if (!progress.boolDone) missing.push("boolean"); - let hint = ""; - if (missing.length > 0) { - hint = "I still need you to use print() with a "; - hint += missing.length === 1 - ? missing[0] - : missing.slice(0, -1).join(", ") + " and " + missing[missing.length - 1]; - } + let hint = ""; + if (missing.length > 0) { + hint = "I still need you to use print() with a "; + hint += missing.length === 1 + ? missing[0] + : missing.slice(0, -1).join(", ") + " and " + missing[missing.length - 1]; + } - const done = progress.stringDone && progress.intDone && progress.floatDone && progress.boolDone; + const done = progress.stringDone && progress.intDone && progress.floatDone && progress.boolDone; - return { - done, - progressArray: Object.values(progress), - hint - }; - }; -})() + return { + done, + progressArray: Object.values(progress), + hint + }; + }; + })() @@ -872,59 +872,31 @@ robot.move(0) # Stop the robot `, objectives: [ - "Import the time module", - "Print something", - "Use time.sleep() to pause for an amount of time", - "Print something else after the pause" + "Reach the first checkpoint", + "Reach the second checkpoint", + + "Code should complete without errors" ], doneCondition: (() => { - return ({ code, consoleText, codeRanGood }) => { + return ({ code, consoleText, codeRanGood, gameWorld }) => { const progress = { - importedTime: false, - printedBefore: false, - usedSleep: false, - printedAfter: false, + firstCheckpoint: gameWorld.waypointsReached[0], + secondCheckpoint: gameWorld.waypointsReached[1], + codeRanGood: codeRanGood, + }; if (!codeRanGood) { return { done: false, hint: "" }; } - // 1. Check for "import time" - const importRegex = /^\s*import\s+time\b/m; - progress.importedTime = importRegex.test(code); - // 2. Match all print(...) calls - const printRegex = /print\s*\(.*?\)/g; - const printMatches = [...code.matchAll(printRegex)]; - - // 3. Match time.sleep(...) - const sleepRegex = /time\.sleep\s*\(\s*[\d.]+\s*\)/; - const sleepMatch = sleepRegex.exec(code); - progress.usedSleep = !!sleepMatch; - - // 4. Handle print position logic - if (printMatches.length > 0) { - // If there's no sleep, we just say "they printed something" — early lesson support - if (!sleepMatch) { - progress.printedBefore = true; // consider *any* print valid before sleep - } else { - const sleepIndex = sleepMatch.index; - for (const m of printMatches) { - if (m.index < sleepIndex) progress.printedBefore = true; - if (m.index > sleepIndex) progress.printedAfter = true; - } - } - } // 5. Build hint const missing = []; - if (!progress.importedTime) missing.push("import the time module"); - if (!progress.printedBefore) missing.push("print something before sleeping"); - if (!progress.usedSleep) missing.push("use time.sleep()"); - if (!progress.printedAfter && progress.usedSleep) - missing.push("print something after sleeping"); + if (!progress.firstCheckpoint) missing.push("reach the first checkpoint"); + if (!progress.secondCheckpoint) missing.push("reach the second checkpoint"); let hint = ""; if (missing.length === 1) { @@ -935,10 +907,9 @@ robot.move(0) # Stop the robot return { done: - progress.importedTime && - progress.printedBefore && - progress.usedSleep && - progress.printedAfter, + progress.firstCheckpoint && + progress.secondCheckpoint && + progress.codeRanGood, progressArray: Object.values(progress), hint, }; diff --git a/data/levels.json b/data/levels.json index 2ce93d5..6f224d3 100644 --- a/data/levels.json +++ b/data/levels.json @@ -13,7 +13,33 @@ { "position": { "x": 420, - "y": 600 + "y": 200 + }, + "vertices": [ + { + "x": -50, + "y": -50 + }, + { + "x": 50, + "y": -50 + }, + { + "x": 50, + "y": 50 + }, + { + "x": -50, + "y": 50 + } + ], + "strokeColor": "#0000FF", + "fillColor": "#0000CC" + }, + { + "position": { + "x": 50, + "y": 200 }, "vertices": [ { diff --git a/game.js b/game.js index 53fef31..561bb64 100644 --- a/game.js +++ b/game.js @@ -92,7 +92,8 @@ function checkLessonDone() { const result = lesson.doneCondition({ code: mostRecentCode, consoleText: consoleText, - codeRanGood: codeRanGood + codeRanGood: codeRanGood, + gameWorld: gameWorld }); if (result.done) { markLessonDone(lesson.id); @@ -220,7 +221,7 @@ function toggleObjective(index, completed = true) { } //clearLessonProgress(); // Clear progress on load for testing -showLesson(1); +showLesson(9); const consoleElement = document.getElementById("console"); const gameCanvas = document.getElementById("gameCanvas"); @@ -269,6 +270,7 @@ function startPyodideWorker() { break; case "execution_done": console.log("Execution done"); + document.getElementById('compile-button').disabled = false; checkLessonDone(); break; } @@ -355,6 +357,12 @@ function togglePause() { document.getElementById("pause-button").innerText = paused ? "Resume" : "Pause"; } +function resetGameWorld() { + robots = createInitialRobots(); + gameWorld.reset(robots["player"]); + pyodideWorker.postMessage({ type: "interrupt" }); +} + // ✅ Reset Function (Fixed) function resetGame() { // Terminate the worker @@ -400,11 +408,12 @@ function gameLoop(timestamp) { gameWorld.update(); gameWorld.draw(ctx); - if (gameWorld.checkPlayerCompletedTask()) { - logToConsole("✅ Task Completed! ✅"); - togglePause(); - gameWorld.currentLevel++; - } + + // if (gameWorld.checkPlayerCompletedTask()) { + // logToConsole("✅ Task Completed! ✅"); + // togglePause(); + // gameWorld.currentLevel++; + // } pyodideWorker.postMessage({ type: "game_state", @@ -435,7 +444,8 @@ function updateSensorData() { document.getElementById("compile-button").addEventListener("click", () => { if (paused) return; - + document.getElementById('compile-button').disabled = true; + resetGameWorld(); // Use the Monaco Editor instance to get the code let code = monacoEditor.getValue(); // Get text from the editor //console.log(code); diff --git a/gameworld.js b/gameworld.js index 3084451..5a480b1 100644 --- a/gameworld.js +++ b/gameworld.js @@ -14,6 +14,7 @@ export class GameWorld { this.currentLevel = 0; this.waypoints = []; + this.waypointsReached = []; this.obstacles = []; this.robots = []; @@ -28,6 +29,7 @@ export class GameWorld { this.robots = [] this.obstacles = [] this.waypoints = []; + this.waypointsReached = []; Matter.World.clear(this.world); // Clear the world without resetting the engine let level = this.levelData[this.currentLevel]; @@ -51,7 +53,9 @@ export class GameWorld { let obstacle = level.waypoints[i]; console.log("Adding waypoint:", obstacle); this.addWaypoint(obstacle.vertices, obstacle.position, obstacle.strokeColor, obstacle.fillColor); + this.waypointsReached.push(false); // Initialize as not reached } + console.log(level.waypointsReached) // this.addObstacle([ // { x: -100, y: -100 }, // Vertex 1 @@ -77,7 +81,19 @@ export class GameWorld { this.robots[id].update(this); } + for (let i = 0; i < this.waypoints.length; i++) { + if (this.waypointsReached[i]) { + } else { + let waypoint = this.waypoints[i]; + let playerPos = this.robots[0].body.position; + let waypointBounds = waypoint.bounds; + if (Matter.Bounds.contains(waypointBounds, playerPos)) { + console.log("Player is inside the waypoint's bounding box."); + this.waypointsReached[i] = true; // Mark this waypoint as reached + } + } + } //this.checkPlayerCompletedTask(); @@ -86,17 +102,21 @@ export class GameWorld { } - // Needs to be updated to handle different win conditions - checkPlayerCompletedTask() { - let playerPos = this.robots[0].body.position; - let waypointBounds = this.waypoints[0].bounds; - if (Matter.Bounds.contains(waypointBounds, playerPos)) { - console.log("Player is inside the waypoint's bounding box."); - return true; - } - return false; + waypointsReached() { + return this.waypointsReached; } + // Needs to be updated to handle different win conditions + // checkPlayerCompletedTask() { + // let playerPos = this.robots[0].body.position; + // let waypointBounds = this.waypoints[0].bounds; + // if (Matter.Bounds.contains(waypointBounds, playerPos)) { + // console.log("Player is inside the waypoint's bounding box."); + // return true; + // } + // return false; + // } + 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], { @@ -167,22 +187,29 @@ export class GameWorld { }); // Draw Waypoints - this.waypoints.forEach(body => { + for (let w = 0; w < this.waypoints.length; w++) { + let body = this.waypoints[w]; ctx.beginPath(); let vertices = body.vertices; ctx.moveTo(vertices[0].x, vertices[0].y); for (let i = 1; i < vertices.length; i++) { ctx.lineTo(vertices[i].x, vertices[i].y); } - ctx.fillStyle = body.fillColor;; - ctx.strokeStyle = body.strokeColor; + if (this.waypointsReached[w]) { + ctx.globalAlpha = 0.05; // Applies to all drawing + ctx.fillStyle = "green"; // Color for reached waypoints + ctx.strokeStyle = "rgba(0, 128, 0, 0.2)"; // green with 80% opacity + } else { + ctx.globalAlpha = 0.2; // Applies to all drawing + ctx.fillStyle = body.fillColor; // Default color for waypoints + ctx.strokeStyle = body.strokeColor; + } ctx.closePath(); - ctx.globalAlpha = 0.2; // Applies to all drawing ctx.fill(); ctx.globalAlpha = 1.0; // Reset after if needed ctx.stroke(); - }); + } this.robots.forEach(robot => { let body = robot.body; // Get the Matter.js body from the robot object diff --git a/pyodide-worker.js b/pyodide-worker.js index c83232d..293c638 100644 --- a/pyodide-worker.js +++ b/pyodide-worker.js @@ -1,12 +1,15 @@ -importScripts("https://cdn.jsdelivr.net/pyodide/v0.23.4/full/pyodide.js"); +importScripts("https://cdn.jsdelivr.net/pyodide/v0.27.7/full/pyodide.js"); let sensorData = {}; // ✅ Store sensor values let gameWorld = null; async function initializePyodide() { self.pyodide = await loadPyodide({ - indexURL: "https://cdn.jsdelivr.net/pyodide/v0.23.4/full/" + indexURL: "https://cdn.jsdelivr.net/pyodide/v0.27.7/full/" }); + console.log("Pyodide loaded:", self.pyodide.version); + + self.pyodide.globals.set("send_to_main", (event, data) => { self.postMessage({ type: event, data: data }); @@ -26,10 +29,10 @@ async function initializePyodide() { //console.log(robot); let sensorArray = robot.sensors || []; // Assuming `sensors` is an array inside the robot object sensorData = sensorArray.map(sensor => ({ - "type": sensor.type, // Individual sensor's type - "angle": sensor.angle, // Individual sensor's angle + "type": sensor.type, // Individual sensor's type + "angle": sensor.angle, // Individual sensor's angle "distance": sensor.distance, // Individual sensor's distance - "hitpoint": sensor.hitpoint // Whatever other attributes you need + "hitpoint": sensor.hitpoint // Whatever other attributes you need })); //console.log(sensorData["x"]); sensorData = JSON.stringify(sensorData); @@ -139,6 +142,8 @@ async def async_sleep(seconds): time.sleep = async_sleep # ✅ Monkey-patch time.sleep() + + `); self.postMessage({ type: "ready" }); // ✅ Notify main thread that Pyodide is ready @@ -146,6 +151,11 @@ time.sleep = async_sleep # ✅ Monkey-patch time.sleep() initializePyodide(); +// Add this function to trigger interrupts +function interruptExecution() { + interruptBuffer[0] = 2; // 2 is the magic number for KeyboardInterrupt +} + self.onmessage = async (event) => { if (!self.pyodide) { self.postMessage({ type: "error", message: "Pyodide not initialized yet." }); diff --git a/style.css b/style.css index b7370e3..d254510 100644 --- a/style.css +++ b/style.css @@ -108,6 +108,10 @@ button { background-color: #1d4ed8; /* blue-700 */ } +#compile-button:disabled { + background-color: #bbbbbb; + /* blue-700 */ +} #pause-button { background-color: #eab308;