HiveBrain v1.2.0
Get Started
← Back to all entries
principlecppMinor

Battleship strategy evaluation framework

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
frameworkevaluationbattleshipstrategy

Problem

In preparation for the May 2015 Community Challenge, I decided to build a Battleship strategy tester.

Implementing an ocean

First, there is an underlying Ocean class that represents both the 10x10 grid that is the playing field. It has an internal representation for both the grid and for the ships that are placed on it. It has feature that allow it to be used as both the grid on which ships are placed (defense, if you will) and the grid on which the results of guesses are plotted (offense).

Two kinds of strategies

In the classic Battleship game, there are actually two different kinds of strategies used. There is a placement strategy and a bombardment strategy. It seemed to me that it would be useful to match different placement strategies against different bombardment strategies, so I created two different things -- a place() function that places the ships and returns an Ocean object, and a Bomber class which is a pure virtual class that is intended to serve as a base class. It operates on a local copy of a passed Ocean object.

The purpose for doing it this way is that it will allow for various different kinds of placement strategies to create many different varieties of Ocean and then any number of bombardment strategy classes (each derived from the base Bomber class) can all be handed those Ocean objects for a quick head-to-head evaluation. Since I'm interested in the effectiveness of the strategies more than in the actual details of game play, the play object of the Bomber class simply takes as many turns as necessary to sink all of the ships and reports the number of turns.

The concept is that each pairing of placement and bombardment strategies can be evaluated multiple times and descriptive statistics can be generated to answer the question of which strategies are most effective.

Minimal example

At the moment, only one example each of a placement strategy (which always puts the ships side by side in the upper left corner

Solution

Bomber

class Bomber
{
public:
    Bomber(Ocean &o) : ocean(o), tracking(), turns(), verbose(false) {}
    virtual ~Bomber() = default;
    virtual bool turn() = 0;
    unsigned play() { while(turn()); return turns; }
protected:
    Ocean ocean;
    Ocean tracking;
    unsigned turns;
    bool verbose;
};


Don't like how the derived bomber decides that the game is finished (turn() returns false). Or that it is the responsibility of the base class to provide tracking information (if the derived class wants that it can implement it). Also tracking the turn is not the responsibility of the derived class.

Also I would add a check to stop a bomber playing forever.

I would make all those members private.

class Bomber
{
public:
    Bomber(Ocean &o) : ocean(o), turns(), verbose(false) {}
    virtual ~Bomber() = default;
    bool allBoatsDestroyed(char h)
    {
        // keep track of hits and decide when game is over
    }
    unsigned play()
    {
        for(;;)
        {
            unsigned decision = turn();

            // Once the decision has been made
            // The framework plays the move and checks the result.
            char r = ocean.bomb(decision);
            youHit(r);

            // turn is incremented only after bomb dropped.
            ++turns;

            if (allBoatsDestroyed(r)) {
                break;
            }

            if (turns > failThreshold) {
                throw std::runtime_error("Failed to complete task");
            }
        }
        return turns;
    }
    bool isVerbose() {return verbose;}

    // The turn does not need to know anything from
    // the base class it just decides where to to go
    // next.
    virtual int turn() = 0;

    // The result of calling bomb on ocean is fed back
    // to the derived class so it on the next iteration
    // it may use this information to make its decision.
    //
    // Note: Has a default implementation as the strategy
    //       may not even need this.
    virtual void youHit(char x)   {}
private:
    Ocean ocean;
    unsigned turns;
    bool verbose;
};


Placer

In the same vein. For a framework we need a more rigorous way of working with potentially broken (bad code). I like that the output is an Ocean object. But we need a standardized way of asking the caller for placement. Then exiting on an error.

enum {Horz, Vert} DirectionOfBoat;
struct Placement
{
     unsigned         topLeft;
     DirectionOfBoat  direction;
};
class Placer
{
    public:
        virtual ~Placer() = default;
        virtual Placement getPlacement(Boat const& boat) = 0;
};
void doPlacement(Ocean& ocean, Placer& placer)
{
     std::vector  boats = ocean.getBoatList();
     for(Boat const& boat: boats)
     {
         Placement place = placer.getPlacement(boat);

         // We assume the placer knows where it has placed
         // other boats so any error indicates a bug in the
         // placer algorithm
         if (!ocean.place(place)) {
             throw std::runtime_error("Bad placement");
         }
    }
}

Code Snippets

class Bomber
{
public:
    Bomber(Ocean &o) : ocean(o), tracking(), turns(), verbose(false) {}
    virtual ~Bomber() = default;
    virtual bool turn() = 0;
    unsigned play() { while(turn()); return turns; }
protected:
    Ocean ocean;
    Ocean tracking;
    unsigned turns;
    bool verbose;
};
class Bomber
{
public:
    Bomber(Ocean &o) : ocean(o), turns(), verbose(false) {}
    virtual ~Bomber() = default;
    bool allBoatsDestroyed(char h)
    {
        // keep track of hits and decide when game is over
    }
    unsigned play()
    {
        for(;;)
        {
            unsigned decision = turn();

            // Once the decision has been made
            // The framework plays the move and checks the result.
            char r = ocean.bomb(decision);
            youHit(r);

            // turn is incremented only after bomb dropped.
            ++turns;

            if (allBoatsDestroyed(r)) {
                break;
            }

            if (turns > failThreshold) {
                throw std::runtime_error("Failed to complete task");
            }
        }
        return turns;
    }
    bool isVerbose() {return verbose;}

    // The turn does not need to know anything from
    // the base class it just decides where to to go
    // next.
    virtual int turn() = 0;

    // The result of calling bomb on ocean is fed back
    // to the derived class so it on the next iteration
    // it may use this information to make its decision.
    //
    // Note: Has a default implementation as the strategy
    //       may not even need this.
    virtual void youHit(char x)   {}
private:
    Ocean ocean;
    unsigned turns;
    bool verbose;
};
enum {Horz, Vert} DirectionOfBoat;
struct Placement
{
     unsigned         topLeft;
     DirectionOfBoat  direction;
};
class Placer
{
    public:
        virtual ~Placer() = default;
        virtual Placement getPlacement(Boat const& boat) = 0;
};
void doPlacement(Ocean& ocean, Placer& placer)
{
     std::vector<Boat>  boats = ocean.getBoatList();
     for(Boat const& boat: boats)
     {
         Placement place = placer.getPlacement(boat);

         // We assume the placer knows where it has placed
         // other boats so any error indicates a bug in the
         // placer algorithm
         if (!ocean.place(place)) {
             throw std::runtime_error("Bad placement");
         }
    }
}

Context

StackExchange Code Review Q#90275, answer score: 6

Revisions (0)

No revisions yet.