import { Connection, Keypair, PublicKey, SystemProgram } from '@solana/web3.js'; import { AnchorProvider, BN, Program, Wallet } from '@staratlas/anchor'; import { CARGO_IDL, CargoPermissions, CargoStatsDefinition, } from '@staratlas/cargo'; import { CRAFTING_IDL, CraftingDomain, CraftingPermissions, } from '@staratlas/crafting'; import { AsyncSigner, FixedSizeArray, arrayDeepEquals, buildSendAndCheck, byteArrayToString, createAssociatedTokenAccount, createMint, keypairToAsyncSigner, readFromRPCNullable, readFromRPCOrError, stringToByteArray, } from '@staratlas/data-source'; import { PLAYER_PROFILE_IDL, PlayerProfile, ProfilePermissions, } from '@staratlas/player-profile'; import { POINTS_IDL, PointsCategory, PointsModifier } from '@staratlas/points'; import { Faction, PROFILE_FACTION_IDL } from '@staratlas/profile-faction'; import { DEFAULT_RISK_ZONES, FactionsStarbaseLevelInfo, GLOBAL_SCALE_DECIMALS_4, Game, GameState, Planet, PlanetType, PointsCategoryType, ProgressionConfig, ProgressionItemType, RegisterShipInput, SagePermissions, SagePointsCategory, SageProgram, Sector, SectorRing, Ship, SizeClass, Star, StarType, Starbase, StarbaseAccount, StarbaseLevelInfo, StarbaseState, StarbaseUpgradeState, StarbaseUpkeepInfo, StarbaseUpkeepLevels, UpdateShipInput, connectionFlagsToByte, gameDataEquals, gameStateDataEquals, getSizeClassAnchorEnum, planetDataEquals, progressionConfigDataEquals, sectorConnectionEquals, sectorDataEquals, shipDataEquals, starDataEquals, starbaseDataEquals, } from '../src'; import { SAGE_PROGRAM_ID, SageGameTest, createAndFundTokenAccounts, createProfile, existOrError, sendManyToChain, setupProgression, } from './helpers'; const U64_MAX = new BN(2).pow(new BN(64)).sub(new BN(1)); describe('Sector', () => { const connection = new Connection('http://localhost:8899', 'confirmed'); const walletKeypair = Keypair.generate(); const walletSigner = keypairToAsyncSigner(walletKeypair); const provider = new AnchorProvider( connection, new Wallet(walletKeypair), AnchorProvider.defaultOptions(), ); const program = SageProgram.buildProgram(SAGE_PROGRAM_ID, provider); const playerProfileProgram = new Program( PLAYER_PROFILE_IDL, new PublicKey('PprofUW1pURCnMW2si88GWPXEEK3Bvh9Tksy8WtnoYJ'), provider, ); const profileFactionProgram = new Program( PROFILE_FACTION_IDL, new PublicKey('pFACzkX2eSpAjDyEohD6i3VRJvREtH9ynbtM1DwVFsj'), provider, ); const craftingProgram = new Program( CRAFTING_IDL, new PublicKey('CRAFtUSjCW74gQtCS6LyJH33rhhVhdPhZxbPegE4Qwfq'), provider, ); const cargoProgram = new Program( CARGO_IDL, new PublicKey('CArGoi989iv3VL3xArrJXmYYDNhjwCX5ey5sY5KKwMG'), provider, ); const pointsProgram = new Program( POINTS_IDL, new PublicKey('PointJfvuHi8DgGsPCy97EaZkQ6NvpghAAVkuquLf3w'), provider, ); const authority = keypairToAsyncSigner(Keypair.generate()); const sectorProfile = keypairToAsyncSigner(Keypair.generate()); const gameManagerKey = keypairToAsyncSigner(Keypair.generate()); const sectorManagerKey = keypairToAsyncSigner(Keypair.generate()); const shipManagerKey = keypairToAsyncSigner(Keypair.generate()); const starManagerKey = keypairToAsyncSigner(Keypair.generate()); const planetManagerKey = keypairToAsyncSigner(Keypair.generate()); // sector stuff let gameKey: PublicKey; let gameStateKey: PublicKey; let sectorKey: PublicKey; const star = keypairToAsyncSigner(Keypair.generate()); const planet = keypairToAsyncSigner(Keypair.generate()); const ship = keypairToAsyncSigner(Keypair.generate()); const ship2 = keypairToAsyncSigner(Keypair.generate()); const ship3 = keypairToAsyncSigner(Keypair.generate()); const ship3d = keypairToAsyncSigner(Keypair.generate()); const ship4 = keypairToAsyncSigner(Keypair.generate()); const ship5 = keypairToAsyncSigner(Keypair.generate()); const shipMint = keypairToAsyncSigner(Keypair.generate()); const shipMint2 = keypairToAsyncSigner(Keypair.generate()); // these correspond to keys array in _createSectorProfile const GAME_MANAGER = 1; const SECTOR_MANAGER = 2; const SHIP_MANAGER = 3; const PLANET_MANAGER = 4; const STAR_MANAGER = 5; // crafting stuff const _craftingProfile = keypairToAsyncSigner(Keypair.generate()); const _craftingNamespace = stringToByteArray('sector crafting 🎮', 32); const _craftingDomainSigner = keypairToAsyncSigner(Keypair.generate()); const _craftingSigner = keypairToAsyncSigner(Keypair.generate()); // cargo stuff const _cargoStatsDefinitionSigner = keypairToAsyncSigner(Keypair.generate()); const _cargoProfile = keypairToAsyncSigner(Keypair.generate()); const _definitionManagerKey = keypairToAsyncSigner(Keypair.generate()); // points stuff const _lpCategory = keypairToAsyncSigner(Keypair.generate()); const _councilRankXpCategory = keypairToAsyncSigner(Keypair.generate()); const _pilotXpCategory = keypairToAsyncSigner(Keypair.generate()); const _dataRunningXpCategory = keypairToAsyncSigner(Keypair.generate()); const _miningXpCategory = keypairToAsyncSigner(Keypair.generate()); const _craftingXpCategory = keypairToAsyncSigner(Keypair.generate()); const _combatXpCategory = keypairToAsyncSigner(Keypair.generate()); const _updatedRiskZones = { mudSecurityZone: { center: [new BN(-5), new BN(-3)], radius: new BN(2), } as never, oniSecurityZone: { center: [new BN(5), new BN(3)], radius: new BN(2), } as never, usturSecurityZone: { center: [new BN(10), new BN(10)], radius: new BN(2), } as never, highRiskZone: { center: [new BN(0), new BN(0)], radius: new BN(2), } as never, mediumRiskZone: { center: [new BN(0), new BN(0)], radius: new BN(20), } as never, }; const sb_lvl_0s: StarbaseLevelInfo = { recipeForUpgrade: PublicKey.default, recipeCategoryForLevel: PublicKey.default, hp: new BN(0), sp: new BN(0), sectorRingAvailable: 0, warpLaneMovementFee: new BN(0), repairFee: new BN(0), repairEfficiency: 0, shieldRechargeRate: 0, shieldBreakDelay: 0, }; /** this is just a way to test that memory space has been initiated to 0s*/ const DEFAULT_STARBASE_LEVELS: FactionsStarbaseLevelInfo = { mud: [...Array(7).keys()].map(() => sb_lvl_0s) as FixedSizeArray< StarbaseLevelInfo, 7 >, oni: [...Array(7).keys()].map(() => sb_lvl_0s) as FixedSizeArray< StarbaseLevelInfo, 7 >, ustur: [...Array(7).keys()].map(() => sb_lvl_0s) as FixedSizeArray< StarbaseLevelInfo, 7 >, }; const DEFAULT_UPKEEP_INFO_ITEM: StarbaseUpkeepInfo = { ammoReserve: new BN(0), ammoDepletionRate: 0, foodReserve: new BN(0), foodDepletionRate: 0, toolkitReserve: new BN(0), toolkitDepletionRate: 0, }; const DEFAULT_STARBASE_UPKEEP: StarbaseUpkeepLevels = { level0: DEFAULT_UPKEEP_INFO_ITEM, level1: DEFAULT_UPKEEP_INFO_ITEM, level2: DEFAULT_UPKEEP_INFO_ITEM, level3: DEFAULT_UPKEEP_INFO_ITEM, level4: DEFAULT_UPKEEP_INFO_ITEM, level5: DEFAULT_UPKEEP_INFO_ITEM, level6: DEFAULT_UPKEEP_INFO_ITEM, }; const _createSectorProfile = async (profileId = sectorProfile) => { const createIx = PlayerProfile.createProfile( playerProfileProgram, profileId, [ { key: authority, expireTime: null, scope: playerProfileProgram.programId, permissions: ProfilePermissions.all(), }, { key: gameManagerKey.publicKey(), expireTime: null, scope: program.programId, permissions: SagePermissions.manageGamePermissions(), }, { key: sectorManagerKey.publicKey(), expireTime: null, scope: program.programId, permissions: SagePermissions.manageSectorPermissions(), }, { key: shipManagerKey.publicKey(), expireTime: null, scope: program.programId, permissions: SagePermissions.manageShipPermissions(), }, { key: planetManagerKey.publicKey(), expireTime: null, scope: program.programId, permissions: SagePermissions.managePlanetPermissions(), }, { key: starManagerKey.publicKey(), expireTime: null, scope: program.programId, permissions: SagePermissions.manageStarPermissions(), }, ], 1, ); await buildSendAndCheck(createIx, walletSigner, provider.connection); }; const _createCraftingProfile = async (profileId = _craftingProfile) => { const createIx = PlayerProfile.createProfile( playerProfileProgram, profileId, [ { key: walletSigner, expireTime: null, scope: playerProfileProgram.programId, permissions: ProfilePermissions.all(), }, { key: _craftingSigner.publicKey(), expireTime: null, scope: craftingProgram.programId, permissions: CraftingPermissions.all(), }, ], 1, ); await buildSendAndCheck(createIx, walletSigner, provider.connection); }; const _createCargoStatsDefinition = async ( statsDefinitionSigner = _cargoStatsDefinitionSigner, statsCount = 1, ) => { await buildSendAndCheck( CargoStatsDefinition.initDefinition({ program: cargoProgram, profile: _cargoProfile.publicKey(), statsDefinition: statsDefinitionSigner, input: { cargoStats: statsCount, }, }), walletSigner, provider.connection, ); }; const _createCraftingDomain = async () => { await buildSendAndCheck( CraftingDomain.initializeDomain( craftingProgram, _craftingDomainSigner, _craftingSigner, _craftingProfile.publicKey(), _craftingNamespace, ), walletSigner, provider.connection, ); }; const _createPointsCategories = async () => { await buildSendAndCheck( [ PointsCategory.registerPointCategory( pointsProgram, sectorProfile.publicKey(), _lpCategory, { licenseType: { type: 'none' }, pointLimit: new BN(1_000_000_000), isSpendable: true, }, ), PointsCategory.registerPointCategory( pointsProgram, sectorProfile.publicKey(), _councilRankXpCategory, { licenseType: { type: 'none' }, pointLimit: new BN(1_000_000_000), isSpendable: true, }, ), PointsCategory.registerPointCategory( pointsProgram, sectorProfile.publicKey(), _pilotXpCategory, { licenseType: { type: 'none' }, pointLimit: new BN(1_000_000_000), isSpendable: true, }, ), PointsCategory.registerPointCategory( pointsProgram, sectorProfile.publicKey(), _dataRunningXpCategory, { licenseType: { type: 'none' }, pointLimit: new BN(1_000_000_000), isSpendable: true, }, ), PointsCategory.registerPointCategory( pointsProgram, sectorProfile.publicKey(), _miningXpCategory, { licenseType: { type: 'none' }, pointLimit: new BN(1_000_000_000), isSpendable: true, }, ), PointsCategory.registerPointCategory( pointsProgram, sectorProfile.publicKey(), _craftingXpCategory, { licenseType: { type: 'none' }, pointLimit: new BN(1_000_000_000), isSpendable: true, }, ), PointsCategory.registerPointCategory( pointsProgram, sectorProfile.publicKey(), _combatXpCategory, { licenseType: { type: 'none' }, pointLimit: new BN(1_000_000_000), isSpendable: true, }, ), ], walletSigner, provider.connection, ); }; const _createShip = async ( shipKey = ship, mint = shipMint.publicKey(), sizeClass = SizeClass.Titan, crew = 0, name = 'ship1', activate = false, ) => { const gameAccount = await readFromRPCOrError( provider.connection, program, gameKey, Game, 'confirmed', ); const stats = SageGameTest.getDefaultShipStats(crew); const input: RegisterShipInput = { name: stringToByteArray(name, 64) as FixedSizeArray, sizeClass: getSizeClassAnchorEnum(sizeClass), stats, isActive: activate, keyIndex: SHIP_MANAGER, }; const ix = Ship.registerShip( program, shipManagerKey, sectorProfile.publicKey(), shipKey, mint, gameKey, input, ); // simulate transaction await buildSendAndCheck(ix, walletSigner, provider.connection); // check `Ship` init is successful const shipAccount = await readFromRPCOrError( provider.connection, program, shipKey.publicKey(), Ship, 'confirmed', ); expect(shipAccount.key.equals(shipKey.publicKey())).toBe(true); expect( shipDataEquals(shipAccount.data, { version: 0, gameId: gameKey, mint: mint, name: input.name, sizeClass, stats: stats as never, updateId: activate ? gameAccount.data.updateId : gameAccount.data.updateId.add(new BN(1)), maxUpdateId: U64_MAX, next: { key: PublicKey.default }, }), ).toBe(true); }; // init Ship const _createCargoProfile = async () => { await createProfile( playerProfileProgram, profileFactionProgram, _cargoProfile, walletSigner, [ { key: _definitionManagerKey.publicKey(), expireTime: null, scope: cargoProgram.programId, permissions: CargoPermissions.definitionPermissions(), }, ], provider.connection, walletSigner, ); }; beforeAll(async () => { const airdropTx = await connection.requestAirdrop( walletSigner.publicKey(), 500_000_000_000, ); await connection.confirmTransaction(airdropTx); await _createSectorProfile(); await _createCraftingProfile(); await _createCargoProfile(); await _createCargoStatsDefinition(); await _createCraftingDomain(); await _createPointsCategories(); }); it('should create a game', async function () { const gameId = keypairToAsyncSigner(Keypair.generate()); const gameIx = Game.initGame( program, authority, sectorProfile.publicKey(), gameId, ); await buildSendAndCheck(gameIx, walletSigner, provider.connection); const game = await readFromRPCOrError( provider.connection, program, gameId.publicKey(), Game, 'confirmed', ); expect(game.key.equals(gameId.publicKey())).toBe(true); const defaultModifier: SagePointsCategory = { category: SystemProgram.programId, modifier: SystemProgram.programId, modifierBump: 0, }; expect( gameDataEquals(game.data, { profile: sectorProfile.publicKey(), mints: { atlas: SystemProgram.programId, polis: SystemProgram.programId, ammo: SystemProgram.programId, food: SystemProgram.programId, fuel: SystemProgram.programId, repairKit: SystemProgram.programId, }, vaults: { atlas: SystemProgram.programId, polis: SystemProgram.programId, }, points: { lpCategory: defaultModifier as never, councilRankXpCategory: defaultModifier as never, pilotXpCategory: defaultModifier as never, combatXpCategory: defaultModifier as never, dataRunningXpCategory: defaultModifier as never, miningXpCategory: defaultModifier as never, craftingXpCategory: defaultModifier as never, }, cargo: { statsDefinition: SystemProgram.programId, }, crafting: { domain: SystemProgram.programId, }, gameState: SystemProgram.programId, version: 0, updateId: new BN(0), riskZones: DEFAULT_RISK_ZONES, }), ).toBe(true); }); it('should update a game', async function () { const gameId = keypairToAsyncSigner(Keypair.generate()); const atlas = keypairToAsyncSigner(Keypair.generate()); const polis = keypairToAsyncSigner(Keypair.generate()); const ammo = keypairToAsyncSigner(Keypair.generate()); const food = keypairToAsyncSigner(Keypair.generate()); const fuel = keypairToAsyncSigner(Keypair.generate()); const repairKit = keypairToAsyncSigner(Keypair.generate()); await buildSendAndCheck( [ createMint(ammo, 0, authority.publicKey(), null), createMint(food, 0, authority.publicKey(), null), createMint(fuel, 0, authority.publicKey(), null), createMint(repairKit, 0, authority.publicKey(), null), ], walletSigner, provider.connection, ); // need to create mints in two instructions coz of tx size limits await buildSendAndCheck( [ createMint(atlas, 0, authority.publicKey(), null), createMint(polis, 0, authority.publicKey(), null), ], walletSigner, provider.connection, ); const atlasVaultResults = createAssociatedTokenAccount( atlas.publicKey(), authority.publicKey(), ); const polisVaultResults = createAssociatedTokenAccount( polis.publicKey(), authority.publicKey(), ); const vaults = [atlasVaultResults, polisVaultResults]; await buildSendAndCheck( vaults.map((it) => it.instructions).flat(), walletSigner, provider.connection, ); const gameIx = Game.initGame( program, authority, sectorProfile.publicKey(), gameId, ); const gameIx2 = Game.updateGame( gameId.publicKey(), program, gameManagerKey, sectorProfile.publicKey(), GAME_MANAGER, { lpCategory: _lpCategory.publicKey(), councilRankXpCategory: _councilRankXpCategory.publicKey(), pilotXpCategory: _pilotXpCategory.publicKey(), dataRunningXpCategory: _dataRunningXpCategory.publicKey(), miningXpCategory: _miningXpCategory.publicKey(), craftingXpCategory: _craftingXpCategory.publicKey(), combatXpCategory: _combatXpCategory.publicKey(), }, undefined /** mints */, undefined /** vaults */, { domain: _craftingDomainSigner.publicKey() }, { statsDefinition: _cargoStatsDefinitionSigner.publicKey() }, ); await buildSendAndCheck( [gameIx, gameIx2], walletSigner, provider.connection, ); // update the mints & vaults await buildSendAndCheck( Game.updateGame( gameId.publicKey(), program, gameManagerKey, sectorProfile.publicKey(), GAME_MANAGER, undefined /** points */, { atlas: atlas.publicKey(), polis: polis.publicKey(), ammo: ammo.publicKey(), food: food.publicKey(), fuel: fuel.publicKey(), repairKit: repairKit.publicKey(), } /** mints */, { atlas: existOrError(vaults[0]).address, polis: existOrError(vaults[1]).address, }, undefined /** crafting */, undefined /** cargo */, _updatedRiskZones /** risk zones */, ), walletSigner, provider.connection, ); const game = await readFromRPCOrError( provider.connection, program, gameId.publicKey(), Game, 'confirmed', ); gameKey = gameId.publicKey(); const getPointsCategoryAndModifier = ( category: AsyncSigner, ): SagePointsCategory => { const modifierResult = Game.findPointsModifierAddress( program, gameId.publicKey(), category.publicKey(), ); return { category: category.publicKey(), modifier: modifierResult[0], modifierBump: modifierResult[1], }; }; expect(game.key.equals(gameId.publicKey())).toBe(true); expect( gameDataEquals(game.data, { profile: sectorProfile.publicKey(), mints: { atlas: atlas.publicKey(), polis: polis.publicKey(), ammo: ammo.publicKey(), food: food.publicKey(), fuel: fuel.publicKey(), repairKit: repairKit.publicKey(), }, vaults: { atlas: existOrError(vaults[0]).address, polis: existOrError(vaults[1]).address, }, points: { lpCategory: getPointsCategoryAndModifier(_lpCategory) as never, councilRankXpCategory: getPointsCategoryAndModifier( _councilRankXpCategory, ) as never, pilotXpCategory: getPointsCategoryAndModifier( _pilotXpCategory, ) as never, combatXpCategory: getPointsCategoryAndModifier( _combatXpCategory, ) as never, dataRunningXpCategory: getPointsCategoryAndModifier( _dataRunningXpCategory, ) as never, miningXpCategory: getPointsCategoryAndModifier( _miningXpCategory, ) as never, craftingXpCategory: getPointsCategoryAndModifier( _craftingXpCategory, ) as never, }, cargo: { statsDefinition: _cargoStatsDefinitionSigner.publicKey(), }, crafting: { domain: _craftingDomainSigner.publicKey(), }, gameState: SystemProgram.programId, version: 0, updateId: new BN(0), riskZones: _updatedRiskZones, }), ).toBe(true); }); it('should not allow game with cargo stats count > 1', async () => { // create stats definition with 2 stats const newStatsDefinition = keypairToAsyncSigner(Keypair.generate()); await _createCargoStatsDefinition(newStatsDefinition, 2); // using this stats definition in a SAGE game should fail const newGameId = keypairToAsyncSigner(Keypair.generate()); const createGameIx = Game.initGame( program, authority, sectorProfile.publicKey(), newGameId, ); const updateGameIx = Game.updateGame( newGameId.publicKey(), program, gameManagerKey, sectorProfile.publicKey(), GAME_MANAGER, undefined /** points */, undefined /** mints */, undefined /** vaults */, undefined /** crafting */, { statsDefinition: newStatsDefinition.publicKey(), } /** cargo */, undefined /** risk zones */, ); let success = true; try { await buildSendAndCheck( [createGameIx, updateGameIx], walletSigner, provider.connection, ); } catch (err) { expect(JSON.stringify(err as Error)).toContain('TooManyStats'); success = false; } expect(success).toBe(false); }); it('should create a game state account', async function () { const gameStateKeyResult = GameState.findAddress( program, gameKey, new BN(1), ); gameStateKey = gameStateKeyResult[0]; const ix = GameState.initGameState( program, gameManagerKey, sectorProfile.publicKey(), gameKey, gameStateKey, { keyIndex: GAME_MANAGER, }, ); await buildSendAndCheck(ix, walletSigner, provider.connection); const gameStateAcc = await readFromRPCOrError( provider.connection, program, gameStateKey, GameState, 'confirmed', ); expect(gameStateAcc.key.equals(gameStateKey)).toBe(true); expect( gameStateDataEquals(gameStateAcc.data, { gameId: gameKey, fleet: { starbaseLevels: DEFAULT_STARBASE_LEVELS as never, upkeep: DEFAULT_STARBASE_UPKEEP as never, maxFleetSize: 0, }, misc: { warpLaneFuelCostReduction: 0, upkeepMiningEmissionsPenalty: 0, respawnFee: new BN(0), }, version: 0, updateId: new BN(1), bump: gameStateKeyResult[1], }), ).toBe(true); }); it('should update a game state account', async function () { const gameStateBefore = await readFromRPCOrError( provider.connection, program, gameStateKey, GameState, 'confirmed', ); const craftingFacility1 = keypairToAsyncSigner(Keypair.generate()); const upgradeFacility1 = keypairToAsyncSigner(Keypair.generate()); const craftingFacility2 = keypairToAsyncSigner(Keypair.generate()); const upgradeFacility2 = keypairToAsyncSigner(Keypair.generate()); const starbaseLevelIndex7 = { faction: Faction.MUD as number, hp: new BN(99999), level: 2, sectorRingAvailable: SectorRing.Outer, recipeCategoryForLevel: craftingFacility2.publicKey(), oldRecipeForUpgrade: PublicKey.default, newRecipeForUpgrade: upgradeFacility2.publicKey(), sp: new BN(999), warpLaneMovementFee: new BN(500), repairFee: new BN(1337), repairEfficiency: 150, shieldRechargeRate: 120, shieldBreakDelay: 130, }; const starbaseLevelIndex6 = { faction: Faction.MUD as number, hp: new BN(1337), level: 1, sectorRingAvailable: SectorRing.Inner, recipeCategoryForLevel: craftingFacility1.publicKey(), oldRecipeForUpgrade: PublicKey.default, newRecipeForUpgrade: upgradeFacility1.publicKey(), sp: new BN(7331), warpLaneMovementFee: new BN(600), repairFee: new BN(337), repairEfficiency: 75, shieldRechargeRate: 65, shieldBreakDelay: 50, }; const starbaseLevels = [starbaseLevelIndex7, starbaseLevelIndex6]; const updatedStarbaseLevels = { ...DEFAULT_STARBASE_LEVELS, mud: [ sb_lvl_0s, { hp: starbaseLevelIndex6.hp, sectorRingAvailable: starbaseLevelIndex6.sectorRingAvailable, recipeCategoryForLevel: starbaseLevelIndex6.recipeCategoryForLevel, recipeForUpgrade: starbaseLevelIndex6.newRecipeForUpgrade, sp: starbaseLevelIndex6.sp, warpLaneMovementFee: starbaseLevelIndex6.warpLaneMovementFee, repairFee: starbaseLevelIndex6.repairFee, repairEfficiency: starbaseLevelIndex6.repairEfficiency, shieldRechargeRate: starbaseLevelIndex6.shieldRechargeRate, shieldBreakDelay: starbaseLevelIndex6.shieldBreakDelay, }, { hp: starbaseLevelIndex7.hp, sectorRingAvailable: starbaseLevelIndex7.sectorRingAvailable, recipeCategoryForLevel: starbaseLevelIndex7.recipeCategoryForLevel, recipeForUpgrade: starbaseLevelIndex7.newRecipeForUpgrade, sp: starbaseLevelIndex7.sp, warpLaneMovementFee: starbaseLevelIndex7.warpLaneMovementFee, repairFee: starbaseLevelIndex7.repairFee, repairEfficiency: starbaseLevelIndex7.repairEfficiency, shieldRechargeRate: starbaseLevelIndex7.shieldRechargeRate, shieldBreakDelay: starbaseLevelIndex7.shieldBreakDelay, }, ...[...Array(4).keys()].map(() => sb_lvl_0s), ], }; const upkeepUpdates = { level1: { ammoReserve: new BN(1000), ammoDepletionRate: 10, foodReserve: new BN(1000), foodDepletionRate: 10, toolkitReserve: new BN(1000), toolkitDepletionRate: 10, }, level5: { ammoReserve: new BN(3000), ammoDepletionRate: 30, foodReserve: new BN(3000), foodDepletionRate: 30, toolkitReserve: new BN(3000), toolkitDepletionRate: 30, }, }; const updatedUpkeep: StarbaseUpkeepLevels = { ...DEFAULT_STARBASE_UPKEEP, level1: upkeepUpdates.level1, level5: upkeepUpdates.level5, }; const starbaseUpkeep = [ { level: 1, info: upkeepUpdates.level1 }, { level: 5, info: upkeepUpdates.level5 }, ]; const maxFleetSize = 64; const fees = { warpLaneMovementFee: new BN(999) }; const misc = { warpLaneFuelCostReduction: 75, upkeepMiningEmissionsPenalty: 50, respawnFee: new BN(1000), }; const ix = GameState.updateGameState( program, gameManagerKey, sectorProfile.publicKey(), gameKey, gameStateKey, GAME_MANAGER, { starbaseLevels, starbaseUpkeep, maxFleetSize, } /** fleet */, misc /** misc */, ); await buildSendAndCheck(ix, walletSigner, provider.connection); const gameStateAccAfter = await readFromRPCOrError( provider.connection, program, gameStateKey, GameState, 'confirmed', ); const expectedData = { ...gameStateBefore.data, fleet: { starbaseLevels: updatedStarbaseLevels as never, upkeep: updatedUpkeep as never, maxFleetSize, }, fees, misc, }; expect(gameStateDataEquals(gameStateAccAfter.data, expectedData)).toBe( true, ); }); it('should activate a game', async function () { await buildSendAndCheck( ( await Promise.all([ GameState.activateGameState( program, gameManagerKey, sectorProfile.publicKey(), gameKey, gameStateKey, { keyIndex: GAME_MANAGER, }, ), ]) ).flat(), walletSigner, provider.connection, ); const gameAccount = await readFromRPCOrError( provider.connection, program, gameKey, Game, 'confirmed', ); expect(gameAccount.data.updateId.eq(new BN(1))).toBe(true); const gameStateAcc = await readFromRPCOrError( provider.connection, program, gameStateKey, GameState, 'confirmed', ); expect(gameAccount.data.updateId.eq(gameStateAcc.data.updateId)).toBe(true); }); it('should create a sector', async function () { const discoverer = Keypair.generate().publicKey; let sectorKeyResults: [PublicKey, number] | undefined = undefined; const sectorName = 'Test Sector'; const ix = Sector.registerSector( program, sectorManagerKey, sectorProfile.publicKey(), discoverer, gameKey, [new BN(-1), new BN(1)], sectorName, SECTOR_MANAGER, ); sectorKeyResults = ix.sectorKey; if (sectorKeyResults === undefined) { throw new Error('Sector key not returned'); } sectorKey = sectorKeyResults[0]; await buildSendAndCheck( ix.instructions, walletSigner, provider.connection, { sendOptions: { skipPreflight: true, }, }, ); const sector = await readFromRPCOrError( provider.connection, program, sectorKey, Sector, 'confirmed', ); expect(sector.key.equals(sectorKey)).toBe(true); expect( sectorDataEquals(sector.data, { version: 0, gameId: gameKey, coordinates: [new BN(-1), new BN(1)], discoverer, name: stringToByteArray(sectorName, 64), numStars: 0, numPlanets: 0, numMoons: 0, numConnections: 0, numAsteroidBelts: 0, lastScanTime: new BN(0), lastScanChance: 0, bump: sectorKeyResults[1], }), ).toBe(true); expect(sector.connections.length).toBe(0); }); it('should add and remove a connection', async function () { const gameId = keypairToAsyncSigner(Keypair.generate()); const discoverer = Keypair.generate().publicKey; let sector1Key: [PublicKey, number] | undefined = undefined; let sector2Key: [PublicKey, number] | undefined = undefined; const sector1Name = 'Test Sector1'; const sector2Name = 'Test Sector2'; const sector1IX = Sector.registerSector( program, sectorManagerKey, sectorProfile.publicKey(), discoverer, gameId.publicKey(), [new BN(1), new BN(1)], sector1Name, SECTOR_MANAGER, ); sector1Key = sector1IX.sectorKey; const sector2IX = Sector.registerSector( program, sectorManagerKey, sectorProfile.publicKey(), discoverer, gameId.publicKey(), [new BN(2), new BN(2)], sector2Name, SECTOR_MANAGER, ); sector2Key = sector2IX.sectorKey; const ix1 = [ Game.initGame(program, authority, sectorProfile.publicKey(), gameId), sector1IX.instructions, sector2IX.instructions, Sector.addConnection( program, sectorManagerKey, sectorProfile.publicKey(), sector1Key[0], sector2Key[0], gameId.publicKey(), [new BN(3), new BN(3)], { isJumpPoint: false }, [new BN(4), new BN(4)], { isJumpPoint: true }, SECTOR_MANAGER, ), ]; if (sector1Key === undefined || sector2Key === undefined) { throw new Error('Sector key not returned'); } await buildSendAndCheck(ix1, walletSigner, provider.connection); const sector1 = await readFromRPCOrError( provider.connection, program, sector1Key[0], Sector, 'confirmed', ); const sector2 = await readFromRPCOrError( provider.connection, program, sector2Key[0], Sector, 'confirmed', ); expect(sector1.connections.length).toBe(1); expect(sector2.connections.length).toBe(1); expect( // Okay because we verified the length // eslint-disable-next-line @typescript-eslint/no-non-null-assertion sectorConnectionEquals(sector1.connections[0]!, { connectionSector: sector2.key, flags: connectionFlagsToByte({ isJumpPoint: false }), subCoordinates: [new BN(3), new BN(3)], }), ); expect( // Okay because we verified the length // eslint-disable-next-line @typescript-eslint/no-non-null-assertion sectorConnectionEquals(sector2.connections[0]!, { connectionSector: sector1.key, flags: connectionFlagsToByte({ isJumpPoint: true }), subCoordinates: [new BN(4), new BN(4)], }), ); const ix2 = Sector.removeConnection( program, sectorManagerKey, sectorProfile.publicKey(), walletSigner.publicKey(), sector1, sector2, SECTOR_MANAGER, gameId.publicKey(), ); await buildSendAndCheck(ix2, walletSigner, provider.connection); const sector1After = await readFromRPCOrError( provider.connection, program, sector1Key[0], Sector, 'confirmed', ); const sector2After = await readFromRPCOrError( provider.connection, program, sector2Key[0], Sector, 'confirmed', ); expect(sector1After.connections.length).toBe(0); expect(sector2After.connections.length).toBe(0); }); it('should create connections within the same faction security zone', async () => { const oniSector1Ix = Sector.registerSector( program, sectorManagerKey, sectorProfile.publicKey(), sectorProfile.publicKey() /** discoverer */, gameKey, [new BN(5), new BN(3)], 'ONI 1', SECTOR_MANAGER, ); const oniSector2Ix = Sector.registerSector( program, sectorManagerKey, sectorProfile.publicKey(), sectorProfile.publicKey() /** discoverer */, gameKey, [new BN(5), new BN(4)], 'ONI 2', SECTOR_MANAGER, ); await buildSendAndCheck( [oniSector1Ix.instructions, oniSector2Ix.instructions], walletSigner, provider.connection, ); await buildSendAndCheck( Sector.addConnection( program, sectorManagerKey, sectorProfile.publicKey(), oniSector1Ix.sectorKey[0], oniSector2Ix.sectorKey[0], gameKey, [new BN(0), new BN(0)], { isJumpPoint: true, }, [new BN(0), new BN(0)], { isJumpPoint: true, }, SECTOR_MANAGER, ), walletSigner, provider.connection, ); const oniSector1 = await readFromRPCOrError( provider.connection, program, oniSector1Ix.sectorKey[0], Sector, 'confirmed', ); const oniSector2 = await readFromRPCOrError( provider.connection, program, oniSector2Ix.sectorKey[0], Sector, 'confirmed', ); expect(oniSector1.connections.length).toBe(1); expect(oniSector2.connections.length).toBe(1); expect( // Okay because we verified the length // eslint-disable-next-line @typescript-eslint/no-non-null-assertion sectorConnectionEquals(oniSector1.connections[0]!, { connectionSector: oniSector2.key, flags: connectionFlagsToByte({ isJumpPoint: true }), subCoordinates: [new BN(0), new BN(0)], }), ); expect( // Okay because we verified the length // eslint-disable-next-line @typescript-eslint/no-non-null-assertion sectorConnectionEquals(oniSector2.connections[0]!, { connectionSector: oniSector1.key, flags: connectionFlagsToByte({ isJumpPoint: true }), subCoordinates: [new BN(0), new BN(0)], }), ); }); it('should not create connections between different faction security zones', async () => { const oniSectorIx = Sector.registerSector( program, sectorManagerKey, sectorProfile.publicKey(), sectorProfile.publicKey() /** discoverer */, gameKey, [new BN(4), new BN(4)], 'ONI 3', SECTOR_MANAGER, ); const usturSectorIx = Sector.registerSector( program, sectorManagerKey, sectorProfile.publicKey(), sectorProfile.publicKey() /** discoverer */, gameKey, [new BN(10), new BN(10)], 'USTUR 1', SECTOR_MANAGER, ); await buildSendAndCheck( [oniSectorIx.instructions, usturSectorIx.instructions], walletSigner, provider.connection, ); let success = true; try { await buildSendAndCheck( Sector.addConnection( program, sectorManagerKey, sectorProfile.publicKey(), oniSectorIx.sectorKey[0], usturSectorIx.sectorKey[0], gameKey, [new BN(0), new BN(0)], { isJumpPoint: true, }, [new BN(0), new BN(0)], { isJumpPoint: true, }, SECTOR_MANAGER, ), walletSigner, provider.connection, ); } catch (err) { expect(JSON.stringify(err as Error)).toContain('InvalidSectorConnection'); success = false; } expect(success).toBe(false); }); it('should create a star', async function () { const sectorBefore = await readFromRPCOrError( provider.connection, program, sectorKey, Sector, 'confirmed', ); const input = { name: 'Redoubt III', size: new BN(7), subCoordinates: [new BN(2), new BN(4)] as FixedSizeArray, starType: StarType.WhiteDwarf, keyIndex: STAR_MANAGER, }; await buildSendAndCheck( Star.registerStar({ program, key: starManagerKey, profile: sectorProfile.publicKey(), star, sector: sectorKey, gameId: gameKey, input, }), walletSigner, provider.connection, ); const sectorAfter = await readFromRPCOrError( provider.connection, program, sectorKey, Sector, 'confirmed', ); const starAcc = await readFromRPCOrError( provider.connection, program, star.publicKey(), Star, 'confirmed', ); expect(starAcc.key.equals(star.publicKey())).toBe(true); expect(starAcc.prettyName).toBe(input.name); expect( starDataEquals(starAcc.data, { version: 0, gameId: gameKey, sector: sectorBefore.data.coordinates, size: input.size, starType: input.starType, subCoordinates: input.subCoordinates, name: stringToByteArray(input.name, 64), }), ).toBe(true); expect(sectorAfter.data.numStars).toBe(sectorBefore.data.numStars + 1); }); it('should update a star', async function () { const input = { name: 'Redoubt IV', size: new BN(100_000_000_000), starType: StarType.RedGiant, keyIndex: STAR_MANAGER, }; await buildSendAndCheck( Star.updateStar({ program, key: starManagerKey, profile: sectorProfile.publicKey(), star: star.publicKey(), gameId: gameKey, input, }), walletSigner, provider.connection, ); const starAccAfter = await readFromRPCOrError( provider.connection, program, star.publicKey(), Star, 'confirmed', ); expect(starAccAfter.prettyName).toBe(input.name); expect(starAccAfter.data.starType).toBe(input.starType); expect(starAccAfter.data.size.eq(input.size)).toBe(true); }); it('should create a planet', async function () { const input = { name: 'Much ❤️', size: new BN(99999), maxHp: new BN(1337), subCoordinates: [new BN(2), new BN(2)] as FixedSizeArray, planetType: PlanetType.IceGiant, position: SectorRing.Inner, keyIndex: PLANET_MANAGER, }; await buildSendAndCheck( Planet.registerPlanet( program, planetManagerKey, sectorProfile.publicKey(), planet, sectorKey, gameKey, input, ), walletSigner, provider.connection, ); const sectorAcc = await readFromRPCOrError( provider.connection, program, sectorKey, Sector, 'confirmed', ); const planetAcc = await readFromRPCOrError( provider.connection, program, planet.publicKey(), Planet, 'confirmed', ); expect(planetAcc.key.equals(planet.publicKey())).toBe(true); expect(planetAcc.prettyName).toBe(input.name); expect( planetDataEquals(planetAcc.data, { version: 0, gameId: gameKey, sector: sectorAcc.data.coordinates, maxHp: input.maxHp, currentHealth: input.maxHp, size: input.size, planetType: input.planetType, position: input.position, subCoordinates: input.subCoordinates, name: stringToByteArray(input.name, 64), numResources: 0, amountMined: new BN(0), numMiners: new BN(0), }), ).toBe(true); }); it('should update a planet', async function () { const planetAccBefore = await readFromRPCOrError( provider.connection, program, planet.publicKey(), Planet, 'confirmed', ); const input = { name: 'Not ❤️', size: new BN(100_000), maxHp: new BN(50_000) /** this is increasing */, subCoordinates: [new BN(3), new BN(3)] as FixedSizeArray, keyIndex: PLANET_MANAGER, }; await buildSendAndCheck( Planet.updatePlanet( program, planetManagerKey, sectorProfile.publicKey(), planet.publicKey(), gameKey, input, ), walletSigner, provider.connection, ); const planetAccAfter = await readFromRPCOrError( provider.connection, program, planet.publicKey(), Planet, 'confirmed', ); expect(planetAccAfter.prettyName).toBe(input.name); expect(planetAccAfter.data.maxHp.eq(input.maxHp)).toBe(true); expect( planetAccAfter.data.currentHealth.eq(planetAccBefore.data.currentHealth), ).toBe(true); expect( arrayDeepEquals( planetAccAfter.data.subCoordinates, input.subCoordinates, (a, b) => a.eq(b), ), ).toBe(true); expect(planetAccAfter.data.size.eq(input.size)).toBe(true); }); it("should update a planet's currentHealth when higher than maxHp", async function () { // current value of currentHealth => 1337 const input = { name: 'Not ❤️', size: new BN(100_000), maxHp: new BN(200) /** this is decreasing to below currentHealth */, keyIndex: PLANET_MANAGER, }; await buildSendAndCheck( Planet.updatePlanet( program, planetManagerKey, sectorProfile.publicKey(), planet.publicKey(), gameKey, input, ), walletSigner, provider.connection, ); const planetAccAfter = await readFromRPCOrError( provider.connection, program, planet.publicKey(), Planet, 'confirmed', ); expect(planetAccAfter.prettyName).toBe(input.name); expect(planetAccAfter.data.maxHp.eq(input.maxHp)).toBe(true); expect(planetAccAfter.data.currentHealth.eq(input.maxHp)).toBe(true); expect(planetAccAfter.data.size.eq(input.size)).toBe(true); }); it('should create ships', async function () { await buildSendAndCheck( [ createMint(shipMint, 0, authority.publicKey(), null), createMint(shipMint2, 0, authority.publicKey(), null), ], walletSigner, provider.connection, ); await Promise.all([ createAndFundTokenAccounts( authority, [ [shipMint.publicKey(), 1], [shipMint2.publicKey(), 1], ], authority.publicKey(), walletSigner, walletSigner, provider.connection, ), ]); await _createShip( ship, shipMint.publicKey(), SizeClass.Large, 12, 'ship', true, ); await _createShip( ship2, shipMint.publicKey(), SizeClass.Large, 13, 'ship2', true, ); await _createShip( ship3, shipMint.publicKey(), SizeClass.Medium, 14, 'ship3', ); await _createShip( ship3d, shipMint.publicKey(), SizeClass.Medium, 14, 'ship3b', ); await _createShip( ship4, shipMint.publicKey(), SizeClass.Large, 15, 'ship4', true, ); await _createShip( ship5, shipMint2.publicKey(), SizeClass.Large, 16, 'ship5', ); // test that Titan works okay await _createShip( keypairToAsyncSigner(Keypair.generate()), shipMint.publicKey(), SizeClass.Titan, 1, 'mighty titan', true, ); }); it('should update ship', async function () { const ship3Before = await readFromRPCOrError( provider.connection, program, ship3.publicKey(), Ship, 'confirmed', ); const sizeClass = SizeClass.Large; const stats = SageGameTest.getDefaultShipStats(67); const input: UpdateShipInput = { name: stringToByteArray('ship 1337', 64) as FixedSizeArray, sizeClass: getSizeClassAnchorEnum(sizeClass), stats, keyIndex: SHIP_MANAGER, }; await buildSendAndCheck( Ship.updateShip( program, shipManagerKey, sectorProfile.publicKey(), ship3.publicKey(), gameKey, input, ), walletSigner, provider.connection, ); const ship3After = await readFromRPCOrError( provider.connection, program, ship3.publicKey(), Ship, 'confirmed', ); expect(ship3After.key.equals(ship3.publicKey())).toBe(true); expect( shipDataEquals(ship3After.data, { version: 0, gameId: gameKey, mint: shipMint.publicKey(), sizeClass, stats: stats as never, updateId: ship3Before.data.updateId, maxUpdateId: ship3Before.data.maxUpdateId, next: { key: PublicKey.default }, name: input.name, }), ).toBe(true); // can't update already active ship let success = true; try { await buildSendAndCheck( Ship.updateShip( program, shipManagerKey, sectorProfile.publicKey(), ship.publicKey(), gameKey, input, ), walletSigner, provider.connection, { suppressLogging: true }, ); } catch (err) { success = false; expect(JSON.stringify(err as Error)).toContain('AlreadyActive'); } expect(success).toBe(false); }); it('should invalidate ship', async function () { const gameAccountBefore = await readFromRPCOrError( provider.connection, program, gameKey, Game, 'confirmed', ); await buildSendAndCheck( Ship.invalidateShip( program, shipManagerKey, sectorProfile.publicKey(), ship2.publicKey(), gameKey, SHIP_MANAGER, ), walletSigner, provider.connection, ); const account2 = await readFromRPCOrError( provider.connection, program, ship2.publicKey(), Ship, 'confirmed', ); expect(account2.data.updateId.eq(gameAccountBefore.data.updateId)).toBe( true, ); expect(account2.data.maxUpdateId.eq(gameAccountBefore.data.updateId)).toBe( true, ); expect(account2.data.next.key.equals(PublicKey.default)).toBe(true); // Game update_id should not change const gameAccountAfter = await readFromRPCOrError( provider.connection, program, gameKey, Game, 'confirmed', ); expect( gameAccountAfter.data.updateId.eq(gameAccountBefore.data.updateId), ).toBe(true); // can't invalidate inactive ship let success = true; try { await buildSendAndCheck( Ship.invalidateShip( program, shipManagerKey, sectorProfile.publicKey(), ship3.publicKey(), gameKey, SHIP_MANAGER, ), walletSigner, provider.connection, { suppressLogging: true }, ); } catch (err) { success = false; expect(JSON.stringify(err as Error)).toContain('InactiveAccount'); } expect(success).toBe(false); }); it('should set next ship', async function () { const gameAccountBefore = await readFromRPCOrError( provider.connection, program, gameKey, Game, 'confirmed', ); // can't work for inactive ship let success1 = true; try { await buildSendAndCheck( Ship.setNextShip( program, shipManagerKey, sectorProfile.publicKey(), ship3.publicKey(), ship4.publicKey(), gameKey, SHIP_MANAGER, ), walletSigner, provider.connection, { suppressLogging: true }, ); } catch (err) { expect(JSON.stringify(err as Error)).toContain('InactiveAccount'); success1 = false; } expect(success1).toBe(false); // can't work when next ship mints are different let success2 = true; try { await buildSendAndCheck( Ship.setNextShip( program, shipManagerKey, sectorProfile.publicKey(), ship.publicKey(), ship5.publicKey(), gameKey, SHIP_MANAGER, ), walletSigner, provider.connection, { suppressLogging: true }, ); } catch (err) { expect(JSON.stringify(err as Error)).toContain('MintMismatch'); success2 = false; } expect(success2).toBe(false); // can't work when next ship size classes are different let success3 = true; try { await buildSendAndCheck( Ship.setNextShip( program, shipManagerKey, sectorProfile.publicKey(), ship.publicKey(), ship3d.publicKey(), gameKey, SHIP_MANAGER, ), walletSigner, provider.connection, { suppressLogging: true }, ); } catch (err) { expect(JSON.stringify(err as Error)).toContain('InvalidShipSizeClass'); success3 = false; } expect(success3).toBe(false); // works okay when valid await buildSendAndCheck( Ship.setNextShip( program, shipManagerKey, sectorProfile.publicKey(), ship.publicKey(), ship3.publicKey(), gameKey, SHIP_MANAGER, ), walletSigner, provider.connection, ); const account = await readFromRPCOrError( provider.connection, program, ship.publicKey(), Ship, 'confirmed', ); const account3 = await readFromRPCOrError( provider.connection, program, ship3.publicKey(), Ship, 'confirmed', ); // Game update_id should not change const gameAccountAfter = await readFromRPCOrError( provider.connection, program, gameKey, Game, 'confirmed', ); expect( gameAccountAfter.data.updateId.eq(gameAccountBefore.data.updateId), ).toBe(true); // Ship update_id tests // old ship is stale but points to new ship expect(account.data.updateId.eq(gameAccountBefore.data.updateId)).toBe( true, ) /** does not change */; expect(account.data.maxUpdateId.eq(gameAccountBefore.data.updateId)).toBe( true, ) /** changes */; expect(account.data.next.key.equals(ship3.publicKey())).toBe(true); // new ship is up-to-date expect(account3.data.updateId.eq(gameAccountAfter.data.updateId)).toBe( true, ); expect(account3.data.maxUpdateId.eq(U64_MAX)).toBe(true); expect(account3.data.next.key.equals(PublicKey.default)).toBe(true); // can't work when next ship is active let success4 = true; try { await buildSendAndCheck( Ship.setNextShip( program, shipManagerKey, sectorProfile.publicKey(), ship3.publicKey(), ship4.publicKey(), gameKey, SHIP_MANAGER, ), walletSigner, provider.connection, { suppressLogging: true }, ); } catch (err) { expect(JSON.stringify(err as Error)).toContain('AlreadyActive'); success4 = false; } expect(success4).toBe(false); }); it('should copy a game state account', async function () { const oldGameStateAcc = await readFromRPCOrError( provider.connection, program, gameStateKey, GameState, 'confirmed', ); const gameAccountBefore = await readFromRPCOrError( provider.connection, program, gameKey, Game, 'confirmed', ); const newGameStateKey = GameState.findAddress( program, gameKey, gameAccountBefore.data.updateId.add(new BN(1)), ); await buildSendAndCheck( GameState.copyGameState( program, gameManagerKey, sectorProfile.publicKey(), gameKey, gameStateKey, newGameStateKey[0], { keyIndex: GAME_MANAGER }, ), walletSigner, provider.connection, ); const newGameStateAcc = await readFromRPCOrError( provider.connection, program, newGameStateKey[0], GameState, 'confirmed', ); expect( gameStateDataEquals(newGameStateAcc.data, { gameId: gameKey, fleet: oldGameStateAcc.data.fleet, misc: oldGameStateAcc.data.misc, version: oldGameStateAcc.data.version, updateId: gameAccountBefore.data.updateId.add(new BN(1)), bump: newGameStateKey[1], }), ).toBe(true); const gameAccountAfter1 = await readFromRPCOrError( provider.connection, program, gameKey, Game, 'confirmed', ); expect( gameAccountAfter1.data.updateId.eq(gameAccountBefore.data.updateId), ).toBe(true); // can activate copied game state await buildSendAndCheck( GameState.activateGameState( program, gameManagerKey, sectorProfile.publicKey(), gameKey, newGameStateKey[0], { keyIndex: GAME_MANAGER, }, ), walletSigner, provider.connection, ); const gameAccountAfter2 = await readFromRPCOrError( provider.connection, program, gameKey, Game, 'confirmed', ); expect( gameAccountAfter2.data.updateId.eq( gameAccountBefore.data.updateId.add(new BN(1)), ), ).toBe(true); expect( gameAccountAfter2.data.updateId.eq(newGameStateAcc.data.updateId), ).toBe(true); gameStateKey = newGameStateKey[0]; }); it('should register & update starbase', async function () { const discoverer = Keypair.generate().publicKey; const sector: [BN, BN] = [new BN(-13), new BN(0)]; const sectorResult = Sector.registerSector( program, sectorManagerKey, sectorProfile.publicKey(), discoverer, gameKey, sector, 'Test Sector', SECTOR_MANAGER, ); await buildSendAndCheck( [ sectorResult.instructions, Starbase.registerStarbase( program, authority, sectorProfile.publicKey(), sectorResult.sectorKey[0], gameKey, gameStateKey, { name: stringToByteArray('MXC 2', 64), subCoordinates: sector, sectorCoordinates: sector, starbaseLevelIndex: 2, faction: Faction.MUD, keyIndex: 0, }, ), ], walletSigner, provider.connection, ); const starbaseKey = Starbase.findAddress(program, gameKey, sector); const starbaseAccount = await readFromRPCOrError( provider.connection, program, starbaseKey[0], Starbase, 'confirmed', ); const gameState = await readFromRPCOrError( provider.connection, program, gameStateKey, GameState, 'confirmed', ); const starbaseLevelInfo = ( gameState.data.fleet.starbaseLevels as FactionsStarbaseLevelInfo ).mud[2]; const expectedStarbase: StarbaseAccount = { version: 0, gameId: gameKey, sector, subCoordinates: sector, craftingFacility: PublicKey.default, upgradeFacility: PublicKey.default, name: stringToByteArray('MXC 2', 64), faction: Faction.MUD, bump: starbaseKey[1], seqId: 0, hp: starbaseLevelInfo.hp, sp: starbaseLevelInfo.sp, state: StarbaseState.Active, level: 2, sectorRingAvailable: starbaseLevelInfo.sectorRingAvailable, upgradeState: StarbaseUpgradeState.NotStarted, numUpgradeIngredients: 0, upgradeIngredientsChecksum: Array(16).fill(0), upkeepAmmoBalance: new BN(0), upkeepAmmoLastUpdate: starbaseAccount.data.upkeepAmmoLastUpdate, upkeepAmmoGlobalLastUpdate: starbaseAccount.data.upkeepAmmoGlobalLastUpdate, upkeepFoodBalance: new BN(0), upkeepFoodLastUpdate: starbaseAccount.data.upkeepFoodLastUpdate, upkeepFoodGlobalLastUpdate: starbaseAccount.data.upkeepFoodGlobalLastUpdate, upkeepToolkitBalance: new BN(0), upkeepToolkitLastUpdate: starbaseAccount.data.upkeepToolkitLastUpdate, upkeepToolkitGlobalLastUpdate: starbaseAccount.data.upkeepToolkitGlobalLastUpdate, builtDestroyedTimestamp: new BN(0), shieldBreakDelayExpiresAt: new BN(0), }; expect(starbaseDataEquals(expectedStarbase, starbaseAccount.data)).toBe( true, ); expect(starbaseAccount.upgradeIngredientAmounts.length).toBe(0); await buildSendAndCheck( Starbase.updateStarbase( program, starbaseKey[0], authority, sectorProfile.publicKey(), gameKey, { name: stringToByteArray('Starbase Alpha', 64), subCoordinates: [new BN(1), new BN(1)], keyIndex: 0, }, ), walletSigner, provider.connection, ); const starbaseAccount2 = await readFromRPCOrError( provider.connection, program, starbaseKey[0], Starbase, 'confirmed', ); expect(byteArrayToString(starbaseAccount2.data.name)).toBe( 'Starbase Alpha', ); expect( arrayDeepEquals( starbaseAccount2.data.subCoordinates, [new BN(1), new BN(1)], (a, b) => a.eq(b), ), ).toBe(true); }); it('should register negative sector', async function () { const discoverer = Keypair.generate().publicKey; const max = Number.MAX_SAFE_INTEGER; const sectors: [BN, BN][] = [ [new BN(max), new BN(max)], [new BN(max), new BN(-max)], [new BN(-max), new BN(max)], [new BN(-max), new BN(-max)], ]; for (const sector of sectors) { const ix = Sector.registerSector( program, sectorManagerKey, sectorProfile.publicKey(), discoverer, gameKey, sector, 'Test Sector', SECTOR_MANAGER, ); expect(ix.sectorKey).toBeTruthy(); await buildSendAndCheck(ix.instructions, walletSigner, connection); const sectorData = await readFromRPCOrError( provider.connection, program, ix.sectorKey[0], Sector, 'confirmed', ); const expectedSector = { version: 0, gameId: gameKey, coordinates: sector, discoverer, name: stringToByteArray('Test Sector', 64), numStars: 0, numPlanets: 0, numMoons: 0, numConnections: 0, numAsteroidBelts: 0, lastScanTime: new BN(0), lastScanChance: 0, bump: ix.sectorKey[1], }; expect(sectorData.key.equals(ix.sectorKey[0])).toBe(true); expect(sectorDataEquals(sectorData.data, expectedSector)).toBe(true); expect(sectorData.connections.length).toBe(0); await buildSendAndCheck( Starbase.registerStarbase( program, authority, sectorProfile.publicKey(), ix.sectorKey[0], gameKey, gameStateKey, { name: stringToByteArray('MXC 2', 64), subCoordinates: sector, sectorCoordinates: sector, starbaseLevelIndex: 2, faction: Faction.MUD, keyIndex: 0, }, ), walletSigner, provider.connection, ); const starbaseKey = Starbase.findAddress(program, gameKey, sector); const starbaseAccount = await readFromRPCOrError( provider.connection, program, starbaseKey[0], Starbase, 'confirmed', ); expect( arrayDeepEquals(starbaseAccount.data.sector, sector, (a, b) => a.eq(b)), ).toBe(true); expect( arrayDeepEquals(starbaseAccount.data.subCoordinates, sector, (a, b) => a.eq(b), ), ).toBe(true); } }); it('should register points modifier', async function () { const categories: Array<[PublicKey, PointsCategoryType]> = [ [_lpCategory.publicKey(), PointsCategoryType.LP], [_councilRankXpCategory.publicKey(), PointsCategoryType.CRXP], [_pilotXpCategory.publicKey(), PointsCategoryType.PXP], [_dataRunningXpCategory.publicKey(), PointsCategoryType.DRXP], [_miningXpCategory.publicKey(), PointsCategoryType.MXP], [_craftingXpCategory.publicKey(), PointsCategoryType.CXP], [_combatXpCategory.publicKey(), PointsCategoryType.COXP], ]; for (let index = 0; index < categories.length; index++) { const element = categories[index]; const category = element[0]; const categoryType = element[1]; const modifierResult = Game.registerSagePointModifier( program, pointsProgram, authority, sectorProfile.publicKey(), gameKey, category, categoryType, 0, ); await buildSendAndCheck( modifierResult.instructions, walletSigner, provider.connection, ); const modifierData = await readFromRPCOrError( provider.connection, pointsProgram, modifierResult.modifierKey[0], PointsModifier, ); expect(modifierData.data.pointCategory.equals(category)).toBe(true); expect(modifierData.data.canIncrement).toBe(1); expect(modifierData.data.canDecrement).toBe(1); } }); it('should setup progression config', async function () { await setupProgression( authority, walletSigner, sectorProfile.publicKey(), gameKey, program, connection, ); const pointLimits = { dailyLpLimit: new BN(1_000_000), dailyCouncilRankXpLimit: new BN(1_100_000), dailyPilotXpLimit: new BN(1_200_000), dailyDataRunningXpLimit: new BN(1_300_000), dailyMiningXpLimit: new BN(1_400_000), dailyCraftingXpLimit: new BN(1_500_000), }; await sendManyToChain( [ ProgressionConfig.updateProgressionConfig( program, authority, sectorProfile.publicKey(), gameKey, { ...pointLimits, keyIndex: 0, }, ), ], walletSigner, connection, ); const configKey = ProgressionConfig.findAddress(program, gameKey); const configData = await readFromRPCOrError( provider.connection, program, configKey[0], ProgressionConfig, ); expect( progressionConfigDataEquals(configData.data, { ...pointLimits, version: 0, gameId: gameKey, numItems: Object.keys(ProgressionItemType).filter((key) => isNaN(Number(key)), ).length, }), ).toBe(true); expect(configData.items.length).toBe( Object.keys(ProgressionItemType).filter((key) => isNaN(Number(key))) .length, ); for (let index = 0; index < configData.items.length; index++) { const element = configData.items[index]; expect(element.value).toBe((index + 1) * GLOBAL_SCALE_DECIMALS_4); } // deregister await sendManyToChain( [ ProgressionConfig.deregisterProgressionConfig( program, authority, sectorProfile.publicKey(), 'funder', gameKey, { keyIndex: 0, }, ), ], walletSigner, connection, ); const configData2 = await readFromRPCNullable( provider.connection, program, configKey[0], ProgressionConfig, ); expect(configData2).toBeNull; }); });