bonk-challanges 0.1.0 → 0.2.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/README.md +50 -0
- package/index.js +122 -23
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# bonk-challanges
|
|
2
|
+
|
|
3
|
+
Reusable React Native challenge components for Bonk wake-up flows.
|
|
4
|
+
|
|
5
|
+
## Exports
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
const { BonkChallengeRunner } = require('bonk-challanges');
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`BonkChallengeRunner` renders one challenge at a time and calls `onSolved(stats)` when the current challenge is complete.
|
|
12
|
+
|
|
13
|
+
## Challenge Types
|
|
14
|
+
|
|
15
|
+
### Math
|
|
16
|
+
|
|
17
|
+
```jsx
|
|
18
|
+
<BonkChallengeRunner type="math" difficulty="advanced" colors={colors} onSolved={onSolved} />
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Difficulties:
|
|
22
|
+
|
|
23
|
+
- `easy`
|
|
24
|
+
- `intermediate`
|
|
25
|
+
- `advanced`
|
|
26
|
+
|
|
27
|
+
Stats include `type`, `attempts`, `durationMs`, and `difficulty`.
|
|
28
|
+
|
|
29
|
+
### Tile Puzzle
|
|
30
|
+
|
|
31
|
+
```jsx
|
|
32
|
+
<BonkChallengeRunner type="tile" tileCount={8} colors={colors} onSolved={onSolved} />
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Tile sizes:
|
|
36
|
+
|
|
37
|
+
- `8` = 3x3 puzzle with 8 numbered tiles and one blank space.
|
|
38
|
+
- `15` = 4x4 puzzle with 15 numbered tiles and one blank space.
|
|
39
|
+
|
|
40
|
+
The old 2x2 puzzle has been removed. Stats include `type`, `moves`, `durationMs`, and `tileCount`.
|
|
41
|
+
|
|
42
|
+
## Packaging
|
|
43
|
+
|
|
44
|
+
This package is consumed by the mobile app as a local packed tarball. After editing:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm pack
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Then update the mobile app dependency/lockfile or reinstall so `mobile/node_modules/bonk-challanges` receives the new package contents.
|
package/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const React = require('react');
|
|
2
|
-
const { PanResponder, Pressable, StyleSheet, Text, View } = require('react-native');
|
|
2
|
+
const { PanResponder, Pressable, StyleSheet, Text, TextInput, View } = require('react-native');
|
|
3
3
|
|
|
4
4
|
function randomInt(min, max) {
|
|
5
5
|
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
@@ -7,25 +7,95 @@ function randomInt(min, max) {
|
|
|
7
7
|
|
|
8
8
|
function createMathProblem(difficulty = 'intermediate') {
|
|
9
9
|
if (difficulty === 'easy') {
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
return { text: `${a}
|
|
10
|
+
const a = randomInt(-12, 18);
|
|
11
|
+
const b = randomInt(-10, 14);
|
|
12
|
+
const op = ['+', '-', 'x'][randomInt(0, 2)];
|
|
13
|
+
if (op === '+') return { text: `${a} + ${b} = ?`, answer: String(a + b), label: 'Answer' };
|
|
14
|
+
if (op === '-') return { text: `${a} - ${b} = ?`, answer: String(a - b), label: 'Answer' };
|
|
15
|
+
return { text: `${a} x ${b} = ?`, answer: String(a * b), label: 'Answer' };
|
|
14
16
|
}
|
|
15
17
|
if (difficulty === 'advanced') {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
const kind = randomInt(0, 2);
|
|
19
|
+
if (kind === 0) {
|
|
20
|
+
const x = randomInt(-9, 9);
|
|
21
|
+
const a = randomInt(2, 8);
|
|
22
|
+
const b = randomInt(-18, 18);
|
|
23
|
+
return { text: `${a}x^2 ${b >= 0 ? '+' : '-'} ${Math.abs(b)} = ${a * x * x + b}`, answer: String(Math.abs(x)), label: '|x|' };
|
|
24
|
+
}
|
|
25
|
+
if (kind === 1) {
|
|
26
|
+
const x = randomInt(2, 12);
|
|
27
|
+
const a = randomInt(2, 5);
|
|
28
|
+
return { text: `sqrt(${a * x * x}) / sqrt(${a}) = ?`, answer: String(x), label: 'Answer' };
|
|
29
|
+
}
|
|
30
|
+
const x = randomInt(-6, 6);
|
|
31
|
+
const a = randomInt(2, 5);
|
|
32
|
+
const b = randomInt(2, 4);
|
|
33
|
+
return { text: `${a}^${b} + x = ${a ** b + x}`, answer: String(x), label: 'x' };
|
|
20
34
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
35
|
+
if (difficulty === 'professional') {
|
|
36
|
+
const ar = randomInt(-7, 7);
|
|
37
|
+
const ai = randomInt(-7, 7);
|
|
38
|
+
const br = randomInt(-7, 7);
|
|
39
|
+
const bi = randomInt(-7, 7);
|
|
40
|
+
const kind = randomInt(0, 2);
|
|
41
|
+
if (kind === 0) {
|
|
42
|
+
return {
|
|
43
|
+
text: `(${formatComplex(ar, ai)}) + (${formatComplex(br, bi)}) = ?`,
|
|
44
|
+
answer: formatComplex(ar + br, ai + bi),
|
|
45
|
+
label: 'Answer',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (kind === 1) {
|
|
49
|
+
return {
|
|
50
|
+
text: `(${formatComplex(ar, ai)}) - (${formatComplex(br, bi)}) = ?`,
|
|
51
|
+
answer: formatComplex(ar - br, ai - bi),
|
|
52
|
+
label: 'Answer',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
text: `i x (${formatComplex(ar, ai)}) = ?`,
|
|
57
|
+
answer: formatComplex(-ai, ar),
|
|
58
|
+
label: 'Answer',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const x = randomInt(-12, 12);
|
|
62
|
+
const a = randomInt(2, 10);
|
|
63
|
+
const b = randomInt(-24, 24);
|
|
64
|
+
const kind = randomInt(0, 3);
|
|
65
|
+
if (kind === 0) return { text: `${a}x ${b >= 0 ? '+' : '-'} ${Math.abs(b)} = ${a * x + b}`, answer: String(x), label: 'x' };
|
|
66
|
+
if (kind === 1) return { text: `${a}(x ${b >= 0 ? '+' : '-'} ${Math.abs(b)}) = ${a * (x + b)}`, answer: String(x), label: 'x' };
|
|
67
|
+
if (kind === 2) {
|
|
68
|
+
const quotient = randomInt(-12, 12);
|
|
69
|
+
const integerX = quotient * a;
|
|
70
|
+
return { text: `(x / ${a}) ${b >= 0 ? '+' : '-'} ${Math.abs(b)} = ${quotient + b}`, answer: String(integerX), label: 'x' };
|
|
71
|
+
}
|
|
72
|
+
return { text: `${a}x - ${b} = ${a * x - b}`, answer: String(x), label: 'x' };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function formatComplex(real, imag) {
|
|
76
|
+
if (imag === 0) return String(real);
|
|
77
|
+
if (real === 0) {
|
|
78
|
+
if (imag === 1) return 'i';
|
|
79
|
+
if (imag === -1) return '-i';
|
|
80
|
+
return `${imag}i`;
|
|
81
|
+
}
|
|
82
|
+
const sign = imag >= 0 ? '+' : '-';
|
|
83
|
+
const mag = Math.abs(imag);
|
|
84
|
+
return `${real}${sign}${mag === 1 ? '' : mag}i`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function normalizeAnswer(value) {
|
|
88
|
+
return String(value)
|
|
89
|
+
.trim()
|
|
90
|
+
.toLowerCase()
|
|
91
|
+
.replace(/\s+/g, '')
|
|
92
|
+
.replace(/−/g, '-')
|
|
93
|
+
.replace(/\*/g, '')
|
|
94
|
+
.replace(/\+-/g, '-');
|
|
25
95
|
}
|
|
26
96
|
|
|
27
97
|
function boardSizeFromTileCount(tileCount) {
|
|
28
|
-
return tileCount ===
|
|
98
|
+
return tileCount === 15 ? 4 : 3;
|
|
29
99
|
}
|
|
30
100
|
|
|
31
101
|
function createSolvedBoard(tileCount) {
|
|
@@ -112,7 +182,7 @@ function useMathChallenge(difficulty) {
|
|
|
112
182
|
(value, onSolved) => {
|
|
113
183
|
const nextAttempts = attempts + 1;
|
|
114
184
|
setAttempts(nextAttempts);
|
|
115
|
-
if (
|
|
185
|
+
if (normalizeAnswer(value) === normalizeAnswer(problem.answer)) {
|
|
116
186
|
setMessage('Correct');
|
|
117
187
|
onSolved({
|
|
118
188
|
type: 'math',
|
|
@@ -135,13 +205,28 @@ function MathChallenge({ colors, stylesConfig, onSolved, difficulty = 'intermedi
|
|
|
135
205
|
const [input, setInput] = React.useState('');
|
|
136
206
|
const { problem, message, submit } = useMathChallenge(difficulty);
|
|
137
207
|
|
|
138
|
-
|
|
208
|
+
React.useEffect(() => {
|
|
209
|
+
setInput('');
|
|
210
|
+
}, [problem.text]);
|
|
211
|
+
|
|
212
|
+
const pad = ['1', '2', '3', '-', '4', '5', '6', '+', '7', '8', '9', 'i', 'C', '0', 'OK'];
|
|
139
213
|
|
|
140
214
|
return React.createElement(
|
|
141
215
|
View,
|
|
142
216
|
{ style: [styles.block, stylesConfig?.block] },
|
|
143
|
-
React.createElement(Text, { style: [styles.prompt, { color: colors.text }, stylesConfig?.prompt] },
|
|
144
|
-
React.createElement(
|
|
217
|
+
React.createElement(Text, { style: [styles.prompt, { color: colors.text }, stylesConfig?.prompt] }, problem.text),
|
|
218
|
+
React.createElement(
|
|
219
|
+
TextInput,
|
|
220
|
+
{
|
|
221
|
+
value: input,
|
|
222
|
+
onChangeText: setInput,
|
|
223
|
+
placeholder: problem.label || 'Answer',
|
|
224
|
+
placeholderTextColor: colors.subtleText,
|
|
225
|
+
autoCapitalize: 'none',
|
|
226
|
+
autoCorrect: false,
|
|
227
|
+
style: [styles.answerInput, { color: colors.text, borderColor: colors.border, backgroundColor: colors.cardMuted }, stylesConfig?.answer],
|
|
228
|
+
}
|
|
229
|
+
),
|
|
145
230
|
message
|
|
146
231
|
? React.createElement(Text, { style: [styles.feedback, { color: colors.subtleText }, stylesConfig?.feedback] }, message)
|
|
147
232
|
: null,
|
|
@@ -162,7 +247,7 @@ function MathChallenge({ colors, stylesConfig, onSolved, difficulty = 'intermedi
|
|
|
162
247
|
submit(input, onSolved);
|
|
163
248
|
return;
|
|
164
249
|
}
|
|
165
|
-
if (input.length >=
|
|
250
|
+
if (input.length >= 12) return;
|
|
166
251
|
setInput((v) => `${v}${k}`);
|
|
167
252
|
},
|
|
168
253
|
style: ({ pressed }) => [
|
|
@@ -183,9 +268,9 @@ function MathChallenge({ colors, stylesConfig, onSolved, difficulty = 'intermedi
|
|
|
183
268
|
|
|
184
269
|
function TileChallenge({ colors, stylesConfig, onSolved, tileCount = 8 }) {
|
|
185
270
|
const size = boardSizeFromTileCount(tileCount);
|
|
186
|
-
const sizeLabel = size ===
|
|
187
|
-
const tileSide = tileCount ===
|
|
188
|
-
const [board, setBoard] = React.useState(() => shuffleBoard(tileCount, tileCount ===
|
|
271
|
+
const sizeLabel = size === 4 ? '4x4' : '3x3';
|
|
272
|
+
const tileSide = tileCount === 15 ? 62 : 80;
|
|
273
|
+
const [board, setBoard] = React.useState(() => shuffleBoard(tileCount, tileCount === 15 ? 160 : 80));
|
|
189
274
|
const [moves, setMoves] = React.useState(0);
|
|
190
275
|
const startedAtRef = React.useRef(Date.now());
|
|
191
276
|
|
|
@@ -256,7 +341,13 @@ function TileChallenge({ colors, stylesConfig, onSolved, tileCount = 8 }) {
|
|
|
256
341
|
stylesConfig?.tile,
|
|
257
342
|
],
|
|
258
343
|
},
|
|
259
|
-
value === 0
|
|
344
|
+
value === 0
|
|
345
|
+
? null
|
|
346
|
+
: React.createElement(
|
|
347
|
+
Text,
|
|
348
|
+
{ style: [styles.tileText, { color: colors.text, fontSize: tileCount === 15 ? 24 : 30 }, stylesConfig?.tileText] },
|
|
349
|
+
String(value)
|
|
350
|
+
)
|
|
260
351
|
);
|
|
261
352
|
})
|
|
262
353
|
)
|
|
@@ -282,6 +373,14 @@ const styles = StyleSheet.create({
|
|
|
282
373
|
fontSize: 15,
|
|
283
374
|
fontWeight: '600',
|
|
284
375
|
},
|
|
376
|
+
answerInput: {
|
|
377
|
+
minHeight: 46,
|
|
378
|
+
borderWidth: 1,
|
|
379
|
+
borderRadius: 10,
|
|
380
|
+
paddingHorizontal: 12,
|
|
381
|
+
fontSize: 18,
|
|
382
|
+
fontWeight: '700',
|
|
383
|
+
},
|
|
285
384
|
feedback: {
|
|
286
385
|
fontSize: 13,
|
|
287
386
|
fontWeight: '600',
|