Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 39 additions & 3 deletions .github/workflows/buildAndTest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
pull_request:
branches: [ "main" ]
jobs:
build:
buildAndTest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -18,5 +18,41 @@ jobs:
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Test
run: dotnet test --no-build --verbosity normal
- name: Test (with Coverage)
# Collect coverage using the built-in data collector (coverlet.collector must be referenced by each test project)
run: dotnet test --no-build --verbosity normal --collect:"XPlat Code Coverage"
# Install ReportGenerator to turn Cobertura XML into HTML + text summary
- name: Install ReportGenerator tool
run: |
dotnet tool install --global dotnet-reportgenerator-globaltool
echo "$HOME/.dotnet/tools" >> $GITHUB_PATH
# Generate HTML and Text summary reports
- name: Generate coverage report
run: |
reportgenerator \
-reports:"**/TestResults/**/coverage.cobertura.xml" \
-targetdir:"coveragereport" \
-reporttypes:"Html;TextSummary"

# Print the summary to the job log
- name: Print coverage summary
run: cat coveragereport/Summary.txt

# Always upload the nice HTML report for inspection (even if threshold fails)
- name: Upload coverage report artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: coveragereport
path: coveragereport

# Fail the build if Line coverage < 50%
- name: Enforce minimum coverage (50% lines)
run: |
LINE_COV=$(grep -Po 'Line coverage:\s*\K[0-9.]+(?=%)' coveragereport/Summary.txt || echo 0)
echo "Detected line coverage: ${LINE_COV}%"
THRESHOLD=50
awk -v a="$LINE_COV" -v b="$THRESHOLD" 'BEGIN {
printf("Minimum required: %s%%\n", b);
exit (a>=b)?0:1
}'
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -416,3 +416,4 @@ FodyWeavers.xsd
*.msix
*.msm
*.msp
/Roguelike.Core.Tests/coveragereport
54 changes: 53 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,54 @@
# oneheim
A detailled roguelike in console

A detailled roguelike in console.

Explore the region :

![image](screenshots/exploring.png)

Discuss with NPCs :

![image](screenshots/dialogue.png)

Fights various enemies :

![image](screenshots/combat.png)

## How to run unit tests and show code coverage

### 1. Run tests and collect data

```powershell
cd Roguelike.Core.Tests
dotnet test --collect:"XPlat Code Coverage"
```

Coverage results will be saved under: `TestResults/<GUID>/coverage.cobertura.xml`

### 2. Use ReportGenerator to create an HTML report

```powershell
reportgenerator -reports:TestResults/**/coverage.cobertura.xml -targetdir:coveragereport
```

### 3. Open the report in your browser

The report will be generated in `coveragereport/index.html`

#### Windows

```powershell
start coveragereport/index.html
```

#### macOS

```bash
open coveragereport/index.html
```

#### Linux

```bash
xdg-open coveragereport/index.html
```
9 changes: 4 additions & 5 deletions Roguelike.Core.Tests/Fakes/FakeInventoryUI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ namespace Roguelike.Core.Tests.Fakes;

public sealed class FakeInventoryUI : IInventoryUI
{
public int PromptDropIndex(Player player, Item newItem, GameSettings settings)
{
return 0;
}

public void Show(object player) { }

private readonly int _dropIndex;
public FakeInventoryUI(int dropIndex) => _dropIndex = dropIndex;
public int PromptDropIndex(Player player, Item newItem, GameSettings settings) => _dropIndex;
}
10 changes: 9 additions & 1 deletion Roguelike.Core.Tests/Fakes/FakeTreasurePicker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,16 @@ namespace Roguelike.Core.Tests.Fakes;

public sealed class FakeTreasurePicker : ITreasurePicker
{
private readonly int _toReturn;
public TreasurePickerContext? LastContext { get; private set; }
public IReadOnlyList<TreasureOptionView>? LastViews { get; private set; }

public FakeTreasurePicker(int toReturn) => _toReturn = toReturn;

public int Pick(TreasurePickerContext context, IReadOnlyList<TreasureOptionView> options)
{
return 0;
LastContext = context;
LastViews = options;
return _toReturn;
}
}
168 changes: 168 additions & 0 deletions Roguelike.Core.Tests/Game/Characters/Enemies/EnemyTypeHelperTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
using Roguelike.Core.Game.Characters.Enemies;

namespace Roguelike.Core.Tests.Game.Characters.Enemies;

[TestClass]
public class EnemyTypeHelperTests
{
[TestMethod]
public void GetRandomEnemyTypesBag_InvalidSize_Throws()
{
int enumCount = Enum.GetNames<EnemyType>().Length;

Assert.ThrowsException<ArgumentOutOfRangeException>(() =>
EnemyTypeHelper.GetRandomEnemyTypesBag(-1));

Assert.ThrowsException<ArgumentOutOfRangeException>(() =>
EnemyTypeHelper.GetRandomEnemyTypesBag(0));

Assert.ThrowsException<ArgumentOutOfRangeException>(() =>
EnemyTypeHelper.GetRandomEnemyTypesBag(enumCount));

Assert.ThrowsException<ArgumentOutOfRangeException>(() =>
EnemyTypeHelper.GetRandomEnemyTypesBag(enumCount + 1));
}

[TestMethod]
public void GetRandomEnemyTypesBag_ValidSize_ReturnsThatManyDistinctValidTypes()
{
int enumCount = Enum.GetNames<EnemyType>().Length;
// valid size is any 1..(enumCount-1)
int size = Math.Max(1, enumCount - 2);

var result = EnemyTypeHelper.GetRandomEnemyTypesBag(size);

Assert.IsNotNull(result);
Assert.AreEqual(size, result.Count, "Returned bag does not match requested size.");

// With current implementation, previously-picked types are excluded,
// so all items should be distinct.
Assert.AreEqual(size, result.Distinct().Count(), "Bag should not contain duplicates with current exclusion logic.");

// All must be known/valid types present in the weights dictionary (and not Unknown).
var validTypes = EnemyTypeHelper.Weights.Keys.ToHashSet();
foreach (var t in result)
{
Assert.AreNotEqual(EnemyType.Unknown, t, "Bag must not contain EnemyType.Unknown.");
Assert.IsTrue(validTypes.Contains(t), $"Bag contains an invalid EnemyType: {t}");
}
}

[TestMethod]
public void GetRandomEnemyTypesBag_NoImmediateDuplicates_AndNoUnknown()
{
int enumCount = Enum.GetNames<EnemyType>().Length;
int size = Math.Max(2, enumCount - 1); // take the max valid size to stress the path

var bag = EnemyTypeHelper.GetRandomEnemyTypesBag(size);

// No immediate duplicates
for (int i = 1; i < bag.Count; i++)
{
Assert.AreNotEqual(bag[i - 1], bag[i], $"Immediate duplicate at indices {i - 1} and {i}");
}

// No Unknown
Assert.IsFalse(bag.Contains(EnemyType.Unknown), "Bag must not contain EnemyType.Unknown.");
}

[TestMethod]
public void GetEnemyIdsByType_ReturnsExpectedLists()
{
// Undead
var undead = EnemyTypeHelper.GetEnemyIdsByType(EnemyType.Undead);
CollectionAssert.AreEquivalent(
new List<EnemyId>
{
EnemyId.LeglessZombie,
EnemyId.Skeleton,
EnemyId.Zombie,
EnemyId.ArmoredZombie,
EnemyId.PlagueGhoul,
EnemyId.Revenant
},
undead,
"Undead enemy IDs mismatch."
);

// Wild
var wild = EnemyTypeHelper.GetEnemyIdsByType(EnemyType.Wild);
CollectionAssert.AreEquivalent(
new List<EnemyId>
{
EnemyId.WildLittleBear,
EnemyId.WildBear,
EnemyId.Wolf,
EnemyId.AlphaWolf,
EnemyId.GiantSpider,
EnemyId.Werewolf
},
wild,
"Wild enemy IDs mismatch."
);

// Outlaws
var outlaws = EnemyTypeHelper.GetEnemyIdsByType(EnemyType.Outlaws);
CollectionAssert.AreEquivalent(
new List<EnemyId>
{
EnemyId.Drunkard,
EnemyId.Pickpocket,
EnemyId.Brigand,
EnemyId.Mercenary,
EnemyId.WatchtowerArcher,
EnemyId.Assassin
},
outlaws,
"Outlaws enemy IDs mismatch."
);

// Cultist
var cultist = EnemyTypeHelper.GetEnemyIdsByType(EnemyType.Cultist);
CollectionAssert.AreEquivalent(
new List<EnemyId>
{
EnemyId.Novice,
EnemyId.Acolyte,
EnemyId.Cultist,
EnemyId.Zealot,
EnemyId.Priest,
EnemyId.Champion
},
cultist,
"Cultist enemy IDs mismatch."
);

// Demon
var demon = EnemyTypeHelper.GetEnemyIdsByType(EnemyType.Demon);
CollectionAssert.AreEquivalent(
new List<EnemyId>
{
EnemyId.Imp,
EnemyId.DemonSlave,
EnemyId.Hellhound,
EnemyId.Overseer,
EnemyId.HellObelisk,
EnemyId.DoomReaper
},
demon,
"Demon enemy IDs mismatch."
);

// Default / unknown
var none = EnemyTypeHelper.GetEnemyIdsByType((EnemyType)999);
Assert.IsNotNull(none);
Assert.AreEqual(0, none.Count, "Unknown enemy type should return an empty list.");
}

[TestMethod]
public void Weights_Definition_IsConsistent()
{
// Ensure that all weighted types are valid enum values (not Unknown) and weights are positive.
foreach (var kv in EnemyTypeHelper.Weights)
{
Assert.AreNotEqual(EnemyType.Unknown, kv.Key, "Weights should not include Unknown.");
Assert.IsTrue(kv.Value > 0, $"Weight for {kv.Key} should be positive.");
}
}
}
Loading
Loading