What we will do:
In this part we will implement shooting functionality to check if shot is possible, to make a shot and to do some actions after shot. We will also implement code responsible for making ship layout -- automatically put them in legal places.
We will learn new things:
- set data structure.
Table of contents
Source code for this chapter.
Board
class modificationSo far both ships and boards are controlled by
EngineGameBattleship
1 2 3 4 5 6 7 8 9 |
class EngineGameBattleship { private var boardPlayer: Board private var boardOpponent: Board private var shipsPlayer: Ships private var shipsOpponent: Ships ... } |
With this there is no relation between board and ships on that board -- we still have to remember that shipsPlayer
are ships related to boardPlayer
. This could lead to problems in a future, so let's chane it. Remove from EngineGameBattleship
lines
1 2 |
private var shipsPlayer: Ships private var shipsOpponent: Ships |
and add to Board
class the code
1 |
var ships: Ships |
At this moment a list of errors both in EngineGameBattleship
as well as in Board
should appear. To fix them, in EngineGameBattleship
:
- Remove two lines
12self.shipsPlayer = Ships()self.shipsOpponent = Ships()
from initializer. - Method
getWhoBoardAndShips(who: Who) -> (board: Board, ships: Ships)
1234567func getWhoBoardAndShips(who: Who) -> (board: Board, ships: Ships) {if who == Who.player {return (board: boardPlayer, ships: shipsPlayer)}return (board: boardOpponent, ships: shipsOpponent)}
should be simplified to the form
1234567private func getWhoBoard(who: Who) -> Board {if who == Who.player {return boardPlayer}return boardOpponent} - Line
1let (boardWho, _) = getWhoBoardAndShips(who: who)
inmayPlaceShip(who: Who, size: Int, anchorRow: Int, anchorCol: Int, direction: Ship.Direction) -> Bool
should be simplified to the form
1let boardWho = getWhoBoard(who: who) - Method
placeShip(who: Who, size: Int, anchorRow: Int, anchorCol: Int, direction: Ship.Direction)
123456789func placeShip(who: Who, size: Int, anchorRow: Int, anchorCol: Int, direction: Ship.Direction) {let anchor = (row: anchorRow, col: anchorCol)let (boardWho, shipsWho) = getWhoBoardAndShips(who: who)let ship = boardWho.placeShip(size: size,anchor: anchor,direction: direction)shipsWho.ships["\(anchor)"] = ship}
should take the form
12345func placeShip(who: Who, size: Int, anchorRow: Int, anchorCol: Int, direction: Ship.Direction) {let anchor = (row: anchorRow, col: anchorCol)let boardWho = getWhoBoard(who: who)boardWho.placeShip(size: size, anchor: anchor, direction: direction)}
In Board
class:
- add
1ships = Ships()
line in the initializer body.
When ship is totaly destroyed (sunken) all its sorrounding cells shoud be marked as it is showned at the figure below
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
1 1 1 1234567890 1234567890 1234567890 ++++++++++++ ++++++++++++ ++++++++++++ 1+..........+ 1+..........+ 1+..........+ 2+..........+ 2+..........+ 2+..........+ 3+..........+ 3+..........+ 3+..OOO.....+ 4+...X......+ 4+...!......+ 4+..O!O.....+ 5+...X......+ ===> 5+...X......+ ===> 5+..O!O.....+ 6+..........+ hit at 6+..........+ hit at 6+..OOO.....+ 7+..........+ (4, 4) 7+..........+ (5, 4) 7+..........+ 8+..........+ 8+..........+ 8+..........+ 9+..........+ 9+..........+ 9+..........+ 10+..........+ 10+..........+ 10+..........+ ++++++++++++ ++++++++++++ ++++++++++++ |
We can do this becaus of our assumption that no ship can "touch" other ship. Add to Board
class method implemmenting this
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
private func markWhenShipDestroyed(ship: Ship) { let allCellsArround = [(rowModifier: -1, colModifier: 0),// top (rowModifier: -1, colModifier: +1),// top-right (rowModifier: 0, colModifier: +1),// right (rowModifier: +1, colModifier: +1),// bottom-right (rowModifier: +1, colModifier: 0),// bottom (rowModifier: +1, colModifier: -1),// bottom-left (rowModifier: 0, colModifier: -1),// left (rowModifier: -1, colModifier: -1)// top-left ] var row, col: Int for (r, c, _) in ship.position { for modifier in allCellsArround { row = r + modifier.rowModifier col = c + modifier.colModifier if board[row][col] == Board.CellType.empty || board[row][col] == Board.CellType.shot || board[row][col] == Board.CellType.notAllowed { board[row][col] = Board.CellType.rescue } } } } |
We marked it as a private
because this is a helper method called only from the body of its class. Although we have a nice and correct if
statement, it is a little bit clumsy. Me may turn it into something more readable with teh help of set data structure.
A set stores distinc values of the same type in a collection with no defined ordering. We can use a set instead of an array when the order of elements is not relevant, or when we need to ensure that an element only appears once. A type of objects stored in a set must be hashable.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
import Foundation var setOfDigitsNames1 = Set<String>() setOfDigitsNames1.insert("one") setOfDigitsNames1.insert("two") print(setOfDigitsNames1) var setOfDigitsNames2: Set<String> = ["one", "two"] var setOfDigitsNames3: Set = ["one", "two"] print(setOfDigitsNames2) print(setOfDigitsNames3) if !setOfDigitsNames3.isEmpty { if setOfDigitsNames3.contains("two") { setOfDigitsNames3.remove("two") } if !setOfDigitsNames3.contains("three") { setOfDigitsNames3.insert("three") } } print(setOfDigitsNames3) var setOfInts1: Set = [1, 2, 3, 4] var setOfInts2: Set = [3, 4, 5, 6] print(setOfInts1) print(setOfInts2) print(setOfInts1.union(setOfInts2)) print(setOfInts1.intersection(setOfInts2)) print(setOfInts1.subtracting(setOfInts2)) print(setOfInts1.symmetricDifference(setOfInts2)) |
["one", "two"]
["one", "two"]
["one", "two"]
["one", "three"]
[2, 3, 1, 4]
[5, 6, 3, 4]
[5, 6, 2, 3, 1, 4]
[3, 4]
[2, 1]
[5, 6, 2, 1]
To test set membership or equality we can use
- To test if two sets contain exactly the same values, we use
==
operator. - To test if all of the values of a set are contained in the specified set, we use
isSubset(of:)
method. We useisStrictSubset(of:)
if we want to exclude equality of both sets. - To test if a set contains all of the values from a specified set, we use
isSuperset(of:)
method. We useisStrictSuperset(of:)
if we want to exclude equality of both sets. - To test if both sets have no common values, we use
isDisjoint(with:)
method.
Going back to our mathod, we can replace
1 2 3 4 5 |
if board[row][col] == Board.CellType.empty || board[row][col] == Board.CellType.shot || board[row][col] == Board.CellType.notAllowed { board[row][col] = Board.CellType.rescue } |
with dictionary
1 2 3 4 5 6 7 |
let chageToRescue: Set<Board.CellType> = [.empty, .shot, .notAllowed] if chageToRescue.contains(board[row][col]) { board[row][col] = Board.CellType.rescue } |
Just created method markWhenShipDestroyed(ship: Ship)
is called by
1 2 3 4 5 6 7 8 9 10 |
private func afterHitAction(row: Int, col: Int) { for (_, ship) in ships.ships { if ship.isLocatedAt(row: row, col: col) { ship.hitAt(row: row, col: col) if ship.isDestroyed { markWhenShipDestroyed(ship: ship) } } } } |
This method performs some actions every time a ship gets hit. What is important, methods doesn't check if hit was possible or not -- it assumes that it was feasible. So first we should implement mayShot(row: Int, col: Int) -> Bool
to check if shot is possible. Next we should implement func shot(row: Int, col: Int)
method to make a shot. And finally we will call afterHitAction(row: Int, col: Int)
. Both methods may be implemented as it is showned below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
func mayShot(row: Int, col: Int) -> Bool { let yes: Set<Board.CellType> = [.empty, .ship, .notAllowed] if yes.contains(board[row][col]) { return true } let no: Set<Board.CellType> = [.hit, .rescue, .shot] if no.contains(board[row][col]) { return false } return false } |
1 2 3 4 5 6 7 8 |
func shot(row: Int, col: Int) { if board[row][col] == .empty || board[row][col] == .notAllowed { board[row][col] = .shot } else if board[row][col] == .ship { board[row][col] = .hit afterHitAction(row: row, col: col) } } |
Next method we will implement is a method intended to automaticaly position all ships of a defined sizes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
func shipsAutoSetup(shipsSize: [Int], maxTriesPerShip: Int) -> Int { var shipDirection = Ship.Direction.up var possible = true var success: Bool var positioned = 0 var anchor: (row: Int, col: Int) for size in shipsSize { success = false for _ in 1...maxTriesPerShip { if let r = EngineGameBattleshipUtils.getRandomInt(from: 1, to: rows), let c = EngineGameBattleshipUtils.getRandomInt(from: 1, to: cols) { anchor = (row: r, col: c) if let direction = EngineGameBattleshipUtils.getRandomInt(from: 1, to: 4) { switch direction { case 1: shipDirection = .up case 2: shipDirection = .right case 3: shipDirection = .down default: shipDirection = .left } possible = mayPlaceShip(size: size, anchor: anchor, direction: shipDirection) if possible { placeShip(size: size, anchor: anchor, direction: shipDirection) positioned += 1 success = true break } } } } if !success { return positioned } } return positioned } |
For every number size
defined in shipSize
we try at most maxTriesPerShip
times to place ship of that size on a board. For every try we randomly select its anchor
's row r
and column c
as well as shipDirection
. If it is possible to put the ship of the size size
, started at row r
and column c
and directed to shipDirection
direction (this is checked with mayPlaceShip
method call) we place it (with placeShip
method call) we increase the total number of all positioned ships (variable positioned
) and set success
variable to true
. Variable success
equals to true
informs that ship was successfuly placed on a board. If it is false
when for
loop ends for a given size
it means that we tried without success maxTriesPerShip
times to position this ship and it doesn't make any sense to try more times or for other ships. As a result we return the number of ships which were successfuly positioned on the board. If this number is equal to shipSize
array it means that all ships were positioned successfully. To increase the chances of success it is important to put bigger ships as first in shipSize
array and smaller at the end.
Two things should be fixed
- Warning
Result of call to 'placeShip(size:anchor:direction:)' is unused
because we don't use result returned byplaceShip(size:anchor:direction:)
method. We will fix this in a moment. - Error
Type 'EngineGameBattleshipUtils' has no member 'getRandomInt'
. We will fix it addinggetRandomInt
method to theEngineGameBattleshipUtils
class.
Let's fix placeShip(size:anchor:direction:)
method
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
func placeShip(size: Int, anchor: (row: Int, col: Int), direction: Ship.Direction) { var modifier: (forRow: Int, forCol: Int)! var r: Int! var c: Int! var position = [(row: Int, col: Int, status: Ship.Status)]() switch direction { case .up: modifier = (forRow: -1, forCol: 0) case .down: modifier = (forRow: +1, forCol: 0) case .left: modifier = (forRow: 0, forCol: -1) case .right: modifier = (forRow: 0, forCol: +1) } for i in 0...size-1 { r = anchor.row + i*modifier.forRow c = anchor.col + i*modifier.forCol board[r][c] = CellType.ship position.append((row: r, col: c, status: .ready)) // BEGIN: To add border along ship r = anchor.row+(modifier.forCol) + i*modifier.forRow c = anchor.col+(modifier.forRow) + i*modifier.forCol board[r][c] = .notAllowed r = anchor.row-(modifier.forCol) + i*modifier.forRow c = anchor.col-(modifier.forRow) + i*modifier.forCol board[r][c] = .notAllowed // END: To add border along ship } // BEGIN: To add borders at the ends of ship // Next to anchor r = anchor.row + (-1)*modifier.forRow c = anchor.col + (-1)*modifier.forCol board[r][c] = .notAllowed r = anchor.row+(modifier.forCol) + (-1)*modifier.forRow c = anchor.col+(modifier.forRow) + (-1)*modifier.forCol board[r][c] = .notAllowed r = anchor.row-(modifier.forCol) + (-1)*modifier.forRow c = anchor.col-(modifier.forRow) + (-1)*modifier.forCol board[r][c] = .notAllowed // Next to anchor opposit end r = anchor.row + (size)*modifier.forRow c = anchor.col + (size)*modifier.forCol board[r][c] = .notAllowed r = anchor.row+(modifier.forCol) + (size)*modifier.forRow c = anchor.col+(modifier.forRow) + (size)*modifier.forCol board[r][c] = .notAllowed r = anchor.row-(modifier.forCol) + (size)*modifier.forRow c = anchor.col-(modifier.forRow) + (size)*modifier.forCol board[r][c] = .notAllowed // END: To add borders at the ends of ship ships.ships["\(anchor)"] = Ship(size: size, position: position) } |
Compared to existing code two new blocks of code started at // BEGIN: To add border along ship
and // BEGIN: To add borders at the ends of ship
are added and final return (as well as return typ from function header) is removed. Both blocks are used to mark all cells directly sorrounding ship as notAllowed
to prevent orther ships to be placed too close.
EngineGameBattleshipUtils
classTo be able to position ships we need a method returning an integer from given range (including both range ends) and such that is not contained in
excluding
array to prevent from selecting occupied cells. Mathod returns nil
in case of failure or integer if it has been found.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class func getRandomInt(from: Int, to: Int, excluding: [Int]? = nil) -> Int? { let maxTries = 10 var candidate = -1 if from == to { return from } for _ in 0 ..< maxTries { candidate = Int.random(in: from ... to) if excluding != nil { if !excluding!.contains(candidate) { return candidate } } else { return candidate } } return nil } |
Finally it's time to add test code to
main
file. To make test possible, remove for a while private
in front of boardPlayer
declaration in EngineGameBattleship
class
1 |
var boardPlayer: Board |
Change main
file contents to the following form
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
import Foundation let game = EngineGameBattleship() let player = EngineGameBattleship.Who.player var r = game.mayPlaceShip(who: player, size: 2, anchorRow: 2, anchorCol: 4, direction: .down) if r { game.placeShip(who: player, size: 2, anchorRow: 2, anchorCol: 4, direction: .down) r = game.boardPlayer.mayShot(row: 3, col: 4) if r { game.boardPlayer.shot(row: 3, col: 4) } else { print("Shot is not possible") } game.printBoards() r = game.boardPlayer.mayShot(row: 2, col: 4) if r { game.boardPlayer.shot(row: 2, col: 4) } else { print("Shot is not possible") } game.printBoards() } else { print("Ship may not be placed") } |
Now we can run our code. As a result, we should see
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
PLAYER 1 1234567890 ++++++++++++ 1+..+++.....+ 2+..+X+.....+ 3+..+!+.....+ 4+..+++.....+ 5+..........+ 6+..........+ 7+..........+ 8+..........+ 9+..........+ 10+..........+ ++++++++++++ OPPONENT 1 1234567890 ++++++++++++ 1+..........+ 2+..........+ 3+..........+ 4+..........+ 5+..........+ 6+..........+ 7+..........+ 8+..........+ 9+..........+ 10+..........+ ++++++++++++ PLAYER 1 1234567890 ++++++++++++ 1+..OOO.....+ 2+..O!O.....+ 3+..O!O.....+ 4+..OOO.....+ 5+..........+ 6+..........+ 7+..........+ 8+..........+ 9+..........+ 10+..........+ ++++++++++++ OPPONENT 1 1234567890 ++++++++++++ 1+..........+ 2+..........+ 3+..........+ 4+..........+ 5+..........+ 6+..........+ 7+..........+ 8+..........+ 9+..........+ 10+..........+ ++++++++++++ Program ended with exit code: 0 |
As we can se first shot hits the ship at row 3, column 4 which is marked with !
character. Second shot hit the ship at row 2 and column 4. This hit sunk the ship so it is marked with O
characters.
Change again main
file contents to the form
1 2 3 4 5 6 |
import Foundation let game = EngineGameBattleship() game.boardPlayer.shipsAutoSetup(shipsSize: [4,3,3,2,2,2,1,1,1,1], maxTriesPerShip: 10) game.printBoards() |
After code execution, we should see a result similar to presented below
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
PLAYER 1 1234567890 ++++++++++++ 1++X+.....+++ 2++++.....+X+ 3+X+....+++X+ 4+++.++++X+X+ 5+X+.+XX+X+++ 6+++.++++++X+ 7++++.+++++++ 8++X+.+XXXX++ 9++X+.+++++++ 10++X+.+XX+..+ ++++++++++++ OPPONENT 1 1234567890 ++++++++++++ 1+..........+ 2+..........+ 3+..........+ 4+..........+ 5+..........+ 6+..........+ 7+..........+ 8+..........+ 9+..........+ 10+..........+ ++++++++++++ Program ended with exit code: 0 |
We tried to put 10 ships: one of size 4, two of size 3, three of size 2 and four of size 1. All of them were placed successfuly.
Finally add back private
in front of boardPlayer
declaration in EngineGameBattleship
class
1 |
private var boardPlayer: Board |