ライブラリ「SwiftShogi」のソースコードを読んでみた
はじめに
将棋のアプリをリリースしたいなと思って調べていると、Swiftのみで将棋のロジックを書いている凄いライブラリを発見!ソースコードを読んで将棋のロジックをプログラミングで書くとどのようになるのか勉強させて頂く!GitHubはコチラから
※この記事は勉強の時にNotionにメモっていたのをコピペしたものです。中身も途中ですし、見にくいですがご了承下さい。いつかもう少しいいものを書きます。
Game
- board、color、capturedPiecesの3つのプロパティを持つ構造体。それぞれ盤の駒の配置、手番、持ち駒を表す。イニシャライザは普通のものの他に引数にSFEEN クラスを使って初期化することも出来るが、これは盤を文字列で表したもの。構造はGameとよく似ている。初期位置で初期化すると↓のような感じになる。
プロパティ
- movesFromBoard: [Move]
- movesFromCapturedPieces: [Move]
メソッド
- perform(_ move: Move)
- validate(_ move: Move)
- validMoves() -> [Move]
- validMoves(from source: Move.Source, piece: Piece) -> [Move]
- sortCapturedPieces()
- capturePieceIfNeeded(from destination: Move.Destination)
- remove(_ piece: Piece, from source: Move.Source)
- insert(_ piece: Piece, to destination: Move.Destination, shouldPromote: Bool)
- validateSource(_ source: Move.Source, piece: Piece) 動かそうとする駒が大丈夫なものか検証するメソッド。盤上の駒でその駒がpieceと一致しないなら「盤上に駒がない」エラー。持ち駒で持ち駒にpieceが含まれていないなら「持ち駒に駒がない」エラー。pieceの先手後手がcolorと一致しないなら「自分の駒ではない」エラー。
- validateDestination(_ destination: Move.Destination) 駒がboard(square)に動いてもいいか検証するメソッド。case let .board(square)はバリューバイディングパターンと言う書き方。(参考、参考)board(square)に駒がありかつ、その駒が自分の駒なら「そのマスには自分の駒がすでにある」のエラーを投げる
func validateDestination(_ destination: Move.Destination) throws { switch destination { case let .board(square): // If a piece at the destination does not exist, no validation is required guard let piece = board[square] else { return } guard piece.color != color else { throw MoveValidationError.friendlyPieceAlreadyExists } } }
- validatePromotion(source: Move.Source, destination: Move.Destination, piece: Piece)
- isValid(for move: Move) -> Bool do-catch文を使っている唯一のメソッド。ここからthrowsキーワードを指定したメソッドを実行していく。このような形にしているのは処理ごとにメソッドを定義することで可読性を上げる為だと思われる。また定義にthrowsキーワードを指定していない場合、do-catch文に囲まれていないthrow文によるエラーはコンパイルエラーになるため、エラーが起こりうるメソッドには全てthrowsが指定されている。そしてthrowsキーワードが指定された処理を呼び出すためには呼び出す前にtryキーワードが必要となるので、このメソッド内ではもちろん、他のthrowsキーワードが指定されたメソッド内でtryキーワードが使われている。その2つ以外で使うとコンパイルエラー。
- boardPieceMoves(for piece: Piece, from square: Square) -> [Move]
- capturedPieceMoves(for piece: Piece) -> [Move]
ネストされた型
- MoveValidationError: Error throw文で投げるエラーを表した列挙体。当然ながらエラープロトコルに準拠している。上から順に「盤上に駒がない」、「持ち駒に駒がない」、「自分の駒ではない」、「そのマスには自分の駒がすでにある」、「駒は成れない」、「ルール違反な盤上の駒の成」、「ルール違反な持ち駒の駒の成」、「ルール違反な駒の動き」、「王手がかかっている」、「駒は既に成っている」を表している
public enum MoveValidationError: Error { case boardPieceDoesNotExist case capturedPieceDoesNotExist case invalidPieceColor case friendlyPieceAlreadyExists case pieceCannotPromote case illegalBoardPiecePromotion case illegalCapturedPiecePromotion case illegalAttack case kingPieceIsChecked case pieceAlreadyPromoted }
Square
- 盤のマスを表した列挙型。file、rankのプロパティを持つ。file、rankはチェスの縦、横の意味。プロパティも列挙型で配列として使えるようにCaselterableプロトコルに準拠している。
- この辺りのコードの意味がよく分からない
extension Square { public init(file: File, rank: Rank) { self = Self.allCases.first { $0.file == file && $0.rank == rank }! } public var file: File { File(rawValue: rawValue / File.allCases.count)! } public var rank: Rank { Rank(rawValue: rawValue % Rank.allCases.count)! } public static func cases(at file: File) -> [Self] { allCases.filter { $0.file == file } } public static func cases(at rank: Rank) -> [Self] { allCases.filter { $0.rank == rank } } public static func promotableCases(for color: Color) -> [Self] { let ranks: [Rank] = color.isBlack ? [.a, .b, .c] : [.g, .h, .i] return allCases.filter { ranks.contains($0.rank) } } }
- この記事 がenumについて分かりやすかった。rawValeuはenumの宣言の後にIntを指定することで使用することが出来るようになっている。Square.rawValueで各caseの0~80の番号を取得出来る。またFile(rawValue: Int)やRank(rawValue: Int)でインスタンス化出来る。これは失敗可能イニシャライザなので強制アンラッピングをしている。
- Selfキーワードで型内部で型自身へアクセス出来るためスタティックプロパティにアクセス出来る。allCasesはCaselterableプロトコルで定義されているスタティックプロパティ。またSelfキーワードはメソッドの返り値の型として扱うことが出来る。 参考
- スタティックメソッドとして、ある行や列と同じマスを返すものと、手番から成駒に変わることが出来るマスを返すメソッドを定義している。
- file、rankはコンピューテッドプロパティ。returnがないがswift5.1からgetterの中身の式が1つだけなら省略してもいいことになったみたい。参考 つまりSquareのイニシャライザはallCasesを使うことでrawValueを取得でき、そこから計算してfileとrankプロパティを初期化し、引数と一致するものを返すようにしている。
Color
- 手番を表す列挙型。blackとwhiteの2つ。CaseIterableプロトコルに準拠している。
プロパティ
- isBlack : Bool 先手か後手かを判定するコンピューテッドプロパティ
メソッド
- toggle() Colorのインスタンスの値をもう一方の値に変える。enumの値を変えるためmutatingキーワードが使われている。
- toggled() - > Self toggle()を使い、現在の Colorインスタンスの値を返す
- init?(character: Character) characterが”b”ならblack、”w”ならwhite、それ以外ならnilを返す失敗可能イニシャライザ。character型はstring型とよく似ているがcharacter特有のメソッド等が定義されているみたい。しかし、ここでなぜわざわざcharacterを採用しているかは分からない
- < (lhs: Color, rhs: Color) -> Bool Comparebleに準拠した時に必要なメソッド。Color.allCases.firstIndex(of:Element)を使って先手後手を比較している。このメソッドの使い道はよく分からない
Move
- 反則がないように動ける範囲を示す構造体。source、destination、piece、shouldPromoteプロパティを持つ。shouldPromoteのデフォルトはfalse。動いた先に駒があるかどうかとかのプロパティはいらないのだろうか?
プロパティ
- source : Sorce Sorceはboard(Square)とcapturedPieceの2つのcaseを持つ列挙型。盤上の駒か持ち駒かを分けるため。boardは連想値にSquare型を持つ。連想値によって盤上のどのマスにある駒かを表す
- destination : Destination Destinationはboard(Square)飲みをcaseに持つ列挙型。Destinationは英語で行き先という意味なので駒の動けるマスではなく、動いた先の1マスを表すのだろう。動けるマスとかなら配列とかになるだろうし。1caseのみで列挙型にしているのはよく分からない。コンピューテッドプロパティにするとか、メソッドでSquareを返すとかではまずかったのだろうか。
- piece : Piece 駒の種類を表す
- shouldPromote : Bool 駒が必ず成らなければならないかどうかを示す。駒が進めないように駒を配置することは反則なので、盤上から駒が動いた時成が必要かどうかを示す。これに関係あるのは歩、桂馬、香車。
Direction
- 駒の動き方を表す列挙体。CaseIterableに準拠する。north、sourth、east、west、northEast、northWest、southEast、southWest、northNorthEast、northNorthWest、southSouthEast、southSouthWestの12個のcaseがある。northNorthEast、northNorthWest、southSouthEast、southSouthWestは桂馬の動きを表し、自分駒と敵駒とで分けている
プロパティ
- flippedVertically: Self componentsの要素の.northを.sorthに.sorthを.northに変えて、その動きにあったDirectionを返すコンピューテッドプロパティ。要はPieceの駒の種類ごとに先手の駒の動きを設定した時、これを使えば後手でのその駒の動きも取得できるというもの。将棋の駒の動き方は全て左右対象であるが、上下対象ではないためこのようになる
- containsNorth: Bool componentsの中に.northが含まれるかどうかの真偽を表すコンピューテッドプロパティ。flippedVerticallyを使うかどうかの判断するのには必要がないことなので、どういった場面で使うか疑問
- containsSouth: Bool componentsの中に.southが含まれるかどうかの真偽を表すコンピューテッドプロパティ。
- shift: Int componentsの各要素のshiftを全て足し合わせたコンピューテッドプロパティ。これによってPieceのSquareのRawValueとこのshiftを足し合わせることで、駒がどのマスに動くのかが分かる。足し合わせた時0~80以外の数字ならそこには移動できないということもすぐ分かる
- components: [Component] 駒の動き方がどのようなマスの移動の組み合わせによって成り立つかを表したコンピューテッドプロパティ
var components: [Component] { switch self { case .north: return [.north] case .south: return [.south] case .east: return [.east] case .west: return [.west] case .northEast: return [.north, .east] case .northWest: return [.north, .west] case .southEast: return [.south, .east] case .southWest: return [.south, .west] case .northNorthEast: return [.north, .north, .east] case .northNorthWest: return [.north, .north, .west] case .southSouthEast: return [.south, .south, .east] case .southSouthWest: return [.south, .south, .west] } }
ネストした型
- Component Directionの各caseでSquareで考えた時どこに移動するのかを表した列挙型。north、sourth、east、westの4個のcaseがある。プロパティにshift: Intを持つ。Squareは右縦1列から順に並んでいる。前へ1マス移動するとSquare.allCasesの配列番号は1下がることになる。後ろへ1マス移動する時はその逆。右に1マス移動する時は縦1列分下に下がることでちょうど隣に移動するので、縦1列のマス分マイナス。左に1マス移動する時はその逆
var shift: Int { switch self { case .north: return -1 case .south: return 1 case .east: return -File.allCases.count case .west: return File.allCases.count } }
Piece
- 駒の種類を表す構造体。構造体のインスタンスに関わる部分で1つのネストがある。extensionで成駒に関するネストがある。
プロパティ
- kind : Kind 列挙体Kind
- color : Color 先手か後手かを表す
- isPromoted: Bool 駒の種類が成駒かどうかを表すコンピューテッドプロパティ。↓のように書くことで何度もtureと書くのを省略できる
public var isPromoted: Bool { switch kind { case .pawn(.promoted), .lance(.promoted), .knight(.promoted), .silver(.promoted), .bishop(.promoted), .rook(.promoted): return true default: return false } }
- canPromote: Bool 駒の種類が成駒になれるかどうかを表すコンピューテッドプロパティ。上のコードのpromotedをnormalに変えたもの
- attacks: Set
コンピューテッドプロパティ。Setについてはこの記事が分かり安かった。Setは大体配列と同じ。要素はユニーク。Set型を2つを合わせる時などはSetならではのメソッドが使える。要素はHashableに準拠する必要があるのでAttack定義でHashableに準拠させている。pieceAttacksをスタティックプロパティにすることで、インスタンス化せずにpieceAttacksを使って定義することができている。
var attacks: Set<Attack> { Self.pieceAttacks[self]! }
- pieceAttacks: [Self: Set
] スタティックプロパティ。定数なのmutatingを使っても値を変えられない。Dictionary(uniqueKeysWithValues:)はArrayやDictionaryのようなSequenceに準拠した型を引数にとり、それを元に新たにDictionary型を生成するメソッド。
static let pieceAttacks: [Self: Set<Attack>] = Dictionary(uniqueKeysWithValues: piecesAndAttacks)
- piecesAndAttacks: [(Self, Set
)] - attackableDirections: [Direction] 駒が動くことができる方向を表したコンピューテッドプロパティ。駒に当たるまで動けることなどは考慮しない。kindの各caseに応じて[Direction]を設定する。colorが.whiteの場合は[Direction]の各要素をflippedVerticallyを使って変換
- farReachingDirections: [Direction] 駒に当たるまで動くことができる駒の動きを表したコンピューテッドプロパティ。kindが.lance(normal)、.bishop、.rookの場合に[Direction]を設定。他の場合は[]。colorが.whiteの場合は[Direction]の各要素をflippedVerticallyを使って変換
- allCases: [Self] kindsAndColorsの各要素を引数にしてイニシャライザでPieceを生成。全てのパターンとPieceがいっちょ上がり
- kindsAndColors: [(Kind, Color)] KindとColorの全ての組み合わせパターンを配列にしたスタティックプロパティ。Kind.allCases.flatMapを処理内で使っているが、flatMapは多次元配列を1次元配列にするもの。(参考)これは型に合わせるために必要なもの。タプルと言う型みたい。(参考)わざわざこの型にしているのはイニシャライザを使う時に使いやすくする為だと思われる。↓は実際のコードと実行結果
private static var kindsAndColors: [(Kind, Color)] { Kind.allCases.flatMap { kind in Color.allCases.map { color in (kind, color) } } }
Kind.allCase.flatMapバージョン
[(__lldb_expr_15.Kind.pawn(__lldb_expr_15.State.normal), __lldb_expr_15.Color.black), (__lldb_expr_15.Kind.pawn(__lldb_expr_15.State.normal), __lldb_expr_15.Color.white), (__lldb_expr_15.Kind.pawn(__lldb_expr_15.State.promoted), __lldb_expr_15.Color.black), (__lldb_expr_15.Kind.pawn(__lldb_expr_15.State.promoted), __lldb_expr_15.Color.white), (__lldb_expr_15.Kind.lance(__lldb_expr_15.State.normal), __lldb_expr_15.Color.black), (__lldb_expr_15.Kind.lance(__lldb_expr_15.State.normal), __lldb_expr_15.Color.white), (__lldb_expr_15.Kind.lance(__lldb_expr_15.State.promoted), __lldb_expr_15.Color.black), (__lldb_expr_15.Kind.lance(__lldb_expr_15.State.promoted), __lldb_expr_15.Color.white), (__lldb_expr_15.Kind.knight(__lldb_expr_15.State.normal), __lldb_expr_15.Color.black), (__lldb_expr_15.Kind.knight(__lldb_expr_15.State.normal), __lldb_expr_15.Color.white), (__lldb_expr_15.Kind.knight(__lldb_expr_15.State.promoted), __lldb_expr_15.Color.black), (__lldb_expr_15.Kind.knight(__lldb_expr_15.State.promoted), __lldb_expr_15.Color.white), (__lldb_expr_15.Kind.silver(__lldb_expr_15.State.normal), __lldb_expr_15.Color.black), (__lldb_expr_15.Kind.silver(__lldb_expr_15.State.normal), __lldb_expr_15.Color.white), (__lldb_expr_15.Kind.silver(__lldb_expr_15.State.promoted), __lldb_expr_15.Color.black), (__lldb_expr_15.Kind.silver(__lldb_expr_15.State.promoted), __lldb_expr_15.Color.white), (__lldb_expr_15.Kind.gold, __lldb_expr_15.Color.black), (__lldb_expr_15.Kind.gold, __lldb_expr_15.Color.white), (__lldb_expr_15.Kind.bishop(__lldb_expr_15.State.normal), __lldb_expr_15.Color.black), (__lldb_expr_15.Kind.bishop(__lldb_expr_15.State.normal), __lldb_expr_15.Color.white), (__lldb_expr_15.Kind.bishop(__lldb_expr_15.State.promoted), __lldb_expr_15.Color.black), (__lldb_expr_15.Kind.bishop(__lldb_expr_15.State.promoted), __lldb_expr_15.Color.white), (__lldb_expr_15.Kind.rook(__lldb_expr_15.State.normal), __lldb_expr_15.Color.black), (__lldb_expr_15.Kind.rook(__lldb_expr_15.State.normal), __lldb_expr_15.Color.white), (__lldb_expr_15.Kind.rook(__lldb_expr_15.State.promoted), __lldb_expr_15.Color.black), (__lldb_expr_15.Kind.rook(__lldb_expr_15.State.promoted), __lldb_expr_15.Color.white), (__lldb_expr_15.Kind.king, __lldb_expr_15.Color.black), (__lldb_expr_15.Kind.king, __lldb_expr_15.Color.white)]
Kind.allCases.mapバージョン
[[(__lldb_expr_22.Kind.pawn(__lldb_expr_22.State.normal), __lldb_expr_22.Color.black), (__lldb_expr_22.Kind.pawn(__lldb_expr_22.State.normal), __lldb_expr_22.Color.white)], [(__lldb_expr_22.Kind.pawn(__lldb_expr_22.State.promoted), __lldb_expr_22.Color.black), (__lldb_expr_22.Kind.pawn(__lldb_expr_22.State.promoted), __lldb_expr_22.Color.white)], [(__lldb_expr_22.Kind.lance(__lldb_expr_22.State.normal), __lldb_expr_22.Color.black), (__lldb_expr_22.Kind.lance(__lldb_expr_22.State.normal), __lldb_expr_22.Color.white)], [(__lldb_expr_22.Kind.lance(__lldb_expr_22.State.promoted), __lldb_expr_22.Color.black), (__lldb_expr_22.Kind.lance(__lldb_expr_22.State.promoted), __lldb_expr_22.Color.white)], [(__lldb_expr_22.Kind.knight(__lldb_expr_22.State.normal), __lldb_expr_22.Color.black), (__lldb_expr_22.Kind.knight(__lldb_expr_22.State.normal), __lldb_expr_22.Color.white)], [(__lldb_expr_22.Kind.knight(__lldb_expr_22.State.promoted), __lldb_expr_22.Color.black), (__lldb_expr_22.Kind.knight(__lldb_expr_22.State.promoted), __lldb_expr_22.Color.white)], [(__lldb_expr_22.Kind.silver(__lldb_expr_22.State.normal), __lldb_expr_22.Color.black), (__lldb_expr_22.Kind.silver(__lldb_expr_22.State.normal), __lldb_expr_22.Color.white)], [(__lldb_expr_22.Kind.silver(__lldb_expr_22.State.promoted), __lldb_expr_22.Color.black), (__lldb_expr_22.Kind.silver(__lldb_expr_22.State.promoted), __lldb_expr_22.Color.white)], [(__lldb_expr_22.Kind.gold, __lldb_expr_22.Color.black), (__lldb_expr_22.Kind.gold, __lldb_expr_22.Color.white)], [(__lldb_expr_22.Kind.bishop(__lldb_expr_22.State.normal), __lldb_expr_22.Color.black), (__lldb_expr_22.Kind.bishop(__lldb_expr_22.State.normal), __lldb_expr_22.Color.white)], [(__lldb_expr_22.Kind.bishop(__lldb_expr_22.State.promoted), __lldb_expr_22.Color.black), (__lldb_expr_22.Kind.bishop(__lldb_expr_22.State.promoted), __lldb_expr_22.Color.white)], [(__lldb_expr_22.Kind.rook(__lldb_expr_22.State.normal), __lldb_expr_22.Color.black), (__lldb_expr_22.Kind.rook(__lldb_expr_22.State.normal), __lldb_expr_22.Color.white)], [(__lldb_expr_22.Kind.rook(__lldb_expr_22.State.promoted), __lldb_expr_22.Color.black), (__lldb_expr_22.Kind.rook(__lldb_expr_22.State.promoted), __lldb_expr_22.Color.white)], [(__lldb_expr_22.Kind.king, __lldb_expr_22.Color.black), (__lldb_expr_22.Kind.king, __lldb_expr_22.Color.white)]]
メソッド
- init(kind: Kind, color: Color) 駒の種類、先手か後手かの2つで生成
- promote() kindの連想値がnormalならpromotedに変えるメソッド。構造体のプロパティの値を変えるのでmutatingキーワードが使われている
- unpromote() kindの連想値がpromotedならnormalに変えるメソッド。mutaitingキーワードが使われている。成駒が盤上でまた裏返ることは将棋で起こり得ないが、成駒が取られて持ち駒になる時などに使うのだろうか?⇦そうだった
- capture(by color: Color) unpromote()してcolorを引数のものに変える。mutatingキーワードが使われている。しかし、引数で指定する形だと誤ってもともとのcolorと同じものを指定してしまう可能性がある。そのような状況になることは将棋のルール上あり得ないのでColorのメソッドtoggle()を使った方がいいと思う
- init?(character: Character, isPromoted: Bool) 棋譜とかの文字から生成する用のイニシャライザ。引数をStringではなくCharacterを使うことで、文字を小文字にするlowercased()と文字が大文字かどうかを判定するisUppercaseを処理の中で使っている。isPromotedからstate:Stateを定義する。character.lowercased()によってpieceを設定する。character.isUppercaseがtrueならcolorに.black、falseなら.whiteを設定する。先手の”歩”ならinit?(character:”P”,isPromoted:false)、後手の”と”ならinit?(character:”p”,isPromoted:true)のように使う
ネストした型
- Kind pawn(State)、lance(State)、night(State)、silver(State)、gold、bishop(State)、rook(State)、kingの8つのcaseからなる列挙体。連想値Stateで成駒かどうかを表す。連想値を使用しているため、CaseIterableに準拠しても自動的にallCasesプロパティが作られないためallCasesプロパティを実装している。またComparableに準拠して演算子を実装しているが、こちらは活用法はよく分からない。
extension Piece.Kind: CaseIterable { public static let allCases: [Self] = [ .pawn(.normal), .pawn(.promoted), .lance(.normal), .lance(.promoted), .knight(.normal), .knight(.promoted), .silver(.normal), .silver(.promoted), .gold, .bishop(.normal), .bishop(.promoted), .rook(.normal), .rook(.promoted), .king, ] }
extension Piece.Kind: Comparable { public static func < (lhs: Piece.Kind, rhs: Piece.Kind) -> Bool { return allCases.firstIndex(of: lhs)! < allCases.firstIndex(of: rhs)! } }
- State Stateはnormal、promotedの2つのcaseからなる列挙体。成駒かどうかを表す
- Attack: Hashable プロパティにdirection: DirectionとisFarReaching: Boolを持つ構造体。Hashableに準拠しているので辞書型のキーに使える