我猜我猜我猜猜猜之Bulls & Cows

[ iOS App Development]使用Swift進行數學運算

以前的小孩沒有什麼3C也沒有隨手可得的網路,只有象棋、猜數字和踩地雷這些簡單燒腦的遊戲,不僅能殺時間還可以拿來訓練邏輯。

遊戲規則如下:

  1. 答案為4位不重複0~9整數,但user猜測時可以重複數字。
  2. 一次遊玩可以猜10次,猜10次都未猜中遊戲自動結束。
  3. 每次猜測都會紀錄猜測紀錄(user猜測的數字和?A?B)供user邏輯推理。
  4. 猜中答案(4A0B)和用完次數都會遊戲結束,並且詢問user是否要重玩。

除了規則之外,這個練習我給自己額外的挑戰是將四個數字拆成四個TextField,類似一些帳號登入時OTP(One-Time Password)驗證的介面;輸入上也結合了信用卡號碼輸入的巧思,輸入一個數字後會自動判斷並跳到下一格,使用者不需要手動去點下一個TextField才能編輯。

介面規劃

介面規劃和各個outlet的佈局請參考下圖:

前置作業

・使用struct來建立猜測紀錄類別:因為要顯示10筆猜測紀錄,所以練習使用struct類別物件來儲存紀錄,並作為更新介面的資料來源。

struct GuessingRecord {
var numberGuessed = [String]()
var countOfBMatch = Int()
var countOfAMatch = Int()
}

每次猜測後都會生成一個GuessingRecord類別物件,並且被賦予三個屬性:

numberGuessed — user猜的四個數字。

countOfBMatch — 與答案比對後符合B的個數。

countOfAMatch — 與答案比對後符合A的個數。

・全域變數和outlet

var answer = [String]()
var userInputs = [String]()
var timesOfTry = 0
var isPlaying = true

@IBOutlet var inputTextFields: [UITextField]!

@IBOutlet weak var guessButton: UIButton!

@IBOutlet var recordLabels: [UILabel]!

@IBOutlet weak var resultMessageLabel: UILabel!
@IBOutlet weak var newGameButton: UIButton!

answer和userInputs都是字串的陣列,方便從TextField後直接比對;timesOfTry紀錄使用者猜的次數,isPlaying為user是否能繼續猜測,outlet可以參考前面的介面佈局。

・函式說明

generateAnswer():生成新答案的函式,遊戲每次開始都會呼叫。

check(userGuess: [String], answer: [String]) -> GuessingRecord:將user猜的數字和答案比對並產生出猜測紀錄的函式。

setGradientBackground():加上漸層背景,另外推薦給大家一個可以無腦挑出漸層色彩的配色網站。

setTextFieldBorderBottom(_ textField: UITextField):為指定的TextField加上底線,這是自己從其他地方找到一個不錯又不難理解的practice。

關鍵是將border的範圍全部壓在TextField的底部,所以座標y起點是整的TextField高度減去border width,然後整個layer的寬度會等於TextField的寬度,高度則是等於border width。

showResult(trial: Int, input: String, aMatch: Int, bMatch: Int ):將儲存的猜測紀錄顯示出來,參數包含trial猜測次數、input使用者猜測的所有數字、aMatch符合A的個數、bMatch符合B的個數。這邊希望做出來的text中”?A?B是紅色的,所以使用了NSMutableAttributedString來製作。

@objc func closeNumberPad():收鍵盤的函式,用在點選非view中TextField的區塊時會結束編輯,會在後面的viewDidLoad中新增的UITapGestureRecognizer用到。

viewDidLoad

viewDidLoad初始設定:

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
//設定漸層背景
setGradientBackground()
//生成答案
generateAnswer()
//將所有text設底線
for textField in inputTextFields {
setTextFieldBorderBottom(textField)
}
//新增tapRecognizer到view上
let tapRecognizer = UITapGestureRecognizer()
tapRecognizer.addTarget(self, action: #selector(self.closeNumberPad))
self.view.addGestureRecognizer(tapRecognizer)
}

IBAction說明

・inputNumberTextField
因為數字有四位,故TextField也有四個,為了讓輸入時比較友善,增加以下功能:

  1. 1個TextField只能輸入一個數字,輸入完會自動跳到下一格編輯(如果有),且鍵盤不會收起來。
  2. 刪除某一格TextField的數字時會自動跳回前一格(如果有),且鍵盤不會收起來。
  3. 可以點選特定一個TextField跳著編輯。

可以先看一下demo畫面就大概知道道上述具體是什麼意思,過程中都只依賴鍵盤輸入來移動游標不用手指去逐一點選TextField。

4個TextField都是連到相同的IBAction程式碼:

分別介紹一下有用到的兩個方法:一個是字串的.suffix(n),會將字串從最後數過來n個字元擷取出來;另外是TextField的becomeFirstResponder()會使游標跳到指定的TextField上開始編輯。

・submitNumbersButton

這個Action的主要功能就是user按了”Guess”按鈕後就會收集並整理user的答案,並觸發比對函式進而更新此次猜測紀錄到到介面上。程式碼大綱為:

  1. 更新userInputs。
  2. 與答案比對生成GuessingRecord類別物件。
  3. 將GuessingRecord的資料顯示到介面上。
  4. 根據猜測次數判斷遊戲是否結束並更新介面。

・anotherGameButton

如果user猜中答案或者用完猜測次數就會遊戲結束,並顯示”Try again“的按鈕,按下後可以再玩一次。

DEMO

・猜對操作

・用完猜測次數操作

Github

--

--