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

@ -77,71 +77,71 @@ export const lessons = [
"Print a boolean (True/False) to the console" "Print a boolean (True/False) to the console"
], ],
doneCondition: (() => { doneCondition: (() => {
// Matches print("something") or print('something') // Matches print("something") or print('something')
const stringPrintRegex = /print\s*\(\s*(['"]).*?\1\s*\)/; const stringPrintRegex = /print\s*\(\s*(['"]).*?\1\s*\)/;
// Matches print of float literal like 3.14, .5, -2.0, 1e-3 // 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*\)/; const floatPrintRegex = /print\s*\(\s*[-+]?(?:\d+\.\d*|\.\d+|\d+[eE][-+]?\d+)\s*\)/;
// Matches print of int literal like 3, -42 (excluding floats) // Matches print of int literal like 3, -42 (excluding floats)
const intPrintRegex = /print\s*\(\s*[-+]?\d+\s*\)/; const intPrintRegex = /print\s*\(\s*[-+]?\d+\s*\)/;
// Matches print(True) or print(False), case-insensitive // Matches print(True) or print(False), case-insensitive
const boolPrintRegex = /print\s*\(\s*(True|False)\s*\)/i; const boolPrintRegex = /print\s*\(\s*(True|False)\s*\)/i;
return ({ code, codeRanGood }) => { return ({ code, codeRanGood }) => {
if (!codeRanGood) { if (!codeRanGood) {
return { return {
done: false, done: false,
hint: "Your code had an error — try fixing it and run again." hint: "Your code had an error — try fixing it and run again."
}; };
} }
const progress = { const progress = {
stringDone: stringPrintRegex.test(code), stringDone: stringPrintRegex.test(code),
intDone: intPrintRegex.test(code), intDone: intPrintRegex.test(code),
floatDone: floatPrintRegex.test(code), floatDone: floatPrintRegex.test(code),
boolDone: boolPrintRegex.test(code), boolDone: boolPrintRegex.test(code),
}; };
// Fix false positives where float also matches int // Fix false positives where float also matches int
if (progress.floatDone && progress.intDone) { if (progress.floatDone && progress.intDone) {
// Check if the float number has a dot or exponent; if not, it was a false int match // Check if the float number has a dot or exponent; if not, it was a false int match
const matches = code.match(floatPrintRegex); const matches = code.match(floatPrintRegex);
if (matches) { if (matches) {
const numbers = matches.map(m => m.match(/[-+]?(?:\d+\.\d*|\.\d+|\d+[eE][-+]?\d+)/)?.[0]); const numbers = matches.map(m => m.match(/[-+]?(?:\d+\.\d*|\.\d+|\d+[eE][-+]?\d+)/)?.[0]);
for (const num of numbers) { for (const num of numbers) {
if (num && !num.includes('.') && !num.toLowerCase().includes('e')) { if (num && !num.includes('.') && !num.toLowerCase().includes('e')) {
// It's not a float, remove the float flag // It's not a float, remove the float flag
progress.floatDone = false; progress.floatDone = false;
}
}
} }
} }
}
}
const missing = []; const missing = [];
if (!progress.stringDone) missing.push("string"); if (!progress.stringDone) missing.push("string");
if (!progress.floatDone) missing.push("float"); if (!progress.floatDone) missing.push("float");
if (!progress.intDone) missing.push("int"); if (!progress.intDone) missing.push("int");
if (!progress.boolDone) missing.push("boolean"); if (!progress.boolDone) missing.push("boolean");
let hint = ""; let hint = "";
if (missing.length > 0) { if (missing.length > 0) {
hint = "I still need you to use print() with a "; hint = "I still need you to use print() with a ";
hint += missing.length === 1 hint += missing.length === 1
? missing[0] ? missing[0]
: missing.slice(0, -1).join(", ") + " and " + missing[missing.length - 1]; : 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 { return {
done, done,
progressArray: Object.values(progress), progressArray: Object.values(progress),
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,17 +102,21 @@ export class GameWorld {
} }
// Needs to be updated to handle different win conditions waypointsReached() {
checkPlayerCompletedTask() { return this.waypointsReached;
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;
} }
// 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") { 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], { let body = Matter.Bodies.fromVertices(position.x, position.y, [vertices], {
@ -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.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.closePath();
ctx.globalAlpha = 0.2; // Applies to all drawing
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 });
@ -26,10 +29,10 @@ async function initializePyodide() {
//console.log(robot); //console.log(robot);
let sensorArray = robot.sensors || []; // Assuming `sensors` is an array inside the robot object let sensorArray = robot.sensors || []; // Assuming `sensors` is an array inside the robot object
sensorData = sensorArray.map(sensor => ({ sensorData = sensorArray.map(sensor => ({
"type": sensor.type, // Individual sensor's type "type": sensor.type, // Individual sensor's type
"angle": sensor.angle, // Individual sensor's angle "angle": sensor.angle, // Individual sensor's angle
"distance": sensor.distance, // Individual sensor's distance "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"]); //console.log(sensorData["x"]);
sensorData = JSON.stringify(sensorData); sensorData = JSON.stringify(sensorData);
@ -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;