bonk-challanges 0.1.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.
Files changed (2) hide show
  1. package/index.js +328 -0
  2. package/package.json +10 -0
package/index.js ADDED
@@ -0,0 +1,328 @@
1
+ const React = require('react');
2
+ const { PanResponder, Pressable, StyleSheet, Text, View } = require('react-native');
3
+
4
+ function randomInt(min, max) {
5
+ return Math.floor(Math.random() * (max - min + 1)) + min;
6
+ }
7
+
8
+ function createMathProblem(difficulty = 'intermediate') {
9
+ if (difficulty === 'easy') {
10
+ const x = randomInt(1, 9);
11
+ const a = randomInt(1, 4);
12
+ const b = randomInt(0, 10);
13
+ return { text: `${a}x + ${b} = ${a * x + b}`, answer: x };
14
+ }
15
+ if (difficulty === 'advanced') {
16
+ const x = randomInt(-12, 12);
17
+ const a = randomInt(3, 12);
18
+ const b = randomInt(-24, 24);
19
+ return { text: `${a}x ${b >= 0 ? '+' : '-'} ${Math.abs(b)} = ${a * x + b}`, answer: x };
20
+ }
21
+ const x = randomInt(2, 12);
22
+ const a = randomInt(2, 9);
23
+ const b = randomInt(1, 25);
24
+ return { text: `${a}x + ${b} = ${a * x + b}`, answer: x };
25
+ }
26
+
27
+ function boardSizeFromTileCount(tileCount) {
28
+ return tileCount === 3 ? 2 : 3;
29
+ }
30
+
31
+ function createSolvedBoard(tileCount) {
32
+ const out = [];
33
+ for (let i = 1; i <= tileCount; i += 1) out.push(i);
34
+ out.push(0);
35
+ return out;
36
+ }
37
+
38
+ function neighbors(index, size) {
39
+ const row = Math.floor(index / size);
40
+ const col = index % size;
41
+ const out = [];
42
+ if (row > 0) out.push(index - size);
43
+ if (row < size - 1) out.push(index + size);
44
+ if (col > 0) out.push(index - 1);
45
+ if (col < size - 1) out.push(index + 1);
46
+ return out;
47
+ }
48
+
49
+ function shuffleBoard(tileCount, steps = 80) {
50
+ const size = boardSizeFromTileCount(tileCount);
51
+ const board = createSolvedBoard(tileCount);
52
+ let emptyIndex = board.length - 1;
53
+ let prev = -1;
54
+ for (let i = 0; i < steps; i += 1) {
55
+ const options = neighbors(emptyIndex, size).filter((n) => n !== prev);
56
+ const target = options[randomInt(0, options.length - 1)];
57
+ board[emptyIndex] = board[target];
58
+ board[target] = 0;
59
+ prev = emptyIndex;
60
+ emptyIndex = target;
61
+ }
62
+ return board;
63
+ }
64
+
65
+ function isSolved(board, tileCount) {
66
+ for (let i = 0; i < tileCount; i += 1) {
67
+ if (board[i] !== i + 1) return false;
68
+ }
69
+ return board[tileCount] === 0;
70
+ }
71
+
72
+ function moveBoard(board, index, tileCount) {
73
+ const size = boardSizeFromTileCount(tileCount);
74
+ const emptyIndex = board.indexOf(0);
75
+ const canMove = board[index] !== 0 && neighbors(index, size).includes(emptyIndex);
76
+ if (!canMove) return null;
77
+ const next = [...board];
78
+ next[emptyIndex] = board[index];
79
+ next[index] = 0;
80
+ return next;
81
+ }
82
+
83
+ function swipeTargetFromGesture(index, dx, dy, tileCount) {
84
+ const size = boardSizeFromTileCount(tileCount);
85
+ const row = Math.floor(index / size);
86
+ const col = index % size;
87
+ const horizontal = Math.abs(dx) >= Math.abs(dy);
88
+ if (horizontal) {
89
+ if (dx > 0 && col < size - 1) return index + 1;
90
+ if (dx < 0 && col > 0) return index - 1;
91
+ return null;
92
+ }
93
+ if (dy > 0 && row < size - 1) return index + size;
94
+ if (dy < 0 && row > 0) return index - size;
95
+ return null;
96
+ }
97
+
98
+ function useMathChallenge(difficulty) {
99
+ const [problem, setProblem] = React.useState(() => createMathProblem(difficulty));
100
+ const [message, setMessage] = React.useState('');
101
+ const [attempts, setAttempts] = React.useState(0);
102
+ const startedAtRef = React.useRef(Date.now());
103
+
104
+ React.useEffect(() => {
105
+ setProblem(createMathProblem(difficulty));
106
+ setMessage('');
107
+ setAttempts(0);
108
+ startedAtRef.current = Date.now();
109
+ }, [difficulty]);
110
+
111
+ const submit = React.useCallback(
112
+ (value, onSolved) => {
113
+ const nextAttempts = attempts + 1;
114
+ setAttempts(nextAttempts);
115
+ if (Number(value) === problem.answer) {
116
+ setMessage('Correct');
117
+ onSolved({
118
+ type: 'math',
119
+ attempts: nextAttempts,
120
+ durationMs: Date.now() - startedAtRef.current,
121
+ difficulty,
122
+ });
123
+ return;
124
+ }
125
+ setMessage('Try again');
126
+ setProblem(createMathProblem(difficulty));
127
+ },
128
+ [attempts, difficulty, problem.answer]
129
+ );
130
+
131
+ return { problem, message, submit };
132
+ }
133
+
134
+ function MathChallenge({ colors, stylesConfig, onSolved, difficulty = 'intermediate' }) {
135
+ const [input, setInput] = React.useState('');
136
+ const { problem, message, submit } = useMathChallenge(difficulty);
137
+
138
+ const pad = ['1', '2', '3', '4', '5', '6', '7', '8', '9', 'C', '0', 'OK'];
139
+
140
+ return React.createElement(
141
+ View,
142
+ { style: [styles.block, stylesConfig?.block] },
143
+ React.createElement(Text, { style: [styles.prompt, { color: colors.text }, stylesConfig?.prompt] }, `Solve for x: ${problem.text}`),
144
+ React.createElement(Text, { style: [styles.answer, { color: colors.subtleText }, stylesConfig?.answer] }, input ? `x = ${input}` : 'Enter x'),
145
+ message
146
+ ? React.createElement(Text, { style: [styles.feedback, { color: colors.subtleText }, stylesConfig?.feedback] }, message)
147
+ : null,
148
+ React.createElement(
149
+ View,
150
+ { style: styles.padGrid },
151
+ pad.map((k) =>
152
+ React.createElement(
153
+ Pressable,
154
+ {
155
+ key: k,
156
+ onPress: () => {
157
+ if (k === 'C') {
158
+ setInput('');
159
+ return;
160
+ }
161
+ if (k === 'OK') {
162
+ submit(input, onSolved);
163
+ return;
164
+ }
165
+ if (input.length >= 3) return;
166
+ setInput((v) => `${v}${k}`);
167
+ },
168
+ style: ({ pressed }) => [
169
+ styles.key,
170
+ {
171
+ borderColor: colors.border,
172
+ backgroundColor: pressed ? colors.primaryPressed : colors.cardMuted,
173
+ },
174
+ stylesConfig?.key,
175
+ ],
176
+ },
177
+ React.createElement(Text, { style: [styles.keyText, { color: colors.text }, stylesConfig?.keyText] }, k)
178
+ )
179
+ )
180
+ )
181
+ );
182
+ }
183
+
184
+ function TileChallenge({ colors, stylesConfig, onSolved, tileCount = 8 }) {
185
+ const size = boardSizeFromTileCount(tileCount);
186
+ const sizeLabel = size === 2 ? '2x2' : '3x3';
187
+ const tileSide = tileCount === 3 ? 96 : 80;
188
+ const [board, setBoard] = React.useState(() => shuffleBoard(tileCount, tileCount === 3 ? 28 : 80));
189
+ const [moves, setMoves] = React.useState(0);
190
+ const startedAtRef = React.useRef(Date.now());
191
+
192
+ const applyMove = React.useCallback(
193
+ (fromIndex) => {
194
+ const next = moveBoard(board, fromIndex, tileCount);
195
+ if (!next) return;
196
+ const nextMoves = moves + 1;
197
+ setBoard(next);
198
+ setMoves(nextMoves);
199
+ if (isSolved(next, tileCount)) {
200
+ onSolved({
201
+ type: 'tile',
202
+ moves: nextMoves,
203
+ durationMs: Date.now() - startedAtRef.current,
204
+ tileCount,
205
+ });
206
+ }
207
+ },
208
+ [board, moves, onSolved, tileCount]
209
+ );
210
+
211
+ return React.createElement(
212
+ View,
213
+ { style: [styles.block, stylesConfig?.block] },
214
+ React.createElement(Text, { style: [styles.prompt, { color: colors.text }, stylesConfig?.prompt] }, `Slide tiles into order (${sizeLabel})`),
215
+ React.createElement(Text, { style: [styles.answer, { color: colors.subtleText }, stylesConfig?.answer] }, `Moves: ${moves}`),
216
+ React.createElement(
217
+ View,
218
+ {
219
+ style: [
220
+ styles.tileGrid,
221
+ {
222
+ width: size * tileSide + (size - 1) * 8,
223
+ },
224
+ ],
225
+ },
226
+ board.map((value, index) => {
227
+ const emptyIndex = board.indexOf(0);
228
+ const canMove = value !== 0 && neighbors(index, size).includes(emptyIndex);
229
+ const responder = PanResponder.create({
230
+ onStartShouldSetPanResponder: () => value !== 0,
231
+ onMoveShouldSetPanResponder: (_evt, g) => Math.abs(g.dx) > 8 || Math.abs(g.dy) > 8,
232
+ onPanResponderRelease: (_evt, g) => {
233
+ if (value === 0) return;
234
+ const target = swipeTargetFromGesture(index, g.dx, g.dy, tileCount);
235
+ if (target === null) return;
236
+ if (target === emptyIndex) applyMove(index);
237
+ },
238
+ });
239
+
240
+ return React.createElement(
241
+ Pressable,
242
+ {
243
+ key: `${index}-${value}`,
244
+ disabled: value === 0,
245
+ onPress: () => applyMove(index),
246
+ ...responder.panHandlers,
247
+ style: ({ pressed }) => [
248
+ styles.tile,
249
+ {
250
+ width: tileSide,
251
+ height: tileSide,
252
+ borderColor: colors.border,
253
+ backgroundColor: value === 0 ? 'transparent' : pressed ? colors.primaryPressed : colors.cardMuted,
254
+ opacity: value === 0 ? 0.2 : canMove ? 1 : 0.7,
255
+ },
256
+ stylesConfig?.tile,
257
+ ],
258
+ },
259
+ value === 0 ? null : React.createElement(Text, { style: [styles.tileText, { color: colors.text }, stylesConfig?.tileText] }, String(value))
260
+ );
261
+ })
262
+ )
263
+ );
264
+ }
265
+
266
+ function BonkChallengeRunner({ type, tileCount = 8, difficulty = 'intermediate', colors, stylesConfig, onSolved }) {
267
+ if (type === 'tile') {
268
+ return React.createElement(TileChallenge, { colors, stylesConfig, onSolved, tileCount });
269
+ }
270
+ return React.createElement(MathChallenge, { colors, stylesConfig, onSolved, difficulty });
271
+ }
272
+
273
+ const styles = StyleSheet.create({
274
+ block: {
275
+ gap: 10,
276
+ },
277
+ prompt: {
278
+ fontSize: 20,
279
+ fontWeight: '700',
280
+ },
281
+ answer: {
282
+ fontSize: 15,
283
+ fontWeight: '600',
284
+ },
285
+ feedback: {
286
+ fontSize: 13,
287
+ fontWeight: '600',
288
+ },
289
+ padGrid: {
290
+ marginTop: 2,
291
+ flexDirection: 'row',
292
+ flexWrap: 'wrap',
293
+ gap: 8,
294
+ },
295
+ key: {
296
+ width: '30%',
297
+ borderRadius: 10,
298
+ borderWidth: 1,
299
+ alignItems: 'center',
300
+ justifyContent: 'center',
301
+ minHeight: 42,
302
+ },
303
+ keyText: {
304
+ fontSize: 17,
305
+ fontWeight: '700',
306
+ },
307
+ tileGrid: {
308
+ marginTop: 6,
309
+ flexDirection: 'row',
310
+ flexWrap: 'wrap',
311
+ gap: 8,
312
+ alignSelf: 'center',
313
+ },
314
+ tile: {
315
+ borderRadius: 10,
316
+ borderWidth: 1,
317
+ alignItems: 'center',
318
+ justifyContent: 'center',
319
+ },
320
+ tileText: {
321
+ fontSize: 30,
322
+ fontWeight: '800',
323
+ },
324
+ });
325
+
326
+ module.exports = {
327
+ BonkChallengeRunner,
328
+ };
package/package.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "bonk-challanges",
3
+ "version": "0.1.0",
4
+ "main": "index.js",
5
+ "license": "MIT",
6
+ "peerDependencies": {
7
+ "react": ">=18",
8
+ "react-native": ">=0.70"
9
+ }
10
+ }