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
};
};
})()
})()
@ -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,16 +102,20 @@ export class GameWorld {
}
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;
}
// 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
@ -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;
ctx.closePath();
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.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 });
@ -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;