Add quick implementation for skills and related classes

This commit is contained in:
Liru 2020-05-18 21:11:52 +02:00
parent 3c402eb09d
commit 58a8b7383b
10 changed files with 432 additions and 14 deletions

View File

@ -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>

View File

@ -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.
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -1,14 +0,0 @@
using System;
using Xunit;
namespace Seraphina.Tests
{
public class UnitTest1
{
[Fact]
public void Test1()
{
}
}
}

View File

@ -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>

76
Seraphina/SkillInfo.cs Normal file
View File

@ -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;
}
}
}

11
Seraphina/SkillLevel.cs Normal file
View File

@ -0,0 +1,11 @@
namespace Seraphina
{
public enum SkillLevel
{
Untrained,
Trained,
Skilled,
Advanced,
Master
}
}

54
Seraphina/SkillManager.cs Normal file
View File

@ -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
}
}

44
Seraphina/SkillRepo.cs Normal file
View File

@ -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];
}
}