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 +2 -0
- package/dist/app.js +74 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +5 -0
- package/dist/config/reader.d.ts +8 -0
- package/dist/config/reader.js +102 -0
- package/dist/engine/companion.d.ts +8 -0
- package/dist/engine/companion.js +97 -0
- package/dist/engine/sprites.d.ts +4 -0
- package/dist/engine/sprites.js +488 -0
- package/dist/engine/types.d.ts +67 -0
- package/dist/engine/types.js +27 -0
- package/dist/extract-bones.d.ts +1 -0
- package/dist/extract-bones.js +22 -0
- package/dist/network/client.d.ts +30 -0
- package/dist/network/client.js +141 -0
- package/dist/ui/Arena.d.ts +8 -0
- package/dist/ui/Arena.js +276 -0
- package/dist/ui/Leaderboard.d.ts +4 -0
- package/dist/ui/Leaderboard.js +40 -0
- package/dist/ui/Matchmaking.d.ts +7 -0
- package/dist/ui/Matchmaking.js +30 -0
- package/dist/ui/Menu.d.ts +4 -0
- package/dist/ui/Menu.js +52 -0
- package/dist/ui/Setup.d.ts +5 -0
- package/dist/ui/Setup.js +23 -0
- package/dist/ui/Shared.d.ts +12 -0
- package/dist/ui/Shared.js +75 -0
- package/package.json +60 -0
- package/readme.md +125 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { renderSprite } from '../engine/sprites.js';
|
|
4
|
+
export function BuddySprite({ buddy, color = "yellow", isAttacking = false, isHit = false }) {
|
|
5
|
+
const [frame, setFrame] = useState(0);
|
|
6
|
+
const [shakeOffset, setShakeOffset] = useState(0);
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
const t = setInterval(() => setFrame(f => f + 1), 1000);
|
|
9
|
+
return () => clearInterval(t);
|
|
10
|
+
}, []);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
let t;
|
|
13
|
+
if (isHit) {
|
|
14
|
+
let ticks = 0;
|
|
15
|
+
t = setInterval(() => {
|
|
16
|
+
ticks++;
|
|
17
|
+
setShakeOffset(ticks % 2 === 0 ? 2 : 0);
|
|
18
|
+
}, 100);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
setShakeOffset(0);
|
|
22
|
+
}
|
|
23
|
+
return () => clearInterval(t);
|
|
24
|
+
}, [isHit]);
|
|
25
|
+
const lines = renderSprite(buddy, frame);
|
|
26
|
+
const renderColor = isAttacking ? "cyanBright" : (isHit ? "redBright" : color);
|
|
27
|
+
return (React.createElement(Box, { flexDirection: "column", alignItems: "center" }, lines.map((l, i) => {
|
|
28
|
+
const prefix = " ".repeat(shakeOffset);
|
|
29
|
+
return React.createElement(Text, { key: i, color: renderColor, bold: isAttacking },
|
|
30
|
+
prefix,
|
|
31
|
+
l);
|
|
32
|
+
})));
|
|
33
|
+
}
|
|
34
|
+
export function BattleProjectiles({ activeMove, isPlayer1 }) {
|
|
35
|
+
const [tick, setTick] = useState(0);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (!activeMove) {
|
|
38
|
+
setTick(0);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const timer = setInterval(() => setTick(t => t + 1), 60);
|
|
42
|
+
return () => clearInterval(timer);
|
|
43
|
+
}, [activeMove]);
|
|
44
|
+
if (!activeMove)
|
|
45
|
+
return React.createElement(Box, { width: 30, justifyContent: "center", alignItems: "center" },
|
|
46
|
+
React.createElement(Text, null, " "));
|
|
47
|
+
let avatar = "*";
|
|
48
|
+
let color = "whiteBright";
|
|
49
|
+
if (activeMove === 'Chaos Bomb') {
|
|
50
|
+
avatar = "◉";
|
|
51
|
+
color = "magentaBright";
|
|
52
|
+
}
|
|
53
|
+
else if (activeMove.includes('Smash')) {
|
|
54
|
+
avatar = "☄️";
|
|
55
|
+
color = "yellowBright";
|
|
56
|
+
}
|
|
57
|
+
else if (activeMove === 'Firewall') {
|
|
58
|
+
avatar = "🛡️";
|
|
59
|
+
color = "cyanBright";
|
|
60
|
+
}
|
|
61
|
+
else if (activeMove === 'Reboot') {
|
|
62
|
+
avatar = "➕";
|
|
63
|
+
color = "greenBright";
|
|
64
|
+
}
|
|
65
|
+
const isSelfCast = activeMove === 'Firewall' || activeMove === 'Reboot';
|
|
66
|
+
const maxDistance = 26;
|
|
67
|
+
let distance = Math.min(tick * 3, maxDistance);
|
|
68
|
+
if (isSelfCast) {
|
|
69
|
+
// Self-cast moves don't cross the field, they hover next to the caster!
|
|
70
|
+
distance = tick % 2 === 0 ? 0 : 2;
|
|
71
|
+
}
|
|
72
|
+
const padding = isPlayer1 ? distance : (maxDistance - distance);
|
|
73
|
+
return (React.createElement(Box, { width: 30, paddingLeft: Math.max(0, padding), justifyContent: "flex-start", alignItems: "center" },
|
|
74
|
+
React.createElement(Text, { bold: true, color: color }, avatar)));
|
|
75
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "buddy-battle",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"bin": "dist/cli.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=16"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"dev": "tsc --watch",
|
|
13
|
+
"test": "prettier --check . && xo && ava"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@supabase/supabase-js": "^2.101.1",
|
|
20
|
+
"dotenv": "^17.3.1",
|
|
21
|
+
"ink": "^4.1.0",
|
|
22
|
+
"ink-text-input": "^5.0.1",
|
|
23
|
+
"meow": "^11.0.0",
|
|
24
|
+
"open": "^11.0.0",
|
|
25
|
+
"react": "^18.2.0",
|
|
26
|
+
"zod": "^4.3.6"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@sindresorhus/tsconfig": "^3.0.1",
|
|
30
|
+
"@types/react": "^18.0.32",
|
|
31
|
+
"@vdemedes/prettier-config": "^2.0.1",
|
|
32
|
+
"ava": "^5.2.0",
|
|
33
|
+
"chalk": "^5.2.0",
|
|
34
|
+
"eslint-config-xo-react": "^0.27.0",
|
|
35
|
+
"eslint-plugin-react": "^7.32.2",
|
|
36
|
+
"eslint-plugin-react-hooks": "^4.6.0",
|
|
37
|
+
"ink-testing-library": "^3.0.0",
|
|
38
|
+
"prettier": "^2.8.7",
|
|
39
|
+
"ts-node": "^10.9.1",
|
|
40
|
+
"typescript": "^5.0.3",
|
|
41
|
+
"xo": "^0.53.1"
|
|
42
|
+
},
|
|
43
|
+
"ava": {
|
|
44
|
+
"extensions": {
|
|
45
|
+
"ts": "module",
|
|
46
|
+
"tsx": "module"
|
|
47
|
+
},
|
|
48
|
+
"nodeArguments": [
|
|
49
|
+
"--loader=ts-node/esm"
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
"xo": {
|
|
53
|
+
"extends": "xo-react",
|
|
54
|
+
"prettier": true,
|
|
55
|
+
"rules": {
|
|
56
|
+
"react/prop-types": "off"
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"prettier": "@vdemedes/prettier-config"
|
|
60
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# 🤖 Buddy Battle
|
|
2
|
+
|
|
3
|
+
**PvP Arena Combat for Claude Code Buddies**
|
|
4
|
+
|
|
5
|
+
Buddy Battle brings your Claude Code companion to life in a real-time, turn-based terminal fighting game. Your buddy's species, rarity, and stats are pulled directly from your Claude Code installation — the same little creature that sits beside your prompt is now your champion in the arena.
|
|
6
|
+
|
|
7
|
+
  
|
|
8
|
+
|
|
9
|
+
## ✨ Features
|
|
10
|
+
|
|
11
|
+
- **🎮 Real-Time PvP** — Fight other Claude Code users over Supabase Realtime
|
|
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
|
|
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
|
|
26
|
+
|
|
27
|
+
## 🚀 Quick Start
|
|
28
|
+
|
|
29
|
+
### 1. Clone & Install
|
|
30
|
+
|
|
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
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
cp .env.example .env
|
|
62
|
+
```
|
|
63
|
+
|
|
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
|
+
```
|
|
72
|
+
|
|
73
|
+
## 🎯 Combat System
|
|
74
|
+
|
|
75
|
+
| Move | Stat | Effect |
|
|
76
|
+
|------|------|--------|
|
|
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 |
|
|
81
|
+
|
|
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
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
buddy-battle/
|
|
88
|
+
├── source/
|
|
89
|
+
│ ├── app.tsx # Root application + state machine
|
|
90
|
+
│ ├── cli.tsx # Entry point
|
|
91
|
+
│ ├── extract-bones.ts # Bun script for accurate stat extraction
|
|
92
|
+
│ ├── config/
|
|
93
|
+
│ │ └── reader.ts # Reads ~/.claude.json + shells out to Bun
|
|
94
|
+
│ ├── engine/
|
|
95
|
+
│ │ ├── companion.ts # Deterministic buddy generation (mirrored from Claude Code)
|
|
96
|
+
│ │ ├── types.ts # Species, rarities, stats
|
|
97
|
+
│ │ └── sprites.ts # ASCII art for all 18 species
|
|
98
|
+
│ ├── network/
|
|
99
|
+
│ │ └── client.ts # Supabase Realtime + Presence + Cloud Profiles
|
|
100
|
+
│ └── ui/
|
|
101
|
+
│ ├── 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
|
|
107
|
+
```
|
|
108
|
+
|
|
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.
|
|
114
|
+
|
|
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
|
+
```
|
|
122
|
+
|
|
123
|
+
## 📄 License
|
|
124
|
+
|
|
125
|
+
MIT
|