Apple Watch : Mine Sweeper

Minesweeper for WatchOS running in the WatchOS simulator

This tutorial will show you how to create a version of the game MineSweeper for Apple Watch in Swift.

Whilst there is a fair bit of logic to the game, this tutorial concentrates on interactions within the interface controller, the game logic is provided 'as is' and should be largely self explanatory.

Creating a new project

Create a new Swift project using the iOS App with WatchKit App template, name it 'MineSweeper" and uncheck all the 'Include' options at the bottom of the screen

Creating the model

Minesweeper relies on any given point (or cell) on the grid being able to know about its neighbours.

  1. Start by adding a Swift file to the project (using 'File' -> 'New' -> 'File' from the menu). Name the File 'Point' and click 'Next'
  2. Ensure that the file is added to both the 'MineSweeper' and 'MineSweeper WatchKitExtension' targets before continuing, as shown here: Setting Targets when adding a new file to an Xcode project
  3. Add the following code to the file:
    struct Point : Equatable{
    
        var x: Int
        var y: Int
    
        init(x: Int, y: Int){
            self.x = x
            self.y = y
        }
    }
    
    func ==(lhs: Point, rhs: Point) -> Bool {
        return lhs.x == rhs.x && lhs.y == rhs.y
    }

    This creates a structure to represent a point and also allows two points to be compared for equality

  4. The next step is to create another file to handle the representation of the minefield, this will contain a structure to contain information about a certain location on the grid (whether it is a mine and how many mines are adjacent to it). It will also contain much of the logic to create the minefield and check cells

    Add another Swift file, named Minesweeper (ensuring you choose the same targets as before) and add the following code to the file (it can be copy and pasted):
    struct MineInfo {
        var isMine = false
        var adjacentMineCount = 0
    }
    
    class MineField {
        
        var hasStarted = false
        var isExploded = false
        var mineCount = 0
        var field = Array<Array<MineInfo>>()
        
        var width: Int
        var height: Int
        
        init(width: Int, height: Int){
            
            self.width = width
            self.height = height
            
            for _ in 0..<height {
                //create row
                var row = [MineInfo]()
                for _ in 0..<width {
                    row.append(MineInfo())
                }
                field.append(row)
            }
        }
        
        func checkMine(_ point: Point) -> MineInfo{
            if !hasStarted{
                hasStarted = true
                determineMinesWithStart(point: point)
            }
            
            let location = field[point.y][point.x]
            if location.isMine {
                isExploded = true
            }
            var mineInfo = MineInfo()
            mineInfo.adjacentMineCount = location.adjacentMineCount
            mineInfo.isMine = location.isMine
            return mineInfo
        }
        
        func determineMinesWithStart(point: Point){
           
            mineCount = Int(sqrt(Double(width * height)))
            populateMines(mineCount, avoidingPoint: point)
            populateAdjacentMineCount()
        }
        
        func populateMines(_ count: Int, avoidingPoint startPoint: Point){
            var mineLocations = [Point]()
            repeat {
                let randPoint = getRandomPoint(width: width, height: height)
                if startPoint != randPoint && !mineLocations.contains(randPoint){
                    mineLocations.append(randPoint)
                }
            } while mineLocations.count < count
            
            for point in mineLocations {
                field[point.y][point.x].isMine = true
            }
            
        }
        
        func mineLocations() -> [Point] {
            var mines = [Point]()
            for y in 0..<field.count {
                for x in 0..<field[y].count{
                    if field[y][x].isMine {
                        mines.append(Point(x: x, y: y))
                    }
                }
            }
            return mines
        }
        
        func validNeighboursFor(point: Point) -> [Point] {
            
            var neighbours = [Point]()
            let minX = max(0,point.x - 1)
            let maxX = min(width-1,point.x + 1)
            let minY = max(0, point.y-1)
            let maxY = min(height-1,point.y+1)
            
            for x in minX...maxX {
                for y in minY...maxY {
                    if !(point.x == x && point.y == y) {
                        neighbours.append(Point(x: x, y: y))
                    }
                }
            }
            return neighbours
        }
        
        func countMinesAt(points: [Point]) -> Int{
            
            var count = 0
            for point in points {
                if field[point.y][point.x].isMine{
                    count += 1
                }
            }
            return count
        }
        
        func populateAdjacentMineCount(){
            
            for y in 0..<field.count {
                for x in 0..<field[y].count{
                    
                    let neighbours = validNeighboursFor(point: Point(x: x, y: y))
                    let nearbyMines = countMinesAt(points: neighbours)
                    field[y][x].adjacentMineCount = nearbyMines
                }
            }
            
        }
        
        func getRandomPoint(width: Int, height: Int) -> Point{
            let x = Int(arc4random_uniform(UInt32(width)))
            let y = Int(arc4random_uniform(UInt32(height)))
            return Point(x: x, y: y)
        }
    }
    

Getting it to work with WatchKit

The User Interface

One of the restrictions (at least of WatchKit 2.1) is that elements cannot be added to scenes programmatically. For a game like minesweeper this is a rather irritating and means for our grid we have to create 25 individual buttons in the scene.

  1. Start by adding elements to the interface controller so you have a group containing five buttons, each button should be 0.2 of the width of the its group and the group should be 0.15 the height of its container, similar to how the image here shows: Interface builder showing 5 buttons in a group in a WatchKit interface
  2. Repeat the process so you have a grid as shown, along with a button to reset the game, another button to toggle the flag option (hint: use an emoji of a flag as the text for the button title), and a label showing how many flags/mines are remaining as follows: WatchKit interface with 5x5 grid of buttons, reset button, flag button and label showing how many flags remaining
  3. Each of the mine locations interface elements need to be connected up to the interface controller as both actions and outlets. Unfortunately we cannot differentiate between the buttons programmatically (there is no 'tag' property as can be found in iOS), so each button will need its own action. Create outlets as follows:
    • Connect up the flagButton as an outlet named flagButton
    • Connect the 5 x label as an outlet named mineCountLabel
  4. Next add code to represent the outlets for each of the buttons to the the view controller, then connect them up by control (or right) click and dragging from the button to the code - the code to do that is shown below and can be copied and pasted:
    @IBOutlet var mineSpace0_0: WKInterfaceButton!
    @IBOutlet var mineSpace0_1: WKInterfaceButton!
    @IBOutlet var mineSpace0_2: WKInterfaceButton!
    @IBOutlet var mineSpace0_3: WKInterfaceButton!
    @IBOutlet var mineSpace0_4: WKInterfaceButton!
    
    @IBOutlet var mineSpace1_0: WKInterfaceButton!
    @IBOutlet var mineSpace1_1: WKInterfaceButton!
    @IBOutlet var mineSpace1_2: WKInterfaceButton!
    @IBOutlet var mineSpace1_3: WKInterfaceButton!
    @IBOutlet var mineSpace1_4: WKInterfaceButton!
    
    @IBOutlet var mineSpace2_0: WKInterfaceButton!
    @IBOutlet var mineSpace2_1: WKInterfaceButton!
    @IBOutlet var mineSpace2_2: WKInterfaceButton!
    @IBOutlet var mineSpace2_3: WKInterfaceButton!
    @IBOutlet var mineSpace2_4: WKInterfaceButton!
    
    @IBOutlet var mineSpace3_0: WKInterfaceButton!
    @IBOutlet var mineSpace3_1: WKInterfaceButton!
    @IBOutlet var mineSpace3_2: WKInterfaceButton!
    @IBOutlet var mineSpace3_3: WKInterfaceButton!
    @IBOutlet var mineSpace3_4: WKInterfaceButton!
    
    @IBOutlet var mineSpace4_0: WKInterfaceButton!
    @IBOutlet var mineSpace4_1: WKInterfaceButton!
    @IBOutlet var mineSpace4_2: WKInterfaceButton!
    @IBOutlet var mineSpace4_3: WKInterfaceButton!
    @IBOutlet var mineSpace4_4: WKInterfaceButton!
  5. Next connect actions as follows
    • The reset button to an action named resetPressed
    • The flag button to an action named toggleFlag
  6. We will also need to connect each of the buttons to an action, however the code in the action will rely on another method not yet written, so we will return to that later.

Interface controller code

In order to keep track of both the minesweeper 'model' and the userinterface components that provide a visual representation of the minefield, we need to create some variables.

  1. Add the following code to do just that:
    var flag = false
    var possibleMines = Array<Array<WKInterfaceButton>>()
    var minefield = MineField(width: 5, height: 5)
    var flaggedCells = [WKInterfaceButton]()
    var clearedCells = [WKInterfaceButton]()
  2. This should allow us to implement the bodies of the methods to toggle the flagging mode and reset the game. Implement the toggleFlag method has shown here:
    @IBAction func toggleFlag()
    {
        if minefield.hasStarted
        {
            flag = !flag
            flagButton.setBackgroundColor(flag ? UIColor.redColor() : UIColor.clearColor())
        }
    }
  3. Implement the resetButton method as follows:
    @IBAction func resetPressed()
    {
        if flag {toggleFlag()}
        minefield = MineField(width: 5, height: 5)
        
        for row in possibleMines {
            for cell in row {
                cell.setTitle("")
            }
        }
        flaggedCells = [WKInterfaceButton]()
        clearedCells = [WKInterfaceButton]()
        mineCountLabel.setText("5 x")
    }
  4. Next add the following methods, each of which should be fairly self explanatory:
    func checkForCompletion(){
        if clearedCells.count + flaggedCells.count == 25 {
            mineCountLabel.setText("👍")
            WKInterfaceDevice().playHaptic(.Success)
        }
    }
     
    func showAllMines()
    {
        for minePoint in minefield.mineLocations()
        {
            possibleMines[minePoint.y][minePoint.x].setTitle("💣")
            mineCountLabel.setText("0 x")
        }
    }
     
     func deFlag(cell: WKInterfaceButton)
    {
        cell.setTitle("")
        flaggedCells.removeAtIndex(flaggedCells.indexOf(cell)!)
        updateMineCountLabel()
    }
     
    func flag(cell: WKInterfaceButton)
    {
        if !flaggedCells.contains(cell)
        {
            cell.setTitle("🇰🇵")
            flaggedCells.append(cell)
            updateMineCountLabel()
        }
    }
     
    func updateMineCountLabel()
    {
        let estimatedMinesLeft = minefield.mineCount - flaggedCells.count
        mineCountLabel.setText("\(estimatedMinesLeft) x")
    }
  5. Next add the following code to the willActivate method to add each of the buttons to a two dimensional array so we can keep track of them more easily:
    override func willActivate()
    {
        for _ in 0..<5
        {
            possibleMines.append([WKInterfaceButton]())
        }
    
        //add mines to array
        possibleMines[0].append(mineSpace0_0)
        possibleMines[0].append(mineSpace0_1)
        possibleMines[0].append(mineSpace0_2)
        possibleMines[0].append(mineSpace0_3)
        possibleMines[0].append(mineSpace0_4)
        
        possibleMines[1].append(mineSpace1_0)
        possibleMines[1].append(mineSpace1_1)
        possibleMines[1].append(mineSpace1_2)
        possibleMines[1].append(mineSpace1_3)
        possibleMines[1].append(mineSpace1_4)
        
        possibleMines[2].append(mineSpace2_0)
        possibleMines[2].append(mineSpace2_1)
        possibleMines[2].append(mineSpace2_2)
        possibleMines[2].append(mineSpace2_3)
        possibleMines[2].append(mineSpace2_4)
        
        possibleMines[3].append(mineSpace3_0)
        possibleMines[3].append(mineSpace3_1)
        possibleMines[3].append(mineSpace3_2)
        possibleMines[3].append(mineSpace3_3)
        possibleMines[3].append(mineSpace3_4)
        
        possibleMines[4].append(mineSpace4_0)
        possibleMines[4].append(mineSpace4_1)
        possibleMines[4].append(mineSpace4_2)
        possibleMines[4].append(mineSpace4_3)
        possibleMines[4].append(mineSpace4_4)
        
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
    }
    
  6. Next add a method which will check a given point and update it appropriately - making use of the methods we wrote above:
    func check(point: Point)
    {      
        if minefield.isExploded
        {
            return
        }
       
        let cell = possibleMines[point.y][point.x]
        
        if !flag && flaggedCells.contains(cell)
        {
            deFlag(cell: cell)
            return
        }
       
        if flag && !clearedCells.contains(cell)
        {
            flag(cell: cell)
            checkForCompletion()
            return
        }
       
        let status = minefield.checkMine(point)
        if status.isMine
        {
            showAllMines()
            WKInterfaceDevice().playHaptic(.Failure)
            return
        }
        
        cell.setTitle("\(status.adjacentMineCount)")
        clearedCells.append(cell)
        checkForCompletion()
    }
    
  7. The final stage is to create actions for each of the button, passing the relevant point to the checkPoint method, add the code and connect up the buttons as appropriate (this can be copy and pasted):
    @IBAction func checkMine0_0() { check(point:Point(x: 0, y: 0)) }
    @IBAction func checkMine0_1() { check(point:Point(x: 1, y: 0)) }
    @IBAction func checkMine0_2() { check(point:Point(x: 2, y: 0)) }
    @IBAction func checkMine0_3() { check(point:Point(x: 3, y: 0)) }
    @IBAction func checkMine0_4() { check(point:Point(x: 4, y: 0)) }
    
    @IBAction func checkMine1_0() { check(point:Point(x: 0, y: 1)) }
    @IBAction func checkMine1_1() { check(point:Point(x: 1, y: 1)) }
    @IBAction func checkMine1_2() { check(point:Point(x: 2, y: 1)) }
    @IBAction func checkMine1_3() { check(point:Point(x: 3, y: 1)) }
    @IBAction func checkMine1_4() { check(point:Point(x: 4, y: 1)) }
    
    @IBAction func checkMine2_0() { check(point:Point(x: 0, y: 2)) }
    @IBAction func checkMine2_1() { check(point:Point(x: 1, y: 2)) }
    @IBAction func checkMine2_2() { check(point:Point(x: 2, y: 2)) }
    @IBAction func checkMine2_3() { check(point:Point(x: 3, y: 2)) }
    @IBAction func checkMine2_4() { check(point:Point(x: 4, y: 2)) }
    
    @IBAction func checkMine3_0() { check(point:Point(x: 0, y: 3)) }
    @IBAction func checkMine3_1() { check(point:Point(x: 1, y: 3)) }
    @IBAction func checkMine3_2() { check(point:Point(x: 2, y: 3)) }
    @IBAction func checkMine3_3() { check(point:Point(x: 3, y: 3)) }
    @IBAction func checkMine3_4() { check(point:Point(x: 4, y: 3)) }
    
    @IBAction func checkMine4_0() { check(point:Point(x: 0, y: 4)) }
    @IBAction func checkMine4_1() { check(point:Point(x: 1, y: 4)) }
    @IBAction func checkMine4_2() { check(point:Point(x: 2, y: 4)) }
    @IBAction func checkMine4_3() { check(point:Point(x: 3, y: 4)) }
    @IBAction func checkMine4_4() { check(point:Point(x: 4, y: 4)) }
    
  8. In the scheme selector next to the run button, change the scheme to select the watchkit app, hit run and the game should launch in the watch simulator.

You can download a version of this project from GitHub

Additional Task

Add a timer that counts up - ensure that it is frozen upon completion (including failure) of the game