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:
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 @@
+
+
+
+
+ Basics
+
+
+
+
+ Robot
+
+
+
+
+ Challenges
+
+
+
+
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;