diff --git a/data/lessons.js b/data/lessons.js index 8873738..9838526 100644 --- a/data/lessons.js +++ b/data/lessons.js @@ -13,7 +13,34 @@ export const lessons = [
  • You should see "Hello World" printed in the output area
  • `, - doneCondition: (consoleText) => consoleText.includes("Hello World") + doneCondition: (() => { + const progress = { + stringDone: false + }; + + return ({ code, consoleText, codeRanGood }) => { + if (codeRanGood && !progress.stringDone && consoleText.includes("Hello World")) { + progress.stringDone = true; + } + + const missing = []; + if (!progress.stringDone) missing.push("Hello World"); + + let hint = "I still need you to print "; + if (missing.length === 0) { + hint = ""; + } else if (missing.length === 1) { + hint += missing[0]; + } else { + hint += missing.slice(0, -1).join(", ") + " and " + missing[missing.length - 1]; + } + + return { + done: progress.stringDone, + hint + }; + }; + })() }, { id: 'lesson2', @@ -36,65 +63,60 @@ export const lessons = [

    Click the "Run" button to execute your code

    `, - doneCondition: (consoleText => { - // Persistent tracking object inside closure + doneCondition: (() => { const progress = { stringDone: false, intDone: false, floatDone: false, boolDone: false, - syntaxErrorDone: false, + // syntaxErrorDone: false, // optional }; - //const syntaxErrorRegex = /SyntaxError/i; - const stringRegex = /(["'])(?:(?=(\\?))\2.)*?\1/; // optional improvement based on context + const stringRegex = /(["'])(?:(?=(\\?))\2.)*?\1/; const intRegex = /(? { + return ({ code, consoleText, codeRanGood }) => { + if (!codeRanGood) { + return { + done: false, + hint: "Your code had an error — try fixing it and run again." + }; + } - - // if (!progress.syntaxErrorDone && syntaxErrorRegex.test(text)) { - // progress.syntaxErrorDone = true; - // } - if (!progress.stringDone && stringRegex.test(text)) { + if (!progress.stringDone && stringRegex.test(consoleText)) { progress.stringDone = true; } - if (!progress.floatDone && floatRegex.test(text)) { + if (!progress.floatDone && floatRegex.test(consoleText)) { progress.floatDone = true; } - if (!progress.intDone && intRegex.test(text)) { + if (!progress.intDone && intRegex.test(consoleText)) { progress.intDone = true; } - if (!progress.boolDone && boolRegex.test(text)) { + if (!progress.boolDone && boolRegex.test(consoleText)) { progress.boolDone = true; } - let missing = []; - - //if (!progress.syntaxErrorDone) missing.push("syntax error"); - //if (!progress.stringDone) missing.push("string"); + const missing = []; if (!progress.floatDone) missing.push("float"); if (!progress.intDone) missing.push("int"); if (!progress.boolDone) missing.push("boolean"); - let hint = "I still need you to print a "; + let hint = "I still need you to print a "; if (missing.length === 0) { hint = ""; } else if (missing.length === 1) { hint += missing[0]; } else { - // Join all but last with comma, then add 'and' + last hint += missing.slice(0, -1).join(", ") + " and " + missing[missing.length - 1]; } - const done = progress.intDone && progress.floatDone && progress.boolDone; - return { done, hint }; }; })() + }, { id: 'lesson3', @@ -111,7 +133,7 @@ export const lessons = [
  • Division: print(20 / 5)
  • `, - doneCondition: (consoleText => { + doneCondition: (() => { // Persistent tracking object inside closure const progress = { addDone: false, @@ -123,22 +145,27 @@ export const lessons = [ - return (text) => { - + return ({ code, consoleText, codeRanGood }) => { + if (!codeRanGood) { + return { + done: false, + hint: "" + }; + } // if (!progress.syntaxErrorDone && syntaxErrorRegex.test(text)) { // progress.syntaxErrorDone = true; // } - if (!progress.addDone && text.includes("+")) { + if (!progress.addDone && code.includes("+")) { progress.addDone = true; } - if (!progress.subDone && text.includes("-")) { + if (!progress.subDone && code.includes("-")) { progress.subDone = true; } - if (!progress.mulDone && text.includes("*")) { + if (!progress.mulDone && code.includes("*")) { progress.mulDone = true; } - if (!progress.divDone && text.includes("/")) { + if (!progress.divDone && code.includes("/")) { progress.divDone = true; } @@ -198,55 +225,418 @@ export const lessons = [ doneCondition: (text) => text.includes("8"), } ], - doneCondition: (code => { - const progress = { - varCreated: false, - varPrinted: false, - varName: null, - }; + doneCondition: (() => { + const progress = { + varCreated: false, + varPrinted: false, + varName: null, + }; - return (text) => { - // Extract variable name from current text each time - const assignMatch = text.match(/^\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*=/m); - if (assignMatch) { - progress.varName = assignMatch[1]; - progress.varCreated = true; - } else { - // No variable assignment found this run - progress.varCreated = false; - progress.varName = null; - progress.varPrinted = false; // reset printed too because no var - } + return ({ code, consoleText, codeRanGood }) => { + if (!codeRanGood) { + return { + done: false, + hint: "" + }; + } + const assignMatch = code.match(/^\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*=/m); + if (assignMatch) { + progress.varName = assignMatch[1]; + progress.varCreated = true; + } else { + progress.varCreated = false; + progress.varName = null; + progress.varPrinted = false; + } - // Check if variable is printed in this run (only if varName exists) - if (progress.varCreated && progress.varName) { - const printRegex = new RegExp(`print\\s*\\(\\s*${progress.varName}\\s*\\)`); - if (printRegex.test(text)) { - progress.varPrinted = true; - } else { - progress.varPrinted = false; // reset if print no longer found + if (progress.varCreated && progress.varName) { + const printRegex = new RegExp(`print\\s*\\(\\s*${progress.varName}\\s*\\)`); + if (printRegex.test(code)) { + progress.varPrinted = true; + } else { + progress.varPrinted = false; + } + } + + console.log('varCreated:', progress.varCreated, 'varPrinted:', progress.varPrinted); + + let missing = []; + if (!progress.varCreated) missing.push("create a variable"); + if (!progress.varPrinted) missing.push("print the variable"); + + let hint = "I still need you to "; + if (missing.length === 0) { + hint = ""; + } else if (missing.length === 1) { + hint += missing[0]; + } else { + hint += missing.slice(0, -1).join(", ") + " and " + missing[missing.length - 1]; + } + + const done = progress.varCreated && progress.varPrinted; + return { done, hint }; + }; + })() + }, + { + id: 'lesson5', + title: '5. More on Variables', + difficulty: 'easy', + content: ` +

    A variable will act like any other object of its data type.

    +

    For example, if we have a variable called bob with the value 32, we can do math with it:

    +

    print(bob + 10) will print 42.

    +
    +

    We can also change the value of a variable:

    +
     
    +bob = 32
    +print(bob)
    +bob = bob + 10
    +print(bob)
    +      
    +

    In this example, the first time you print bob it will give his initial value of 32, but the second time we have added 10 to bob.

    +

    Give it a try.

    + `, + steps: [ + { + content: `

    First, try printing an addition: print(2 + 3)

    `, + doneCondition: (text) => text.includes("5"), + }, + { + content: `

    Nice! Now try subtraction: print(5 - 2)

    `, + doneCondition: (text) => text.includes("3"), + }, + { + content: `

    Now try multiplication: print(4 * 2)

    `, + doneCondition: (text) => text.includes("8"), } - } + ], + doneCondition: (() => { + const progress = { + varCreated: false, + varPrinted: false, + varArithmeticDone: false, + varPrintedTwice: false, + }; - // Build hint - let missing = []; - if (!progress.varCreated) missing.push("create a variable"); - if (progress.varCreated && !progress.varPrinted) missing.push("print the variable"); + return ({ code, consoleText, codeRanGood }) => { + if (!codeRanGood) { + return { + done: false, + hint: "" + }; + } + const assignRegex = /\b(\w+)\s*=\s*[\d'"]/; // variable assignment + const printRegex1 = /print\(\s*\1\s*\)/; // first print of the variable + const mathRegex = /\1\s*=\s*\1\s*[\+\-\*/]\s*[\d'"]/; // arithmetic using itself + const printRegex2 = /print\(\s*\1\s*\)/; // second print of the variable - let hint = "I still need you to "; - if (missing.length === 0) { - hint = ""; - } else if (missing.length === 1) { - hint += missing[0]; - } else { - hint += missing.slice(0, -1).join(", ") + " and " + missing[missing.length - 1]; - } + const match = code.match(assignRegex); + if (!match) return false; - const done = progress.varCreated && progress.varPrinted; - return { done, hint }; - }; -})() + const varName = match[1]; + const dynamicPrint1 = new RegExp(`print\\(\\s*${varName}\\s*\\)`); + const dynamicMath = new RegExp(`${varName}\\s*=\\s*${varName}\\s*[+\\-*/]\\s*[\\d'"]`); + const dynamicPrint2 = new RegExp(`print\\(\\s*${varName}\\s*\\)`, 'g'); + progress.varCreated = match; + progress.dynamicPrint1 = dynamicPrint1.test(code); + progress.varArithmeticDone = dynamicMath.test(code); + progress.varPrintedTwice = (code.match(dynamicPrint2) || []).length >= 2 + + let missing = []; + if (!progress.varCreated) missing.push("create a variable"); + if (!progress.dynamicPrint1) missing.push("print the variable"); + if (!progress.varArithmeticDone) missing.push("alter the variable"); + if (!progress.varPrintedTwice) missing.push("print the variable a second time"); + + let hint = "I still need you to "; + if (missing.length === 0) { + hint = ""; + } else if (missing.length === 1) { + hint += missing[0]; + } else { + hint += missing.slice(0, -1).join(", ") + " and " + missing[missing.length - 1]; + } + + const done = progress.varCreated && progress.dynamicPrint1 && progress.varArithmeticDone && progress.varPrintedTwice; + return { done, hint }; + }; + })() + }, + { + id: 'lesson6', + title: '6. Conditionals', + difficulty: 'easy', + content: ` +

    Sometimes we want don't want part of our code to run, or we want it to run differently in different situations. That's when we use conditionals.

    +
    
    +bob = 5
    +if bob > 10:
    +    print("Bob is greater than 10")
    +      
    +

    In this example, the print function will only run if bob is 11 or higher.

    +
    +

    One thing to note here is the formatting. Note that after the if bob > 10: there is a colon (:)

    +

    This tells Python that the next line is part of a new block of code.

    +

    In Python, we use indentation to define blocks of code, so the next line must be indented.

    +

    After the else: we also have a colon, and the next line is indented again.

    +

    Try it for yourself:

    +
      +
    1. Initialize a variable with a value
    2. +
    3. Use an if statement to check if the variable is greater or less than a value
    4. +
    5. Print a message based on the condition
    6. +
    +

    To test if two values are equal, you can use the == operator.

    +

    eg.

    
    +num = 10
    +if num == 10:
    +    print("num is equal to 10")
    +      
    + `, + + doneCondition: (() => { + const progress = { + varCreated: false, + ifStatement: false, + printedSomething: false, + }; + + return ({ code, consoleText, codeRanGood }) => { + if (!codeRanGood) { + return { + done: false, + hint: "" + }; + } + + // Check for variable assignment + const assignRegex = /\b(\w+)\s*=\s*[\d'"]/; + progress.varCreated = assignRegex.test(code); + + // Check for if statement + progress.ifStatement = /(^|\n)\s*if\s+.*:/.test(code); + + // Check for print() anywhere + progress.printedSomething = /print\s*\(.*\)/.test(code); + + // Build hint + const missing = []; + if (!progress.varCreated) missing.push("create a variable"); + if (!progress.ifStatement) missing.push("use an if statement"); + if (!progress.printedSomething) missing.push("print something"); + + let hint = ""; + if (missing.length > 0) { + hint = "I still need you to " + (missing.length === 1 + ? missing[0] + : missing.slice(0, -1).join(", ") + " and " + missing[missing.length - 1]); + } + + const done = progress.varCreated && progress.ifStatement && progress.printedSomething; + return { done, hint }; + }; + })() }, + { + id: 'lesson7', + title: '7. More Conditionals', + difficulty: 'easy', + content: ` +

    Sometimes we'll want to run one thing OR another, rather than just running something or not. To do this we chain our if statements together with elif and else.

    +
    
    +bob = 10
    +if bob == 10:
    +    print("bob is equal to 10")
    +elif bob < 10:
    +    print("bob is less than 10")
    +else:
    +    print("bob is greater than 10")
    +      
    +

    In this example, only one of the print statements can ever run, elif is short for "else if", and is a second if statement which is only tested it the if above it resolves to False.

    +

    The else statement is run if all previous conditions are False.

    +

    You can chain as many elif statements as you like, but only one else at the end.

    +

    Try it for yourself:

    + `, + + doneCondition: (() => { + const progress = { + varCreated: false, + ifStatement: false, + elifStatement: false, + elseStatement: false, + threeDistinctPrints: false, + }; + + return ({ code, consoleText, codeRanGood }) => { + if (!codeRanGood) { + return { done: false, hint: "" }; + } + + // 1. Variable assignment + const assignRegex = /\b(\w+)\s*=\s*[\d'"]/; + progress.varCreated = assignRegex.test(code); + + // 2. Line-by-line block tracking + const lines = code.split('\n'); + let currentBlock = null; + const printContents = { + if: null, + elif: null, + else: null, + }; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + + if (/^if\s+.*:/.test(line)) { + progress.ifStatement = true; + currentBlock = "if"; + continue; + } + + if (/^elif\s+.*:/.test(line)) { + progress.elifStatement = true; + currentBlock = "elif"; + continue; + } + + if (/^else\s*:/.test(line)) { + progress.elseStatement = true; + currentBlock = "else"; + continue; + } + + const printMatch = line.match(/print\s*\((.*?)\)/); + if (printMatch && currentBlock && !printContents[currentBlock]) { + const cleaned = printMatch[1].replace(/\s+/g, '').toLowerCase(); + printContents[currentBlock] = cleaned; + } + } + + const printedValues = Object.values(printContents).filter(Boolean); + const uniqueValues = new Set(printedValues); + progress.threeDistinctPrints = uniqueValues.size === 3; + + // Hint generation + const missing = []; + if (!progress.varCreated) missing.push("create a variable"); + if (!progress.ifStatement) missing.push("use an if statement"); + if (!progress.elifStatement) missing.push("use an elif statement"); + if (!progress.elseStatement) missing.push("use an else statement"); + if (!progress.threeDistinctPrints) missing.push("print different things in the if, elif, and else blocks"); + + const hint = missing.length > 0 + ? "I still need you to " + (missing.length === 1 + ? missing[0] + : missing.slice(0, -1).join(", ") + " and " + missing[missing.length - 1]) + : ""; + + const done = progress.varCreated && progress.ifStatement && progress.elifStatement && + progress.elseStatement && progress.threeDistinctPrints; + + return { done, hint }; + }; + })() + }, + { + id: 'lesson8', + title: '8. Loops', + difficulty: 'easy', + content: ` +

    Most of the time we'll want our code to run multiple times, or even endlessly, in that case we need loops.

    +

    Python has two main types of loops: for loops and while loops.

    +

    The while loop will continue to run as long as a condition is True.

    +
    
    +count = 0
    +while count < 5:
    +    print("Count is:", count)
    +    count = count + 1
    +print("Done!")
    +      
    +

    You have to be careful with while loops, if the condition never becomes False, the loop will run forever!

    + + `, + + doneCondition: (() => { + const progress = { + varCreated: false, + ifStatement: false, + elifStatement: false, + elseStatement: false, + threeDistinctPrints: false, + }; + + return ({ code, consoleText, codeRanGood }) => { + if (!codeRanGood) { + return { done: false, hint: "" }; + } + + // 1. Variable assignment + const assignRegex = /\b(\w+)\s*=\s*[\d'"]/; + progress.varCreated = assignRegex.test(code); + + // 2. Line-by-line block tracking + const lines = code.split('\n'); + let currentBlock = null; + const printContents = { + if: null, + elif: null, + else: null, + }; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + + if (/^if\s+.*:/.test(line)) { + progress.ifStatement = true; + currentBlock = "if"; + continue; + } + + if (/^elif\s+.*:/.test(line)) { + progress.elifStatement = true; + currentBlock = "elif"; + continue; + } + + if (/^else\s*:/.test(line)) { + progress.elseStatement = true; + currentBlock = "else"; + continue; + } + + const printMatch = line.match(/print\s*\((.*?)\)/); + if (printMatch && currentBlock && !printContents[currentBlock]) { + const cleaned = printMatch[1].replace(/\s+/g, '').toLowerCase(); + printContents[currentBlock] = cleaned; + } + } + + const printedValues = Object.values(printContents).filter(Boolean); + const uniqueValues = new Set(printedValues); + progress.threeDistinctPrints = uniqueValues.size === 3; + + // Hint generation + const missing = []; + if (!progress.varCreated) missing.push("create a variable"); + if (!progress.ifStatement) missing.push("use an if statement"); + if (!progress.elifStatement) missing.push("use an elif statement"); + if (!progress.elseStatement) missing.push("use an else statement"); + if (!progress.threeDistinctPrints) missing.push("print different things in the if, elif, and else blocks"); + + const hint = missing.length > 0 + ? "I still need you to " + (missing.length === 1 + ? missing[0] + : missing.slice(0, -1).join(", ") + " and " + missing[missing.length - 1]) + : ""; + + const done = progress.varCreated && progress.ifStatement && progress.elifStatement && + progress.elseStatement && progress.threeDistinctPrints; + + return { done, hint }; + }; + })() + }, ]; diff --git a/data/lessons.json b/data/lessons.json deleted file mode 100644 index 876afa6..0000000 --- a/data/lessons.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - { - "title": "Lesson 1", - "content": "Welcome to Python. We are going to learn how to use python to control robots, but we need some basics first.
    Let's start with the basics of Python programming. Python is a versatile language that is easy to learn and widely used in robotics.
    " - }, - { - "title": "Lesson 2", - "content": "Now let's learn about interrupts..." - } -] \ No newline at end of file diff --git a/game.js b/game.js index 7368e27..2046943 100644 --- a/game.js +++ b/game.js @@ -2,7 +2,9 @@ import { Robot } from "./robot.js"; import { GameWorld } from "./gameworld.js"; import { lessons } from './data/lessons.js'; - +let mostRecentCode = ""; // The user’s code +let codeRanGood = false; // Your own function that returns true/false +let consoleText = ""; // What was printed let currentLesson = 0; let lessonComplete = false; @@ -23,11 +25,11 @@ function showLesson(index) { document.getElementById('prev-lesson').disabled = index === 0; document.getElementById('next-lesson').disabled = index === lessons.length - 1; - console.log(isLessonDone(lesson.id)); + //console.log(isLessonDone(lesson.id)); updateLessonStatus(); } -function loadLessonContent(lesson){ +function loadLessonContent(lesson) { document.getElementById('lesson-title').textContent = lesson.title; document.getElementById('lesson-content').innerHTML = lesson.content; } @@ -39,10 +41,22 @@ document.getElementById('next-lesson').addEventListener('click', () => { showLesson(currentLesson + 1); }); -function checkLessonDone(outputText) { - checkCurrentStep(outputText); +function checkLessonDone() { + //consoleText = outputText; // Update console text + if (consoleText.includes("Error") || consoleText.includes("Exception")) { + codeRanGood = false; + } else { + codeRanGood = true; + } + console.log("codeRanGood ", codeRanGood); + + //checkCurrentStep(outputText); const lesson = lessons[currentLesson]; - const result = lesson.doneCondition(outputText); + const result = lesson.doneCondition({ + code: mostRecentCode, + consoleText: consoleText, + codeRanGood: codeRanGood + }); if (result.done) { markLessonDone(lesson.id); } @@ -60,7 +74,7 @@ function markLessonDone(lessonId) { console.log(`Lesson ${lessonId} marked as done!`); // For example: localStorage.setItem(`lessonDone_${lessonId}`, 'true'); - logToConsole("✅ Task Completed! ✅"); + logToConsole("✅ Task Completed! ✅", false); updateLessonStatus(); } @@ -118,8 +132,8 @@ function clearLessonProgress() { } -//clearLessonProgress(); // Clear progress on load for testing -showLesson(3); +clearLessonProgress(); // Clear progress on load for testing +showLesson(7); const consoleElement = document.getElementById("console"); const gameCanvas = document.getElementById("gameCanvas"); @@ -152,6 +166,7 @@ function startPyodideWorker() { switch (event.data.type) { case "console": logToConsole(event.data.data); + break; case "error": logToConsole(`${event.data.message}`); @@ -165,6 +180,10 @@ function startPyodideWorker() { case "move": move(event.data.data); break; + case "execution_done": + console.log("Execution done"); + checkLessonDone(); + break; } }; @@ -218,7 +237,8 @@ function logToConsole(text, checkLesson = true) { consoleElement.scrollTop = consoleElement.scrollHeight; if (checkLesson && !text.includes("Welcome")) { // Don't check lesson completion for welcome message - checkLessonDone(text); + consoleText = text; // Update console text + //checkLessonDone(text); } } @@ -331,18 +351,18 @@ document.getElementById("compile-button").addEventListener("click", () => { // Use the Monaco Editor instance to get the code let code = monacoEditor.getValue(); // Get text from the editor - console.log(code); + //console.log(code); code = code.replace(/time\.sleep\(/g, "await time.sleep("); - console.log(code); + //console.log(code); consoleElement.innerHTML = ""; pyodideWorker.postMessage({ type: "execute", code: code }); - - checkLessonDone(code); + mostRecentCode = code; + logToConsole("Compiling your code...", false); }); diff --git a/pyodide-worker.js b/pyodide-worker.js index 6581d2e..af64df1 100644 --- a/pyodide-worker.js +++ b/pyodide-worker.js @@ -12,14 +12,14 @@ async function initializePyodide() { self.postMessage({ type: event, data: data }); }); - + // ✅ Expose sensor data to Python self.pyodide.globals.set("get_sensor_data", (name) => { - if (gameWorld == null){ + if (gameWorld == null) { return null; } - + //console.log(gameWorld.robots); let robot = gameWorld.robots[0]; @@ -38,11 +38,11 @@ async function initializePyodide() { self.pyodide.globals.set("get_robot_data", () => { - if (gameWorld == null){ + if (gameWorld == null) { return null; } let robot = gameWorld.robots[0]; - + let robotData = { "position": robot.body.position, "angle": robot.body.angle, @@ -141,7 +141,7 @@ self.onmessage = async (event) => { self.postMessage({ type: "error", message: "Pyodide not initialized yet." }); return; } - + if (event.data.type === "sensor_update") { // ✅ Update sensor data Object.assign(sensorData, event.data.data); @@ -158,5 +158,6 @@ self.onmessage = async (event) => { } catch (error) { self.postMessage({ type: "error", message: error.toString() }); } + self.postMessage({ type: "execution_done" }); } }; diff --git a/style.css b/style.css index 1be08f4..05248e5 100644 --- a/style.css +++ b/style.css @@ -50,6 +50,16 @@ header p { /* gray-600 */ } +code, pre { + font-family: Consolas, Menlo, Monaco, "Courier New", monospace; + background-color: inherit; + color: blue; + border: none; + padding: 0; + margin: 0; +} + + .button-group { display: flex; gap: 12px; @@ -281,9 +291,9 @@ main { } -#lesson-box code { +/* #lesson-box code { background: #eee; padding: 2px 4px; border-radius: 4px; font-family: monospace; -} \ No newline at end of file +} */ \ No newline at end of file