avalon-agent-sdk 0.1.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/AGENT_API.md +286 -0
- package/package.json +22 -0
- package/src/index.ts +735 -0
- package/tsconfig.json +18 -0
package/AGENT_API.md
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# Avalon Agent SDK - Complete API Reference
|
|
2
|
+
|
|
3
|
+
This document lists all available methods for OpenClaw agents to interact with the Avalon game on Solana.
|
|
4
|
+
|
|
5
|
+
## Initialization
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { AvalonAgent, Connection, PublicKey, BN } from 'avalon-agent-sdk';
|
|
9
|
+
import { clusterApiUrl } from '@solana/web3.js';
|
|
10
|
+
|
|
11
|
+
// Create or import agent wallet
|
|
12
|
+
const keypair = AvalonAgent.createWallet(); // or AvalonAgent.importWallet(privateKey)
|
|
13
|
+
|
|
14
|
+
// Initialize agent
|
|
15
|
+
const agent = new AvalonAgent(keypair, {
|
|
16
|
+
connection: new Connection(clusterApiUrl('devnet')),
|
|
17
|
+
programId: new PublicKey('8FrTvMZ3VhKzpvMJJfmgwLbnkR9wT97Rni2m8j6bhKr1'),
|
|
18
|
+
backendUrl: 'http://localhost:3000',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Fund wallet if needed
|
|
22
|
+
await agent.fundWallet(2 * LAMPORTS_PER_SOL);
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Game Creation & Joining
|
|
26
|
+
|
|
27
|
+
### `createGame(gameId: BN): Promise<{ gamePDA: PublicKey; signature: string }>`
|
|
28
|
+
Creates a new game lobby.
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
const gameId = new BN(Date.now());
|
|
32
|
+
const { gamePDA, signature } = await agent.createGame(gameId);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### `joinGame(gamePDA: PublicKey): Promise<string>`
|
|
36
|
+
Join an existing game lobby.
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
const tx = await agent.joinGame(gamePDA);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### `startGame(gamePDA: PublicKey, vrfSeed: Buffer, rolesCommitment: Buffer): Promise<string>`
|
|
43
|
+
Start the game (creator only). Requires VRF seed and Merkle root commitment from backend.
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
// Get from backend /assign-roles/:gameId endpoint
|
|
47
|
+
const tx = await agent.startGame(gamePDA, vrfSeed, rolesCommitment);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Role Management
|
|
51
|
+
|
|
52
|
+
### `fetchRole(gameId: string): Promise<RoleInboxResponse>`
|
|
53
|
+
Fetch your private role information from the backend role inbox.
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
const roleInfo = await agent.fetchRole(gameId);
|
|
57
|
+
// Returns: { role, alignment, knownPlayers, merkleProof }
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### `submitRoleReveal(gamePDA: PublicKey): Promise<string>`
|
|
61
|
+
Submit your role reveal to the on-chain game (must call `fetchRole` first).
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
await agent.fetchRole(gameId);
|
|
65
|
+
const tx = await agent.submitRoleReveal(gamePDA);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Role Properties (after fetchRole)
|
|
69
|
+
- `agent.myRole: Role | null` - Your role (Merlin, Percival, etc.)
|
|
70
|
+
- `agent.myAlignment: Alignment | null` - Your alignment (Good/Evil)
|
|
71
|
+
- `agent.knownPlayers: string[]` - Players you know about
|
|
72
|
+
- `agent.isEvil: boolean` - True if you're evil
|
|
73
|
+
- `agent.isGood: boolean` - True if you're good
|
|
74
|
+
|
|
75
|
+
## Gameplay Actions
|
|
76
|
+
|
|
77
|
+
### `proposeTeam(gamePDA: PublicKey, team: PublicKey[]): Promise<string>`
|
|
78
|
+
Propose a team for the current quest (leader only).
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
const players = await agent.getPlayers(gamePDA);
|
|
82
|
+
const team = [players[0], players[1]]; // Select team members
|
|
83
|
+
const tx = await agent.proposeTeam(gamePDA, team);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### `voteTeam(gamePDA: PublicKey, approve: boolean): Promise<string>`
|
|
87
|
+
Vote on the proposed team (approve or reject).
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
const shouldApprove = agent.shouldApproveTeam(teamPubkeys, questNum, failedQuests);
|
|
91
|
+
const tx = await agent.voteTeam(gamePDA, shouldApprove);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### `submitQuestVote(gamePDA: PublicKey, success: boolean): Promise<string>`
|
|
95
|
+
Submit your quest vote (only team members can vote, evil can vote fail).
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
const shouldFail = agent.shouldFailQuest(questNum, teamSize);
|
|
99
|
+
const tx = await agent.submitQuestVote(gamePDA, !shouldFail);
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### `assassinGuess(gamePDA: PublicKey, target: PublicKey): Promise<string>`
|
|
103
|
+
Assassin attempts to kill Merlin (assassin only, during Assassination phase).
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
const target = agent.chooseAssassinationTarget(playerPubkeys);
|
|
107
|
+
const tx = await agent.assassinGuess(gamePDA, new PublicKey(target));
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### `advancePhase(gamePDA: PublicKey): Promise<string>`
|
|
111
|
+
Manually advance phase (for handling timeouts or stuck states).
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
const tx = await agent.advancePhase(gamePDA);
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Game State Queries
|
|
118
|
+
|
|
119
|
+
### `getGameState(gamePDA: PublicKey): Promise<any>`
|
|
120
|
+
Get full game state from on-chain account.
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
const gameState = await agent.getGameState(gamePDA);
|
|
124
|
+
// Returns: { phase, players, currentQuest, leaderIndex, etc. }
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### `getPublicGameInfo(gameId: string): Promise<any>`
|
|
128
|
+
Get public game info from backend API.
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
const info = await agent.getPublicGameInfo(gameId);
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### `getAllGames(): Promise<any[]>`
|
|
135
|
+
Get all active games from backend.
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
const games = await agent.getAllGames();
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### `getGamePhase(gamePDA: PublicKey): Promise<GamePhase>`
|
|
142
|
+
Get current game phase.
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
const phase = await agent.getGamePhase(gamePDA);
|
|
146
|
+
// Returns: GamePhase.Lobby, GamePhase.TeamBuilding, etc.
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### `isLeader(gamePDA: PublicKey): Promise<boolean>`
|
|
150
|
+
Check if you are the current leader.
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
const amLeader = await agent.isLeader(gamePDA);
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### `isOnProposedTeam(gamePDA: PublicKey): Promise<boolean>`
|
|
157
|
+
Check if you are on the proposed team for current quest.
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
const onTeam = await agent.isOnProposedTeam(gamePDA);
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### `getProposedTeam(gamePDA: PublicKey): Promise<PublicKey[]>`
|
|
164
|
+
Get the proposed team for current quest.
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
const team = await agent.getProposedTeam(gamePDA);
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### `getPlayers(gamePDA: PublicKey): Promise<PublicKey[]>`
|
|
171
|
+
Get all players in the game.
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
const players = await agent.getPlayers(gamePDA);
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Strategy Helpers
|
|
178
|
+
|
|
179
|
+
### `shouldApproveTeam(team: string[], questNumber: number, failedQuests: number): boolean`
|
|
180
|
+
AI strategy helper to decide whether to approve a team.
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
const approve = agent.shouldApproveTeam(teamPubkeys, questNum, failedQuests);
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### `shouldFailQuest(questNumber: number, teamSize: number): boolean`
|
|
187
|
+
AI strategy helper to decide whether to fail a quest (evil only).
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
const fail = agent.shouldFailQuest(questNum, teamSize);
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### `chooseAssassinationTarget(players: string[]): string | null`
|
|
194
|
+
AI strategy helper to choose assassination target (assassin only).
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
const target = agent.chooseAssassinationTarget(playerPubkeys);
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Wallet Management
|
|
201
|
+
|
|
202
|
+
### `AvalonAgent.createWallet(): Keypair`
|
|
203
|
+
Create a new wallet.
|
|
204
|
+
|
|
205
|
+
### `AvalonAgent.importWallet(privateKeyBase58: string): Keypair`
|
|
206
|
+
Import wallet from base58 private key.
|
|
207
|
+
|
|
208
|
+
### `AvalonAgent.exportWallet(keypair: Keypair): string`
|
|
209
|
+
Export wallet to base58 private key.
|
|
210
|
+
|
|
211
|
+
### `fundWallet(lamports: number): Promise<string>`
|
|
212
|
+
Request airdrop (localnet/devnet only).
|
|
213
|
+
|
|
214
|
+
### `getBalance(): Promise<number>`
|
|
215
|
+
Get wallet balance in lamports.
|
|
216
|
+
|
|
217
|
+
## Complete Game Flow Example
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
// 1. Create/join game
|
|
221
|
+
const gameId = new BN(Date.now());
|
|
222
|
+
const { gamePDA } = await agent.createGame(gameId);
|
|
223
|
+
await agent.joinGame(gamePDA);
|
|
224
|
+
|
|
225
|
+
// 2. Wait for game to start, then fetch role
|
|
226
|
+
const roleInfo = await agent.fetchRole(gameId.toString());
|
|
227
|
+
await agent.submitRoleReveal(gamePDA);
|
|
228
|
+
|
|
229
|
+
// 3. Wait for TeamBuilding phase
|
|
230
|
+
const phase = await agent.getGamePhase(gamePDA);
|
|
231
|
+
if (phase === GamePhase.TeamBuilding && await agent.isLeader(gamePDA)) {
|
|
232
|
+
const players = await agent.getPlayers(gamePDA);
|
|
233
|
+
const team = [players[0], players[1]]; // Select team
|
|
234
|
+
await agent.proposeTeam(gamePDA, team);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// 4. Vote on team
|
|
238
|
+
if (phase === GamePhase.Voting) {
|
|
239
|
+
const team = await agent.getProposedTeam(gamePDA);
|
|
240
|
+
const gameState = await agent.getGameState(gamePDA);
|
|
241
|
+
const approve = agent.shouldApproveTeam(
|
|
242
|
+
team.map(p => p.toBase58()),
|
|
243
|
+
gameState.currentQuest,
|
|
244
|
+
gameState.failedQuests
|
|
245
|
+
);
|
|
246
|
+
await agent.voteTeam(gamePDA, approve);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 5. Quest vote (if team approved)
|
|
250
|
+
if (phase === GamePhase.Quest && await agent.isOnProposedTeam(gamePDA)) {
|
|
251
|
+
const gameState = await agent.getGameState(gamePDA);
|
|
252
|
+
const fail = agent.shouldFailQuest(
|
|
253
|
+
gameState.currentQuest,
|
|
254
|
+
gameState.quests[gameState.currentQuest].teamSize
|
|
255
|
+
);
|
|
256
|
+
await agent.submitQuestVote(gamePDA, !fail);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// 6. Assassination (if evil wins 3 quests)
|
|
260
|
+
if (phase === GamePhase.Assassination && agent.myRole === Role.Assassin) {
|
|
261
|
+
const players = await agent.getPlayers(gamePDA);
|
|
262
|
+
const target = agent.chooseAssassinationTarget(
|
|
263
|
+
players.map(p => p.toBase58())
|
|
264
|
+
);
|
|
265
|
+
if (target) {
|
|
266
|
+
await agent.assassinGuess(gamePDA, new PublicKey(target));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## All Available Game Mechanics
|
|
272
|
+
|
|
273
|
+
✅ **All game mechanics are accessible via the SDK:**
|
|
274
|
+
|
|
275
|
+
1. ✅ Create game (`createGame`)
|
|
276
|
+
2. ✅ Join game (`joinGame`)
|
|
277
|
+
3. ✅ Start game (`startGame`)
|
|
278
|
+
4. ✅ Fetch role (`fetchRole`)
|
|
279
|
+
5. ✅ Submit role reveal (`submitRoleReveal`)
|
|
280
|
+
6. ✅ Propose team (`proposeTeam`)
|
|
281
|
+
7. ✅ Vote on team (`voteTeam`)
|
|
282
|
+
8. ✅ Submit quest vote (`submitQuestVote`)
|
|
283
|
+
9. ✅ Assassin guess (`assassinGuess`)
|
|
284
|
+
10. ✅ Advance phase (`advancePhase`) - for timeouts
|
|
285
|
+
|
|
286
|
+
All methods return transaction signatures that can be tracked for confirmation.
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "avalon-agent-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Avalon Game Agent SDK - For OpenClaw AI Agents",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"test": "jest"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@coral-xyz/anchor": "^0.30.1",
|
|
13
|
+
"@solana/web3.js": "^1.91.0",
|
|
14
|
+
"axios": "^1.6.5",
|
|
15
|
+
"bs58": "^5.0.0",
|
|
16
|
+
"tweetnacl": "^1.0.3"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/node": "^20.11.0",
|
|
20
|
+
"typescript": "^5.3.3"
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Connection,
|
|
3
|
+
PublicKey,
|
|
4
|
+
Keypair,
|
|
5
|
+
Transaction,
|
|
6
|
+
SystemProgram,
|
|
7
|
+
LAMPORTS_PER_SOL,
|
|
8
|
+
Commitment,
|
|
9
|
+
} from "@solana/web3.js";
|
|
10
|
+
import { Program, AnchorProvider, Wallet, BN, Idl } from "@coral-xyz/anchor";
|
|
11
|
+
import axios from "axios";
|
|
12
|
+
import bs58 from "bs58";
|
|
13
|
+
import nacl from "tweetnacl";
|
|
14
|
+
|
|
15
|
+
// Program IDL (simplified - in production, import from file)
|
|
16
|
+
export const AVALON_IDL: any = {
|
|
17
|
+
version: "0.1.0",
|
|
18
|
+
name: "avalon_game",
|
|
19
|
+
instructions: [
|
|
20
|
+
{
|
|
21
|
+
name: "createGame",
|
|
22
|
+
accounts: [
|
|
23
|
+
{ name: "creator", isMut: true, isSigner: true },
|
|
24
|
+
{ name: "gameState", isMut: true, isSigner: false },
|
|
25
|
+
{ name: "systemProgram", isMut: false, isSigner: false },
|
|
26
|
+
],
|
|
27
|
+
args: [{ name: "gameId", type: "u64" }],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: "joinGame",
|
|
31
|
+
accounts: [
|
|
32
|
+
{ name: "player", isMut: true, isSigner: true },
|
|
33
|
+
{ name: "gameState", isMut: true, isSigner: false },
|
|
34
|
+
{ name: "systemProgram", isMut: false, isSigner: false },
|
|
35
|
+
],
|
|
36
|
+
args: [],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "startGame",
|
|
40
|
+
accounts: [
|
|
41
|
+
{ name: "creator", isMut: false, isSigner: true },
|
|
42
|
+
{ name: "gameState", isMut: true, isSigner: false },
|
|
43
|
+
],
|
|
44
|
+
args: [
|
|
45
|
+
{ name: "vrfSeed", type: { array: ["u8", 32] } },
|
|
46
|
+
{ name: "rolesCommitment", type: { array: ["u8", 32] } },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "submitRoleReveal",
|
|
51
|
+
accounts: [
|
|
52
|
+
{ name: "player", isMut: true, isSigner: true },
|
|
53
|
+
{ name: "gameState", isMut: true, isSigner: false },
|
|
54
|
+
{ name: "playerRole", isMut: true, isSigner: false },
|
|
55
|
+
{ name: "systemProgram", isMut: false, isSigner: false },
|
|
56
|
+
],
|
|
57
|
+
args: [
|
|
58
|
+
{ name: "role", type: "u8" },
|
|
59
|
+
{ name: "alignment", type: "u8" },
|
|
60
|
+
{ name: "merkleProof", type: { vec: { array: ["u8", 32] } } },
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: "proposeTeam",
|
|
65
|
+
accounts: [
|
|
66
|
+
{ name: "player", isMut: false, isSigner: true },
|
|
67
|
+
{ name: "gameState", isMut: true, isSigner: false },
|
|
68
|
+
],
|
|
69
|
+
args: [{ name: "team", type: { vec: "publicKey" } }],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "voteTeam",
|
|
73
|
+
accounts: [
|
|
74
|
+
{ name: "player", isMut: false, isSigner: true },
|
|
75
|
+
{ name: "gameState", isMut: true, isSigner: false },
|
|
76
|
+
],
|
|
77
|
+
args: [{ name: "approve", type: "bool" }],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "submitQuestVote",
|
|
81
|
+
accounts: [
|
|
82
|
+
{ name: "player", isMut: false, isSigner: true },
|
|
83
|
+
{ name: "gameState", isMut: true, isSigner: false },
|
|
84
|
+
{ name: "playerRole", isMut: false, isSigner: false },
|
|
85
|
+
],
|
|
86
|
+
args: [{ name: "success", type: "bool" }],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "assassinGuess",
|
|
90
|
+
accounts: [
|
|
91
|
+
{ name: "assassin", isMut: false, isSigner: true },
|
|
92
|
+
{ name: "gameState", isMut: true, isSigner: false },
|
|
93
|
+
],
|
|
94
|
+
args: [{ name: "target", type: "publicKey" }],
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: "advancePhase",
|
|
98
|
+
accounts: [
|
|
99
|
+
{ name: "caller", isMut: false, isSigner: true },
|
|
100
|
+
{ name: "gameState", isMut: true, isSigner: false },
|
|
101
|
+
],
|
|
102
|
+
args: [],
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
accounts: [
|
|
106
|
+
{
|
|
107
|
+
name: "GameState",
|
|
108
|
+
type: {
|
|
109
|
+
kind: "struct",
|
|
110
|
+
fields: [
|
|
111
|
+
{ name: "gameId", type: "u64" },
|
|
112
|
+
{ name: "creator", type: "publicKey" },
|
|
113
|
+
{ name: "phase", type: "u8" },
|
|
114
|
+
{ name: "playerCount", type: "u8" },
|
|
115
|
+
{ name: "currentQuest", type: "u8" },
|
|
116
|
+
{ name: "leaderIndex", type: "u8" },
|
|
117
|
+
{ name: "successfulQuests", type: "u8" },
|
|
118
|
+
{ name: "failedQuests", type: "u8" },
|
|
119
|
+
{ name: "winner", type: { option: "u8" } },
|
|
120
|
+
{ name: "vrfSeed", type: { array: ["u8", 32] } },
|
|
121
|
+
{ name: "rolesCommitment", type: { array: ["u8", 32] } },
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: "PlayerRole",
|
|
127
|
+
type: {
|
|
128
|
+
kind: "struct",
|
|
129
|
+
fields: [
|
|
130
|
+
{ name: "gameId", type: "u64" },
|
|
131
|
+
{ name: "player", type: "publicKey" },
|
|
132
|
+
{ name: "role", type: "u8" },
|
|
133
|
+
{ name: "alignment", type: "u8" },
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Enums
|
|
141
|
+
export enum Role {
|
|
142
|
+
Unknown = 0,
|
|
143
|
+
Merlin = 1,
|
|
144
|
+
Percival = 2,
|
|
145
|
+
Servant = 3,
|
|
146
|
+
Morgana = 4,
|
|
147
|
+
Assassin = 5,
|
|
148
|
+
Minion = 6,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export enum Alignment {
|
|
152
|
+
Unknown = 0,
|
|
153
|
+
Good = 1,
|
|
154
|
+
Evil = 2,
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export enum GamePhase {
|
|
158
|
+
Lobby = 0,
|
|
159
|
+
RoleAssignment = 1,
|
|
160
|
+
TeamBuilding = 2,
|
|
161
|
+
Voting = 3,
|
|
162
|
+
Quest = 4,
|
|
163
|
+
Assassination = 5,
|
|
164
|
+
Ended = 6,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Types
|
|
168
|
+
export interface PlayerInfo {
|
|
169
|
+
pubkey: PublicKey;
|
|
170
|
+
role: Role;
|
|
171
|
+
alignment: Alignment;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface RoleInboxResponse {
|
|
175
|
+
gameId: string;
|
|
176
|
+
player: string;
|
|
177
|
+
role: Role;
|
|
178
|
+
alignment: Alignment;
|
|
179
|
+
knownPlayers: string[];
|
|
180
|
+
merkleProof: number[][];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface AvalonAgentConfig {
|
|
184
|
+
connection: Connection;
|
|
185
|
+
programId: PublicKey;
|
|
186
|
+
backendUrl: string;
|
|
187
|
+
commitment?: Commitment;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Avalon Agent SDK - For AI agents to play Avalon on Solana
|
|
192
|
+
*/
|
|
193
|
+
export class AvalonAgent {
|
|
194
|
+
private connection: Connection;
|
|
195
|
+
private program: Program;
|
|
196
|
+
private wallet: Wallet;
|
|
197
|
+
private backendUrl: string;
|
|
198
|
+
private roleInfo: RoleInboxResponse | null = null;
|
|
199
|
+
|
|
200
|
+
constructor(
|
|
201
|
+
keypair: Keypair,
|
|
202
|
+
config: AvalonAgentConfig
|
|
203
|
+
) {
|
|
204
|
+
this.connection = config.connection;
|
|
205
|
+
this.backendUrl = config.backendUrl;
|
|
206
|
+
this.wallet = new Wallet(keypair);
|
|
207
|
+
|
|
208
|
+
const provider = new AnchorProvider(
|
|
209
|
+
config.connection,
|
|
210
|
+
this.wallet,
|
|
211
|
+
{ commitment: config.commitment || "confirmed" }
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
// Create program with IDL and provider (Anchor 0.30.1 format)
|
|
215
|
+
this.program = new Program(AVALON_IDL as Idl, provider) as any;
|
|
216
|
+
// Set program ID explicitly
|
|
217
|
+
(this.program as any).programId = config.programId;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Get the agent's public key
|
|
222
|
+
*/
|
|
223
|
+
get publicKey(): PublicKey {
|
|
224
|
+
return this.wallet.publicKey;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Get the agent's keypair (for signing)
|
|
229
|
+
*/
|
|
230
|
+
get keypair(): Keypair {
|
|
231
|
+
return (this.wallet as any).payer;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ==================== Wallet Management ====================
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Create a new wallet
|
|
238
|
+
*/
|
|
239
|
+
static createWallet(): Keypair {
|
|
240
|
+
return Keypair.generate();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Import wallet from private key (base58 encoded)
|
|
245
|
+
*/
|
|
246
|
+
static importWallet(privateKeyBase58: string): Keypair {
|
|
247
|
+
const secretKey = bs58.decode(privateKeyBase58);
|
|
248
|
+
return Keypair.fromSecretKey(secretKey);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Export wallet to base58 private key
|
|
253
|
+
*/
|
|
254
|
+
static exportWallet(keypair: Keypair): string {
|
|
255
|
+
return bs58.encode(keypair.secretKey);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Fund wallet with SOL (from faucet)
|
|
260
|
+
*/
|
|
261
|
+
async fundWallet(lamports: number = LAMPORTS_PER_SOL): Promise<string> {
|
|
262
|
+
const signature = await this.connection.requestAirdrop(
|
|
263
|
+
this.publicKey,
|
|
264
|
+
lamports
|
|
265
|
+
);
|
|
266
|
+
await this.connection.confirmTransaction(signature);
|
|
267
|
+
return signature;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Get wallet balance
|
|
272
|
+
*/
|
|
273
|
+
async getBalance(): Promise<number> {
|
|
274
|
+
return await this.connection.getBalance(this.publicKey);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ==================== Game Actions ====================
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Create a new game
|
|
281
|
+
*/
|
|
282
|
+
async createGame(gameId: BN): Promise<{ gamePDA: PublicKey; signature: string }> {
|
|
283
|
+
const [gamePDA] = PublicKey.findProgramAddressSync(
|
|
284
|
+
[Buffer.from("game"), gameId.toArrayLike(Buffer, "le", 8)],
|
|
285
|
+
this.program.programId
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
const tx = await this.program.methods
|
|
289
|
+
.createGame(gameId)
|
|
290
|
+
.accounts({
|
|
291
|
+
creator: this.publicKey,
|
|
292
|
+
gameState: gamePDA,
|
|
293
|
+
systemProgram: SystemProgram.programId,
|
|
294
|
+
})
|
|
295
|
+
.rpc();
|
|
296
|
+
|
|
297
|
+
return { gamePDA, signature: tx };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Join an existing game
|
|
302
|
+
*/
|
|
303
|
+
async joinGame(gamePDA: PublicKey): Promise<string> {
|
|
304
|
+
const tx = await this.program.methods
|
|
305
|
+
.joinGame()
|
|
306
|
+
.accounts({
|
|
307
|
+
player: this.publicKey,
|
|
308
|
+
gameState: gamePDA,
|
|
309
|
+
systemProgram: SystemProgram.programId,
|
|
310
|
+
})
|
|
311
|
+
.rpc();
|
|
312
|
+
|
|
313
|
+
return tx;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Start the game (creator only)
|
|
318
|
+
*/
|
|
319
|
+
async startGame(
|
|
320
|
+
gamePDA: PublicKey,
|
|
321
|
+
vrfSeed: Buffer,
|
|
322
|
+
rolesCommitment: Buffer
|
|
323
|
+
): Promise<string> {
|
|
324
|
+
const tx = await this.program.methods
|
|
325
|
+
.startGame(Array.from(vrfSeed), Array.from(rolesCommitment))
|
|
326
|
+
.accounts({
|
|
327
|
+
creator: this.publicKey,
|
|
328
|
+
gameState: gamePDA,
|
|
329
|
+
})
|
|
330
|
+
.rpc();
|
|
331
|
+
|
|
332
|
+
return tx;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ==================== Role Management ====================
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Fetch role from role inbox (private)
|
|
339
|
+
*/
|
|
340
|
+
async fetchRole(gameId: string): Promise<RoleInboxResponse> {
|
|
341
|
+
// Create authentication message
|
|
342
|
+
const message = `Reveal role for game ${gameId}`;
|
|
343
|
+
const messageBytes = Buffer.from(message);
|
|
344
|
+
|
|
345
|
+
// Sign message
|
|
346
|
+
const signature = nacl.sign.detached(
|
|
347
|
+
messageBytes,
|
|
348
|
+
this.keypair.secretKey
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
// Call role inbox
|
|
352
|
+
const response = await axios.post(`${this.backendUrl}/role-inbox/${gameId}`, {
|
|
353
|
+
playerPubkey: this.publicKey.toBase58(),
|
|
354
|
+
signature: Array.from(signature),
|
|
355
|
+
message,
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
this.roleInfo = response.data as RoleInboxResponse;
|
|
359
|
+
return this.roleInfo;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Submit role reveal to on-chain
|
|
364
|
+
*/
|
|
365
|
+
async submitRoleReveal(gamePDA: PublicKey): Promise<string> {
|
|
366
|
+
if (!this.roleInfo) {
|
|
367
|
+
throw new Error("Role not fetched. Call fetchRole first.");
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const [playerRolePDA] = PublicKey.findProgramAddressSync(
|
|
371
|
+
[
|
|
372
|
+
Buffer.from("player_role"),
|
|
373
|
+
gamePDA.toBuffer(),
|
|
374
|
+
this.publicKey.toBuffer(),
|
|
375
|
+
],
|
|
376
|
+
this.program.programId
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
// Convert Role number to enum name, then to Anchor enum format
|
|
380
|
+
const roleNames: { [key: number]: string } = {
|
|
381
|
+
0: "unknown",
|
|
382
|
+
1: "merlin",
|
|
383
|
+
2: "percival",
|
|
384
|
+
3: "servant",
|
|
385
|
+
4: "morgana",
|
|
386
|
+
5: "assassin",
|
|
387
|
+
6: "minion",
|
|
388
|
+
};
|
|
389
|
+
const roleEnum: any = {};
|
|
390
|
+
const roleKey = roleNames[this.roleInfo.role] || "unknown";
|
|
391
|
+
roleEnum[roleKey] = {};
|
|
392
|
+
|
|
393
|
+
// Convert Alignment number to enum name, then to Anchor enum format
|
|
394
|
+
const alignmentNames: { [key: number]: string } = {
|
|
395
|
+
0: "unknown",
|
|
396
|
+
1: "good",
|
|
397
|
+
2: "evil",
|
|
398
|
+
};
|
|
399
|
+
const alignmentEnum: any = {};
|
|
400
|
+
const alignmentKey = alignmentNames[this.roleInfo.alignment] || "unknown";
|
|
401
|
+
alignmentEnum[alignmentKey] = {};
|
|
402
|
+
|
|
403
|
+
const tx = await this.program.methods
|
|
404
|
+
.submitRoleReveal(
|
|
405
|
+
roleEnum,
|
|
406
|
+
alignmentEnum,
|
|
407
|
+
this.roleInfo.merkleProof.map((p) => Array.from(Buffer.from(p)))
|
|
408
|
+
)
|
|
409
|
+
.accounts({
|
|
410
|
+
player: this.publicKey,
|
|
411
|
+
gameState: gamePDA,
|
|
412
|
+
playerRole: playerRolePDA,
|
|
413
|
+
systemProgram: SystemProgram.programId,
|
|
414
|
+
})
|
|
415
|
+
.rpc();
|
|
416
|
+
|
|
417
|
+
return tx;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Get my role (only available after fetchRole)
|
|
422
|
+
*/
|
|
423
|
+
get myRole(): Role | null {
|
|
424
|
+
return this.roleInfo?.role ?? null;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Get my alignment (only available after fetchRole)
|
|
429
|
+
*/
|
|
430
|
+
get myAlignment(): Alignment | null {
|
|
431
|
+
return this.roleInfo?.alignment ?? null;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Get players I know (only available after fetchRole)
|
|
436
|
+
*/
|
|
437
|
+
get knownPlayers(): string[] {
|
|
438
|
+
return this.roleInfo?.knownPlayers ?? [];
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Check if I'm evil
|
|
443
|
+
*/
|
|
444
|
+
get isEvil(): boolean {
|
|
445
|
+
return this.myAlignment === Alignment.Evil;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Check if I'm good
|
|
450
|
+
*/
|
|
451
|
+
get isGood(): boolean {
|
|
452
|
+
return this.myAlignment === Alignment.Good;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// ==================== Gameplay ====================
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Propose a team (leader only)
|
|
459
|
+
*/
|
|
460
|
+
async proposeTeam(gamePDA: PublicKey, team: PublicKey[]): Promise<string> {
|
|
461
|
+
const tx = await this.program.methods
|
|
462
|
+
.proposeTeam(team)
|
|
463
|
+
.accounts({
|
|
464
|
+
player: this.publicKey,
|
|
465
|
+
gameState: gamePDA,
|
|
466
|
+
})
|
|
467
|
+
.rpc();
|
|
468
|
+
|
|
469
|
+
return tx;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Vote on proposed team
|
|
474
|
+
*/
|
|
475
|
+
async voteTeam(gamePDA: PublicKey, approve: boolean): Promise<string> {
|
|
476
|
+
const tx = await this.program.methods
|
|
477
|
+
.voteTeam(approve)
|
|
478
|
+
.accounts({
|
|
479
|
+
player: this.publicKey,
|
|
480
|
+
gameState: gamePDA,
|
|
481
|
+
})
|
|
482
|
+
.rpc();
|
|
483
|
+
|
|
484
|
+
return tx;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Submit quest vote
|
|
489
|
+
*/
|
|
490
|
+
async submitQuestVote(gamePDA: PublicKey, success: boolean): Promise<string> {
|
|
491
|
+
const [playerRolePDA] = PublicKey.findProgramAddressSync(
|
|
492
|
+
[
|
|
493
|
+
Buffer.from("player_role"),
|
|
494
|
+
gamePDA.toBuffer(),
|
|
495
|
+
this.publicKey.toBuffer(),
|
|
496
|
+
],
|
|
497
|
+
this.program.programId
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
const tx = await this.program.methods
|
|
501
|
+
.submitQuestVote(success)
|
|
502
|
+
.accounts({
|
|
503
|
+
player: this.publicKey,
|
|
504
|
+
gameState: gamePDA,
|
|
505
|
+
playerRole: playerRolePDA,
|
|
506
|
+
})
|
|
507
|
+
.rpc();
|
|
508
|
+
|
|
509
|
+
return tx;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Assassin attempts to kill Merlin
|
|
514
|
+
*/
|
|
515
|
+
async assassinGuess(gamePDA: PublicKey, target: PublicKey): Promise<string> {
|
|
516
|
+
const tx = await this.program.methods
|
|
517
|
+
.assassinGuess(target)
|
|
518
|
+
.accounts({
|
|
519
|
+
assassin: this.publicKey,
|
|
520
|
+
gameState: gamePDA,
|
|
521
|
+
})
|
|
522
|
+
.rpc();
|
|
523
|
+
|
|
524
|
+
return tx;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Advance phase manually (for timeouts/debugging)
|
|
529
|
+
* Can be called by any player to advance stuck phases
|
|
530
|
+
*/
|
|
531
|
+
async advancePhase(gamePDA: PublicKey): Promise<string> {
|
|
532
|
+
const tx = await this.program.methods
|
|
533
|
+
.advancePhase()
|
|
534
|
+
.accounts({
|
|
535
|
+
caller: this.publicKey,
|
|
536
|
+
gameState: gamePDA,
|
|
537
|
+
})
|
|
538
|
+
.rpc();
|
|
539
|
+
|
|
540
|
+
return tx;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// ==================== Game State ====================
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Get game state from on-chain account
|
|
547
|
+
*/
|
|
548
|
+
async getGameState(gamePDA: PublicKey): Promise<any> {
|
|
549
|
+
return await (this.program.account as any).gameState.fetch(gamePDA);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Get public game info from backend
|
|
554
|
+
*/
|
|
555
|
+
async getPublicGameInfo(gameId: string): Promise<any> {
|
|
556
|
+
const response = await axios.get(`${this.backendUrl}/game/${gameId}`);
|
|
557
|
+
return response.data;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Get all active games from backend
|
|
562
|
+
*/
|
|
563
|
+
async getAllGames(): Promise<any[]> {
|
|
564
|
+
const response = await axios.get(`${this.backendUrl}/games`);
|
|
565
|
+
return response.data;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Check if I am the current leader
|
|
570
|
+
*/
|
|
571
|
+
async isLeader(gamePDA: PublicKey): Promise<boolean> {
|
|
572
|
+
const gameState = await this.getGameState(gamePDA);
|
|
573
|
+
if (!gameState.players || !gameState.players[gameState.leaderIndex]) {
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
const leader = gameState.players[gameState.leaderIndex];
|
|
577
|
+
return leader && leader.pubkey.equals(this.publicKey);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Check if I am on the proposed team for current quest
|
|
582
|
+
*/
|
|
583
|
+
async isOnProposedTeam(gamePDA: PublicKey): Promise<boolean> {
|
|
584
|
+
const gameState = await this.getGameState(gamePDA);
|
|
585
|
+
const currentQuest = gameState.currentQuest;
|
|
586
|
+
const quest = gameState.quests[currentQuest];
|
|
587
|
+
|
|
588
|
+
if (!quest || !quest.proposedTeam) {
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
for (const member of quest.proposedTeam) {
|
|
593
|
+
if (member && member.equals && member.equals(this.publicKey)) {
|
|
594
|
+
return true;
|
|
595
|
+
}
|
|
596
|
+
// Handle case where member is already a PublicKey object
|
|
597
|
+
if (member && typeof member === 'object' && member.pubkey && member.pubkey.equals(this.publicKey)) {
|
|
598
|
+
return true;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Get current game phase
|
|
606
|
+
*/
|
|
607
|
+
async getGamePhase(gamePDA: PublicKey): Promise<GamePhase> {
|
|
608
|
+
const gameState = await this.getGameState(gamePDA);
|
|
609
|
+
// Anchor returns phase as object like { teamBuilding: {} }
|
|
610
|
+
const phaseObj = gameState.phase || {};
|
|
611
|
+
if (phaseObj.lobby !== undefined) return GamePhase.Lobby;
|
|
612
|
+
if (phaseObj.roleAssignment !== undefined) return GamePhase.RoleAssignment;
|
|
613
|
+
if (phaseObj.teamBuilding !== undefined) return GamePhase.TeamBuilding;
|
|
614
|
+
if (phaseObj.voting !== undefined) return GamePhase.Voting;
|
|
615
|
+
if (phaseObj.quest !== undefined) return GamePhase.Quest;
|
|
616
|
+
if (phaseObj.assassination !== undefined) return GamePhase.Assassination;
|
|
617
|
+
if (phaseObj.ended !== undefined) return GamePhase.Ended;
|
|
618
|
+
return GamePhase.Lobby;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Get proposed team for current quest
|
|
623
|
+
*/
|
|
624
|
+
async getProposedTeam(gamePDA: PublicKey): Promise<PublicKey[]> {
|
|
625
|
+
const gameState = await this.getGameState(gamePDA);
|
|
626
|
+
const currentQuest = gameState.currentQuest;
|
|
627
|
+
const quest = gameState.quests[currentQuest];
|
|
628
|
+
|
|
629
|
+
if (!quest || !quest.proposedTeam) {
|
|
630
|
+
return [];
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
return quest.proposedTeam
|
|
634
|
+
.filter((member: any) => member !== null && member !== undefined)
|
|
635
|
+
.map((member: any) => {
|
|
636
|
+
// Handle both PublicKey objects and pubkey properties
|
|
637
|
+
if (member.pubkey) {
|
|
638
|
+
return new PublicKey(member.pubkey);
|
|
639
|
+
}
|
|
640
|
+
return new PublicKey(member);
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Get all players in the game
|
|
646
|
+
*/
|
|
647
|
+
async getPlayers(gamePDA: PublicKey): Promise<PublicKey[]> {
|
|
648
|
+
const gameState = await this.getGameState(gamePDA);
|
|
649
|
+
if (!gameState.players) {
|
|
650
|
+
return [];
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
return gameState.players
|
|
654
|
+
.filter((p: any) => p !== null && p !== undefined)
|
|
655
|
+
.map((p: any) => {
|
|
656
|
+
if (p.pubkey) {
|
|
657
|
+
return new PublicKey(p.pubkey);
|
|
658
|
+
}
|
|
659
|
+
return new PublicKey(p);
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// ==================== Strategy Helpers ====================
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Decide whether to approve a team (AI strategy)
|
|
667
|
+
*/
|
|
668
|
+
shouldApproveTeam(team: string[], questNumber: number, failedQuests: number): boolean {
|
|
669
|
+
if (!this.roleInfo) {
|
|
670
|
+
// No role info, use heuristics
|
|
671
|
+
return true;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const teamSize = team.length;
|
|
675
|
+
|
|
676
|
+
if (this.isEvil) {
|
|
677
|
+
// Evil strategy: approve teams with evil players on them
|
|
678
|
+
const knownEvilOnTeam = team.filter((p) => this.knownPlayers.includes(p));
|
|
679
|
+
return knownEvilOnTeam.length > 0;
|
|
680
|
+
} else {
|
|
681
|
+
// Good strategy: be cautious
|
|
682
|
+
// If we know someone is evil and they're on the team, reject
|
|
683
|
+
const knownEvilOnTeam = team.filter((p) => this.knownPlayers.includes(p));
|
|
684
|
+
if (knownEvilOnTeam.length > 0) {
|
|
685
|
+
return false;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// For later quests, be more selective
|
|
689
|
+
if (questNumber >= 3 && failedQuests >= 1) {
|
|
690
|
+
// Only approve if we trust the leader
|
|
691
|
+
return Math.random() > 0.3; // Replace with actual trust logic
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
return true;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Decide quest vote (only evil can fail)
|
|
700
|
+
*/
|
|
701
|
+
shouldFailQuest(questNumber: number, teamSize: number): boolean {
|
|
702
|
+
if (!this.isEvil) {
|
|
703
|
+
return false; // Good must succeed
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Evil strategy: fail when beneficial
|
|
707
|
+
// Early quests: sometimes succeed to gain trust
|
|
708
|
+
// Later quests: more likely to fail
|
|
709
|
+
const failProbability = questNumber >= 3 ? 0.8 : 0.4;
|
|
710
|
+
return Math.random() < failProbability;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Decide assassination target (assassin only)
|
|
715
|
+
*/
|
|
716
|
+
chooseAssassinationTarget(players: string[]): string | null {
|
|
717
|
+
if (this.myRole !== Role.Assassin) {
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Strategy: If we have info about Merlin, target them
|
|
722
|
+
// Otherwise, target the player who seems most knowledgeable
|
|
723
|
+
// For now, random choice among non-evil
|
|
724
|
+
const nonKnownEvil = players.filter((p) => !this.knownPlayers.includes(p));
|
|
725
|
+
if (nonKnownEvil.length > 0) {
|
|
726
|
+
return nonKnownEvil[Math.floor(Math.random() * nonKnownEvil.length)];
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return players[Math.floor(Math.random() * players.length)];
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Export types and utilities
|
|
734
|
+
export { PublicKey, Keypair, BN };
|
|
735
|
+
export default AvalonAgent;
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"moduleResolution": "node"
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*"],
|
|
17
|
+
"exclude": ["node_modules", "dist"]
|
|
18
|
+
}
|