
If you recall the AIPlayer class from Part 2, the Create factory method takes an IGameStrategy parameter:
Public Function Create(ByVal gridId As Byte, ByVal GameStrategy As IGameStrategy) As IPlayer
With New AIPlayer
.PlayerType = ComputerControlled
.GridIndex = gridId
Set .Strategy = GameStrategy
Set .PlayGrid = PlayerGrid.Create(gridId)
Set Create = .Self
End With
End Function
An AIPlayer can be created with an instance of any class that implements the IGameStrategy interface.
In any OOP language that supports class inheritance, we could have a base class e.g. GameStrategyBase, from which we could derive the various implementations, and with that we would have a place to write all the code that’s common to all implementations, …or that all implementations would possibly need to use… or not. See, class inheritance is the most important language feature that the “VBA can’t do OOP” or “VBA is not a real language” crowd love to bring up. And yet, more often than not, class inheritance isn’t the ideal solution – composition is.
And we’re going to do exactly that, by composing all IGameStrategy implementations with a GameStrategyBase class:

Coupling a game strategy with this “base” class isn’t an issue: the class is specifically meant to be used by IGameStrategy implementations. So we can shamelessly do this:
Option Explicit
Implements IGameStrategy
Private base As GameStrategyBase
Private Sub Class_Initialize()
Set base = New GameStrategyBase
End Sub
And then proceed with implementing the PlaceShip method, given that AI player’s own PlayerGrid and the IShip the game controller is asking us to place on the grid. The base.PlaceShip method simply returns the first legal position+direction it can find.
Then we can implement the Play function to return an IGridCoord position and let the controller know what position this player is shooting at. We have a number of helper functions in GameStrategyBase we can use for that.
Random
The RandomShotStrategy shoots at random coordinates until it has located all enemy ships …then proceeds to sink them all, one after the other. It also places its ships randomly, regardless of whether the ships are adjacent or not.
Private Sub IGameStrategy_PlaceShip(ByVal grid As PlayerGrid, ByVal currentShip As IShip)
Dim direction As ShipOrientation
Dim position As IGridCoord
Set position = base.PlaceShip(Random, grid, currentShip, direction)
grid.AddShip Ship.Create(currentShip.ShipKind, direction, position)
If grid.shipCount = PlayerGrid.ShipsPerGrid Then grid.Scramble
End Sub
Private Function IGameStrategy_Play(ByVal enemyGrid As PlayerGrid) As IGridCoord
Dim position As IGridCoord
Do
If EnemyShipsNotAcquired(enemyGrid) 0 Then
Set position = base.ShootRandomPosition(Random, enemyGrid)
Else
Set position = base.DestroyTarget(Random, enemyGrid, enemyGrid.FindHitArea)
End If
Loop Until base.IsLegalPosition(enemyGrid, position)
Set IGameStrategy_Play = position
End Function
Here the double-negative in the statement “the number of enemy ships not acquired, is not equal to zero” (WordPress is having a hard time with rendering that operator, apparently), will probably be end up being inverted into a positive statement, which would make it read better. Perhaps If EnemyShipsToFind = 0 Then, and invert the Else logic. Or…
Private Function IGameStrategy_Play(ByVal enemyGrid As PlayerGrid) As IGridCoord
Dim position As IGridCoord
Do
If EnemyShipsToFind(enemyGrid) > 0 Then
Set position = base.ShootRandomPosition(Random, enemyGrid)
enemyGrid.FindHitArea)
Else
Set position = base.DestroyTarget(Random, enemyGrid,
End If
Loop Until base.IsLegalPosition(enemyGrid, position)
Set IGameStrategy_Play = position
End Function
That EnemyShipsToFind function should probably be a member of the PlayerGrid class.
FairPlay
The FairPlayStrategy is similar, except it will proceed to destroy an enemy ship as soon as it’s located. It also takes care to avoid placing ships adjacent to each other.
Private Sub IGameStrategy_PlaceShip(ByVal grid As PlayerGrid, ByVal currentShip As IShip)
Do
Dim direction As ShipOrientation
Dim position As IGridCoord
Set position = base.PlaceShip(Random, grid, currentShip, direction)
Loop Until Not grid.HasAdjacentShip(position, direction, currentShip.Size)
grid.AddShip Ship.Create(currentShip.ShipKind, direction, position)
If grid.shipCount = PlayerGrid.ShipsPerGrid Then grid.Scramble
End Sub
Private Function IGameStrategy_Play(ByVal enemyGrid As PlayerGrid) As IGridCoord
Dim position As GridCoord
Do
Dim area As Collection
Set area = enemyGrid.FindHitArea
If Not area Is Nothing Then
Set position = base.DestroyTarget(Random, enemyGrid, area)
Else
Set position = base.ShootRandomPosition(Random, enemyGrid)
End If
Loop Until base.IsLegalPosition(enemyGrid, position)
Set IGameStrategy_Play = position
End Function
Merciless
The MercilessStrategy is more elaborate: it doesn’t just shoot at random – it shoots in patterns, targeting the edges and/or the center areas of the grid. It will destroy an enemy ship as soon as it’s found, and will avoid shooting in an area that couldn’t possibly host the smallest enemy ship that’s still afloat. And yet, it’s possible it just shoots a random position, too:
Private Sub IGameStrategy_PlaceShip(ByVal grid As PlayerGrid, ByVal currentShip As IShip)
Do
Dim direction As ShipOrientation
Dim position As IGridCoord
Set position = base.PlaceShip(Random, grid, currentShip, direction)
Loop Until Not grid.HasAdjacentShip(position, direction, currentShip.Size)
grid.AddShip Ship.Create(currentShip.ShipKind, direction, position)
If grid.shipCount = PlayerGrid.ShipsPerGrid Then grid.Scramble
End Sub
Private Function IGameStrategy_Play(ByVal enemyGrid As PlayerGrid) As IGridCoord
Dim position As GridCoord
Do
Dim area As Collection
Set area = enemyGrid.FindHitArea
If Not area Is Nothing Then
Set position = base.DestroyTarget(Random, enemyGrid, area)
Else
If this.Random.NextSingle < 0.1 Then
Set position = base.ShootRandomPosition(this.Random, enemyGrid)
ElseIf this.Random.NextSingle < 0.6 Then
Set position = ScanCenter(enemyGrid)
Else
Set position = ScanEdges(enemyGrid)
End If
End If
Loop Until base.IsLegalPosition(enemyGrid, position) And _
base.VerifyShipFits(enemyGrid, position, enemyGrid.SmallestShipSize) And _
AvoidAdjacentHitPosition(enemyGrid, position)
Set IGameStrategy_Play = position
End Function
In most cases (ScanCenter and ScanEdges do), the AI doesn’t even care to “remember” the last hit it made: instead, it asks the enemy grid to give it a “hit area”. It then proceeds to analyze whether that area is horizontal or vertical, and then attempts to extend it further.
It’s Open-Source!
I uploaded the complete code to GitHub: https://github.com/rubberduck-vba/Battleship.

Man there is an awful lot to consume here! Thank you Mathieu!
LikeLiked by 1 person
Mathieu, I noticed that RandomShotStrategy’s Self property returns type RandomShotStrategy, but FairPlayStrategy & MercilessStrategy are of type IGameStrategy. I don’t think it really matters much, since the Self property is only called in the Create function (which returns IGameStrategy anyway).
LikeLike
Good eye! Indeed there’s a superfluous cast going on here, should be IGameStrategy =)
LikeLike