added objectives to first robot lesson, locked compile button while executing, game world resets on compile & run

master
Jake 2025-07-04 15:50:06 +08:00
parent 9b702b6ff1
commit 8a0219500d
6 changed files with 172 additions and 124 deletions

View File

@ -141,7 +141,7 @@ export const lessons = [
hint hint
}; };
}; };
})() })()
@ -872,59 +872,31 @@ robot.move(0) # Stop the robot
</code></pre> </code></pre>
`, `,
objectives: [ objectives: [
"Import the time module", "Reach the first checkpoint",
"Print something", "Reach the second checkpoint",
"Use time.sleep() to pause for an amount of time",
"Print something else after the pause" "Code should complete without errors"
], ],
doneCondition: (() => { doneCondition: (() => {
return ({ code, consoleText, codeRanGood }) => { return ({ code, consoleText, codeRanGood, gameWorld }) => {
const progress = { const progress = {
importedTime: false, firstCheckpoint: gameWorld.waypointsReached[0],
printedBefore: false, secondCheckpoint: gameWorld.waypointsReached[1],
usedSleep: false, codeRanGood: codeRanGood,
printedAfter: false,
}; };
if (!codeRanGood) { if (!codeRanGood) {
return { done: false, hint: "" }; 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 // 5. Build hint
const missing = []; const missing = [];
if (!progress.importedTime) missing.push("import the time module"); if (!progress.firstCheckpoint) missing.push("reach the first checkpoint");
if (!progress.printedBefore) missing.push("print something before sleeping"); if (!progress.secondCheckpoint) missing.push("reach the second checkpoint");
if (!progress.usedSleep) missing.push("use time.sleep()");
if (!progress.printedAfter && progress.usedSleep)
missing.push("print something after sleeping");
let hint = ""; let hint = "";
if (missing.length === 1) { if (missing.length === 1) {
@ -935,10 +907,9 @@ robot.move(0) # Stop the robot
return { return {
done: done:
progress.importedTime && progress.firstCheckpoint &&
progress.printedBefore && progress.secondCheckpoint &&
progress.usedSleep && progress.codeRanGood,
progress.printedAfter,
progressArray: Object.values(progress), progressArray: Object.values(progress),
hint, hint,
}; };

View File

@ -13,7 +13,33 @@
{ {
"position": { "position": {
"x": 420, "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": [ "vertices": [
{ {

26
game.js
View File

@ -92,7 +92,8 @@ function checkLessonDone() {
const result = lesson.doneCondition({ const result = lesson.doneCondition({
code: mostRecentCode, code: mostRecentCode,
consoleText: consoleText, consoleText: consoleText,
codeRanGood: codeRanGood codeRanGood: codeRanGood,
gameWorld: gameWorld
}); });
if (result.done) { if (result.done) {
markLessonDone(lesson.id); markLessonDone(lesson.id);
@ -220,7 +221,7 @@ function toggleObjective(index, completed = true) {
} }
//clearLessonProgress(); // Clear progress on load for testing //clearLessonProgress(); // Clear progress on load for testing
showLesson(1); showLesson(9);
const consoleElement = document.getElementById("console"); const consoleElement = document.getElementById("console");
const gameCanvas = document.getElementById("gameCanvas"); const gameCanvas = document.getElementById("gameCanvas");
@ -269,6 +270,7 @@ function startPyodideWorker() {
break; break;
case "execution_done": case "execution_done":
console.log("Execution done"); console.log("Execution done");
document.getElementById('compile-button').disabled = false;
checkLessonDone(); checkLessonDone();
break; break;
} }
@ -355,6 +357,12 @@ function togglePause() {
document.getElementById("pause-button").innerText = paused ? "Resume" : "Pause"; document.getElementById("pause-button").innerText = paused ? "Resume" : "Pause";
} }
function resetGameWorld() {
robots = createInitialRobots();
gameWorld.reset(robots["player"]);
pyodideWorker.postMessage({ type: "interrupt" });
}
// ✅ Reset Function (Fixed) // ✅ Reset Function (Fixed)
function resetGame() { function resetGame() {
// Terminate the worker // Terminate the worker
@ -400,11 +408,12 @@ function gameLoop(timestamp) {
gameWorld.update(); gameWorld.update();
gameWorld.draw(ctx); gameWorld.draw(ctx);
if (gameWorld.checkPlayerCompletedTask()) {
logToConsole("✅ Task Completed! ✅"); // if (gameWorld.checkPlayerCompletedTask()) {
togglePause(); // logToConsole("✅ Task Completed! ✅");
gameWorld.currentLevel++; // togglePause();
} // gameWorld.currentLevel++;
// }
pyodideWorker.postMessage({ pyodideWorker.postMessage({
type: "game_state", type: "game_state",
@ -435,7 +444,8 @@ 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;
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);

View File

@ -14,6 +14,7 @@ export class GameWorld {
this.currentLevel = 0; this.currentLevel = 0;
this.waypoints = []; this.waypoints = [];
this.waypointsReached = [];
this.obstacles = []; this.obstacles = [];
this.robots = []; this.robots = [];
@ -28,6 +29,7 @@ export class GameWorld {
this.robots = [] this.robots = []
this.obstacles = [] this.obstacles = []
this.waypoints = []; this.waypoints = [];
this.waypointsReached = [];
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];
@ -51,7 +53,9 @@ export class GameWorld {
let obstacle = level.waypoints[i]; let obstacle = level.waypoints[i];
console.log("Adding waypoint:", obstacle); console.log("Adding waypoint:", obstacle);
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
} }
console.log(level.waypointsReached)
// this.addObstacle([ // this.addObstacle([
// { x: -100, y: -100 }, // Vertex 1 // { x: -100, y: -100 }, // Vertex 1
@ -77,7 +81,19 @@ export class GameWorld {
this.robots[id].update(this); 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(); //this.checkPlayerCompletedTask();
@ -86,16 +102,20 @@ export class GameWorld {
} }
waypointsReached() {
return this.waypointsReached;
}
// 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;
let waypointBounds = this.waypoints[0].bounds; // let waypointBounds = this.waypoints[0].bounds;
if (Matter.Bounds.contains(waypointBounds, playerPos)) { // if (Matter.Bounds.contains(waypointBounds, playerPos)) {
console.log("Player is inside the waypoint's bounding box."); // console.log("Player is inside the waypoint's bounding box.");
return true; // return true;
} // }
return false; // return false;
} // }
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
@ -167,22 +187,29 @@ export class GameWorld {
}); });
// Draw Waypoints // Draw Waypoints
this.waypoints.forEach(body => { for (let w = 0; w < this.waypoints.length; w++) {
let body = this.waypoints[w];
ctx.beginPath(); ctx.beginPath();
let vertices = body.vertices; let vertices = body.vertices;
ctx.moveTo(vertices[0].x, vertices[0].y); ctx.moveTo(vertices[0].x, vertices[0].y);
for (let i = 1; i < vertices.length; i++) { for (let i = 1; i < vertices.length; i++) {
ctx.lineTo(vertices[i].x, vertices[i].y); ctx.lineTo(vertices[i].x, vertices[i].y);
} }
ctx.fillStyle = body.fillColor;;
ctx.strokeStyle = body.strokeColor; if (this.waypointsReached[w]) {
ctx.closePath(); 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.globalAlpha = 0.2; // Applies to all drawing
ctx.fillStyle = body.fillColor; // Default color for waypoints
ctx.strokeStyle = body.strokeColor;
}
ctx.closePath();
ctx.fill(); ctx.fill();
ctx.globalAlpha = 1.0; // Reset after if needed ctx.globalAlpha = 1.0; // Reset after if needed
ctx.stroke(); ctx.stroke();
}); }
this.robots.forEach(robot => { this.robots.forEach(robot => {
let body = robot.body; // Get the Matter.js body from the robot object let body = robot.body; // Get the Matter.js body from the robot object

View File

@ -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 sensorData = {}; // ✅ Store sensor values
let gameWorld = null; let gameWorld = null;
async function initializePyodide() { async function initializePyodide() {
self.pyodide = await loadPyodide({ 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.pyodide.globals.set("send_to_main", (event, data) => {
self.postMessage({ type: event, data: data }); self.postMessage({ type: event, data: data });
@ -139,6 +142,8 @@ async def async_sleep(seconds):
time.sleep = async_sleep # Monkey-patch time.sleep() time.sleep = async_sleep # Monkey-patch time.sleep()
`); `);
self.postMessage({ type: "ready" }); // ✅ Notify main thread that Pyodide is ready self.postMessage({ type: "ready" }); // ✅ Notify main thread that Pyodide is ready
@ -146,6 +151,11 @@ time.sleep = async_sleep # ✅ Monkey-patch time.sleep()
initializePyodide(); initializePyodide();
// Add this function to trigger interrupts
function interruptExecution() {
interruptBuffer[0] = 2; // 2 is the magic number for KeyboardInterrupt
}
self.onmessage = async (event) => { self.onmessage = async (event) => {
if (!self.pyodide) { if (!self.pyodide) {
self.postMessage({ type: "error", message: "Pyodide not initialized yet." }); self.postMessage({ type: "error", message: "Pyodide not initialized yet." });

View File

@ -108,6 +108,10 @@ button {
background-color: #1d4ed8; background-color: #1d4ed8;
/* blue-700 */ /* blue-700 */
} }
#compile-button:disabled {
background-color: #bbbbbb;
/* blue-700 */
}
#pause-button { #pause-button {
background-color: #eab308; background-color: #eab308;