見出し画像

Gym Retroに新しいゲームを追加する

以下の記事が面白かったので、ざっくり訳してみました。

Integrating New Games into Retro Gym

OpenAIの「Gym Retro」は、スーパーファミコン、メガドライブ、ゲームボーイ、アタリなどの古典的なゲーム機で強化学習アルゴリズムを使用するための優れたツールです。最新バージョンは、多数のゲーム(ROMは含まれていません)でエージェントを訓練するように構成されています。独自のゲームを追加したい場合はどうしたらよいでしょうか?この投稿ではそのための手順を説明します

1. ゲームの選択

ゲームを「Gym Retro」に追加するには、ゲームのROMを見つける必要があります。レトロコンテストでは、SteamからソニックザヘッジホッグのROMを購入しなければなりませんでした。Steamには、より古典的なSega ROMがあります。ROMを取得したら、正しい拡張子であることを確認してください。私が選んだスーパーファミコンのゲームには、「.smc」および「.sfc」のROMがありました。「Gym Retro」は、「.sfc」を必要としています。私は、ROMにパズルゲーム「Bust A Move」(別名Puzzle Bobble)を選択しました。

2. Gym Retroのインストール

「paperspace.com」の「ML-in-a-box」インスタンスにインストールしました(この投稿のように)。

git clone --recursive https://github.com/openai/retro.git gym-retro
cd gym-retro
pip3 install -e .

3. Integration UIのインストール

「Integration UI」を使用すると、ゲームをプレイして保存状態を作成し、ゲームメモリの重要な領域を見つけることができます。ゲームのメモリのこれらの領域を使用して、報酬関数、完了条件、およびエージェントの訓練に役立つその他のデータを見つけることができます。「Gym Retro」のREADME.mdで紹介あれているLinuxのインストール手順は次の通りです。

sudo apt-get install capnproto libcapnp-dev libqt5opengl5-dev qtbase5-dev
cmake . -DBUILD_UI=ON -UPYLIB_DIRECTORY
make -j$(grep -c ^processor /proc/cpuinfo)
./gym-retro-integration

4. ROMの構成

「./gym-retro-integration」は、「Integration UI」を起動します。
ここでの目標は次のとおりです。

・読み込むGym Retroの状態を作成する
・興味のあるポイントのROMのメモリを調べる
・それらの興味のあるポイントのアドレスと型を見つける

公式ガイドこのスレッド(特にMaxStrange)いくつかのポインタを提供します。

状態の作成は非常に簡単です。

(1)「Gym Retro Integration」を起動します。
(2)新しいゲームをロードします(Command-Shift-O)。
(3)追加するゲームのROMをメニューで選択します。
(4)ゲームに名前を付けます。
(5)ゲームが開きます。ゲーム内のコントロールに対応するキーを確認するには、「Window → Control」に移動します。
(6)使用可能なコントロールを使用して、レベル、オプション、モード、キャラクターなどを選択し、これらのオプションをメモします。
(7)最終的にゲームの最初のプレイ可能な瞬間になったら、ゲームを一時停止し(実際のゲーム内ではなくIntegration UIで)(Command-P)、状態を保存します(Command-S)。この瞬間を見つけるのは難しい場合があり、その正確な状態を見つけて保存するために、ゲームに戻って再始動する必要があるかもしれません(Command-R)。
(8)状態を保存します。状態名には選択したオプションも含めます。
【例】SailorMoon.QueenBerylsCastle.Easy.Level1.state

5. ゲームのメモリを調べる

ROMから興味のあるポイントを探すのは、試行錯誤のプロセスです。これをスピードアップするためのヒントについては、公式ガイドとスレッドを読むことをお勧めします。

私はこれを行うのに、「BizHawk」というツールを使用しました(Windowsが必要です)。「BizHawk」には、RAM値を検索してRAMウォッチリストに追加できるRAM検索などの便利なツールがあります。「Gym Retro」には、テストして動作する同様のツールがあります。私はすでに「BizHawk」に精通していたので、主にそれを使用しました。「BizHawk」の使用に関する多くのガイドがあり、「BizHawk」には便利なフレームごとの再生方法があります。

「Bust A Move」では、次の3つの特定のことについて、ROMのメモリのどこにあるかを見つけることに興味がありました。

・バブル値(報酬機能の代替)
・ゲームオーバー状態
・スコア(報酬機能の代替)

メモリ内の「バブル値」を見つけるのは簡単でした。画面の上部に泡が表示されるゲームモードでゲームを少しプレイしました。次に、RAMでその値を検索しました。BizHawkでは、これはRAM検索ツールを使用して行われます。バブル値を持つメモリアドレスを見つけました(つまり、16個のバブルがポップされていた場合、値16のすべてのメモリアドレスを検索しました)。

次に、それらをRAMウォッチリストに追加しました。私はゲームをプレイし続け、バブルスコアが増加するにつれてどのメモリアドレスが続くかを観察しました。これは、「Gym Retro」でも行うことができます。「Gym Retro」では、アドレスリストでバブルの値を検索し、この値が正しいことを確認するために監視できる変数を作成しました。

ゲーム中に1、ラウンドが失われたときに0であったすべての値を探して、「ゲームオーバー状態」を見つけました。それを実現する100個の値を考え出し、その値が異なるゲームモードとプレイスルーで一貫していることをテストしました。私は自分のゲームオーバー条件として100個の値の中から1つを選びました。

「スコア」はトリッキーでした。公式ガイドのヒントは、何が起こっているのかを理解するのに役立ちました。特に、1つの値を複数のアドレスに分割する方法と、それらのアドレスが互いに近くにあることがよくあります。
スコアは1つの場所に保存されるのではなく、複数の異なる場所にある10の累乗の組み合わせによって保存されます。10のスコアは1つのアドレスに格納され、数百と数千は別のアドレスに、数万と数千は3分の1に格納されます。10のスコアは単に0から9(つまり、20の場合は2、90の場合は9など)として保存されますが、100のスコアは次の式で保存されます。

(100の数)+ 16 *(1000の数)=アドレスに格納されている値

メモリアドレスがわかったら、それらを16進数から10進数に変換し、エミュレータシステム固有のrombase番号を追加する必要があります。rombase番号は、retro/coresにある.jsonファイルにあります。スーパーファミコンの場合、バブル16進アドレス(000A5C)を10進数(2652)に変換し、rombase番号(8257536)を追加し、ゲーム固有のdata.jsonファイルでこの値(8260188)を使用することを意味しました(以下を参照)。

システム固有の.jsonファイルにも許可されたタイプがあります。data.jsonファイルのアドレスとともにタイプが必要になります。公式ガイド、README.mdファイル、retro/coresにある.jsonファイル、およびタイプを見つける方法のアドレスを見つけるために使用するツールを参照してください。

6. ゲームファイルの作成

「Gym Retro」にゲームを追加するいは、次のファイルが必要になります。

・metadata.json:Gym Retroにロードするデフォルトの状態を指示
・data.json:Gym Retroに読み込むメモリアドレスを指示
・Scenario.json:RLエージェントの報酬関数と完了条件を指定。オプションで、このファイルを使用して.luaスクリプトにリンクし、より高度な機能を作成できます。
・script.lua(オプション):より高度な報酬関数と完了条件の指定。

Sonic(Retro Contestのセットアップ用)やAirstriker(Retro Gymに付属のGenesis ROM)などのゲームをクリックして例を表示します。
これらのファイルを作成したら、ゲームを実行するためにBustAMove-Snesディレクトリをretro/retro/data/contribからretro/retro/data/stableに移動しました。

◎metadata.json

{
 "default_state": "BustAMove.Challengeplay0"
}

◎data.json

{
 "info": {
   "gameover": {
     "address": 8294221,
     "type": "|u1"
   },
   "bubbles": {
     "address": 8260188,
     "type": "<u4"
   },
   "score_jyuu": {
     "address":8259924,
     "type": "|u1"
   },
   "score_hyaku": {
     "address":8259925,
     "type": "|u1"
   },
   "score_man": {
     "address":8259928,
     "type": "<u4"
   }
 }
}

Gymに精通している人は、Gym環境を呼び出すたびに、step()でobservation, reward, done, infoが返されることを知っています。data.jsonファイルに何を入れても、このinfoからアクセスできます。

import retro

env = retro.make(game='BustAMove-Snes', state='BustAMove-Snes.Challengeplay0')
env.reset()
while True:
   _obs, _rew, done, _info = env.step(env.action_space.sample())
   print('I have popped {}.format(_info['bubbles']))

◎scenario.json

{
 "done": {
   "script": "lua:done_check"
 },
 "reward": {
   "script": "lua:correct_bubbles"
 },
 "scripts": [
   "script.lua"
 ]
}

このScenario.jsonは、計算を行うためにscript.luaに指示します。あるいは、Airstrikerはシナリオファイルで作業を行い、luaスクリプトを使用しません。

{
 "done": {
   "condition": "all",
   "variables": {
     "gameover": {
       "op": "equal",
       "reference": 1
     },
     "lives": {
       "op": "zero"
     }
   }
 },
 "reward": {
   "variables": {
     "score": {
       "reward": 1.0
     }
   }
 }
}

◎script.lua

previous_bubbles = 0
function correct_bubbles()
 if data.bubbles > previous_bubbles then
   local delta = data.bubbles - previous_bubbles
   previous_bubbles = data.bubbles
   return delta
 else
   return 0
 end
end
function done_check()
 if data.gameover == 0 then
   return true
 end
 return false
end
previous_score = 0
function correct_score ()
 local current_score = 0
 local hundreds = (data.score_hyaku % 16)*100
 local thousands = (math.floor(data.score_hyaku/16))*1000
 local ten_thousands = (data.score_man % 16)*10000
 local hundred_thousands = (math.floor(data.score_man/16))*100000
 current_score = data.score_jyuu * 10 + hundreds + thousands + ten_thousands + hundred_thousands
 if current_score > previous_score then
   local delta = current_score - previous_score
   previous_score = current_score
   return delta
 else
   return 0
 end
end


この記事が気に入ったらサポートをしてみませんか?