From 58a8b7383b2925efd807b7aa6e03c1f144fdbf32 Mon Sep 17 00:00:00 2001 From: Liru Date: Mon, 18 May 2020 21:11:52 +0200 Subject: [PATCH] Add quick implementation for skills and related classes --- Seraphina.Tests/Seraphina.Tests.csproj | 5 + Seraphina.Tests/SkillInfoTest.cs | 70 +++++++++++++ Seraphina.Tests/SkillManagerTest.cs | 28 +++++ Seraphina.Tests/SkillRepoTest.cs | 139 +++++++++++++++++++++++++ Seraphina.Tests/UnitTest1.cs | 14 --- Seraphina/Seraphina.csproj | 5 + Seraphina/SkillInfo.cs | 76 ++++++++++++++ Seraphina/SkillLevel.cs | 11 ++ Seraphina/SkillManager.cs | 54 ++++++++++ Seraphina/SkillRepo.cs | 44 ++++++++ 10 files changed, 432 insertions(+), 14 deletions(-) create mode 100644 Seraphina.Tests/SkillInfoTest.cs create mode 100644 Seraphina.Tests/SkillManagerTest.cs create mode 100644 Seraphina.Tests/SkillRepoTest.cs delete mode 100644 Seraphina.Tests/UnitTest1.cs create mode 100644 Seraphina/SkillInfo.cs create mode 100644 Seraphina/SkillLevel.cs create mode 100644 Seraphina/SkillManager.cs create mode 100644 Seraphina/SkillRepo.cs diff --git a/Seraphina.Tests/Seraphina.Tests.csproj b/Seraphina.Tests/Seraphina.Tests.csproj index a2352e4..626d8c7 100644 --- a/Seraphina.Tests/Seraphina.Tests.csproj +++ b/Seraphina.Tests/Seraphina.Tests.csproj @@ -7,10 +7,15 @@ + + + + + diff --git a/Seraphina.Tests/SkillInfoTest.cs b/Seraphina.Tests/SkillInfoTest.cs new file mode 100644 index 0000000..eff25fd --- /dev/null +++ b/Seraphina.Tests/SkillInfoTest.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FsCheck; +using FsCheck.Xunit; +using Seraphina.Core; +using Xunit; + +namespace Seraphina.Tests +{ + public class SkillInfoTest + { + static readonly List validList = new List { 1, 2, 3, 4 }; + + [Fact] + public void Ctor_ThrowsOnNullAndEmptyId() + { + Assert.Throws("id", () => new SkillInfo(null, "test", validList)); + Assert.Throws("id", () => new SkillInfo("", "test", validList)); + } + + [Fact] + public void Ctor_ThrowsOnNullAndEmptyName() + { + Assert.Throws("name", () => new SkillInfo("test", null, validList)); + Assert.Throws("name", () => new SkillInfo("test", "", validList)); + } + + [Fact] + public void Ctor_ThrowsOnNullAndEmptySkillCosts() + { + Assert.Throws("skillPointCosts", () => new SkillInfo("test", "test", null)); + Assert.Throws("skillPointCosts", () => new SkillInfo("test", "test", new List { })); + } + + + [Fact] + public void Ctor_ThrowsOnNullDescription() + { + Assert.Throws("description", () => new SkillInfo("test", "test", null, validList)); + } + + public static IEnumerable GetSkillInvalidLengths() + { + for (int i = 1; i < SkillInfo.NumLevelUpgrades; i++) + { + yield return new object[] { i }; + } + } + + + [Theory] + [MemberData(nameof(GetSkillInvalidLengths))] + public void Ctor_ThrowsWhenSkillCostsTooShort(int length) + { + var skillCosts = Enumerable.Range(0, length).Select(x => new SkillPoint(x)).ToList(); + Assert.Throws("skillPointCosts", () => new SkillInfo("test", "test", skillCosts)); + } + + [Fact] + public void Ctor_AcceptsNormalParameters() + { + var _ = new SkillInfo("test", "test", validList); + // Shouldn't do anything special here, just pass. + } + + + + } +} diff --git a/Seraphina.Tests/SkillManagerTest.cs b/Seraphina.Tests/SkillManagerTest.cs new file mode 100644 index 0000000..adc7b95 --- /dev/null +++ b/Seraphina.Tests/SkillManagerTest.cs @@ -0,0 +1,28 @@ +using Seraphina.Core; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Xunit; + +namespace Seraphina.Tests +{ + public class SkillManagerTest + { + private static readonly IEnumerable validSkillPointCosts = new[] { 1, 2, 3, 4 }.Select(x => new SkillPoint(x)); + private static SkillInfo MakeSkill(int id) => new SkillInfo($"id_{id}", $"name_{id}", validSkillPointCosts); + + [Fact] + public void Ctor_AddsEventToSkillRepo() + { + var repo = new SkillRepo(); + var mgr = new SkillManager(repo); + + var skill = MakeSkill(1); + + repo.AddSkill(skill); + Assert.Contains(skill.Id, mgr.Levels.Keys); + } + + } +} diff --git a/Seraphina.Tests/SkillRepoTest.cs b/Seraphina.Tests/SkillRepoTest.cs new file mode 100644 index 0000000..5517179 --- /dev/null +++ b/Seraphina.Tests/SkillRepoTest.cs @@ -0,0 +1,139 @@ +using FsCheck; +using FsCheck.Xunit; +using Seraphina.Core; +using System; +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Seraphina.Tests +{ + public class SkillRepoTest + { + [Fact] + public void Ctor_CanAcceptNoParams() + { + var repo = new SkillRepo(); + Assert.Empty(repo.Skills); + } + + [Fact] + public void Ctor_CanAcceptEmptyEnumerable() + { + var repo = new SkillRepo(Enumerable.Empty()); + Assert.Empty(repo.Skills); + } + + private static readonly IEnumerable validSkillPointCosts = new[] { 1, 2, 3, 4 }.Select(x => new SkillPoint(x)); + private static SkillInfo MakeSkill(int id) => new SkillInfo($"id_{id}", $"name_{id}", validSkillPointCosts); + + private static readonly List validSkills = new List + { + MakeSkill(1), + MakeSkill(2), + MakeSkill(3) + }; + + [Fact] + public void Ctor_SavesSkills() + { + var repo = new SkillRepo(validSkills); + + Assert.Equal(3, repo.Skills.Count); + } + + [Fact] + public void Ctor_OverwritesSkillsWithSameId() + { + var skills = new List + { + new SkillInfo("id1", "name1", validSkillPointCosts), + new SkillInfo("id2", "name2", validSkillPointCosts), + new SkillInfo("id1", "name3", validSkillPointCosts), + }; + + var repo = new SkillRepo(skills); + + Assert.Equal(2, repo.Skills.Count); + Assert.Equal("name3", repo["id1"].Name); + } + + [Fact] + public void AddSkill_ThrowsExceptionIfSkillAlreadyExists() + { + // NOTE: May want to remove this later, depending on future needs. + // For instance, overwriting, or skipping. + var repo = new SkillRepo(validSkills); + + Assert.Throws(() => repo.AddSkill(MakeSkill(1))); + } + + [Fact] + public void AddSkill_AddsSkillToRepo() + { + var repo = new SkillRepo(); + + repo.AddSkill(MakeSkill(1)); + + Assert.Single(repo.Skills); + + repo.AddSkill(MakeSkill(2)); + + Assert.Equal(2, repo.Skills.Count); + } + + [Fact] + public void AddSkill_TriggersSkillAddedEvent() + { + var repo = new SkillRepo(); + + var counter = 0; + repo.SkillAdded += (sender, e) => counter++; + + repo.AddSkill(MakeSkill(1)); + + Assert.Equal(1, counter); + + repo.AddSkill(MakeSkill(2)); + + Assert.Equal(2, counter); + } + + [Fact] + public void AddSkill_DoesNotTriggerSkillAddedEventIfInvalidParameter() + { + var repo = new SkillRepo(); + + var counter = 0; + repo.SkillAdded += (sender, e) => counter++; + + repo.AddSkill(MakeSkill(1)); + + Assert.Equal(1, counter); + + try + { + repo.AddSkill(MakeSkill(1)); + } + catch (InvalidOperationException) { } + + Assert.Equal(1, counter); + } + + [Fact] + public void SkillAdded_GivesAddedSkill() + { + var repo = new SkillRepo(); + + var skill = MakeSkill(1); + + var str = ""; + repo.SkillAdded += (obj, e) => str = e; + + repo.AddSkill(skill); + + Assert.Equal(skill.Id, str); + + } + } +} diff --git a/Seraphina.Tests/UnitTest1.cs b/Seraphina.Tests/UnitTest1.cs deleted file mode 100644 index 09aab11..0000000 --- a/Seraphina.Tests/UnitTest1.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using Xunit; - -namespace Seraphina.Tests -{ - public class UnitTest1 - { - [Fact] - public void Test1() - { - - } - } -} diff --git a/Seraphina/Seraphina.csproj b/Seraphina/Seraphina.csproj index c73e0d1..74fe973 100644 --- a/Seraphina/Seraphina.csproj +++ b/Seraphina/Seraphina.csproj @@ -3,6 +3,11 @@ Exe netcoreapp3.1 + enable + + + + diff --git a/Seraphina/SkillInfo.cs b/Seraphina/SkillInfo.cs new file mode 100644 index 0000000..13f2b22 --- /dev/null +++ b/Seraphina/SkillInfo.cs @@ -0,0 +1,76 @@ +#nullable enable + +using Seraphina.Core; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Seraphina +{ + /// + /// Contains information about a skill. + /// + public class SkillInfo + { + public static readonly int NumLevelUpgrades = Enum.GetValues(typeof(SkillLevel)).Length - 1; + + public string Id { get; } + public string Name { get; } + public string Description { get; } + public IReadOnlyList UpgradeCost { get; } + + public SkillInfo(string id, string name, IEnumerable skillPointCosts) + : this(id, name, "", skillPointCosts) { } + + public SkillInfo(string id, string name, string description, IEnumerable skillPointCosts) + { + if (id is null) throw new ArgumentNullException(nameof(id)); + if (id.Length == 0) throw new ArgumentException("Must not be empty", nameof(id)); + if (name is null) throw new ArgumentNullException(nameof(name)); + if (name.Length == 0) throw new ArgumentException("Must not be empty", nameof(name)); + if (description is null) throw new ArgumentNullException(nameof(description)); + + if (skillPointCosts is null) throw new ArgumentNullException(nameof(skillPointCosts)); + + var lst = skillPointCosts.ToList(); + if (lst.Count() < NumLevelUpgrades) + throw new ArgumentException($"Must have at least {NumLevelUpgrades} entries", nameof(skillPointCosts)); + + Id = id; + Name = name; + Description = description; + + UpgradeCost = lst; + } + + /// + /// Gets the cost required to upgrade the skill from the + /// current level. + /// + /// The skill's current level. + /// + /// A value, or null if the skill is at max level. + /// + public SkillPoint? CostToUpgrade(SkillLevel currentLevel) + { + if (currentLevel == SkillLevel.Master) return null; + + return UpgradeCost[(int)currentLevel]; + } + + public SkillPoint CostToMaster(SkillLevel currentLevel = SkillLevel.Untrained) + { + var level = (int)currentLevel; + + SkillPoint sum = 0; + + for (int i = level; i < (int) SkillLevel.Master; i++) + { + sum += UpgradeCost[i]; + } + + return sum; + } + } +} diff --git a/Seraphina/SkillLevel.cs b/Seraphina/SkillLevel.cs new file mode 100644 index 0000000..7584dc2 --- /dev/null +++ b/Seraphina/SkillLevel.cs @@ -0,0 +1,11 @@ +namespace Seraphina +{ + public enum SkillLevel + { + Untrained, + Trained, + Skilled, + Advanced, + Master + } +} diff --git a/Seraphina/SkillManager.cs b/Seraphina/SkillManager.cs new file mode 100644 index 0000000..29a7115 --- /dev/null +++ b/Seraphina/SkillManager.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace Seraphina +{ + /// + /// Manages the skill list for a player, keeping track of their levels. + /// + public class SkillManager : IDisposable + { + private readonly SkillRepo Repo; + public Dictionary Levels { get; } = new Dictionary(); + + public event EventHandler<(SkillInfo, SkillLevel)>? SkillUpgraded; + + public SkillManager(SkillRepo repo) + { + Repo = repo; + Repo.SkillAdded += Repo_OnSkillAdded; + + foreach (var item in repo.Skills.Keys) + { + Levels[item] = SkillLevel.Untrained; + } + } + + private void Repo_OnSkillAdded(object? sender, string e) => Levels[e] = SkillLevel.Untrained; + + #region IDisposable Support + private bool disposedValue = false; // To detect redundant calls + + protected virtual void Dispose(bool disposing) + { + if (disposedValue) return; + + if (disposing) + { + Repo.SkillAdded -= Repo_OnSkillAdded; + } + + disposedValue = true; + + } + + public void Dispose() + { + Dispose(true); + } + #endregion + + } +} diff --git a/Seraphina/SkillRepo.cs b/Seraphina/SkillRepo.cs new file mode 100644 index 0000000..81e260c --- /dev/null +++ b/Seraphina/SkillRepo.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Seraphina +{ + /// + /// Contains a list of all the in-game skills. + /// + public class SkillRepo + { + public ConcurrentDictionary Skills { get; } = new ConcurrentDictionary(); + + public event EventHandler? SkillAdded; + + public SkillRepo() : this(Enumerable.Empty()) { } + public SkillRepo(IEnumerable skills) + { + // TODO: Should skills with duplicate IDs be checked? + foreach (var skill in skills) + { + Skills[skill.Id] = skill; + } + } + + public SkillRepo AddSkill(SkillInfo skill) + { + string id = skill.Id; + if (Skills.ContainsKey(id)) throw new InvalidOperationException($"Skill {skill.Id} already exists."); + + Skills[id] = skill; + + OnSkillAdded(id); + + return this; + } + + protected void OnSkillAdded(string skillId) => SkillAdded?.Invoke(this, skillId); + + public SkillInfo this[string idx] => Skills[idx]; + } +}