@conduction/docusaurus-preset 3.41.1 → 3.43.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/package.json +1 -1
- package/src/components/DetailHero/DetailHero.jsx +6 -0
- package/src/components/HiddenGame/HiddenGame.jsx +190 -0
- package/src/components/HiddenGame/HiddenGame.module.css +42 -0
- package/src/components/HiddenGame/__tests__/matchers.test.js +117 -0
- package/src/components/HiddenGame/matchers.js +168 -0
- package/src/components/MonsterRun/MonsterRun.jsx +233 -0
- package/src/components/MonsterRun/MonsterRun.module.css +231 -0
- package/src/components/MonsterRun/__tests__/engine.test.js +218 -0
- package/src/components/MonsterRun/engine.js +194 -0
- package/src/components/index.js +2 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Monster run — the rules, with no DOM and no clock of its own.
|
|
3
|
+
*
|
|
4
|
+
* La Frankendesk's game, and the only one that belongs to a blog post
|
|
5
|
+
* rather than an app. The monster the post assembles goes for a run:
|
|
6
|
+
* jump the forks, duck the patch sets, and pick up the parts it was
|
|
7
|
+
* stitched together from.
|
|
8
|
+
*
|
|
9
|
+
* The joke is the obstacles. A fork is what a maintenance bill looks
|
|
10
|
+
* like, a patch set is what you carry forever, and a release train
|
|
11
|
+
* arrives whether you are ready or not. The things worth collecting
|
|
12
|
+
* are the ones the post argues are already shared: a token, a
|
|
13
|
+
* protocol, a component.
|
|
14
|
+
*
|
|
15
|
+
* Two inputs, jump and duck, which is what makes it a runner rather
|
|
16
|
+
* than a fourth game about picking the right answer. Time and
|
|
17
|
+
* randomness are injected; the component owns the clock.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/* Where a thing sits, which decides what gets you past it. */
|
|
21
|
+
export const LOW = 'low'; // on the ground: jump it
|
|
22
|
+
export const HIGH = 'high'; // overhead: duck under it
|
|
23
|
+
|
|
24
|
+
export const OBSTACLES = [
|
|
25
|
+
{key: 'fork', lane: LOW},
|
|
26
|
+
{key: 'patchset', lane: HIGH},
|
|
27
|
+
{key: 'releaseTrain', lane: LOW},
|
|
28
|
+
{key: 'designLanguage', lane: HIGH},
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
export const PARTS = ['token', 'protocol', 'component'];
|
|
32
|
+
|
|
33
|
+
/* The track is a line of columns. The monster stands at column 0 and
|
|
34
|
+
the world walks towards it. */
|
|
35
|
+
export const TRACK = 14;
|
|
36
|
+
|
|
37
|
+
export const DEFAULTS = {
|
|
38
|
+
lives: 3,
|
|
39
|
+
stepStartMs: 300,
|
|
40
|
+
stepFloorMs: 120,
|
|
41
|
+
rampPerPoint: 0.9,
|
|
42
|
+
/* Airborne and crouched both last a couple of steps, so timing is a
|
|
43
|
+
decision rather than a reflex: commit early and you land in it. */
|
|
44
|
+
jumpSteps: 2,
|
|
45
|
+
duckSteps: 2,
|
|
46
|
+
obstacleChance: 0.42,
|
|
47
|
+
partChance: 0.26,
|
|
48
|
+
/* Never two obstacles back to back: the monster cannot be in the air
|
|
49
|
+
and crouched at once, and a run that kills you whatever you do is
|
|
50
|
+
the fastest way to make a player stop. */
|
|
51
|
+
minGap: 2,
|
|
52
|
+
/* The first stretch is always empty. The track is filled before the
|
|
53
|
+
run starts so obstacles are visible before they arrive, but that
|
|
54
|
+
fill can put one at the monster's feet: the run then costs a life
|
|
55
|
+
in its first second, before the player has touched a key. */
|
|
56
|
+
leadIn: 5,
|
|
57
|
+
pointsPerStep: 1,
|
|
58
|
+
pointsPerPart: 6,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
function mulberry32(seed) {
|
|
62
|
+
let a = seed >>> 0;
|
|
63
|
+
return function random() {
|
|
64
|
+
a |= 0; a = (a + 0x6D2B79F5) | 0;
|
|
65
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
66
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
67
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** What the far end of the track should hold next. */
|
|
72
|
+
function spawn(state) {
|
|
73
|
+
if (state.sinceObstacle < state.cfg.minGap) {
|
|
74
|
+
/* Too soon for another obstacle, but a part is welcome. */
|
|
75
|
+
if (state.random() < state.cfg.partChance) {
|
|
76
|
+
return {kind: 'part', what: PARTS[Math.floor(state.random() * PARTS.length)], lane: state.random() < 0.5 ? LOW : HIGH};
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
if (state.random() < state.cfg.obstacleChance) {
|
|
81
|
+
const pick = OBSTACLES[Math.floor(state.random() * OBSTACLES.length)];
|
|
82
|
+
return {kind: 'obstacle', what: pick.key, lane: pick.lane};
|
|
83
|
+
}
|
|
84
|
+
if (state.random() < state.cfg.partChance) {
|
|
85
|
+
return {kind: 'part', what: PARTS[Math.floor(state.random() * PARTS.length)], lane: state.random() < 0.5 ? LOW : HIGH};
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function createGame({seed = Date.now(), now = 0, config = {}} = {}) {
|
|
91
|
+
const cfg = {...DEFAULTS, ...config};
|
|
92
|
+
let state = {
|
|
93
|
+
cfg,
|
|
94
|
+
random: mulberry32(seed),
|
|
95
|
+
track: Array.from({length: TRACK}, () => null),
|
|
96
|
+
posture: 'run', // run | jump | duck
|
|
97
|
+
postureFor: 0, // steps left in the current posture
|
|
98
|
+
lives: cfg.lives,
|
|
99
|
+
score: 0,
|
|
100
|
+
distance: 0,
|
|
101
|
+
collected: 0,
|
|
102
|
+
hits: 0,
|
|
103
|
+
sinceObstacle: cfg.minGap,
|
|
104
|
+
nextStepAt: now + cfg.stepStartMs,
|
|
105
|
+
last: null,
|
|
106
|
+
over: false,
|
|
107
|
+
};
|
|
108
|
+
/* Fill the track so the first obstacles are visible before they
|
|
109
|
+
arrive, rather than appearing under the monster's feet. */
|
|
110
|
+
for (let i = 0; i < TRACK; i++) {
|
|
111
|
+
const cell = spawn(state);
|
|
112
|
+
state = {
|
|
113
|
+
...state,
|
|
114
|
+
track: [...state.track.slice(1), cell],
|
|
115
|
+
sinceObstacle: cell && cell.kind === 'obstacle' ? 0 : state.sinceObstacle + 1,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/* Then clear the run-up, so the first thing to answer is one the
|
|
119
|
+
player watched coming. */
|
|
120
|
+
const track = [...state.track];
|
|
121
|
+
for (let i = 0; i < Math.min(cfg.leadIn, track.length); i++) track[i] = null;
|
|
122
|
+
return {...state, track};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function stepMs(state) {
|
|
126
|
+
return Math.max(state.cfg.stepFloorMs, state.cfg.stepStartMs - state.score * state.cfg.rampPerPoint);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Jump, if the monster has its feet on the ground. */
|
|
130
|
+
export function jump(state) {
|
|
131
|
+
if (state.over || state.posture !== 'run') return state;
|
|
132
|
+
return {...state, posture: 'jump', postureFor: state.cfg.jumpSteps};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Duck, same rule: no changing your mind mid-air. */
|
|
136
|
+
export function duck(state) {
|
|
137
|
+
if (state.over || state.posture !== 'run') return state;
|
|
138
|
+
return {...state, posture: 'duck', postureFor: state.cfg.duckSteps};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Does the current posture get the monster past this cell? */
|
|
142
|
+
export function clears(posture, cell) {
|
|
143
|
+
if (!cell || cell.kind !== 'obstacle') return true;
|
|
144
|
+
if (cell.lane === LOW) return posture === 'jump';
|
|
145
|
+
return posture === 'duck';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Advance one step: the world moves one column closer, the monster
|
|
150
|
+
* meets whatever arrives at its own column, and its posture wears off.
|
|
151
|
+
*/
|
|
152
|
+
export function step(state, now) {
|
|
153
|
+
if (state.over || now < state.nextStepAt) return state;
|
|
154
|
+
|
|
155
|
+
const arriving = state.track[0];
|
|
156
|
+
let next = {...state, distance: state.distance + 1};
|
|
157
|
+
|
|
158
|
+
if (arriving && arriving.kind === 'obstacle' && !clears(state.posture, arriving)) {
|
|
159
|
+
const lives = next.lives - 1;
|
|
160
|
+
next = {...next, lives, hits: next.hits + 1, last: {result: 'hit', what: arriving.what, at: now}, over: lives <= 0};
|
|
161
|
+
} else if (arriving && arriving.kind === 'part') {
|
|
162
|
+
/* A part is taken by running into it, whatever the posture: the
|
|
163
|
+
monster is made of these, it does not have to reach. */
|
|
164
|
+
next = {
|
|
165
|
+
...next,
|
|
166
|
+
score: next.score + next.cfg.pointsPerPart,
|
|
167
|
+
collected: next.collected + 1,
|
|
168
|
+
last: {result: 'part', what: arriving.what, at: now},
|
|
169
|
+
};
|
|
170
|
+
} else {
|
|
171
|
+
next = {...next, score: next.score + next.cfg.pointsPerStep, last: {result: 'clear', at: now}};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (next.over) return next;
|
|
175
|
+
|
|
176
|
+
const postureFor = Math.max(0, next.postureFor - 1);
|
|
177
|
+
const cell = spawn(next);
|
|
178
|
+
return {
|
|
179
|
+
...next,
|
|
180
|
+
posture: postureFor === 0 ? 'run' : next.posture,
|
|
181
|
+
postureFor,
|
|
182
|
+
track: [...next.track.slice(1), cell],
|
|
183
|
+
sinceObstacle: cell && cell.kind === 'obstacle' ? 0 : next.sinceObstacle + 1,
|
|
184
|
+
nextStepAt: now + stepMs(next),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** The line that goes on the game-over card and into the post. */
|
|
189
|
+
export function summarise(state, locale = 'en') {
|
|
190
|
+
const n = (v) => Number(v || 0).toLocaleString(locale);
|
|
191
|
+
return locale === 'nl'
|
|
192
|
+
? `${n(state.distance)} stappen · ${n(state.collected)} onderdelen`
|
|
193
|
+
: `${n(state.distance)} strides · ${n(state.collected)} parts`;
|
|
194
|
+
}
|
package/src/components/index.js
CHANGED
|
@@ -64,6 +64,8 @@ export {default as StampRush} from './StampRush/StampRush.jsx';
|
|
|
64
64
|
export {default as DeadlineDefender} from './DeadlineDefender/DeadlineDefender.jsx';
|
|
65
65
|
export {default as BlueprintRush} from './BlueprintRush/BlueprintRush.jsx';
|
|
66
66
|
export {default as RecordRun} from './RecordRun/RecordRun.jsx';
|
|
67
|
+
export {default as HiddenGame} from './HiddenGame/HiddenGame.jsx';
|
|
68
|
+
export {default as MonsterRun} from './MonsterRun/MonsterRun.jsx';
|
|
67
69
|
export {default as LockPick} from './LockPick/LockPick.jsx';
|
|
68
70
|
export {default as PaintByTokens} from './PaintByTokens/PaintByTokens.jsx';
|
|
69
71
|
export {default as Redaction} from './Redaction/Redaction.jsx';
|