@plasius/learning 0.4.0 → 0.5.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 +16 -0
- package/dist/chunk-2UIOBMJZ.js +44 -0
- package/dist/chunk-2UIOBMJZ.js.map +1 -0
- package/dist/courses/meteor-shield.cjs +369 -0
- package/dist/courses/meteor-shield.cjs.map +1 -0
- package/dist/courses/meteor-shield.d.cts +6 -0
- package/dist/courses/meteor-shield.d.ts +6 -0
- package/dist/courses/meteor-shield.js +175 -0
- package/dist/courses/meteor-shield.js.map +1 -0
- package/dist/courses/pixel-trail-challenge.cjs +366 -0
- package/dist/courses/pixel-trail-challenge.cjs.map +1 -0
- package/dist/courses/pixel-trail-challenge.d.cts +6 -0
- package/dist/courses/pixel-trail-challenge.d.ts +6 -0
- package/dist/courses/pixel-trail-challenge.js +172 -0
- package/dist/courses/pixel-trail-challenge.js.map +1 -0
- package/dist/courses/rescue-crew-commander.cjs +369 -0
- package/dist/courses/rescue-crew-commander.cjs.map +1 -0
- package/dist/courses/rescue-crew-commander.d.cts +6 -0
- package/dist/courses/rescue-crew-commander.d.ts +6 -0
- package/dist/courses/rescue-crew-commander.js +175 -0
- package/dist/courses/rescue-crew-commander.js.map +1 -0
- package/dist/courses/robot-maze-dash.js +3 -38
- package/dist/courses/robot-maze-dash.js.map +1 -1
- package/dist/courses/skywing-sprint.cjs +367 -0
- package/dist/courses/skywing-sprint.cjs.map +1 -0
- package/dist/courses/skywing-sprint.d.cts +6 -0
- package/dist/courses/skywing-sprint.d.ts +6 -0
- package/dist/courses/skywing-sprint.js +173 -0
- package/dist/courses/skywing-sprint.js.map +1 -0
- package/dist/courses/star-defender-squadron.cjs +369 -0
- package/dist/courses/star-defender-squadron.cjs.map +1 -0
- package/dist/courses/star-defender-squadron.d.cts +6 -0
- package/dist/courses/star-defender-squadron.d.ts +6 -0
- package/dist/courses/star-defender-squadron.js +175 -0
- package/dist/courses/star-defender-squadron.js.map +1 -0
- package/package.json +26 -1
|
@@ -0,0 +1,367 @@
|
|
|
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/skywing-sprint.ts
|
|
21
|
+
var skywing_sprint_exports = {};
|
|
22
|
+
__export(skywing_sprint_exports, {
|
|
23
|
+
course: () => course,
|
|
24
|
+
practice: () => practice
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(skywing_sprint_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/skywing-sprint.ts
|
|
200
|
+
var { course, practice } = authorCourse({
|
|
201
|
+
slug: "skywing-sprint",
|
|
202
|
+
title: "Skywing Sprint",
|
|
203
|
+
category: "game",
|
|
204
|
+
summary: "Create a side-scrolling flight game from a stationary bird. Give it weight and a flap, move gates across the sky, make collisions fair, and award each gate once. Finish with a repeatable twilight race that can be paused, restarted and played with a keyboard or touch controls.",
|
|
205
|
+
projectFiles: [{ path: "game.js", language: "javascript", maximumCharacters: 32e3 }],
|
|
206
|
+
starterProject: { files: [{ path: "game.js", source: `function initialState() {
|
|
207
|
+
return {
|
|
208
|
+
bird: { x: 80, y: 180, vy: 0, radius: 10 },
|
|
209
|
+
gates: [], score: 0, elapsed: 0, status: "ready"
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function update(state, input) {
|
|
214
|
+
// Return the next state after this action. The host draws that state.
|
|
215
|
+
return state;
|
|
216
|
+
}
|
|
217
|
+
` }] },
|
|
218
|
+
reference: [
|
|
219
|
+
{ name: "initialState", signature: "initialState() -> state", description: "Return a fresh race state. The world is 480 by 360 logical pixels; y increases downwards. Restart must create fresh arrays as well as resetting the bird.", example: 'return { bird: { x: 80, y: 180, vy: 0, radius: 10 }, gates: [], score: 0, elapsed: 0, status: "ready" };' },
|
|
220
|
+
{ name: "update", signature: "update(state, input) -> state", description: "Handle one start, flap, tick or restart action and return the complete next state. Only a playing race advances on tick. Rendering and button events are supplied by the host.", example: 'if (input.action === "restart") return initialState();' },
|
|
221
|
+
{ name: "input.dt", signature: "input.dt: seconds", description: "A tick supplies 0.02 simulated seconds. Express velocity in pixels per second and multiply by dt when changing position. No wall clock or display frame rate is needed.", example: "const nextY = state.bird.y + state.bird.vy * input.dt;" },
|
|
222
|
+
{ name: "flap", signature: 'input.action === "flap"', description: "A press creates one flap action. Set upward velocity to -180; do not repeatedly add an impulse while a button remains held. Gravity is 420 pixels per second squared.", example: 'if (input.action === "flap" && state.status === "playing") state.bird.vy = -180;' },
|
|
223
|
+
{ name: "input.gate", signature: "input.gate: gate | null", description: "A tick may carry one seeded gate with id, x, gapY, gapHeight and width. Add it once, then move gates left at 90 pixels per second. The same seed supplies the same course.", example: "if (input.gate && !state.gates.some(g => g.id === input.gate.id)) state.gates.push({ ...input.gate, passed: false });" },
|
|
224
|
+
{ name: "bird.radius", signature: "bird.radius = 10", description: "The bird has a collision radius. It fits a gate only when its top and bottom are inside the gap while its horizontal extent overlaps the gate. The floor is y=360 and ceiling y=0.", example: "const top = state.bird.y - state.bird.radius; const bottom = state.bird.y + state.bird.radius;" },
|
|
225
|
+
{ name: "gate.passed", signature: "gate.passed: boolean", description: "Award a point once after the entire gate moves behind the bird's left edge. Set passed immediately so later ticks cannot award the same gate again. Remove gates only after they leave the world.", example: "const behind = gate.x + gate.width < state.bird.x - state.bird.radius;" },
|
|
226
|
+
{ name: "status", signature: '"ready" | "playing" | "over"', description: "Start changes ready to playing. A collision changes playing to over. Over freezes physics and score until restart returns a fresh ready state; pause is controlled by the host.", example: 'if (state.status !== "playing" || input.action !== "tick") return state;' }
|
|
227
|
+
]
|
|
228
|
+
}, [
|
|
229
|
+
{
|
|
230
|
+
title: "A bird with weight",
|
|
231
|
+
concepts: ["State", "Velocity", "Simulation time"],
|
|
232
|
+
goals: ["Update vertical velocity and position using the supplied simulated time.", "Keep ready and finished races still until a valid start or restart action."],
|
|
233
|
+
extension: "Compare gravity values 300 and 420 in separate named saves. Use the same starting height and tick count to explain the difference in the fall.",
|
|
234
|
+
activities: {
|
|
235
|
+
learn: ["Position tells us where the bird is; velocity tells us how quickly it moves. Down is positive y in this world. On each playing tick, first add 420 \xD7 dt to vy, then add the new vy \xD7 dt to y. These are pixels per second and seconds, not pixels per screen redraw.", "At rest, vy is zero. Gravity changes vy even before the first visible movement. Keep both numbers in the state."],
|
|
236
|
+
predict: ["Start at y=180 with vy=0 and use dt=0.02. Calculate the new velocity, then the new position for one tick. Use the updated velocity in the second calculation and keep the fractional part.", "420 \xD7 0.02 is 8.4. Multiplying that by 0.02 gives the first displacement; adding 8.4 directly to y mixes units."],
|
|
237
|
+
build: ["In game.js, handle start by setting status to playing. Handle tick only while playing: update vy, update y and add dt to elapsed. Return the whole state so that the preview can draw the new bird and read its numbers.", "Keep the starter fields and initialState function. A missing return or returning only bird loses the rest of the race state."],
|
|
238
|
+
run: ["Start the preview, step through several ticks and watch the velocity and height readouts. Restart the simulation and repeat the same number of steps. The numbers should match even if you paused for a while between steps.", "The preview supplies simulated time. Waiting with the preview paused must not make the bird jump when you resume."],
|
|
239
|
+
assess: ["Check the saved flight model. Tests compare a fresh state, the first ticks of a falling bird and ticks received while ready. They check the calculation order as well as whether the bird appears to fall.", "A visually moving bird can still use the wrong units. Compare the expected and actual vy and y values in the first failing tick."],
|
|
240
|
+
inspect: ["Find the earliest mismatch in the flight trace. If velocity is right but displacement is far too large, inspect the second dt multiplication. If nothing changes, inspect the start transition and the tick condition.", "Change one calculation at a time. A later wrong height is often the sum of an earlier small error."],
|
|
241
|
+
fix: ["Repair the integration and state conditions, then rerun the same tick sequence and assess the current saved source. Keep the bird still in ready state and retain fractional coordinates during simulation.", "Round only a displayed label, never the stored physics values. Repeated rounding changes the trajectory."],
|
|
242
|
+
explain: ["Choose why a game should multiply speed by simulated seconds rather than adding that speed on every redraw. Relate your answer to the paused and stepped runs you compared.", "Two devices can draw different numbers of frames while simulating the same amount of game time."],
|
|
243
|
+
reward: ["Save this project as Falling bird. You now have a small physics model with a reproducible trace. The next mission adds a single upward impulse so that the player can influence the flight.", "Keep a named save before changing the controls; it gives you a simple physics baseline to revisit."]
|
|
244
|
+
},
|
|
245
|
+
questions: {
|
|
246
|
+
learn: { question: "Which quantity describes how fast the bird moves vertically?", choices: ["Its score", "Its velocity vy", "Its gate count"], correctChoice: 1, feedback: "The y coordinate is position; vy is the rate at which that position changes over simulated seconds." },
|
|
247
|
+
predict: { question: "After the first 0.02-second tick, what are vy and y?", choices: ["8.4 and 180.168", "420 and 600", "8.4 and 188.4"], correctChoice: 0, feedback: "Velocity becomes 8.4 pixels per second. The displacement is 8.4 \xD7 0.02 = 0.168 pixels." },
|
|
248
|
+
explain: { question: "Why use dt instead of adding a fixed displacement per redraw?", choices: ["It makes gravity unnecessary", "It guarantees every device draws at the same rate", "It ties movement to game time rather than drawing frequency"], correctChoice: 2, feedback: "The simulation measures elapsed seconds, so an extra screen redraw does not create an extra movement step." }
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
title: "One flap, one impulse",
|
|
253
|
+
concepts: ["Events", "Assignment", "State transitions"],
|
|
254
|
+
goals: ["Make one flap press set a bounded upward velocity during a race.", "Handle repeated presses and held controls without accumulating unlimited lift."],
|
|
255
|
+
extension: "Save two flap strengths and compare their highest point from the same starting state. Explain the trade-off between recovery and fine control.",
|
|
256
|
+
activities: {
|
|
257
|
+
learn: ["A flap is an event, not a permanent force. Set vy to -180 when a flap action arrives while playing. Negative velocity moves upwards. Setting a value differs from subtracting 180: two immediate presses should still leave vy at -180.", "The host converts a keyboard or touch press to a flap action. A held key should not become an uncontrolled stream of repeat events."],
|
|
258
|
+
predict: ["The bird is falling with vy=70. Predict vy after one flap and after a second flap with no tick between them. Then predict what the first gravity tick does to that upward velocity.", "Assignment replaces the old value. Gravity adds a positive amount, so upward speed gradually becomes less negative."],
|
|
259
|
+
build: ["Add the flap branch to update. Require playing status and assign vy=-180. Keep start separate from flap, and add restart handling that calls initialState rather than resetting only the bird's y coordinate.", "Return after handling an event if that makes your branches clearer. A flap event is not a tick and must not also advance elapsed time."],
|
|
260
|
+
run: ["Start a race and try single presses, quick double presses, holding a key and using the touch flap button. Step after a flap to observe rising, slowing, stopping at the top and falling again. Check that restart returns to ready.", "Use the velocity readout to distinguish a fresh impulse from a frame update. Two presses do not mean twice the upward speed."],
|
|
261
|
+
assess: ["Check flap handling during playing, ready and over states. The checks also send two consecutive flap actions and a restart after movement. Only the intended action should change the corresponding state.", "Tests call update directly as well as exercising controls. Your project still needs safe event handling even when the interface normally filters held keys."],
|
|
262
|
+
inspect: ["If the double-flap check fails, compare assignment with subtraction. If a finished bird keeps moving after a press, inspect the status guard. If restart inherits old velocity, compare every fresh-state field.", "A working button is only one part of the behaviour. The state transition must be correct for actions received at awkward times."],
|
|
263
|
+
fix: ["Repair the smallest failing control branch and replay the fall model from the previous mission. Verify that flap changes velocity without changing position immediately, and that restart returns a fresh state with elapsed zero.", "An upward teleport can look like a flap but bypasses the physics model. Let the next tick apply the new velocity to position."],
|
|
264
|
+
explain: ["Choose why assigning a fixed impulse makes rapid presses more predictable than subtracting from the current velocity. Use the double-press trace as evidence, then describe where the playing-state check belongs.", "The same control action should have a clear, bounded meaning even if it arrives twice quickly."],
|
|
265
|
+
reward: ["Save this version as Controlled flight. You can now start, flap and reset a reproducible bird simulation. Next you will create a world that moves past the bird and gives those controls a purpose.", "Try a short flight without repeatedly pressing the button. You will soon need deliberate timing to pass gates."]
|
|
266
|
+
},
|
|
267
|
+
questions: {
|
|
268
|
+
learn: { question: "What should a flap action do while the race is playing?", choices: ["Set vy to -180", "Teleport y to zero", "Subtract 180 on every display frame"], correctChoice: 0, feedback: "A flap sets one bounded upward velocity. Position changes later through the ordinary physics tick." },
|
|
269
|
+
predict: { question: "Two flap actions without a tick between them leave vy at what value?", choices: ["-360", "-180", "70"], correctChoice: 1, feedback: "Both actions assign the same value, so the second press does not add another -180 to the first." },
|
|
270
|
+
explain: { question: "Why ignore flap events after the race is over?", choices: ["To erase the previous score", "To make the controls depend on frame rate", "To keep the finished race stable until restart"], correctChoice: 2, feedback: "An over state preserves the final result. Restart is the explicit action that creates another race." }
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
title: "Gates on the horizon",
|
|
275
|
+
concepts: ["Arrays", "Identity", "Object lifecycle"],
|
|
276
|
+
goals: ["Add and move supplied gates without duplicating their stable identifiers.", "Remove offscreen gates while retaining the objects still needed for collisions and scoring."],
|
|
277
|
+
extension: "Compare a wide and a narrow gap on the same seeded course. Keep the movement speed unchanged so the comparison isolates gap size.",
|
|
278
|
+
activities: {
|
|
279
|
+
learn: ["A gate is a record in the gates array, with a stable id, x position, width and vertical gap. Some ticks supply input.gate. Add a fresh copy once, set passed=false and move every gate left by 90 \xD7 dt on each playing tick.", "The bird can stay at x=80 while the scenery moves. Relative motion makes the race feel like forward flight."],
|
|
280
|
+
predict: ["A gate starts at x=480 and the next tick has dt=0.02. Predict its new x. Then consider two input messages carrying the same id: decide whether they describe two gates or the same gate arriving twice.", "The displacement is speed multiplied by seconds. Identity comes from id, not from the gate's current position."],
|
|
281
|
+
build: ["Extend the playing tick to accept a supplied gate only if its id is absent. Copy its fields into your array and initialise passed. Move each gate, then filter out gates whose right edge is left of zero. Preserve all other race state.", "Use gate.x + gate.width for the right edge. Removing a gate when its left edge reaches zero deletes it while part remains onscreen."],
|
|
282
|
+
run: ["Run a seeded course long enough for several gates to enter and leave. Pause and inspect each id and x position. Restart with the same seed to compare the sequence, and watch the array length settle rather than grow forever.", "A simulation may keep several visible gates at once. Filtering should remove only those entirely outside the left edge."],
|
|
283
|
+
assess: ["Assess insertion, duplicate-id handling, movement and cleanup. Cases include no incoming gate, an already known id and a gate with only its left edge offscreen. The checks compare gate data, not just the picture.", "An empty input.gate means no new gate this tick; it does not mean clear the existing array."],
|
|
284
|
+
inspect: ["If gates flicker or disappear, inspect the filter boundary and whether you overwrite the array on empty inputs. If a gate appears twice, compare its stable id before insertion. If x changes too quickly, revisit the units.", "Logically inspect the before and after arrays in the trace. A changed index is not a changed identity."],
|
|
285
|
+
fix: ["Repair the gate lifecycle and rerun a course with duplicate and empty inputs. Keep each object's id and gap measurements intact while changing x. Recheck the flap and falling tests to catch accidental changes to the bird.", "Copying a supplied object keeps the project state separate from its input record. Avoid deleting properties from input.gate."],
|
|
286
|
+
explain: ["Choose why stable identifiers are useful when objects move and arrays are filtered. Explain why the screen position cannot reliably identify the same gate on the next tick.", "Position changes during play and array indices change after cleanup; a gate's id stays the same."],
|
|
287
|
+
reward: ["Save Moving gates. Your project now manages objects across their whole lifetime instead of drawing a single fixed obstacle. The next mission decides when the bird actually touches one of them.", "Keep this version for checking collision bugs: it already establishes the expected obstacle movement."]
|
|
288
|
+
},
|
|
289
|
+
questions: {
|
|
290
|
+
learn: { question: "Which value should identify the same gate across movement and cleanup?", choices: ["Its current array index", "Its current x coordinate", "Its stable id"], correctChoice: 2, feedback: "The id remains attached to that gate while x and its array index can change during the race." },
|
|
291
|
+
predict: { question: "At 90 pixels per second, where is a gate after one 0.02-second tick from x=480?", choices: ["478.2", "390", "481.8"], correctChoice: 0, feedback: "The gate moves left by 90 \xD7 0.02 = 1.8 pixels, so its new x coordinate is 478.2." },
|
|
292
|
+
explain: { question: "When is it safe to remove a gate for leaving the left edge?", choices: ["As soon as x is less than zero", "When x plus width is less than zero", "Whenever a new gate arrives"], correctChoice: 1, feedback: "The complete gate is outside the world only after its right edge passes the left boundary." }
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
title: "A fair landing",
|
|
297
|
+
concepts: ["Bounds", "Collision", "Terminal states"],
|
|
298
|
+
goals: ["Detect floor, ceiling and gate contacts using the bird's full radius.", "Freeze a lost race and reset every field before the next attempt."],
|
|
299
|
+
extension: "Design three near-miss cases on paper: just inside, exactly touching and just outside a gap. Explain which should be safe under the documented contact rule.",
|
|
300
|
+
activities: {
|
|
301
|
+
learn: ["The bird is larger than a point. Its top is y-radius and bottom is y+radius. Contact with the floor, ceiling or a solid gate ends the race. For a gate, first test horizontal overlap, then whether the whole bird fits strictly inside the vertical gap.", "Use the same contact rule consistently: touching a solid edge is a collision. A gate elsewhere on the screen cannot collide with the bird yet."],
|
|
302
|
+
predict: ["A gap runs from y=120 to y=220 and overlaps the bird horizontally. Predict whether a bird at y=125 with radius 10 fits, even though its centre is inside the gap. Then compare a bird centred at y=170.", "Subtract and add the radius before judging the gap. The first bird's top is y=115."],
|
|
303
|
+
build: ["After moving the bird and gates, check world boundaries and each overlapping gate. Set status to over on contact. Make later tick and flap actions preserve an over state. Restart must return initialState with no gates, no score and no old velocity.", "A clear gate gap is empty space. Test its top and bottom edges, not collision with the gate's whole rectangular outline."],
|
|
304
|
+
run: ["Try a centred pass, a scrape against a gap edge and a floor collision. Pause at the last safe tick and the first contact tick. Restart after each loss and verify that old gates do not remain in the new race.", "The text trace shows bounds and status, so you can inspect a close collision without relying on fast animation."],
|
|
305
|
+
assess: ["Check contacts at all four relevant edges, safe passage through the gap and a gate that is vertically dangerous but not horizontally near the bird. Also check that over states cannot gain movement from later actions.", "Correct collision needs both axes. A height outside one distant gate's gap must not end the race."],
|
|
306
|
+
inspect: ["If safe passes fail, separate horizontal overlap from vertical fit. If the bird clips an edge, inspect radius and equality comparisons. If a lost race continues, follow the status guard before the tick calculations.", "Write the bird's left, right, top and bottom next to the gate bounds. Use the first contact case instead of guessing from a later frame."],
|
|
307
|
+
fix: ["Repair the earliest incorrect boundary comparison, then repeat inside, touching and outside cases. Recheck restart from both ready and over states and ensure no score or gate data leaks between attempts.", "Changing all comparisons at once can swap one edge bug for another. Keep an explicit example for each edge."],
|
|
308
|
+
explain: ["Choose why centre-only collision checks can look unfair to the player. Refer to the bird's radius and explain how a terminal over state makes the last valid race result stable.", "The drawn bird and its logical bounds should agree. A visible wing passing through a solid wall breaks that agreement."],
|
|
309
|
+
reward: ["Save Fair collisions. Your race can now end consistently and begin again with a clean state. Next you will count successful gate passages without rewarding the same obstacle on every tick.", "Keep your near-miss examples; they become regression cases for later changes to difficulty."]
|
|
310
|
+
},
|
|
311
|
+
questions: {
|
|
312
|
+
learn: { question: "What must be checked before a gate can collide with the bird?", choices: ["Horizontal overlap as well as vertical bounds", "Only the gate's colour", "Only whether the centre is below the gap"], correctChoice: 0, feedback: "Collision requires the gate to overlap the bird horizontally and the bird to touch a solid vertical region." },
|
|
313
|
+
predict: { question: "Does a radius-10 bird centred at y=125 fit a gap from 120 to 220?", choices: ["Yes, because its centre is inside", "No, because its top reaches y=115", "Only if the score is zero"], correctChoice: 1, feedback: "The bird extends beyond its centre. Its top is five pixels above the gap, so it overlaps the upper obstacle." },
|
|
314
|
+
explain: { question: "What should a restart after a collision create?", choices: ["The old gates with only y reset", "The old score and a new bird", "A fresh ready state for every race field"], correctChoice: 2, feedback: "Restart creates a new attempt. Old velocity, score, gates and elapsed time must not carry into it." }
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
title: "A score you can trust",
|
|
319
|
+
concepts: ["One-time events", "Reproducibility", "Invariants"],
|
|
320
|
+
goals: ["Award each fully passed gate once and never award points after a collision.", "Reproduce a score with the same seed and sequence of player inputs."],
|
|
321
|
+
extension: "Record two flap schedules for the same seed and compare their results. Change only the schedule so that the obstacle layout remains a fair comparison.",
|
|
322
|
+
activities: {
|
|
323
|
+
learn: ["A passed gate is an event hidden inside repeated ticks. When its right edge is behind the bird's left edge and passed is false, add one point and set passed=true. Run collision checks first so a losing tick cannot also reward an unsafe passage.", "The passed marker remembers that this gate has already contributed. A condition can stay true for many later ticks."],
|
|
324
|
+
predict: ["A gate becomes fully behind the bird and remains visible for eight more ticks. Predict the score increase with and without a passed marker. Then decide whether changing only the display frame rate should alter that score.", "Ask how many gates were completed, not how many ticks observed the same completed gate."],
|
|
325
|
+
build: ["Add one-time passage scoring after collision resolution while status remains playing. Preserve each gate's passed field during movement. Keep supplied seeded gates as the source of obstacles instead of calling a clock or inventing a new course every frame.", "Use the complete gate edge, not its centre, to decide it has passed. A point should mean the whole obstacle was cleared."],
|
|
326
|
+
run: ["Pass one gate, pause and step several more ticks while it is still visible. The score should remain one. Restart with the same seed and repeat the same input schedule; compare both score and gate positions at the same elapsed time.", "Human button timing can vary. Stepped actions make an exact comparison easier than two hand-played attempts."],
|
|
327
|
+
assess: ["Check first passage, repeated ticks behind the bird, two different gate ids and a passage on a collision tick. Checks also repeat an identical seeded input sequence and compare the final state.", "A high score alone proves little. The tests care that each point has a distinct, safe passage behind it."],
|
|
328
|
+
inspect: ["If score grows continuously, find where passed is set or accidentally cleared. If replay changes, inspect calls to ambient time or uncontrolled randomness. If a losing tick awards a point, inspect the order of collision and scoring.", "Keep the gate id in your reasoning. Two snapshots of one id still describe only one obstacle."],
|
|
329
|
+
fix: ["Repair scoring and replay differences, then recheck insertion, cleanup and restart. A restarted race must clear score and passed markers by creating a new gate array rather than reusing the last attempt's objects.", "Use the same small scenario before and after the fix. A different course can hide whether your change caused the improvement."],
|
|
330
|
+
explain: ["Choose why a stable seed and input sequence are useful when investigating a bug. Explain separately why the passed flag is still needed even when the course is deterministic.", "Reproducibility helps you repeat a fault; identity and state prevent counting that same event repeatedly."],
|
|
331
|
+
reward: ["Save Honest score. Your race now turns actual safe passages into a repeatable result. The capstone combines movement, controls, gate lifecycle, collisions and scoring into a polished complete attempt.", "Name a second save before experimenting with speed or gap size so the tested score model remains available."]
|
|
332
|
+
},
|
|
333
|
+
questions: {
|
|
334
|
+
learn: { question: "Which action prevents one passed gate from awarding a point every tick?", choices: ["Drawing the score in a different colour", "Making the gate wider", "Setting that gate's passed marker"], correctChoice: 2, feedback: "The marker records that this specific gate has already been counted, even while the passage condition stays true." },
|
|
335
|
+
predict: { question: "One gate stays behind the bird for nine ticks; how many points should it award?", choices: ["One", "Nine", "As many as the display draws"], correctChoice: 0, feedback: "The score counts distinct completed gates. Repeated observations of the same gate do not create new passages." },
|
|
336
|
+
explain: { question: "Why keep the same seed and action sequence when reproducing a scoring bug?", choices: ["It makes all possible bugs disappear", "It keeps the experiment's conditions repeatable", "It removes the need to check collisions"], correctChoice: 1, feedback: "A repeated scenario lets you compare behaviour before and after a change without a different course confusing the result." }
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
title: "The twilight sprint",
|
|
341
|
+
concepts: ["Integration", "Regression testing", "Accessible play"],
|
|
342
|
+
goals: ["Complete a playable race with bounded physics, fair gates and repeatable scoring.", "Support start, flap, pause and restart without losing the latest saved project or earned progress."],
|
|
343
|
+
extension: "Design a gentler practice mode in a separate save. Explain the changed gravity, gate spacing or flap strength, then rerun collision and scoring cases before showing it to someone else.",
|
|
344
|
+
activities: {
|
|
345
|
+
learn: ["A finished race is more than a successful flap. Its controls, state transitions and rules must agree from ready through over and back to ready. Use a checklist covering flight, input, gates, collision, score and restart, then add keyboard, touch and reduced motion checks.", "The host owns pause and accessibility controls. Your state update must respect the supplied actions and simulated time rather than assuming animation is always running."],
|
|
346
|
+
predict: ["Predict the state after this sequence: start, flap, several ticks, pause, resume, collision, flap, restart. Identify which actions should leave elapsed time unchanged and which action should remove every gate.", "Pausing stops ticks; it does not reset the game. A flap after over is ignored, and restart makes a fresh ready state."],
|
|
347
|
+
build: ["Combine your passing mission versions into the final game.js. Keep small named helpers for physics, gate movement and collision if they improve clarity. Preserve the documented input/state contract and ensure restart creates a fresh object instead of editing an old saved reference.", "Restore your latest good named save if an experiment tangles several behaviours. Rebuild one verified change at a time."],
|
|
348
|
+
run: ["Play a full attempt using only the keyboard, then try the onscreen touch controls. Pause, step and restart at different moments. Inspect the text state in reduced motion mode and compare a replay with the same seed, keeping the project saved throughout.", "Do not rely on colour or animation alone to understand over, score or collision. The host's status text should tell the same story as the scene."],
|
|
349
|
+
assess: ["Run the final course checks against the current saved source. They combine physics, valid and mistimed inputs, duplicate gates, near-edge collisions, one-time scoring and clean restarts across several seeded courses.", "Passing one favourite course is not the whole capstone. Read which scenario failed and retain the source version attached to that result."],
|
|
350
|
+
inspect: ["Find the first difference in a failing capstone trace and connect it to the mission that taught that behaviour. Distinguish a physics error from an input error or an object-lifecycle error before choosing a repair.", "A late wrong score may come from an earlier collision or duplicate insertion. Start with the earliest incorrect state, not the final number."],
|
|
351
|
+
fix: ["Repair the final project and rerun both the failing case and previously passing mission cases. Confirm that changing the latest source invalidates old assessment evidence, then check the newly saved version before continuing.", "Avoid fixing a seeded case by hard-coding its gate positions. The project should respond to the supplied course data."],
|
|
352
|
+
explain: ["Choose what the final tests establish about your race, then describe one change you would explore next and which existing cases that change could affect. Be honest about the tested scenarios and the limits of the simplified collision model.", "A well-tested educational game can still have future improvements. Explain the evidence you have rather than promising that no bug is possible."],
|
|
353
|
+
reward: ["Save Twilight sprint, finish the final assessment and replay any mission. You have built a complete flight game, made its score explainable and preserved your work in account saves. Keep a stable named version before trying the practice-mode extension.", "Earned completion remains while you experiment. A later unsuccessful edit is a new learning opportunity, not a reason to lose your badge."]
|
|
354
|
+
},
|
|
355
|
+
questions: {
|
|
356
|
+
learn: { question: "What should an integrated race checklist include besides a working flap?", choices: ["Only a decorative background", "State transitions, rules, replay and usable controls", "Only the highest score ever seen"], correctChoice: 1, feedback: "The whole journey matters: starting, playing, losing, pausing and restarting must preserve consistent game rules." },
|
|
357
|
+
predict: { question: "After a collision followed by flap and restart, what state should remain?", choices: ["The old race flying again", "An over state with its old gates", "A fresh ready state with zero score and no gates"], correctChoice: 2, feedback: "Flap cannot revive an over race. Restart explicitly replaces it with initialState, including fresh arrays and counters." },
|
|
358
|
+
explain: { question: "What does passing the final scenarios establish?", choices: ["Evidence that the tested game rules behave as specified", "A proof that no possible bug remains", "Permission to skip tests after future edits"], correctChoice: 0, feedback: "Tests provide evidence for their scenarios. Changes and new edge cases still need investigation and fresh checks." }
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
]);
|
|
362
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
363
|
+
0 && (module.exports = {
|
|
364
|
+
course,
|
|
365
|
+
practice
|
|
366
|
+
});
|
|
367
|
+
//# sourceMappingURL=skywing-sprint.cjs.map
|