@plasius/learning 0.2.25 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,355 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/courses/robot-maze-dash.ts
21
+ var robot_maze_dash_exports = {};
22
+ __export(robot_maze_dash_exports, {
23
+ course: () => course,
24
+ practice: () => practice
25
+ });
26
+ module.exports = __toCommonJS(robot_maze_dash_exports);
27
+
28
+ // src/mission-authoring.ts
29
+ var JUNIOR_CODER_MISSION_STAGE_ORDER_V1 = [
30
+ "learn",
31
+ "predict",
32
+ "build",
33
+ "run",
34
+ "assess",
35
+ "inspect",
36
+ "fix",
37
+ "explain",
38
+ "reward"
39
+ ];
40
+
41
+ // src/course-contracts.ts
42
+ var LEARNING_COURSE_STAGE_ORDER = JUNIOR_CODER_MISSION_STAGE_ORDER_V1;
43
+ var LEARNING_COURSE_LIMITS = Object.freeze({
44
+ missions: 6,
45
+ stagesPerMission: 9,
46
+ projectFiles: 8,
47
+ sourceCharactersPerFile: 64e3,
48
+ sourceCharactersPerProject: 96e3
49
+ });
50
+ var ID = /^[a-z0-9][a-z0-9.-]{0,159}$/u;
51
+ var FILE_PATH = /^[a-z0-9][a-z0-9_-]{0,63}\.(?:js|py|cpp|html|css|json)$/u;
52
+ var VERSION = /^\d+\.\d+\.\d+$/u;
53
+ var LANGUAGES = ["javascript", "python", "cpp", "html", "css", "blocks", "json"];
54
+ var CATEGORIES = ["game", "robot", "vibe", "web-app"];
55
+ var PLACEHOLDER = /\b(?:TODO|TBD|coming soon|placeholder|lorem ipsum)\b/iu;
56
+ var record = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
57
+ var text = (value, minimum = 1, maximum = 8e3) => typeof value === "string" && value.trim().length >= minimum && value.length <= maximum;
58
+ var id = (value) => typeof value === "string" && ID.test(value);
59
+ var integer = (value, minimum, maximum) => typeof value === "number" && Number.isSafeInteger(value) && value >= minimum && value <= maximum;
60
+ var exactKeys = (value, keys) => Object.keys(value).length === keys.length && keys.every((key) => Object.hasOwn(value, key));
61
+ var LearningCourseInputError = class extends Error {
62
+ constructor() {
63
+ super("Invalid learning course input.");
64
+ this.name = "LearningCourseInputError";
65
+ }
66
+ };
67
+ function fileDefinitions(value) {
68
+ return Array.isArray(value) && value.length >= 1 && value.length <= LEARNING_COURSE_LIMITS.projectFiles && value.every((file) => record(file) && exactKeys(file, ["path", "language", "maximumCharacters"]) && typeof file.path === "string" && FILE_PATH.test(file.path) && typeof file.language === "string" && LANGUAGES.includes(file.language) && integer(file.maximumCharacters, 1, LEARNING_COURSE_LIMITS.sourceCharactersPerFile)) && new Set(value.map((file) => file.path)).size === value.length;
69
+ }
70
+ function parseLearningProject(course2, value) {
71
+ if (!fileDefinitions(course2.projectFiles) || !record(value) || !exactKeys(value, ["files"]) || !Array.isArray(value.files) || value.files.length !== course2.projectFiles.length) throw new LearningCourseInputError();
72
+ const files = [];
73
+ const seen = /* @__PURE__ */ new Set();
74
+ let characters = 0;
75
+ for (const file of value.files) {
76
+ if (!record(file) || !exactKeys(file, ["path", "source"]) || typeof file.path !== "string" || typeof file.source !== "string" || seen.has(file.path)) throw new LearningCourseInputError();
77
+ const definition = course2.projectFiles.find((candidate) => candidate.path === file.path);
78
+ if (!definition || file.source.length > definition.maximumCharacters || file.source.includes("\0")) throw new LearningCourseInputError();
79
+ characters += file.source.length;
80
+ if (characters > LEARNING_COURSE_LIMITS.sourceCharactersPerProject) throw new LearningCourseInputError();
81
+ seen.add(file.path);
82
+ files.push({ path: file.path, source: file.source });
83
+ }
84
+ return { files: course2.projectFiles.map((definition) => files.find((file) => file.path === definition.path)) };
85
+ }
86
+ function validateLearningCourse(value) {
87
+ const issues = [];
88
+ const add = (code, path) => {
89
+ issues.push({ code, path });
90
+ };
91
+ if (!record(value)) return [{ code: "invalid-manifest", path: "$" }];
92
+ if (!exactKeys(value, [
93
+ "schemaVersion",
94
+ "moduleId",
95
+ "moduleVersion",
96
+ "slug",
97
+ "title",
98
+ "summary",
99
+ "runtimeId",
100
+ "category",
101
+ "estimatedMinutes",
102
+ "completionAssessmentId",
103
+ "projectFiles",
104
+ "starterProject",
105
+ "reference",
106
+ "missions",
107
+ "completionBadge"
108
+ ]) || value.schemaVersion !== "1" || !id(value.moduleId) || !id(value.slug) || !id(value.runtimeId) || !text(value.moduleVersion) || !VERSION.test(value.moduleVersion) || typeof value.category !== "string" || !CATEGORIES.includes(value.category) || !text(value.title, 3, 160) || !text(value.summary, 40, 2e3) || !integer(value.estimatedMinutes, 60, 3600) || !id(value.completionAssessmentId) || !record(value.completionBadge) || !exactKeys(value.completionBadge, ["id", "title"]) || !id(value.completionBadge.id) || !text(value.completionBadge.title, 3, 160)) add("invalid-manifest", "$");
109
+ if (!fileDefinitions(value.projectFiles)) add("invalid-project", "projectFiles");
110
+ else {
111
+ try {
112
+ parseLearningProject({ projectFiles: value.projectFiles }, value.starterProject);
113
+ } catch {
114
+ add("invalid-project", "starterProject");
115
+ }
116
+ }
117
+ if (!Array.isArray(value.reference) || value.reference.length < 1 || value.reference.length > 80 || value.reference.some((entry) => !record(entry) || !exactKeys(entry, ["name", "signature", "description", "example"]) || !text(entry.name) || !text(entry.signature) || !text(entry.description, 20) || !text(entry.example))) add("incomplete-content", "reference");
118
+ if (!Array.isArray(value.missions)) {
119
+ add("mission-count", "missions");
120
+ return issues;
121
+ }
122
+ if (value.missions.length !== LEARNING_COURSE_LIMITS.missions) add("mission-count", "missions");
123
+ if (value.missions.length > LEARNING_COURSE_LIMITS.missions) return issues;
124
+ const seen = /* @__PURE__ */ new Set();
125
+ const checkId = (candidate, path) => {
126
+ if (!id(candidate)) add("invalid-manifest", path);
127
+ else if (seen.has(candidate)) add("duplicate-id", path);
128
+ else seen.add(candidate);
129
+ };
130
+ let minutes = 0;
131
+ value.missions.forEach((mission, missionIndex) => {
132
+ const path = `missions[${missionIndex}]`;
133
+ if (!record(mission)) {
134
+ add("invalid-manifest", path);
135
+ return;
136
+ }
137
+ checkId(mission.id, `${path}.id`);
138
+ checkId(mission.assessmentId, `${path}.assessmentId`);
139
+ if (!exactKeys(mission, ["id", "title", "concepts", "estimatedMinutes", "goals", "assessmentId", "stages", "extension"]) || !text(mission.title, 3, 160) || !integer(mission.estimatedMinutes, 10, 600) || !Array.isArray(mission.concepts) || mission.concepts.length < 1 || mission.concepts.length > 12 || mission.concepts.some((concept) => !text(concept, 2, 120)) || !Array.isArray(mission.goals) || mission.goals.length < 1 || mission.goals.length > 12 || mission.goals.some((goal) => !text(goal, 20, 2e3)) || !text(mission.extension, 30, 2e3)) add("incomplete-content", path);
140
+ if (typeof mission.estimatedMinutes === "number") minutes += mission.estimatedMinutes;
141
+ if (!Array.isArray(mission.stages) || mission.stages.length !== LEARNING_COURSE_LIMITS.stagesPerMission) {
142
+ add("stage-order", `${path}.stages`);
143
+ return;
144
+ }
145
+ mission.stages.forEach((stage, stageIndex) => {
146
+ const stagePath = `${path}.stages[${stageIndex}]`;
147
+ if (!record(stage)) {
148
+ add("invalid-manifest", stagePath);
149
+ return;
150
+ }
151
+ checkId(stage.id, `${stagePath}.id`);
152
+ if (stage.kind !== LEARNING_COURSE_STAGE_ORDER[stageIndex]) add("stage-order", stagePath);
153
+ if (!exactKeys(stage, ["id", "kind", "title", "instruction", "help"]) || !text(stage.title, 3, 160) || !text(stage.instruction, 40) || !text(stage.help, 30) || typeof stage.instruction === "string" && PLACEHOLDER.test(stage.instruction)) add("incomplete-content", stagePath);
154
+ });
155
+ });
156
+ if (minutes !== value.estimatedMinutes) add("invalid-manifest", "estimatedMinutes");
157
+ return issues;
158
+ }
159
+ function parseLearningCourse(value) {
160
+ if (validateLearningCourse(value).length) throw new LearningCourseInputError();
161
+ return structuredClone(value);
162
+ }
163
+
164
+ // src/courses/course-authoring.ts
165
+ function authorCourse(header, missions) {
166
+ const course2 = parseLearningCourse({
167
+ ...header,
168
+ schemaVersion: "1",
169
+ moduleVersion: "2.0.0",
170
+ moduleId: `junior-coder.${header.slug}`,
171
+ runtimeId: `${header.slug}.v2`,
172
+ estimatedMinutes: missions.length * 60,
173
+ completionAssessmentId: `${header.slug}.final`,
174
+ completionBadge: { id: `${header.slug}.completed`, title: `${header.title} creator` },
175
+ missions: missions.map((mission, index) => ({
176
+ id: `${header.slug}.m${index + 1}`,
177
+ title: mission.title,
178
+ concepts: mission.concepts,
179
+ goals: mission.goals,
180
+ estimatedMinutes: 60,
181
+ assessmentId: `${header.slug}.m${index + 1}.assessment`,
182
+ extension: mission.extension,
183
+ stages: LEARNING_COURSE_STAGE_ORDER.map((kind) => ({
184
+ id: `${header.slug}.m${index + 1}.${kind}`,
185
+ kind,
186
+ title: `${kind.charAt(0).toUpperCase()}${kind.slice(1)}: ${mission.title}`,
187
+ instruction: mission.activities[kind][0],
188
+ help: mission.activities[kind][1]
189
+ }))
190
+ }))
191
+ });
192
+ const practice2 = structuredClone(missions.flatMap((mission, index) => ["learn", "predict", "explain"].map((kind) => ({ ...mission.questions[kind], stageId: `${header.slug}.m${index + 1}.${kind}` }))));
193
+ for (const question of practice2) {
194
+ if (question.question.trim().length < 20 || question.feedback.trim().length < 40 || question.choices.length !== 3 || question.choices.some((choice) => choice.trim().length < 2) || new Set(question.choices).size !== 3 || !Number.isInteger(question.correctChoice) || question.correctChoice < 0 || question.correctChoice > 2) throw new Error("Invalid course practice question.");
195
+ }
196
+ return { course: course2, practice: practice2 };
197
+ }
198
+
199
+ // src/courses/robot-maze-dash.ts
200
+ var { course, practice } = authorCourse({
201
+ slug: "robot-maze-dash",
202
+ title: "Robot Maze Dash",
203
+ category: "game",
204
+ summary: "Build a rescue robot that can navigate an unfamiliar maze. Start with a short route, discover repeats and sensors, name useful routines, and collect stranded explorers. A rescue ends as soon as the robot reaches the pad with every explorer collected. Your final program must work when the maze changes.",
205
+ projectFiles: [{ path: "maze.json", language: "blocks", maximumCharacters: 16e3 }],
206
+ starterProject: { files: [{ path: "maze.json", source: '{\n "routines": {},\n "program": [{ "do": "forward" }]\n}\n' }] },
207
+ reference: [
208
+ { name: "forward", signature: '{ "do": "forward" }', description: "Move one square in the direction the robot faces. A wall blocks the move; it never teleports the robot.", example: '{ "do": "forward" }' },
209
+ { name: "left", signature: '{ "do": "left" }', description: "Turn a quarter-turn left without changing the robot's square.", example: '{ "do": "left" }' },
210
+ { name: "right", signature: '{ "do": "right" }', description: "Turn a quarter-turn right. Remember that the next forward follows the new heading.", example: '{ "do": "right" }' },
211
+ { name: "repeat", signature: '{ "do": "repeat", "times": 3, "body": [...] }', description: "Run the enclosed sequence a whole-number count from 1 to 20. Every enclosed action still uses the run's step budget.", example: '{ "do": "repeat", "times": 3, "body": [{ "do": "forward" }] }' },
212
+ { name: "if-clear", signature: '{ "do": "if-clear", "then": [...], "else": [...] }', description: "Read the square directly ahead at this moment, then run one branch. An earlier reading cannot describe a later square.", example: '{ "do": "if-clear", "then": [{ "do": "forward" }], "else": [{ "do": "right" }] }' },
213
+ { name: "call", signature: '{ "do": "call", "name": "search" }', description: "Run a named sequence from routines. A routine can call other routines within the bounded call depth.", example: '"routines": { "search": [{ "do": "forward" }, { "do": "collect" }] }' },
214
+ { name: "collect", signature: '{ "do": "collect" }', description: "Collect the explorer at the current square once. An empty square or a second collection adds nothing.", example: '{ "do": "collect" }' }
215
+ ]
216
+ }, [
217
+ {
218
+ title: "A route to the rescue pad",
219
+ concepts: ["Sequence", "Position", "Direction"],
220
+ goals: ["Reach the rescue pad from the marked start without entering a wall.", "Describe how a turn changes direction without changing position."],
221
+ extension: "Find a second route to the same pad. Compare how many moves and turns it needs, then save both versions with names that describe their routes.",
222
+ activities: {
223
+ learn: ["The robot follows instructions in order. Forward moves one square; left and right change its heading. On the map, columns increase to the right and rows increase downwards. Trace forward, right, forward with your finger or the text map before editing the blocks.", "Use the direction arrow and coordinate readout together. Facing east, right means south; it does not always mean move towards the right edge of the screen."],
224
+ predict: ["Start at column 1, row 1, facing east. Predict the final square after forward, right, forward. Keep separate notes for position and heading so that turning does not accidentally add a move.", "Write each instruction on a new row: start (1,1,east), then update either the square or the heading."],
225
+ build: ["Replace the single forward block with a route to the marked rescue pad. Use only forward, left and right in this mission. You can add and reorder blocks with the keyboard, or edit the equivalent maze.json project.", "Place one instruction at a time. If the route changes direction, put a turn before the forward instruction that enters the new corridor."],
226
+ run: ["Run the route and watch the highlighted square advance. Pause or step through the trace if the robot stops early. Compare the final coordinates and heading with your prediction; the text trace contains the same information as the map.", "A blocked move ends the run at the last safe square. Look one instruction before the blocked move to check the heading."],
227
+ assess: ["Check the saved route against the rescue-pad mission. The checks look for a valid sequence, arrival at the pad, a clear path and a bounded number of actions. A route that only looks close has not reached the pad.", "Save or let autosave finish before checking. The result belongs to those exact project files, so changes need a fresh check."],
228
+ inspect: ["Read the first failed route check and find its square in the trace. Decide whether the robot took a wrong turn, walked too far, or stopped too soon. If every check passes, identify the turn where direction mattered most.", "Change the earliest wrong instruction first. Later positions depend on it, so repairing the end of the route first can hide the actual mistake."],
229
+ fix: ["Repair the route using the trace, run it again and check the new saved version. Keep the robot inside the maze and finish on the pad. A successful old result cannot prove that an edited route still works.", "Count the corridor squares from the robot's current position, not from the beginning of the map."],
230
+ explain: ["Explain why turning right and moving forward are separate instructions. Use the position and heading from your successful trace to choose the explanation that matches what the robot did.", "A turn changes the next move's direction. It does not move the robot into an adjacent square by itself."],
231
+ reward: ["Save your working route as First rescue. You have built a complete sequence, used a trace to debug it and reached the rescue pad. In the next mission you will replace repeated instructions with a repeat block.", "Keep this named save so you can compare the expanded route with its shorter repeated form later."]
232
+ },
233
+ questions: {
234
+ learn: { question: "What changes when a robot executes a right turn?", choices: ["Its heading", "Its column and row", "Its collected count"], correctChoice: 0, feedback: "A right turn changes heading only. Movement happens when a later forward instruction runs." },
235
+ predict: { question: "From (1,1,east), where does forward, right, forward finish?", choices: ["(3,1,east)", "(2,2,south)", "(1,2,west)"], correctChoice: 1, feedback: "Forward reaches (2,1); right faces south; the next forward reaches (2,2)." },
236
+ explain: { question: "Why did the turn belong before the second corridor's forward move?", choices: ["Turns collect an explorer", "Forward always travels east", "Forward uses the current heading"], correctChoice: 2, feedback: "The heading is part of the robot's state. Forward reads that state each time it moves." }
237
+ }
238
+ },
239
+ {
240
+ title: "Repeat the useful part",
241
+ concepts: ["Loops", "Loop body", "Counting"],
242
+ goals: ["Use a repeat block to traverse equal corridor sections without copying each instruction.", "Show that repeating a turn differs from repeating a move-and-turn pair."],
243
+ extension: "Draw a small square route with a repeated body, then change only the repeat count. Predict whether the robot returns to the start before you run it.",
244
+ activities: {
245
+ learn: ["A repeat block runs its whole body again. Repeating forward three times moves three squares; repeating forward then right traces three sides of a small square. Put only the useful repeated part inside the body.", "Read the opening and closing body brackets as a box. Everything inside that box happens on each repetition."],
246
+ predict: ["Predict the heading after repeat four times with a body containing forward and right. Assume all four moves are clear. Count turns as well as moves, then decide whether the robot returns to its starting square.", "Draw one short edge per repetition. Four quarter-turns make one complete turn."],
247
+ build: ["Open your First rescue save and find equal runs of forward instructions. Replace each run with a repeat block. Extend the route through the longer corridor shown for this mission without copying dozens of individual moves.", "The times value is a whole number from 1 to 20. A repeat body is an array just like program; do not put the times value inside the body."],
248
+ run: ["Step through the expanded trace of the repeated route. Find where one repetition ends and the next begins. Confirm that the same robot actions happen as in the expanded version, including the final heading.", "A short program can produce many actions. The trace counts executed actions, not just the number of visible blocks."],
249
+ assess: ["Check that the repeated route reaches the pad with the required loop structure and no blocked moves. The evaluator also checks action limits, so a repeat cannot be used to create an unbounded journey.", "Use the repeat for repeated work and keep any one-off turn outside it. Repeating a route's final turn can change its destination."],
250
+ inspect: ["Inspect the first repetition whose starting heading differs from your expectation. If the robot turns too often, check whether the turn belongs inside or outside the loop body. For a passing route, explain why its count is sufficient.", "Compare the body with the actual repeated pattern: straight corridors usually repeat only forward."],
251
+ fix: ["Adjust the loop body or count, then run and assess the saved route again. Keep the destination unchanged while making the repeated pattern explicit. Avoid adding spare moves after a loop to conceal an incorrect count.", "Expand a two-iteration example on paper. If that behaves incorrectly, increasing the repeat count will repeat the same mistake."],
252
+ explain: ["Choose why wrapping forward and right in one loop produces a different path from looping forward and turning once afterwards. Refer to when each turn executes in the trace.", "The loop body defines both what repeats and its order. Moving an instruction across its boundary changes the algorithm."],
253
+ reward: ["Save the shorter working project as Repeated route. You have used a loop to express a pattern and checked that it preserves the intended route. Next you will stop relying on a fixed clear corridor.", "Keep the earlier expanded route as a comparison. Both saves remain yours even as your course progress moves forward."]
254
+ },
255
+ questions: {
256
+ learn: { question: "Which instructions run again on every repetition?", choices: ["Only the first instruction", "Every instruction in the body", "Only the instructions after the repeat"], correctChoice: 1, feedback: "The whole body runs in order on each repetition. Instructions outside it do not automatically repeat." },
257
+ predict: { question: "Four repeats of forward then right on clear squares produce what path?", choices: ["A four-square straight corridor", "A three-sided path", "A square returning to the start and heading"], correctChoice: 2, feedback: "Each repetition draws one edge and turns once. Four equal edges and four right turns close the square." },
258
+ explain: { question: "Why does moving right outside a forward/right repeat change the path?", choices: ["The turn happens once instead of each repetition", "The repeat stops counting", "Forward becomes a left turn"], correctChoice: 0, feedback: "Instructions outside the repeated body execute once at their position in the surrounding program." }
259
+ }
260
+ },
261
+ {
262
+ title: "Read the walls",
263
+ concepts: ["Conditions", "Sensors", "Fresh state"],
264
+ goals: ["Choose a safe action using a fresh wall reading before each move.", "Navigate both clear and blocked corridors without assuming a sensor result stays true."],
265
+ extension: "Try a corridor with a wall one square further along. Keep the same program and explain which sensor reading changes its behaviour.",
266
+ activities: {
267
+ learn: ["An if-clear block asks whether the square immediately ahead is free now. It runs then for a clear square and else for a wall. A sensor result describes one position and direction; moving or turning can make the next answer different.", "Use forward in the clear branch and a turn in the blocked branch. Read again before the next move."],
268
+ predict: ["A robot sees a clear square, moves into it and now faces a wall. Predict the next reading. The first clear result is not a promise about the rest of the corridor.", "Follow the robot's new position, then inspect exactly one square ahead using its current heading."],
269
+ build: ["Replace blind movement in your repeated route with if-clear. Move forward when clear; turn right when blocked. Repeat the sense-and-act sequence so the robot can follow the mission corridor as the wall position changes.", "Both then and else contain arrays of actions. Avoid placing an unconditional forward after the branches."],
270
+ run: ["Run the same saved program in the clear and blocked training layouts. In the text trace, find the first blocked reading and confirm that the chosen action turns rather than entering the wall.", "If a turn exposes another wall, the next repetition must read the new direction before deciding whether to move."],
271
+ assess: ["Check the program on more than one corridor layout. Arrival and collision checks must pass for both clear and blocked readings. A hard-coded sequence that fits only the visible map does not yet solve this mission.", "The check is about behaviour, not a particular number of copied forwards. Use a current reading to choose each move."],
272
+ inspect: ["Inspect a failing trace and separate the sensor's answer from the branch that ran. Was the reading correct but the branches reversed, or did an unconditional movement ignore the reading entirely?", "Look for a forward immediately after a blocked reading. That pair shows where the safety decision was lost."],
273
+ fix: ["Repair the condition or branch order and rerun both layouts. Then assess the current saved program. Keep the earlier loop so sensing happens repeatedly instead of only at the start.", "A single correct if-clear does not protect later blind moves. Place the whole decision inside the repeat."],
274
+ explain: ["Choose why the robot must read the wall sensor again after moving. Use the two different sensor results from the corridor trace to support your explanation.", "A reading belongs to a particular state. Moving changes that state even when the program text stays the same."],
275
+ reward: ["Save the working project as Sensing route. Your robot now makes decisions from its surroundings instead of following a completely memorised path. Next you will give that repeated decision a useful name.", "Try the previous saved route on the changed layout if you want to see why the sensor made the program more reusable."]
276
+ },
277
+ questions: {
278
+ learn: { question: "Which branch runs when the square ahead contains a wall?", choices: ["The then branch", "Both branches", "The else branch"], correctChoice: 2, feedback: "If-clear is false when the next square is blocked, so only the else branch executes." },
279
+ predict: { question: "After moving into a clear square with a wall immediately beyond, what is the next reading?", choices: ["Blocked", "Always still clear", "The previous reading is reused"], correctChoice: 0, feedback: "The next reading examines the square ahead of the new position. That square now contains the wall." },
280
+ explain: { question: "Why must a moving robot take fresh wall readings?", choices: ["Sensors randomly change every time", "Position or heading changes what is ahead", "A repeat cannot contain a condition"], correctChoice: 1, feedback: "A sensor reads the current world state. A move or turn changes which square the sensor examines." }
281
+ }
282
+ },
283
+ {
284
+ title: "Name a rescue routine",
285
+ concepts: ["Functions", "Decomposition", "Call order"],
286
+ goals: ["Define and call a named routine for the repeated sense-and-move decision.", "Reuse the routine while keeping its movement behaviour visible in the trace."],
287
+ extension: "Create a second routine that turns around using two right turns. Call it from two places and compare the trace with copying both turns each time.",
288
+ activities: {
289
+ learn: ["A routine gives a name to a useful sequence. Defining search in routines does not run it; a call block runs its actions at that point in the program. Names help you explain a plan without hiding what each action does.", "Put the reusable sequence under routines.search and put a call to search inside the main program."],
290
+ predict: ["Define a routine named turn-around with two right turns, but leave program empty. Predict whether the robot turns. Then predict its heading after program calls that routine once.", "A definition describes work that can be done. A call is the instruction that actually requests the work."],
291
+ build: ["Move the sense-and-act block from your previous project into a routine named search. Replace its old location with a call block and keep the outer repeat. Use the same routine from the mission's second route segment.", "Use exactly the same name in the definition and call. The project reports an unknown routine instead of guessing a close match."],
292
+ run: ["Run the project and locate the search calls in the trace. Step into the routine to see its sensor decision, then return to the surrounding repeat. Confirm that naming the sequence did not change the route.", "The robot keeps its position and heading when a routine returns. A routine is not a reset or a teleport."],
293
+ assess: ["Assess the reusable search routine on the mission layouts. The checks require a valid call, a successful route and bounded execution. Accidentally calling search from itself must stop with a useful limit error.", "A function can be useful without calling itself. This route needs a repeated call from program, not recursive calls inside search."],
294
+ inspect: ["Inspect an unknown-name or call-depth failure separately from a route failure. For a route failure, expand the routine mentally at its call site and compare that action order with your earlier working project.", "Check the spelling first, then inspect the routine body. Renaming a routine requires updating all its call sites."],
295
+ fix: ["Repair the call or routine body, run the project and assess the newly saved files. Keep one definition of the shared search behaviour so that a correction improves both uses.", "If two copied sequences need the same fix, replace them with calls to the single corrected routine."],
296
+ explain: ["Choose why a named routine helped when both route segments needed the same sensor correction. Explain the difference between storing the routine's definition and executing a call.", "A shared definition reduces the number of places that must agree. It still needs a call each time you want its work done."],
297
+ reward: ["Save this project as Rescue routines. You have decomposed the route into a named behaviour and verified that its calls preserve the intended movement. Next, your robot will rescue explorers rather than just reach a pad.", "Give routines names that describe their purpose; that makes both the code and future debugging easier to follow."]
298
+ },
299
+ questions: {
300
+ learn: { question: "What makes the actions in a named routine execute?", choices: ["A call using that routine's name", "Saving the file", "Writing the name in a comment"], correctChoice: 0, feedback: "A definition makes the routine available; the call instruction executes its actions at that point in the program." },
301
+ predict: { question: "What happens when turn-around is defined but program is empty?", choices: ["The robot turns twice", "No movement or turn occurs", "The robot calls every routine"], correctChoice: 1, feedback: "Nothing calls the routine, so its two right turns never run. An empty main program leaves the robot at the start." },
302
+ explain: { question: "Why does one shared search routine help when two route segments need a correction?", choices: ["It removes all testing", "It automatically finds every goal", "One corrected definition serves both calls"], correctChoice: 2, feedback: "Both call sites use the same definition. You fix the behaviour once and still test both contexts." }
303
+ }
304
+ },
305
+ {
306
+ title: "Collect and count",
307
+ concepts: ["State", "Unique collection", "Order of operations"],
308
+ goals: ["Collect every explorer on the route before reaching the rescue pad.", "Show that repeated collection on the same square does not increase the count twice."],
309
+ extension: "Move an explorer to a different corridor square and rerun without changing the route. Explain how collecting after each safe move makes the program adapt.",
310
+ activities: {
311
+ learn: ["The world remembers which explorers remain. Collect rescues an explorer on the robot's current square and increases the count once. Collect on an empty or already-cleared square leaves the count unchanged; moving alone does not collect.", "Check the explorer marker and rescued count together. One collected explorer must disappear from the remaining set."],
312
+ predict: ["Predict the count after collecting twice on a square that starts with one explorer. Then predict what changes if the robot moves to a second explorer and collects there.", "Count explorers, not collect instructions. A repeated instruction cannot rescue the same person twice."],
313
+ build: ["Add collect to your search routine after a successful forward move. Also handle an explorer at the starting square. Keep wall sensing, repeats and named routines so the robot can collect along the whole route.", "Order matters: collect before moving checks the old square. Collect after moving checks the new one."],
314
+ run: ["Run the route and watch the rescued count alongside the remaining explorer markers. Pause after the first collection, try collecting again in the trace scenario, and verify that the count stays unchanged.", "The final pad alone is not the whole goal now. Compare rescued and remaining counts before declaring success."],
315
+ assess: ["Check that the saved program reaches the pad after rescuing every explorer, including a possible explorer at the starting square. Duplicate collection must not inflate the total, and the route must remain collision-free.", "Use the remaining-explorers feedback to distinguish a missed square from an incorrect collection order."],
316
+ inspect: ["Inspect the location of any missed explorer. Did the route never visit that square, or did it visit without collecting? For a passing run, identify the evidence that a duplicate collect did not increase the total.", "Match the collection action to the robot position on that trace row. Do not use the next row's position."],
317
+ fix: ["Repair the route or collect placement and reassess the current project. Keep the initial collection outside the movement loop, and collect new squares after safe moves inside the reusable routine.", "Do not repair the visible total with a hard-coded score. The world should count explorers that were actually removed."],
318
+ explain: ["Choose why duplicate collection should leave the count unchanged. Relate your explanation to the remaining-explorer set and explain why the program should count successful rescues rather than attempted actions.", "Once an explorer has been collected, there is no explorer at that square for the next collect to remove."],
319
+ reward: ["Save your project as Counted rescues. You have added changing world state to a reusable, sensor-driven route and checked an important edge case. The final expedition combines all five ideas on an unfamiliar layout.", "Keep the named save as your stable checkpoint before experimenting with the capstone route."]
320
+ },
321
+ questions: {
322
+ learn: { question: "When does collect increase the rescued count?", choices: ["On every call", "When an uncollected explorer occupies the current square", "Whenever the robot turns"], correctChoice: 1, feedback: "The count changes only when a real remaining explorer is removed from the current square." },
323
+ predict: { question: "Starting at zero, what is the count after collecting twice on one explorer's square?", choices: ["Two", "Zero", "One"], correctChoice: 2, feedback: "The first collection removes the explorer. The second finds an empty square and cannot count the explorer again." },
324
+ explain: { question: "Why should the count track removed explorers instead of collect calls?", choices: ["A repeated action must not invent another rescue", "Every empty square is an explorer", "Turning should reset the count"], correctChoice: 0, feedback: "Counting successful state changes keeps the result correct even when a safe operation is repeated." }
325
+ }
326
+ },
327
+ {
328
+ title: "The lost-robot expedition",
329
+ concepts: ["Integration", "Test cases", "Algorithm limits"],
330
+ goals: ["Combine routines, repeats, current wall readings and unique collection into one rescue program.", "Complete varied training corridors and explain where the chosen navigation strategy would need improving."],
331
+ extension: "Design a corridor that defeats the right-turn strategy. Describe the missing capability, such as remembering visited squares, before proposing a future version of your algorithm.",
332
+ activities: {
333
+ learn: ["A complete rescue algorithm must work beyond one memorised route. Combine movement, loops, conditions, routines and collection. Our expedition uses bounded corridor mazes; a right-turn strategy is useful here but is not a promise to solve every possible maze.", "Write the algorithm in words first: collect here, repeatedly inspect ahead, move and collect if clear, otherwise turn."],
334
+ predict: ["Predict which test best challenges a program that worked on one clear corridor: the same layout again, a changed wall and explorer layout, or only changing the robot's colour. Explain what new behaviour the test can reveal.", "Choose a change that affects the decisions or state the algorithm uses, rather than a purely visual change."],
335
+ build: ["Create your expedition version from Counted rescues. Use a named search routine, bounded repetition, a fresh if-clear decision and collection at the start and after moves. Choose a repeat budget that finishes the supported corridors without exceeding the action limit.", "Do not paste a separate hard-coded route for each map. Keep one program and let its sensor decisions adapt."],
336
+ run: ["Play your expedition across the supplied corridor layouts. Use pause, step and the text trace to compare successful routes. Record a layout that required a different decision and explain why the same program handled it.", "Resetting the preview restores the starting world. It does not remove your source or earned course progress."],
337
+ assess: ["Assess the expedition mission against arrival, complete rescue, changed layouts and execution bounds. Once all six missions are complete, run the final whole-course assessment to earn the course badge from the accumulated project.", "A mission pass and a course badge are separate. The final assessment checks the combined project, not whether you opened all the lessons."],
338
+ inspect: ["Inspect any remaining failure using the earliest incorrect decision in its trace. Distinguish a code defect, too-small action budget and a layout outside the supported corridor strategy. Use an actual trace to support that distinction.", "A bigger repeat count cannot repair reversed branches or collecting on the wrong square."],
339
+ fix: ["Repair the expedition, rerun all supplied layouts and check the current saved version. Make sure the fix preserves the earlier movement, loop, sensor, routine and counting behaviours instead of improving only one visible map.", "After a change, rerun a previously passing layout as a regression test. Successful old evidence does not cover the new source."],
340
+ explain: ["Choose an honest explanation of what your expedition proves. It solves the tested corridor family within a bounded action budget; it does not prove that every maze is solvable by always turning right.", "Good programmers describe both what their tests cover and the limits that remain. That is part of making a useful finished project."],
341
+ reward: ["Save your finished expedition with a name of your own, complete the final assessment, and replay any of the six missions. Your badge remains earned while you experiment, and your nine named saves let you compare different rescue strategies.", "Try the extension only after keeping a stable saved version. An unsuccessful experiment is useful evidence and does not erase earned completion."]
342
+ },
343
+ questions: {
344
+ learn: { question: "What should the final rescue project combine?", choices: ["Only a long copied list of moves", "Only a high displayed score", "Safe movement, decisions, reuse and real collection"], correctChoice: 2, feedback: "The capstone integrates the course concepts into behaviour that responds to the world and counts actual rescues." },
345
+ predict: { question: "Which test best challenges a program that passed a single clear corridor?", choices: ["Change the walls and explorer positions", "Run only the identical map again", "Change only the robot's colour"], correctChoice: 0, feedback: "Changed walls and explorer positions test the condition and collection behaviour that a fixed route may not handle." },
346
+ explain: { question: "What does passing the expedition's corridor tests establish?", choices: ["The algorithm solves every possible maze", "It handles the tested corridor family within its limits", "Future changes need no tests"], correctChoice: 1, feedback: "Passing tests provides evidence for their scenarios and stated limits. Other maze structures can require a different navigation strategy." }
347
+ }
348
+ }
349
+ ]);
350
+ // Annotate the CommonJS export names for ESM import in node:
351
+ 0 && (module.exports = {
352
+ course,
353
+ practice
354
+ });
355
+ //# sourceMappingURL=robot-maze-dash.cjs.map