claude-petpet 1.0.0 → 1.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-petpet",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Find UUIDs that produce specific Claude Code buddy companions",
5
5
  "keywords": [
6
6
  "buddy",
package/src/cli.ts CHANGED
@@ -4,12 +4,14 @@
4
4
  import * as p from "@clack/prompts";
5
5
 
6
6
  import { RARITIES, SPECIES, EYES, HATS, STAT_NAMES } from "./enums.ts";
7
- import { STARS, YIELD_EVERY } from "./consts.ts";
8
- import { rollFrom, matchesFilters } from "./helpers.ts";
7
+ import { STARS } from "./consts.ts";
8
+ import { rollFromFiltered } from "./helpers.ts";
9
9
  import { renderSprite } from "./sprites.ts";
10
10
  import { c } from "./color.ts";
11
11
  import type { Rarity, StatName, SearchFilters, SearchResult } from "./types.ts";
12
12
 
13
+ const WORKER_THRESHOLD = 500_000;
14
+
13
15
  const NONE = "__none__";
14
16
 
15
17
  async function promptFilters() {
@@ -156,9 +158,7 @@ function formatSummary(filters: SearchFilters): string {
156
158
  return parts.join(", ") || "(any)";
157
159
  }
158
160
 
159
- async function searchWithProgress(
160
- filters: SearchFilters,
161
- ): Promise<{ results: SearchResult[]; searched: number }> {
161
+ async function searchSingleThread(filters: SearchFilters): Promise<{ results: SearchResult[]; searched: number }> {
162
162
  const s = p.spinner({ indicator: "timer" });
163
163
  s.start("Searching...");
164
164
 
@@ -166,15 +166,14 @@ async function searchWithProgress(
166
166
  let searched = 0;
167
167
 
168
168
  for (let i = 0; i < filters.max; i++) {
169
- if (i % YIELD_EVERY === 0 && i > 0) {
169
+ if (i % 50_000 === 0 && i > 0) {
170
170
  s.message(`Found ${results.length} match(es), ${i.toLocaleString()} searched`);
171
171
  await Bun.sleep(0);
172
172
  }
173
173
 
174
174
  const uuid = crypto.randomUUID();
175
- const roll = rollFrom(uuid);
176
-
177
- if (!matchesFilters(roll, filters)) continue;
175
+ const roll = rollFromFiltered(uuid, filters);
176
+ if (!roll) continue;
178
177
 
179
178
  results.push({ ...roll, uuid });
180
179
  results.sort((a, b) => b.total - a.total);
@@ -186,10 +185,81 @@ async function searchWithProgress(
186
185
 
187
186
  searched = searched || filters.max;
188
187
  s.stop(`Searched ${searched.toLocaleString()} seeds`);
189
-
190
188
  return { results, searched };
191
189
  }
192
190
 
191
+ async function searchParallel(filters: SearchFilters): Promise<{ results: SearchResult[]; searched: number }> {
192
+ const numWorkers = navigator.hardwareConcurrency || 4;
193
+ const perWorker = Math.ceil(filters.max / numWorkers);
194
+
195
+ const s = p.spinner({ indicator: "timer" });
196
+ s.start(`Searching across ${numWorkers} workers...`);
197
+
198
+ const results: SearchResult[] = [];
199
+ const workerProgress = new Array<number>(numWorkers).fill(0);
200
+ let totalSearched = 0;
201
+
202
+ return new Promise((resolve) => {
203
+ const workers: Worker[] = [];
204
+ let doneCount = 0;
205
+
206
+ function terminateAll() {
207
+ for (const w of workers) w.terminate();
208
+ }
209
+
210
+ function finish() {
211
+ results.sort((a, b) => b.total - a.total);
212
+ results.length = Math.min(results.length, filters.limit);
213
+ s.stop(`Searched ${totalSearched.toLocaleString()} seeds across ${numWorkers} workers`);
214
+ resolve({ results, searched: totalSearched });
215
+ }
216
+
217
+ for (let i = 0; i < numWorkers; i++) {
218
+ const worker = new Worker(new URL("./worker.ts", import.meta.url));
219
+ workers.push(worker);
220
+
221
+ worker.onmessage = (event) => {
222
+ const msg = event.data;
223
+
224
+ if (msg.type === "match") {
225
+ results.push(msg.result);
226
+ // Keep only top N while collecting
227
+ if (results.length > filters.limit * 2) {
228
+ results.sort((a, b) => b.total - a.total);
229
+ results.length = filters.limit;
230
+ }
231
+ const searched = workerProgress.reduce((a, b) => a + b, 0);
232
+ s.message(`Found ${results.length} match(es), ${searched.toLocaleString()} searched`);
233
+
234
+ if (results.length >= filters.limit) {
235
+ totalSearched = searched;
236
+ terminateAll();
237
+ finish();
238
+ }
239
+ } else if (msg.type === "progress") {
240
+ workerProgress[i] = msg.searched;
241
+ const searched = workerProgress.reduce((a, b) => a + b, 0);
242
+ s.message(`Found ${results.length} match(es), ${searched.toLocaleString()} searched`);
243
+ } else if (msg.type === "done") {
244
+ workerProgress[i] = msg.searched;
245
+ doneCount++;
246
+ if (doneCount === numWorkers) {
247
+ totalSearched = workerProgress.reduce((a, b) => a + b, 0);
248
+ terminateAll();
249
+ finish();
250
+ }
251
+ }
252
+ };
253
+
254
+ worker.postMessage({ filters, count: perWorker });
255
+ }
256
+ });
257
+ }
258
+
259
+ async function searchWithProgress(filters: SearchFilters): Promise<{ results: SearchResult[]; searched: number }> {
260
+ return filters.max >= WORKER_THRESHOLD ? searchParallel(filters) : searchSingleThread(filters);
261
+ }
262
+
193
263
  function padVisual(s: string, width: number): string {
194
264
  return s + " ".repeat(Math.max(0, width - Bun.stringWidth(s)));
195
265
  }
package/src/helpers.ts CHANGED
@@ -73,6 +73,36 @@ export function rollFrom(userId: string): Roll {
73
73
  return { rarity, species, eye, hat, shiny, stats, peak, dump, total };
74
74
  }
75
75
 
76
+ // Like rollFrom, but returns null early when any filter fails.
77
+ // Avoids computing stats/hat/shiny when an earlier property already mismatches.
78
+ export function rollFromFiltered(userId: string, filters: SearchFilters): Roll | null {
79
+ const rng = mulberry32(hashString(userId + SALT));
80
+
81
+ const rarity = rollRarity(rng);
82
+ if (filters.rarity && rarity !== filters.rarity) return null;
83
+
84
+ const species = pick(rng, SPECIES);
85
+ if (filters.species && species !== filters.species) return null;
86
+
87
+ const eye = pick(rng, EYES);
88
+ if (filters.eye && eye !== filters.eye) return null;
89
+
90
+ const hat = rarity === "common" ? "none" : pick(rng, HATS);
91
+ if (filters.hat && hat !== filters.hat) return null;
92
+
93
+ const shiny = rng() < 0.01;
94
+ if (filters.shiny && !shiny) return null;
95
+
96
+ const { stats, peak, dump } = rollStats(rng, rarity);
97
+ if (filters.peak && peak !== filters.peak) return null;
98
+ if (filters.dump && dump !== filters.dump) return null;
99
+
100
+ const total = Object.values(stats).reduce((a, b) => a + b, 0);
101
+ if (filters.minTotal && total < filters.minTotal) return null;
102
+
103
+ return { rarity, species, eye, hat, shiny, stats, peak, dump, total };
104
+ }
105
+
76
106
  export function matchesFilters(roll: Roll, filters: SearchFilters): boolean {
77
107
  if (filters.species && roll.species !== filters.species) return false;
78
108
  if (filters.rarity && roll.rarity !== filters.rarity) return false;
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ export {
7
7
  rollRarity,
8
8
  rollStats,
9
9
  rollFrom,
10
+ rollFromFiltered,
10
11
  matchesFilters,
11
12
  search,
12
13
  } from "./helpers.ts";
package/src/worker.ts ADDED
@@ -0,0 +1,28 @@
1
+ // Worker script for parallel buddy search.
2
+
3
+ import { rollFromFiltered } from "./helpers.ts";
4
+ import type { SearchFilters, SearchResult } from "./types.ts";
5
+
6
+ declare var self: Worker;
7
+
8
+ const PROGRESS_EVERY = 100_000;
9
+
10
+ self.onmessage = (event: MessageEvent<{ filters: SearchFilters; count: number }>) => {
11
+ const { filters, count } = event.data;
12
+ let searched = 0;
13
+
14
+ for (let i = 0; i < count; i++) {
15
+ if (i % PROGRESS_EVERY === 0 && i > 0) {
16
+ self.postMessage({ type: "progress", searched: i });
17
+ }
18
+
19
+ const uuid = crypto.randomUUID();
20
+ const roll = rollFromFiltered(uuid, filters);
21
+ if (!roll) continue;
22
+
23
+ self.postMessage({ type: "match", result: { ...roll, uuid } });
24
+ searched = i + 1;
25
+ }
26
+
27
+ self.postMessage({ type: "done", searched: searched || count });
28
+ };