Python
JavaScript
Selenium
Python3
CookieClicker

Cookie Clickerを「全自動化」する

More than 1 year has passed since last update.

目的

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

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

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

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

そもそもCookie Clickerとは

ババアがクッキーを焼くゲームである。以上。
遊びたい人はこちらから

作ってみたもの

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

ファイル構成

Cookie Clicker/
    ├ Building.py
    ├ AutoCookieClicker.py
    └ save(git)/
      └ savedata

ソースコード

ガバ設計、動けばいいや程度のものなので色々とひどい状態です。
githubはこちら:https://github.com/RyuSA/CookieClicker

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")
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を使うを参考にしました。

Building

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

CookieClicker

今のところそんなに煩雑なことはしないので、ただFirefoxを起動してjavascriptを走らせているだけ。
javascriptはCookieClickerのソースコードを読んで持ってきました。

セーブデータ管理(Import/Export_Savedata)

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

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

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

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

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

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購入に時間がかかるため)

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

感想

クッキークリッカーたのしい