buddy-battle 1.0.0 → 1.0.2

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.js CHANGED
@@ -42,15 +42,14 @@ export default function App() {
42
42
  }
43
43
  return (React.createElement(Box, { flexDirection: "column", padding: 1 },
44
44
  React.createElement(Box, { marginBottom: 1, flexDirection: "column" },
45
- React.createElement(Text, { color: "gray" },
45
+ React.createElement(Text, { dimColor: true },
46
46
  "Logged in as ",
47
47
  trainerName ?? buddy.name,
48
48
  " (",
49
49
  buddy.rarity,
50
50
  " ",
51
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"))),
52
+ ")")),
54
53
  view === 'loading' && (React.createElement(Text, { color: "cyan" }, "Authenticating profile...")),
55
54
  view === 'setup' && (React.createElement(Setup, { errorMsg: setupError, onComplete: async (name) => {
56
55
  setSetupError(undefined);
@@ -63,7 +62,7 @@ export default function App() {
63
62
  setSetupError(`Trainer name '${name}' is already taken. Please try another!`);
64
63
  }
65
64
  } })),
66
- view === 'menu' && (React.createElement(Menu, { onSelect: (opt) => setView(opt) })),
65
+ view === 'menu' && (React.createElement(Menu, { onSelect: (opt) => setView(opt), bunWarning: bunWarning })),
67
66
  view === 'leaderboard' && (React.createElement(Leaderboard, { onBack: () => setView('menu') })),
68
67
  view === 'matchmaking' && (React.createElement(Matchmaking, { myBuddy: sanitizeBuddyForNetwork(buddy, trainerName), onCancel: () => setView('menu'), onMatch: (opp, id) => {
69
68
  setOpponent(opp);
@@ -26,21 +26,31 @@ const GUEST_BUDDY = {
26
26
  personality: 'Mysterious and ready to battle',
27
27
  hatchedAt: Date.now()
28
28
  };
29
- // Check if Bun is available in PATH
30
- function isBunAvailable() {
29
+ // Find Bun path if available, checking PATH then ~/.bun/bin/bun directly
30
+ function getBunPath() {
31
31
  try {
32
32
  execSync('bun --version', { timeout: 3000, stdio: 'pipe' });
33
- return true;
33
+ return 'bun';
34
34
  }
35
- catch {
36
- return false;
35
+ catch { }
36
+ const isWin = os.platform() === 'win32';
37
+ const bunExecutable = isWin ? 'bun.exe' : 'bun';
38
+ const homeBunPath = path.join(os.homedir(), '.bun', 'bin', bunExecutable);
39
+ if (fs.existsSync(homeBunPath)) {
40
+ try {
41
+ execSync(`"${homeBunPath}" --version`, { timeout: 3000, stdio: 'pipe' });
42
+ return homeBunPath;
43
+ }
44
+ catch { }
37
45
  }
46
+ return null;
38
47
  }
39
48
  // Shell out to Bun to extract the real buddy bones using Bun.hash (wyhash).
40
49
  // Claude Code runs on Bun, so the deterministic RNG seed differs from Node's
41
50
  // FNV hash. Since Claude Code users already have Bun installed, we leverage it.
42
51
  function extractRealBones() {
43
- if (!isBunAvailable())
52
+ const bunPath = getBunPath();
53
+ if (!bunPath)
44
54
  return null;
45
55
  try {
46
56
  // Try common locations relative to the project root
@@ -50,7 +60,7 @@ function extractRealBones() {
50
60
  ];
51
61
  for (const candidate of candidates) {
52
62
  if (fs.existsSync(candidate)) {
53
- const result = execSync(`bun "${candidate}"`, { timeout: 5000, encoding: 'utf-8' });
63
+ const result = execSync(`"${bunPath}" "${candidate}"`, { timeout: 5000, encoding: 'utf-8' });
54
64
  const parsed = JSON.parse(result.trim());
55
65
  if (!parsed.error)
56
66
  return parsed;
@@ -59,7 +59,7 @@ export declare const RARITY_STARS: {
59
59
  readonly legendary: "★★★★★";
60
60
  };
61
61
  export declare const RARITY_COLORS: {
62
- readonly common: "gray";
62
+ readonly common: "white";
63
63
  readonly uncommon: "green";
64
64
  readonly rare: "cyan";
65
65
  readonly epic: "magenta";
@@ -24,4 +24,4 @@ export const HATS = ['none', 'crown', 'tophat', 'propeller', 'halo', 'wizard', '
24
24
  export const STAT_NAMES = ['DEBUGGING', 'PATIENCE', 'CHAOS', 'WISDOM', 'SNARK'];
25
25
  export const RARITY_WEIGHTS = { common: 60, uncommon: 25, rare: 10, epic: 4, legendary: 1 };
26
26
  export const RARITY_STARS = { common: '★', uncommon: '★★', rare: '★★★', epic: '★★★★', legendary: '★★★★★' };
27
- export const RARITY_COLORS = { common: 'gray', uncommon: 'green', rare: 'cyan', epic: 'magenta', legendary: 'yellow' };
27
+ export const RARITY_COLORS = { common: 'white', uncommon: 'green', rare: 'cyan', epic: 'magenta', legendary: 'yellow' };
@@ -22,7 +22,7 @@ export type NetworkBuddy = {
22
22
  };
23
23
  };
24
24
  export declare function sanitizeBuddyForNetwork(myBuddy: Companion, trainerName: string): NetworkBuddy;
25
- export declare function joinMatchmaking(myBuddy: NetworkBuddy, onMatchFound: (opponent: NetworkBuddy, battleId: string) => void): RealtimeChannel;
25
+ export declare function joinMatchmaking(myBuddy: NetworkBuddy, onMatchFound: (opponent: NetworkBuddy, battleId: string) => void, onConnected?: () => void): RealtimeChannel;
26
26
  export declare function joinBattle(battleId: string, onActionReceived: (payloadData: any) => void, onOpponentDisconnect: () => void): RealtimeChannel;
27
27
  export declare function fetchCloudProfile(hashedId: string): Promise<string | null>;
28
28
  export declare function createCloudProfile(hashedId: string, trainerName: string): Promise<boolean>;
@@ -26,7 +26,7 @@ export function sanitizeBuddyForNetwork(myBuddy, trainerName) {
26
26
  stats: myBuddy.stats,
27
27
  };
28
28
  }
29
- export function joinMatchmaking(myBuddy, onMatchFound) {
29
+ export function joinMatchmaking(myBuddy, onMatchFound, onConnected) {
30
30
  debugLog(`Joining matchmaking as ${myBuddy.trainerName} (${SESSION_ID})`);
31
31
  const channel = supabase.channel('room:matchmaking-lobby');
32
32
  channel
@@ -57,6 +57,8 @@ export function joinMatchmaking(myBuddy, onMatchFound) {
57
57
  .subscribe(async (status, err) => {
58
58
  debugLog(`Matchmaking subscribe status: ${status} | err: ${err}`);
59
59
  if (status === 'SUBSCRIBED') {
60
+ if (onConnected)
61
+ onConnected();
60
62
  // Pre-register our battle token so opponents can claim victory if we lose/quit
61
63
  await supabase.rpc('create_battle_token', {
62
64
  h_id: getHashedUserId(),
package/dist/ui/Arena.js CHANGED
@@ -251,10 +251,10 @@ export function Arena({ myBuddy, myTrainerName, opponent, battleId, onLeave }) {
251
251
  React.createElement(BattleProjectiles, { activeMove: projectiles?.move || null, isPlayer1: projectiles?.p1 || false }),
252
252
  React.createElement(BuddySprite, { buddy: opponent, color: "cyan", isHit: p2Hit, isAttacking: p2Attacking })),
253
253
  React.createElement(Box, { flexDirection: "column", marginTop: 1 },
254
- React.createElement(Box, { flexDirection: "column", width: "100%", height: 11, padding: 1, borderStyle: "round", borderColor: "gray" },
254
+ React.createElement(Box, { flexDirection: "column", width: "100%", height: 11, padding: 1, borderStyle: "round" },
255
255
  !gameOver && (React.createElement(Box, { marginBottom: 1 },
256
- React.createElement(Text, { bold: true, color: isMyTurn ? 'yellow' : 'gray' }, isMyTurn ? `Your Turn! (${timeLeft}s)` : `Waiting... (${timeLeft}s)`))),
257
- isMyTurn && !gameOver && (React.createElement(Box, { flexDirection: "column" }, moves.map((m, i) => (React.createElement(Text, { key: m.name, color: cursor === i ? 'white' : 'gray' },
256
+ React.createElement(Text, { bold: true, color: isMyTurn ? 'yellow' : undefined, dimColor: !isMyTurn }, isMyTurn ? `Your Turn! (${timeLeft}s)` : `Waiting... (${timeLeft}s)`))),
257
+ isMyTurn && !gameOver && (React.createElement(Box, { flexDirection: "column" }, moves.map((m, i) => (React.createElement(Text, { key: m.name, color: cursor === i ? 'whiteBright' : undefined, dimColor: cursor !== i },
258
258
  cursor === i ? '> ' : ' ',
259
259
  m.name,
260
260
  " ",
@@ -2,17 +2,26 @@ import React, { useEffect, useState, useRef } from 'react';
2
2
  import { Box, Text, useInput } from 'ink';
3
3
  import { joinMatchmaking, cleanupChannel, debugLog } from '../network/client.js';
4
4
  export function Matchmaking({ myBuddy, onMatch, onCancel }) {
5
- const [status, setStatus] = useState("Connecting to lobby...");
5
+ const [statusBase, setStatusBase] = useState("Connecting to lobby");
6
+ const [tick, setTick] = useState(0);
6
7
  const matched = useRef(false);
8
+ useEffect(() => {
9
+ const timer = setInterval(() => setTick(t => t + 1), 400);
10
+ return () => clearInterval(timer);
11
+ }, []);
7
12
  useEffect(() => {
8
13
  debugLog("Matchmaking component mounted");
9
14
  const channel = joinMatchmaking(myBuddy, (opponent, battleId) => {
10
15
  if (matched.current)
11
16
  return;
12
17
  matched.current = true;
13
- setStatus(`Found opponent: ${opponent.trainerName}. Joining match...`);
18
+ setStatusBase(`Found opponent: ${opponent.trainerName}. Joining match`);
14
19
  // Wait 2 seconds before executing onMatch so that any broadcasts have time to transmit
15
20
  setTimeout(() => onMatch(opponent, battleId), 2000);
21
+ }, () => {
22
+ if (!matched.current) {
23
+ setStatusBase("Awaiting opponent");
24
+ }
16
25
  });
17
26
  return () => {
18
27
  debugLog("Matchmaking component unmounted");
@@ -25,6 +34,10 @@ export function Matchmaking({ myBuddy, onMatch, onCancel }) {
25
34
  onCancel();
26
35
  }
27
36
  });
37
+ const dotFrames = [' ', '. ', '.. ', '...'];
38
+ const dots = dotFrames[tick % 4];
28
39
  return (React.createElement(Box, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "cyan", width: 80, minHeight: 28, alignItems: "center", justifyContent: "center" },
29
- React.createElement(Text, { color: "cyan" }, status)));
40
+ React.createElement(Text, { color: "cyan" },
41
+ statusBase,
42
+ dots)));
30
43
  }
package/dist/ui/Menu.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
- export declare function Menu({ onSelect }: {
2
+ export declare function Menu({ onSelect, bunWarning }: {
3
3
  onSelect: (val: string) => void;
4
+ bunWarning?: boolean;
4
5
  }): React.JSX.Element;
package/dist/ui/Menu.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import React, { useState, useEffect } from 'react';
2
2
  import { Box, Text, useInput } from 'ink';
3
- export function Menu({ onSelect }) {
3
+ export function Menu({ onSelect, bunWarning }) {
4
4
  const options = [
5
5
  { label: 'Find Online Match', value: 'matchmaking' },
6
6
  { label: 'View Leaderboard', value: 'leaderboard' },
@@ -46,7 +46,9 @@ export function Menu({ onSelect }) {
46
46
  rumbleEnd))),
47
47
  React.createElement(Box, { flexDirection: "column", marginTop: 2 }, options.map((opt, i) => (React.createElement(Text, { key: opt.value },
48
48
  i === index ? (React.createElement(Text, { color: "yellowBright", bold: true }, '►► ')) : (' '),
49
- React.createElement(Text, { color: i === index ? 'white' : 'gray', bold: i === index }, opt.label))))),
50
- React.createElement(Box, { marginTop: 3 },
51
- React.createElement(Text, { dimColor: true }, "Use \u2191/\u2193 arrows to navigate, Enter to select."))));
49
+ React.createElement(Text, { color: i === index ? 'whiteBright' : undefined, dimColor: i !== index, bold: i === index }, opt.label))))),
50
+ React.createElement(Box, { marginTop: 3, flexDirection: "column", alignItems: "center" },
51
+ React.createElement(Text, { dimColor: true }, "Use \u2191/\u2193 arrows to navigate, Enter to select."),
52
+ bunWarning && (React.createElement(Box, { marginTop: 1 },
53
+ React.createElement(Text, { color: "yellow" }, "\u26A0 Bun not found \u2014 buddy stats may be inaccurate. Install Bun: curl -fsSL https://bun.sh/install | bash"))))));
52
54
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buddy-battle",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "license": "MIT",
5
5
  "bin": "dist/cli.js",
6
6
  "type": "module",
package/readme.md CHANGED
@@ -8,80 +8,38 @@ Buddy Battle brings your Claude Code companion to life in a real-time, turn-base
8
8
 
9
9
  ## ✨ Features
10
10
 
11
- - **🎮 Real-Time PvP** — Fight other Claude Code users over Supabase Realtime
11
+ - **🎮 Real-Time PvP** — Fight other Claude Code users globally over Supabase Realtime
12
12
  - **🧬 Deterministic Buddies** — Your buddy's species, rarity, and stats are derived from your Anthropic account via `Bun.hash`
13
- - **⚔️ 4-Move Combat System** — Debug Smash, Chaos Bomb, Firewall, and Reboot — each powered by your buddy's unique stats
13
+ - **⚔️ 4-Move Combat System** — Debug Smash, Chaos Bomb, Firewall, and Reboot — each scaled mathematically to your buddy's unique stats
14
14
  - **💥 Battle Animations** — Projectiles fly across the terminal, hits shake the screen
15
- - **🏆 Global Leaderboard** — ELO-tracked rankings stored in Supabase Postgres
16
- - **🔐 Cloud Profiles** — Choose a Trainer Name, synced across all your machines via SHA-256 hashed Anthropic UUID
17
- - **📡 Disconnect Protection** — Supabase Presence detects ragequits and awards the survivor a win
18
- - **🛡️ Privacy First** — No Anthropic credentials or account IDs ever leave your machine
19
-
20
- ## 📋 Prerequisites
21
-
22
- - **[Claude Code](https://docs.anthropic.com/en/docs/claude-code)** — You need a hatched buddy (`/buddy` command)
23
- - **[Node.js](https://nodejs.org/) 18+**
24
- - **[Bun](https://bun.sh/)** — Required for accurate buddy stat extraction (Claude Code already installs this)
25
- - **A Supabase project** — Free tier works fine
15
+ - **🏆 Global Leaderboard** — ELO-tracked rankings
16
+ - **🔗 X (Twitter) Integration** — Brag about your knockout victories directly to X from the terminal
17
+ - **🔐 Secure Architecture** — Match validation tokens prevent forged wins, and your real Anthropic UUID is SHA-256 hashed to protect your identity.
26
18
 
27
19
  ## 🚀 Quick Start
28
20
 
29
- ### 1. Clone & Install
21
+ Ensure you have [Claude Code](https://docs.anthropic.com/en/docs/claude-code) installed and have hatched a buddy using the `/buddy` command.
30
22
 
31
- ```bash
32
- git clone https://github.com/Grenghis-Khan/buddy-battle.git
33
- cd buddy-battle
34
- npm install
35
- ```
36
-
37
- ### 2. Set Up Supabase
38
-
39
- Create a new [Supabase](https://supabase.com) project, then run this SQL in the SQL Editor:
40
-
41
- ```sql
42
- CREATE TABLE public.leaderboard (
43
- id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
44
- trainer_name text UNIQUE NOT NULL,
45
- hashed_id text UNIQUE,
46
- wins integer DEFAULT 0,
47
- losses integer DEFAULT 0,
48
- elo integer DEFAULT 1000,
49
- created_at timestamptz DEFAULT now()
50
- );
51
-
52
- ALTER TABLE public.leaderboard ENABLE ROW LEVEL SECURITY;
53
- CREATE POLICY "Public read" ON public.leaderboard FOR SELECT USING (true);
54
- CREATE POLICY "Public insert" ON public.leaderboard FOR INSERT WITH CHECK (true);
55
- CREATE POLICY "Public update" ON public.leaderboard FOR UPDATE USING (true);
56
- ```
57
-
58
- ### 3. Configure Environment
23
+ Then just jump right into the arena:
59
24
 
60
25
  ```bash
61
- cp .env.example .env
26
+ npx buddy-battle
62
27
  ```
63
28
 
64
- Edit `.env` with your Supabase project URL and anon key (found in Settings API).
65
-
66
- ### 4. Build & Play!
67
-
68
- ```bash
69
- npm run build
70
- node dist/cli.js
71
- ```
29
+ *Note: For the most accurate buddy stats, ensure [Bun](https://bun.sh/) is installed on your machine so the client can perfectly match Anthropic's internal hashing algorithm.*
72
30
 
73
31
  ## 🎯 Combat System
74
32
 
33
+ Every match is different because every buddy has unique stats! Combat has been completely mathematically balanced to ensure that even a level 100 stat won't instantly one-shot a level 0 buddy, but it will give you a major thematic advantage.
34
+
75
35
  | Move | Stat | Effect |
76
36
  |------|------|--------|
77
- | **Debug Smash** | DEBUGGING | Reliable damage |
78
- | **Chaos Bomb** | CHAOS | Massive damage, 30% miss chance |
79
- | **Firewall** | WISDOM | Blocks 50% of next incoming hit |
80
- | **Reboot** | PATIENCE | Heals your buddy |
37
+ | **Debug Smash** | DEBUGGING | Fast, highly reliable light attack. |
38
+ | **Chaos Bomb** | CHAOS | Heavy attack with a high chance to miss. |
39
+ | **Firewall** | WISDOM | Defensive block. The higher your wisdom, the larger the percentage of the next incoming hit you block. |
40
+ | **Reboot** | PATIENCE | Heals your buddy for a burst of HP. |
81
41
 
82
- Each buddy's stats are unique and deterministic — a high-CHAOS buddy will devastate with Chaos Bombs, while a high-PATIENCE buddy can outlast opponents with Reboots.
83
-
84
- ## 🏗️ Architecture
42
+ ## 🏗️ Technical Architecture
85
43
 
86
44
  ```
87
45
  buddy-battle/
@@ -92,33 +50,20 @@ buddy-battle/
92
50
  │ ├── config/
93
51
  │ │ └── reader.ts # Reads ~/.claude.json + shells out to Bun
94
52
  │ ├── engine/
95
- │ │ ├── companion.ts # Deterministic buddy generation (mirrored from Claude Code)
96
53
  │ │ ├── types.ts # Species, rarities, stats
97
54
  │ │ └── sprites.ts # ASCII art for all 18 species
98
55
  │ ├── network/
99
- │ │ └── client.ts # Supabase Realtime + Presence + Cloud Profiles
56
+ │ │ └── client.ts # Supabase RPCs + Match Validators
100
57
  │ └── ui/
101
58
  │ ├── Arena.tsx # Battle screen with animations
102
- │ ├── Menu.tsx # Main menu
103
- ├── Matchmaking.tsx # Lobby + opponent discovery
104
- │ ├── Leaderboard.tsx # Global rankings
105
- │ ├── Setup.tsx # First-time Trainer Name registration
106
- │ └── Shared.tsx # Sprites + projectile components
59
+ │ ├── Matchmaking.tsx # Lobby + Match Validator Handshake
60
+ └── ...
107
61
  ```
108
62
 
109
- ### Key Technical Decisions
110
-
111
- - **Bun.hash bridge**: Claude Code runs on Bun and uses `Bun.hash()` (wyhash) to seed buddy generation. Since Node.js uses a different hash, we shell out to `bun source/extract-bones.ts` at boot to get bit-identical results.
112
- - **SHA-256 identity**: Your Anthropic `accountUuid` is hashed before touching the network — Supabase never sees your real account ID.
113
- - **Peer-to-peer combat**: No server-side game logic. Both clients independently compute damage from broadcast payloads, keeping the architecture simple.
63
+ ### Key Security Decisions
114
64
 
115
- ## ⚠️ Bun Warning
116
-
117
- If Bun is not installed, the game will still work but your buddy's species, rarity, and combat stats may not match what Claude Code shows. Install Bun to fix this:
118
-
119
- ```bash
120
- curl -fsSL https://bun.sh/install | bash
121
- ```
65
+ - **Match Validator Handshake**: To prevent ELO spoofing, players generate and exchange a cryptographic `battle_token` upon connecting. The winner must submit their opponent's token to securely claim the win via a locked-down PostgreSQL RPC.
66
+ - **Embedded Client**: Server-side credentials are safely embedded right in the package — zero setup required for players. The database is strictly protected by Row Level Security (RLS) so the `anon` key cannot be abused.
122
67
 
123
68
  ## 📄 License
124
69