Edited at

CookieClickerチートで学ぶJavaScript

More than 1 year has passed since last update.


これは何?

プログラムやJavaScriptに縁のない人が、CookieClickerのチートを通じてJavaScriptとはどんな言語か、触れてみることを目的としたページです。

使用するブラウザはfirefoxを想定していますが、他のブラウザでも、UIの多少の差異はあるものの、同様のことが行えます。


大量のクッキーを獲得しよう

まず手始めに、大量のクッキーを獲得してみましょう。そのためには、ソースコードの表示、CookieClickerの処理モデルの把握、JavaScriptの操作を行う必要がありますので、順を追って説明していきます。


コンソールの表示

ブラウザの [ツール]-[Web開発]-[開発ツールを表示]をクリックするか、F12を押下することで、開発コンソールを表示することができます。


ソースコードの表示

開発コンソールを表示したら、[デバッガ]をクリックしてみましょう。するとソースコードが表示されるはずです。CookieClickerのソースコードは複数のファイルに分割して実装されていますが、主となるのはmain.jsです。なので、main.jsをクリックしてみましょう。

一万行近いソースコードが表示されて面食らったかもしれませんが、気にすることはありません。必要な場所だけ見ていけば良いのです。


CookieClickerの処理モデルの把握

プログラム経験のない人はこの部分は読み飛ばしてもらって問題ありません。そういうものだ、程度に思っていてください。

プログラム経験のある人は、main.jsの450行目近辺を見てください。以下の記述が見つかるはずです。

var Game={}

これが、CookieClickerというゲームの処理やゲーム内データを保持するためのグローバルインスタンスです。

このインスタンスのプロパティを上書きすることで、ゲーム内データを操作することができます。

また、このインスタンスのメソッドを上書きすることで、ゲームの処理を上書き定義してやることが可能です。


コンソールからのJavaScriptの操作

コンソールに以下を入力し、Enterを押してみてください。

Game.cookies

すると、現在の所有クッキー数が表示されたはずです。

次に、以下を入力し、Enterを押してみてください。

Game.cookies=1000000000

すると、所有クッキー数が1000000000になるはずです。

しかしそれと同時にCheated cookies taste awefulという隠し実績も解除されてしまいました。チーター呼ばわりです。


汚名を返上しよう

別にチーター呼ばわりされるのは構いませんが、いきなり首根っこを押さえつけられたようで、どうにも癪です。我々チーターとCookieClickerのプログラム、どちらが立場が上か分からせてやるために、この実績をなかったことにしてしまいましょう。


チート実績の消し方を調べよう

先ほどは、Game.cookieを上書きすれば所有クッキー数を増やせると解説しましたが、今回は、何をどうすればチート実績を消せるか、自分で調べてみましょう。

まずは、先ほどのmain.js内を、cheatの文字で検索してみてください。すると十数か所、該当箇所が見つかったはずです。

チート実績はおそらく0か1か、あるいはtrueかfalseかのフラグで管理されていると思われます。それを踏まえて該当箇所を眺めてみると、以下の処理があからさまに怪しいです。

Game.Achievements['Cheated cookies taste awful'].won=1;

試しにコンソールに、

Game.Achievements['Cheated cookies taste awful'].won=0;

と打ち込んでみましょう。

実績は消えましたか?消えましたね。


心を入れ替えよう

チートの練習とはいえ、クッキーの数を直接いじるのは、さすがにちょっと味気ないです。というわけで心を入れ替えて自分でクッキーを焼くことにしましょう。

とはいえ、なるべく楽をしたいのが人情です。CookieClickerで楽といえばゴールデンクッキー。ゴールデンクッキーの出現時間を予測できたら、今までよりも楽に、かつ自分の力でクッキーを焼くことができます。

さっそく始めてみましょう。


ゴールデンクッキーの出現仕様を調べる

例のごとくソースコードをgoldenの文字で検索してみましょう。どうやら3013行目近辺に、ゴールデンクッキー関係の処理がまとめて実装されているようです。以下の処理がゴールデンクッキーの出現仕様に関係していそうです。

Game.goldenCookie.spawn=function()

Game.goldenCookie.update=function()

名前と内容から察するに、spawnメソッドがクッキー出現用のメソッド、そしてクッキー出現判定を行っているのが、updateメソッドであることが伺えます。

そしてupdateメソッド内、以下の処理が見つかりました。

if (this.toDie==0 && this.life<=0 && Math.random()<Math.pow(Math.max(0,(this.time-this.minTime)/(this.maxTime-this.minTime)),5)) this.spawn();

わからないなりに、頑張って読み解いてみましょう。

プログラムを読むコツはとりあえずダメ元で挑戦してみることです。わかればラッキーですし、わからなかったら少し凹むかもしれませんが、自分に都合の悪い記憶はさっさと忘れてしまえば実質ノーダメージです。

そしてどうやらこのif文は


  • 死亡フラグが立っておらず

  • ライフが残っている状態で

  • 乱数値がMath.pow(Math.max(0,(this.time-this.minTime)/(this.maxTime-this.minTime)),5)を超えていたら

  • ゴールデンクッキーが出現する

という処理のようです。

さて、Math.randomやMath.pow、Math.maxは各自ぐぐってもらうとして、this.timeやthis.minTimeとは何でしょうか?そういうときはコンソールで

Game.goldenCookie

と打ち込んでみましょう。

Object { x: 602, y: 382, life: 0, time: 6719, minTime: 9000, maxTime: 27000, dur: 13, toDie: 0, wrath: 0, chain: 0, 他 14 個... }

といったような表示が出力されたはずです。ゲームを操作しつつこの値を観察し、先ほどの数式と合わせて考えると次のことがわかります。


  • ゴールデンクッキー消滅から一定時間は出現率はゼロ

  • ゴールデンクッキー消滅から一定時間後は、対数的に出現率が上昇していく

以上から、次の情報を画面に表示させることができれば、ゲームを快適にプレイすることができそうです。


  • ゴールデンクッキー発生確率が0を超えるまで/超えてからの秒数

  • 現在のゴールデンクッキー出現確率


情報の表示方法を考えよう

みなさん英語は読めますか?私は読めません。

というわけで、ゲーム上部のNews表示欄は邪魔です。我々のチート情報表示領域として活用してあげましょう。

例のごとくソースコードをNewsと検索すると、どうやら

Game.getNewTicker=function()

が、表示するニュース記事を決める処理のようです。というわけでこの処理を上書きしてしまいましょう。


実装です

テキストエディタを開き、Game.getNewTicker メソッドの不要な部分を削除したものを作成します。

Game.getNewTicker = function() {

Game.TickerAge=Game.fps*10;
Game.Ticker='hello ticker';
Game.AddToLog(Game.Ticker);
Game.TickerN++;
Game.TickerDraw();
};

そしてそれをコンソールに貼り付けて実行してみましょう。

hello tickerと表示されましたね。

ではコードをちょちょいと書き足して、完成です。

/* ニュースの代わりに以下を表示

* - ゴールデンクッキーのクールダウン期間
* - ゴールデンクッキーの出現率
*/

Game.getNewTicker=function()
{
function calcCd(o) {
var cooldownTime = ((o.minTime - o.time)/30).toFixed(0);
var popupRate = (Math.pow(Math.max(0,(o.time-o.minTime)/(o.maxTime-o.minTime)),5)*Game.fps*100).toFixed(2) + '%' ;
return cooldownTime + ',' + popupRate;
}
Game.TickerAge=Game.fps;

// クールダウン期間
Game.Ticker = 'cooldown:' + calcCd(Game.goldenCookie) + '<br/>';

Game.TickerAge=Game.fps*10;
Game.AddToLog(Game.Ticker);
Game.TickerN++;
Game.TickerDraw();
};


一番お買い得な施設を買おう

267クッキーのおばあちゃんと、1,265クッキーの農場、どちらがお買い得ですか?割り算しないとわかりませんね。

割り算は難しいので、計算を自動化してしまいましょう。

理想は、施設の価格の下に投資効率が表示されていることです。そしてこのゲームでは、施設を買えば買うほど投資効率が等比級数的に落ちていくので、投資効率表示には対数を使用することとしましょう。

また、秒間17クッキーが自動生産されているときに12,000クッキーの鉱山を買うために、何分待機すればよいのか計算するのも面倒です。なので、施設の価格の隣に、12,000(0:11:46)といった形で、表示してあったら嬉しいですね。作りましょう。


実装方針を考えよう

ソースコードをfarm等のキーワードで調べた結果、Game.Objectsが施設を表すインスタンス群であることがわかりました。そしてrebuildが施設のDOMを生成するメソッドのようです。

しかしここで問題が。rebuildメソッドはそれなりに分量があるため、getNewTickerのように無理やり上書き実装してしまうと、CookieClickerのバージョンアップのときに、チートコードまで書き直す必要が出てきてしまいそうです。

そこで次のような方針を採ります。


  • オリジナルのrebuildメソッドを退避

  • 新しいrebuildメソッドを定義しその中からオリジナルのrebuildメソッドを呼び出す

  • その後、DOMの末尾に追加表示したいhtmlを付け足す


そして実装

というわけでコードはこんな感じです。

function TimeBeautify(sec) {

function f(s) {
if ((''+s).length == 1) {
return '0' + s;
}
return s;
}
var d = Math.floor(sec/60/60/24);
var h = Math.floor((sec/60/60)%24);
var m = Math.floor((sec/60)%60);
var s = Math.floor(sec%60);
return ((d>0) ? (d + 'd ') : '') + h + ':' + f(m) + ':' + f(s);
}

for (var i in Game.Objects) {

var me=Game.Objects[i];
if (me._rebuild === undefined) {
me._rebuild = me.rebuild;
}
(function(me) {
me.rebuild = function() {
me._rebuild();
var baseCps = (Game.frenzy > 0) ? (Game.cookiesPs/Game.frenzyPower) : Game.cookiesPs;
var appendText = '(' + TimeBeautify((me.price/baseCps).toFixed(0)) + ')<br/><span class="price">'+(Math.log(me.storedCps/me.price)/Math.log(10)).toFixed(4)+'</span>';
l('productPrice'+me.id).innerHTML=Beautify(Math.round(me.price)) + appendText;
}
})(me);
me.rebuild();
}


心を入れ替えるといったな、あれは嘘だ

やっぱりゴールデンクッキーを手動でクリックするのはめんどうなのでそれだけ自動化しちゃえ。

if (Game._Loop === undefined) {

Game._Loop = Game.Loop;
}
Game.Loop = function() {
if (Game.goldenCookie.life>0) Game.goldenCookie.click();
if (Game.seasonPopup.life>0) Game.seasonPopup.click();
Game._Loop();
}

(続く?)