@conduction/docusaurus-preset 3.42.0 → 3.44.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/DiceDuel/DiceDuel.jsx +190 -0
- package/src/components/DiceDuel/DiceDuel.module.css +191 -0
- package/src/components/DiceDuel/__tests__/engine.test.js +159 -0
- package/src/components/DiceDuel/engine.js +182 -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/PipeFit/PipeFit.jsx +221 -0
- package/src/components/PipeFit/PipeFit.module.css +203 -0
- package/src/components/PipeFit/__tests__/engine.test.js +176 -0
- package/src/components/PipeFit/engine.js +178 -0
- package/src/components/Reconcile/Reconcile.jsx +240 -0
- package/src/components/Reconcile/Reconcile.module.css +217 -0
- package/src/components/Reconcile/__tests__/engine.test.js +168 -0
- package/src/components/Reconcile/engine.js +228 -0
- package/src/components/index.js +4 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reconcile — the rules, with no DOM and no clock of its own.
|
|
3
|
+
*
|
|
4
|
+
* Shillinq's game. The bank statement is in and the invoices are open.
|
|
5
|
+
* Match each payment to the invoice it settles, before the month
|
|
6
|
+
* closes. One of the lines on the statement matches nothing at all,
|
|
7
|
+
* and sending that one to an invoice is how money goes missing
|
|
8
|
+
* quietly.
|
|
9
|
+
*
|
|
10
|
+
* So there are three moves, not two: match it, or flag it as the one
|
|
11
|
+
* that does not belong. Flagging a genuine payment is a mistake too,
|
|
12
|
+
* because a bookkeeper who cries wolf at every line is a bookkeeper
|
|
13
|
+
* nobody listens to.
|
|
14
|
+
*
|
|
15
|
+
* Time and randomness are injected; the component owns the clock.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/* Round amounts in whole euros: the game is about matching, not about
|
|
19
|
+
arithmetic with cents. */
|
|
20
|
+
const AMOUNTS = [120, 240, 360, 480, 750, 900, 1240, 1600, 2100, 3400];
|
|
21
|
+
const REFERENCES = ['2026-014', '2026-027', '2026-031', '2026-048', '2026-052', '2026-066'];
|
|
22
|
+
|
|
23
|
+
export const DEFAULTS = {
|
|
24
|
+
lives: 3,
|
|
25
|
+
invoices: 4,
|
|
26
|
+
clockStartMs: 24000,
|
|
27
|
+
clockFloorMs: 9000,
|
|
28
|
+
rampPerPoint: 90,
|
|
29
|
+
pointsPerMatch: 8,
|
|
30
|
+
pointsPerFraud: 20,
|
|
31
|
+
pointsPerSheet: 12,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function mulberry32(seed) {
|
|
35
|
+
let a = seed >>> 0;
|
|
36
|
+
return function random() {
|
|
37
|
+
a |= 0; a = (a + 0x6D2B79F5) | 0;
|
|
38
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
39
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
40
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function shuffled(list, random) {
|
|
45
|
+
const out = [...list];
|
|
46
|
+
for (let i = out.length - 1; i > 0; i--) {
|
|
47
|
+
const j = Math.floor(random() * (i + 1));
|
|
48
|
+
[out[i], out[j]] = [out[j], out[i]];
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function clockMs(state) {
|
|
54
|
+
return Math.max(state.cfg.clockFloorMs, state.cfg.clockStartMs - state.score * state.cfg.rampPerPoint);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Deal a sheet: N invoices, N payments that settle them, and one more
|
|
59
|
+
* payment that settles nothing.
|
|
60
|
+
*
|
|
61
|
+
* The odd one out is built from an amount no invoice carries, because
|
|
62
|
+
* a fraud line that happens to equal a real invoice would make the
|
|
63
|
+
* right answer unknowable.
|
|
64
|
+
*/
|
|
65
|
+
function deal(state, now) {
|
|
66
|
+
const amounts = shuffled(AMOUNTS, state.random).slice(0, state.cfg.invoices);
|
|
67
|
+
const references = shuffled(REFERENCES, state.random);
|
|
68
|
+
|
|
69
|
+
const invoices = amounts.map((amount, i) => ({
|
|
70
|
+
id: `inv-${i}`,
|
|
71
|
+
reference: references[i % references.length],
|
|
72
|
+
amount,
|
|
73
|
+
settled: false,
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
const payments = invoices.map((invoice, i) => ({
|
|
77
|
+
id: `pay-${i}`,
|
|
78
|
+
amount: invoice.amount,
|
|
79
|
+
reference: invoice.reference,
|
|
80
|
+
fraud: false,
|
|
81
|
+
done: false,
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
const spare = AMOUNTS.filter((a) => !amounts.includes(a));
|
|
85
|
+
payments.push({
|
|
86
|
+
id: 'pay-odd',
|
|
87
|
+
amount: spare[Math.floor(state.random() * spare.length)],
|
|
88
|
+
/* A reference nobody issued: close enough to look filed, wrong
|
|
89
|
+
enough to be findable. */
|
|
90
|
+
reference: `2026-${900 + Math.floor(state.random() * 90)}`,
|
|
91
|
+
fraud: true,
|
|
92
|
+
done: false,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
...state,
|
|
97
|
+
sheet: {
|
|
98
|
+
invoices,
|
|
99
|
+
payments: shuffled(payments, state.random),
|
|
100
|
+
startedAt: now,
|
|
101
|
+
expiresAt: now + clockMs(state),
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function createGame({seed = Date.now(), now = 0, config = {}} = {}) {
|
|
107
|
+
const cfg = {...DEFAULTS, ...config};
|
|
108
|
+
const base = {
|
|
109
|
+
cfg,
|
|
110
|
+
random: mulberry32(seed),
|
|
111
|
+
sheet: null,
|
|
112
|
+
score: 0,
|
|
113
|
+
lives: cfg.lives,
|
|
114
|
+
matched: 0,
|
|
115
|
+
caught: 0,
|
|
116
|
+
mistakes: 0,
|
|
117
|
+
sheets: 0,
|
|
118
|
+
last: null,
|
|
119
|
+
over: false,
|
|
120
|
+
};
|
|
121
|
+
return deal(base, now);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function remaining(state, now) {
|
|
125
|
+
if (!state.sheet) return 0;
|
|
126
|
+
const total = state.sheet.expiresAt - state.sheet.startedAt;
|
|
127
|
+
if (total <= 0) return 0;
|
|
128
|
+
return Math.min(1, Math.max(0, (state.sheet.expiresAt - now) / total));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Everything on this sheet handled? */
|
|
132
|
+
export function sheetDone(state) {
|
|
133
|
+
return Boolean(state.sheet) && state.sheet.payments.every((p) => p.done);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function loseLife(state, reason, now) {
|
|
137
|
+
const lives = state.lives - 1;
|
|
138
|
+
return {
|
|
139
|
+
...state,
|
|
140
|
+
lives,
|
|
141
|
+
mistakes: state.mistakes + 1,
|
|
142
|
+
last: {result: reason, at: now},
|
|
143
|
+
over: lives <= 0,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function advance(state, now) {
|
|
148
|
+
if (!sheetDone(state)) return state;
|
|
149
|
+
const cleared = {
|
|
150
|
+
...state,
|
|
151
|
+
score: state.score + state.cfg.pointsPerSheet,
|
|
152
|
+
sheets: state.sheets + 1,
|
|
153
|
+
last: {result: 'sheet', at: now},
|
|
154
|
+
};
|
|
155
|
+
return deal(cleared, now);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Send a payment to an invoice.
|
|
160
|
+
*
|
|
161
|
+
* Right pays, wrong costs a life, and the fraud line costs a life
|
|
162
|
+
* wherever it is sent: that is the mistake the app exists to prevent.
|
|
163
|
+
*/
|
|
164
|
+
export function match(state, paymentId, invoiceId, now) {
|
|
165
|
+
if (state.over || !state.sheet) return state;
|
|
166
|
+
const payment = state.sheet.payments.find((p) => p.id === paymentId);
|
|
167
|
+
const invoice = state.sheet.invoices.find((i) => i.id === invoiceId);
|
|
168
|
+
if (!payment || !invoice || payment.done || invoice.settled) return state;
|
|
169
|
+
|
|
170
|
+
if (payment.fraud || payment.amount !== invoice.amount) {
|
|
171
|
+
return loseLife(state, payment.fraud ? 'paidFraud' : 'mismatch', now);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const next = {
|
|
175
|
+
...state,
|
|
176
|
+
sheet: {
|
|
177
|
+
...state.sheet,
|
|
178
|
+
payments: state.sheet.payments.map((p) => (p.id === paymentId ? {...p, done: true} : p)),
|
|
179
|
+
invoices: state.sheet.invoices.map((i) => (i.id === invoiceId ? {...i, settled: true} : i)),
|
|
180
|
+
},
|
|
181
|
+
score: state.score + state.cfg.pointsPerMatch,
|
|
182
|
+
matched: state.matched + 1,
|
|
183
|
+
last: {result: 'matched', at: now},
|
|
184
|
+
};
|
|
185
|
+
return advance(next, now);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Flag a payment as the one that belongs to nobody.
|
|
190
|
+
*
|
|
191
|
+
* Crying wolf at a genuine payment costs a life too. A game that only
|
|
192
|
+
* punished missing the fraud would teach you to flag everything.
|
|
193
|
+
*/
|
|
194
|
+
export function flag(state, paymentId, now) {
|
|
195
|
+
if (state.over || !state.sheet) return state;
|
|
196
|
+
const payment = state.sheet.payments.find((p) => p.id === paymentId);
|
|
197
|
+
if (!payment || payment.done) return state;
|
|
198
|
+
|
|
199
|
+
if (!payment.fraud) return loseLife(state, 'flaggedGood', now);
|
|
200
|
+
|
|
201
|
+
const next = {
|
|
202
|
+
...state,
|
|
203
|
+
sheet: {
|
|
204
|
+
...state.sheet,
|
|
205
|
+
payments: state.sheet.payments.map((p) => (p.id === paymentId ? {...p, done: true} : p)),
|
|
206
|
+
},
|
|
207
|
+
score: state.score + state.cfg.pointsPerFraud,
|
|
208
|
+
caught: state.caught + 1,
|
|
209
|
+
last: {result: 'caught', at: now},
|
|
210
|
+
};
|
|
211
|
+
return advance(next, now);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** The month closes on its own, and an unfinished sheet costs a life. */
|
|
215
|
+
export function step(state, now) {
|
|
216
|
+
if (state.over || !state.sheet) return state;
|
|
217
|
+
if (now < state.sheet.expiresAt) return state;
|
|
218
|
+
const hit = loseLife(state, 'monthClosed', now);
|
|
219
|
+
return hit.over ? {...hit, sheet: null} : deal(hit, now);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** The line that goes on the game-over card and into the post. */
|
|
223
|
+
export function summarise(state, locale = 'en') {
|
|
224
|
+
const n = (v) => Number(v || 0).toLocaleString(locale);
|
|
225
|
+
return locale === 'nl'
|
|
226
|
+
? `${n(state.matched)} regels gematcht · ${n(state.caught)} keer fraude gevonden`
|
|
227
|
+
: `${n(state.matched)} lines matched · ${n(state.caught)} caught out`;
|
|
228
|
+
}
|
package/src/components/index.js
CHANGED
|
@@ -65,6 +65,10 @@ 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 HiddenGame} from './HiddenGame/HiddenGame.jsx';
|
|
68
|
+
export {default as MonsterRun} from './MonsterRun/MonsterRun.jsx';
|
|
69
|
+
export {default as DiceDuel} from './DiceDuel/DiceDuel.jsx';
|
|
70
|
+
export {default as Reconcile} from './Reconcile/Reconcile.jsx';
|
|
71
|
+
export {default as PipeFit} from './PipeFit/PipeFit.jsx';
|
|
68
72
|
export {default as LockPick} from './LockPick/LockPick.jsx';
|
|
69
73
|
export {default as PaintByTokens} from './PaintByTokens/PaintByTokens.jsx';
|
|
70
74
|
export {default as Redaction} from './Redaction/Redaction.jsx';
|