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

前回、遺伝的アルゴリズムで50~60世代位でステージ1-1をクリアできるようになりました。

今回は世界最速を目指そう!できるかな?

 
世界最速動画です。つい最近、World Recordが更新されました。

で、よく見ると1-1ドラム管を使ってワープしているじゃないですか。いやあ面倒なことになりました。

つまり、下キーを使わなければドラム管ワープできません。前回は上キーと下キーは 入力しない設定にしていました。

ワープなしの最速を目指すということで逃げましょうか。

1-1のタイムを見ると29秒66です。約30秒。どこまで近づけるか?

とりあえず前回のプログラムを改良していきます。

クリアフラグを付けます。マリオがゴールポールにしがみつくと、メモリーアドレス 0x001Dが3になります。

ポールの下に着地するとメモリーアドレス 0x001Dが2になります。これを利用します。そのときのタイムを取得します。

タイムは0x07F8/A Digits of time (100 10 1)です。399から減っていきます。

とりあえずゴールしたらタイムボーナス。TIme * 100点追加。これで動かしてみます。

あとFceuxでLuaScriptを動かすとき、TurboModeで高速に動かしています。

Windowsだと早いのですが、ラズベリーパイだと遅い。ほとんどノーマルスピードと変わりありません。

やりました!コツがわかってきました。まずは結果画像を見てください。
smb-ga1
左から世代、平均評価値、クリアしたマリオの数です。

35世代でまずクリアするようになりました。

41世代、20人中14人がクリア!30分動かしてこの成果です。

評価値も結構安定するようになりました。

遺伝的アルゴリズムでやってみて、数字をちょこちょこ調整しています。

今の段階では
マリオは20体用意。
優秀なマリオを4体に絞る
一点交差、
突然変異は1%がベストです。

あとは下キー入力とベストタイムを表示させてみます。
2016/11/25

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体用意する
marioValue1 = {} --評価値 右へ行くほど高くなる
marioValue2 = {} --評価値 降順ソート用
marioValue3 = {} --評価値 値が小さい程優秀なマリオ
totalValue = 0
averageValue = 0
averageValue2 = 0
marioClear = 0
clearFlag = 0
timeBonus = 0
deadBonus = 0
deadFlag = 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
z = 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

--1から4までの数字を返す
--数字の小さい方が優秀なため、1と2を多く返したい
function onetoeight()
local i
local r
r = math.random(100)
if (r < 40) then
i = 1
elseif (40 <= r and r < 80) then
i = 2
elseif (80 <= r and r < 90) then
i = 3
elseif (90 <= r) then
i = 4
end
--print(i)
return i
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  Value = "..averageValue.."  Clear = "..marioClear.." / 20")
marioClear = 0
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 = onetoeight()
y = onetoeight()
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 == 1) then
marioCross[i][j] = math.random(13)
end
r = math.random(100)
if (r == 1) then
 marioA[i][j] = math.random(20)
end
r = math.random(100)
if (r == 1) then
 marioB[i][j] = math.random(10)
end
end
end
--前世代より上回ったらキー入力を増やす
if(averageValue2 < averageValue ) then
totalInput = totalInput + 70
--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
averageValue2 = averageValue
end

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

--変数初期化 ソフトリセット
function resetFunction()
  marioHPosition = 0
  marioXPosition = 0
  marioDistance = 0
  timeBonus = 0
  clearFlag = 0
  deadBonus = 0
  deadFlag = 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(memory.readbyte(0x001D) == 3 and clearFlag == 0) then
  local timeA, timeH, timeT, timeO
  clearFlag = 1
marioClear = marioClear + 1
timeH = memory.readbyte(0x07F8)
timeT = memory.readbyte(0x07F9)
timeO = memory.readbyte(0x07FA)
timeA = (100 * timeH) + (10 * timeT) + timeO
--print(timeA)
timeBonus = timeA * 10
  end
--死亡した場合
if(memory.readbyte(0x075A) == 1 and deadFlag == 0) then
  deadFlag = 1
deadBonus = -200
  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
end