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.
Create a new Swift project using the Watch App template, name it 'MineSweeper", set the User Interface option to Storyboard and uncheck all the 'Include' options at the bottom of the screen
Minesweeper relies on any given point (or cell) on the grid being able to know about its neighbours.
struct Point : Equatable, Hashable {
let x: Int
let y: Int
init(x: Int, y: Int){
self.x = x
self.y = y
}
}
This creates a structure to represent a point and implementing Hashable (for a struct) also allows two points to be compared for equality with no extra work
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 Cell {
var isMine = false
var adjacentMineCount = 0
}
class MineField {
private(set) var hasStarted = false
private(set) var isExploded = false
let mineCount : Int
var field = Array<Array<Cell>>()
let width: Int
let height: Int
init(width: Int, height: Int){
self.width = width
self.height = height
for _ in 0..<height {
//create row
var row = [Cell]()
for _ in 0..<width {
row.append(Cell())
}
field.append(row)
}
}
func checkMine(_ point: Point) -> Cell{
if !hasStarted{
hasStarted = true
determineMinesWithStart(point: point)
}
let location = field[point.y][point.x]
if location.isMine {
isExploded = true
}
var cell = Cell()
cell.adjacentMineCount = location.adjacentMineCount
cell.isMine = location.isMine
return cell
}
private func determineMinesWithStart(point: Point){
populateMines(mineCount, avoiding: point)
populateAdjacentMineCount()
}
private func populateMines(_ count: Int, avoiding point: Point){
var mineLocations = Set<Point>()
repeat {
let randPoint = randomPoint()
if point != randPoint && !mineLocations.contains(randPoint){
mineLocations.insert(randPoint)
}
} while mineLocations.count < count
for point in mineLocations {
field[point.y][point.x].isMine = true
}
}
func mineLocations() -> Set<Point> {
var mines = Set<Point>()
for y in 0..<field.count {
for x in 0..<field[y].count{
if field[y][x].isMine {
mines.insert(Point(x: x, y: y))
}
}
}
return mines
}
private func neighboursFor(_ point: Point) -> Set {
var neighbours = Set<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.insert(Point(x: x, y: y))
}
}
}
return neighbours
}
private func adjacementMineCount(for point: Point) -> Int{
var count = 0
for point in neighboursFor(point) {
if field[point.y][point.x].isMine{
count += 1
}
}
return count
}
private func populateAdjacentMineCount(){
for y in 0..<field.count {
for x in 0..<field[y].count{
field[y][x].adjacentMineCount = adjacementMineCount(for: Point(x: x, y: y))
}
}
}
private func randomPoint() -> Point{
return Point(x: Int.random(in: 0..<width), y: Int.random(in: 0..<height))
}
}
One of the restrictions 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.
flagButton
mineCountLabel
@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!
resetPressed
toggleFlaggingState
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.
private var flagging = false
private var mineButtons : Array<Array<WKInterfaceButton>>()
private var minefield : MineField!
private var flaggedCells : Set<WKInterfaceButton>!
private var clearedCells : Set<WKInterfaceButton>!
toggleFlaggingState
method has shown here: @IBAction func toggleFlaggingState()
{
if minefield.hasStarted
{
flagging.toggle()
flagButton.setBackgroundColor(flagging ? .red : .clear)
}
}
func setup(){
minefield = MineField(width: 5, height: 5)
flaggedCells = Set()
clearedCells = Set()
}
resetPressed
method as follows: @IBAction func resetPressed()
{
if flagging {toggleFlaggingState()}
setup()
for row in mineButtons {
for cell in row {
cell.setTitle("")
}
}
updateMineCountLabel()
}
func checkForCompletion(){
if clearedCells.count + min(flaggedCells.count,minefield.mineCount) == 25 {
mineCountLabel.setText("👍")
WKInterfaceDevice().play(.success)
}
}
func showAllMines()
{
for minePoint in minefield.mineLocations()
{
mineButtons[minePoint.y][minePoint.x].setTitle("💣")
mineCountLabel.setText("0 x")
}
}
func deFlag(_ cell: WKInterfaceButton)
{
cell.setTitle("")
flaggedCells.remove(at: flaggedCells.firstIndex(of: cell)!)
updateMineCountLabel()
}
func flag(_ cell: WKInterfaceButton)
{
if !flaggedCells.contains(cell)
{
cell.setTitle("🇰🇵")
flaggedCells.insert(cell)
updateMineCountLabel()
}
}
func updateMineCountLabel()
{
let minesLeft = minefield.mineCount - flaggedCells.count
mineCountLabel.setText("\(minesLeft) x")
}
override func awake(withContext context: Any?) {
setup()
mineButtons = Array<Array<WKInterfaceButton>>()
for _ in 0..<5
{
mineButtons.append([WKInterfaceButton]())
}
//add mines to array
mineButtons[0].append(mineSpace0_0)
mineButtons[0].append(mineSpace0_1)
mineButtons[0].append(mineSpace0_2)
mineButtons[0].append(mineSpace0_3)
mineButtons[0].append(mineSpace0_4)
mineButtons[1].append(mineSpace1_0)
mineButtons[1].append(mineSpace1_1)
mineButtons[1].append(mineSpace1_2)
mineButtons[1].append(mineSpace1_3)
mineButtons[1].append(mineSpace1_4)
mineButtons[2].append(mineSpace2_0)
mineButtons[2].append(mineSpace2_1)
mineButtons[2].append(mineSpace2_2)
mineButtons[2].append(mineSpace2_3)
mineButtons[2].append(mineSpace2_4)
mineButtons[3].append(mineSpace3_0)
mineButtons[3].append(mineSpace3_1)
mineButtons[3].append(mineSpace3_2)
mineButtons[3].append(mineSpace3_3)
mineButtons[3].append(mineSpace3_4)
mineButtons[4].append(mineSpace4_0)
mineButtons[4].append(mineSpace4_1)
mineButtons[4].append(mineSpace4_2)
mineButtons[4].append(mineSpace4_3)
mineButtons[4].append(mineSpace4_4)
}
func check(_ point: Point)
{
if minefield.isExploded
{
return
}
let cell = mineButtons[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().play(.failure)
return
}
cell.setTitle("\(status.adjacentMineCount)")
clearedCells.insert(cell)
checkForCompletion()
}
@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)) }
You can download a version of this project from GitHub
Add a timer that counts up - ensure that it is frozen upon completion (including failure) of the game
Create a new WatchKit app that allows the user to play Noughts and Crosses on the watch. Start by designing it for two human players, then improve by building an AI to play against a single player.