NOTE: This is Part 2 of a three-part series demonstrating how we might model the classic game Battleship as a C# program. Part 1 is over here. You might want to use the sample project over on GitHub to follow along with this post. Also, check out my other posts in the Modeling Practice series!

In the first part of this series we discussed how to play a game of Battleship and what kinds of components and strategies we would need to use. With those in place, we can begin modeling the game. Let's build some objects!

Coordinates

The first and most basic object we are going to model is the Coordinates object, which represents a location on a board that can be fired at.

public class Coordinates
{
    public int Row { get; set; }
    public int Column { get; set; }

    public Coordinates(int row, int column)
    {
        Row = row;
        Column = column;
    }
}

You might be wondering why those properties Row and Column are not a part of a different model, e.g. the Panel model that we're about to define. This is because whenever a shot is fired, the person firing the shot does so by calling out coordinates, and so this class will not only represent coordinates on the game and firing boards, but also coordinates that are under attack.

More Modeling Practice:

(NOTE: In the game, rows are given letter designations, e.g. "A", "B", etc. Here, we'll be using integers, as it makes several calculations easier).

OccupationType

For any given panel, there a few possibilities as to what can be on that panel:

  • If a ship is on the panel, then the panel is occupied. Two ships cannot occupy the same panel.
  • If a shot was fired at that panel, then either a hit or a miss was recorded on that panel.
  • If there's nothing on that panel, the panel is said to be empty.

To represent all of these statuses, I created an enumeration called OccupationType:

public enum OccupationType
{
    [Description("o")]
    Empty,

    [Description("B")]
    Battleship,

    [Description("C")]
    Cruiser,

    [Description("D")]
    Destroyer,

    [Description("S")]
    Submarine,

    [Description("A")]
    Carrier,

    [Description("X")]
    Hit,

    [Description("M")]
    Miss
}

The Description attribute records the display character used for each of these statuses. We'll see a lot of those characters when we show how to play a game in the next part of this series.

Panel

The next object we need represents a single space on the game boards. I've taken to calling this space a Panel.

public class Panel
{
    public OccupationType OccupationType { get; set; }
    public Coordinates Coordinates { get; set; }

    public Panel(int row, int column)
    {
        Coordinates = new Coordinates(row, column);
        OccupationType = OccupationType.Empty;
    }

    public string Status
    {
        get
        {
            return OccupationType.GetAttributeOfType<DescriptionAttribute>().Description;
        }
    }

    public bool IsOccupied
    {
        get
        {
            return OccupationType == OccupationType.Battleship
                || OccupationType == OccupationType.Destroyer
                || OccupationType == OccupationType.Cruiser
                || OccupationType == OccupationType.Submarine
                || OccupationType == OccupationType.Carrier;
        }
    }

    public bool IsRandomAvailable
    {
        get
        {
            return (Coordinates.Row % 2 == 0 && Coordinates.Column % 2 == 0)
                || (Coordinates.Row % 2 == 1 && Coordinates.Column % 2 == 1);
        }
    }
}

We should make special note of the IsRandomAvailable property. Remember from the previous part of this series that when we are firing random shots, we don't need to target every panel, but rather every other panel, like so:

IsRandomAvailable helps us implement that strategy. It designates every panel where both row and column coordinates are odd, or both coordinates are even, as being available for a "random" shot selection.

Finally, note the IsOccupied property. We'll be using that property in a later part to determine where to place the ships.

Ships

Speaking of the ships, let's define their base class now.

public abstract class Ship
{
    public string Name { get; set; }
    public int Width { get; set; }
    public int Hits { get; set; }
    public OccupationType OccupationType { get; set; }
    public bool IsSunk
    {
        get
        {
            return Hits >= Width;
        }
    }
}

The only real trick to this class is the IsSunk property, which merely returns true if the number of hits the ship has sustained is greater than or equal to its width.

Let's also define five additional classes, one for each kind of ship.

public class Destroyer : Ship
{
    public Destroyer()
    {
        Name = "Destroyer";
        Width = 2;
        OccupationType = OccupationType.Destroyer;
    }
}

public class Submarine : Ship
{
    public Submarine()
    {
        Name = "Submarine";
        Width = 3;
        OccupationType = OccupationType.Submarine;
    }
}

public class Cruiser : Ship
{
    public Cruiser()
    {
        Name = "Cruiser";
        Width = 3;
        OccupationType = OccupationType.Cruiser;
    }
}

public class Battleship : Ship
{
    public Battleship()
    {
        Name = "Battleship";
        Width = 4;
        OccupationType = OccupationType.Battleship;
    }
}

public class Carrier : Ship
{
    public Carrier()
    {
        Name = "Aircraft Carrier";
        Width = 5;
        OccupationType = OccupationType.Carrier;
    }
}

Each player will instantiate one of each kind of ship in order to play a game.

Game Board

Each player will also need an instance of class GameBoard, which tracks where that player's ships are placed and where their opponent's shots have been fired.

When you get right down to it, a GameBoard is really just a collection of Panel objects that we defined earlier.

public class GameBoard
{
    public List<Panel> Panels { get; set; }

    public GameBoard()
    {
        Panels = new List<Panel>();
        for (int i = 1; i <= 10; i++)
        {
            for (int j = 1; j <= 10; j++)
            {
                Panels.Add(new Panel(i, j));
            }
        }
    }
}

Firing Board

In addition to the GameBoard, we also need a special kind of GameBoard called FiringBoard, which tracks each players shots and whether they were hits or misses.

public class FiringBoard : GameBoard
{
    public List<Coordinates> GetOpenRandomPanels() { }

    public List<Coordinates> GetHitNeighbors() { }

    public List<Panel> GetNeighbors(Coordinates coordinates) { }
}

We will define each of those methods in the next (and final) part of this series.

Player

Now we can write up our Player class. Each player will need a collection of ships, an instance of GameBoard, an instance of FiringBoard, and a flag to show whether or not they have lost the game:

public class Player
{
    public string Name { get; set; }
    public GameBoard GameBoard { get; set; }
    public FiringBoard FiringBoard { get; set; }
    public List<Ship> Ships { get; set; }
    public bool HasLost
    {
        get
        {
            return Ships.All(x => x.IsSunk);
        }
    }

    public Player(string name)
    {
        Name = name;
        Ships = new List<Ship>()
        {
            new Destroyer(),
            new Submarine(),
            new Cruiser(),
            new Battleship(),
            new Carrier()
        };
        GameBoard = new GameBoard();
        FiringBoard = new FiringBoard();
    }
}

The Player class also has a ton of methods which we define in Part 3.

Game

Finally, we need a Game class. This is because, in the final part of this series, we're going to run a bunch of games to see if this system gives any inherent bias to one of the Player objects.

public class Game
{
    public Player Player1 { get; set; }
    public Player Player2 { get; set; }

    public Game() { }

    public void PlayRound() { }

    public void PlayToEnd() { }
}

Our first objective is achieved: we've created the classes necessary to play a game of Battleship. Now, let's work though how to set up a game.

Setting Up the Game

To start, let's think about what a Player would need to do, once s/he has all their pieces, to set up a game of Battleship. S/he needs to:

  • Place his/her ships on the GameBoard.
  • That's it!

So, okay, there's not a whole lot of setup involved in a game of Battleship. However, there is some, so in this section we're going to implement the code which places a Player's ships, as well as output what their boards look like.

Ship Placement

There are a lot of articles out there that purport to help you win a game of Battleship each time you play (and many of them correspond with the release of that god-awful movie), but for this practice we're not going to bother with more advanced strategies since our goal is not to win games, but to understand the game itself better by modeling it.

In short: our ship placement will be effectively random.

But it cannot be truly random, since two ships cannot occupy the same panel. Therefore we must implement a placement algorithm which places each ship on the board but ensures that each ship does not occupy the same Panel as any other ship.

More Modeling Practice:

Here's the rundown of that algorithm:

  1. For each ship we have left to place:
  2. Pick a random panel which is currently unoccupied.
  3. Select an orientation (horizontal or vertical) at random.
  4. Attempt to place the ship on the proposed panels. If any of those panels are already occupied, or are outside the boundaries of the game board, start over from 1.

Given that the total number of panels (100) is much greater than the space we need to occupy (2 + 3 + 3 + 4 + 5 = 16), this is actually relatively efficient, but not perfect.

Let's start coding up that algorithm, using the Player class we defined in Part 2. We'll create a new method PlaceShips in the Player class and define it like so, and use a random number generator that I stole from StackOverflow:

public void PlaceShips()
{
    Random rand = new Random(Guid.NewGuid().GetHashCode());
    foreach (var ship in Ships)
    {
        //Select a random row/column combination, then select a random orientation.
        //If none of the proposed panels are occupied, place the ship
        //Do this for all ships

        bool isOpen = true;
        while (isOpen)
        {
            //Next() has the second parameter be exclusive, while the first parameter is inclusive.
            var startcolumn = rand.Next(1,11); 
            var startrow = rand.Next(1, 11);
            int endrow = startrow, endcolumn = startcolumn;
            var orientation = rand.Next(1, 101) % 2; //0 for Horizontal

            List<int> panelNumbers = new List<int>();
            if (orientation == 0)
            {
                for (int i = 1; i < ship.Width; i++)
                {
                    endrow++;
                }
            }
            else
            {
                for (int i = 1; i < ship.Width; i++)
                {
                    endcolumn++;
                }
            }

            //We cannot place ships beyond the boundaries of the board
            if(endrow > 10 || endcolumn > 10)
            {
                isOpen = true;
                continue; //Restart the while loop to select a new random panel
            }

            //Check if specified panels are occupied
            var affectedPanels = GameBoard.Panels.Range(startrow, startcolumn, endrow, endcolumn);
            if(affectedPanels.Any(x=>x.IsOccupied))
            {
                isOpen = true;
                continue;
            }

            foreach(var panel in affectedPanels)
            {
                panel.OccupationType = ship.OccupationType;
            }
            isOpen = false;
        }
    }
}

You may have noticed the following call in the above method:

var affectedPanels = GameBoard.Panels.Range(startrow, startcolumn, endrow, endcolumn);

Range() is an extension method we defined for this project, and looks like this:

public static class PanelExtensions
{
    public static List<Panel> Range(this List<Panel> panels, int startRow, int startColumn, int endRow, int endColumn)
    {
        return panels.Where(x => x.Coordinates.Row >= startRow 
                                    && x.Coordinates.Column >= startColumn 
                                    && x.Coordinates.Row <= endRow 
                                    && x.Coordinates.Column <= endColumn).ToList();
    }
}

As you can see, Range() just gives all the panels which are in the square defined by the passed-in row and column coordinates (and is inclusive of those panels).

Show the Boards

The method PlaceShips places each ship on the Player's board. But how can we tell where the ships are? Let's implement another method in the Player class, called OutputBoards:

public void OutputBoards()
{
    Console.WriteLine(Name);
    Console.WriteLine("Own Board:                          Firing Board:");
    for(int row = 1; row <= 10; row++)
    {
        for(int ownColumn = 1; ownColumn <= 10; ownColumn++)
        {
            Console.Write(GameBoard.Panels.At(row, ownColumn).Status + " ");
        }
        Console.Write("                ");
        for (int firingColumn = 1; firingColumn <= 10; firingColumn++)
        {
            Console.Write(FiringBoard.Panels.At(row, firingColumn).Status + " ");
        }
        Console.WriteLine(Environment.NewLine);
    }
    Console.WriteLine(Environment.NewLine);
}

This method outputs the current boards to the command line. Running a sample application and calling this method, we get the following output:

Shows the placement of Player 1's ships

Shows the placement of Player 2's ships

Summary

In this part, we:

  • Created the components needed to play a game of Battleship.
  • Created an algorithm to allow our Player objects to place their Ships on the board.
  • Created a method to display the current GameBoard and FiringBoard for each player.

Our game is now ready to play! But... how do we do so? That's coming up in Part 3 of Modeling Battleship in C#!

Don't forget to check out the GitHub repository for this series!

Happy Modeling!