2025年3月24日月曜日

LOMのステータスを総当たり探査した話(技術者向け)

 たまには技術ノートでも書こうかなという話
今回のお題はlomのベストパターン探査です。

VBS:
Function CalcStats(w)
s=array(5,5,5,5,5,5,5)
s(0)=s(0)+0.75*w(0)+1 *w(1)+1 *w(2)+1.25*w(3)+1.25*w(4)+1.75*w(5)+0.75*w(6)+0.5 *w(7)+1.25*w(8)+0.75*w(9)+0.5 *w(10)
s(1)=s(1)+1.25*w(0)+1 *w(1)+0.75*w(2)+1 *w(3)+0.75*w(4)+0.75*w(5)+1.25*w(6)+0.75*w(7)+0.75*w(8)+1.25*w(9)+1.75*w(10)
s(2)=s(2)+0.75*w(0)+1 *w(1)+1 *w(2)+0.75*w(3)+1.25*w(4)+0.5 *w(5)+1 *w(6)+0.75*w(7)+0.75*w(8)+0.5 *w(9)+0.75*w(10)
s(3)=s(3)+0.75*w(0)+0.75*w(1)+0.75*w(2)+0.75*w(3)+0.75*w(4)+0.75*w(5)+0.75*w(6)+1.25*w(7)+0.5 *w(8)+0.75*w(9)+0.75*w(10)
s(4)=s(4)+0.5 *w(0)+0.75*w(1)+0.75*w(2)+0.75*w(3)+0.5 *w(4)+0.75*w(5)+0.5 *w(6)+0.5 *w(7)+1 *w(8)+0.5 *w(9)+0.5 *w(10)
s(5)=s(5)+1 *w(0)+0.75*w(1)+1 *w(2)+0.5 *w(3)+0.75*w(4)+0.75*w(5)+0.75*w(6)+0.75*w(7)+0.75*w(8)+0.75*w(9)+0.75*w(10)
s(6)=s(6)+1 *w(0)+0.75*w(1)+0.5 *w(2)+0.75*w(3)+0.5 *w(4)+0.75*w(5)+1 *w(6)+1.25*w(7)+0.75*w(8)+1.25*w(9)+1 *w(10)
for i=0 to ubound(s) s(i)=fix(s(i) if s(i)>99 then s(i)=99 next CalcStats=s end function function t619(s) t=0 for i=0 to ubound(s):t=t+s(i):next if t>=619 then t619=true else t619=false end function function s79t615(s) f=true t=0 for i=0 to ubound(s) if s(i)<79 then f=false t=t+s(i) next if t<615 then f=false s79t615=f end function msgbox s79t615(CalcStats(array(1,81,1,0,0,0,0,6,9,0,0)))
msgbox t619(CalcStats(array(1,81,1,0,0,0,0,6,9,0,0)))

考えなくちゃいけないのは計算と探査判定
探査方法の考え方としては、武器別にステータス配列を作って配列を合算しても良いんだけど配列同士の合算が面倒くさい。
考え方を変えて関数で各武器の回数だけを貰ってその分だけ能力値を掛ける。
これをCscriptで多重forに放り投げるだけ。

Jscript:

function CalcStats(w) {
  var s = [5, 5, 5, 5, 5, 5, 5];
  s[0]=s[0]+0.75*w[0]+1*w[1]+1*w[2]+1.25*w[3]+1.25*w[4]+1.75*w[5]+0.75*w[6]+0.5*w[7]+1.25*w[8]+0.75*w[9]+0.5*w[10];
  s[1]=s[1]+1.25*w[0]+1*w[1]+0.75*w[2]+1*w[3]+0.75*w[4]+0.75*w[5]+1.25*w[6]+0.75*w[7]+0.75*w[8]+1.25*w[9]+1.75*w[10];
  s[2]=s[2]+0.75*w[0]+1*w[1]+1*w[2]+0.75*w[3]+1.25*w[4]+0.5*w[5]+1*w[6]+0.75*w[7]+0.75*w[8]+0.5*w[9]+0.75*w[10];
  s[3]=s[3]+0.75*w[0]+0.75*w[1]+0.75*w[2]+0.75*w[3]+0.75*w[4]+0.75*w[5]+0.75*w[6]+1.25*w[7]+0.5*w[8]+0.75*w[9]+0.75*w[10];
  s[4]=s[4]+0.5*w[0]+0.75*w[1]+0.75*w[2]+0.75*w[3]+0.5*w[4]+0.75*w[5]+0.5*w[6]+0.5*w[7]+1*w[8]+0.5*w[9]+0.5*w[10];
  s[5]=s[5]+1*w[0]+0.75*w[1]+1*w[2]+0.5*w[3]+0.75*w[4]+0.75*w[5]+0.75*w[6]+0.75*w[7]+0.75*w[8]+0.75*w[9]+0.75*w[10];
  s[6]=s[6]+1*w[0]+0.75*w[1]+0.5*w[2]+0.75*w[3]+0.5*w[4]+0.75*w[5]+1*w[6]+1.25*w[7]+0.75*w[8]+1.25*w[9]+1*w[10];
  for (var i = 0; i < s.length; i++) {
      s[i] = Math.floor(s[i]);
      if (s[i] > 99) s[i] = 99;
  }
  return s;
}

function t619(s) {
  var t = 0;
  for (var i = 0; i < s.length; i++) {t += s[i];}
  return t >= 619;
}
function s79t615(s) {
  var f = true;
  var t = 0;
  for (var i = 0; i < s.length; i++) {
     if (s[i] < 79) f = false;
     t += s[i];
  }
  if (t < 615) f = false;
  return f;
}
function s80t607(s) {
  var f = true;
  var t = 0;
  for (var i = 0; i < s.length; i++) {
      if (s[i] < 80) f = false;
      t += s[i];
  }
  if (t < 607) f = false;
  return f;
}

速度比較用にJscriptにそのまま書き直したものがこれ。同じくCscriptで実行したけど速度的に大きな差は無かった。
別にいらなかったんだけど全ステ80以上パターンも探査に追加この時点では607が最低値で分かっていたのでそれを指定。
ちなみにあとで総当たりさせると合計607は三千パターン以上ある為後で合計範囲を610以上に変更した。
619のif elseはそもそも比較演算すればboolなので修正他はそのまま。

javascript(node.js実行):

function CalcStats(w) {
  let s = [5, 5, 5, 5, 5, 5, 5];
  s[0]=s[0]+0.75*w[0]+1*w[1]+1*w[2]+1.25*w[3]+1.25*w[4]+1.75*w[5]+0.75*w[6]+0.5*w[7]+1.25*w[8]+0.75*w[9]+0.5*w[10];
  s[1]=s[1]+1.25*w[0]+1*w[1]+0.75*w[2]+1*w[3]+0.75*w[4]+0.75*w[5]+1.25*w[6]+0.75*w[7]+0.75*w[8]+1.25*w[9]+1.75*w[10];
  s[2]=s[2]+0.75*w[0]+1*w[1]+1*w[2]+0.75*w[3]+1.25*w[4]+0.5*w[5]+1*w[6]+0.75*w[7]+0.75*w[8]+0.5*w[9]+0.75*w[10];
  s[3]=s[3]+0.75*w[0]+0.75*w[1]+0.75*w[2]+0.75*w[3]+0.75*w[4]+0.75*w[5]+0.75*w[6]+1.25*w[7]+0.5*w[8]+0.75*w[9]+0.75*w[10];
  s[4]=s[4]+0.5*w[0]+0.75*w[1]+0.75*w[2]+0.75*w[3]+0.5*w[4]+0.75*w[5]+0.5*w[6]+0.5*w[7]+1*w[8]+0.5*w[9]+0.5*w[10];
  s[5]=s[5]+1*w[0]+0.75*w[1]+1*w[2]+0.5*w[3]+0.75*w[4]+0.75*w[5]+0.75*w[6]+0.75*w[7]+0.75*w[8]+0.75*w[9]+0.75*w[10];
  s[6]=s[6]+1*w[0]+0.75*w[1]+0.5*w[2]+0.75*w[3]+0.5*w[4]+0.75*w[5]+1*w[6]+1.25*w[7]+0.75*w[8]+1.25*w[9]+1*w[10];
  for (let i = 0; i < s.length; i++) {
  s[i] = Math.floor(s[i]);
  if (s[i] > 99) s[i] = 99;
  }
  return s;
  }
function t619(s) {
  return s.reduce((t, val) => t + val, 0) >= 619;
}
function s79t615(s) {
  const t = s.reduce((sum, val) => sum + val, 0);
  return s.every(val => val >= 79) && t >= 615;
}
  function s80t607(s) {
  const t = s.reduce((sum, val) => sum + val, 0);
  return s.every(val => val >= 80) && t >= 607;
}

前回の記事で初めてnode.jsをインストールしたんだけどjavascriptがコーディング出来てローカル処理をしたいなら絶対インストールした方がいい。
処理速度は100倍以上速くなる。
Jscriptでは変数定義にvarしか使えなかったけどjavascriptなのでletやconstに修正して、判定関数も処理を整理。

この後さらにこれをC#に書き換えて並列処理で高速化するんだけど、現状で片手剣30回前後まで絞っても目新しいパターン結果は出なかったので省略。

発見パターンのうち有益なパターンは「LOMリマスター:レベルアップ能力値シミュレーター」の方を参照してください。
※Lvシステムは変わってないのでリマスターじゃない場合も同じです

結局の所、能力調整的に微妙な…いわゆる最大パターン619は6パターンしかありません。
逆に一番いいパターン、79以上615は27パターン程あります。

(オマケとして探査した80以上の合計上限はどう頑張っても611が最大で、79より合計が下がるので微妙。ちなみにこの611パターンは1つしか見つかってないです。80以上610なら18パターンほど見つかってます。)

■大まかな探査時間
片手剣の回数をベースに杖とグラブは無制限、他は10回という制限で
片手剣の使用回数を62回にした場合はだいたい以下の通り。

個別の時間差
 0% →   9%: 13:07:43 - 12:28:35 = 39分 8秒 = 2,348秒
 9% →  18%: 13:40:55 - 13:07:43 = 33分12秒 = 1,992秒
18% →  27%: 14:09:01 - 13:40:55 = 28分 6秒 = 1,686秒
27% →  36%: 14:34:30 - 14:09:01 = 25分29秒 = 1,529秒
36% →  45%: 14:54:26 - 14:34:30 = 19分56秒 = 1,196秒
45% →  54%: 15:10:44 - 14:54:26 = 16分18秒 =   978秒
54% →  63%: 15:24:24 - 15:10:44 = 13分40秒 =   820秒
63% →  72%: 15:35:43 - 15:24:24 = 11分19秒 =   679秒
72% →  81%: 15:45:03 - 15:35:43 =  9分20秒 =   560秒
81% →  90%: 15:52:29 - 15:45:03 =  7分26秒 =   446秒
90% → 100%: 15:58:26 - 15:52:29 =  5分57秒 =   357秒 

ループの外側の数値が大きくなるほど使える回数は減るので内側に向かってどんどん処理時間が早まっていく。
だいたい50前後くらいなら少し放置しておけばリストアップ出来る。
これより片手剣の回数が減ると一日放置とかになるかも

■C#その後:
・並列でも全てをなげちゃうとCPU90%くらいで数日帰ってこないことになるので片手剣の回数を指定出来るように修正、関数として分離(全体を回したい時はこれにfor)
全体に投げてた並列処理をfor内部を並列化して内部のforを高速化

・条件を変更するたびにコンパイルが面倒になったので設置textから判定条件を読み込めるように修正
File.ReadAllLines(conditionsFile);で読み込んで改行でsplitして
読み込むテキストは改行区切りで「80,80,80,80,80,80,80,610」「81,81,81,81,81,81,81,」「,,,,,,,619」こんな感じ。空部分は条件指定なし

・全部デスクトップに吐いて、読み込みもデスクトップだったのを全部実行exeから相対パスに出力と読み込みするように変更「Environment.GetFolderPath(Environment.SpecialFolder.Desktop)→System.Reflection.Assembly.GetExecutingAssembly().Location」とか

・各ループ上限を柔軟に調整したくなったのでリミットテキストを外部において武器の回数上限を変更出来るように修正

・引数で回数を指定して実行する事で入力待ちするのを省略するように変更
片手剣回数を関数分離して回したいときはforにしようと思ってたけど適当にスクリプトで引数付きで実行出来るようにした

などの修正をしたけど検証終わったらもう使わないよなぁという気持ち。
いやまぁ初見言語のC#の勉強にはなったけども

C#(for抜粋):

for (int w0 = 0; w0 <= w0Limit; w0++)
{
  int w2Limit = Math.Min(remaining - w0, loopLimits[2]);
  Console.WriteLine("\n片手剣 " + actualOneHandSword + "回 - 進捗: " + (w0 * 100 / w0Max) + "% (" + w0 + "/" + w0Max + ") - " + DateTime.Now);
  for (int w2 = 0; w2 <= w2Limit; w2++)
  {
    int w3Limit = Math.Min(remaining - w0 - w2, loopLimits[3]);
    for (int w3 = 0; w3 <= w3Limit; w3++)
    {
      int w4Limit = Math.Min(remaining - w0 - w2 - w3, loopLimits[4]);
      for (int w4 = 0; w4 <= w4Limit; w4++)
      {
        Console.Write(".");
        Console.Out.Flush();
        int w5Limit = Math.Min(remaining - w0 - w2 - w3 - w4, loopLimits[5]);
        for (int w5 = 0; w5 <= w5Limit; w5++)
        {
          int w6Limit = Math.Min(remaining - w0 - w2 - w3 - w4 - w5, loopLimits[6]);
          for (int w6 = 0; w6 <= w6Limit; w6++)
          {
            int w7Limit = Math.Min(remaining - w0 - w2 - w3 - w4 - w5 - w6, loopLimits[7]);
            Parallel.For(0, w7Limit + 1, w7 =>
            {
              int w8Limit = Math.Min(remaining - w0 - w2 - w3 - w4 - w5 - w6 - w7, loopLimits[8]);
              Parallel.For(0, w8Limit + 1, w8 =>
              {
                int w9Limit = Math.Min(remaining - w0 - w2 - w3 - w4 - w5 - w6 - w7 - w8, loopLimits[9]);
                for (int w9 = 0; w9 <= w9Limit; w9++)
                {
                  int w10 = remaining - w0 - w2 - w3 - w4 - w5 - w6 - w7 - w8 - w9;
                  if (w10 >= 0 && w10 <= loopLimits[10])
                  {
                    int[] localW = new int[11];
                    localW[0] = w0; localW[1] = actualOneHandSword; localW[2] = w2; localW[3] = w3; localW[4] = w4;
                    localW[5] = w5; localW[6] = w6; localW[7] = w7; localW[8] = w8; localW[9] = w9; localW[10] = w10;
                    bool localHasMatch = false;
                    StatsWrite(localW, actualOneHandSword, limitPrefix, ref localHasMatch);
                    if (localHasMatch) hasMatch = true;
                  }
                }
              });
            });
          }
        }
      }
    }
  }
}

C#のコードだけ記載しないのもあれだったから。総当たり処理部分はこんな感じ。
ループは最初forではなく再帰にしてたんだけどforの方が早いらしい事が分かったのでforに戻した。1秒でも早くというかミリ秒でも早く動いて欲しかった故に・・・。

判定処理をforに入れるとfor内が厚くるのでStatsWrite側に判定処理を入れています。Parallel.Forで並列化してるのは杖とグラブ部分。

0 件のコメント:

コメントを投稿