Pull to refresh

Реверс-инжинеринг и написание бота для flash игры на Go

Reading time 19 min
Views 9.6K
image В этой статье я расскажу, как декомпилировать flash-приложение с последующим извлечением алгоритма подписи запросов на игровой сервер. И как на основе этой информации написать игрового бота на Go.
Все началось с того, что я искал стратегию, чтобы поиграть со своего android смартфона. Нашел неплохую игру под названием «Throne rush». Потом оказалось, что есть и браузерный клиент, что делает удобнее некоторые действия. Но все же игровой процесс явно требовал автоматизации. Я воспользовался прекрасным инструментом JPEXS Free Flash Decompiler, о котором далее пойдет речь.

Исходный код проекта на github.

Итак, стоит задача эмулировать работу родного клиента.
Firebug’ом смотрю общение flash приложения
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}


Тут проблема в «X-Auth-Signature». Я не знал, откуда его брать. Очевидно, что это какой-то хеш чего-то. Но какой и чего, неизвестно. В этом мне помог «FFDec». Опенсорс программа, которая написана java, работает под линукс. Идеально подходит для этой работы.
В html коде видно, какой файл грузится.

<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">

Но на самом деле, это не то что нужно. Смотрим flashvars. Там «preloader=https%3A%2F%2Fepicwar-a.akamaihd.net%2Ffacebook%2Fv042%2FFbLoader.swf%3Fv%3D2». Но это опять не то. А нужный код, не помню уже как именно я это понял, на самом деле здесь https://epicwar-a.akamaihd.net/facebook/v042/EpicWar.swf.

Декомпилируем.
Окно программы


На самом деле там есть экспорт.
Вот декомпилированный код
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()));
   }
}


Все просто. Обычный md5. Ниже представлен этот код на 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
}


Как видно, я не шлю лишние заголовки. В action script коде класс RpcClientBase наследуется классом RpcClient из «com.progrestar.game.server.rpc» и дописывает еще заголовки. Например, «X-Env-Library-Version» я взял оттуда. Без него не работает.

Функции генерации запросов
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"}})
}


Структуры данных
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"`
}


Часть реверс-инжинеринга на этом закончена. Теперь часть логики бота. Я оставил возможность играть в родной клиент. Для этого в браузере нужно перейти по адресу http://localhost:8080/original.
Код модуля
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)
        }
    })
}


Оставил все flashvars, хотя думаю, в некоторых необходимости нет.

Протокол общения довольно прост. Только POST запросы и только на один URL. Нет обилия GET/POST запросов и параметров как, например, в Grepolis.
Бот, в свою очередь, отдельно получает игровую карту, ее можно посмотреть по адресу http://localhost:8080/bot. Здесь просто карта без красивостей и изометрии. Но зато хорошо видно где какая постройка, что уменьшает вероятность появление пустых мест посреди города, где неприятель может высадить десант. Я сам часто так делал. Да и в родном клиенте неудобная изометрическая проекция, где нельзя менять угол, не всегда удобно.

Начну со сбора ресурсов. Это очень просто. Создается отдельный гороутин, который собирает ресурсы, если это возможно, и засыпает. В ответе приходят здания и ресурсы, так называемый state.

Вот исходный код сборщика ресурсов
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)
}


В отдельном файле константы.
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}}}


И парсер
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))})
    }
}


Автоулучшение зданий я сделал только для стен, потому что их много и кликать на каждом отдельном блоке очень неудобно. В ответ приходит все тот же 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)
}


Оговорюсь еще, что просто так передать по ссылке структуру в гороутин можно. Но когда я так сделал, то сборщик мусора через время очищал память и у меня выбрасывался nullPointer exception. Поэтому я использую каналы.
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))
    })
}


Представлю еще клиентскую часть бота. Самым сложным было оптимальным образом отобразить места, где нет зданий, но десант там тоже нельзя высадить. Остальное предельно просто.
Кому интересно, код клиента бота
<!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>


Так он выглядит

Tags:
Hubs:
0
Comments 6
Comments Comments 6

Articles