From 9b702b6ff14ea079aa15bb6099a242a78954618b Mon Sep 17 00:00:00 2001 From: Jake Date: Thu, 3 Jul 2025 23:25:13 +0800 Subject: [PATCH] added dropdown for lessons, and added first robot lesson to second dropdown --- .vscode/settings.json | 3 + data/lessons.js | 253 +++++++++++++++++++++++++++++++----------- data/levels.json | 4 +- game.js | 42 ++++++- index.html | 19 ++++ pyodide-worker.js | 10 ++ style.css | 48 +++++++- 7 files changed, 305 insertions(+), 74 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..6f3a291 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "liveServer.settings.port": 5501 +} \ No newline at end of file diff --git a/data/lessons.js b/data/lessons.js index 78c82da..f7f8ac3 100644 --- a/data/lessons.js +++ b/data/lessons.js @@ -2,7 +2,8 @@ export const lessons = [ { id: 'lesson1', title: '1. Introduction to Python', - difficulty: 'easy', + tabtitle: 'Hello World', + level: 'basics', content: `

Let's learn some Python..

We'll start with what's called a "Hello World" program to make sure everythings working.

@@ -49,8 +50,9 @@ export const lessons = [ }, { id: 'lesson2', - title: 'Data Types and Variables', - difficulty: 'easy', + title: '2. Data Types', + tabtitle: 'Data Types', + level: 'basics', content: `

Did you try typeing print(Hello World), without the quotation marks?

If you didn't, give it a try now.

@@ -75,70 +77,82 @@ 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 of float literal like 3.14, .5, -2.0, 1e-3 + const floatPrintRegex = /print\s*\(\s*[-+]?(?:\d+\.\d*|\.\d+|\d+[eE][-+]?\d+)\s*\)/; - const stringRegex = /print\s*\(\s*(['"]).*?\1\s*\)/; - const intRegex = /(? { - const progress = { - stringDone: false, - intDone: false, - floatDone: false, - boolDone: false, - // syntaxErrorDone: false, // optional - }; - if (!codeRanGood) { - return { - done: false, - hint: "Your code had an error — try fixing it and run again." - }; - } + // Matches print(True) or print(False), case-insensitive + const boolPrintRegex = /print\s*\(\s*(True|False)\s*\)/i; - if (!progress.stringDone && stringRegex.test(code)) { - progress.stringDone = true; - } - if (!progress.floatDone && floatRegex.test(consoleText)) { - progress.floatDone = true; - } - if (!progress.intDone && intRegex.test(consoleText)) { - progress.intDone = true; - } - if (!progress.boolDone && boolRegex.test(consoleText)) { - progress.boolDone = true; - } - - 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 = "I still need you to print a "; - 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.stringDone && progress.intDone && progress.floatDone && progress.boolDone; - return { - done, - progressArray: Object.values(progress), - hint - }; + 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), + }; + + // 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"); + + 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; + + return { + done, + progressArray: Object.values(progress), + hint + }; + }; +})() + + + + }, { id: 'lesson3', title: '3. Arithmetic Operations', - difficulty: 'easy', + tabtitle: 'Arithmetic', + level: 'basics', content: `

Let's get python to do our math homework for us.

Python can handle basic arithmetic operations like addition, subtraction, multiplication, and division.

@@ -224,7 +238,8 @@ export const lessons = [ { id: 'lesson4', title: '4. Variables', - difficulty: 'easy', + tabtitle: 'Variables', + level: 'basics', content: `

It's common for us to need to keep some data or objects over multiple lines, for that we use variables.

Think of a variable as a container that holds an object for us to use later.

@@ -303,7 +318,8 @@ export const lessons = [ { id: 'lesson5', title: '5. More on Variables', - difficulty: 'easy', + tabtitle: 'Changing Variables', + level: 'basics', 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:

@@ -322,7 +338,7 @@ print(bob) objectives: [ "Initialize a variable with a value", "Print the variable using print()", - "Alter the value of the variable", + "Alter the value of the variable by adding, subtracting, multiplying, or dividing by itself", "Print the variable again using print()", ], doneCondition: (() => { @@ -428,7 +444,8 @@ print(bob) { id: 'lesson6', title: '6. Conditionals', - difficulty: 'easy', + tabtitle: 'if', + level: 'basics', 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.


@@ -512,7 +529,8 @@ if num == 10:
   {
     id: 'lesson7',
     title: '7. More Conditionals',
-    difficulty: 'easy',
+    tabtitle: 'if/elif/else',
+    level: 'basics',
     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.


@@ -624,7 +642,8 @@ else:
   {
     id: 'lesson8',
     title: '8. While Loops',
-    difficulty: 'easy',
+    tabtitle: 'While Loops',
+    level: 'basics',
     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.

@@ -632,7 +651,7 @@ else:

 count = 0
 while count < 5:
-    print("Count is:", count)
+    print(count)
     count = count + 1
 print("Done!")
       
@@ -742,7 +761,8 @@ print("Done!") { id: 'lesson9', title: '8. For Loops', - difficulty: 'easy', + tabtitle: 'For Loops', + level: 'basics', content: `

A more common type of loop is the for loop.

A for loop includes the creation of a variable that will be used to count how many times it should run.

@@ -828,11 +848,112 @@ print("Done!") })() + }, + { + id: 'robot1', + title: '1. Moving the Robot', + tabtitle: 'Importing Modules', + level: 'robot', + content: ` +

This robot simulation is a simplified version of a real robot.

+

It has a single library which you can use to access all its controls and sensors.

+

In a real robot you would have many different libraries for different parts, sensors, and even microcontroller functions.

+
+

We'll start by importing the robot library, and using it to move.

+

+import robot  # Import the robot library
+import time  # Import the time module
+
+robot.move(1) # Move forward at max speed
+time.sleep(2)  # Wait for 2 seconds
+robot.move(-1) # Move backward at max speed
+time.sleep(2)  # Wait for 2 seconds
+robot.move(0) # Stop the robot
+
+ `, + objectives: [ + "Import the time module", + "Print something", + "Use time.sleep() to pause for an amount of time", + "Print something else after the pause" + ], + + doneCondition: (() => { + return ({ code, consoleText, codeRanGood }) => { + const progress = { + importedTime: false, + printedBefore: false, + usedSleep: false, + printedAfter: false, + }; + + 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"); + + let hint = ""; + if (missing.length === 1) { + hint = `I still need you to ${missing[0]}`; + } else if (missing.length > 1) { + hint = `I still need you to ${missing.slice(0, -1).join(", ")} and ${missing.at(-1)}`; + } + + return { + done: + progress.importedTime && + progress.printedBefore && + progress.usedSleep && + progress.printedAfter, + progressArray: Object.values(progress), + hint, + }; + }; + })() + + + + }, { id: 'lesson10', title: '9. Libraries & Modules (time)', - difficulty: 'easy', + tabtitle: 'Importing Modules', + level: 'basics', content: `

A lot of the time we need more code than can fit in a single file, or we want to reuse our own, or someone elses code.

For that we use libraries and modules.

diff --git a/data/levels.json b/data/levels.json index b267d13..2ce93d5 100644 --- a/data/levels.json +++ b/data/levels.json @@ -13,7 +13,7 @@ { "position": { "x": 420, - "y": 200 + "y": 600 }, "vertices": [ { @@ -230,7 +230,7 @@ ], "position": { "x": 200, - "y": 300 + "y": 400 }, "strokeColor": "#999999", "fillColor": "#CCCCCC" diff --git a/game.js b/game.js index c10f2c4..53fef31 100644 --- a/game.js +++ b/game.js @@ -21,6 +21,7 @@ function showLesson(index) { const lesson = lessons[index]; loadLessonContent(lesson); + updateTabs(lessons, index); document.getElementById('prev-lesson').disabled = index === 0; document.getElementById('next-lesson').disabled = index === lessons.length - 1; @@ -35,6 +36,41 @@ function loadLessonContent(lesson) { document.getElementById('lesson-content').innerHTML = lesson.content; } +function updateTabs(lessons, currentIndex) { + let select = document.getElementById('lesson-select'); + let select_robot = document.getElementById('robot-select'); + select.innerHTML = ''; // Clear old options + select_robot.innerHTML = ''; // Clear old options + + + lessons.forEach((lesson, index) => { + const option = document.createElement('option'); + option.value = index; + option.textContent = lesson.title || `Lesson ${index + 1}`; + if (lesson.level == "basics") { + select.appendChild(option); + } else { + select_robot.appendChild(option); + } + }); + + // Set current selected lesson + select.value = currentIndex; + select_robot.value = currentIndex; + // Handle user selection + select.addEventListener('change', () => { + const selectedIndex = Number(select.value); + console.log("Selected lesson index:", selectedIndex); + showLesson(selectedIndex); + }); + select_robot.addEventListener('change', () => { + const selectedIndex = Number(select_robot.value); + console.log("Selected lesson index:", selectedIndex); + showLesson(selectedIndex); + }); +} + + document.getElementById('prev-lesson').addEventListener('click', () => { showLesson(currentLesson - 1); }); @@ -61,7 +97,7 @@ function checkLessonDone() { if (result.done) { markLessonDone(lesson.id); } - if (result.progressArray){ + if (result.progressArray) { console.log("Progress: ", result.progressArray); for (let i = 0; i < result.progressArray.length; i++) { const objective = result.progressArray[i]; @@ -183,8 +219,8 @@ function toggleObjective(index, completed = true) { } } -clearLessonProgress(); // Clear progress on load for testing -showLesson(9); +//clearLessonProgress(); // Clear progress on load for testing +showLesson(1); const consoleElement = document.getElementById("console"); const gameCanvas = document.getElementById("gameCanvas"); diff --git a/index.html b/index.html index 4eca3c0..4c66d7a 100644 --- a/index.html +++ b/index.html @@ -25,8 +25,27 @@ +
+ + +
diff --git a/pyodide-worker.js b/pyodide-worker.js index af64df1..c83232d 100644 --- a/pyodide-worker.js +++ b/pyodide-worker.js @@ -98,12 +98,22 @@ class RobotModule: return magnitude def move(self, speed): + if speed < -1: + speed = -1 + elif speed > 1: + speed = 1 + speed = speed/5000 send_to_main("move", speed) def fire(self): send_to_main("fire", None) def turn(self, deg): + if deg < -1: + deg = -1 + elif deg > 1: + deg = 1 + deg = deg/250 send_to_main("turn", deg) robot = RobotModule() diff --git a/style.css b/style.css index b2c03b1..b7370e3 100644 --- a/style.css +++ b/style.css @@ -9,7 +9,7 @@ html { padding: 0; height: 100%; font-family: Arial, sans-serif; - background-color: #fff; + background-color: #ffffff; display: flex; flex-direction: column; @@ -40,7 +40,7 @@ html { /* ===== Header (top bar) ===== */ header { - background-color: #f3f4f6; + background-color: #ffffff; /* light gray */ padding: 16px 0; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); @@ -193,6 +193,48 @@ main { } +.dropdown-row { + display: flex; + gap: 20px; + padding: 12px 0; +} + +.dropdown-group { + flex: 1; /* Equal horizontal space */ + display: flex; + flex-direction: column; +} + +.dropdown-title { + font-weight: 600; + margin-bottom: 4px; + font-size: 14px; + color: #333; +} + +.dropdown-group select { + width: 100%; + padding: 4px 8px; + font-size: 14px; + border-radius: 4px; + border: 1px solid #aaa; + background-color: white; + color: #333; +} + + + +#lesson-box { + background: white; + border: 1px solid #ffffff; + border-top: none; + border-radius: 0 0 8px 8px; + padding-top: 0; /* prevents double padding below tabs */ + margin-top: -1px; /* overlap the active tab */ +} + + + /* Monaco editor - left half */ #monaco-editor { @@ -289,7 +331,7 @@ main { } #lesson-box { - background: #f9f9f9; + background: #ffffff; padding: 8px 16px; border-bottom: 1px solid #ddd; font-family: sans-serif;