Skip to content
This repository was archived by the owner on Feb 7, 2025. It is now read-only.

[SVR-296] [던전] 던전 점령 플레이어의 주기적 보상 제공 기능 #164

Merged
merged 3 commits into from
Apr 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,11 @@ public void Execute_Success_AlreadyUsedKey()
IAccountStateDelta beforeState = new DummyState();
var state = new RootState(
new InventoryState(),
new UserDungeonState(dungeonHistories, ImmutableList<DungeonConquestHistoryState>.Empty)
new UserDungeonState(
dungeonHistories,
ImmutableList<DungeonConquestHistoryState>.Empty,
ImmutableList<DungeonConquestPeriodicRewardHistoryState>.Empty
)
);
beforeState = beforeState.SetState(SignerAddress(), state.Serialize());

Expand Down Expand Up @@ -123,7 +127,11 @@ public void Execute_Fail_NotHaveRequiredDungeonKey()
IAccountStateDelta beforeState = new DummyState();
var state = new RootState(
new InventoryState(),
new UserDungeonState(dungeonHistories, ImmutableList<DungeonConquestHistoryState>.Empty)
new UserDungeonState(
dungeonHistories,
ImmutableList<DungeonConquestHistoryState>.Empty,
ImmutableList<DungeonConquestPeriodicRewardHistoryState>.Empty
)
);
beforeState = beforeState.SetState(SignerAddress(), state.Serialize());

Expand Down
Original file line number Diff line number Diff line change
@@ -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<int>.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<string, Address> { [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<int>.Empty)),
ImmutableList.Create(new DungeonConquestHistoryState(1, 2, 1)),
ImmutableList<DungeonConquestPeriodicRewardHistoryState>.Empty
)
);
beforeState = beforeState.SetState(SignerAddress(), state.Serialize());

PeriodicDungeonRewardAction action = new PeriodicDungeonRewardAction(TestDungeonId);

Assert.Throws<PermissionDeniedException>(
() =>
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<int>.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<string, Address> { [TestDungeonId.ToString()] = SignerAddress() }
);

beforeState = beforeState
.SetState(SignerAddress(), state.Serialize())
.SetState(Addresses.DungeonDataAddress, globalDungeonState.Serialize());

PeriodicDungeonRewardAction action = new PeriodicDungeonRewardAction(TestDungeonId);

Assert.Throws<ResourceIsNotReadyException>(
() =>
action.Execute(
new DummyActionContext
{
PreviousStates = beforeState,
Signer = SignerAddress(),
Random = random,
Rehearsal = false,
BlockIndex = currentBlock,
}
)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Savor22b.Action.Exceptions;

[Serializable]
public class AlreadyRequestedException : ActionException
{
public AlreadyRequestedException(string message, int? errorCode = null)
: base(message, "AlreadyRequested", errorCode) { }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Savor22b.Action.Exceptions;

[Serializable]
public class ResourceIsNotReadyException : ActionException
{
public ResourceIsNotReadyException(string message, int? errorCode = null)
: base(message, "ResourceIsNotReady", errorCode) { }
}
127 changes: 127 additions & 0 deletions backend/app/Savor22b/Action/PeriodicDungeonRewardAction.cs
Original file line number Diff line number Diff line change
@@ -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<string, IValue> PlainValueInternal =>
new Dictionary<string, IValue>()
{
[nameof(DungeonId)] = DungeonId.Serialize(),
}.ToImmutableDictionary();

protected override void LoadPlainValueInternal(IImmutableDictionary<string, IValue> plainValue)
{
DungeonId = plainValue[nameof(DungeonId)].ToInteger();
}

private List<SeedState> GenerateSeedStates(IRandom random, ImmutableList<int> seedIds)
{
List<SeedState> seedStates = new List<SeedState>();

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());
}
}
10 changes: 10 additions & 0 deletions backend/app/Savor22b/Action/Util/Validation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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}");
}
}
}
Loading
Loading