diff --git a/.Lib9c.Tests/Action/RuneEnhancementTest.cs b/.Lib9c.Tests/Action/RuneEnhancementTest.cs index ec108d4b75..5395b93701 100644 --- a/.Lib9c.Tests/Action/RuneEnhancementTest.cs +++ b/.Lib9c.Tests/Action/RuneEnhancementTest.cs @@ -26,10 +26,34 @@ public RuneEnhancementTest() } [Theory] - [InlineData(10000)] - [InlineData(1)] - public void Execute(int seed) + // All success + [InlineData(1, 1, 2, 0, 0, 1, null, 0)] + [InlineData(1, 2, 3, 0, 0, 2, null, 0)] + // Success 1 of 2 + [InlineData(202, 2, 203, 0, 1000, 40, null, 2)] + // All fail + [InlineData(202, 2, 202, 0, 1000, 40, null, 0)] + // Reaching max level + [InlineData(299, 1, 300, 0, 500, 20, null, 1)] + // Cannot exceed max level + [InlineData(299, 2, 299, 0, 0, 0, typeof(RuneCostDataNotFoundException), 0)] + [InlineData(300, 1, 300, 0, 0, 0, typeof(RuneCostDataNotFoundException), 0)] + public void Execute( + int startLevel, + int tryCount, + int expectedLevel, + int expectedNcgCost, + int expectedCrystalCost, + int expectedRuneCost, + Type expectedException, + int seed + ) { + const int initialNcg = 10_000; + const int initialCrystal = 1_000_000; + const int initialRune = 1_000; + var s = seed; + // Set states var agentAddress = new PrivateKey().Address; var avatarAddress = new PrivateKey().Address; var sheets = TableSheetsImporter.ImportSheets(); @@ -66,55 +90,22 @@ public void Execute(int seed) var runeId = runeListSheet.First().Value.Id; var runeStateAddress = RuneState.DeriveAddress(avatarState.address, runeId); var runeState = new RuneState(runeId); + runeState.LevelUp(startLevel); state = state.SetLegacyState(runeStateAddress, runeState.Serialize()); - var costSheet = state.GetSheet(); - if (!costSheet.TryGetValue(runeId, out var costRow)) - { - throw new RuneCostNotFoundException($"[{nameof(Execute)}] "); - } - - if (!costRow.TryGetCost(runeState.Level + 1, out var cost)) - { - throw new RuneCostDataNotFoundException($"[{nameof(Execute)}] "); - } - - var runeSheet = state.GetSheet(); - if (!runeSheet.TryGetValue(runeId, out var runeRow)) - { - throw new RuneNotFoundException($"[{nameof(Execute)}] "); - } - + // Prepare materials var ncgCurrency = state.GetGoldCurrency(); var crystalCurrency = CrystalCalculator.CRYSTAL; - var runeCurrency = Currency.Legacy(runeRow.Ticker, 0, minters: null); - - var ncgBal = cost.NcgQuantity * ncgCurrency * 10000; - var crystalBal = cost.CrystalQuantity * crystalCurrency * 10000; - var runeBal = cost.RuneStoneQuantity * runeCurrency * 10000; - - var rand = new TestRandom(seed); - if (!RuneHelper.TryEnhancement(ncgBal, crystalBal, runeBal, ncgCurrency, crystalCurrency, runeCurrency, cost, rand, 99, out var tryCount)) - { - throw new RuneNotFoundException($"[{nameof(Execute)}] "); - } - - if (ncgBal.Sign > 0) - { - state = state.MintAsset(context, agentAddress, ncgBal); - } + var runeTicker = tableSheets.RuneSheet.Values.First(r => r.Id == runeId).Ticker; + var runeCurrency = Currency.Legacy(runeTicker, 0, minters: null); + var r = new TestRandom(seed: 1); - if (crystalBal.Sign > 0) - { - state = state.MintAsset(context, agentAddress, crystalBal); - } + state = state.MintAsset(context, agentAddress, ncgCurrency * initialNcg); + state = state.MintAsset(context, agentAddress, crystalCurrency * initialCrystal); + state = state.MintAsset(context, avatarAddress, runeCurrency * initialRune); - if (runeBal.Sign > 0) - { - state = state.MintAsset(context, avatarState.address, runeBal); - } - - var action = new RuneEnhancement() + // Action + var action = new RuneEnhancement { AvatarAddress = avatarState.address, RuneId = runeId, @@ -124,62 +115,35 @@ public void Execute(int seed) { BlockIndex = blockIndex, PreviousState = state, - RandomSeed = rand.Seed, + RandomSeed = seed, Signer = agentAddress, }; - var nextState = action.Execute(ctx); - if (!nextState.TryGetLegacyState(runeStateAddress, out List nextRuneRawState)) - { - throw new Exception(); - } - - var nextRunState = new RuneState(nextRuneRawState); - var nextNcgBal = nextState.GetBalance(agentAddress, ncgCurrency); - var nextCrystalBal = nextState.GetBalance(agentAddress, crystalCurrency); - var nextRuneBal = nextState.GetBalance(agentAddress, runeCurrency); - - if (cost.NcgQuantity != 0) - { - Assert.NotEqual(ncgBal, nextNcgBal); - } - - if (cost.CrystalQuantity != 0) + if (expectedException is not null) { - Assert.NotEqual(crystalBal, nextCrystalBal); + Assert.Throws(expectedException, () => { action.Execute(ctx); }); } - - if (cost.RuneStoneQuantity != 0) + else { - Assert.NotEqual(runeBal, nextRuneBal); - } - - var costNcg = tryCount * cost.NcgQuantity * ncgCurrency; - var costCrystal = tryCount * cost.CrystalQuantity * crystalCurrency; - var costRune = tryCount * cost.RuneStoneQuantity * runeCurrency; - - if (costNcg.Sign > 0) - { - nextState = nextState.MintAsset(context, agentAddress, costNcg); - } - - if (costCrystal.Sign > 0) - { - nextState = nextState.MintAsset(context, agentAddress, costCrystal); - } - - if (costRune.Sign > 0) - { - nextState = nextState.MintAsset(context, avatarState.address, costRune); + var nextState = action.Execute(ctx); + if (!nextState.TryGetLegacyState(runeStateAddress, out List nextRuneRawState)) + { + throw new Exception(); + } + + var nextRuneState = new RuneState(nextRuneRawState); + var nextNcgBal = nextState.GetBalance(agentAddress, ncgCurrency); + var nextCrystalBal = nextState.GetBalance(agentAddress, crystalCurrency); + var nextRuneBal = nextState.GetBalance(avatarAddress, runeCurrency); + + Assert.Equal((initialNcg - expectedNcgCost) * ncgCurrency, nextNcgBal); + Assert.Equal( + (initialCrystal - expectedCrystalCost) * crystalCurrency, + nextCrystalBal + ); + Assert.Equal((initialRune - expectedRuneCost) * runeCurrency, nextRuneBal); + Assert.Equal(expectedLevel, nextRuneState.Level); } - - var finalNcgBal = nextState.GetBalance(agentAddress, ncgCurrency); - var finalCrystalBal = nextState.GetBalance(agentAddress, crystalCurrency); - var finalRuneBal = nextState.GetBalance(avatarState.address, runeCurrency); - Assert.Equal(ncgBal, finalNcgBal); - Assert.Equal(crystalBal, finalCrystalBal); - Assert.Equal(runeBal, finalRuneBal); - Assert.Equal(runeState.Level + 1, nextRunState.Level); } [Fact] diff --git a/Lib9c/Action/RuneEnhancement.cs b/Lib9c/Action/RuneEnhancement.cs index f2ed4f09f2..c02afe0e0a 100644 --- a/Lib9c/Action/RuneEnhancement.cs +++ b/Lib9c/Action/RuneEnhancement.cs @@ -24,6 +24,17 @@ public class RuneEnhancement : GameAction, IRuneEnhancementV1 public int RuneId; public int TryCount = 1; + public struct LevelUpResult + { + public int LevelUpCount { get; set; } + public int NcgCost { get; set; } + public int CrystalCost { get; set; } + public int RuneCost { get; set; } + + public override string ToString() => + $"{LevelUpCount} level up with cost {NcgCost} NCG, {CrystalCost} Crystal, {RuneCost} Runestone."; + } + Address IRuneEnhancementV1.AvatarAddress => AvatarAddress; int IRuneEnhancementV1.RuneId => RuneId; int IRuneEnhancementV1.TryCount => TryCount; @@ -64,6 +75,7 @@ public override IWorld Execute(IActionContext context) typeof(RuneCostSheet), }); + // Validation if (TryCount < 1) { throw new TryCountIsZeroException( @@ -71,16 +83,10 @@ public override IWorld Execute(IActionContext context) $"current TryCount : {TryCount}"); } - RuneState runeState; var runeStateAddress = RuneState.DeriveAddress(AvatarAddress, RuneId); - if (states.TryGetLegacyState(runeStateAddress, out List rawState)) - { - runeState = new RuneState(rawState); - } - else - { - runeState = new RuneState(RuneId); - } + var runeState = states.TryGetLegacyState(runeStateAddress, out List rawState) + ? new RuneState(rawState) + : new RuneState(RuneId); var costSheet = sheets.GetSheet(); if (!costSheet.TryGetValue(runeState.RuneId, out var costRow)) @@ -89,11 +95,11 @@ public override IWorld Execute(IActionContext context) $"[{nameof(RuneEnhancement)}] my avatar address : {AvatarAddress}"); } - var targetLevel = runeState.Level + 1; - if (!costRow.TryGetCost(targetLevel, out var cost)) + var targetLevel = runeState.Level + TryCount; + if (!costRow.TryGetCost(targetLevel, out _)) { throw new RuneCostDataNotFoundException( - $"[{nameof(RuneEnhancement)}] my avatar address : {AvatarAddress}"); + $"[{nameof(RuneEnhancement)}] my avatar address : {AvatarAddress} : Maybe max level reached"); } var runeSheet = sheets.GetSheet(); @@ -103,41 +109,60 @@ public override IWorld Execute(IActionContext context) $"[{nameof(RuneEnhancement)}] my avatar address : {AvatarAddress}"); } + var random = context.GetRandom(); + if (!RuneHelper.TryEnhancement(runeState.Level, costRow, random, TryCount, + out var levelUpResult)) + { + // Rune cost not found while level up + throw new RuneCostDataNotFoundException( + $"[{nameof(RuneEnhancement)}] my avatar address : {AvatarAddress} : Maybe max level reached"); + } + + // Check final balance var ncgCurrency = states.GetGoldCurrency(); var crystalCurrency = CrystalCalculator.CRYSTAL; var runeCurrency = Currency.Legacy(runeRow.Ticker, 0, minters: null); var ncgBalance = states.GetBalance(context.Signer, ncgCurrency); var crystalBalance = states.GetBalance(context.Signer, crystalCurrency); var runeBalance = states.GetBalance(AvatarAddress, runeCurrency); - var random = context.GetRandom(); - if (RuneHelper.TryEnhancement(ncgBalance, crystalBalance, runeBalance, - ncgCurrency, crystalCurrency, runeCurrency, - cost, random, TryCount, out var tryCount)) + + if (ncgBalance < levelUpResult.NcgCost * ncgCurrency || + crystalBalance < levelUpResult.CrystalCost * crystalCurrency || + runeBalance < levelUpResult.RuneCost * runeCurrency) { - runeState.LevelUp(); - states = states.SetLegacyState(runeStateAddress, runeState.Serialize()); + throw new NotEnoughFungibleAssetValueException( + $"{nameof(RuneEnhancement)}" + + $"[ncg:{ncgBalance} < {levelUpResult.NcgCost * ncgCurrency}] " + + $"[crystal:{crystalBalance} < {levelUpResult.CrystalCost * crystalCurrency}] " + + $"[rune:{runeBalance} < {levelUpResult.RuneCost * runeCurrency}]" + ); } + runeState.LevelUp(levelUpResult.LevelUpCount); + states = states.SetLegacyState(runeStateAddress, runeState.Serialize()); + var arenaSheet = sheets.GetSheet(); var arenaData = arenaSheet.GetRoundByBlockIndex(context.BlockIndex); - var feeStoreAddress = Addresses.GetBlacksmithFeeAddress(arenaData.ChampionshipId, arenaData.Round); + var feeStoreAddress = + Addresses.GetBlacksmithFeeAddress(arenaData.ChampionshipId, arenaData.Round); - var ncgCost = cost.NcgQuantity * tryCount * ncgCurrency; - if (cost.NcgQuantity > 0) + // Burn costs + if (levelUpResult.NcgCost > 0) { - states = states.TransferAsset(context, context.Signer, feeStoreAddress, ncgCost); + states = states.TransferAsset(context, context.Signer, feeStoreAddress, + levelUpResult.NcgCost * ncgCurrency); } - var crystalCost = cost.CrystalQuantity * tryCount * crystalCurrency; - if (cost.CrystalQuantity > 0) + if (levelUpResult.CrystalCost > 0) { - states = states.TransferAsset(context, context.Signer, feeStoreAddress, crystalCost); + states = states.TransferAsset(context, context.Signer, feeStoreAddress, + levelUpResult.CrystalCost * crystalCurrency); } - var runeCost = cost.RuneStoneQuantity * tryCount * runeCurrency; - if (cost.RuneStoneQuantity > 0) + if (levelUpResult.RuneCost > 0) { - states = states.TransferAsset(context, AvatarAddress, feeStoreAddress, runeCost); + states = states.TransferAsset(context, AvatarAddress, feeStoreAddress, + levelUpResult.RuneCost * runeCurrency); } return states; diff --git a/Lib9c/Helper/RuneHelper.cs b/Lib9c/Helper/RuneHelper.cs index ee25a43ff1..c741586273 100644 --- a/Lib9c/Helper/RuneHelper.cs +++ b/Lib9c/Helper/RuneHelper.cs @@ -46,6 +46,7 @@ IRandom random { rr.SetRune(random); } + var total = 0; var dictionary = new Dictionary(); while (total < rewardRow.Rune) @@ -82,65 +83,37 @@ IRandom random { result.Add(rewardRow.Crystal * CrystalCalculator.CRYSTAL); } + return result; } public static bool TryEnhancement( - FungibleAssetValue ncg, - FungibleAssetValue crystal, - FungibleAssetValue rune, - Currency ncgCurrency, - Currency crystalCurrency, - Currency runeCurrency, - RuneCostSheet.RuneCostData cost, + int startRuneLevel, + RuneCostSheet.Row costRow, IRandom random, - int maxTryCount, - out int tryCount) + int tryCount, + out RuneEnhancement.LevelUpResult levelUpResult) { - tryCount = 0; - var value = cost.LevelUpSuccessRate + 1; - while (value > cost.LevelUpSuccessRate) - { - tryCount++; - if (tryCount > maxTryCount) - { - tryCount = maxTryCount; - return false; - } + levelUpResult = new RuneEnhancement.LevelUpResult(); - if (!CheckBalance(ncg, crystal, rune, ncgCurrency, crystalCurrency, runeCurrency, cost, tryCount)) + for (var i = 0; i < tryCount; i++) + { + // No cost Found : throw exception at caller + if (!costRow.TryGetCost(startRuneLevel + levelUpResult.LevelUpCount + 1, + out var cost)) { return false; } - value = random.Next(1, GameConfig.MaximumProbability + 1); - } - - return true; - } + // Cost burns in every try + levelUpResult.NcgCost += cost.NcgQuantity; + levelUpResult.CrystalCost += cost.CrystalQuantity; + levelUpResult.RuneCost += cost.RuneStoneQuantity; - private static bool CheckBalance( - FungibleAssetValue ncg, - FungibleAssetValue crystal, - FungibleAssetValue rune, - Currency ncgCurrency, - Currency crystalCurrency, - Currency runeCurrency, - RuneCostSheet.RuneCostData cost, - int tryCount) - { - var ncgCost = tryCount * cost.NcgQuantity * ncgCurrency; - var crystalCost = tryCount * cost.CrystalQuantity * crystalCurrency; - var runeCost = tryCount * cost.RuneStoneQuantity * runeCurrency; - if (ncg < ncgCost || crystal < crystalCost || rune < runeCost) - { - if (tryCount == 1) + if (random.Next(0, GameConfig.MaximumProbability) < cost.LevelUpSuccessRate) { - throw new NotEnoughFungibleAssetValueException($"{nameof(RuneHelper)}" + - $"[ncg:{ncg} < {ncgCost}] [crystal:{crystal} < {crystalCost}] [rune:{rune} < {runeCost}]"); + levelUpResult.LevelUpCount++; } - - return false; } return true; diff --git a/Lib9c/Model/State/RuneState.cs b/Lib9c/Model/State/RuneState.cs index cf4507ce80..59ee1ff24b 100644 --- a/Lib9c/Model/State/RuneState.cs +++ b/Lib9c/Model/State/RuneState.cs @@ -31,9 +31,9 @@ public IValue Serialize() return result; } - public void LevelUp() + public void LevelUp(int level = 1) { - Level++; + Level += level; } } }