Learning Swift — Day 147 to 149

Hacking with Swift: learning project 34, Four in a Row

Setting up

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 UIStackView

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, width and 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 Board property.

Inside 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.

Write the 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, red and 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 nextEmptySlot(inColumn:) method.

Declare the 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 placedChips array.

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 column, row and .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: GKGameModelPlayer

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 .red and .black as arguments.

Import GameplayKit and make the class conform to the GKGameModelPlayer protocol, thus described in the Documentation:

Summary

Implement this protocol to describe a player in your turn-based game so that a strategist object can plan game moves.

Declaration

Discussion

You adopt this protocol to describe the gameplay of your turn-based game for use by a GKStrategist object. The strategist uses your player class, along with other custom classes you implement (adopting the GKGameModel and GKGameModelUpdate protocols) 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.

For more information about describing your gameplay model and using a strategist, see The Minmax Strategist in GameplayKit Programming Guide.

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 super.init().

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 return false, isFull() and 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 Player class.

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 continueGame().

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 column and row parameter, that one adding the moveX and moveY parameter and then the double and triple of that … REALLY, THIS DOESN’T MAKE ANY SENSE TO ME!

Import 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!

How GameplayKit AI works: GKGameModel, GKGameModelPlayer and GKGameModelUpdate .

Create a new Cocoa Touch Class subclassing NSObject called “Move”. Import GameplayKit into it, give it two integer properties, value and column, give the first one a value of 0 and initialise the second with the common class initialiser.

Implementing GKGameModel: gameModelUpdates(for:) and apply().

Make the Board class conform to the GKGameModel protocol. Add two methods: copy(with:) and setGameModel(_:).

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.

Implement the 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 nil.

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.

Implement the 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 Player.allPlayers while activePlayer will return currentPlayer.

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…

Creating a GameplayKit AI using GKMinmaxStrategist.

Open ViewController.swift, import GameplayKit and create a new implicitly unwrapped property of type GKMinMaxStrategist called “strategist”. Here is the description of this class:

Summary

An AI that chooses moves in turn-based games using a deterministic strategy.

Declaration

Discussion

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.

Inside 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 nil.

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.

Write the 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 makeAIMove(inColumn:) method.

Inside 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! in 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!

Published by Michele Galvagno

Professional Musical Scores Designer and Engraver Graduated Classical Musician (cello) and Teacher Tech Enthusiast and Apprentice iOS / macOS Developer Grafico di Partiture Musicali Professionista Musicista classico diplomato (violoncello) ed insegnante Appassionato di tecnologia ed apprendista Sviluppatore iOS / macOS

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: