1181 lines
39 KiB
JavaScript
1181 lines
39 KiB
JavaScript
export const lessons = [
|
||
{
|
||
id: 'lesson1',
|
||
title: '1. Introduction to Python',
|
||
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>
|
||
<p>In Python, this is done with the <code>print</code> function.</p>
|
||
<ol>
|
||
<li>In the code editor below, type <code>print("Hello World")</code></li>
|
||
<li>Click the "Run" button to execute your code</li>
|
||
<li>You should see "Hello World" printed in the output area</li>
|
||
</ol>
|
||
`,
|
||
objectives: [
|
||
"Print \"Hello World\" to the console"
|
||
],
|
||
doneCondition: (() => {
|
||
|
||
|
||
return ({ lesson, code, consoleText, codeRanGood }) => {
|
||
const progress = {
|
||
stringDone: false
|
||
};
|
||
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,
|
||
progressArray: Object.values(progress),
|
||
hint
|
||
};
|
||
};
|
||
})()
|
||
},
|
||
{
|
||
id: 'lesson2',
|
||
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>
|
||
</br>
|
||
<p>Thats what we call a <strong>Syntax Error</strong>.</p>
|
||
<p>Python doesn't know what the term (Hello World) means, the "" we added before tell Python that this is a "string", a series of characters that it doesn't need to try and understand, just to repeat.</p>
|
||
</br>
|
||
String is just one of the many <strong>Data Types</strong> that Python has.</p>
|
||
To continue, I want you to print the following all at once, each on a new line:
|
||
<ol>
|
||
<li>String: <code>print("Hello World")</code></li>
|
||
<li>Integer: <code>print(42)</code></li>
|
||
<li>Float: <code>print(3.14)</code></li>
|
||
<li>Boolean: <code>print(True)</code></li>
|
||
</ol>
|
||
<p>Click the "Run" button to execute your code</p>
|
||
`,
|
||
objectives: [
|
||
"Print a string to the console",
|
||
"Print an int to the console",
|
||
"Print a float to the console",
|
||
"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*\)/;
|
||
|
||
// Matches print of int literal like 3, -42 (excluding floats)
|
||
const intPrintRegex = /print\s*\(\s*[-+]?\d+\s*\)/;
|
||
|
||
// 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,
|
||
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',
|
||
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>
|
||
<p>Try the following operations in the code editor:</p>
|
||
<ol>
|
||
<li>Addition: <code>print(5 + 3)</code></li>
|
||
<li>Subtraction: <code>print(10 - 2)</code></li>
|
||
<li>Multiplication: <code>print(4 * 7)</code></li>
|
||
<li>Division: <code>print(20 / 5)</code></li>
|
||
</ol>
|
||
`,
|
||
objectives: [
|
||
"Do some addition",
|
||
"Do some subtraction",
|
||
"Do some multiplication",
|
||
"Do some division",
|
||
],
|
||
doneCondition: (() => {
|
||
// Persistent tracking object inside closure
|
||
|
||
|
||
return ({ code, consoleText, codeRanGood }) => {
|
||
const progress = {
|
||
addDone: false,
|
||
subDone: false,
|
||
mulDone: false,
|
||
divDone: false,
|
||
|
||
};
|
||
|
||
if (!codeRanGood) {
|
||
return {
|
||
done: false,
|
||
hint: ""
|
||
};
|
||
}
|
||
|
||
// if (!progress.syntaxErrorDone && syntaxErrorRegex.test(text)) {
|
||
// progress.syntaxErrorDone = true;
|
||
// }
|
||
if (!progress.addDone && code.includes("+")) {
|
||
progress.addDone = true;
|
||
}
|
||
if (!progress.subDone && code.includes("-")) {
|
||
progress.subDone = true;
|
||
}
|
||
if (!progress.mulDone && code.includes("*")) {
|
||
progress.mulDone = true;
|
||
}
|
||
if (!progress.divDone && code.includes("/")) {
|
||
progress.divDone = true;
|
||
}
|
||
|
||
let missing = [];
|
||
|
||
//if (!progress.syntaxErrorDone) missing.push("syntax error");
|
||
//if (!progress.stringDone) missing.push("string");
|
||
if (!progress.addDone) missing.push("addition");
|
||
if (!progress.subDone) missing.push("subtraction");
|
||
if (!progress.mulDone) missing.push("multiplication");
|
||
if (!progress.divDone) missing.push("division");
|
||
let hint = "I still need you to do some ";
|
||
|
||
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.addDone && progress.subDone && progress.mulDone && progress.divDone;
|
||
return {
|
||
done,
|
||
progressArray: Object.values(progress),
|
||
hint
|
||
};
|
||
};
|
||
})()
|
||
},
|
||
{
|
||
id: 'lesson4',
|
||
title: '4. Variables',
|
||
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>
|
||
<p>To make one, we only need to give it a name, and assign it a value.</p>
|
||
<p><code>bob = 32</code></p>
|
||
<p>In this case, we created a variable called <code>bob</code> and assigned it the value <code>32</code>.</p>
|
||
<p>As far as Python is concerned, <code>bob</code> is just a name for the number <code>32</code>.</p>
|
||
<p>Lets test it</p>
|
||
<ol>
|
||
<li>Initialize bob with the value 32: <code>bob = 32</code></li>
|
||
<li>Print the value of bob: <code>print(bob)</code></
|
||
</ol>
|
||
`,
|
||
objectives: [
|
||
"Initialize a variable with a value",
|
||
"Print the variable using print()"
|
||
],
|
||
doneCondition: (() => {
|
||
|
||
|
||
return ({ code, consoleText, codeRanGood }) => {
|
||
const progress = {
|
||
varCreated: false,
|
||
varPrinted: false,
|
||
varName: null,
|
||
};
|
||
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;
|
||
}
|
||
|
||
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,
|
||
progressArray: Object.values(progress),
|
||
hint
|
||
};
|
||
};
|
||
})()
|
||
},
|
||
{
|
||
id: 'lesson5',
|
||
title: '5. More on Variables',
|
||
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>
|
||
<p><code>print(bob + 10)</code> will print <code>42</code>.</p>
|
||
</br>
|
||
<p>We can also change the value of a variable:</p>
|
||
<pre><code>
|
||
bob = 32
|
||
print(bob)
|
||
bob = bob + 10
|
||
print(bob)
|
||
</code></pre>
|
||
<p>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.</p>
|
||
<p>Give it a try.</p>
|
||
`,
|
||
objectives: [
|
||
"Initialize a variable with a value",
|
||
"Print the variable using print()",
|
||
"Alter the value of the variable by adding, subtracting, multiplying, or dividing by itself",
|
||
"Print the variable again using print()",
|
||
],
|
||
doneCondition: (() => {
|
||
|
||
|
||
return ({ code, consoleText, codeRanGood }) => {
|
||
const progress = {
|
||
varCreated: false,
|
||
varPrinted: false,
|
||
varArithmeticDone: false,
|
||
varPrintedTwice: false,
|
||
};
|
||
if (!codeRanGood) {
|
||
return {
|
||
done: false,
|
||
progressArray: Object.values(progress),
|
||
hint: ""
|
||
};
|
||
}
|
||
|
||
const assignRegex = /^(\w+)\s*=\s*.+$/m;
|
||
const assignMatch = assignRegex.exec(code);
|
||
if (!assignMatch) {
|
||
return {
|
||
done: false,
|
||
progressArray: Object.values(progress),
|
||
hint: "I still need you to create a variable"
|
||
};
|
||
}
|
||
|
||
const varName = assignMatch[1];
|
||
|
||
const printRegex = new RegExp(`print\\s*\\(\\s*${varName}\\s*\\)`, "g");
|
||
const assignAllRegex = new RegExp(`^${varName}\\s*=.+$`, "gm");
|
||
const arithmeticRegex = new RegExp(
|
||
`^${varName}\\s*=\\s*${varName}\\s*[+\\-*/]\\s*.+$|^${varName}\\s*[+\\-*/]=\\s*.+$`,
|
||
"gm"
|
||
);
|
||
|
||
progress.varCreated = true;
|
||
|
||
const printMatches = [...code.matchAll(printRegex)];
|
||
progress.varPrinted = printMatches.length > 0;
|
||
if (!progress.varPrinted) {
|
||
return {
|
||
done: false,
|
||
progressArray: Object.values(progress),
|
||
hint: "I still need you to print the variable"
|
||
};
|
||
}
|
||
|
||
progress.varArithmeticDone = arithmeticRegex.test(code);
|
||
if (!progress.varArithmeticDone) {
|
||
return {
|
||
done: false,
|
||
progressArray: Object.values(progress),
|
||
|
||
hint: "I still need you to alter the variable (e.g. with arithmetic)"
|
||
};
|
||
}
|
||
|
||
const assignMatches = [...code.matchAll(assignAllRegex)];
|
||
if (assignMatches.length < 2) {
|
||
return {
|
||
done: false,
|
||
progressArray: Object.values(progress),
|
||
hint: "I still need you to alter the variable before printing it again"
|
||
};
|
||
}
|
||
|
||
// Debug logs
|
||
console.log("Variable:", varName);
|
||
console.log("Assignments found:", assignMatches.length);
|
||
assignMatches.forEach((m, i) =>
|
||
console.log(`Assign #${i}: index=${m.index}, text=${m[0].trim()}`)
|
||
);
|
||
console.log("Prints found:", printMatches.length);
|
||
printMatches.forEach((m, i) =>
|
||
console.log(`Print #${i}: index=${m.index}, text=${code.slice(m.index, m.index + 20)}`)
|
||
);
|
||
console.log("Arithmetic detected:", progress.varArithmeticDone);
|
||
|
||
const secondAssignIndex = assignMatches[1].index;
|
||
progress.varPrintedTwice = printMatches.some((m) => m.index > secondAssignIndex);
|
||
|
||
if (!progress.varPrintedTwice) {
|
||
return {
|
||
done: false,
|
||
progressArray: Object.values(progress),
|
||
hint: "I still need you to print the variable a second time after altering it",
|
||
};
|
||
}
|
||
|
||
return {
|
||
done: true,
|
||
progressArray: Object.values(progress),
|
||
hint: "",
|
||
};
|
||
};
|
||
})()
|
||
|
||
},
|
||
{
|
||
id: 'lesson6',
|
||
title: '6. Conditionals',
|
||
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>
|
||
bob = 5
|
||
if bob > 10:
|
||
print("Bob is greater than 10")
|
||
</code></pre>
|
||
<p>In this example, the print function will only run if <code>bob</code> is 11 or higher.</p>
|
||
</br>
|
||
<p>One thing to note here is the formatting. Note that after the <code>if bob > 10:</code> there is a colon (:)<p>
|
||
<p>This tells Python that the next line is part of a new block of code.</p>
|
||
<p>In Python, we use indentation to define blocks of code, so the next line must be indented.</p>
|
||
<p>After the <code>else:</code> we also have a colon, and the next line is indented again.</p>
|
||
<p>Try it for yourself:</p>
|
||
<ol>
|
||
<li>Initialize a variable with a value</li>
|
||
<li>Use an <code>if</code> statement to check if the variable is greater or less than a value</li>
|
||
<li>Print a message based on the condition</li>
|
||
</ol>
|
||
<p>To test if two values are equal, you can use the <code>==</code> operator.</p>
|
||
<p>eg. <pre><code>
|
||
num = 10
|
||
if num == 10:
|
||
print("num is equal to 10")
|
||
</code></pre>
|
||
`,
|
||
objectives: [
|
||
"Initialize a variable with a value",
|
||
"Create an if statement to check a condition", ,
|
||
"Print a message based on the condition",
|
||
],
|
||
|
||
doneCondition: (() => {
|
||
|
||
return ({ code, consoleText, codeRanGood }) => {
|
||
const progress = {
|
||
varCreated: false,
|
||
ifStatement: false,
|
||
printedSomething: false,
|
||
};
|
||
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,
|
||
progressArray: Object.values(progress),
|
||
hint
|
||
};
|
||
};
|
||
})()
|
||
|
||
},
|
||
{
|
||
id: 'lesson7',
|
||
title: '7. More Conditionals',
|
||
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>
|
||
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")
|
||
</code></pre>
|
||
<p>In this example, only one of the print statements can ever run, <code>elif</code> is short for "else if", and is a second if statement which is only tested it the <code>if</code> above it resolves to False.</p>
|
||
<p>The <code>else</code> statement is run if all previous conditions are False.</p>
|
||
<p>You can chain as many <code>elif</code> statements as you like, but only one <code>else</code> at the end.</p>
|
||
<p>Try it for yourself:</p>
|
||
`,
|
||
objectives: [
|
||
"Initialize a variable with a value",
|
||
"Create an if statement to check a condition", ,
|
||
"Create an elif statement to check another condition",
|
||
"Create an else statement to catch all other conditions",
|
||
"Print a message based on each of the conditions",
|
||
],
|
||
doneCondition: (() => {
|
||
|
||
return ({ code, consoleText, codeRanGood }) => {
|
||
|
||
const progress = {
|
||
varCreated: false,
|
||
ifStatement: false,
|
||
elifStatement: false,
|
||
elseStatement: false,
|
||
threeDistinctPrints: false,
|
||
};
|
||
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,
|
||
progressArray: Object.values(progress),
|
||
hint
|
||
};
|
||
};
|
||
})()
|
||
},
|
||
{
|
||
id: 'lesson8',
|
||
title: '8. While Loops',
|
||
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>
|
||
<p>The <code>while</code> loop will continue to run as long as a condition is True.</p>
|
||
<pre><code>
|
||
count = 0
|
||
while count < 5:
|
||
print(count)
|
||
count = count + 1
|
||
print("Done!")
|
||
</code></pre>
|
||
<p>You have to be careful with <code>while</code> loops, if the condition never becomes False, the loop will run forever!</p>
|
||
|
||
`,
|
||
objectives: [
|
||
"Create a variable",
|
||
"Create a while loop ",
|
||
"that runs while the value of that variable is less than a number",
|
||
"Print the value of the variable inside the loop",
|
||
"Add one or more to the variable inside the loop",
|
||
"Your loop should end on its own"
|
||
],
|
||
|
||
doneCondition: (() => {
|
||
return ({ code, consoleText, codeRanGood }) => {
|
||
const progress = {
|
||
varCreated: false,
|
||
hasWhileLoop: false,
|
||
whileConditionChecksVar: false,
|
||
printsVarInLoop: false,
|
||
updatesVarInLoop: false,
|
||
codeTerminates: codeRanGood
|
||
};
|
||
|
||
if (!codeRanGood) {
|
||
return { done: false, hint: "" };
|
||
}
|
||
|
||
// Step 1: Variable assignment (e.g., x = 0)
|
||
const assignRegex = /\b(\w+)\s*=\s*[\d'"]/;
|
||
const match = code.match(assignRegex);
|
||
const varName = match ? match[1] : null;
|
||
progress.varCreated = !!varName;
|
||
|
||
const lines = code.split('\n');
|
||
let inWhile = false;
|
||
|
||
for (let line of lines) {
|
||
const trimmed = line.trim();
|
||
|
||
// Step 2: Detect while loop and condition
|
||
if (/^while\s+(.*):/.test(trimmed)) {
|
||
progress.hasWhileLoop = true;
|
||
const condition = trimmed.match(/^while\s+(.*):/)[1];
|
||
if (varName && condition.includes(varName) && condition.includes('<')) {
|
||
progress.whileConditionChecksVar = true;
|
||
}
|
||
inWhile = true;
|
||
continue;
|
||
}
|
||
|
||
// Step 3–4: Check inside loop
|
||
if (inWhile) {
|
||
// Stop tracking if the line is dedented (exits the loop)
|
||
if (/^\S/.test(line)) {
|
||
inWhile = false;
|
||
continue;
|
||
}
|
||
|
||
// Step 3: print(varName)
|
||
const printMatch = trimmed.match(/^print\(([^)]+)\)$/);
|
||
if (printMatch) {
|
||
const printedValue = printMatch[1].trim();
|
||
if (printedValue === varName) {
|
||
progress.printsVarInLoop = true;
|
||
}
|
||
}
|
||
|
||
|
||
// Step 4: update variable (x += 1 or x = x + 1)
|
||
const updateRegex1 = new RegExp(`\\b${varName}\\s*\\+=\\s*\\d+`);
|
||
const updateRegex2 = new RegExp(`\\b${varName}\\s*=\\s*${varName}\\s*\\+\\s*\\d+`);
|
||
if (updateRegex1.test(trimmed) || updateRegex2.test(trimmed)) {
|
||
progress.updatesVarInLoop = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
// ✅ Build hint
|
||
const missing = [];
|
||
if (!progress.varCreated) missing.push("create a variable");
|
||
if (!progress.hasWhileLoop) missing.push("add a while loop");
|
||
if (!progress.whileConditionChecksVar) missing.push("make the while loop check if the variable is less than something");
|
||
if (!progress.printsVarInLoop) missing.push("print the variable inside the loop");
|
||
if (!progress.updatesVarInLoop) missing.push("update the variable in the loop");
|
||
if (!progress.codeTerminates) missing.push("make sure the loop ends on its own");
|
||
|
||
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 = Object.values(progress).every(Boolean);
|
||
|
||
return {
|
||
done,
|
||
progressArray: Object.values(progress),
|
||
hint
|
||
};
|
||
};
|
||
})()
|
||
|
||
},
|
||
{
|
||
id: 'lesson9',
|
||
title: '8. For Loops',
|
||
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>
|
||
<p>This makes them much safer in most cases, as it's much harder to accidentally create an infinite loop.</p>
|
||
<p>Here's an example:</p>
|
||
<pre><code>
|
||
for i in range(5):
|
||
print(i)
|
||
print("Done!")
|
||
</code></pre>
|
||
<p>The <code>range(n)</code> function actually creates a list of numbers, and each will be assigned to <code>i</code> in turn.</p>
|
||
<p>You can also add a second number, which will let you set a starting number as well as an ending one.</p>
|
||
<p></p>
|
||
<pre><code>
|
||
# Create a for loop that runs from 100 to 150
|
||
for i in range(100, 150):
|
||
print(i)
|
||
print("Done!")
|
||
</code></pre>
|
||
<p>You can add a third number, which will change how much your loop should iterate by, the default being 1</p>
|
||
<pre><code>
|
||
# Create a for loop that runs from 0 to 27, by 3s
|
||
for i in range(0, 27, 3):
|
||
print(i)
|
||
print("Done!")
|
||
</code></pre>
|
||
`,
|
||
objectives: [
|
||
"Create a loop",
|
||
"The loop should use range() and have all three arguments (start, end, step)",
|
||
"Your loop should end on its own (i.e. not run forever)",
|
||
],
|
||
|
||
doneCondition: (() => {
|
||
return ({ code, consoleText, codeRanGood }) => {
|
||
const progress = {
|
||
forLoopFound: false,
|
||
rangeHas3Args: false,
|
||
codeTerminates: codeRanGood,
|
||
};
|
||
|
||
if (!codeRanGood) {
|
||
return { done: false, hint: "" };
|
||
}
|
||
|
||
const lines = code.split('\n');
|
||
for (let line of lines) {
|
||
const trimmed = line.trim();
|
||
|
||
// Look for a 'for' loop using range()
|
||
const forMatch = trimmed.match(/^for\s+\w+\s+in\s+range\((.*?)\):/);
|
||
if (forMatch) {
|
||
progress.forLoopFound = true;
|
||
|
||
const rangeArgs = forMatch[1].split(',').map(s => s.trim());
|
||
if (rangeArgs.length === 3) {
|
||
progress.rangeHas3Args = true;
|
||
}
|
||
break; // Found the loop, no need to keep scanning
|
||
}
|
||
}
|
||
|
||
// Hints
|
||
const missing = [];
|
||
if (!progress.forLoopFound) missing.push("write a for loop using range()");
|
||
else if (!progress.rangeHas3Args) missing.push("use start, end, and step in the range()");
|
||
if (!progress.codeTerminates) missing.push("make sure the loop ends on its own");
|
||
|
||
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 = Object.values(progress).every(Boolean);
|
||
|
||
return {
|
||
done,
|
||
progressArray: Object.values(progress),
|
||
hint
|
||
};
|
||
};
|
||
})()
|
||
|
||
|
||
},
|
||
{
|
||
id: 'lesson10',
|
||
title: '9. Libraries & Modules (time)',
|
||
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>
|
||
</br>
|
||
<p>As an example, we'll import the "time" module, which lets do things like measure time, which we can use for all sorts of useful things.</p>
|
||
<p>To import a module, we use the <code>import</code> statement:</p>
|
||
<p>eg. <code>import time</code></p>
|
||
</br>
|
||
<p>This effectively runs all the code in that module, and gives us access to all the functions and variables it defines.</p>
|
||
<p>Try this:</p>
|
||
<pre><code>
|
||
import time # Import the time module
|
||
for i in range(1, 10+1):
|
||
print(i)
|
||
time.sleep(1) # Wait for 1 second (0.5 for half a second)
|
||
print("Done!")
|
||
</code></pre>
|
||
</br>
|
||
<p><strong>Note:</strong> To count from actual 1-10 our <code>range(start, end)</code> starts at 1, and ends at 11, as the <code>end</code> number is exclusive.</p>
|
||
`,
|
||
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: 'robot1',
|
||
title: '1. Moving the Robot',
|
||
tabtitle: 'Importing Modules',
|
||
level: 'robot',
|
||
map: 'Level 1',
|
||
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: [
|
||
"Reach the first checkpoint",
|
||
"Reach the second checkpoint",
|
||
|
||
"Code should complete without errors"
|
||
],
|
||
|
||
doneCondition: (() => {
|
||
return ({ code, consoleText, codeRanGood, gameWorld }) => {
|
||
const progress = {
|
||
firstCheckpoint: gameWorld.waypointsReached[0],
|
||
secondCheckpoint: gameWorld.waypointsReached[1],
|
||
codeRanGood: codeRanGood,
|
||
|
||
};
|
||
|
||
// if (!codeRanGood) {
|
||
// return { done: false, progressArray: Object.values(progress), hint: "" };
|
||
// }
|
||
|
||
|
||
|
||
// 5. Build hint
|
||
const missing = [];
|
||
if (!progress.firstCheckpoint) missing.push("reach the first checkpoint");
|
||
if (!progress.secondCheckpoint) missing.push("reach the second checkpoint");
|
||
|
||
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.firstCheckpoint &&
|
||
progress.secondCheckpoint &&
|
||
progress.codeRanGood,
|
||
progressArray: Object.values(progress),
|
||
hint,
|
||
};
|
||
};
|
||
})()
|
||
},
|
||
{
|
||
id: 'robot2',
|
||
title: '2. Steering the Robot',
|
||
tabtitle: 'Importing Modules',
|
||
level: 'robot',
|
||
map: 'Level 2',
|
||
content: `
|
||
<p>Turning is very similar to moving, we use the <code>robot.turn(amount)</code> function.</p>
|
||
<p>The <code>amount</code> parameter is a number between -1 and 1, where -1 is full left, 0 is no turn, and 1 is full right.</p>
|
||
|
||
<pre><code>
|
||
import robot
|
||
import time
|
||
|
||
robot.turn(1)
|
||
time.sleep(2)
|
||
robot.turn(0)
|
||
</code></pre>
|
||
</br>
|
||
<p>This code causes the robot to turn right at max speed for 2 seconds, then stop.</p>
|
||
|
||
<p>You'll need to combine moving, turning, and waiting to reach all the checkpoints.</p>
|
||
<p><strong>Note:</strong> The values for move, turn, and sleep can all be decimal numbers (floats). ie <code>time.sleep(0.5)</code> or <code>robot.move(0.8)</code></p>
|
||
`,
|
||
objectives: [
|
||
"Reach the first checkpoint",
|
||
"Reach the second checkpoint",
|
||
|
||
"Code should complete without errors"
|
||
],
|
||
|
||
doneCondition: (() => {
|
||
return ({ code, consoleText, codeRanGood, gameWorld }) => {
|
||
const progress = {
|
||
firstCheckpoint: gameWorld.waypointsReached[0],
|
||
secondCheckpoint: gameWorld.waypointsReached[1],
|
||
codeRanGood: codeRanGood,
|
||
|
||
};
|
||
|
||
// if (!codeRanGood) {
|
||
// return { done: false, progressArray: Object.values(progress), hint: "" };
|
||
// }
|
||
|
||
|
||
|
||
// 5. Build hint
|
||
const missing = [];
|
||
if (!progress.firstCheckpoint) missing.push("reach the first checkpoint");
|
||
if (!progress.secondCheckpoint) missing.push("reach the second checkpoint");
|
||
|
||
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.firstCheckpoint &&
|
||
progress.secondCheckpoint &&
|
||
progress.codeRanGood,
|
||
progressArray: Object.values(progress),
|
||
hint,
|
||
};
|
||
};
|
||
})()
|
||
|
||
|
||
|
||
|
||
},
|
||
{
|
||
id: 'robot3',
|
||
title: '3. Using the Distance Sensors',
|
||
tabtitle: 'Importing Modules',
|
||
level: 'robot',
|
||
map: 'Level 3',
|
||
content: `
|
||
<p>We can't do much just by moving a robot using delays, so lets start using its sensors.</p>
|
||
<p>The <code>robot.get_distance_left()</code> function returns the length of the left sensor beam.</p>
|
||
<p>When there is something blocking it, it gets smaller, and you can use this to detect obstacles.</p>
|
||
|
||
<p>Try this code, and watch the console output:</p>
|
||
<pre><code>
|
||
import robot
|
||
import time
|
||
|
||
robot.move(0.5)
|
||
while True:
|
||
distance = robot.get_distance_left() # Get the distance from the left sensor
|
||
print(distance) # Print the distance
|
||
time.sleep(0.1) # Wait for on tenth of a second
|
||
</code></pre>
|
||
</br>
|
||
<p>Note how the distance changes as the robot moves.</p>
|
||
<p>We can use this to automatically steer around obstacles.</p>
|
||
<p>Add the following code AFTER the <code>print(distance)</code> line:</p>
|
||
<pre><code>
|
||
if distance < 50: # If the distance is less than 0.5 meters
|
||
robot.turn(1) # Turn right at max speed
|
||
else:
|
||
robot.turn(0) # Stop turning
|
||
</code></pre>
|
||
</br>
|
||
<p>If you want to get the length of the right sensor, it's <code>robot.get_distance_right()</code>.</p>
|
||
|
||
</br>
|
||
<p><strong>Note:</strong> Having that short <code>time.sleep(0.1)</code> is very important, if the loop runs too fast it overwhelms the system and nothing will happen.</p>
|
||
`,
|
||
objectives: [
|
||
"Reach the checkpoint",
|
||
|
||
"Code should complete without errors"
|
||
],
|
||
|
||
doneCondition: (() => {
|
||
return ({ code, consoleText, codeRanGood, gameWorld }) => {
|
||
const progress = {
|
||
firstCheckpoint: gameWorld.waypointsReached[0],
|
||
codeRanGood: codeRanGood,
|
||
|
||
};
|
||
|
||
// if (!codeRanGood) {
|
||
// return { done: false, progressArray: Object.values(progress), hint: "" };
|
||
// }
|
||
|
||
|
||
|
||
// 5. Build hint
|
||
const missing = [];
|
||
if (!progress.firstCheckpoint) missing.push("reach the first checkpoint");
|
||
|
||
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.firstCheckpoint &&
|
||
progress.codeRanGood,
|
||
progressArray: Object.values(progress),
|
||
hint,
|
||
};
|
||
};
|
||
})()
|
||
|
||
|
||
|
||
|
||
},
|
||
];
|