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 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
+ }