Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion _includes/interactive/first-sequence.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
<div class="cz-basic__warning"></div>
</div>

<p class="cz-basic__prompt">Tap a step to add it to the routine.</p>
<p class="cz-basic__prompt">The steps are not in order. Tap a step to add it to the routine.</p>
<div class="cz-seq__bank"></div>

<p class="cz-basic__prompt">Your routine:</p>
Expand Down
52 changes: 51 additions & 1 deletion assets/js/interactive/beginner.js
Original file line number Diff line number Diff line change
Expand Up @@ -387,10 +387,59 @@
{ id: "shoot", label: "Shoot", python: "self.robot.coroutines.shoot" }
];

/* Every order that scores. The intake keeps running once it starts, so the
* robot can collect the game piece as it drives through, and that gives more
* than one answer. `node tools/validate-sequence.js` replays all 120 orders
* against the same rules the widget uses and checks this list is still the
* whole set.
*/
var WORKING_ORDERS = [
"lower drive_piece intake drive_goal shoot",
"lower intake drive_piece drive_goal shoot",
"drive_piece lower intake drive_goal shoot",
"intake lower drive_piece drive_goal shoot"
];

function isWorkingOrder(ids) {
return WORKING_ORDERS.indexOf(ids.join(" ")) !== -1;
}

function stepById(id) {
return STEPS.filter(function (s) { return s.id === id; })[0];
}

/* The order the cards are shown in. STEPS is written in an order that works,
* because that is the order a person reads it in, and the widget must not
* hand that answer to a student. So shuffle the cards, and never show them
* in an order that would score if the student tapped straight along the row.
*/
function shuffledIds() {
var ids = STEPS.map(function (s) { return s.id; });
do {
for (var i = ids.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var tmp = ids[i];
ids[i] = ids[j];
ids[j] = tmp;
}
} while (isWorkingOrder(ids));
return ids;
}

/* Keep one student on one shuffle. The routine is saved, so a bank that
* rearranged itself on every visit would not agree with the routine below
* it. A saved order from an older list of steps is thrown away.
*/
function bankOrder() {
var saved = load("sequence.bank");
var whole = Array.isArray(saved) && saved.length === STEPS.length &&
STEPS.every(function (s) { return saved.indexOf(s.id) !== -1; });
if (whole && !isWorkingOrder(saved)) return saved;
var fresh = shuffledIds();
save("sequence.bank", fresh);
return fresh;
}

function setupSequence(root) {
var canvas = root.querySelector("canvas");
var bank = root.querySelector(".cz-seq__bank");
Expand All @@ -404,7 +453,8 @@
var world = window.CZScene.createWorld();
var running = null;

STEPS.forEach(function (step) {
bankOrder().forEach(function (id) {
var step = stepById(id);
var card = el("button", "cz-seq__card", step.label);
card.type = "button";
card.addEventListener("click", function () {
Expand Down
4 changes: 2 additions & 2 deletions docs/Code/Tutorials/first-sequence.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ You write the list. The robot does the steps in the order you write them.

Get the game piece into the goal.

You have five steps. Tap a step to add it to the routine. Use the arrows to
move a step. Use ✕ to remove it.
You have five steps. They are not in order. Tap a step to add it to the
routine. Use the arrows to move a step. Use ✕ to remove it.

Then tap **Play** and watch.

Expand Down
1 change: 1 addition & 0 deletions docs/Code/Tutorials/tutorials.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ PyYAML for the first one.
| `node tools/validate-tuning.js` | Every gain quoted on a tuning page. Each one must be reachable on its slider and must pass at every setpoint |
| `node tools/validate-auto-task.js` | The autonomous task is possible, and a routine with its actions in series is not |
| `node tools/validate-swerve.js` | The swerve maths on the wiki agrees with `ctre_module_state.py` on the robot |
| `node tools/validate-sequence.js` | Every order a student can build in tutorial 2. The list of orders that score is complete, so the shuffled step cards can never be tapped from left to right for the answer |
| `node tools/validate-rule.js` | Every rule a student can build in tutorial 3. Each sensor that can work has an unbroken band of values that pass, and every failure gives a reason |
| `python3 tools/validate-jam.py` | The jam-detector challenge. No fixed threshold can pass it, and the intended answer can |
| `python3 tools/validate-shoot.py` | The shoot-on-the-fly challenge. Measures how close each number of corrections gets |
Expand Down
203 changes: 203 additions & 0 deletions tools/validate-sequence.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
#!/usr/bin/env node
/*
* Check the second beginner tutorial: "Put the steps in order".
*
* node tools/validate-sequence.js
*
* No dependencies.
*
* The widget shuffles the five step cards, and it refuses any shuffle that
* would score if a student simply tapped along the row. It decides that from
* the WORKING_ORDERS list in beginner.js, so the whole guarantee rests on that
* list being right. This runs all 120 orders through the real robot and checks
* that the list is still exactly the set that scores.
*
* The rules a step finishes by live inside a closure in beginner.js, so there
* is no module to load and this file holds a copy of them. The first check
* compares that copy with beginner.js and fails if the two have parted, which
* is the signal to read the widget again and bring this file back in line.
*/
const fs = require("fs");
const path = require("path");
const CZScene = require("../assets/js/interactive/robot-scene.js");

const WIDGET = path.join(__dirname, "..", "assets", "js", "interactive", "beginner.js");
const source = fs.readFileSync(WIDGET, "utf8");

let failures = 0;
function check(ok, what) {
console.log((ok ? "ok " : "FAIL ") + what);
if (!ok) failures++;
}

/* ------------------------------------------------------------------ *
* The copy of the widget's rules.
* ------------------------------------------------------------------ */

const IDS = ["lower", "drive_piece", "intake", "drive_goal", "shoot"];
const DT = 1 / 60; // the frame the widget draws at
const GIVE_UP_AFTER = 40; // seconds; a routine that stalls never ends

function beginStep(world, id) {
if (id === "lower") world.armDown = true;
if (id === "drive_piece") world.targetX = CZScene.PIECE_AT;
if (id === "intake") world.intakeOn = true;
if (id === "drive_goal") world.targetX = CZScene.GOAL_AT;
if (id === "shoot") CZScene.shoot(world);
return { id: id, t: 0 };
}

function stepDone(world, state) {
if (state.id === "lower") return world.armAngle > 0.98;
if (state.id === "drive_piece") {
var there = Math.abs(world.x - CZScene.PIECE_AT) < 0.004;
if (!there) return false;
if (world.intakeOn && world.pieceOnFloor && state.t < 3) return false;
return true;
}
if (state.id === "drive_goal") return Math.abs(world.x - CZScene.GOAL_AT) < 0.004;
if (state.id === "intake") return state.t > 0.8;
if (state.id === "shoot") return state.t > 1.0;
return true;
}

/* ------------------------------------------------------------------ *
* Read what the widget says, so this file cannot drift away from it.
* ------------------------------------------------------------------ */

/* The text of a top level `function name(...) {...}`, by counting braces. */
function functionSource(name) {
const start = source.indexOf("function " + name + "(");
if (start === -1) return null;
let depth = 0;
for (let i = source.indexOf("{", start); i < source.length; i++) {
if (source[i] === "{") depth++;
else if (source[i] === "}" && --depth === 0) return source.slice(start, i + 1);
}
return null;
}

/* Comments and layout are free to change. Anything else is a rule change. */
function logic(text) {
return String(text)
.replace(/\/\*[\s\S]*?\*\//g, " ")
.replace(/\/\/[^\n]*/g, " ")
.replace(/\s+/g, " ")
.trim();
}

/* The widget reads `routine[index]` and keeps its own `running`. Compare the
* part that decides what a step does to the world, which is the part copied. */
function conditions(text) {
return logic(text).match(/if \(id === [\s\S]*?;/g) || [];
}

function readWorkingOrders() {
const block = source.match(/var WORKING_ORDERS = \[([\s\S]*?)\];/);
if (!block) return null;
return (block[1].match(/"([^"]+)"/g) || []).map((s) => s.slice(1, -1));
}

function readSteps() {
const block = source.match(/var STEPS = \[([\s\S]*?)\];/);
if (!block) return null;
return (block[1].match(/id: "([^"]+)"/g) || []).map((s) => s.slice(5, -1));
}

console.log("--- this file still matches the widget ---");

const widgetSteps = readSteps();
check(widgetSteps && widgetSteps.length === IDS.length &&
IDS.every((id) => widgetSteps.indexOf(id) !== -1),
"the five steps are the ones this file replays" +
(widgetSteps ? "" : "\n could not read STEPS out of beginner.js"));

const widgetBegin = functionSource("beginStep");
const mine = conditions(beginStep.toString()).map((s) => s.replace(/CZScene\./g, "window.CZScene."));
check(widgetBegin && conditions(widgetBegin).join("\n") === mine.join("\n"),
"what each step does to the world matches beginStep() in beginner.js" +
(widgetBegin ? "" : "\n could not find beginStep() in beginner.js"));

const widgetDone = functionSource("stepDone");
check(widgetDone &&
logic(widgetDone).replace(/window\.CZScene\./g, "CZScene.") ===
logic(stepDone.toString()).replace(/function stepDone\(world, state\)/, "function stepDone(state)"),
"when a step finishes matches stepDone() in beginner.js" +
(widgetDone ? "" : "\n could not find stepDone() in beginner.js"));

/* ------------------------------------------------------------------ *
* Run every order the student can build.
* ------------------------------------------------------------------ */

function run(order) {
const world = CZScene.createWorld();
let i = 0;
let t = 0;
let state = beginStep(world, order[0]);
while (i < order.length && t < GIVE_UP_AFTER) {
state.t += DT;
t += DT;
CZScene.step(world, DT);
if (stepDone(world, state)) {
i += 1;
if (i < order.length) state = beginStep(world, order[i]);
}
}
// The widget keeps drawing while a shot is in the air. Let it land.
for (let k = 0; k < 120; k++) CZScene.step(world, DT);
// The same two checks the widget grades on.
return { scored: !world.pieceOnFloor && world.scored, stalled: t >= GIVE_UP_AFTER };
}

function orders(rest) {
if (rest.length <= 1) return [rest];
const out = [];
rest.forEach((id, i) => {
orders(rest.slice(0, i).concat(rest.slice(i + 1)))
.forEach((tail) => out.push([id].concat(tail)));
});
return out;
}

console.log("--- which orders score ---");

const all = orders(IDS);
const scored = all.filter((o) => run(o).scored).map((o) => o.join(" "));
scored.forEach((o) => console.log(" " + o));

const listed = readWorkingOrders();
check(listed !== null, "beginner.js has a WORKING_ORDERS list");

if (listed) {
const missing = scored.filter((o) => listed.indexOf(o) === -1);
const extra = listed.filter((o) => scored.indexOf(o) === -1);
check(missing.length === 0,
"every order that scores is in WORKING_ORDERS" +
(missing.length ? "\n not listed: " + missing.join("\n ") : ""));
check(extra.length === 0,
"every order in WORKING_ORDERS scores" +
(extra.length ? "\n does not score: " + extra.join("\n ") : ""));
}

check(scored.length > 0, `${all.length} orders tried, ${scored.length} score`);

console.log("--- the shuffle cannot give the answer away ---");

/* This is the reason the list has to be exact. The widget shuffles until the
* order is not in WORKING_ORDERS, so a wrong list is a bank that a student can
* solve by tapping from left to right. */
check(widgetSteps !== null && scored.indexOf(widgetSteps.join(" ")) !== -1,
"STEPS is written in an order that scores, so the bank must be shuffled");

const guard = functionSource("shuffledIds");
check(guard !== null && /while \(isWorkingOrder\(ids\)\)/.test(logic(guard)),
"the shuffle throws away any order that scores");

const bank = functionSource("bankOrder");
check(bank !== null && /isWorkingOrder\(saved\)/.test(logic(bank)),
"a saved shuffle that scores is thrown away too");

console.log(failures === 0
? "\nAll sequence checks passed."
: `\n${failures} check(s) failed.`);
process.exit(failures === 0 ? 0 : 1);
Loading