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"
],
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
</code></pre>
`,
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,
};

View File

@ -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": [
{

26
game.js
View File

@ -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);

View File

@ -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

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 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." });

View File

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