NOTE: This is the final part 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!

With all the components and setup taken care of in the previous part of this series, it's now time to finish our system and get to the real interesting part of modeling Battleship as a C# program: how do we actually play a game?

Taking a Turn

Battleship is a turn-based game. When it becomes a player's turn to take a shot, the ensuing process generally goes something like this:

  1. The attacking player selects a panel and calls out the coordinates.
  2. The defending player calls out whether or not that shot was a hit, and marks the shot on his/her game board.
  3. The attacking player marks the result of the shot on his/her firing board.

We'll need to modify our Player class to handle each of these three steps. Let's start with determining what panel to fire at, our Player objects' shot selection.

Step 1: Shot Selection

All of Battleship's strategy comes down to where do we fire the next shot? Remember from Part 1 that there are two shot strategies we are going to employ:

  1. "Random" shots: fire at every other panel until a hit is scored.
  2. "Searching" shots: After making a hit, fire at neighboring panels until the ship is sunk.

Since in a real game it would be the Player that makes these decisions, we're going to improve our Player class by implementing the following methods:

public Coordinates FireShot() { }

private Coordinates RandomShot() { }

private Coordinates SearchingShot() { }

Let's break down what each of these methods will do.

  1. FireShot() is the method which will return coordinates for the shot the Player wants to fire. It will call either RandomShot() or SearchingShot(), depending on the current status of the FiringBoard for the attacking player.
  2. RandomShot(), as you might have guessed, fire our semi-random shots.
  3. SearchingShot() fires at nearby panels from the last hit.

FireShot()

We'll start with theFireShot() method. In order to determine which kind of shot (random or searching) to fire, we need a way to know if any panels exist that are next to a known hit AND have not been fired at yet.

In this solution, we do this by modifying the FiringBoard class from Part 2 and implementing the following method:

public class FiringBoard : GameBoard
{
    public List<Coordinates> GetHitNeighbors()
    {
        List<Panel> panels = new List<Panel>();
        var hits = Panels.Where(x => x.OccupationType == OccupationType.Hit);
        foreach(var hit in hits)
        {
            panels.AddRange(GetNeighbors(hit.Coordinates).ToList());
        }
        return panels.Distinct()
                     .Where(x => x.OccupationType ==
                                   OccupationType.Empty)
                     .Select(x => x.Coordinates)
                     .ToList();
    }
}

...which we then call in FireShot():

public Coordinates FireShot()
{
    //If there are hits on the board with neighbors which don't have shots,
    //we should fire at those first.
    var hitNeighbors = FiringBoard.GetHitNeighbors();
    Coordinates coords;
    if (hitNeighbors.Any())
    {
        coords = SearchingShot();
    }
    else
    {
        coords = RandomShot();
    }
    Console.WriteLine(Name + " says: \"Firing shot at " 
                      + coords.Row.ToString() 
                      + ", " + coords.Column.ToString() 
                      + "\"");
    return coords;
}

RandomShot()

Now we can implement the RandomShot() method.

private Coordinates RandomShot()
{
    var availablePanels = FiringBoard.GetOpenRandomPanels();
    Random rand = new Random(Guid.NewGuid().GetHashCode());
    var panelID = rand.Next(availablePanels.Count);
    return availablePanels[panelID];
}

Notice that this method also relies on a method in the FiringBoard class, called GetOpenRandomPanels(). Here's how that method works:

public List<Coordinates> GetOpenRandomPanels()
{
    return Panels.Where(x => x.OccupationType == OccupationType.Empty 
                             && x.IsRandomAvailable)
                 .Select(x=>x.Coordinates)
                 .ToList();
}

All GetOpenRandomPanels() really does is select panels where:

  • No shot has been fired AND
  • The panels coordinates are both odd or both even (the IsRandomAvailable property).

SearchingShot()

Finally, we can implement the SearchingShot() method. Here's what that looks like:

private Coordinates SearchingShot()
{
    Random rand = new Random(Guid.NewGuid().GetHashCode());
    var hitNeighbors = FiringBoard.GetHitNeighbors();
    var neighborID = rand.Next(hitNeighbors.Count);
    return hitNeighbors[neighborID];
}

SearchingShot() reuses the GetHitNeighbors() method from earlier, and randomly targets one of those neighbor panels.

With those methods in place, our Player objects can now calculate where their shot will go. But, Player objects must also be able to react to shots being fired at them, so let's implement those methods now.

Step 2: Reacting to Shots Fired

Player needs an additional method to react to shots fired at them, and I called this method ProcessShot().  Let's see what this method does.

public ShotResult ProcessShot(Coordinates coords)
{
    //Locate the targeted panel on the GameBoard
    var panel = GameBoard.Panels.At(coords.Row, coords.Column);

    //If the panel is NOT occupied by a ship
    if(!panel.IsOccupied)
    {
        //Call out a miss
        Console.WriteLine(Name + " says: \"Miss!\"");
        return ShotResult.Miss;
    }
    
    //If the panel IS occupied by a ship, determine which one.
    var ship = Ships.First(x => x.OccupationType == panel.OccupationType);

    //Increment the hit counter
    ship.Hits++;

    //Call out a hit
    Console.WriteLine(Name + " says: \"Hit!\"");

    //If the ship is now sunk, call out which ship was sunk
    if (ship.IsSunk)
    {
        Console.WriteLine(Name + " says: \"You sunk my " + ship.Name + "!\"");
    }

    //For either a hit or a sunk, return a Hit status
    return ShotResult.Hit;
}

Notice the use of the ShotResult enumeration. All this enum does is pass the result of the shot from the defending player (who calls "Hit" or "Miss") to the attacking player. But what will the attacking player do with that info?

Step 3: The Shot Result

The last method our Player class needs is ProcessShotResult(), which is implemented like so:

public void ProcessShotResult(Coordinates coords, ShotResult result)
{
    var panel = FiringBoard.Panels.At(coords.Row, coords.Column);
    switch(result)
    {
        case ShotResult.Hit:
            panel.OccupationType = OccupationType.Hit;
            break;

        default:
            panel.OccupationType = OccupationType.Miss;
            break;
    }
}

With all these methods in place, it's finally time to set up our Game object and actually play a game!

Playing a Game

The Game object from Part 2 represents a game in progress; here's what it looked like when we last left it.

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

    public Game() { }

    public void PlayRound() { }

    public void PlayToEnd() { }
}

We now need to define the constructor, PlayRound() and PlayToEnd() methods.

Constructor

The Game constructor needs to:

  1. Create the players (and by extension create things like the GameBoard and FiringBoard instances for those players).
  2. Have the players place their ships.
  3. Output the status of the boards.

Here's our (relatively simple) constructor:

public Game()
{
    Player1 = new Player("Amy");
    Player2 = new Player("Vince");

    Player1.PlaceShips();
    Player2.PlaceShips();

    Player1.OutputBoards();
    Player2.OutputBoards();
}

We finally have names for our players! From here on out we'll be calling Player 1 "Amy" and Player 2 "Vince".

PlayRound()

A "round" in this context is one shot by Amy and one shot by Vince. The only real trick here is that it is possible for Vince (since he is Player 2) to lose the game before he has a chance to take a shot. Here's the PlayRound() method:

public void PlayRound()
{
    var coordinates = Player1.FireShot();
    var result = Player2.ProcessShot(coordinates);
    Player1.ProcessShotResult(coordinates, result);

    if (!Player2.HasLost) //If player 2 already lost, 
                          //we can't let them take another turn.
    {
        coordinates = Player2.FireShot();
        result = Player1.ProcessShot(coordinates);
        Player2.ProcessShotResult(coordinates, result);
    }
}

PlayToEnd()

The final piece to this whole puzzle is the PlayToEnd() method, which will repeatedly call PlayRound() until one of the players loses. Here's that final method:

public void PlayToEnd()
{
    while (!Player1.HasLost && !Player2.HasLost)
    {
        PlayRound();
    }

    Player1.OutputBoards();
    Player2.OutputBoards();

    if (Player1.HasLost)
    {
        Console.WriteLine(Player2.Name + " has won the game!");
    }
    else if (Player2.HasLost)
    {
        Console.WriteLine(Player1.Name + " has won the game!");
    }
}

Now that we've got almost our entire system designed, all that's left to do is write a bit more code to automate playing some games and do some simple statistics.

Let's Play

Here's the last bit of code we need to run these games:

class Program
{
    static void Main(string[] args)
    {
        int player1Wins = 0, player2Wins = 0;

        Console.WriteLine("How many games do you want to play?");
        var numGames = int.Parse(Console.ReadLine());

        for (int i = 0; i < numGames; i++)
        {
            Game game1 = new Game();
            game1.PlayToEnd();
            if(game1.Player1.HasLost)
            {
                player2Wins++;
            }
            else
            {
                player1Wins++;
            }
        }

        Console.WriteLine("Player 1 Wins: " + player1Wins.ToString());
        Console.WriteLine("Player 2 Wins: " + player2Wins.ToString());
        Console.ReadLine();
           
    }
}

All this Program class does it take a number from the user, play that many games, and then output Player 1's wins and Player 2's wins.

To start with, let's just play one game.

Now we can see how our players (Amy and Vince) have placed their ships.

So far, so good. Amy and Vince have not placed their ships in the same pattern as the other, and the ships are (for the most part) spread out evenly on the board.

Once we start playing a game, the entire game goes past very quickly. Here's a screenshot of what the output looks like:

We can see from this output that our searching strategy seems to be working. Vince gets a hit on Amy at (9, 3), so he then tries neighboring square (8, 3), which is a miss, before getting the killing blow on Amy's Aircraft Carrier at (9, 2).

Let's see the final result of the game.

Looks like Amy won this round, but just by a hair. Vince only had Amy's Destroyer left to find.

OK great, so playing one game seems to work. Let's try playing a thousand.

Stats

If we run 1000 games using this setup, will one of the two players be favored to win more of the games? I'll run three sets of 1000 games, and you, dear readers, can decide for yourselves if my system is biased or not (or, even better, download and run the sample project to try it for yourself!):

Round 1

Round 2

Round 3

Drawbacks

There are a couple significant improvements I could make to this system:

Determining orientation: When a hit is made, we don't yet know the orientation (e.g. up-down or left-right) of the attacked ship. However, once a second hit is made, we do know the orientation. A more complete system would take this into account to sink hit ships even faster.

Probability shots: There's some research to suggest that ship placement can actually be predicted with a certain amount of accuracy. A more complete system would understand these probabilities and take them into account when selecting a shot.

However, as is always true with my Modeling Practice series, the point of modeling Battleship is not to solve the game perfectly, it's to practice taking a large, complex problem and breaking it down into solvable pieces, and I feel pretty good about how this particular one went.

Summary

Battleship is a beloved game; it's been around in one form or another for 100+ years and continues to entertain generations of children and adults, including me and my family.

By pulling it apart, seeing how it works, and eventually creating a fully-functional model program for it, we (you and me, dear readers) have hopefully gained a little more insight into how to break seemingly large, difficult problems down into their constituent pieces to make modeling them just a bit easier.

In this final part of our modeling practice, we implemented quite a bit of functionality. We can now:

  • Have the attacking player select a shot.
  • Have the defending player call out the status of that shot.
  • Have the attacking player mark the status of the shot on the firing board.
  • Play a game round-by-round.
  • Play a game all the way to completion.

As always, the sample project is available for anyone to download, change, improve, whatever.

exceptionnotfound/BattleshipModellingPractice
Sample project for my Modeling Practice for the game Battleship. - exceptionnotfound/BattleshipModellingPractice

If this series helped you, or if you see something we could improve on, let me know in the comments!

Happy Modeling!