とあるダイソーのボドゲの実現可能局面数を見積もってみた(バーンサイドの補題)

お久しぶりです。今日は Tokyo Doves とはまた別のダイソーボドゲについてお話しします。

今回のテーマ

今回取り扱うのは、こちらの記事でも紹介されている、「セカンドベスト」というボードゲームです。
bodoge-nya.hatenablog.com
何度か遊んでみていますが、シンプルながら先を読み通すのが難しい、非常によくできたゲームだなという感想を持っています。
楽しいゲームを見つけると、ついつい「完全解析したい・・・!」と思うのは自然な考えかなと思います。え?思わない?私は思います。
今回は解析に先立って、このゲームの可能局面数をおおよそ見積もってみることを考えてみます。

ざっくり概観

一定程度慣れた状態でルールに従ってゲームを進めていくと、概ね以下のような流れになります:

  1. 黒と白が交互に石を置いていく
  2. それぞれの手持ちの8個の石をすべて置いた状態になる
  3. 石を動かしていく
  4. ある瞬間に唐突にゲームが終了する

このゲームの進行上現れる各局面は、少なくとも次を満たすことが分かります。

  • (黒を先手番として) 黒石は白石と同数か、または1個多い
  • ボード上に置かれている石の数は0個以上16個以下
  • 同じポジションに重なっている石の数は0個以上3個以下
  • 同色の石は区別しない

この条件を満たすものをすべて数え上げれば、可能局面数の悪くない上界を与えるだろうと予想します。まずはこれを数えてみましょう。

単純にカウント

各ポジションに 1~8 と番号を付け、各ポジションに置かれる石の数を a_{1}, a_{2}, ..., a_{8} とします。このとき

  •  a_{1}+a_{2}+\dots+a_{8} \leq 16
  •  0 \leq a_{i} \leq 3  (i = 1, 2, \dots, 8)

という条件を満たすことが分かります。この a_{1}, a_{2}, \dots, a_{8} への値の割り振り方 (=石の色を区別しない配置の総数) は
(1 + x + x^{2} + x^{3})^{8}x^{s} の係数 (ただし s = a_{1} + a_{2} + \dots + a_{8})
となります。この値を c_{s} とします。
白石の個数は \left\lfloor \dfrac{s}{2} \right\rfloor なので*1、各々の割り振り方に対する石の並べ方は
{}_{s}\mathrm{C}_{\lfloor s/2 \rfloor}
となります*2。よって、求めたい総数は
\displaystyle\sum_{s=0}^{16} c_{s} \cdot {}_{s}\mathrm{C}_{\lfloor s/2 \rfloor}
で求まります。
各々の項を気合で計算すると、以下のような結果になります:

s 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
c_{s} 1 8 36 120 322 728 1428 2472 3823 5328 6728 7728 8092 7728 6728 5328 3823
{}_{s}\mathrm{C}_{\lfloor s/2 \rfloor} 1 1 2 3 6 10 20 35 70 126 252 462 924 1716 3432 6435 12870
c_{s} \cdot {}_{s}\mathrm{C}_{\lfloor s/2 \rfloor} 1 8 72 360 1932 7280 28560 86520 267610 671328 1695456 3570336 7477008 13261248 23090496 34285680 49202010

一番下の段の総和を求めると 133,645,905 となります。ハトのボドゲと比べると*3、この時点でもかなり少ないですね。

対称性を考える

さて、実際に解析を行う際は、ルール上区別されない全く同じ局面は同一視することを考えます。例えば以下のいずれかを満たすような局面のペアは同じものと考えてよさそうです。

  • ポジションをひとつずらして一致する局面のペア
  • ある対称軸に対してポジションを入れ替えると一致する局面のペア

このように対称性を課した際の個数を数えるときに便利なのが、以下のバーンサイド補題です*4
ja.wikipedia.org
主張だけ抜き出すと、以下の通りです。

G が集合 X に作用しているとする。各 g \in G に対し、g で不変な X の元全体の集合を X^{g} とする。このとき以下が成り立つ:
|X/G| = \dfrac{1}{|G|} \displaystyle \sum_{g \in G} |X^{g}|

今回のケースに適用するなら、群 G は「ポジションを1つ分となりに回転させる」と「ポジションをある対称軸について反転させる」で生成される変換の群になります。これはちょうど正8角形の二面体群と同型になります (したがって |G|=16)。また X はまさに前節で計算した場合全体に対応します。対称性を加味して省いた局面数は |X/G| に対応するので、そのままこの補題が使えることが分かります。

対称性を加味して改めてカウント

G の元は以下の6種類に分類されます:
(a) 単位元 (ポジションを変えないもの):1個
(b) ポジションを180°回転させるもの:1個
(c) ポジションを90°回転させるもの (時計回り・反時計回り):2個
(d) ポジションを45° または 135°回転させるもの (時計回り・反時計まわり):4個
(e) ポジションを通らない対称軸で反転させるもの:4個
(f) 二つのポジションを通る対称軸で反転させるもの:4個
ここで、同じ分類に属す g については |X^{g}| は共通です(証明は省きます)。よって各分類に対して |X^{g}| を数えればよさそうです。
以下では数が比較的多い s=15, 16 の場合について数えてみることにします*5

s=16 のケース

白黒共に8個ずつボードに置かれている局面です。

(a) 単位元

|X^g|=49202010 (対称性を課さないケースで計算済み)

(b) ポジションを180°回転させるもの

この変換で対象な割り振り方は a_{i}=a_{i+4} (i = 1, 2, 3, 4) を満たすもので、この総数は
(1 + x^{2} + x^{4} + x^{6})^{4}x^{16} の係数 = 31
となります。各割り振り方への白石の割り当て方は
{}_{8}\mathrm{C}_{4} = 70 通り
なので |X^{g}| = 31 \times 70 = 2170 となります。

(c) ポジションを90°回転させるもの (時計回り・反時計回り)

この変換で対象な割り振り方は a_{i}=a_{i+2}=a_{i+4}=a_{i+6} (i = 1, 2) を満たすもので、この総数は
(1 + x^{4} + x^{8} + x^{12})^{2}x^{16} の係数 = 3
となります。各割り振り方への白石の割り当て方は
{}_{4}\mathrm{C}_{2} = 6 通り
なので |X^{g}| = 3 \times 6 = 18 となります。

(d) ポジションを45° または 135°回転させるもの (時計回り・反時計まわり)

この変換で対象な割り振り方は a_{1}=a_{2}=\dots=a_{8} を満たすもので、これは1通りです。各割り振り方への白石の割り当て方は
{}_{2}\mathrm{C}_{1} = 2 通り
なので |X^{g}| = 2 となります。

(e) ポジションを通らない対称軸で反転させるもの

この変換で対象な割り振り方は (番号を適当に付け替えて) a_{i}=a_{9-i} (i = 1, 2, 3, 4) を満たすもので、この総数は
(1 + x^{2} + x^{4} + x^{6})^{4}x^{16} の係数 = 31
となります。各割り振り方への白石の割り当て方は
{}_{8}\mathrm{C}_{4} = 70 通り
なので |X^{g}| = 31 \times 70 = 2170 となります。

(f) 二つのポジションを通る対称軸で反転させるもの

この変換で対象な割り振り方は (番号を適当に付け替えて) a_{i}=a_{9-i} (i = 2, 3, 4) を満たすもので、この総数は
(1 + x + x^{2} + x^{3})^{2} (1 + x^{2} + x^{4} + x^{6})^{3}x^{16} の係数
となります。ここで (1 + x + x^{2} + x^{3})^{2}a_{1}, a_{5} の割り振り、もう一方は残りの割り振りに対応します。
ただ、このケースは割り振り方によって白石の割り当て方に違いが出てきます。
対称軸の通るポジションに置かれる石の総数を p, それ以外を 2q とすると、p + 2q = 16 となります。これを用いると、白石の割り当て方は
\displaystyle\sum_{k = 0}^{ 4 } {}_{q}\mathrm{C}_{k} \cdot {}_{p}\mathrm{C}_{8-2k} 通り
となります*6。各 (p, q) の組み合わせに対応する値は以下の通りです。

(p, q) (0, 8) (2, 7) (4, 6) (6, 5)
\displaystyle\sum_{k = 0}^{ 4 } {}_{q}\mathrm{C}_{k} \cdot {}_{p}\mathrm{C}_{8-2k} 70 70 150 310

さて
(1 + x + x^{2} + x^{3})^{2} = 1 + 3x^{2} + 3x^{4} + 1x^{6} +(その他の項)
となり、各項がそれぞれ p = 0, 2, 4, 6 に対応する割り振りを表していることを考えれば
|X^{g}| = (1 \cdot 70 + 3 \cdot 70 x^{2} + 3 \cdot 150 x^{4} + 1 \cdot 310 x^{6})(1 + x^{2} + x^{4} + x^{6})^{3}x^{16} の係数 =9690
となります。

総計

以上の計算をまとめると
|X/G| = \dfrac{1}{16} \left( 49202010 + 2170 + 18 \cdot 2 + 2 \cdot 4 + 2170 \cdot 4 + 9690 \cdot 4 \right) = 3078229
となります。

s=15 のケース

黒石が8個、白石が7個ボードに置かれている局面です。

(a) 単位元

|X^{g}| = 34285680 (対称性を課さないケースで計算済み)

(b) ポジションを180°回転させるもの

いずれかの石の数が奇数の場合、この対称性を持つ配置はありません。

(c) ポジションを90°回転させるもの (時計回り・反時計回り)

いずれかの石の数が4の倍数でない場合、この対称性を持つ配置はありません。

(d) ポジションを45° または 135°回転させるもの (時計回り・反時計まわり)

いずれかの石の数が8の倍数でない場合、この対称性を持つ配置はありません。

(e) ポジションを通らない対称軸で反転させるもの

いずれかの石の数が奇数の場合、この対称性を持つ配置はありません。

(f) 二つのポジションを通る対称軸で反転させるもの

s=16 の場合と同じ記法を用いると、今回は p + 2q = 15 なので

(p, q) (1, 7) (3, 6) (5, 4)
\displaystyle\sum_{k = 0}^{ 3 } {}_{q}\mathrm{C}_{k} \cdot {}_{p}\mathrm{C}_{7-2k} 35 75 155

となります。さて
(1 + x + x^{2} + x^{3})^{2} = 2x + 4x^{3} + 2x^{5} +(その他の項)
となり、各項がそれぞれ p = 1, 3, 5 に対応する割り振りを表していることを考えれば
|X^{g}| = (2 \cdot 35 x + 4 \cdot 75 x^{3} + 2 \cdot 155 x^{5})(1 + x^{2} + x^{4} + x^{6})^{3}x^{15} の係数 =7140
となります。

総計

以上の計算をまとめると
|X/G| = \dfrac{1}{16} \left( 34285680 + 7140 \cdot 4 \right) = 2144640
となります。

対称性を加味してざっくりカウント

s=15, 16 の場合について説明してきましたが、きっちり計算するのはそこそこ面倒くさいことが分かります。ただよく計算内容を観察してみると、ほぼほぼ (a) の数値÷16になっていることがわかります。(a) の数値はすなわち対称性を加味せずカウントした数に他なりません。なので、ざっくり考えれば、対称性を課したカウントは、対称性を課さない数値を16で割れば大体外さないでしょう。この値は
133,645,905/16 \approx 835,2869 \approx 8 \cdot 10^{6}
となります。

結果の吟味

対称性を加味した上ですべての局面を保存するのにどれぐらいの保存容量が必要か考えてみます。仮にひとつの局面を保存するのに4バイト必要だと考えると*7、おおよそ32MBあれば十分であることが分かります。これぐらいであればそのへんのノートPCでも余裕でメモリに乗る範囲でしょう。ハトの解析で必要になって128GBのメモリを積んだ身としては、全然楽勝そうだなと思えます。
気が向いたら、このボドゲも解析したいと思います。

追記 (2023/12/19)

プログラムを使って実際にカウントしてみました。
(石の総数)⇒(パターン数) と表記しています。

0 => 1
1 => 1
2 => 6
3 => 27
4 => 139
5 => 478
6 => 1826
7 => 5487
8 => 16933
9 => 42192
10 => 106332
11 => 223700
12 => 468444
13 => 829912
14 => 1444680
15 => 2144640
16 => 3078229
Total = 8363027

*1:\lfloor x \rfloorx 以下の最大の整数を表します。

*2:石の場所はすべて区別されます。ポジションに加えて、何段目かという情報が加わるので、s 個の場所はすべて別物として考えられます。

*3:【Tokyo Doves】実現可能局面数をざっくり見積もってみる - ぷりんの雑記帳

*4:京都大学数学作問サークルのしゃかやみさん https://twitter.com/shakayami_ に教えてもらいました。ありがとうございます!

*5:実は (f) 以外は自然に一般化できます。(f) だけは少々面倒くさいです。

*6:ゲームのルールから 0 \leq p \leq 6 なので 5 \leq q \leq 8 であり, {}_{q}\mathrm{C}_{k} が未定義になることはありません。

*7:対称性を考慮しないパターンは 2^{32} より小さいので、素朴には各局面は4バイトぐらいあれば表現できそうです。

【Tokyo Doves】Rust のクレートを crates.io に公開しました

タイトルの通りですが、Tokyo Doves の解析で使った Rust のコア部分を抽出して整理したものを crates.io に公開しました。URL は以下です。
https://crates.io/crates/tokyodoves

使い方は上記ページにある Documentation のページに詳しめに書いたので、そちらを読んでみてください。同じ内容のものは github の以下のページにも置いています。
mat-der-d.github.io

今後はこのクレートを使って、過去の記事で行った解析の実装方法を紹介していけたらいいなと思っています。

とりいそぎご報告でした。ではまた。

Rust の cargo test でセキュリティソフトと格闘していたら doctest 激重問題が解消された件

こんにちは。Rust の doctest まわりでひとつ問題回避に成功したので、メモとして残しておきます。

doctest でエラー発生

先ほど元気に Rust の開発をしていたら、doctest で以下のようなテストが出てしまいました。

doctest errors

このエラーが出る前に、そういえば下のようなポップアップが出ていたような気がします。

セキュリティソフトの警告

見るからに、doctest を実行するときに生成された実行ファイルを消しにかかったな・・・ということで、回避のために奮闘しました。

回避策を模索

まずなによりセキュリティソフトの設定で例外設定を試みます。オンラインドキュメントを見ると「例外のファイルとかフォルダとか設定できるよ!」とのこと。早速設定しようとしたのですが、上記のファイルの場所からわかる通り、実行の際にフォルダ名を自動生成しているようで、直接指定は適いそうにありません。かといって %USERPROFILE%\AppData\Local\Temp を例外設定してしまうのはセキュリティ的に不安しかありません。

そこで cargo test で生成される一時ファイルの保存先を変える方法はないか・・・と思ってググったところ、まさに同じ状況のやり取りが見つかりました↓
github.com
要は「一時フォルダを指定する環境変数を、テストの実行直前に書き換えればOK」ということのようです。

解決編

上を組み合わせて以下のようにしてみました (OS は Windows11 で, PowerShell 上で実行の例):

  1. 適当な場所に作業用フォルダを作り、セキュリティソフトの例外設定をしておく
  2. 上記フォルダを環境変数に登録しておく (今回は TEMP_NO_SECURITY にします)
  3. テスト実行時に直前で環境変数 TMP を TEMP_NO_SECURITY に書き換え + テスト実行後に戻し (私の環境では TEMP も同じ値を入れているので、それで上書き)*1
PS $> $env:TMP=$env:TEMP_NO_SECURITY; cargo test --release; $env:TMP=$env:TEMP

これによりセキュリティソフトの監視から外れて無事テストを実行できるようになりました。
上記リンク先にあるように、これは windows の一時フォルダの取得先を変更していることに起因するので、Rust のコード自身が一時フォルダを参照するものであれば挙動が変更しうることには注意してください。
めでたしめでたし。

うれしい副産物編

で、めでたいついでに、嬉しい副産物がありました。
doctest を実行すると、その数や内容に対して理不尽なほど重いという問題がありました。
参考:
minerva.mamansoft.net
私のコードの場合 doctest 用のコード片が20個ほどあった (いずれも大したことのない内容で、通常の test の方が100倍以上重いものです) のですが、すべて実行するのに5分程度かかっていました。
今回の一時フォルダのパスを書き換える操作を行ったところ、なんと7秒ぐらいで終わるようになりました!(というかこうでないと困るのですが・・・)
なぜフォルダを切り替えただけでまともな時間になったのか、セキュリティソフトが問題だったのか他の何かなのかは不明ですが、小一時間ググっても解決策が出てこなかった問題が突然解決したので、お困りの方はお試しください(何か問題が起きても責任は取れないので自己責任で・・・)。

おまけ

VSCode の tasks.json には以下のように設定しました:

{
    "version": "2.0.0",
    "tasks": [
        // 中略
        {
            "type": "shell",
            "command": "$env:TMP=$env:TEMP_NO_SECURITY; cargo test --release",
            "problemMatcher": [],
            "group": {
                "kind": "build",
                "isDefault": false
            },
            "label": "rust: cargo test release full (bypass security check)"
        }
    ]
}

*1:$env:***=*** という構文はそのプロセスでのみ一時的に環境変数を書き換えるコマンドなので、テストが終わったら PowerShell のプロセスを終了させるのであれば最後の戻し作業は不要です。

Rust の Rc<T> をディープコピーする方法

こんにちは。Rust には参照カウント方式で複数の所有権を持たせるスマートポインタの Rc ("R"eference "C"ount) があります。
doc.rust-lang.org
こいつのディープコピー、すなわち参照カウントが1で参照先が clone された状態のものを作りたい状況があったのですが、やりかたを調べるのに意外と時間がかかったのでメモとして残しておきます。

まず Rust だと「clone は何もせずとも deep copy」という雰囲気をまとっているので *1、何も考えず書くと以下のようになると思います。がこれは assertion error になります。

use std::rc::Rc;

#[derive(Debug, Clone)]
struct Foo {}

fn main() {
    let rcfoo = Rc::new(Foo{});
    let rcfoo2 = rcfoo.clone(); // 実は shallow copy
    assert_eq!(1, Rc::strong_count(&rcfoo)); // assertion error!
    println!("{:?}", rcfoo2);
}

理由は、Rc における clone は「参照カウントをインクリメントして新しい所有権を作る」という操作に割り当てられているからです。まさに shallow copy です。中身を clone して参照カウント1のものを作るには、文字通り、中身への参照を as_ref() でもらった上でそれを clone して、さらに Rc::new してやれば実現できます。

use std::rc::Rc;

#[derive(Debug, Clone)]
struct Foo {}

fn main() {
    let rcfoo = Rc::new(Foo{});
    let rcfoo2 = Rc::new(rcfoo.as_ref().clone()); // deep copy
    assert_eq!(1, Rc::strong_count(&rcfoo)); // ok
    println!("{:?}", rcfoo2);
}

これでめでたしめでたし…でも良かったんですが、Rc の中に入っている struct のフィールドにまた Rc に包まった値が入っているケースでは、やはりそのフィールドについては shallow copy になってしまいます。これは clone() の仕様として、各フィールドに対して再帰的に clone() するように実装されていることが原因です。

use std::rc::Rc;

#[derive(Debug, Clone)]
struct Foo {rcbaa: Rc<Baa>}

impl Foo {
    fn new() -> Self {
        Foo { rcbaa: Rc::new(Baa{}) }
    }
}

#[derive(Debug, Clone)]
struct Baa {}

fn main() {
    let rcfoo = Rc::new(Foo::new());
    let rcfoo2 = Rc::new(rcfoo.as_ref().clone()); // deep copy...?
    assert_eq!(1, Rc::strong_count(&rcfoo)); // ok
    assert_eq!(1, Rc::strong_count(&rcfoo.rcbaa)); // assestion error!
    println!("{:?}", rcfoo2);
}

解決方法はほぼ同じで、フィールドに対しても Rc::new(as_ref().clone()) を行うようにすれば ok です。
以下はこれを自前の trait DeepClone を作って実装する形で実現してみた例です。

use std::rc::Rc;

trait DeepClone {
    fn deep_clone(&self) -> Self;
}

impl<T> DeepClone for Rc<T>
where
    T: DeepClone,
{
    fn deep_clone(&self) -> Self {
        Rc::new(self.as_ref().deep_clone())
    }
}

#[derive(Debug, Clone)]
struct Foo {rcbaa: Rc<Baa>}

impl Foo {
    fn new() -> Self {
        Foo { rcbaa: Rc::new(Baa{}) }
    }
}

impl DeepClone for Foo {
    fn deep_clone(&self) -> Self {
        Foo { rcbaa: self.rcbaa.deep_clone() }
    }
}

#[derive(Debug, Clone)]
struct Baa {}

impl DeepClone for Baa {
    fn deep_clone(&self) -> Self {
        self.clone()
    }
}

fn main() {
    let rcfoo = Rc::new(Foo::new());
    let rcfoo2 = rcfoo.deep_clone(); // deep copy!
    assert_eq!(1, Rc::strong_count(&rcfoo)); // ok
    assert_eq!(1, Rc::strong_count(&rcfoo.rcbaa)); // ok
    println!("{:?}", rcfoo2);
}

最後にグラフを扱うときに出てくるような、再帰的な型で実装してみた例を書いておきます。

use std::rc::Rc;

trait DeepClone {
    fn deep_clone(&self) -> Self;
}

impl<T> DeepClone for Rc<T>
where
    T: DeepClone,
{
    fn deep_clone(&self) -> Self {
        Rc::new(self.as_ref().deep_clone())
    }
}

#[derive(Debug, Clone)]
enum Foo {
    Nil,
    Con(Rc<Foo>)
}

impl DeepClone for Foo {
    fn deep_clone(&self) -> Self {
        use Foo::*;
        match self {
            Nil => Nil,
            Con(rcfoo) => {
                Con(rcfoo.deep_clone())
            }
        }
    }
}

fn main() {
    let foo = Foo::Con(Rc::new(Foo::Nil));
    let rcfoo = Rc::new(foo);
    let rcfoo2 = rcfoo.deep_clone(); // deep copy!
    assert_eq!(1, Rc::strong_count(&rcfoo)); // ok
    if let Foo::Con(rcfoo_) = rcfoo.as_ref() {
        assert_eq!(1, Rc::strong_count(rcfoo_)); // ok
    }
    println!("{:?}", rcfoo2);
}

実は上記の書き方では、ループのあるグラフの場合には無限ループになるので、そのうち stackoverflow を起こして死にます。しかしまともにループのあるグラフを実装している場合は弱参照を適切に使うはずなので*2、たぶん std::rc::Weak にも DeepClone を実装しておいてあげれば解決するのかな?と想像します。がそろそろ疲れたので、今回はこのへんで終わりにします。ではまた。

*1:参考: https://qiita.com/namn1125/items/2d84086394e97489776a

*2:参照カウント方式で何も考えず循環参照をつくると、互いに参照カウント1つ分を持ってしまうので、メモリ解放できなくなります。参考: https://doc.rust-jp.rs/book-ja/ch15-06-reference-cycles.html

Rust で入れ子のコンテナを平たくイテレートするイテレータ

こんにちは。
Rust のイテレータまわりでちょっと手こずったので、後日のためのメモです。

まずは次のコードを見てください:

fn main() {
    let vec = vec![vec![0], vec![1, 2], vec![3, 4, 5]];

    for sub_vec in vec.iter() {
        for x in sub_vec.iter() {
            println!("{}", x);
        }
    }
}

実行したい方はこちら:
https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=9eb206547a1d0390ad22b5137bae4086

これを実行すると 0, 1, 2, 3, 4, 5 の順に表示されます。vec がちょうど入れ子のコンテナになっていて、二重ループで中身をひとつひとつ取り出している形です。
これを、以下のようなコードで実現することを考えます:

struct MyContainer(Vec<Vec<u32>>);

impl MyContainer {
    fn iter(&self) -> ??? {
        ???
    }
}

fn main() {
    let vec = vec![vec![0], vec![1, 2], vec![3, 4, 5]];
    let con = MyContainer(vec);
    
    for x in con.iter() {
        println!("{}", x);
    }
}

iter メソッドの返り値となる型を定義する必要があり、そこでは Iterator を実装している必要がある、という感じです。
私なりの答えはこうです↓

struct MyContainer(Vec<Vec<u32>>);

impl MyContainer {
    fn iter(&self) -> MyIter<'_> {
        MyIter::new(self)
    }
}

struct MyIter<'a> {
    con: &'a MyContainer,
    iter: Option<(std::slice::Iter<'a, Vec<u32>>, std::slice::Iter<'a, u32>)>,
}

impl<'a> MyIter<'a> {
    fn new(con: &'a MyContainer) -> Self {
        Self { con, iter: None }
    }
}

impl<'a> Iterator for MyIter<'a> {
    type Item = &'a u32;
    fn next(&mut self) -> Option<Self::Item> {
        let Some(iter) = &mut self.iter else {
            // self.iter を準備する
            let mut outer = self.con.0.iter();
            let Some(inner) = outer.next() else {
                return None; // 内側の Vec がひとつも入っていなかった場合: おしまい
            };
            self.iter = Some((outer, inner.iter()));
            return self.next(); // 仕切り直し
        };

        let outer = &mut iter.0;
        let inner = &mut iter.1;

        // inner から取り出す
        let Some(item) = inner.next() else {
            // なかった場合は, outer からとってきて inner に入れる
            let Some(new_inner) = outer.next() else {
                return None; // outer が品切れパターン: おしまい
            };
            iter.1 = new_inner.iter();
            return self.next(); // 仕切り直し
        };
        Some(item)
    }
}

fn main() {
    let vec = vec![vec![0], vec![1, 2], vec![3, 4, 5]];
    let con = MyContainer(vec);

    for x in con.iter() {
        println!("{}", x);
    }
}

実行したい方はこちら:
play.rust-lang.org

中間状態を保存するために、外側のイテレータと内側のイテレータを補完するためのフィールドを準備しています。あえて Option にすることでコンストラクタを呼ぶタイミングではイテレータをセットせず、next のタイミングですっからかん判定ができるようになっています。これをしないと「あれっ??取り出せると思ってた内側のイテレータ準備できない・・・」ってなってつらくなります。
三重・四重の入れ子でも似たような実装で実現できると思うんですが、多少面倒くさいので、もっと楽な方法 (あるいは↑を書かなくてもさっと実現できるクレートとか) があれば教えてください。

【Tokyo Doves】棋譜の書き方

お久しぶりです。ここ数カ月いろいろバタバタしていて久しぶりの更新です。今回は Tokyo Doves棋譜の記法について考えた案を紹介したいと思います。

ハトの種類の表記法

ハトは6種類います。それぞれをローマ字にした時の頭文字をとるようにします。

  • ボスハト → Boss hato
  • アニキハト → Aniki hato
  • ヤイバト → Yaibato
  • 豆でっぽうバト → Mamedeppo bato
  • トツハト → Totsu hato
  • ハジケハト → Hajike hato

ところでハトには赤色と緑色がいます。赤色は大文字で、緑色は小文字で表すようにします。
例1. 赤のボスハト → B
例2. 緑のハジケハト → h

座標の表記法

Tokyo Doves は盤面が理論上いくらでも動くことが特徴なので、常に盤上にいることが保証される赤のボスハトを基準に座標を振ります。

N3W3
N3W2
N3W1
N3
N3E1
N3E2
N3E3
N2W3
N2W2
N2W1
N2
N2E1
N2E2
N2E3
N1W3
N1W2
N1W1
N1
N1E1
N1E2
N1E3
W3
W2
W1
B
E1
E2
E3
S1W3
S1W2
S1W1
S1
S1E1
S1E2
S1E3
S2W3
S2W2
S2W1
S2
S2E1
S2E2
S2E3
S3W3
S3W2
S3W1
S3
S3E1
S3E2
S3E3

赤のプレイヤーから見て、マスのボスハトからの相対的な位置を東(E)西(W)南(S)北(N)の座標で表しています。

操作の表記

手番で行える動作は3つあります:

  • ハトを出す
  • ハトを動かす
  • ハトを回収する

ハトを出す場合は "+", ハトを回収する場合は "-" を先頭につけて表現します*1
例1) +HN1E2 → 赤のハジケハトをN1E2に出す
例2) mS2W1 → 緑の豆でっぽうバトをS2W1に動かす
例3) BN1W1 → 赤のボスハトをN1W1に動かす
例4) -a → 緑のアニキハトを回収する

棋譜の例

今回の記法を使って、試しに以下の動画の棋譜を起こしてみたいと思います。
www.youtube.com

以下が棋譜です。

+yN1W1
+TE1
yW1
+AS1W1
BS1*2
+hW2
TN1
+tN1E1
BE1
hS1W1
+MN1E1
hE1
mN2
+aS1E1
TS1W1
aS1
BN1E1
hW2
+HS1W2
hN1
BS1
aS1
HN2W3
bN1W2
BW1
+mN1E1
TW1
aS1
BS1W1
mN1E1
MN2E2
tN3E1
HN2E1
tN3W1
+YN3E1
bN3
AN2

以上37手

*1:実はどちらの記号も省略しようと思えばできます。ハトの種類と座標を書いたとき、場に出ていなければ「出す」場に出ていれば「動かす」と解釈することができるからです。回収に至っては「座標がなければ回収」と考えることができます。しかしこの記号を省くことでかえって読みにくくなる気がするので、あえてつけています。

*2:本当はルール違反。チュートリアルの作成ミス?

謎のボードゲーム「OQ」のルールを類推してみた

こんにちは。今日たまたまこんな youtube チャンネルを見つけました↓
www.youtube.com
投稿者さんの自作?のボードゲームで自己対戦する動画をひたすら投稿されています。
見てみると結構面白いゲームだな~となったので、数本みて類推したルールを書き出しておきます。

駒の種類

駒は20枚あり、表にA、裏に別の文字が書いてある。以下はその一覧。

文字 由来? 枚数 召喚効果 移動 その他
A Anonymous? 20 - 上下左右 相手の駒を攻撃できない
O Out? 10 自分を除外 -
Q Queen 1 八方を攻撃 飛車+角行 -
K King 2 八方を攻撃 王将 ひとつでも取られると負け
R Rook 1 十字を攻撃 飛車 -
B Bishop 1 斜めを攻撃 角行 -
G Gold 1 十字を攻撃 金将 -
S Silver 1 斜めを攻撃 銀将 -
W Wall? 3 なし 動けない 攻撃されない(無効)

おそらくゲーム名の OQ は O と Q の駒から来ている?

初期配置

Aの面を上にして以下のように配置する:

A A A A A A A
A A A

手番の行動

次のいずれかを選んで実行

  • 自分のいずれかのAをひっくり返す
  • 自分のいずれかの駒を動かす
  • 手駒をいずれかの空きマスに置く

自分のいずれかのAをひっくり返す場合

ひっくり返したら、出た文字に応じて召喚効果を発動させる。

  • O → その駒をゲームから除外
  • W → なにもしない
  • R/G → 上下左右4マスに相手の駒があれば、それらすべてに同時に攻撃する
  • B/S → 斜め4マスに相手の駒があれば、それらすべてに同時に攻撃する
  • Q/K → 上下左右斜め8マスに相手の駒があれば、それらすべてに同時に攻撃する

攻撃対象がある場合、その駒の文字に応じて以下を行う:

  • A → 盤面から取り除き、あなたの手駒とする
  • W → なにもしない (攻撃無効化)
  • その他 → ゲームから除外 (like チェス)

自分のいずれかの駒を動かす場合

各駒の種類に合わせた動かし方ができる。

  • A → 上下左右1マスに移動可能
  • W → 動かせない
  • その他 → 「駒の種類」の表参照。将棋の駒の名前と同じ動かし方ができる。

駒を動かすルールは将棋やチェスとほぼ同様。移動先に相手の駒がある場合は、その駒を攻撃したとみなす (=Aなら手駒、それ以外なら除外)。W はそもそも攻撃が無効化されるので、Wがあるマスには移動させることができない。また A は相手の駒を攻撃できない (=相手の駒があるマスには移動できない)。

手駒をいずれかの空きマスに置く場合

置くだけ。

勝敗判定

追記(2023/1/9): 概要欄にルール説明があるのを見つけたので編集しました

手番の行動直後に勝敗判定を行う。以下いずれかに該当すると敗北

  • 手駒を含めてW以外の自分の駒がない (out)
  • K を一枚でも取られる (K.out)
  • Rock: 基準不明。コマを動かせなくなった場合に成立*1
  • Draw: お互いが合意した場合の引き分け(2023/1/9 追記)

勝敗がつかない場合は相手の手番。

*1:一回だけこのルールで勝敗が決まった回があったのですが、どの回だったか忘れてしまいました・・・見つけたらリンクを貼っておきます。(追記) 見つけました: 私はわたしと対戦する ボードゲーム「OQ」 52 - YouTube