⚔️

【Python】OpenCV + PyAutoGUIでドラクエ自動レベル上げ

2024/12/22に公開

はじめに

こんにちは、Yusei です。記念すべき Tech Blog 第1稿ということで、まずは簡単に自己紹介を。

現在は修士2年で、分子シミュレーションの研究をしています。誇れる結果はありません😵‍💫

最近、インターンをしているベンチャー企業で Tech Conference に登壇させていただき、to B 向けのWebアプリを紹介しました。

簡単にですが、一応アプリの機能紹介ブログも書いています。

学生は自分だけだったので、名刺交換のときにちいかわになっていました。来年からは某メガベンチャーでデータサイエンティストとして働きます。とにかく早く名刺をください。

そして、「ギャンブルは運ではない」をモットーに競馬や麻雀にデータサイエンスで立ち向かっていますが、なぜか負けてばかりですね。。。勝ちたい気持ちが足りないのでしょう。(結局根性論かい)

今回のネタ

さて、今回は昔から好きなゲーム、ドラゴンクエストのレベル上げ自動化システムをご紹介します。最近リリースされた、ドラゴンクエストⅢのHD-2Dリメイク版(Steam)のお話です。

RPGにはつきものであるレベル上げ、普通に面倒じゃないですか?そうです。大体たくさんの経験値がもらえるモンスターとその出現場所が決まっていて、ひたすら狩りまくる作業のことです。

「そこが醍醐味なんでしょ」という意見は受け付けません。僕はボスと戦うときのバトル演出やストーリーに重きを置いているのです。心を空にして何時間も単純作業できるほどメンタルは強くありません。

こういう単純作業を全自動化したくなるのが、エンジニアとしての素質ですよね。いや、めんどくさがりやなだけですね🙄

ドラクエファンの風上にも置けないような発言をしておりますが、許してくださいスクエニさん...

前提知識

ドラクエにはさまざまなモンスターがフィールドに出現します。「ドラクエとかやったことないよ」という方々のために、最低限の知識をお伝えしておきましょう。

  • 戦闘系

    • はぐれメタル(以下はぐメタ)
      今作最大の経験値を持つモンスターです。かわいい見た目ですが異常に防御力が高く、通常攻撃では1ずつしかダメージを与えられません。魔法耐性も最強で、何も通用しません。HPは5しかないのですが、その圧倒的な素早さを活かして高確率で逃げます。普通にイライラします。
    • 会心必中
      会心自体は、相手モンスターの防御力を無視して攻撃できます。通常では2~5%しか発生しませんが、特定の職業で特定の特技を使うと、100%この会心を出せます。
    • ドラゴラム
      自分の姿を龍に変える魔法です。ブレス攻撃をするので、はぐメタの防御力を無視して攻撃できます。魔法は効かないくせにブレス攻撃なら通るんですね。よくわかりません。
  • フィールド系

    • スライム島
      スライム系統のモンスターしか出現しない島です。はぐメタもスライム系統に属する(顔しか似てないだろ)ので、今作ではこの島が一番経験値回収効率が高いと言われています。
    • エンカウント
      ドラクエⅢリメイクはランダムエンカウントです。フィールドを歩いていると、一定確率でモンスターと遭遇します。よって、エンカウントタイミングとモンスターを恣意的に選択できません。

どう実装するのだ

以外と単純です。以下のフローチャートに沿って設計していきます。

はぐメタがいないバトルで「にげる」を選択するのは、単純に時間の無駄だからです。スライム島では、もちろんはぐメタ以外のモンスターも出現します。

中にはこちらを眠らせてきたり、無駄に味方モンスターを回復させる魔法を使うモンスターもいて、まともに戦うと時間を食うことが多々あるのです。効率化の敵です。

ドラクエを少しでもかじったことがある方ならば、「いやいや、にげるコマンド失敗したらどうするよ」というツッコミも思いつくでしょう。実はドラクエの仕様上、一定のレベル差があれば逃げるコマンドは失敗しません。

今回はスライム島ということで、エンカウントするのはしょうもないモンスターがほとんどですので、その心配はほぼ無いに等しいです。

主要ライブラリ

なんだか前置きが長すぎて技術的な話が少しもできていませんでしたが、ご安心ください。ここからはコーディング周りの話に入っていきます。

OpenCV

OpenCVは、コンピュータビジョンと機械学習のためのオープンソースライブラリです。画像処理や画像認識に広く使用されており、今回は主に画面上の特定の要素を検出するために使います。

  • 主な機能
    • 画像の読み込み、表示、保存
    • 画像処理(フィルタリング、エッジ検出など)
    • オブジェクト検出(パターンマッチング)

PyAutoGUI

PyAutoGUIは、GUIの自動化を可能にするPythonライブラリです。今まで「これ全然使い道なくないか?」と思っていましたが、このためにあったんですね。納得。

  • 主な機能
    • マウス操作(移動、クリックなど)
    • キーボード操作(キー入力、ホットキーなど)
    • スクリーンショット取得

コード概観

全てのコードを見る
import cv2
import numpy as np
import mss
import time
import pyautogui
import random

def get_monitor_area_for_top_left_quarter():
    """
    画面左上の1/4領域を計算して返す。
    """
    with mss.mss() as sct:
        monitor = sct.monitors[0]  # 全画面モニタ情報を取得
        return {
            "top": monitor["top"],
            "left": monitor["left"],
            "width": monitor["width"] // 2,
            "height": monitor["height"] // 2
        }

def detect_template_from_screenshot(template_img, threshold, monitor_area):
    """
    指定されたテンプレート画像が画面に存在するかを検出。

    :param template_img: 検出対象のテンプレート画像
    :param threshold: 検出閾値 (0.8推奨)
    :param monitor_area: スクリーンキャプチャ領域 (Noneの場合、全画面)
    :return: True if detected, else False
    """
    with mss.mss() as sct:
        screenshot = sct.grab(monitor_area) if monitor_area else sct.grab(sct.monitors[0])
        screen_img = cv2.cvtColor(np.array(screenshot), cv2.COLOR_BGRA2BGR)

        result = cv2.matchTemplate(screen_img, template_img, cv2.TM_CCOEFF_NORMED)
        return cv2.minMaxLoc(result)[1] >= threshold

def detect_state(templates, threshold, monitor_area):
    """
    現在の状態を検出する。

    :param templates: 各状態のテンプレート画像の辞書
    :param threshold: 検出閾値
    :param monitor_area: スクリーンキャプチャ領域
    :return: 状態 ('lost_metal', 'on_field', 'in_battle', or None)
    """
    for state, img in templates.items():
        if detect_template_from_screenshot(img, threshold, monitor_area):
            return state
    return None

def press_key(key, duration=0.1):
    """キーを指定された時間押す"""
    pyautogui.keyDown(key)
    time.sleep(duration)
    pyautogui.keyUp(key)

def perform_action(state, templates, threshold, monitor_area):
    """
    検出された状態に基づいて適切なキー操作を実行。

    :param state: 検出された状態
    :param templates: 各状態のテンプレート画像の辞書
    :param threshold: 検出閾値
    :param monitor_area: スクリーンキャプチャ領域
    """
    if state == "lost_metal":
        print("はぐれメタル出現!")
        while not detect_template_from_screenshot(templates["on_field"], threshold, monitor_area):
            press_key("space")

    elif state == "in_battle":
        print("バトル中")
        for _ in range(3):
            press_key("up")
            time.sleep(0.1)
            press_key("space")

    elif state == "on_field":
        print("フィールド上")
        for _ in range(5):
            keys = random.sample(["right", "left"], 2)
            for key in keys:
                press_key(key, 0.05)

def main_loop(template_paths, interval, threshold, save_folder):
    """
    連続的に画面左上1/4をキャプチャして状態を検出し、対応する動作を実行する。

    :param template_paths: 各状態のテンプレート画像パスの辞書
    :param interval: キャプチャ間隔(秒)
    :param threshold: 検出閾値
    :param save_folder: キャプチャ画像の保存先フォルダ
    """
    templates = {state: cv2.imread(path, cv2.IMREAD_COLOR) for state, path in template_paths.items()}
    if any(img is None for img in templates.values()):
        raise ValueError("いずれかのテンプレート画像が読み込めませんでした。パスを確認してください。")

    monitor_area = get_monitor_area_for_top_left_quarter()

    print("左上1/4領域での状態検出を開始します...")

    while True:
        start_time = time.time()
        state = detect_state(templates, threshold, monitor_area)
        if state:
            perform_action(state, templates, threshold, monitor_area)

        time.sleep(max(0, interval - (time.time() - start_time)))

# 使用例
if __name__ == "__main__":
    base_path = "自分のプロジェクトルートディレクトリ"
    template_paths = {
        "lost_metal": f"{base_path}\\img\\template\\lost_metal.png",
        "on_field": f"{base_path}\\img\\template\\field.png",
        "in_battle": f"{base_path}\\img\\template\\battle_cmd.png"
    }
    save_folder = f"{base_path}\\img\\screenshots"

    main_loop(
        template_paths, 
        interval=3, 
        threshold=0.62, 
        save_folder=save_folder
    )

各関数の設定意図

画面設定

get_monitor_area_for_top_left_quarter

ゲーム画面を画面左上1/4領域に固定します。(デフォルトはフルスクリーンですが、ゲーム内設定で変更可能です) 別にフルスクリーン状態でも良いですが、以下のような環境で作業を進めたかったので。

左上のゲーム画面で動作確認、左下のターミナルでコードの出力確認、右側のVSCodeですぐにコード編集ができます。

状態検出

detect_template_from_screenshot
detect_state

今回のシステムの要です。指定したテンプレート画像と合致するオブジェクトが画面上に存在するのか否かを判定し、現在の状態を分類します。

判定するのは以下の3種類です。

キー操作

perform_action

各状態に対応する適切なキー操作を自動でやってもらいます。使うキーは以下の通りです。

  • スペースキー
    決定ボタン。最もよく使う。
  • 方向キー
    フィールドをうろうろしたり、逃げるコマンドを選択する際に使う。

もし僕のコードを参考にする場合、ご自身のキー設定と照らし合わせてみてください。極力簡素化しているので、たったの2種類しか使いませんがね😸

実行してみる

さて、いよいよコードを実行していきましょう。デモ動画を撮ってみました。

画質が悪いです。Zennの仕様上、動画はgif形式で3MBまでしか受け付けないのでこうなってしまいました。雰囲気だけで許してください...

フィールド上

戦闘中(対はぐメタ)

戦闘中(対モブ)

もちろん、僕自身はマウスやキーボードに指一本触れていません。はぐメタは「前提知識」
で挙げた会心必中やドラゴラムなどで倒していきます。

この自動レベル上げシステムを開発したばかりのとき、主人公の勇者はLv.50くらいでしたが、6時間くらい放置していたらカンスト(Lv.99)していました。

寝てたら勝手にカンストしているとは。なんと素晴らしい👏

特にドラクエⅢって職業転職でレベルが1からになるので、今までうんうん唸って転職先を選んでいたのもテキトーで良くなり、ラク〜に強力な仲間が作れますね!😎 (賢者だけは気をつけないと...)

終わりに

いかがでしたでしょうか?今回は初回ということもあって、全体の雰囲気を知ってもらいたい記事になりました。

正直OpenCVや謎のマジックナンバーについては語り足りないところが多いので、次回はもっと詳しい工夫点を知ってもらえるような内容を書きたいと思います。

よかったらZennとGithubもフォローしてくださると嬉しいです。今回のネタも、もちろんGithub上に公開しています。

ではでは。

Discussion