The Candy Crush Saga saga

I find Flash games on Facebook great fun. Not playing them, of course, that’s boring. As you may remember from my previous post, “winning at Puzzle Adventures“, I like to take a look into their guts and figure out how they work, and whether or not I can get insane scores with no effort.

When I discovered Candy Crush Saga, I was intrigued. All my friends appeared mad about this game, sending me so many requests for candy that their dentist would surely commit harakiri. I started playing a bit, and it wasn’t long until I had to stop playing, since the game only allows you a set number of lives per hour in an attempt to either extract money from you or coax you into spamming your friends with requests for the game, to increase its popularity.

Cheating at online games

This, however, wouldn’t do, so I fired up the Swiss army knife of web debugging, Charles Proxy (it’s a fantastic tool for this job). I started looking at the requests the game was making to the server, and saw one that looked promising:

http://candycrush.king.com/api/gameInitLight "currentUser" : { "lives" : 5 , "maxLives" : 5 , }

I added a breakpoint and edited lives to always be 5, which did allow me to play for ever, no matter how much I lost. Yay for non-existent server checks! This wasn’t great, though. Sure, I could play as much as I want, but I don’t want to play at all! I wanted to see if I could make the game much easier somehow.

Looking some more, I found the call that loads the level, and the level details specify the number of colors to use on the level. Since the game is played by getting candy of one color to line up in a row, fewer colors means a considerably easier game. However, too few colors and the game won’t end at all! I discovered that four colors is the sweet spot, and edited the level accordingly:

http://candycrush.king.com/api/gameStart { "levelData" :{ "numberOfColours" : 4 , "gameModeName" : "Light up" , "pepperCandyMax" : 1 , "pepperCandySpawn" : 3 , "chameleonCandyMax" : 0 }, }

Success! There were only four colors in the level, and the game pretty much cascaded into a huge victory after two or three moves.

Diving deeper

Even this, though, was very time-consuming. The game has 500 levels, and, with 3 minutes per level, it would take way too long to win every single one. I needed to discover a faster way to “play” games. Helpfully, the server gives you a list of all the API methods when there’s an error:

com . king . saga . api . CurrentUserResponse poll (); com . king . saga . api . GetMessagesResponse getMessages (); [ Lcom . king . saga . api . ApiItemInfo ; unlockItem ( java . lang . String , java . lang . String ); com . king . saga . api . ApiGameEnd gameEnd ( com . king . saga . api . ApiGameResult ); com . king . saga . api . GameInit gameInit (); com . king . saga . api . ApiGameStart gameStart ( int , int ); com . king . saga . api . PeekMessagesResponse peekMessages (); com . king . saga . api . GetMessagesResponse removeMessages ( [ Ljava . lang . Long ;, boolean ); com . king . saga . api . GameInit gameInitLight (); com . king . saga . api . ApiGameEnd gameEnd2 ( com . king . saga . api . ApiGameResult );

Looking at those and the responses, we can immediately see that the gameEnd method is very interesting indeed:

http://candycrush.king.com/api/gameEnd { "score" : 373400 , "variant" : 0 , "seed" : 1384024506403 , "reason" : 0 , "timeLeftPercent" : -1 , "cs" : "040a6a" , "episodeId" : 34 , "levelId" : 4 }

What’s this? It looks like we can just tell the game we finished a level, without any other hassle. I tried to replay that request by changing the score, but nothing happened. Nothing happened if I specified different episode ids, either. There must be some signature we can’t duplicate, and the “cs” field sounds awfully close to “checksum”. I was very close to solving the whole puzzle, but the checksum field means that I will have to fake the signature (which, unless the Candy Crush developers are clueless, should contain a secret key).

There’s really no way to figure out how the signature is produced without looking at the source, so that’s what I did. Using my trusty Flash decompiler, I rummaged around in the source code, and found that the cs parameter is basically an MD5 of the following:

"episodeId:levelId:score:timeLeftPercent:userId:seed:secretkey"

Finding the secret key took a bit more digging, but, with the final piece of the puzzle uncovered, I could finally make make requests to the server and make it seems as if we legitimately finished a game. I’m not sure if you really have to make the gameStarted call, or if you can pass in any random seed you want, but I think the call is optional. At this point, I could pretty much finish any level I want, with any score I want, in about half a second.

A script to make it easier

However, having to do all this in a shell (or curl) was still too hard. To remove that final obstacle, i wrote a small CCrush Python class with various methods to make automatically winning in the game easier. Here’s the listing:

import requests import hashlib import json import random import time import sys class CCrush ( object ): def __init__ ( self , session ): self . session = session def hand_out_winnings ( self , item_type , amount ): item = [{ "type" : item_type , "amount" : amount }] params = { "_session" : self . session , "arg0" : json . dumps ( item ), "arg1" : 1 , "arg2" : 1 , "arg3" : "hash" , } return requests . get ( "http://candycrush.king.com/api/handOutItemWinnings" , params = params ) def add_life ( self ): params = { "_session" : self . session } return requests . get ( "http://candycrush.king.com/api/addLife" , params = params ) def start_game ( self , episode , level ): params = { "_session" : self . session , "arg0" : episode , "arg1" : level } response = requests . get ( "http://candycrush.king.com/api/gameStart" , params = params ) return response . json ()[ "seed" ] def end_game ( self , episode , level , seed , score = None ): if score is None : score = random . randrange ( 3000 , 6000 ) * 100 dic = { "timeLeftPercent" : - 1 , "episodeId" : episode , "levelId" : level , "score" : score , "variant" : 0 , "seed" : seed , "reason" : 0 , } dic [ "cs" ] = hashlib . md5 ( " %(episodeId)s : %(levelId)s : %(score)s : %(timeLeftPercent)s :userid: %(seed)s :thesecret" % dic ) . hexdigest ()[: 6 ] params = { "_session" : self . session , "arg0" : json . dumps ( dic )} response = requests . get ( "http://candycrush.king.com/api/gameEnd" , params = params ) return response def play_game ( self , episode , level , score = None ): seed = self . start_game ( episode , level ) return self . end_game ( episode , level , seed , score ) if __name__ == "__main__" : ccrush = CCrush ( sys . argv [ 1 ]) episode = int ( sys . argv [ 2 ]) level = int ( sys . argv [ 3 ]) seed = ccrush . start_game ( episode , level ) ccrush . end_game ( episode , level , seed )

This script can be invoked with python ccrush.py <sessionid> <episode> <level> , and it will automatically pass that level with a random score. Here’s the result:

The script doesn’t actually work without the secret key, but that’s left as an exercise for the reader. It does work for getting extra lives for free, or extra helper items/gold, though. My aim with this post is less about telling people how to cheat in the game and more about the thought process behind analyzing these things.

NOTE: After the amazing response to this post, I feel like I need to clarify something: It’s not a bug, or a fault on the part of the developers of Candy Crush that this is possible. Spending time and effort implementing anti-cheat measures would just be wasted, since it brings them no benefit (the most cheaters can do is brag to their friends, and they most probably weren’t going to pay anyway). So, I’m not implying that King should fix this, or that they don’t know what they’re doing. If I were developing Candy Crush, I wouldn’t put in any anti-cheating code either (as I said above, it would bring no benefit).

I hope you enjoyed this short exploration of game APIs, and got a bit more knowledge in the process. For more posts like this, you can read my “winning” series, subscribe to my mailing list below to be notified of new posts, or follow me on Twitter.