Adsense

2018年1月18日木曜日

Spring Statemachineを使ってドラクエ11のポーカーを自動プレイするプログラムを作ってロイヤルストレートスライムを出すまでの道のり 第4回

今回からいいかげんドラクエ11のポーカーの話をしていきたいと思います。

状態遷移図

さっそくですが、今回のツールで作成した状態遷移図は下記のとおりです。

初期状態

まず、状態遷移マシンが起動すると最初のアクションとしてPS4リモートプレイのウィンドウを前面に出す処理を実行します。JavaのRobot APIでキー操作するには、PS4リモートプレイのウィンドウが最前面に出ていないといけないので、初期処理として実行しています。

PLAYING_POKER_STATE

続いてPLAYING_POKER_STATEという状態に入ります。これは単純にポーカーを自動プレイ中の状態を表しているだけですね。

重要なのはPLAYING_POKER_STATEの子ステートである、OTHER_STATEとDEALT_CARDS_STATEです。

DEALT_CARDS_STATE

まずは右側のDEALT_CARDS_STATE状態ですが、これは5枚のカードが配られてカードを選択する状態を示します。下記の画像の状態ですね。

この状態では、残すカードを選択して"くばる"ボタンを押すことでポーカーを進める必要があるわけですが、"くばる"ボタンを押すためにはカーソルを下に移動する必要があります。つまり、この画面ではEnterキー(○ボタン)を押しているだけではポーカーは進まないわけです。(1番左のカードの"かえる"と"のこす"が交互に切り替わるだけ)
この画面の状態を正しく認識して"くばる"ボタンを押せるようになることが、ポーカーを延々自動プレイするための一番の鍵になるということです。

OTHER_STATE

次にOTHER_STATEというやる気のない名前の状態が左側にありますが、これは簡単に言うとEnterキー(○ボタン)さえ押していれば進んでいく状態です。本来は役が完成した後のダブルアップチャンスをチャレンジしている状態や、役ができなかった場合に再度かけ金を設定する状態など、より細かい状態を定義することはできるのですが、今回のツールの性質を考えればそのような細かい状態を認識する必要は(今のところ)ありません。このツールとしてはEnterさえ押しておけばOKという状態を認識出れば十分です。

PLAYING_POKER状態内では、上記の2つの状態を行ったりきたりしながら操作を行っていきます。具体的には、OTHER_STATE中に画面キャプチャから"くばる"ボタンが表示されていることを検出したら、DEAL_CARDS_EVENTを投げてDEALT_CARDS_STATEに遷移します。DEALT_CARDS_STATEのEntryアクションには、配られたカードを認識した上で残すカードを選択する操作をRobot APIを通じて行い、最後に下キー→Enterキーと押して"くばる"ボタンを押下します。

また、DEALT_CARDS_STATEではOTHER_EVENT(特に画面キャプチャに特殊な状態を表すものが存在しない場合に発行される)を受け取るとOTHER_STATEに状態を戻します。OTHER_STATEではEntryアクションで単純にEnterキーを押すというアクションを設定していて、OTHER_EVENTが受信されるたびにEnterキーを押してポーカーを進めます。(OTHER_STATEからOTHER_STATEの遷移は自己遷移ですが、External Transitionなので一旦OTHER_STATEから出て再度OTHER_STATEに進入するため、Entryアクションは毎回実行されます)

やがて、再度カードが5枚配られ、画面上に"くばる"ボタンが表示されると、DEAL_CARDS_EVENTが発行されて、DEALT_CARDS_STATEに状態を移すという塩梅です。

ROYAL_STRAIGHT_SLIME_EVENT

ここからは、この状態遷移マシンの終了について見ていきます。

まずは中央下のROYAL_STRAIGHT_SLIME_EVENTの遷移ですが、これは画面上の5枚のカードがロイヤルストレートスライムを構成するカードになっていることを検出したら発火されるイベントです。このイベントが発生すると30秒待機したのち、Shareボタンを押してから状態遷移マシンが終了します。

Shareボタンを押すのは、そのままビデオクリップを保存すればロイヤルストレートスライム達成時の動画を保存することができるためです。また、30秒待つのはロイヤルストレートスライム達成時のファンファーレが終了してからビデオクリップを保存したいからです。

BEFORE_BET_COIN_EVENT

次に、右下のBEFORE_BET_COIN_EVENTの線ですが、これは通常は起こり得ない遷移なのですが、念のため保険として設定しているものです。

開発中にツールを実行していると何度か掛け金の設定画面でポーカーが進まなくなっている状況がありました。よくよく見てみると、かけ金の設定が0になっており、Enterキーを押しても進まないという状態です。
どうやら、ポーカーで負けたときに、ポーカーを続けるか聞いてくるダイアログで"いいえ"を選択してしまい、ポーカーを一度終了してしまっているようでした。ポーカーを終了しても状態遷移としてはOTHER_STATEとして認識されるだけなので、OTHER_EVENTが受信され続け、Enterキーをどんどん押していきます。そうすると、ディーラーに再度話しかけ、ポーカーをプレイし始めるところまでは行くのですが、掛け金のデフォルトが0のため、そこでEnterキーを押しても進めなくなっているということのようでした。

通常、ポーカーの継続で"いいえ"を選択することはありえないのですが、ネットワーク遅延やその他要因によるキーの取りこぼし等の予期せぬ動作が重なり、そういったことが発生しうるようです。そこで、掛け金入力欄が表示されており、その値が0であることを認識した場合は、BEFOR_BET_COIN_EVENTを送信して、RETRY_OR_END_STATEという選択状態に遷移し、ポーカーをリトライするか、ツールの終了を選択するようモデリングしました。


RETRY_OR_END_STATE

BEFORE_BET_COIN_EVENTを受信して、予期せぬ状態(一旦ポーカーを終了してしまった状態)になった場合に、ポーカーをリトライするか、ツールを終了するかを選択する状態に入りします。

デフォルトの設定ではリトライを行うようになっており、上キーを押してかけ金の決定を行った後にEnterキーを押すアクションを設定しています。このアクションの結果、OTHER_STATEに復帰します。

DEALT_CARDS_STATEのInternal Transition

最後に、DEALT_CARDS_STATEの箱の中に、「状態に入って60秒たった」、「"くばる"ボタンを再度押下」という記述があります。これも、予期せぬ状態になったときの防護策として入れているもので、DEALT_CARDS_STATEに入ってから60秒たっても他の状態に遷移しない場合は、"くばる"ボタンを押すのに失敗したとみなし、下キー→Enterキーと押して再度"くばる"ボタンを押そうとします。

これはキーの取りこぼし等でうまく下キーが押せずに、"くばる"ボタンにカーソルが移動しなかったときのことを考慮した結果入れている処理です。上でも書いたように、ここで下キーを押せるかどうかがポーカーを延々プレイし続けることができるかどうかの鍵になっているので、下キーの取りこぼしが万が一起きても復帰できるように念には念を入れています。

なお、この処理はInternal Transition(内部遷移)とガード条件を組み合わせてモデリングしています。内部遷移は状態変化を伴わない遷移です。DEALT_CARDS_STATE中にDEAL_CARDS_EVENT("くばる"ボタンの検出イベント)を受信すると内部遷移のガード条件が検証されます。ガード条件では、あらかじめ記録してあったDEALT_CARDS_STATEの状態に入った際の時刻を参照し、60秒経過しているかを確認します。60秒経過していなければガード条件はfalseとなり内部遷移は実行されません。60秒が経過していれば内部遷移が実行され、遷移に紐付いたアクションで"くばる"ボタンを再度押しに行きます。

ソースコード

では、さっそく上記のようにモデリングした状態遷移図のSpring Statemachineでの実装を見ていきましょう。

なお、ソースはGitHubにあげていますので、詳細が見たければご参照ください。

States


/**
 * 状態。
 */
public enum States {

    /** ポーカープレイ中の状態。 */
    PLAYING_POKER_STATE,
    
    /** カード配布済み状態("くばる"ボタンが表示されている)。 */
    DEALT_CARDS_STATE,

    /** 予期せぬ状態になったとき、リトライかアプリケーション終了を選択する状態。 */
    RETRY_OR_END_STATE,

    /** その他の状態。 */
    OTHER_STATE,

    /** 終了状態。 */
    FINAL_STATE

}

Events


/**
 * イベント。
 */
public enum Events {

    /** カード配布検知イベント。 */
    DEAL_CARDS_EVENT,

    /** ロイヤルストレートスライム検知イベント。 */
    ROYAL_STRAIGHT_SLIME_EVENT,

    /** かけ金入力待ち検知イベント。 */
    BEFORE_BET_COIN_EVENT,

    /** その他イベント。 */
    OTHER_EVENT,

}

Config


    @Override
    public void configure(StateMachineStateConfigurer<States, Events> states) throws Exception {
        // 各状態とEntry Actionの定義。
        states.withStates()
                .initial(PLAYING_POKER_STATE, activateWindowAction())
                .choice(RETRY_OR_END_STATE)
                .end(FINAL_STATE)
                .and()
                .withStates()
                    .parent(PLAYING_POKER_STATE)
                    .initial(OTHER_STATE)
                    .state(OTHER_STATE, enterKeyPushAction(), null)
                    .state(DEALT_CARDS_STATE, decideExchangeCardAction(), null);
    }

    @Override
    public void configure(StateMachineTransitionConfigurer<States, Events> transitions) throws Exception {
        transitions
                // ポーカープレイ中状態でロイヤルストレートスライムを検出。終了状態へ遷移。
                .withExternal()
                    .source(PLAYING_POKER_STATE)
                    .target(FINAL_STATE)
                    .event(ROYAL_STRAIGHT_SLIME_EVENT)
                    .action(pushShareButtonAction())
                    .and()
                // かけ金入力欄が0の状態で見つかった場合、何らかの原因でポーカーを終了してしまったとみなし、リトライ・終了選択状態へ遷移。
                .withExternal()
                    .source(PLAYING_POKER_STATE)
                    .target(RETRY_OR_END_STATE)
                    .event(BEFORE_BET_COIN_EVENT)
                    .and()
                // "くばる"ボタンを見つけたら、その他状態からカード配布済み状態へ遷移。
                .withExternal()
                    .source(OTHER_STATE)
                    .target(DEALT_CARDS_STATE)
                    .event(DEAL_CARDS_EVENT)
                    .and()
                // その他状態の継続。Entry ActionでEnterキーを押して先に進める。
                .withExternal()
                    .source(OTHER_STATE)
                    .target(OTHER_STATE)
                    .event(OTHER_EVENT)
                    .and()
                // カード配布済み状態でロイヤルストレートスライムを検出。終了状態へ遷移。
                .withExternal()
                    .source(DEALT_CARDS_STATE)
                    .target(OTHER_STATE)
                    .event(OTHER_EVENT)
                    .and()
                // 予期せぬ状態になったときにリトライかアプリケーション終了を判定する。
                .withChoice()
                    .source(RETRY_OR_END_STATE)
                    .first(PLAYING_POKER_STATE, retryOnUnexpectedStateGuard(), betCoinAction())
                    .last(FINAL_STATE)
                    .and()
                // カード配布済み状態が長く続く場合、"くばる"ボタンの押下に失敗したとみなし、再度ボタンの押下を試みる。
                .withInternal()
                    .source(DEALT_CARDS_STATE)
                    .event(DEAL_CARDS_EVENT)
                    .guard(retryPushDealButtonGuard())
                    .action(pushDealButtonAction())
                    .and();
    }

}

Configクラスでは今まで説明していない要素が出てきています。ひとつひとつ見ていきましょう。

選択状態の定義

7行目でchoiceメソッドを使って選択状態を定義しています。通常の状態定義に使うstateメソッドではないことに注意が必要です。

状態の親子関係定義

状態の親子関係を定義する場合は、10行目のようにwithStates()メソッドのチェーンの中でさらにwithStates()メソッドを呼び出します。さらに11行目のようにparentメソッドを使用して親となる状態を定義します。後は通常通りの状態定義を行っていけば、parentメソッドで定義した状態の子となる状態を定義していくことができます。

選択状態からの遷移

52行目から55行目までは選択状態に入ってからの遷移を定義しています。withChoiceメソッドを呼び出した後、souceメソッドで選択状態を指定します。さらにfirstメソッドでは、第1引数に遷移先状態、第2引数にガード条件(trueになったらこの遷移が行われる)、遷移時のActionを指定します。最後にlastメソッドでfirstメソッドのガード条件がfalseだった場合の遷移先状態を設定しています。

これは、if-else文のうち、ifがfirstメソッド、elseがlastメソッドだと思えば理解しやすいです。なお、3分岐以上する場合(if-elseif-else)、firstメソッドとlastメソッドの間にthenメソッドを呼び出すことで実現できます。

内部遷移(Internal Transition)

58行目では状態遷移を伴わない内部遷移をwithInternalメソッドで定義しています。遷移先がないのでtargetメソッドを呼び出しません。ここの処理については別途後日に取り上げたいと思います。

とりあえず、状態・イベント・Configクラス(状態遷移の設定)の実装をざっと見てみました。

0 件のコメント:

コメントを投稿