import { getAccount, getAssociatedTokenAddressSync } from '@solana/spl-token'; import { Connection, Keypair, PublicKey } from '@solana/web3.js'; import { BN } from '@staratlas/anchor'; import { createAssociatedTokenAccountIdempotent, getCurrentTimestampOnChain, keypairToAsyncSigner, readFromRPCNullable, readFromRPCOrError, } from '@staratlas/data-source'; import { UserPoints } from '@staratlas/points'; import { UserRedemption } from '@staratlas/points-store'; import { Fleet, GLOBAL_SCALE_DECIMALS_2, ProgressionItemType, ShipStats, Starbase, UpkeepResourceType, } from '../src'; import { SageGameTest, calculateMiningResults } from './helpers'; describe('Starbase Upkeep', () => { const connection = new Connection('http://localhost:8899', 'confirmed'); const walletKeypair = Keypair.generate(); const walletSigner = keypairToAsyncSigner(walletKeypair); let ammoRecipe: PublicKey; let foodRecipe: PublicKey; let toolkitRecipe: PublicKey; const ammoRecipeValue = 1.1; const foodRecipeValue = 0.78; const toolKitRecipeValue = 2; const redemptionConfig = keypairToAsyncSigner(Keypair.generate()); const userRedemption = keypairToAsyncSigner(Keypair.generate()); let sageGameTest: SageGameTest; const userRedemptionExists = async () => { return ( (await readFromRPCNullable( sageGameTest.connection, sageGameTest.pointsStoreProgram, userRedemption.publicKey(), UserRedemption, 'confirmed', )) === null ); }; const loadUserRedemption = async () => { return await readFromRPCOrError( sageGameTest.connection, sageGameTest.pointsStoreProgram, userRedemption.publicKey(), UserRedemption, 'confirmed', ); }; beforeAll(async () => { const airdropTx = await connection.requestAirdrop( walletSigner.publicKey(), 500_000_000_000, ); await connection.confirmTransaction(airdropTx); // trying to get fleets with 0 crew to allow for the large number of ships we need for this test const defaultShipStats = SageGameTest.getDefaultShipStats(); defaultShipStats.miscStats.requiredCrew = 0; // create game sageGameTest = await SageGameTest.createGame({ funder: walletKeypair, connection, numberOfShips: 1, setupCraftingAccounts: true, fleet: { maxFleetSize: 1_333_337 }, numberOfShipsInFleet: 1337, addCargoToFleet: false, fleetToIdle: false, shipStats: defaultShipStats, }); const resourceRecipes = await sageGameTest.setupResourceRecipes([ { name: SageGameTest.AMMO, recipeValue: ammoRecipeValue, }, { name: SageGameTest.FOOD, recipeValue: foodRecipeValue, }, { name: SageGameTest.REPAIR_KIT, recipeValue: toolKitRecipeValue, }, ]); ammoRecipe = resourceRecipes[0]; foodRecipe = resourceRecipes[1]; toolkitRecipe = resourceRecipes[2]; await sageGameTest.setupAtlasRedemption(redemptionConfig, userRedemption); }); it('prepare', async () => { if ( !sageGameTest.gameState || !sageGameTest.cargoStatsDefinition || !sageGameTest.craftingDomain || !sageGameTest.mints || !sageGameTest.sectors || !sageGameTest.fleets || !sageGameTest.recipes ) { throw 'game incorrectly set up'; } // create upgradeable starbase and move to it const newSectorCoords = [new BN(0), new BN(-1)] as [BN, BN]; await sageGameTest.createSector({ coordinates: newSectorCoords, addResourcesToStarbaseCargoPod: false, setUpPlayer: false, starbaseLevel: 1, createAsteroid: true, }); const oldSector = sageGameTest.sectors[0]; const newSector = sageGameTest.sectors[1]; const fleet1 = sageGameTest.fleets[0]; const fleetAccount = await sageGameTest.loadFleet(fleet1); // move to upgradeable starbase const cargoAmount = Math.floor( (fleetAccount.data.stats).cargoStats.cargoCapacity / 3, ); const miningFoodAmount = Math.min( Math.floor( (fleetAccount.data.stats).cargoStats.cargoCapacity / 1, ), 10_000, ); await sageGameTest.sendManyToChain( [ ...sageGameTest.depositCargoToFleetInstructions( fleet1, fleetAccount.data.fuelTank, oldSector.starbases[0], oldSector.starbasePlayers[0].starbasePlayer, sageGameTest.mints[SageGameTest.FUEL], (fleetAccount.data.stats).cargoStats.fuelCapacity, ), ...sageGameTest.depositCargoToFleetInstructions( fleet1, fleetAccount.data.ammoBank, oldSector.starbases[0], oldSector.starbasePlayers[0].starbasePlayer, sageGameTest.mints[SageGameTest.AMMO], (fleetAccount.data.stats).cargoStats.ammoCapacity, ), ...sageGameTest.depositCargoToFleetInstructions( fleet1, fleetAccount.data.cargoHold, oldSector.starbases[0], oldSector.starbasePlayers[0].starbasePlayer, sageGameTest.mints[SageGameTest.AMMO], cargoAmount, ), ...sageGameTest.depositCargoToFleetInstructions( fleet1, fleetAccount.data.cargoHold, oldSector.starbases[0], oldSector.starbasePlayers[0].starbasePlayer, sageGameTest.mints[SageGameTest.FOOD], cargoAmount, ), ...sageGameTest.depositCargoToFleetInstructions( fleet1, fleetAccount.data.cargoHold, oldSector.starbases[0], oldSector.starbasePlayers[0].starbasePlayer, sageGameTest.mints[SageGameTest.REPAIR_KIT], cargoAmount, ), Fleet.loadingBayToIdle( sageGameTest.program, sageGameTest.profiles.player1.key, sageGameTest.profiles.player1.profile, sageGameTest.getProfileFactionAddress( sageGameTest.profiles.player1.profile, ), fleet1, oldSector.starbases[0], oldSector.starbasePlayers[0].starbasePlayer, sageGameTest.gameId, sageGameTest.gameState, sageGameTest.profiles.player1.index, ), ...sageGameTest.warpToCoordinateInstructions( fleetAccount.key, fleetAccount.data.fuelTank, newSectorCoords, ), ], true, ); const fleetAccount2 = await sageGameTest.loadFleet(fleet1); if (!fleetAccount2.state.MoveWarp) { throw 'Fleet should be in MoveWarp state'; } const moveTime = fleetAccount2.state.MoveWarp.warpFinish .sub(fleetAccount2.state.MoveWarp.warpStart) .toNumber(); // wait for movement to finish await new Promise((r) => setTimeout( r, (Math.max( moveTime, (fleetAccount2.data.stats).movementStats.warpCoolDown, ) + 1) * 1000, ), ); // withdraw cargo & undock await sageGameTest.sendManyToChain( [ ...sageGameTest.moveWarpHandlerInstructions(fleet1), Fleet.idleToLoadingBay( sageGameTest.program, sageGameTest.profiles.player1.key, sageGameTest.profiles.player1.profile, sageGameTest.getProfileFactionAddress( sageGameTest.profiles.player1.profile, ), fleet1, newSector.starbases[0], newSector.starbasePlayers[0].starbasePlayer, sageGameTest.gameId, sageGameTest.gameState, sageGameTest.profiles.player1.index, ), ...sageGameTest.withdrawCargoFromFleetInstructions( fleet1, fleetAccount.data.cargoHold, newSector.starbases[0], newSector.starbasePlayers[0].starbasePlayer, sageGameTest.mints[SageGameTest.AMMO], cargoAmount, ), ...sageGameTest.withdrawCargoFromFleetInstructions( fleet1, fleetAccount.data.cargoHold, newSector.starbases[0], newSector.starbasePlayers[0].starbasePlayer, sageGameTest.mints[SageGameTest.FOOD], cargoAmount, ), ...sageGameTest.withdrawCargoFromFleetInstructions( fleet1, fleetAccount.data.cargoHold, newSector.starbases[0], newSector.starbasePlayers[0].starbasePlayer, sageGameTest.mints[SageGameTest.REPAIR_KIT], cargoAmount, ), ...sageGameTest.depositCargoToFleetInstructions( fleet1, fleetAccount.data.cargoHold, newSector.starbases[0], newSector.starbasePlayers[0].starbasePlayer, sageGameTest.mints[SageGameTest.FOOD], miningFoodAmount, ), Fleet.loadingBayToIdle( sageGameTest.program, sageGameTest.profiles.player1.key, sageGameTest.profiles.player1.profile, sageGameTest.getProfileFactionAddress( sageGameTest.profiles.player1.profile, ), fleet1, newSector.starbases[0], newSector.starbasePlayers[0].starbasePlayer, sageGameTest.gameId, sageGameTest.gameState, sageGameTest.profiles.player1.index, ), ], true, ); }); it('mining penalty applies when ammo upkeep balance is 0', async () => { if ( !sageGameTest.gameState || !sageGameTest.cargoStatsDefinition || !sageGameTest.craftingDomain || !sageGameTest.mints || !sageGameTest.sectors || !sageGameTest.fleets || !sageGameTest.recipes ) { throw 'game incorrectly set up'; } const sector = sageGameTest.sectors[1]; const asteroid = sageGameTest.sectors[1].planets[1]; const mint = sageGameTest.mints[SageGameTest.REPAIR_KIT]; const fleet1 = sageGameTest.fleets[0]; const fleetAccount = await sageGameTest.loadFleet(fleet1); const fleetAmmoToken = createAssociatedTokenAccountIdempotent( sageGameTest.mints[SageGameTest.AMMO], fleetAccount.data.ammoBank, true, ); const fleetFoodToken = createAssociatedTokenAccountIdempotent( sageGameTest.mints[SageGameTest.FOOD], fleetAccount.data.cargoHold, true, ); const fleetResourceToken = createAssociatedTokenAccountIdempotent( mint, fleetAccount.data.cargoHold, true, ); const gameState = await sageGameTest.loadGameState(); const mineItem = await sageGameTest.loadMineItemByMint(mint); const resource = await sageGameTest.loadResourceByMintAndLocation( mint, asteroid, ); // start mining await sageGameTest.sendManyToChain([ fleetAmmoToken.instructions, fleetFoodToken.instructions, fleetResourceToken.instructions, ...sageGameTest.startMiningAsteroidInstructions( fleet1, sector.starbases[0], sector.starbasePlayers[0].starbasePlayer, asteroid, mint, fleetAccount.data.fuelTank, ), ]); const resourceTokenBefore = await getAccount( connection, fleetResourceToken.address, 'processed', ); const fleetAmmoTokenBefore = await getAccount( connection, fleetAmmoToken.address, 'processed', ); const fleetFoodTokenBefore = await getAccount( connection, fleetFoodToken.address, 'processed', ); // wait for some mining to happen and harvest await new Promise((r) => setTimeout(r, 2000)); await sageGameTest.sendManyToChain( sageGameTest.asteroidMiningHandlerInstructions( fleet1, sector.starbases[0], asteroid, mint, fleetAccount.data.cargoHold, fleetAccount.data.ammoBank, ), ); const resourceTokenAfter = await getAccount( connection, fleetResourceToken.address, 'processed', ); const fleetAccountAfter = await sageGameTest.loadFleet(fleet1); const penalty = gameState.data.misc.upkeepMiningEmissionsPenalty / GLOBAL_SCALE_DECIMALS_2; const miningResults = calculateMiningResults( fleetAccountAfter, fleetFoodTokenBefore, fleetAmmoTokenBefore, mineItem.data, resource.data, penalty, ); expect( Number(resourceTokenAfter.delegatedAmount) - Number(resourceTokenBefore.delegatedAmount), ).toBe(Math.floor(miningResults.resourceExtracted)); }); it('deposit upkeep resources', async () => { if ( !sageGameTest.gameState || !sageGameTest.cargoStatsDefinition || !sageGameTest.craftingDomain || !sageGameTest.mints || !sageGameTest.sectors || !sageGameTest.fleets || !sageGameTest.pointCategories || !sageGameTest.recipes ) { throw 'game incorrectly set up'; } const sector = sageGameTest.sectors[1]; const starbaseAcct = await readFromRPCOrError( sageGameTest.connection, sageGameTest.program, sector.starbases[0], Starbase, 'confirmed', ); const lpBefore = ( await readFromRPCOrError( connection, sageGameTest.pointsProgram, sageGameTest.getUserPointsAccountAddress( sageGameTest.pointCategories.lpCategory, ), UserPoints, ) ).data.earnedPoints.toNumber(); const ammoAmount = new BN(700); const foodAmount = new BN(650); const toolkitAmount = new BN(900); const ammoReserve = new BN(1000); const ammoDepletionRate = 10.0; const foodReserve = new BN(1010); const foodDepletionRate = 11.0; const toolkitReserve = new BN(1020); const toolkitDepletionRate = 500.0; const ammoTokenKey = getAssociatedTokenAddressSync( sageGameTest.mints[SageGameTest.AMMO], sector.starbasePlayers[0].cargoPod, true, ); let initializeUserRedemption = false; let initialRedeemedPoints = new BN(0); try { initialRedeemedPoints = (await loadUserRedemption()).data.points; } catch { initializeUserRedemption = true; } const sageGameTestGameState = sageGameTest.gameState; const sageGameTestMints = sageGameTest.mints; const sageGameTestCargoStatsDefinition = sageGameTest.cargoStatsDefinition; const sageGameTestPointCategories = sageGameTest.pointCategories; const getDepositAmmo = (startingRedemption = false) => Starbase.depositStarbaseUpkeepResource( sageGameTest.program, sageGameTest.cargoProgram, sageGameTest.pointsProgram, sageGameTest.profiles.player1.key, sageGameTest.profiles.player1.profile, sageGameTest.getProfileFactionAddress( sageGameTest.profiles.player1.profile, ), 'funder', sector.starbasePlayers[0].starbasePlayer, sector.starbases[0], sageGameTest.gameId, sageGameTestGameState, sector.starbasePlayers[0].cargoPod, sageGameTest.getCargoTypeAddress(sageGameTestMints[SageGameTest.AMMO]), sageGameTestCargoStatsDefinition, ammoRecipe, ammoTokenKey, sageGameTestMints[SageGameTest.AMMO], sageGameTest.getUserPointsAccountAddress( sageGameTestPointCategories.lpCategory, ), sageGameTestPointCategories.lpCategory, sageGameTest.getPointsModifierAddress( sageGameTestPointCategories.lpCategory, ), startingRedemption ? userRedemption : userRedemption.publicKey(), // userRedemption redemptionConfig.publicKey(), // redemptionConfig sageGameTest.pointsStoreProgram, // pointsStoreProgram { pointsProgramPermissionsKeyIndex: sageGameTest.profiles.player1.index, sagePermissionsKeyIndex: sageGameTest.profiles.player1.index, amount: ammoAmount, resourceIndex: 0, epochIndex: 0, resourceType: UpkeepResourceType.Ammo, }, ); const foodTokenKey = getAssociatedTokenAddressSync( sageGameTest.mints[SageGameTest.FOOD], sector.starbasePlayers[0].cargoPod, true, ); const depositFood = Starbase.depositStarbaseUpkeepResource( sageGameTest.program, sageGameTest.cargoProgram, sageGameTest.pointsProgram, sageGameTest.profiles.player1.key, sageGameTest.profiles.player1.profile, sageGameTest.getProfileFactionAddress( sageGameTest.profiles.player1.profile, ), 'funder', sector.starbasePlayers[0].starbasePlayer, sector.starbases[0], sageGameTest.gameId, sageGameTest.gameState, sector.starbasePlayers[0].cargoPod, sageGameTest.getCargoTypeAddress(sageGameTest.mints[SageGameTest.FOOD]), sageGameTest.cargoStatsDefinition, foodRecipe, foodTokenKey, sageGameTest.mints[SageGameTest.FOOD], sageGameTest.getUserPointsAccountAddress( sageGameTest.pointCategories.lpCategory, ), sageGameTest.pointCategories.lpCategory, sageGameTest.getPointsModifierAddress( sageGameTest.pointCategories.lpCategory, ), userRedemption.publicKey(), // userRedemption redemptionConfig.publicKey(), // redemptionConfig sageGameTest.pointsStoreProgram, // pointsStoreProgram { sagePermissionsKeyIndex: sageGameTest.profiles.player1.index, pointsProgramPermissionsKeyIndex: sageGameTest.profiles.player1.index, amount: foodAmount, resourceIndex: 0, resourceType: UpkeepResourceType.Food, epochIndex: 0, }, ); const toolkitTokenKey = getAssociatedTokenAddressSync( sageGameTest.mints[SageGameTest.REPAIR_KIT], sector.starbasePlayers[0].cargoPod, true, ); const depositToolkit = Starbase.depositStarbaseUpkeepResource( sageGameTest.program, sageGameTest.cargoProgram, sageGameTest.pointsProgram, sageGameTest.profiles.player1.key, sageGameTest.profiles.player1.profile, sageGameTest.getProfileFactionAddress( sageGameTest.profiles.player1.profile, ), 'funder', sector.starbasePlayers[0].starbasePlayer, sector.starbases[0], sageGameTest.gameId, sageGameTest.gameState, sector.starbasePlayers[0].cargoPod, sageGameTest.getCargoTypeAddress( sageGameTest.mints[SageGameTest.REPAIR_KIT], ), sageGameTest.cargoStatsDefinition, toolkitRecipe, toolkitTokenKey, sageGameTest.mints[SageGameTest.REPAIR_KIT], sageGameTest.getUserPointsAccountAddress( sageGameTest.pointCategories.lpCategory, ), sageGameTest.pointCategories.lpCategory, sageGameTest.getPointsModifierAddress( sageGameTest.pointCategories.lpCategory, ), userRedemption.publicKey(), // userRedemption redemptionConfig.publicKey(), // redemptionConfig sageGameTest.pointsStoreProgram, // pointsStoreProgram { pointsProgramPermissionsKeyIndex: sageGameTest.profiles.player1.index, sagePermissionsKeyIndex: sageGameTest.profiles.player1.index, amount: toolkitAmount, epochIndex: 0, resourceIndex: 0, resourceType: UpkeepResourceType.Toolkit, }, ); const ammoTokenAcc = await getAccount( connection, ammoTokenKey, 'confirmed', ); const foodTokenAcc = await getAccount( connection, foodTokenKey, 'confirmed', ); const toolkitTokenAcc = await getAccount( connection, toolkitTokenKey, 'confirmed', ); await sageGameTest.sendManyToChain( [getDepositAmmo(initializeUserRedemption), depositFood, depositToolkit], true, ); const starbaseAcct1 = await readFromRPCOrError( sageGameTest.connection, sageGameTest.program, sector.starbases[0], Starbase, 'confirmed', ); expect( starbaseAcct1.data.upkeepAmmoBalance .sub(starbaseAcct.data.upkeepAmmoBalance) .eq(ammoAmount), ).toBe(true); expect( starbaseAcct1.data.upkeepFoodBalance .sub(starbaseAcct.data.upkeepFoodBalance) .eq(foodAmount), ).toBe(true); expect( starbaseAcct1.data.upkeepToolkitBalance .sub(starbaseAcct.data.upkeepToolkitBalance) .eq(toolkitAmount), ).toBe(true); expect( starbaseAcct1.data.upkeepAmmoLastUpdate.eq( starbaseAcct.data.upkeepAmmoLastUpdate, ), ).toBe(true); // Because starbases now continue to operate at some modified rate even without food, the last update timer // should be greater than the previous one. Without using a custom clock, it is hard to make this test deterministic. // Testing that the last update is greater than the previous one at least demonstrates that the starbase is still // functioning expect( starbaseAcct1.data.upkeepFoodLastUpdate.gt( starbaseAcct.data.upkeepFoodLastUpdate, ), ).toBe(true); expect( starbaseAcct1.data.upkeepToolkitLastUpdate.eq( starbaseAcct.data.upkeepToolkitLastUpdate, ), ).toBe(true); expect( starbaseAcct1.data.upkeepAmmoGlobalLastUpdate.gt( starbaseAcct.data.upkeepAmmoGlobalLastUpdate, ), ).toBe(true); expect( starbaseAcct1.data.upkeepFoodGlobalLastUpdate.gt( starbaseAcct.data.upkeepFoodGlobalLastUpdate, ), ).toBe(true); expect( starbaseAcct1.data.upkeepToolkitGlobalLastUpdate.gt( starbaseAcct.data.upkeepToolkitGlobalLastUpdate, ), ).toBe(true); const ammoTokenAcc1 = await getAccount( connection, ammoTokenKey, 'confirmed', ); const foodTokenAcc1 = await getAccount( connection, foodTokenKey, 'confirmed', ); const toolkitTokenAcc1 = await getAccount( connection, toolkitTokenKey, 'confirmed', ); expect( Number(ammoTokenAcc.delegatedAmount - ammoTokenAcc1.delegatedAmount), ).toBe(ammoAmount.toNumber()); expect( Number(foodTokenAcc.delegatedAmount - foodTokenAcc1.delegatedAmount), ).toBe(foodAmount.toNumber()); expect( Number( toolkitTokenAcc.delegatedAmount - toolkitTokenAcc1.delegatedAmount, ), ).toBe(toolkitAmount.toNumber()); const lpAfter = ( await readFromRPCOrError( connection, sageGameTest.pointsProgram, sageGameTest.getUserPointsAccountAddress( sageGameTest.pointCategories.lpCategory, ), UserPoints, ) ).data.earnedPoints.toNumber(); const earnedLP = Math.floor( toolKitRecipeValue * toolkitAmount.toNumber() * (ProgressionItemType.Upkeep + 1), ) + Math.floor( ammoRecipeValue * ammoAmount.toNumber() * (ProgressionItemType.Upkeep + 1), ) + Math.floor( foodRecipeValue * foodAmount.toNumber() * (ProgressionItemType.Upkeep + 1), ); expect(lpAfter).toBe(lpBefore + earnedLP); // confirm auto-redemption worked for starbase upkeep expect( (await loadUserRedemption()).data.points.eq( initialRedeemedPoints.add(new BN(earnedLP)), ), ).toBe(true); // wait for some resource consumption await new Promise((r) => setTimeout(r, 2000)); await sageGameTest.sendManyToChain( [getDepositAmmo(false), depositFood, depositToolkit].flatMap((i) => [ i, i, i, ]) /** deposit everything three times */, true, ); const starbaseAcct2 = await readFromRPCOrError( sageGameTest.connection, sageGameTest.program, sector.starbases[0], Starbase, 'confirmed', ); expect(starbaseAcct2.data.upkeepAmmoBalance.eq(ammoReserve)).toBe(true); expect(starbaseAcct2.data.upkeepFoodBalance.eq(foodReserve)).toBe(true); expect(starbaseAcct2.data.upkeepToolkitBalance.eq(toolkitReserve)).toBe( true, ); const ammoTokenAcc2 = await getAccount( connection, ammoTokenKey, 'confirmed', ); const foodTokenAcc2 = await getAccount( connection, foodTokenKey, 'confirmed', ); const toolkitTokenAcc2 = await getAccount( connection, toolkitTokenKey, 'confirmed', ); const expectedAmmoDeposited = ammoReserve.sub(starbaseAcct1.data.upkeepAmmoBalance).toNumber() + (starbaseAcct2.data.upkeepAmmoLastUpdate.toNumber() - starbaseAcct1.data.upkeepAmmoLastUpdate.toNumber()) * ammoDepletionRate; expect( Number(ammoTokenAcc1.delegatedAmount - ammoTokenAcc2.delegatedAmount), ).toBe(expectedAmmoDeposited); // The starbase is still functioning at a modified rate between `starbaseAcct` and `starbaseAcct1` so there are // changes to `upkeepFoodBalance` prior to fetching `starbaseAcct1` but these changes are dependent on the time and // attempting to test them would be non-deterministic and lead to flaky tests. Instead, we can test that there is // a change in `upkeepFoodBalance` between `starbaseAcct1` and `starbaseAcct2` which demonstrates that the starbase // is still functioning const expectedFoodDeposited = foodReserve.sub(starbaseAcct1.data.upkeepFoodBalance).toNumber() + (starbaseAcct2.data.upkeepFoodLastUpdate.toNumber() - starbaseAcct1.data.upkeepFoodLastUpdate.toNumber()) * foodDepletionRate; expect( Number(foodTokenAcc1.delegatedAmount - foodTokenAcc2.delegatedAmount), ).toBeGreaterThanOrEqual(expectedFoodDeposited); const expectedToolkitDeposited = toolkitReserve.toNumber(); expect( Number( toolkitTokenAcc1.delegatedAmount - toolkitTokenAcc2.delegatedAmount, ), ).toBe(expectedToolkitDeposited); }); it('upkeeping ammo removes mining penalty', async () => { if ( !sageGameTest.gameState || !sageGameTest.cargoStatsDefinition || !sageGameTest.craftingDomain || !sageGameTest.mints || !sageGameTest.sectors || !sageGameTest.fleets || !sageGameTest.recipes ) { throw 'game incorrectly set up'; } const sector = sageGameTest.sectors[1]; const asteroid = sageGameTest.sectors[1].planets[1]; const mint = sageGameTest.mints[SageGameTest.REPAIR_KIT]; const fleet1 = sageGameTest.fleets[0]; const fleetAccount = await sageGameTest.loadFleet(fleet1); if (!fleetAccount.state.MineAsteroid) { throw 'Fleet not mining'; } const fleetAmmoToken = createAssociatedTokenAccountIdempotent( sageGameTest.mints[SageGameTest.AMMO], fleetAccount.data.ammoBank, true, ); const fleetFoodToken = createAssociatedTokenAccountIdempotent( sageGameTest.mints[SageGameTest.FOOD], fleetAccount.data.cargoHold, true, ); const fleetResourceToken = createAssociatedTokenAccountIdempotent( mint, fleetAccount.data.cargoHold, true, ); const mineItem = await sageGameTest.loadMineItemByMint(mint); const resource = await sageGameTest.loadResourceByMintAndLocation( mint, asteroid, ); const resourceTokenBefore = await getAccount( connection, fleetResourceToken.address, 'processed', ); const fleetAmmoTokenBefore = await getAccount( connection, fleetAmmoToken.address, 'processed', ); const fleetFoodTokenBefore = await getAccount( connection, fleetFoodToken.address, 'processed', ); // wait for some mining to happen and harvest await new Promise((r) => setTimeout(r, 1500)); await sageGameTest.sendManyToChain( sageGameTest.asteroidMiningHandlerInstructions( fleet1, sector.starbases[0], asteroid, mint, fleetAccount.data.cargoHold, fleetAccount.data.ammoBank, ), ); const resourceTokenAfter = await getAccount( connection, fleetResourceToken.address, 'processed', ); const fleetAccountAfter = await sageGameTest.loadFleet(fleet1); const penalty = 0; /** no penalty */ const miningResults = calculateMiningResults( fleetAccountAfter, fleetFoodTokenBefore, fleetAmmoTokenBefore, mineItem.data, resource.data, penalty, fleetAccount.state.MineAsteroid.lastUpdate, ); expect( Number(resourceTokenAfter.delegatedAmount) - Number(resourceTokenBefore.delegatedAmount), ).toBe(Math.floor(miningResults.resourceExtracted)); }); it('starbase time works', async () => { if ( !sageGameTest.gameState || !sageGameTest.cargoStatsDefinition || !sageGameTest.craftingDomain || !sageGameTest.mints || !sageGameTest.sectors || !sageGameTest.fleets || !sageGameTest.pointCategories || !sageGameTest.recipes ) { throw 'game incorrectly set up'; } const sageGameTestGameState = sageGameTest.gameState; const sageGameTestMints = sageGameTest.mints; const sageGameTestCargoStatsDefinition = sageGameTest.cargoStatsDefinition; const sageGameTestPointCategories = sageGameTest.pointCategories; const sector = sageGameTest.sectors[1]; const gameStateAccount = await sageGameTest.loadGameState(); const toolkitTokenKey = getAssociatedTokenAddressSync( sageGameTest.mints[SageGameTest.REPAIR_KIT], sector.starbasePlayers[0].cargoPod, true, ); const toolkitReserve = new BN(1020); const toolkitDepletionRate = 500.0; const getData = async () => { const starbaseAcct = await readFromRPCOrError( sageGameTest.connection, sageGameTest.program, sector.starbases[0], Starbase, 'confirmed', ); const upkeepInfo = starbaseAcct.getUpkeepInfo(gameStateAccount); return { depletionRate: upkeepInfo.toolkitDepletionRate, lastUpdate: starbaseAcct.data.upkeepToolkitLastUpdate, globalLastUpdate: starbaseAcct.data.upkeepToolkitGlobalLastUpdate, balance: starbaseAcct.data.upkeepToolkitBalance, starbaseTime: starbaseAcct.getStarbaseTime( UpkeepResourceType.Toolkit, new BN((await getCurrentTimestampOnChain(connection)).toString()), gameStateAccount, ), }; }; const depositToolkit = async (newToolkitAmount: number) => { const initializeUserRedemption = await userRedemptionExists(); await sageGameTest.sendManyToChain( [ Starbase.depositStarbaseUpkeepResource( sageGameTest.program, sageGameTest.cargoProgram, sageGameTest.pointsProgram, sageGameTest.profiles.player1.key, sageGameTest.profiles.player1.profile, sageGameTest.getProfileFactionAddress( sageGameTest.profiles.player1.profile, ), 'funder', sector.starbasePlayers[0].starbasePlayer, sector.starbases[0], sageGameTest.gameId, sageGameTestGameState, sector.starbasePlayers[0].cargoPod, sageGameTest.getCargoTypeAddress( sageGameTestMints[SageGameTest.REPAIR_KIT], ), sageGameTestCargoStatsDefinition, toolkitRecipe, toolkitTokenKey, sageGameTestMints[SageGameTest.REPAIR_KIT], sageGameTest.getUserPointsAccountAddress( sageGameTestPointCategories.lpCategory, ), sageGameTestPointCategories.lpCategory, sageGameTest.getPointsModifierAddress( sageGameTestPointCategories.lpCategory, ), initializeUserRedemption ? userRedemption : userRedemption.publicKey(), // userRedemption redemptionConfig.publicKey(), // redemptionConfig sageGameTest.pointsStoreProgram, // pointsStoreProgram { pointsProgramPermissionsKeyIndex: sageGameTest.profiles.player1.index, sagePermissionsKeyIndex: sageGameTest.profiles.player1.index, amount: new BN(newToolkitAmount), resourceIndex: 0, epochIndex: 0, resourceType: UpkeepResourceType.Toolkit, }, ), ], true, ); }; const { lastUpdate, starbaseTime, balance } = await getData(); // reserve is full expect(balance.eq(toolkitReserve)).toBe(true); expect(starbaseTime.gt(lastUpdate)).toBe(true); // await getData(); let depositAmount = 1; await depositToolkit(depositAmount); const { lastUpdate: lastUpdate1, starbaseTime: starbaseTime1, balance: balance1, } = await getData(); const amountConsumed = Math.floor( lastUpdate1.sub(lastUpdate).toNumber() * toolkitDepletionRate, ); const expectedBalance = balance .sub(new BN(amountConsumed)) .add(new BN(depositAmount)); expect(balance1.eq(expectedBalance)).toBe(true); const { starbaseTime: starbaseTime2 } = await getData(); // time is stopped expect(starbaseTime2.eq(starbaseTime1)).toBe(true); depositAmount = 1000; await depositToolkit(depositAmount); const { lastUpdate: lastUpdate3, starbaseTime: starbaseTime3 } = await getData(); expect(starbaseTime3.eq(starbaseTime2)).toBe(true); await new Promise((r) => setTimeout(r, 1000)); const { lastUpdate: lastUpdate4, starbaseTime: starbaseTime4 } = await getData(); expect(lastUpdate4.eq(lastUpdate3)).toBe(true); expect(starbaseTime4.gte(starbaseTime3)).toBe(true); }); });