悪魔の館から脱出するゲームの別解

## 問題
Twitterで見かけたこれ。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ある悪魔の館に23人の人間が閉じ込められた。<br>これから悪魔は、その23人の脱出を賭けてゲームをするという。<br>以下、ルールを説明する。<br><br>ルール①<br>これから23人はそれぞれ別の個室に一人ずつ閉じ込められる。悪魔から呼び出されるか、脱出条件を満たすまでは、勝手に部屋を出入りしてはならない。</p>&mdash; axel (@axel_tewi) <a href="https://twitter.com/axel_tewi/status/1191632806675304449?ref_src=twsrc%5Etfw">November 5, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 

回答はこっち。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">論理クイズ、答え合わせの時間だオラァ！<br><br>まず例として、<br>・広間の像が北か西を向いている状態をＯＮ<br>・広間の像が東か南を向いている状態をＯＦＦ<br>とする。 <a href="https://t.co/PfCPvraqJa">pic.twitter.com/PfCPvraqJa</a></p>&mdash; axel (@axel_tewi) <a href="https://twitter.com/axel_tewi/status/1192049887723966464?ref_src=twsrc%5Etfw">November 6, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 

## 別解

22カウントで破壊出来る方法。

### 考え方
* 広間の像が北か西を向いている状態をONとする
* 広間の像が東か南を向いている状態をOFFとする
* カウント兼破壊役1人とその他のメンバー22人に分かれる。

ここまでは同じ。

ON/OFFの「切り替え」をメッセージとして扱う。

* OFF->ONをカウント役の「送信」とする
* ON->OFFをメンバーの「返信」とする
* カウント役「送信」→メンバーが確認し「返信」→カウント役が確認。の手順でメンバーをカウントする
* カウント済みのメンバーはON/OFFを切り替えないことでメッセージのやり取りに関わらない

### カウント役のルール
* 像がONの場合、送信の準備をする（ONをOFFにする）
* 像がOFFの場合、「送信」を送る（OFFをONにする）
* 像の向きを変更した後のON/OFFを記憶する
* メンバーの「返信」（ONがOFFになった）を確認したら、カウントを加算する
* カウントが22になったら像を壊す

### メンバーのルール
* カウント役の「送信」（OFFがONになった）を確認したら、「返信」を送る（ONをOFFにする）
* 「返信」を送ったら自分をカウント済みにする
* それ以外（初回・前回ON・像がOFF）の場合、ON/OFFを切り替えない
* カウント済みの場合、ON/OFFを切り替えない
* 像の向きを変更した後のON/OFFを記憶する

## 解説
* 未カウントのメンバー
  - 像がOFFの場合、ON/OFFを切り替えない（OFFを見る）
* カウント役
  - 像がOFFの場合、「送信」を送る（OFFをONにする）
* 未カウントのメンバー
  - カウント役の「送信」（OFFがONになった）を確認したら、「返信」を送る（ONをOFFにする）
* カウント役
  - メンバーの「返信」（ONがOFFになった）を確認したら、カウントを加算する

この流れで未カウントのメンバーを一人カウント出来ます。

像がOFFの場合、メンバーはON/OFFを変更しません。
「送信」後、最初の未カウントメンバーが像をOFFにするため、その後に何人メンバーが来ようとON/OFFは変わらず、カウント役に「返信」が届きます。
これを未カウントのメンバーがいなくなる22カウントまで繰り返せばOK。

「送信」後、未カウントのメンバー（※）が現れる前に、カウント役が広間に入った場合、ONの像を見ることになり、
像がONの場合、送信の準備をする（ONをOFFにする）
↓メンバーはOFFを変更しない
像がOFFの場合、「送信」を送る（OFFをONにする）
と「送信」を再送します。

※「送信」前のOFFを見ていない場合も同じ

初期状態がONの場合も、メンバーは一度OFFを見るまでON/OFFを変更しないので、カウント役の準備→再送の流れになります。

## 実験コード

長々書きましたが、単にコード書いてみたので張りたかっただけという。

```typescript:main.ts
enum Direction {
    North,
    South,
    East,
    West,
}

/**
 * 像
 * 
 * 東西南北の向きがあり、初期方向はランダム
 * 右か左に90度方向を変えることができる
 * 像を壊すことができる
 * 
 * 像の向きが北か西ならONとする
 * 像の向きが東か南ならOFFとする
 * keep()でON/OFFを変更せずに向きを変える
 * change()でON/OFFを変更して向きを変える
 */
class Statue {
    private _direction: Direction;
    private _broken: boolean;

    constructor () {
        this._direction = Math.floor(Math.random() * 4);
        this._broken = false;
    }

    get direction () {
        return this._direction;
    }

    get broken () {
        return this._broken;
    }

    turnRight () {
        switch (this._direction) {
            case Direction.North:
                this._direction = Direction.East;
                break;
            case Direction.South:
                this._direction = Direction.West;
                break;
            case Direction.East:
                this._direction = Direction.South;
                break;
            case Direction.West:
                this._direction = Direction.North;
                break;
        }
    }

    turnLeft () {
        switch (this._direction) {
            case Direction.North:
                this._direction = Direction.West;
                break;
            case Direction.South:
                this._direction = Direction.East;
                break;
            case Direction.East:
                this._direction = Direction.North;
                break;
            case Direction.West:
                this._direction = Direction.South;
                break;
        }
    }

    break () {
        this._broken = true;
    }

    isOn (): boolean {
        return [Direction.North, Direction.West].some(d => d === this._direction);
    }

    isOff (): boolean {
        return [Direction.East, Direction.South].some(d => d === this._direction);
    }

    keep () {
        switch (this._direction) {
            // 北(ON) -> 西(ON)
            case Direction.North:
                this.turnLeft();
                break;
            // 南(OFF) -> 東(OFF)
            case Direction.South:
                this.turnLeft();
                break;
            // 東(OFF) -> 南(ON)
            case Direction.East:
                this.turnRight();
                break;
            // 西(ON) -> 北(ON)
            case Direction.West:
                this.turnRight();
                break;
        }
    }

    change () {
        switch (this._direction) {
            // 北(ON) -> 東(OFF)
            case Direction.North:
                this.turnRight();
                break;
            // 南(OFF) -> 西(ON)
            case Direction.South:
                this.turnRight();
                break;
            // 東(OFF) -> 北(ON)
            case Direction.East:
                this.turnLeft();
                break;
            // 西(ON) -> 南(OFF)
            case Direction.West:
                this.turnLeft();
                break;
        }
    }
}

enum Role {
    Member,
    Counter,
}

/**
 * 参加者
 * 
 * 役割（カウント役orメンバー）があり、
 * 呼び出されたか、カウントされた（と判断した）か、メモ（前回の像のON/OFF）、カウント（カウント役用）を記憶できる
 */
class Player {
    readonly role: Role;
    called: boolean;
    counted: boolean;
    memo: string|null;
    count: number;

    constructor (role: Role) {
        this.role = role;
        this.called = false;
        this.counted = false;
        this.memo = null;
        this.count = 0;
    }
}

function game () {
    // 像を置く。
    let statue = new Statue();
    // 23人で参加する。1人はカウント役とする。
    const players = [];
    const counter = Math.floor(Math.random() * 23);
    for (let i = 0; i < 23; i++) {
        const role = (i === counter) ? Role.Counter : Role.Member;
        players.push(new Player(role));
    }
    // ゲーム開始。像が壊れるまで続く。
    while (!statue.broken) {
        // ランダムで一人呼ばれる。呼ばれたら呼び出し済みにする。
        const player = players[Math.floor(Math.random() * 23)];
        player.called = true;
        // 役割に応じて行動する。
        switch (player.role) {
            case Role.Counter:
                // メンバーメンバーの返信（ONがOFFになった）を確認したら、カウントを加算する
                if (player.memo === 'ON' && statue.isOff()) {
                    player.count++;
                }
                // カウントが22になったら像を壊す
                if (player.count === 22) {
                    statue.break();
                    continue;
                }
                // 像がONの場合、送信の準備をする（ONをOFFにする）
                // 像がOFFの場合、「送信」を送る（OFFをONにする）
                statue.change();
                // 像の向きを変更した後のON/OFFを記憶する
                if (statue.isOn()) player.memo = 'ON';
                if (statue.isOff()) player.memo = 'OFF';
                continue;
            case Role.Member:
                // カウント済みの場合、ON/OFFを切り替えない
                if (player.counted) {
                    statue.keep();
                    continue;
                }
                // カウント役の「送信」（OFFがONになった）を確認したら、「返信」を送る（ONをOFFにする）
                // 「返信」を送ったら自分をカウント済みにする
                if (player.memo === 'OFF' && statue.isOn()) {
                    statue.change();
                    player.counted = true;
                    continue;
                }
                // それ以外（初回・前回ON・今回OFF）の場合、ON/OFFを切り替えない
                statue.keep();
                // ON/OFFを記憶する。
                if (statue.isOn()) player.memo = 'ON';
                if (statue.isOff()) player.memo = 'OFF';
                continue;
        }
    }
    // 像が壊れた後に全員呼び出されていればOK(trueを返す)
    return players.every(p => p.called);
}

for (let i = 0; i < 100000; i++) { // 適当に10万ゲーム実行
    if (!game()) throw "脱出に失敗しました…";
}
console.log('全員生還しました！');

```

ところで、10万回程度だと成功しますが、これで正しいという証明は出来てない気がします。
数学的に証明出来るのかな…