@conduction/docusaurus-preset 3.40.0 → 3.41.1
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/package.json +1 -1
- package/src/components/LockPick/LockPick.jsx +20 -7
- package/src/components/LockPick/LockPick.module.css +7 -0
- package/src/components/PaintByTokens/PaintByTokens.jsx +226 -0
- package/src/components/PaintByTokens/PaintByTokens.module.css +202 -0
- package/src/components/PaintByTokens/__tests__/engine.test.js +150 -0
- package/src/components/PaintByTokens/engine.js +194 -0
- package/src/components/Redaction/Redaction.jsx +259 -0
- package/src/components/Redaction/Redaction.module.css +180 -0
- package/src/components/Redaction/__tests__/engine.test.js +162 -0
- package/src/components/Redaction/engine.js +186 -0
- package/src/components/index.js +2 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* engine.test.js — the redaction rules.
|
|
3
|
+
*
|
|
4
|
+
* The asymmetry is the game: missing something costs a life, blacking
|
|
5
|
+
* out too much costs points. A version that scored both the same would
|
|
6
|
+
* teach people to black out the whole page, which is the other way of
|
|
7
|
+
* failing at this job.
|
|
8
|
+
*
|
|
9
|
+
* The second property is that every document has something to find. A
|
|
10
|
+
* document with no secrets in it is a free life the player cannot tell
|
|
11
|
+
* apart from a trap.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const test = require('node:test');
|
|
17
|
+
const assert = require('node:assert/strict');
|
|
18
|
+
|
|
19
|
+
const {
|
|
20
|
+
createGame, black, publish, step, clockMs, remaining, summarise,
|
|
21
|
+
DOCUMENTS, DEFAULTS,
|
|
22
|
+
} = require('../engine.js');
|
|
23
|
+
|
|
24
|
+
/** Black out every secret in the current document. */
|
|
25
|
+
function redactAll(state) {
|
|
26
|
+
let s = state;
|
|
27
|
+
s.doc.tokens.forEach((token, i) => { if (token.secret) s = black(s, i); });
|
|
28
|
+
return s;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
test('every document has something to find, and something that must stay', () => {
|
|
32
|
+
for (const doc of DOCUMENTS) {
|
|
33
|
+
const secrets = doc.tokens.filter((t) => t.secret).length;
|
|
34
|
+
const plain = doc.tokens.filter((t) => !t.secret).length;
|
|
35
|
+
assert.ok(secrets > 0, `${doc.key}: nothing to redact`);
|
|
36
|
+
assert.ok(plain > 0, `${doc.key}: nothing but secrets`);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('a game starts with a document, a clock and three lives', () => {
|
|
41
|
+
const s = createGame({seed: 1, now: 0});
|
|
42
|
+
assert.ok(s.doc);
|
|
43
|
+
assert.equal(s.lives, DEFAULTS.lives);
|
|
44
|
+
assert.equal(s.doc.expiresAt - s.doc.startedAt, DEFAULTS.clockStartMs);
|
|
45
|
+
assert.equal(remaining(s, 0), 1);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('publishing with every secret blacked out is clean, and pays', () => {
|
|
49
|
+
let s = createGame({seed: 2, now: 0});
|
|
50
|
+
const secrets = s.doc.tokens.filter((t) => t.secret).length;
|
|
51
|
+
s = publish(redactAll(s), 100);
|
|
52
|
+
assert.equal(s.published, 1);
|
|
53
|
+
assert.equal(s.clean, 1);
|
|
54
|
+
assert.equal(s.lives, DEFAULTS.lives);
|
|
55
|
+
assert.equal(s.score, secrets * DEFAULTS.pointsPerSecret + DEFAULTS.pointsPerClean);
|
|
56
|
+
assert.ok(s.doc, 'no next document was dealt');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('leaving one secret visible is a breach, whatever else was right', () => {
|
|
60
|
+
let s = createGame({seed: 3, now: 0});
|
|
61
|
+
const secretIndexes = s.doc.tokens.map((t, i) => (t.secret ? i : -1)).filter((i) => i >= 0);
|
|
62
|
+
/* Catch all but the last one: the player did almost everything. */
|
|
63
|
+
secretIndexes.slice(0, -1).forEach((i) => { s = black(s, i); });
|
|
64
|
+
const before = s.score;
|
|
65
|
+
s = publish(s, 100);
|
|
66
|
+
assert.equal(s.lives, DEFAULTS.lives - 1);
|
|
67
|
+
assert.equal(s.breaches, 1);
|
|
68
|
+
assert.equal(s.score, before, 'a breach still paid for the ones that were caught');
|
|
69
|
+
assert.equal(s.published, 0, 'a breach counted as a published document');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('blacking out ordinary text costs points, not a life', () => {
|
|
73
|
+
let s = createGame({seed: 4, now: 0});
|
|
74
|
+
const plain = s.doc.tokens.findIndex((t) => !t.secret);
|
|
75
|
+
s = black(redactAll(s), plain);
|
|
76
|
+
const secrets = s.doc.tokens.filter((t) => t.secret).length;
|
|
77
|
+
s = publish(s, 100);
|
|
78
|
+
assert.equal(s.lives, DEFAULTS.lives, 'over-redaction cost a life');
|
|
79
|
+
assert.equal(s.overRedacted, 1);
|
|
80
|
+
assert.equal(s.clean, 0, 'a striped page counted as clean');
|
|
81
|
+
assert.equal(
|
|
82
|
+
s.score,
|
|
83
|
+
secrets * DEFAULTS.pointsPerSecret + DEFAULTS.pointsPerClean - DEFAULTS.overRedactionPenalty,
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('a badly over-redacted page never costs more points than it earned', () => {
|
|
88
|
+
let s = createGame({seed: 5, now: 0, config: {pointsPerSecret: 1, pointsPerClean: 1, overRedactionPenalty: 50}});
|
|
89
|
+
s = redactAll(s);
|
|
90
|
+
s.doc.tokens.forEach((token, i) => { if (!token.secret) s = black(s, i); });
|
|
91
|
+
s = publish(s, 100);
|
|
92
|
+
assert.ok(s.score >= 0, 'the score went negative');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('blacking out the same word twice changes nothing', () => {
|
|
96
|
+
let s = createGame({seed: 6, now: 0});
|
|
97
|
+
const i = s.doc.tokens.findIndex((t) => t.secret);
|
|
98
|
+
s = black(s, i);
|
|
99
|
+
const once = JSON.stringify(s.doc.tokens);
|
|
100
|
+
s = black(s, i);
|
|
101
|
+
assert.equal(JSON.stringify(s.doc.tokens), once);
|
|
102
|
+
assert.equal(black(s, 9999), s, 'a word that does not exist was blacked out');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('the clock publishes the document for you, exactly as it would', () => {
|
|
106
|
+
let s = createGame({seed: 7, now: 0});
|
|
107
|
+
const expires = s.doc.expiresAt;
|
|
108
|
+
/* Do nothing: the secrets are still visible when it goes out. */
|
|
109
|
+
s = step(s, expires + 1);
|
|
110
|
+
assert.equal(s.lives, DEFAULTS.lives - 1);
|
|
111
|
+
assert.equal(s.breaches, 1);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('a document finished in time is not published twice by the clock', () => {
|
|
115
|
+
let s = createGame({seed: 8, now: 0});
|
|
116
|
+
const expires = s.doc.expiresAt;
|
|
117
|
+
s = publish(redactAll(s), 100);
|
|
118
|
+
const published = s.published;
|
|
119
|
+
s = step(s, expires + 1);
|
|
120
|
+
assert.equal(s.published, published, 'the clock published the next document early');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('three breaches end the run, and nothing publishes afterwards', () => {
|
|
124
|
+
let s = createGame({seed: 9, now: 0});
|
|
125
|
+
for (let i = 0; i < 3; i++) s = publish(s, 100 * i);
|
|
126
|
+
assert.equal(s.over, true);
|
|
127
|
+
assert.equal(s.lives, 0);
|
|
128
|
+
assert.equal(publish(s, 999).score, s.score);
|
|
129
|
+
assert.equal(black(s, 0), s);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('the clock tightens with the score, down to a readable floor', () => {
|
|
133
|
+
const fresh = createGame({seed: 10, now: 0});
|
|
134
|
+
assert.equal(clockMs(fresh), DEFAULTS.clockStartMs);
|
|
135
|
+
assert.ok(clockMs({...fresh, score: 50}) < DEFAULTS.clockStartMs);
|
|
136
|
+
assert.equal(clockMs({...fresh, score: 5000}), DEFAULTS.clockFloorMs);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('the countdown runs full to empty and never past either end', () => {
|
|
140
|
+
const s = createGame({seed: 11, now: 0});
|
|
141
|
+
assert.equal(remaining(s, 0), 1);
|
|
142
|
+
assert.ok(Math.abs(remaining(s, DEFAULTS.clockStartMs / 2) - 0.5) < 0.01);
|
|
143
|
+
assert.equal(remaining(s, DEFAULTS.clockStartMs * 4), 0);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('a careful player clears documents rather than losing lives', () => {
|
|
147
|
+
let s = createGame({seed: 12, now: 0});
|
|
148
|
+
let t = 0;
|
|
149
|
+
for (let i = 0; i < 12 && !s.over; i++) {
|
|
150
|
+
s = publish(redactAll(s), t);
|
|
151
|
+
t += 200;
|
|
152
|
+
}
|
|
153
|
+
assert.equal(s.lives, DEFAULTS.lives);
|
|
154
|
+
assert.equal(s.published, 12);
|
|
155
|
+
assert.ok(s.score > 0);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('the summary reads as a sentence in both locales', () => {
|
|
159
|
+
const s = {published: 7, clean: 5};
|
|
160
|
+
assert.match(summarise(s, 'en'), /7 documents out · 5 clean/);
|
|
161
|
+
assert.match(summarise(s, 'nl'), /7 documenten uit · 5 schoon/);
|
|
162
|
+
});
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redaction — the rules, with no DOM and no clock of its own.
|
|
3
|
+
*
|
|
4
|
+
* Filinq's game. A document is about to be published. Black out
|
|
5
|
+
* everything in it that may not go out, before it does. A name, a
|
|
6
|
+
* citizen number, an address, a bank account, a date of birth: leave
|
|
7
|
+
* one in and it is a data breach, black out the whole thing and you
|
|
8
|
+
* have published a page of stripes.
|
|
9
|
+
*
|
|
10
|
+
* Both mistakes cost, and they cost differently, which is the point.
|
|
11
|
+
* Missing something is a breach and ends the document. Over-redacting
|
|
12
|
+
* is a nuisance: it costs points, not the run.
|
|
13
|
+
*
|
|
14
|
+
* Time and randomness are injected. The component owns the clock.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/* Every document is a list of tokens. `secret` marks what may not be
|
|
18
|
+
published; everything else is the sentence around it. The component
|
|
19
|
+
supplies the words. */
|
|
20
|
+
export const DOCUMENTS = [
|
|
21
|
+
{
|
|
22
|
+
key: 'permit',
|
|
23
|
+
tokens: [
|
|
24
|
+
{t: 'permitIntro'}, {t: 'name', secret: true}, {t: 'permitMiddle'},
|
|
25
|
+
{t: 'address', secret: true}, {t: 'permitTail'}, {t: 'bsn', secret: true},
|
|
26
|
+
{t: 'permitEnd'},
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
key: 'invoice',
|
|
31
|
+
tokens: [
|
|
32
|
+
{t: 'invoiceIntro'}, {t: 'company'}, {t: 'invoiceMiddle'},
|
|
33
|
+
{t: 'iban', secret: true}, {t: 'invoiceTail'}, {t: 'amount'},
|
|
34
|
+
{t: 'invoiceEnd'}, {t: 'email', secret: true},
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
key: 'objection',
|
|
39
|
+
tokens: [
|
|
40
|
+
{t: 'objectionIntro'}, {t: 'name', secret: true}, {t: 'objectionMiddle'},
|
|
41
|
+
{t: 'birthdate', secret: true}, {t: 'objectionTail'}, {t: 'caseNumber'},
|
|
42
|
+
{t: 'objectionEnd'},
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
key: 'report',
|
|
47
|
+
tokens: [
|
|
48
|
+
{t: 'reportIntro'}, {t: 'department'}, {t: 'reportMiddle'},
|
|
49
|
+
{t: 'phone', secret: true}, {t: 'reportTail'}, {t: 'name', secret: true},
|
|
50
|
+
{t: 'reportEnd'}, {t: 'policy'},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
export const DEFAULTS = {
|
|
56
|
+
lives: 3,
|
|
57
|
+
/* Reading a document takes longer than judging a card, and the whole
|
|
58
|
+
game is reading. The floor is where a fast reader still finishes
|
|
59
|
+
the page. */
|
|
60
|
+
clockStartMs: 14000,
|
|
61
|
+
clockFloorMs: 6000,
|
|
62
|
+
rampPerPoint: 45,
|
|
63
|
+
pointsPerSecret: 10,
|
|
64
|
+
pointsPerClean: 15,
|
|
65
|
+
overRedactionPenalty: 5,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
function mulberry32(seed) {
|
|
69
|
+
let a = seed >>> 0;
|
|
70
|
+
return function random() {
|
|
71
|
+
a |= 0; a = (a + 0x6D2B79F5) | 0;
|
|
72
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
73
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
74
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** How long this document gives you, at the current score. */
|
|
79
|
+
export function clockMs(state) {
|
|
80
|
+
return Math.max(state.cfg.clockFloorMs, state.cfg.clockStartMs - state.score * state.cfg.rampPerPoint);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function deal(state, now) {
|
|
84
|
+
const doc = DOCUMENTS[Math.floor(state.random() * DOCUMENTS.length)];
|
|
85
|
+
return {
|
|
86
|
+
...state,
|
|
87
|
+
doc: {
|
|
88
|
+
key: doc.key,
|
|
89
|
+
tokens: doc.tokens.map((token) => ({...token, blacked: false})),
|
|
90
|
+
startedAt: now,
|
|
91
|
+
expiresAt: now + clockMs(state),
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function createGame({seed = Date.now(), now = 0, config = {}} = {}) {
|
|
97
|
+
const cfg = {...DEFAULTS, ...config};
|
|
98
|
+
const base = {
|
|
99
|
+
cfg,
|
|
100
|
+
random: mulberry32(seed),
|
|
101
|
+
doc: null,
|
|
102
|
+
score: 0,
|
|
103
|
+
lives: cfg.lives,
|
|
104
|
+
published: 0,
|
|
105
|
+
clean: 0,
|
|
106
|
+
breaches: 0,
|
|
107
|
+
overRedacted: 0,
|
|
108
|
+
last: null,
|
|
109
|
+
over: false,
|
|
110
|
+
};
|
|
111
|
+
return deal(base, now);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function remaining(state, now) {
|
|
115
|
+
if (!state.doc) return 0;
|
|
116
|
+
const total = state.doc.expiresAt - state.doc.startedAt;
|
|
117
|
+
if (total <= 0) return 0;
|
|
118
|
+
return Math.min(1, Math.max(0, (state.doc.expiresAt - now) / total));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Black out one token. Blacking out the same one twice is a no-op. */
|
|
122
|
+
export function black(state, index) {
|
|
123
|
+
if (state.over || !state.doc) return state;
|
|
124
|
+
const token = state.doc.tokens[index];
|
|
125
|
+
if (!token || token.blacked) return state;
|
|
126
|
+
const tokens = [...state.doc.tokens];
|
|
127
|
+
tokens[index] = {...token, blacked: true};
|
|
128
|
+
return {...state, doc: {...state.doc, tokens}};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function loseLife(state, reason, now) {
|
|
132
|
+
const lives = state.lives - 1;
|
|
133
|
+
return {...state, lives, last: {result: reason, at: now}, over: lives <= 0};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Publish what is on the screen.
|
|
138
|
+
*
|
|
139
|
+
* Everything secret blacked out is a clean publication and pays a
|
|
140
|
+
* bonus. Anything secret left visible is a breach: a life, whatever
|
|
141
|
+
* else was done right. Blacking out ordinary text costs points per
|
|
142
|
+
* word, because a page of stripes is not a published document either.
|
|
143
|
+
*/
|
|
144
|
+
export function publish(state, now) {
|
|
145
|
+
if (state.over || !state.doc) return state;
|
|
146
|
+
|
|
147
|
+
const missed = state.doc.tokens.filter((t) => t.secret && !t.blacked).length;
|
|
148
|
+
const over = state.doc.tokens.filter((t) => !t.secret && t.blacked).length;
|
|
149
|
+
const caught = state.doc.tokens.filter((t) => t.secret && t.blacked).length;
|
|
150
|
+
|
|
151
|
+
if (missed > 0) {
|
|
152
|
+
const hit = {...loseLife(state, 'breach', now), breaches: state.breaches + 1, missed};
|
|
153
|
+
return hit.over ? {...hit, doc: null} : deal({...hit, doc: null}, now);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const gained = caught * state.cfg.pointsPerSecret
|
|
157
|
+
+ state.cfg.pointsPerClean
|
|
158
|
+
- over * state.cfg.overRedactionPenalty;
|
|
159
|
+
|
|
160
|
+
const next = {
|
|
161
|
+
...state,
|
|
162
|
+
/* A document can score badly, but publishing correctly never costs
|
|
163
|
+
points overall: the floor is zero for the page. */
|
|
164
|
+
score: state.score + Math.max(0, gained),
|
|
165
|
+
published: state.published + 1,
|
|
166
|
+
clean: over === 0 ? state.clean + 1 : state.clean,
|
|
167
|
+
overRedacted: state.overRedacted + over,
|
|
168
|
+
last: {result: over === 0 ? 'clean' : 'overRedacted', over, points: Math.max(0, gained), at: now},
|
|
169
|
+
};
|
|
170
|
+
return deal(next, now);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Let the clock run out: the document publishes itself, as it would. */
|
|
174
|
+
export function step(state, now) {
|
|
175
|
+
if (state.over || !state.doc) return state;
|
|
176
|
+
if (now < state.doc.expiresAt) return state;
|
|
177
|
+
return publish(state, now);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** The line that goes on the game-over card and into the post. */
|
|
181
|
+
export function summarise(state, locale = 'en') {
|
|
182
|
+
const n = (v) => Number(v || 0).toLocaleString(locale);
|
|
183
|
+
return locale === 'nl'
|
|
184
|
+
? `${n(state.published)} documenten uit · ${n(state.clean)} schoon`
|
|
185
|
+
: `${n(state.published)} documents out · ${n(state.clean)} clean`;
|
|
186
|
+
}
|
package/src/components/index.js
CHANGED
|
@@ -65,6 +65,8 @@ export {default as DeadlineDefender} from './DeadlineDefender/DeadlineDefender.j
|
|
|
65
65
|
export {default as BlueprintRush} from './BlueprintRush/BlueprintRush.jsx';
|
|
66
66
|
export {default as RecordRun} from './RecordRun/RecordRun.jsx';
|
|
67
67
|
export {default as LockPick} from './LockPick/LockPick.jsx';
|
|
68
|
+
export {default as PaintByTokens} from './PaintByTokens/PaintByTokens.jsx';
|
|
69
|
+
export {default as Redaction} from './Redaction/Redaction.jsx';
|
|
68
70
|
|
|
69
71
|
/* Diagram-set web-component React wrappers (cn-hex, cn-platform,
|
|
70
72
|
cn-domain-tree, cn-pipeline, cn-side-box, cn-honeycomb-bg, cn-pair,
|