Arquitectura y Diseño del Sistema
Para implementar un juego de tablero basado en texto para dos jugadores en la consola de C#, es fundamental establecer una arquitectura modular y limpia. El juego consiste en un tablero de 100 casillas con diferentes efectos (Normales, Minas, Pausas, Túneles y Ruletas). A diferencia de enfoques iniciales que utilizan estructuras de datos por valor (struct) y paso por referencia (ref), una práctica más robusta en C# es utilizar clases (class) para las entidades del juego, lo que facilita la gestión del estado y evita efectos secundarios no deseados.
Modelado de Datos
Definimos los tipos de casillas mediante una enumeración y encapsulamos las propiedades de los jugadores y las casillas en clases dedicadas.
public enum CellType
{
Normal,
Pause,
Roulette,
Mine,
Tunnel
}
public class Player
{
public string Name { get; set; }
public string Token { get; set; }
public int Position { get; set; }
public bool SkipNextTurn { get; set; }
}
public class BoardCell
{
public CellType Type { get; set; }
public string Symbol { get; set; }
public string Description { get; set; }
}
Generación Dinámica del Tablero
El tablero se genera algorítmicamente en cada ejecución. Se inicializan las 100 casillas como "Normales", y luego se distribuyen aleatoriamente 5 casillas de cada tipo especial, asegurando que la casilla de inicio (0) y la de meta (99) permanezcan inalteradas.
static void InitializeBoard(BoardCell[] board)
{
var specialCells = new[]
{
new BoardCell { Type = CellType.Pause, Symbol = "■", Description = "Pierde el siguiente turno" },
new BoardCell { Type = CellType.Roulette, Symbol = "¤", Description = "Intercambiar o Bombardear" },
new BoardCell { Type = CellType.Mine, Symbol = "★", Description = "Retrocede 6 casillas" },
new BoardCell { Type = CellType.Tunnel, Symbol = "〓", Description = "Avanza 10 casillas" }
};
// Inicializar tablero con casillas normales
for (int i = 0; i < board.Length; i++)
{
board[i] = new BoardCell { Type = CellType.Normal, Symbol = "∷", Description = "Casilla normal" };
}
Random rnd = new Random();
foreach (var special in specialCells)
{
int placedCount = 0;
while (placedCount < 5)
{
int index = rnd.Next(1, 99); // Excluye inicio y fin
if (board[index].Type == CellType.Normal)
{
board[index] = special;
placedCount++;
}
}
}
}
Renderizado del Tablero en Consola
El mapeo de un array unidimensional a una forma visual en la consola requiere un control preciso de los índices. El tablero se dibuja en forma de "U" o serpentina: 30 casillas hacia la derecha, 5 hacia abajo, 30 hacia la izquierda, 5 hacia abajo, y las últimas 30 hacia la derecha.
static string GetCellDisplay(int index, Player p1, Player p2, BoardCell[] board)
{
if (index == p1.Position && index == p2.Position) return "@@";
if (index == p1.Position) return p1.Token;
if (index == p2.Position) return p2.Token;
return board[index].Symbol;
}
static void RenderBoard(Player p1, Player p2, BoardCell[] board)
{
// Fila 1: Izquierda a Derecha (0 - 29)
for (int i = 0; i < 30; i++) Console.Write(GetCellDisplay(i, p1, p2, board));
Console.WriteLine();
// Columna 1: Arriba a Abajo (30 - 34)
for (int i = 30; i < 35; i++)
{
Console.Write(new string(' ', 58));
Console.WriteLine(GetCellDisplay(i, p1, p2, board));
}
// Fila 2: Derecha a Izquierda (64 - 35)
for (int i = 64; i >= 35; i--) Console.Write(GetCellDisplay(i, p1, p2, board));
Console.WriteLine();
// Columna 2: Arriba a Abajo (65 - 69)
for (int i = 65; i < 70; i++) Console.WriteLine(GetCellDisplay(i, p1, p2, board));
// Fila 3: Izquierda a Derecha (70 - 99)
for (int i = 70; i < 100; i++) Console.Write(GetCellDisplay(i, p1, p2, board));
Console.WriteLine();
}
Lógica de Movimiento y Resolución de Efectos
Un error común al procesar efectos de casillas encadenados (por ejemplo, caer en una mina, retroceder y caer en un túnel) es utilizar recursividad. Esto puede provocar desbordamientos de pila (StackOverflowException). La solución óptima es utilizar un bucle iterativo que continúe evaluando la posición hasta que el jugador descanse en una casilla normal o de efecto terminal.
static void ProcessTurn(Player current, Player opponent, BoardCell[] board)
{
if (current.SkipNextTurn)
{
Console.WriteLine($"{current.Name} pierde este turno por estar pausado.");
current.SkipNextTurn = false;
return;
}
Console.WriteLine($"\n{current.Name}, presiona ENTER para lanzar el dado...");
Console.ReadLine();
Random rnd = new Random();
int steps = rnd.Next(1, 7);
Console.WriteLine($"Resultado del dado: {steps}");
current.Position += steps;
current.Position = Math.Min(99, Math.Max(0, current.Position));
// Resolución iterativa de efectos en cadena
bool chainReaction = true;
while (chainReaction)
{
chainReaction = false;
var cell = board[current.Position];
switch (cell.Type)
{
case CellType.Pause:
Console.WriteLine("Casilla de Pausa: Perderás el siguiente turno.");
current.SkipNextTurn = true;
break;
case CellType.Mine:
Console.WriteLine("¡Mina! Retrocedes 6 casillas.");
current.Position = Math.Max(0, current.Position - 6);
chainReaction = true; // Reevaluar nueva posición
break;
case CellType.Tunnel:
Console.WriteLine("¡Túnel Espacial! Avanzas 10 casillas.");
current.Position = Math.Min(99, current.Position + 10);
chainReaction = true; // Reevaluar nueva posición
break;
case CellType.Roulette:
Console.WriteLine("Ruleta de la Suerte: 1. Intercambiar posiciones | 2. Bombardear oponente (-6)");
string choice = Console.ReadLine();
if (choice == "1")
{
int temp = current.Position;
current.Position = opponent.Position;
opponent.Position = temp;
}
else
{
opponent.Position = Math.Max(0, opponent.Position - 6);
}
break;
case CellType.Normal:
if (current.Position == opponent.Position)
{
Console.WriteLine("¡Colisión! El oponente vuelve al inicio.");
opponent.Position = 0;
}
break;
}
}
}
Bucle Principal del Juego
El núcleo del juego alterna los turnos entre los dos jugadores hasta que uno de ellos alcanza o supera la casilla 99. La inicialización de los jugadores se realiza solicitando los nombres por consola y asignando tokens únicos.
static void Main(string[] args)
{
Console.WriteLine("=== JUEGO DE TABLERO LUDO ===");
Player player1 = new Player { Name = "Jugador 1", Token = "A", Position = 0 };
Player player2 = new Player { Name = "Jugador 2", Token = "B", Position = 0 };
BoardCell[] board = new BoardCell[100];
InitializeBoard(board);
bool gameRunning = true;
while (gameRunning)
{
RenderBoard(player1, player2, board);
ProcessTurn(player1, player2, board);
if (player1.Position >= 99)
{
Console.WriteLine($"{player1.Name} ha ganado la partida!");
break;
}
RenderBoard(player1, player2, board);
ProcessTurn(player2, player1, board);
if (player2.Position >= 99)
{
Console.WriteLine($"{player2.Name} ha ganado la partida!");
break;
}
}
}