@plasius/learning 0.5.0 → 0.6.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.
- package/README.md +12 -0
- package/dist/courses/beacon-bot.cjs +362 -0
- package/dist/courses/beacon-bot.cjs.map +1 -0
- package/dist/courses/beacon-bot.d.cts +6 -0
- package/dist/courses/beacon-bot.d.ts +6 -0
- package/dist/courses/beacon-bot.js +168 -0
- package/dist/courses/beacon-bot.js.map +1 -0
- package/dist/courses/dance-rover.cjs +374 -0
- package/dist/courses/dance-rover.cjs.map +1 -0
- package/dist/courses/dance-rover.d.cts +6 -0
- package/dist/courses/dance-rover.d.ts +6 -0
- package/dist/courses/dance-rover.js +180 -0
- package/dist/courses/dance-rover.js.map +1 -0
- package/dist/courses/obstacle-explorer.cjs +368 -0
- package/dist/courses/obstacle-explorer.cjs.map +1 -0
- package/dist/courses/obstacle-explorer.d.cts +6 -0
- package/dist/courses/obstacle-explorer.d.ts +6 -0
- package/dist/courses/obstacle-explorer.js +174 -0
- package/dist/courses/obstacle-explorer.js.map +1 -0
- package/dist/courses/rainbow-rescue-rover.cjs +377 -0
- package/dist/courses/rainbow-rescue-rover.cjs.map +1 -0
- package/dist/courses/rainbow-rescue-rover.d.cts +6 -0
- package/dist/courses/rainbow-rescue-rover.d.ts +6 -0
- package/dist/courses/rainbow-rescue-rover.js +183 -0
- package/dist/courses/rainbow-rescue-rover.js.map +1 -0
- package/dist/courses/servo-creature.cjs +369 -0
- package/dist/courses/servo-creature.cjs.map +1 -0
- package/dist/courses/servo-creature.d.cts +6 -0
- package/dist/courses/servo-creature.d.ts +6 -0
- package/dist/courses/servo-creature.js +175 -0
- package/dist/courses/servo-creature.js.map +1 -0
- package/package.json +26 -1
|
@@ -0,0 +1,368 @@
|
|
|
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/obstacle-explorer.ts
|
|
21
|
+
var obstacle_explorer_exports = {};
|
|
22
|
+
__export(obstacle_explorer_exports, {
|
|
23
|
+
course: () => course,
|
|
24
|
+
practice: () => practice
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(obstacle_explorer_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/obstacle-explorer.ts
|
|
200
|
+
var { course, practice } = authorCourse({
|
|
201
|
+
slug: "obstacle-explorer",
|
|
202
|
+
title: "Obstacle Explorer",
|
|
203
|
+
category: "robot",
|
|
204
|
+
summary: "Programme a simulated rover to explore a course using distance observations. Build a controller that remembers its decisions, turns for a bounded time and recognises stale data. Finish with deliberate recovery, a clear arrival state and evidence from both successful and interrupted journeys. No physical equipment is required.",
|
|
205
|
+
projectFiles: [{ path: "robot.cpp", language: "cpp", maximumCharacters: 24e3 }],
|
|
206
|
+
starterProject: { files: [{ path: "robot.cpp", source: `bool armed = false;
|
|
207
|
+
bool wasEnabled = false;
|
|
208
|
+
int mode = 0;
|
|
209
|
+
int attempts = 0;
|
|
210
|
+
int clearCount = 0;
|
|
211
|
+
int lastSampleId = -1;
|
|
212
|
+
int turnStart = 0;
|
|
213
|
+
|
|
214
|
+
void setup() { stopMotors(); }
|
|
215
|
+
|
|
216
|
+
void loop() {
|
|
217
|
+
stopMotors();
|
|
218
|
+
}
|
|
219
|
+
` }] },
|
|
220
|
+
reference: [
|
|
221
|
+
{ name: "distance observation", signature: 'numberSensor("distanceCm"); numberSensor("sampleAgeMs"); numberSensor("sampleId");', description: "Distance is valid from 0 through 400 cm, age from 0 through 250 ms and sampleId is a nonnegative integer. A repeated ID is the same observation; a decreasing ID is invalid. Read supplied values on every loop. Missing or malformed input stops the host simulation.", example: 'double distance = numberSensor("distanceCm");' },
|
|
222
|
+
{ name: "stopMotors", signature: "stopMotors()", description: "Immediately set both simulated wheel powers to zero. Use at startup, explicit stop, disable, bumper, invalid or stale observation, turn deadline and arrival. A stop branch must return before another movement command.", example: 'if (boolSensor("stop")) { armed = false; stopMotors(); return; }' },
|
|
223
|
+
{ name: "setMotorPower", signature: "setMotorPower(double left, double right)", description: "Each simulated output must stay from -25 through 25. Use (20,20) for driving and (-15,15) for a left turn on the spot. The simplified differential model has 120 mm wheel spacing and 5 mm/s per power unit. These outputs have no physical device connection.", example: "setMotorPower(-15, 15);" },
|
|
224
|
+
{ name: "clearance state", signature: "mode: 0 driving, 1 halted, 2 turning", description: "At distance at or below 20 cm, stop driving and enter halted. Resuming drive requires two distinct increasing sample IDs at or above 30 cm; another distinct sample below 30 resets clearCount. The 20\u201330 cm band preserves driving but cannot establish clearance while halted or turning.", example: "if (distance >= 30) { clearCount += 1; } else { clearCount = 0; }" },
|
|
225
|
+
{ name: "turn deadline", signature: "millis() - turnStart >= 600", description: "After entering halted, emit a stopped observation before considering a turn on the next loop. With no confirmed clearance, begin a left turn and increment attempts. Stop at 600 ms; the deadline wins over clearance on that observation. After the first two expired turns, a later halted observation may drive if clear or try another turn. The third expired turn disarms even if clearance arrives at its deadline.", example: "if (millis() - turnStart >= 600) { mode = 1; stopMotors(); return; }" },
|
|
226
|
+
{ name: "permission and recovery", signature: 'boolSensor("enabled"); boolSensor("stop"); boolSensor("bumper");', description: "A fresh safe enable edge arms in halted mode, resets attempts and clearCount, and stops for that observation. Stop, disable, bumper, invalid or stale data disarm immediately. Track enable history even while blocked. After interruption or three failed turns, release and enable again; fault clearance alone never starts movement.", example: 'bool enabled = boolSensor("enabled"); bool rising = enabled && !wasEnabled; wasEnabled = enabled;' },
|
|
227
|
+
{ name: "arrival", signature: 'boolSensor("goal")', description: "The simulator supplies goal when the rover reaches the course destination. Goal disarms and stops before movement. It does not award course completion, and a still-true goal prevents rearming. Restarting the simulator creates fresh programme state and a fresh course position.", example: 'if (boolSensor("goal")) { armed = false; stopMotors(); return; }' }
|
|
228
|
+
]
|
|
229
|
+
}, [
|
|
230
|
+
{
|
|
231
|
+
title: "A distance you can trust",
|
|
232
|
+
concepts: ["Sensor observations", "Ranges", "Safe default"],
|
|
233
|
+
goals: ["Read the current distance and reject values outside its stated range.", "Command bounded forward motion only with permission and adequate clearance."],
|
|
234
|
+
extension: "Draw a distance number line including -1, 0, 20, 21, 400 and 401. Annotate invalid, too near and usable readings before testing them.",
|
|
235
|
+
activities: {
|
|
236
|
+
learn: ["A distance is an observation, not a promise of safety. In this simulator valid readings span 0\u2013400 cm. Begin stopped; while enabled and not stopped or touching a bumper, drive at (20,20) only above 20 cm. Otherwise stop immediately.", "A negative reading or a value beyond the stated maximum is invalid, not a conveniently large empty space."],
|
|
237
|
+
predict: ["Predict the output for distances 19, 20, 21 and 401 with enable on. Then keep a clear reading but press stop. Identify the condition that must win before a motor command is emitted.", "At the 20 cm boundary the rover stops. The stop input has priority even when distance appears clear."],
|
|
238
|
+
build: ["Read distanceCm in loop and validate its range before using it. Add guards for disabled input, stop and bumper. Return after stopMotors on the stopped path; use setMotorPower(20,20) only on the valid clear path.", "Keep one movement command at the end so a later statement cannot accidentally undo the stop."],
|
|
239
|
+
run: ["Use the simulator's distance controls to cross the 20 cm threshold. Try an invalid reading and a bumper event while moving. Inspect the numeric wheel outputs and stopped reason alongside the rover picture.", "Pause and step through observations when continuous motion makes the first incorrect decision hard to find."],
|
|
240
|
+
assess: ["Check clear travel, the exact near boundary, both invalid range ends, disable, stop and bumper. All wheel outputs must remain within -25 through 25, including during awkward input combinations.", "The host checks emitted commands, so moving slowly on screen cannot conceal an out-of-range output."],
|
|
241
|
+
inspect: ["If 401 permits motion, inspect range validation before clearance. If stop appears briefly then motion returns on the same observation, inspect code after stopMotors. If 20 moves, inspect the comparison boundary.", "Find the earliest wrong command rather than relying on the final displayed position."],
|
|
242
|
+
fix: ["Repair the responsible comparison or control branch. Replay the failing boundary and a valid forward journey, then stop during movement to ensure the correction preserves ordinary control.", "Replacing every output with stop passes no genuine travel check; the project needs useful motion as well as safe refusal."],
|
|
243
|
+
explain: ["Choose why an invalid large distance must stop the controller. Explain the difference between an observation being inside its valid range and that observation providing enough clearance to drive.", "Validity and clearance answer different questions and deserve separate conditions."],
|
|
244
|
+
reward: ["Save Trusted distance. Your rover can make a simple decision from a valid current observation. Next you will add memory so small changes near the boundary do not make it repeatedly start and stop.", "Keep the exact 20 cm case as a regression example for later state changes."]
|
|
245
|
+
},
|
|
246
|
+
questions: {
|
|
247
|
+
learn: { question: "How should this controller treat a distance of 401 cm?", choices: ["As invalid and stop", "As guaranteed open space", "As a command to turn faster"], correctChoice: 0, feedback: "401 exceeds the documented sensor range, so it cannot be used as trustworthy evidence of clearance." },
|
|
248
|
+
predict: { question: "With permission active and distance exactly 20 cm, which output is required?", choices: ["Forward at full power", "Both wheels stopped", "Reverse until the reading changes"], correctChoice: 1, feedback: "The near boundary includes 20 cm, so the forward path must stop at that exact value." },
|
|
249
|
+
explain: { question: "Why validate range separately from available clearance?", choices: ["To avoid reading the sensor", "To make every valid reading move", "Because a valid reading can still describe an obstacle"], correctChoice: 2, feedback: "A reading such as 10 cm is valid sensor data but provides too little clearance for forward movement." }
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
title: "Remember the last decision",
|
|
254
|
+
concepts: ["State", "Hysteresis", "Distinct observations"],
|
|
255
|
+
goals: ["Use separate stop and resume thresholds to avoid repeated switching.", "Confirm clearance from two distinct samples instead of counting duplicate observations."],
|
|
256
|
+
extension: "Write two traces containing the same distances but different sample IDs. Explain why only the trace with new observations can confirm clearance.",
|
|
257
|
+
activities: {
|
|
258
|
+
learn: ["Use mode=0 for driving and mode=1 for halted. Driving stops at distance\u226420. A halted rover resumes only after two distinct increasing sample IDs with distance\u226530. A new sample below 30 resets clearCount. Repeating one ID must not increase it.", "The gap between stop and resume thresholds is hysteresis: the decision depends on both distance and previous state."],
|
|
259
|
+
predict: ["While halted, observe (ID 7,35 cm), repeat ID 7, then observe (ID 8,32 cm). Predict clearCount after each. Compare a driving rover and a halted rover seeing 25 cm.", "A duplicate supplies no new evidence. In the 20\u201330 band, driving continues but a halted rover has not established clearance."],
|
|
260
|
+
build: ["Store mode, clearCount and lastSampleId. Update clearance only when sampleId increases; copy that ID after processing. Stop and reset clearance on the transition from driving to halted. Return to driving once two new clear samples confirm the path.", "Reset the counter for a new below-30 sample even if it is not close enough to trigger the driving stop threshold."],
|
|
261
|
+
run: ["Play a noisy sequence around 20 cm, then a clear sequence above 30 cm. Pause on a duplicated ID and compare the counter before and after. Inspect state text and the distance chart together.", "Use sample identity and value as two separate columns in your trace."],
|
|
262
|
+
assess: ["Check both threshold boundaries, the middle band, interrupted clearance and repeated IDs. The controller must neither chatter on every distance change nor restart from a single duplicated sample.", "An increasing display time does not automatically make an unchanged sensor ID a fresh sample."],
|
|
263
|
+
inspect: ["If a repeated reading resumes travel, inspect the ID comparison. If 25 cm restarts a halted rover, inspect the resume threshold. If it stops a driving rover, inspect which threshold applies to that state.", "Trace mode before the decision; the same distance can correctly produce different outcomes in different states."],
|
|
264
|
+
fix: ["Repair state or counter handling and replay duplicate-clear-clear and clear-near-clear sequences. Confirm the rover can still resume after two genuinely new clear samples.", "Do not count the near sample that caused the stop as evidence that the path is now clear."],
|
|
265
|
+
explain: ["Choose why two observations with one repeated ID count as one sample. Explain how hysteresis gives the controller a stable decision around a noisy boundary.", "Memory is useful when a decision needs evidence across time rather than one isolated reading."],
|
|
266
|
+
reward: ["Save Stable clearance. Your rover now distinguishes new evidence from repeated data. Next it will try a short turn when a halted route does not become clear.", "Keep a trace through the middle band to verify that the turn state uses the same clearance rule."]
|
|
267
|
+
},
|
|
268
|
+
questions: {
|
|
269
|
+
learn: { question: "What allows a halted rover to confirm clearance?", choices: ["One sample copied repeatedly", "Two distinct increasing IDs with distance at least 30 cm", "Any reading above zero"], correctChoice: 1, feedback: "Two genuinely new clear observations establish clearance; copies of one observation do not add evidence." },
|
|
270
|
+
predict: { question: "A halted rover sees ID 7 twice at 35 cm; how many clear samples has it counted?", choices: ["Zero", "Two", "One"], correctChoice: 2, feedback: "The repeated ID describes the same observation, so the clearance counter remains one." },
|
|
271
|
+
explain: { question: "Why use different stop and resume thresholds?", choices: ["To avoid repeated switching near one noisy boundary", "To ignore state entirely", "To allow invalid distances"], correctChoice: 0, feedback: "The gap between thresholds prevents a small fluctuation from repeatedly reversing the previous decision." }
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
title: "Turn with a deadline",
|
|
276
|
+
concepts: ["Finite state machine", "Non-blocking time", "Bounded attempts"],
|
|
277
|
+
goals: ["Try a short left turn without blocking fresh observations.", "Stop each turn at its deadline and bound the number of attempts."],
|
|
278
|
+
extension: "Compare a 300 ms and a 600 ms turn in a separate saved experiment. Keep the same power and explain which property changes and which limits must remain.",
|
|
279
|
+
activities: {
|
|
280
|
+
learn: ["Add mode=2 for turning. After a stopped halted observation, an uncleared route may begin a left turn at (-15,15). Record turnStart and increment attempts once. At 600 ms stop and return to halted. Try at most three turns before waiting for deliberate recovery.", "Do not wait inside a loop for time to pass. Each observation must return so stop and sensor updates remain responsive."],
|
|
281
|
+
predict: ["A turn starts at 1000 ms. Predict outputs at 1599 and 1600 ms, then consider confirmed clearance arriving exactly at 1600. Decide which rule wins on the deadline observation.", "The turn deadline requires a stopped observation even when another decision could otherwise permit movement."],
|
|
282
|
+
build: ["Add a timed turning branch using millis. Evaluate the deadline before clearance in that branch. A turn may end earlier by returning to drive after confirmed clearance; reset attempts on that successful drive. Otherwise stop at the deadline and try again from halted on a later observation.", "Increment attempts on entry to turning, not on every loop that remains in the turning state."],
|
|
283
|
+
run: ["Step through stopped, turning and deadline observations. Present fresh clear samples during one turn and a permanently blocked distance during another. Check the turn counter and complete command trace.", "The controller makes bounded attempts; it is not a general solver for every possible maze."],
|
|
284
|
+
assess: ["Check motor direction, exact deadline, early confirmed clearance, one increment per turn, three-attempt bound and stop priority during turning. A fourth automatic turn must not start.", "A successful ordinary turn does not replace checking the persistent-obstacle case."],
|
|
285
|
+
inspect: ["If a turn never ends, inspect elapsed simulated time. If attempts rise too quickly, inspect the entry transition. If a deadline emits forward movement, inspect the order of the time and clearance branches.", "Inspect every command in the observation, including a movement command accidentally emitted after a stop."],
|
|
286
|
+
fix: ["Repair the transition and replay the deadline with simultaneous clearance, followed by a persistent obstacle. Keep a normal early-clearance turn as a regression case.", "Do not extend the turn indefinitely to force a favourable scene outcome; the documented deadline is part of the controller."],
|
|
287
|
+
explain: ["Choose why timed states are more responsive than a busy wait. Explain how a bounded attempt gives the rover an understandable failure state when its simple strategy cannot find a route.", "An honest stopped outcome is better evidence than silently repeating an action without a limit."],
|
|
288
|
+
reward: ["Save Bounded turn. Your rover can try a different heading while still accepting fresh input. Next you will handle the case where the distance stream itself stops providing usable observations.", "Preserve the third-turn deadline trace for later recovery checks."]
|
|
289
|
+
},
|
|
290
|
+
questions: {
|
|
291
|
+
learn: { question: "When should the turn attempt counter increase?", choices: ["On every displayed frame", "Whenever the distance is repeated", "Once on entry to the turning state"], correctChoice: 2, feedback: "The counter measures attempted turns, so each state entry adds one regardless of how many observations the turn spans." },
|
|
292
|
+
predict: { question: "A turn began at 1000 ms; what must happen at 1600 ms?", choices: ["Emit a stop before any later movement decision", "Always begin another turn immediately", "Ignore new stop input"], correctChoice: 0, feedback: "The 600 ms deadline is inclusive and takes priority over clearance on that observation." },
|
|
293
|
+
explain: { question: "Why return from loop while waiting for a turn deadline?", choices: ["To remove timing entirely", "So new stop and sensor observations can be processed", "To make the turn last forever"], correctChoice: 1, feedback: "A non-blocking timed state allows each new observation to apply safety and control decisions promptly." }
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
title: "Notice a missing observation",
|
|
298
|
+
concepts: ["Freshness", "Watchdog", "Fail-safe state"],
|
|
299
|
+
goals: ["Stop on stale, malformed or out-of-order sensor observations.", "Prevent old clear data from continuing a moving command."],
|
|
300
|
+
extension: "Build a trace where distance remains 100 cm but sample age increases past 250 ms. Explain why unchanged distance is not evidence that the route stayed clear.",
|
|
301
|
+
activities: {
|
|
302
|
+
learn: ["A sensor watchdog checks freshness before movement. Valid sampleAgeMs is 0\u2013250 inclusive. sampleId must be a nonnegative integer and may repeat but must not decrease. Invalid range, age or identity stops and disarms, even if the last distance looked clear.", "A duplicate within its age limit may preserve state, but it cannot add a new clearance sample."],
|
|
303
|
+
predict: ["Keep distance at 100 cm while age changes from 250 to 251 ms. Then supply a lower sample ID with age zero. Predict armed state and outputs for both failures.", "A recent timestamp cannot make an out-of-order identity valid, and a large distance cannot make an old sample fresh."],
|
|
304
|
+
build: ["Validate distance, age and identity before updating counters or emitting movement. On failure stop, disarm and reset clearance. Keep current enable history updated so a fault clearing under a held enable cannot create a false new start.", "Store the last accepted sample ID; do not replace it with a malformed or backwards observation."],
|
|
305
|
+
run: ["Freeze the distance stream during forward travel and during a turn. Watch age cross the limit. Try a backwards ID, a fractional ID and an invalid distance. Inspect the first stopped observation and the reason shown.", "The simulator supplies these fault cases; no physical sensor connection or permission is needed."],
|
|
306
|
+
assess: ["Check exact age boundaries, negative age, malformed identity, backwards identity, frozen samples and errors during turning. Programme or input failure must leave the host's virtual motors stopped.", "Passing the ordinary movement path does not establish a working watchdog; the missing-data paths are checked separately."],
|
|
307
|
+
inspect: ["If motion persists after expiry, inspect whether validation happens only at startup. If clearing a fault resumes movement, inspect armed state and enable history. If duplicate IDs increase clearCount, inspect the freshness and novelty checks separately.", "Fresh enough and genuinely new are different properties of an observation."],
|
|
308
|
+
fix: ["Repair validation order and replay clear travel, frozen input, expiry, fresh input and a still-held enable. The new valid sample removes the fault but must not itself restart the disarmed rover.", "Never replace missing data with a favourable distance just to keep the animation moving."],
|
|
309
|
+
explain: ["Choose why a watchdog uses the age of the observation rather than the age of the page. Explain why input freshness must be checked during every moving state.", "The browser can continue rendering normally while its simulated sensor stream has stopped."],
|
|
310
|
+
reward: ["Save Fresh observations. Your controller now recognises when its evidence is no longer usable. Next you will make starting again a clear, deliberate and testable action.", "Keep the 250/251 ms boundary to check later refactors."]
|
|
311
|
+
},
|
|
312
|
+
questions: {
|
|
313
|
+
learn: { question: "Which sample age is the first whole millisecond outside the allowed window?", choices: ["250 ms", "251 ms", "0 ms"], correctChoice: 1, feedback: "The valid range includes 250 ms; 251 ms exceeds it and must stop and disarm the controller." },
|
|
314
|
+
predict: { question: "A lower sample ID arrives with age zero; how should the controller respond?", choices: ["Treat it as two clear samples", "Ignore its identity and move", "Stop because the identity moved backwards"], correctChoice: 2, feedback: "Freshness does not repair an out-of-order sample identity; both validity requirements must hold." },
|
|
315
|
+
explain: { question: "Why check observation age while the preview still animates?", choices: ["Rendering can continue while sensor evidence becomes stale", "Animation guarantees fresh observations", "Only stopped robots need sensors"], correctChoice: 0, feedback: "The display and the observation stream are separate, so visible animation cannot prove that control data is current." }
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
title: "Try again deliberately",
|
|
320
|
+
concepts: ["Enable edges", "Recovery", "State reset"],
|
|
321
|
+
goals: ["Require a fresh safe enable after interruption or exhausted turns.", "Reset one attempt coherently without erasing the learner's saved work."],
|
|
322
|
+
extension: "Draw the difference between clearing a bumper and asking the rover to explore again. Include a held enable and the later release/new-enable action.",
|
|
323
|
+
activities: {
|
|
324
|
+
learn: ["Detect rising=enabled&&!wasEnabled and update wasEnabled on every observation. A safe rising edge arms in halted mode, clears attempts and clearance, and emits a stop for that observation. Fault clearance, a held enable and completed turns do not create new permission.", "Disabling, stop, bumper and invalid observations clear armed immediately. Three unsuccessful turns also leave it disarmed."],
|
|
325
|
+
predict: ["After a bumper event, clear the bumper while enable remains true. Predict the result, then release and enable with valid fresh distance. Identify which observation resets attempts and which later observations can confirm clearance.", "The new enable observation begins from halted and stopped; it does not jump directly into movement."],
|
|
326
|
+
build: ["Add the explicit arming path before the state machine. On a safe edge initialise halted state and counters, stop and return. After the third unsuccessful turn, remain stopped; permit no new automatic turn until a fresh safe edge begins another attempt.", "Keep accepted sample history coherent across attempts so an old ID cannot masquerade as a new observation."],
|
|
327
|
+
run: ["Use keyboard and onscreen controls through start, obstacle, bumper, clearance, release and new enable. Repeat after exhausted turns and after watchdog expiry. Read the waiting reason at each stopped stage.", "The next required action should be stated in text instead of leaving apparently unresponsive controls unexplained."],
|
|
328
|
+
assess: ["Check held-enable interruption, deliberate recovery, stale input on a new edge, counter reset and the initially stopped arming observation. Repeated identical enable values must never create extra attempts.", "A request to begin is necessary but does not override unsafe current observations."],
|
|
329
|
+
inspect: ["If the rover resumes immediately on clearance, inspect rising versus enabled. If it cannot start after a release, inspect wasEnabled updates in stopped branches. If attempts remain exhausted, inspect the arming reset as one coherent transition.", "Trace permission separately from mode; a halted but armed rover and a disarmed rover have different next actions."],
|
|
330
|
+
fix: ["Repair recovery and run the full interrupted timeline. Confirm a fresh safe enable works, while a fresh enable during an active fault remains stopped and needs another deliberate edge after the fault clears.", "Update input history even when permission is refused, preventing a held unsafe request from later becoming an accidental start."],
|
|
331
|
+
explain: ["Choose why clearing a fault and starting another attempt are separate actions. Explain why resetting simulator state does not mean deleting course progress or named saves.", "Control state belongs to the current attempt; saved source and earned learning progress have a different lifetime."],
|
|
332
|
+
reward: ["Save Deliberate recovery. Your rover now communicates why it is waiting and how to begin again. The final mission combines distance, state, bounded turns and recovery on a complete exploration course.", "Keep a stopped recovery trace beside a successful journey for the final assessment."]
|
|
333
|
+
},
|
|
334
|
+
questions: {
|
|
335
|
+
learn: { question: "What output belongs to a successful fresh arming observation?", choices: ["Maximum forward motion", "An immediate repeated turn", "A stop with reset halted state"], correctChoice: 2, feedback: "Arming establishes a fresh attempt but emits a stopped observation before subsequent state-machine movement." },
|
|
336
|
+
predict: { question: "A bumper clears while enable remains held; what happens next?", choices: ["The rover waits for release and a new safe enable", "The old movement resumes", "The turn counter increases forever"], correctChoice: 0, feedback: "Fault clearance removes one barrier but supplies no fresh enable edge, so the interrupted rover stays disarmed." },
|
|
337
|
+
explain: { question: "Why update enable history while a fault blocks starting?", choices: ["To erase sensor history", "To avoid turning a held unsafe request into a later automatic start", "To ignore future enable changes"], correctChoice: 1, feedback: "Recording the held value prevents fault clearance from being misread as a new user invitation." }
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
title: "The exploration course",
|
|
342
|
+
concepts: ["Integration", "Arrival", "Evidence and limitations"],
|
|
343
|
+
goals: ["Complete a simulated route with a clear stopped arrival state.", "Explain successful, blocked and interrupted journeys using the actual control trace."],
|
|
344
|
+
extension: "Design a route where a left-turn strategy gets stuck and keep the stopped result. Explain what extra sensing or planning another design would need rather than weakening the attempt limit.",
|
|
345
|
+
activities: {
|
|
346
|
+
learn: ["The finished controller reads inputs, updates enable history, validates observations, applies stop and goal, recognises safe arming, then runs one bounded state transition. goal is supplied on arrival and stops and disarms before any movement; it cannot directly award learning completion.", "This simple left-turn strategy succeeds on the supplied practice routes, not on every maze or real-world obstacle."],
|
|
347
|
+
predict: ["Predict a goal observation arriving at the same time as a new enable and confirmed clearance. Compare a persistent blocked route with a reachable one. Identify the legitimate terminal behaviour in each case.", "Goal and safety guards precede arming and movement; a route need not succeed to produce a correct safe outcome."],
|
|
348
|
+
build: ["Combine the verified distance, clearance, timed-turn, watchdog and recovery helpers. Add goal priority and preserve the motor bounds. Keep one readable decision order and ensure every stopped branch returns before another command.", "Avoid separate copies of safety checks in each movement mode; one shared guard prevents them drifting."],
|
|
349
|
+
run: ["Explore the complete course with keyboard controls and repeat using onscreen controls. Reach the goal, interrupt a turn and try a blocked route. Compare state, distance, sample age, attempts and path text with reduced motion enabled.", "The trace and status text must explain the journey without requiring continuous animation or colour recognition."],
|
|
350
|
+
assess: ["Run the final saved-source checks across reachable routes, persistent obstacles, noisy boundaries, duplicate data, stale sensors, simultaneous goal and stop, and fresh restart. Completion requires the integrated controller, not just visiting this lesson.", "The assessment executes the saved current source; save changes before expecting them to alter the result."],
|
|
351
|
+
inspect: ["Locate the earliest wrong transition and classify it as permission, observation, state, timing or output. Compare a failing route with a passing trace and select a focused change that explains both.", "A correct final stopped picture can hide an earlier incorrect command; inspect the full sequence."],
|
|
352
|
+
fix: ["Repair that decision and replay its scenario plus a complete normal route. Save and assess the corrected source. Keep a named final version before extending the strategy or changing the course layout.", "A later experiment may fail without removing already earned completion, but its new source still needs its own evidence."],
|
|
353
|
+
explain: ["Choose what the final assessment establishes and describe a limitation of the supplied observation model. Explain why the free account-bound simulator does not grant access to physical motors or replace separate guardian protections.", "Distinguish demonstrated programme behaviour from claims about equipment or environments you have not tested."],
|
|
354
|
+
reward: ["Save Exploration controller and finish the final assessment. You have built an observable rover with bounded decisions and deliberate recovery. Replay a mission or compare another route while keeping your earned progress and tested project.", "Use the final trace as the starting point for future design changes, including cases where stopping is the correct result."]
|
|
355
|
+
},
|
|
356
|
+
questions: {
|
|
357
|
+
learn: { question: "Where should the goal stop be checked in the controller?", choices: ["After every movement command", "Before arming and movement decisions", "Only after a long turn finishes"], correctChoice: 1, feedback: "Arrival must stop and disarm before another action can command movement or grant fresh permission." },
|
|
358
|
+
predict: { question: "What is correct after three unsuccessful turns on a blocked route?", choices: ["A fourth automatic attempt", "A claim that every maze is solved", "A stopped state awaiting deliberate recovery"], correctChoice: 2, feedback: "The strategy has a bounded failure outcome, so persistent obstruction leads to a clear stopped state instead of endless attempts." },
|
|
359
|
+
explain: { question: "What does the final assessment provide evidence for?", choices: ["The saved controller's documented simulated behaviour", "Guaranteed performance with any real sensor", "Automatic permission to drive physical hardware"], correctChoice: 0, feedback: "Its evidence applies to the saved source and documented simulated scenarios, with physical operation requiring separate protections and verification." }
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
]);
|
|
363
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
364
|
+
0 && (module.exports = {
|
|
365
|
+
course,
|
|
366
|
+
practice
|
|
367
|
+
});
|
|
368
|
+
//# sourceMappingURL=obstacle-explorer.cjs.map
|