buddy-battle 1.0.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/dist/app.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import React from 'react';
2
+ export default function App(): React.JSX.Element | null;
package/dist/app.js ADDED
@@ -0,0 +1,74 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { readLocalBuddy, getHashedUserId } from './config/reader.js';
4
+ import { sanitizeBuddyForNetwork, fetchCloudProfile, createCloudProfile } from './network/client.js';
5
+ import { Menu } from './ui/Menu.js';
6
+ import { Leaderboard } from './ui/Leaderboard.js';
7
+ import { Matchmaking } from './ui/Matchmaking.js';
8
+ import { Arena } from './ui/Arena.js';
9
+ import { Setup } from './ui/Setup.js';
10
+ export default function App() {
11
+ const [buddy, setBuddy] = useState(undefined);
12
+ const [trainerName, setTrainerName] = useState(null);
13
+ const [bunWarning, setBunWarning] = useState(false);
14
+ const [view, setView] = useState('loading');
15
+ const [setupError, setSetupError] = useState(undefined);
16
+ const [opponent, setOpponent] = useState(null);
17
+ const [battleId, setBattleId] = useState(null);
18
+ useEffect(() => {
19
+ async function boot() {
20
+ const { buddy: localBuddy, bunWarning: noBun } = readLocalBuddy();
21
+ setBunWarning(noBun);
22
+ const hashedId = getHashedUserId();
23
+ const cloudName = await fetchCloudProfile(hashedId);
24
+ if (cloudName) {
25
+ setTrainerName(cloudName);
26
+ setBuddy(localBuddy);
27
+ setView('menu');
28
+ }
29
+ else {
30
+ setBuddy(localBuddy);
31
+ setView('setup');
32
+ }
33
+ }
34
+ boot();
35
+ }, []);
36
+ if (!buddy) {
37
+ return React.createElement(Text, { color: "cyan" }, "Syncing Cloud Profile...");
38
+ }
39
+ if (view === 'exit') {
40
+ process.exit(0);
41
+ return null; // unreachable
42
+ }
43
+ return (React.createElement(Box, { flexDirection: "column", padding: 1 },
44
+ React.createElement(Box, { marginBottom: 1, flexDirection: "column" },
45
+ React.createElement(Text, { color: "gray" },
46
+ "Logged in as ",
47
+ trainerName ?? buddy.name,
48
+ " (",
49
+ buddy.rarity,
50
+ " ",
51
+ buddy.species,
52
+ ")"),
53
+ bunWarning && (React.createElement(Text, { color: "yellow" }, "\u26A0 Bun not found \u2014 buddy stats may be inaccurate. Install Bun: curl -fsSL https://bun.sh/install | bash"))),
54
+ view === 'loading' && (React.createElement(Text, { color: "cyan" }, "Authenticating profile...")),
55
+ view === 'setup' && (React.createElement(Setup, { errorMsg: setupError, onComplete: async (name) => {
56
+ setSetupError(undefined);
57
+ const success = await createCloudProfile(getHashedUserId(), name);
58
+ if (success) {
59
+ setTrainerName(name);
60
+ setView('menu');
61
+ }
62
+ else {
63
+ setSetupError(`Trainer name '${name}' is already taken. Please try another!`);
64
+ }
65
+ } })),
66
+ view === 'menu' && (React.createElement(Menu, { onSelect: (opt) => setView(opt) })),
67
+ view === 'leaderboard' && (React.createElement(Leaderboard, { onBack: () => setView('menu') })),
68
+ view === 'matchmaking' && (React.createElement(Matchmaking, { myBuddy: sanitizeBuddyForNetwork(buddy, trainerName), onCancel: () => setView('menu'), onMatch: (opp, id) => {
69
+ setOpponent(opp);
70
+ setBattleId(id);
71
+ setView('battle');
72
+ } })),
73
+ view === 'battle' && opponent && battleId && (React.createElement(Arena, { myBuddy: buddy, myTrainerName: trainerName ?? buddy.name, opponent: opponent, battleId: battleId, onLeave: () => setView('menu') }))));
74
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import React from 'react';
3
+ import { render } from 'ink';
4
+ import App from './app.js';
5
+ render(React.createElement(App, null));
@@ -0,0 +1,8 @@
1
+ import { type Companion } from '../engine/types.js';
2
+ export declare function getClaudeConfigHomeDir(): string;
3
+ export type BuddyLoadResult = {
4
+ buddy: Companion;
5
+ bunWarning: boolean;
6
+ };
7
+ export declare function readLocalBuddy(): BuddyLoadResult;
8
+ export declare function getHashedUserId(): string;
@@ -0,0 +1,102 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import crypto from 'crypto';
4
+ import path from 'path';
5
+ import { execSync } from 'child_process';
6
+ import { z } from 'zod';
7
+ import { getCompanion } from '../engine/companion.js';
8
+ // Try to resolve exactly where claude.json lives
9
+ export function getClaudeConfigHomeDir() {
10
+ const baseDir = process.env['CLAUDE_CONFIG_DIR'] ?? path.join(os.homedir(), '.claude');
11
+ return baseDir.normalize('NFC');
12
+ }
13
+ const ClaudeConfigSchema = z.object({
14
+ userID: z.string().optional(),
15
+ oauthAccount: z.object({
16
+ accountUuid: z.string()
17
+ }).optional(),
18
+ companion: z.object({
19
+ name: z.string(),
20
+ personality: z.string(),
21
+ hatchedAt: z.number()
22
+ }).optional()
23
+ }).passthrough();
24
+ const GUEST_BUDDY = {
25
+ name: 'Stranger',
26
+ personality: 'Mysterious and ready to battle',
27
+ hatchedAt: Date.now()
28
+ };
29
+ // Check if Bun is available in PATH
30
+ function isBunAvailable() {
31
+ try {
32
+ execSync('bun --version', { timeout: 3000, stdio: 'pipe' });
33
+ return true;
34
+ }
35
+ catch {
36
+ return false;
37
+ }
38
+ }
39
+ // Shell out to Bun to extract the real buddy bones using Bun.hash (wyhash).
40
+ // Claude Code runs on Bun, so the deterministic RNG seed differs from Node's
41
+ // FNV hash. Since Claude Code users already have Bun installed, we leverage it.
42
+ function extractRealBones() {
43
+ if (!isBunAvailable())
44
+ return null;
45
+ try {
46
+ // Try common locations relative to the project root
47
+ const candidates = [
48
+ path.resolve(process.cwd(), 'source', 'extract-bones.ts'),
49
+ path.resolve(path.dirname(process.argv[1] || ''), '..', 'source', 'extract-bones.ts'),
50
+ ];
51
+ for (const candidate of candidates) {
52
+ if (fs.existsSync(candidate)) {
53
+ const result = execSync(`bun "${candidate}"`, { timeout: 5000, encoding: 'utf-8' });
54
+ const parsed = JSON.parse(result.trim());
55
+ if (!parsed.error)
56
+ return parsed;
57
+ }
58
+ }
59
+ }
60
+ catch (e) { }
61
+ return null;
62
+ }
63
+ export function readLocalBuddy() {
64
+ const configPath = path.join(os.homedir(), '.claude.json');
65
+ if (!fs.existsSync(configPath)) {
66
+ return { buddy: getCompanion('guest-123', GUEST_BUDDY), bunWarning: false };
67
+ }
68
+ try {
69
+ const fileContent = fs.readFileSync(configPath, 'utf-8');
70
+ const parsed = ClaudeConfigSchema.parse(JSON.parse(fileContent));
71
+ const userId = parsed.oauthAccount?.accountUuid ?? parsed.userID ?? 'guest-123';
72
+ const stored = parsed.companion;
73
+ const buddy = getCompanion(userId, stored ?? GUEST_BUDDY);
74
+ // Overlay the real bones from Bun.hash if available
75
+ const realBones = extractRealBones();
76
+ if (realBones) {
77
+ buddy.species = realBones.species;
78
+ buddy.rarity = realBones.rarity;
79
+ buddy.eye = realBones.eye;
80
+ buddy.hat = realBones.hat;
81
+ buddy.shiny = realBones.shiny;
82
+ buddy.stats = realBones.stats;
83
+ }
84
+ return { buddy, bunWarning: !realBones && !!stored };
85
+ }
86
+ catch (error) {
87
+ return { buddy: getCompanion('guest-123', GUEST_BUDDY), bunWarning: false };
88
+ }
89
+ }
90
+ export function getHashedUserId() {
91
+ const configPath = path.join(os.homedir(), '.claude.json');
92
+ let rawId = 'guest-123';
93
+ if (fs.existsSync(configPath)) {
94
+ try {
95
+ const fileContent = fs.readFileSync(configPath, 'utf-8');
96
+ const parsed = ClaudeConfigSchema.parse(JSON.parse(fileContent));
97
+ rawId = parsed.oauthAccount?.accountUuid ?? parsed.userID ?? 'guest-123';
98
+ }
99
+ catch (error) { }
100
+ }
101
+ return crypto.createHash('sha256').update(rawId + "buddy-battle-secret-salt").digest('hex');
102
+ }
@@ -0,0 +1,8 @@
1
+ import { type Companion, type CompanionBones, type StoredCompanion } from './types.js';
2
+ export type Roll = {
3
+ bones: CompanionBones;
4
+ inspirationSeed: number;
5
+ };
6
+ export declare function roll(userId: string): Roll;
7
+ export declare function rollWithSeed(seed: string): Roll;
8
+ export declare function getCompanion(userId: string | undefined, stored: StoredCompanion | undefined): Companion | undefined;
@@ -0,0 +1,97 @@
1
+ import { EYES, HATS, RARITIES, RARITY_WEIGHTS, SPECIES, STAT_NAMES } from './types.js';
2
+ // Mulberry32 — tiny seeded PRNG, good enough for picking ducks
3
+ function mulberry32(seed) {
4
+ let a = seed >>> 0;
5
+ return function () {
6
+ a |= 0;
7
+ a = (a + 0x6d2b79f5) | 0;
8
+ let t = Math.imul(a ^ (a >>> 15), 1 | a);
9
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
10
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
11
+ };
12
+ }
13
+ function hashString(s) {
14
+ if (typeof Bun !== 'undefined') {
15
+ return Number(BigInt(Bun.hash(s)) & 0xffffffffn);
16
+ }
17
+ let h = 2166136261;
18
+ for (let i = 0; i < s.length; i++) {
19
+ h ^= s.charCodeAt(i);
20
+ h = Math.imul(h, 16777619);
21
+ }
22
+ return h >>> 0;
23
+ }
24
+ function pick(rng, arr) {
25
+ return arr[Math.floor(rng() * arr.length)];
26
+ }
27
+ function rollRarity(rng) {
28
+ const total = Object.values(RARITY_WEIGHTS).reduce((a, b) => a + b, 0);
29
+ let roll = rng() * total;
30
+ for (const rarity of RARITIES) {
31
+ roll -= RARITY_WEIGHTS[rarity];
32
+ if (roll < 0)
33
+ return rarity;
34
+ }
35
+ return 'common';
36
+ }
37
+ const RARITY_FLOOR = {
38
+ common: 5,
39
+ uncommon: 15,
40
+ rare: 25,
41
+ epic: 35,
42
+ legendary: 50,
43
+ };
44
+ // One peak stat, one dump stat, rest scattered. Rarity bumps the floor.
45
+ function rollStats(rng, rarity) {
46
+ const floor = RARITY_FLOOR[rarity];
47
+ const peak = pick(rng, STAT_NAMES);
48
+ let dump = pick(rng, STAT_NAMES);
49
+ while (dump === peak)
50
+ dump = pick(rng, STAT_NAMES);
51
+ const stats = {};
52
+ for (const name of STAT_NAMES) {
53
+ if (name === peak) {
54
+ stats[name] = Math.min(100, floor + 50 + Math.floor(rng() * 30));
55
+ }
56
+ else if (name === dump) {
57
+ stats[name] = Math.max(1, floor - 10 + Math.floor(rng() * 15));
58
+ }
59
+ else {
60
+ stats[name] = floor + Math.floor(rng() * 40);
61
+ }
62
+ }
63
+ return stats;
64
+ }
65
+ const SALT = 'friend-2026-401';
66
+ function rollFrom(rng) {
67
+ const rarity = rollRarity(rng);
68
+ const bones = {
69
+ rarity,
70
+ species: pick(rng, SPECIES),
71
+ eye: pick(rng, EYES),
72
+ hat: rarity === 'common' ? 'none' : pick(rng, HATS),
73
+ shiny: rng() < 0.01,
74
+ stats: rollStats(rng, rarity),
75
+ };
76
+ return { bones, inspirationSeed: Math.floor(rng() * 1e9) };
77
+ }
78
+ // Called from three hot paths (500ms sprite tick, per-keystroke PromptInput,
79
+ // per-turn observer) with the same userId → cache the deterministic result.
80
+ let rollCache;
81
+ export function roll(userId) {
82
+ const key = userId + SALT;
83
+ if (rollCache?.key === key)
84
+ return rollCache.value;
85
+ const value = rollFrom(mulberry32(hashString(key)));
86
+ rollCache = { key, value };
87
+ return value;
88
+ }
89
+ export function rollWithSeed(seed) {
90
+ return rollFrom(mulberry32(hashString(seed)));
91
+ }
92
+ export function getCompanion(userId, stored) {
93
+ if (!stored || !userId)
94
+ return undefined;
95
+ const { bones } = roll(userId);
96
+ return { ...stored, ...bones };
97
+ }
@@ -0,0 +1,4 @@
1
+ import type { CompanionBones, Species } from './types.js';
2
+ export declare function renderSprite(bones: CompanionBones, frame?: number): string[];
3
+ export declare function spriteFrameCount(species: Species): number;
4
+ export declare function renderFace(bones: CompanionBones): string;