From 21d908ac07a4d3830c7ca36458cabeff7cb778de Mon Sep 17 00:00:00 2001 From: Masaki Inoue <45062199+2RiniaR@users.noreply.github.com> Date: Sun, 1 Dec 2024 23:15:55 +0900 Subject: [PATCH] =?UTF-8?q?Common:=20=E5=80=8D=E7=8E=87=E3=82=92=E8=A1=A8?= =?UTF-8?q?=E3=81=99=E6=A7=8B=E9=80=A0=E4=BD=93=E3=82=92=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=20(#46)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Common/Master/SettingMaster.cs | 10 ++ Common/Master/SlotItemMaster.cs | 4 + Common/Models/AppService.cs | 19 ++- Common/Models/AppState.cs | 61 -------- Common/Models/Gacha.cs | 112 ++++++++++++++ Common/Models/GachaItem.cs | 27 ++++ Common/Models/GachaManager.cs | 140 ----------------- Common/Models/GachaProbability.cs | 15 -- Common/Models/Slot.cs | 27 ++-- Common/Models/User.cs | 14 +- Common/Multiplier.cs | 141 ++++++++++++++++++ Common/Random/RandomManager.cs | 30 ++-- Common/Utility/NumberUtility.cs | 30 ---- Events/Admin/AdminMasterReloadPresenter.cs | 22 --- Events/DailyReset/DailyResetPresenter.cs | 15 +- Events/Gacha/GachaCommandPresenter.cs | 7 +- Events/Gacha/GachaInfoCommandPresenter.cs | 5 +- Events/Gacha/GachaInteractReplyPresenter.cs | 3 +- Events/Gacha/GachaRareReplyPresenter.cs | 3 +- Events/Gacha/GachaUtility.cs | 13 +- Events/Slot/SlotExecutePresenter.cs | 3 +- .../20241201133806_CreateGacha.Designer.cs | 105 +++++++++++++ Migrations/20241201133806_CreateGacha.cs | 86 +++++++++++ Migrations/AppServiceModelSnapshot.cs | 45 ++++-- Program.cs | 43 ++++-- 25 files changed, 639 insertions(+), 341 deletions(-) delete mode 100644 Common/Models/AppState.cs create mode 100644 Common/Models/Gacha.cs create mode 100644 Common/Models/GachaItem.cs delete mode 100644 Common/Models/GachaManager.cs delete mode 100644 Common/Models/GachaProbability.cs create mode 100644 Common/Multiplier.cs create mode 100644 Migrations/20241201133806_CreateGacha.Designer.cs create mode 100644 Migrations/20241201133806_CreateGacha.cs diff --git a/Common/Master/SettingMaster.cs b/Common/Master/SettingMaster.cs index 0b486a8..6a22337 100644 --- a/Common/Master/SettingMaster.cs +++ b/Common/Master/SettingMaster.cs @@ -70,11 +70,15 @@ private int GetInt(string key) /// public int MaxRareReplyProbabilityPermillage => GetInt(nameof(MaxRareReplyProbabilityPermillage)); + public Multiplier MaxRareReplyProbability => Multiplier.FromPermillage(MaxRareReplyProbabilityPermillage); + /// /// 確率返信の抽選単位(千分率) /// public int RareReplyProbabilityStepPermillage => GetInt(nameof(RareReplyProbabilityStepPermillage)); + public Multiplier RareReplyProbabilityStep => Multiplier.FromPermillage(RareReplyProbabilityStepPermillage); + /// /// 単発ガチャ1回の価格 /// @@ -120,15 +124,21 @@ private int GetInt(string key) /// public int SlotMaxConditionOffsetPermillage => GetInt(nameof(SlotMaxConditionOffsetPermillage)); + public Multiplier SlotMaxConditionOffset => Multiplier.FromPermillage(SlotMaxConditionOffsetPermillage); + /// /// スロットの調子の最小値(千分率) /// public int SlotMinConditionOffsetPermillage => GetInt(nameof(SlotMinConditionOffsetPermillage)); + public Multiplier SlotMinConditionOffset => Multiplier.FromPermillage(SlotMinConditionOffsetPermillage); + /// /// スロットの次に同じ出目が確定する確率の最大値(千分率) /// public int SlotRepeatPermillageUpperBound => GetInt(nameof(SlotRepeatPermillageUpperBound)); + + public Multiplier SlotRepeatUpperBound => Multiplier.FromPermillage(SlotRepeatPermillageUpperBound); } [SuppressMessage("ReSharper", "UnassignedGetOnlyAutoProperty")] diff --git a/Common/Master/SlotItemMaster.cs b/Common/Master/SlotItemMaster.cs index acca821..a7709c6 100644 --- a/Common/Master/SlotItemMaster.cs +++ b/Common/Master/SlotItemMaster.cs @@ -29,9 +29,13 @@ public class SlotItem : MasterRecord [field: MasterIntValue("return_rate_permillage")] public int ReturnRatePermillage { get; } + public Multiplier ReturnRate => Multiplier.FromPermillage(ReturnRatePermillage); + /// /// 次に同じ出目が確定する確率(千分率) /// [field: MasterIntValue("repeat_permillage")] public int RepeatPermillage { get; } + + public Multiplier RepeatProbability => Multiplier.FromPermillage(RepeatPermillage); } diff --git a/Common/Models/AppService.cs b/Common/Models/AppService.cs index 30f5fe1..1b6ce65 100644 --- a/Common/Models/AppService.cs +++ b/Common/Models/AppService.cs @@ -7,10 +7,10 @@ namespace Approvers.King.Common; /// public class AppService : DbContext { - public DbSet AppStates { get; set; } public DbSet Slots { get; set; } + public DbSet Gachas { get; set; } public DbSet Users { get; set; } - public DbSet GachaProbabilities { get; set; } + public DbSet GachaItems { get; set; } /// /// セッションを作成する @@ -41,7 +41,7 @@ public async Task FindOrCreateUserAsync(ulong discordId) Add(user); return user; } - + public async Task GetDefaultSlotAsync() { var slot = await Slots.FirstOrDefaultAsync(); @@ -54,4 +54,17 @@ public async Task GetDefaultSlotAsync() Add(slot); return slot; } + + public async Task GetDefaultGachaAsync() + { + var gacha = await Gachas.Include(x => x.GachaItems).FirstOrDefaultAsync(); + if (gacha != null) + { + return gacha; + } + + gacha = new Gacha { Id = Guid.NewGuid() }; + Add(gacha); + return gacha; + } } diff --git a/Common/Models/AppState.cs b/Common/Models/AppState.cs deleted file mode 100644 index 9ccdba9..0000000 --- a/Common/Models/AppState.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using Microsoft.EntityFrameworkCore; - -namespace Approvers.King.Common; - -public static class AppStateDbSetExtensions -{ - public async static Task GetIntAsync(this DbSet source, AppStateType type) - { - var state = await source.FindAsync(type); - return int.TryParse(state?.Value ?? string.Empty, out var result) ? result : null; - } - - public async static Task SetIntAsync(this DbSet source, AppStateType type, int value) - { - var state = await source.FindAsync(type); - var valueString = value.ToString(); - if (state == null) - { - source.Add(new AppState { Type = type, Value = valueString }); - } - else - { - state.Value = valueString; - } - } - - public async static Task GetStringAsync(this DbSet source, AppStateType type) - { - var state = await source.FindAsync(type); - return state?.Value; - } - - public async static Task SetStringAsync(this DbSet source, AppStateType type, string value) - { - var state = await source.FindAsync(type); - var valueString = value; - if (state == null) - { - source.Add(new AppState { Type = type, Value = valueString }); - } - else - { - state.Value = valueString; - } - } -} - -/// -/// アプリ全体の状態 -/// -public class AppState -{ - [Key] public AppStateType Type { get; set; } - public string Value { get; set; } = null!; -} - -public enum AppStateType -{ - RareReplyProbabilityPermillage, -} diff --git a/Common/Models/Gacha.cs b/Common/Models/Gacha.cs new file mode 100644 index 0000000..254e884 --- /dev/null +++ b/Common/Models/Gacha.cs @@ -0,0 +1,112 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Approvers.King.Common; + +public class Gacha +{ + [Key] public Guid Id { get; set; } + + public int HitProbabilityPermillage { get; set; } + + /// + /// 現在のメッセージに反応する確率 + /// + [NotMapped] + public Multiplier HitProbability + { + get => Multiplier.FromPermillage(HitProbabilityPermillage); + set => HitProbabilityPermillage = value.Permillage; + } + + /// + /// 各メッセージの確率 + /// + public List GachaItems { get; set; } = []; + + /// + /// 単発ガチャを回す + /// + public GachaItem? RollOnce() + { + if (RandomManager.IsHit(HitProbability) == false) + { + return null; + } + + return GetRandomResult(); + } + + /// + /// 確定ガチャを回す + /// + public GachaItem RollOnceCertain() + { + return GetRandomResult(); + } + + private GachaItem GetRandomResult() + { + var total = GachaItems.Sum(x => x.Probability.Permillage); + if (total <= 0) + { + return GachaItems.First(); + } + + var value = RandomManager.GetRandomInt(total); + + foreach (var element in GachaItems) + { + if (value < element.Probability.Permillage) return element; + value -= element.Probability.Permillage; + } + + return GachaItems.Last(); + } + + /// + /// メッセージに反応する確率を再抽選する + /// + public void ShuffleRareReplyRate() + { + // 確率は step の単位で max まで変動(ただし0にはならない) + var step = MasterManager.SettingMaster.RareReplyProbabilityStep; + var max = MasterManager.SettingMaster.MaxRareReplyProbability; + var rates = Enumerable.Range(0, max.Permillage / step.Permillage).Select(i => step * (i + 1)); + HitProbability = RandomManager.PickRandom(rates); + } + + /// + /// 各メッセージの確率を再抽選する + /// + public void ShuffleMessageRates() + { + var items = MasterManager.RandomMessageMaster + .GetAll(x => x.Type == RandomMessageType.GeneralReply) + .Select(randomMessage => GachaItems.FirstOrDefault(item => item.RandomMessageId == randomMessage.Id) ?? new GachaItem() + { + GachaId = Id, + RandomMessageId = randomMessage.Id, + Probability = Multiplier.Zero + }) + .ToList(); + + // いい感じに確率がばらけるように、カイ二乗分布を適用 + var borders = Enumerable.Range(0, items.Count - 1) + .Select(_ => (float)Math.Pow(RandomManager.GetRandomFloat(1f), 2)) + .Select(Multiplier.FromRate) + .OrderBy(x => x) + .ToList(); + borders.Add(Multiplier.One); + var randomIndices = RandomManager.Shuffle(Enumerable.Range(0, items.Count)).ToList(); + + items[randomIndices[0]].Probability = borders[0]; + for (var i = 1; i < items.Count; i++) + { + items[randomIndices[i]].Probability = borders[i] - borders[i - 1]; + } + + GachaItems.Clear(); + GachaItems.AddRange(items); + } +} diff --git a/Common/Models/GachaItem.cs b/Common/Models/GachaItem.cs new file mode 100644 index 0000000..4e0f778 --- /dev/null +++ b/Common/Models/GachaItem.cs @@ -0,0 +1,27 @@ +using System.ComponentModel.DataAnnotations.Schema; +using Microsoft.EntityFrameworkCore; + +namespace Approvers.King.Common; + +[PrimaryKey(nameof(GachaId), nameof(RandomMessageId))] +public class GachaItem +{ + public Guid GachaId { get; set; } + public Gacha Gacha { get; set; } = null!; + public string RandomMessageId { get; set; } = null!; + + public int ProbabilityPermillage { get; set; } + + [NotMapped] + public Multiplier Probability + { + get => Multiplier.FromPermillage(ProbabilityPermillage); + set => ProbabilityPermillage = value.Permillage; + } + + private RandomMessage? _randomMessage; + + public RandomMessage? RandomMessage => _randomMessage == null || _randomMessage.Id != RandomMessageId + ? _randomMessage = MasterManager.RandomMessageMaster.Find(RandomMessageId) + : _randomMessage; +} diff --git a/Common/Models/GachaManager.cs b/Common/Models/GachaManager.cs deleted file mode 100644 index 1b96a69..0000000 --- a/Common/Models/GachaManager.cs +++ /dev/null @@ -1,140 +0,0 @@ -using Microsoft.EntityFrameworkCore; - -namespace Approvers.King.Common; - -public class GachaManager : Singleton -{ - private readonly List _replyMessageTable = new(); - - /// - /// 現在のメッセージに反応する確率 - /// - public float RareReplyRate { get; private set; } - - /// - /// 各メッセージの確率 - /// - public IReadOnlyList ReplyMessageTable => _replyMessageTable; - - public bool IsTableEmpty => ReplyMessageTable.Count == 0; - - /// - /// 現在の状態を読み込む - /// - public async Task LoadAsync() - { - await using var app = AppService.CreateSession(); - - var probabilities = await app.GachaProbabilities.ToListAsync(); - _replyMessageTable.Clear(); - _replyMessageTable.AddRange(probabilities.Where(x => x.RandomMessage != null)); - - var rareReplyRatePermillage = await app.AppStates.GetIntAsync(AppStateType.RareReplyProbabilityPermillage); - RareReplyRate = NumberUtility.GetPercentFromPermillage(rareReplyRatePermillage ?? 0); - } - - /// - /// 現在の状態を保存する - /// - public async Task SaveAsync() - { - await using var app = AppService.CreateSession(); - - app.GachaProbabilities.RemoveRange(app.GachaProbabilities); - app.GachaProbabilities.AddRange(_replyMessageTable); - - var rareReplyRatePermillage = NumberUtility.GetPermillageFromPercent(RareReplyRate); - await app.AppStates.SetIntAsync(AppStateType.RareReplyProbabilityPermillage, rareReplyRatePermillage); - - await app.SaveChangesAsync(); - } - - /// - /// マスタデータを読み込む - /// - public void LoadMaster() - { - // 同じIDのメッセージは確率を保持し、新規追加分は確率0%で初期化する - var messages = MasterManager.RandomMessageMaster - .GetAll(x => x.Type == RandomMessageType.GeneralReply) - .Select(x => new GachaProbability() - { - RandomMessageId = x.Id, - Probability = _replyMessageTable.FirstOrDefault(m => m.RandomMessageId == x.Id)?.Probability ?? 0 - }) - .ToList(); - - _replyMessageTable.Clear(); - _replyMessageTable.AddRange(messages); - } - - /// - /// 単発ガチャを回す - /// - public GachaProbability? Roll() - { - if (RandomManager.IsHit(RareReplyRate) == false) return null; - return GetRandomResult(); - } - - /// - /// 確定ガチャを回す - /// - public GachaProbability RollWithoutNone() - { - return GetRandomResult(); - } - - private GachaProbability GetRandomResult() - { - var totalRate = _replyMessageTable.Sum(x => x.Probability); - if (totalRate <= 0) - { - return _replyMessageTable[0]; - } - - var value = RandomManager.GetRandomFloat(totalRate); - - foreach (var element in _replyMessageTable) - { - if (value < element.Probability) return element; - value -= element.Probability; - } - - return _replyMessageTable[^1]; - } - - /// - /// メッセージに反応する確率を再抽選する - /// - public void ShuffleRareReplyRate() - { - // 確率は step の単位で max まで変動(ただし0にはならない) - var step = MasterManager.SettingMaster.RareReplyProbabilityStepPermillage; - var max = MasterManager.SettingMaster.MaxRareReplyProbabilityPermillage; - var rates = Enumerable.Range(0, max / step) - .Select(i => NumberUtility.GetPercentFromPermillage((i + 1) * step)); - RareReplyRate = RandomManager.PickRandom(rates); - } - - /// - /// 各メッセージの確率を再抽選する - /// - public void ShuffleMessageRates() - { - // いい感じに確率がばらけるように、カイ二乗分布を適用 - var borders = Enumerable.Range(0, _replyMessageTable.Count - 1) - .Select(x => (float)Math.Pow(RandomManager.GetRandomFloat(1f), 2)) - .Select(x => (int)Math.Floor(x * 100f)) - .OrderBy(x => x) - .ToList(); - borders.Add(100); - var randomIndices = RandomManager.Shuffle(Enumerable.Range(0, _replyMessageTable.Count)).ToList(); - - _replyMessageTable[randomIndices[0]].Probability = borders[0] * 0.01f; - for (var i = 1; i < _replyMessageTable.Count; i++) - { - _replyMessageTable[randomIndices[i]].Probability = (borders[i] - borders[i - 1]) * 0.01f; - } - } -} diff --git a/Common/Models/GachaProbability.cs b/Common/Models/GachaProbability.cs deleted file mode 100644 index eda0547..0000000 --- a/Common/Models/GachaProbability.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Approvers.King.Common; - -public class GachaProbability -{ - [Key] public string RandomMessageId { get; set; } = null!; - public float Probability { get; set; } - - private RandomMessage? _randomMessage; - - public RandomMessage? RandomMessage => _randomMessage == null || _randomMessage.Id != RandomMessageId - ? _randomMessage = MasterManager.RandomMessageMaster.Find(RandomMessageId) - : _randomMessage; -} diff --git a/Common/Models/Slot.cs b/Common/Models/Slot.cs index 721d82c..bdd76d7 100644 --- a/Common/Models/Slot.cs +++ b/Common/Models/Slot.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; namespace Approvers.King.Common; @@ -8,10 +9,17 @@ public class Slot [Key] public Guid Id { get; set; } + public int ConditionPermillage { get; set; } + /// /// 調子(千分率) /// - public int ConditionPermillage { get; set; } + [NotMapped] + public Multiplier Condition + { + get => Multiplier.FromPermillage(ConditionPermillage); + set => ConditionPermillage = value.Permillage; + } public int ExecutePrice => MasterManager.SettingMaster.PricePerSlotOnce; @@ -20,9 +28,9 @@ public class Slot /// public void ShuffleCondition() { - var max = MasterManager.SettingMaster.SlotMaxConditionOffsetPermillage; - var min = MasterManager.SettingMaster.SlotMinConditionOffsetPermillage; - ConditionPermillage = RandomManager.GetRandomInt(min, max + 1); + var max = MasterManager.SettingMaster.SlotMaxConditionOffset; + var min = MasterManager.SettingMaster.SlotMinConditionOffset; + Condition = RandomManager.GetRandomMultiplier(min, max); } /// @@ -44,8 +52,7 @@ public SlotExecuteResult Execute() // 一定確率で直前と同じ出目が出る // 確率はマスタデータの設定値に加え、調子により変動する - var repeatPermillage = Math.Clamp(reelItems[i - 1].RepeatPermillage + ConditionPermillage, 0, MasterManager.SettingMaster.SlotRepeatPermillageUpperBound); - var repeatProbability = NumberUtility.GetProbabilityFromPermillage(repeatPermillage); + var repeatProbability = (reelItems[i - 1].RepeatProbability + Condition).Clamp(Multiplier.Zero, MasterManager.SettingMaster.SlotRepeatUpperBound); var isRepeat = RandomManager.IsHit(repeatProbability); if (isRepeat) { @@ -57,13 +64,13 @@ public SlotExecuteResult Execute() } var isWin = reelItems.Select(x => x.Id).Distinct().Count() == 1; - var resultRatePermillage = isWin ? reelItems[0].ReturnRatePermillage : 0; + var resultRate = isWin ? reelItems[0].ReturnRate : Multiplier.Zero; return new SlotExecuteResult() { ReelItems = reelItems, IsWin = isWin, - ResultRatePermillage = resultRatePermillage + ResultRate = resultRate }; } } @@ -81,7 +88,7 @@ public class SlotExecuteResult public bool IsWin { get; init; } /// - /// キャッシュバック倍率(千分率) + /// キャッシュバック倍率 /// - public int ResultRatePermillage { get; init; } + public Multiplier ResultRate { get; init; } } diff --git a/Common/Models/User.cs b/Common/Models/User.cs index 100df88..548de7a 100644 --- a/Common/Models/User.cs +++ b/Common/Models/User.cs @@ -29,29 +29,29 @@ public void ResetDailyState() /// /// 単発ガチャを回す /// - public GachaProbability? RollGachaOnce() + public GachaItem? RollGachaOnce(Gacha gacha) { MonthlyGachaPurchasePrice += MasterManager.SettingMaster.PricePerGachaOnce; - return GachaManager.Instance.Roll(); + return gacha.RollOnce(); } /// /// 単発確定ガチャを回す /// - public GachaProbability RollGachaOnceCertain() + public GachaItem RollGachaOnceCertain(Gacha gacha) { MonthlyGachaPurchasePrice += MasterManager.SettingMaster.PricePerGachaOnceCertain; - return GachaManager.Instance.RollWithoutNone(); + return gacha.RollOnceCertain(); } /// /// 10連ガチャを回す /// - public List RollGachaTenTimes() + public List RollGachaTenTimes(Gacha gacha) { const int pickCount = 10; MonthlyGachaPurchasePrice += MasterManager.SettingMaster.PricePerGachaTenTimes; - return Enumerable.Range(0, pickCount).Select(_ => GachaManager.Instance.Roll()).ToList(); + return Enumerable.Range(0, pickCount).Select(_ => gacha.RollOnce()).ToList(); } /// @@ -66,7 +66,7 @@ public SlotExecuteResult ExecuteSlot(Slot slot) var price = slot.ExecutePrice; var result = slot.Execute(); - var reward = (int)(NumberUtility.GetPercentFromPermillage(result.ResultRatePermillage) * price); + var reward = (int)(price * result.ResultRate); MonthlySlotProfitPrice += reward - price; TodaySlotExecuteCount++; return result; diff --git a/Common/Multiplier.cs b/Common/Multiplier.cs new file mode 100644 index 0000000..b2e77dd --- /dev/null +++ b/Common/Multiplier.cs @@ -0,0 +1,141 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Approvers.King.Common; + +/// +/// 確率・倍率を表す構造体 +/// +public readonly struct Multiplier : IEquatable, IComparable, IComparable +{ + /// + /// 千分率 (50% -> 500) + /// + public int Permillage { get; } + + /// + /// 倍率 (50% -> 0.5) + /// + public float Rate => Permillage / 1000f; + + /// + /// パーセント (50% -> 50) + /// + public float Percent => Permillage / 10f; + + public static Multiplier Zero { get; } = new(0); + public static Multiplier One { get; } = new(1000); + public static Multiplier FromRate(float rate) => new((int)(rate * 1000f)); + public static Multiplier FromPercent(float percent) => new((int)(percent * 10f)); + public static Multiplier FromPermillage(int permillage) => new(permillage); + + private Multiplier(int permillage) + { + Permillage = permillage; + } + + public int CompareTo(Multiplier other) + { + return Permillage.CompareTo(other.Permillage); + } + + public int CompareTo(object? obj) + { + if (obj is null) return 1; + return obj is Multiplier other ? CompareTo(other) : throw new ArgumentException($"Object must be of type {nameof(Multiplier)}"); + } + + public static bool operator <(Multiplier left, Multiplier right) + { + return left.CompareTo(right) < 0; + } + + public static bool operator >(Multiplier left, Multiplier right) + { + return left.CompareTo(right) > 0; + } + + public static bool operator <=(Multiplier left, Multiplier right) + { + return left.CompareTo(right) <= 0; + } + + public static bool operator >=(Multiplier left, Multiplier right) + { + return left.CompareTo(right) >= 0; + } + + public override bool Equals([NotNullWhen(true)] object? obj) + { + return base.Equals(obj); + } + + public bool Equals(Multiplier other) + { + return Permillage == other.Permillage; + } + + public static bool operator ==(Multiplier left, Multiplier right) + { + return left.Equals(right); + } + + public static bool operator !=(Multiplier left, Multiplier right) + { + return !(left == right); + } + + public override int GetHashCode() + { + return Permillage; + } + + public override string ToString() + { + return $"{Rate:P0}"; + } + + public static Multiplier operator +(Multiplier left, Multiplier right) + { + return new Multiplier(left.Permillage + right.Permillage); + } + + public static Multiplier operator -(Multiplier left, Multiplier right) + { + return new Multiplier(left.Permillage - right.Permillage); + } + + public static Multiplier operator *(Multiplier left, Multiplier right) + { + return new Multiplier((int)(left.Rate * right.Rate * 1000f)); + } + + public static Multiplier operator /(Multiplier left, Multiplier right) + { + return new Multiplier((int)(left.Rate / right.Rate * 1000f)); + } + + public static Multiplier operator *(Multiplier left, int right) + { + return new Multiplier(left.Permillage * right); + } + + public static Multiplier operator /(Multiplier left, int right) + { + return new Multiplier(left.Permillage / right); + } + + public static float operator *(float left, Multiplier right) + { + return left * right.Rate; + } + + public static float operator /(float left, Multiplier right) + { + return left / right.Rate; + } + + public Multiplier Clamp(Multiplier min, Multiplier max) + { + return new Multiplier(Math.Clamp(Permillage, min.Permillage, max.Permillage)); + } +} diff --git a/Common/Random/RandomManager.cs b/Common/Random/RandomManager.cs index be34623..ad6b0ae 100644 --- a/Common/Random/RandomManager.cs +++ b/Common/Random/RandomManager.cs @@ -4,29 +4,39 @@ public class RandomManager : Singleton { private readonly Random _random = new((int)DateTime.Now.Ticks & 0x0000FFFF); - public static float GetRandomFloat(float max) + public static float GetRandomFloat(float maxExclusive) { - return GetRandomFloat(0f, max); + return GetRandomFloat(0f, maxExclusive); } - public static float GetRandomFloat(float min, float max) + public static float GetRandomFloat(float minInclusive, float maxExclusive) { - return (float)(min + (max - min) * Instance._random.NextDouble()); + return (float)(minInclusive + (maxExclusive - minInclusive) * Instance._random.NextDouble()); } - public static int GetRandomInt(int max) + public static int GetRandomInt(int maxExclusive) { - return GetRandomInt(0, max); + return GetRandomInt(0, maxExclusive); } - public static int GetRandomInt(int min, int max) + public static int GetRandomInt(int minInclusive, int maxExclusive) { - return Instance._random.Next(min, max); + return Instance._random.Next(minInclusive, maxExclusive); } - public static bool IsHit(float probability) + public static Multiplier GetRandomMultiplier(Multiplier maxInclusive) { - return GetRandomFloat(1f) <= probability; + return GetRandomMultiplier(Multiplier.Zero, maxInclusive); + } + + public static Multiplier GetRandomMultiplier(Multiplier minInclusive, Multiplier maxInclusive) + { + return Multiplier.FromPermillage(Instance._random.Next(minInclusive.Permillage, maxInclusive.Permillage + 1)); + } + + public static bool IsHit(Multiplier probability) + { + return GetRandomMultiplier(Multiplier.One) <= probability; } public static T PickRandom(IEnumerable source) diff --git a/Common/Utility/NumberUtility.cs b/Common/Utility/NumberUtility.cs index a4246a9..fc6bcee 100644 --- a/Common/Utility/NumberUtility.cs +++ b/Common/Utility/NumberUtility.cs @@ -2,38 +2,8 @@ namespace Approvers.King.Common; public static class NumberUtility { - public static DateTime GetBaseDateTime() - { - return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); - } - - public static bool IsApproximate(this float a, float b) - { - return Math.Abs(a - b) < float.Epsilon; - } - public static float GetSecondsFromMilliseconds(float milliseconds) { return milliseconds / 1000f; } - - public static TimeSpan GetTimeSpanFromMilliseconds(float milliseconds) - { - return TimeSpan.FromMilliseconds(milliseconds); - } - - public static float GetProbabilityFromPermillage(int permillage) - { - return permillage / 100000f; - } - - public static float GetPercentFromPermillage(int permillage) - { - return permillage / 1000f; - } - - public static int GetPermillageFromPercent(float percent) - { - return (int)(percent * 1000f); - } } diff --git a/Events/Admin/AdminMasterReloadPresenter.cs b/Events/Admin/AdminMasterReloadPresenter.cs index b8de11f..5120b94 100644 --- a/Events/Admin/AdminMasterReloadPresenter.cs +++ b/Events/Admin/AdminMasterReloadPresenter.cs @@ -13,27 +13,5 @@ protected override async Task MainAsync() await Message.ReplyAsync("マスターをリロードするぞ"); await MasterManager.FetchAsync(); await Message.ReplyAsync("マスターをリロードしたぞ"); - - await UpdateGachaTableAsync(); - } - - private async Task UpdateGachaTableAsync() - { - var beforeTable = GachaManager.Instance.ReplyMessageTable.Select(x => x.RandomMessageId).ToHashSet(); - GachaManager.Instance.LoadMaster(); - var afterTable = GachaManager.Instance.ReplyMessageTable.Select(x => x.RandomMessageId).ToHashSet(); - - // テーブルに差分がある場合は排出率を更新する - var hasDiff = beforeTable.SetEquals(afterTable); - if (hasDiff == false) - { - GachaManager.Instance.ShuffleMessageRates(); - await GachaManager.Instance.SaveAsync(); - - // 排出率を投稿する - var guild = DiscordManager.Client.GetGuild(EnvironmentManager.DiscordTargetGuildId); - await guild.GetTextChannel(EnvironmentManager.DiscordMainChannelId) - .SendMessageAsync(embed: GachaUtility.GetInfoEmbedBuilder().Build()); - } } } diff --git a/Events/DailyReset/DailyResetPresenter.cs b/Events/DailyReset/DailyResetPresenter.cs index e9d56e7..806c972 100644 --- a/Events/DailyReset/DailyResetPresenter.cs +++ b/Events/DailyReset/DailyResetPresenter.cs @@ -13,24 +13,25 @@ public class DailyResetPresenter : SchedulerJobPresenterBase protected override async Task MainAsync() { await using var app = AppService.CreateSession(); + var slotMaxUsers = await app.Users .Where(x => x.TodaySlotExecuteCount >= MasterManager.SettingMaster.UserSlotExecuteLimitPerDay) .Select(x => x.DeepCopy()) .ToListAsync(); await app.Users.ForEachAsync(user => user.ResetDailyState()); - await app.SaveChangesAsync(); // 排出確率を変える - GachaManager.Instance.LoadMaster(); - GachaManager.Instance.ShuffleRareReplyRate(); - GachaManager.Instance.ShuffleMessageRates(); - await GachaManager.Instance.SaveAsync(); + var gacha = await app.GetDefaultGachaAsync(); + gacha.ShuffleRareReplyRate(); + gacha.ShuffleMessageRates(); + + await app.SaveChangesAsync(); // 名前を更新する - await DiscordManager.GetClientUser().ModifyAsync(x => x.Nickname = $"{GachaManager.Instance.RareReplyRate:P0}の確率でわかってくれる創造主"); + await DiscordManager.GetClientUser().ModifyAsync(x => x.Nickname = $"{gacha.HitProbability.Rate:P0}の確率でわかってくれる創造主"); // 排出率を投稿する - await DiscordManager.GetMainChannel().SendMessageAsync(embed: GachaUtility.GetInfoEmbedBuilder().Build()); + await DiscordManager.GetMainChannel().SendMessageAsync(embed: GachaUtility.GetInfoEmbedBuilder(gacha).Build()); // スロットの実行回数が最大になったユーザーを通知する await NotifySlotMaxUsers(slotMaxUsers); diff --git a/Events/Gacha/GachaCommandPresenter.cs b/Events/Gacha/GachaCommandPresenter.cs index 6c67b82..9f9daf6 100644 --- a/Events/Gacha/GachaCommandPresenter.cs +++ b/Events/Gacha/GachaCommandPresenter.cs @@ -13,14 +13,15 @@ protected override async Task MainAsync() { await using var app = AppService.CreateSession(); var user = await app.FindOrCreateUserAsync(Message.Author.Id); + var gacha = await app.GetDefaultGachaAsync(); - var results = user.RollGachaTenTimes(); + var results = user.RollGachaTenTimes(gacha); await SendReplyAsync(user, results); await app.SaveChangesAsync(); } - private async Task SendReplyAsync(User user, IReadOnlyList results) + private async Task SendReplyAsync(User user, IReadOnlyList results) { var builder = new StringBuilder(); builder.AppendLine($"↓↓↓ いっそう{results.Count}連おみくじ ↓↓↓"); @@ -28,7 +29,7 @@ private async Task SendReplyAsync(User user, IReadOnlyList re { builder.AppendLine(result != null ? Format.Bold( - $"・{result.RandomMessage?.Content ?? MessageConst.MissingMessage} ({result.Probability:P0})") + $"・{result.RandomMessage?.Content ?? MessageConst.MissingMessage} ({result.Probability.Rate:P0})") : Format.Code("x")); } diff --git a/Events/Gacha/GachaInfoCommandPresenter.cs b/Events/Gacha/GachaInfoCommandPresenter.cs index f50d18b..509b7cf 100644 --- a/Events/Gacha/GachaInfoCommandPresenter.cs +++ b/Events/Gacha/GachaInfoCommandPresenter.cs @@ -9,10 +9,13 @@ public class GachaInfoCommandPresenter : DiscordMessagePresenterBase { protected override async Task MainAsync() { + await using var app = AppService.CreateSession(); + var gacha = await app.GetDefaultGachaAsync(); + // 排出率を投稿する await DiscordManager.Client .GetGuild(EnvironmentManager.DiscordTargetGuildId) .GetTextChannel(EnvironmentManager.DiscordMainChannelId) - .SendMessageAsync(embed: GachaUtility.GetInfoEmbedBuilder().Build()); + .SendMessageAsync(embed: GachaUtility.GetInfoEmbedBuilder(gacha).Build()); } } diff --git a/Events/Gacha/GachaInteractReplyPresenter.cs b/Events/Gacha/GachaInteractReplyPresenter.cs index 6c19514..2db828c 100644 --- a/Events/Gacha/GachaInteractReplyPresenter.cs +++ b/Events/Gacha/GachaInteractReplyPresenter.cs @@ -12,8 +12,9 @@ protected override async Task MainAsync() { await using var app = AppService.CreateSession(); var user = await app.FindOrCreateUserAsync(Message.Author.Id); + var gacha = await app.GetDefaultGachaAsync(); - var message = user.RollGachaOnceCertain(); + var message = user.RollGachaOnceCertain(gacha); await SendReplyAsync(message.RandomMessage?.Content ?? MessageConst.MissingMessage); await app.SaveChangesAsync(); diff --git a/Events/Gacha/GachaRareReplyPresenter.cs b/Events/Gacha/GachaRareReplyPresenter.cs index d77b81c..fe44977 100644 --- a/Events/Gacha/GachaRareReplyPresenter.cs +++ b/Events/Gacha/GachaRareReplyPresenter.cs @@ -17,8 +17,9 @@ protected override async Task MainAsync() await using var app = AppService.CreateSession(); var user = await app.FindOrCreateUserAsync(Message.Author.Id); + var gacha = await app.GetDefaultGachaAsync(); - var message = user.RollGachaOnce(); + var message = user.RollGachaOnce(gacha); if (message != null) { await SendReplyAsync(message.RandomMessage?.Content ?? MessageConst.MissingMessage); diff --git a/Events/Gacha/GachaUtility.cs b/Events/Gacha/GachaUtility.cs index ee27bf1..ba9ce85 100644 --- a/Events/Gacha/GachaUtility.cs +++ b/Events/Gacha/GachaUtility.cs @@ -6,20 +6,21 @@ namespace Approvers.King.Events; public static class GachaUtility { - public static EmbedBuilder GetInfoEmbedBuilder() + public static EmbedBuilder GetInfoEmbedBuilder(Gacha gacha) { - var records = GachaManager.Instance.ReplyMessageTable + var minProbability = Multiplier.FromPercent(1); + var records = gacha.GachaItems .OrderByDescending(x => x.Probability) - .Where(x => x.Probability.IsApproximate(0f) == false) - .Select(x => (x.RandomMessage?.Content ?? MessageConst.MissingMessage, x.Probability.ToString("P0"))); + .Where(x => x.Probability > minProbability) + .Select(x => (x.RandomMessage?.Content ?? MessageConst.MissingMessage, x.Probability.Rate.ToString("P0"))); return new EmbedBuilder() .WithTitle( $"{IssoUtility.SmileStamp}{IssoUtility.SmileStamp}{IssoUtility.SmileStamp} 本日のいっそう {IssoUtility.SmileStamp}{IssoUtility.SmileStamp}{IssoUtility.SmileStamp}") .WithColor(new Color(0xf1, 0xc4, 0x0f)) - .WithDescription($"本日は {Format.Bold($"{GachaManager.Instance.RareReplyRate:P0}")} の確率で反応します") + .WithDescription($"本日は {Format.Bold($"{gacha.HitProbability.Rate:P0}")} の確率で反応します") .AddField("排出確率", DiscordMessageUtility.Table(records)); } - + public static string CreateRankingView(IReadOnlyList rankingUsers) { var embedBuilder = new StringBuilder(); diff --git a/Events/Slot/SlotExecutePresenter.cs b/Events/Slot/SlotExecutePresenter.cs index 85cf44a..2c62c5d 100644 --- a/Events/Slot/SlotExecutePresenter.cs +++ b/Events/Slot/SlotExecutePresenter.cs @@ -72,8 +72,7 @@ private static string CreatePurchaseMessage(SlotExecuteResult result, User user, { if (result.IsWin) { - var rate = NumberUtility.GetPercentFromPermillage(result.ResultRatePermillage); - sb.AppendLine(Format.Bold($"Y O U W I N ! ! x{rate:F1}")); + sb.AppendLine(Format.Bold($"Y O U W I N ! ! x{result.ResultRate.Rate:F1}")); } else { diff --git a/Migrations/20241201133806_CreateGacha.Designer.cs b/Migrations/20241201133806_CreateGacha.Designer.cs new file mode 100644 index 0000000..c8e89ba --- /dev/null +++ b/Migrations/20241201133806_CreateGacha.Designer.cs @@ -0,0 +1,105 @@ +// +using System; +using Approvers.King.Common; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Approvers.King.Migrations +{ + [DbContext(typeof(AppService))] + [Migration("20241201133806_CreateGacha")] + partial class CreateGacha + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); + + modelBuilder.Entity("Approvers.King.Common.Gacha", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("HitProbabilityPermillage") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Gachas"); + }); + + modelBuilder.Entity("Approvers.King.Common.GachaItem", b => + { + b.Property("GachaId") + .HasColumnType("TEXT"); + + b.Property("RandomMessageId") + .HasColumnType("TEXT"); + + b.Property("ProbabilityPermillage") + .HasColumnType("INTEGER"); + + b.HasKey("GachaId", "RandomMessageId"); + + b.ToTable("GachaItems"); + }); + + modelBuilder.Entity("Approvers.King.Common.Slot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ConditionPermillage") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Slots"); + }); + + modelBuilder.Entity("Approvers.King.Common.User", b => + { + b.Property("DiscordId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("MonthlyGachaPurchasePrice") + .HasColumnType("INTEGER"); + + b.Property("MonthlySlotProfitPrice") + .HasColumnType("INTEGER"); + + b.Property("TodaySlotExecuteCount") + .HasColumnType("INTEGER"); + + b.HasKey("DiscordId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Approvers.King.Common.GachaItem", b => + { + b.HasOne("Approvers.King.Common.Gacha", "Gacha") + .WithMany("GachaItems") + .HasForeignKey("GachaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Gacha"); + }); + + modelBuilder.Entity("Approvers.King.Common.Gacha", b => + { + b.Navigation("GachaItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Migrations/20241201133806_CreateGacha.cs b/Migrations/20241201133806_CreateGacha.cs new file mode 100644 index 0000000..0e16716 --- /dev/null +++ b/Migrations/20241201133806_CreateGacha.cs @@ -0,0 +1,86 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Approvers.King.Migrations +{ + /// + public partial class CreateGacha : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AppStates"); + + migrationBuilder.DropTable( + name: "GachaProbabilities"); + + migrationBuilder.CreateTable( + name: "Gachas", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + HitProbabilityPermillage = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Gachas", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "GachaItems", + columns: table => new + { + GachaId = table.Column(type: "TEXT", nullable: false), + RandomMessageId = table.Column(type: "TEXT", nullable: false), + ProbabilityPermillage = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_GachaItems", x => new { x.GachaId, x.RandomMessageId }); + table.ForeignKey( + name: "FK_GachaItems_Gachas_GachaId", + column: x => x.GachaId, + principalTable: "Gachas", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "GachaItems"); + + migrationBuilder.DropTable( + name: "Gachas"); + + migrationBuilder.CreateTable( + name: "AppStates", + columns: table => new + { + Type = table.Column(type: "INTEGER", nullable: false), + Value = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AppStates", x => x.Type); + }); + + migrationBuilder.CreateTable( + name: "GachaProbabilities", + columns: table => new + { + RandomMessageId = table.Column(type: "TEXT", nullable: false), + Probability = table.Column(type: "REAL", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_GachaProbabilities", x => x.RandomMessageId); + }); + } + } +} diff --git a/Migrations/AppServiceModelSnapshot.cs b/Migrations/AppServiceModelSnapshot.cs index b27d054..8217dec 100644 --- a/Migrations/AppServiceModelSnapshot.cs +++ b/Migrations/AppServiceModelSnapshot.cs @@ -17,31 +17,34 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder.HasAnnotation("ProductVersion", "8.0.10"); - modelBuilder.Entity("Approvers.King.Common.AppState", b => + modelBuilder.Entity("Approvers.King.Common.Gacha", b => { - b.Property("Type") - .HasColumnType("INTEGER"); - - b.Property("Value") - .IsRequired() + b.Property("Id") + .ValueGeneratedOnAdd() .HasColumnType("TEXT"); - b.HasKey("Type"); + b.Property("HitProbabilityPermillage") + .HasColumnType("INTEGER"); - b.ToTable("AppStates"); + b.HasKey("Id"); + + b.ToTable("Gachas"); }); - modelBuilder.Entity("Approvers.King.Common.GachaProbability", b => + modelBuilder.Entity("Approvers.King.Common.GachaItem", b => { + b.Property("GachaId") + .HasColumnType("TEXT"); + b.Property("RandomMessageId") .HasColumnType("TEXT"); - b.Property("Probability") - .HasColumnType("REAL"); + b.Property("ProbabilityPermillage") + .HasColumnType("INTEGER"); - b.HasKey("RandomMessageId"); + b.HasKey("GachaId", "RandomMessageId"); - b.ToTable("GachaProbabilities"); + b.ToTable("GachaItems"); }); modelBuilder.Entity("Approvers.King.Common.Slot", b => @@ -77,6 +80,22 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Users"); }); + + modelBuilder.Entity("Approvers.King.Common.GachaItem", b => + { + b.HasOne("Approvers.King.Common.Gacha", "Gacha") + .WithMany("GachaItems") + .HasForeignKey("GachaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Gacha"); + }); + + modelBuilder.Entity("Approvers.King.Common.Gacha", b => + { + b.Navigation("GachaItems"); + }); #pragma warning restore 612, 618 } } diff --git a/Program.cs b/Program.cs index 26a06d6..99ad03b 100644 --- a/Program.cs +++ b/Program.cs @@ -16,20 +16,48 @@ private static void Main(string[] args) private static async Task BuildAsync(string[] args) { - // 共通基盤系を初期化する + await InitializeModulesAsync(); + await InitializeStatesAsync(); + RegisterEvents(); + + // 永久に待つ + await Task.Delay(-1); + } + + /// + /// 共通基盤系を初期化する + /// + private static async Task InitializeModulesAsync() + { TimeManager.Instance.Initialize(); await MasterManager.FetchAsync(); - await GachaManager.Instance.LoadAsync(); SchedulerManager.Initialize(); await DiscordManager.InitializeAsync(); + } + + private static async Task InitializeStatesAsync() + { + await using var app = AppService.CreateSession(); - if (GachaManager.Instance.IsTableEmpty) + var gacha = await app.GetDefaultGachaAsync(); + if (gacha.GachaItems.Count == 0) { - // 起動時にデータがない場合、ガチャ確率を初期化する - await new DailyResetPresenter().RunAsync(); + gacha.ShuffleMessageRates(); } - // ここからイベント登録 + if (gacha.HitProbability == Multiplier.Zero) + { + gacha.ShuffleRareReplyRate(); + } + + await app.SaveChangesAsync(); + } + + /// + /// 全てのイベントを登録する + /// + private static void RegisterEvents() + { DiscordManager.Client.MessageReceived += message => { OnMessageReceived(message); @@ -40,9 +68,6 @@ private static async Task BuildAsync(string[] args) SchedulerManager.RegisterYearly(TimeManager.Birthday + TimeManager.DailyResetTime + TimeSpan.FromSeconds(1)); SchedulerManager.RegisterMonthly(TimeManager.MonthlyResetDay, TimeManager.DailyResetTime); SchedulerManager.RegisterOn(x => x.Minute is 0); - - // 永久に待つ - await Task.Delay(-1); } ///