Fix bugs, add endgame/completion features, adventure dedup, and comprehensive renderer tests
Bug fixes:
- Fix double "NEW FEATURE UNLOCKED" message and broken enum-to-key mapping for all UIFeatures
- Fix Spectre markup crash when opening inventory with colors unlocked (unescaped rarity brackets)
- Fix latent Spectre markup crash in ResourcePanel (unescaped bar brackets)
- Fix WeightedRandom.PickMultiple picking with replacement causing duplicate drops
- Fix double AddItem bug (BoxEngine + RenderEvents both adding to state)
- Add StatType and FontStyle fields to ItemDefinition (were in JSON but missing from C# record)
New features:
- Endgame box (Mythic) that appears when all 8 resources are discovered, contains Crown of Completion
- Completion percentage tracker as mid-game meta unlock (tier 3), shown in both renderers
- Adventure pool depletion: adventure sub-boxes with already-unlocked themes are removed from loot pool
- Global error handling with log file output (openthebox-error.log)
- Tiered meta progression: 5 sequential meta boxes replacing single box_meta
New tests (180 new, 228 total):
- 5 panel rendering test classes covering all enum values (Portrait, Resource, Stats, Inventory, Chat)
- RenderContext and RendererFactory logic tests
- SpectreRenderer output tests across 4 context configurations
- BasicRenderer output and input method tests
- Simulation state mutation and full-run completion tests
2026-03-11 09:34:30 +01:00
|
|
|
using OpenTheBox.Core;
|
|
|
|
|
using OpenTheBox.Core.Characters;
|
|
|
|
|
using OpenTheBox.Core.Enums;
|
|
|
|
|
using OpenTheBox.Core.Items;
|
|
|
|
|
using OpenTheBox.Localization;
|
|
|
|
|
using OpenTheBox.Rendering;
|
|
|
|
|
using OpenTheBox.Rendering.Panels;
|
|
|
|
|
using Spectre.Console;
|
|
|
|
|
using Spectre.Console.Rendering;
|
|
|
|
|
|
|
|
|
|
namespace OpenTheBox.Tests;
|
|
|
|
|
|
|
|
|
|
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Renders an IRenderable to string using a local AnsiConsole. No shared state.
|
|
|
|
|
/// </summary>
|
|
|
|
|
static class RenderHelper
|
|
|
|
|
{
|
|
|
|
|
public static string RenderToString(IRenderable renderable)
|
|
|
|
|
{
|
|
|
|
|
var writer = new StringWriter();
|
|
|
|
|
var console = AnsiConsole.Create(new AnsiConsoleSettings
|
|
|
|
|
{
|
|
|
|
|
Out = new AnsiConsoleOutput(writer),
|
|
|
|
|
Ansi = AnsiSupport.Yes,
|
|
|
|
|
ColorSystem = ColorSystemSupport.TrueColor
|
|
|
|
|
});
|
|
|
|
|
console.Write(renderable);
|
|
|
|
|
return writer.ToString();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Captures AnsiConsole.Console and Console.Out output for renderer tests.
|
|
|
|
|
/// MUST NOT be used in parallel — use [Collection("ConsoleTests")].
|
|
|
|
|
/// </summary>
|
|
|
|
|
sealed class ConsoleCapture : IDisposable
|
|
|
|
|
{
|
|
|
|
|
private readonly IAnsiConsole _origAnsi;
|
|
|
|
|
private readonly TextWriter _origOut;
|
|
|
|
|
private readonly StringWriter _ansiWriter = new();
|
|
|
|
|
private readonly StringWriter _consoleWriter = new();
|
|
|
|
|
|
|
|
|
|
public ConsoleCapture()
|
|
|
|
|
{
|
|
|
|
|
_origAnsi = AnsiConsole.Console;
|
|
|
|
|
_origOut = Console.Out;
|
|
|
|
|
|
|
|
|
|
AnsiConsole.Console = AnsiConsole.Create(new AnsiConsoleSettings
|
|
|
|
|
{
|
|
|
|
|
Out = new AnsiConsoleOutput(_ansiWriter),
|
|
|
|
|
Interactive = InteractionSupport.No,
|
|
|
|
|
Ansi = AnsiSupport.Yes,
|
|
|
|
|
ColorSystem = ColorSystemSupport.TrueColor
|
|
|
|
|
});
|
|
|
|
|
Console.SetOut(_consoleWriter);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public string Output => _ansiWriter.ToString() + _consoleWriter.ToString();
|
|
|
|
|
|
|
|
|
|
public void Dispose()
|
|
|
|
|
{
|
|
|
|
|
AnsiConsole.Console = _origAnsi;
|
|
|
|
|
Console.SetOut(_origOut);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Panel Tests ──────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
public class PortraitPanelTests
|
|
|
|
|
{
|
|
|
|
|
[Fact]
|
|
|
|
|
public void Render_DefaultAppearance_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var result = RenderHelper.RenderToString(PortraitPanel.Render(new PlayerAppearance()));
|
|
|
|
|
Assert.NotEmpty(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void Render_FullyEquipped_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var appearance = new PlayerAppearance
|
|
|
|
|
{
|
|
|
|
|
HairStyle = HairStyle.Cyberpunk,
|
|
|
|
|
HairTint = TintColor.Neon,
|
|
|
|
|
EyeStyle = EyeStyle.CyberneticEyes,
|
|
|
|
|
BodyStyle = BodyStyle.Armored,
|
|
|
|
|
BodyTint = TintColor.Gold,
|
|
|
|
|
LegStyle = LegStyle.RocketBoots,
|
|
|
|
|
ArmStyle = ArmStyle.Wings
|
|
|
|
|
};
|
|
|
|
|
var result = RenderHelper.RenderToString(PortraitPanel.Render(appearance));
|
|
|
|
|
Assert.NotEmpty(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Theory]
|
|
|
|
|
[MemberData(nameof(AllHairStyles))]
|
|
|
|
|
public void Render_EachHairStyle_DoesNotThrow(HairStyle style)
|
|
|
|
|
{
|
|
|
|
|
var result = RenderHelper.RenderToString(
|
|
|
|
|
PortraitPanel.Render(new PlayerAppearance { HairStyle = style }));
|
|
|
|
|
Assert.NotEmpty(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Theory]
|
|
|
|
|
[MemberData(nameof(AllEyeStyles))]
|
|
|
|
|
public void Render_EachEyeStyle_DoesNotThrow(EyeStyle style)
|
|
|
|
|
{
|
|
|
|
|
var result = RenderHelper.RenderToString(
|
|
|
|
|
PortraitPanel.Render(new PlayerAppearance { EyeStyle = style }));
|
|
|
|
|
Assert.NotEmpty(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Theory]
|
|
|
|
|
[MemberData(nameof(AllBodyStyles))]
|
|
|
|
|
public void Render_EachBodyStyle_DoesNotThrow(BodyStyle style)
|
|
|
|
|
{
|
|
|
|
|
var result = RenderHelper.RenderToString(
|
|
|
|
|
PortraitPanel.Render(new PlayerAppearance { BodyStyle = style }));
|
|
|
|
|
Assert.NotEmpty(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Theory]
|
|
|
|
|
[MemberData(nameof(AllLegStyles))]
|
|
|
|
|
public void Render_EachLegStyle_DoesNotThrow(LegStyle style)
|
|
|
|
|
{
|
|
|
|
|
var result = RenderHelper.RenderToString(
|
|
|
|
|
PortraitPanel.Render(new PlayerAppearance { LegStyle = style }));
|
|
|
|
|
Assert.NotEmpty(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Theory]
|
|
|
|
|
[MemberData(nameof(AllArmStyles))]
|
|
|
|
|
public void Render_EachArmStyle_DoesNotThrow(ArmStyle style)
|
|
|
|
|
{
|
|
|
|
|
var result = RenderHelper.RenderToString(
|
|
|
|
|
PortraitPanel.Render(new PlayerAppearance { ArmStyle = style }));
|
|
|
|
|
Assert.NotEmpty(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Theory]
|
|
|
|
|
[MemberData(nameof(AllTintColors))]
|
|
|
|
|
public void Render_EachHairTint_DoesNotThrow(TintColor tint)
|
|
|
|
|
{
|
|
|
|
|
var result = RenderHelper.RenderToString(
|
|
|
|
|
PortraitPanel.Render(new PlayerAppearance { HairTint = tint }));
|
|
|
|
|
Assert.NotEmpty(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Theory]
|
|
|
|
|
[MemberData(nameof(AllTintColors))]
|
|
|
|
|
public void Render_EachBodyTint_DoesNotThrow(TintColor tint)
|
|
|
|
|
{
|
|
|
|
|
var result = RenderHelper.RenderToString(
|
|
|
|
|
PortraitPanel.Render(new PlayerAppearance { BodyTint = tint }));
|
|
|
|
|
Assert.NotEmpty(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static IEnumerable<object[]> AllHairStyles() =>
|
|
|
|
|
Enum.GetValues<HairStyle>().Select(v => new object[] { v });
|
|
|
|
|
public static IEnumerable<object[]> AllEyeStyles() =>
|
|
|
|
|
Enum.GetValues<EyeStyle>().Select(v => new object[] { v });
|
|
|
|
|
public static IEnumerable<object[]> AllBodyStyles() =>
|
|
|
|
|
Enum.GetValues<BodyStyle>().Select(v => new object[] { v });
|
|
|
|
|
public static IEnumerable<object[]> AllLegStyles() =>
|
|
|
|
|
Enum.GetValues<LegStyle>().Select(v => new object[] { v });
|
|
|
|
|
public static IEnumerable<object[]> AllArmStyles() =>
|
|
|
|
|
Enum.GetValues<ArmStyle>().Select(v => new object[] { v });
|
|
|
|
|
public static IEnumerable<object[]> AllTintColors() =>
|
|
|
|
|
Enum.GetValues<TintColor>().Select(v => new object[] { v });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class ResourcePanelTests
|
|
|
|
|
{
|
|
|
|
|
[Fact]
|
|
|
|
|
public void Render_Empty_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var state = GameState.Create("Test", Locale.EN);
|
|
|
|
|
var result = RenderHelper.RenderToString(ResourcePanel.Render(state));
|
|
|
|
|
Assert.NotEmpty(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Theory]
|
|
|
|
|
[MemberData(nameof(AllResourceTypes))]
|
|
|
|
|
public void Render_SingleResource_DoesNotThrow(ResourceType type)
|
|
|
|
|
{
|
|
|
|
|
var state = GameState.Create("Test", Locale.EN);
|
|
|
|
|
state.VisibleResources.Add(type);
|
|
|
|
|
state.Resources[type] = new ResourceState(50, 100);
|
|
|
|
|
var result = RenderHelper.RenderToString(ResourcePanel.Render(state));
|
|
|
|
|
Assert.NotEmpty(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void Render_AllResources_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var state = GameState.Create("Test", Locale.EN);
|
|
|
|
|
foreach (var rt in Enum.GetValues<ResourceType>())
|
|
|
|
|
{
|
|
|
|
|
state.VisibleResources.Add(rt);
|
|
|
|
|
state.Resources[rt] = new ResourceState(30 + (int)rt * 5, 100);
|
|
|
|
|
}
|
|
|
|
|
var result = RenderHelper.RenderToString(ResourcePanel.Render(state));
|
|
|
|
|
Assert.NotEmpty(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void Render_ZeroCurrent_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var state = GameState.Create("Test", Locale.EN);
|
Remove 6 unused resource types and add item utility snapshot test
Strip Health, Mana, Food, Stamina, Oxygen, Energy — only Gold and Blood
remain as they serve as adventure gates (Contemporary ≥30, DarkFantasy ≥20).
Remove 22 orphaned items, 5 recipes, and the AlchemyTable workstation.
Replace energy_cell in rocket_boots recipe with cosmic_shard.
Change box_endgame condition from AllResourcesVisible to BoxesOpenedAbove:500.
Add ItemUtilitySnapshot test that maps every item to its usage contexts
(loot sources, crafting, interactions, adventures) and generates a report.
DEBUG overwrites the snapshot; RELEASE asserts no changes.
Update specifications.md and CLAUDE.md to reflect resource cleanup.
Remove obsolete bugs.md and refactoring_plan.md.
2026-03-15 15:05:45 +01:00
|
|
|
state.VisibleResources.Add(ResourceType.Gold);
|
|
|
|
|
state.Resources[ResourceType.Gold] = new ResourceState(0, 100);
|
Fix bugs, add endgame/completion features, adventure dedup, and comprehensive renderer tests
Bug fixes:
- Fix double "NEW FEATURE UNLOCKED" message and broken enum-to-key mapping for all UIFeatures
- Fix Spectre markup crash when opening inventory with colors unlocked (unescaped rarity brackets)
- Fix latent Spectre markup crash in ResourcePanel (unescaped bar brackets)
- Fix WeightedRandom.PickMultiple picking with replacement causing duplicate drops
- Fix double AddItem bug (BoxEngine + RenderEvents both adding to state)
- Add StatType and FontStyle fields to ItemDefinition (were in JSON but missing from C# record)
New features:
- Endgame box (Mythic) that appears when all 8 resources are discovered, contains Crown of Completion
- Completion percentage tracker as mid-game meta unlock (tier 3), shown in both renderers
- Adventure pool depletion: adventure sub-boxes with already-unlocked themes are removed from loot pool
- Global error handling with log file output (openthebox-error.log)
- Tiered meta progression: 5 sequential meta boxes replacing single box_meta
New tests (180 new, 228 total):
- 5 panel rendering test classes covering all enum values (Portrait, Resource, Stats, Inventory, Chat)
- RenderContext and RendererFactory logic tests
- SpectreRenderer output tests across 4 context configurations
- BasicRenderer output and input method tests
- Simulation state mutation and full-run completion tests
2026-03-11 09:34:30 +01:00
|
|
|
var result = RenderHelper.RenderToString(ResourcePanel.Render(state));
|
|
|
|
|
Assert.NotEmpty(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void Render_ZeroMax_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var state = GameState.Create("Test", Locale.EN);
|
Remove 6 unused resource types and add item utility snapshot test
Strip Health, Mana, Food, Stamina, Oxygen, Energy — only Gold and Blood
remain as they serve as adventure gates (Contemporary ≥30, DarkFantasy ≥20).
Remove 22 orphaned items, 5 recipes, and the AlchemyTable workstation.
Replace energy_cell in rocket_boots recipe with cosmic_shard.
Change box_endgame condition from AllResourcesVisible to BoxesOpenedAbove:500.
Add ItemUtilitySnapshot test that maps every item to its usage contexts
(loot sources, crafting, interactions, adventures) and generates a report.
DEBUG overwrites the snapshot; RELEASE asserts no changes.
Update specifications.md and CLAUDE.md to reflect resource cleanup.
Remove obsolete bugs.md and refactoring_plan.md.
2026-03-15 15:05:45 +01:00
|
|
|
state.VisibleResources.Add(ResourceType.Blood);
|
|
|
|
|
state.Resources[ResourceType.Blood] = new ResourceState(0, 0);
|
Fix bugs, add endgame/completion features, adventure dedup, and comprehensive renderer tests
Bug fixes:
- Fix double "NEW FEATURE UNLOCKED" message and broken enum-to-key mapping for all UIFeatures
- Fix Spectre markup crash when opening inventory with colors unlocked (unescaped rarity brackets)
- Fix latent Spectre markup crash in ResourcePanel (unescaped bar brackets)
- Fix WeightedRandom.PickMultiple picking with replacement causing duplicate drops
- Fix double AddItem bug (BoxEngine + RenderEvents both adding to state)
- Add StatType and FontStyle fields to ItemDefinition (were in JSON but missing from C# record)
New features:
- Endgame box (Mythic) that appears when all 8 resources are discovered, contains Crown of Completion
- Completion percentage tracker as mid-game meta unlock (tier 3), shown in both renderers
- Adventure pool depletion: adventure sub-boxes with already-unlocked themes are removed from loot pool
- Global error handling with log file output (openthebox-error.log)
- Tiered meta progression: 5 sequential meta boxes replacing single box_meta
New tests (180 new, 228 total):
- 5 panel rendering test classes covering all enum values (Portrait, Resource, Stats, Inventory, Chat)
- RenderContext and RendererFactory logic tests
- SpectreRenderer output tests across 4 context configurations
- BasicRenderer output and input method tests
- Simulation state mutation and full-run completion tests
2026-03-11 09:34:30 +01:00
|
|
|
var result = RenderHelper.RenderToString(ResourcePanel.Render(state));
|
|
|
|
|
Assert.NotEmpty(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void Render_FullResource_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var state = GameState.Create("Test", Locale.EN);
|
|
|
|
|
state.VisibleResources.Add(ResourceType.Gold);
|
|
|
|
|
state.Resources[ResourceType.Gold] = new ResourceState(100, 100);
|
|
|
|
|
var result = RenderHelper.RenderToString(ResourcePanel.Render(state));
|
|
|
|
|
Assert.NotEmpty(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void Render_VisibleButNotInDict_Skipped()
|
|
|
|
|
{
|
|
|
|
|
var state = GameState.Create("Test", Locale.EN);
|
Remove 6 unused resource types and add item utility snapshot test
Strip Health, Mana, Food, Stamina, Oxygen, Energy — only Gold and Blood
remain as they serve as adventure gates (Contemporary ≥30, DarkFantasy ≥20).
Remove 22 orphaned items, 5 recipes, and the AlchemyTable workstation.
Replace energy_cell in rocket_boots recipe with cosmic_shard.
Change box_endgame condition from AllResourcesVisible to BoxesOpenedAbove:500.
Add ItemUtilitySnapshot test that maps every item to its usage contexts
(loot sources, crafting, interactions, adventures) and generates a report.
DEBUG overwrites the snapshot; RELEASE asserts no changes.
Update specifications.md and CLAUDE.md to reflect resource cleanup.
Remove obsolete bugs.md and refactoring_plan.md.
2026-03-15 15:05:45 +01:00
|
|
|
state.VisibleResources.Add(ResourceType.Blood);
|
Fix bugs, add endgame/completion features, adventure dedup, and comprehensive renderer tests
Bug fixes:
- Fix double "NEW FEATURE UNLOCKED" message and broken enum-to-key mapping for all UIFeatures
- Fix Spectre markup crash when opening inventory with colors unlocked (unescaped rarity brackets)
- Fix latent Spectre markup crash in ResourcePanel (unescaped bar brackets)
- Fix WeightedRandom.PickMultiple picking with replacement causing duplicate drops
- Fix double AddItem bug (BoxEngine + RenderEvents both adding to state)
- Add StatType and FontStyle fields to ItemDefinition (were in JSON but missing from C# record)
New features:
- Endgame box (Mythic) that appears when all 8 resources are discovered, contains Crown of Completion
- Completion percentage tracker as mid-game meta unlock (tier 3), shown in both renderers
- Adventure pool depletion: adventure sub-boxes with already-unlocked themes are removed from loot pool
- Global error handling with log file output (openthebox-error.log)
- Tiered meta progression: 5 sequential meta boxes replacing single box_meta
New tests (180 new, 228 total):
- 5 panel rendering test classes covering all enum values (Portrait, Resource, Stats, Inventory, Chat)
- RenderContext and RendererFactory logic tests
- SpectreRenderer output tests across 4 context configurations
- BasicRenderer output and input method tests
- Simulation state mutation and full-run completion tests
2026-03-11 09:34:30 +01:00
|
|
|
// Not adding to Resources dict → should skip without crash
|
|
|
|
|
var result = RenderHelper.RenderToString(ResourcePanel.Render(state));
|
|
|
|
|
Assert.NotEmpty(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static IEnumerable<object[]> AllResourceTypes() =>
|
|
|
|
|
Enum.GetValues<ResourceType>().Select(v => new object[] { v });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class StatsPanelTests
|
|
|
|
|
{
|
|
|
|
|
[Fact]
|
|
|
|
|
public void Render_Empty_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var state = GameState.Create("Test", Locale.EN);
|
|
|
|
|
var result = RenderHelper.RenderToString(StatsPanel.Render(state));
|
|
|
|
|
Assert.NotEmpty(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Theory]
|
|
|
|
|
[MemberData(nameof(AllStatTypes))]
|
|
|
|
|
public void Render_SingleStat_DoesNotThrow(StatType type)
|
|
|
|
|
{
|
|
|
|
|
var state = GameState.Create("Test", Locale.EN);
|
|
|
|
|
state.VisibleStats.Add(type);
|
|
|
|
|
state.Stats[type] = 42;
|
|
|
|
|
var result = RenderHelper.RenderToString(StatsPanel.Render(state));
|
|
|
|
|
Assert.NotEmpty(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void Render_AllStats_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var state = GameState.Create("Test", Locale.EN);
|
|
|
|
|
foreach (var st in Enum.GetValues<StatType>())
|
|
|
|
|
{
|
|
|
|
|
state.VisibleStats.Add(st);
|
|
|
|
|
state.Stats[st] = 10 + (int)st * 3;
|
|
|
|
|
}
|
|
|
|
|
state.TotalBoxesOpened = 999;
|
|
|
|
|
var result = RenderHelper.RenderToString(StatsPanel.Render(state));
|
|
|
|
|
Assert.NotEmpty(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void Render_HighBoxCount_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var state = GameState.Create("Test", Locale.EN);
|
|
|
|
|
state.TotalBoxesOpened = 999_999;
|
|
|
|
|
var result = RenderHelper.RenderToString(StatsPanel.Render(state));
|
|
|
|
|
Assert.NotEmpty(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static IEnumerable<object[]> AllStatTypes() =>
|
|
|
|
|
Enum.GetValues<StatType>().Select(v => new object[] { v });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class InventoryPanelTests
|
|
|
|
|
{
|
|
|
|
|
[Fact]
|
|
|
|
|
public void Render_Empty_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var state = GameState.Create("Test", Locale.EN);
|
|
|
|
|
var result = RenderHelper.RenderToString(InventoryPanel.Render(state));
|
|
|
|
|
Assert.NotEmpty(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void Render_SingleItem_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var state = GameState.Create("Test", Locale.EN);
|
|
|
|
|
state.AddItem(ItemInstance.Create("health_potion_small"));
|
|
|
|
|
var result = RenderHelper.RenderToString(InventoryPanel.Render(state));
|
|
|
|
|
Assert.NotEmpty(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void Render_GroupedItems_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var state = GameState.Create("Test", Locale.EN);
|
|
|
|
|
state.AddItem(ItemInstance.Create("health_potion_small"));
|
|
|
|
|
state.AddItem(ItemInstance.Create("health_potion_small"));
|
|
|
|
|
state.AddItem(ItemInstance.Create("mana_crystal_small"));
|
|
|
|
|
var result = RenderHelper.RenderToString(InventoryPanel.Render(state));
|
|
|
|
|
Assert.NotEmpty(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void Render_WithLocalization_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var state = GameState.Create("Test", Locale.EN);
|
|
|
|
|
state.AddItem(ItemInstance.Create("health_potion_small"));
|
|
|
|
|
var loc = new LocalizationManager(Locale.EN);
|
2026-03-13 21:37:09 +01:00
|
|
|
var result = RenderHelper.RenderToString(InventoryPanel.Render(state, loc: loc));
|
Fix bugs, add endgame/completion features, adventure dedup, and comprehensive renderer tests
Bug fixes:
- Fix double "NEW FEATURE UNLOCKED" message and broken enum-to-key mapping for all UIFeatures
- Fix Spectre markup crash when opening inventory with colors unlocked (unescaped rarity brackets)
- Fix latent Spectre markup crash in ResourcePanel (unescaped bar brackets)
- Fix WeightedRandom.PickMultiple picking with replacement causing duplicate drops
- Fix double AddItem bug (BoxEngine + RenderEvents both adding to state)
- Add StatType and FontStyle fields to ItemDefinition (were in JSON but missing from C# record)
New features:
- Endgame box (Mythic) that appears when all 8 resources are discovered, contains Crown of Completion
- Completion percentage tracker as mid-game meta unlock (tier 3), shown in both renderers
- Adventure pool depletion: adventure sub-boxes with already-unlocked themes are removed from loot pool
- Global error handling with log file output (openthebox-error.log)
- Tiered meta progression: 5 sequential meta boxes replacing single box_meta
New tests (180 new, 228 total):
- 5 panel rendering test classes covering all enum values (Portrait, Resource, Stats, Inventory, Chat)
- RenderContext and RendererFactory logic tests
- SpectreRenderer output tests across 4 context configurations
- BasicRenderer output and input method tests
- Simulation state mutation and full-run completion tests
2026-03-11 09:34:30 +01:00
|
|
|
Assert.NotEmpty(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void Render_ManyItems_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var state = GameState.Create("Test", Locale.EN);
|
|
|
|
|
for (int i = 0; i < 50; i++)
|
|
|
|
|
state.AddItem(ItemInstance.Create($"item_{i}"));
|
|
|
|
|
var result = RenderHelper.RenderToString(InventoryPanel.Render(state));
|
|
|
|
|
Assert.NotEmpty(result);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── RenderContext + RendererFactory Tests ────────────────────────────────
|
|
|
|
|
|
|
|
|
|
public class RenderContextTests
|
|
|
|
|
{
|
|
|
|
|
[Theory]
|
|
|
|
|
[MemberData(nameof(AllUIFeatures))]
|
|
|
|
|
public void Unlock_SetsFeature(UIFeature feature)
|
|
|
|
|
{
|
|
|
|
|
var ctx = new RenderContext();
|
|
|
|
|
Assert.False(ctx.Has(feature));
|
|
|
|
|
ctx.Unlock(feature);
|
|
|
|
|
Assert.True(ctx.Has(feature));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void CompletionPercent_DefaultsToZero()
|
|
|
|
|
{
|
|
|
|
|
var ctx = new RenderContext();
|
|
|
|
|
Assert.Equal(0, ctx.CompletionPercent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void CompletionPercent_CanBeSet()
|
|
|
|
|
{
|
|
|
|
|
var ctx = new RenderContext();
|
|
|
|
|
ctx.CompletionPercent = 75;
|
|
|
|
|
Assert.Equal(75, ctx.CompletionPercent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void FromGameState_MirrorsUnlockedFeatures()
|
|
|
|
|
{
|
|
|
|
|
var state = GameState.Create("Test", Locale.EN);
|
|
|
|
|
state.UnlockedUIFeatures.Add(UIFeature.TextColors);
|
|
|
|
|
state.UnlockedUIFeatures.Add(UIFeature.ArrowKeySelection);
|
|
|
|
|
|
|
|
|
|
var ctx = RenderContext.FromGameState(state);
|
|
|
|
|
Assert.True(ctx.HasColors);
|
|
|
|
|
Assert.True(ctx.HasArrowSelection);
|
|
|
|
|
Assert.False(ctx.HasInventoryPanel);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void FromGameState_Empty_AllFalse()
|
|
|
|
|
{
|
|
|
|
|
var state = GameState.Create("Test", Locale.EN);
|
|
|
|
|
var ctx = RenderContext.FromGameState(state);
|
|
|
|
|
|
|
|
|
|
Assert.False(ctx.HasColors);
|
|
|
|
|
Assert.False(ctx.HasExtendedColors);
|
|
|
|
|
Assert.False(ctx.HasArrowSelection);
|
|
|
|
|
Assert.False(ctx.HasInventoryPanel);
|
|
|
|
|
Assert.False(ctx.HasResourcePanel);
|
|
|
|
|
Assert.False(ctx.HasStatsPanel);
|
|
|
|
|
Assert.False(ctx.HasPortraitPanel);
|
|
|
|
|
Assert.False(ctx.HasFullLayout);
|
|
|
|
|
Assert.False(ctx.HasKeyboardShortcuts);
|
|
|
|
|
Assert.False(ctx.HasBoxAnimation);
|
|
|
|
|
Assert.False(ctx.HasCraftingPanel);
|
|
|
|
|
Assert.False(ctx.HasCompletionTracker);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static IEnumerable<object[]> AllUIFeatures() =>
|
|
|
|
|
Enum.GetValues<UIFeature>().Select(v => new object[] { v });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class RendererFactoryTests
|
|
|
|
|
{
|
|
|
|
|
[Fact]
|
|
|
|
|
public void Create_NoFeatures_ReturnsBasicRenderer()
|
|
|
|
|
{
|
|
|
|
|
var ctx = new RenderContext();
|
|
|
|
|
var loc = new LocalizationManager(Locale.EN);
|
|
|
|
|
var renderer = RendererFactory.Create(ctx, loc);
|
|
|
|
|
Assert.IsType<BasicRenderer>(renderer);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Theory]
|
|
|
|
|
[InlineData(UIFeature.TextColors)]
|
|
|
|
|
[InlineData(UIFeature.ExtendedColors)]
|
|
|
|
|
[InlineData(UIFeature.ArrowKeySelection)]
|
|
|
|
|
[InlineData(UIFeature.InventoryPanel)]
|
|
|
|
|
[InlineData(UIFeature.ResourcePanel)]
|
|
|
|
|
[InlineData(UIFeature.StatsPanel)]
|
|
|
|
|
[InlineData(UIFeature.PortraitPanel)]
|
|
|
|
|
[InlineData(UIFeature.FullLayout)]
|
|
|
|
|
[InlineData(UIFeature.KeyboardShortcuts)]
|
|
|
|
|
[InlineData(UIFeature.BoxAnimation)]
|
|
|
|
|
[InlineData(UIFeature.CraftingPanel)]
|
|
|
|
|
public void Create_WithSpectreFeature_ReturnsSpectreRenderer(UIFeature feature)
|
|
|
|
|
{
|
|
|
|
|
var ctx = new RenderContext();
|
|
|
|
|
ctx.Unlock(feature);
|
|
|
|
|
var loc = new LocalizationManager(Locale.EN);
|
|
|
|
|
var renderer = RendererFactory.Create(ctx, loc);
|
|
|
|
|
Assert.IsType<SpectreRenderer>(renderer);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void Create_CompletionTrackerOnly_ReturnsBasicRenderer()
|
|
|
|
|
{
|
|
|
|
|
// CompletionTracker alone doesn't require Spectre features
|
|
|
|
|
var ctx = new RenderContext();
|
|
|
|
|
ctx.Unlock(UIFeature.CompletionTracker);
|
|
|
|
|
var loc = new LocalizationManager(Locale.EN);
|
|
|
|
|
var renderer = RendererFactory.Create(ctx, loc);
|
|
|
|
|
Assert.IsType<BasicRenderer>(renderer);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── SpectreRenderer Output Tests ────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
[Collection("ConsoleTests")]
|
|
|
|
|
public class SpectreRendererOutputTests : IDisposable
|
|
|
|
|
{
|
|
|
|
|
private readonly ConsoleCapture _capture = new();
|
|
|
|
|
private readonly LocalizationManager _loc = new(Locale.EN);
|
|
|
|
|
|
|
|
|
|
public void Dispose() => _capture.Dispose();
|
|
|
|
|
|
|
|
|
|
private SpectreRenderer CreateRenderer(params UIFeature[] features)
|
|
|
|
|
{
|
|
|
|
|
var ctx = new RenderContext();
|
|
|
|
|
foreach (var f in features) ctx.Unlock(f);
|
|
|
|
|
return new SpectreRenderer(ctx, _loc);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── ShowMessage ──
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ShowMessage_NoFeatures_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var r = CreateRenderer();
|
|
|
|
|
r.ShowMessage("Hello world");
|
|
|
|
|
Assert.NotEmpty(_capture.Output);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ShowMessage_HasColors_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var r = CreateRenderer(UIFeature.TextColors);
|
|
|
|
|
r.ShowMessage("Hello world");
|
|
|
|
|
Assert.NotEmpty(_capture.Output);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ShowMessage_WithBrackets_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var r = CreateRenderer(UIFeature.TextColors);
|
|
|
|
|
r.ShowMessage("This has [red] brackets [/] in it");
|
|
|
|
|
Assert.NotEmpty(_capture.Output);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── ShowError ──
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ShowError_NoFeatures_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var r = CreateRenderer();
|
|
|
|
|
r.ShowError("Something went wrong");
|
|
|
|
|
Assert.NotEmpty(_capture.Output);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ShowError_HasColors_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var r = CreateRenderer(UIFeature.TextColors);
|
|
|
|
|
r.ShowError("Something went wrong");
|
|
|
|
|
Assert.NotEmpty(_capture.Output);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── ShowBoxOpening (no animation) ──
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ShowBoxOpening_NoFeatures_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var r = CreateRenderer();
|
|
|
|
|
r.ShowBoxOpening("Test Box", "Common");
|
|
|
|
|
Assert.NotEmpty(_capture.Output);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ShowBoxOpening_HasColors_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var r = CreateRenderer(UIFeature.TextColors);
|
|
|
|
|
r.ShowBoxOpening("Cool Box", "Rare");
|
|
|
|
|
Assert.NotEmpty(_capture.Output);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Theory]
|
|
|
|
|
[InlineData("Common")]
|
|
|
|
|
[InlineData("Uncommon")]
|
|
|
|
|
[InlineData("Rare")]
|
|
|
|
|
[InlineData("Epic")]
|
|
|
|
|
[InlineData("Legendary")]
|
|
|
|
|
[InlineData("Mythic")]
|
|
|
|
|
[InlineData("Unknown")]
|
|
|
|
|
public void ShowBoxOpening_AllRarities_DoesNotThrow(string rarity)
|
|
|
|
|
{
|
|
|
|
|
var r = CreateRenderer(UIFeature.TextColors);
|
|
|
|
|
r.ShowBoxOpening("Box", rarity);
|
|
|
|
|
Assert.NotEmpty(_capture.Output);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── ShowLootReveal ──
|
|
|
|
|
|
|
|
|
|
private static List<(string, string, string)> SampleLoot() =>
|
|
|
|
|
[
|
|
|
|
|
("Health Potion", "Common", "Consumable"),
|
|
|
|
|
("Epic Sword", "Epic", "Equipment"),
|
|
|
|
|
("Box of Boxes", "Uncommon", "Box")
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ShowLootReveal_NoFeatures_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var r = CreateRenderer();
|
|
|
|
|
r.ShowLootReveal(SampleLoot());
|
|
|
|
|
Assert.NotEmpty(_capture.Output);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ShowLootReveal_HasColors_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var r = CreateRenderer(UIFeature.TextColors);
|
|
|
|
|
r.ShowLootReveal(SampleLoot());
|
|
|
|
|
Assert.NotEmpty(_capture.Output);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ShowLootReveal_HasInventoryPanel_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var r = CreateRenderer(UIFeature.TextColors, UIFeature.InventoryPanel);
|
|
|
|
|
r.ShowLootReveal(SampleLoot());
|
|
|
|
|
Assert.NotEmpty(_capture.Output);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ShowLootReveal_EmptyList_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var r = CreateRenderer(UIFeature.TextColors, UIFeature.InventoryPanel);
|
|
|
|
|
r.ShowLootReveal([]);
|
|
|
|
|
// Empty is OK, no crash
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ShowLootReveal_ItemNameWithBrackets_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var r = CreateRenderer(UIFeature.TextColors);
|
|
|
|
|
r.ShowLootReveal([("[Special] Item", "Rare", "Box")]);
|
|
|
|
|
Assert.NotEmpty(_capture.Output);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── ShowAdventureDialogue ──
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ShowAdventureDialogue_WithCharacter_NoFeatures_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var r = CreateRenderer();
|
|
|
|
|
r.ShowAdventureDialogue("Guard", "Halt!");
|
|
|
|
|
Assert.NotEmpty(_capture.Output);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ShowAdventureDialogue_WithoutCharacter_NoFeatures_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var r = CreateRenderer();
|
|
|
|
|
r.ShowAdventureDialogue(null, "The wind howls.");
|
|
|
|
|
Assert.NotEmpty(_capture.Output);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ShowAdventureDialogue_WithCharacter_HasColors_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var r = CreateRenderer(UIFeature.TextColors);
|
|
|
|
|
r.ShowAdventureDialogue("Dragon", "Prepare yourself!");
|
|
|
|
|
Assert.NotEmpty(_capture.Output);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ShowAdventureDialogue_WithoutCharacter_HasColors_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var r = CreateRenderer(UIFeature.TextColors);
|
|
|
|
|
r.ShowAdventureDialogue(null, "A mysterious voice echoes.");
|
|
|
|
|
Assert.NotEmpty(_capture.Output);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── ShowUIFeatureUnlocked ──
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ShowUIFeatureUnlocked_NoFeatures_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var r = CreateRenderer();
|
|
|
|
|
r.ShowUIFeatureUnlocked("Text Colors");
|
|
|
|
|
Assert.NotEmpty(_capture.Output);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ShowUIFeatureUnlocked_HasColors_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var r = CreateRenderer(UIFeature.TextColors);
|
|
|
|
|
r.ShowUIFeatureUnlocked("Inventory Panel");
|
|
|
|
|
Assert.NotEmpty(_capture.Output);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── ShowInteraction ──
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ShowInteraction_NoFeatures_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var r = CreateRenderer();
|
|
|
|
|
r.ShowInteraction("You found something interesting.");
|
|
|
|
|
Assert.NotEmpty(_capture.Output);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ShowInteraction_HasColors_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var r = CreateRenderer(UIFeature.TextColors);
|
|
|
|
|
r.ShowInteraction("A secret passage opens.");
|
|
|
|
|
Assert.NotEmpty(_capture.Output);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── ShowGameState ──
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ShowGameState_NoFeatures_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var state = GameState.Create("Test", Locale.EN);
|
|
|
|
|
var ctx = new RenderContext();
|
|
|
|
|
var r = new SpectreRenderer(ctx, _loc);
|
|
|
|
|
r.ShowGameState(state, ctx);
|
|
|
|
|
// No panels → minimal or no output, but no crash
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ShowGameState_SequentialPanels_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var state = CreateFullState();
|
|
|
|
|
var ctx = new RenderContext();
|
|
|
|
|
ctx.Unlock(UIFeature.TextColors);
|
|
|
|
|
ctx.Unlock(UIFeature.PortraitPanel);
|
|
|
|
|
ctx.Unlock(UIFeature.StatsPanel);
|
|
|
|
|
ctx.Unlock(UIFeature.ResourcePanel);
|
|
|
|
|
ctx.Unlock(UIFeature.InventoryPanel);
|
|
|
|
|
ctx.Unlock(UIFeature.CompletionTracker);
|
|
|
|
|
ctx.CompletionPercent = 42;
|
|
|
|
|
var r = new SpectreRenderer(ctx, _loc);
|
|
|
|
|
r.ShowGameState(state, ctx);
|
|
|
|
|
Assert.NotEmpty(_capture.Output);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ShowGameState_FullLayout_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var state = CreateFullState();
|
|
|
|
|
var ctx = new RenderContext();
|
|
|
|
|
foreach (var f in Enum.GetValues<UIFeature>())
|
|
|
|
|
ctx.Unlock(f);
|
|
|
|
|
ctx.CompletionPercent = 88;
|
|
|
|
|
var r = new SpectreRenderer(ctx, _loc);
|
|
|
|
|
r.ShowGameState(state, ctx);
|
|
|
|
|
Assert.NotEmpty(_capture.Output);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ShowGameState_FullLayout_PartialPanels_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var state = GameState.Create("Test", Locale.EN);
|
|
|
|
|
var ctx = new RenderContext();
|
|
|
|
|
ctx.Unlock(UIFeature.TextColors);
|
|
|
|
|
ctx.Unlock(UIFeature.FullLayout);
|
|
|
|
|
// FullLayout unlocked but individual panels are NOT → shows placeholder "???"
|
|
|
|
|
var r = new SpectreRenderer(ctx, _loc);
|
|
|
|
|
r.ShowGameState(state, ctx);
|
|
|
|
|
Assert.NotEmpty(_capture.Output);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Clear ──
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void Clear_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
var r = CreateRenderer(UIFeature.TextColors);
|
|
|
|
|
r.Clear(); // May throw IOException internally, should be caught
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Helpers ──
|
|
|
|
|
|
|
|
|
|
private static GameState CreateFullState()
|
|
|
|
|
{
|
|
|
|
|
var state = GameState.Create("Test", Locale.EN);
|
|
|
|
|
state.Appearance = new PlayerAppearance
|
|
|
|
|
{
|
|
|
|
|
HairStyle = HairStyle.Short,
|
|
|
|
|
EyeStyle = EyeStyle.Blue,
|
|
|
|
|
BodyStyle = BodyStyle.RegularTShirt,
|
|
|
|
|
LegStyle = LegStyle.Short,
|
|
|
|
|
ArmStyle = ArmStyle.Regular
|
|
|
|
|
};
|
Remove 6 unused resource types and add item utility snapshot test
Strip Health, Mana, Food, Stamina, Oxygen, Energy — only Gold and Blood
remain as they serve as adventure gates (Contemporary ≥30, DarkFantasy ≥20).
Remove 22 orphaned items, 5 recipes, and the AlchemyTable workstation.
Replace energy_cell in rocket_boots recipe with cosmic_shard.
Change box_endgame condition from AllResourcesVisible to BoxesOpenedAbove:500.
Add ItemUtilitySnapshot test that maps every item to its usage contexts
(loot sources, crafting, interactions, adventures) and generates a report.
DEBUG overwrites the snapshot; RELEASE asserts no changes.
Update specifications.md and CLAUDE.md to reflect resource cleanup.
Remove obsolete bugs.md and refactoring_plan.md.
2026-03-15 15:05:45 +01:00
|
|
|
state.AddItem(ItemInstance.Create("gold_pouch"));
|
Fix bugs, add endgame/completion features, adventure dedup, and comprehensive renderer tests
Bug fixes:
- Fix double "NEW FEATURE UNLOCKED" message and broken enum-to-key mapping for all UIFeatures
- Fix Spectre markup crash when opening inventory with colors unlocked (unescaped rarity brackets)
- Fix latent Spectre markup crash in ResourcePanel (unescaped bar brackets)
- Fix WeightedRandom.PickMultiple picking with replacement causing duplicate drops
- Fix double AddItem bug (BoxEngine + RenderEvents both adding to state)
- Add StatType and FontStyle fields to ItemDefinition (were in JSON but missing from C# record)
New features:
- Endgame box (Mythic) that appears when all 8 resources are discovered, contains Crown of Completion
- Completion percentage tracker as mid-game meta unlock (tier 3), shown in both renderers
- Adventure pool depletion: adventure sub-boxes with already-unlocked themes are removed from loot pool
- Global error handling with log file output (openthebox-error.log)
- Tiered meta progression: 5 sequential meta boxes replacing single box_meta
New tests (180 new, 228 total):
- 5 panel rendering test classes covering all enum values (Portrait, Resource, Stats, Inventory, Chat)
- RenderContext and RendererFactory logic tests
- SpectreRenderer output tests across 4 context configurations
- BasicRenderer output and input method tests
- Simulation state mutation and full-run completion tests
2026-03-11 09:34:30 +01:00
|
|
|
state.AddItem(ItemInstance.Create("box_of_boxes"));
|
Remove 6 unused resource types and add item utility snapshot test
Strip Health, Mana, Food, Stamina, Oxygen, Energy — only Gold and Blood
remain as they serve as adventure gates (Contemporary ≥30, DarkFantasy ≥20).
Remove 22 orphaned items, 5 recipes, and the AlchemyTable workstation.
Replace energy_cell in rocket_boots recipe with cosmic_shard.
Change box_endgame condition from AllResourcesVisible to BoxesOpenedAbove:500.
Add ItemUtilitySnapshot test that maps every item to its usage contexts
(loot sources, crafting, interactions, adventures) and generates a report.
DEBUG overwrites the snapshot; RELEASE asserts no changes.
Update specifications.md and CLAUDE.md to reflect resource cleanup.
Remove obsolete bugs.md and refactoring_plan.md.
2026-03-15 15:05:45 +01:00
|
|
|
state.VisibleResources.Add(ResourceType.Gold);
|
|
|
|
|
state.Resources[ResourceType.Gold] = new ResourceState(75, 100);
|
Fix bugs, add endgame/completion features, adventure dedup, and comprehensive renderer tests
Bug fixes:
- Fix double "NEW FEATURE UNLOCKED" message and broken enum-to-key mapping for all UIFeatures
- Fix Spectre markup crash when opening inventory with colors unlocked (unescaped rarity brackets)
- Fix latent Spectre markup crash in ResourcePanel (unescaped bar brackets)
- Fix WeightedRandom.PickMultiple picking with replacement causing duplicate drops
- Fix double AddItem bug (BoxEngine + RenderEvents both adding to state)
- Add StatType and FontStyle fields to ItemDefinition (were in JSON but missing from C# record)
New features:
- Endgame box (Mythic) that appears when all 8 resources are discovered, contains Crown of Completion
- Completion percentage tracker as mid-game meta unlock (tier 3), shown in both renderers
- Adventure pool depletion: adventure sub-boxes with already-unlocked themes are removed from loot pool
- Global error handling with log file output (openthebox-error.log)
- Tiered meta progression: 5 sequential meta boxes replacing single box_meta
New tests (180 new, 228 total):
- 5 panel rendering test classes covering all enum values (Portrait, Resource, Stats, Inventory, Chat)
- RenderContext and RendererFactory logic tests
- SpectreRenderer output tests across 4 context configurations
- BasicRenderer output and input method tests
- Simulation state mutation and full-run completion tests
2026-03-11 09:34:30 +01:00
|
|
|
state.VisibleStats.Add(StatType.Strength);
|
|
|
|
|
state.Stats[StatType.Strength] = 15;
|
|
|
|
|
state.TotalBoxesOpened = 42;
|
|
|
|
|
return state;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── BasicRenderer Output Tests ──────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
[Collection("ConsoleTests")]
|
|
|
|
|
public class BasicRendererOutputTests : IDisposable
|
|
|
|
|
{
|
|
|
|
|
private readonly ConsoleCapture _capture = new();
|
|
|
|
|
private readonly LocalizationManager _loc = new(Locale.EN);
|
|
|
|
|
private readonly BasicRenderer _renderer;
|
|
|
|
|
|
|
|
|
|
public BasicRendererOutputTests()
|
|
|
|
|
{
|
|
|
|
|
_renderer = new BasicRenderer(_loc);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Dispose() => _capture.Dispose();
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ShowMessage_WritesToConsole()
|
|
|
|
|
{
|
|
|
|
|
_renderer.ShowMessage("Test message");
|
|
|
|
|
Assert.Contains("Test message", _capture.Output);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ShowError_WritesWithPrefix()
|
|
|
|
|
{
|
|
|
|
|
_renderer.ShowError("Bad thing");
|
|
|
|
|
Assert.Contains("ERROR", _capture.Output);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ShowBoxOpening_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
_renderer.ShowBoxOpening("Test Box", "Rare");
|
|
|
|
|
Assert.NotEmpty(_capture.Output);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ShowLootReveal_WritesItems()
|
|
|
|
|
{
|
|
|
|
|
_renderer.ShowLootReveal([("Sword", "Epic", "Equipment")]);
|
|
|
|
|
Assert.Contains("Sword", _capture.Output);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ShowLootReveal_EmptyList_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
_renderer.ShowLootReveal([]);
|
|
|
|
|
// No items → just the header line
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ShowGameState_WithCompletionTracker_ShowsPercent()
|
|
|
|
|
{
|
|
|
|
|
var state = GameState.Create("Test", Locale.EN);
|
|
|
|
|
var ctx = new RenderContext();
|
|
|
|
|
ctx.Unlock(UIFeature.CompletionTracker);
|
|
|
|
|
ctx.CompletionPercent = 55;
|
|
|
|
|
_renderer.ShowGameState(state, ctx);
|
|
|
|
|
Assert.Contains("55", _capture.Output);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
2026-03-14 09:56:53 +01:00
|
|
|
public void ShowGameState_WithoutCompletionTracker_ShowsBoxCount()
|
Fix bugs, add endgame/completion features, adventure dedup, and comprehensive renderer tests
Bug fixes:
- Fix double "NEW FEATURE UNLOCKED" message and broken enum-to-key mapping for all UIFeatures
- Fix Spectre markup crash when opening inventory with colors unlocked (unescaped rarity brackets)
- Fix latent Spectre markup crash in ResourcePanel (unescaped bar brackets)
- Fix WeightedRandom.PickMultiple picking with replacement causing duplicate drops
- Fix double AddItem bug (BoxEngine + RenderEvents both adding to state)
- Add StatType and FontStyle fields to ItemDefinition (were in JSON but missing from C# record)
New features:
- Endgame box (Mythic) that appears when all 8 resources are discovered, contains Crown of Completion
- Completion percentage tracker as mid-game meta unlock (tier 3), shown in both renderers
- Adventure pool depletion: adventure sub-boxes with already-unlocked themes are removed from loot pool
- Global error handling with log file output (openthebox-error.log)
- Tiered meta progression: 5 sequential meta boxes replacing single box_meta
New tests (180 new, 228 total):
- 5 panel rendering test classes covering all enum values (Portrait, Resource, Stats, Inventory, Chat)
- RenderContext and RendererFactory logic tests
- SpectreRenderer output tests across 4 context configurations
- BasicRenderer output and input method tests
- Simulation state mutation and full-run completion tests
2026-03-11 09:34:30 +01:00
|
|
|
{
|
|
|
|
|
var state = GameState.Create("Test", Locale.EN);
|
|
|
|
|
var ctx = new RenderContext();
|
|
|
|
|
_renderer.ShowGameState(state, ctx);
|
2026-03-14 09:56:53 +01:00
|
|
|
// Early-game: shows box counter even without panels unlocked
|
|
|
|
|
Assert.Contains("Boxes Opened", _capture.Output);
|
Fix bugs, add endgame/completion features, adventure dedup, and comprehensive renderer tests
Bug fixes:
- Fix double "NEW FEATURE UNLOCKED" message and broken enum-to-key mapping for all UIFeatures
- Fix Spectre markup crash when opening inventory with colors unlocked (unescaped rarity brackets)
- Fix latent Spectre markup crash in ResourcePanel (unescaped bar brackets)
- Fix WeightedRandom.PickMultiple picking with replacement causing duplicate drops
- Fix double AddItem bug (BoxEngine + RenderEvents both adding to state)
- Add StatType and FontStyle fields to ItemDefinition (were in JSON but missing from C# record)
New features:
- Endgame box (Mythic) that appears when all 8 resources are discovered, contains Crown of Completion
- Completion percentage tracker as mid-game meta unlock (tier 3), shown in both renderers
- Adventure pool depletion: adventure sub-boxes with already-unlocked themes are removed from loot pool
- Global error handling with log file output (openthebox-error.log)
- Tiered meta progression: 5 sequential meta boxes replacing single box_meta
New tests (180 new, 228 total):
- 5 panel rendering test classes covering all enum values (Portrait, Resource, Stats, Inventory, Chat)
- RenderContext and RendererFactory logic tests
- SpectreRenderer output tests across 4 context configurations
- BasicRenderer output and input method tests
- Simulation state mutation and full-run completion tests
2026-03-11 09:34:30 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ShowAdventureDialogue_WithCharacter_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
_renderer.ShowAdventureDialogue("NPC", "Hello!");
|
|
|
|
|
Assert.Contains("NPC", _capture.Output);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ShowAdventureDialogue_WithoutCharacter_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
_renderer.ShowAdventureDialogue(null, "Narration text.");
|
|
|
|
|
Assert.Contains("Narration", _capture.Output);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ShowUIFeatureUnlocked_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
_renderer.ShowUIFeatureUnlocked("Text Colors");
|
|
|
|
|
Assert.Contains("Text Colors", _capture.Output);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void ShowInteraction_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
_renderer.ShowInteraction("Something happens");
|
|
|
|
|
Assert.Contains("Something happens", _capture.Output);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void Clear_DoesNotThrow()
|
|
|
|
|
{
|
|
|
|
|
_renderer.Clear();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Input Method Tests ──────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
[Collection("ConsoleTests")]
|
|
|
|
|
public class RendererInputTests : IDisposable
|
|
|
|
|
{
|
|
|
|
|
private readonly ConsoleCapture _capture = new();
|
|
|
|
|
private readonly LocalizationManager _loc = new(Locale.EN);
|
|
|
|
|
private readonly TextReader _origIn;
|
|
|
|
|
|
|
|
|
|
public RendererInputTests()
|
|
|
|
|
{
|
|
|
|
|
_origIn = Console.In;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Dispose()
|
|
|
|
|
{
|
|
|
|
|
Console.SetIn(_origIn);
|
|
|
|
|
_capture.Dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void BasicRenderer_ShowSelection_ReturnsCorrectIndex()
|
|
|
|
|
{
|
|
|
|
|
Console.SetIn(new StringReader("2\n"));
|
|
|
|
|
var r = new BasicRenderer(_loc);
|
|
|
|
|
int result = r.ShowSelection("Choose:", ["A", "B", "C"]);
|
|
|
|
|
Assert.Equal(1, result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void BasicRenderer_ShowTextInput_ReturnsInput()
|
|
|
|
|
{
|
|
|
|
|
Console.SetIn(new StringReader("Hello\n"));
|
|
|
|
|
var r = new BasicRenderer(_loc);
|
|
|
|
|
string result = r.ShowTextInput("Name");
|
|
|
|
|
Assert.Equal("Hello", result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void BasicRenderer_ShowAdventureChoice_ReturnsIndex()
|
|
|
|
|
{
|
|
|
|
|
Console.SetIn(new StringReader("1\n"));
|
|
|
|
|
var r = new BasicRenderer(_loc);
|
|
|
|
|
int result = r.ShowAdventureChoice(["Fight", "Run"]);
|
|
|
|
|
Assert.Equal(0, result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void SpectreRenderer_ShowSelection_NoArrow_ReturnsIndex()
|
|
|
|
|
{
|
|
|
|
|
Console.SetIn(new StringReader("3\n"));
|
|
|
|
|
var ctx = new RenderContext();
|
|
|
|
|
// No arrow selection → falls through to number input path
|
|
|
|
|
var r = new SpectreRenderer(ctx, _loc);
|
|
|
|
|
int result = r.ShowSelection("Pick:", ["X", "Y", "Z"]);
|
|
|
|
|
Assert.Equal(2, result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public void SpectreRenderer_ShowSelection_HasColorsNoArrow_ReturnsIndex()
|
|
|
|
|
{
|
|
|
|
|
Console.SetIn(new StringReader("1\n"));
|
|
|
|
|
var ctx = new RenderContext();
|
|
|
|
|
ctx.Unlock(UIFeature.TextColors);
|
|
|
|
|
var r = new SpectreRenderer(ctx, _loc);
|
|
|
|
|
int result = r.ShowSelection("Pick:", ["Only"]);
|
|
|
|
|
Assert.Equal(0, result);
|
|
|
|
|
}
|
|
|
|
|
}
|