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 +3 -4
- package/dist/config/reader.js +17 -7
- package/dist/engine/types.d.ts +1 -1
- package/dist/engine/types.js +1 -1
- package/dist/network/client.d.ts +1 -1
- package/dist/network/client.js +3 -1
- package/dist/ui/Arena.js +3 -3
- package/dist/ui/Matchmaking.js +16 -3
- package/dist/ui/Menu.d.ts +2 -1
- package/dist/ui/Menu.js +6 -4
- package/package.json +1 -1
- package/readme.md +22 -77
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, {
|
|
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);
|
package/dist/config/reader.js
CHANGED
|
@@ -26,21 +26,31 @@ const GUEST_BUDDY = {
|
|
|
26
26
|
personality: 'Mysterious and ready to battle',
|
|
27
27
|
hatchedAt: Date.now()
|
|
28
28
|
};
|
|
29
|
-
//
|
|
30
|
-
function
|
|
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
|
|
33
|
+
return 'bun';
|
|
34
34
|
}
|
|
35
|
-
catch {
|
|
36
|
-
|
|
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
|
-
|
|
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(`
|
|
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;
|
package/dist/engine/types.d.ts
CHANGED
package/dist/engine/types.js
CHANGED
|
@@ -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: '
|
|
27
|
+
export const RARITY_COLORS = { common: 'white', uncommon: 'green', rare: 'cyan', epic: 'magenta', legendary: 'yellow' };
|
package/dist/network/client.d.ts
CHANGED
|
@@ -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>;
|
package/dist/network/client.js
CHANGED
|
@@ -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"
|
|
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' :
|
|
257
|
-
isMyTurn && !gameOver && (React.createElement(Box, { flexDirection: "column" }, moves.map((m, i) => (React.createElement(Text, { key: m.name, color: cursor === i ? '
|
|
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
|
" ",
|
package/dist/ui/Matchmaking.js
CHANGED
|
@@ -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 [
|
|
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
|
-
|
|
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" },
|
|
40
|
+
React.createElement(Text, { color: "cyan" },
|
|
41
|
+
statusBase,
|
|
42
|
+
dots)));
|
|
30
43
|
}
|
package/dist/ui/Menu.d.ts
CHANGED
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 ? '
|
|
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
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
|
|
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
|
|
16
|
-
-
|
|
17
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
+
npx buddy-battle
|
|
62
27
|
```
|
|
63
28
|
|
|
64
|
-
|
|
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 |
|
|
78
|
-
| **Chaos Bomb** | CHAOS |
|
|
79
|
-
| **Firewall** | WISDOM |
|
|
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
|
-
|
|
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
|
|
56
|
+
│ │ └── client.ts # Supabase RPCs + Match Validators
|
|
100
57
|
│ └── ui/
|
|
101
58
|
│ ├── Arena.tsx # Battle screen with animations
|
|
102
|
-
│ ├──
|
|
103
|
-
│
|
|
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
|
|
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
|
-
|
|
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
|
|