Add quick implementation for skills and related classes
This commit is contained in:
parent
3c402eb09d
commit
58a8b7383b
|
|
@ -7,10 +7,15 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="FsCheck.Xunit" Version="2.14.2" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
|
||||||
<PackageReference Include="xunit" Version="2.4.0" />
|
<PackageReference Include="xunit" Version="2.4.0" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
|
||||||
<PackageReference Include="coverlet.collector" Version="1.0.1" />
|
<PackageReference Include="coverlet.collector" Version="1.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Seraphina\Seraphina.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -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<SkillPoint> validList = new List<SkillPoint> { 1, 2, 3, 4 };
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Ctor_ThrowsOnNullAndEmptyId()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>("id", () => new SkillInfo(null, "test", validList));
|
||||||
|
Assert.Throws<ArgumentException>("id", () => new SkillInfo("", "test", validList));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Ctor_ThrowsOnNullAndEmptyName()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>("name", () => new SkillInfo("test", null, validList));
|
||||||
|
Assert.Throws<ArgumentException>("name", () => new SkillInfo("test", "", validList));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Ctor_ThrowsOnNullAndEmptySkillCosts()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>("skillPointCosts", () => new SkillInfo("test", "test", null));
|
||||||
|
Assert.Throws<ArgumentException>("skillPointCosts", () => new SkillInfo("test", "test", new List<SkillPoint> { }));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Ctor_ThrowsOnNullDescription()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>("description", () => new SkillInfo("test", "test", null, validList));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<object[]> 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<ArgumentException>("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.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<SkillPoint> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<SkillInfo>());
|
||||||
|
Assert.Empty(repo.Skills);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly IEnumerable<SkillPoint> 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<SkillInfo> validSkills = new List<SkillInfo>
|
||||||
|
{
|
||||||
|
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<SkillInfo>
|
||||||
|
{
|
||||||
|
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<InvalidOperationException>(() => 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);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
using System;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace Seraphina.Tests
|
|
||||||
{
|
|
||||||
public class UnitTest1
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public void Test1()
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -3,6 +3,11 @@
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="System.Collections.Concurrent" Version="4.3.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using Seraphina.Core;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Seraphina
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contains information about a skill.
|
||||||
|
/// </summary>
|
||||||
|
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<SkillPoint> UpgradeCost { get; }
|
||||||
|
|
||||||
|
public SkillInfo(string id, string name, IEnumerable<SkillPoint> skillPointCosts)
|
||||||
|
: this(id, name, "", skillPointCosts) { }
|
||||||
|
|
||||||
|
public SkillInfo(string id, string name, string description, IEnumerable<SkillPoint> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="SkillPoint"/> cost required to upgrade the skill from the
|
||||||
|
/// current level.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="currentLevel">The skill's current level.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// A <see cref="SkillPoint"/> value, or null if the skill is at max level.
|
||||||
|
/// </returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
namespace Seraphina
|
||||||
|
{
|
||||||
|
public enum SkillLevel
|
||||||
|
{
|
||||||
|
Untrained,
|
||||||
|
Trained,
|
||||||
|
Skilled,
|
||||||
|
Advanced,
|
||||||
|
Master
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Seraphina
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Manages the skill list for a player, keeping track of their levels.
|
||||||
|
/// </summary>
|
||||||
|
public class SkillManager : IDisposable
|
||||||
|
{
|
||||||
|
private readonly SkillRepo Repo;
|
||||||
|
public Dictionary<string, SkillLevel> Levels { get; } = new Dictionary<string, SkillLevel>();
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Seraphina
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Contains a list of all the in-game skills.
|
||||||
|
/// </summary>
|
||||||
|
public class SkillRepo
|
||||||
|
{
|
||||||
|
public ConcurrentDictionary<string, SkillInfo> Skills { get; } = new ConcurrentDictionary<string, SkillInfo>();
|
||||||
|
|
||||||
|
public event EventHandler<string>? SkillAdded;
|
||||||
|
|
||||||
|
public SkillRepo() : this(Enumerable.Empty<SkillInfo>()) { }
|
||||||
|
public SkillRepo(IEnumerable<SkillInfo> 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];
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue