Hacking with Swift: learning project 34, Four in a Row
Create a new Xcode project, call it “Four in a row” and save it somewhere sensible. Set its deployment info to use only iPad and to be only in Landscape orientation.
Creating the interface with
Open Main.storyboard, select the view controller and embed it into a Navigation Controller. Select the Navigation Controller, select its Navigation Bar in the document outline then, in the Attributes Inspector, uncheck the Translucent property so that we can avoid our game going behind the bar.
Drag a Horizontal Stack View from the Object Library to the view controller, make it fill all the available space (navigation bar excluded) then go to Editor > Resolve Auto Layout Issues >Reset to Suggested Constraints.
Drag 7 buttons to the stack view, then select the stack view, go to the Attributes Inspector and change its Distribution property to “Fill equally” and its Spacing property to “2”.
Select all buttons, change their background colour to be “White Color” and remove their text. Select each one of the buttons one after the other and give them increasing tag values from 0 to 6. Finally, select the View and give it a Light Grey background colour.
Switch to the Assistant Editor, Ctrl-drag from the first button to create an Outlet Collection called
columnButtons and then drag from the black dot in the Swift file to each of the other buttons to connect them. Ctrl-drag again from the first button and create an action called
makeMove(), change its sender type to
UIButton and then connect all the other buttons.
Preparing for basic play
Create a new Cocoa Touch Class file, subclass
NSObject and name it “Board”. Give it two static properties,
height, respectively with 7 and 6 as value.
Back in ViewController.swift add an array of
UIView arrays called
placedChips property and an implicitly unwrapped
viewDidLoad() set up a
for-in loop from 0 up to and excluding the value of the width of the board (7, so count from 0 to 6) so that for each run it would append an empty array of
UIViews to the
placedChips array. After the loop, call the yet unwritten
resetBoard() method, assigning a new
Board() to the
board object then looping over each value from 0 up to and excluding the elements’ count of the
placedChips array (storing that value in a
i constant). Inside create another
for-in loop to loop over each element inside the
[i] indexed element of the
placedChips array and remove it from the superview. Then, outside of the first loop, remove all elements from that same indexed array while keeping its capacity (this should save memory).
Back in Board.swift create a new enumeration called
ChipColor: Int above the class definition and give it three cases:
none = 0,
black to define what can go in a slot.
Below, declare a new property
slots of type
[ChipColor](). Then override the
init() method with a loop from 0 up to and excluding the width and the height of the board multiplied together and then, for each of those elements, append a
.none enumeration object to the
slots array. After this call
super.init(). I wonder why this last thing is called after and not before … mysteries of coding.
Write two new methods:
chip(inColumn:row:) will return the element of the
slots array at the
[row + column * Board.height] index (I am very curious how he just got to that formula from a one-dimensional array without explaining to us where the array would start counting from, bottom, top, left, right…?) while
set(chip:inColumn:row:) to set the slot’s color.
Let’s try to understand the math here by reverse engineering his formula: if we want a chip placed in column 3 (fourth column!!) row 5 (sixth column!!) the element of the array to be accessed would be (5 + (3 x 6)), which is element 23. It is pretty obvious that the columns would go from left to right but what about the rows? Should we assume that, being in UIKit world, they go from top to bottom? Probably, but for such a game, I would count them from the bottom so our chip will be the one marked with the x in the following wonderful primitive drawing:
o o o x o o o
o o o o o o o
o o o o o o o
o o o o o o o
o o o o o o o
o o o o o o o
Or it could be, in the same column, the last one on the bottom. Who knows? If anyone knows the answer I would be more than happy to credit you here for your help.
Always in Board.swift write the
nextEmptySlot(inColumn:) method, which takes an integer parameter and returns an optional integer. Loop over the row from 0 up to and excluding the board’s height (so from 0 to 5) and for each of them call the
chip(inColumn:row:) method verifying if its return result is equal to
.none, i.e. the column is empty. If that is the case return the found row. Otherwise, at the end of the method, return
nil, to show that the column is full.
After this, write the
canMove(inColumn:) method, which accepts an
Int parameter and returns a
Bool, returning the result of a check against not being
nil of the just written
add(chip:inColumn:) method. Inside, conditionally bind the return result of the
nextEmptySlot(inColumn:) method and, if that succeeds, call
set(chip:inColumn) on the column parameter and the just bound row.
Go to ViewController.swift and add the
addChip(inColumn:row:color:) method to the end of the class. Extract the button found at the
column index of the
columnButtons array and store it in a
button constant property. Declare a
size property which is the minimum of either the width of the button’s frame of 1/6 of its height. Create then a rectangle with
x: 0, y: 0 and dimensions the just declared
size property. After this, if the count of the elements of the
placedChips array at the
column index (that is, if the array found in the super-array’s count) is less than the chosen row + 1 (that is, if the column is not full), create a new
UIView, set its frame to be the rectangle just created, make it non-user interactive, set its background color to be the color parameter, set its layer’s corner radius to the half of its size (so that it becomes a circle) then determine its center with the return result of the yet unwritten method
positionForChip(inColumn:row:), transform its position to a
y: -800 so that it is completely out of the screen and add it as a subview. Finally animate it during half a second with a “curveEaseIn” option (that is start slowly and accelerate) to move it from way out of the screen to its original place. Then, lastly, append it to the
column-indexed array of the
Create now the
positionForChip(inColumn:row:) method, which will take two integers and return a
CGPoint. As before, declare a button and a size in the same way, then declare an
xOffset equal to the x-coordinate of the center of the frame’s rectangle and a
yOffset that is equal to the largest value of the y-coordinate of the button’s frame (that is, its bottom line) to which the half of the size will be subtracted (o__O?) — so we get to the bottom of the button and a bit up, ok… breath —. Now subtract from and assign the new value to the
yOffset the size multiplied by the row… so that we get progressively towards the top of the screen the higher the row gets … phew … FInally, return the point made up of these two last values.
The last thing to do for this part is to complete the
makeMove() action method. Set a constant
column equal to the sender’s tag than, conditionally bind a
row constant to the next empty slot in the board’s column (passing the
column constant as the argument), then call the
add(chip:inColumn:) on the board and the
addChip(inColumn:row:color:) of the view controller passing in
.red as arguments.
At two days after writing the rest of the code I sincerely do not remember how these things get together but I will trust this working code!
Adding in players:
Create a new Cocoa Touch Class called “Player” (subclassing NSObject) and give it 5 properties a
chip: ChipColor, a
color: UIColor, a
name: String, a
playerId: Int (be careful to write the ‘d’ as a lowercase letter…) and a
static var allPlayers that will be an array of players with an initialiser that will consider only its
chip property and have
.black as arguments.
GameplayKit and make the class conform to the
GKGameModelPlayer protocol, thus described in the Documentation:
Implement this protocol to describe a player in your turn-based game so that a strategist object can plan game moves.
You adopt this protocol to describe the gameplay of your turn-based game for use by a
GKStrategistobject. The strategist uses your player class, along with other custom classes you implement (adopting the
GKGameModelUpdateprotocols) to plan moves in your game.
You use your custom class implementing this protocol in several places:
Your class that implements this protocol can also contain properties and methods relevant to the implementation of your game—for example, an identifying color or name.
Now write the initialiser for the class which will take only the
chip parameter. Set
self.chip equal to the
chip property and
self.playerId equal to the raw value of the chip property (it is an enumeration). Then, if the chip is red, set the color property to be
.red and the name one to be “Red”, otherwise set it to
.black and “Black”. Finally, call
Add the last bit of code to this class which is the computed property
opponent: Player. If the chip is red we will return the second object in the
allPlayers array, otherwise we will return the first one.
Go to Board.swift and write two stub method that just
isWin(for player: Player).
Inside ViewController.swift write an
updateUI() method which will change the view controller’s title to show the turn of the current player using
(board.currentPlayer.name) as string interpolation. We don’t have such a property yet for the board but we’ll fix that pretty soon. Now write a
continueGame() method which will do several things: create an optional string for the game over title and setting its default value to
nil. Then, if the game is won for board’s current player, we set the title sensibly, otherwise if the board is full we do likewise. At this point, if the game over title is not nil, we present an alert controller with the game over title and we set an action to reset the board for a new game AND return from the method. If nothing of this is not happening we are still in the game so we change the board’s current player to his opponent and update the UI.
Go back to Board .swift and create a new property called
currentPlayer of type
Player. Being it non optional we need to initialise it at the top of the initialiser with a set to the first element of the
allPlayers array of the
Back in ViewController.swift, change the
makeMove method so that the
.add() method has
board.currentPlayer.chip as first argument and the
addChip() method has
board.currentPlayer.color as last argument. Add also a call to
Add a line at the beginning of the
updateUI method so that it resets the board also in code:
board = Board().
Detecting wins and draws in Four in a Row
In Board.swift, change the
isFull() method so that, for every column from 0 up to and excluding the board’s width, if we can make a move in the column, we return false, otherwise, outside of the loop (so that we know that all of the board has been checked), we return true.
And now we write the most obscure method I have ever seen, no really … I have not understood a single line of its logic and why it makes any sense at all. I mean … all this project is really making me boil… I want to learn the logic, the reasoning behind things… Doing things this way doesn’t make me able to reproduce these steps or apply this “knowledge” to other future apps. But fine … we need to buy a book right? Not learn from it…
So, the method is called
squaresMatch(initialChip:row:column:moveX:moveY), it takes 5 parameters, the first of type
ChipColor and the other four of type
Int and it returns a
Bool. First we need to check if we cannot win in the current position (I mean, already the name of the method, squares… which squares? We have circles…chips…). To do so we check if the
row argument plus the triple of the
moveY argument (the triple? Why? Explain this to me!) is either less than zero or greater that the board’s height. I get where we want to head, that we do not accept things going outside of the board, fine, but why is this code good and something else? Then we repeat the same thing for the (column + triple of
moveX) — why the triple for my lunch’s sake! If any of these checks returns true, we need to
return false from the method as this is not a valid move…
Then, if this so obvious code didn’t make us exit the method we check every square… the
row parameter, that one adding the
moveY parameter and then the double and triple of that … REALLY, THIS DOESN’T MAKE ANY SENSE TO ME!
GameplayKit to Board.swift. Now modify the
isWin() method so that the
for: parameter is of type
GKGameModelPlayer, declare a
chip constant equal to the player parameter downcast with a force unwrap to a
Player object’s chip then enter a nested loop over each row and each column. Inside there will be an
if-else if statement which will call each time the
squaresMatches method and always pass in
chip as first parameter ,
row as second,
column as third, then 1, 0, 1, 1 as fourth and 0, 1, 1, -1 as fifth. For every one of these cases it will return true, otherwise, at the end of the method, keep the
return false line.
I’m still not understanding this but well … I don’t really care too much at this point. I hate programming games without NO ONE explaining to me with calm and precision the logic and WHY it works like that… There is really no point doing things this way!
GameplayKit AI works:
Create a new Cocoa Touch Class subclassing
NSObject called “Move”. Import
GameplayKit into it, give it two integer properties,
column, give the first one a value of
0 and initialise the second with the common class initialiser.
Board class conform to the
GKGameModel protocol. Add two methods:
The first one will accept an optional
NSZone parameter set to
nil by default and return an
Any object. Inside, it creates a new
Board() object called
copy, then calls the
setGameModel method on it passing it
self (the board instance) before returning it.
The second one conditionally binds the
gameModel parameter to a
board constant after optionally down casting it to a
Board object, then sets the
slots array to the board’s slots and the current player to the board’s current player.
gameModelUpdates(for:) method, which accepts a
GKGameModelPlayer object and returns an optional array of
GKGameModelUpdate objects. Inside, conditionally bind the
player parameter into a
playerObject constant after optionally down casting it as
Player. If this is possible check if it the game is a win for either this player or its opponent, in which case return
nil (no move available). Declare an empty array of
[Move] and, for each column from 0 up to and excluding the Board’s width, if we can make a move in such a column, append a new
Move object to the
moves array. After the loop, return the
moves object, which will give you an error and about which Paul says nothing … At the end of the method return
Implement now the
apply(_:) method which accepts a single parameter of type
GKGameModelUpdate. Conditionally bind that parameter to a constant and optionally downcast it to a
Move object. Call the
add(chip:inColumn:)passing as arguments the current player’s chip and the move’s column, then switch players.
score(for:) object, which accepts a
GKGameModelPlayer parameter and returns an
Int. Conditionally bind such parameter in a constant and optionally downcast it as a
Player. If the move (but where is it taken from in here?) is a win for the AI, return 1000, else if the move is a win for the player return -1000, otherwise return 0.
At the top of this class create two computed properties (which you get from the protocol’s stubs).
players will return
activePlayer will return
We still have an error in our code and we are not told about it. No, it’s not me badly copying. Everything is as in Paul’s code… When we return
moves from the
gameModelUpdates(for:) method, we are asked to downcast this result as the type expected for the return type of the method. I will leave it as an error right now and then see if the following pages talk about this…
GameplayKit AI using
Open ViewController.swift, import
GameplayKit and create a new implicitly unwrapped property of type
GKMinMaxStrategist called “strategist”. Here is the description of this class:
An AI that chooses moves in turn-based games using a deterministic strategy.
To use this strategy, you provide scores that rate possible states of your game model for their desirability to a player, and the strategist exhaustively searches all possible game model states in order to make choices that maximize the rating for its own moves and minimize the rating for an opponent’s moves. You provide information about your game model to the strategist by implementing the GKGameModel, GKGameModelPlayer, and GKGameModelUpdate protocols in your custom classes, and then use the strategist’s methods to find optimal moves.
viewDidLoad() initialise the strategist, give it a max look ahead depth of 7 (that is, make it look 7 moves in the future!) and set its random source (i.e., what to do if two moves are equally good) to
nil, that is to just return the first move.
Insert inside the
resetBoard() method the setting of
board as the strategist’s game model, just before updating the UI.
Create a new method to let the AI figure out what’s the best next move:
columnForAIMove(), which just returns an Optional Integer. If we can conditionally bind the result of the
bastMove(for:) method (optionally downcast as a
Move object) in a
aiMove constant, return its column, then outside this call, return
Create a new
makeAIMove(inColumn:) method, which takes an integer parameter, that conditionally binds the board’s next empty slot in the passed column argument to a
row integer constant, then calls
add(chip:inColumn:) on the board and the
addChip(inColumn:row:color:) method, before calling the
continueGame() method in the end.
startAIMove() method, which invokes the global dispatch queue to execute the code asynchronously, weakly captures
self, then declares a new
strategistTime constant equal to the CoreFoundation instance of the current absolute time, check that we have a column for the AI to move into, then create a
delta equal to the absolute time minus the strategist Time (why?!), then create a time ceiling of one second to generate a sensible delay (which will be yet another constant equal to this ceiling minus the delta), then — finally — dispatch the work on the main thread with a deadline of
.now() + delay and execute the
updateUI() add a check to call
startAIMove() if the board’s current player’s chip is black.
Back in the
startAIMove() method, disable all buttons at the beginning of the method using a
.forEach closure on the
columnButtons array, create a spinner grey activity indicator view and start its animation before adding it as a custom view of the left bar button item. Inside the main dispatch queue, reenable the buttons and destroy the left bar button item.
The app is officially finished but the error in Board.swift remains so we will accept the solution provided by Xcode, just changing
as?. Doing this, though, makes the project build but the AI never stops thinking… Putting the force unwrap, instead, makes the app crash at the
columnForAIMove method… I added some breakpoints to see what was happening but… by now… no real helpful things.
So … I asked help but this just doesn’t work … It either crashes if I make a force downcast or it doesn’t load the next move if I make the optional one. Again, all this time completely lost for nothing …
I really wonder how much I can go on like this, reading tutorial, not understanding what is going on and then? Just not getting any explanation back…
Anyway … enjoy the rest of your day, and thank you for reading.
If you like what I’m doing here please consider liking this article and sharing it with some of your peers. If you are feeling like being really awesome, please consider making a small donation to support my studies and my writing (please appreciate that I am not using advertisement on my articles).
If you are interested in my music engraving and my publications don’t forget visit my Facebook page and the pages where I publish my scores (Gumroad, SheetMusicPlus, ScoreExchange and on Apple Books).
You can also support me by buying Paul Hudson’s books from this Affiliate Link.
Anyways, thank you so much for reading!
Till the next one!