0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

8×8の将棋系ゲーム「Ōgi」のルールエンジンを Rust で書いて学んだ3つのこと

0
Posted at

TL;DR — 将棋系 8×8 ゲームのルールエンジンを Rust で実装して学んだ3点:持ち駒は局面の同一性を変える/打ちは分岐係数を支配する/打ち歩詰めは「合成ルール」なのでレイヤー設計が要る。コードはすべて実行済みの出力つきです。

将棋・チェス・シャンチーは、同じ祖先から育った兄弟のようなゲームです。Ōgi(オーギ、「王のゲーム」)は将棋に着想を得た 8×8 の二人零和ゲームで、持ち駒(打ち)を残しつつ、王の隣に「姫」を置くなどの再構成をしています。正式ルールは ja.ogi.page にあります。以下、駒は英語名で書きます — fu は将棋の歩に相当する駒です。

この記事は、そのルールエンジン(sashite-sanki-engine、Rust、Apache-2.0)を実装して面白かった3点のメモです。手元で試すには:

cargo new ogi-notes && cd ogi-notes
cargo add sashite-sanki-engine@0.9

各スニペットは use sashite_sanki_engine::prelude::*; を先頭に置けば main にそのまま貼れます。

1. 持ち駒があると「局面」は盤面だけでは決まらない

チェスの FEN に慣れていると、局面 = 盤面 + 手番 + 少々、と思いがちです。持ち駒のあるゲームでは、手の中の駒まで含めて初めて局面が定まります。使っている表記 FEEN は3フィールド構成で、盤面/持ち駒/スタイルを持ち、手番はスタイル欄の大文字・小文字の順で表現されます。

use sashite_sanki_engine::prelude::*;

let feen = "k^7/8/8/3f4/8/8/8/3R3K^ / J/j";
let pos = Position::parse(feen).unwrap();
let mv = Move::parse(r#"["d1","d5",null]"#).unwrap();
let after = engine::apply(&pos, &mv).unwrap();
println!("{}", after.to_feen());
// => k^7/8/8/3R4/8/8/8/7K^ F/ j/J

ルークが d5 の fu を取ると、持ち駒欄に F/(先手の手に fu が1枚)が現れ、スタイル欄が J/j から j/J へ反転して手番の移動を示します。千日手(三回同一局面)の判定キーもこの文字列そのものです — 盤面が同じでも手の中身が違えば別局面、が自動的に成立します。

2. 打ちは分岐係数を爆発させる

打てる駒が1枚あるだけで、合法手の数は桁が変わります。この局面で数えてみます(先手の持ち駒に F が1枚):

8 | k . . . . . . .
7 | . . . . . . . .
6 | . . N . . . . .
5 | . . . . . . . .
4 | . . . . . . . .
3 | . . . . . . . .
2 | . . . . . . . .
1 | . R . . . . . K
    a b c d e f g h     先手の持ち駒: F(fu)
let feen = "k^7/8/2N5/8/8/8/8/1R5K^ F/ J/j";
let pos = Position::parse(feen).unwrap();
let moves = engine::legal_moves(&pos);
let drops = moves.iter()
    .filter(|m| matches!(m, Move::Drop { .. }))
    .count();
println!("legal: {} (drops: {})", moves.len(), drops);
// => legal: 76 (drops: 52)
局面 合法手 うち打ち
初期局面 22 0
上図(手に fu ×1) 76 52

fu 1枚で合法手の 3 分の 2 が打ちになります。探索・評価を書く側にとって、これは「将棋系ゲームのエンジンはチェスエンジンの定数調整では済まない」ことのいちばん簡潔な証拠だと思います。指し手の表現は [出発地, 目的地, actor] の JSON で、打ちは出発地を null にして駒名を actor に書きます。

3. 打ち歩詰めは「合成されたルール」

Ōgi は将棋から打ち歩詰めの禁止も継承しています。実装して初めて分かるのは、このルールの層の深さです。「fu を打って詰みになる手は指せない」を判定するには、打った後の局面で相手の合法手を全探索する必要があります。つまり合法性レイヤーの上に、終局判定レイヤーを重ねた合成ルールであり、単発の駒移動チェックと同じ層には書けません。

先ほどの図をもう一度。a7 に fu を打つと a8 の玉に王手(fu は前に利く)。逃げ場は b8(ナイト c6 が利く)と b7(ルーク b1 が利く)のみ、a7 の fu 自体もナイト c6 が守っている — 詰みです。だからこの打ちは違法:

let drop = Move::parse(r#"[null,"a7","fu"]"#).unwrap();
match engine::validate(&pos, &drop) {
    Ok(()) => println!("legal"),
    Err(e) => println!("rejected: {e:?}"),
}
// => rejected: Uchifuzume

let elsewhere = Move::parse(r#"[null,"e5","fu"]"#).unwrap();
println!("{}", engine::validate(&pos, &elsewhere).is_ok());
// => true(詰みでない打ちは合法)

同じ詰みでも盤上の駒で届ける分には合法です。禁じられているのは「fu の打ち」による詰みだけ — ルールの非対称性がそのまま実装のレイヤー構造に写る、気持ちのよい例でした。

まとめ

  • 持ち駒のあるゲームでは、局面の同一性に手の中身を含める(表記が最初からそれを強制してくれると楽)。
  • 打ちは分岐係数を支配する。データで見ると一目瞭然(76手中52手)。
  • 打ち歩詰めのような合成ルールは、合法性チェックを層で設計していないと後付けできない。

ルール: ja.ogi.page / ogi.page(英語)。表記の仕様: sashite.dev。エンジンと探索: sashite-sanki-enginesashite-sanki-player(どちらも crates.io、Apache-2.0)。ブラウザでそのまま遊べます: play.ogi.page(アカウント不要)。

ルールの解釈やエンジンの判定がおかしい局面を見つけたら、それがいちばんありがたいフィードバックです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?