Cookie Clickerを「全自動化」する

# 目的
~~研究に疲れきってしまって~~せっかくプログラミングできるのだからと、昔遊んでいたCookie Clickerというゲームを完全自動化を目指してみた。

## ここでの「完全自動化」とは

こんなことができたら嬉しい

python CookieClicker.py
→ クッキークリッカーが起動
→ ファイルからセーブ読み込み
→ 大クッキー＆ゴールデンクッキーの自動クリック
→ 適宜アップグレード＆building購入
→ 適宜セーブ

## そもそもCookie Clickerとは
ババアがクッキーを焼くゲームである。以上。
遊びたい人は[こちら](http://orteil.dashnet.org/cookieclicker/)から


# 作ってみたもの

基本的には、python(selenium)でFirefoxをリモートで動かしつつ、javascriptをselenium越しに操作させる。
自動クリック部分のソースコードは、[Cookie Cliker Wiki](https://www55.atwiki.jp/cookieclickerjpn/)からお借りしました。

## ファイル構成
Cookie Clicker/
　　　　├ Building.py
　　　　├ AutoCookieClicker.py
　　　　└ save(git)/
　　　　   　└ savedata

## ソースコード
ガバ設計、動けばいいや程度のものなので色々とひどい状態です。
githubはこちら：[https://github.com/RyuSA/CookieClicker](https://github.com/RyuSA/CookieClicker)

```python:CookieClicker.py
import sys
import git
import csv
import time
from selenium import webdriver
from time import sleep
from datetime import datetime
from buildings import Building
from configparser import SafeConfigParser

URL_CookieClicker = "http://orteil.dashnet.org/cookieclicker/"
EXECUTE_CLICK_BIGCOOKIE = "setInterval(function() {Game.ClickCookie(); Game.lastClick=0; }, 1000/1000);"
EXECUTE_CLICK_GOLDENCOOKIE = "setInterval(function(){for (var i in Game.shimmers) { Game.shimmers[i].pop(); }}, 500);"

def git_commit():
    repository = git.Repo("save")
    repository.git.add(".")
    commit_meessage = datetime.now().isoformat()
    repository.index.commit(commit_meessage)

class CookieCliker(object):
    Path = "save/savedata"
    WINDOW_WIDTH = 1200
    WINDOW_HEIGHT = 500
    SAVE_INTERVAL = 30
    NUMBER_OF_BUY = 1000
    INTERVAL = 30
    PRODUCTS_LIST = ("Cursor", "Grandma", "Farm", "Mine", "Factory", "Bank", "Temple", "Tower", "Shipment", "Lab", "Portal", "Time_Machine", "Condefsor", "Prism", "Chansemaker")
    def __init__(self, url):
        self.driver = webdriver.Firefox()
        self.driver.set_window_size(CookieCliker.WINDOW_WIDTH, CookieCliker.WINDOW_HEIGHT)        
        self.driver.get(url)
        # 2秒待てば、ブラウザの読み込みが間に合う
        sleep(2)
        # クリック用の大クッキー取得
        self.BigCookie = self.driver.find_element_by_id("bigCookie")
        self.products = []
        for id, name in enumerate(CookieCliker.PRODUCTS_LIST):
            self.products.append(Building(name = name, id = id, driver = self.driver))
        print("Cookie Clicker has been initialized")

# セーブデータをブラウザからコピぺして保存
    def Export_Savedata(self):
        OpenTextAreaScript = "Game.ExportSave();"
        self.driver.execute_script(OpenTextAreaScript)
        savedata = self.driver.find_element_by_id("textareaPrompt").text
        f = open(CookieCliker.Path, 'w')
        f.write(savedata)
        f.close()
        self.driver.execute_script("Game.ClosePrompt();")
        git_commit()

    def Import_Savedata(self):
        OpenTextAreaScript = "Game.ImportSave();"
        ImportScript = "Game.ImportSaveCode(l('textareaPrompt').value);"
        LoadScript = "Game.ClosePrompt();"

        # Importを開く
        self.driver.execute_script(OpenTextAreaScript)

        # 注意：savefileの中には1行しか記述されていないはず
        f = open(CookieCliker.Path, 'r')
        # savedataが読み込めないことがあったら、空のセーブデータでプレイ
        savefile = ""
        for a in f:
            savefile = a
        f.close()

        self.driver.find_element_by_id("textareaPrompt").send_keys(savefile)
        self.driver.execute_script(ImportScript)
        self.driver.execute_script(LoadScript)
        for p in self.products:
            p.Update(self.driver)

        print("/////////////////////////////////////")
        print("/           Load done               /")
        print("/////////////////////////////////////")

    def Auto_Clicker(self):
        print("set auto click")
        global EXECUTE_CLICK_BIGCOOKIE, EXECUTE_CLICK_GOLDENCOOKIE
        self.driver.execute_script(EXECUTE_CLICK_BIGCOOKIE)
        self.driver.execute_script(EXECUTE_CLICK_GOLDENCOOKIE)

    def Buy_allUpgrades(self):
        print("Buy all upgrades")
        while True:
            try:
                upgrade = self.driver.find_element_by_id("upgrade0")
                if "enabled" in upgrade.get_attribute("class"):
                    upgrade.click()
                else:
                    return
            except:
                # print(something happened : at buyallupgrades)
                return

    def Standard_Strategy(self):
        # productsの中で、Cps_per_priceの高いもの順に並び替え
        temp = sorted(self.products, key=lambda p:-p.Cps_per_price)
        count = 0
        max_count = 3
        buythem = []
        # Cps_per_priceの高い順に、unlockされているものを4つ持ってくる
        for p in temp:
            if p.is_unlocked:
                buythem.append(p)
                count += 1
                if count > max_count:
                    break
        print("Standard Strategy begin")
        for p in buythem:
            if p.is_active(self.driver):
                # NUMBER_OF_BUYだけ購入
                p.Buy(CookieCliker.NUMBER_OF_BUY, self.driver)
        print("Standard Strategy end")

    def Run(self):
        print("#####################################")
        print("#        Running CookieCliker       #")
        print("#####################################")
        self.Import_Savedata()
        self.Auto_Clicker()
        fieldnames = ("time","Cookies", "Cps")          
        save_point = 0
        data = [0, 0, 0]
        start = time.time()
        while True:
            self.Buy_allUpgrades()
            self.Standard_Strategy()
            print("wait" + str(CookieCliker.INTERVAL) + "sec")
            sleep(CookieCliker.INTERVAL)
            save_point += 1
            if save_point > 10:
                self.Export_Savedata()
                data = {}
                data["time"] = time.time() - start
                data["Cookies"] = self.driver.execute_script("return Game.cookies")
                data["Cps"] = self.driver.execute_script("return Game.cookiesPs")
                save_point = 0
                with open("log.csv", "a", newline="") as log:
                    temp = csv.DictWriter(log, fieldnames=fieldnames)
                    temp.writerow(data)
            print("--------------------------------------")

clicker = CookieCliker(URL_CookieClicker)

clicker.Run()
    
print("cookie Clicker end")
```

```py3:buildings.py
class Building(object):
    product = "product"

    def __init__(self, name ,id , driver):
        self.id = id
        self.productid = "product" + str(id)
        self.name = name
        self.execute_ThisBuilding = "Game.ObjectsById[" + str(id) + "]"
        self.Cps = driver.execute_script("return " + self.execute_ThisBuilding + ".storedCps;")
        self.price =  driver.execute_script("return " + self.execute_ThisBuilding + ".price;")
        self.Cps_per_price = self.Cps / self.price

    def Buy(self, bought_number, driver):
        # クリックさせるよりも高速
        driver.execute_script(self.execute_ThisBuilding + ".buy(" + str(bought_number) + ");")
        self.Cps = driver.execute_script("return " + self.execute_ThisBuilding + ".storedCps;")
        self.price =  driver.execute_script("return " + self.execute_ThisBuilding + ".price;")
        self.Cps_per_price = self.Cps / self.price
    
    def Sell(self, sold_number, driver):
        driver.execute_script(self.execute_ThisBuilding + ".sell(" + str(bought_number) + ");")
        self.Cps = driver.execute_script("return " + self.execute_ThisBuilding + ".storedCps;")
        self.price =  driver.execute_script("return " + self.execute_ThisBuilding + ".price;")
        self.Cps_per_price = self.Cps / self.price
    
    def is_active(self, driver):
        return "enabled" in driver.find_element_by_id(self.productid).get_attribute("class")

    def is_unlocked(self, driver):
        return "unlocked" in driver.find_element_by_id(self.productid).get_attribute("class")

    def Update(self, driver):
        self.Cps = driver.execute_script("return " + self.execute_ThisBuilding + ".storedCps;")
        self.price =  driver.execute_script("return " + self.execute_ThisBuilding + ".price;")
        self.Cps_per_price = self.Cps / self.price
```

# 少し解説。

## git_commit
Gitpythonを使って、pythonからsaveリポジトリのコミットをさせる関数。
自動化しつつセーブデータを個別に保存すると膨大な数のセーブデータが生まれてしまうので、今回はセーブデータ部分をgitで管理することにした。

[GitPythonを使う](https://qiita.com/TakesxiSximada/items/ea778bb98b8c057ffd07)を参考にしました。

# Building
基本的に、時間経過のみで変更のない情報(Cps, price)のみを保有し
残りは適宜javascriptを走らせて調べるようにしました。
……プログラム設計的には、どうした方がよいのでしょうかね？？

## CookieClicker

今のところそんなに煩雑なことはしないので、ただFirefoxを起動してjavascriptを走らせているだけ。
javascriptはCookieClickerの[ソースコード](https://orteil.dashnet.org/cookieclicker/main.js)を読んで持ってきました。

### セーブデータ管理(Import/Export_Savedata)
Cookie ClickerのOptionsをクリックして～という単純な方法を取ってもよかったのですが、javascriptを読んでみると以下のようにすればデータのロードができるようになることがわかりました。

```js:firefox_console
Game.ImportSave();
// id = textareaPrompt のテキストエリアが出現
// id = textareaPrompt のテキストエリアにセーブデータを入力する

Game.ImportSaveCode(l('textareaPrompt').value);
//セーブデータがロードされる

Game.ClosePrompt();
// テキストエリアを閉じる
```

同様に、Exportも以下のようにすれば良いことがわかりました。

```js:firefox_console
Game.ExportSave();
// id = textareaPrompt のテキストエリアが出現
// id = textareaPrompt のテキストエリアのセーブデータを保存

Game.ClosePrompt();
// テキストエリアを閉じる
```

### 自動アップグレード購入(Buy_allUpgrades)

アップグレードには、"id = upgradeX" というidが振られており(Xには0以上の整数)
購入できるものには"class enabled"が振られていました。 なので、"id = upgrade0"のアップグレードが"class enabled"を保有しているときに購入、という購入をできる限り行うようにさせました。

### 自動Building購入(Standard_Strategy)

一番悩みましたが、結局「(unclock済みの中で)Cps/priceの高い順に並べた上位4位の中で、購入可能なbuildingをできる限り購入する」という単純な手法を取りました。
ただし、この方法だと

- アップグレードの順番考慮0
- シナジーも考慮0
- なぞの新要素(ドラゴンエッグやSugar lumps)
- 後半、Cpsが全く伸びない(Building購入に時間がかかるため)

などの問題が発生して全然効率的じゃないです……(購入タイミングも含めて)


# 感想
クッキークリッカーたのしい