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:
parent
9704516dd7
commit
8d23f152eb
@ -4,7 +4,8 @@
|
||||
"allow": [
|
||||
"Bash(dotnet build*)",
|
||||
"Bash(dotnet clean*)",
|
||||
"Bash(dotnet restore*)"
|
||||
"Bash(dotnet restore*)",
|
||||
"WebFetch(domain:code.claude.com)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
76
CLAUDE.md
76
CLAUDE.md
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
33
TradingCardMod.Tests.csproj
Normal file
33
TradingCardMod.Tests.csproj
Normal 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>
|
||||
@ -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
157
src/CardParser.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
55
src/TradingCard.cs
Normal 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
402
tests/CardParserTests.cs
Normal 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
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user