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