(Swift) E卡遊戲:給開司一罐啤酒

E 卡遊戲是一種出現在賭博默示錄中的卡牌遊戲。故事中的主角開司因為欠債,被迫登上了希望號遊輪,在那裡他遇到了一位老頭,並與他成為了夥伴,但老頭在遊戲結束時,剩下一張卡牌,開司選擇了與他分擔欠債,結果兩人都被送到了地下勞動。在那裡勞作每個月都能拿到一些薪水,但是班長會用高價的食物和飲料誘惑他們,讓他們把錢都花光。開司一開始還能抵抗,但是班長給了他一罐啤酒,從此他就墮落了。但開司不想在地下等死,後來聽說了『勇士之路』能夠離開地下,隨後他和幾個夥伴一同前往挑戰。沒想到『勇士之路』竟然是在兩棟高樓之間的一座鐵橋,只有鞋子寬,十幾個人中只有開司成功過關,到了對面的大樓後,開司對於這些人的惡行感到憤怒,決定挑戰帝愛集團的幹部利根川,玩了 E 卡遊戲,開司選擇了賠率是 10:1 的奴隸方,最後利用了利根川的猜疑心理,打敗了他,並贏得了巨額的獎金。

E 卡遊戲其實非常簡單,規則如下:

皇帝克制平民

平民克制奴隸

奴隸克制皇帝

Xcode

新增一個 Swift File,設計 Model

import Foundation

class EGame {

// 初始化角色
var character: Role
// 初始化 我的卡牌
var myCards: [Role] = []
// 初始化 電腦卡牌
var aiCards: [Role] = []
// 當前遊戲狀態
var state: String?

// 初始遊戲時選擇丟入的角色
init(character: Role) {
self.character = character
// 玩家選擇國王 卡牌
if character == .king {
myCards = [.civilian, .civilian, .king, .civilian, .civilian]
aiCards = [.civilian, .civilian, .slave, .civilian, .civilian]
// 玩家選擇奴隸 卡牌
}else {
myCards = [.civilian, .civilian, .slave, .civilian, .civilian]
aiCards = [.civilian, .civilian, .king, .civilian, .civilian]
}
}

// 遊戲邏輯- 選擇出牌
// 回傳電腦 出的卡牌
func selectCardToPlay(Card: Role) -> Role {
// 電腦隨機出一張卡牌
let random = Int.random(in: 0..<aiCards.count)
// 更新遊戲狀態
state = gameOver(player: Card, ai: aiCards[random])
return aiCards.remove(at: random)
}

// 判斷輸贏
func gameOver(player: Role, ai: Role) -> String {
// 邏輯判斷
if player == .king && ai == .civilian {
return "Victory"
}else if player == .slave && ai == .king {
return "Victory"
}else if player == .civilian && ai == .slave {
return "Victory"
}else if player == .civilian && ai == .civilian {
return "Tie"
}else if player == .civilian && ai == .king {
return "Loss"
}else if player == .king && ai == .slave {
return "Loss"
}else if player == .slave && ai == .civilian {
return "Loss"
}else {
return "Unknown result"
}
}

}

// 所有卡牌的腳色
enum Role: String {
case king
case civilian
case slave
}

首先呢可以先使用 enum 列舉出 我們的所有卡牌角色,這樣在丟入卡牌的時候會非常方便。

接下來是遊戲開始時,你會選擇一個角色,皇帝或是奴隸方,也就是 character,選擇角色後就要配置給你五張手牌,myCards 是玩家的手牌,aiCards 是電腦的手牌。

再來是遊戲開始的方始,我們選擇出牌,電腦也必須出一張卡牌,這邊使用到了 selectCardToPlay 來撰寫,最後是遊戲的勝負判斷,使用了 gameOver 來判斷,在裡面同時丟入玩家出的卡牌與電腦的卡牌,來更新 state

ViewController

從 Xcode 截圖出來的圖片有點不清楚,但是我背景採用的顏色與卡片的顏色略有不同,我用來製作卡牌選用的是 View,View 使用到了 UIButton 來顯示背景以及處理點選時的動畫。

先拉畫面中的元件 @IBOutlet,並且新增 model,這裡比比較特別的是,也要拉兩張卡牌上方的 AutoLayout -> slaveTopConstraint kingTopConstraint

import UIKit

class ViewController: UIViewController {

@IBOutlet weak var slaveView: UIView!
@IBOutlet weak var kingView: UIView!

@IBOutlet weak var slaveTopConstraint: NSLayoutConstraint!
@IBOutlet weak var kingTopConstraint: NSLayoutConstraint!

@IBOutlet weak var characterLabel: UILabel!

var model: EGame!

設定 Button 的圖片,由於圖片過大,必須使用 background image 來設定,並搭配 Content Mode 來設定,可以參考下方的文章參考

新增 function viewStyle 用於顯示卡牌的邊框以及顏色

    // view 的外觀 
func viewStyle(view: UIView, color: UIColor) {
view.layer.borderWidth = 10
view.layer.borderColor = color.cgColor
}

再來是在 viewDidLoad 完成載入及初始化角色

    override func viewDidLoad() {
super.viewDidLoad()

// 設定 View 外觀
// 被選取的角色 邊框為 黃色
viewStyle(view: slaveView, color: .yellow)
viewStyle(view: kingView, color: .white)

// 初始化角色 奴隸
model = EGame(character: .slave)

}

新增點選 角色時產生的動畫 function addAnimation

//加入動畫
func addAnimation(_ cardView: NSLayoutConstraint) {
let animator = UIViewPropertyAnimator(duration: 2.5, dampingRatio: 0.1) {
cardView.constant = 150
self.view.layoutIfNeeded()
}
animator.startAnimation()
UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 0.1, delay: 0) {
cardView.constant = 200
self.view.layoutIfNeeded()
}
}

這邊採用的是兩段動畫,產生的效果如下

在拉兩張卡牌的 @IBAction 前,必須設定一下兩個 Button 的 tag,選取 Button 後,右邊的畫面滑鼠向下,滑動找到 View 設定 tag = 1 與 tag = 2

    // 選擇角色
@IBAction func selectRole(_ sender: UIButton) {
// 使用 button 的 tag 來進行判斷
if sender.tag == 1 {
viewStyle(view: slaveView, color: .yellow)
viewStyle(view: kingView, color: .white)
model = EGame(character: .slave)
characterLabel.text = "Slave"
// 加入動畫
addAnimation(slaveTopConstraint)
}else {
viewStyle(view: kingView, color: .yellow)
viewStyle(view: slaveView, color: .white)
model = EGame(character: .king)
characterLabel.text = "King"
// 加入動畫
addAnimation(kingTopConstraint)
}
}

上面的程式就蠻簡單的,就是剛剛我們有用過的 viewStyle 設定View 外觀以及 addAnimation 新增動畫

GameViewController

新增一個 GameViewController,這時需要把 ViewController 的 model 傳送到這一頁

class GameViewController: UIViewController {


var model: EGame!


init?(coder: NSCoder, model: EGame) {
self.model = model
super.init(coder: coder)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

回到 ViewController,拉 @IBSegueAction

    @IBSegueAction func Start(_ coder: NSCoder) -> GameViewController? {
let controller = GameViewController(coder: coder, model: model)
return controller
}

在 GameViewController 畫面中新增 StackView 將五個 UIButton 包住,上方的是 aiCardsButton,下方的則是 myCardsButton,這邊拉的 Button 是 Outlet Collection,底部的卡牌則是包在每個 View 裡面

    @IBOutlet var myCardsButton: [UIButton]!
@IBOutlet var aiCardsButton: [UIButton]!
@IBOutlet var cardsView: [UIView]!

對上下的 StackView 做完 AutoLayout 之後,也要拉 @IBOutlet,這是為了出牌後,能夠保持每張卡牌的寬度,並新增 viewSplitIntoFiveEqualWidths

    // 保存 每張卡片的寬度
var viewSplitIntoFiveEqualWidths: CGFloat = 0.0

// stackview 左右的 constraints
@IBOutlet weak var myCardsTrailing: NSLayoutConstraint!
@IBOutlet weak var myCardsLeading: NSLayoutConstraint!

@IBOutlet weak var aiCardsTrailing: NSLayoutConstraint!
@IBOutlet weak var aiCardsLeading: NSLayoutConstraint!Y94

在 viewDidLoad 抓取畫面的寬度(除上 5 即為每張卡的寬度)

      override func viewDidLoad() {
super.viewDidLoad()

// 保存卡片的寬度
viewSplitIntoFiveEqualWidths = view.frame.width / 5


}

新增 viewStyle 來設置 view 的寬度以及邊框顏色

    // view 的外觀
func viewStyle(view: UIView) {
view.layer.cornerRadius = 10
view.layer.borderWidth = 5
view.layer.borderColor = UIColor.white.cgColor
}

新增 setupFrontButtonConfiguration (卡牌正面)來設置 Button 的圖片以及卡牌角色

    // button 卡片正面的 Configuration
func setupFrontButtonConfiguration(_ button: UIButton, role: Role) {
var configuration = UIButton.Configuration.plain()
configuration.background.image = UIImage(named: role.rawValue)
configuration.background.imageContentMode = .scaleAspectFit
button.configuration = configuration
}

新增 setupBackButtonConfiguration (卡牌背面)來設置卡片背面的樣式

    // button 卡片背面的 Configuration
func setupBackButtonConfiguration(_ button: UIButton) {
// 設定 button 的 Configuration
var config = UIButton.Configuration.plain()
var title = AttributedString("E")
title.font = UIFont(name: "Party LET", size: 100)
config.attributedTitle = title
button.configuration = config
button.tintColor = .white
// 調整內邊距
button.configuration?.contentInsets = NSDirectionalEdgeInsets(top: 20, leading: 0, bottom: 0, trailing: 0)

button.backgroundColor = .black
button.layer.cornerRadius = 15
button.layer.borderColor = UIColor.white.cgColor
button.layer.borderWidth = 5
}

回到 viewDidLoad 新增以下的程式碼

      override func viewDidLoad() {
super.viewDidLoad()

// 保存卡片的寬度
viewSplitIntoFiveEqualWidths = view.frame.width / 5

// 將我方手牌給予 cardsButton
for (index, cards) in model.myCards.enumerated() {
setupFrontButtonConfiguration(myCardsButton[index], role: cards)
viewStyle(view: cardsView[index])
}

// 設定 button 背面的 Configuration
for card in aiCardsButton {
setupBackButtonConfiguration(card)
}
}

接下來拉 myCardsButton 的 @IBAction

    @IBAction func playCard(_ sender: UIButton) {
// 抓取點選 button 的 id
if let index = myCardsButton.firstIndex(of: sender) {
// 將 點選的 button 設為 enable
myCardsButton[index].isEnabled = false
myCardsButton[index].isHidden = true
cardsView[index].isHidden = true
aiCardsButton[index].isHidden = true


// 更新 myCards 的距離
updateStackViewConstraints()

// 關閉所有 cardsButton
for card in myCardsButton {
card.isEnabled = false
}

// 出牌的動畫
generatePlayingCard(myrole: model.myCards[index], airole: model.selectCardToPlay(Card: model.myCards[index]))
}

}

我們需要的功能是 點選一張卡牌後,我方手牌以及電腦手牌要扣一張,並在雙方的畫面上各自產生一張卡牌,並且移動到上方,並開牌確認勝負

我方手牌以及電腦手牌要扣一張

這邊會透過 .firstIndex 來抓取你點到的是哪一個 Button

        // 抓取點選 button 的 id
if let index = myCardsButton.firstIndex(of: sender) {
// 將 點選的 button 設為 enable
myCardsButton[index].isEnabled = false
myCardsButton[index].isHidden = true
cardsView[index].isHidden = true
aiCardsButton[index].isHidden = true

在出牌的時候,也要避免你點選其他卡牌,所以要先將其它卡牌的 isEnabled 設為 false

            // 關閉所有 cardsButton
for card in myCardsButton {
card.isEnabled = false
}

Stackview 減少一張卡牌後,要維持卡牌的大小必須扣除每張卡牌的寬度

    // 更新 myCards 的距離
func updateStackViewConstraints() {
myCardsTrailing.constant += viewSplitIntoFiveEqualWidths / 2
myCardsLeading.constant += viewSplitIntoFiveEqualWidths / 2

aiCardsTrailing.constant += viewSplitIntoFiveEqualWidths / 2
aiCardsLeading.constant += viewSplitIntoFiveEqualWidths / 2
}

製作出牌的動畫 generatePlayingCard,大致就是在畫面中,產生兩張卡牌(背面),並開始往中間移動,最後進行翻牌的動作,最後確認遊戲狀態

func generatePlayingCard(myrole: Role, airole: Role) {
// 在畫面中生成一個 Button
let mybutton = UIButton()
let aibutton = UIButton()

// 初始卡牌的 constraint
var mybuttonConstraint = mybutton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -80)
var aibuttonConstraint = aibutton.topAnchor.constraint(equalTo: view.topAnchor, constant: 30)

// 設定 button 背面的 Configuration
setupBackButtonConfiguration(mybutton)
setupBackButtonConfiguration(aibutton)


mybutton.translatesAutoresizingMaskIntoConstraints = false
aibutton.translatesAutoresizingMaskIntoConstraints = false


view.addSubview(mybutton)
view.addSubview(aibutton)


// 設定約束
let myConstraints = [
mybutton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
mybuttonConstraint,
mybutton.widthAnchor.constraint(equalToConstant: viewSplitIntoFiveEqualWidths),
mybutton.heightAnchor.constraint(equalToConstant: 125)
]

let aiConstraints = [
aibutton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
aibuttonConstraint,
aibutton.widthAnchor.constraint(equalToConstant: viewSplitIntoFiveEqualWidths),
aibutton.heightAnchor.constraint(equalToConstant: 125)
]


// 啟用約束
NSLayoutConstraint.activate(myConstraints)
NSLayoutConstraint.activate(aiConstraints)

// 延遲 0.5 秒後執行動畫
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
// 添加動畫
let animator = UIViewPropertyAnimator(duration: 1, dampingRatio: 1) {
mybuttonConstraint.isActive = false
mybuttonConstraint = mybutton.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -250)
mybuttonConstraint.isActive = true

aibuttonConstraint.isActive = false
aibuttonConstraint = aibutton.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 230)
aibuttonConstraint.isActive = true

self.view.layoutIfNeeded()
}


animator.addCompletion { _ in
UIView.transition(with: mybutton, duration: 0.3, options: .transitionFlipFromLeft, animations: {
// 翻面後的樣式
self.setupFrontButtonConfiguration(mybutton, role: myrole)
}, completion: nil)

UIView.transition(with: aibutton, duration: 0.3, options: .transitionFlipFromRight, animations: {
// 翻面後的樣式
self.setupFrontButtonConfiguration(aibutton, role: airole)
}) { _ in
// 延遲 0.5秒後移除視圖
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
mybutton.removeFromSuperview()
aibutton.removeFromSuperview()
// 重置遊戲
self.checkVictory(state: self.model.state!)
// 開啟所有 cardsButton
for card in self.myCardsButton {
card.isEnabled = true
}
}
}
}
animator.startAnimation()
}

}

新增確認遊戲勝負的 function checkVictory,主要就是丟入 model 中的 state 就可以知道目前遊戲的狀態

    // 確認是否勝利
func checkVictory(state: String) {
switch state {
case "Victory":
print("You Win!!!!")
alertAction(text: "You Win!!!!")
// 初始化遊戲
InitializeGame(role: model.character)

case "Loss":
print("Game Over")
alertAction(text: "You Loss")
// 初始化遊戲
InitializeGame(role: model.character)

default :
print("")
}
}

製作遊戲結束的警示窗 alertAction

    func alertAction(text: String) {
let controller = UIAlertController(title: "Game Over", message: text, preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
controller.addAction(okAction)
okAction.setValue(UIColor(red: 91/255, green: 154/255, blue: 139/255, alpha: 1), forKey: "titleTextColor")
present(controller, animated: true)
}

重新初始化遊戲 InitializeGame

    // 初始化 遊戲
func InitializeGame(role: Role) {
// 初始化 button view 狀態
InitializeButton(myCardsButton)
InitializeButton(aiCardsButton)
InitializeView(cardsView)
// 重新初始化 model
model = EGame(character: role)
// 將 constraint 歸 0
myCardsTrailing.constant = 0
myCardsLeading.constant = 0

aiCardsTrailing.constant = 0
aiCardsLeading.constant = 0

}

// 初始化 View 狀態
func InitializeView(_ view: [UIView]) {
for view in view {
view.isHidden = false
}
}

// 初始化 Button 狀態
func InitializeButton(_ button: [UIButton]) {
for button in button {
button.isEnabled = true
button.isHidden = false
}
}

RuleViewController

在規則的頁面,我只是簡單新增 6 個 UIButton ,並新增一些動畫而已,有就是最初的卡牌遊戲規則,這個畫面的功能就是左邊的卡牌會移動到右邊的卡牌旁邊後,右邊的卡牌會隨之消失

程式碼如下

import UIKit

class RuleViewController: UIViewController {


@IBOutlet weak var kingCardConstraint: NSLayoutConstraint!
@IBOutlet weak var civilianCardConstraint: NSLayoutConstraint!
@IBOutlet weak var slaveCardConstraint: NSLayoutConstraint!

@IBOutlet var disappearCards: [UIButton]!


override func viewDidLoad() {
super.viewDidLoad()

// 延遲 0.5 秒後執行動畫
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
// 添加動畫
UIView.animate(withDuration: 1, animations: {
// 將 leftCardConstraint 的值變為 0
self.kingCardConstraint.constant = 0
self.civilianCardConstraint.constant = 0
self.slaveCardConstraint.constant = 0

self.view.layoutIfNeeded()
}) { _ in
// 延遲 0.5 秒後執行動畫
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
// 添加動畫
UIView.animate(withDuration: 1, animations: {
// 將 disappearCard 慢慢消失
self.disappearCards[0].alpha = 0
self.disappearCards[1].alpha = 0
self.disappearCards[2].alpha = 0
}) { _ in
// 延遲 0.5 秒後執行動畫
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
// 添加動畫
UIView.animate(withDuration: 1, animations: {
// 將 disappearCard 慢慢出現
self.disappearCards[0].alpha = 1
self.disappearCards[1].alpha = 1
self.disappearCards[2].alpha = 1
})
}
}
}
}
}

}
}

最終的成果

--

--