added dropdown for lessons, and added first robot lesson to second dropdown
parent
4c970c8faf
commit
9b702b6ff1
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"liveServer.settings.port": 5501
|
||||
}
|
||||
201
data/lessons.js
201
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: `
|
||||
<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,16 +125,16 @@ 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),
|
||||
|
|
@ -134,11 +143,16 @@ export const lessons = [
|
|||
};
|
||||
})()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
},
|
||||
{
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
40
game.js
40
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);
|
||||
});
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
19
index.html
19
index.html
|
|
@ -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) -->
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
48
style.css
48
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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue