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

[SVR-294] [던전] 던전 Mock 전투 기능 #149

Merged
merged 4 commits into from
Apr 1, 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
170 changes: 170 additions & 0 deletions backend/app/Savor22b.Tests/Action/ExplorationDungeonActionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
namespace Savor22b.Tests.Action;

using System;
using System.Collections.Immutable;
using Libplanet.State;
using Savor22b.Action;
using Savor22b.Action.Exceptions;
using Savor22b.States;
using Xunit;

public class ExplorationDungeonActionTests : ActionTests
{
public static int TestDungeonId = 0;

public ExplorationDungeonActionTests() { }

[Fact]
public void Execute_Success_Normal()
{
IAccountStateDelta beforeState = new DummyState();
var state = new RootState();
beforeState = beforeState.SetState(SignerAddress(), state.Serialize());

ExplorationDungeonAction action = new ExplorationDungeonAction(TestDungeonId);

IAccountStateDelta afterState = action.Execute(
new DummyActionContext
{
PreviousStates = beforeState,
Signer = SignerAddress(),
Random = random,
Rehearsal = false,
BlockIndex = 1,
}
);

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.True(userDungeonState.DungeonHistories.Count == 1);
Assert.Equal(TestDungeonId, userDungeonState.DungeonHistories[0].DungeonId);
Assert.Equal(UserDungeonState.MaxDungeonKey - 1, userDungeonState.GetDungeonKeyCount(2));
Assert.True(
CsvDataHelper
.GetDungeonById(TestDungeonId)!
.RewardSeedIdList.SequenceEqual(
userDungeonState.DungeonHistories[0].DungeonClearRewardSeedIdList
)
);
}

[Fact]
public void Execute_Success_AlreadyUsedKey()
{
var currentBlockIndex =
UserDungeonState.DungeonKeyChargeIntervalBlock * UserDungeonState.MaxDungeonKey + 4;
var dungeonHistories = ImmutableList.Create(
new DungeonHistoryState(1, TestDungeonId, 0, ImmutableList<int>.Empty),
new DungeonHistoryState(2, TestDungeonId, 0, ImmutableList<int>.Empty),
new DungeonHistoryState(3, TestDungeonId, 0, ImmutableList<int>.Empty),
new DungeonHistoryState(4, TestDungeonId, 0, ImmutableList<int>.Empty),
new DungeonHistoryState(5, TestDungeonId, 0, ImmutableList<int>.Empty)
);

IAccountStateDelta beforeState = new DummyState();
var state = new RootState(
new InventoryState(),
new UserDungeonState(dungeonHistories, ImmutableList<DungeonConquestHistoryState>.Empty)
);
beforeState = beforeState.SetState(SignerAddress(), state.Serialize());

ExplorationDungeonAction action = new ExplorationDungeonAction(TestDungeonId);

IAccountStateDelta afterState = action.Execute(
new DummyActionContext
{
PreviousStates = beforeState,
Signer = SignerAddress(),
Random = random,
Rehearsal = false,
BlockIndex = currentBlockIndex,
}
);

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.True(userDungeonState.DungeonHistories.Count == 6);
Assert.Equal(TestDungeonId, userDungeonState.DungeonHistories[5].DungeonId);
Assert.Equal(
UserDungeonState.MaxDungeonKey - 2,
userDungeonState.GetDungeonKeyCount(currentBlockIndex)
);
Assert.True(
CsvDataHelper
.GetDungeonById(TestDungeonId)!
.RewardSeedIdList.SequenceEqual(
userDungeonState.DungeonHistories[5].DungeonClearRewardSeedIdList
)
);
}

[Fact]
public void Execute_Fail_NotHaveRequiredDungeonKey()
{
var currentBlockIndex =
UserDungeonState.DungeonKeyChargeIntervalBlock * UserDungeonState.MaxDungeonKey + 3;
var dungeonHistories = ImmutableList.Create(
new DungeonHistoryState(1, TestDungeonId, 0, ImmutableList<int>.Empty),
new DungeonHistoryState(4, TestDungeonId, 0, ImmutableList<int>.Empty),
new DungeonHistoryState(5, TestDungeonId, 0, ImmutableList<int>.Empty),
new DungeonHistoryState(6, TestDungeonId, 0, ImmutableList<int>.Empty),
new DungeonHistoryState(7, TestDungeonId, 0, ImmutableList<int>.Empty),
new DungeonHistoryState(8, TestDungeonId, 0, ImmutableList<int>.Empty)
);

IAccountStateDelta beforeState = new DummyState();
var state = new RootState(
new InventoryState(),
new UserDungeonState(dungeonHistories, ImmutableList<DungeonConquestHistoryState>.Empty)
);
beforeState = beforeState.SetState(SignerAddress(), state.Serialize());

ExplorationDungeonAction action = new ExplorationDungeonAction(TestDungeonId);

Assert.Throws<NotHaveRequiredException>(
() =>
action.Execute(
new DummyActionContext
{
PreviousStates = beforeState,
Signer = SignerAddress(),
Random = random,
Rehearsal = false,
BlockIndex = currentBlockIndex,
}
)
);
}

[Fact]
public void Execute_Fail_InvalidDungeonId()
{
IAccountStateDelta beforeState = new DummyState();
var state = new RootState();
beforeState = beforeState.SetState(SignerAddress(), state.Serialize());

ExplorationDungeonAction action = new ExplorationDungeonAction(-1);

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

[Serializable]
public class InvalidDungeonException : ActionException
{
public InvalidDungeonException(string message, int? errorCode = null)
: base(message, "InvalidDungeon", errorCode) { }
}
89 changes: 89 additions & 0 deletions backend/app/Savor22b/Action/ExplorationDungeonAction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
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;

[ActionType(nameof(ExplorationDungeonAction))]
public class ExplorationDungeonAction : SVRAction
{
public int DungeonId { get; private set; }

public ExplorationDungeonAction() { }

public ExplorationDungeonAction(int dungeonId)
{
DungeonId = dungeonId;
}

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 Dungeon ValidateDungeon(int dungeonId)
{
Dungeon? dungeon = CsvDataHelper.GetDungeonById(dungeonId);

if (dungeon == null)
{
throw new InvalidDungeonException($"Invalid dungeon ID: {dungeonId}");
}

return dungeon;
}

private void ValidateUseDungeonKey(UserDungeonState userDungeonState, long blockIndex)
{
if (!userDungeonState.CanUseDungeonKey(blockIndex))
{
throw new NotHaveRequiredException("You don't have enough dungeon key");
}
}

public override IAccountStateDelta Execute(IActionContext ctx)
{
if (ctx.Rehearsal)
{
return ctx.PreviousStates;
}

Dungeon dungeon = ValidateDungeon(DungeonId);

IAccountStateDelta states = ctx.PreviousStates;

RootState rootState = states.GetState(ctx.Signer) is Dictionary rootStateEncoded
? new RootState(rootStateEncoded)
: new RootState();

UserDungeonState userDungeonState = rootState.UserDungeonState;

ValidateUseDungeonKey(userDungeonState, ctx.BlockIndex);

DungeonHistoryState dungeonHistory = new DungeonHistoryState(
ctx.BlockIndex,
DungeonId,
userDungeonState.CalculateDungeonClear(ctx.Random),
dungeon.RewardSeedIdList
);

userDungeonState = userDungeonState.AddDungeonHistory(dungeonHistory);
rootState.SetUserDungeonState(userDungeonState);

return states.SetState(ctx.Signer, rootState.Serialize());
}
}
Original file line number Diff line number Diff line change
@@ -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 DungeonExplorationQuery : FieldType
{
public DungeonExplorationQuery(BlockChain blockChain, Swarm swarm)
: base()
{
Name = "createAction_DungeonExploration";
Type = typeof(NonNullGraphType<StringGraphType>);
Description = "던전 키를 소모하여 특정 던전의 스테이지를 모두 돌고, 클리어를 시도합니다.";
Arguments = new QueryArguments(
new QueryArgument<NonNullGraphType<StringGraphType>>
{
Name = "publicKey",
Description = "대상 유저의 40-hex 형태의 address 입니다.",
},
new QueryArgument<NonNullGraphType<IntGraphType>>
{
Name = "dungeonId",
Description = "탐험할 던전의 ID 입니다.",
}
);
Resolver = new FuncFieldResolver<string>(context =>
{
try
{
var publicKey = new PublicKey(
Libplanet.ByteUtil.ParseHex(context.GetArgument<string>("publicKey"))
);

var action = new ExplorationDungeonAction(context.GetArgument<int>("dungeonId"));

return new GetUnsignedTransactionHex(
action,
publicKey,
blockChain,
swarm
).UnsignedTransactionHex;
}
catch (Exception e)
{
throw new ExecutionError(e.Message);
}
});
}
}
23 changes: 18 additions & 5 deletions backend/app/Savor22b/GraphTypes/Query/Query.cs
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,7 @@ swarm is null
}
);

AddField(new DungeonExplorationQuery(blockChain, swarm));
AddField(new CalculateRelocationCostQuery());
AddField(new ShopQuery());
}
Expand All @@ -536,7 +537,9 @@ private List<RecipeResponse> combineRecipeData()
{
var foodDict = CsvDataHelper.GetFoodCSVData().ToDictionary(x => x.ID);
var ingredientDict = CsvDataHelper.GetIngredientCSVData().ToDictionary(x => x.ID);
var kitchenEquipmentCategoryDict = CsvDataHelper.GetKitchenEquipmentCategoryCSVData().ToDictionary(x => x.ID);
var kitchenEquipmentCategoryDict = CsvDataHelper
.GetKitchenEquipmentCategoryCSVData()
.ToDictionary(x => x.ID);

var recipes = new List<RecipeResponse>();
foreach (var recipe in CsvDataHelper.GetRecipeCSVData())
Expand All @@ -550,10 +553,20 @@ private List<RecipeResponse> combineRecipeData()
var recipeFoodComponents = recipe.FoodIDList
.Select(foodID => new RecipeComponent(foodID, foodDict[foodID].Name))
.ToList();
var requiredKitchenEquipmentCategoryComponents = recipe.RequiredKitchenEquipmentCategoryList
.Select(equipment => new RecipeComponent(equipment, kitchenEquipmentCategoryDict[equipment].Name))
.ToList();
var resultFoodComponent = new RecipeComponent(recipe.ResultFoodID, foodDict[recipe.ResultFoodID].Name);
var requiredKitchenEquipmentCategoryComponents =
recipe.RequiredKitchenEquipmentCategoryList
.Select(
equipment =>
new RecipeComponent(
equipment,
kitchenEquipmentCategoryDict[equipment].Name
)
)
.ToList();
var resultFoodComponent = new RecipeComponent(
recipe.ResultFoodID,
foodDict[recipe.ResultFoodID].Name
);

recipes.Add(
new RecipeResponse(
Expand Down
2 changes: 2 additions & 0 deletions backend/app/Savor22b/States/DungeonHistoryState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ public class DungeonHistoryState : State
public long BlockIndex { get; private set; }
public int DungeonId { get; private set; }
public int DungeonClearStatus { get; private set; }

// 0: Not Cleared, 1: Cleared
public ImmutableList<int> DungeonClearRewardSeedIdList { get; private set; }

public DungeonHistoryState(
Expand Down
5 changes: 5 additions & 0 deletions backend/app/Savor22b/States/RootState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ public void SetRelocationState(RelocationState relocationState)
RelocationState = relocationState;
}

public void SetUserDungeonState(UserDungeonState userDungeonState)
{
UserDungeonState = userDungeonState;
}

public IValue Serialize()
{
var pairs = new[]
Expand Down
Loading
Loading