Add quick implementation for skills and related classes
This commit is contained in:
parent
3c402eb09d
commit
58a8b7383b
|
|
@ -7,10 +7,15 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FsCheck.Xunit" Version="2.14.2" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
|
||||
<PackageReference Include="xunit" Version="2.4.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
|
||||
<PackageReference Include="coverlet.collector" Version="1.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Seraphina\Seraphina.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</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>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Collections.Concurrent" Version="4.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</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