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

[SVR-295] [던전] 던전 점령 기능 #162

Merged
merged 4 commits into from
Apr 14, 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
173 changes: 173 additions & 0 deletions backend/app/Savor22b.Tests/Action/ConquestDungeonActionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
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 ConquestDungeonActionTests : ActionTests
{
public static int TestDungeonId = 0;

public ConquestDungeonActionTests() { }

[Fact]
public void Execute_Success_InitialConquest()
{
IAccountStateDelta beforeState = new DummyState();
var state = new RootState();
state.SetUserDungeonState(
state.UserDungeonState.AddDungeonHistory(
new DungeonHistoryState(1, TestDungeonId, 1, ImmutableList<int>.Empty)
)
);
beforeState = beforeState.SetState(SignerAddress(), state.Serialize());

ConquestDungeonAction action = new ConquestDungeonAction(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;

GlobalDungeonState globalDungeonState = afterState.GetState(Addresses.DungeonDataAddress)
is Bencodex.Types.Dictionary bdict2
? new GlobalDungeonState(bdict2)
: throw new Exception();

Assert.True(userDungeonState.DungeonConquestHistories.Count == 1);
Assert.Equal(1, userDungeonState.DungeonHistories[0].DungeonClearStatus);
Assert.Equal(TestDungeonId, userDungeonState.DungeonConquestHistories[0].DungeonId);
Assert.Equal(1, userDungeonState.DungeonConquestHistories[0].DungeonConquestStatus);
Assert.Equal(globalDungeonState.DungeonConquestAddress(TestDungeonId), SignerAddress());
}

[Fact]
public void Execute_Success_Normal()
{
IAccountStateDelta beforeState = new DummyState();
var state = new RootState();
state.SetUserDungeonState(
state.UserDungeonState.AddDungeonHistory(
new DungeonHistoryState(1, TestDungeonId, 1, ImmutableList<int>.Empty)
)
);

var dungeonState = new GlobalDungeonState();
dungeonState = dungeonState.SetDungeonConquestAddress(TestDungeonId, new Address());

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

ConquestDungeonAction action = new ConquestDungeonAction(TestDungeonId);

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

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.DungeonConquestHistories.Count == 1);
Assert.Equal(1, userDungeonState.DungeonHistories[0].DungeonClearStatus);
Assert.Equal(TestDungeonId, userDungeonState.DungeonConquestHistories[0].DungeonId);
}

[Fact]
public void Execute_Fail_NotClear()
{
IAccountStateDelta beforeState = new DummyState();
var state = new RootState();
state.SetUserDungeonState(
state.UserDungeonState.AddDungeonHistory(
new DungeonHistoryState(1, TestDungeonId, 0, ImmutableList<int>.Empty)
)
);
beforeState = beforeState.SetState(SignerAddress(), state.Serialize());

ConquestDungeonAction action = new ConquestDungeonAction(TestDungeonId);

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

[Fact]
public void Execute_Fail_NotHaveRequiredDungeonKey()
{
IAccountStateDelta beforeState = new DummyState();
var state = new RootState();
var userDungeonState = state.UserDungeonState;

userDungeonState = userDungeonState.AddDungeonHistory(
new DungeonHistoryState(1, TestDungeonId, 1, ImmutableList<int>.Empty)
);
userDungeonState = userDungeonState.AddDungeonConquestHistory(
new DungeonConquestHistoryState(1, TestDungeonId, 0)
);
userDungeonState = userDungeonState.AddDungeonConquestHistory(
new DungeonConquestHistoryState(2, TestDungeonId, 0)
);
userDungeonState = userDungeonState.AddDungeonConquestHistory(
new DungeonConquestHistoryState(3, TestDungeonId, 0)
);

state.SetUserDungeonState(userDungeonState);

beforeState = beforeState.SetState(SignerAddress(), state.Serialize());

ConquestDungeonAction action = new ConquestDungeonAction(TestDungeonId);

Assert.Throws<NotHaveRequiredException>(
() =>
action.Execute(
new DummyActionContext
{
PreviousStates = beforeState,
Signer = SignerAddress(),
Random = random,
Rehearsal = false,
BlockIndex = 5,
}
)
);
}
}
115 changes: 115 additions & 0 deletions backend/app/Savor22b/Action/ConquestDungeonAction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
namespace Savor22b.Action;

using Libplanet.Action;
using Savor22b.States;
using Savor22b.Model;
using System.Collections.Immutable;
using Bencodex.Types;
using Libplanet.Headless.Extensions;
using Libplanet.State;
using Savor22b.Action.Exceptions;
using Savor22b.Constants;
using Libplanet;

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

public ConquestDungeonAction() { }

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

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

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

AssertDungeonExists(DungeonId);

IAccountStateDelta states = ctx.PreviousStates;

RootState rootState = states.GetState(ctx.Signer) is Dictionary rootStateEncoded
? new RootState(rootStateEncoded)
: new RootState();
UserDungeonState userDungeonState = rootState.UserDungeonState;
GlobalDungeonState globalDungeonState = states.GetState(Addresses.DungeonDataAddress)
is Dictionary encoded
? new GlobalDungeonState(encoded)
: new GlobalDungeonState();

if (!userDungeonState.IsDungeonCleared(DungeonId, ctx.BlockIndex))
{
throw new DungeonNotClearedException($"Dungeon {DungeonId} is not cleared yet.");
}

if (!userDungeonState.CanUseDungeonConquestKey(DungeonId, ctx.BlockIndex))
{
throw new NotHaveRequiredException("You don't have enough dungeon conquest key");
}

Address? presentDungeonOwner = globalDungeonState.DungeonConquestAddress(DungeonId);

if (presentDungeonOwner == null)
{
globalDungeonState = globalDungeonState.SetDungeonConquestAddress(
DungeonId,
ctx.Signer
);
userDungeonState = userDungeonState.AddDungeonConquestHistory(
new DungeonConquestHistoryState(ctx.BlockIndex, DungeonId, 1)
);
}
else if (presentDungeonOwner == ctx.Signer)
{
throw new AlreadyOwnedException("You already own this dungeon");
}
else
{
globalDungeonState = globalDungeonState.SetDungeonConquestAddress(
DungeonId,
ctx.Signer
);
userDungeonState = userDungeonState.AddDungeonConquestHistory(
new DungeonConquestHistoryState(
ctx.BlockIndex,
DungeonId,
userDungeonState.CalculateDungeonConquest(ctx.Random),
presentDungeonOwner
)
);
}

rootState.SetUserDungeonState(userDungeonState);

return states
.SetState(Addresses.DungeonDataAddress, globalDungeonState.Serialize())
.SetState(ctx.Signer, rootState.Serialize());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Savor22b.Action.Exceptions;

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

[Serializable]
public class DungeonNotClearedException : ActionException
{
public DungeonNotClearedException(string message, int? errorCode = null)
: base(message, "DungeonNotCleared", errorCode) { }
}
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 ConquestDungeonActionQuery : FieldType
{
public ConquestDungeonActionQuery(BlockChain blockChain, Swarm swarm)
: base()
{
Name = "createAction_ConquestDungeonActionQuery";
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 ConquestDungeonAction(context.GetArgument<int>("dungeonId"));

return new GetUnsignedTransactionHex(
action,
publicKey,
blockChain,
swarm
).UnsignedTransactionHex;
}
catch (Exception e)
{
throw new ExecutionError(e.Message);
}
});
}
}
1 change: 1 addition & 0 deletions backend/app/Savor22b/GraphTypes/Query/Query.cs
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,7 @@ swarm is null
AddField(new ShopQuery());
AddField(new VillageField(blockChain, subject));
AddField(new ShowMeTheMoney(blockChain, swarm));
AddField(new ConquestDungeonActionQuery(blockChain, swarm));
}

private List<RecipeResponse> combineRecipeData()
Expand Down
Loading
Loading