opencode-buddy 0.2.0 → 0.3.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/src/species.js ADDED
@@ -0,0 +1,541 @@
1
+ // 6 species × 5 states. Each "frame" is an array of rows.
2
+ // All frames in a state share the same width so the canvas doesn't jitter.
3
+ // The `idle` state is an array of frames (animated).
4
+ // Other states are single-frame (set in a one-element array).
5
+
6
+ const RESET = "\x1b[0m";
7
+
8
+ const C = {
9
+ reset: RESET,
10
+ dim: "\x1b[2m",
11
+ bold: "\x1b[1m",
12
+ red: "\x1b[31m",
13
+ green: "\x1b[32m",
14
+ yellow: "\x1b[33m",
15
+ blue: "\x1b[34m",
16
+ magenta: "\x1b[35m",
17
+ cyan: "\x1b[36m",
18
+ gray: "\x1b[90m",
19
+ brightRed: "\x1b[91m",
20
+ brightGreen: "\x1b[92m",
21
+ brightYellow: "\x1b[93m",
22
+ brightBlue: "\x1b[94m",
23
+ brightMagenta: "\x1b[95m",
24
+ brightCyan: "\x1b[96m",
25
+ };
26
+
27
+ const FRAMES = {
28
+ duck: {
29
+ idle: [
30
+ // frame 0: blink open
31
+ [
32
+ ` __ `,
33
+ ` <(o )___ `,
34
+ ` ( ._> / `,
35
+ ` \`--' `,
36
+ ` `,
37
+ ` ~ idle ~ `,
38
+ ],
39
+ // frame 1: blink closed
40
+ [
41
+ ` __ `,
42
+ ` <(- )___ `,
43
+ ` ( ._> / `,
44
+ ` \`--' `,
45
+ ` `,
46
+ ` ~ idle ~ `,
47
+ ],
48
+ // frame 2: blink open (same as 0, ends loop)
49
+ [
50
+ ` __ `,
51
+ ` <(o )___ `,
52
+ ` ( ._> / `,
53
+ ` \`--' `,
54
+ ` `,
55
+ ` ~ idle ~ `,
56
+ ],
57
+ ],
58
+ working: [
59
+ [
60
+ ` __ `,
61
+ ` <(o )___ `,
62
+ ` ( ._> / `,
63
+ ` *\`--'* .. .. `,
64
+ ` / / `,
65
+ ` coding... `,
66
+ ],
67
+ ],
68
+ celebrating: [
69
+ [
70
+ ` \\o/ `,
71
+ ` <(o )___ ! `,
72
+ ` ( ._> / `,
73
+ ` \`--' \\o/ `,
74
+ ` `,
75
+ ` \\o/ nice! `,
76
+ ],
77
+ ],
78
+ scared: [
79
+ [
80
+ ` __ `,
81
+ ` <(O )___ `,
82
+ ` ( ._> / `,
83
+ ` \`--' !! `,
84
+ ` `,
85
+ ` !! oh no `,
86
+ ],
87
+ ],
88
+ sleeping: [
89
+ [
90
+ ` __ `,
91
+ ` <(o )___ `,
92
+ ` ( -_> z `,
93
+ ` \`--' z `,
94
+ ` `,
95
+ ` z z z z z `,
96
+ ],
97
+ ],
98
+ },
99
+
100
+ cat: {
101
+ idle: [
102
+ [
103
+ ` /\\_/\\ `,
104
+ ` ( o.o ) `,
105
+ ` > ^ < `,
106
+ ` /| |\\ `,
107
+ ` (_| |_) `,
108
+ ` meow `,
109
+ ],
110
+ [
111
+ ` /\\_/\\ `,
112
+ ` ( -.o ) `,
113
+ ` > ^ < `,
114
+ ` /| |\\ `,
115
+ ` (_| |_) `,
116
+ ` meow `,
117
+ ],
118
+ [
119
+ ` /\\_/\\ `,
120
+ ` ( o.o ) `,
121
+ ` > ^ < `,
122
+ ` /| |\\ `,
123
+ ` (_| |_) `,
124
+ ` meow `,
125
+ ],
126
+ ],
127
+ working: [
128
+ [
129
+ ` /\\_/\\ ... `,
130
+ ` ( o.o ) `,
131
+ ` > ^ < `,
132
+ ` /| |\\ .. `,
133
+ ` (_| |_) `,
134
+ ` typing... `,
135
+ ],
136
+ ],
137
+ celebrating: [
138
+ [
139
+ ` /\\_/\\ `,
140
+ ` ( ^.^ ) \\o/ `,
141
+ ` > ~ < `,
142
+ ` /| |\\ \\o/ `,
143
+ ` (_| |_) `,
144
+ ` purrrfect! `,
145
+ ],
146
+ ],
147
+ scared: [
148
+ [
149
+ ` /\\_/\\ !! `,
150
+ ` ( O.O ) `,
151
+ ` > ^ < `,
152
+ ` /| |\\ `,
153
+ ` (_| |_) /\\ `,
154
+ ` !! hiss `,
155
+ ],
156
+ ],
157
+ sleeping: [
158
+ [
159
+ ` /\\_/\\ `,
160
+ ` ( -.- ) z `,
161
+ ` > ^ < `,
162
+ ` /| |\\ z `,
163
+ ` (_| |_) `,
164
+ ` z z z z z `,
165
+ ],
166
+ ],
167
+ },
168
+
169
+ dragon: {
170
+ idle: [
171
+ [
172
+ ` /\\_/\\ `,
173
+ ` ( o o ) ~~ `,
174
+ ` > ^ < / `,
175
+ ` /| |\\ `,
176
+ ` (_| |_) `,
177
+ ` rawr `,
178
+ ],
179
+ [
180
+ ` /\\_/\\ `,
181
+ ` ( - - ) ~~ `,
182
+ ` > ^ < / `,
183
+ ` /| |\\ `,
184
+ ` (_| |_) `,
185
+ ` rawr `,
186
+ ],
187
+ [
188
+ ` /\\_/\\ `,
189
+ ` ( o o ) ~~ `,
190
+ ` > ^ < / `,
191
+ ` /| |\\ `,
192
+ ` (_| |_) `,
193
+ ` rawr `,
194
+ ],
195
+ ],
196
+ working: [
197
+ [
198
+ ` /\\_/\\ ~~~ `,
199
+ ` ( o o ) ~~ `,
200
+ ` > ^ < `,
201
+ ` /| |\\ ~~ `,
202
+ ` (_| |_) `,
203
+ ` forging... `,
204
+ ],
205
+ ],
206
+ celebrating: [
207
+ [
208
+ ` /\\^/\\ \\o/ `,
209
+ ` ( ^.^ ) `,
210
+ ` > ~ < \\o/ `,
211
+ ` /| |\\ `,
212
+ ` (_| |_) `,
213
+ ` +50 xp!! \\o/ `,
214
+ ],
215
+ ],
216
+ scared: [
217
+ [
218
+ ` /\\_/\\ !! `,
219
+ ` ( O O ) `,
220
+ ` > ^ < `,
221
+ ` /| |\\ !! `,
222
+ ` (_| |_) `,
223
+ ` !! yikes `,
224
+ ],
225
+ ],
226
+ sleeping: [
227
+ [
228
+ ` /\\_/\\ `,
229
+ ` ( - - ) z `,
230
+ ` > ^ < `,
231
+ ` /| |\\ z `,
232
+ ` (_| |_) `,
233
+ ` z z z z z `,
234
+ ],
235
+ ],
236
+ },
237
+
238
+ axolotl: {
239
+ idle: [
240
+ [
241
+ ` ^___^ `,
242
+ ` (o . o) `,
243
+ ` \\|_|_|/ `,
244
+ ` \\| |/ `,
245
+ ` ) ( `,
246
+ ` ~ ambien `,
247
+ ],
248
+ [
249
+ ` ^___^ `,
250
+ ` (o - o) `,
251
+ ` \\|_|_|/ `,
252
+ ` \\| |/ `,
253
+ ` ) ( `,
254
+ ` ~ ambien `,
255
+ ],
256
+ [
257
+ ` ^___^ `,
258
+ ` (o . o) `,
259
+ ` \\|_|_|/ `,
260
+ ` \\| |/ `,
261
+ ` ) ( `,
262
+ ` ~ ambien `,
263
+ ],
264
+ ],
265
+ working: [
266
+ [
267
+ ` ^___^ .. `,
268
+ ` (o . o) `,
269
+ ` \\|_|_|/ .. `,
270
+ ` \\| |/ `,
271
+ ` ) ( `,
272
+ ` swimming... `,
273
+ ],
274
+ ],
275
+ celebrating: [
276
+ [
277
+ ` ^___^ \\o/ `,
278
+ ` (o ^ o) `,
279
+ ` \\|_|_|/ \\o/ `,
280
+ ` \\| |/ `,
281
+ ` ) ( `,
282
+ ` \\o/ splish! `,
283
+ ],
284
+ ],
285
+ scared: [
286
+ [
287
+ ` ^___^ !! `,
288
+ ` (O . O) `,
289
+ ` \\|_|_|/ `,
290
+ ` \\| |/ !! `,
291
+ ` ) ( `,
292
+ ` !! gulp `,
293
+ ],
294
+ ],
295
+ sleeping: [
296
+ [
297
+ ` ^___^ `,
298
+ ` (o - o) z `,
299
+ ` \\|_|_|/ `,
300
+ ` \\| |/ z `,
301
+ ` ) ( `,
302
+ ` z z z z z `,
303
+ ],
304
+ ],
305
+ },
306
+
307
+ robot: {
308
+ idle: [
309
+ [
310
+ ` [ O . O ] `,
311
+ ` /|#####|\\ `,
312
+ ` / |#####| \\ `,
313
+ ` | | `,
314
+ ` /| | | |\\ `,
315
+ ` beep `,
316
+ ],
317
+ [
318
+ ` [ O . O ] `,
319
+ ` /|#####|\\ `,
320
+ ` / |#####| \\ `,
321
+ ` | | `,
322
+ ` /| | | |\\ `,
323
+ ` beep `,
324
+ ],
325
+ [
326
+ ` [ O . O ] `,
327
+ ` /|#####|\\ `,
328
+ ` / |#####| \\ `,
329
+ ` | | `,
330
+ ` /| | | |\\ `,
331
+ ` beep `,
332
+ ],
333
+ ],
334
+ working: [
335
+ [
336
+ ` [ O . O ] ... `,
337
+ ` /|#####|\\ `,
338
+ ` / |#####| \\ .. `,
339
+ ` | | `,
340
+ ` /| | | |\\ `,
341
+ ` compiling... `,
342
+ ],
343
+ ],
344
+ celebrating: [
345
+ [
346
+ ` [ ^ . ^ ] \\o/ `,
347
+ ` /|#####|\\ `,
348
+ ` / |#####| \\ `,
349
+ ` | | `,
350
+ ` /| | | |\\ \\o/ `,
351
+ ` pass tests! `,
352
+ ],
353
+ ],
354
+ scared: [
355
+ [
356
+ ` [ O . O ] !! `,
357
+ ` /|#####|\\ `,
358
+ ` / |#####| \\ `,
359
+ ` | | !! `,
360
+ ` /| | | |\\ `,
361
+ ` !! segfault `,
362
+ ],
363
+ ],
364
+ sleeping: [
365
+ [
366
+ ` [ - . - ] z `,
367
+ ` /|#####|\\ `,
368
+ ` / |#####| \\ z `,
369
+ ` | | `,
370
+ ` /| | | |\\ `,
371
+ ` z z z z z `,
372
+ ],
373
+ ],
374
+ },
375
+
376
+ ghost: {
377
+ idle: [
378
+ [
379
+ ` .-""-. `,
380
+ ` ( o . o ) `,
381
+ ` | ~ ~ | `,
382
+ ` | | `,
383
+ ` \\uuuuu/ `,
384
+ ` boo `,
385
+ ],
386
+ [
387
+ ` .-""-. `,
388
+ ` ( o . o ) `,
389
+ ` | ~ ~ | `,
390
+ ` | | `,
391
+ ` \\uuuuu/ `,
392
+ ` boo `,
393
+ ],
394
+ [
395
+ ` .-""-. `,
396
+ ` ( o . o ) `,
397
+ ` | ~ ~ | `,
398
+ ` | | `,
399
+ ` \\uuuuu/ `,
400
+ ` boo `,
401
+ ],
402
+ ],
403
+ working: [
404
+ [
405
+ ` .-""-. ... `,
406
+ ` ( o . o ) `,
407
+ ` | ~ ~ | `,
408
+ ` | | `,
409
+ ` \\uuuuu/ `,
410
+ ` haunting... `,
411
+ ],
412
+ ],
413
+ celebrating: [
414
+ [
415
+ ` .-""-. \\o/ `,
416
+ ` ( ^ . ^ ) `,
417
+ ` | ~ ~ | \\o/ `,
418
+ ` | | `,
419
+ ` \\uuuuu/ `,
420
+ ` \\o/ spooky! `,
421
+ ],
422
+ ],
423
+ scared: [
424
+ [
425
+ ` .-""-. !! `,
426
+ ` ( O . O ) `,
427
+ ` | ~ ~ | `,
428
+ ` | | !! `,
429
+ ` \\uuuuu/ `,
430
+ ` !! yeek `,
431
+ ],
432
+ ],
433
+ sleeping: [
434
+ [
435
+ ` .-""-. `,
436
+ ` ( - . - ) z `,
437
+ ` | ~ ~ | `,
438
+ ` | | `,
439
+ ` \\uuuuu/ z `,
440
+ ` z z z z z `,
441
+ ],
442
+ ],
443
+ },
444
+ };
445
+
446
+ const PALETTES = {
447
+ duck: (ch) => {
448
+ if (ch === "o" || ch === "O") return C.brightYellow + ch + RESET;
449
+ if (ch === "<" || ch === ">" || ch === "(" || ch === ")") return C.yellow + ch + RESET;
450
+ if (ch === "_" || ch === "-" || ch === ".") return C.yellow + ch + RESET;
451
+ if (ch === "~" || ch === "*") return C.cyan + ch + RESET;
452
+ if (ch === "z") return C.gray + ch + RESET;
453
+ if (ch === "!") return C.brightYellow + ch + RESET;
454
+ if (ch === " ") return ch;
455
+ return C.yellow + ch + RESET;
456
+ },
457
+ cat: (ch) => {
458
+ if (ch === "o" || ch === "O" || ch === "^") return C.brightGreen + ch + RESET;
459
+ if (ch === "!" || ch === "~" || ch === "o") return C.brightMagenta + ch + RESET;
460
+ if (ch === "z") return C.gray + ch + RESET;
461
+ if (ch === ".") return C.gray + ch + RESET;
462
+ if (ch === " ") return ch;
463
+ return C.green + ch + RESET;
464
+ },
465
+ dragon: (ch) => {
466
+ if (ch === "o" || ch === "O" || ch === "^") return C.brightRed + ch + RESET;
467
+ if (ch === "~" || ch === "!") return C.brightRed + ch + RESET;
468
+ if (ch === "z") return C.gray + ch + RESET;
469
+ if (ch === " ") return ch;
470
+ return C.green + ch + RESET;
471
+ },
472
+ axolotl: (ch) => {
473
+ if (ch === "o" || ch === "O") return C.brightMagenta + ch + RESET;
474
+ if (ch === "^" || ch === "_") return C.magenta + ch + RESET;
475
+ if (ch === "!" || ch === "o") return C.brightYellow + ch + RESET;
476
+ if (ch === "z") return C.gray + ch + RESET;
477
+ if (ch === ".") return C.cyan + ch + RESET;
478
+ if (ch === " ") return ch;
479
+ return C.magenta + ch + RESET;
480
+ },
481
+ robot: (ch) => {
482
+ if (ch === "O" || ch === "#") return C.brightCyan + ch + RESET;
483
+ if (ch === "o") return C.cyan + ch + RESET;
484
+ if (ch === ".") return C.gray + ch + RESET;
485
+ if (ch === "!" || ch === "o") return C.brightYellow + ch + RESET;
486
+ if (ch === "z") return C.gray + ch + RESET;
487
+ if (ch === " ") return ch;
488
+ return C.gray + ch + RESET;
489
+ },
490
+ ghost: (ch) => {
491
+ if (ch === "o" || ch === "O") return C.brightCyan + ch + RESET;
492
+ if (ch === "~" || ch === "!") return C.brightMagenta + ch + RESET;
493
+ if (ch === "z") return C.gray + ch + RESET;
494
+ if (ch === ".") return C.gray + ch + RESET;
495
+ if (ch === " ") return ch;
496
+ return C.brightCyan + ch + RESET;
497
+ },
498
+ };
499
+
500
+ export const SPECIES = ["duck", "cat", "dragon", "axolotl", "robot", "ghost"];
501
+ export const STATES = ["idle", "working", "celebrating", "scared", "sleeping"];
502
+
503
+ const FRAME_INTERVAL_MS = 600;
504
+
505
+ export function framesFor(species, state) {
506
+ const list = FRAMES[species]?.[state] ?? FRAMES.duck.idle;
507
+ return list;
508
+ }
509
+
510
+ export function frameIntervalMs() {
511
+ return FRAME_INTERVAL_MS;
512
+ }
513
+
514
+ export function frameCount(species, state) {
515
+ return framesFor(species, state).length;
516
+ }
517
+
518
+ const FRAME_WIDTH = 20;
519
+
520
+ export function maxFrameHeight() {
521
+ return 6;
522
+ }
523
+
524
+ export function frameWidth() {
525
+ return FRAME_WIDTH;
526
+ }
527
+
528
+ export function renderFrame(species, state, frameIdx) {
529
+ const frames = framesFor(species, state);
530
+ const i = ((frameIdx % frames.length) + frames.length) % frames.length;
531
+ const palette = PALETTES[species];
532
+ return frames[i].map((row) => {
533
+ let out = "";
534
+ for (const ch of row) out += palette(ch);
535
+ // Auto-pad to FRAME_WIDTH so off-by-one spacing in raw art doesn't
536
+ // cause render jitter.
537
+ const visible = out.replace(/\x1b\[[0-9;]*m/g, "").length;
538
+ if (visible < FRAME_WIDTH) out += " ".repeat(FRAME_WIDTH - visible);
539
+ return out;
540
+ });
541
+ }
package/src/state.js ADDED
@@ -0,0 +1,120 @@
1
+ export const DEFAULT_STATE = {
2
+ species: "duck",
3
+ name: "Quack",
4
+ hunger: 80,
5
+ happiness: 80,
6
+ energy: 100,
7
+ xp: 0,
8
+ level: 1,
9
+ hatchedAt: Date.now(),
10
+ lastFed: Date.now(),
11
+ lastPlayed: Date.now(),
12
+ lastTick: Date.now(),
13
+ state: "idle",
14
+ stateUntil: 0,
15
+ stateReason: "",
16
+ };
17
+
18
+ const CLAMP = (n, lo = 0, hi = 100) => Math.max(lo, Math.min(hi, n));
19
+
20
+ export function tick(state, now = Date.now()) {
21
+ const minutes = (now - state.lastTick) / 60_000;
22
+ if (minutes <= 0) return state;
23
+ return {
24
+ ...state,
25
+ hunger: CLAMP(state.hunger - 0.5 * minutes),
26
+ happiness: CLAMP(state.happiness - 0.3 * minutes),
27
+ energy: CLAMP(state.energy - 0.2 * minutes),
28
+ lastTick: now,
29
+ };
30
+ }
31
+
32
+ export function feed(state) {
33
+ return {
34
+ ...state,
35
+ hunger: CLAMP(state.hunger + 25),
36
+ energy: CLAMP(state.energy - 5),
37
+ lastFed: Date.now(),
38
+ lastTick: Date.now(),
39
+ };
40
+ }
41
+
42
+ export function play(state) {
43
+ const next = {
44
+ ...state,
45
+ happiness: CLAMP(state.happiness + 20),
46
+ energy: CLAMP(state.energy - 10),
47
+ hunger: CLAMP(state.hunger - 5),
48
+ lastPlayed: Date.now(),
49
+ lastTick: Date.now(),
50
+ xp: state.xp + 5,
51
+ };
52
+ return maybeLevelUp(next);
53
+ }
54
+
55
+ export function rest(state) {
56
+ return { ...state, energy: CLAMP(state.energy + 30), lastTick: Date.now() };
57
+ }
58
+
59
+ export function rename(state, name) {
60
+ return { ...state, name: String(name).slice(0, 20), lastTick: Date.now() };
61
+ }
62
+
63
+ export function switchSpecies(state, species) {
64
+ return { ...state, species, lastTick: Date.now() };
65
+ }
66
+
67
+ export function hatch(overrides = {}) {
68
+ return {
69
+ ...DEFAULT_STATE,
70
+ ...overrides,
71
+ lastTick: Date.now(),
72
+ };
73
+ }
74
+
75
+ export function celebrate(state) {
76
+ return {
77
+ ...state,
78
+ state: "celebrating",
79
+ stateUntil: Date.now() + 4000,
80
+ lastTick: Date.now(),
81
+ };
82
+ }
83
+
84
+ export function scared(state) {
85
+ return {
86
+ ...state,
87
+ state: "scared",
88
+ stateUntil: Date.now() + 5000,
89
+ lastTick: Date.now(),
90
+ };
91
+ }
92
+
93
+ export function working(state) {
94
+ if (state.state === "celebrating" && Date.now() < state.stateUntil) return state;
95
+ if (state.state === "scared" && Date.now() < state.stateUntil) return state;
96
+ return { ...state, state: "working", stateUntil: 0, lastTick: Date.now() };
97
+ }
98
+
99
+ export function deriveState(state, now = Date.now()) {
100
+ if (state.state === "celebrating" && now < state.stateUntil) return state;
101
+ if (state.state === "scared" && now < state.stateUntil) return state;
102
+ if (state.energy < 20) return { ...state, state: "sleeping", lastTick: now };
103
+ if (state.hunger < 25) {
104
+ return { ...state, state: "scared", stateUntil: now + 30_000, lastTick: now };
105
+ }
106
+ if (state.state === "working") return state;
107
+ return { ...state, state: "idle", lastTick: now };
108
+ }
109
+
110
+ function maybeLevelUp(state) {
111
+ const xpNeeded = state.level * 50;
112
+ if (state.xp < xpNeeded) return state;
113
+ return {
114
+ ...state,
115
+ level: state.level + 1,
116
+ xp: state.xp - xpNeeded,
117
+ happiness: CLAMP(state.happiness + 10),
118
+ lastTick: Date.now(),
119
+ };
120
+ }