スーパーマリオブラザーズ 人工知能で世界最速を目指そう!

 

これをマネしてやってみたいと思います。

ステージ1-1をクリアできることは確認しています。

スーパーマリオブラザーズ ステージ1-1世界最速を目指して見ましょう。

必要なものはFCEUXというエミュレーターとスーパーマリオブラザーズのロムです。 あとはメモ帳のようなテキストエディタです。

Win、Linux, Macでもできるでしょう。

FCEUXにはコミュニケーション機能が付いており、LuaScriptを使うことでゲームをコントロールすることができます。

私もLuaScriptは初めてでしたが、Pythonみたいなものと思って構いません。ほぼPythonです。

やってみて思ったのが非常に面白い。だってマリオが動くんですから。 300px-スクラッチキャット
スクラッチの猫を動かすようなものです。マリオを動かしてみましょう。

辛気臭いC言語やるよりよっぽど良いでしょう。 

まず初めはhelloworldを表示させます。

ファイル名は何でもいいのですが、SMB-GA.luaにしました。

print ("Hello World!")
スーパーマリオブラザーズを読み込んでSMB-GA.luaを実行するとHello Worldが表示されます。

 fceux1
マリオを右に動かしてみましょう。
print ("Hello World!")

--無限ループ
while true do
  joypad.set(1, {right=true})
  emu.frameadvance()
end 

スタートを押してゲームを開始、スクリプトを実行するとマリオが右へ進みます。
もちろん最初のクリボーにぶつかり、死にます。

ゲームを改造します。
マリオの残機を99にしてみましょう。

ToolからMemory Searchを起動します。
fceux2
アドレス075AのValueというところを見てください。2になっています。

この2がマリオの残り数を表示しています。ここを99にすると99人になります。
同じように点数を12345670、コインを99に変更します。

ゲームによってメモリーの意味が違ってきます。
Super Mario Bros.:RAM map このサイトを参考にするといいでしょう。

fceux3
print ("Hello World!")

--無限ループ
while true do
  joypad.set(1, {right=true})
  
  --マリオの残機を99に変更する
  memory.writebyte(0x075A, 99)
  
  --得点を12345670に変更する
  memory.writebyte(0x07DD, 1);
  memory.writebyte(0x07DE, 2);
  memory.writebyte(0x07DF, 3);
  memory.writebyte(0x07E0, 4);
  memory.writebyte(0x07E1, 5);
  memory.writebyte(0x07E2, 6);
  memory.writebyte(0x07E2, 7);
  
  --コインを99に変更する
  memory.writebyte(0x07ED, 9);
  memory.writebyte(0x07EE, 9);
  
  emu.frameadvance()
end 

ここまでくればできたようなもので、あとはクリアできるようにキーボード入力を入れたらいいだけです。もちろんそこは機械学習で最適化をさせていきます。

難しいです。ある程度こちらで動きを促してやらないと永久にクリアできません。
右を多く、大ジャンプを多くしてやります。

2016/11/23現在のプログラム
60世代ぐらいで1-1をクリアできるようになります。試してみてください。

require("auxlib");
print ("Hello World!")


key = {}

key["up"] = false;
key["left"] = false;
key["down"] = false;
key["right"] = false;
key["A"] = false;
key["B"] = false;
key["start"] = false;
key["select"] = false;

math.randomseed(os.time())


marioCross = {} --マリオの十字キー
marioA = {} --マリオのAボタン
marioB = {} --マリオのBボタン

marioCross2 = {} --マリオの十字キー コピー用
marioA2 = {} --マリオのAボタン コピー用
marioB2 = {} --マリオのBボタン コピー用

marioGeneration = 1 --世代
marioPlayer = 1
marioMaxPlayer = 20 --マリオ20体用意する
marioHalfPlayer = 20 / 2
marioValue1 = {} --評価値 右へ行くほど高くなる
marioValue2 = {} --評価値 降順ソート用
marioValue3 = {} --評価値 値が小さい程優秀なマリオ
totalValue = 0
averageValue = 0
averageValue2 = 0

marioDistance = 0 --マリオが進んだ距離
marioXPosition = 0 -- 0~最大値255までの値をとる
marioHPosition = 0 -- ステージ1-1は12分割される 0~12までの値をとる
totalInput = 100
frameCount = 1
p = 1
q = true
r = true
x = 1
y = 1
tableLen = 0
select = false


function tableReset(t)
for k in pairs (t) do
t[k] = nil
end
for i = 1, marioMaxPlayer do
t[i]  = {}
end
end

--マリオを20体生成する
function marioGenerator()
for i = 1, marioMaxPlayer do
marioCross[i]  = {} 
marioA[i] = {}
marioB[i] = {}
end
for i = 1, marioMaxPlayer do
for j = 1, totalInput do
 marioCross[i][j] = math.random(13)
 marioA[i][j] = math.random(20)
 marioB[i][j] = math.random(10)
 --print(i, j, marioA[i][j])
end
end
end

--マリオを遺伝的アルゴリズムで再生成する
function marioReGenerator()
--table.sort(marioValue2)
--降順でソートする
table.sort(marioValue2, function (a,b) return a>b end)
--print(marioValue1)
--print(marioValue2)
totalValue = 0
averageValue = 0
for i = 1, marioMaxPlayer do
for j = 1, marioMaxPlayer do
 if(marioValue1[i] == marioValue2[j]) then
  marioValue3[i] = j
 end
end
totalValue = totalValue + marioValue1[i]
end
--print(marioValue3)
averageValue = totalValue / marioMaxPlayer
print(marioGeneration.." Generation  averageValue  "..averageValue)
tableReset(marioCross2)
tableReset(marioA2)
tableReset(marioB2)
--優秀なマリオ 1~8位までをコピーする
p = 1
for i = 1, marioMaxPlayer do
if( marioValue3[i] <= 8) then
marioCross2[p] = marioCross[i]
marioA2[p] = marioA[i]
marioB2[p] = marioB[i]
p = p + 1
end
end
tableReset(marioCross)
tableReset(marioA)
tableReset(marioB)
--いわゆる交叉 xとyが選ばれた親マリオの変数
for i = 1, marioMaxPlayer do
x = math.random(8)
y = math.random(8)
for j = 1, totalInput / 2 do
marioCross[i][j] = marioCross2[x][j]
marioA[i][j] = marioA2[x][j]
marioB[i][j] = marioB2[x][j]
end
for j = totalInput / 2, totalInput do
marioCross[i][j] = marioCross2[y][j]
marioA[i][j] = marioA2[y][j]
marioB[i][j] = marioB2[y][j]
end
end
--突然変異
for i = 1, marioMaxPlayer do
for j = 1, totalInput do
r = math.random(100)
if (r == 3) then
marioCross[i][j] = math.random(13)
 marioA[i][j] = math.random(20)
 marioB[i][j] = math.random(10)
end
end
end
if(averageValue2 < averageValue ) then
totalInput = totalInput + 50
end
averageValue2 = averageValue
--print(totalInput)
tableLen =table.maxn(marioCross[1])
--print(tableLen)
for i = 1, marioMaxPlayer do
for j = tableLen + 1, totalInput do
 marioCross[i][j] = math.random(13)
 marioA[i][j] = math.random(20)
 marioB[i][j] = math.random(10)
 --print(i, j, marioA[i][j])
end
end
end

--評価値を計算する
function calculateValue()
marioXPosition = memory.readbyte(0x0086)
--0x006D mario Position max 12
  marioHPosition = memory.readbyte(0x006D)
  marioDistance = marioHPosition * 256 + marioXPosition
  marioValue1[marioPlayer] = marioDistance
  marioValue2[marioPlayer] = marioDistance
  --print(marioPlayer, marioValue1[marioPlayer])
end

--変数初期化 ソフトリセット
function resetFunction()
  marioHPosition = 0
  marioXPosition = 0
  marioDistance = 0
  --totalInput = totalInput + 10
  --marioGenerator()
  
  if(marioPlayer < marioMaxPlayer) then
  --print(marioPlayer)
  marioPlayer = marioPlayer + 1
  else
  --世代交代
 
  marioReGenerator()
  marioPlayer = 1
  marioGeneration = marioGeneration + 1
  end
  emu.softreset()
  frameCount = 1
end


function testiup()
function turboAction(self, a) 
emu.speedmode("maximum")
gui.text(10,10,"pressed me!");
end;
function normalAction(self, a) 
emu.speedmode("normal")
gui.text(10,10,"pressed me!");
end;
-- Create a button
turboButton = iup.button{title="Turbo Speed Button"};
-- Set the callback
turboButton.action = turboAction;
normalButton = iup.button{title="Normal Speed Button"};
normalButton.action = normalAction;
box = iup.vbox {turboButton,normalButton}
-- Create the dialog
dialogs = dialogs + 1;
handles[dialogs] = iup.dialog{ box, title="IupDialog Title"; };
-- Show the dialog (the colon notation is equal 
-- to calling handles[dialogs].show(handles[dialogs]); )
handles[dialogs]:show();

end

testiup();
marioGenerator()

while true do
--if (marioPlayer == 1) then
--print(marioGeneration.."generation")
--end
--print(marioPlayer)
local joy = joypad.read(1)
if (joy["select"]) then
if (select == true) then
emu.speedmode("nothrottle")
gui.text(10,10,"High Speed")
else
emu.speedmode("normal")
gui.text(10,10,"Normal Speed")
end
select = not select
end
--入力が無くなった場合と死亡した場合、評価値を算出しリセット
if(totalInput < frameCount or memory.readbyte(0x075A) == 1) then
memory.writebyte(0x075A, 8)
--print(marioPlayer, totalInput, frameCount, memory.readbyte(0x075A))
    calculateValue()
    resetFunction()
  end

key["up"] = false;
key["down"] = false;
if( marioCross[marioPlayer][frameCount] == 1) then
key["left"] = true;
key["right"] = false;
else
key["right"] = true;
key["left"] = false;
  end

  if(marioA[marioPlayer][frameCount] == 1) then
key["A"] = false;
else
key["A"] = true;
end

  if(marioB[marioPlayer][frameCount] == 1 ) then
key["B"] = false;
else 
key["B"] = true;
end

  -- Execute instructions for FCEUX
  joypad.set(1, key)

  
  

  --スタート画面に戻ったらスタートボタンを押す
  if(memory.readbyte(0x07F8) == 4 and memory.readbyte(0x07F9) == 0  and memory.readbyte(0x07FA) == 1) then
    joypad.set(1, {start = true})
    --print("reset")
    frameCount = 1
  end

  emu.frameadvance() -- This essentially tells FCEUX to keep running

  --print (memory.readbyte(0x0086));

  frameCount = frameCount + 1