Add unit testing framework and refactor parsing logic

- Created TradingCardMod.Tests.csproj with xUnit for testable components
- Extracted CardParser.cs with pure parsing logic (no Unity deps)
- Extracted TradingCard.cs data class from ModBehaviour
- Added 37 unit tests covering parsing, validation, rarity mapping
- Updated cards.txt format with optional description field
- Fixed DLL references (explicit HintPath for paths with spaces)
- Fixed Harmony UnpatchAll API usage
- Updated CLAUDE.md with test commands and current project status

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2025-11-18 19:40:43 -06:00
parent 9704516dd7
commit 8d23f152eb
9 changed files with 756 additions and 25 deletions

View File

@ -4,7 +4,8 @@
"allow": [
"Bash(dotnet build*)",
"Bash(dotnet clean*)",
"Bash(dotnet restore*)"
"Bash(dotnet restore*)",
"WebFetch(domain:code.claude.com)"
]
}
}

View File

@ -10,16 +10,34 @@ A Unity mod for Escape from Duckov that adds a customizable trading card system
```bash
# Build the mod
dotnet build
dotnet build TradingCardMod.csproj
# Build release version
dotnet build -c Release
dotnet build TradingCardMod.csproj -c Release
# Output location: bin/Debug/netstandard2.1/TradingCardMod.dll
```
**Important**: Before building, update `DuckovPath` in `TradingCardMod.csproj` (line 10) to your actual game installation path.
## Testing
```bash
# Run all unit tests
dotnet test TradingCardMod.Tests.csproj
# Run tests with verbose output
dotnet test TradingCardMod.Tests.csproj --verbosity normal
# Run specific test class
dotnet test TradingCardMod.Tests.csproj --filter "FullyQualifiedName~CardParserTests"
# Run single test
dotnet test TradingCardMod.Tests.csproj --filter "ParseLine_ValidLineWithoutDescription_ReturnsCard"
```
**Test Coverage:** Parsing logic, validation, rarity mapping, TypeID generation, and description auto-generation are all tested. Unity-dependent code (item creation, tags) cannot be unit tested.
## Architecture
### Mod Loading System
@ -72,9 +90,53 @@ Key namespaces and APIs from the game:
3. Launch game and enable mod in Mods menu
4. Check game logs for `[TradingCardMod]` messages
## TODO Items in Code
## Current Project Status
The following features need implementation (marked with TODO comments):
- `RegisterCardWithGame()` - Create Unity prefabs and register with ItemAssetsCollection
- Item cleanup in `OnDestroy()` - Remove registered items on mod unload
- `TradingCard.ItemPrefab` property - Store Unity prefab reference
**Phase:** 2 - Core Card Framework
**Project Plan:** `.claude/scratchpad/PROJECT_PLAN.md`
**Technical Analysis:** `.claude/scratchpad/item-system-analysis.md`
### Implementation Approach: Clone + Reflection
Based on analysis of the AdditionalCollectibles mod, we're using a viable approach:
1. **Clone existing game items** as templates (not create from scratch)
2. **Use reflection** to set private fields (typeID, weight, value, etc.)
3. **Create custom tags** by cloning existing ScriptableObject tags
4. **Load sprites** from user files in `CardSets/*/images/`
Key patterns:
```csharp
// Clone base item
Item original = ItemAssetsCollection.GetPrefab(135);
GameObject clone = Object.Instantiate(original.gameObject);
Object.DontDestroyOnLoad(clone);
// Set properties via reflection
item.SetPrivateField("typeID", newId);
item.SetPrivateField("value", cardValue);
// Get/create tags
Tag tag = Resources.FindObjectsOfTypeAll<Tag>()
.FirstOrDefault(t => t.name == "Luxury");
```
### Next Implementation Steps
1. Create `src/ItemExtensions.cs` - reflection helper methods
2. Create `src/TagHelper.cs` - tag operations
3. Update `src/ModBehaviour.cs` - use clone+reflection approach
4. Test card creation in-game
### Files to Create
| File | Purpose |
|------|---------|
| `src/ItemExtensions.cs` | `SetPrivateField()`, `GetPrivateField()` extensions |
| `src/TagHelper.cs` | `GetTargetTag()`, `CreateOrCloneTag()` |
## Research References
- **Decompiled game code:** `.claude/scratchpad/decompiled/`
- **Item system analysis:** `.claude/scratchpad/item-system-analysis.md`
- **AdditionalCollectibles mod:** Workshop ID 3591453758 (reference implementation)

View File

@ -1,20 +1,21 @@
# Example Card Set - Trading Card Mod for Escape from Duckov
# Format: CardName | SetName | SetNumber | ImageFile | Rarity | Weight | Value
# Format: CardName | SetName | SetNumber | ImageFile | Rarity | Weight | Value | Description (optional)
#
# Fields:
# CardName - Display name of the card
# SetName - Name of the card collection
# SetNumber - Number within the set (for sorting)
# ImageFile - Filename of the card image (must be in images/ subfolder)
# Rarity - Card rarity (Common, Uncommon, Rare, Ultra Rare)
# Weight - Physical weight in game units
# Value - In-game currency value
# CardName - Display name of the card
# SetName - Name of the card collection
# SetNumber - Number within the set (for sorting)
# ImageFile - Filename of the card image (must be in images/ subfolder)
# Rarity - Card rarity. Valid values: Common, Uncommon, Rare, Very Rare, Ultra Rare, Legendary
# Weight - Physical weight in game units
# Value - In-game currency value
# Description - (Optional) Custom tooltip text. If omitted, auto-generates as "SetName #SetNumber - Rarity"
#
# Add your own cards below! Just follow the format above.
# Place corresponding images in the images/ subfolder.
Duck Hero | Example Set | 001 | duck_hero.png | Rare | 0.01 | 100
Golden Quacker | Example Set | 002 | golden_quacker.png | Ultra Rare | 0.01 | 500
Duck Hero | Example Set | 001 | duck_hero.png | Rare | 0.01 | 100 | The brave defender of all ponds
Golden Quacker | Example Set | 002 | golden_quacker.png | Ultra Rare | 0.01 | 500 | A legendary duck made of pure gold
Pond Guardian | Example Set | 003 | pond_guardian.png | Uncommon | 0.01 | 25
Bread Seeker | Example Set | 004 | bread_seeker.png | Common | 0.01 | 10
Feathered Fury | Example Set | 005 | feathered_fury.png | Rare | 0.01 | 75
Feathered Fury | Example Set | 005 | feathered_fury.png | Rare | 0.01 | 75 | Known for its fierce battle cry

View File

@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<RootNamespace>TradingCardMod.Tests</RootNamespace>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.6.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<!-- Test files -->
<Compile Include="tests/**/*.cs" />
<!-- Reference the main project's testable code -->
<Compile Include="src/CardParser.cs" Link="CardParser.cs" />
<Compile Include="src/TradingCard.cs" Link="TradingCard.cs" />
</ItemGroup>
</Project>

View File

@ -20,10 +20,30 @@
<ItemGroup>
<!-- Game DLL references -->
<Reference Include="$(DuckovPath)$(SubPath)TeamSoda.*"/>
<Reference Include="$(DuckovPath)$(SubPath)ItemStatsSystem.dll"/>
<Reference Include="$(DuckovPath)$(SubPath)Unity*"/>
<Reference Include="$(DuckovPath)$(SubPath)Duckov.dll"/>
<Reference Include="TeamSoda.Duckov.Core">
<HintPath>$(DuckovPath)$(SubPath)TeamSoda.Duckov.Core.dll</HintPath>
</Reference>
<Reference Include="TeamSoda.Duckov.Utilities">
<HintPath>$(DuckovPath)$(SubPath)TeamSoda.Duckov.Utilities.dll</HintPath>
</Reference>
<Reference Include="TeamSoda.MiniLocalizor">
<HintPath>$(DuckovPath)$(SubPath)TeamSoda.MiniLocalizor.dll</HintPath>
</Reference>
<Reference Include="SodaLocalization">
<HintPath>$(DuckovPath)$(SubPath)SodaLocalization.dll</HintPath>
</Reference>
<Reference Include="ItemStatsSystem">
<HintPath>$(DuckovPath)$(SubPath)ItemStatsSystem.dll</HintPath>
</Reference>
<Reference Include="Duckov">
<HintPath>$(DuckovPath)$(SubPath)Duckov.dll</HintPath>
</Reference>
<Reference Include="UnityEngine">
<HintPath>$(DuckovPath)$(SubPath)UnityEngine.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.CoreModule">
<HintPath>$(DuckovPath)$(SubPath)UnityEngine.CoreModule.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
@ -35,6 +55,6 @@
<!-- Exclude non-code files from compilation -->
<None Include="CardSets/**/*" CopyToOutputDirectory="PreserveNewest" />
<None Include="info.ini" CopyToOutputDirectory="PreserveNewest" />
<None Include="preview.png" CopyToOutputDirectory="PreserveNewest" />
<None Include="preview.png" CopyToOutputDirectory="PreserveNewest" Condition="Exists('preview.png')" />
</ItemGroup>
</Project>

157
src/CardParser.cs Normal file
View File

@ -0,0 +1,157 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace TradingCardMod
{
/// <summary>
/// Handles parsing of card definition files.
/// This class contains no Unity dependencies and is fully testable.
/// </summary>
public static class CardParser
{
/// <summary>
/// Maps rarity string to game quality level.
/// Quality levels: 2=Common, 3=Uncommon, 4=Rare, 5=Very Rare, 6=Ultra Rare/Legendary
/// </summary>
public static int RarityToQuality(string rarity)
{
return rarity.Trim().ToLowerInvariant() switch
{
"common" => 2,
"uncommon" => 3,
"rare" => 4,
"very rare" => 5,
"ultra rare" => 6,
"legendary" => 6,
_ => 3 // Default to uncommon
};
}
/// <summary>
/// Parses a single line from cards.txt into a TradingCard object.
/// Format: CardName | SetName | SetNumber | ImageFile | Rarity | Weight | Value | Description (optional)
/// </summary>
/// <param name="line">The line to parse</param>
/// <returns>A TradingCard if parsing succeeds, null otherwise</returns>
public static TradingCard? ParseLine(string line)
{
if (string.IsNullOrWhiteSpace(line))
return null;
// Skip comments
if (line.TrimStart().StartsWith("#"))
return null;
string[] parts = line.Split('|');
if (parts.Length < 7)
return null;
try
{
var card = new TradingCard
{
CardName = parts[0].Trim(),
SetName = parts[1].Trim(),
SetNumber = int.Parse(parts[2].Trim()),
ImageFile = parts[3].Trim(),
Rarity = parts[4].Trim(),
Weight = float.Parse(parts[5].Trim()),
Value = int.Parse(parts[6].Trim())
};
// Optional description field
if (parts.Length >= 8 && !string.IsNullOrWhiteSpace(parts[7]))
{
card.Description = parts[7].Trim();
}
return card;
}
catch
{
return null;
}
}
/// <summary>
/// Parses all cards from a cards.txt file.
/// </summary>
/// <param name="filePath">Path to the cards.txt file</param>
/// <param name="imagesDirectory">Directory containing card images</param>
/// <returns>List of parsed cards with ImagePath set</returns>
public static List<TradingCard> ParseFile(string filePath, string imagesDirectory)
{
var cards = new List<TradingCard>();
if (!File.Exists(filePath))
return cards;
string[] lines = File.ReadAllLines(filePath);
foreach (string line in lines)
{
TradingCard? card = ParseLine(line);
if (card != null)
{
card.ImagePath = Path.Combine(imagesDirectory, card.ImageFile);
cards.Add(card);
}
}
return cards;
}
/// <summary>
/// Validates a TradingCard for required fields.
/// </summary>
/// <param name="card">The card to validate</param>
/// <returns>List of validation errors, empty if valid</returns>
public static List<string> ValidateCard(TradingCard card)
{
var errors = new List<string>();
if (string.IsNullOrWhiteSpace(card.CardName))
errors.Add("CardName is required");
if (string.IsNullOrWhiteSpace(card.SetName))
errors.Add("SetName is required");
if (card.SetNumber < 0)
errors.Add("SetNumber must be non-negative");
if (string.IsNullOrWhiteSpace(card.ImageFile))
errors.Add("ImageFile is required");
if (string.IsNullOrWhiteSpace(card.Rarity))
errors.Add("Rarity is required");
if (card.Weight < 0)
errors.Add("Weight must be non-negative");
if (card.Value < 0)
errors.Add("Value must be non-negative");
return errors;
}
/// <summary>
/// Checks if a rarity string is valid.
/// </summary>
public static bool IsValidRarity(string rarity)
{
string normalized = rarity.Trim().ToLowerInvariant();
return normalized switch
{
"common" => true,
"uncommon" => true,
"rare" => true,
"very rare" => true,
"ultra rare" => true,
"legendary" => true,
_ => false
};
}
}
}

View File

@ -43,7 +43,7 @@ namespace TradingCardMod
{
try
{
_harmony?.UnpatchSelf();
_harmony?.UnpatchAll(HarmonyId);
Debug.Log($"[TradingCardMod] Harmony patches removed");
}
catch (Exception ex)

55
src/TradingCard.cs Normal file
View File

@ -0,0 +1,55 @@
using System;
namespace TradingCardMod
{
/// <summary>
/// Represents a trading card's data loaded from a card set file.
/// This class contains no Unity dependencies and is fully testable.
/// </summary>
public class TradingCard
{
public string CardName { get; set; } = string.Empty;
public string SetName { get; set; } = string.Empty;
public int SetNumber { get; set; }
public string ImageFile { get; set; } = string.Empty;
public string Rarity { get; set; } = string.Empty;
public float Weight { get; set; }
public int Value { get; set; }
public string? Description { get; set; }
// Set at runtime after loading
public string ImagePath { get; set; } = string.Empty;
/// <summary>
/// Generates a unique TypeID for this card to avoid conflicts.
/// Uses hash of set name + card name for uniqueness.
/// </summary>
public int GenerateTypeID()
{
// Start from a high number to avoid conflicts with base game items
// Use hash to ensure consistency across loads
string uniqueKey = $"TradingCard_{SetName}_{CardName}";
return 100000 + Math.Abs(uniqueKey.GetHashCode() % 900000);
}
/// <summary>
/// Gets the quality level (0-6) based on rarity string.
/// </summary>
public int GetQuality()
{
return CardParser.RarityToQuality(Rarity);
}
/// <summary>
/// Gets the description, auto-generating if not provided.
/// </summary>
public string GetDescription()
{
if (!string.IsNullOrWhiteSpace(Description))
{
return Description;
}
return $"{SetName} #{SetNumber:D3} - {Rarity}";
}
}
}

402
tests/CardParserTests.cs Normal file
View File

@ -0,0 +1,402 @@
using Xunit;
using TradingCardMod;
namespace TradingCardMod.Tests
{
/// <summary>
/// Unit tests for the CardParser class.
/// Tests cover parsing, validation, and rarity mapping functionality.
/// </summary>
public class CardParserTests
{
#region ParseLine Tests
[Fact]
public void ParseLine_ValidLineWithoutDescription_ReturnsCard()
{
// Arrange
string line = "Duck Hero | Example Set | 001 | duck_hero.png | Rare | 0.01 | 100";
// Act
var card = CardParser.ParseLine(line);
// Assert
Assert.NotNull(card);
Assert.Equal("Duck Hero", card.CardName);
Assert.Equal("Example Set", card.SetName);
Assert.Equal(1, card.SetNumber);
Assert.Equal("duck_hero.png", card.ImageFile);
Assert.Equal("Rare", card.Rarity);
Assert.Equal(0.01f, card.Weight);
Assert.Equal(100, card.Value);
Assert.Null(card.Description);
}
[Fact]
public void ParseLine_ValidLineWithDescription_ReturnsCardWithDescription()
{
// Arrange
string line = "Golden Quacker | Example Set | 002 | golden_quacker.png | Ultra Rare | 0.01 | 500 | A legendary duck made of pure gold";
// Act
var card = CardParser.ParseLine(line);
// Assert
Assert.NotNull(card);
Assert.Equal("Golden Quacker", card.CardName);
Assert.Equal("A legendary duck made of pure gold", card.Description);
}
[Fact]
public void ParseLine_CommentLine_ReturnsNull()
{
// Arrange
string line = "# This is a comment";
// Act
var card = CardParser.ParseLine(line);
// Assert
Assert.Null(card);
}
[Fact]
public void ParseLine_EmptyLine_ReturnsNull()
{
// Arrange
string line = "";
// Act
var card = CardParser.ParseLine(line);
// Assert
Assert.Null(card);
}
[Fact]
public void ParseLine_WhitespaceLine_ReturnsNull()
{
// Arrange
string line = " \t ";
// Act
var card = CardParser.ParseLine(line);
// Assert
Assert.Null(card);
}
[Fact]
public void ParseLine_TooFewFields_ReturnsNull()
{
// Arrange - only 6 fields instead of required 7
string line = "Duck Hero | Example Set | 001 | duck_hero.png | Rare | 0.01";
// Act
var card = CardParser.ParseLine(line);
// Assert
Assert.Null(card);
}
[Fact]
public void ParseLine_InvalidNumber_ReturnsNull()
{
// Arrange - SetNumber is not a valid integer
string line = "Duck Hero | Example Set | ABC | duck_hero.png | Rare | 0.01 | 100";
// Act
var card = CardParser.ParseLine(line);
// Assert
Assert.Null(card);
}
[Fact]
public void ParseLine_InvalidWeight_ReturnsNull()
{
// Arrange - Weight is not a valid float
string line = "Duck Hero | Example Set | 001 | duck_hero.png | Rare | heavy | 100";
// Act
var card = CardParser.ParseLine(line);
// Assert
Assert.Null(card);
}
[Fact]
public void ParseLine_TrimsWhitespace_ReturnsCleanCard()
{
// Arrange - extra whitespace around values
string line = " Duck Hero | Example Set | 001 | duck_hero.png | Rare | 0.01 | 100 ";
// Act
var card = CardParser.ParseLine(line);
// Assert
Assert.NotNull(card);
Assert.Equal("Duck Hero", card.CardName);
Assert.Equal("Example Set", card.SetName);
}
#endregion
#region RarityToQuality Tests
[Theory]
[InlineData("Common", 2)]
[InlineData("common", 2)]
[InlineData("COMMON", 2)]
[InlineData("Uncommon", 3)]
[InlineData("Rare", 4)]
[InlineData("Very Rare", 5)]
[InlineData("Ultra Rare", 6)]
[InlineData("Legendary", 6)]
public void RarityToQuality_ValidRarity_ReturnsCorrectQuality(string rarity, int expectedQuality)
{
// Act
int quality = CardParser.RarityToQuality(rarity);
// Assert
Assert.Equal(expectedQuality, quality);
}
[Fact]
public void RarityToQuality_UnknownRarity_ReturnsDefault()
{
// Arrange - unknown rarity should default to uncommon (3)
string rarity = "Super Duper Rare";
// Act
int quality = CardParser.RarityToQuality(rarity);
// Assert
Assert.Equal(3, quality);
}
[Fact]
public void RarityToQuality_WithExtraWhitespace_ReturnsCorrectQuality()
{
// Arrange
string rarity = " Rare ";
// Act
int quality = CardParser.RarityToQuality(rarity);
// Assert
Assert.Equal(4, quality);
}
#endregion
#region IsValidRarity Tests
[Theory]
[InlineData("Common", true)]
[InlineData("Uncommon", true)]
[InlineData("Rare", true)]
[InlineData("Very Rare", true)]
[InlineData("Ultra Rare", true)]
[InlineData("Legendary", true)]
[InlineData("Invalid", false)]
[InlineData("Super Rare", false)]
[InlineData("", false)]
public void IsValidRarity_ReturnsExpectedResult(string rarity, bool expected)
{
// Act
bool result = CardParser.IsValidRarity(rarity);
// Assert
Assert.Equal(expected, result);
}
#endregion
#region ValidateCard Tests
[Fact]
public void ValidateCard_ValidCard_ReturnsNoErrors()
{
// Arrange
var card = new TradingCard
{
CardName = "Duck Hero",
SetName = "Example Set",
SetNumber = 1,
ImageFile = "duck_hero.png",
Rarity = "Rare",
Weight = 0.01f,
Value = 100
};
// Act
var errors = CardParser.ValidateCard(card);
// Assert
Assert.Empty(errors);
}
[Fact]
public void ValidateCard_MissingCardName_ReturnsError()
{
// Arrange
var card = new TradingCard
{
CardName = "",
SetName = "Example Set",
SetNumber = 1,
ImageFile = "duck_hero.png",
Rarity = "Rare",
Weight = 0.01f,
Value = 100
};
// Act
var errors = CardParser.ValidateCard(card);
// Assert
Assert.Single(errors);
Assert.Contains("CardName", errors[0]);
}
[Fact]
public void ValidateCard_NegativeValue_ReturnsError()
{
// Arrange
var card = new TradingCard
{
CardName = "Duck Hero",
SetName = "Example Set",
SetNumber = 1,
ImageFile = "duck_hero.png",
Rarity = "Rare",
Weight = 0.01f,
Value = -100
};
// Act
var errors = CardParser.ValidateCard(card);
// Assert
Assert.Single(errors);
Assert.Contains("Value", errors[0]);
}
[Fact]
public void ValidateCard_MultipleErrors_ReturnsAllErrors()
{
// Arrange - missing card name and negative weight
var card = new TradingCard
{
CardName = "",
SetName = "Example Set",
SetNumber = 1,
ImageFile = "duck_hero.png",
Rarity = "Rare",
Weight = -0.01f,
Value = 100
};
// Act
var errors = CardParser.ValidateCard(card);
// Assert
Assert.Equal(2, errors.Count);
}
#endregion
#region TradingCard Tests
[Fact]
public void TradingCard_GenerateTypeID_ReturnsConsistentValue()
{
// Arrange
var card = new TradingCard
{
CardName = "Duck Hero",
SetName = "Example Set"
};
// Act
int id1 = card.GenerateTypeID();
int id2 = card.GenerateTypeID();
// Assert - same card should always generate same ID
Assert.Equal(id1, id2);
Assert.True(id1 >= 100000);
Assert.True(id1 < 1000000);
}
[Fact]
public void TradingCard_GenerateTypeID_DifferentCardsGetDifferentIDs()
{
// Arrange
var card1 = new TradingCard { CardName = "Duck Hero", SetName = "Set A" };
var card2 = new TradingCard { CardName = "Golden Quacker", SetName = "Set A" };
// Act
int id1 = card1.GenerateTypeID();
int id2 = card2.GenerateTypeID();
// Assert
Assert.NotEqual(id1, id2);
}
[Fact]
public void TradingCard_GetDescription_WithCustomDescription_ReturnsCustom()
{
// Arrange
var card = new TradingCard
{
CardName = "Duck Hero",
SetName = "Example Set",
SetNumber = 1,
Rarity = "Rare",
Description = "Custom description"
};
// Act
string description = card.GetDescription();
// Assert
Assert.Equal("Custom description", description);
}
[Fact]
public void TradingCard_GetDescription_WithoutDescription_ReturnsAutoGenerated()
{
// Arrange
var card = new TradingCard
{
CardName = "Duck Hero",
SetName = "Example Set",
SetNumber = 1,
Rarity = "Rare",
Description = null
};
// Act
string description = card.GetDescription();
// Assert
Assert.Equal("Example Set #001 - Rare", description);
}
[Fact]
public void TradingCard_GetQuality_ReturnsCorrectQuality()
{
// Arrange
var card = new TradingCard { Rarity = "Ultra Rare" };
// Act
int quality = card.GetQuality();
// Assert
Assert.Equal(6, quality);
}
#endregion
}
}