Swift で将棋盤 を実装する

  • 54
    いいね
  • 0
    コメント

将棋盤Kit

Swift 3.0 に対応しました。記事内の説明は一部 2.2 の場合があります。追ってアップデートしたいと思います。

日本の将棋はルールが複雑でプログラミングには大変扱いにくい課題の一つだと思います。Swiftの登場で、複雑難解な課題のコード化もきっと書きやすくなるだろうと思い、本将棋の基本的なルールや挙動を実装するフレームワーク「将棋盤Kit」を開発してみましたので紹介したいと思います。

まず、最初にはっきりさせておきたいのですが、将棋盤Kitは以下の事を目指してはいません。
* グラフィカルなユーザーインターフェースを提供する
* 人口知能の実装

しかし、将棋盤Kit はそんなグラフィカルなユーザーインターフェースの実現や棋譜の研究さらに将棋の思考エンジンを実装しようと考えた場合に、便利となる基本的なルールの実装を目的としています。全く個人的なSwiftの技術探求との噂も。

ちなみに現状では、一部の機能が未実装なのとさらなるテストが必要です。

実験的な試み

物議を醸す事になると思いましたが、実験な試みとして一つ、クラス名などの型の名前、変数名などの表記に日本語を思い切って使ってみました。将棋関連の用語は元々日本語ですから、銀将を Silver General とか 成銀を Promoted Silver とかのネーミングにするより自然にコードが書けるはずと考えたからです。そして、きっとメンテンス性も良いのではと考えていますが、現時点では結論は出ていません。

enum 駒種型 : Int8 {
    case , , , , , , , 
    // ...
}

型の名前を日本語にするとキャピタライゼーションの問題があるので、触れておきます。例えば「駒」を型の名前につけると「駒」という名前の変数がつけられないので、今回は型の名前には「型」をつけてこの問題を解決しています。

enum Koma { ... }
var koma = Koma(...) // OK: no problem!

enum  { ... }
var  = (...) // Error: variable name cannot be same as type name
enum 駒型 { ... }
var  = 駒型(...) // OK: no problem!

あと、日本語では単数か複数かをはっきりさせない傾向があり、単数と複数と名前が衝突する可能性があるので、思い切って Array 相当の名前には「列」をつけてみました。コードを読むときには、今のとことやや気になりますが、今後、こう言ったネーミングに慣れていくのかどうか、見守っていきたいと思います。

代表的な型

以下に将棋盤Kit の基本的な型を一覧にしました。

Name Type Description
筋型 enum 筋(右から左)
段型 enum
位置型 enum 盤の位置 (筋と段)
駒種型 enum 表裏を気にしない駒の種類
駒面型 enum 表裏を気にする駒の種類
持駒型 struct 持ち駒の状態を保持
先手後手型 enum 先手、後手を区別
升型 enum マスの情報(駒の種類、先手後手、空など)
指手型 enum 指手を表現する
局面型 class 対戦中の盤のイチ局面を表現

位置情報

盤面上の位置を特定する。筋も段も 1 から始まりますが、実際の rawValue は 0 から始まります。つまり、0 は 一段もしくは 1筋(スジ) となります。

let col = .  // (全角)
let row = .  // (全角)
let position1 = 位置.5五  // (全角)
let position2 = 位置(: col, : row)

先手後手の表現

enum 先手後手型 {
    case 先手, 後手
}

駒の表現

駒には 駒種型駒面型があります。駒種は裏表を意識しない持ち駒のような状態を表現し、駒面は裏表を意識する状態を表現します。駒種駒面も造語です。将棋では駒で「表裏を意識しない状態」も「表裏を意識する状態」も単なる「駒」であるので、プログラミングの便宜上「駒種」「駒面」と名付けさせていただきました。もし、もっと良さそうな名前があれば賜りたいと思います。

let fu = 駒種型.
let to = 駒面型.

あと、「成香, 成桂, 成銀」は、「, , 」と表現しました。おそらく正式な名称ではありませんが、コンピュータ上の都合上こう表現される事はしばしばあるようです。

enum 駒面型 {
    case , , , , , , , 
    case , , , , , 
}

持ち駒の表現

持駒型は先手または後手の持駒の状態を保持します。以下のコードは先手の持駒に銀が何枚あるかを調べます。

let 先手持駒: 持駒型 = ...
let 銀の持ち駒数 = 先手持駒[.]

局面の表現

局面型 は対局中の一番を表現しています。それぞれのマスは升型で、駒の種類・先手後手さらに空か否かの状態も保持します。局面は文字列から生成する事もできます。

let 局面 = 局面型(string:
            "▽持駒:なし\r" +
            "|▽香|▽桂|▽銀|▽金|▽王|▽金|▽銀|▽桂|▽香|\r" +
            "|  |▽飛|  |  |  |  |  |▽角|  |\r" +
            "|▽歩|▽歩|▽歩|▽歩|▽歩|▽歩|▽歩|▽歩|▽歩|\r" +
            "|  |  |  |  |  |  |  |  |  |\r" +
            "|  |  |  |  |  |  |  |  |  |\r" +
            "|  |  |  |  |  |  |  |  |  |\r" +
            "|▲歩|▲歩|▲歩|▲歩|▲歩|▲歩|▲歩|▲歩|▲歩|\r" +
            "|  |▲角|  |  |  |  |  |▲飛|  |\r" +
            "|▲香|▲桂|▲銀|▲金|▲王|▲金|▲銀|▲桂|▲香|\r" +
            "▲持駒:なし\r",
        手番: .先手)

もしくは 指手型 を実行させると次の局面が取得できます。

let 前の局面: 局面型  = ...
let 指手 = ...
let 次の局面 = 局面型.指手を実行(指手)

局面型のインスタンスは、.stringprint() でフォーマットされた文字列を生成する事や、print()でコンソールに出力する事ができます。また、ここで取得された文字列から再度、局面型のインスタンスを作る事ができるので、デバックには便利だと思われます。

let ある局面: = ...
print("\(ある局面)")
let stringRepresentaion = ある局面.string
let 手番 = ある局面.手番
let 再現局面 = 局面(stringRepresentaion, 手番: 手番)
後手持駒:桂
|▽香|▲龍|  |  |  |  |  |  |▽香|
|  |  |▲金|  |▲龍|  |  |  |  |
|▽歩|▲全|  |  |▽金|  |▽歩|▽桂|▽歩|
|  |▽歩|  |  |▽王|  |▽銀|  |  |
|  |  |  |▲金|▲角|  |  |  |  |
|  |  |▽香|▲歩|▲歩|▲銀|▲桂|  |  |
|▲歩|▲歩|  |  |  |▲歩|▲銀|  |▲歩|
|  |  |  |▲金|  |  |  |▲歩|  |
|▲香|  |  |▲玉|  |  |  |▽馬|  |
先手持駒:桂,歩7

指手の表現

指手は以下のように表現されます。は盤面上の駒が移動する事を表現し、 は持駒を打つ事を表現しています。終了については今は投了しかありませんが、アマチュアの試合などでは、王手に気がつかずうっかり負ける事もあるので、そうした表現をどうするかは今後の課題です。

enum 指手型 {
    case (先後: 先手後手型, 移動前の位置: 位置型, 移動後の位置: 位置型, 移動後の駒面: 駒面型)
    case (先後: 先手後手型, 位置:位置型, 駒種:駒種型)
    case 投了(先後: 先手後手型)
}

手作りのコードで、指手型を作る事はできますが、有効な手でなくてはなりません。無効な指手は、局面型指手を実行()で弾かれてしまいます。

var 局面 = 局面(string: 手合割型.平手初期盤面, 手番: .先手)
局面 = 局面.指手を実行(指手型.(先後: .先手, 移動前の位置: .7七, 移動後の位置: .7六, 移動後の駒面: .))
局面 = 局面.指手を実行(指手型.(先後: .後手, 移動前の位置: .3三, 移動後の位置: .3四, 移動後の駒面: .))

局面型全可能指手列()は手番の全て可能な指手を戻してきます。これを逐次、指手を実行()すれば次の局面を得る事ができますが、メモリは有限なので、再帰的に巨大なインスタンスを生成する事は適切とは言えないでしょう。

let 局面: 局面型 = ...
let 全可能指手 = 局面.全可能指手列()
for 候補指手 in 全可能指手 {
    if let 次の局面 = 局面.指手を実行(候補指手) {
        // find one you like
    } 
}

メソッドやプロパティの使い方

  • 局面型 の全てのマス(位置)について処理
let 局面: 局面型 = ...
for 位置 in 局面 {
    let マス = 局面[位置]
    let 駒面 = マス.駒面
    // ...
}
  • 指定された場所(位置)の駒が移動可能な全ての移動先(またはその指手)
let 局面: 局面型 = ...
let 位置列 = 局面.指定位置の駒の移動可能位置列(.5五) // only location
let 指手列 = 局面.指定位置の駒の移動可能指手列(.5五)
  • 盤面上で特定の駒が盤上のどこにあるか調べる(複数あれは全て)
let 局面: 局面型 = ...
let 先手の桂の位置 = 局面.駒の位置列(., 先後: .先手)
for 位置 in 先手の桂の位置 {
    // ...
}
  • 盤上の指定された位置に移動可能な盤面上の駒の全ての位置(または指手)
let 局面: 局面型 = ...
let 味方の駒の位置列 = 局面.指定位置へ移動可能な全ての駒の位置列(.7六, 先後: .先手)
let 敵味方双方の駒の位置列 = 局面.指定位置へ移動可能な全ての駒の位置列(.7六, 先後: nil)
let 後手の移動指手列 = 局面.指定位置へ移動可能な全ての駒を移動させる指手(.7六, .後手)
  • 王を取る事ができる全ての指手を調べる
let 局面: 局面型 = ...
let 王手列: [指手型] = 局面.王手列(.先手)
  • 詰みか?(要テスト)
let 局面: 局面型 = ...
if 局面.詰みか() {
    // checkmate!
}
  • 手番の敵方を求める(先手後手)
let 局面: 局面型 = ...
let 手番の敵方 = 局面.手番.敵方
  • 先手の王が後手の角道上にあるか調べる
let 局面: 局面型 = ...
let 後手の角の位置 = ...
for (vx, vy) in 駒面..移動可能なベクトル {
    var (x, y) = (後手の角の位置. + vx, 後手の角の位置. + vy)
    while let x = x, let y = y {
        if let マス = 局面[x, y] where マス.駒面 == . && マス.先後 == .先手 {
            // King is on the line
        }
        (x, y) = (x + vx, y + vy)
    }
}

局面イメージの生成

局面から Image を生成する事もできます。サイズは正方形を推奨します。多少のアスペクト比の歪みであれば表示に問題無いと思われますが、大きな歪みは表示が崩れる事が予想されます。

extension 局面型 {
    func imageForSize(size: CGSize) -> CGImage
}

呼び出し方法の例を示します。

let image = 局面.imageForSize(CGSizeMake(300, 300))

そして実際に局面から生成された画像のサンプルを以下に示します。「成香、成桂、成銀」はそれぞれ「杏、圭、全」と表示され、最終手は太字になります。デバック時、ログにテキストを出力するだけでははっきりしない場合もあるのでそんな場合にはイメージを生成すると便利です。

image.tiff

テストコードの追加

テスト用のアプリが含まれています。テスト用のコードを追加するには、MenuTableViewController にコードを追加します。以下にその例を示します。まずは、クロージャを書きます。クロージャーは文字列を返します。

class MenuTableViewController: NSViewController {
    ...
    let yourTestCode: (() -> String) = {
        var string = ""
        let 局面 = 局面型(string:
            "後手持駒:桂\r" +
            "|▽香|▲竜|  |  |  |  |  |  |▽香|\r" +
            "|  |  |▲金|  |▲竜|  |  |  |  |\r" +
            "|▽歩|▲全|  |  |▽金|  |▽歩|▽桂|▽歩|\r" +
            "|  |▽歩|  |  |▽王|  |▽銀|  |  |\r" +
            "|  |  |  |▲金|▲角|  |  |  |  |\r" +
            "|  |  |▽香|▲歩|▲歩|▲銀|▲桂|  |  |\r" +
            "|▲歩|▲歩|  |  |  |▲歩|▲銀|  |▲歩|\r" +
            "|  |  |  |▲金|  |  |  |▲歩|  |\r" +
            "|▲香|  |  |▲玉|  |  |  |▽馬|  |\r" +
            "先手持駒:桂,歩7", 手番: .後手)!
        print("\(局面)")
        string += "\(局面)"
        let 詰み = 局面.詰みか()
        string += "詰み: \(詰み)"
        return string
    }
    ...
}

次に、menuItems に名前をつけて追加します。これで、アプリの TableView 上にタイトルが表示され、クリックすると実行された結果が TextView に表示されます。また、別の方法で実装しても構いません。

class MenuTableViewController: NSViewController {
    ...
    lazy var menuItems: [MenuItem] = [
        ...
        (title: "YourTestCode", closure: self.yourTestCode) // <-- added
    ]
    ...
}

Github

将棋盤Kitのコードは Github で入手できます。

https://github.com/codelynx/ShogibanKit

TIPS

Source コードを欧文フォントの「Menlo」だったり、日本語フォントの「Hiragino」を使うと、日本語の文字幅が揃わなかったり、和欧混合のテキストになった時に行の高さが不揃いになったりと、局面をテキストで表現したり、ログに出力した時に、残念な表示になってしまう事があります。ちなみに、駒のないマスは全角スペース二文字で表現されています。

そこで、Adobe から無料で利用出来るプログラマー向けフォント「Source Hand Code JP」を使ってみましょう。日本語、欧文でそれぞれ等幅になっているので、将棋の局面をテキスト表現されたログも見やすくなるかと思います。

マスの幅が綺麗に揃いましたね。ちなみに、全角は欧文の2倍というわけではなく、欧文3文字に対して、全角2文字という比率なので、文字揃えにはやや工夫が必要ですが、それでも綺麗に揃える事ができます。

Source Han Code JP の入手先は以下の通りです。
https://github.com/adobe-fonts/source-han-code-jp

フィードバック

フィードバックはなどございましたら、よろしくお願いいたします。

変更

  • 「駒型」を「駒種型」と名称を変更しました。付随する表現も変更されています。2016.5.15
  • 局面から画像を生成する方法を加筆しました。2016.7.8

(当初執筆時の環境)

swift --version
Apple Swift version 2.2 (swiftlang-703.0.18.1 clang-703.0.29)

(アップデート時の環境)

swift --version
Apple Swift version 3.0 (swiftlang-800.0.46.2 clang-800.0.38)