Pull to refresh

Гиковский подход к несложной браузерной игре

Reading time 18 min
Views 18K


Несколько лет назад с удовольствием играл в небольшую любительскую браузерную игру, созданную по мотивам классической «Majesty: The Fantasy Kingdom Sim». Очень скоро в ней обнаружилось несколько уязвимостей и багов, включая весьма пригодные для «эксплуатации». История того, как я писал скрипты-эксплоиты, репортил баги и позже немного участвовал в разработке игры – ниже. Думаю, она неплохо проиллюстрирует несколько нестандартный, гиковский подход пользователя к игре, о котором может быть полезно знать разработчикам.

Сначала о самой игре. Ссылку не привожу, но при желании ее можно найти внутри исходников ниже. Задумка игры довольно проста: игрок создает одного или нескольких героев на выбор из 16 классов (Воин, Варвар, Монах и прочие, в точности как в Majesty) и затем исследует окружающий мир, побеждая монстров, постепенно набирая уровни и подбирая хорошую экипировку. Интерфейс гипертекстовый, графики минимум. Вся графика взята из Majesty, причем, насколько я помню, с разрешения правообладателей. Взаимодействие между игроками отсутствует, за исключением совершенно независимой от основного игрового процесса соревновательной арены и одного весьма косвенного пути, о котором я расскажу ниже.

Главная же особенность игры состоит в том, что герой начинает с запасом в 200 «ходов», которые довольно быстро тратятся на исследование стартовой локаций и первые путешествия. Новые ходы накапливаются со скоростью 1 в час реального времени, независимо от действий игрока. В сутки набирается 24 хода, что очень мало, хватает на 5-10 минут игры. Игра бесплатна, в ней нет доната, купить за «реал» нельзя ни ходы, ни что-либо другое. Единственный способ играть больше – создать много героев, и каждым играть 5-10 минут в день или порядка часа в неделю. Однако создавать более 16 героев – одного на каждый класс – не представляет особого интереса в силу отсутствия внутриигрового взаимодействия. Именно так я и играл – по одному герою каждого типа. Такого количества хватает примерно на час спокойной игры в день.

Генерация героев


При создании героя открывается первая возможность для гиковской оптимизации: генерация героя с наилучшими характеристиками. Статов в игре пять: Strength, Vitality, Intelligence, Willpower, Artifice. После небольшого тестирования я предположил, что значение каждого выбирается случайно из диапазона вида 20–25, то есть по 6 возможных значений на характеристику. Всего возможных комбинаций 6^5 = 7776, из которых идеальна лишь одна. Очевидно, вручную пересоздавать героя замучаешься, нужно писать скрипт!

Однако сначала все же нужно точно определить диапазоны статов для каждого героя. Это можно сделать вручную (в среднем 5-6 генераций на каждый класс героя), но это долго и неинтересно. Для меня интереснее и быстрее всего было взять уже немного освоенный на других играх скриптовый язык Autohotkey, и написать код для управления браузером Opera. Капчи при создании героя тогда не было – ее добавили позже с моей подачи, – поэтому схема работы весьма проста:
— выбрать класс, принять отправную гипотезу о диапазонах параметров (мин 50, макс 1);
— залогиниться, отправить форму создания нового героя;
— посмотреть, какие статы получились, поправить гипотезу (уменьшить мин, увеличить макс);
— если все пять диапазонов стали по 6 единиц длиной, то записать диапазоны в файл и перейти к следующему классу.

Старый грязный код для любопытствующих
Login = <..test login..>
Password = <..pwd..>
Name = testing hero
nClass = 1 ;1=adept 2=barbarian 3=cultist 4=dwarf 5=elf 6=gnome 7=healer 8=monk
;9=paladin 10=priest 11=ranger 12=rogue 13=solarus 14=warrior 15=WoD 16=Wizard
nGender = 1 ;1=male 2=female
Email = omican@yandex.ru
MinStr = 50
MinVit = 50
MinInt = 50
MinWil = 50
MinArt = 50
MaxStr = 1
MaxVit = 1
MaxInt = 1
MaxWil = 1
MaxArt = 1

Delete = heroesofardania.net/options/DeleteHero.asp?_sn=831067546&State=1
Register = heroesofardania.net/register.asp

run e:\program files\opera\opera.exe
WinWait, Blank page — Opera,
IfWinNotActive, Blank page — Opera,, WinActivate, Blank page — Opera,
WinWaitActive, Blank page — Opera,

send ^t
sendinput heroesofardania.net{enter}
send 2
; ожидание полной загрузки страницы — изменения цвета пикселя в Опере
loop, 300
{
sleep, 100
PixelGetColor, temp, 287, 62
if(temp=0xFF0000)
break
if(a_index=299)
{
clipboard=-1
exitapp
}
}
sleep, 100
send {tab}
send 2
sleep, 100

; залогинивание
sendinput {tab}%Login%{tab}%Password%{tab}{enter}
send 2
loop, 300
{
sleep, 100
PixelGetColor, temp, 287, 62
if(temp=0xFF0000)
break
if(a_index=299)
{
clipboard=-1
exitapp
}
}
sleep, 100
send 2
sleep, 100

; удаление старого героя перед началом цикла
sendinput h%Delete%{enter}
send 2
loop, 300
{
sleep, 100
PixelGetColor, temp, 287, 62
if(temp=0xFF0000)
break
if(a_index=299)
{
clipboard=-1
exitapp
}
}
sleep, 100
send ^+!w

;---------------- основной цикл
loop
{
;----------------
send ^t
sendinput %register%{enter}
send 2

loop, 300
{
sleep, 100
PixelGetColor, temp, 287, 62
if(temp=0xFF0000)
break
if(a_index=299)
{
clipboard=-1
exitapp
}
}
sleep, 100
send 2
sleep, 100

; создание героя
sleep, 200
send {tab}
sendinput %Login%{tab}%Password%{tab}%Password%{tab}
loop, %nClass%
send {down}
send {tab}
loop, %nGender%
send {down}
sendinput {tab}%Name%{tab}%Email%{tab}{tab}{enter}
sleep, 100
send 2
sleep, 100

sleep, 200
loop, 300
{
sleep, 100
PixelGetColor, temp, 287, 62
if(temp=0xFF0000)
break
if(a_index=299)
{
clipboard=-1
exitapp
}
}
send 2

;--------------------------------------------------------------------герой создан
sleep, 100
; определение выпавшей статы и поправка гипотезы
if(MaxStr-MinStr!=5)
{
mouseclickdrag, l, 113, 415, 130, 420; выделение статы мышкой
send ^c
if(clipboard<MinStr)
MinStr=%clipboard%
if(clipboard>MaxStr)
MaxStr=%clipboard%
sleep, 100
}
if(MaxVit-MinVit!=5)
{
mouseclickdrag, l, 113, 430, 130, 435
send ^c
if(clipboard<MinVit)
MinVit=%clipboard%
if(clipboard>MaxVit)
MaxVit=%clipboard%
sleep, 100
}
if(MaxInt-MinInt!=5)
{
mouseclickdrag, l, 113, 445, 130, 450
send ^c
if(clipboard<MinInt)
MinInt=%clipboard%
if(clipboard>MaxInt)
MaxInt=%clipboard%
sleep, 100
}
if(MaxWil-MinWil!=5)
{
mouseclickdrag, l, 113, 460, 130, 465
send ^c
if(clipboard<MinWil)
MinWil=%clipboard%
if(clipboard>MaxWil)
MaxWil=%clipboard%
sleep, 100
}
if(MaxArt-MinArt!=5)
{
mouseclickdrag, l, 113, 475, 130, 480
send ^c
if(clipboard<MinArt)
MinArt=%clipboard%
if(clipboard>MaxArt)
MaxArt=%clipboard%
sleep, 100
}

; запись в файл и переход к следующему классу
if(MaxStr-MinStr=5&&MaxVit-MinVit=5&&MaxInt-MinInt=5&&MaxWil-MinWil=5&&MaxArt-MinArt=5)
{
FileAppend, Class: %nClass%`n, E:\games\HoA Scripts\Stats.txt
Fileappend, Str: %MinStr%-%MaxStr%`n, E:\games\HoA Scripts\Stats.txt
Fileappend, Vit: %MinVit%-%MaxVit%`n, E:\games\HoA Scripts\Stats.txt
Fileappend, Int: %MinInt%-%MaxInt%`n, E:\games\HoA Scripts\Stats.txt
Fileappend, Wil: %MinWil%-%MaxWil%`n, E:\games\HoA Scripts\Stats.txt
Fileappend, Art: %MinArt%-%MaxArt%`n`n, E:\games\HoA Scripts\Stats.txt
nClass++
MinStr = 50
MinVit = 50
MinInt = 50
MinWil = 50
MinArt = 50
MaxStr = 1
MaxVit = 1
MaxInt = 1
MaxWil = 1
MaxArt = 1
}

sendinput h%delete%{enter}
send 2
loop, 300
{
sleep, 100
PixelGetColor, temp, 287, 62
if(temp=0xFF0000)
break
if(a_index=299)
{
clipboard=-1
exitapp
}
}
send ^+!w

; готово!
if(nClass>16)
{
MsgBox Done!
exitapp
}

;-----------------
}
;-----------------

В результате получаем искомые диапазоны для всех классов и (нестрогое) подтверждение гипотезы – все характеристики имеют ровно 6 возможных значений. Далее, для каждого класса важны лишь некоторые из характеристик, причем не всегда нужен самый верх диапазона, а достаточно промежуточного порогового значения. Например, параметр Strength дает прибавку +1 к урону за каждые 6 единиц, поэтому из диапазона 21-26 нам подойдут 24, 25 и 26. Это позволяет поднять вероятность получения подходящей комбинации с [1 из 7776] до разумного значения в [1 из нескольких десятков или сотен]. Это важно, потому что пересоздание героя занимает 10-15 секунд, и идеальные статы генерились бы в среднем более суток, а нам помимо них нужно еще кое-что.

А именно стартовые деньги – вторая возможность для гиковской оптимизации. Их мало, всего 200 монет, чего недостаточно для покупки даже слабенькой экипировки. Хватает лишь на самую простую дубинку и слабую броню, которая не спасает от укусов самых слабых монстров в первой же боевой локации. Поэтому обычно свежесозданным героям приходится тратить много ходов на лечение от ран или даже воскрешение после смерти (в игре смерть это потеря части денег и нескольких бесценных ходов). Однако в стартовом городе есть казино! Размеры ставок 50, 100, 500 и 1000 монет, типы ставок такие:
— «на черное»: шанс 1/2 удвоить ставку;
— «на красное»: шанс 1/5 упятерить ставку;
— «на золотое»: шанс 1/20 «удвадцатерить» ставку.

Очевидно, что в отличие от реальных казино, матожидание выигрыша равно нулю. Однако каждая ставка тратит один ход, что делает игру в казино в конечном счете невыгодной для всех кроме героев класса Gnome (у них повышенная удача и матожидание положительно). Для генерации же героев казино очень полезно: можно поставить стартовые деньги на золотое, и в случае неудачи просто создать героя заново. Ставки в 200 монет нет, поэтому приходится делать две ставки по 100 и рассчитывать на выигрыш 2000 монет (суммарная вероятность примерно 1/10). Столько монет все равно маловато для хорошего старта, поэтому нужно ставить опять: две ставки по 1000 с надеждой получить 20,000 монет (вероятность снова ~1/10).



Соответствующий скрипт устроен просто:
— пересоздавать героя до тех пор, пока статы не окажутся подходящими;
— сходить в казино, сделать 2 ставки по 100, если выиграл, то еще 2 по 1000;
— если выиграл, то перейти к следующему классу героя;
— в случае неуспеха на любом этапе вернуться к пересозданию.

Код реролла героев

#SingleInstance force

; подходящие статы классов
hero_class2=1
hero_name2=Mano Rigor
hero_gender2=1
min_str2=22
min_vit2=21
min_int2=24
min_wil2=22
min_art2=0

hero_class3=2
hero_name3=Tiger Grin
hero_gender3=1
min_str3=31
min_vit3=26
min_int3=11
min_wil3=0 ;9-14
min_art3=15

hero_class4=3
hero_name4=Tigra
hero_gender4=2
min_str4=17
min_vit4=14
min_int4=27
min_wil4=0 ;5-10
min_art4=21

hero_class5=4
hero_name5=Dorn Bumpkin
hero_gender5=1
min_str5=25
min_vit5=28
min_int5=15
min_wil5=0 ;17-22
min_art5=29

hero_class6=6
hero_name6=Mister Pronka
hero_gender6=1
min_str6=7
min_vit6=9
min_int6=18
min_wil6=24
min_art6=0

hero_class7=7
hero_name7=Sister of Silence
hero_gender7=2
min_str7=7
min_vit7=13
min_int7=28
min_wil7=31
min_art7=0

hero_class8=8
hero_name8=Sphinx
hero_gender8=2
min_str8=23
min_vit8=25
min_int8=26
min_wil8=33
min_art8=0

hero_class9=9
hero_name9=Tessa Virtue
hero_gender9=2
min_str9=27
min_vit9=26
min_int9=20
min_wil9=29
min_art9=0

hero_class10=10
hero_name10=Fess
hero_gender10=1
min_str10=8
min_vit10=12
min_int10=30
min_wil10=24
min_art10=0

hero_class11=11
hero_name11=Nausika
hero_gender11=2
min_str11=18
min_vit11=19
min_int11=22
min_wil11=0 ;19-24
min_art11=24

hero_class12=12
hero_name12=Aleyak Sumakai
hero_gender12=1
min_str12=15
min_vit12=17
min_int12=21
min_wil12=0 ;4-9
min_art12=30

hero_class13=13
hero_name13=Morpheus
hero_gender13=1
min_str13=25
min_vit13=24
min_int13=22
min_wil13=21
min_art13=0

hero_class14=14
hero_name14=Saudade Jim
hero_gender14=1
min_str14=24
min_vit14=24
min_int14=14
min_wil14=20
min_art14=0

hero_class15=15
hero_name15=Tor Dur Bar
hero_gender15=1
min_str15=41
min_vit15=41
min_int15=8
min_wil15=18
min_art15=0

hero_class16=16
hero_name16=Wahooka
hero_gender16=1
min_str16=7
min_vit16=10
min_int16=31
min_wil16=23
min_art16=0

log_file=e:\games\scripts\log.txt

gold1=0
str1=0
vit1=0
int1=0
wil1=0
art1=0

; основная функция
reroll(i)
{
global
loop
{
send +{enter}
wait_image(«crier», 190, 150, 370, 370); ожидание загрузки страницы по картинке
get_data()
if(str1<min_str%i%||vit1<min_vit%i%||int1<min_int%i%||wil1<min_wil%i%||art1<min_art%i%)
{
;log(«Stats: ». str1. "(". min_str. "), ". vit1. "(". min_vit. "), ". int1. "(". min_int. "), ". wil1. "(". min_wil. "), ". art1. "(". min_art. ")")
delete_hero()
continue
}
break
}
log(hero_name%i%. " is ready!")
send ^{F4}
; ожидание загрузки страницы
PixelGetColor, color, 33, 147
while color!=0xFFFFFF
{
sleep, 30
PixelGetColor, color, 33, 147
}
sleep, 50
send {tab}
sleep, 20
return
}

; функция логирования строки
log(str)
{
global
formattime, time,, H:mm:ss
fileappend, %time% %str%`n, %log_file%
}

; функция ставки в казино
gamble(bet)
{
send {end}
sleep, 20
send {F8}
sleep, 50
clipboard=http://www.heroesofardania.net/Content.asp?State=1&Bet=%bet%
send^v
sleep, 30
send {enter}
wait_image(«hall», 190, 150, 370, 370)
loop, 2
{
send {end}
sleep, 20
send {F8}
sleep, 50
clipboard=http://www.heroesofardania.net/Content.asp?State=2&Colour=Gold&Process=Y
send^v
send {enter}
wait_image(«hall», 190, 150, 370, 370)
}
return
}

; функция удаления текущего героя
delete_hero()
{
send {F8}
sleep, 50
clipboard=http://www.heroesofardania.net/options/DeleteHero.asp?_sn=490&State=1
send^v
sleep, 30
send {enter}
wait_image(«paladin», 50, 235, 200, 400)
send ^{F4}
PixelGetColor, color, 33, 147
while color!=0xFFFFFF
{
sleep, 30
PixelGetColor, color, 33, 147
}
sleep, 50
send {tab}
sleep, 20
return
}

; получение состояния героя
get_data()
{
global
send ^a
sleep, 30
send ^c
sleep, 30
StringReplace, clipboard, clipboard, `r`n,, All
RegExMatch(clipboard, «i)Gold[^0-9]{0,10}([0-9]+)», gold)
RegExMatch(clipboard, «i)Strength[^0-9]{0,10}([0-9]+)», str)
RegExMatch(clipboard, «i)Vitality[^0-9]{0,10}([0-9]+)», vit)
RegExMatch(clipboard, «i)Intelligence[^0-9]{0,10}([0-9]+)», int)
RegExMatch(clipboard, «i)Willpower[^0-9]{0,10}([0-9]+)», wil)
RegExMatch(clipboard, «i)Artifice[^0-9]{0,10}([0-9]+)», art)
return
}

; ожидание появления нужной картинки в нужном месте
wait_image(name, x1=1, y1=1, x2=1024, y2=768)
{
ImageSearch, x, y, %x1%, %y1%, %x2%, %y2%, *80 E:\Games\Scripts\%name%.bmp
while ErrorLevel
{
sleep, 150
ImageSearch, x, y, %x1%, %y1%, %x2%, %y2%, *80 E:\Games\Scripts\%name%.bmp
}
sleep, 50
}

; клавиша запуска скрипта
~^!r::
{
IfWinNotActive, Majesty
return
sleep, 1000
loop, 16
{
if A_index<2 ;starting number
continue
; первичное заполнение формы создания героя — нужно лишь один раз на класс
send ^a
sleep, 50
send {del}
sleep, 20
send % hero_name%A_index%
send {tab}
sleep, 20
send {home}
loop, % hero_class%A_index%
send, {down}
send {tab}
sleep, 20
send {home}
loop, % hero_gender%A_index%
send, {down}
loop, 3
send {tab}
reroll(A_index)
sleep, 1000
}
return
}
^!q::exitapp

В сумме герою нужно подобрать характеристики (вероятность ~1/200 – 1/50) и выиграть два раза подряд в казино (вероятность ~1/100). Общая вероятность весьма невелика (~1/10,000), поэтому в среднем скрипт у меня работал по 20-30 часов на каждого героя. Делать несколько потоков я счел излишним, да и не хотелось загружать сервер игры и портить жизнь любителям-разработчикам. С этими приятными людьми, к тому же, я успел познакомиться, зарепортив до этого несколько багов. Самым фееричным багам и тому, как я стал Королевским Ловцом Багов, посвящена последняя часть статьи.

Итак, в итоге я получил 16 героев, каждый с хорошими статами и 20к золота. С этими деньгами за стартовые 200 ходов уже можно закупить неплохую стартовую экипировку, сбегать с другой город за супер-полезными амулетами и с удобством набрать десяток уровней. Однако на этом преимущество «улучшенного старта» заканчивается, и дальше герой играется как любой другой. Уровню к 20-25-му разница нивелируется, и главным становится уже знание игры – в какую локацию идти на текущем уровне и с имеющимся снаряжением, чтобы эффективно тратить ходы на развитие героя. Как помочь своим героям на данном этапе? Оказывается, и тут есть возможность немного облегчить им жизнь.

«Отмывание денег» через систему кланов


Здесь всплывает тот самый единственный косвенный способ взаимодействия героев. Каждый герой с 5 уровня может вступить в созданный другим героем клан, а с 10 – и создать свой. В общую казну клана можно жертвовать деньги и на эти деньги потом возводить постройки типа тренажерного зала, сада для медитаций и т.д. Эти постройки позволяют несколько более эффективно тратить ходы – быстрее лечиться, получать временные бонусы, даже немного улучшить статы. Все эти бонусы невелики за одним исключением – заклинаний из клановых храмов. После постройки храмов, посвященных местным божествам, в них можно жертвовать деньги на временные полезные заклинания, распространяющиеся на всех героев клана. Бонусы от некоторых из заклинаний уже очень ощутимы – полное лечение после каждого боя (экономия ходов и денег), усиление брони, увеличение количества атак. Стоит это огромных денег, поэтому при нормальной игре даже очень крупные кланы высокоразвитых героев не могут себе позволить часто активировать эти заклинания. Кстати, в клане не может быть более 15 или 20 героев, и цена заклинаний в храме пропорциональна числу членов клана.

Обойти эту проблему можно с помощью временных членов клана, каждый из которых вступает в клан, жертвует крупную сумму денег в казну, и покидает клан – так стоимость храмовых заклинаний не увеличивается, а казна растет. Вручную это делать не эффективно – для постоянного поддержания всех полезных бонусов на каждого героя-бенефициара нужно порядка дюжины героев-доноров, причем за каждого донора еще нужно не забывать играть, чтобы фармить деньги. По моим подсчетам, для непрерывного обеспечения всех 16 героев бонусами нужно было запустить в работу 200 гномов-доноров. Именно гномов, а не героев другого класса, потому что из-за повышенной удачи они намного легче выигрывают стартовые 20к (иногда даже 40к) золота, а после очень быстро могут получить нужный уровень, скинуть деньги в клан и начать продуктивно фармить.

Тут нам снова поможет скрипт, чуть более сложный:
— сгененировать 200 гномов с уникальными именами и достаточными статами и 20-40к золота из казино;
— каждым из них сбегать с другой город, закупить хорошую экипировку и амулеты;
— почти все оставшиеся ходы потратить на прокачку и фарминг золота в подходящей локации;
— оставшиеся несколько ходов потратить на путешествие в клановый город и пожертвование всех денег в нужный клан.

Код был создан только для первого пункта:

launcher
; инкремент счетчика в файле
increment(filename)
{
fileread, count, %filename%
count+=1
filedelete, %filename%
fileappend, %count%, %filename%
return %count%
}

; создание логина
gen_login(n)
{
ret=omican_cash%n%
return ret
}

; простейшее создание уникального имени вида Lucky PQ
gen_name(n)
{
let1:=floor(n/26)+65
let1:=chr(let1)
let2:=mod(n,26)+65
let2:=chr(let2)
ret=Lucky %let1%%let2%
return ret
}

;main loop
msgbox CLick OK to launch main script
loop
{
fileread, count, e:\games\scripts\login_index.txt
if(count>200)
{
break
}
login:=gen_login(count)
name:=gen_name(count)
filedelete, e:\games\scripts\data.txt
fileappend, %login%`n, e:\games\scripts\data.txt
fileappend, %name%, e:\games\scripts\data.txt
filedelete, e:\games\scripts\output.txt
filedelete, e:\games\scripts\index.txt
fileappend, 0001, e:\games\scripts\index.txt
run e:\games\Scripts\HoA main.ahk
SetTitleMatchMode, 2
Loop
{
; если все зависло
if(clipboard=-1)
{
sleep, 1000
winkill, Opera
clipboard=0
sleep, 3000
run e:\games\Scripts\HoA main.ahk
}
sleep, 5000
; если текущий гном готов
if(clipboard=1)
{
sleep, 1000
winkill, Opera
clipboard=0
increment(«e:\games\scripts\login_index.txt»)
sleep, 3000
break
}
}
}

main cycle
filereadline, login, e:\games\scripts\data.txt, 1
filereadline, Name, e:\games\scripts\data.txt, 2

nClass = 6 ;1=adept 2=barb 3=cult 4=dwarf 5=elf 6=gnome 7=heal 8=monk 9=pal
;10=priest 11=ranger 12=rogue 13=sol 14=warrior 15=WoD 16=Wizard
nGender = 1 ;1=male 2=female
Str = 0
Vit = 0
Int = 0
Wil = 0
Attempts = 0

Password = <..pwd..>
index_file=e:\games\scripts\index.txt
Email = <..email..>
Delete = www.heroesofardania.net/options/DeleteHero.asp?_sn=490&State=1
Register = heroesofardania.net/register.asp
Hall = heroesofardania.net/Content.asp?_sn=3808&_dt=1%2F29%2F2009+11%3A45%3A44+AM&ContentID=251
G100 = heroesofardania.net/Content.asp?_sn=1649&_dt=1%2F29%2F2009+11%3A54%3A42+AM&State=1&Bet=100
G1000 = heroesofardania.net/Content.asp?_sn=1649&_dt=1%2F29%2F2009+11%3A54%3A42+AM&State=1&Bet=1000
Gold = heroesofardania.net/Content.asp?_sn=9942&_dt=1%2F29%2F2009+11%3A56%3A44+AM&State=2&Colour=Gold
logout = www.heroesofardania.net/logout.asp?_sn=163&_dt=2%2F10%2F2009+2%3A09%3A03+PM&

; ожидание полной загрузки страницы (определяем по цвету пикселя в опере)
wait_load()
{
loop, 300
{
sleep, 100
PixelGetColor, temp, 290, 62
if(temp=0xFF0000)
break
if(a_index=299)
{
clipboard=-1
exitapp
}
}
}

; переход по адресу в новой вкладке
address_new(adr)
{
send ^t
sleep, 200
clipboard = %adr%
sendinput ^v{enter}
send 2
}

; переход по адресу во вкладке номер 2
address_exist(adr)
{
send 2
sleep, 200
sendinput h
sleep, 200
clipboard = %adr%
sendinput ^v{enter}
send 2
}

; переход по адресу в текущей вкладке
address_here(adr)
{
send h
sleep, 200
clipboard = %adr%
sendinput ^v{enter}
send 2
}

; инкремент счетчика гномов в файле
increment(filename)
{
fileread, count, %filename%
count+=1
if(count<1000)
count=0%count%
if(count<100)
count=0%count%
if(count<10)
count=0%count%
filedelete, %filename%
fileappend, %count%, %filename%
return %count%
}

flag = 0
run e:\program files\opera\opera.exe
WinWait, Blank page — Opera,
IfWinNotActive, Blank page — Opera,, WinActivate, Blank page — Opera,
WinWaitActive, Blank page — Opera,

count=0001
send {tab}
;---------------- основной цикл
loop
{
;----------------
send ^{F4}
sleep, 100
send ^{F4}
address_new(«heroesofardania.net»)
wait_load()
send 2
sleep, 200
sendinput {tab}%Login%{tab}%Password%{tab}{enter}
sleep, 200
send 2
wait_load()
address_exist(Delete)
wait_load()
address_exist(Register)
wait_load()
send 2
sleep, 200

; проверка на редкий баг «пустое окно создания героя»
loop
{
PixelGetColor, temp, 234, 715
if(temp=0x800080)
{
send 2
sleep, 200
address_new(Register)
wait_load()
sleep, 200
send 2
sleep, 200
}
else
break
}

; заполнение формы создания героя
sleep, 200
send {tab}
sendinput %Login%{tab}%Password%{tab}%Password%{tab}
loop, %nClass%
send {down}
send {tab}
loop, %nGender%
send {down}
sendinput {tab}%Name%{tab}%Email%{tab}{tab}{enter}
sleep, 200
send 2
sleep, 200
wait_load()
send 2

;-------------------------------------------------------------------- герой создан
sleep, 200
; проверка статов выделением мышкой
if(Str)
{
mouseclickdrag, l, 79, 407, 97, 407
send ^c
if(clipboard<Str && clipboard>0)
flag=1
sleep, 200
}
if(Vit)
{
mouseclickdrag, l, 79, 424, 97, 424
send ^c
if(clipboard<Vit && clipboard>0)
flag=1
sleep, 200
}
if(Int)
{
mouseclickdrag, l, 79, 441, 97, 441
send ^c
if(clipboard<Int && clipboard>0)
flag=1
sleep, 200
}
if(Wil)
{
mouseclickdrag, l, 79, 458, 97, 458
send ^c
if(clipboard<Wil && clipboard>0)
flag=1
sleep, 200
}

; если плохие статы
if(flag)
{
count:=increment(index_file)
sleep, 200
}
;------------------------------------------------------------- конец случая плохих статов
if(!flag)
{
; выигрывание 2000 в казино
address_here(Hall)
wait_load()
address_exist(G100)
wait_load()
address_exist(Gold)
wait_load()
address_exist(Gold)
wait_load()

send 2
sleep, 200
mouseclickdrag, l, 80, 289, 120, 289; выделение денег мышкой
send ^c
if(clipboard<1900 && clipboard>-1)
{
FileAppend, %count%: %A_Hour%:%A_Min%:%A_sec% Lose 1`n, E:\games\Scripts\output.txt
count:=increment(index_file)
sleep, 200
}
if(clipboard>1900 && clipboard<1000000)
{
; выигрывание 20,000
address_here(G1000)
wait_load()
address_exist(Gold)
wait_load()
address_exist(Gold)
wait_load()

send 2
sleep, 200
mouseclickdrag, l, 80, 289, 120, 289
send ^c
if(clipboard>19000 && clipboard<1000000)
{
if(Attempts<1)
{
FileAppend, Done! %clipboard%`n, E:\games\Scripts\output.txt
address_here(logout)
clipboard=1
exitapp
}
if(Attempts>0)
{
; выигрывание еще 20,000
address_here(G1000)
loop, %Attempts%
{
if(a_index=1)
{
wait_load()
address_exist(Gold)
wait_load()
}
else
{
address_here(Gold)
wait_load()
}
send 2
sleep, 200
mouseclickdrag, l, 80, 289, 120, 289
send ^c
if(clipboard>22000)
break
}
if(clipboard>22000 && clipboard<1000000)
{
FileAppend, Done! %clipboard%`n, E:\games\Scripts\output.txt
MsgBox Done!
exitapp
}
FileAppend, %count%: %A_Hour%:%A_Min%:%A_sec% Lose 3`n, E:\games\Scripts\output.txt
count:=increment(index_file)
}
}
if(clipboard<19000 && clipboard>-1)
{
FileAppend, %count%: %A_Hour%:%A_Min%:%A_sec% Lose 2`n, E:\games\Scripts\output.txt
count:=increment(index_file)
sleep, 200
}

}
}
flag=0
;-----------------
}
;-----------------


Остальные пункты так и не были реализованы, потому что одновременно произошло несколько событий:
— играть и писать эксплоиты мне уже несколько надоело;
— от разработчиков поступило предложение помочь с разработкой игры;
— толпы подозрительных гномов были замечены игроками. Дело в том, что писать генератор реалистичных уникальных имен для гномов мне было лень, и гномы имели имена типа Lucky XY, Lucky PQ и т.д. Оказалось, что при последовательной игре несколькими героями за последние 10 минут они все отображались в списке онлайн-игроков, и моих слишком-похожих гномов там стабильно висело 5-6 штук. На форуме появился топик, в котором озадаченные игроки гадали, что это за гномы и кому они нужны. Тут уже я понял, что пора раскрыться, рассказал про эксплоиты и свернул свою темную деятельность.

Немного о моральной стороне вопроса. В силу отсутствия любой возможности испортить жизнь другим игрокам, а также слабой соревновательной составляющей игры на тот момент, получение мною нечестных преимуществ совершенно не сказывалось на игровом социуме. Да и вообще проходило незамеченным. Мои герои были молоды, из-за системы «1 ход в час» не могли тягаться со старожилами и совершенно терялись в нижней части всех рейтингов. В процессе же моего копания в игре я нашел и зарепортил массу багов и уязвимостей, включая и те, которые поначалу эксплоитил.

Bug Catching


Первый из обнаруженных мною крупных багов был связан с вещами, которые давали +Vitality. Согласно данным в игровой википедии, увеличение HPmax при повышении уровня подчиняется формуле HPmax += floor(Vitality/4) + rand(1, floor(Vitality/4)). Таким образом, каждые 4 очка Vitality давали в среднем +1.5HP на уровень. Если на герое в момент повышения уровня были одеты вещи с +Vitality, то и добавочные HP должны были быть выше. Однако из-за бага эти вещи не учитывались (как мне рассказал разработчик, зачем-то перед повышением уровня все вещи снимались с героя, а после надевались заново).

После моего репорта баг пофиксили, что привело к совершенно неожиданным результатам. Герои с очень низким показателем Vitality, ранее еле-еле выживавшие в боях, теперь стали получать намного больше HP на уровень. В частности, гномы, у которых базовая Vitality лежит в диапазоне 4-9, за счет экипировки легко удвоили и даже утроили свое здоровье и, с учетом других своих способностей, превратились из самых задохликов в потенциально сильнейших героев. То же коснулось и в оригинале чрезвычайно хрупких Healer’ов. Крайне важным для таких героев стало на как можно низком уровне найти соответствующие вещи, чтобы пораньше начать получать бонусы к HP.

Небольшая недоработка была в казино: в него можно было проходить с вещами, повышающими удачу и чаще выигрывать. После моего репорта в казино добавили вышибал, которые вынюхивали такие вещи и просто не пускали внутрь. В казино же в другом городе путем несложных махинаций с GET-параметрами запроса можно было всегда выигрывать в одной из игр. Теперь там тоже вышибалы: «Cheaters are not welcomed here!»

Игра в целом делалась на коленке, и поэтому была чрезвычайно дырявой. В частности, для выхода из первого города нужно было купить карту за 600 или 800 монет, и только тогда в диалоге у ворот появлялась опция «покинуть город». Однако все опции в диалогах имели ссылки вида “…&State=n”, и подбор нужного значения GET-параметра позволял воспользоваться скрытой опцией диалога.

Самая же феерическая уязвимость связана с числовыми полями ввода типа «сколько ходов отдохнуть» или «сколько ходов молиться божеству». В большинстве случаев проверка неотрицательности ввода делалась Javascript’ом на стороне клиента. В результате однажды мой герой для эксперимента отдохнул -1000 ходов и… умер! «You have rested for -1000 turns and restored -6000 health points. You are killed!». Но смерть это лишь потеря денег и нескольких ходов. Которых у меня теперь почти на 1000 больше… При молитве же -1000 ходов божеству, вместо кучи благословлений герой получает не меньшую кучу проклятий.

Впечатленные моими репортами, разработчики выдали мне для более эффективного поиска багов специального героя с непробиваемой броней и кучей ходов:



Позднее разработчики дали мне доступ к тестовой ветке, где я с грехом пополам (оказалось, что вся игра написана на совершенно неизвестном мне VBScript) написал новую фичу – быстрое переключение наборов экипировки. Фича прошла «в продакшн», но на этом мой вклад закончился, ибо я все же не смог преодолеть свою неприязнь к VBScript’у и продолжить разработку.

В заключение можно сказать, что игрок-гик имеет нестандартный взгляд на игру. Ему интереснее (по крайней мере, в моем случае) изучать игру, искать несбалансированно сильные стратегии, уязвимости и эксплоиты, чем просто играть «как положено». Если результаты своих исследований он не поленится донести до разработчиков, то игра от этого только выиграет. Конечно, нового в этом рассуждении мало – классификацию игроков, включающую такого «исследователя» я, кажется, видел пару лет назад именно на Хабре. Однако лично мне, как и наверное всякому на моем месте, приятно вспоминать это время и свой скромный вклад в ту замечательную уютную игру.
Tags:
Hubs:
+35
Comments 7
Comments Comments 7

Articles