
Все началось с того, что я искал стратегию, чтобы поиграть со своего 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>
Так он выглядит
