added dropdown for lessons, and added first robot lesson to second dropdown

master
Jake 2025-07-03 23:25:13 +08:00
parent 4c970c8faf
commit 9b702b6ff1
7 changed files with 305 additions and 74 deletions

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"liveServer.settings.port": 5501
}

View File

@ -2,7 +2,8 @@ export const lessons = [
{
id: 'lesson1',
title: '1. Introduction to Python',
difficulty: 'easy',
tabtitle: 'Hello World',
level: 'basics',
content: `
<p>Let's learn some Python..</p>
<p>We'll start with what's called a "Hello World" program to make sure everythings working.</p>
@ -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: `
<p>Did you try typeing <code>print(Hello World)</code>, without the quotation marks?</p>
<p>If you didn't, give it a try now.</p>
@ -75,21 +77,19 @@ 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 = /(?<![\d.])[-+]?\d+(?![\d.])/;
const floatRegex = /[-+]?(?:\d+\.\d*|\.\d+)(?:[eE][-+]?\d+)?/;
const boolRegex = /\b(True|False|true|false)\b/;
// Matches print of int literal like 3, -42 (excluding floats)
const intPrintRegex = /print\s*\(\s*[-+]?\d+\s*\)/;
return ({ code, consoleText, codeRanGood }) => {
const progress = {
stringDone: false,
intDone: false,
floatDone: false,
boolDone: false,
// syntaxErrorDone: false, // optional
};
// 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,
@ -97,17 +97,26 @@ export const lessons = [
};
}
if (!progress.stringDone && stringRegex.test(code)) {
progress.stringDone = true;
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;
}
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 = [];
@ -116,29 +125,34 @@ export const lessons = [
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];
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: `
<p>Let's get python to do our math homework for us.</p>
<p>Python can handle basic arithmetic operations like addition, subtraction, multiplication, and division.</p>
@ -224,7 +238,8 @@ export const lessons = [
{
id: 'lesson4',
title: '4. Variables',
difficulty: 'easy',
tabtitle: 'Variables',
level: 'basics',
content: `
<p>It's common for us to need to keep some data or objects over multiple lines, for that we use <strong>variables</strong>.</p>
<p>Think of a variable as a container that holds an object for us to use later.</p>
@ -303,7 +318,8 @@ export const lessons = [
{
id: 'lesson5',
title: '5. More on Variables',
difficulty: 'easy',
tabtitle: 'Changing Variables',
level: 'basics',
content: `
<p>A variable will act like any other object of its data type.</p>
<p>For example, if we have a variable called <code>bob</code> with the value <code>32</code>, we can do math with it:</p>
@ -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: `
<p>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 <strong>conditionals</strong>.</p>
<pre><code>
@ -512,7 +529,8 @@ if num == 10:
{
id: 'lesson7',
title: '7. More Conditionals',
difficulty: 'easy',
tabtitle: 'if/elif/else',
level: 'basics',
content: `
<p>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 <code>elif</code> and <code>else</code>.</p>
<pre><code>
@ -624,7 +642,8 @@ else:
{
id: 'lesson8',
title: '8. While Loops',
difficulty: 'easy',
tabtitle: 'While Loops',
level: 'basics',
content: `
<p>Most of the time we'll want our code to run multiple times, or even endlessly, in that case we need <strong>loops</strong>.</p>
<p>Python has two main types of loops: <code>for</code> loops and <code>while</code> loops.</p>
@ -632,7 +651,7 @@ else:
<pre><code>
count = 0
while count < 5:
print("Count is:", count)
print(count)
count = count + 1
print("Done!")
</code></pre>
@ -742,7 +761,8 @@ print("Done!")
{
id: 'lesson9',
title: '8. For Loops',
difficulty: 'easy',
tabtitle: 'For Loops',
level: 'basics',
content: `
<p>A more common type of loop is the <strong>for</strong> loop.</p>
<p>A <code>for</code> loop includes the creation of a variable that will be used to count how many times it should run.</p>
@ -828,11 +848,112 @@ print("Done!")
})()
},
{
id: 'robot1',
title: '1. Moving the Robot',
tabtitle: 'Importing Modules',
level: 'robot',
content: `
<p>This robot simulation is a simplified version of a real robot.</p>
<p>It has a single library which you can use to access all its controls and sensors.</p>
<p>In a real robot you would have many different libraries for different parts, sensors, and even microcontroller functions.</p>
</br>
<p>We'll start by importing the robot library, and using it to move.</p>
<pre><code>
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
</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"
],
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: `
<p>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.</p>
<p>For that we use <strong>libraries</strong> and <strong>modules</strong>.</p>

View File

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

42
game.js
View File

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

View File

@ -25,8 +25,27 @@
</div>
</header>
<!-- New Lesson Box / Instructions Area -->
<section id="lesson-box">
<div class="dropdown-row content-width">
<div class="dropdown-group">
<div class="dropdown-title">Basics</div>
<select id="lesson-select"></select>
</div>
<div class="dropdown-group">
<div class="dropdown-title">Robot</div>
<select id="robot-select"></select>
</div>
<div class="dropdown-group">
<div class="dropdown-title">Challenges</div>
<select id="challenge-select"></select>
</div>
</div>
<div class="content-width" style="display: flex; gap: 20px; align-items: flex-start;">
<!-- 📘 Lesson content area (3/4 width) -->

View File

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

View File

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