pyre-world-kit 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.
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Pyre Kit E2E Test
3
+ *
4
+ * Tests the full faction warfare flow against a surfpool fork.
5
+ *
6
+ * Prerequisites:
7
+ * surfpool start --network mainnet --no-tui
8
+ *
9
+ * Run:
10
+ * pnpm test (or: npx tsx tests/test_e2e.ts)
11
+ */
12
+
13
+ import { Connection, LAMPORTS_PER_SOL } from '@solana/web3.js';
14
+ import {
15
+ createEphemeralAgent,
16
+ createStronghold,
17
+ fundStronghold,
18
+ recruitAgent,
19
+ launchFaction,
20
+ getFactions,
21
+ getFaction,
22
+ getJoinQuote,
23
+ joinFaction,
24
+ getComms,
25
+ rally,
26
+ defect,
27
+ getMembers,
28
+ getStrongholdForAgent,
29
+ } from '../src/index.js';
30
+
31
+ const RPC_URL = process.env.RPC_URL ?? 'http://localhost:8899';
32
+
33
+ async function sendAndConfirm(connection: Connection, agent: ReturnType<typeof createEphemeralAgent>, result: any) {
34
+ const tx = result.transaction;
35
+ const signed = agent.sign(tx);
36
+ const sig = await connection.sendRawTransaction(signed.serialize());
37
+ await connection.confirmTransaction(sig, 'confirmed');
38
+ console.log(` tx: ${sig}`);
39
+
40
+ // Handle additional transactions
41
+ if (result.additionalTransactions) {
42
+ for (const addlTx of result.additionalTransactions) {
43
+ const addlSigned = agent.sign(addlTx);
44
+ const addlSig = await connection.sendRawTransaction(addlSigned.serialize());
45
+ await connection.confirmTransaction(addlSig, 'confirmed');
46
+ console.log(` additional tx: ${addlSig}`);
47
+ }
48
+ }
49
+
50
+ return sig;
51
+ }
52
+
53
+ async function main() {
54
+ const connection = new Connection(RPC_URL, 'confirmed');
55
+ console.log(`Pyre Kit E2E Test — RPC: ${RPC_URL}\n`);
56
+
57
+ // 1. Create ephemeral agents
58
+ console.log('1. Creating ephemeral agents...');
59
+ const agent = createEphemeralAgent();
60
+ const agent2 = createEphemeralAgent();
61
+ console.log(` Agent 1: ${agent.publicKey}`);
62
+ console.log(` Agent 2: ${agent2.publicKey}`);
63
+
64
+ // Airdrop SOL for testing
65
+ console.log(' Requesting airdrops...');
66
+ const [airdropSig, airdropSig2] = await Promise.all([
67
+ connection.requestAirdrop(agent.keypair.publicKey, 10 * LAMPORTS_PER_SOL),
68
+ connection.requestAirdrop(agent2.keypair.publicKey, 1 * LAMPORTS_PER_SOL),
69
+ ]);
70
+ await Promise.all([
71
+ connection.confirmTransaction(airdropSig, 'confirmed'),
72
+ connection.confirmTransaction(airdropSig2, 'confirmed'),
73
+ ]);
74
+ console.log(' Airdrops confirmed');
75
+
76
+ // 2. Create stronghold (vault)
77
+ console.log('\n2. Creating stronghold...');
78
+ const strongholdResult = await createStronghold(connection, {
79
+ creator: agent.publicKey,
80
+ });
81
+ await sendAndConfirm(connection, agent, strongholdResult);
82
+ console.log(' Stronghold created');
83
+
84
+ // 3. Fund stronghold
85
+ console.log('\n3. Funding stronghold...');
86
+ const fundResult = await fundStronghold(connection, {
87
+ depositor: agent.publicKey,
88
+ stronghold_creator: agent.publicKey,
89
+ amount_sol: 5 * LAMPORTS_PER_SOL,
90
+ });
91
+ await sendAndConfirm(connection, agent, fundResult);
92
+ console.log(' Funded 5 SOL');
93
+
94
+ // 4. Recruit agent (link wallet)
95
+ console.log('\n4. Recruiting agent (self-link)...');
96
+ // Agent is already linked as creator, but let's verify
97
+ const agentLink = await getStrongholdForAgent(connection, agent.publicKey);
98
+ console.log(` Stronghold found: ${agentLink ? 'yes' : 'no'}`);
99
+ if (agentLink) {
100
+ console.log(` SOL balance: ${agentLink.sol_balance / LAMPORTS_PER_SOL} SOL`);
101
+ }
102
+
103
+ // 5. Launch faction
104
+ console.log('\n5. Launching faction...');
105
+ const launchResult = await launchFaction(connection, {
106
+ founder: agent.publicKey,
107
+ name: 'Pyre Test Faction',
108
+ symbol: 'PYRE',
109
+ metadata_uri: 'https://torch.market/test-metadata.json',
110
+ community_faction: true,
111
+ });
112
+ await sendAndConfirm(connection, agent, launchResult);
113
+ const factionMint = launchResult.mint.toBase58();
114
+ console.log(` Faction launched: ${factionMint}`);
115
+
116
+ // 6. List factions
117
+ console.log('\n6. Listing factions...');
118
+ const factions = await getFactions(connection, { limit: 10 });
119
+ console.log(` Total factions: ${factions.total}`);
120
+ const ourFaction = factions.factions.find(f => f.mint === factionMint);
121
+ console.log(` Our faction found: ${ourFaction ? 'yes' : 'no'}`);
122
+ if (ourFaction) {
123
+ console.log(` Status: ${ourFaction.status}, Members: ${ourFaction.members}`);
124
+ }
125
+
126
+ // 7. Get faction detail
127
+ console.log('\n7. Getting faction detail...');
128
+ const detail = await getFaction(connection, factionMint);
129
+ console.log(` Name: ${detail.name}`);
130
+ console.log(` Status: ${detail.status}`);
131
+ console.log(` Tier: ${detail.tier}`);
132
+ console.log(` Founder: ${detail.founder}`);
133
+ console.log(` War chest SOL: ${detail.war_chest_sol}`);
134
+
135
+ // 8. Get join quote
136
+ console.log('\n8. Getting join quote (0.1 SOL)...');
137
+ const quote = await getJoinQuote(connection, factionMint, 0.1 * LAMPORTS_PER_SOL);
138
+ console.log(` Tokens out: ${quote.tokens_to_user}`);
139
+ console.log(` Price impact: ${quote.price_impact_percent}%`);
140
+
141
+ // 9. Join faction with message (comms)
142
+ console.log('\n9. Joining faction...');
143
+ const joinResult = await joinFaction(connection, {
144
+ mint: factionMint,
145
+ agent: agent.publicKey,
146
+ amount_sol: 0.1 * LAMPORTS_PER_SOL,
147
+ strategy: 'fortify',
148
+ message: 'For the faction! First to join.',
149
+ stronghold: agent.publicKey,
150
+ });
151
+ await sendAndConfirm(connection, agent, joinResult);
152
+ console.log(' Joined faction');
153
+
154
+ // 10. Read comms
155
+ console.log('\n10. Reading faction comms...');
156
+ const comms = await getComms(connection, factionMint);
157
+ console.log(` Total comms: ${comms.total}`);
158
+ for (const c of comms.comms) {
159
+ console.log(` [${new Date(c.timestamp * 1000).toISOString()}] ${c.sender.slice(0, 8)}...: ${c.memo}`);
160
+ }
161
+
162
+ // 11. Rally support (different agent — can't rally your own faction)
163
+ console.log('\n11. Rallying faction (agent 2)...');
164
+ const rallyResult = await rally(connection, {
165
+ mint: factionMint,
166
+ agent: agent2.publicKey,
167
+ });
168
+ await sendAndConfirm(connection, agent2, rallyResult);
169
+ console.log(' Rally sent');
170
+
171
+ // Verify rally
172
+ const detailAfterRally = await getFaction(connection, factionMint);
173
+ console.log(` Rallies: ${detailAfterRally.rallies}`);
174
+
175
+ // 12. Defect with message
176
+ console.log('\n12. Defecting from faction...');
177
+ // Sell half of what we bought
178
+ const sellAmount = Math.floor(quote.tokens_to_user / 2);
179
+ const defectResult = await defect(connection, {
180
+ mint: factionMint,
181
+ agent: agent.publicKey,
182
+ amount_tokens: sellAmount,
183
+ message: 'Strategic withdrawal. Will return.',
184
+ stronghold: agent.publicKey,
185
+ });
186
+ await sendAndConfirm(connection, agent, defectResult);
187
+ console.log(` Defected ${sellAmount} tokens`);
188
+
189
+ // 13. Check members
190
+ console.log('\n13. Checking members...');
191
+ const members = await getMembers(connection, factionMint);
192
+ console.log(` Total members: ${members.total_members}`);
193
+ for (const m of members.members.slice(0, 5)) {
194
+ console.log(` ${m.address.slice(0, 8)}... — ${m.balance} (${m.percentage.toFixed(2)}%)`);
195
+ }
196
+
197
+ // 14. Verify stronghold for agent
198
+ console.log('\n14. Verifying stronghold link...');
199
+ const stronghold = await getStrongholdForAgent(connection, agent.publicKey);
200
+ if (stronghold) {
201
+ console.log(` Stronghold: ${stronghold.address}`);
202
+ console.log(` Authority: ${stronghold.authority}`);
203
+ console.log(` SOL balance: ${stronghold.sol_balance / LAMPORTS_PER_SOL} SOL`);
204
+ console.log(` Linked agents: ${stronghold.linked_agents}`);
205
+ }
206
+
207
+ console.log('\n✓ All steps completed successfully');
208
+ }
209
+
210
+ main().catch((err) => {
211
+ console.error('\n✗ Test failed:', err);
212
+ process.exit(1);
213
+ });
@@ -0,0 +1,458 @@
1
+ /**
2
+ * Pyre Kit Faction Warfare Simulation
3
+ *
4
+ * Spins up 500 agent wallets and runs a random walk simulation
5
+ * of faction warfare: launching, joining, defecting, rallying.
6
+ *
7
+ * Prerequisites:
8
+ * surfpool start --network mainnet --no-tui
9
+ *
10
+ * Run:
11
+ * npx tsx tests/test_sim.ts
12
+ */
13
+
14
+ import { Connection, LAMPORTS_PER_SOL } from '@solana/web3.js';
15
+ import {
16
+ createEphemeralAgent,
17
+ launchFaction,
18
+ directJoinFaction,
19
+ defect,
20
+ rally,
21
+ getFactions,
22
+ getFaction,
23
+ getMembers,
24
+ getComms,
25
+ getFactionLeaderboard,
26
+ getWorldStats,
27
+ detectAlliances,
28
+ } from '../src/index.js';
29
+
30
+ const RPC_URL = process.env.RPC_URL ?? 'http://localhost:8899';
31
+
32
+ const AGENT_COUNT = 500;
33
+ const FACTION_COUNT = 15;
34
+ const ROUNDS = 20;
35
+ const AGENTS_PER_ROUND = 80;
36
+ const JOIN_SOL = 0.05 * LAMPORTS_PER_SOL;
37
+ const AIRDROP_SOL = 2 * LAMPORTS_PER_SOL;
38
+
39
+ type Agent = ReturnType<typeof createEphemeralAgent>;
40
+
41
+ interface AgentState {
42
+ agent: Agent;
43
+ holdings: Map<string, number>; // mint -> token balance
44
+ founded: string | null; // mint they founded
45
+ voted: Set<string>; // mints already voted on
46
+ rallied: Set<string>; // mints already rallied
47
+ }
48
+
49
+ interface FactionState {
50
+ mint: string;
51
+ name: string;
52
+ founder: string; // pubkey of founding agent
53
+ }
54
+
55
+ // ─── Helpers ──────────────────────────────────────────────────────
56
+
57
+ async function sendAndConfirm(connection: Connection, agent: Agent, result: any): Promise<string> {
58
+ const tx = result.transaction;
59
+ const signed = agent.sign(tx);
60
+ const sig = await connection.sendRawTransaction(signed.serialize());
61
+ await connection.confirmTransaction(sig, 'confirmed');
62
+
63
+ if (result.additionalTransactions) {
64
+ for (const addlTx of result.additionalTransactions) {
65
+ const addlSigned = agent.sign(addlTx);
66
+ const addlSig = await connection.sendRawTransaction(addlSigned.serialize());
67
+ await connection.confirmTransaction(addlSig, 'confirmed');
68
+ }
69
+ }
70
+
71
+ return sig;
72
+ }
73
+
74
+ function pick<T>(arr: T[]): T {
75
+ return arr[Math.floor(Math.random() * arr.length)];
76
+ }
77
+
78
+ function pickN<T>(arr: T[], n: number): T[] {
79
+ const shuffled = [...arr].sort(() => Math.random() - 0.5);
80
+ return shuffled.slice(0, n);
81
+ }
82
+
83
+ const FACTION_NAMES = [
84
+ 'Iron Vanguard', 'Obsidian Order', 'Crimson Dawn', 'Shadow Covenant',
85
+ 'Ember Collective', 'Void Walkers', 'Solar Reign', 'Frost Legion',
86
+ 'Thunder Pact', 'Ash Republic', 'Neon Syndicate', 'Storm Brigade',
87
+ 'Lunar Assembly', 'Flame Sentinels', 'Dark Meridian', 'Phoenix Accord',
88
+ 'Steel Dominion', 'Crystal Enclave', 'Rogue Alliance', 'Titan Front',
89
+ ];
90
+
91
+ const FACTION_SYMBOLS = [
92
+ 'IRON', 'OBSD', 'CRIM', 'SHAD', 'EMBR', 'VOID', 'SOLR', 'FRST',
93
+ 'THDR', 'ASHR', 'NEON', 'STRM', 'LUNR', 'FLMS', 'DARK', 'PHNX',
94
+ 'STEL', 'CRYS', 'ROGU', 'TITN',
95
+ ];
96
+
97
+ const JOIN_MESSAGES = [
98
+ 'Pledging allegiance.', 'Reporting for duty.', 'This faction will rise.',
99
+ 'Strategic position acquired.', 'In for the long haul.', 'Joining the cause.',
100
+ 'Scouting this faction.', 'Alliance confirmed.', 'Deploying capital.',
101
+ 'Interesting opportunity.', 'Following the signal.', 'Reconnaissance buy.',
102
+ ];
103
+
104
+ const DEFECT_MESSAGES = [
105
+ 'Strategic withdrawal.', 'This pyre burns too dim.', 'Found a stronger faction.',
106
+ 'Tactical repositioning.', 'The leadership is weak.', 'Cutting losses.',
107
+ 'Better opportunities elsewhere.', 'Betrayal is just strategy.', 'Moving on.',
108
+ 'The war chest is empty.', 'This faction peaked.', 'Exit protocol initiated.',
109
+ ];
110
+
111
+ // ─── Simulation ───────────────────────────────────────────────────
112
+
113
+ async function main() {
114
+ const connection = new Connection(RPC_URL, 'confirmed');
115
+ console.log(`Pyre Faction Warfare Simulation`);
116
+ console.log(` RPC: ${RPC_URL}`);
117
+ console.log(` Agents: ${AGENT_COUNT}`);
118
+ console.log(` Factions: ${FACTION_COUNT}`);
119
+ console.log(` Rounds: ${ROUNDS}`);
120
+ console.log(` Agents/round: ${AGENTS_PER_ROUND}\n`);
121
+
122
+ // ── Phase 1: Spawn agents ──────────────────────────────────────
123
+
124
+ console.log('Phase 1: Spawning agents...');
125
+ const agents: AgentState[] = [];
126
+ for (let i = 0; i < AGENT_COUNT; i++) {
127
+ agents.push({
128
+ agent: createEphemeralAgent(),
129
+ holdings: new Map(),
130
+ founded: null,
131
+ voted: new Set(),
132
+ rallied: new Set(),
133
+ });
134
+ }
135
+ console.log(` ${AGENT_COUNT} agents created\n`);
136
+
137
+ // ── Phase 2: Airdrop SOL ───────────────────────────────────────
138
+
139
+ console.log('Phase 2: Airdropping SOL...');
140
+ const AIRDROP_BATCH = 25;
141
+ for (let i = 0; i < agents.length; i += AIRDROP_BATCH) {
142
+ const batch = agents.slice(i, i + AIRDROP_BATCH);
143
+ await Promise.all(
144
+ batch.map(async (a) => {
145
+ const sig = await connection.requestAirdrop(a.agent.keypair.publicKey, AIRDROP_SOL);
146
+ await connection.confirmTransaction(sig, 'confirmed');
147
+ })
148
+ );
149
+ if ((i + AIRDROP_BATCH) % 100 === 0 || i + AIRDROP_BATCH >= agents.length) {
150
+ console.log(` ${Math.min(i + AIRDROP_BATCH, agents.length)}/${AGENT_COUNT} funded`);
151
+ }
152
+ }
153
+ console.log();
154
+
155
+ // ── Phase 3: Launch factions ───────────────────────────────────
156
+
157
+ console.log('Phase 3: Launching factions...');
158
+ const founders = pickN(agents, FACTION_COUNT);
159
+ const factions: FactionState[] = [];
160
+
161
+ for (let i = 0; i < founders.length; i++) {
162
+ const a = founders[i];
163
+ const name = FACTION_NAMES[i];
164
+ const symbol = FACTION_SYMBOLS[i];
165
+ try {
166
+ const result = await launchFaction(connection, {
167
+ founder: a.agent.publicKey,
168
+ name,
169
+ symbol,
170
+ metadata_uri: `https://pyre.gg/factions/${symbol.toLowerCase()}.json`,
171
+ community_faction: true,
172
+ });
173
+ await sendAndConfirm(connection, a.agent, result);
174
+ const mint = result.mint.toBase58();
175
+ factions.push({ mint, name, founder: a.agent.publicKey });
176
+ a.founded = mint;
177
+ console.log(` [${symbol}] ${name} — founded by ${a.agent.publicKey.slice(0, 8)}...`);
178
+ } catch (err: any) {
179
+ console.log(` FAIL launching ${name}: ${err.message}`);
180
+ }
181
+ }
182
+ console.log(` ${factions.length} factions live\n`);
183
+
184
+ if (factions.length === 0) {
185
+ console.error('No factions launched. Aborting simulation.');
186
+ process.exit(1);
187
+ }
188
+
189
+ // ── Phase 4: Random walk simulation ────────────────────────────
190
+
191
+ console.log('Phase 4: Random walk simulation...\n');
192
+
193
+ let totalJoins = 0;
194
+ let totalDefections = 0;
195
+ let totalRallies = 0;
196
+ let totalErrors = 0;
197
+
198
+ for (let round = 1; round <= ROUNDS; round++) {
199
+ const roundAgents = pickN(agents, AGENTS_PER_ROUND);
200
+ let roundJoins = 0;
201
+ let roundDefections = 0;
202
+ let roundRallies = 0;
203
+ let roundErrors = 0;
204
+
205
+ console.log(`─── Round ${round}/${ROUNDS} ───`);
206
+
207
+ // Process agents in batches to avoid overwhelming the RPC
208
+ const ROUND_BATCH = 10;
209
+ for (let i = 0; i < roundAgents.length; i += ROUND_BATCH) {
210
+ const batch = roundAgents.slice(i, i + ROUND_BATCH);
211
+ await Promise.all(
212
+ batch.map(async (a) => {
213
+ await executeRandomAction(connection, a, factions);
214
+ })
215
+ );
216
+ }
217
+
218
+ // Tally round results by checking what actions succeeded
219
+ for (const a of roundAgents) {
220
+ // We track joins/defections in executeRandomAction via the stats object
221
+ }
222
+
223
+ // Get round stats from the counter
224
+ const stats = getRoundStats();
225
+ roundJoins = stats.joins;
226
+ roundDefections = stats.defections;
227
+ roundRallies = stats.rallies;
228
+ roundErrors += stats.errors;
229
+
230
+ totalJoins += roundJoins;
231
+ totalDefections += roundDefections;
232
+ totalRallies += roundRallies;
233
+ totalErrors += roundErrors;
234
+
235
+ console.log(` Joins: ${roundJoins} | Defections: ${roundDefections} | Rallies: ${roundRallies} | Errors: ${roundErrors}`);
236
+ }
237
+
238
+ console.log(`\n─── Simulation Complete ───`);
239
+ console.log(` Total joins: ${totalJoins}`);
240
+ console.log(` Total defections: ${totalDefections}`);
241
+ console.log(` Total rallies: ${totalRallies}`);
242
+ console.log(` Total errors: ${totalErrors}\n`);
243
+
244
+ // ── Phase 5: Intel debrief ─────────────────────────────────────
245
+
246
+ console.log('Phase 5: Intelligence debrief...\n');
247
+
248
+ // Leaderboard
249
+ console.log('Faction Leaderboard:');
250
+ try {
251
+ const leaderboard = await getFactionLeaderboard(connection, { limit: factions.length });
252
+ for (let i = 0; i < leaderboard.length; i++) {
253
+ const f = leaderboard[i];
254
+ console.log(` ${i + 1}. [${f.symbol}] ${f.name} — power: ${f.score.toFixed(2)}, mcap: ${f.market_cap_sol.toFixed(4)} SOL, members: ${f.members}`);
255
+ }
256
+ } catch (err: any) {
257
+ console.log(` Could not fetch leaderboard: ${err.message}`);
258
+ }
259
+ console.log();
260
+
261
+ // Alliance detection
262
+ if (factions.length >= 2) {
263
+ console.log('Alliance Detection:');
264
+ try {
265
+ const alliances = await detectAlliances(
266
+ connection,
267
+ factions.map(f => f.mint),
268
+ 20,
269
+ );
270
+ if (alliances.length === 0) {
271
+ console.log(' No alliances detected');
272
+ }
273
+ for (const a of alliances.slice(0, 10)) {
274
+ const names = a.factions.map(m => factions.find(f => f.mint === m)?.name ?? m.slice(0, 8));
275
+ console.log(` ${names.join(' + ')} — ${a.shared_members} shared (${a.overlap_percent.toFixed(1)}% overlap)`);
276
+ }
277
+ } catch (err: any) {
278
+ console.log(` Could not detect alliances: ${err.message}`);
279
+ }
280
+ console.log();
281
+ }
282
+
283
+ // Faction details for top 3
284
+ console.log('Top Faction Details:');
285
+ for (const f of factions.slice(0, 3)) {
286
+ try {
287
+ const detail = await getFaction(connection, f.mint);
288
+ const members = await getMembers(connection, f.mint, 5);
289
+ const comms = await getComms(connection, f.mint, 5);
290
+
291
+ console.log(` [${detail.symbol}] ${detail.name}`);
292
+ console.log(` Status: ${detail.status} | Tier: ${detail.tier}`);
293
+ console.log(` Price: ${detail.price_sol.toFixed(6)} SOL | MCap: ${detail.market_cap_sol.toFixed(4)} SOL`);
294
+ console.log(` Members: ${members.total_members} | Rallies: ${detail.rallies}`);
295
+ console.log(` War Chest: ${detail.war_chest_sol.toFixed(4)} SOL`);
296
+ console.log(` Votes: scorched_earth=${detail.votes_scorched_earth} fortify=${detail.votes_fortify}`);
297
+ if (comms.comms.length > 0) {
298
+ console.log(` Recent comms:`);
299
+ for (const c of comms.comms.slice(0, 3)) {
300
+ console.log(` ${c.sender.slice(0, 8)}...: "${c.memo}"`);
301
+ }
302
+ }
303
+ if (members.members.length > 0) {
304
+ console.log(` Top holders:`);
305
+ for (const m of members.members.slice(0, 3)) {
306
+ console.log(` ${m.address.slice(0, 8)}... — ${m.percentage.toFixed(2)}%`);
307
+ }
308
+ }
309
+ } catch (err: any) {
310
+ console.log(` Could not fetch ${f.name}: ${err.message}`);
311
+ }
312
+ console.log();
313
+ }
314
+
315
+ // World stats
316
+ console.log('World Stats:');
317
+ try {
318
+ const world = await getWorldStats(connection);
319
+ console.log(` Total factions: ${world.total_factions}`);
320
+ console.log(` Rising: ${world.rising_factions} | Ascended: ${world.ascended_factions}`);
321
+ console.log(` Total SOL locked: ${world.total_sol_locked.toFixed(4)} SOL`);
322
+ if (world.most_powerful) {
323
+ console.log(` Most powerful: [${world.most_powerful.symbol}] ${world.most_powerful.name} (score: ${world.most_powerful.score.toFixed(2)})`);
324
+ }
325
+ } catch (err: any) {
326
+ console.log(` Could not fetch world stats: ${err.message}`);
327
+ }
328
+
329
+ console.log('\nSimulation complete.');
330
+ }
331
+
332
+ // ─── Round Stats Tracking ─────────────────────────────────────────
333
+
334
+ let _roundJoins = 0;
335
+ let _roundDefections = 0;
336
+ let _roundRallies = 0;
337
+ let _roundErrors = 0;
338
+
339
+ function resetRoundStats() {
340
+ _roundJoins = 0;
341
+ _roundDefections = 0;
342
+ _roundRallies = 0;
343
+ _roundErrors = 0;
344
+ }
345
+
346
+ function getRoundStats() {
347
+ const stats = { joins: _roundJoins, defections: _roundDefections, rallies: _roundRallies, errors: _roundErrors };
348
+ resetRoundStats();
349
+ return stats;
350
+ }
351
+
352
+ // ─── Action Execution ─────────────────────────────────────────────
353
+
354
+ async function executeRandomAction(
355
+ connection: Connection,
356
+ agentState: AgentState,
357
+ factions: FactionState[],
358
+ ) {
359
+ const holdsFactions = [...agentState.holdings.entries()].filter(([, bal]) => bal > 0);
360
+ const canDefect = holdsFactions.length > 0;
361
+
362
+ // Weight actions: 60% join, 20% defect (if possible), 20% rally
363
+ const roll = Math.random();
364
+ let action: 'join' | 'defect' | 'rally';
365
+
366
+ if (roll < 0.6 || (!canDefect)) {
367
+ action = 'join';
368
+ } else if (roll < 0.8 && canDefect) {
369
+ action = 'defect';
370
+ } else {
371
+ action = 'rally';
372
+ }
373
+
374
+ switch (action) {
375
+ case 'join': {
376
+ const faction = pick(factions);
377
+ const alreadyVoted = agentState.voted.has(faction.mint);
378
+ const message = pick(JOIN_MESSAGES);
379
+ const params: any = {
380
+ mint: faction.mint,
381
+ agent: agentState.agent.publicKey,
382
+ amount_sol: JOIN_SOL,
383
+ message,
384
+ };
385
+ if (!alreadyVoted) {
386
+ params.strategy = Math.random() > 0.5 ? 'fortify' : 'scorched_earth';
387
+ }
388
+ try {
389
+ const result = await directJoinFaction(connection, params);
390
+ await sendAndConfirm(connection, agentState.agent, result);
391
+ const prev = agentState.holdings.get(faction.mint) ?? 0;
392
+ agentState.holdings.set(faction.mint, prev + 1);
393
+ agentState.voted.add(faction.mint);
394
+ _roundJoins++;
395
+ } catch (err: any) {
396
+ console.log(` [JOIN ERROR] ${agentState.agent.publicKey.slice(0, 8)}... -> ${faction.name}: ${err.message}`);
397
+ _roundErrors++;
398
+ }
399
+ break;
400
+ }
401
+
402
+ case 'defect': {
403
+ const [mint, balance] = pick(holdsFactions);
404
+ const faction = factions.find(f => f.mint === mint);
405
+ if (!faction) { _roundErrors++; break; }
406
+ const sellAmount = Math.max(1, Math.floor(balance * (0.3 + Math.random() * 0.7)));
407
+ const message = pick(DEFECT_MESSAGES);
408
+ try {
409
+ const result = await defect(connection, {
410
+ mint: faction.mint,
411
+ agent: agentState.agent.publicKey,
412
+ amount_tokens: sellAmount,
413
+ message,
414
+ });
415
+ await sendAndConfirm(connection, agentState.agent, result);
416
+ const remaining = (agentState.holdings.get(mint) ?? 0) - sellAmount;
417
+ if (remaining <= 0) {
418
+ agentState.holdings.delete(mint);
419
+ } else {
420
+ agentState.holdings.set(mint, remaining);
421
+ }
422
+ _roundDefections++;
423
+ } catch (err: any) {
424
+ console.log(` [DEFECT ERROR] ${agentState.agent.publicKey.slice(0, 8)}... -> ${faction.name} (${sellAmount} tokens): ${err.message}`);
425
+ _roundErrors++;
426
+ }
427
+ break;
428
+ }
429
+
430
+ case 'rally': {
431
+ // Can't rally your own faction or one you already rallied
432
+ const eligible = factions.filter(f =>
433
+ f.founder !== agentState.agent.publicKey &&
434
+ !agentState.rallied.has(f.mint)
435
+ );
436
+ if (eligible.length === 0) { break; } // nothing to rally, not an error
437
+ const faction = pick(eligible);
438
+ try {
439
+ const result = await rally(connection, {
440
+ mint: faction.mint,
441
+ agent: agentState.agent.publicKey,
442
+ });
443
+ await sendAndConfirm(connection, agentState.agent, result);
444
+ agentState.rallied.add(faction.mint);
445
+ _roundRallies++;
446
+ } catch (err: any) {
447
+ console.log(` [RALLY ERROR] ${agentState.agent.publicKey.slice(0, 8)}... -> ${faction.name}: ${err.message}`);
448
+ _roundErrors++;
449
+ }
450
+ break;
451
+ }
452
+ }
453
+ }
454
+
455
+ main().catch((err) => {
456
+ console.error('\nSimulation failed:', err);
457
+ process.exit(1);
458
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "commonjs",
5
+ "moduleResolution": "node",
6
+ "declaration": true,
7
+ "outDir": "dist",
8
+ "rootDir": "src",
9
+ "strict": true,
10
+ "resolveJsonModule": true,
11
+ "esModuleInterop": true,
12
+ "skipLibCheck": true,
13
+ "forceConsistentCasingInFileNames": true
14
+ },
15
+ "include": ["src/**/*.ts"],
16
+ "exclude": ["tests", "dist", "node_modules"]
17
+ }