rust
将棋

Rustで将棋AI入門 1-動かしてみる

この記事はIS17erアドベントカレンダー19日目の記事として書かれました。

はじめに

第五回電王トーナメントにソフト名Girigiriとして参加してきました。
将棋AIの開発はC++が主流であり、大会はC++で参加したのですがデバッグにかなりうんざりしたので、その後Rustを試してみることにしました。

Rustとは

関数型などに由来した様々なパラダイムに対応した現代的な言語である一方で、性能はC++に匹敵すると言われている。
おそらく並列処理もC++に比べてかなり楽になるはず。
欠点は日本語の資料が少ないこと、過去のコンピュータ将棋の資産を活用できないことなどでしょうか。

Rust入門

ドキュメントがかなり丁寧に説明してくれている。
個人的には、C++とOCamlを触ったことがあればシンタックスが親しみやすいように感じた。
Cargoというパッケージ管理ツールがあり、tomlファイルで設定を記述したり、cargo runでコンパイルして実行するなどのことができる。

駒の定義

まず14種類(先手後手区別すると28種類)ある将棋の駒を定義しましょう。
C++ではEnumを使うのが普通だと思うのですが、RustのEnumが少し特殊なのもあって、今回はintのまま持つことにしました。

  • 符号付き整数:i8、i16、i32、i64
  • 符号なし整数:u8、u16、u32、u64
  • ポインタ型:isize、usize
  • 浮動小数点:f32、f64

さて、Rustにはプリミティブに定義されている数値型がこれだけあるのですが、今回は8ビットの符号なしで良さそうです。
それでは、駒の種類を日本語表示する関数を定義します。

pub fn piece_to_japanese(piece : u8) -> &'static str {
    match piece {
        0   => " 口",

        // black's pieces
        1   => " 歩",
        2   => " 香",
        3   => " 桂",
        4   => " 銀",
        5   => " 角",
        6   => " 飛",

        7   => " と",
        8   => " 杏",
        9   => " 圭",
        10  => " 全",
        11  => " 馬",
        12  => " 龍",

        13  => " 金",
        14  => " 王",

        // white's pieces
        15  => "^歩",
        16  => "^香",
        17  => "^桂",
        18  => "^銀",
        19  => "^角",
        20  => "^飛",

        21  => "^と",
        22  => "^杏",
        23  => "^圭",
        24  => "^全",
        25  => "^馬",
        26  => "^龍",

        27  => "^金",
        28  => "^王",

        _   => "not a piece",
    }
}

match式を使えるのが便利ですね。

局面のデータ構造

pub struct State {
    pub color: bool,                    // true: black, false: white
    pub board: [[u8; 9]; 9],
    pub hand: [Hand; 2],                // hand[0]: white, hand[1]: black
}

次に局面のStructを定義します。
colorは手番を表し、先手番か後手番なのでboolを使います。
boardは盤上の駒を表現し、今回は9x9の二次元配列で実装しています。
持ち駒のhandはHandという別のStructを用いて定義しているのですが、今回は省略します。

RustにClassはないのですが、このStructに対する関数を定義することでClassと同じように扱うことができます。

impl State {
    pub fn new() -> State {
        State {
            color: true,
            board: // initial position
                [[16, 17, 18, 27, 28, 27, 18, 17, 16],
                 [0,  20, 0,  0,  0,  0,  0,  19, 0 ],
                 [15, 15, 15, 15, 15, 15, 15, 15, 15],
                 [0; 9],
                 [0; 9],
                 [0; 9],
                 [1,  1,  1,  1,  1,  1,  1,  1,  1 ],
                 [0,  5,  0,  0,  0,  0,  0,  6,  0 ],
                 [2,  3,  4,  13, 14, 13, 4,  3,  2 ]],
            hand: [Hand::new(), Hand::new()],
        }
    }

    pub fn print(&self) {
        if self.color { println!("color: black"); }
        else { println!("color: white"); }
        for i in 0..9 {
            for j in 0..9 {
                print!("{}", piece_to_japanese(self.board[i][j]));
            }
            println!("");
        }
        print!("先手の持ち駒: ");

        // 省略

    }
}

&selfを引数に持たない関数new()がコンストラクタに相当するものです。n要素配列の要素を全てxで初期化するときに、
[x; n]
と書けるのが地味にありがたいですね。
colorは先手番となるように、boardには初期配置の駒を設定しました。

局面の状態を表示するメソッドも定義しておきましょう。&selfを引数にとり、クラス変数の前にはself.をつけるのが少し特徴的でしょうか。

指し手のパース

それでは最後に、実際に指し手を受け取って動かしてみましょう。
ここでは、USIプロトコルというコンピューター将棋のスタンダードになっているプロトコルで指し手を受け取ることにします。
例えば、

  • 73歩不成:7g7f
  • 73歩成:7g7f+
  • 73歩打:P*7f

のように表します。

fn main() {
    let mut state = State::new();
    let mut mv = NULL_MOVE;
    loop {
        state.print_debug();
        let mut input = String::new();
        io::stdin().read_line(&mut input)
            .expect("failed to read line");
        let to_j: i8 = 9 - (input.chars().nth(2).unwrap().to_digit(10).unwrap() as i8);
        let to_i: i8 = input.chars().nth(3).unwrap() as i8 - 'a' as i8;
        if input.chars().nth(1) == Some('*') {
            // drop
            // 省略

RustではStringに添え字でアクセスができないようで、このような冗長な書き方しか思いつきませんでした。
エラー処理も雑ですが、正しい文字列のみが来るとして先に進みます。

追記

@tatsuya6502さんから指摘をいただきましたが、今回は入力が英数字であることがわかっているため、Stringをあらかじめバイトコードに変換することで以下のように書くことができるようです。

fn main() {
    let mut state = State::new();
    let mut mv = NULL_MOVE;
    loop {
        state.print_debug();
        let mut input = String::new();
        io::stdin().read_line(&mut input)
            .expect("failed to read line");
        let bytes = input.as_bytes();    // Stringをバイトコードに変換
        let to_j = '9' as i8 - bytes[2] as i8;
        let to_i = bytes[3] as i8 - 'a' as i8;
        if bytes[1] == '*' as u8 {
            // drop
            // 省略

余計なunwrap()が減ってすっきりしました。

動かしてみる

$ cargo run --bin debug

0th, color: black
^香^桂^銀^金^王^金^銀^桂^香
 口^飛 口 口 口 口 口^角 口
^歩^歩^歩^歩^歩^歩^歩^歩^歩
 口 口 口 口 口 口 口 口 口
 口 口 口 口 口 口 口 口 口
 口 口 口 口 口 口 口 口 口
 歩 歩 歩 歩 歩 歩 歩 歩 歩
 口 角 口 口 口 口 口 飛 口
 香 桂 銀 金 王 金 銀 桂 香
先手の持ち駒:
後手の持ち駒:

...............省略................

7g7f
▲76 歩
1th, color: white
^香^桂^銀^金^王^金^銀^桂^香
 口^飛 口 口 口 口 口^角 口
^歩^歩^歩^歩^歩^歩^歩^歩^歩
 口 口 口 口 口 口 口 口 口
 口 口 口 口 口 口 口 口 口
 口 口 歩 口 口 口 口 口 口
 歩 歩 口 歩 歩 歩 歩 歩 歩
 口 角 口 口 口 口 口 飛 口
 香 桂 銀 金 王 金 銀 桂 香
先手の持ち駒:
後手の持ち駒:

感動ですね。

Rustの感想

C++と比べるとエラー表示が圧倒的に充実していてよかったです。体感的に、C++はなんのバグがわからずに詰まってしまうことが多かったのですが、Rustの場合コンパイラが情報を出してくれるのでそれが少ないように感じました。
入出力がやや複雑ですが、慣れれば余裕になるのでしょうか。

最後に

色々省略してしまいましたが、今回使ったプログラムはアップしているので動かしてみてください。
https://github.com/bknshn/girigiri_rust

また、

  • 合法手生成
  • 探索
  • 評価関数の作成

などについても機会があったら書きたいと思います。