diff --git a/.Lib9c.Tests/Action/CombinationConsumableTest.cs b/.Lib9c.Tests/Action/CombinationConsumableTest.cs new file mode 100644 index 0000000000..44844b56f3 --- /dev/null +++ b/.Lib9c.Tests/Action/CombinationConsumableTest.cs @@ -0,0 +1,133 @@ +namespace Lib9c.Tests.Action +{ + using System.Globalization; + using System.Linq; + using Libplanet.Action; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume; + using Nekoyume.Action; + using Nekoyume.Model; + using Nekoyume.Model.Item; + using Nekoyume.Model.Mail; + using Nekoyume.Model.State; + using Nekoyume.Module; + using Xunit; + + public class CombinationConsumableTest + { + private readonly Address _agentAddress; + private readonly Address _avatarAddress; + private readonly IRandom _random; + private readonly TableSheets _tableSheets; + private IWorld _initialState; + + public CombinationConsumableTest() + { + _agentAddress = new PrivateKey().Address; + _avatarAddress = _agentAddress.Derive("avatar"); + var slotAddress = _avatarAddress.Derive( + string.Format( + CultureInfo.InvariantCulture, + CombinationSlotState.DeriveFormat, + 0 + ) + ); + var sheets = TableSheetsImporter.ImportSheets(); + _random = new TestRandom(); + _tableSheets = new TableSheets(sheets); + + var agentState = new AgentState(_agentAddress); + agentState.avatarAddresses[0] = _avatarAddress; + + var gameConfigState = new GameConfigState(); + + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 1, + _tableSheets.GetAvatarSheets(), + gameConfigState, + default + ); + +#pragma warning disable CS0618 + // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 + var gold = new GoldCurrencyState(Currency.Legacy("NCG", 2, null)); +#pragma warning restore CS0618 + + _initialState = new World(new MockWorldState()) + .SetAgentState(_agentAddress, agentState) + .SetAvatarState(_avatarAddress, avatarState) + .SetLegacyState( + slotAddress, + new CombinationSlotState( + slotAddress, + GameConfig.RequireClearedStageLevel.CombinationConsumableAction).Serialize()) + .SetLegacyState(GameConfigState.Address, gold.Serialize()); + + foreach (var (key, value) in sheets) + { + _initialState = + _initialState.SetLegacyState(Addresses.TableSheet.Derive(key), value.Serialize()); + } + } + + [Fact] + public void Execute() + { + var avatarState = _initialState.GetAvatarState(_avatarAddress); + var row = _tableSheets.ConsumableItemRecipeSheet.Values.First(); + var costActionPoint = row.RequiredActionPoint; + foreach (var materialInfo in row.Materials) + { + var materialRow = _tableSheets.MaterialItemSheet[materialInfo.Id]; + var material = ItemFactory.CreateItem(materialRow, _random); + avatarState.inventory.AddItem(material, materialInfo.Count); + } + + var previousActionPoint = avatarState.actionPoint; + var previousResultConsumableCount = + avatarState.inventory.Equipments.Count(e => e.Id == row.ResultConsumableItemId); + var previousMailCount = avatarState.mailBox.Count; + + avatarState.worldInformation = new WorldInformation( + 0, + _tableSheets.WorldSheet, + GameConfig.RequireClearedStageLevel.CombinationConsumableAction); + + IWorld previousState = _initialState.SetAvatarState(_avatarAddress, avatarState); + + var action = new CombinationConsumable + { + avatarAddress = _avatarAddress, + recipeId = row.Id, + slotIndex = 0, + }; + + var nextState = action.Execute(new ActionContext + { + PreviousState = previousState, + Signer = _agentAddress, + BlockIndex = 1, + RandomSeed = _random.Seed, + }); + + var slotState = nextState.GetCombinationSlotState(_avatarAddress, 0); + Assert.NotNull(slotState.Result); + Assert.NotNull(slotState.Result.itemUsable); + + var consumable = (Consumable)slotState.Result.itemUsable; + Assert.NotNull(consumable); + + var nextAvatarState = nextState.GetAvatarState(_avatarAddress); + Assert.Equal(previousActionPoint - costActionPoint, nextAvatarState.actionPoint); + Assert.Equal(previousMailCount + 1, nextAvatarState.mailBox.Count); + Assert.IsType(nextAvatarState.mailBox.First()); + Assert.Equal( + previousResultConsumableCount + 1, + nextAvatarState.inventory.Consumables.Count(e => e.Id == row.ResultConsumableItemId)); + } + } +} diff --git a/.Lib9c.Tests/Action/CreateAvatarTest.cs b/.Lib9c.Tests/Action/CreateAvatarTest.cs new file mode 100644 index 0000000000..127a3d172e --- /dev/null +++ b/.Lib9c.Tests/Action/CreateAvatarTest.cs @@ -0,0 +1,280 @@ +namespace Lib9c.Tests.Action +{ + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Runtime.Serialization.Formatters.Binary; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume; + using Nekoyume.Action; + using Nekoyume.Helper; + using Nekoyume.Model.State; + using Nekoyume.Module; + using Nekoyume.TableData; + using Xunit; + using static Lib9c.SerializeKeys; + + public class CreateAvatarTest + { + private readonly Address _agentAddress; + private readonly TableSheets _tableSheets; + + public CreateAvatarTest() + { + _agentAddress = default; + _tableSheets = new TableSheets(TableSheetsImporter.ImportSheets()); + } + + [Theory] + [InlineData(0L)] + [InlineData(7_210_000L)] + [InlineData(7_210_001L)] + public void Execute(long blockIndex) + { + var action = new CreateAvatar() + { + index = 0, + hair = 0, + ear = 0, + lens = 0, + tail = 0, + name = "test", + }; + + var sheets = TableSheetsImporter.ImportSheets(); + var state = new World(new MockWorldState()) + .SetLegacyState( + Addresses.GameConfig, + new GameConfigState(sheets[nameof(GameConfigSheet)]).Serialize() + ); + + foreach (var (key, value) in sheets) + { + state = state.SetLegacyState(Addresses.TableSheet.Derive(key), value.Serialize()); + } + + Assert.Equal(0 * CrystalCalculator.CRYSTAL, state.GetBalance(_agentAddress, CrystalCalculator.CRYSTAL)); + + var nextState = action.Execute(new ActionContext() + { + PreviousState = state, + Signer = _agentAddress, + BlockIndex = blockIndex, + RandomSeed = 0, + }); + + var avatarAddress = _agentAddress.Derive( + string.Format( + CultureInfo.InvariantCulture, + CreateAvatar.DeriveFormat, + 0 + ) + ); + Assert.True(nextState.TryGetAvatarState( + default, + avatarAddress, + out var nextAvatarState) + ); + var agentState = nextState.GetAgentState(default); + Assert.NotNull(agentState); + Assert.True(agentState.avatarAddresses.Any()); + Assert.Equal("test", nextAvatarState.name); + Assert.Equal(200_000 * CrystalCalculator.CRYSTAL, nextState.GetBalance(_agentAddress, CrystalCalculator.CRYSTAL)); + var avatarItemSheet = nextState.GetSheet(); + foreach (var row in avatarItemSheet.Values) + { + Assert.True(nextAvatarState.inventory.HasItem(row.ItemId, row.Count)); + } + + var avatarFavSheet = nextState.GetSheet(); + foreach (var row in avatarFavSheet.Values) + { + var targetAddress = row.Target == CreateAvatarFavSheet.Target.Agent + ? _agentAddress + : avatarAddress; + Assert.Equal(row.Currency * row.Quantity, nextState.GetBalance(targetAddress, row.Currency)); + } + } + + [Theory] + [InlineData("홍길동")] + [InlineData("山田太郎")] + public void ExecuteThrowInvalidNamePatterException(string nickName) + { + var agentAddress = default(Address); + + var action = new CreateAvatar() + { + index = 0, + hair = 0, + ear = 0, + lens = 0, + tail = 0, + name = nickName, + }; + + var state = new World(new MockWorldState()); + + Assert.Throws(() => action.Execute(new ActionContext() + { + PreviousState = state, + Signer = agentAddress, + BlockIndex = 0, + }) + ); + } + + [Fact] + public void ExecuteThrowInvalidAddressException() + { + var avatarAddress = _agentAddress.Derive( + string.Format( + CultureInfo.InvariantCulture, + CreateAvatar.DeriveFormat, + 0 + ) + ); + + var avatarState = new AvatarState( + avatarAddress, + _agentAddress, + 0, + _tableSheets.GetAvatarSheets(), + new GameConfigState(), + default + ); + + var action = new CreateAvatar() + { + index = 0, + hair = 0, + ear = 0, + lens = 0, + tail = 0, + name = "test", + }; + + var state = new World(new MockWorldState()).SetAvatarState(avatarAddress, avatarState); + + Assert.Throws(() => action.Execute(new ActionContext() + { + PreviousState = state, + Signer = _agentAddress, + BlockIndex = 0, + }) + ); + } + + [Theory] + [InlineData(-1)] + [InlineData(3)] + public void ExecuteThrowAvatarIndexOutOfRangeException(int index) + { + var agentState = new AgentState(_agentAddress); + var state = new World(new MockWorldState()).SetAgentState(_agentAddress, agentState); + var action = new CreateAvatar() + { + index = index, + hair = 0, + ear = 0, + lens = 0, + tail = 0, + name = "test", + }; + + Assert.Throws(() => action.Execute(new ActionContext + { + PreviousState = state, + Signer = _agentAddress, + BlockIndex = 0, + }) + ); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + public void ExecuteThrowAvatarIndexAlreadyUsedException(int index) + { + var agentState = new AgentState(_agentAddress); + var avatarAddress = _agentAddress.Derive( + string.Format( + CultureInfo.InvariantCulture, + CreateAvatar.DeriveFormat, + 0 + ) + ); + agentState.avatarAddresses[index] = avatarAddress; + var state = new World(new MockWorldState()).SetAgentState(_agentAddress, agentState); + + var action = new CreateAvatar() + { + index = index, + hair = 0, + ear = 0, + lens = 0, + tail = 0, + name = "test", + }; + + Assert.Throws(() => action.Execute(new ActionContext() + { + PreviousState = state, + Signer = _agentAddress, + BlockIndex = 0, + }) + ); + } + + [Fact] + public void AddItem() + { + var itemSheet = _tableSheets.ItemSheet; + var createAvatarItemSheet = new CreateAvatarItemSheet(); + createAvatarItemSheet.Set(@"item_id,count +10112000,2 +10512000,2 +600201,2 +"); + var avatarState = new AvatarState(default, default, 0L, _tableSheets.GetAvatarSheets(), new GameConfigState(), default, "test"); + CreateAvatar.AddItem(itemSheet, createAvatarItemSheet, avatarState, new TestRandom()); + foreach (var row in createAvatarItemSheet.Values) + { + Assert.True(avatarState.inventory.HasItem(row.ItemId, row.Count)); + } + + Assert.Equal(4, avatarState.inventory.Equipments.Count()); + foreach (var equipment in avatarState.inventory.Equipments) + { + var equipmentRow = _tableSheets.EquipmentItemSheet[equipment.Id]; + Assert.Equal(equipmentRow.Stat, equipment.Stat); + } + } + + [Fact] + public void MintAsset() + { + var createAvatarFavSheet = new CreateAvatarFavSheet(); + createAvatarFavSheet.Set(@"currency,quantity,target +CRYSTAL,200000,Agent +RUNE_GOLDENLEAF,200000,Avatar +"); + var avatarAddress = new PrivateKey().Address; + var agentAddress = new PrivateKey().Address; + var avatarState = new AvatarState(avatarAddress, agentAddress, 0L, _tableSheets.GetAvatarSheets(), new GameConfigState(), default, "test"); + var nextState = CreateAvatar.MintAsset(createAvatarFavSheet, avatarState, new World(new MockWorldState()), new ActionContext()); + foreach (var row in createAvatarFavSheet.Values) + { + var targetAddress = row.Target == CreateAvatarFavSheet.Target.Agent + ? agentAddress + : avatarAddress; + Assert.Equal(row.Currency * row.Quantity, nextState.GetBalance(targetAddress, row.Currency)); + } + } + } +} diff --git a/.Lib9c.Tests/Action/EventDungeonBattleTest.cs b/.Lib9c.Tests/Action/EventDungeonBattleTest.cs new file mode 100644 index 0000000000..92d23c0ced --- /dev/null +++ b/.Lib9c.Tests/Action/EventDungeonBattleTest.cs @@ -0,0 +1,492 @@ +namespace Lib9c.Tests.Action +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume; + using Nekoyume.Action; + using Nekoyume.Exceptions; + using Nekoyume.Extensions; + using Nekoyume.Model.Event; + using Nekoyume.Model.Rune; + using Nekoyume.Model.State; + using Nekoyume.Module; + using Nekoyume.TableData; + using Nekoyume.TableData.Event; + using Xunit; + + public class EventDungeonBattleTest + { + private readonly Currency _ncgCurrency; + private readonly TableSheets _tableSheets; + + private readonly Address _agentAddress; + private readonly Address _avatarAddress; + private IWorld _initialStates; + + public EventDungeonBattleTest() + { + _initialStates = new World(new MockWorldState()); + +#pragma warning disable CS0618 + // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 + _ncgCurrency = Currency.Legacy("NCG", 2, null); +#pragma warning restore CS0618 + _initialStates = _initialStates.SetLegacyState( + GoldCurrencyState.Address, + new GoldCurrencyState(_ncgCurrency).Serialize()); + var sheets = TableSheetsImporter.ImportSheets(); + foreach (var (key, value) in sheets) + { + _initialStates = _initialStates + .SetLegacyState(Addresses.TableSheet.Derive(key), value.Serialize()); + } + + _tableSheets = new TableSheets(sheets); + + _agentAddress = new PrivateKey().Address; + _avatarAddress = _agentAddress.Derive("avatar"); + + var agentState = new AgentState(_agentAddress); + agentState.avatarAddresses.Add(0, _avatarAddress); + + var gameConfigState = new GameConfigState(sheets[nameof(GameConfigSheet)]); + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _tableSheets.GetAvatarSheets(), + gameConfigState, + new PrivateKey().Address + ) + { + level = 100, + }; + + _initialStates = _initialStates + .SetAgentState(_agentAddress, agentState) + .SetAvatarState(_avatarAddress, avatarState) + .SetLegacyState(gameConfigState.address, gameConfigState.Serialize()); + } + + [Theory] + [InlineData(1001, 10010001, 10010001)] + public void Execute_Success_Within_Event_Period( + int eventScheduleId, + int eventDungeonId, + int eventDungeonStageId) + { + Assert.True(_tableSheets.EventScheduleSheet + .TryGetValue(eventScheduleId, out var scheduleRow)); + var contextBlockIndex = scheduleRow.StartBlockIndex; + var nextStates = Execute( + _initialStates, + eventScheduleId, + eventDungeonId, + eventDungeonStageId, + blockIndex: contextBlockIndex); + var eventDungeonInfoAddr = + EventDungeonInfo.DeriveAddress(_avatarAddress, eventDungeonId); + var eventDungeonInfo = + new EventDungeonInfo(nextStates.GetLegacyState(eventDungeonInfoAddr)); + Assert.Equal( + scheduleRow.DungeonTicketsMax - 1, + eventDungeonInfo.RemainingTickets); + + contextBlockIndex = scheduleRow.DungeonEndBlockIndex; + nextStates = Execute( + _initialStates, + eventScheduleId, + eventDungeonId, + eventDungeonStageId, + blockIndex: contextBlockIndex); + eventDungeonInfo = + new EventDungeonInfo(nextStates.GetLegacyState(eventDungeonInfoAddr)); + Assert.Equal( + scheduleRow.DungeonTicketsMax - 1, + eventDungeonInfo.RemainingTickets); + } + + [Theory] + [InlineData(1001, 10010001, 10010001, 0, 0, 0)] + [InlineData(1001, 10010001, 10010001, 1, 1, 1)] + [InlineData(1001, 10010001, 10010001, int.MaxValue, int.MaxValue, int.MaxValue - 1)] + public void Execute_Success_With_Ticket_Purchase( + int eventScheduleId, + int eventDungeonId, + int eventDungeonStageId, + int dungeonTicketPrice, + int dungeonTicketAdditionalPrice, + int numberOfTicketPurchases) + { + var context = new ActionContext(); + var previousStates = _initialStates; + var scheduleSheet = _tableSheets.EventScheduleSheet; + Assert.True(scheduleSheet.TryGetValue(eventScheduleId, out var scheduleRow)); + var sb = new StringBuilder(); + sb.AppendLine( + "id,_name,start_block_index,dungeon_end_block_index,dungeon_tickets_max,dungeon_tickets_reset_interval_block_range,dungeon_exp_seed_value,recipe_end_block_index,dungeon_ticket_price,dungeon_ticket_additional_price"); + sb.AppendLine( + $"{eventScheduleId}" + + $",\"2022 Summer Event\"" + + $",{scheduleRow.StartBlockIndex}" + + $",{scheduleRow.DungeonEndBlockIndex}" + + $",{scheduleRow.DungeonTicketsMax}" + + $",{scheduleRow.DungeonTicketsResetIntervalBlockRange}" + + $",{dungeonTicketPrice}" + + $",{dungeonTicketAdditionalPrice}" + + $",{scheduleRow.DungeonExpSeedValue}" + + $",{scheduleRow.RecipeEndBlockIndex}"); + previousStates = previousStates.SetLegacyState( + Addresses.GetSheetAddress(), + sb.ToString().Serialize()); + + var eventDungeonInfoAddr = + EventDungeonInfo.DeriveAddress(_avatarAddress, eventDungeonId); + var eventDungeonInfo = new EventDungeonInfo( + remainingTickets: 0, + numberOfTicketPurchases: numberOfTicketPurchases); + previousStates = previousStates.SetLegacyState( + eventDungeonInfoAddr, + eventDungeonInfo.Serialize()); + + Assert.True(previousStates.GetSheet() + .TryGetValue(eventScheduleId, out var newScheduleRow)); + var ncgHas = newScheduleRow.GetDungeonTicketCost( + numberOfTicketPurchases, + _ncgCurrency); + if (ncgHas.Sign > 0) + { + previousStates = previousStates.MintAsset(context, _agentAddress, ncgHas); + } + + var nextStates = Execute( + previousStates, + eventScheduleId, + eventDungeonId, + eventDungeonStageId, + buyTicketIfNeeded: true, + blockIndex: scheduleRow.StartBlockIndex); + var nextEventDungeonInfoList = + (Bencodex.Types.List)nextStates.GetLegacyState(eventDungeonInfoAddr)!; + Assert.Equal( + numberOfTicketPurchases + 1, + nextEventDungeonInfoList[2].ToInteger()); + Assert.True( + nextStates.TryGetGoldBalance( + _agentAddress, + _ncgCurrency, + out FungibleAssetValue balance + ) + ); + Assert.Equal(0 * _ncgCurrency, balance); + } + + [Theory] + [InlineData(10000001, 10010001, 10010001)] + [InlineData(10010001, 10010001, 10010001)] + public void Execute_Throw_InvalidActionFieldException_By_EventScheduleId( + int eventScheduleId, + int eventDungeonId, + int eventDungeonStageId) => + Assert.Throws(() => + Execute( + _initialStates, + eventScheduleId, + eventDungeonId, + eventDungeonStageId)); + + [Theory] + [InlineData(1001, 10010001, 10010001)] + public void Execute_Throw_InvalidActionFieldException_By_ContextBlockIndex( + int eventScheduleId, + int eventDungeonId, + int eventDungeonStageId) + { + Assert.True(_tableSheets.EventScheduleSheet + .TryGetValue(eventScheduleId, out var scheduleRow)); + var contextBlockIndex = scheduleRow.StartBlockIndex - 1; + Assert.Throws(() => + Execute( + _initialStates, + eventScheduleId, + eventDungeonId, + eventDungeonStageId, + blockIndex: contextBlockIndex)); + contextBlockIndex = scheduleRow.DungeonEndBlockIndex + 1; + Assert.Throws(() => + Execute( + _initialStates, + eventScheduleId, + eventDungeonId, + eventDungeonStageId, + blockIndex: contextBlockIndex)); + } + + [Theory] + [InlineData(1001, 10020001, 10010001)] + [InlineData(1001, 1001, 10010001)] + public void Execute_Throw_InvalidActionFieldException_By_EventDungeonId( + int eventScheduleId, + int eventDungeonId, + int eventDungeonStageId) + { + Assert.True(_tableSheets.EventScheduleSheet + .TryGetValue(eventScheduleId, out var scheduleRow)); + Assert.Throws(() => + Execute( + _initialStates, + eventScheduleId, + eventDungeonId, + eventDungeonStageId, + blockIndex: scheduleRow.StartBlockIndex)); + } + + [Theory] + [InlineData(1001, 10010001, 10020001)] + [InlineData(1001, 10010001, 1001)] + public void Execute_Throw_InvalidActionFieldException_By_EventDungeonStageId( + int eventScheduleId, + int eventDungeonId, + int eventDungeonStageId) + { + Assert.True(_tableSheets.EventScheduleSheet + .TryGetValue(eventScheduleId, out var scheduleRow)); + Assert.Throws(() => + Execute( + _initialStates, + eventScheduleId, + eventDungeonId, + eventDungeonStageId, + blockIndex: scheduleRow.StartBlockIndex)); + } + + [Theory] + [InlineData(1001, 10010001, 10010001)] + public void Execute_Throw_NotEnoughEventDungeonTicketsException( + int eventScheduleId, + int eventDungeonId, + int eventDungeonStageId) + { + var previousStates = _initialStates; + var eventDungeonInfoAddr = + EventDungeonInfo.DeriveAddress(_avatarAddress, eventDungeonId); + var eventDungeonInfo = new EventDungeonInfo(); + previousStates = previousStates + .SetLegacyState(eventDungeonInfoAddr, eventDungeonInfo.Serialize()); + Assert.True(_tableSheets.EventScheduleSheet + .TryGetValue(eventScheduleId, out var scheduleRow)); + Assert.Throws(() => + Execute( + previousStates, + eventScheduleId, + eventDungeonId, + eventDungeonStageId, + blockIndex: scheduleRow.StartBlockIndex)); + } + + [Theory] + [InlineData(1001, 10010001, 10010001, 0)] + [InlineData(1001, 10010001, 10010001, int.MaxValue - 1)] + public void Execute_Throw_InsufficientBalanceException( + int eventScheduleId, + int eventDungeonId, + int eventDungeonStageId, + int numberOfTicketPurchases) + { + var context = new ActionContext(); + var previousStates = _initialStates; + var eventDungeonInfoAddr = + EventDungeonInfo.DeriveAddress(_avatarAddress, eventDungeonId); + var eventDungeonInfo = new EventDungeonInfo( + remainingTickets: 0, + numberOfTicketPurchases: numberOfTicketPurchases); + previousStates = previousStates + .SetLegacyState(eventDungeonInfoAddr, eventDungeonInfo.Serialize()); + + Assert.True(_tableSheets.EventScheduleSheet + .TryGetValue(eventScheduleId, out var scheduleRow)); + var ncgHas = scheduleRow.GetDungeonTicketCost( + numberOfTicketPurchases, + _ncgCurrency) - 1 * _ncgCurrency; + if (ncgHas.Sign > 0) + { + previousStates = previousStates.MintAsset(context, _agentAddress, ncgHas); + } + + Assert.Throws(() => + Execute( + previousStates, + eventScheduleId, + eventDungeonId, + eventDungeonStageId, + buyTicketIfNeeded: true, + blockIndex: scheduleRow.StartBlockIndex)); + } + + [Theory] + [InlineData(1001, 10010001, 10010002)] + public void Execute_Throw_StageNotClearedException( + int eventScheduleId, + int eventDungeonId, + int eventDungeonStageId) + { + Assert.True(_tableSheets.EventScheduleSheet + .TryGetValue(eventScheduleId, out var scheduleRow)); + Assert.Throws(() => + Execute( + _initialStates, + eventScheduleId, + eventDungeonId, + eventDungeonStageId, + blockIndex: scheduleRow.StartBlockIndex)); + } + + [Theory] + [InlineData(0, 30001, 1, 30001, typeof(DuplicatedRuneIdException))] + [InlineData(1, 10002, 1, 30001, typeof(DuplicatedRuneSlotIndexException))] + public void Execute_DuplicatedException(int slotIndex, int runeId, int slotIndex2, int runeId2, Type exception) + { + Assert.True(_tableSheets.EventScheduleSheet + .TryGetValue(1001, out var scheduleRow)); + + var context = new ActionContext(); + _initialStates = _initialStates.MintAsset(context, _agentAddress, 99999 * _ncgCurrency); + + var unlockRuneSlot = new UnlockRuneSlot() + { + AvatarAddress = _avatarAddress, + SlotIndex = 1, + }; + + _initialStates = unlockRuneSlot.Execute(new ActionContext + { + BlockIndex = 1, + PreviousState = _initialStates, + Signer = _agentAddress, + RandomSeed = 0, + }); + + Assert.Throws(exception, () => + Execute( + _initialStates, + 1001, + 10010001, + 10010001, + false, + scheduleRow.StartBlockIndex, + slotIndex, + runeId, + slotIndex2, + runeId2)); + } + + [Fact] + public void Execute_V100301() + { + int eventScheduleId = 1001; + int eventDungeonId = 10010001; + int eventDungeonStageId = 10010001; + var csv = $@"id,_name,start_block_index,dungeon_end_block_index,dungeon_tickets_max,dungeon_tickets_reset_interval_block_range,dungeon_ticket_price,dungeon_ticket_additional_price,dungeon_exp_seed_value,recipe_end_block_index + 1001,2022 Summer Event,{ActionObsoleteConfig.V100301ExecutedBlockIndex},{ActionObsoleteConfig.V100301ExecutedBlockIndex + 100},5,7200,5,2,1,5018000"; + _initialStates = + _initialStates.SetLegacyState( + Addresses.GetSheetAddress(), + csv.Serialize()); + var sheet = new EventScheduleSheet(); + sheet.Set(csv); + Assert.True(sheet.TryGetValue(eventScheduleId, out var scheduleRow)); + var contextBlockIndex = scheduleRow.StartBlockIndex; + var nextStates = Execute( + _initialStates, + eventScheduleId, + eventDungeonId, + eventDungeonStageId, + blockIndex: contextBlockIndex); + var eventDungeonInfoAddr = + EventDungeonInfo.DeriveAddress(_avatarAddress, eventDungeonId); + var eventDungeonInfo = + new EventDungeonInfo(nextStates.GetLegacyState(eventDungeonInfoAddr)); + Assert.Equal( + scheduleRow.DungeonTicketsMax - 1, + eventDungeonInfo.RemainingTickets); + + contextBlockIndex = scheduleRow.DungeonEndBlockIndex; + nextStates = Execute( + _initialStates, + eventScheduleId, + eventDungeonId, + eventDungeonStageId, + blockIndex: contextBlockIndex); + eventDungeonInfo = + new EventDungeonInfo(nextStates.GetLegacyState(eventDungeonInfoAddr)); + Assert.Equal( + scheduleRow.DungeonTicketsMax - 1, + eventDungeonInfo.RemainingTickets); + } + + private IWorld Execute( + IWorld previousStates, + int eventScheduleId, + int eventDungeonId, + int eventDungeonStageId, + bool buyTicketIfNeeded = false, + long blockIndex = 0, + int slotIndex = 0, + int runeId = 10002, + int slotIndex2 = 1, + int runeId2 = 30001) + { + var previousAvatarState = previousStates.GetAvatarState(_avatarAddress); + var equipments = + Doomfist.GetAllParts(_tableSheets, previousAvatarState.level); + foreach (var equipment in equipments) + { + previousAvatarState.inventory.AddItem(equipment, iLock: null); + } + + var action = new EventDungeonBattle + { + AvatarAddress = _avatarAddress, + EventScheduleId = eventScheduleId, + EventDungeonId = eventDungeonId, + EventDungeonStageId = eventDungeonStageId, + Equipments = equipments + .Select(e => e.NonFungibleId) + .ToList(), + Costumes = new List(), + Foods = new List(), + RuneInfos = new List() + { + new RuneSlotInfo(slotIndex, runeId), + new RuneSlotInfo(slotIndex2, runeId2), + }, + BuyTicketIfNeeded = buyTicketIfNeeded, + }; + + var nextStates = action.Execute(new ActionContext + { + PreviousState = previousStates, + Signer = _agentAddress, + RandomSeed = 0, + BlockIndex = blockIndex, + }); + + Assert.True(nextStates.GetSheet().TryGetValue( + eventScheduleId, + out var scheduleRow)); + var nextAvatarState = nextStates.GetAvatarState(_avatarAddress); + var expectExp = scheduleRow.GetStageExp( + eventDungeonStageId.ToEventDungeonStageNumber()); + Assert.Equal( + previousAvatarState.exp + expectExp, + nextAvatarState.exp); + + return nextStates; + } + } +} diff --git a/.Lib9c.Tests/Action/HackAndSlashSweepTest.cs b/.Lib9c.Tests/Action/HackAndSlashSweepTest.cs new file mode 100644 index 0000000000..f14fe1e177 --- /dev/null +++ b/.Lib9c.Tests/Action/HackAndSlashSweepTest.cs @@ -0,0 +1,712 @@ +namespace Lib9c.Tests.Action +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Bencodex.Types; + using Libplanet.Action; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume; + using Nekoyume.Action; + using Nekoyume.Extensions; + using Nekoyume.Helper; + using Nekoyume.Model; + using Nekoyume.Model.Item; + using Nekoyume.Model.Rune; + using Nekoyume.Model.State; + using Nekoyume.Module; + using Nekoyume.TableData; + using Xunit; + + public class HackAndSlashSweepTest + { + private readonly Dictionary _sheets; + private readonly TableSheets _tableSheets; + + private readonly Address _agentAddress; + + private readonly Address _avatarAddress; + private readonly AvatarState _avatarState; + + private readonly Address _rankingMapAddress; + + private readonly WeeklyArenaState _weeklyArenaState; + private readonly IWorld _initialState; + private readonly IRandom _random; + + public HackAndSlashSweepTest() + { + _random = new TestRandom(); + _sheets = TableSheetsImporter.ImportSheets(); + _tableSheets = new TableSheets(_sheets); + + var privateKey = new PrivateKey(); + _agentAddress = privateKey.PublicKey.Address; + var agentState = new AgentState(_agentAddress); + + _avatarAddress = _agentAddress.Derive("avatar"); + var gameConfigState = new GameConfigState(_sheets[nameof(GameConfigSheet)]); + _rankingMapAddress = _avatarAddress.Derive("ranking_map"); + _avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _tableSheets.GetAvatarSheets(), + gameConfigState, + _rankingMapAddress + ) + { + level = 100, + }; + agentState.avatarAddresses.Add(0, _avatarAddress); + +#pragma warning disable CS0618 + // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 + var currency = Currency.Legacy("NCG", 2, null); +#pragma warning restore CS0618 + var goldCurrencyState = new GoldCurrencyState(currency); + _weeklyArenaState = new WeeklyArenaState(0); + _initialState = new World(new MockWorldState()) + .SetLegacyState(_weeklyArenaState.address, _weeklyArenaState.Serialize()) + .SetAgentState(_agentAddress, agentState) + .SetAvatarState(_avatarAddress, _avatarState) + .SetLegacyState(gameConfigState.address, gameConfigState.Serialize()) + .SetLegacyState(Addresses.GoldCurrency, goldCurrencyState.Serialize()); + + foreach (var (key, value) in _sheets) + { + _initialState = _initialState + .SetLegacyState(Addresses.TableSheet.Derive(key), value.Serialize()); + } + + foreach (var address in _avatarState.combinationSlotAddresses) + { + var slotState = new CombinationSlotState( + address, + GameConfig.RequireClearedStageLevel.CombinationEquipmentAction); + _initialState = _initialState.SetLegacyState(address, slotState.Serialize()); + } + } + + public (List Equipments, List Costumes) GetDummyItems(AvatarState avatarState) + { + var equipments = Doomfist.GetAllParts(_tableSheets, avatarState.level) + .Select(e => e.NonFungibleId).ToList(); + var random = new TestRandom(); + var costumes = new List(); + if (avatarState.level >= GameConfig.RequireCharacterLevel.CharacterFullCostumeSlot) + { + var costumeId = _tableSheets + .CostumeItemSheet + .Values + .First(r => r.ItemSubType == ItemSubType.FullCostume) + .Id; + + var costume = (Costume)ItemFactory.CreateItem( + _tableSheets.ItemSheet[costumeId], random); + avatarState.inventory.AddItem(costume); + costumes.Add(costume.ItemId); + } + + return (equipments, costumes); + } + + [Fact] + public void Execute_FailedLoadStateException() + { + var action = new HackAndSlashSweep + { + runeInfos = new List(), + apStoneCount = 1, + avatarAddress = _avatarAddress, + worldId = 1, + stageId = 1, + }; + + IWorld state = new World(new MockWorldState()); + + Assert.Throws(() => action.Execute(new ActionContext() + { + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + })); + } + + [Theory] + [InlineData(100, 1)] + public void Execute_SheetRowNotFoundException(int worldId, int stageId) + { + var action = new HackAndSlashSweep + { + runeInfos = new List(), + apStoneCount = 1, + avatarAddress = _avatarAddress, + worldId = worldId, + stageId = stageId, + }; + + var state = _initialState.SetLegacyState( + _avatarAddress.Derive("world_ids"), + List.Empty.Add(worldId.Serialize()) + ); + + Assert.Throws(() => action.Execute(new ActionContext() + { + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + })); + } + + [Theory] + [InlineData(1, 999)] + [InlineData(2, 50)] + public void Execute_SheetRowColumnException(int worldId, int stageId) + { + var action = new HackAndSlashSweep + { + runeInfos = new List(), + apStoneCount = 1, + avatarAddress = _avatarAddress, + worldId = worldId, + stageId = stageId, + }; + + var state = _initialState.SetLegacyState( + _avatarAddress.Derive("world_ids"), + List.Empty.Add(worldId.Serialize()) + ); + + Assert.Throws(() => action.Execute(new ActionContext() + { + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + })); + } + + [Theory] + [InlineData(1, 48, 1, 50)] + [InlineData(1, 49, 2, 51)] + public void Execute_InvalidStageException(int clearedWorldId, int clearedStageId, int worldId, int stageId) + { + var action = new HackAndSlashSweep + { + runeInfos = new List(), + apStoneCount = 1, + avatarAddress = _avatarAddress, + worldId = worldId, + stageId = stageId, + }; + var worldSheet = _initialState.GetSheet(); + var worldUnlockSheet = _initialState.GetSheet(); + + _avatarState.worldInformation.ClearStage(clearedWorldId, clearedStageId, 1, worldSheet, worldUnlockSheet); + + var state = _initialState + .SetLegacyState( + _avatarAddress.Derive("world_ids"), + List.Empty.Add(worldId.Serialize()) + ) + .SetAvatarState(_avatarAddress, _avatarState); + + Assert.Throws(() => action.Execute(new ActionContext() + { + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + })); + } + + [Theory] + [InlineData(GameConfig.MimisbrunnrWorldId, 10000001, false)] + [InlineData(GameConfig.MimisbrunnrWorldId, 10000001, true)] + // Unlock CRYSTAL first. + [InlineData(2, 51, false)] + public void Execute_InvalidWorldException(int worldId, int stageId, bool unlockedIdsExist) + { + var gameConfigState = new GameConfigState(_sheets[nameof(GameConfigSheet)]); + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _initialState.GetAvatarSheets(), + gameConfigState, + _rankingMapAddress) + { + worldInformation = + new WorldInformation(0, _initialState.GetSheet(), 10000001), + }; + + IWorld state = _initialState.SetAvatarState(_avatarAddress, avatarState); + + if (unlockedIdsExist) + { + state = state.SetLegacyState( + _avatarAddress.Derive("world_ids"), + List.Empty.Add(worldId.Serialize()) + ); + } + + var action = new HackAndSlashSweep + { + runeInfos = new List(), + apStoneCount = 1, + avatarAddress = _avatarAddress, + worldId = worldId, + stageId = stageId, + }; + + Assert.Throws(() => action.Execute(new ActionContext() + { + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + })); + } + + [Fact] + public void Execute_UsageLimitExceedException() + { + var gameConfigState = new GameConfigState(_sheets[nameof(GameConfigSheet)]); + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _initialState.GetAvatarSheets(), + gameConfigState, + _rankingMapAddress) + { + worldInformation = + new WorldInformation(0, _initialState.GetSheet(), 25), + }; + + IWorld state = _initialState.SetAvatarState(_avatarAddress, avatarState); + + var action = new HackAndSlashSweep + { + runeInfos = new List(), + apStoneCount = 99, + avatarAddress = _avatarAddress, + worldId = 1, + stageId = 2, + }; + + Assert.Throws(() => action.Execute(new ActionContext() + { + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + })); + } + + [Theory] + [InlineData(3, 2)] + [InlineData(7, 5)] + public void Execute_NotEnoughMaterialException(int useApStoneCount, int holdingApStoneCount) + { + var gameConfigState = _initialState.GetGameConfigState(); + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _initialState.GetAvatarSheets(), + gameConfigState, + _rankingMapAddress) + { + worldInformation = + new WorldInformation(0, _initialState.GetSheet(), 25), + level = 400, + }; + + var row = _tableSheets.MaterialItemSheet.Values.First(r => + r.ItemSubType == ItemSubType.ApStone); + var apStone = ItemFactory.CreateTradableMaterial(row); + avatarState.inventory.AddItem(apStone, holdingApStoneCount); + + IWorld state = _initialState.SetAvatarState(_avatarAddress, avatarState); + + var stageSheet = _initialState.GetSheet(); + var (expectedLevel, expectedExp) = (0, 0L); + if (stageSheet.TryGetValue(2, out var stageRow)) + { + var itemPlayCount = + gameConfigState.ActionPointMax / stageRow.CostAP * useApStoneCount; + var apPlayCount = avatarState.actionPoint / stageRow.CostAP; + var playCount = apPlayCount + itemPlayCount; + (expectedLevel, expectedExp) = avatarState.GetLevelAndExp( + _tableSheets.CharacterLevelSheet, + 2, + playCount); + + var (equipments, costumes) = GetDummyItems(avatarState); + + var action = new HackAndSlashSweep + { + equipments = equipments, + costumes = costumes, + runeInfos = new List(), + avatarAddress = _avatarAddress, + actionPoint = avatarState.actionPoint, + apStoneCount = useApStoneCount, + worldId = 1, + stageId = 2, + }; + + Assert.Throws(() => action.Execute(new ActionContext() + { + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + })); + } + } + + [Fact] + public void Execute_NotEnoughActionPointException() + { + var gameConfigState = _initialState.GetGameConfigState(); + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _initialState.GetAvatarSheets(), + gameConfigState, + _rankingMapAddress) + { + worldInformation = + new WorldInformation(0, _initialState.GetSheet(), 25), + level = 400, + actionPoint = 0, + }; + + IWorld state = _initialState.SetAvatarState(_avatarAddress, avatarState); + + var stageSheet = _initialState.GetSheet(); + var (expectedLevel, expectedExp) = (0, 0L); + if (stageSheet.TryGetValue(2, out var stageRow)) + { + var itemPlayCount = + gameConfigState.ActionPointMax / stageRow.CostAP * 1; + var apPlayCount = avatarState.actionPoint / stageRow.CostAP; + var playCount = apPlayCount + itemPlayCount; + (expectedLevel, expectedExp) = avatarState.GetLevelAndExp( + _tableSheets.CharacterLevelSheet, + 2, + playCount); + + var (equipments, costumes) = GetDummyItems(avatarState); + var action = new HackAndSlashSweep + { + runeInfos = new List(), + costumes = costumes, + equipments = equipments, + avatarAddress = _avatarAddress, + actionPoint = 999999, + apStoneCount = 0, + worldId = 1, + stageId = 2, + }; + + Assert.Throws(() => + action.Execute(new ActionContext() + { + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + })); + } + } + + [Fact] + public void Execute_PlayCountIsZeroException() + { + var gameConfigState = _initialState.GetGameConfigState(); + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _initialState.GetAvatarSheets(), + gameConfigState, + _rankingMapAddress) + { + worldInformation = + new WorldInformation(0, _initialState.GetSheet(), 25), + level = 400, + actionPoint = 0, + }; + + IWorld state = _initialState.SetAvatarState(_avatarAddress, avatarState); + + var stageSheet = _initialState.GetSheet(); + var (expectedLevel, expectedExp) = (0, 0L); + if (stageSheet.TryGetValue(2, out var stageRow)) + { + var itemPlayCount = + gameConfigState.ActionPointMax / stageRow.CostAP * 1; + var apPlayCount = avatarState.actionPoint / stageRow.CostAP; + var playCount = apPlayCount + itemPlayCount; + (expectedLevel, expectedExp) = avatarState.GetLevelAndExp( + _tableSheets.CharacterLevelSheet, + 2, + playCount); + + var (equipments, costumes) = GetDummyItems(avatarState); + var action = new HackAndSlashSweep + { + costumes = costumes, + equipments = equipments, + runeInfos = new List(), + avatarAddress = _avatarAddress, + actionPoint = 0, + apStoneCount = 0, + worldId = 1, + stageId = 2, + }; + + Assert.Throws(() => + action.Execute(new ActionContext() + { + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + })); + } + } + + [Fact] + public void Execute_NotEnoughCombatPointException() + { + var gameConfigState = _initialState.GetGameConfigState(); + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _initialState.GetAvatarSheets(), + gameConfigState, + _rankingMapAddress) + { + worldInformation = + new WorldInformation(0, _initialState.GetSheet(), 25), + actionPoint = 0, + level = 1, + }; + + IWorld state = _initialState.SetAvatarState(_avatarAddress, avatarState); + + var stageSheet = _initialState.GetSheet(); + var (expectedLevel, expectedExp) = (0, 0L); + int stageId = 24; + if (stageSheet.TryGetValue(stageId, out var stageRow)) + { + var itemPlayCount = + gameConfigState.ActionPointMax / stageRow.CostAP * 1; + var apPlayCount = avatarState.actionPoint / stageRow.CostAP; + var playCount = apPlayCount + itemPlayCount; + (expectedLevel, expectedExp) = avatarState.GetLevelAndExp( + _tableSheets.CharacterLevelSheet, + stageId, + playCount); + + var action = new HackAndSlashSweep + { + costumes = new List(), + equipments = new List(), + runeInfos = new List(), + avatarAddress = _avatarAddress, + actionPoint = avatarState.actionPoint, + apStoneCount = 1, + worldId = 1, + stageId = stageId, + }; + + Assert.Throws(() => + action.Execute(new ActionContext() + { + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + })); + } + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + [InlineData(5)] + public void ExecuteWithStake(int stakingLevel) + { + const int worldId = 1; + const int stageId = 1; + var gameConfigState = _initialState.GetGameConfigState(); + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _initialState.GetAvatarSheets(), + gameConfigState, + _rankingMapAddress) + { + worldInformation = + new WorldInformation(0, _initialState.GetSheet(), 25), + actionPoint = 120, + level = 3, + }; + var itemRow = _tableSheets.MaterialItemSheet.Values.First(r => + r.ItemSubType == ItemSubType.ApStone); + var apStone = ItemFactory.CreateTradableMaterial(itemRow); + avatarState.inventory.AddItem(apStone); + + var stakeStateAddress = StakeState.DeriveAddress(_agentAddress); + var stakeState = new StakeState(stakeStateAddress, 1); + var requiredGold = _tableSheets.StakeRegularRewardSheet.OrderedRows + .FirstOrDefault(r => r.Level == stakingLevel)?.RequiredGold ?? 0; + var context = new ActionContext(); + var state = _initialState + .SetAvatarState(_avatarAddress, avatarState) + .SetLegacyState(stakeStateAddress, stakeState.Serialize()) + .MintAsset(context, stakeStateAddress, requiredGold * _initialState.GetGoldCurrency()); + var stageSheet = _initialState.GetSheet(); + if (stageSheet.TryGetValue(stageId, out var stageRow)) + { + var apSheet = _initialState.GetSheet(); + var costAp = apSheet.GetActionPointByStaking(stageRow.CostAP, 1, stakingLevel); + var itemPlayCount = + gameConfigState.ActionPointMax / costAp * 1; + var apPlayCount = avatarState.actionPoint / costAp; + var playCount = apPlayCount + itemPlayCount; + var (expectedLevel, expectedExp) = avatarState.GetLevelAndExp( + _initialState.GetSheet(), + stageId, + playCount); + + var action = new HackAndSlashSweep + { + costumes = new List(), + equipments = new List(), + runeInfos = new List(), + avatarAddress = _avatarAddress, + actionPoint = avatarState.actionPoint, + apStoneCount = 1, + worldId = worldId, + stageId = stageId, + }; + + var nextState = action.Execute(new ActionContext + { + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + }); + var nextAvatar = nextState.GetAvatarState(_avatarAddress); + Assert.Equal(expectedLevel, nextAvatar.level); + Assert.Equal(expectedExp, nextAvatar.exp); + } + else + { + throw new SheetRowNotFoundException(nameof(StageSheet), stageId); + } + } + + [Theory] + [InlineData(0, 30001, 1, 30001, typeof(DuplicatedRuneIdException))] + [InlineData(1, 10002, 1, 30001, typeof(DuplicatedRuneSlotIndexException))] + public void ExecuteDuplicatedException(int slotIndex, int runeId, int slotIndex2, int runeId2, Type exception) + { + var stakingLevel = 1; + const int worldId = 1; + const int stageId = 1; + var gameConfigState = _initialState.GetGameConfigState(); + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _initialState.GetAvatarSheets(), + gameConfigState, + _rankingMapAddress) + { + worldInformation = + new WorldInformation(0, _initialState.GetSheet(), 25), + actionPoint = 120, + level = 3, + }; + var itemRow = _tableSheets.MaterialItemSheet.Values.First(r => + r.ItemSubType == ItemSubType.ApStone); + var apStone = ItemFactory.CreateTradableMaterial(itemRow); + avatarState.inventory.AddItem(apStone); + + var stakeStateAddress = StakeState.DeriveAddress(_agentAddress); + var stakeState = new StakeState(stakeStateAddress, 1); + var requiredGold = _tableSheets.StakeRegularRewardSheet.OrderedRows + .FirstOrDefault(r => r.Level == stakingLevel)?.RequiredGold ?? 0; + var context = new ActionContext(); + var state = _initialState + .SetAvatarState(_avatarAddress, avatarState) + .SetLegacyState(stakeStateAddress, stakeState.Serialize()) + .MintAsset(context, stakeStateAddress, requiredGold * _initialState.GetGoldCurrency()); + var stageSheet = _initialState.GetSheet(); + if (stageSheet.TryGetValue(stageId, out var stageRow)) + { + var apSheet = _initialState.GetSheet(); + var costAp = apSheet.GetActionPointByStaking(stageRow.CostAP, 1, stakingLevel); + var itemPlayCount = + gameConfigState.ActionPointMax / costAp * 1; + var apPlayCount = avatarState.actionPoint / costAp; + var playCount = apPlayCount + itemPlayCount; + var (expectedLevel, expectedExp) = avatarState.GetLevelAndExp( + _initialState.GetSheet(), + stageId, + playCount); + + var ncgCurrency = state.GetGoldCurrency(); + state = state.MintAsset(context, _agentAddress, 99999 * ncgCurrency); + + var unlockRuneSlot = new UnlockRuneSlot() + { + AvatarAddress = _avatarAddress, + SlotIndex = 1, + }; + + state = unlockRuneSlot.Execute(new ActionContext + { + BlockIndex = 1, + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + }); + + var action = new HackAndSlashSweep + { + costumes = new List(), + equipments = new List(), + runeInfos = new List() + { + new RuneSlotInfo(slotIndex, runeId), + new RuneSlotInfo(slotIndex2, runeId2), + }, + avatarAddress = _avatarAddress, + actionPoint = avatarState.actionPoint, + apStoneCount = 1, + worldId = worldId, + stageId = stageId, + }; + + Assert.Throws(exception, () => action.Execute(new ActionContext + { + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + })); + } + else + { + throw new SheetRowNotFoundException(nameof(StageSheet), stageId); + } + } + } +} diff --git a/.Lib9c.Tests/Action/ItemEnhancementTest.cs b/.Lib9c.Tests/Action/ItemEnhancementTest.cs new file mode 100644 index 0000000000..901db08f20 --- /dev/null +++ b/.Lib9c.Tests/Action/ItemEnhancementTest.cs @@ -0,0 +1,366 @@ +namespace Lib9c.Tests.Action +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using Bencodex.Types; + using Lib9c.Tests.Fixtures.TableCSV.Cost; + using Lib9c.Tests.Util; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume; + using Nekoyume.Action; + using Nekoyume.Extensions; + using Nekoyume.Model.Item; + using Nekoyume.Model.Mail; + using Nekoyume.Model.State; + using Nekoyume.Module; + using Xunit; + + public class ItemEnhancementTest + { + private readonly TableSheets _tableSheets; + private readonly Address _agentAddress; + private readonly Address _avatarAddress; + private readonly AvatarState _avatarState; + private readonly Currency _currency; + private IWorld _initialState; + + public ItemEnhancementTest() + { + _initialState = new World(new MockWorldState()); + Dictionary sheets; + (_initialState, sheets) = InitializeUtil.InitializeTableSheets( + _initialState, + sheetsOverride: new Dictionary + { + { + "EnhancementCostSheetV3", + EnhancementCostSheetFixtures.V3 + }, + }); + _tableSheets = new TableSheets(sheets); + foreach (var (key, value) in sheets) + { + _initialState = + _initialState.SetLegacyState(Addresses.TableSheet.Derive(key), value.Serialize()); + } + + var privateKey = new PrivateKey(); + _agentAddress = privateKey.PublicKey.Address; + var agentState = new AgentState(_agentAddress); + + _avatarAddress = _agentAddress.Derive("avatar"); + _avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _tableSheets.GetAvatarSheets(), + new GameConfigState(), + default + ); + + agentState.avatarAddresses.Add(0, _avatarAddress); + +#pragma warning disable CS0618 + // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 + _currency = Currency.Legacy("NCG", 2, null); +#pragma warning restore CS0618 + var gold = new GoldCurrencyState(_currency); + var slotAddress = _avatarAddress.Derive(string.Format( + CultureInfo.InvariantCulture, + CombinationSlotState.DeriveFormat, + 0 + )); + + var context = new ActionContext(); + _initialState = _initialState + .SetAgentState(_agentAddress, agentState) + .SetAvatarState(_avatarAddress, _avatarState) + .SetLegacyState(slotAddress, new CombinationSlotState(slotAddress, 0).Serialize()) + .SetLegacyState(GoldCurrencyState.Address, gold.Serialize()) + .MintAsset(context, GoldCurrencyState.Address, gold.Currency * 100_000_000_000) + .TransferAsset( + context, + Addresses.GoldCurrency, + _agentAddress, + gold.Currency * 3_000_000 + ); + + Assert.Equal( + gold.Currency * 99_997_000_000, + _initialState.GetBalance(Addresses.GoldCurrency, gold.Currency) + ); + Assert.Equal( + gold.Currency * 3_000_000, + _initialState.GetBalance(_agentAddress, gold.Currency) + ); + } + + [Theory] + // from 0 to 0 using one level 0 material + [InlineData(0, false, 0, false, 1)] + [InlineData(0, false, 0, true, 1)] + [InlineData(0, true, 0, false, 1)] + [InlineData(0, true, 0, true, 1)] + // from 0 to 1 using two level 0 material + [InlineData(0, false, 0, false, 3)] + [InlineData(0, false, 0, true, 3)] + [InlineData(0, true, 0, false, 3)] + [InlineData(0, true, 0, true, 3)] + // // Duplicated > from 0 to 0 + [InlineData(0, false, 0, false, 3, true)] + [InlineData(0, false, 0, true, 3, true)] + [InlineData(0, true, 0, false, 3, true)] + [InlineData(0, true, 0, true, 3, true)] + // from 0 to N using multiple level 0 materials + [InlineData(0, false, 0, false, 7)] + [InlineData(0, false, 0, false, 31)] + [InlineData(0, false, 0, true, 7)] + [InlineData(0, false, 0, true, 31)] + [InlineData(0, true, 0, false, 7)] + [InlineData(0, true, 0, false, 31)] + [InlineData(0, true, 0, true, 7)] + [InlineData(0, true, 0, true, 31)] + // // Duplicated > from 0 to 0 + [InlineData(0, false, 0, false, 7, true)] + [InlineData(0, false, 0, false, 31, true)] + [InlineData(0, false, 0, true, 7, true)] + [InlineData(0, false, 0, true, 31, true)] + [InlineData(0, true, 0, false, 7, true)] + [InlineData(0, true, 0, false, 31, true)] + [InlineData(0, true, 0, true, 7, true)] + [InlineData(0, true, 0, true, 31, true)] + // from K to K with material(s). Check requiredBlock == 0 + [InlineData(10, false, 0, false, 1)] + [InlineData(10, false, 0, true, 1)] + [InlineData(10, true, 0, false, 1)] + [InlineData(10, true, 0, true, 1)] + // from K to N using one level X material + [InlineData(5, false, 6, false, 1)] + [InlineData(5, false, 6, true, 1)] + [InlineData(5, true, 6, false, 1)] + [InlineData(5, true, 6, true, 1)] + // from K to N using multiple materials + [InlineData(5, false, 4, false, 6)] + [InlineData(5, false, 7, false, 5)] + [InlineData(5, false, 4, true, 6)] + [InlineData(5, false, 7, true, 5)] + [InlineData(5, true, 4, false, 6)] + [InlineData(5, true, 7, false, 5)] + [InlineData(5, true, 4, true, 6)] + [InlineData(5, true, 7, true, 5)] + // // Duplicated: from K to K + [InlineData(5, true, 4, true, 6, true)] + [InlineData(5, true, 7, true, 5, true)] + [InlineData(5, true, 4, false, 6, true)] + [InlineData(5, true, 7, false, 5, true)] + [InlineData(5, false, 4, true, 6, true)] + [InlineData(5, false, 7, true, 5, true)] + [InlineData(5, false, 4, false, 6, true)] + [InlineData(5, false, 7, false, 5, true)] + // from 20 to 21 (just to reach level 21 exp) + [InlineData(20, false, 20, false, 1)] + [InlineData(20, false, 20, true, 1)] + [InlineData(20, true, 20, false, 1)] + [InlineData(20, true, 20, true, 1)] + // from 20 to 21 (over level 21) + [InlineData(20, false, 20, false, 2)] + [InlineData(20, false, 20, true, 2)] + [InlineData(20, true, 20, false, 2)] + [InlineData(20, true, 20, true, 2)] + // from 21 to 21 (no level up) + [InlineData(21, false, 1, false, 1)] + [InlineData(21, false, 21, false, 1)] + [InlineData(21, false, 1, true, 1)] + [InlineData(21, false, 21, true, 1)] + [InlineData(21, true, 1, false, 1)] + [InlineData(21, true, 21, false, 1)] + [InlineData(21, true, 1, true, 1)] + [InlineData(21, true, 21, true, 1)] + public void Execute( + int startLevel, + bool oldStart, + int materialLevel, + bool oldMaterial, + int materialCount, + bool duplicated = false + ) + { + var row = _tableSheets.EquipmentItemSheet.Values.First(r => r.Id == 10110000); + var equipment = (Equipment)ItemFactory.CreateItemUsable(row, default, 0, startLevel); + if (startLevel == 0) + { + equipment.Exp = (long)row.Exp!; + } + else + { + equipment.Exp = _tableSheets.EnhancementCostSheetV3.OrderedList.First(r => + r.ItemSubType == equipment.ItemSubType && r.Grade == equipment.Grade && + r.Level == equipment.level).Exp; + } + + var startExp = equipment.Exp; + if (oldStart) + { + equipment.Exp = 0L; + } + + _avatarState.inventory.AddItem(equipment, count: 1); + + var startRow = _tableSheets.EnhancementCostSheetV3.OrderedList.FirstOrDefault(r => + r.Grade == equipment.Grade && r.ItemSubType == equipment.ItemSubType && + r.Level == startLevel); + var expectedExpIncrement = 0L; + var materialIds = new List(); + var duplicatedGuid = Guid.NewGuid(); + for (var i = 0; i < materialCount; i++) + { + var materialId = duplicated ? duplicatedGuid : Guid.NewGuid(); + materialIds.Add(materialId); + var material = + (Equipment)ItemFactory.CreateItemUsable(row, materialId, 0, materialLevel); + if (materialLevel == 0) + { + material.Exp = (long)row.Exp!; + } + else + { + material.Exp = _tableSheets.EnhancementCostSheetV3.OrderedList.First(r => + r.ItemSubType == material.ItemSubType && r.Grade == material.Grade && + r.Level == material.level).Exp; + } + + if (!(duplicated && i > 0)) + { + expectedExpIncrement += material.Exp; + } + + if (oldMaterial) + { + material.Exp = 0L; + } + + _avatarState.inventory.AddItem(material, count: 1); + } + + var result = new CombinationConsumable5.ResultModel() + { + id = default, + gold = 0, + actionPoint = 0, + recipeId = 1, + materials = new Dictionary(), + itemUsable = equipment, + }; + var preItemUsable = new Equipment((Dictionary)equipment.Serialize()); + + for (var i = 0; i < 100; i++) + { + var mail = new CombinationMail(result, i, default, 0); + _avatarState.Update(mail); + } + + _avatarState.worldInformation.ClearStage( + 1, + 1, + 1, + _tableSheets.WorldSheet, + _tableSheets.WorldUnlockSheet + ); + + var slotAddress = + _avatarAddress.Derive(string.Format( + CultureInfo.InvariantCulture, + CombinationSlotState.DeriveFormat, + 0 + )); + + Assert.Equal(startLevel, equipment.level); + + _initialState = _initialState.SetAvatarState(_avatarAddress, _avatarState); + + var action = new ItemEnhancement + { + itemId = default, + materialIds = materialIds, + avatarAddress = _avatarAddress, + slotIndex = 0, + }; + + var nextState = action.Execute(new ActionContext() + { + PreviousState = _initialState, + Signer = _agentAddress, + BlockIndex = 1, + RandomSeed = 0, + }); + + var slotState = nextState.GetCombinationSlotState(_avatarAddress, 0); + var resultEquipment = (Equipment)slotState.Result.itemUsable; + var level = resultEquipment.level; + var nextAvatarState = nextState.GetAvatarState(_avatarAddress); + var expectedTargetRow = _tableSheets.EnhancementCostSheetV3.OrderedList.FirstOrDefault( + r => r.Grade == equipment.Grade && r.ItemSubType == equipment.ItemSubType && + r.Level == level); + var expectedCost = (expectedTargetRow?.Cost ?? 0) - (startRow?.Cost ?? 0); + var expectedBlockIndex = + (expectedTargetRow?.RequiredBlockIndex ?? 0) - (startRow?.RequiredBlockIndex ?? 0); + Assert.Equal(default, resultEquipment.ItemId); + Assert.Equal(startExp + expectedExpIncrement, resultEquipment.Exp); + Assert.Equal( + (3_000_000 - expectedCost) * _currency, + nextState.GetBalance(_agentAddress, _currency) + ); + + var arenaSheet = _tableSheets.ArenaSheet; + var arenaData = arenaSheet.GetRoundByBlockIndex(1); + var feeStoreAddress = + Addresses.GetBlacksmithFeeAddress(arenaData.ChampionshipId, arenaData.Round); + Assert.Equal( + expectedCost * _currency, + nextState.GetBalance(feeStoreAddress, _currency) + ); + Assert.Equal(30, nextAvatarState.mailBox.Count); + + var stateDict = (Dictionary)nextState.GetLegacyState(slotAddress); + var slot = new CombinationSlotState(stateDict); + var slotResult = (ItemEnhancement13.ResultModel)slot.Result; + if (startLevel != level) + { + var baseMinAtk = (decimal)preItemUsable.StatsMap.BaseATK; + var baseMaxAtk = (decimal)preItemUsable.StatsMap.BaseATK; + var extraMinAtk = (decimal)preItemUsable.StatsMap.AdditionalATK; + var extraMaxAtk = (decimal)preItemUsable.StatsMap.AdditionalATK; + + for (var i = startLevel + 1; i <= level; i++) + { + var currentRow = _tableSheets.EnhancementCostSheetV3.OrderedList + .First(x => + x.Grade == 1 && x.ItemSubType == equipment.ItemSubType && x.Level == i); + + baseMinAtk *= currentRow.BaseStatGrowthMin.NormalizeFromTenThousandths() + 1; + baseMaxAtk *= currentRow.BaseStatGrowthMax.NormalizeFromTenThousandths() + 1; + extraMinAtk *= currentRow.ExtraStatGrowthMin.NormalizeFromTenThousandths() + 1; + extraMaxAtk *= currentRow.ExtraStatGrowthMax.NormalizeFromTenThousandths() + 1; + } + + Assert.InRange( + resultEquipment.StatsMap.ATK, + baseMinAtk + extraMinAtk, + baseMaxAtk + extraMaxAtk + 1 + ); + } + + Assert.Equal( + expectedBlockIndex + 1, // +1 for execution + resultEquipment.RequiredBlockIndex + ); + Assert.Equal(preItemUsable.ItemId, slotResult.preItemUsable.ItemId); + Assert.Equal(preItemUsable.ItemId, resultEquipment.ItemId); + Assert.Equal(expectedCost, slotResult.gold); + } + } +} diff --git a/.Lib9c.Tests/Action/RaidTest.cs b/.Lib9c.Tests/Action/RaidTest.cs new file mode 100644 index 0000000000..3a383361a0 --- /dev/null +++ b/.Lib9c.Tests/Action/RaidTest.cs @@ -0,0 +1,626 @@ +namespace Lib9c.Tests.Action +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Bencodex.Types; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume; + using Nekoyume.Action; + using Nekoyume.Battle; + using Nekoyume.Extensions; + using Nekoyume.Helper; + using Nekoyume.Model.Arena; + using Nekoyume.Model.Rune; + using Nekoyume.Model.Stat; + using Nekoyume.Model.State; + using Nekoyume.Module; + using Nekoyume.TableData; + using Xunit; + using static SerializeKeys; + + public class RaidTest + { + private readonly Dictionary _sheets; + private readonly Address _agentAddress; + private readonly Address _avatarAddress; + private readonly TableSheets _tableSheets; + private readonly Currency _goldCurrency; + + public RaidTest() + { + _sheets = TableSheetsImporter.ImportSheets(); + _tableSheets = new TableSheets(_sheets); + _agentAddress = new PrivateKey().Address; + _avatarAddress = new PrivateKey().Address; +#pragma warning disable CS0618 + // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 + _goldCurrency = Currency.Legacy("NCG", 2, null); +#pragma warning restore CS0618 + } + + [Theory] + // Join new raid. + [InlineData(null, true, true, true, false, 0, 0L, false, false, 0, false, false, false, 5, false, 0, 10002, 1, 30001)] + [InlineData(null, true, true, true, false, 0, 0L, false, false, 0, false, false, false, 5, true, 0, 10002, 1, 30001)] + // Refill by interval. + [InlineData(null, true, true, false, true, 0, -10368, false, false, 0, false, false, false, 5, true, 0, 10002, 1, 30001)] + // Refill by NCG. + [InlineData(null, true, true, false, true, 0, 200L, true, true, 0, false, false, false, 5, true, 0, 10002, 1, 30001)] + [InlineData(null, true, true, false, true, 0, 200L, true, true, 1, false, false, false, 5, true, 0, 10002, 1, 30001)] + // Boss level up. + [InlineData(null, true, true, false, true, 3, 100L, false, false, 0, true, true, false, 5, true, 0, 10002, 1, 30001)] + // Update RaidRewardInfo. + [InlineData(null, true, true, false, true, 3, 100L, false, false, 0, true, true, true, 5, true, 0, 10002, 1, 30001)] + // Boss skip level up. + [InlineData(null, true, true, false, true, 3, 100L, false, false, 0, true, false, false, 5, true, 0, 10002, 1, 30001)] + // AvatarState null. + [InlineData(typeof(FailedLoadStateException), false, false, false, false, 0, 0L, false, false, 0, false, false, false, 5, false, 0, 10002, 1, 30001)] + // Insufficient CRYSTAL. + [InlineData(typeof(InsufficientBalanceException), true, true, false, false, 0, 0L, false, false, 0, false, false, false, 5, false, 0, 10002, 1, 30001)] + // Insufficient NCG. + [InlineData(typeof(InsufficientBalanceException), true, true, false, true, 0, 0L, true, false, 0, false, false, false, 5, false, 0, 10002, 1, 30001)] + // Wait interval. + [InlineData(typeof(RequiredBlockIntervalException), true, true, false, true, 3, 10L, false, false, 0, false, false, false, 1, false, 0, 10002, 1, 30001)] + // Exceed purchase limit. + [InlineData(typeof(ExceedTicketPurchaseLimitException), true, true, false, true, 0, 100L, true, false, 1_000, false, false, false, 5, false, 0, 10002, 1, 30001)] + // Exceed challenge count. + [InlineData(typeof(ExceedPlayCountException), true, true, false, true, 0, 100L, false, false, 0, false, false, false, 5, false, 0, 10002, 1, 30001)] + [InlineData(typeof(DuplicatedRuneIdException), true, true, false, true, 3, 100L, true, false, 0, false, false, false, 5, false, 0, 30001, 1, 30001)] + [InlineData(typeof(DuplicatedRuneSlotIndexException), true, true, false, true, 3, 100L, true, false, 0, false, false, false, 5, false, 1, 10002, 1, 30001)] + public void Execute( + Type exc, + bool avatarExist, + bool stageCleared, + bool crystalExist, + bool raiderStateExist, + int remainChallengeCount, + long refillBlockIndexOffset, + bool payNcg, + bool ncgExist, + int purchaseCount, + bool kill, + bool levelUp, + bool rewardRecordExist, + long executeOffset, + bool raiderListExist, + int slotIndex, + int runeId, + int slotIndex2, + int runeId2 + ) + { + var blockIndex = _tableSheets.WorldBossListSheet.Values + .OrderBy(x => x.StartedBlockIndex) + .First(x => + { + if (exc == typeof(InsufficientBalanceException)) + { + return ncgExist ? x.TicketPrice > 0 : x.EntranceFee > 0; + } + + return true; + }) + .StartedBlockIndex; + + var action = new Raid + { + AvatarAddress = _avatarAddress, + EquipmentIds = new List(), + CostumeIds = new List(), + FoodIds = new List(), + RuneInfos = new List() + { + new RuneSlotInfo(slotIndex, runeId), + new RuneSlotInfo(slotIndex2, runeId2), + }, + PayNcg = payNcg, + }; + Currency crystal = CrystalCalculator.CRYSTAL; + int raidId = _tableSheets.WorldBossListSheet.FindRaidIdByBlockIndex(blockIndex); + Address raiderAddress = Addresses.GetRaiderAddress(_avatarAddress, raidId); + var goldCurrencyState = new GoldCurrencyState(_goldCurrency); + WorldBossListSheet.Row worldBossRow = _tableSheets.WorldBossListSheet.FindRowByBlockIndex(blockIndex); + var hpSheet = _tableSheets.WorldBossGlobalHpSheet; + Address bossAddress = Addresses.GetWorldBossAddress(raidId); + Address worldBossKillRewardRecordAddress = Addresses.GetWorldBossKillRewardRecordAddress(_avatarAddress, raidId); + Address raiderListAddress = Addresses.GetRaiderListAddress(raidId); + int level = 1; + if (kill & !levelUp) + { + level = hpSheet.OrderedList.Last().Level; + } + + var fee = _tableSheets.WorldBossListSheet[raidId].EntranceFee; + + var context = new ActionContext(); + IWorld state = new World(new MockWorldState()) + .SetLegacyState(goldCurrencyState.address, goldCurrencyState.Serialize()) + .SetAgentState(_agentAddress, new AgentState(_agentAddress)); + + foreach (var (key, value) in _sheets) + { + state = state.SetLegacyState(Addresses.TableSheet.Derive(key), value.Serialize()); + } + + var gameConfigState = new GameConfigState(_sheets[nameof(GameConfigSheet)]); + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _tableSheets.GetAvatarSheets(), + gameConfigState, + default + ); + + if (avatarExist) + { + var equipments = Doomfist.GetAllParts(_tableSheets, avatarState.level); + foreach (var equipment in equipments) + { + avatarState.inventory.AddItem(equipment); + } + + if (stageCleared) + { + for (int i = 0; i < 50; i++) + { + avatarState.worldInformation.ClearStage(1, i + 1, 0, _tableSheets.WorldSheet, _tableSheets.WorldUnlockSheet); + } + } + + if (crystalExist) + { + var price = _tableSheets.WorldBossListSheet[raidId].EntranceFee; + state = state.MintAsset(context, _agentAddress, price * crystal); + } + + if (raiderStateExist) + { + var raiderState = new RaiderState(); + raiderState.RefillBlockIndex = blockIndex + refillBlockIndexOffset; + raiderState.RemainChallengeCount = remainChallengeCount; + raiderState.TotalScore = 1_000; + raiderState.HighScore = 0; + raiderState.TotalChallengeCount = 1; + raiderState.PurchaseCount = purchaseCount; + raiderState.Cp = 0; + raiderState.Level = 0; + raiderState.IconId = 0; + raiderState.AvatarName = "hash"; + raiderState.AvatarAddress = _avatarAddress; + raiderState.UpdatedBlockIndex = blockIndex; + + state = state.SetLegacyState(raiderAddress, raiderState.Serialize()); + + var raiderList = new List().Add(raiderAddress.Serialize()); + + if (raiderListExist) + { + raiderList = raiderList.Add(new PrivateKey().Address.Serialize()); + } + + state = state.SetLegacyState(raiderListAddress, raiderList); + } + + if (rewardRecordExist) + { + var rewardRecord = new WorldBossKillRewardRecord + { + [0] = false, + }; + state = state.SetLegacyState(worldBossKillRewardRecordAddress, rewardRecord.Serialize()); + } + + if (ncgExist) + { + var row = _tableSheets.WorldBossListSheet.FindRowByBlockIndex(blockIndex); + state = state.MintAsset(context, _agentAddress, (row.TicketPrice + row.AdditionalTicketPrice * purchaseCount) * _goldCurrency); + } + + state = state + .SetAvatarState(_avatarAddress, avatarState) + .SetLegacyState(gameConfigState.address, gameConfigState.Serialize()); + } + + if (kill) + { + var bossState = + new WorldBossState(worldBossRow, _tableSheets.WorldBossGlobalHpSheet[level]) + { + CurrentHp = 0, + Level = level, + }; + state = state.SetLegacyState(bossAddress, bossState.Serialize()); + } + + if (exc is null) + { + var randomSeed = 0; + var ctx = new ActionContext + { + BlockIndex = blockIndex + executeOffset, + PreviousState = state, + RandomSeed = randomSeed, + Signer = _agentAddress, + }; + + var nextState = action.Execute(ctx); + + var random = new TestRandom(randomSeed); + var bossListRow = _tableSheets.WorldBossListSheet.FindRowByBlockIndex(ctx.BlockIndex); + var raidSimulatorSheets = _tableSheets.GetRaidSimulatorSheets(); + var simulator = new RaidSimulator( + bossListRow.BossId, + random, + avatarState, + action.FoodIds, + null, + raidSimulatorSheets, + _tableSheets.CostumeStatSheet, + new List()); + simulator.Simulate(); + var score = simulator.DamageDealt; + + Dictionary rewardMap + = new Dictionary(); + foreach (var reward in simulator.AssetReward) + { + rewardMap[reward.Currency] = reward; + } + + if (rewardRecordExist) + { + var bossRow = raidSimulatorSheets.WorldBossCharacterSheet[bossListRow.BossId]; + Assert.True(state.TryGetLegacyState(bossAddress, out List prevRawBoss)); + var prevBossState = new WorldBossState(prevRawBoss); + int rank = WorldBossHelper.CalculateRank(bossRow, raiderStateExist ? 1_000 : 0); + var rewards = RuneHelper.CalculateReward( + rank, + prevBossState.Id, + _tableSheets.RuneWeightSheet, + _tableSheets.WorldBossKillRewardSheet, + _tableSheets.RuneSheet, + random + ); + + foreach (var reward in rewards) + { + if (!rewardMap.ContainsKey(reward.Currency)) + { + rewardMap[reward.Currency] = reward; + } + else + { + rewardMap[reward.Currency] += reward; + } + } + + foreach (var reward in rewardMap) + { + if (reward.Key.Equals(CrystalCalculator.CRYSTAL)) + { + Assert.Equal(reward.Value, nextState.GetBalance(_agentAddress, reward.Key)); + } + else + { + Assert.Equal(reward.Value, nextState.GetBalance(_avatarAddress, reward.Key)); + } + } + } + + if (rewardMap.ContainsKey(crystal)) + { + Assert.Equal(rewardMap[crystal], nextState.GetBalance(_agentAddress, crystal)); + } + + if (crystalExist) + { + Assert.Equal(fee * crystal, nextState.GetBalance(bossAddress, crystal)); + } + + Assert.True(nextState.TryGetLegacyState(raiderAddress, out List rawRaider)); + var raiderState = new RaiderState(rawRaider); + long expectedTotalScore = raiderStateExist ? 1_000 + score : score; + int expectedRemainChallenge = payNcg ? 0 : 2; + int expectedTotalChallenge = raiderStateExist ? 2 : 1; + + Assert.Equal(score, raiderState.HighScore); + Assert.Equal(expectedTotalScore, raiderState.TotalScore); + Assert.Equal(expectedRemainChallenge, raiderState.RemainChallengeCount); + Assert.Equal(expectedTotalChallenge, raiderState.TotalChallengeCount); + Assert.Equal(1, raiderState.Level); + Assert.Equal(GameConfig.DefaultAvatarArmorId, raiderState.IconId); + Assert.True(raiderState.Cp > 0); + + Assert.True(nextState.TryGetLegacyState(bossAddress, out List rawBoss)); + var bossState = new WorldBossState(rawBoss); + int expectedLevel = level; + if (kill & levelUp) + { + expectedLevel++; + } + + Assert.Equal(expectedLevel, bossState.Level); + Assert.Equal(expectedLevel, raiderState.LatestBossLevel); + if (kill) + { + Assert.Equal(hpSheet[expectedLevel].Hp, bossState.CurrentHp); + } + + if (payNcg) + { + Assert.Equal(0 * _goldCurrency, nextState.GetBalance(_agentAddress, _goldCurrency)); + Assert.Equal(purchaseCount + 1, nextState.GetRaiderState(raiderAddress).PurchaseCount); + } + + Assert.True(nextState.TryGetLegacyState(worldBossKillRewardRecordAddress, out List rawRewardInfo)); + var rewardRecord = new WorldBossKillRewardRecord(rawRewardInfo); + Assert.Contains(expectedLevel, rewardRecord.Keys); + if (rewardRecordExist) + { + Assert.True(rewardRecord[0]); + } + else + { + if (expectedLevel == 1) + { + Assert.False(rewardRecord[1]); + } + else + { + Assert.DoesNotContain(1, rewardRecord.Keys); + } + } + + Assert.True(nextState.TryGetLegacyState(raiderListAddress, out List rawRaiderList)); + List
raiderList = rawRaiderList.ToList(StateExtensions.ToAddress); + + Assert.Contains(raiderAddress, raiderList); + } + else + { + if (exc == typeof(DuplicatedRuneIdException) || exc == typeof(DuplicatedRuneSlotIndexException)) + { + var ncgCurrency = state.GetGoldCurrency(); + state = state.MintAsset(context, _agentAddress, 99999 * ncgCurrency); + + var unlockRuneSlot = new UnlockRuneSlot() + { + AvatarAddress = _avatarAddress, + SlotIndex = 1, + }; + + state = unlockRuneSlot.Execute(new ActionContext + { + BlockIndex = 1, + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + }); + } + + Assert.Throws(exc, () => action.Execute(new ActionContext + { + BlockIndex = blockIndex + executeOffset, + PreviousState = state, + RandomSeed = 0, + Signer = _agentAddress, + })); + } + } + + [Fact] + public void Execute_With_Reward() + { + var action = new Raid + { + AvatarAddress = _avatarAddress, + EquipmentIds = new List(), + CostumeIds = new List(), + FoodIds = new List(), + RuneInfos = new List(), + PayNcg = false, + }; + + var worldBossRow = _tableSheets.WorldBossListSheet.First().Value; + int raidId = worldBossRow.Id; + Address raiderAddress = Addresses.GetRaiderAddress(_avatarAddress, raidId); + var goldCurrencyState = new GoldCurrencyState(_goldCurrency); + Address bossAddress = Addresses.GetWorldBossAddress(raidId); + Address worldBossKillRewardRecordAddress = Addresses.GetWorldBossKillRewardRecordAddress(_avatarAddress, raidId); + + IWorld state = new World(new MockWorldState()) + .SetLegacyState(goldCurrencyState.address, goldCurrencyState.Serialize()) + .SetAgentState(_agentAddress, new AgentState(_agentAddress)); + + foreach (var (key, value) in _sheets) + { + state = state.SetLegacyState(Addresses.TableSheet.Derive(key), value.Serialize()); + } + + var gameConfigState = new GameConfigState(_sheets[nameof(GameConfigSheet)]); + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _tableSheets.GetAvatarSheets(), + gameConfigState, + default + ); + + for (int i = 0; i < 50; i++) + { + avatarState.worldInformation.ClearStage(1, i + 1, 0, _tableSheets.WorldSheet, _tableSheets.WorldUnlockSheet); + } + + var raiderState = new RaiderState(); + raiderState.RefillBlockIndex = 0; + raiderState.RemainChallengeCount = WorldBossHelper.MaxChallengeCount; + raiderState.TotalScore = 1_000; + raiderState.TotalChallengeCount = 1; + raiderState.PurchaseCount = 0; + raiderState.Cp = 0; + raiderState.Level = 0; + raiderState.IconId = 0; + raiderState.AvatarName = "hash"; + raiderState.AvatarAddress = _avatarAddress; + state = state.SetLegacyState(raiderAddress, raiderState.Serialize()); + + var rewardRecord = new WorldBossKillRewardRecord + { + [1] = false, + }; + state = state.SetLegacyState(worldBossKillRewardRecordAddress, rewardRecord.Serialize()); + + state = state + .SetAvatarState(_avatarAddress, avatarState) + .SetLegacyState(gameConfigState.address, gameConfigState.Serialize()); + + var bossState = + new WorldBossState(worldBossRow, _tableSheets.WorldBossGlobalHpSheet[2]) + { + CurrentHp = 0, + Level = 2, + }; + state = state.SetLegacyState(bossAddress, bossState.Serialize()); + var randomSeed = 0; + var random = new TestRandom(randomSeed); + + var simulator = new RaidSimulator( + worldBossRow.BossId, + random, + avatarState, + action.FoodIds, + null, + _tableSheets.GetRaidSimulatorSheets(), + _tableSheets.CostumeStatSheet, + new List()); + simulator.Simulate(); + + Dictionary rewardMap + = new Dictionary(); + foreach (var reward in simulator.AssetReward) + { + rewardMap[reward.Currency] = reward; + } + + List killRewards = RuneHelper.CalculateReward( + 0, + bossState.Id, + _tableSheets.RuneWeightSheet, + _tableSheets.WorldBossKillRewardSheet, + _tableSheets.RuneSheet, + random + ); + + var nextState = action.Execute(new ActionContext + { + BlockIndex = worldBossRow.StartedBlockIndex + gameConfigState.WorldBossRequiredInterval, + PreviousState = state, + RandomSeed = randomSeed, + Signer = _agentAddress, + }); + + Assert.True(nextState.TryGetLegacyState(raiderAddress, out List rawRaider)); + var nextRaiderState = new RaiderState(rawRaider); + Assert.Equal(simulator.DamageDealt, nextRaiderState.HighScore); + + foreach (var reward in killRewards) + { + if (!rewardMap.ContainsKey(reward.Currency)) + { + rewardMap[reward.Currency] = reward; + } + else + { + rewardMap[reward.Currency] += reward; + } + } + + foreach (var reward in rewardMap) + { + if (reward.Key.Equals(CrystalCalculator.CRYSTAL)) + { + Assert.Equal(reward.Value, nextState.GetBalance(_agentAddress, reward.Key)); + } + else + { + Assert.Equal(reward.Value, nextState.GetBalance(_avatarAddress, reward.Key)); + } + } + + Assert.Equal(1, nextRaiderState.Level); + Assert.Equal(GameConfig.DefaultAvatarArmorId, nextRaiderState.IconId); + Assert.True(nextRaiderState.Cp > 0); + Assert.Equal(3, nextRaiderState.LatestBossLevel); + Assert.True(nextState.TryGetLegacyState(bossAddress, out List rawBoss)); + var nextBossState = new WorldBossState(rawBoss); + Assert.Equal(3, nextBossState.Level); + Assert.True(nextState.TryGetLegacyState(worldBossKillRewardRecordAddress, out List rawRewardInfo)); + var nextRewardInfo = new WorldBossKillRewardRecord(rawRewardInfo); + Assert.True(nextRewardInfo[1]); + } + + [Fact] + public void Execute_With_Free_Crystal_Fee() + { + var action = new Raid + { + AvatarAddress = _avatarAddress, + EquipmentIds = new List(), + CostumeIds = new List(), + FoodIds = new List(), + RuneInfos = new List(), + PayNcg = false, + }; + Currency crystal = CrystalCalculator.CRYSTAL; + + _sheets[nameof(WorldBossListSheet)] = + "id,boss_id,started_block_index,ended_block_index,fee,ticket_price,additional_ticket_price,max_purchase_count\r\n" + + "1,900002,0,100,0,1,1,40"; + + var goldCurrencyState = new GoldCurrencyState(_goldCurrency); + IWorld state = new World(new MockWorldState()) + .SetLegacyState(goldCurrencyState.address, goldCurrencyState.Serialize()) + .SetAgentState(_agentAddress, new AgentState(_agentAddress)); + + foreach (var (key, value) in _sheets) + { + state = state.SetLegacyState(Addresses.TableSheet.Derive(key), value.Serialize()); + } + + var gameConfigState = new GameConfigState(_sheets[nameof(GameConfigSheet)]); + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _tableSheets.GetAvatarSheets(), + gameConfigState, + default + ); + + for (int i = 0; i < 50; i++) + { + avatarState.worldInformation.ClearStage(1, i + 1, 0, _tableSheets.WorldSheet, _tableSheets.WorldUnlockSheet); + } + + state = state + .SetAvatarState(_avatarAddress, avatarState) + .SetLegacyState(gameConfigState.address, gameConfigState.Serialize()); + + var blockIndex = gameConfigState.WorldBossRequiredInterval; + var randomSeed = 0; + var ctx = new ActionContext + { + BlockIndex = blockIndex, + PreviousState = state, + RandomSeed = randomSeed, + Signer = _agentAddress, + }; + action.Execute(ctx); + } + } +} diff --git a/.Lib9c.Tests/Action/RapidCombinationTest.cs b/.Lib9c.Tests/Action/RapidCombinationTest.cs new file mode 100644 index 0000000000..fa6976dbcf --- /dev/null +++ b/.Lib9c.Tests/Action/RapidCombinationTest.cs @@ -0,0 +1,615 @@ +namespace Lib9c.Tests.Action +{ + using System; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Globalization; + using System.Linq; + using Bencodex.Types; + using Lib9c.Tests.Fixtures.TableCSV; + using Lib9c.Tests.Fixtures.TableCSV.Item; + using Lib9c.Tests.Util; + using Libplanet.Action; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Nekoyume; + using Nekoyume.Action; + using Nekoyume.Helper; + using Nekoyume.Model; + using Nekoyume.Model.Item; + using Nekoyume.Model.Mail; + using Nekoyume.Model.State; + using Nekoyume.Module; + using Nekoyume.TableData; + using Xunit; + using static Lib9c.SerializeKeys; + + public class RapidCombinationTest + { + private readonly IWorld _initialState; + + private readonly TableSheets _tableSheets; + + private readonly Address _agentAddress; + private readonly Address _avatarAddress; + + public RapidCombinationTest() + { + _initialState = new World(new MockWorldState()); + Dictionary sheets; + (_initialState, sheets) = InitializeUtil.InitializeTableSheets( + _initialState, + sheetsOverride: new Dictionary + { + { + "EquipmentItemRecipeSheet", + EquipmentItemRecipeSheetFixtures.Default + }, + { + "EquipmentItemSubRecipeSheet", + EquipmentItemSubRecipeSheetFixtures.V1 + }, + { + "GameConfigSheet", + GameConfigSheetFixtures.Default + }, + }); + _tableSheets = new TableSheets(sheets); + foreach (var (key, value) in sheets) + { + _initialState = + _initialState.SetLegacyState(Addresses.TableSheet.Derive(key), value.Serialize()); + } + + _agentAddress = new PrivateKey().Address; + var agentState = new AgentState(_agentAddress); + + _avatarAddress = new PrivateKey().Address; + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _tableSheets.GetAvatarSheets(), + new GameConfigState(), + default + ); + + agentState.avatarAddresses[0] = _avatarAddress; + + _initialState = _initialState + .SetLegacyState(Addresses.GameConfig, new GameConfigState(sheets[nameof(GameConfigSheet)]).Serialize()) + .SetAgentState(_agentAddress, agentState) + .SetAvatarState(_avatarAddress, avatarState); + } + + [Fact] + public void Execute() + { + const int slotStateUnlockStage = 1; + + var avatarState = _initialState.GetAvatarState(_avatarAddress); + avatarState.worldInformation = new WorldInformation( + 0, + _initialState.GetSheet(), + slotStateUnlockStage); + + var row = _tableSheets.MaterialItemSheet.Values.First(r => + r.ItemSubType == ItemSubType.Hourglass); + avatarState.inventory.AddItem(ItemFactory.CreateMaterial(row), 83); + avatarState.inventory.AddItem(ItemFactory.CreateTradableMaterial(row), 100); + Assert.True(avatarState.inventory.HasFungibleItem(row.ItemId, 0, 183)); + + var firstEquipmentRow = _tableSheets.EquipmentItemSheet.First; + Assert.NotNull(firstEquipmentRow); + + var gameConfigState = _initialState.GetGameConfigState(); + var requiredBlockIndex = gameConfigState.HourglassPerBlock * 200; + var equipment = (Equipment)ItemFactory.CreateItemUsable( + firstEquipmentRow, + Guid.NewGuid(), + requiredBlockIndex); + avatarState.inventory.AddItem(equipment); + + var result = new CombinationConsumable5.ResultModel + { + actionPoint = 0, + gold = 0, + materials = new Dictionary(), + itemUsable = equipment, + recipeId = 0, + itemType = ItemType.Equipment, + }; + + var mail = new CombinationMail(result, 0, default, requiredBlockIndex); + result.id = mail.id; + avatarState.Update2(mail); + + var slotAddress = _avatarAddress.Derive(string.Format( + CultureInfo.InvariantCulture, + CombinationSlotState.DeriveFormat, + 0)); + var slotState = new CombinationSlotState(slotAddress, slotStateUnlockStage); + slotState.Update(result, 0, requiredBlockIndex); + + var tempState = _initialState + .SetLegacyState(slotAddress, slotState.Serialize()) + .SetAvatarState(_avatarAddress, avatarState); + + var action = new RapidCombination + { + avatarAddress = _avatarAddress, + slotIndex = 0, + }; + + var nextState = action.Execute(new ActionContext + { + PreviousState = tempState, + Signer = _agentAddress, + BlockIndex = 51, + }); + + var nextAvatarState = nextState.GetAvatarState(_avatarAddress); + var item = nextAvatarState.inventory.Equipments.First(); + + Assert.Empty(nextAvatarState.inventory.Materials.Select(r => r.ItemSubType == ItemSubType.Hourglass)); + Assert.Equal(equipment.ItemId, item.ItemId); + Assert.Equal(51, item.RequiredBlockIndex); + } + + [Fact] + public void Execute_Throw_CombinationSlotResultNullException() + { + var slotAddress = _avatarAddress.Derive(string.Format( + CultureInfo.InvariantCulture, + CombinationSlotState.DeriveFormat, + 0)); + var slotState = new CombinationSlotState(slotAddress, 0); + slotState.Update(null, 0, 0); + + var tempState = _initialState + .SetLegacyState(slotAddress, slotState.Serialize()); + + var action = new RapidCombination + { + avatarAddress = _avatarAddress, + slotIndex = 0, + }; + + Assert.Throws(() => action.Execute(new ActionContext + { + PreviousState = tempState, + Signer = _agentAddress, + BlockIndex = 1, + })); + } + + [Theory] + [InlineData(0, 0)] + [InlineData(10, 100)] + public void Execute_Throw_RequiredBlockIndexException(int itemRequiredBlockIndex, int contextBlockIndex) + { + const int avatarClearedStage = 1; + + var avatarState = _initialState.GetAvatarState(_avatarAddress); + avatarState.worldInformation = new WorldInformation( + 0, + _initialState.GetSheet(), + avatarClearedStage); + + var firstEquipmentRow = _tableSheets.EquipmentItemSheet.First; + Assert.NotNull(firstEquipmentRow); + + var equipment = (Equipment)ItemFactory.CreateItemUsable( + firstEquipmentRow, + Guid.NewGuid(), + itemRequiredBlockIndex); + + var result = new CombinationConsumable5.ResultModel + { + actionPoint = 0, + gold = 0, + materials = new Dictionary(), + itemUsable = equipment, + recipeId = 0, + itemType = ItemType.Equipment, + }; + + var slotAddress = _avatarAddress.Derive(string.Format( + CultureInfo.InvariantCulture, + CombinationSlotState.DeriveFormat, + 0)); + var slotState = new CombinationSlotState(slotAddress, avatarClearedStage); + slotState.Update(result, 0, 0); + + var tempState = _initialState + .SetAvatarState(_avatarAddress, avatarState) + .SetLegacyState(slotAddress, slotState.Serialize()); + + var action = new RapidCombination + { + avatarAddress = _avatarAddress, + slotIndex = 0, + }; + + Assert.Throws(() => action.Execute(new ActionContext + { + PreviousState = tempState, + Signer = _agentAddress, + BlockIndex = contextBlockIndex, + })); + } + + [Theory] + [InlineData(0, 0, 0, 40)] + [InlineData(0, 1, 2, 40)] + [InlineData(22, 0, 0, 40)] + [InlineData(0, 22, 0, 40)] + [InlineData(0, 22, 2, 40)] + [InlineData(2, 10, 2, 40)] + public void Execute_Throw_NotEnoughMaterialException(int materialCount, int tradableCount, long blockIndex, int requiredCount) + { + const int slotStateUnlockStage = 1; + + var avatarState = _initialState.GetAvatarState(_avatarAddress); + avatarState.worldInformation = new WorldInformation( + 0, + _initialState.GetSheet(), + slotStateUnlockStage); + + var row = _tableSheets.MaterialItemSheet.Values.First(r => r.ItemSubType == ItemSubType.Hourglass); + avatarState.inventory.AddItem(ItemFactory.CreateMaterial(row), count: materialCount); + if (tradableCount > 0) + { + var material = ItemFactory.CreateTradableMaterial(row); + material.RequiredBlockIndex = blockIndex; + avatarState.inventory.AddItem(material, count: tradableCount); + } + + var firstEquipmentRow = _tableSheets.EquipmentItemSheet.First; + Assert.NotNull(firstEquipmentRow); + + var gameConfigState = _initialState.GetGameConfigState(); + var requiredBlockIndex = gameConfigState.HourglassPerBlock * requiredCount; + var equipment = (Equipment)ItemFactory.CreateItemUsable( + firstEquipmentRow, + Guid.NewGuid(), + requiredBlockIndex); + avatarState.inventory.AddItem(equipment); + + var result = new CombinationConsumable5.ResultModel + { + actionPoint = 0, + gold = 0, + materials = new Dictionary(), + itemUsable = equipment, + recipeId = 0, + itemType = ItemType.Equipment, + }; + + var mail = new CombinationMail(result, 0, default, requiredBlockIndex); + result.id = mail.id; + avatarState.Update2(mail); + + var slotAddress = _avatarAddress.Derive(string.Format( + CultureInfo.InvariantCulture, + CombinationSlotState.DeriveFormat, + 0)); + var slotState = new CombinationSlotState(slotAddress, slotStateUnlockStage); + slotState.Update(result, 0, 0); + + var tempState = _initialState + .SetAvatarState(_avatarAddress, avatarState) + .SetLegacyState(slotAddress, slotState.Serialize()); + + var action = new RapidCombination + { + avatarAddress = _avatarAddress, + slotIndex = 0, + }; + + Assert.Throws(() => action.Execute(new ActionContext + { + PreviousState = tempState, + Signer = _agentAddress, + BlockIndex = 51, + })); + } + + [Theory] + [InlineData(null)] + [InlineData(1)] + public void ResultModelDeterministic(int? subRecipeId) + { + var row = _tableSheets.MaterialItemSheet.Values.First(); + var row2 = _tableSheets.MaterialItemSheet.Values.Last(); + + Assert.True(row.Id < row2.Id); + + var material = ItemFactory.CreateMaterial(row); + var material2 = ItemFactory.CreateMaterial(row2); + + var itemUsable = ItemFactory.CreateItemUsable(_tableSheets.EquipmentItemSheet.Values.First(), default, 0); + var r = new CombinationConsumable5.ResultModel + { + id = default, + gold = 0, + actionPoint = 0, + recipeId = 1, + subRecipeId = subRecipeId, + materials = new Dictionary + { + [material] = 1, + [material2] = 1, + }, + itemUsable = itemUsable, + }; + var result = new RapidCombination0.ResultModel((Dictionary)r.Serialize()) + { + cost = new Dictionary + { + [material] = 1, + [material2] = 1, + }, + }; + + var r2 = new CombinationConsumable5.ResultModel + { + id = default, + gold = 0, + actionPoint = 0, + recipeId = 1, + subRecipeId = subRecipeId, + materials = new Dictionary + { + [material2] = 1, + [material] = 1, + }, + itemUsable = itemUsable, + }; + + var result2 = new RapidCombination0.ResultModel((Dictionary)r2.Serialize()) + { + cost = new Dictionary + { + [material2] = 1, + [material] = 1, + }, + }; + + Assert.Equal(result.Serialize(), result2.Serialize()); + } + + [Fact] + public void Execute_Throw_RequiredAppraiseBlockException() + { + const int slotStateUnlockStage = 1; + + var avatarState = _initialState.GetAvatarState(_avatarAddress); + avatarState.worldInformation = new WorldInformation( + 0, + _initialState.GetSheet(), + slotStateUnlockStage); + + var row = _tableSheets.MaterialItemSheet.Values.First(r => + r.ItemSubType == ItemSubType.Hourglass); + avatarState.inventory.AddItem(ItemFactory.CreateMaterial(row), count: 22); + + var firstEquipmentRow = _tableSheets.EquipmentItemSheet.First; + Assert.NotNull(firstEquipmentRow); + + var gameConfigState = _initialState.GetGameConfigState(); + var requiredBlockIndex = gameConfigState.HourglassPerBlock * 40; + var equipment = (Equipment)ItemFactory.CreateItemUsable( + firstEquipmentRow, + Guid.NewGuid(), + requiredBlockIndex); + avatarState.inventory.AddItem(equipment); + + var result = new CombinationConsumable5.ResultModel + { + actionPoint = 0, + gold = 0, + materials = new Dictionary(), + itemUsable = equipment, + recipeId = 0, + itemType = ItemType.Equipment, + }; + + var mail = new CombinationMail(result, 0, default, requiredBlockIndex); + result.id = mail.id; + avatarState.Update(mail); + + var slotAddress = _avatarAddress.Derive(string.Format( + CultureInfo.InvariantCulture, + CombinationSlotState.DeriveFormat, + 0)); + var slotState = new CombinationSlotState(slotAddress, slotStateUnlockStage); + slotState.Update(result, 0, 0); + + var tempState = _initialState + .SetAvatarState(_avatarAddress, avatarState) + .SetLegacyState(slotAddress, slotState.Serialize()); + + var action = new RapidCombination + { + avatarAddress = _avatarAddress, + slotIndex = 0, + }; + + Assert.Throws(() => action.Execute(new ActionContext + { + PreviousState = tempState, + Signer = _agentAddress, + BlockIndex = 1, + })); + } + + [Theory] + [InlineData(7)] + [InlineData(9)] + [InlineData(10)] + [InlineData(11)] + public void Execute_NotThrow_InvalidOperationException_When_TargetSlotCreatedBy( + int itemEnhancementResultModelNumber) + { + const int slotStateUnlockStage = 1; + + var avatarState = _initialState.GetAvatarState(_avatarAddress); + avatarState.worldInformation = new WorldInformation( + 0, + _initialState.GetSheet(), + slotStateUnlockStage); + + var row = _tableSheets.MaterialItemSheet.Values.First(r => + r.ItemSubType == ItemSubType.Hourglass); + avatarState.inventory.AddItem(ItemFactory.CreateMaterial(row), 83); + avatarState.inventory.AddItem(ItemFactory.CreateTradableMaterial(row), 100); + Assert.True(avatarState.inventory.HasFungibleItem(row.ItemId, 0, 183)); + + var firstEquipmentRow = _tableSheets.EquipmentItemSheet + .OrderedList.First(e => e.Grade >= 1); + Assert.NotNull(firstEquipmentRow); + + var gameConfigState = _initialState.GetGameConfigState(); + var requiredBlockIndex = gameConfigState.HourglassPerBlock * 200; + var equipment = (Equipment)ItemFactory.CreateItemUsable( + firstEquipmentRow, + Guid.NewGuid(), + requiredBlockIndex); + var materialEquipment = (Equipment)ItemFactory.CreateItemUsable( + firstEquipmentRow, + Guid.NewGuid(), + requiredBlockIndex); + avatarState.inventory.AddItem(equipment); + avatarState.inventory.AddItem(materialEquipment); + + AttachmentActionResult resultModel = null; + var random = new TestRandom(); + var mailId = random.GenerateRandomGuid(); + var preItemUsable = new Equipment((Dictionary)equipment.Serialize()); + switch (itemEnhancementResultModelNumber) + { + case 7: + { + equipment = ItemEnhancement7.UpgradeEquipment(equipment); + resultModel = new ItemEnhancement7.ResultModel + { + id = mailId, + itemUsable = equipment, + materialItemIdList = new[] { materialEquipment.NonFungibleId }, + }; + + break; + } + + case 9: + { + Assert.True(ItemEnhancement9.TryGetRow( + equipment, + _tableSheets.EnhancementCostSheetV2, + out var costRow)); + var equipmentResult = ItemEnhancement9.GetEnhancementResult(costRow, random); + equipment.LevelUp( + random, + costRow, + equipmentResult == ItemEnhancement9.EnhancementResult.GreatSuccess); + resultModel = new ItemEnhancement9.ResultModel + { + id = mailId, + preItemUsable = preItemUsable, + itemUsable = equipment, + materialItemIdList = new[] { materialEquipment.NonFungibleId }, + gold = 0, + actionPoint = 0, + enhancementResult = ItemEnhancement9.EnhancementResult.GreatSuccess, + }; + + break; + } + + case 10: + { + Assert.True(ItemEnhancement10.TryGetRow( + equipment, + _tableSheets.EnhancementCostSheetV2, + out var costRow)); + var equipmentResult = ItemEnhancement10.GetEnhancementResult(costRow, random); + equipment.LevelUp( + random, + costRow, + equipmentResult == ItemEnhancement10.EnhancementResult.GreatSuccess); + resultModel = new ItemEnhancement10.ResultModel + { + id = mailId, + preItemUsable = preItemUsable, + itemUsable = equipment, + materialItemIdList = new[] { materialEquipment.NonFungibleId }, + gold = 0, + actionPoint = 0, + enhancementResult = ItemEnhancement10.EnhancementResult.GreatSuccess, + }; + + break; + } + + case 11: + { + Assert.True(ItemEnhancement11.TryGetRow( + equipment, + _tableSheets.EnhancementCostSheetV2, + out var costRow)); + var equipmentResult = ItemEnhancement11.GetEnhancementResult(costRow, random); + equipment.LevelUp( + random, + costRow, + equipmentResult == ItemEnhancement11.EnhancementResult.GreatSuccess); + resultModel = new ItemEnhancement11.ResultModel + { + id = mailId, + preItemUsable = preItemUsable, + itemUsable = equipment, + materialItemIdList = new[] { materialEquipment.NonFungibleId }, + gold = 0, + actionPoint = 0, + enhancementResult = ItemEnhancement11.EnhancementResult.GreatSuccess, + CRYSTAL = 0 * CrystalCalculator.CRYSTAL, + }; + + break; + } + + default: + break; + } + + // NOTE: Do not update `mail`, because this test assumes that the `mail` was removed. + { + // var mail = new ItemEnhanceMail(resultModel, 0, random.GenerateRandomGuid(), requiredBlockIndex); + // avatarState.Update(mail); + } + + var slotAddress = _avatarAddress.Derive(string.Format( + CultureInfo.InvariantCulture, + CombinationSlotState.DeriveFormat, + 0)); + var slotState = new CombinationSlotState(slotAddress, slotStateUnlockStage); + slotState.Update(resultModel, 0, requiredBlockIndex); + + var tempState = _initialState.SetLegacyState(slotAddress, slotState.Serialize()) + .SetAvatarState(_avatarAddress, avatarState); + + var action = new RapidCombination + { + avatarAddress = _avatarAddress, + slotIndex = 0, + }; + + action.Execute(new ActionContext + { + PreviousState = tempState, + Signer = _agentAddress, + BlockIndex = 51, + }); + } + } +} diff --git a/.Lib9c.Tests/Model/Skill/Arena/ArenaCombatTest.cs b/.Lib9c.Tests/Model/Skill/Arena/ArenaCombatTest.cs index 97307544db..1cee8a57dd 100644 --- a/.Lib9c.Tests/Model/Skill/Arena/ArenaCombatTest.cs +++ b/.Lib9c.Tests/Model/Skill/Arena/ArenaCombatTest.cs @@ -6,6 +6,7 @@ namespace Lib9c.Tests.Model.Skill.Arena using Nekoyume.Arena; using Nekoyume.Model; using Nekoyume.Model.Buff; + using Nekoyume.Model.Skill; using Nekoyume.Model.Skill.Arena; using Nekoyume.Model.Stat; using Nekoyume.Model.State; @@ -13,8 +14,6 @@ namespace Lib9c.Tests.Model.Skill.Arena public class ArenaCombatTest { - private const int ActionBuffId = 708000; // Dispel with duration - private readonly TableSheets _tableSheets; private readonly AvatarState _avatar1; private readonly AvatarState _avatar2; @@ -48,7 +47,7 @@ public ArenaCombatTest() [Theory] [InlineData(700009, new[] { 600001 })] - [InlineData(700010, new[] { 600001, 704000 })] + [InlineData(700009, new[] { 600001, 704000 })] public void DispelOnUse(int dispelId, int[] debuffIdList) { var arenaSheets = _tableSheets.GetArenaSimulatorSheets(); @@ -122,7 +121,7 @@ public void DispelOnDuration_Block() ); // Use Dispel first - var dispel = _tableSheets.ActionBuffSheet.Values.First(bf => bf.Id == ActionBuffId); + var dispel = _tableSheets.ActionBuffSheet.Values.First(bf => bf.ActionBuffType == ActionBuffType.Dispel); challenger.AddBuff(BuffFactory.GetActionBuff(challenger.Stats, dispel)); Assert.Single(challenger.Buffs); @@ -170,7 +169,7 @@ public void DispelOnDuration_Affect() ); // Use Dispel first - var dispel = _tableSheets.ActionBuffSheet.Values.First(bf => bf.Id == ActionBuffId); + var dispel = _tableSheets.ActionBuffSheet.Values.First(bf => bf.ActionBuffType == ActionBuffType.Dispel); challenger.AddBuff(BuffFactory.GetActionBuff(challenger.Stats, dispel)); Assert.Single(challenger.Buffs); diff --git a/.Lib9c.Tests/Model/Skill/Arena/ArenaShatterStrikeTest.cs b/.Lib9c.Tests/Model/Skill/Arena/ArenaShatterStrikeTest.cs index 4aaec7502b..c616f3e393 100644 --- a/.Lib9c.Tests/Model/Skill/Arena/ArenaShatterStrikeTest.cs +++ b/.Lib9c.Tests/Model/Skill/Arena/ArenaShatterStrikeTest.cs @@ -73,7 +73,7 @@ public void Use(int ratioBp) new List() ); - var skillRow = _tableSheets.SkillSheet.OrderedList.First(s => s.Id == 700011); + var skillRow = _tableSheets.SkillSheet.OrderedList.First(s => s.Id == 700010); var shatterStrike = new ArenaShatterStrike(skillRow, 0, 0, ratioBp, StatType.NONE); var used = shatterStrike.Use(challenger, enemy, simulator.Turn, new List()); Assert.Single(used.SkillInfos); diff --git a/.Lib9c.Tests/Model/Skill/CombatTest.cs b/.Lib9c.Tests/Model/Skill/CombatTest.cs index 652f03b4b2..b01d7fce99 100644 --- a/.Lib9c.Tests/Model/Skill/CombatTest.cs +++ b/.Lib9c.Tests/Model/Skill/CombatTest.cs @@ -139,7 +139,7 @@ public void Bleed() [Theory] [InlineData(700009, new[] { 600001 })] - [InlineData(700010, new[] { 600001, 704000 })] + [InlineData(700009, new[] { 600001, 704000 })] public void DispelOnUse(int dispelId, int[] debuffIdList) { var actionBuffSheet = _tableSheets.ActionBuffSheet; @@ -182,11 +182,10 @@ public void DispelOnUse(int dispelId, int[] debuffIdList) [Fact] public void DispelOnDuration_Block() { - const int actionBuffId = 708000; // Dispel with duration var actionBuffSheet = _tableSheets.ActionBuffSheet; // Use Dispel first - var dispel = actionBuffSheet.Values.First(bf => bf.Id == actionBuffId); + var dispel = actionBuffSheet.Values.First(bf => bf.ActionBuffType == ActionBuffType.Dispel); _player.AddBuff(BuffFactory.GetActionBuff(_player.Stats, dispel)); Assert.Single(_player.Buffs); @@ -217,11 +216,10 @@ public void DispelOnDuration_Block() [Fact] public void DispelOnDuration_Affect() { - const int actionBuffId = 708000; // Dispel with duration var actionBuffSheet = _tableSheets.ActionBuffSheet; // Use Dispel first - var dispel = actionBuffSheet.Values.First(bf => bf.Id == actionBuffId); + var dispel = actionBuffSheet.Values.First(bf => bf.ActionBuffType == ActionBuffType.Dispel); _player.AddBuff(BuffFactory.GetActionBuff(_player.Stats, dispel)); Assert.Single(_player.Buffs); diff --git a/.Lib9c.Tests/Model/Skill/ShatterStrikeTest.cs b/.Lib9c.Tests/Model/Skill/ShatterStrikeTest.cs index 190ce2540c..142a08efbb 100644 --- a/.Lib9c.Tests/Model/Skill/ShatterStrikeTest.cs +++ b/.Lib9c.Tests/Model/Skill/ShatterStrikeTest.cs @@ -30,7 +30,7 @@ public class ShatterStrikeTest public void Use(int ratioBp, bool copyCharacter) { Assert.True( - _tableSheets.SkillSheet.TryGetValue(700011, out var skillRow) + _tableSheets.SkillSheet.TryGetValue(700010, out var skillRow) ); // 700011 is ShatterStrike var shatterStrike = new ShatterStrike(skillRow, 0, 0, ratioBp, StatType.NONE);