test: raise EditMode coverage to 98.16% global, all classes >=95%

This commit is contained in:
2026-08-25 02:30:58 -03:00
parent ff33c43293
commit 916acd3d79
22 changed files with 2130 additions and 0 deletions
@@ -0,0 +1,87 @@
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
/// <summary>
/// Cobertura de las ramas de error de CampaignConfig.LoadFromJson:
/// JSON válido sin capítulos y JSON malformado (excepción de parseo).
/// Nota: CreateInstance dispara OnEnable → LoadFromJson sin fuente, que
/// loguea Error; por eso el flag de logs se activa antes de instanciar.
/// </summary>
[TestFixture]
[Category("Unit")]
[Category("Campaign")]
[Category("FastTests")]
public class CampaignConfigCoverageTests
{
[TearDown]
public void Teardown()
{
LogAssert.ignoreFailingMessages = false;
}
private CampaignConfig CreateUnloadedConfig()
{
// CreateInstance dispara OnEnable → LoadFromJson sin fuente (Error):
// se espera explícitamente porque ignoreFailingMessages en SetUp no persiste.
LogAssert.Expect(LogType.Error, "[CampaignConfig] No se asignó archivo JSON de campaña.");
return ScriptableObject.CreateInstance<CampaignConfig>();
}
[Test]
public void LoadFromJson_EmptyChapters_LogsErrorAndIsNotLoaded()
{
var config = CreateUnloadedConfig();
try
{
config.SetJsonSource(new TextAsset("{\"campaignId\":\"_test\",\"chapters\":[]}"));
LogAssert.Expect(LogType.Error, "[CampaignConfig] El JSON no contiene capítulos válidos.");
config.LoadFromJson();
Assert.IsFalse(config.IsLoaded);
Assert.AreEqual(0, config.ChapterCount);
}
finally
{
Object.DestroyImmediate(config);
}
}
[Test]
public void LoadFromJson_MalformedJson_CatchesExceptionAndIsNotLoaded()
{
var config = CreateUnloadedConfig();
try
{
config.SetJsonSource(new TextAsset("{capítulos inválidos::"));
LogAssert.ignoreFailingMessages = true; // catch + fallback: textos variables
config.LoadFromJson();
LogAssert.ignoreFailingMessages = false;
Assert.IsFalse(config.IsLoaded);
}
finally
{
Object.DestroyImmediate(config);
}
}
[Test]
public void GetChapterById_WithoutLoadedData_LogsErrorAndReturnsNull()
{
var config = CreateUnloadedConfig();
try
{
LogAssert.Expect(LogType.Error, "[CampaignConfig] Datos no cargados.");
Assert.IsNull(config.GetChapterById("cualquiera"));
LogAssert.Expect(LogType.Error, "[CampaignConfig] Índice de capítulo inválido: 0");
Assert.IsNull(config.GetChapter(0));
}
finally
{
Object.DestroyImmediate(config);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e81772098be30d24e883aeb5197f307f
@@ -0,0 +1,193 @@
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
using UnityEditor;
/// <summary>
/// Cobertura de ramas restantes de CampaignManager: registro de campañas
/// (config del Inspector, JSON inválido y duplicado en Resources), guards de
/// LoadChapter, EnsureChildSystem y cierre forzado del diálogo mid-turn.
/// </summary>
[TestFixture]
[Category("Unit")]
[Category("Campaign")]
[Category("FastTests")]
public class CampaignManagerCoverageTests
{
private static readonly BindingFlags Flags =
BindingFlags.NonPublic | BindingFlags.Instance;
private const string ResourcesFolder = "Assets/Game/Data/Resources/Campaigns";
private GameObject _root;
private CampaignManager _manager;
private readonly List<string> _tempFiles = new List<string>();
[SetUp]
public void Setup()
{
_root = new GameObject("CampaignManagerCoverageRoot");
_manager = _root.AddComponent<CampaignManager>();
}
[TearDown]
public void Teardown()
{
bool removedAny = false;
foreach (string file in _tempFiles)
{
if (File.Exists(file))
{
File.Delete(file);
removedAny = true;
}
}
_tempFiles.Clear();
if (removedAny)
{
AssetDatabase.Refresh();
}
Object.DestroyImmediate(_root);
LogAssert.ignoreFailingMessages = false;
}
private object GetPrivate(string name)
{
return typeof(CampaignManager).GetField(name, Flags).GetValue(_manager);
}
private void InvokePrivate(string name, params object[] args)
{
var method = typeof(CampaignManager).GetMethod(name, Flags);
Assert.IsNotNull(method, $"{name} no encontrado.");
method.Invoke(_manager, args);
}
private CampaignConfig MakeConfigFromJson(string json)
{
// CreateInstance dispara OnEnable → LoadFromJson sin fuente (Error esperado).
LogAssert.Expect(LogType.Error, "[CampaignConfig] No se asignó archivo JSON de campaña.");
var config = ScriptableObject.CreateInstance<CampaignConfig>();
config.SetJsonSource(new TextAsset(json));
config.LoadFromJson();
return config;
}
private string WriteTempResource(string fileName, string contents)
{
string path = Path.Combine(ResourcesFolder, fileName);
File.WriteAllText(path, contents);
_tempFiles.Add(path);
AssetDatabase.Refresh();
return path;
}
[Test]
public void BuildCampaignRegistry_InspectorConfig_IsFirstAndActive()
{
var inspectorConfig = MakeConfigFromJson(
"{\"campaignId\":\"_inspector\",\"chapters\":[{\"id\":\"c1\"}]}");
typeof(CampaignManager).GetField("_campaignConfig", Flags)
.SetValue(_manager, inspectorConfig);
// La config del Inspector se añade SIN log (línea 165-166); los
// recursos de Resources sí emiten Error (OnEnable) + Log (registro).
LogAssert.ignoreFailingMessages = true;
InvokePrivate("BuildCampaignRegistry");
LogAssert.ignoreFailingMessages = false;
var registry = GetPrivate("_campaigns") as List<CampaignConfig>;
Assert.IsNotNull(registry);
Assert.IsTrue(RegistryHasId(registry, "_inspector"),
"La config del Inspector debe estar registrada.");
Assert.AreSame(inspectorConfig, GetPrivate("_activeConfig"),
"La config del Inspector debe ser la activa por defecto.");
Object.DestroyImmediate(inspectorConfig);
}
[Test]
public void BuildCampaignRegistry_InvalidResourceJson_IsSkipped()
{
WriteTempResource("_zz_invalid_test.json", "{esto no es json válido::");
LogAssert.ignoreFailingMessages = true;
InvokePrivate("BuildCampaignRegistry");
LogAssert.ignoreFailingMessages = false;
var registry = GetPrivate("_campaigns") as List<CampaignConfig>;
Assert.IsNotNull(registry);
Assert.IsFalse(RegistryHasId(registry, "_zz_invalid_test"),
"El JSON inválido no debe registrarse.");
}
[Test]
public void BuildCampaignRegistry_DuplicateCampaignId_SecondIsIgnored()
{
WriteTempResource("_zz_dup_a.json", "{\"campaignId\":\"_zzdup\",\"chapters\":[{\"id\":\"z1\"}]}");
WriteTempResource("_zz_dup_b.json", "{\"campaignId\":\"_zzdup\",\"chapters\":[{\"id\":\"z2\"}]}");
LogAssert.ignoreFailingMessages = true;
InvokePrivate("BuildCampaignRegistry");
LogAssert.ignoreFailingMessages = false;
var registry = GetPrivate("_campaigns") as List<CampaignConfig>;
Assert.IsNotNull(registry);
int dupCount = 0;
foreach (var campaign in registry)
{
if (campaign.CampaignId == "_zzdup") dupCount++;
}
Assert.AreEqual(1, dupCount, "Solo la primera campaña con el id debe registrarse.");
}
private static bool RegistryHasId(List<CampaignConfig> registry, string id)
{
foreach (var campaign in registry)
{
if (campaign.CampaignId == id) return true;
}
return false;
}
[Test]
public void LoadChapter_WithoutActiveConfig_LogsError()
{
LogAssert.Expect(LogType.Error, "[CampaignManager] CampaignConfig no asignado.");
_manager.LoadChapter(0);
}
[Test]
public void LoadChapter_IndexOutOfRange_LogsError()
{
var config = MakeConfigFromJson("{\"campaignId\":\"_one\",\"chapters\":[{\"id\":\"c1\"}]}");
typeof(CampaignManager).GetField("_activeConfig", Flags).SetValue(_manager, config);
LogAssert.ignoreFailingMessages = false;
LogAssert.Expect(LogType.Error, "[CampaignManager] Índice fuera de rango: 7");
_manager.LoadChapter(7);
Object.DestroyImmediate(config);
}
[Test]
public void EnsureChildSystem_CreatesOnceAndReusesAfterwards()
{
var ensureMethod = typeof(CampaignManager).GetMethod("EnsureChildSystem", Flags);
Assert.IsNotNull(ensureMethod, "EnsureChildSystem no encontrado.");
var generic = ensureMethod.MakeGenericMethod(typeof(DialogueTriggerSystem));
DialogueTriggerSystem first = (DialogueTriggerSystem)generic.Invoke(_manager, null);
DialogueTriggerSystem second = (DialogueTriggerSystem)generic.Invoke(_manager, null);
Assert.IsNotNull(first);
Assert.AreSame(first, second, "La segunda llamada debe reutilizar el sistema existente.");
Assert.IsTrue(first.transform.IsChildOf(_root.transform));
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 038322fe64063f0438d6040f2f848819
@@ -0,0 +1,107 @@
using System.Collections.Generic;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
/// <summary>
/// Cobertura de ramas restantes de CampaignState: SyncFromBoard con tablero
/// nulo y Reset con identidades registradas (re-inicialización de vivos).
/// </summary>
[TestFixture]
[Category("Unit")]
[Category("Campaign")]
[Category("FastTests")]
public class CampaignStateCoverageTests
{
private CampaignState _state;
private GameObject _mockRoot;
private readonly List<GameObject> _spawned = new List<GameObject>();
[SetUp]
public void Setup()
{
_state = ScriptableObject.CreateInstance<CampaignState>();
_mockRoot = new GameObject("MockRoot");
}
[TearDown]
public void Teardown()
{
Object.DestroyImmediate(_state);
Object.DestroyImmediate(_mockRoot);
foreach (var obj in _spawned)
{
if (obj != null) Object.DestroyImmediate(obj);
}
_spawned.Clear();
}
private void InjectIdentities(List<PieceIdentity> identities)
{
var field = typeof(CampaignState).GetField("_allIdentities",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
Assert.IsNotNull(field, "_allIdentities no encontrado.");
field.SetValue(_state, identities);
}
[Test]
public void SyncFromBoard_NullBoard_WarnsAndKeepsData()
{
LogAssert.Expect(LogType.Warning, "[CampaignState] Board es null, no se puede sincronizar.");
_state.RegisterPieceAlive("elena");
_state.SyncFromBoard(null);
Assert.IsTrue(_state.IsPieceAlive("elena"),
"Un tablero nulo no debe limpiar las piezas vivas registradas.");
}
[Test]
public void Reset_WithRegisteredIdentities_ReinitializesAliveList()
{
var elena = ScriptableObject.CreateInstance<PieceIdentity>();
elena.characterName = "Elena"; // mayúsculas: Reset guarda lowercase
var ricardo = ScriptableObject.CreateInstance<PieceIdentity>();
ricardo.characterName = "ricardo";
InjectIdentities(new List<PieceIdentity> { elena, null, ricardo });
// Ensuciar el estado antes del reset.
_state.RegisterPieceLost("alguien_mas");
_state.StartChapter("ch1");
_state.CompleteChapter("ch1", wasVictory: true);
_state.Reset();
Assert.IsTrue(_state.IsPieceAlive("elena"), "Las identidades registradas renacen como vivas.");
Assert.IsTrue(_state.IsPieceAlive("ricardo"));
Assert.IsFalse(_state.IsPieceAlive("alguien_mas"), "Vivos previos sin identidad deben purgarse.");
Object.DestroyImmediate(elena);
Object.DestroyImmediate(ricardo);
}
[Test]
public void Reset_SkipsIdentitiesWithEmptyName()
{
var anon = ScriptableObject.CreateInstance<PieceIdentity>();
anon.characterName = "";
InjectIdentities(new List<PieceIdentity> { anon });
LogAssert.NoUnexpectedReceived();
_state.Reset();
Assert.AreEqual(0, CountAlive(), "Identidad sin nombre no debe entrar al pool de vivos.");
Object.DestroyImmediate(anon);
}
private int CountAlive()
{
var field = typeof(CampaignState).GetField("_alivePieces",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
var list = field.GetValue(_state) as List<string>;
return list?.Count ?? 0;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c00afa67a8981b4499781d1896d8c64f
@@ -0,0 +1,238 @@
using System.Collections.Generic;
using System.Reflection;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
/// <summary>
/// Cobertura de ramas restantes de CheckDetector (jaque mate completo,
/// en passant con pieza adyacente que no es peón, guards nulos) y de
/// DrawDetector (stalemate, símbolos del hash por tipo de pieza, repetición).
/// </summary>
public class CheckDrawCoverageTests
{
private readonly List<GameObject> _spawned = new List<GameObject>();
[SetUp]
public void SetUp() { }
[TearDown]
public void TearDown()
{
foreach (var obj in _spawned)
{
if (obj != null) Object.DestroyImmediate(obj);
}
_spawned.Clear();
}
private Piece[,] NewBoard()
{
return new Piece[8, 8];
}
private T Spawn<T>(Piece[,] board, int x, int y, bool isWhite) where T : Piece
{
var obj = new GameObject($"{typeof(T).Name}_{x}_{y}_{_spawned.Count}");
_spawned.Add(obj);
var piece = obj.AddComponent<T>();
piece.currentPos = new Vector2Int(x, y);
piece.isWhite = isWhite;
board[x, y] = piece;
return piece;
}
// ── CheckDetector ────────────────────────────────────────────────
[Test]
public void IsCheckmate_NullBoard_LogsErrorAndReturnsFalse()
{
LogAssert.Expect(LogType.Error, "[CheckDetector] Board es null.");
Assert.IsFalse(new CheckDetector().IsCheckmate(null, forWhite: true));
}
[Test]
public void IsCheckmate_FoolsMatePosition_ReturnsTrue()
{
// Mate del loco tras 1.f3 e5 2.g4 Qh4#: rey blanco sin escapes,
// sin bloqueos y sin capturas de la dama atacante.
var board = NewBoard();
Spawn<King>(board, 4, 0, isWhite: true); // Ke1
Spawn<Queen>(board, 3, 0, isWhite: true); // Qd1
Spawn<Bishop>(board, 5, 0, isWhite: true); // Bf1
Spawn<Knight>(board, 6, 0, isWhite: true); // Ng1
for (int x = 0; x < 8; x++)
{
if (x == 5 || x == 6) continue; // f2/g2 avanzados
Spawn<Pawn>(board, x, 1, isWhite: true);
}
Spawn<Pawn>(board, 5, 2, isWhite: true); // f3
Spawn<Pawn>(board, 6, 3, isWhite: true); // g4
Spawn<King>(board, 4, 7, isWhite: false); // Ke8
Spawn<Queen>(board, 7, 3, isWhite: false); // Qh4#
Spawn<Pawn>(board, 4, 4, isWhite: false); // e5
var detector = new CheckDetector();
Assert.IsTrue(detector.IsInCheck(board, forWhite: true), "Qh4 debe dar jaque a Ke1.");
Assert.IsTrue(detector.IsCheckmate(board, forWhite: true), "Mate del loco debe detectarse como jaque mate.");
}
[Test]
public void IsCheckmate_NotInCheck_ReturnsFalseEarly()
{
var board = NewBoard();
Spawn<King>(board, 4, 0, isWhite: true);
Spawn<King>(board, 4, 7, isWhite: false);
Assert.IsFalse(new CheckDetector().IsCheckmate(board, forWhite: true));
}
[Test]
public void HasAnyLegalMove_NullBoard_LogsErrorAndReturnsFalse()
{
LogAssert.Expect(LogType.Error, "[CheckDetector] Board es null.");
Assert.IsFalse(new CheckDetector().HasAnyLegalMove(null, forWhite: true));
}
[Test]
public void WouldLeaveKingInCheck_EnPassantAdjacentNotPawn_IgnoresCapture()
{
// Peón blanco captura en diagonal a casilla vacía con un CABALLO
// enemigo en la casilla adyacente: la rama "no es peón" debe dejar
// enPassantCaptured en null y restaurar el tablero intacto.
var board = NewBoard();
var pawn = Spawn<Pawn>(board, 4, 4, isWhite: true);
Spawn<King>(board, 0, 0, isWhite: true);
Spawn<Knight>(board, 5, 4, isWhite: false);
Spawn<King>(board, 7, 7, isWhite: false);
bool leavesInCheck = new CheckDetector()
.WouldLeaveKingInCheck(board, pawn, new Vector2Int(5, 5));
Assert.IsFalse(leavesInCheck, "La captura diagonal no debe exponer al rey.");
Assert.IsNotNull(board[4, 4], "El peón debe restaurarse a su casilla origen.");
Assert.IsNull(board[5, 5], "La casilla destino debe quedar como estaba.");
Assert.IsInstanceOf<Knight>(board[5, 4], "El caballo adyacente no debe tocarse.");
}
// ── DrawDetector ─────────────────────────────────────────────────
[Test]
public void IsStalemate_NotInCheckNoLegalMoves_ReturnsTrue()
{
// WK a1 atrapado por Qc2/BKb3: sin jaque y sin movimientos legales.
var board = NewBoard();
Spawn<King>(board, 0, 0, isWhite: true);
Spawn<King>(board, 1, 2, isWhite: false);
Spawn<Queen>(board, 2, 1, isWhite: false);
var detector = new DrawDetector();
var check = new CheckDetector();
Assert.IsFalse(check.IsInCheck(board, forWhite: true), "Precondición: sin jaque.");
Assert.IsTrue(detector.IsStalemate(board, forWhite: true, check));
}
[Test]
public void IsStalemate_InCheck_ReturnsFalseViaGuard()
{
var board = NewBoard();
Spawn<King>(board, 0, 0, isWhite: true);
Spawn<King>(board, 1, 2, isWhite: false);
Spawn<Queen>(board, 2, 1, isWhite: false);
Spawn<Rook>(board, 0, 4, isWhite: false); // torre en columna a: ¡jaque!
LogAssert.NoUnexpectedReceived();
Assert.IsFalse(new DrawDetector().IsStalemate(board, forWhite: true, new CheckDetector()));
}
[Test]
public void RecordMove_NullBoard_LogsErrorAndKeepsState()
{
var detector = new DrawDetector();
LogAssert.Expect(LogType.Error, "[DrawDetector] Board es null.");
detector.RecordMove(null, whiteTurn: true, movedPiece: null, wasCapture: false);
Assert.IsFalse(detector.IsFiftyMoveRule(), "El clock no debe cambiar con tablero nulo.");
Assert.IsFalse(detector.IsThreefoldRepetition(), "Sin historial nuevo no hay repetición.");
}
[Test]
public void GeneratePositionHash_EncodesAllPieceTypesWithCase()
{
var board = NewBoard();
Spawn<King>(board, 0, 0, isWhite: true);
Spawn<Queen>(board, 1, 0, isWhite: true);
Spawn<Rook>(board, 2, 0, isWhite: true);
Spawn<Bishop>(board, 3, 0, isWhite: true);
Spawn<Knight>(board, 4, 0, isWhite: true);
Spawn<Pawn>(board, 5, 0, isWhite: true);
Spawn<King>(board, 0, 7, isWhite: false);
Spawn<Queen>(board, 1, 7, isWhite: false);
Spawn<Rook>(board, 2, 7, isWhite: false);
Spawn<Bishop>(board, 3, 7, isWhite: false);
Spawn<Knight>(board, 4, 7, isWhite: false);
Spawn<Pawn>(board, 5, 7, isWhite: false);
string hash = InvokeGeneratePositionHash(new DrawDetector(), board, whiteTurn: true);
StringAssert.Contains("K", hash);
StringAssert.Contains("Q", hash);
StringAssert.Contains("R", hash);
StringAssert.Contains("B", hash);
StringAssert.Contains("N", hash);
StringAssert.Contains("P", hash);
StringAssert.Contains("k", hash);
StringAssert.Contains("q", hash);
StringAssert.Contains("r", hash);
StringAssert.Contains("b", hash);
StringAssert.Contains("n", hash);
StringAssert.Contains("p", hash);
}
[Test]
public void GeneratePositionHash_DistinguishesTurn()
{
var board = NewBoard();
Spawn<King>(board, 4, 0, isWhite: true);
Spawn<King>(board, 4, 7, isWhite: false);
var detector = new DrawDetector();
string whiteHash = InvokeGeneratePositionHash(detector, board, whiteTurn: true);
string blackHash = InvokeGeneratePositionHash(detector, board, whiteTurn: false);
Assert.AreNotEqual(whiteHash, blackHash, "El turno debe formar parte del hash de posición.");
}
[Test]
public void IsThreefoldRepetition_TrueOnlyAfterThreeIdenticalPositions()
{
var board = NewBoard();
Spawn<King>(board, 4, 0, isWhite: true);
Spawn<King>(board, 4, 7, isWhite: false);
var detector = new DrawDetector();
// Mismo tablero y MISMO turno: el hash de posición incluye el turno,
// así que repetir la posición exige repetirla con el turno idéntico.
detector.RecordMove(board, whiteTurn: true, movedPiece: null, wasCapture: false);
Assert.IsFalse(detector.IsThreefoldRepetition());
detector.RecordMove(board, whiteTurn: true, movedPiece: null, wasCapture: false);
Assert.IsFalse(detector.IsThreefoldRepetition());
detector.RecordMove(board, whiteTurn: true, movedPiece: null, wasCapture: false);
Assert.IsTrue(detector.IsThreefoldRepetition(), "Tres posiciones idénticas deben disparar la regla.");
}
private static string InvokeGeneratePositionHash(DrawDetector detector, Piece[,] board, bool whiteTurn)
{
var method = typeof(DrawDetector).GetMethod(
"GeneratePositionHash",
BindingFlags.NonPublic | BindingFlags.Instance);
Assert.IsNotNull(method, "GeneratePositionHash no encontrado.");
return (string)method.Invoke(detector, new object[] { board, whiteTurn });
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f2f8db82376f06847a30567ddbb0ff8d
@@ -0,0 +1,70 @@
using System.Collections.Generic;
using NUnit.Framework;
using UnityEngine;
/// <summary>
/// Cobertura de GameManagerClassic.IsCheckmate: posición con jaque mate
/// real vs. simple ausencia de jaque (retorno temprano).
/// </summary>
public class GameManagerClassicCoverageTests
{
private GameManagerClassic _gm;
private readonly List<GameObject> _spawned = new List<GameObject>();
[SetUp]
public void SetUp()
{
var gmObject = new GameObject("GameManagerClassicCoverage");
_spawned.Add(gmObject);
_gm = gmObject.AddComponent<GameManagerClassic>();
_gm.board = new Piece[8, 8];
GameManagerClassic.Instance = _gm;
}
[TearDown]
public void TearDown()
{
foreach (var obj in _spawned)
{
if (obj != null) Object.DestroyImmediate(obj);
}
_spawned.Clear();
GameManagerClassic.Instance = null;
}
private T SpawnPiece<T>(int x, int y, bool isWhite) where T : Piece
{
var obj = new GameObject($"{typeof(T).Name}_{x}_{y}");
_spawned.Add(obj);
var piece = obj.AddComponent<T>();
piece.currentPos = new Vector2Int(x, y);
piece.isWhite = isWhite;
_gm.board[x, y] = piece;
return piece;
}
[Test]
public void IsCheckmate_NotInCheck_ReturnsFalseImmediately()
{
SpawnPiece<King>(4, 0, isWhite: true);
SpawnPiece<King>(4, 7, isWhite: false);
Assert.IsFalse(_gm.IsCheckmate(forWhite: true),
"Sin jaque no puede haber jaque mate.");
}
[Test]
public void IsCheckmate_BackRankMate_ReturnsTrue()
{
// Mate de pasillo: rey blanco en la primera fila, encerrado por sus
// propios peones de a2/b2, con torre negra en h1 atacando toda la fila.
SpawnPiece<King>(0, 0, isWhite: true); // Ka1
SpawnPiece<Pawn>(0, 1, isWhite: true); // a2
SpawnPiece<Pawn>(1, 1, isWhite: true); // b2
SpawnPiece<Rook>(7, 0, isWhite: false); // Rh1#
SpawnPiece<King>(7, 5, isWhite: false); // rey negro lejos
Assert.IsTrue(_gm.IsInCheck(forWhite: true), "Precondición: Rh1 debe dar jaque por la fila 1.");
Assert.IsTrue(_gm.IsCheckmate(forWhite: true));
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e67d15f770c99df47abc62eaea7f018e
@@ -0,0 +1,497 @@
using System.Collections.Generic;
using System.Reflection;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
/// <summary>
/// Cobertura de ramas restantes de DialogueSystem: skip con coroutine activa,
/// navegación GetNextNode (next nulo/vacío/desconocido), branching con
/// condición default y fallback, guards sin CampaignState y parseo completo
/// de pieces_lost_count.
/// </summary>
public class DialogueSystemCoverageTests
{
private static readonly BindingFlags Flags =
BindingFlags.NonPublic | BindingFlags.Instance;
private GameObject _root;
private DialogueSystem _dialogue;
private CampaignState _campaignState;
private readonly FieldInfo _isActiveField;
private readonly FieldInfo _currentNodeField;
private readonly FieldInfo _currentDialogueField;
private readonly FieldInfo _isTypingField;
private readonly FieldInfo _coroutineField;
private readonly FieldInfo _stateField;
public DialogueSystemCoverageTests()
{
_isActiveField = typeof(DialogueSystem).GetField("_isActive", Flags);
_currentNodeField = typeof(DialogueSystem).GetField("_currentNode", Flags);
_currentDialogueField = typeof(DialogueSystem).GetField("_currentDialogue", Flags);
_isTypingField = typeof(DialogueSystem).GetField("_isTyping", Flags);
_coroutineField = typeof(DialogueSystem).GetField("_typewriterCoroutine", Flags);
_stateField = typeof(DialogueSystem).GetField("_campaignState", Flags);
}
[SetUp]
public void Setup()
{
_root = new GameObject("DialogueCoverageRoot");
_dialogue = _root.AddComponent<DialogueSystem>();
_campaignState = ScriptableObject.CreateInstance<CampaignState>();
_stateField.SetValue(_dialogue, _campaignState);
}
[TearDown]
public void Teardown()
{
Object.DestroyImmediate(_root);
Object.DestroyImmediate(_campaignState);
}
private void Activate(DialogueData data, DialogueNode current)
{
_currentDialogueField.SetValue(_dialogue, data);
_currentNodeField.SetValue(_dialogue, current);
_isActiveField.SetValue(_dialogue, true);
_isTypingField.SetValue(_dialogue, false);
}
private int _completions;
private bool _dialogueEnded;
private void Subscribe()
{
_completions = 0;
_dialogueEnded = false;
_dialogue.OnNodeComplete += _ => _completions++;
_dialogue.OnDialogueComplete += () => _dialogueEnded = true;
}
private DialogueNode Node(string id, string text, string next = null)
{
return new DialogueNode { id = id, speaker = "muerte", text = text, next = next };
}
// ──────────── Flujo real: DisplayNode → typewriter vivo ────────────
[Test]
public void CompleteCurrentNode_LiveTypewriter_StopsCoroutineAndEmitsFullText()
{
var nodeA = Node("a1", "texto largo que escribe despacio", "b1");
var data = new DialogueData { id = "d", nodes = new List<DialogueNode> { nodeA } };
_currentDialogueField.SetValue(_dialogue, data);
_isActiveField.SetValue(_dialogue, true);
// DisplayNode arranca la corrutina real del typewriter.
InvokePrivate("DisplayNode", nodeA);
Assert.IsTrue((bool)_isTypingField.GetValue(_dialogue),
"DisplayNode debe marcar _isTyping mientras el typewriter vive.");
Assert.IsNotNull(_coroutineField.GetValue(_dialogue),
"DisplayNode debe guardar el handle de la corrutina.");
string fullText = null;
int completions = 0;
_dialogue.OnTextUpdated += t => fullText = t;
_dialogue.OnNodeComplete += _ => completions++;
_dialogue.CompleteCurrentNode();
Assert.IsFalse((bool)_isTypingField.GetValue(_dialogue));
Assert.IsNull(_coroutineField.GetValue(_dialogue));
Assert.AreEqual(nodeA.text, fullText, "El texto completo debe emitirse al saltar.");
Assert.AreEqual(1, completions);
}
[Test]
public void AdvanceDialogue_FullChain_SecondNodeDisplaysAndEndStopsLiveCoroutine()
{
var n1 = Node("p1", "primero", "p2");
var n2 = Node("p2", "último", null);
var data = new DialogueData { id = "adv", nodes = new List<DialogueNode> { n1, n2 } };
_currentDialogueField.SetValue(_dialogue, data);
_isActiveField.SetValue(_dialogue, true);
bool ended = false;
_dialogue.OnDialogueComplete += () => ended = true;
InvokePrivate("DisplayNode", n1); // corrutina #1 viva
_isTypingField.SetValue(_dialogue, false); // typing terminado (handle residual)
_dialogue.AdvanceDialogue(); // p1 → p2 vía GetNextNode
var current = _currentNodeField.GetValue(_dialogue) as DialogueNode;
Assert.AreEqual("p2", current.id, "Debe avanzar y mostrar el nodo siguiente.");
_isTypingField.SetValue(_dialogue, false);
LogAssert.Expect(LogType.Log, "[DialogueSystem] Diálogo terminado: adv");
_dialogue.AdvanceDialogue(); // p2 terminal → EndDialogue
Assert.IsTrue(ended);
Assert.IsNull(_coroutineField.GetValue(_dialogue),
"EndDialogue debe detener y limpiar la corrutina viva.");
Assert.IsFalse((bool)_isActiveField.GetValue(_dialogue));
}
[Test]
public void Guards_InactiveOrEmptyDialogue_AreNoOps()
{
// Sin diálogo activo: ni CompleteCurrentNode ni AdvanceDialogue hacen nada.
_dialogue.CompleteCurrentNode();
_dialogue.AdvanceDialogue();
Assert.Pass("Sin excepciones ni eventos: guards correctos.");
}
[Test]
public void GetNextNode_BranchingResolvesToEmpty_ReturnsNull()
{
var branchy = Node("x1", "con default vacío", "real");
branchy.conditions = new List<DialogueCondition>
{
new DialogueCondition { type = "default", nextNode = "" }
};
// EvaluateBranching devuelve "" → nextNodeId vacío → return null (282-283).
var result = (DialogueNode)InvokePrivate("GetNextNode", branchy);
Assert.IsNull(result);
}
// ── CompleteCurrentNode con coroutine real ───────────────────────
[Test]
public void CompleteCurrentNode_ActiveCoroutine_IsStoppedAndFullTextShown()
{
var node = Node("n1", "Texto que se corta");
Subscribe();
System.Collections.IEnumerator Dummy() { yield break; }
Coroutine coroutine;
try
{
coroutine = _dialogue.StartCoroutine(Dummy());
}
catch (UnityException)
{
Assert.Ignore("StartCoroutine no disponible en este contexto de edición.");
return;
}
Activate(new DialogueData { id = "d", nodes = new List<DialogueNode> { node } }, node);
_isTypingField.SetValue(_dialogue, true);
_coroutineField.SetValue(_dialogue, coroutine);
_dialogue.CompleteCurrentNode();
Assert.IsFalse((bool)_isTypingField.GetValue(_dialogue), "El skip debe apagar _isTyping.");
Assert.IsNull(_coroutineField.GetValue(_dialogue), "La referencia a la coroutine debe limpiarse.");
Assert.AreEqual(1, _completions, "Skip dispara exactamente una completion.");
Assert.IsFalse(_dialogueEnded, "Skip de nodo no termina el diálogo.");
}
// ── AdvanceDialogue ──────────────────────────────────────────────
[Test]
public void AdvanceDialogue_WhileTyping_CompletesInsteadOfAdvancing()
{
var n1 = Node("n1", "hola", "n2");
var n2 = Node("n2", "chau");
Activate(new DialogueData { id = "d", nodes = new List<DialogueNode> { n1, n2 } }, n1);
Subscribe();
_isTypingField.SetValue(_dialogue, true); // coroutine null: solo completa texto
LogAssert.ignoreFailingMessages = true;
_dialogue.AdvanceDialogue();
LogAssert.ignoreFailingMessages = false;
Assert.AreSame(n1, _currentNodeField.GetValue(_dialogue),
"Mientras escribe, Advance completa el nodo y NO avanza.");
Assert.IsFalse(_dialogueEnded, "No debe terminar el diálogo.");
}
[Test]
public void AdvanceDialogue_LastNode_EndsDialogue()
{
var n1 = Node("n1", "último", null); // sin next
Activate(new DialogueData { id = "d", nodes = new List<DialogueNode> { n1 } }, n1);
Subscribe();
LogAssert.Expect(LogType.Log, "[DialogueSystem] Diálogo terminado: d");
_dialogue.AdvanceDialogue();
Assert.IsTrue(_dialogueEnded, "Sin nodo siguiente debe disparar OnDialogueComplete.");
Assert.IsFalse((bool)_isActiveField.GetValue(_dialogue));
}
// ── GetNextNode (privado) ────────────────────────────────────────
[Test]
public void GetNextNode_NullNext_ReturnsNull()
{
var n1 = Node("n1", "sin salida", null);
Activate(new DialogueData { id = "d", nodes = new List<DialogueNode> { n1 } }, n1);
Assert.IsNull(InvokeGetNextNode(n1));
}
[Test]
public void GetNextNode_ConditionMet_GoesToBranch()
{
_campaignState.RegisterPieceAlive("elena");
var branch = Node("branch_a", "ramo vivo");
var n1 = new DialogueNode
{
id = "n1",
text = "con condiciones",
next = "fallback",
conditions = new List<DialogueCondition>
{
new DialogueCondition { type = "piece_alive", param = "elena", nextNode = "branch_a" }
}
};
Activate(new DialogueData { id = "d", nodes = new List<DialogueNode> { n1, branch } }, n1);
Assert.AreSame(branch, InvokeGetNextNode(n1), "Debe resolver el branching al nodo vivo.");
}
[Test]
public void GetNextNode_BranchedIdDoesNotExist_ReturnsNull()
{
_campaignState.RegisterPieceAlive("elena");
var n1 = new DialogueNode
{
id = "n1",
text = "a nodo fantasma",
next = "fallback",
conditions = new List<DialogueCondition>
{
new DialogueCondition { type = "piece_alive", param = "elena", nextNode = "fantasma" }
}
};
Activate(new DialogueData { id = "d", nodes = new List<DialogueNode> { n1 } }, n1);
Assert.IsNull(InvokeGetNextNode(n1), "Un id de rama inexistente debe retornar null.");
}
[Test]
public void GetNextNode_EmptyResolvedId_ReturnsNull()
{
// Condición fallida + sin default → usa next, que está vacío.
var n1 = new DialogueNode
{
id = "n1",
text = "next vacío",
next = "",
conditions = new List<DialogueCondition>
{
new DialogueCondition { type = "piece_alive", param = "fantasma", nextNode = "x" }
}
};
Activate(new DialogueData { id = "d", nodes = new List<DialogueNode> { n1 } }, n1);
Assert.IsNull(InvokeGetNextNode(n1));
}
// ── EvaluateBranching ────────────────────────────────────────────
[Test]
public void EvaluateBranching_NoConditionMatches_UsesDefaultCondition()
{
_campaignState.RegisterPieceAlive("elena");
_campaignState.RegisterPieceLost("elena"); // muerta
var node = new DialogueNode
{
id = "n",
text = "t",
next = "normal",
conditions = new List<DialogueCondition>
{
new DialogueCondition { type = "piece_alive", param = "elena", nextNode = "rama_muerta" },
new DialogueCondition { type = "default", nextNode = "rama_default" }
}
};
Assert.AreEqual("rama_default", InvokeEvaluateBranching(node),
"Ninguna condición real cumplida → usa la condición 'default'.");
}
[Test]
public void EvaluateBranching_NoDefault_FallsBackToPlainNext()
{
_campaignState.RegisterPieceAlive("elena");
_campaignState.RegisterPieceLost("elena");
var node = new DialogueNode
{
id = "n",
text = "t",
next = "camino_normal",
conditions = new List<DialogueCondition>
{
new DialogueCondition { type = "piece_alive", param = "elena", nextNode = "rama" }
}
};
Assert.AreEqual("camino_normal", InvokeEvaluateBranching(node),
"Sin match y sin default → campo next normal.");
}
// ── CheckCondition ───────────────────────────────────────────────
[Test]
public void CheckCondition_DefaultType_ReturnsTrue()
{
Assert.IsTrue(InvokeCheckCondition(new DialogueCondition { type = "default" }));
}
[Test]
public void CheckCondition_UnknownType_WarnsAndReturnsFalse()
{
LogAssert.Expect(LogType.Warning, "[DialogueSystem] Tipo de condición desconocido: telepatia");
Assert.IsFalse(InvokeCheckCondition(new DialogueCondition { type = "telepatia", param = "x" }));
}
[Test]
public void CheckCondition_NullCondition_ReturnsFalse()
{
var method = typeof(DialogueSystem).GetMethod("CheckCondition", Flags);
Assert.IsNotNull(method);
Assert.IsFalse((bool)method.Invoke(_dialogue, new object[] { null }));
}
// ── Guards sin CampaignState ─────────────────────────────────────
[Test]
public void Conditions_WithoutCampaignState_UseDocumentedFallbacks()
{
_stateField.SetValue(_dialogue, null);
try
{
LogAssert.Expect(LogType.Warning,
"[DialogueSystem] CampaignState no asignado. Condición piece_alive siempre retorna true.");
bool pieceAlive = InvokeCheckCondition("piece_alive", "elena");
LogAssert.Expect(LogType.Warning,
"[DialogueSystem] CampaignState no asignado. Condición chapter_complete siempre retorna false.");
bool chapterComplete = InvokeCheckCondition("chapter_complete", "ch1");
LogAssert.Expect(LogType.Warning,
"[DialogueSystem] CampaignState no asignado. Condición pieces_lost_count siempre retorna false.");
bool lostCount = InvokeCheckCondition("pieces_lost_count", "<3");
Assert.IsTrue(pieceAlive, "piece_alive sin estado → true (documentado).");
Assert.IsFalse(chapterComplete, "chapter_complete sin estado → false.");
Assert.IsFalse(lostCount, "pieces_lost_count sin estado → false.");
}
finally
{
_stateField.SetValue(_dialogue, _campaignState);
}
}
// ── chapter_complete especial ch3 ────────────────────────────────
[Test]
public void CheckChapterComplete_Ch3_ReadsVictoryFlag()
{
_campaignState.LastChapterWasVictory = true;
Assert.IsTrue(InvokeCheckCondition("chapter_complete", "ch3"),
"ch3 se resuelve por LastChapterWasVictory.");
_campaignState.LastChapterWasVictory = false;
Assert.IsFalse(InvokeCheckCondition("chapter_complete", "ch3"));
}
[Test]
public void CheckChapterComplete_OtherChapters_ReadCompletionSet()
{
Assert.IsFalse(InvokeCheckCondition("chapter_complete", "ch1"));
_campaignState.CompleteChapter("ch1", wasVictory: false);
Assert.IsTrue(InvokeCheckCondition("chapter_complete", "ch1"));
}
// ── pieces_lost_count parseo completo ────────────────────────────
[Test]
[TestCase("<=", 3, 2, true)]
[TestCase("<=", 3, 3, true)]
[TestCase(">=", 1, 0, false)]
[TestCase(">=", 1, 1, true)]
[TestCase(">", 2, 5, true)]
[TestCase(">", 2, 2, false)]
[TestCase("==", 4, 4, true)]
[TestCase("==", 4, 1, false)]
public void CheckPiecesLostCount_AllOperators(string op, int target, int actualLost, bool expected)
{
SetupLost(actualLost);
Assert.AreEqual(expected, InvokeCheckCondition("pieces_lost_count", $"{op}{target}"));
}
[Test]
public void CheckPiecesLostCount_PlainNumber_TreatedAsEquality()
{
SetupLost(2);
Assert.IsTrue(InvokeCheckCondition("pieces_lost_count", "2"),
"Sin operador se asume igualdad.");
}
[Test]
public void CheckPiecesLostCount_EmptyParam_WarnsAndReturnsFalse()
{
LogAssert.Expect(LogType.Warning, "[DialogueSystem] Parámetro pieces_lost_count vacío.");
Assert.IsFalse(InvokeCheckCondition("pieces_lost_count", ""));
}
[Test]
public void CheckPiecesLostCount_NonNumericValue_WarnsAndReturnsFalse()
{
SetupLost(1);
LogAssert.Expect(LogType.Warning, "[DialogueSystem] No se pudo parsear valor numérico: abc");
Assert.IsFalse(InvokeCheckCondition("pieces_lost_count", "<abc"));
}
// ── helpers ──────────────────────────────────────────────────────
private void SetupLost(int count)
{
_campaignState.StartChapter("cap_test");
for (int i = 0; i < count; i++)
{
_campaignState.RegisterPieceLost($"pieza_{i}");
}
}
private object InvokePrivate(string methodName, params object[] args)
{
var method = typeof(DialogueSystem).GetMethod(methodName, Flags);
Assert.IsNotNull(method, $"{methodName} no encontrado.");
return method.Invoke(_dialogue, args);
}
private DialogueNode InvokeGetNextNode(DialogueNode node)
{
return (DialogueNode)InvokePrivate("GetNextNode", node);
}
private string InvokeEvaluateBranching(DialogueNode node)
{
return (string)InvokePrivate("EvaluateBranching", node);
}
private bool InvokeCheckCondition(string type, string param)
{
return (bool)InvokePrivate("CheckCondition",
new DialogueCondition { type = type, param = param });
}
private bool InvokeCheckCondition(DialogueCondition condition)
{
return (bool)InvokePrivate("CheckCondition", condition);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 812d7c002b832fe4faf237b200cc0d94
@@ -0,0 +1,166 @@
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
using AjedrezPurgatorio.Meta;
using AjedrezPurgatorio.Data;
/// <summary>
/// Cobertura de ramas restantes de DeadKingPool: rotación FIFO con logs,
/// pool vacío, carga corrupta/excepcional, guardado fallido y filtro de
/// contenido con entradas en blanco.
/// </summary>
[TestFixture]
[Category("Unit")]
[Category("Meta")]
[Category("DeadKingPool")]
[Category("FastTests")]
public class DeadKingPoolCoverageTests
{
private DeadKingPool _pool;
private string _tempDir;
[SetUp]
public void Setup()
{
// DeadKingPool es una clase plana (no MonoBehaviour): se instancia
// directamente y la ruta persistente se inyecta para tests.
_pool = new DeadKingPool();
_tempDir = Path.Combine(Application.temporaryCachePath, "dkp_coverage");
Directory.CreateDirectory(_tempDir);
InjectField("_persistentFilePath", Path.Combine(_tempDir, "dead_kings.json"));
InjectField("_enableDebugLogging", true);
_pool.Initialize();
}
[TearDown]
public void Teardown()
{
if (Directory.Exists(_tempDir))
{
Directory.Delete(_tempDir, true);
}
}
private void InjectField(string name, object value)
{
var field = typeof(DeadKingPool).GetField(name,
BindingFlags.NonPublic | BindingFlags.Instance);
Assert.IsNotNull(field, $"{name} no encontrado en DeadKingPool.");
field.SetValue(_pool, value);
}
private static int _kingCounter;
private static DeadKingData MakeKing(string name = "rey", string message = "hola")
{
_kingCounter++;
return new DeadKingData(name, message, chapter: _kingCounter, piecesLostCount: 0, stats: null);
}
[Test]
public void AddDeadKing_PoolFull_RemovesOldestFirst()
{
InjectField("_maxPoolSize", 2);
_pool.AddDeadKing(MakeKing("viejo"));
_pool.AddDeadKing(MakeKing("medio"));
_pool.AddDeadKing(MakeKing("nuevo")); // dispara la rotación
Assert.AreEqual(2, _pool.Count, "El pool no debe crecer más allá del máximo.");
List<DeadKingData> all = _pool.GetAllDeadKings();
Assert.IsFalse(all.Exists(k => k.playerName == "viejo"), "El más viejo debe salir por FIFO.");
Assert.IsTrue(all.Exists(k => k.playerName == "nuevo"));
}
[Test]
public void GetRandomAndShouldSpawn_EmptyPool_ReturnNullAndFalse()
{
_pool.ClearPool();
LogAssert.Expect(LogType.Log, "[DeadKingPool] Pool is empty. Returning null.");
Assert.IsNull(_pool.GetRandomDeadKing());
LogAssert.Expect(LogType.Log, "[DeadKingPool] Pool is empty. No spawn.");
Assert.IsFalse(_pool.ShouldSpawnDeadKing(5));
}
[Test]
public void GetRandomDeadKing_WithEntries_ReturnsAnEntry()
{
_pool.ClearPool();
_pool.AddDeadKing(MakeKing("unico"));
var drawn = _pool.GetRandomDeadKing();
Assert.IsNotNull(drawn);
Assert.AreEqual("unico", drawn.playerName);
}
[Test]
public void AddDeadKing_BlankMessage_BypassesFilterUnchanged()
{
_pool.ClearPool();
// El constructor sanitiza playerName en blanco, pero message conserva
// el whitespace: FilterContent debe devolverlo sin tocar.
var blank = new DeadKingData("rey", "\t", chapter: 1, piecesLostCount: 0, stats: null);
_pool.AddDeadKing(blank);
var stored = _pool.GetAllDeadKings()[0];
Assert.AreEqual("\t", stored.message, "Espacios en blanco pasan por el filtro sin cambios.");
}
[Test]
public void LoadPool_NullJson_FallsToCatchAndStartsEmpty()
{
// JsonUtility.FromJson("null") lanza ("JSON must represent an object
// type") → la ruta real es el catch con LogError, no el warning.
File.WriteAllText(Path.Combine(_tempDir, "dead_kings.json"), "null");
LogAssert.Expect(LogType.Error,
"[DeadKingPool] Failed to load pool: JSON must represent an object type.. Starting with empty pool.");
_pool.LoadPool();
Assert.AreEqual(0, _pool.Count, "JSON nulo debe iniciar un pool vacío.");
}
[Test]
public void LoadPool_EmptyJsonObject_StartsWithEmptyPool()
{
// "{}" deserializa a poolData con lista vacía (JsonUtility nunca
// devuelve null en campos de colección) → foreach no itera, Count 0.
File.WriteAllText(Path.Combine(_tempDir, "dead_kings.json"), "{}");
_pool.LoadPool();
Assert.AreEqual(0, _pool.Count);
}
[Test]
public void LoadPool_MalformedJson_CatchesExceptionAndStartsEmpty()
{
File.WriteAllText(Path.Combine(_tempDir, "dead_kings.json"), "{json roto::");
LogAssert.ignoreFailingMessages = true;
_pool.LoadPool();
LogAssert.ignoreFailingMessages = false;
Assert.AreEqual(0, _pool.Count, "JSON malformado debe caer al catch e iniciar vacío.");
}
[Test]
public void SavePool_UnwritablePath_CatchesException()
{
// Ruta dentro de un directorio inexistente: WriteAllText lanza.
InjectField("_persistentFilePath", Path.Combine(_tempDir, "no_existe", "x.json"));
LogAssert.ignoreFailingMessages = true;
_pool.ClearPool(); // Initialize + Clear → SavePool sobre ruta inválida
LogAssert.ignoreFailingMessages = false;
Assert.IsTrue(true, "ClearPool completó sin propagar la excepción de escritura.");
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 614d8020ca8122c4f8eafcfa53e13132
@@ -0,0 +1,323 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text.RegularExpressions;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
using AjedrezPurgatorio.Meta;
namespace AjedrezPurgatorio.Tests.Unit.Meta
{
/// <summary>
/// Cobertura de las rutas felices y de error del ciclo completo
/// save/load/delete/info (complementa SaveSystemTests, que solo cubre
/// validación de slots y casos nulos).
/// </summary>
[TestFixture]
[Category("Unit")]
[Category("Meta")]
[Category("SaveSystem")]
[Category("FastTests")]
public class SaveSystemCoverageTests
{
private GameObject _root;
private SaveSystem _saveSystem;
private CampaignState _campaignState;
[SetUp]
public void Setup()
{
_root = new GameObject("SaveSystemCoverageRoot");
_saveSystem = _root.AddComponent<SaveSystem>();
_campaignState = ScriptableObject.CreateInstance<CampaignState>();
foreach (var leftover in Directory.GetFiles(Application.persistentDataPath, "save_slot_*.json"))
{
File.Delete(leftover);
}
}
[TearDown]
public void Teardown()
{
UnityEngine.Object.DestroyImmediate(_campaignState);
UnityEngine.Object.DestroyImmediate(_root);
foreach (var leftover in Directory.GetFiles(Application.persistentDataPath, "save_slot_*.json"))
{
File.Delete(leftover);
}
}
private string SlotPath(int slot)
{
return Path.Combine(Application.persistentDataPath, $"save_slot_{slot}.json");
}
private CampaignState MakeState()
{
var state = ScriptableObject.CreateInstance<CampaignState>();
state.RegisterPieceAlive("elena");
state.RegisterPieceAlive("ricardo");
state.RegisterPieceLost("carlos");
state.CompleteChapter("ch1_factory", wasVictory: true);
return state;
}
[Test]
public void SaveGame_RoundTrip_PreservesData()
{
var state = MakeState();
Assert.IsTrue(_saveSystem.SaveGame(0, state, 2, campaignId: "gambito"));
Assert.IsTrue(_saveSystem.DoesSaveExist(0), "El archivo de save debería existir tras SaveGame.");
Assert.IsFalse(_saveSystem.DoesSaveExist(1), "Otros slots no deberían verse afectados.");
SaveSystem.SaveData data = _saveSystem.LoadGame(0);
Assert.IsNotNull(data);
Assert.AreEqual(0, data.saveSlot);
Assert.AreEqual("gambito", data.campaignId);
Assert.AreEqual(2, data.currentChapterIndex);
Assert.AreEqual(1, data.totalPiecesLost, "RegisterPieceLost en el estado debe reflejarse en el save.");
UnityEngine.Object.DestroyImmediate(state);
}
[Test]
public void LoadGame_MissingFile_ReturnsNull()
{
Assert.IsNull(_saveSystem.LoadGame(2));
}
[Test]
public void LoadGame_NullJson_FallsToCatchAndReturnsNull()
{
// JsonUtility.FromJson("null") lanza en vez de retornar null:
// la ruta real es el catch con su LogError.
File.WriteAllText(SlotPath(0), "null");
LogAssert.Expect(LogType.Error,
"[SaveSystem] Failed to load game from slot 0: JSON must represent an object type.");
Assert.IsNull(_saveSystem.LoadGame(0));
}
[Test]
public void LoadGame_FileLockedByExternalHandle_ReturnsNull()
{
var state = MakeState();
_saveSystem.SaveGame(0, state, 0);
UnityEngine.Object.DestroyImmediate(state);
using (var lockHandle = new FileStream(SlotPath(0), FileMode.Open, FileAccess.Read, FileShare.None))
{
LogAssert.ignoreFailingMessages = true;
bool threwInsideCall = false;
SaveSystem.SaveData result = null;
try
{
result = _saveSystem.LoadGame(0);
}
catch (IOException)
{
threwInsideCall = true;
}
LogAssert.ignoreFailingMessages = false;
lockHandle.Close();
if (threwInsideCall)
{
Assert.Fail("LoadGame debería capturar IOException internamente, no propagarla.");
}
Assert.IsNull(result, "Con el archivo bloqueado, LoadGame debe retornar null vía catch.");
}
}
[Test]
public void DeleteSave_ExistingSave_ReturnsTrueAndRemovesFile()
{
var state = MakeState();
_saveSystem.SaveGame(0, state, 0);
UnityEngine.Object.DestroyImmediate(state);
Assert.IsTrue(_saveSystem.DeleteSave(0));
Assert.IsFalse(File.Exists(SlotPath(0)), "DeleteSave debe eliminar el archivo físicamente.");
Assert.IsNull(_saveSystem.LoadGame(0));
}
[Test]
public void GetSaveInfo_ExistingSave_PopulatesFields()
{
var state = MakeState();
_saveSystem.SaveGame(1, state, 3, campaignId: "ricardo");
UnityEngine.Object.DestroyImmediate(state);
var info = _saveSystem.GetSaveInfo(1);
Assert.IsTrue(info.exists);
Assert.AreEqual(1, info.slot);
Assert.AreEqual(3, info.currentChapter);
Assert.IsNotEmpty(info.saveDate);
}
[Test]
public void GetMostRecentSaveSlot_NewestSlotWins()
{
var state = MakeState();
_saveSystem.SaveGame(0, state, 0);
System.Threading.Thread.Sleep(50); // timestamps ISO con ticks distintos
_saveSystem.SaveGame(2, state, 5);
UnityEngine.Object.DestroyImmediate(state);
Assert.AreEqual(2, _saveSystem.GetMostRecentSaveSlot());
}
[Test]
public void GetMostRecentSaveSlot_InvalidDateIsSkipped()
{
var state = MakeState();
_saveSystem.SaveGame(0, state, 0);
UnityEngine.Object.DestroyImmediate(state);
// Fecha corrupta en slot 1: debe saltarse sin romper la búsqueda.
File.WriteAllText(SlotPath(1), "{\"saveDate\":\"no-es-fecha\",\"currentChapterIndex\":9}");
Assert.AreEqual(0, _saveSystem.GetMostRecentSaveSlot());
}
// ────────────── Logging detallado (_enableDebugLogging) ──────────────
private void EnableDebugLogging()
{
typeof(SaveSystem)
.GetField("_enableDebugLogging", BindingFlags.NonPublic | BindingFlags.Instance)
.SetValue(_saveSystem, true);
}
[Test]
public void SaveGame_WithLogging_EmitsSaveMessage()
{
var state = MakeState();
EnableDebugLogging();
LogAssert.Expect(LogType.Log, new Regex(@"\[SaveSystem\] Game saved to slot 0\..*"));
_saveSystem.SaveGame(0, state, 0);
UnityEngine.Object.DestroyImmediate(state);
Assert.IsTrue(_saveSystem.DoesSaveExist(0));
}
[Test]
public void SaveGame_LockedFile_CatchesExceptionAndReturnsFalse()
{
var state = MakeState();
File.WriteAllText(SlotPath(0), "{}");
using (new FileStream(SlotPath(0), FileMode.Open, FileAccess.Read, FileShare.None))
{
LogAssert.ignoreFailingMessages = true; // texto de IOException variable
bool result = false;
try
{
result = _saveSystem.SaveGame(0, state, 0);
}
finally
{
LogAssert.ignoreFailingMessages = false;
}
Assert.IsFalse(result, "Guardar sobre archivo bloqueado debe fallar controlado.");
}
UnityEngine.Object.DestroyImmediate(state);
}
[Test]
public void LoadGame_MissingFileWithLogging_EmitsNoSaveMessage()
{
EnableDebugLogging();
LogAssert.Expect(LogType.Log, "[SaveSystem] No save file found in slot 0.");
Assert.IsNull(_saveSystem.LoadGame(0));
}
[Test]
public void LoadGame_SuccessWithLogging_EmitsLoadedMessage()
{
var state = MakeState();
_saveSystem.SaveGame(0, state, 4);
UnityEngine.Object.DestroyImmediate(state);
EnableDebugLogging();
LogAssert.Expect(LogType.Log, new Regex(@"\[SaveSystem\] Game loaded from slot 0\..*"));
var data = _saveSystem.LoadGame(0);
Assert.IsNotNull(data);
Assert.AreEqual(4, data.currentChapterIndex);
}
[Test]
public void DeleteSave_MissingFileWithLogging_EmitsNothingToDelete()
{
EnableDebugLogging();
LogAssert.Expect(LogType.Log, "[SaveSystem] No save file to delete in slot 0.");
Assert.IsFalse(_saveSystem.DeleteSave(0));
}
[Test]
public void DeleteSave_SuccessWithLogging_EmitsDeletedMessage()
{
File.WriteAllText(SlotPath(0), "{}");
EnableDebugLogging();
LogAssert.Expect(LogType.Log, "[SaveSystem] Save file deleted from slot 0.");
Assert.IsTrue(_saveSystem.DeleteSave(0));
Assert.IsFalse(File.Exists(SlotPath(0)));
}
[Test]
public void DeleteSave_LockedFile_CatchesExceptionAndReturnsFalse()
{
File.WriteAllText(SlotPath(0), "{}");
using (new FileStream(SlotPath(0), FileMode.Open, FileAccess.Read, FileShare.None))
{
LogAssert.ignoreFailingMessages = true;
bool result;
try
{
result = _saveSystem.DeleteSave(0);
}
finally
{
LogAssert.ignoreFailingMessages = false;
}
Assert.IsFalse(result, "Borrar un archivo bloqueado debe fallar controlado.");
}
}
[Test]
public void GetSaveInfo_UnreadableFile_WarnsAndReturnsDefault()
{
File.WriteAllText(SlotPath(0), "{}");
using (new FileStream(SlotPath(0), FileMode.Open, FileAccess.Read, FileShare.None))
{
LogAssert.ignoreFailingMessages = true; // Warning con mensaje de excepción variable
SaveSystem.SaveInfo info = null;
try
{
info = _saveSystem.GetSaveInfo(0);
}
finally
{
LogAssert.ignoreFailingMessages = false;
}
Assert.IsNotNull(info);
Assert.IsFalse(info.exists, "Sin lectura no hay info real.");
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9fa96bb4632bc5d428329973df53d006
@@ -0,0 +1,124 @@
using System.Collections.Generic;
using NUnit.Framework;
using UnityEngine;
/// <summary>
/// Cobertura de DiceSystem.GetPieceBonus (todos los tipos de pieza) y
/// GetDesperationBonus (con y sin piezas blancas en el tablero).
/// </summary>
[TestFixture]
[Category("Unit")]
[Category("Purgatory")]
[Category("FastTests")]
public class DiceSystemCoverageTests
{
private GameObject _root;
private DiceSystem _dice;
private GameObject _gmRoot;
private readonly List<GameObject> _spawned = new List<GameObject>();
[SetUp]
public void Setup()
{
_root = new GameObject("DiceCoverageRoot");
_dice = _root.AddComponent<DiceSystem>();
}
[TearDown]
public void Teardown()
{
SetGameManagerInstance(null);
if (_gmRoot != null) Object.DestroyImmediate(_gmRoot);
_gmRoot = null;
foreach (var obj in _spawned)
{
if (obj != null) Object.DestroyImmediate(obj);
}
_spawned.Clear();
Object.DestroyImmediate(_root);
}
private static void SetGameManagerInstance(GameManager instance)
{
var prop = typeof(GameManager).GetProperty("Instance");
var setter = prop.GetSetMethod(nonPublic: true);
setter.Invoke(null, new object[] { instance });
}
private Piece SpawnWhite<T>(GameManager gm, int x, int y) where T : Piece
{
var go = new GameObject(typeof(T).Name);
_spawned.Add(go);
var piece = go.AddComponent<T>();
piece.isWhite = true;
piece.currentPos = new Vector2Int(x, y);
gm.board[x, y] = piece;
return piece;
}
[Test]
[TestCase(PieceType.Queen, 2)]
[TestCase(PieceType.Rook, 1)]
[TestCase(PieceType.Bishop, 1)]
[TestCase(PieceType.Knight, 1)]
[TestCase(PieceType.Pawn, 0)]
[TestCase(PieceType.King, 0)]
public void RollForPlayer_PieceBonus_MatchesTypeTable(PieceType type, int expectedBonus)
{
var identity = ScriptableObject.CreateInstance<PieceIdentity>();
identity.pieceType = type;
// Con GameManager ausente el bonus de desesperación es 0: aislamos
// la columna de bonificación por tipo.
for (int roll = 0; roll < 12; roll++)
{
var result = _dice.RollForPlayer(identity, purgatoryVisitsThisBoard: 0);
Assert.AreEqual(expectedBonus, result.pieceBonus,
$"Bonus por pieza {type} debería ser {expectedBonus}.");
}
Object.DestroyImmediate(identity);
}
[Test]
public void GetDesperationBonus_FewPieces_ReturnsBonus()
{
_gmRoot = new GameObject("GM_Test");
var gm = _gmRoot.AddComponent<GameManager>(); // Awake no corre en EditMode
SetGameManagerInstance(gm);
// Tablero vacío (0 blancas <= threshold 4) → desesperación activa.
var identity = ScriptableObject.CreateInstance<PieceIdentity>();
identity.pieceType = PieceType.Pawn;
var result = _dice.RollForPlayer(identity, purgatoryVisitsThisBoard: 0);
Assert.AreEqual(1, result.desperationBonus, "Con pocas piezas el bonus de desesperación debe aplicarse.");
Object.DestroyImmediate(identity);
}
[Test]
public void GetDesperationBonus_EnoughPieces_ReturnsZero()
{
_gmRoot = new GameObject("GM_Test");
var gm = _gmRoot.AddComponent<GameManager>();
SetGameManagerInstance(gm);
// 5 piezas blancas > threshold 4 → sin bonus. El loop de conteo
// recorre las 64 casillas del board real.
for (int i = 0; i < 5; i++)
{
SpawnWhite<Pawn>(gm, i % 8, i / 8 + 3);
}
var identity = ScriptableObject.CreateInstance<PieceIdentity>();
identity.pieceType = PieceType.Pawn;
var result = _dice.RollForPlayer(identity, purgatoryVisitsThisBoard: 0);
Assert.AreEqual(0, result.desperationBonus, "Con suficientes piezas no hay bonus de desesperación.");
Object.DestroyImmediate(identity);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9c4e93663aab5114599cf7fac646965c
@@ -0,0 +1,131 @@
using System.Collections.Generic;
using NUnit.Framework;
using UnityEngine;
/// <summary>
/// Cobertura de King.CanCastle: enroque bloqueado por torre inválida,
/// por casillas intermedias ocupadas y por casillas de tránsito atacadas.
/// Complementa Castling_BothSides_AvailableWhenPathClearAndUnattacked.
/// </summary>
public class KingCastleCoverageTests
{
private GameManagerClassic _gm;
private readonly List<GameObject> _spawned = new List<GameObject>();
[SetUp]
public void SetUp()
{
var gmObject = new GameObject("KingCastle_Test");
_spawned.Add(gmObject);
_gm = gmObject.AddComponent<GameManagerClassic>();
_gm.board = new Piece[8, 8];
GameManagerClassic.Instance = _gm;
}
[TearDown]
public void TearDown()
{
foreach (var obj in _spawned)
{
if (obj != null) Object.DestroyImmediate(obj);
}
_spawned.Clear();
GameManagerClassic.Instance = null;
}
private T SpawnPiece<T>(int x, int y, bool isWhite) where T : Piece
{
var obj = new GameObject($"{typeof(T).Name}_{x}_{y}");
_spawned.Add(obj);
var piece = obj.AddComponent<T>();
piece.currentPos = new Vector2Int(x, y);
piece.isWhite = isWhite;
_gm.board[x, y] = piece;
return piece;
}
private static bool ContainsMove(List<Vector2Int> moves, int x, int y)
{
return moves != null && moves.Contains(new Vector2Int(x, y));
}
[Test]
public void CanCastle_RookHasMoved_CastleExcluded()
{
var king = SpawnPiece<King>(4, 0, isWhite: true);
king.hasMoved = false;
var rook = SpawnPiece<Rook>(7, 0, isWhite: true);
rook.hasMoved = true; // torre ya movida: sin enroque corto
var moves = king.GetAvailableMoves(_gm.board);
Assert.IsFalse(ContainsMove(moves, 6, 0), "Torre movida debe anular el enroque corto.");
}
[Test]
public void CanCastle_WrongColorRookOrNotRook_CastleExcluded()
{
var king = SpawnPiece<King>(4, 0, isWhite: true);
king.hasMoved = false;
SpawnPiece<Rook>(7, 0, isWhite: false); // torre enemiga: no sirve
SpawnPiece<Knight>(0, 0, isWhite: true); // ni es torre en a1
var moves = king.GetAvailableMoves(_gm.board);
Assert.IsFalse(ContainsMove(moves, 6, 0), "Torre enemiga no habilita enroque.");
Assert.IsFalse(ContainsMove(moves, 2, 0), "Una pieza que no es torre no habilita enroque largo.");
}
[Test]
public void CanCastle_MiddleSquareOccupied_CastleExcluded()
{
var king = SpawnPiece<King>(4, 0, isWhite: true);
king.hasMoved = false;
SpawnPiece<Rook>(7, 0, isWhite: true).hasMoved = false;
SpawnPiece<Bishop>(5, 0, isWhite: true); // f1 ocupado
SpawnPiece<Rook>(0, 0, isWhite: true).hasMoved = false;
SpawnPiece<Knight>(3, 0, isWhite: true); // d1 ocupado (enroque largo)
var moves = king.GetAvailableMoves(_gm.board);
Assert.IsFalse(ContainsMove(moves, 6, 0), "Casilla intermedia ocupada anula enroque corto.");
Assert.IsFalse(ContainsMove(moves, 2, 0), "Casilla intermedia ocupada anula enroque largo.");
}
[Test]
public void CanCastle_TransitSquareAttacked_CastleExcluded()
{
var king = SpawnPiece<King>(4, 0, isWhite: true);
king.hasMoved = false;
SpawnPiece<Rook>(7, 0, isWhite: true).hasMoved = false;
// Torre negra en columna f: ataca f1 (tránsito del enroque corto).
var attacker = SpawnPiece<Rook>(5, 6, isWhite: false);
Assert.IsTrue(attacker.GetAttackSquares(_gm.board).Contains(new Vector2Int(5, 0)),
"Precondición: la torre negra debe atacar f1.");
var moves = king.GetAvailableMoves(_gm.board);
Assert.IsFalse(ContainsMove(moves, 6, 0),
"No se puede enrocar cruzando una casilla atacada (f1).");
}
[Test]
public void CanCastle_DestinationAttacked_LongSideExcluded_ShortAllowed()
{
var king = SpawnPiece<King>(4, 0, isWhite: true);
king.hasMoved = false;
SpawnPiece<Rook>(0, 0, isWhite: true).hasMoved = false;
// Torre negra en fila 1 sobre columna c: ataca c1 (destino del largo)
// pero NO d1/b1 (tránsito) — el enroque largo se anula por destino.
SpawnPiece<Rook>(2, 4, isWhite: false);
SpawnPiece<Rook>(7, 0, isWhite: true).hasMoved = false;
var moves = king.GetAvailableMoves(_gm.board);
Assert.IsFalse(ContainsMove(moves, 2, 0), "c1 atacada: enroque largo excluido.");
Assert.IsTrue(ContainsMove(moves, 6, 0), "El lado corto sigue disponible.");
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1a87d5b5d1ce40f4c9e9c9e6743d38bb
@@ -0,0 +1,172 @@
using System.Collections.Generic;
using System.Reflection;
using NUnit.Framework;
using UnityEngine;
/// <summary>
/// Cobertura de capturas diagonales del peón y del bloque completo de
/// al-paso (todas las guardas de GameManager.Instance / último movimiento).
/// </summary>
[TestFixture]
[Category("Unit")]
[Category("Purgatory")]
[Category("FastTests")]
public class PawnCoverageTests
{
private static readonly BindingFlags Flags =
BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance;
private GameObject _root;
private GameObject _piecesRoot;
private GameManager _gameManager;
[SetUp]
public void Setup()
{
_root = new GameObject("PawnCoverageRoot");
_gameManager = _root.AddComponent<GameManager>();
// GameManager.Instance { get; private set; } — inyección por reflexión.
typeof(GameManager)
.GetProperty("Instance", Flags)
.SetValue(_gameManager, _gameManager);
_piecesRoot = new GameObject("PawnCoveragePieces");
}
[TearDown]
public void Teardown()
{
Object.DestroyImmediate(_piecesRoot);
Object.DestroyImmediate(_root);
// No contaminar otros tests con una instancia residual.
typeof(GameManager).GetProperty("Instance", Flags).SetValue(null, null);
}
private T Place<T>(Piece[,] board, int x, int y, bool white) where T : Piece
{
var piece = new GameObject(typeof(T).Name + x + y).AddComponent<T>();
piece.currentPos = new Vector2Int(x, y);
piece.isWhite = white;
board[x, y] = piece;
return piece;
}
private void SetLastMove(Piece moved, Vector2Int from, Vector2Int to)
{
var type = typeof(GameManager);
type.GetField("_lastMovedPiece", BindingFlags.NonPublic | BindingFlags.Instance)
.SetValue(_gameManager, moved);
type.GetField("_lastMoveFrom", BindingFlags.NonPublic | BindingFlags.Instance)
.SetValue(_gameManager, from);
type.GetField("_lastMoveTo", BindingFlags.NonPublic | BindingFlags.Instance)
.SetValue(_gameManager, to);
}
[Test]
public void DiagonalCaptures_EnemyOnBothSides_AreIncluded()
{
var board = new Piece[8, 8];
var pawn = Place<Pawn>(board, 4, 4, white: true);
Place<Pawn>(board, 3, 5, white: false); // d5 negra
Place<Pawn>(board, 5, 5, white: false); // f5 negra
var moves = pawn.GetAvailableMoves(board);
CollectionAssert.Contains(moves, new Vector2Int(3, 5));
CollectionAssert.Contains(moves, new Vector2Int(5, 5));
}
[Test]
public void DiagonalCaptures_FriendlyBlocksSquare()
{
var board = new Piece[8, 8];
var pawn = Place<Pawn>(board, 4, 4, white: true);
Place<Pawn>(board, 3, 5, white: true); // aliada: no capturable
Place<Pawn>(board, 5, 5, white: true); // aliada: no capturable
var moves = pawn.GetAvailableMoves(board);
CollectionAssert.DoesNotContain(moves, new Vector2Int(3, 5));
CollectionAssert.DoesNotContain(moves, new Vector2Int(5, 5));
}
[Test]
public void EnPassant_EnemyPawnDoubleStep_Adjacent_AddsCapture()
{
var board = new Piece[8, 8];
var pawn = Place<Pawn>(board, 3, 4, white: true); // d4... fila 4
var enemy = Place<Pawn>(board, 4, 4, white: false); // e4 negra recién llegada
SetLastMove(enemy, new Vector2Int(4, 6), new Vector2Int(4, 4)); // e7→e5
var moves = pawn.GetAvailableMoves(board);
CollectionAssert.Contains(moves, new Vector2Int(4, 5), "Debe ofrecer la captura al paso.");
}
[Test]
public void EnPassant_LastMovedNotPawn_NoCapture()
{
var board = new Piece[8, 8];
var pawn = Place<Pawn>(board, 3, 4, white: true);
var enemy = Place<Rook>(board, 4, 4, white: false);
SetLastMove(enemy, new Vector2Int(4, 6), new Vector2Int(4, 4));
var moves = pawn.GetAvailableMoves(board);
CollectionAssert.DoesNotContain(moves, new Vector2Int(4, 5));
}
[Test]
public void EnPassant_LastMovedSameColor_NoCapture()
{
var board = new Piece[8, 8];
var pawn = Place<Pawn>(board, 3, 4, white: true);
var friend = Place<Pawn>(board, 4, 4, white: true);
SetLastMove(friend, new Vector2Int(4, 6), new Vector2Int(4, 4));
var moves = pawn.GetAvailableMoves(board);
CollectionAssert.DoesNotContain(moves, new Vector2Int(4, 5));
}
[Test]
public void EnPassant_NotDoubleStep_NoCapture()
{
var board = new Piece[8, 8];
var pawn = Place<Pawn>(board, 3, 4, white: true);
var enemy = Place<Pawn>(board, 4, 4, white: false);
SetLastMove(enemy, new Vector2Int(4, 5), new Vector2Int(4, 4)); // avance simple
var moves = pawn.GetAvailableMoves(board);
CollectionAssert.DoesNotContain(moves, new Vector2Int(4, 5));
}
[Test]
public void EnPassant_DifferentRowOrFarColumn_NoCapture()
{
var board = new Piece[8, 8];
var pawn = Place<Pawn>(board, 1, 1, white: true); // b2, lejos del rival
var enemy = Place<Pawn>(board, 4, 4, white: false); // e5
SetLastMove(enemy, new Vector2Int(4, 6), new Vector2Int(4, 4));
var moves = pawn.GetAvailableMoves(board);
CollectionAssert.DoesNotContain(moves, new Vector2Int(4, 5));
}
[Test]
public void EnPassant_InstanceNull_SkipsWholeBlock()
{
typeof(GameManager).GetProperty("Instance", Flags).SetValue(null, null);
var board = new Piece[8, 8];
var pawn = Place<Pawn>(board, 3, 4, white: true);
Place<Pawn>(board, 4, 4, white: false); // sin Instance no hay al-paso
var moves = pawn.GetAvailableMoves(board);
Assert.AreEqual(1, moves.Count, "Solo el avance simple debe estar disponible.");
CollectionAssert.Contains(moves, new Vector2Int(3, 5));
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 766a96e021b8fb14996ace1df94ac389