CookieClickerのチートプログラム

# あらまし
完全に正月の自由研究で、CookieClickerのチートプログラムを書いた。
# 経緯
1000回転生する隠し実績をちまちま進めていたのだけど飽きがきたし、せっかくなので自動化で遊んでみることにした。元々はSeleniumに挑戦しようと思っていたのだけど、Macへの導入がなかなか大変そう・・Automatorでやろうかな？と色々考えたが
GoogleChrome拡張機能の、
[ScriptAutoRunner](https://chromewebstore.google.com/detail/scriptautorunner/gpgjofmpmjjopcogjgdldidobhmjmdbm?hl=ja)
のJavaScriptで素朴にやることにした。
コンソールのメッセージは気にしない。
# そもそも
 数値操作をすれば転生実績は解除できたりしそうだけど、あくまで「ブラウザ操作の自動化」をやりたいというのが当初の目的だったので、謎に地道な操作を自動化しているかもしれない。チートのことをよくわかっていない。

# コード
```javascript
//フラグ
let statsBtnObsFlg = false;
// 変更を監視するノード
const $statsBtnObsArea = document.getElementById('comments');
// オプション
const statsBtnConfig = { attributes: true, childList: true, subtree: true };
// コールバック
const statsBtnCallback = function (mutationsList, observer) {
  for (const mutation of mutationsList) {
    for(let node of mutation.addedNodes) {
      // 要素のみを追跡し、他のノード（例 テキストノード）はスキップ
      if (!(node instanceof HTMLElement)) continue;

      if(node.getAttribute('id') === 'statsButton' && node.children[0].innerText === '記録' && !statsBtnObsFlg){
        //実績を表示・残り回数を取得
        const $statsBtn = document.getElementById('statsButton');
        $statsBtn.click();
        statsBtnObsFlg = true;
        observer.disconnect();
      }
    }
  }
};
//関数呼び出し
observeElement(statsBtnCallback,$statsBtnObsArea,statsBtnConfig);

//フラグ
let statsGeneralObsFlg = false;
// 変更を監視するノード
const $statsGeneralObsArea = document.getElementById('menu');
// オプション
const statsGeneralConfig = { attributes: true, childList: true, subtree: true };
// コールバック
const statsGeneralCallback = function (mutationsList, observer) {
  for (const mutation of mutationsList) {
    for(let node of mutation.addedNodes) {
      // 要素のみを追跡し、他のノード（例 テキストノード）はスキップ
      if (!(node instanceof HTMLElement)) continue;

      if(node.classList.contains('subsection') && node.querySelector('#statsGeneral') && !statsGeneralObsFlg ){

        //監視を停止
        observer.disconnect();
        statsGeneralObsFlg = true;

        //必要転生回数を取得
        const $reca = document.getElementById('statsGeneral').children[4];
        const recaCount = $reca.innerText.replace(/^.+昇天 (.+)回$/,'$1');
        const recaRemainsCount = 1000 - recaCount;

        //メインプログラム
        async function clickForReincarnation(){

          //通知を削除
          const $notes = document.getElementById('notes').children ? document.getElementById('notes').children : undefined ; 
          if($notes){
            const $notesBtn = $notes[$notes.length - 1];
            $notesBtn.click();
          } 

          //100購入タブに切り替え
          const $storeBulkTab = document.getElementById('storeBulk100');
          $storeBulkTab.click();

          //アップグレードの一括購入ボタンをクリック
          await clickUpgrade(2000,4);

          //すべての施設をループして購入不可能施設の一つ手前の施設の購入ボタンをクリック
          await clickProduct(4000,4);

          //アップグレードの一括購入ボタンを再度クリック
          await clickUpgrade(500,5);

          //天国に行って戻ってくる
          await goToHeaven();

          //アップグレード購入関数
          function clickUpgrade(d,c){
            return new Promise(resolve => {
              let counter = 1;
              let timerID = null;

              const proc = () => {
                if(counter <= c){
                  const $storeAllBtn = document.getElementById('storeBuyAllButton');
                  $storeAllBtn.click();
                  
                  console.log('upgrade',d,c,counter,timerID);
                  
                  //最後のループでループを止める
                  if(counter === c){
                    clearTimeout(timerID);  
                    resolve();
                  }
                  else{
                    timerID = setTimeout(() => { proc(); },d);
                  }
                }
                counter ++;
              }
              proc();
            });
          }    

          //施設購入関数
          function clickProduct(d,c){
            return new Promise(resolve => {
              let counter = 1;
              let timerID = null;

              const proc = () => {
                if(counter <= c){
                  //購入可能な施設に対してループ
                  const $products = document.querySelectorAll('.product');
                  const $enableprods = Array.from($products).filter(el =>
                    !el.classList.contains('locked') && !el.classList.contains('disabled')
                  );
                  $enableprods[$enableprods.length - 1].click();
                  
                  console.log('product',d,c,counter,timerID);
                  
                  //最後のループでループを止める
                  if(counter === c){
                    clearTimeout(timerID);  
                    resolve();
                  }
                  else{
                    timerID = setTimeout(() => { proc(); },d);
                  }
                }
                counter ++;
              }
              proc();
            });
          }

          //天国での手続き
          function goToHeaven(){
            return new Promise(resolve => {

              const proc = () => {

                //フラグ
                let ascendBtnObsFlg = false;
                // 変更を監視するノード
                const $ascendBtnObsArea = document.getElementById('wrapper');
                // オプション
                const ascendBtnConfig = { attributes: true, childList: true, subtree: true };
                // コールバック
                const ascendBtnCallback = function (mutationsList, observer) {
                  for (const mutation of mutationsList) {
                    if(mutation.type === 'attributes' && mutation.attributeName === 'class' && mutation.target.classList.contains('ascending') && !ascendBtnObsFlg){
                      //再転生ボタンをクリック
                      const $ascendBtn = document.getElementById('ascendButton');
                      $ascendBtn.click();

                      //ダイヤログをクリック
                      const $ascendDialogBtn = document.getElementById('promptOption0');
                      $ascendDialogBtn.click();

                      resolve();
                      ascendBtnObsFlg = true;
                      observer.disconnect();
                    }
                  }
                };
                //再転生ボタンのエリアを監視
                observeElement(ascendBtnCallback,$ascendBtnObsArea,ascendBtnConfig);

                //転生ボタンをクリック
                const $legacyBtn = document.getElementById('legacyButton');
                $legacyBtn.click();
                
                //ダイヤログをクリック
                const $legacyDialogBtn = document.getElementById('promptContentAscend').querySelector('.smallFancyButton');
                $legacyDialogBtn.click();
                
              }
              proc();
            });
          }
        }

        // 輪廻転生関数
        async function loopReincarnations(n) {
          for (let i = 0; i < n; i++) {
            await clickForReincarnation();
          }
        }

        // 規定の回数を指定して実行
        loopReincarnations(recaRemainsCount);
      }
    }
  }
};
//監視関数呼び出し
observeElement(statsGeneralCallback,$statsGeneralObsArea,statsGeneralConfig);

//監視関数
function observeElement(cb,t,c){
  // コールバック関数に結びつけられたオブザーバーのインスタンスを生成
  const observer = new MutationObserver(cb);

  // 対象ノードの設定された変更の監視を開始
  observer.observe(t, c);
}
```

# 感想
- MutationObserverとasync/awaitの良い復習になった。
- エラー発生時の対処記述がないのは微妙。でも今回はもうつかれた。これでよしとする。
- 監視関数も都度停止してるけどサーバーにめちゃくちゃ負担かかってたらどうしよう。大丈夫と思うけど
- 次はSpeedBakingⅢのチートコードを書きたいな〜。