mirror of
https://github.com/Earth-Genesis-Games/Ajedrez_Purgatorio.git
synced 2026-09-11 09:47:35 +00:00
test: raise EditMode coverage to 98.16% global, all classes >=95%
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user