понедельник, 28 апреля 2014 г.

Как выбрать наихудший самолёт? Или про странности события Compare.


Когда есть дома свободных минут 10-20 я люблю запустить игру про самолётики и поучаствовать в сражении или двух или даже на всю ночь. Есть такой грех. Ну так вот, (извините за длинное предложение) в каждом сражении я выбираю одну из пяти стран, у каждой из которых несколько экипажей, каждый из которых использует один самолёт из кучи уникальных самолётов, полученных за накопление очков опыта и прочего вознаграждения.  И когда я получаю очередной новый самолёт, то возникает вопрос - какой из старых самолётов заменить на новую модель? Т.е. какой из действующих самолётов наименее эффективен?

Очевидно, что нужно сделать мобильное приложение, чтобы когда на экране компьютера видна игровая статистика можно было бы использовать в качестве калькулятора телефон. Но просто калькулятора недостаточно. Нужно сделать список машин, который можно сортировать по отношению добытых конкретным самолётом богатств к числу катастроф этого самолёта. Для вычисления такого показателя достаточно двух чисел, которые не нужно(!) хранить. Нужно хранить только вычисленное для сортировки значение. Самое сложное - отказаться от создания многофункциональной системы анализа, и оставить только простой список с сортировкой либо по имени, либо по одному показателю. Но всё-таки я не устоял перед соблазном, и добавил для фильтрации списка страну и признак архивности, чтобы выбывших из полётов не убивать совсем (я противник смертной казни), но убрать с глаз долой. Общий вид приложения такой:


Я установил TListBox.Sorted = True, которое вызовет событие OnCompare в момент  TListBox.EndUpdate. Причём при сортировке перед каждым вызовом OnCompare уже успевает отработать стандартное сравнение по TListBoxItem.Text, и результат этого сравнения даётся нам в Result. Если нам это и нужно, то делать уже ничего не надо. Если потребуется сравнение по отношению зарплата/гибель, то нужно изменить Result, воспользовавшись Item.TagFloat, где хранится вычисленное ранее значение. Ничего сложного? Вроде бы да, и я написал так:

  if tacSort.TabIndex > 0 then
    Result := 1 - 2*Byte( Item1.TagFloat > Item2.TagFloat );
 
И вот! Вот из-за этого всё и стало падать! Причём падает не здесь, а в FMX.ListBox.CompareListItem на Item1 is TListBoxItem. Странно, да? Т.е. когда у нас TabIndex = 0, то Result остаётся без изменений и всё хорошо, а когда переключаемся на "Рейтинг", то начинаются неприятности.



Я поясню, пожалуй, свою математику. Берём данные нам алгоритмом сортировки два предмета, сравниваем кто больше, получаем при приведении к байту 0 (False) или 1 (True), что даёт нам либо 1 = 1 - 2*0, либо -1 = 1 - 2*1.

Разумеется, догадаться, что основная причина сбоя программы - это "сам дурак", было не трудно. Дело в том, что алгоритм сортировки подсовывает между делом нам для сравнения в качестве Item1 и Item2 - один и тот же объект! И получается, что каждый элемент никогда не равен сам себе, и этого алгоритм выдержать не может. Он сам не знает и знать не желает, что сравнивается что-то с самим собой. И достаточно дополнить процедуру сравнения условием равенства Item1= Item2, как всё становится хорошо.

  if tacSort.TabIndex > 0 then
  if Item1 <> Item2 then
    Result := 1 - 2*Byte( Item1.TagFloat > Item2.TagFloat )
 else
   Result := 0;
 
Боже мой! Что это за глупость такая? Ясно же как день, что каждый предмет равен сам себе! Для чего же тогда вызывать лишний раз событие? Бред какой-то! Я негодовал. Но проверив VCL мне пришлось смириться с судьбой - там всё то же самое. Ужас.

Я несколько расстроен своим детским  открытием. И как это мне удавалось много лет счастливо жить, не зная таких элементарных вещей? Мне кажется, Quality Central будет надо мной громко смеяться, если я подниму этот вопрос. Мне грустно. Солнце перестало светить в моё окно. Надеюсь, читающие мимо друзья найдут пару слов мне в утешение.

ЗЫ: апк тут: https://www.dropbox.com/s/hzabel4nb6gsu0j/WarTest.apk
Cкриншоты и даже ролик тут: http://alhymov.blogspot.ru/2014/04/wartest-warthunder.html.