#2, iOS 撲克牌型試做小遊戲:High Card

Syuan
彼得潘的 Swift iOS / Flutter App 開發教室

--

其實是把專題之一剪一部份出來當作業紀錄,因為發現即使換成不同的撲克遊戲也還是需要單純的牌型比對,所以乾脆寫一個通用的邏輯比較方便。混了很多不同堂課的東西所以也不知道算哪份…就這樣吧。

撲克牌的構成是花色與數字,

我需要 (花色 + 數字)的輸出名稱給圖檔抓,而且必須能兩張牌做大小比較。

例如黑桃Ace是 Spades1,而且必須比黑桃K大。

  1. 先做數字,使用 enum 把rawValue給予Int,

然後用 CaseIterable陣列化,等等做牌堆要用

這裡把ace擺在最後,相當於 Rank 14 為最大

enum Rank: Int, CaseIterable {case two = 2case three, four, five, six, seven, eight, night, tencase jack, queen, king, ace}

接著我們要讓他可以比較,用comparable。

當左邊牌小於右邊牌時,左右相等不成立,Ace最大在左邊牌時也不成立。

extension Rank: Comparable {public static func <(lhs: Rank, rhs: Rank) -> Bool {switch (lhs, rhs) {case (_, _) where lhs == rhs:return falsecase (.ace, _):return falsedefault:return lhs.rawValue < rhs.rawValue}}}

然後因為數字用的是英文,但我的檔名是用數字命名

所以需要用CustomStringConvertible處理成我想要的名稱

extension Rank: CustomStringConvertible {public var description: String {switch self {case .ace: return "1"case .jack: return "11"case .queen: return "12"case .king: return "13"default: return "\(rawValue)"}}}

2.花色處理,原理大致相似

enum Suit: String, CaseIterable {case clubs, diamonds, hearts, spades}

接著比較花色,我需要梅花<方塊<紅心<黑桃

所以不符合左小於右的情況有:

左右相等時、左為黑桃時、左紅心右梅花時,左方塊右梅花時。

extension Suit: Comparable {public static func < (lhs: Suit, rhs: Suit) -> Bool {switch (lhs, rhs) {case (_, _) where lhs == rhs:return falsecase (.spades, _), (.hearts, .clubs), (.diamonds, .clubs):return falsedefault:return true}}}

然後花色也需要輸出名稱修正

extension Suit: CustomStringConvertible {public var description: String {switch self {case .spades: return "spades"case .hearts: return "hearts"case .diamonds: return "diamonds"case .clubs: return "clubs"}}}

3.牌面比較邏輯,可以來構成牌面了。

struct Card: Equatable {let suit: Suitlet rank: Rankinit(suit: Suit, rank: Rank) {self.suit = suitself.rank = rank}}

接著一樣需要比較牌面,這裡我在那邊想半天結果人家兩行 Ternary 寫完,氣死…

兩種情況:

a.當左邊牌小於右邊牌時,左邊數字是不是等於右邊數字?是的話左邊花色小於右邊花色:否則左邊數字小於右邊數字

b.當左邊牌大於右邊牌時,左邊數字是不是等於右邊數字?是的話左邊花色大於右邊花色:否則左邊數字大於右邊數字

extension Card: Comparable {static func < (lhs: Card, rhs: Card) -> Bool {return lhs.rank == rhs.rank ? lhs.suit < rhs.suit : lhs.rank < rhs.rank}static func > (lhs: Card, rhs: Card) -> Bool {return lhs.rank == rhs.rank ? lhs.suit > rhs.suit : lhs.rank > rhs.rank}}

現在可以把卡面輸出成我要的圖檔名稱了

extension Card: CustomStringConvertible {var description: String {return "\(suit)\(rank)"}}

4.牌堆處理

前面用過CaseIterable,所以這邊 loop 可以用 allCases

這邊還做了洗牌和抽牌用的 function, 抽牌時使用 for in 每次抽第一張

因為會修改 struct 裡面的值,都要所以要加上mutating

struct Deck {private(set) var cards: [Card] = []init() {for suit in Suit.allCases {for rank in Rank.allCases {let card = Card(suit: suit, rank: rank)cards.append(card)}}}//洗牌用mutating func shuffle() {cards.shuffle()}//抽牌用mutating func draw(count:Int) -> [Card]? {//抽牌條件:牌堆不是空的,且剩下牌堆牌數足夠抽牌guard !cards.isEmpty && 1...cards.count ~= count else {return nil}var drawCards: [Card] = []for _ in 0..<count {drawCards.append(cards.removeFirst())}return drawCards}}

以上牌型邏輯完成,可以整理分離在 Rank, Suit, Card 三個檔案。

現在已經可以比較任意兩張牌了,或是套到其他撲克邏輯。

接著來寫個 High Card 簡單比個大小試試。

5.遊戲佈局

卡牌與結果顯示的 outlet 一樣先拉好

接著宣告出牌堆,以及勝利次數

@IBOutlet weak var resultLabel: UILabel!@IBOutlet var cardImageView: [UIImageView]!var deck = Deck()var winCount = 0

做一個 play 按鈕開始遊戲,裡面放了一個 alert 當作重新洗牌

@IBAction func playGame(_ sender: Any) {//牌堆洗牌deck.shuffle()//電腦從牌堆抽牌let computersCard = deck.draw(count: 1)!cardImageView[0].image = UIImage(named: "\(computersCard[0])")//玩家從牌堆抽牌let playersCard = deck.draw(count: 1)!cardImageView[1].image = UIImage(named: "\(playersCard[0])")print("\(computersCard[0]), \(playersCard[0])")//比大小if computersCard[0] < playersCard[0] {resultLabel.text = "You Win"winCount += 1} else {resultLabel.text = "You Lose"}if deck.cards.isEmpty == true {let controller = UIAlertController(title: "牌抽光了,你贏了\(winCount)/26次", message: "按確定重新開始", preferredStyle: .alert)let okAction = UIAlertAction(title: "確定", style: .default) { _ inself.deck = Deck()self.winCount = 0}controller.addAction(okAction)present(controller, animated: true, completion: nil)}}}

以上完成。

--

--