diff --git a/backend/app/Savor22b.Tests/Action/CreateFoodActionTests.cs b/backend/app/Savor22b.Tests/Action/CreateFoodActionTests.cs index 42da1cc0..5065cb6f 100644 --- a/backend/app/Savor22b.Tests/Action/CreateFoodActionTests.cs +++ b/backend/app/Savor22b.Tests/Action/CreateFoodActionTests.cs @@ -3,6 +3,7 @@ namespace Savor22b.Tests.Action; using System; using System.Collections.Immutable; using Libplanet.State; +using Savor22b.Util; using Savor22b.Action; using Savor22b.Action.Exceptions; using Savor22b.Model; @@ -64,6 +65,23 @@ var equipmentCategoryId in recipeCandidate.RequiredKitchenEquipmentCategoryList return recipe!; } + private KitchenEquipment getRandomNotRequiredKitchenEquipment(Recipe recipe) + { + foreach (var kitchenEquipment in CsvDataHelper.GetKitchenEquipmentCSVData()) + { + if ( + !recipe.RequiredKitchenEquipmentCategoryList.Contains( + kitchenEquipment.KitchenEquipmentCategoryID + ) + ) + { + return kitchenEquipment; + } + } + + throw new Exception(""); + } + private List generateMaterials( ImmutableList IngredientIDList, ImmutableList FoodIDList @@ -86,7 +104,10 @@ ImmutableList FoodIDList return RefrigeratorItemList; } - private (RootState, List, List) createPreset(Recipe recipe) + private (RootState, List, List) createPreset( + Recipe recipe, + bool hasBlockReduction = false + ) { int spaceNumber = 1; List kitchenEquipmentsToUse = new List(); @@ -113,9 +134,28 @@ ImmutableList FoodIDList var kitchenEquipments = CsvDataHelper.GetAllKitchenEquipmentByCategoryId( equipmentCategoryId ); + KitchenEquipment targetKitchenEquipment = null; + + if (hasBlockReduction) + { + var higherKitchenEquipment = kitchenEquipments.Find( + k => k.BlockTimeReductionPercent > 1 + ); + if (higherKitchenEquipment == null) + { + throw new Exception(""); + } + + targetKitchenEquipment = higherKitchenEquipment; + } + else + { + targetKitchenEquipment = kitchenEquipments[0]; + } + var kitchenEquipmentState = new KitchenEquipmentState( Guid.NewGuid(), - kitchenEquipments[0].ID, + targetKitchenEquipment.ID, equipmentCategoryId ); inventoryState = inventoryState.AddKitchenEquipmentItem(kitchenEquipmentState); @@ -239,7 +279,7 @@ select stateList.StateID foreach (var spaceNumber in spaceNumbersToUse) { Assert.NotNull( - beforeRootState.VillageState!.HouseState.KitchenState.GetApplianceSpaceStateByNumber( + rootState.VillageState!.HouseState.KitchenState.GetApplianceSpaceStateByNumber( spaceNumber ) ); @@ -264,6 +304,92 @@ select stateList.StateID } } + [Fact] + public void Execute_Success_WithBlockReduction() + { + var recipe = getRandomRecipeWithEquipmentCategory("main"); + var blockIndex = 1; + + IAccountStateDelta beforeState = new DummyState(); + var (beforeRootState, kitchenEquipmentStateIdsToUse, spaceNumbersToUse) = createPreset( + recipe, + true + ); + var edibleStateIdsToUse = ( + from stateList in beforeRootState.InventoryState.RefrigeratorStateList + select stateList.StateID + ).ToList(); + + beforeState = beforeState.SetState(SignerAddress(), beforeRootState.Serialize()); + + var newFoodGuid = Guid.NewGuid(); + var action = new CreateFoodAction( + recipe.ID, + newFoodGuid, + edibleStateIdsToUse, + kitchenEquipmentStateIdsToUse, + spaceNumbersToUse + ); + + var afterState = action.Execute( + new DummyActionContext + { + PreviousStates = beforeState, + Signer = SignerAddress(), + Random = random, + Rehearsal = false, + BlockIndex = blockIndex, + } + ); + + var afterRootStateEncoded = afterState.GetState(SignerAddress()); + RootState afterRootState = afterRootStateEncoded is Bencodex.Types.Dictionary bdict + ? new RootState(bdict) + : throw new Exception(); + InventoryState afterInventoryState = afterRootState.InventoryState; + + var sumReductionPercent = 0; + foreach (var kitchenEquipmentState in afterInventoryState.KitchenEquipmentStateList) + { + var kitchenEquipment = CsvDataHelper.GetKitchenEquipmentByID( + kitchenEquipmentState.KitchenEquipmentID + ); + sumReductionPercent = sumReductionPercent + kitchenEquipment!.BlockTimeReductionPercent; + } + var avgReductionPercent = + sumReductionPercent / afterInventoryState.KitchenEquipmentStateList.Count; + var expectedDurationBlock = MathUtil.ReduceByPercentage( + recipe.RequiredBlock, + avgReductionPercent + ); + + Assert.Equal( + expectedDurationBlock, + afterInventoryState.GetRefrigeratorItem(newFoodGuid).AvailableBlockIndex - blockIndex + ); + + foreach (var spaceNumber in spaceNumbersToUse) + { + Assert.NotNull( + afterRootState.VillageState!.HouseState.KitchenState.GetApplianceSpaceStateByNumber( + spaceNumber + ) + ); + + Assert.True( + afterRootState.VillageState!.HouseState.KitchenState + .GetApplianceSpaceStateByNumber(spaceNumber) + .IsInUse(blockIndex) + ); + Assert.Equal( + expectedDurationBlock, + afterRootState.VillageState!.HouseState.KitchenState + .GetApplianceSpaceStateByNumber(spaceNumber) + .CookingDurationBlock + ); + } + } + [Fact] public void Execute_Failure_NotFoundKitchenEquipmentState() { @@ -404,6 +530,7 @@ public void Execute_Failure_NotHaveRequiredKitchenEquipmentState() { var blockIndex = 1; Recipe recipe = getRandomRecipeWithEquipmentCategory("sub"); + KitchenEquipment notRequiredKitchenEquipment = getRandomNotRequiredKitchenEquipment(recipe); IAccountStateDelta beforeState = new DummyState(); var (beforeRootState, kitchenEquipmentStateIdsToUse, spaceNumbersToUse) = createPreset( @@ -420,7 +547,11 @@ select stateList.StateID ); beforeRootState.SetInventoryState( beforeRootState.InventoryState.AddKitchenEquipmentItem( - new KitchenEquipmentState(kitchenEquipmentStateIdsToUse[0], -1, -1) + new KitchenEquipmentState( + kitchenEquipmentStateIdsToUse[0], + notRequiredKitchenEquipment.ID, + notRequiredKitchenEquipment.KitchenEquipmentCategoryID + ) ) ); beforeState = beforeState.SetState(SignerAddress(), beforeRootState.Serialize()); @@ -454,6 +585,7 @@ public void Execute_Failure_NotHaveRequiredInstalledKitchenEquipmentState() { var blockIndex = 1; Recipe recipe = getRandomRecipeWithEquipmentCategory("main"); + KitchenEquipment notRequiredKitchenEquipment = getRandomNotRequiredKitchenEquipment(recipe); IAccountStateDelta beforeState = new DummyState(); var (beforeRootState, kitchenEquipmentStateIdsToUse, spaceNumbersToUse) = createPreset( @@ -461,7 +593,11 @@ public void Execute_Failure_NotHaveRequiredInstalledKitchenEquipmentState() ); foreach (var spaceNumber in spaceNumbersToUse) { - var illusionKitchenEquipment = new KitchenEquipmentState(Guid.NewGuid(), -1, -1); + var illusionKitchenEquipment = new KitchenEquipmentState( + Guid.NewGuid(), + notRequiredKitchenEquipment.ID, + notRequiredKitchenEquipment.KitchenEquipmentCategoryID + ); beforeRootState.SetInventoryState( beforeRootState.InventoryState.AddKitchenEquipmentItem(illusionKitchenEquipment) ); diff --git a/backend/app/Savor22b/Action/CreateFoodAction.cs b/backend/app/Savor22b/Action/CreateFoodAction.cs index 14b33ebb..e883f063 100644 --- a/backend/app/Savor22b/Action/CreateFoodAction.cs +++ b/backend/app/Savor22b/Action/CreateFoodAction.cs @@ -7,6 +7,7 @@ namespace Savor22b.Action; using Libplanet.Headless.Extensions; using Libplanet.State; using Savor22b.Constants; +using Savor22b.Util; using Savor22b.Action.Exceptions; using Savor22b.Action.Util; using Savor22b.Helpers; @@ -130,7 +131,8 @@ private InventoryState CheckAndChangeEquipmentsStatus( Recipe recipe, InventoryState state, List kitchenEquipmentStateIdsToUse, - long currentBlockIndex + long currentBlockIndex, + long durationBlock ) { ImmutableList kitchenCategoryIds = recipe.RequiredKitchenEquipmentCategoryList; @@ -165,7 +167,7 @@ select category.ID var statusChangedEquipment = kitchenEquipment.StartCooking( currentBlockIndex, - recipe.RequiredBlock + durationBlock ); state = state.RemoveKitchenEquipmentItem(kitchenEquipment.StateID); state = state.AddKitchenEquipmentItem(statusChangedEquipment); @@ -190,7 +192,8 @@ private HouseState CheckAndChangeApplianceSpacesStatus( HouseState houseState, InventoryState inventoryState, List spaceNumbers, - long currentBlockIndex + long currentBlockIndex, + long durationBlock ) { ImmutableList kitchenCategoryIds = recipe.RequiredKitchenEquipmentCategoryList; @@ -219,7 +222,7 @@ select category.ID if ( !recipe.RequiredKitchenEquipmentCategoryList.Contains( - kitchenEquipment.KitchenEquipmentCategoryID + kitchenEquipment!.KitchenEquipmentCategoryID ) ) { @@ -228,7 +231,7 @@ select category.ID ); } - space.StartCooking(currentBlockIndex, recipe.RequiredBlock); + space.StartCooking(currentBlockIndex, durationBlock); requiredKitchenCategoryIds = requiredKitchenCategoryIds.Remove( kitchenEquipment.KitchenEquipmentCategoryID @@ -293,7 +296,89 @@ private Recipe FindRecipeInCsv(int recipeID) return (HP: hp, ATK: attack, DEF: defense, SPD: speed); } - private RefrigeratorState GenerateFood(Recipe recipe, IRandom random, long currentBlockIndex) + private long calcDuractionBlock( + Recipe recipe, + InventoryState inventoryState, + KitchenState kitchenState, + List kitchenEquipmentStateIdsToUse, + List applianceSpaceNumbersToUse + ) + { + var sumReductionPercent = 0; + foreach (var stateId in kitchenEquipmentStateIdsToUse) + { + var kitchenEquipmentState = inventoryState.GetKitchenEquipmentState(stateId); + if (kitchenEquipmentState == null) + { + throw new NotFoundDataException($"You don't have `{stateId}` kitchen equipment"); + } + + var kitchenEquipment = CsvDataHelper.GetKitchenEquipmentByID( + kitchenEquipmentState.KitchenEquipmentID + ); + if (kitchenEquipment == null) + { + throw new NotFoundDataException( + $"NotFound `{kitchenEquipmentState.KitchenEquipmentID}` kitchen equipment in table" + ); + } + + sumReductionPercent = sumReductionPercent + kitchenEquipment!.BlockTimeReductionPercent; + } + + foreach (var spaceNumber in applianceSpaceNumbersToUse) + { + var space = kitchenState.GetApplianceSpaceStateByNumber(spaceNumber); + if (!space.EquipmentIsPresent()) + { + throw new NotFoundDataException($"{spaceNumber} is not installed anything"); + } + + var kitchenEquipmentState = inventoryState.GetKitchenEquipmentState( + space.InstalledKitchenEquipmentStateId!.Value + ); + if (kitchenEquipmentState == null) + { + throw new NotFoundDataException( + $"You don't have {space.InstalledKitchenEquipmentStateId!.Value} kitchen equipment" + ); + } + + var kitchenEquipment = CsvDataHelper.GetKitchenEquipmentByID( + kitchenEquipmentState.KitchenEquipmentID + ); + if (kitchenEquipment == null) + { + throw new NotFoundDataException( + $"NotFound `{kitchenEquipmentState.KitchenEquipmentID}` kitchen equipment in table" + ); + } + + sumReductionPercent = sumReductionPercent + kitchenEquipment!.BlockTimeReductionPercent; + } + + try + { + var avgReductionPercent = + sumReductionPercent / inventoryState.KitchenEquipmentStateList.Count; + var durationBlock = MathUtil.ReduceByPercentage( + recipe.RequiredBlock, + avgReductionPercent + ); + return durationBlock; + } + catch (DivideByZeroException) + { + return recipe.RequiredBlock; + } + } + + private RefrigeratorState GenerateFood( + Recipe recipe, + IRandom random, + long currentBlockIndex, + long durationBlock + ) { var gradeExtractor = new GradeExtractor(random, 0.1); @@ -315,7 +400,7 @@ private RefrigeratorState GenerateFood(Recipe recipe, IRandom random, long curre generatedStat.ATK, generatedStat.DEF, generatedStat.SPD, - currentBlockIndex + recipe.RequiredBlock + currentBlockIndex + durationBlock ); return food; @@ -340,23 +425,32 @@ public override IAccountStateDelta Execute(IActionContext ctx) HouseState houseState = rootState.VillageState!.HouseState; var recipe = FindRecipeInCsv(RecipeID); + var durationBlock = calcDuractionBlock( + recipe, + inventoryState, + houseState.KitchenState, + KitchenEquipmentStateIdsToUse, + ApplianceSpaceNumbersToUse + ); inventoryState = CheckAndChangeEquipmentsStatus( recipe, inventoryState, KitchenEquipmentStateIdsToUse, - ctx.BlockIndex + ctx.BlockIndex, + durationBlock ); houseState = CheckAndChangeApplianceSpacesStatus( recipe, houseState, inventoryState, ApplianceSpaceNumbersToUse, - ctx.BlockIndex + ctx.BlockIndex, + durationBlock ); inventoryState = CheckAndRemoveEdibles(recipe, inventoryState, RefrigeratorStateIdsToUse); - RefrigeratorState food = GenerateFood(recipe, ctx.Random, ctx.BlockIndex); + RefrigeratorState food = GenerateFood(recipe, ctx.Random, ctx.BlockIndex, durationBlock); inventoryState = inventoryState.AddRefrigeratorItem(food); rootState.SetInventoryState(inventoryState); diff --git a/backend/app/Savor22b/Action/Util/CalculatePrice.cs b/backend/app/Savor22b/Action/Util/CalculatePrice.cs index cbabad87..c5278b79 100644 --- a/backend/app/Savor22b/Action/Util/CalculatePrice.cs +++ b/backend/app/Savor22b/Action/Util/CalculatePrice.cs @@ -3,6 +3,7 @@ namespace Savor22b.Action.Util; using System.Globalization; using Libplanet.Assets; using Savor22b.Model; +using Savor22b.Util; public static class CalculatePrice { diff --git a/backend/app/Savor22b/Action/Util/MathUtil.cs b/backend/app/Savor22b/Action/Util/MathUtil.cs deleted file mode 100644 index 6cabafe4..00000000 --- a/backend/app/Savor22b/Action/Util/MathUtil.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Savor22b.Action.Util; - -public static class MathUtil -{ - public static double CalculateEuclideanDistance( - int originTargetX, - int originTargetY, - int targetX, - int targetY - ) - { - double distance = Math.Sqrt( - Math.Pow(targetX - originTargetX, 2) + Math.Pow(targetY - originTargetY, 2) - ); - - return distance; - } -} diff --git a/backend/app/Savor22b/Action/Util/VillageUtil.cs b/backend/app/Savor22b/Action/Util/VillageUtil.cs index aa773887..385c7670 100644 --- a/backend/app/Savor22b/Action/Util/VillageUtil.cs +++ b/backend/app/Savor22b/Action/Util/VillageUtil.cs @@ -3,6 +3,7 @@ namespace Savor22b.Action.Util; using Libplanet.Assets; using Savor22b.DataModel; using Savor22b.Model; +using Savor22b.Util; public static class VillageUtil { diff --git a/backend/app/Savor22b/Model/CookingEquipment.cs b/backend/app/Savor22b/Model/CookingEquipment.cs index 546cddea..c7b03ed0 100644 --- a/backend/app/Savor22b/Model/CookingEquipment.cs +++ b/backend/app/Savor22b/Model/CookingEquipment.cs @@ -1,4 +1,5 @@ namespace Savor22b.Model; + using Libplanet.Assets; public class KitchenEquipment @@ -6,7 +7,7 @@ public class KitchenEquipment public int ID { get; set; } public int KitchenEquipmentCategoryID { get; set; } public string Name { get; set; } - public double BlockTimeReductionPercent { get; set; } + public int BlockTimeReductionPercent { get; set; } public string Price { get; set; } public FungibleAssetValue PriceToFungibleAssetValue() diff --git a/backend/app/Savor22b/Util/MathUtil.cs b/backend/app/Savor22b/Util/MathUtil.cs new file mode 100644 index 00000000..ff875d4b --- /dev/null +++ b/backend/app/Savor22b/Util/MathUtil.cs @@ -0,0 +1,33 @@ +namespace Savor22b.Util; + +using System; + +public static class MathUtil +{ + public static long ReduceByPercentage(long originalValue, int reductionPercentage) + { + if (reductionPercentage < 0 || reductionPercentage > 100) + { + throw new ArgumentOutOfRangeException( + nameof(reductionPercentage), + "Reduction percentage must be between 0 and 100." + ); + } + + return originalValue * (100 - reductionPercentage) / 100; + } + + public static double CalculateEuclideanDistance( + int originTargetX, + int originTargetY, + int targetX, + int targetY + ) + { + double distance = Math.Sqrt( + Math.Pow(targetX - originTargetX, 2) + Math.Pow(targetY - originTargetY, 2) + ); + + return distance; + } +}