こんないい記事をみたので、Swiftでブラックジャックを実装してみよう。
実装するルールは上記サイトを少し改変する。
- 初期カードは52枚。1ゲーム中にカードの重複は起きない。
- ディーラーと任意の数のプレイヤーの対戦。ディーラーは自動、プレイヤーは実行者。
- 実行開始時、プレイヤーとディーラーはそれぞれ、カードを2枚引く。引いたカードは画面に表示する。ただし、ディーラーの2枚目のカードは分からないようにする
- その後、先にプレイヤーがカードを引く。プレイヤーが21を超えていたらバーストしてプレイヤーの負け、その時点でゲーム終了
- プレイヤーは、カードを引くたびに、次のカードを引くか選択できる
- プレイヤーが引き終えたら、その後ディーラーは、自分の手札が17以上になるまで引き続ける
- プレイヤーとディーラーが引き終えたら勝負。より21に近い方の勝ち
- JとQとKは10として扱う
- Aはとりあえず「1」としてだけ扱う。「11」にはしない
- ダブルダウンなし、スプリットなし、サレンダーなし、その他特殊そうなルールなし
成果物
依存関係
View → Presenter → Game → Blackjack →Card
それぞれの責務
概念モジュール(SwiftyBlackjack)
Card
- 表面を向いているかどうか
- ランク
- スート
Blackjack
- 山札
- ディーラー
- プレイヤー → 勝負の結果(Blackjackは複数人で遊べるが、その勝負はディーラー対各プレイヤーで、プレイヤー同士は干渉しない)
- 山札からドローし手札に加えるといったBlackjackの本質的な操作
実装モジュール
Game
進行状況として
- 開始前
- プレイ中 → ゲームの進行状況
- 勝敗 → 勝負の結果
- 実際に進行する
GamePresenter
- Gameの保持しViewと仲介
まずは本質から作成する。
アプリに依存しない最も本質の論理を切り離すためにSwiftyBlackjackという別モジュールとして作成。今回の場合、抽象度を高めるためにSwiftyBlackjackにおいてSwift標準ライブラリ以外のライブラリを使用していない。つまりimport...をしていない
今回の設計の大部分は下記を参考にしている。
詳しくはhttps://www.youtube.com/watch?app=desktop&v=uTaFfUHUmvY
まず、ゲームルール以前にトランプカードを作成しよう。
Card.swift
public struct Card{
public let face:Face
public var isFaceUp:Bool
...
}
extension Card{
public mutating func putFaceUp(){ self = faceUp }
public var faceUp: Card{ .init(face: face, isFaceUp: true) }
...
}
...
extension Card{
public struct Face{
public let rank:Rank
public let suit:Suit
...
}
public enum Rank:String,CaseIterable{
case ace = "A"
case two = "2"
case three = "3"
...
}
public enum Suit:String, CaseIterable{
case heart = "❤︎"
case clover = "♣︎"
case diamond = "◆"
case spade = "♠︎"
}
}
次はblackjackの本質的なオブジェクトやゲーム操作を定義
オブジェクト
- talon(山札)
- Dealer
- Player
- point(点数)
- GameResult
操作
- dealCard
- dealCardToDealer
- flipDealersSecondCard
など
Blackjack.swift
public struct Blackjack {
public private(set) var talon:[Card] //山札
public private(set) var dealer: Dealer
public private(set) var players: [Player]
...
}
//MARK: input interface
extension Blackjack{
mutating public func dealCard(toPlayerIndexOf index: Int){...}
mutating public func dealCardToDealer(isFaceUp: Bool = true){...}
mutating public func flipDealersSecondCard(){...}
mutating private func drawFromTalon(isFaceUp: Bool = true)->Card{...}
}
//MARK: Definition of Types. Dealer/Player/GameResult
extension Blackjack{
public struct Dealer{...}
public struct Player{...}
public enum GameResult: String{
case win = "win"
case lose = "lose"
case draw = "draw"
}
}
//MARK: point
extension Blackjack{
///点数はカードの本質ではなくゲームルールなのでCardではなくBlackjackで定義
static func point(of card:Card)->Int{
switch card.face.rank{
case .ace:
return 1
...
case .nine:
return 9
case .ten:
return 10
...
case .king:
return 10
}
}
}
次にアプリに依存する実装
Game.swift
import SwiftyBlackjack
struct Game{
private var blackJack:Blackjack
//MARK: output interface
public var dealer: Blackjack.Dealer{ blackJack.dealer }
public var players: [Blackjack.Player]{ blackJack.players }
public private(set) var gameState: GameState
public private(set) var message:Message?
init(numberOfPlayers:Int = 1) {
self.blackJack = Blackjack(numberOfPlayers: numberOfPlayers)
gameState = .beforeStart
}
}
...
Viewには可能な限り論理分岐を持たせたくないので、PresenterがUIに関することはなんでも計算。
GamePresenter.swift
import SwiftyBlackjack
struct GamePresenter{
public private(set) var game: Game = Game()
}
// MARK: Output interface
extension GamePresenter{
public var playerCount:Int{ game.players.count }//コンピューテッドプロパティとしてself.gameから求める
public var playerTurnIndex:Int?{...}
public var alert:Message?{...}
public var addPlayerIsEnabled:Bool{...}
...
}
// MARK: Input interface
extension GamePresenter{
public mutating func addPlayer(){...}
public mutating func removePlayer(){...}
public mutating func startToPlay(){ game.startToPlay() }
public mutating func hit(){ game.hit() }
public mutating func stand(){ game.stand() }
public mutating func reset(){ self = .init() }
public mutating func onAlertDismissed(){...}
}
今後改善したい点
ゲームなのでアクションに適切なアニメーションをつけたい