What we will do:
In this part we will implement methods to put ship on a game board. We also add a class to keep together all the information about our ships.
We will learn new things:
- computable property,
- dictionary,
- tuple,
- string interpolation,
- structure,
- how to iterate over an array,
- how to iterate over a dictionary,
Table of contents
- Add properties to
Ship
class - Computed property
- Structures
- Add methods to
Ship
class - Add
Ships
class Board
class modificationEngineGameBattleship
class modification- Last step
Source code for this chapter.
Ship
classExtend previously created
Ship
class with new enumerated type
1 2 3 |
enum Status { case damaged, ready, destroyed } |
and properties
1 2 3 4 5 6 7 |
private let size: Int private var readyLevel: Int var position: [(row: Int, col: Int, status: Status)] var isDestroyed: Bool { return readyLevel == 0 ? true : false } |
Status.damage
andStatus.destroyed
used to mark one ship's segment as damaged or destroyed. We will differentiate both states to allow future improvements in game logic. We may say that.damage
state is reversible -- we may implement method to recover it's state to fully operationalStatus.ready
..destroyed
state will be used in case of crytical not recoverable damages.size
is a size of a ship in terms of cells occuied by this ship (we allow only "stright" ships where all ship's segments are in one line; bends are not allowed).position
is an array of tuples describing each ship's segment: its location (row
andcolumn
) and condition (status
).readyLevel
it describe combat readiness and as for now will expressed the number of ship's.ready
segments.isDestroyed
is true if the ship is destroyed and may not be longer take part in the fight. This variable is an example of computed property.
Added fields require to implement an initializer
1 2 3 4 5 6 |
init(size: Int, position: [(row: Int, col: Int, status: Status)]) { self.size = size self.position = position self.readyLevel = size } |
Classes, structures and enumerations can define computed properties. This type of properties do not actually store a value. Instead, they provide a getter (
get
) and an optional setter (set
) to retrieve and set other properties and values indirectly. Sometimes we don't need to store explicitly (permanently) some value - for example it may be to expensive (taking into consideration memory usage) or this value may be computed based on other values.
To define computed property we use get
and set
keyword. In case we want to have a read only computed property we can skip the get
keyword (and of course there is no set
because this is read only property). The following snippet shows an example of computed properties usage
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
struct StructWithComputedProperty { var simpleProperty = 3 var computedProperty: Int { get { return simpleProperty*2 } set(newValue) { simpleProperty = newValue*3 } } var computedPropertyReadOnly: Int { return simpleProperty*3 } } var swcp = StructWithComputedProperty() print("simpleProperty=\(swcp.simpleProperty) computedProperty=\(swcp.computedProperty) computedPropertyReadOnly=\(swcp.computedPropertyReadOnly)") swcp.computedProperty = 5 print("simpleProperty=\(swcp.simpleProperty) computedProperty=\(swcp.computedProperty) computedPropertyReadOnly=\(swcp.computedPropertyReadOnly)") |
and results of its execution
1 2 |
simpleProperty=3 computedProperty=6 computedPropertyReadOnly=9 simpleProperty=15 computedProperty=30 computedPropertyReadOnly=45 |
Notice that in this example struct was used instead of a class -- see next section for an explanations.
Structures are very similar to classes. Both are general-purpose, flexible constructs that become the building blocks of our program’s code. As we have sen so far, we define properties and methods to add functionality to our classes using the same syntax we use to define constants, variables, and functions; the same we do in case of structures. With structures and classes in Swift can
- define properties to store values;
- define methods to provide functionality;
- define initializesr to set up their initial state;
- conform to protocols to provide standard functionality of a certain kind;
- define subscripts to provide access to their values using subscript syntax;
- be extended to expand their functionality beyond a defaut implementation.
Moreover, classes have some additional capapbilities that structures do not
- inheritance enabling one class to inherit the characteristics of another;
- we can check and interprete the type of a class instance at runtime;
- reference counting allows more than one reference to a class instance;
- deinitializers enable an instance of a class to free up any resorces it has assigned.
The additional capabilities that classes support come at the cost of increased complexity. As a general guideline, prefer structures because they’re easier to reason about, and use classes when they’re appropriate or necessary. In practice, this means most of the custom data types you define will be structures and enumerations.
Another worth mention difference is that structures ale always copied when they are passed around in the code and to not use reference counting. Structure instances are always passed by value, and class instances are always passed by reference.
Objective-C vs. Swift difference
In Onjective-C's Foundation NSArray
, NSDictionary
, NSString
are implemented as classes, not structures. This means that arrays, dictionaries and strings are always passed as a references, rather than as a copy.
In Swift Array
, Dictionary
and String
are implemented as structures. This means that data of this type when are assigned to a new constant or variable, or when they are passed to a function or method, are copied.
Ship
classAdd two methods
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
func hitAt(row: Int, col: Int) { for (index, coordinate) in position.enumerated() { if coordinate.row == row && coordinate.col == col { position[index].status = Status.damaged readyLevel -= 1 break } } } func isLocatedAt(row: Int, col: Int) -> Bool { for coordinate in position { if coordinate.row == row && coordinate.col == col { return true } } return false } |
In the body of hitAt(row: Int, col: Int)
method we can see an example of for loop that allows us to iterate over an array and have both index and element
1 2 3 |
for (index, coordinate) in position.enumerated() { } |
When index is not needed, we can use simpler syntax as it is showned in the isLocatedAt(row: Int, col: Int)
method
1 2 3 |
for coordinate in position { } |
Ships
classShips
class will collect informations about all ships belonging to one of the players. To do so, we create a class with the needed fields
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Ships { var ships = [String: Ship]() var shipsAtCommand: Int { var shipsNumber = ships.count for (_, ship) in ships { if ship.isDestroyed { shipsNumber -= 1 } } return shipsNumber } } |
Variable ships
is defined as a dictionary. A dictionary stores associations between keys (all of the same type) and values (all of the same but possible different than keys type) with no defined order. As a dictionary key we may use any hashable (basic types like booleans, integers, and strings are hashable so they can be used as the keys for).
Let's see some examples and resulting code
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import Foundation var dictionary1 = [String:String]() // shorthand form var dictionary2 = Dictionary<String, String>() // full form var dictionary3 = ["digit0": "zero", "digit1": "one", "digit2": "two"] print(dictionary1) print(dictionary2) print(dictionary3) dictionary3["digit2"] = "TwO" print(dictionary3) |
[:]
[:]
["digit0": "zero", "digit2": "two", "digit1": "one"]
["digit0": "zero", "digit2": "TwO", "digit1": "one"]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// updateValue(_:forKey:) method returns an optional value of the dictionary's // value type; in our case this is String? or "optional String" var oldValue = dictionary3.updateValue("TWO", forKey: "digit2") print(dictionary3) print("Old value: \(oldValue!) (\(oldValue))") dictionary3["digit3"] = "three" print(dictionary3) // Iterating over a dictionary for (key, value) in dictionary3 { print("key: \(key) value: \(value)") } for key in dictionary3.keys { print("key: \(key)") } for value in dictionary3.values { print("value: \(value)") } |
["digit0": "zero", "digit2": "TWO", "digit1": "one"]
Old value: TwO (Optional("TwO"))
["digit0": "zero", "digit2": "TWO", "digit1": "one", "digit3": "three"]
key: digit0 value: zero
key: digit2 value: TWO
key: digit1 value: one
key: digit3 value: three
key: digit0
key: digit2
key: digit1
key: digit3
value: zero
value: TWO
value: one
value: three
Swift 4 brings a number of improvements to make dictionaries more powerful, useful and usable.
Sequence-based initializer.
We can now create dictionaries from a sequence of key-value pairs.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
let dict01 = Dictionary(uniqueKeysWithValues: zip( 1..., words) ) print(dict01) let streamOfTuples = [("one", 1), ("two", 2), ("three", 3), ("four", 4)] let dict02 = Dictionary( uniqueKeysWithValues: streamOfTuples) print(dict02) |
[2: "two", 3: "three", 1: "one", 4: "four"]
["three": 3, "four": 4, "one": 1, "two": 2]
Merging
Dictionary now includes an initializer that allows us to merge two dictionaries together. If we want to merge one dictionary into another, Swift also provides a merge(_:uniquingKeysWith:)
method. Both allow us to specify a closure to resolve merge conflicts caused by duplicate keys.
1 2 3 4 5 6 7 8 |
var dictionary = [("a", 1), ("b", 2), ("a", 3), ("c", 4)] var afterMerge = Dictionary(dictionary, uniquingKeysWith: { (current, _) in current }) print(afterMerge) // Keeping existing value for key "a": afterMerge.merge(zip(["a", "c"], [3, 4])) { (current, _) in current } print(dictionary) |
["b": 2, "a": 1, "c": 4]
[("a", 1), ("b", 2), ("a", 3), ("c", 4)]
More details can be found for example in How to work with Dictionaries in Swift 4.
In the created Ships
class notice the part dedicated to iterate over a dictionary
1 2 3 4 5 |
for (_, ship) in ships { if ship.isDestroyed { shipsNumber -= 1 } } |
ships
is a dictionary, so iterating over this set returns a tupple (in our case) of the form
1 |
(key: String, value: Ship) |
In consequence, we may write the iteration code either in the form
1 2 3 4 5 |
for element in ships { if element.value.isDestroyed { shipsNumber -= 1 } } |
or, as we did, in the form
1 2 3 4 5 |
for (key, value) in ships { if value.isDestroyed { shipsNumber -= 1 } } |
Because in our case we know that value
is a Ship
object, so we use name ship
instead to make code more readable. The first tupple's element, key
is not needed in our code, what is signalled by Xcode with the message Immutable value 'key' was never used; consider replacing with '_' or removing it
. To silent this warning, key
is finally replaced by underscore character _
which is th way wy say to Swift: Swift, I know that there is something here, but I don't need it and so I don't care about it..
Board
class modificationBased on previously created method
mayPlaceShip(size: Int, anchor: (row: Int, col: Int), direction: Ship.Direction)
add a new 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 |
func placeShip(size: Int, anchor: (row: Int, col: Int), direction: Ship.Direction) -> Ship { 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: Ship.Status.ready)) } return Ship(size: size, position: position) } |
This code doesn't introduce any new elements. Variable position
consist of iterarively added ship's parts which is done in
1 2 3 4 5 6 7 |
for i in 0...size-1 { ... position.append((row: r, col: c, status: Ship.Status.ready)) } |
This method assumes that ship placement is possible so every time a mayPlaceShip(size: Int, anchor: (row: Int, col: Int), direction: Ship.Direction)
should be called before.
There is one simplification, still also present in mayPlaceShip(size: Int, anchor: (row: Int, col: Int), direction: Ship.Direction)
method: we don't care about cells sorounding the ship. Thus, as for now, two ships may be placed so they will "touch" -- this will be fixed in next chapter.
EngineGameBattleship
class modificationAdd two new fields to track each player's fleet
1 2 |
private var shipsPlayer: Ships private var shipsOpponent: Ships |
new enumeration type
1 2 3 |
enum Who { case player, opponent } |
to distinguish players. On of them is called player and the other is called his opponent. In practices, player can be identified with human, while oppenent with a computer. In accordance with these changes, also initializer should be modified
1 2 3 4 5 6 7 |
init(rows: Int = 10, cols: Int = 10) { self.boardPlayer = Board(rows: rows, cols: cols) self.boardOpponent = Board(rows: rows, cols: cols) self.shipsPlayer = Ships() self.shipsOpponent = Ships() } |
Add also two "boilerplate" methods which main purpose is to call previously created in Board
class methods with correct arguments (that is board and ships dedicated to given player).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
func mayPlaceShip(who: Who, size: Int, anchorRow: Int, anchorCol: Int, direction: Ship.Direction) -> Bool { let anchor = (row: anchorRow, col: anchorCol) let (boardWho, _) = getWhoBoardAndShips(who: who) return boardWho.mayPlaceShip(size: size, anchor: anchor, direction: direction) } func 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 } |
Both methods assumes existance of a method getWhoBoardAndShips(who: Who)
returning board and ships variable dedicated to a given player who
1 2 3 4 5 6 7 |
func getWhoBoardAndShips(who: Who) -> (board: Board, ships: Ships) { if who == Who.player { return (board: boardPlayer, ships: shipsPlayer) } return (board: boardOpponent, ships: shipsOpponent) } |
Finally the method test()
should be removed.
Before we go to the next step let's stop on the line
1 |
shipsWho.ships["\(anchor)"] = ship |
As we know, ships
is a dictionary with keys of the String
type and Ship
as a value type. Key should uniquely identify every ship. Combining each ship anchor coordinates (its first cell's row and column coordinate) we get a unique ship because no more than one ship can start in a given cell (anchor). So the goal is to create such a string and ma be simpply accomplish with
1 |
"\(anchor)" |
This is an example of string interpolation. String interpolation is a way to construct a new string value from a mix of constants, variables, literals, and expressions by including their values inside a string literal. Each item inserted into the string literal and wrapped in a pair of parentheses, prefixed by a backslash (\ ITEM )
is interpreted and result of interpretation substitutes its call place
1 2 3 |
let age = 12 let message = "Age \(age): is \(age < 30 ? "young" : "middle-aged")" print(message) |
In the example above, the value of age
(number 12
) is inserted into a string literal in place of \(age)
. The value of age is also part of a compound expression later in the string where ternary conditional operator is used.
The ternary conditional operator is a special operator with three parts, which takes the form question ? answer1 : answer2
. It’s a shortcut for evaluating one of two expressions based on whether question
is true or false. If question is true, it evaluates answer1
and returns its value; otherwise, it evaluates answer2
and returns its value. The ternary conditional operator is shorthand for the code below:
1 2 3 4 5 |
if question { answer1 } else { answer2 } |
Expression used in message
string returns either young
or middle-aged
string depending on age
variable value. Finally this short three-line code shoud print
1 |
Age 12: is young |
Strings and Characters
https://docs.swift.org/swift-book/LanguageGuide/StringsAndCharacters.html
Going back to our code "\(anchor)"
results with the following string (of course, numbers 3 and 4 may be different depending on the actual row and column values)
1 |
(row: 3, col: 4) |
What is important, because anchor
is a tuple, also parentheses as well as names of tuple's elements are included in this string.
In the last step we should modify the
main
file.
First, at the begining of the file, add the following 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 |
func test(who: EngineGameBattleship.Who, size: Int, anchorRow: Int, anchorCol: Int, direction: Ship.Direction) -> Bool { let x = game.mayPlaceShip(who: who, size: size, anchorRow: anchorRow, anchorCol: anchorCol, direction: direction) if x { game.placeShip(who: who, size: size, anchorRow: anchorRow, anchorCol: anchorCol, direction: direction) return true } return false } |
This method simply calls the mayPlaceShip(size: Int, anchor: (row: Int, col: Int), direction: Ship.Direction)
metod from Board
class. If there may beplaced a ship of the size size
starting at row row
and column col
and directed to direction
direction then we phisicaly put it on the board with the method placeShip(size: Int, anchor: (row: Int, col: Int), direction: Ship.Direction)
from Board
class call. Having this method, we may implement a loop to make sequence of tests.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
let player = EngineGameBattleship.Who.player let opponent = EngineGameBattleship.Who.opponent let shipsParams = [(who: player, size: 4, row: 3, col: 4, direction: Ship.Direction.down), (who: player, size: 4, row: 5, col: 6, direction: Ship.Direction.left), (who: opponent, size: 4, row: 3, col: 2, direction: Ship.Direction.left), (who: opponent, size: 4, row: 5, col: 6, direction: Ship.Direction.up)] for param in shipsParams { let result = test(who: param.who, size: param.size, anchorRow: param.row, anchorCol: param.col, direction: param.direction) print(result ? "OK" : "ERROR") } game.printBoards() |
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 |
OK ERROR ERROR OK PLAYER 1 1234567890 ++++++++++++ 1+..........+ 2+..........+ 3+...X......+ 4+...X......+ 5+...X......+ 6+...X......+ 7+..........+ 8+..........+ 9+..........+ 10+..........+ ++++++++++++ OPPONENT 1 1234567890 ++++++++++++ 1+..........+ 2+.....X....+ 3+.....X....+ 4+.....X....+ 5+.....X....+ 6+..........+ 7+..........+ 8+..........+ 9+..........+ 10+..........+ ++++++++++++ Program ended with exit code: 0 |