DEV Community

mohamed Tayel
mohamed Tayel

Posted on

Understanding Jagged Arrays in C#

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. A read-only property (Owner): Specifies the current state of the square.
  2. A constructor: Initializes the square with a specific state.
  3. 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
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

Breakdown:

  1. Owner: This is a read-only property that holds the square's state.
  2. Constructor: Initializes a square with a specific owner (e.g., Player.Nought).
  3. ToString:
    • Returns "O" if the square is owned by Player.Nought.
    • Returns "X" if the square is owned by Player.Cross.
    • Returns " " (a space) if the square is empty (Player.NoOne).

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
    }
}
Enter fullscreen mode Exit fullscreen mode

Breakdown:

  1. board:
    • A jagged array (Square[][]) with three rows.
    • Each row is an array of three squares.
  2. Initialization:
    • The outer array has three rows (board = new Square[3][]).
    • Each row is initialized with three empty squares (board[i] = new Square[3]).
  3. currentPlayer:
    • Tracks which player’s turn it is. By default, Player 1 (Nought) starts the game.

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

Breakdown:

  1. Outer loop (for):
    • Iterates over the rows of the board.
  2. Inner loop (for):
    • Iterates over the columns in each row.
  3. Display:
    • Prints each square as [ ], [O], or [X] depending on its state.
    • Moves to a new line after printing each row.

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;
}
Enter fullscreen mode Exit fullscreen mode

Breakdown:

  1. Validation:
    • Ensures the row and column are within bounds (0 to 2).
    • Checks if the square is empty.
  2. 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.
}
Enter fullscreen mode Exit fullscreen mode

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!");
}
Enter fullscreen mode Exit fullscreen mode

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!");
    }
}
Enter fullscreen mode Exit fullscreen mode

Breakdown:

  1. Game loop:
    • Continuously prompts players for their moves.
    • Checks for a winner after each move.
  2. Input handling:
    • Reads the player’s input and parses it into row/column indices.
  3. 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)