From f835adb9a58a1ccc132810eae189df31fd779dec Mon Sep 17 00:00:00 2001 From: upa-r-upa Date: Mon, 15 Apr 2024 20:35:51 +0900 Subject: [PATCH 1/2] feawt: Add periodic dungeon conquest rewards action --- .../Exceptions/AlreadyRequestedException.cs | 8 ++ .../Exceptions/ResourceIsNotReadyException.cs | 8 ++ .../Action/PeriodicDungeonRewardAction.cs | 127 ++++++++++++++++++ .../app/Savor22b/Action/Util/Validation.cs | 10 ++ .../Query/PeriodicDungeonRewardActionQuery.cs | 54 ++++++++ .../app/Savor22b/GraphTypes/Query/Query.cs | 1 + ...ngeonConquestPeriodicRewardHistoryState.cs | 47 +++++++ .../app/Savor22b/States/GlobalDungeonState.cs | 6 + backend/app/Savor22b/States/InventoryState.cs | 11 ++ .../app/Savor22b/States/UserDungeonState.cs | 107 ++++++++++++++- 10 files changed, 376 insertions(+), 3 deletions(-) create mode 100644 backend/app/Savor22b/Action/Exceptions/AlreadyRequestedException.cs create mode 100644 backend/app/Savor22b/Action/Exceptions/ResourceIsNotReadyException.cs create mode 100644 backend/app/Savor22b/Action/PeriodicDungeonRewardAction.cs create mode 100644 backend/app/Savor22b/GraphTypes/Query/PeriodicDungeonRewardActionQuery.cs create mode 100644 backend/app/Savor22b/States/DungeonConquestPeriodicRewardHistoryState.cs diff --git a/backend/app/Savor22b/Action/Exceptions/AlreadyRequestedException.cs b/backend/app/Savor22b/Action/Exceptions/AlreadyRequestedException.cs new file mode 100644 index 00000000..fffa78f9 --- /dev/null +++ b/backend/app/Savor22b/Action/Exceptions/AlreadyRequestedException.cs @@ -0,0 +1,8 @@ +namespace Savor22b.Action.Exceptions; + +[Serializable] +public class AlreadyRequestedException : ActionException +{ + public AlreadyRequestedException(string message, int? errorCode = null) + : base(message, "AlreadyRequested", errorCode) { } +} diff --git a/backend/app/Savor22b/Action/Exceptions/ResourceIsNotReadyException.cs b/backend/app/Savor22b/Action/Exceptions/ResourceIsNotReadyException.cs new file mode 100644 index 00000000..7e295f93 --- /dev/null +++ b/backend/app/Savor22b/Action/Exceptions/ResourceIsNotReadyException.cs @@ -0,0 +1,8 @@ +namespace Savor22b.Action.Exceptions; + +[Serializable] +public class ResourceIsNotReadyException : ActionException +{ + public ResourceIsNotReadyException(string message, int? errorCode = null) + : base(message, "ResourceIsNotReady", errorCode) { } +} diff --git a/backend/app/Savor22b/Action/PeriodicDungeonRewardAction.cs b/backend/app/Savor22b/Action/PeriodicDungeonRewardAction.cs new file mode 100644 index 00000000..2ac9d351 --- /dev/null +++ b/backend/app/Savor22b/Action/PeriodicDungeonRewardAction.cs @@ -0,0 +1,127 @@ +namespace Savor22b.Action; + +using System; +using System.Collections.Immutable; +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Headless.Extensions; +using Libplanet.State; +using Savor22b.Action.Util; +using Savor22b.Action.Exceptions; +using Savor22b.States; +using Savor22b.Model; +using Libplanet.Blockchain; +using Savor22b.Constants; + +[ActionType(nameof(PeriodicDungeonRewardAction))] +public class PeriodicDungeonRewardAction : SVRAction +{ + public PeriodicDungeonRewardAction() { } + + public PeriodicDungeonRewardAction(int dungeonId) + { + DungeonId = dungeonId; + } + + public int DungeonId { get; private set; } + + protected override IImmutableDictionary PlainValueInternal => + new Dictionary() + { + [nameof(DungeonId)] = DungeonId.Serialize(), + }.ToImmutableDictionary(); + + protected override void LoadPlainValueInternal(IImmutableDictionary plainValue) + { + DungeonId = plainValue[nameof(DungeonId)].ToInteger(); + } + + private List GenerateSeedStates(IRandom random, ImmutableList seedIds) + { + List seedStates = new List(); + + foreach (int seedId in seedIds) + { + seedStates.Add(new SeedState(random.GenerateRandomGuid(), seedId)); + } + + return seedStates; + } + + public override IAccountStateDelta Execute(IActionContext ctx) + { + if (ctx.Rehearsal) + { + return ctx.PreviousStates; + } + + Validation.EnsureDungeonExist(DungeonId); + + IAccountStateDelta states = ctx.PreviousStates; + + GlobalDungeonState globalDungeonState = states.GetState(Addresses.DungeonDataAddress) + is Dictionary encoded + ? new GlobalDungeonState(encoded) + : new GlobalDungeonState(); + RootState rootState = states.GetState(ctx.Signer) is Dictionary rootStateEncoded + ? new RootState(rootStateEncoded) + : new RootState(); + UserDungeonState userDungeonState = rootState.UserDungeonState; + InventoryState inventoryState = rootState.InventoryState; + + if (!globalDungeonState.IsDungeonConquestAddress(DungeonId, ctx.Signer)) + { + throw new PermissionDeniedException( + $"The dungeon {DungeonId} has not been conquered by the signer." + ); + } + + DungeonConquestHistoryState? history = userDungeonState.CurrentConquestDungeonHistory( + DungeonId + ); + + if (history is null) + { + throw new PermissionDeniedException( + $"The dungeon {DungeonId} has not been conquered by the signer." + ); + } + + int dungeonPeriodicRewardCount = userDungeonState.PresentDungeonPeriodicRewardCount( + DungeonId, + history.BlockIndex, + ctx.BlockIndex + ); + + if (dungeonPeriodicRewardCount <= 0) + { + throw new ResourceIsNotReadyException( + $"You need to wait blocks to get the periodic reward." + ); + } + + Dungeon dungeon = CsvDataHelper.GetDungeonById(DungeonId)!; + + for (int i = 0; i < dungeonPeriodicRewardCount; i++) + { + DungeonConquestPeriodicRewardHistoryState periodicRewardHistory = + new DungeonConquestPeriodicRewardHistoryState( + ctx.BlockIndex, + DungeonId, + dungeon.RewardSeedIdList + ); + + userDungeonState = userDungeonState.AddDungeonPeriodicRewardHistory( + periodicRewardHistory + ); + inventoryState = inventoryState.AddSeeds( + GenerateSeedStates(ctx.Random, dungeon.RewardSeedIdList) + ); + } + + rootState.SetUserDungeonState(userDungeonState); + rootState.SetInventoryState(inventoryState); + + return states.SetState(ctx.Signer, rootState.Serialize()); + } +} diff --git a/backend/app/Savor22b/Action/Util/Validation.cs b/backend/app/Savor22b/Action/Util/Validation.cs index e32ce25f..528e73d9 100644 --- a/backend/app/Savor22b/Action/Util/Validation.cs +++ b/backend/app/Savor22b/Action/Util/Validation.cs @@ -36,4 +36,14 @@ public static Village GetVillage(int villageID) return village; } + + public static void EnsureDungeonExist(int dungeonId) + { + Dungeon? dungeon = CsvDataHelper.GetDungeonById(dungeonId); + + if (dungeon == null) + { + throw new InvalidDungeonException($"Invalid dungeon ID: {dungeonId}"); + } + } } diff --git a/backend/app/Savor22b/GraphTypes/Query/PeriodicDungeonRewardActionQuery.cs b/backend/app/Savor22b/GraphTypes/Query/PeriodicDungeonRewardActionQuery.cs new file mode 100644 index 00000000..a93b4b47 --- /dev/null +++ b/backend/app/Savor22b/GraphTypes/Query/PeriodicDungeonRewardActionQuery.cs @@ -0,0 +1,54 @@ +namespace Savor22b.GraphTypes.Query; + +using GraphQL; +using GraphQL.Resolvers; +using GraphQL.Types; +using Libplanet.Blockchain; +using Libplanet.Crypto; +using Libplanet.Net; +using Savor22b.Action; + +public class PeriodicDungeonRewardActionQuery : FieldType +{ + public PeriodicDungeonRewardActionQuery(BlockChain blockChain, Swarm swarm) + : base() + { + Name = "createAction_PeriodicDungeonRewardAction"; + Type = typeof(NonNullGraphType); + Description = "점령한 던전에 대한 주기적인 보상을 받습니다."; + Arguments = new QueryArguments( + new QueryArgument> + { + Name = "publicKey", + Description = "대상 유저의 40-hex 형태의 address 입니다.", + }, + new QueryArgument> + { + Name = "dungeonId", + Description = "보상을 받는 대상 던전의 ID 입니다.", + } + ); + Resolver = new FuncFieldResolver(context => + { + try + { + var publicKey = new PublicKey( + Libplanet.ByteUtil.ParseHex(context.GetArgument("publicKey")) + ); + + var action = new PeriodicDungeonRewardAction(context.GetArgument("dungeonId")); + + return new GetUnsignedTransactionHex( + action, + publicKey, + blockChain, + swarm + ).UnsignedTransactionHex; + } + catch (Exception e) + { + throw new ExecutionError(e.Message); + } + }); + } +} diff --git a/backend/app/Savor22b/GraphTypes/Query/Query.cs b/backend/app/Savor22b/GraphTypes/Query/Query.cs index 1303b9f0..b815daf1 100644 --- a/backend/app/Savor22b/GraphTypes/Query/Query.cs +++ b/backend/app/Savor22b/GraphTypes/Query/Query.cs @@ -725,6 +725,7 @@ swarm is null AddField(new ShowMeTheMoney(blockChain, swarm)); AddField(new ConquestDungeonActionQuery(blockChain, swarm)); AddField(new RemoveInstalledKitchenEquipmentActionQuery(blockChain, swarm)); + AddField(new PeriodicDungeonRewardActionQuery(blockChain, swarm)); } private List combineRecipeData() diff --git a/backend/app/Savor22b/States/DungeonConquestPeriodicRewardHistoryState.cs b/backend/app/Savor22b/States/DungeonConquestPeriodicRewardHistoryState.cs new file mode 100644 index 00000000..eb444e2e --- /dev/null +++ b/backend/app/Savor22b/States/DungeonConquestPeriodicRewardHistoryState.cs @@ -0,0 +1,47 @@ +namespace Savor22b.States; + +using System.Collections.Immutable; +using Bencodex.Types; +using Libplanet.Headless.Extensions; + +public class DungeonConquestPeriodicRewardHistoryState : State +{ + public DungeonConquestPeriodicRewardHistoryState( + long blockIndex, + int dungeonId, + ImmutableList rewardSeedIdList + ) + { + BlockIndex = blockIndex; + DungeonId = dungeonId; + RewardSeedIdList = rewardSeedIdList; + } + + public DungeonConquestPeriodicRewardHistoryState(Dictionary encoded) + { + BlockIndex = encoded[nameof(BlockIndex)].ToLong(); + DungeonId = encoded[nameof(DungeonId)].ToInteger(); + RewardSeedIdList = ((List)encoded[nameof(RewardSeedIdList)]) + .Select(e => e.ToInteger()) + .ToImmutableList(); + } + + public long BlockIndex { get; private set; } + public int DungeonId { get; private set; } + public ImmutableList RewardSeedIdList { get; private set; } + + public IValue Serialize() + { + var pairs = new[] + { + new KeyValuePair((Text)nameof(BlockIndex), BlockIndex.Serialize()), + new KeyValuePair((Text)nameof(DungeonId), DungeonId.Serialize()), + new KeyValuePair( + (Text)nameof(RewardSeedIdList), + new List(RewardSeedIdList.Select(e => e.Serialize())) + ), + }; + + return new Dictionary(pairs); + } +} diff --git a/backend/app/Savor22b/States/GlobalDungeonState.cs b/backend/app/Savor22b/States/GlobalDungeonState.cs index 10f45584..39ad9fd7 100644 --- a/backend/app/Savor22b/States/GlobalDungeonState.cs +++ b/backend/app/Savor22b/States/GlobalDungeonState.cs @@ -63,4 +63,10 @@ public GlobalDungeonState SetDungeonConquestAddress(int dungeonId, Address addre DungeonStatus[dungeonId.ToString()] = address; return new GlobalDungeonState(DungeonStatus); } + + public bool IsDungeonConquestAddress(int dungeonId, Address address) + { + return DungeonStatus.TryGetValue(dungeonId.ToString(), out Address conquestAddress) + && conquestAddress == address; + } } diff --git a/backend/app/Savor22b/States/InventoryState.cs b/backend/app/Savor22b/States/InventoryState.cs index eb45a9f4..00bff1bb 100644 --- a/backend/app/Savor22b/States/InventoryState.cs +++ b/backend/app/Savor22b/States/InventoryState.cs @@ -134,6 +134,17 @@ public InventoryState AddSeed(SeedState seedState) ); } + public InventoryState AddSeeds(IEnumerable seedStates) + { + var seedStateList = SeedStateList.AddRange(seedStates); + return new InventoryState( + seedStateList, + RefrigeratorStateList, + KitchenEquipmentStateList, + ItemStateList + ); + } + public InventoryState AddRefrigeratorItem(RefrigeratorState item) { var refrigeratorStateList = RefrigeratorStateList.Add(item); diff --git a/backend/app/Savor22b/States/UserDungeonState.cs b/backend/app/Savor22b/States/UserDungeonState.cs index 855f7df0..af5f9672 100644 --- a/backend/app/Savor22b/States/UserDungeonState.cs +++ b/backend/app/Savor22b/States/UserDungeonState.cs @@ -12,22 +12,33 @@ public class UserDungeonState : State public static readonly int DungeonConquestKeyChargeIntervalBlock = 100; + public static readonly int DungeonConquestPeriodicRewardIntervalBlock = 100; + public ImmutableList DungeonHistories { get; private set; } public ImmutableList DungeonConquestHistories { get; private set; } + public ImmutableList DungeonConquestPeriodicRewardHistories + { + get; + private set; + } public UserDungeonState() { DungeonHistories = ImmutableList.Empty; DungeonConquestHistories = ImmutableList.Empty; + DungeonConquestPeriodicRewardHistories = + ImmutableList.Empty; } public UserDungeonState( ImmutableList dungeonKeyHistories, - ImmutableList dungeonConquestHistories + ImmutableList dungeonConquestHistories, + ImmutableList dungeonConquestPeriodicRewardHistories ) { DungeonHistories = dungeonKeyHistories; DungeonConquestHistories = dungeonConquestHistories; + DungeonConquestPeriodicRewardHistories = dungeonConquestPeriodicRewardHistories; } public UserDungeonState(Dictionary encoded) @@ -46,6 +57,12 @@ public UserDungeonState(Dictionary encoded) DungeonConquestHistories = ((List)encoded[nameof(DungeonConquestHistories)]) .Select(element => new DungeonConquestHistoryState((Dictionary)element)) .ToImmutableList(); + + DungeonConquestPeriodicRewardHistories = ( + (List)encoded[nameof(DungeonConquestPeriodicRewardHistories)] + ) + .Select(element => new DungeonConquestPeriodicRewardHistoryState((Dictionary)element)) + .ToImmutableList(); } public IValue Serialize() @@ -61,6 +78,14 @@ public IValue Serialize() (Text)nameof(DungeonConquestHistories), new List(DungeonConquestHistories.Select(element => element.Serialize())) ), + new KeyValuePair( + (Text)nameof(DungeonConquestPeriodicRewardHistories), + new List( + DungeonConquestPeriodicRewardHistories.Select( + element => element.Serialize() + ) + ) + ), } ); } @@ -118,6 +143,21 @@ public ImmutableList GetCurrentDungeonHistories(long blockI return result.ToImmutableList(); } + public DungeonConquestHistoryState? CurrentConquestDungeonHistory(int dungeonId) + { + for (int i = DungeonConquestHistories.Count - 1; i >= 0; i--) + { + var history = DungeonConquestHistories[i]; + + if (history.DungeonId == dungeonId && history.DungeonConquestStatus == 1) + { + return history; + } + } + + return null; + } + public ImmutableList GetCurrentDungeonConquestHistories( int dungeonID, long blockIndex @@ -165,7 +205,11 @@ public bool IsDungeonCleared(int dungeonID, long blockIndex) public UserDungeonState AddDungeonHistory(DungeonHistoryState dungeonHistory) { - return new UserDungeonState(DungeonHistories.Add(dungeonHistory), DungeonConquestHistories); + return new UserDungeonState( + DungeonHistories.Add(dungeonHistory), + DungeonConquestHistories, + DungeonConquestPeriodicRewardHistories + ); } public UserDungeonState AddDungeonConquestHistory( @@ -174,7 +218,64 @@ DungeonConquestHistoryState dungeonConquestHistory { return new UserDungeonState( DungeonHistories, - DungeonConquestHistories.Add(dungeonConquestHistory) + DungeonConquestHistories.Add(dungeonConquestHistory), + DungeonConquestPeriodicRewardHistories + ); + } + + public UserDungeonState AddDungeonPeriodicRewardHistory( + DungeonConquestPeriodicRewardHistoryState dungeonPeriodicRewardHistory + ) + { + return new UserDungeonState( + DungeonHistories, + DungeonConquestHistories, + DungeonConquestPeriodicRewardHistories.Add(dungeonPeriodicRewardHistory) + ); + } + + public int PresentDungeonPeriodicRewardCount( + int dungeonId, + long startedBlockIndex, + long currentBlockIndex + ) + { + int maxCount = (int)( + (currentBlockIndex - startedBlockIndex) / DungeonConquestPeriodicRewardIntervalBlock ); + int remainCount = + maxCount + - TargetConquestDungeonPeriodicRewardHistories( + dungeonId, + startedBlockIndex, + currentBlockIndex + ).Length; + + return remainCount; + } + + public DungeonConquestPeriodicRewardHistoryState[] TargetConquestDungeonPeriodicRewardHistories( + int dungeonId, + long startedBlockIndex, + long currentBlockIndex + ) + { + var result = new List(); + + for (int i = DungeonConquestPeriodicRewardHistories.Count - 1; i >= 0; i--) + { + var history = DungeonConquestPeriodicRewardHistories[i]; + + if (history.DungeonId == dungeonId && history.BlockIndex > startedBlockIndex) + { + result.Add(history); + } + else if (history.BlockIndex <= startedBlockIndex) + { + break; + } + } + + return result.ToArray(); } } From 93992c17b0989b4a7003bca218b6c1fd3806f541 Mon Sep 17 00:00:00 2001 From: upa-r-upa Date: Mon, 15 Apr 2024 20:35:57 +0900 Subject: [PATCH 2/2] test: Add periodic dungeon conquest rewards tests --- .../Action/ExplorationDungeonActionTests.cs | 12 +- .../PeriodicDungeonRewardActionTests.cs | 165 ++++++++++++++++++ 2 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 backend/app/Savor22b.Tests/Action/PeriodicDungeonRewardActionTests.cs diff --git a/backend/app/Savor22b.Tests/Action/ExplorationDungeonActionTests.cs b/backend/app/Savor22b.Tests/Action/ExplorationDungeonActionTests.cs index ea87959e..cbf22d5a 100644 --- a/backend/app/Savor22b.Tests/Action/ExplorationDungeonActionTests.cs +++ b/backend/app/Savor22b.Tests/Action/ExplorationDungeonActionTests.cs @@ -68,7 +68,11 @@ public void Execute_Success_AlreadyUsedKey() IAccountStateDelta beforeState = new DummyState(); var state = new RootState( new InventoryState(), - new UserDungeonState(dungeonHistories, ImmutableList.Empty) + new UserDungeonState( + dungeonHistories, + ImmutableList.Empty, + ImmutableList.Empty + ) ); beforeState = beforeState.SetState(SignerAddress(), state.Serialize()); @@ -123,7 +127,11 @@ public void Execute_Fail_NotHaveRequiredDungeonKey() IAccountStateDelta beforeState = new DummyState(); var state = new RootState( new InventoryState(), - new UserDungeonState(dungeonHistories, ImmutableList.Empty) + new UserDungeonState( + dungeonHistories, + ImmutableList.Empty, + ImmutableList.Empty + ) ); beforeState = beforeState.SetState(SignerAddress(), state.Serialize()); diff --git a/backend/app/Savor22b.Tests/Action/PeriodicDungeonRewardActionTests.cs b/backend/app/Savor22b.Tests/Action/PeriodicDungeonRewardActionTests.cs new file mode 100644 index 00000000..d5ff0f77 --- /dev/null +++ b/backend/app/Savor22b.Tests/Action/PeriodicDungeonRewardActionTests.cs @@ -0,0 +1,165 @@ +namespace Savor22b.Tests.Action; + +using System; +using System.Collections.Immutable; +using Libplanet; +using Libplanet.State; +using Savor22b.Action; +using Savor22b.Action.Exceptions; +using Savor22b.Constants; +using Savor22b.States; +using Xunit; + +public class PeriodicDungeonRewardActionTests : ActionTests +{ + public static int TestDungeonId = 0; + + public PeriodicDungeonRewardActionTests() { } + + [Fact] + public void Execute_Success_Normal() + { + long currentBlock = 402; + + IAccountStateDelta beforeState = new DummyState(); + var state = new RootState( + new InventoryState(), + new UserDungeonState( + ImmutableList.Create( + new DungeonHistoryState(1, TestDungeonId, 1, ImmutableList.Empty) + ), + ImmutableList.Create(new DungeonConquestHistoryState(1, TestDungeonId, 1)), + ImmutableList.Create( + new DungeonConquestPeriodicRewardHistoryState( + 105, + TestDungeonId, + ImmutableList.Create(1, 2, 3) + ), + new DungeonConquestPeriodicRewardHistoryState( + 205, + TestDungeonId, + ImmutableList.Create(1, 2, 3) + ) + ) + ) + ); + + GlobalDungeonState globalDungeonState = new GlobalDungeonState( + new Dictionary { [TestDungeonId.ToString()] = SignerAddress() } + ); + + beforeState = beforeState + .SetState(SignerAddress(), state.Serialize()) + .SetState(Addresses.DungeonDataAddress, globalDungeonState.Serialize()); + + PeriodicDungeonRewardAction action = new PeriodicDungeonRewardAction(TestDungeonId); + + IAccountStateDelta afterState = action.Execute( + new DummyActionContext + { + PreviousStates = beforeState, + Signer = SignerAddress(), + Random = random, + Rehearsal = false, + BlockIndex = currentBlock, + } + ); + + Bencodex.Types.IValue? afterRootStateEncoded = afterState.GetState(SignerAddress()); + RootState afterRootState = afterRootStateEncoded is Bencodex.Types.Dictionary bdict + ? new RootState(bdict) + : throw new Exception(); + UserDungeonState userDungeonState = afterRootState.UserDungeonState; + + Assert.Equal( + 0, + userDungeonState.PresentDungeonPeriodicRewardCount(TestDungeonId, 1, currentBlock) + ); + Assert.Equal(4, userDungeonState.DungeonConquestPeriodicRewardHistories.Count); + Assert.Equal(5 * 2, afterRootState.InventoryState.SeedStateList.Count); + } + + [Fact] + public void Execute_Fail_NotConquest() + { + IAccountStateDelta beforeState = new DummyState(); + var state = new RootState( + new InventoryState(), + new UserDungeonState( + ImmutableList.Create(new DungeonHistoryState(1, 2, 1, ImmutableList.Empty)), + ImmutableList.Create(new DungeonConquestHistoryState(1, 2, 1)), + ImmutableList.Empty + ) + ); + beforeState = beforeState.SetState(SignerAddress(), state.Serialize()); + + PeriodicDungeonRewardAction action = new PeriodicDungeonRewardAction(TestDungeonId); + + Assert.Throws( + () => + action.Execute( + new DummyActionContext + { + PreviousStates = beforeState, + Signer = SignerAddress(), + Random = random, + Rehearsal = false, + BlockIndex = 200, + } + ) + ); + } + + [Fact] + public void Execute_Fail_AlreadyReceive() + { + long currentBlock = 299; + + IAccountStateDelta beforeState = new DummyState(); + var state = new RootState( + new InventoryState(), + new UserDungeonState( + ImmutableList.Create( + new DungeonHistoryState(1, TestDungeonId, 1, ImmutableList.Empty) + ), + ImmutableList.Create(new DungeonConquestHistoryState(1, TestDungeonId, 1)), + ImmutableList.Create( + new DungeonConquestPeriodicRewardHistoryState( + 105, + TestDungeonId, + ImmutableList.Create(1, 2, 3) + ), + new DungeonConquestPeriodicRewardHistoryState( + 205, + TestDungeonId, + ImmutableList.Create(1, 2, 3) + ) + ) + ) + ); + + GlobalDungeonState globalDungeonState = new GlobalDungeonState( + new Dictionary { [TestDungeonId.ToString()] = SignerAddress() } + ); + + beforeState = beforeState + .SetState(SignerAddress(), state.Serialize()) + .SetState(Addresses.DungeonDataAddress, globalDungeonState.Serialize()); + + PeriodicDungeonRewardAction action = new PeriodicDungeonRewardAction(TestDungeonId); + + Assert.Throws( + () => + action.Execute( + new DummyActionContext + { + PreviousStates = beforeState, + Signer = SignerAddress(), + Random = random, + Rehearsal = false, + BlockIndex = currentBlock, + } + ) + ); + } +}