Jagged arrays are an efficient way to represent collections of collections in C#. In this article, we’ll build a console-based Noughts and Crosses game (Tic-Tac-Toe in some regions) to demonstrate how jagged arrays can simplify complex data structures. Each step is broken down for clarity.
What is a Jagged Array?
A jagged array is an array where each element is another array. Unlike multidimensional arrays, each inner array can have a different size, making jagged arrays flexible.
For example:
int[][] jaggedArray = new int[3][];
jaggedArray[0] = new int[4]; // First row has 4 elements
jaggedArray[1] = new int[2]; // Second row has 2 elements
jaggedArray[2] = new int[3]; // Third row has 3 elements
In our game, we’ll use a jagged array to represent a 3x3 grid where each cell is a Square
.
1. Player Enum
The Player
enum defines the possible states of a square:
-
NoOne
(default): The square is empty. -
Nought
: Represents an "O". -
Cross
: Represents an "X".
public enum Player
{
NoOne, // Default value
Nought,
Cross
}
Breakdown:
-
NoOne
: Indicates that the square is empty. -
Nought
: Represents Player 1. -
Cross
: Represents Player 2. - Using an enum makes the code more readable and helps enforce valid states for each square.
2. Square Struct
Each square on the board is represented by a Square
struct, which has:
-
A read-only property (
Owner
): Specifies the current state of the square. - A constructor: Initializes the square with a specific state.
-
A
ToString
method: Converts the square state to a character for display.
public struct Square
{
public Player Owner { get; }
public Square(Player owner)
{
Owner = owner;
}
public override string ToString()
{
return Owner switch
{
Player.Nought => "O",
Player.Cross => "X",
_ => " " // Empty square
};
}
}
Breakdown:
-
Owner
: This is a read-only property that holds the square's state. -
Constructor: Initializes a square with a specific owner (e.g.,
Player.Nought
). -
ToString
:- Returns
"O"
if the square is owned byPlayer.Nought
. - Returns
"X"
if the square is owned byPlayer.Cross
. - Returns
" "
(a space) if the square is empty (Player.NoOne
).
- Returns
This ensures the board will display correctly in the console.
3. Game Class
The Game
class manages the game board, players, and logic. Let’s break it down step by step:
3.1. Initializing the Board
The Game
constructor initializes a 3x3 jagged array. Each inner array represents a row of the board.
public class Game
{
private Square[][] board;
private Player currentPlayer;
public Game()
{
board = new Square[3][];
for (int i = 0; i < 3; i++)
{
board[i] = new Square[3];
}
currentPlayer = Player.Nought; // Nought starts by convention
}
}
Breakdown:
-
board
:- A jagged array (
Square[][]
) with three rows. - Each row is an array of three squares.
- A jagged array (
-
Initialization:
- The outer array has three rows (
board = new Square[3][]
). - Each row is initialized with three empty squares (
board[i] = new Square[3]
).
- The outer array has three rows (
-
currentPlayer
:- Tracks which player’s turn it is. By default, Player 1 (
Nought
) starts the game.
- Tracks which player’s turn it is. By default, Player 1 (
3.2. Displaying the Board
The DisplayBoard
method prints the current state of the board.
public void DisplayBoard()
{
for (int i = 0; i < board.Length; i++)
{
for (int j = 0; j < board[i].Length; j++)
{
Console.Write($"[{board[i][j]}]");
}
Console.WriteLine();
}
}
Breakdown:
-
Outer loop (
for
):- Iterates over the rows of the board.
-
Inner loop (
for
):- Iterates over the columns in each row.
-
Display:
- Prints each square as
[ ]
,[O]
, or[X]
depending on its state. - Moves to a new line after printing each row.
- Prints each square as
3.3. Making a Move
The MakeMove
method lets the current player place their mark on the board. It ensures the move is valid.
public bool MakeMove(int row, int col)
{
if (row < 0 || row >= 3 || col < 0 || col >= 3)
{
Console.WriteLine("Invalid move! Please choose a row and column between 0 and 2.");
return false;
}
if (board[row][col].Owner != Player.NoOne)
{
Console.WriteLine("That square is already occupied!");
return false;
}
board[row][col] = new Square(currentPlayer);
currentPlayer = currentPlayer == Player.Nought ? Player.Cross : Player.Nought; // Switch player
return true;
}
Breakdown:
-
Validation:
- Ensures the row and column are within bounds (
0 to 2
). - Checks if the square is empty.
- Ensures the row and column are within bounds (
-
Update:
- Sets the square to the current player’s mark.
- Switches the turn to the other player.
3.4. Checking for a Winner
The CheckWinner
method determines if a player has won or if the game is a draw.
public Player CheckWinner()
{
// (Rows and columns logic omitted for brevity; see full code above.)
// Check diagonals, rows, and columns for a win.
// Return Player.NoOne if the game is ongoing or it's a draw.
}
3.5. Resetting the Board
The ResetBoard
method clears the board for a new game.
public void ResetBoard()
{
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
board[i][j] = new Square(Player.NoOne);
}
}
currentPlayer = Player.Nought; // Reset to Nought
Console.WriteLine("The board has been reset!");
}
4. Main Method
The Main
method runs the game loop, allowing two players to take turns until there’s a winner or a draw.
public class Program
{
public static void Main()
{
Game game = new Game();
Player winner;
do
{
Console.Clear();
game.DisplayBoard();
Console.WriteLine("Enter your move (row and column separated by space): ");
string[] input = Console.ReadLine()?.Split();
if (!game.MakeMove(int.Parse(input[0]), int.Parse(input[1])))
continue;
winner = game.CheckWinner();
} while (winner == Player.NoOne);
Console.WriteLine($"Player {winner} wins!");
}
}
Breakdown:
-
Game loop:
- Continuously prompts players for their moves.
- Checks for a winner after each move.
-
Input handling:
- Reads the player’s input and parses it into row/column indices.
-
Endgame:
- Displays the winner or declares a draw.
Why Use Jagged Arrays?
-
Simplicity: Clean and readable syntax (
Square[][]
). - Efficiency: Pre-filled with default values for structs.
- Flexibility: Can adapt to grids of varying dimensions.
Top comments (0)