📜 ⬆️ ⬇️

Reverse engineering and writing a bot for a flash game on Go

image In this article I will explain how to decompile a flash application and then extract the algorithm for signing requests to the game server. And how on the basis of this information to write a game bot on Go .
It all started with the fact that I was looking for a strategy to play with my android smartphone. Found a good game called "Throne rush". Then it turned out that there is a browser client that makes some actions more convenient. But nevertheless game process obviously demanded automation. I used the excellent tool JPEXS Free Flash Decompiler , which will be discussed further.

The source code of the project on github .

So, the task is to emulate the work of the native client.
Firebug'm looking communication flash applications
POST https://epicwar-facebook.progrestar.net/rpc/ Accept text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Encoding gzip, deflate Accept-Language uk Connection keep-alive Cookie __utma=252078920.1702705582.1400051769.1400051769.1400051769.1; __utmz=252078920.1400051769.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none) Host epicwar-facebook.progrestar.net User-Agent Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:29.0) Gecko/20100101 Firefox/29.0 Content-Length 1913 Content-Type application/json; charset=UTF-8 X-Auth-Application-Id 1424411677784893 X-Auth-Network-Ident facebook X-Auth-Session-Id 0n5nkrp20i8sf9 X-Auth-Session-Init 1 X-Auth-Signature 57d0320e91c26cd8e56152d3aad1a809 X-Auth-Token 6514a97ae525f196b8060337380e0cbb X-Auth-User-Id 675063875 X-Env-Library-Version 0 X-Request-Id 1 X-Requested-With XMLHttpRequest X-Server-Time 1400219523 {"calls":[{"name":"registration","args":{"user":{"locale":"en","id":"675063875","birthday":"1970-1-1","referrer":{"type":"bookmark"},"lastName":"","city":null,"country":null,"firstName":"","sex":"female"},"friendIds":[]},"ident":"registration"},{"name":"boostGetAll","args":{},"ident":"boostGetAll"},{"name":"getTime","args":{},"ident":"getTime"},{"name":"getSelfInfo","args":{},"ident":"getSelfInfo"},{"name":"getDynamicParams","args":{},"ident":"getDynamicParams"},{"name":"getArmyQueue","args":{},"ident":"getArmyQueue"},{"name":"getBuildings","args":{},"ident":"getBuildings"},{"name":"heroesGetList","args":{},"ident":"heroesGetList"},{"name":"getResearchQueue","args":{},"ident":"getResearchQueue"},{"name":"getMissions","args":{},"ident":"getMissions"},{"name":"getQuests","args":{},"ident":"getQuests"},{"name":"getProtections","args":{},"ident":"getProtections"},{"name":"getInvitedBy","args":{},"ident":"getInvitedBy"},{"name":"getInvitedUsers","args":{},"ident":"getInvitedUsers"},{"name":"getBonusCrystals","args":{},"ident":"getBonusCrystals"},{"name":"getSettings","args":{},"ident":"getSettings"},{"name":"promoGetHalfBilling","args":{},"ident":"promoGetHalfBilling"},{"name":"giftGetAvailable","args":{},"ident":"giftGetAvailable"},{"name":"giftGetReceivers","args":{},"ident":"giftGetReceivers"},{"name":"cloverGetAll","args":{},"ident":"cloverGetAll"},{"name":"paymentsCount","args":{},"ident":"paymentsCount"},{"name":"cemeteryGet","args":{},"ident":"cemeteryGet"},{"name":"getNotices","args":{},"ident":"getNotices"},{"name":"allianceGetMessages","args":{},"ident":"allianceGetMessages"},{"name":"getGlobalNews","args":{},"ident":"getGlobalNews"},{"name":"battleGetActive","args":{},"ident":"battleGetActive"},{"name":"spellList","args":{},"ident":"spellList"},{"name":"spellProductionQueue","args":{},"ident":"spellProductionQueue"},{"name":"state","args":{},"ident":"state"}],"session":null} 


There is a problem with X-Auth-Signature. I did not know where to get it from. Obviously, this is some kind of hash of something. But what and what, is unknown. “FFDec” helped me in this. Opensource program, which is written by java, works under Linux. Perfect for this job.
In the html code you can see which file is loaded.

 <object id="flash-app" width="100%" height="100%" data="https://epicwar-a.akamaihd.net/facebook/static/assets/Start.swf?v=13" type="application/x-shockwave-flash"> 

But in reality, this is not what is needed. We look flashvars. There "preloader = https% 3A% 2F% 2Fepicwar-a.akamaihd.net% 2Ffacebook% 2Fv042% 2FFbLoader.swf% 3Fv% 3D2". But this is again not the case. And the necessary code, I do not remember exactly how I understood it, in fact, here https://epicwar-a.akamaihd.net/facebook/v042/EpicWar.swf .
')
We decompile.
Program window


In fact, there is export.
Here is the decompiled code
 protected function createHeaders(param1:RpcEntryBase) : Object { var _loc5_:String = null; var _loc2_:String = SocialAdapter.instance.flashVars["session_key"]; var _loc3_:Object = { "Content-Type":"application/json; charset=UTF-8", "X-Request-Id":++this.unionRequestID, "X-Auth-Network-Ident":Env.NETWORK, "X-Auth-Application-Id":SocialAdapter.instance.app_id, "X-Auth-User-Id":SocialAdapter.instance.getPlayer().id, "X-Auth-Session-Id":Env.sessionKey }; if(_loc2_ != null) { _loc3_["X-Auth-Session-Key"] = _loc2_; } var _loc4_:Object = param1.headers; if(_loc4_ != null) { for(_loc5_ in _loc4_) { _loc3_[_loc5_] = _loc4_[_loc5_]; } } return _loc3_; } protected function createAuthSignature(param1:Object, param2:RpcEntryBase) : ByteArray { var _loc5_:ByteArray = null; var _loc3_:Object = param2.request.getFormattedData(); var _loc4_:ByteArray = new ByteArray(); _loc4_.writeUTFBytes(param1["X-Request-Id"]); _loc4_.writeUTFBytes(":"); _loc4_.writeUTFBytes(SocialAdapter.instance.authentication_key); _loc4_.writeUTFBytes(":"); _loc4_.writeUTFBytes(param1["X-Auth-Session-Id"]); _loc4_.writeUTFBytes(":"); if(_loc3_ is ByteArray) { _loc5_ = _loc3_ as ByteArray; _loc5_.position = 0; _loc4_.writeBytes(_loc5_,0,_loc5_.length); } else if(_loc3_ is String) { _loc4_.writeUTFBytes(_loc3_ as String); } _loc4_.writeUTFBytes(":"); _loc4_.writeUTFBytes(this.createFingerprint(param1)); return _loc4_; } private function createFingerprint(param1:Object) : String { var _loc4_:String = null; var _loc5_:* = 0; var _loc6_:* = 0; var _loc7_:String = null; var _loc2_:Array = []; var _loc3_:* = ""; for(_loc4_ in param1) { if(_loc4_.indexOf("X-Env") != -1) { _loc7_ = _loc4_.substr(6); _loc2_.push( { "key":_loc7_.toUpperCase(), "value":param1[_loc4_] }); } } _loc2_.sortOn("key"); _loc5_ = _loc2_.length; _loc6_ = 0; while(_loc6_ < _loc5_) { _loc3_ = _loc3_ + (_loc2_[_loc6_].key + "=" + _loc2_[_loc6_].value); _loc6_++; } return _loc3_; } protected function addHeaders(param1:URLRequest, param2:RpcEntryBase) : void { var _loc4_:String = null; var _loc3_:Object = this.createHeaders(param2); var _loc5_:ByteArray = this.createAuthSignature(_loc3_,param2); _loc3_["X-Auth-Signature"] = MD5.hashBytes(_loc5_); for(_loc4_ in _loc3_) { param1.requestHeaders.push(new URLRequestHeader(_loc4_,_loc3_[_loc4_].toString())); } } 


It's simple. Normal md5. Below is the code on Go.
Network
 package network import ( "net/http" "strings" "bytes" "crypto/md5" "io" "encoding/hex" "strconv" "io/ioutil" "log" ) const SERVER_URL = "https://epicwar-facebook.progrestar.net/rpc/" const APP_ID = "1424411677784893" const AUTH_KEY = "6514a97ae525f196b8060337380e0cbb" const NETWORK = "facebook" var _unionRequestID int var _uid string var _sid string func createFingerprint(headers map[string]string) string { var fingerprint bytes.Buffer preparedHeaders := []Pair{} for header, _ := range headers{ if(strings.Index(header, "X-Env") != -1){ preparedHeaders = append(preparedHeaders, Pair{ key : strings.ToUpper(header[6:len(header)]), value : headers[header]}) } } sortByKey(preparedHeaders) count := len(preparedHeaders); i := 0; for i < count { fingerprint.WriteString(preparedHeaders[i].key) fingerprint.WriteString("=") fingerprint.WriteString(preparedHeaders[i].value) i++; } res := fingerprint.String() return res } func createAuthSignature (headers map[string]string, postData string) string { h := md5.New() io.WriteString(h, headers["X-Request-Id"]) io.WriteString(h, ":") io.WriteString(h, AUTH_KEY); io.WriteString(h, ":"); io.WriteString(h, headers["X-Auth-Session-Id"]); io.WriteString(h, ":"); io.WriteString(h, postData); io.WriteString(h, ":"); io.WriteString(h, createFingerprint(headers)); return hex.EncodeToString(h.Sum(nil)) } func createHeaders() (map[string]string) { _unionRequestID = _unionRequestID + 1 headers := map[string]string{ "X-Request-Id":strconv.Itoa(_unionRequestID), "X-Auth-Network-Ident":NETWORK, "X-Auth-Application-Id":APP_ID, "X-Auth-User-Id":_uid, "X-Auth-Session-Id":_sid, "X-Env-Library-Version": "0"} return headers } func addHeaders(req *http.Request, postData string) { headers := createHeaders(); headers["X-Auth-Signature"] = createAuthSignature(headers, postData); for index, header := range headers { req.Header.Add(index, header) } } func Post(postData []byte) []byte { client := &http.Client{} req, err := http.NewRequest("POST", SERVER_URL, bytes.NewReader(postData)) addHeaders(req, string(postData)) resp, err := client.Do(req) defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if(err != nil){ log.Fatal(err) } return body } func Init(Uid string, Sid string) { _unionRequestID = 0 _uid = Uid _sid = Sid } 


As you can see, I do not send extra headers. In the action script code, the RpcClientBase class is inherited by the RpcClient class from “com.progrestar.game.server.rpc” and appends more headers. For example, "X-Env-Library-Version" I took from there. Without it does not work.

Query generation functions
 package bot import ( "log" "encoding/json" ) func getFormattedData(calls Calls) []byte { data, err := json.Marshal(Request{Calls:calls, Session: nil}) if(err != nil){ log.Fatal(err) } return data } func getStartBoard() []byte { return getFormattedData(Calls{ Call{Name:"getSelfInfo",Args:struct{}{},Ident:"getSelfInfo"}, Call{Name:"getBuildings",Args:struct{}{},Ident:"getBuildings"}}) } func collectResource(building uint64) []byte { return getFormattedData(Calls{ Call{ Name:"collectResource", Args:struct{BuildingId uint64 `json:"buildingId"`}{BuildingId:building}, Ident:"group_0_body"}, Call{Name:"state", Args: struct{}{}, Ident:"group_1_body"}}) } func upgradeBuilding(building uint64) []byte { return getFormattedData(Calls{ Call{ Name:"upgradeBuilding", Args:struct{BuildingId uint64 `json:"buildingId"`}{BuildingId:building}, Ident:"group_0_body"}, Call{Name:"state", Args: struct{}{}, Ident:"group_1_body"}}) } 


Data structures
 package bot type Unit struct { Id uint64 `json:"id"` Amount uint64 `json:"amount"` } type Building struct { Id uint64 `json:"id"` TypeId uint `json:"typeId"` Flip bool `json:"flip"` Level uint `json:"level"` X uint `json:"x"` Y uint `json:"y"` Completed bool `json:"completed"` Volume uint `json:"volume"` StateTimestamp uint64 `json:"stateTimestamp"` Hitpoints uint64 `json:"hitpoints"` CompleteTime uint64 `json:"completeTime"` } type Player struct { Units []Unit Buildings []Building Stars uint //interlan game currency Level uint Builders int //Builder house lvl GoldCapacity uint32 FoodCapacity uint32 Food uint32 Gold uint32 CastleLvl uint } type Result struct { Ident string `json:"ident"` Result map[string]interface{} `json:"result"` } type Error struct { Name string `json:"name"` Description string `json:"description"` Call `json:"call"` } type Response struct{ Date float64 `json:"date"` Results []Result `json:"results"` Error Error `json:"error"` } type BuildingDependency struct { CastleLvl uint Cost uint32 } type BuildingDependencies []BuildingDependency type Buildings struct { Wall BuildingDependencies } type Call struct { Ident string `json:"ident"` Args interface {} `json:"args"` Name string `json:"name"` } type Calls []Call type Request struct { Calls Calls `json:"calls"` Session interface{} `json:"session"` } 


Part of reverse engineering is over. Now part of the logic of the bot. I left the opportunity to play in the native client. To do this in the browser, go to http: // localhost: 8080 / original .
Module code
 package main import ( "net/http" "log" "./utils" ) type Pair struct { Key string Value string } func original(Uid string) { flashvars := []Pair{ Pair{"fb_source", "bookmark"}, Pair{"ref", "bookmarks"}, Pair{"count", "0"}, Pair{"fb_bmpos", "2_0"}, Pair{"code", "AQA9KrPoSxjyTjNoG9B1mUHoZ8ooUxusmPWV6Aa17SfgHIkSVubBwLxCKC5EO7fkfIiC9LvnrDOY35pzlPwyasKVe6q1dcOZzvyQeTmrlf-rhjjMH0FZGh0PWwMp0k3IqZTg1tKunkFFGMALgo4Vf8vSzGFG2r8DiJq5-N7K-5MIg7j3VnR0A0-EzaM5kATvrM5FmI1XWmxHbHFmHpS52rKuTvSuH27Ipwt4p2V2DGayvPDjnvvfs6d5-hdaCtoxoOvBJDfecDakToecSzr3kAU6zF4QiMCIC1MtihaH_3C7a9BeLdVqMhr4w4q33WqEso0"}, Pair{"sys_id", "4"}, Pair{"network", "facebook"}, Pair{"uid", Uid}, Pair{"app_id", "1424411677784893"}, Pair{"interface_lang", "uk"}, Pair{"access_token", "CAAUPfrARez0BAKDb5H1uds5kLg3794HyPAbTYRZAA1H2i43NPl8sSjpxl77gIqDapYZB4QxWrZAK1H6VQUVAFbWuTWr4VYbXagirvciMba7FhyYKSUboICrvSJKYgBndShSZA0n4ZA5JRZBqigVbMRdCsHrjl8AQEmcWfbJqkHflqmv8XEBarKEVJRfLp56ksLZCO7TBzkfVQZDZD"}, Pair{"auth_key", "6514a97ae525f196b8060337380e0cbb"}, Pair{"requestLoadingInfoTimeout", "3000"}, Pair{"ref", "bookmark"}, Pair{"rpc_url", "https%3A%2F%2Fepicwar-facebook.progrestar.net%2Frpc%2F"}, Pair{"preloader_asset", "preloader%2Fpreloader_dwarf.swf"}, Pair{"browser", "chrome"}, Pair{"country_code", "UA"}, Pair{"geoip_city", "Kiev"}, Pair{"index_version", "1398240421"}, Pair{"static_url", "https%3A%2F%2Fepicwar-a.akamaihd.net%2F"}, Pair{"preloader", "https%3A%2F%2Fepicwar-a.akamaihd.net%2Ffacebook%2Fv042%2FFbLoader.swf%3Fv%3D2"}, Pair{"rpc_url", "https%3A%2F%2Fepicwar-facebook.progrestar.net%2Frpc%2F"}, Pair{"stat_url", "https://stat.progrestar.net/collector/client/"}, Pair{"error_url", "https://error.progrestar.net/client/"}, } t := utils.Template("original") http.HandleFunc("/original", func (w http.ResponseWriter, r *http.Request) { err := t.Execute(w, struct{Flashvars []Pair}{Flashvars: flashvars}) if err != nil { log.Fatal("There was an error:", err) } }) } -rhjjMH0FZGh0PWwMp0k3IqZTg1tKunkFFGMALgo4Vf8vSzGFG2r8DiJq5-N7K-5MIg7j3VnR0A0-EzaM5kATvrM5FmI1XWmxHbHFmHpS52rKuTvSuH27Ipwt4p2V2DGayvPDjnvvfs6d5-hdaCtoxoOvBJDfecDakToecSzr3kAU6zF4QiMCIC1MtihaH_3C7a9BeLdVqMhr4w4q33WqEso0"}, package main import ( "net/http" "log" "./utils" ) type Pair struct { Key string Value string } func original(Uid string) { flashvars := []Pair{ Pair{"fb_source", "bookmark"}, Pair{"ref", "bookmarks"}, Pair{"count", "0"}, Pair{"fb_bmpos", "2_0"}, Pair{"code", "AQA9KrPoSxjyTjNoG9B1mUHoZ8ooUxusmPWV6Aa17SfgHIkSVubBwLxCKC5EO7fkfIiC9LvnrDOY35pzlPwyasKVe6q1dcOZzvyQeTmrlf-rhjjMH0FZGh0PWwMp0k3IqZTg1tKunkFFGMALgo4Vf8vSzGFG2r8DiJq5-N7K-5MIg7j3VnR0A0-EzaM5kATvrM5FmI1XWmxHbHFmHpS52rKuTvSuH27Ipwt4p2V2DGayvPDjnvvfs6d5-hdaCtoxoOvBJDfecDakToecSzr3kAU6zF4QiMCIC1MtihaH_3C7a9BeLdVqMhr4w4q33WqEso0"}, Pair{"sys_id", "4"}, Pair{"network", "facebook"}, Pair{"uid", Uid}, Pair{"app_id", "1424411677784893"}, Pair{"interface_lang", "uk"}, Pair{"access_token", "CAAUPfrARez0BAKDb5H1uds5kLg3794HyPAbTYRZAA1H2i43NPl8sSjpxl77gIqDapYZB4QxWrZAK1H6VQUVAFbWuTWr4VYbXagirvciMba7FhyYKSUboICrvSJKYgBndShSZA0n4ZA5JRZBqigVbMRdCsHrjl8AQEmcWfbJqkHflqmv8XEBarKEVJRfLp56ksLZCO7TBzkfVQZDZD"}, Pair{"auth_key", "6514a97ae525f196b8060337380e0cbb"}, Pair{"requestLoadingInfoTimeout", "3000"}, Pair{"ref", "bookmark"}, Pair{"rpc_url", "https%3A%2F%2Fepicwar-facebook.progrestar.net%2Frpc%2F"}, Pair{"preloader_asset", "preloader%2Fpreloader_dwarf.swf"}, Pair{"browser", "chrome"}, Pair{"country_code", "UA"}, Pair{"geoip_city", "Kiev"}, Pair{"index_version", "1398240421"}, Pair{"static_url", "https%3A%2F%2Fepicwar-a.akamaihd.net%2F"}, Pair{"preloader", "https%3A%2F%2Fepicwar-a.akamaihd.net%2Ffacebook%2Fv042%2FFbLoader.swf%3Fv%3D2"}, Pair{"rpc_url", "https%3A%2F%2Fepicwar-facebook.progrestar.net%2Frpc%2F"}, Pair{"stat_url", "https://stat.progrestar.net/collector/client/"}, Pair{"error_url", "https://error.progrestar.net/client/"}, } t := utils.Template("original") http.HandleFunc("/original", func (w http.ResponseWriter, r *http.Request) { err := t.Execute(w, struct{Flashvars []Pair}{Flashvars: flashvars}) if err != nil { log.Fatal("There was an error:", err) } }) } "}, package main import ( "net/http" "log" "./utils" ) type Pair struct { Key string Value string } func original(Uid string) { flashvars := []Pair{ Pair{"fb_source", "bookmark"}, Pair{"ref", "bookmarks"}, Pair{"count", "0"}, Pair{"fb_bmpos", "2_0"}, Pair{"code", "AQA9KrPoSxjyTjNoG9B1mUHoZ8ooUxusmPWV6Aa17SfgHIkSVubBwLxCKC5EO7fkfIiC9LvnrDOY35pzlPwyasKVe6q1dcOZzvyQeTmrlf-rhjjMH0FZGh0PWwMp0k3IqZTg1tKunkFFGMALgo4Vf8vSzGFG2r8DiJq5-N7K-5MIg7j3VnR0A0-EzaM5kATvrM5FmI1XWmxHbHFmHpS52rKuTvSuH27Ipwt4p2V2DGayvPDjnvvfs6d5-hdaCtoxoOvBJDfecDakToecSzr3kAU6zF4QiMCIC1MtihaH_3C7a9BeLdVqMhr4w4q33WqEso0"}, Pair{"sys_id", "4"}, Pair{"network", "facebook"}, Pair{"uid", Uid}, Pair{"app_id", "1424411677784893"}, Pair{"interface_lang", "uk"}, Pair{"access_token", "CAAUPfrARez0BAKDb5H1uds5kLg3794HyPAbTYRZAA1H2i43NPl8sSjpxl77gIqDapYZB4QxWrZAK1H6VQUVAFbWuTWr4VYbXagirvciMba7FhyYKSUboICrvSJKYgBndShSZA0n4ZA5JRZBqigVbMRdCsHrjl8AQEmcWfbJqkHflqmv8XEBarKEVJRfLp56ksLZCO7TBzkfVQZDZD"}, Pair{"auth_key", "6514a97ae525f196b8060337380e0cbb"}, Pair{"requestLoadingInfoTimeout", "3000"}, Pair{"ref", "bookmark"}, Pair{"rpc_url", "https%3A%2F%2Fepicwar-facebook.progrestar.net%2Frpc%2F"}, Pair{"preloader_asset", "preloader%2Fpreloader_dwarf.swf"}, Pair{"browser", "chrome"}, Pair{"country_code", "UA"}, Pair{"geoip_city", "Kiev"}, Pair{"index_version", "1398240421"}, Pair{"static_url", "https%3A%2F%2Fepicwar-a.akamaihd.net%2F"}, Pair{"preloader", "https%3A%2F%2Fepicwar-a.akamaihd.net%2Ffacebook%2Fv042%2FFbLoader.swf%3Fv%3D2"}, Pair{"rpc_url", "https%3A%2F%2Fepicwar-facebook.progrestar.net%2Frpc%2F"}, Pair{"stat_url", "https://stat.progrestar.net/collector/client/"}, Pair{"error_url", "https://error.progrestar.net/client/"}, } t := utils.Template("original") http.HandleFunc("/original", func (w http.ResponseWriter, r *http.Request) { err := t.Execute(w, struct{Flashvars []Pair}{Flashvars: flashvars}) if err != nil { log.Fatal("There was an error:", err) } }) } 


I left all flashvars, although I think there is no need for some.

The communication protocol is quite simple. Only POST requests and only one URL. There is no abundance of GET / POST requests and parameters, such as in Grepolis.
The bot, in turn, separately receives the game card, it can be viewed at http: // localhost: 8080 / bot . Here is just a map without beautiful and isometric. But you can clearly see where the building is, which reduces the likelihood of empty spaces in the middle of the city where the enemy can land. I myself often did that. And in the home client, an inconvenient isometric projection, where the angle cannot be changed, is not always convenient.

I'll start by collecting resources. It is very simple. A separate Goroutine is created, which collects resources, if possible, and falls asleep. In response, buildings and resources come, the so-called state.

Here is the source code for the resource collector.
 func processCollectRequest(player *Player, resp *Response){ for _, result := range resp.Results { if(result.Ident == "group_1_body"){ parseResources(player, result.Result["resource"].([]interface{})) parseBuildings(player, result.Result["building"].([]interface{})) } } } func collectFood(player *Player) *Response{ var resp *Response for _, building := range player.Buildings { if(building.TypeId == MILL_ID){ resp = decodeJson(network.Post(collectResource(building.Id))) } } return resp } func collectGold(player *Player) *Response{ var resp *Response for _, building := range player.Buildings { if(building.TypeId == MINE_ID){ resp = decodeJson(network.Post(collectResource(building.Id))) } } return resp } func resourcesCollector(playerChan chan Player) { var resp *Response var playerStruct Player var player *Player resp = nil playerStruct = <- playerChan player = &playerStruct if(player.FoodCapacity > player.Food){ log.Print("Collect Food") resp = collectFood(player) } if(player.GoldCapacity > player.Gold){ log.Print("Collect Gold") resp = collectGold(player) } if(resp != nil){ processCollectRequest(player, resp) resp = nil } playerChan <- playerStruct time.Sleep(time.Minute * 10) go resourcesCollector(playerChan) } 


In a separate constant file.
 package bot const CASTLE_ID = 1 const MINE_ID = 2 const TREASURY_ID = 3 const MILL_ID = 4 const BARN_ID = 5 const BARRACKS_ID = 6 const STAFF_ID = 7 const BUILDER_HUT_ID = 8 const FORGE_ID = 9 const BALLISTA_ID = 10 const WALL_ID = 11 const ARCHER_TOWER_ID = 12 const CANNON_ID = 13 const THUNDER_TOWER_ID = 14 const ICE_TOWER_ID = 15 const FIRE_TOWER_ID = 16 const CLAN_HOUSE_ID = 17 const DARK_TOWER_ID = 18 const TAVERN_ID = 19 const ALCHEMIST_ID = 20 const GOLD_RESOURCE_ID = 1 const FOOD_RESOURCE_ID = 2 var CAPACITIES = [12]uint32 {0, 5000, 15000, 35000, 75000, 150000, 300000, 600000, 1000000, 2000000, 3000000, 4000000} var BUILDINGS = Buildings{ Wall: BuildingDependencies{ BuildingDependency{CastleLvl: 2, Cost: 250}, BuildingDependency{CastleLvl: 2, Cost: 500}, BuildingDependency{CastleLvl: 3, Cost: 1000}, BuildingDependency{CastleLvl: 4, Cost: 3000}, BuildingDependency{CastleLvl: 5, Cost: 10000}, BuildingDependency{CastleLvl: 6, Cost: 25000}, BuildingDependency{CastleLvl: 7, Cost: 60000}, BuildingDependency{CastleLvl: 8, Cost: 150000}, BuildingDependency{CastleLvl: 9, Cost: 400000}, BuildingDependency{CastleLvl: 10, Cost: 1000000}, BuildingDependency{CastleLvl: 11, Cost: 2000000}}} 


And the parser
 package bot import "log" func parseResources (player *Player, resources []interface{}){ for _, resource := range resources { var Id uint var Amount uint32 Id = uint(resource.(map[string]interface{})["id"].(float64)) Amount = uint32(resource.(map[string]interface{})["amount"].(float64)) if(Id == GOLD_RESOURCE_ID){ player.Gold = Amount log.Print("Gold - ", Amount) } if(Id == FOOD_RESOURCE_ID){ player.Food = Amount log.Print("Food - ", Amount) } } } func parseUnits (player *Player, units []interface{}){ player.Units = []Unit{} for _, unit := range units { player.Units = append( player.Units, Unit{ Id: uint64(unit.(map[string]interface{})["id"].(float64)), Amount: uint64(unit.(map[string]interface{})["amount"].(float64))}) } } func parseBuildings(player *Player, buildings []interface{}){ player.Buildings = []Building{} for _, building := range buildings { var typeId uint var level uint var completed bool typeId = uint(building.(map[string]interface{})["typeId"].(float64)) level = uint(building.(map[string]interface{})["level"].(float64)) completed = building.(map[string]interface{})["completed"].(bool) if(typeId == BARN_ID) { player.FoodCapacity += CAPACITIES[level] } if(typeId == TREASURY_ID) { player.GoldCapacity += CAPACITIES[level] } if(typeId == CASTLE_ID){ player.CastleLvl = level if(completed == false){ player.CastleLvl-- } } player.Buildings = append( player.Buildings, Building{ Id: uint64(building.(map[string]interface{})["id"].(float64)), TypeId: typeId, Flip: building.(map[string]interface{})["flip"].(bool), Level: level, X: uint(building.(map[string]interface{})["x"].(float64)), Y: uint(building.(map[string]interface{})["y"].(float64)), Completed: completed, Volume: uint(building.(map[string]interface{})["volume"].(float64)), StateTimestamp:uint64(building.(map[string]interface{})["stateTimestamp"].(float64)), Hitpoints: uint64(building.(map[string]interface{})["hitpoints"].(float64)), CompleteTime: uint64(building.(map[string]interface{})["completeTime"].(float64))}) } } 


I made the self-improvement of buildings only for the walls, because there are a lot of them to click on each separate block is very inconvenient. The answer is the same state.
builder
 func builder(playerChan chan Player){ var playerStruct Player var player *Player var resp *Response var isBuild bool = false playerStruct = <- playerChan player = &playerStruct for _, building := range player.Buildings { if( building.TypeId == WALL_ID && len(BUILDINGS.Wall) > int(building.Level) && player.CastleLvl >= BUILDINGS.Wall[building.Level].CastleLvl && player.Gold >= BUILDINGS.Wall[building.Level].Cost){ log.Print("Upgrade Wall. Level ", building.Level) resp = decodeJson(network.Post(upgradeBuilding(building.Id))) if(resp != nil){ processCollectRequest(player, resp) isBuild = true } break } } playerChan <- playerStruct if(isBuild){ time.Sleep(time.Second) }else{ time.Sleep(time.Hour) } go builder(playerChan) } 


I’ll also make a reservation that you can simply transfer the structure to Goroutin by reference. But when I did that, the garbage collector cleared the memory after a while and I had a nullPointer exception thrown away. Therefore, I use channels.
Main
 func decodeJson(encoded_json []byte) *Response { var resp *Response err := json.Unmarshal(encoded_json, &resp) if(err != nil){ log.Fatal("decodeJson", err) } if(resp.Error != Error{}){ resp = nil } return resp } func initGame(playerChan chan Player) { var playerStruct Player var player *Player playerStruct = Player{} player = &playerStruct resp := decodeJson(network.Post(getStartBoard())) for _, result := range resp.Results { if(result.Ident == "getSelfInfo"){ user := result.Result["user"].(map[string]interface{}) level, _ := strconv.Atoi(user["level"].(string)) player.Level = uint(level) player.Stars = uint(user["starmoney"].(float64)) parseUnits(player, user["unit"].([]interface{})) parseResources(player, user["resource"].([]interface{})) } player.GoldCapacity = 0 player.FoodCapacity = 0 if(result.Ident == "getBuildings"){ parseBuildings(player, result.Result["building"].([]interface{})) } } playerChan <- playerStruct } func Main(){ var player = make(chan Player, 1) initGame(player) go resourcesCollector(player) go builder(player) http.HandleFunc("/bot", func (w http.ResponseWriter, r *http.Request) { t, err := template.ParseFiles("static/bot.html") if err != nil { log.Fatal("There was an error:", err) } err = t.Execute(w, nil) if err != nil { log.Fatal("There was an error:", err) } }) http.HandleFunc("/bot/map", func (w http.ResponseWriter, r *http.Request) { var playerStruct Player = <- player player <- playerStruct json, err := json.Marshal(playerStruct) if(err != nil){ log.Fatal(err) } io.WriteString(w, string(json)) }) } 


I will also introduce the client part of the bot. The most difficult thing was to optimally display the places where there are no buildings, but the landing cannot be landed there either. The rest is extremely simple.
Who cares about the bot client code
 <!doctype html> <html> <head> <title>Throne Rush Bot</title> <style> #main{ display:block; margin:0 auto; background-color:#00FF33; } body{ background-color:#66CC00; height: 100%; margin: 0; padding: 0; } html{ height: 100%; } </style> </head> <body> <canvas id="main"></canvas> <script> var STYLES = { 1: { name: "Castle", size: 5, fillColor: '#996600' }, 2: { name: "Mine", size: 2, fillColor: '#996600' }, 3: { name: "Treasury", size: 3, fillColor: '#996600' }, 4: { name: "Mill", size: 2, fillColor: '#996600' }, 5: { name: "Barn", size: 3, fillColor: '#996600' }, 6: { name: "Barracs", size: 3, fillColor: '#996600' }, 7: { name: "Staff", size: 2, fillColor: '#996600' }, 8: { name: "Bldrs", size: 2, fillColor: '#996600' }, 9: { name: "Forge", size: 2, fillColor: '#996600' }, 10: { name: "Ballista", size: 2, fillColor: '#996600' }, 11: { name: "", size: 1, fillColor: '#660000' }, 12: { name: "Archrs", size: 2, fillColor: '#996600' }, 13: { name: "Cannon", size: 2, fillColor: '#996600' }, 14: { name: "Thunder", size: 2, fillColor: '#996600' }, 15: { name: "Ice", size: 2, fillColor: '#996600' }, 16: { name: "Fire Tower", size: 2, fillColor: '#996600' }, 17: { name: "Clan house", size: 3, fillColor: '#996600' }, 18: { name: "Dark Tower", size: 2, fillColor: '#996600' }, 19: { name: "Tavern", size: 3, fillColor: '#996600' }, 20: { name: "Alchemist", size: 3, fillColor: '#996600' } }; var MAP_SIZE = 36; var NOT_DESANT_COLOR = "#FF0033"; var height = document.body.clientHeight; var canvas = document.getElementById('main'); var ctx = canvas.getContext('2d'); var step = height / 40; canvas.width = height; canvas.height = height; canvas.style.width = height + 'px'; canvas.style.height = height + 'px'; ctx.strokeStyle="#FF0033"; ctx.textAlign="center"; ctx.textBaseline="middle"; function pointFree(buildings, point){ for(var i in buildings){ var building = buildings[i]; if( point[0] >= building.x && point[0] < building.x + STYLES[building.typeId].size && point[1] >= building.y && point[1] < building.y + STYLES[building.typeId].size ){ return false; } } return true; } function drawMap(player){ var redPoints = []; player.Buildings .forEach(function(building){ for(var i = -1; i < STYLES[building.typeId].size + 1; i++){ var point = [building.x + i, building.y - 1].join(','); if(redPoints.indexOf(point) === -1){ redPoints.push(point); } } for(var i = -1; i < STYLES[building.typeId].size + 1; i++){ var point = [building.x + i, building.y + STYLES[building.typeId].size].join(','); if(redPoints.indexOf(point) === -1){ redPoints.push(point); } } for(var i = -1; i < STYLES[building.typeId].size + 1; i++){ var point = [building.x - 1, building.y + i].join(','); if(redPoints.indexOf(point) === -1){ redPoints.push(point); } } for(var i = -1; i < STYLES[building.typeId].size + 1; i++){ var point = [building.x + STYLES[building.typeId].size, building.y + i].join(','); if(redPoints.indexOf(point) === -1){ redPoints.push(point); } } ctx.fillStyle=STYLES[building.typeId].fillColor; ctx.fillRect( step * building.x, step * building.y, step * STYLES[building.typeId].size, step * STYLES[building.typeId].size); ctx.stroke(); ctx.strokeText( STYLES[building.typeId].name+'('+building.level+')', step * building.x + step * STYLES[building.typeId].size / 2, step * building.y + step * STYLES[building.typeId].size / 2); }); while(redPoints.length > 0){ var point = redPoints.pop().split(','); if(pointFree(player.Buildings, point)){ ctx.fillStyle=NOT_DESANT_COLOR; ctx.fillRect( step * point[0], step * point[1], step, step); ctx.stroke(); } } } var xmlhttp = new XMLHttpRequest(); xmlhttp.open('GET', '/bot/map', true); xmlhttp.onreadystatechange = function() { if (xmlhttp.readyState === 4) { drawMap(JSON.parse(xmlhttp.responseText)) } }; xmlhttp.send(null); </script> </body> </html> 


So he looks like

Source: https://habr.com/ru/post/223013/


All Articles