From 4c970c8fafa47e5550ac7d4c78819c61d6028358 Mon Sep 17 00:00:00 2001 From: Jake Date: Fri, 27 Jun 2025 00:38:55 +0800 Subject: [PATCH] added loops and libraries/modules --- data/lessons.js | 309 ++++++++++++++++++++++++++++++++++++++++-------- game.js | 2 +- 2 files changed, 261 insertions(+), 50 deletions(-) diff --git a/data/lessons.js b/data/lessons.js index a9d77fb..78c82da 100644 --- a/data/lessons.js +++ b/data/lessons.js @@ -623,7 +623,7 @@ else: }, { id: 'lesson8', - title: '8. Loops', + title: '8. While Loops', difficulty: 'easy', content: `

Most of the time we'll want our code to run multiple times, or even endlessly, in that case we need loops.

@@ -639,73 +639,88 @@ print("Done!")

You have to be careful with while loops, if the condition never becomes False, the loop will run forever!

`, + 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: (() => { - const progress = { - varCreated: false, - ifStatement: false, - elifStatement: false, - elseStatement: false, - threeDistinctPrints: false, - }; - return ({ code, consoleText, codeRanGood }) => { + const progress = { + varCreated: false, + hasWhileLoop: false, + whileConditionChecksVar: false, + printsVarInLoop: false, + updatesVarInLoop: false, + codeTerminates: codeRanGood + }; + if (!codeRanGood) { return { done: false, hint: "" }; } - // 1. Variable assignment + // Step 1: Variable assignment (e.g., x = 0) const assignRegex = /\b(\w+)\s*=\s*[\d'"]/; - progress.varCreated = assignRegex.test(code); + const match = code.match(assignRegex); + const varName = match ? match[1] : null; + progress.varCreated = !!varName; - // 2. Line-by-line block tracking const lines = code.split('\n'); - let currentBlock = null; - const printContents = { - if: null, - elif: null, - else: null, - }; + let inWhile = false; - for (let i = 0; i < lines.length; i++) { - const line = lines[i].trim(); + for (let line of lines) { + const trimmed = line.trim(); - if (/^if\s+.*:/.test(line)) { - progress.ifStatement = true; - currentBlock = "if"; + // 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; } - if (/^elif\s+.*:/.test(line)) { - progress.elifStatement = true; - currentBlock = "elif"; - 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; + } - if (/^else\s*:/.test(line)) { - progress.elseStatement = true; - currentBlock = "else"; - continue; - } + // Step 3: print(varName) + const printMatch = trimmed.match(/^print\(([^)]+)\)$/); + if (printMatch) { + const printedValue = printMatch[1].trim(); + if (printedValue === varName) { + progress.printsVarInLoop = true; + } + } - const printMatch = line.match(/print\s*\((.*?)\)/); - if (printMatch && currentBlock && !printContents[currentBlock]) { - const cleaned = printMatch[1].replace(/\s+/g, '').toLowerCase(); - printContents[currentBlock] = cleaned; + + // 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; + } } } - const printedValues = Object.values(printContents).filter(Boolean); - const uniqueValues = new Set(printedValues); - progress.threeDistinctPrints = uniqueValues.size === 3; - - // Hint generation + // ✅ Build hint 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"); + 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 @@ -713,11 +728,207 @@ print("Done!") : missing.slice(0, -1).join(", ") + " and " + missing[missing.length - 1]) : ""; - const done = progress.varCreated && progress.ifStatement && progress.elifStatement && - progress.elseStatement && progress.threeDistinctPrints; + const done = Object.values(progress).every(Boolean); - return { done, hint }; + return { + done, + progressArray: Object.values(progress), + hint + }; }; })() + + }, + { + id: 'lesson9', + title: '8. For Loops', + difficulty: 'easy', + 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.

+

This makes them much safer in most cases, as it's much harder to accidentally create an infinite loop.

+

Here's an example:

+

+for i in range(5):
+    print(i)
+print("Done!")
+      
+

The range(n) function actually creates a list of numbers, and each will be assigned to i in turn.

+

You can also add a second number, which will let you set a starting number as well as an ending one.

+

+

+# Create a for loop that runs from 100 to 150
+for i in range(100, 150):
+    print(i)
+print("Done!")
+      
+

You can add a third number, which will change how much your loop should iterate by, the default being 1

+

+# Create a for loop that runs from 0 to 27, by 3s
+for i in range(0, 27, 3):
+    print(i)
+print("Done!")
+      
+ `, + 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)', + difficulty: 'easy', + 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.

+
+

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.

+

To import a module, we use the import statement:

+

eg. import time

+
+

This effectively runs all the code in that module, and gives us access to all the functions and variables it defines.

+

Try this:

+

+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!")
+
+
+

Note: To count from actual 1-10 our range(start, end) starts at 1, and ends at 11, as the end number is exclusive.

+ `, + 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, + }; + }; + })() + + + + }, ]; diff --git a/game.js b/game.js index 40c967b..c10f2c4 100644 --- a/game.js +++ b/game.js @@ -184,7 +184,7 @@ function toggleObjective(index, completed = true) { } clearLessonProgress(); // Clear progress on load for testing -showLesson(6); +showLesson(9); const consoleElement = document.getElementById("console"); const gameCanvas = document.getElementById("gameCanvas");