Определение постфлоп спектра

Мы делаем ставку на флопе, наш оппонент в позиции может играть фолд, колл или рейз. Интересует, каким образом он разыгрывает руки разной силы

Если у оппа рука-монстр, она дойдет до шоудауна (ШД), т.е. опп никогда с такой рукой не сфолдит. По ШД (отобрав ситуации, в которых у оппа была рука-монстр с флопа) мы сможем понять, как часто опп рейзит или коллит такие руки.

Ситуация усложняется, когда руки не настолько сильны, чтобы всегда идти на ШД. Например, с мусорными руками опп может иногда флоатить на флопе или играть рейз с блефом, Но чаще всего такие руки опп выкинет, поэтому понять как опп разыгрывает такие руки по ШД не получится. Опп может 90% мусорных рук фолдить, 3% колл, 7% рейз. т.е. 90% рук вообще никогда не доходят до ШД, а руки с которыми опп коллирует или рейзит доходят до вскрытия с разной частотой, из-за чего соотношение может измениться в ту или иную сторону. Например, вполне реально анализируя ШД получить распределение: 50% мусорных рук - колл; и 50% - рейз, а это совсем не соответствует тому, как опп играет на самом деле.

Кто-нибудь пробовал разобраться в этом вопросе? Может есть что-нибудь, что можно почитать на эту тему?


Я пробовал. Ответ надо искать в общей частоте фолдов, колов и рейзов. Если показатель фолда у оппа не нулевой, то он фолдит с мусорными руками. Если показатель фолда большой, то в его спектре фолда не только мусор но и какие-то смалл и мидл пары. После чего строятся пропорции для каждой группы рук и по анализу ШД уточняется, что конкретно входит в спектр фолда оппа
Надо учитывать такой момент что если мусорные руки он все-таки показывает на ШД, а спектр фолда не очень широк, то в спектре его фолда есть не только мусорные руки

Ответ надо искать в общей частоте фолдов, колов и рейзов в конкретной ситуации

Ну это очевидно, что раз уж мы не можем определить то, что нужно по ШД, придется смотреть на статы. Но как раз и интересно, как именно можно использовать конкретные значения стат.

Если показатель фолда у оппа не нулевой то в первую очередь он фолдит с мусорными руками

Вопрос заключается не в том, как опп разыгрывает именно мусорные руки. Интересно, как определить ВЕСЬ спектр целиком

Надо к примеру учитывать и корректировать такой момент что если мусорные руки он все таки показывает на ШД, а спектр фолда не очень широк, то в спектре его фолда есть не только мусорные руки

Да, да. Именно в этом и заключается сложность. Что опп может показывать на ШД разные по силе руки при высоком фолде. Очевидно, что он фолдит и мусор, и дро, и слабые готовые руки. Но вычислить сколько именно рук разной силы он фолдит - в этом и заключается задача

Например, меня интересуют две группы рук:
- слабые готовые
- дро средней силы (7-8 аутов)
есть семь оппов с фолдами: 5%,10%,20%,30%,40%,50%,60%. Интересен конкретный алгоритм, как можно было бы задать спектры каждому из этих игроков? Теоретически спектр можно было бы задать так (все цифры вымышленные):
- 5% - руки монстры
- 15% - готовые руки средней силы
- 10% - слабые готовые руки
- 30% - дро руки
- 40% - мусор
Но сейчас я определяю спектр другим образом (как в CREV): из группы "слабые готовые руки" (вся группа принимается за 100%)
- 10% опп фолдит
- 80% опп коллирует
- 10% опп рейзит
Чтобы задать весь спектр, нужно будет подобным образом описать каждую группу рук.

ну если спектр задан как

- 5% - руки монстры
- 15% - готовые руки средней силы
- 10% - слабые готовые руки
- 30% - дро руки
- 40% - мусор
то дело сделано. остается только определить пропорции для статы фолда и посмотреть какие руки она покрывает - начиная от мусора и дальше по рейтингу

Я задал произвольный спектр. Но у нас он не задан - вопрос именно в том, как бы этот спектр задать. Простой вопрос: какую часть (сколько процентов) слабых готовых рук сфолдит опп, если его фолд на кбет на флопе в позиции 40%? Еще раз уточню, что спектр (пусть это будет фолд на кбет в позиции) задается таким образом (цифры произвольные):
- 0% рук монстров
- 0% готовых рук средней силы
- 10% слабых готовых рук
- 20% дро рук
- 90% мусора

какую часть (сколько процентов) слабых готовых рук сфолдит опп, если его фолд на кбет на флопе в позиции = 40%?

если в спектре много мусора то он и сфолдит только мусорные руки

Эта мысль мне непонятна. Может в каком-то частном случае так и будет. Но в общем случае это не так, имхо. Даже опп с 10% фолда вряд ли фолдит 100% мусорных рук. На практике в спектрах колла и рейза у него будет весь набор разных рук (мусорных рук и дро там будет мало, но они будут). Я поясню.. У оппонента в спектре есть слабые готовые руки (все эти руки условно обозначим за 30%). Если опп никогда не фолдит слабые готовые руки, то можно смело утверждать, что опп фолдит 0% слабых готовых рук. Если опп всегда фолдит слабые готовые руки, то можно утверждать, что опп фолдит 100% слабых готовых рук. Искомый процент, очевидно, лежит в диапазоне между 0% и 100%. Каким образом такой процент можно определить? P.S. Меня не покидает ощущение, что мы по-разному представляем себе, каким образом задается спектр оппа. Приведу пример задания спектра вымышленного оппа (таких оппов не бывает), который фолдит 100% рук. Спектр фолда такого оппа выглядит следующим образом:
- 100% рук монстров
- 100% готовых рук средней силы
- 100% слабых готовых рук
- 100% дро рук
- 100% мусора

Существет два матподхода к решению обозначенной проблемы.

Первый подход: опп разыгрывает руки разной силы HS (вещественное число от 0 до 1). Предположение заключается в том, что опп из всего спектра возможных рук сбрасывает low % слабых рук, с mid % средних рук делает колл и с high % сильных рук делает рейз. При этом low + mid + high = 100% Имея 1326 возможных комбинаций оппа, мы вычисляем силу руки каждой и кладем в массив, сортируем этот массив по возрастанию, после чего, зная вероятностую тройку оппа в текущем игровом контексте, разделяем этот массив на 3 части - это и есть наше видение того, как опп разыгрывает разные руки в текущей игровой ситуации. Т.е. если опп делает фолд, то у него рука, сила которой приходится в low-часть массива, если коллирует - то mid, если делает рейз - то high. Например, опп на флопе делает конбет. Вытаскиваем вероятностную тройку этого события - (0, 0.15, 0.85) - Cbet 85%, получаем low - нет рук, младшие + средние = 15 % от массива (т.е. 1326 * 0.15) рук - чек, остальные 85 % - бет. Слабость этого подхода в том, что мы ИЗНАЧАЛЬНО сделали предположение, которое фактически может быть неверным. Т.е. опп может играть конбет с младшими руками, а со старшими делать чек. Сильная сторона этого метода - это индивидуальный подход к каждому оппу, т.к. мы берем вероятностную тройку для каждого оппа. В литературе этот подход называется Specific Opponent Modelling - SOM

Второй подход. Мы используем историю из огромного кол-ва рук N игроков, каждый из которых Hero, т.е. мы знаем его карты, начиная с начала раздачи. На этой объединенной истории (из множества историй разных Hero) тренируем нейронную сеть. Имея такую натренированную сеть, мы для очередной комбинации спрашиваем нейронную сеть о том, как бы она игралась. Для каждого запроса сеть дает нам ответ - вероятностную тройку FCR. Т.к. мы знаем, какое действие своершил игрок по факту, мы берем соотв. значение из тройки (F, C или R) и умножаем на него вес рассматриваемой комбинации (изначально вес равен 1). В итоге после очередного хода игрока мы получаем перераспределение весов его возможных комбинаций, которое соотв. предположению о его спектре. Этот подход очень эффективен, но также имеет недостатки: 1) сложно найти множество адекватных по размеру и стилю игры историй разных Hero; 2) у тебя получается единая объединенная модель всех оппонентов - т.е. некий идеальный оппонент, и если фактический опп играет сильно отличающуюся от универсальной стратегию, то при игре с ним бот будет делать ошибки и при этом не сможет адаптироваться. В литературе этот метод называется Generic Opponent Modelling - GOM

Оба этих подхода широко используются. Первый подход реализован в боте Poki от CPRG. Описанию второго подхода посвящен труд AN ARTIFICIAL INTELLIGENCE AGENT FOR TEXAS HOLD’EM POKER (Patrick Mccurley, 2009) - одна из самых заметных публикаций последнего времени. Ктасти сказать. Если применить второй подход и взять оооочень много разных Hero с большой историей, а затем натренировать нейронную сеть и сохранить ее в каком то формате, но в виде огромного числа текстовых файлов с вероятностными тройками, а затем построить на этой базе AI, то получится идеальный игрок против всех использованных при тренерировке сети Hero. Если для треннига брались качественные и разноплановые Hero, то из этого в принципе получится неплохой бот, но он будет абсолютным болваном против оппа, играющего "как-то не так". Т.е в таком случае бот не будет самоорганизовываться, чтобы устранить слабости в своей игре и эксплуатировать слабости оппа.

Я может чего пропустил, но ИМХО FO2 не использует нейронные сети.

Непосредственно в игре не использует. Но для его построения использовалась нейронная сеть, сделанная так, как я описал выше. Затем вся нейронная сеть была сохранена в текстовом виде в огромное кол-во файлов. При принятии решения он формирует игровой контекст (раунд, история торгов, букет силы руки, показатель стола), по этому контексту формируется имя файла и вытаскивается вероятностная тройка

Да? А вот автор несколько другое пишет

Действительно. Где-то читал про Co-evolutionary алгоритм, но, честно говоря, всегда думал, что этот алгоритм нейронные сети использует. А оказывается нет.

Правильно понимаю что используя Первый подход мы на постфлопе как бы предполагаем у оппа все 1326 руки (за исключение тех карт, что мы видим) и делим весь его спектр ВСЕГО на три части low + mid + high?

Да. При этом мы получаем пороговые значения силы руки для колла и рейза - HScall и HSraise применительно к конкретному оппу (т.е. у каждого они будут разными) в конкретной игровой ситуации. При большой статистике полученные значения будут достаточно точны. Теперь мы проходим все возможные руки оппа и для каждой, вычислив ее HS, задаемся вопросом "как бы опп сыграл эту руку, если мы знаем его пороговые HScall и HSraise". Есть ф-я предиктор, которая возвращает тройку FCR:


if (HS < HScall)       return FCR(1,0,0); 
else if (HS > HSraise) return FCR(0,0,1); 
else                      return FCR(0,1,0);  
Тогда получится, что мы разделили руки оппа на 3 категории. Это очень грубо. Обычно эта ф-я все же не дискретная, т.е. обычно она как-то "проецирует" силу руки в вероятность (интерполируя или еще как то), также она обычно учитывает потенциал для того, чтобы правильно предсказывать колл оппа без силы руки, но по шансам банка, т.е. когда potOdds <= PPot1. А дальше оба подхода делают одно и то же (отличия только в том как реализована ф-я предиктор). В итоге после очередного хода игрока мы получаем перераспределение весов его возможных комбинаций, которое соотв. нашему предположению о его спектре. Псевдокод. На входе: actualAction - действие совершенное оппом, gameContext - игровой контекст, weightTable - спектр карт оппа в виде таблицы весов (1326 значений, вес невозможных = 0). На выходе: обновленный спектр weightTable оппа:

foreach (hand in weightTable)
{
  fcr = Predict(hand, gameContext);
  weightTable[hand] *= fcr[actualAction];
} 
Predict - это ф-я предиктор. В первом подходе - это некая эвристическая ф-я, которая принимает на вход HScall, HSraise, вычисленные выше, а также PPot1 и еще любые необходимые параметры для предсказания. Во втором подходе Predict - это обращение к нейронной сети.

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

"Четкость" границ зависит от того, как ты напишешь ф-ю Predict. Если так, как в примере, то конечно. Если же заложишь туда "шум" и т.п., то для руки с HS < HScall будешь получать, к примеру, FCR(0.9, 0.05, 0.05). Если мы говорим о матподходе, то использование "человеческих" определений типа "мусор", и и т.д. совершенно не приемлемо. Есть четкие матхарактеристики руки - сила и потенциал. Я когда слышу вот это "мусор" там и пр. сразу вспоминаю OPI - "если рейз и у нас две пары", то то, если "2 рейза и у нас пара", то сё - детский сад. Проблема не в группировке рук, а в установлении соответствия между ходом игрока в опред. игровом контексте и характеристикой руки, которую он может держать, сделав этот ход. Если мы используем матхарактеристику, то мы можем решить эту задачу. Если же мы оперируем понятиями нематематическими, то матаппарат применить сложнее. В какой момент к примеру ты в своем подходе перестанешь относить "Оверкаты" к руке, с которой опп может сделать бет?

Ну это зависит оппа. Проблема может быть в том, что руки с примерно равной силой и потенциалом опы могут видеть по разному. И по количеству границ для потенциала и силы руки - сколько у тебя их ? то есть на сколько групп ты делишь все руки ? Обычных групп типа мусор, дро, монстр при всем желании можно выделить не больше десяти, а по цифрам - если шаг будет слишком мелкий, то никаких историй не хватит чтобы набрать реальную статистику на каждую группу, а если слишком большой - то руки вроде бы из одной группы будут совершенно разными в глазах оппа

Второй подход. Мы используем историю из огромного кол-ва рук N игроков, каждый из которых Hero, т.е. мы знаем его карты, начиная с начала раздачи
Если бы у меня была возможность получать и анализировать полные истории раздач на игроков, то этой темы просто не было бы. Понятно, что имея информацию обо всех руках можно взять да и посмотреть - с какой частотой доходят до ШД (WTSD) руки разной силы, и понять, руки какой силы оппы фолдят. Но ведь такой информации у нас нет

Вычисление эквити против взвешенного спектра оппонента

Большинство из нас привыкли использовать понятие эквити EQ (не путать с EV!) для оценки своей руки против оппонентов. Несмотря на то, что эквити по определению подразумевает нашу долю в банке, учитывая наши шансы на его выигрыш, большинство обычно этим пренебрегают и рассматривают эквити просто как вероятность на победу Pr(win) на шоудауне (шоудаун эквити). Т.е. эквити – это число от 0 до 1 (или от 0% до 100%). Для того чтобы перевести эквити в денежное выражение и получить таки долю банка мы должны умножить величину EQ на размер банка.

В литературе обычно не используют понятие эквити (т.к. все-таки по определению оно зависит от размера банка), а вместо этого используют понятие силы руки HS (Hand Strength). Далее я тоже не буду использовать понятие эквити – для меня привычнее показатель HS. К этому, кстати, призываю и всех остальных. Сила HS руки показывает вероятность нашей победы в текущем раунде. HS на ривере – это то же самое, что большинство из нас понимают под шоудаун эквити. Следующая функция вычисляет HS для заданных карманных карт c1c2, карт стола board и взвешенного спектра карт оппонента wt:


double 
CalcHandStrength (Hand const &board, Card c1, Card c2, WeightTable const &wt)
{
   int      i, j ;
   int      opprank ;
   double   wins = 0, ties = 0, loses = 0 ;
   Card     o1, o2 ;
   double   weight;
   char     impossible[Card::NUM_CARDS] ;

   int ourrank = Evaluate (board, c1, c2) ;

   memset (impossible, 0, sizeof(impossible));

   impossible[c1.GetIndex()] = 1 ;
   impossible[c2.GetIndex()] = 1 ;

   for (i = 1; i <= board.GetSize (); i++)
      impossible[board.GetCard(i).GetIndex()] = true ;

   for (i = 0; i < Card::NUM_CARDS; i++)
      for (j = i + 1; j < Card::NUM_CARDS; j++)
      {
         if (impossible[i] || impossible[j]) continue ;

         o1.SetIndex (i);
         o2.SetIndex (j);

         opprank = Evaluate (board, o1, o2);

         weight = wt.GetHandWeight (o1, o2);

         if (ourrank >= opprank)         wins += weight    ;
         else if (ourrank == opprank)    ties += weight    ;
         else // ourrank < opprank       loses += weight   ;
      }
   
   return (wins + ties / 2) / (wins + loses + ties) ;
}
Как видно из кода, вычисление заключается в переборе всех карт оппонента и сравнения очередной получившейся у него руки с учетом карт стола с нашей текущей рукой. Понятно, что вычисленное значение HS показывает наши шансы на победу в текущем раунде, т.е. без учета тех карт, которые могут выйти на стол, поэтому UofA вводит еще одно понятие – EHS (Effective Hand Strength) – эффективную силу руки, которая вычисляется по формуле:
              EHS = HS * (1 – NPot) + (1 – HS) * PPot
EHS уже учитывает те карты, которые могут выйти на стол (одну или две в зависимости от раунда) за счет того, что в нее «замешаны» показатели NPot и PPot – соответственно, отрицательный и положительный потенциалы нашей руки. Эти показатели для заданных карманных карт c1c2, карт стола board и взвешенного спектра оппонента wt вычисляет следующая функция:

void 
CalcHandPotential (Hand const &board, Potential &pot, Card c1, Card c2, bool fullLookahead, WeightTable const &wt)
{
   enum
   {
      AHEAD,
      TIED,
      BEHIND
   } ;

   double         HP[3][3] ;
   double         HPTotal[3] ;

   Hand comm = board ;

   int            i, j, k, l, index ;
   double         weight ;

   for (i = 0 ; i < 3; i++)
   {
      for (j = 0; j < 3; j++) HP[i][j] = 0 ;
      HPTotal[i] = 0 ;
   }

   bool impossible[Card::NUM_CARDS] ;

   memset(impossible, 0, sizeof(impossible)) ;

   impossible[c1.GetIndex()] = 1 ;
   impossible[c2.GetIndex()] = 1 ;

   for (i = 1; i <= comm.GetSize(); i++)
      impossible[comm.GetCard(i).GetIndex()] = 1 ;

   int    ourrank5 = Evaluate(comm, c1, c2) ;
   int    opprank, ourrank7 ;
   Card   o1, o2 ;

   ASSERT (comm.GetSize() == 3 || comm.GetSize() == 4) ;

   fullLookahead = (comm.GetSize() == 3 && fullLookahead) ;

   for (i = 0; i < Card::NUM_CARDS; i++)
      for (j = i + 1; j < Card::NUM_CARDS; j++)
      {
         if (impossible[i] || impossible[j]) continue ;

         impossible[i] = 1 ;
         impossible[j] = 1 ;

         o1.SetIndex(i) ;
         o2.SetIndex(j) ;

         opprank = Evaluate(comm, o1, o2) ;

         if (ourrank5 > opprank)         index = AHEAD ;
         else if (ourrank5 == opprank)   index = TIED ;
         else                            index = BEHIND ;

         weight = wt.GetHandWeight(o1, o2) ;
         HPTotal[index] += weight ;

         for (k = 0; k < Card::NUM_CARDS; k++) 
         {
            if (impossible[k]) continue ;

            impossible[k] = 1 ;
            comm.AddCard(k) ;

            if (fullLookahead) 
            {
               for (l = k + 1; l < Card::NUM_CARDS; l++) 
               {
                  if (impossible[l]) continue ;

                  comm.AddCard(l) ;

                  ourrank7 = Evaluate(comm, c1, c2) ;
                  opprank = Evaluate(comm, o1, o2) ;

                  if (ourrank7 > opprank)       HP[index][AHEAD]  += weight ;
                  else if (ourrank7 == opprank) HP[index][TIED]   += weight ;
                  else                          HP[index][BEHIND] += weight ;

                  comm.RemoveCard() ;
               }
            } 
            else 
            {
               ourrank7 = Evaluate(comm, c1, c2) ;
               opprank = Evaluate(comm, o1, o2) ;

               if (ourrank7 > opprank)       HP[index][AHEAD]  += weight ;
               else if (ourrank7 == opprank) HP[index][TIED]   += weight ;
               else                          HP[index][BEHIND] += weight ;
            }

            comm.RemoveCard() ;
            impossible[k] = 0 ;
         }

         impossible[o1.GetIndex()] = 0 ;
         impossible[o2.GetIndex()] = 0 ;
      }

   int mult = (fullLookahead ? 990 : (Card::NUM_CARDS - 3 - 2 * 2)) ;
   double den1 = mult * (HPTotal[BEHIND] + (double)HPTotal[TIED] / 2) ;
   double den2 = mult * (HPTotal[AHEAD] + (double)HPTotal[TIED] / 2) ;

   if (den1 > 0) 
      pot.ppot = (HP[BEHIND][AHEAD] + (double)HP[BEHIND][TIED] / 2 + 
         (double)HP[TIED][AHEAD] / 2) / den1 ;
   else 
      pot.ppot = 0 ;

   if (den2 > 0) 
      pot.npot = (HP[AHEAD][BEHIND] + (double)HP[AHEAD][TIED] / 2 + 
         (double)HP[TIED][BEHIND] / 2) / den2 ;
   else 
      pot.npot = 0 ;
}

Положительный потенциал PPot показывает шансы на то, что мы, находясь в текущем раунде позади, в следующем раунде (или к шоудану, если fullLookahead == true) улучшимся и победим. Отрицательный потенциал NPot показывает шансы на то, что мы, находясь в текущем раунде впереди, в следующем раунде (или к шоудауну) станем хуже и проиграем. Другими словами PPot – это вероятность того, что «переедем» мы, а NPot – это вероятность того, что «переедут» нас.

Когда показатели HS, PPot и NPot вычислены, мы можем вычислить эффективную силу руки EHS. EHS – это и есть то значение, которое вычисляет любой эквилятор. Спрашивается, для чего все это делать через… PPot и NPot? Нельзя ли просто перебрать все карты оппа и «прогнать» их до шоудауна? Можно! Именно так и делает эквилятор. Однако при этом теряется очень много полезной информации. PPot, например, показывает силу нашего дро. Если PPot >= potOdds, то мы должны колировать по шансам банка. Кроме того, оценивая PPot, можно выбирать разные стратегии розыгрыша. Так, если PPot >= 0.2, то мы знаем, что у нас какое-то сильное дро, мы уже можем делать бет, чек-рейз и пр. Нам не нужно «вычислять гатшот», вычислять комбинированные дро и т.п. Вместо этого у нас появляется соответствующая математическая характеристика, что очень важно. NPot, наоборот, показывает слабость нашей готовой руки, обычно NPot исключают при вычислении EHS. Т.е. EHS = HS + (1 – HS) * PPot. Такой шаг по заверениям UofA поощряет агрессивную игру вообще и, в частности, при защите готовой руки. Я использую NPot для анализа слабости готовой руки. Это позволяет выделить руки с приличным EHS, но являющиеся при этом чрезвычайно уязвимыми. Такие руки нужно выкидывать на флопе. И, благодаря показателю NPot, бот очень просто делает сложные фолды и вообще начинает шикарно играть флоп в стиле Склански (либо рейз, либо пас).

Взвешенный спектр в функции CalcHandStrength и CalcHandPotential передается через таблицу весов wt, в которой каждой из 1326 возможных рук оппонента поставлен в соответствие вес - вероятность того, что оппонент разыграл бы эту руку так, как мы увидели. Т.е. сумма весов в таблице НЕ равна 1. Напротив, вес каждой руки - это число от 0 до 1.

Еще один момент, касающийся вычислений против нескольких оппонентов. UofA делает просто - они вычисляют HS (не EHS!) против каждого оппа (используя для каждого оппа его таблицу весов), перемножают полученные значения, получая HSn. При этом утверждается, что такое перемножение допустимо, т.к. вносит небольшую погрешность. Затем формируют единую таблицу весов всех оппов, которая называется field array и по этой единой таблице вычисляют PPot и NPot, передавая ее в функцию CalcHandPotential в качестве аргумента wt. Затем вычисляют EHSn = HSn + (1 – HSn) * PPot.

Функция Evaluate - точка входа стандартного эвалюатора, который вычисляет ранг руки от flash royal до five/seven high.

Все остальные классы - стандартные из Meerkat API, за исключением WeightTable - 'nj просто таблица весов на 1326 элементов. Два метода SetHandWeight/GetHandWeight.

Вот вам куча алгоритмов

Все остальные классы - стандартные из Meerkat API, за исключением WeightTable просто таблица весов на 1326 элементов. Два метода SetHandWeight/GetHandWeight.
что такое Meerkat API я вообще не в курсе. прог, которые считают эквити с весами, я не нашел и было бы неплохо просто сравнить цифры

в общем, если есть желание, скажи в каком формате у тебя спектр рук с весами я вышлю и проверим результаты. или давай так: AhAd 1.0 AhKd 0.99 и т.д. в обычном тексте. Все 1326 рук.

Ну не знаю на счет всех, можно просто только те руки у которых ненулевой вес - чтобы прога на руки с нулевыми весами не тратила ни секунды своего времени. у меня вызывает некоторые сомнения мои формулы для вычисления ничьих. если у тебя такие же, то значит все ок, если нет - можно будет обсудить