[Из песочницы] Расчет приоритета комбинаций в техасском холдеме (покере) на PHP

Habrahabr 1

В этой статье я расскажу про мой способ расчета приоритета комбинаций в техасском холдеме (покере с 2 карманными картами и 5 общими). Условно разделю статью на 6 частей: Создание массива карт, Расчет Стрита, Расчет Флеша и Стрит Флеша, Расчет Парных комбинаций, Окончательный расчет, Вывод на экран. Код пишу в процедурном стиле.

Цель: получить на выходе программы удобочитаемый результат, доступный для дебаггинга и отладки еще на стадии написания кода. Для достижения поставленной цели делаю разницу между комбинациями в 1e+2 (100) раз.

Примерный вид того, что должно получиться в итоге, где 2 последние карты считаю карманными, а первые 5 общими:

Создание массива карт

В колоде 4 масти и 13 достоинств, поэтому массив из 7 чисел буду формировать в диапазоне [2 — 14, 102 — 114, 202 — 214, 302 — 314], а далее оперировать остатками от деления на 100 (%100) этих чисел. Можно, конечно, формировать массив из чисел в диапазоне [2 — 14, 22 — 34, 42 — 54, 62 — 74], а затем сравнивать остатками от деления на 20 (%20). Но удобочитаемость первого варианта по-моему на лицо, поэтому остановлюсь на нем.
function cardsCreation()
{
    $arrayCards = [];
    for ($i = 0; $i < 7; $i++) {
        $card = mt_rand(2, 14); // создаю случайное число в диапозоне от 2 до 14
        $multiplier = mt_rand(0, 3); //формирую случайным образом множитель
        if (1 == $multiplier) { //если множитель равен 1, добавляю к числу 100
            $card = $card + 100;
        } else if (2 == $multiplier) { //если множитель равен 2, добавляю к числу 200
            $card = $card + 200;
        } else if (3 == $multiplier) { //если множитель равен 3, добавляю к числу 300
            $card = $card + 300;
        }

        if (!in_array($card, $arrayCards)) { //проверяю, есть ли в массиве такое число, если нет добавляю его в массив
            $arrayCards = array_merge($arrayCards, [$card]);
        } else { // если число есть в массиве, откатываю цикл, чтобы сформировать новое число
            $i--;
        }
    }
    return $arrayCards;
}

Определение и расчет комбинации Стрит

Стрит — это комбинация, где достоинства карт идут по порядку. Поэтому нет смысла учитывать при расчете все карты, составляющие данную комбинацию, достаточно лишь первой по старшинству карты.

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

Суть моего метода определения стрита сводится к тому, что после сортировки я буду проверять сначала первые пять карт на стрит, затем вторые и третьи. Также не забываю что туз, двойка, тройка, четверка и пятерка тоже составляю стрит. Поэтому, если в наборе есть туз (значение 14), к массиву добавляю восьмой элемент — 1. И следовательно в этом случае нужно проверить четвертую пятерку карт.

function straight(array $arrayCards) {
    $newArrayCards = [];
    $ace = false;
    foreach ($arrayCards as $arrayCard) {
        $newArrayCards = array_merge($newArrayCards, [$arrayCard % 100]);  //создаю массив с остатками от деления на 100
        if(14 == $arrayCard % 100) { //проверка на туз для комбинации А, 2, 3, 4, 5
            $ace = true;
        }

    }
    if($ace == true) {
        $newArrayCards = array_merge($newArrayCards, [1]); //если в массиве присутствует туз, добавляю к массиву 1
    }
    rsort($newArrayCards); //сортирую массив с числами в порядке убывания

    $count = 0; //счетчик, к которому добавляется 1 в случае, если разница между текущим элементом цикла и предыдущим = 1
    $length = 4; //число, показывает до какого элемента массива проверять
    $result = 0; //окончательный результат
    $begin = 0; //число показывает, с какого элемента массива проверять
    for($i = 1; $i <= $length; $i++) {
        if (-1 == ($newArrayCards[$i] - $newArrayCards[$i - 1])) {
            $count++;
        }
        if($length == $i) {
            if(4 == $count) { //$count == 4, когда стрит
                $result = $newArrayCards[$begin] * 1e+8;
            }
            else if($length < 6 or (6 == $length and $ace == true)) {//если стрита нет, идем еще на один круг
                $length++; //увеличиваем число, до которого будем проверять
                $begin++; //увеличиваем число, с которого будем проверять
                $i = $begin; //при попадании в for к $i автоматически добавится 1 ($i++ в for)
                $count = 0; //обнуляю счетчик
            }
        }
    }
    return $result;
}

Определение и расчет комбинаций Флеш и Стрит Флеш

Флеш — комбинация из карт одной масти. При расчете нужно учитывать все карты, составляющие комбинацию.

При расчете стрит флеша, как и при расчете стрита, можно учитывать только старшую карту комбинации.

Чтобы определить флеш, пробегусь циклом по массиву. В цикле создам 4 вспомогательных массива (по количеству мастей). В перый массив положу все карты первой масти, во второй — второй и т.д. Если какой-то масти нет, соответствующий массив будет пустой. Если количество элементов массива больше либо равно 5, значит данная масть образовала флеш.

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

Если флеш не образует стрит флеш, отсортирую остатки от деления на 100 (%100) исходного массива. Далее умножу первые пять членов полученного массива на 1е+10, 1е+8, 1е+6, 1е+4, 1е+2 соответственно.

Функцию по определению стрит флеша построю на основе функции по определению стрита, с той лишь разницей, что нужно учесть, что в функцию может прийти массив с количеством элементов меньше 7 (я ведь туда отправляю только те карты, которые образуют флеш).

Функция для определения Флеша:

function pokerFlush(array $arrayCards) {
    $suit1 = []; //первая масть
    $suit2 = []; //вторая масть
    $suit3 = []; //третья масть
    $suit4 = []; //четвертая масть
    foreach ($arrayCards as $arrayCard) { //создаю 4 массива, содержащих разные масти исходного массива
        if($arrayCard >= 2 and $arrayCard <= 14) {
            $suit1 = array_merge($suit1, [$arrayCard]);
        } else if($arrayCard >= 102 and $arrayCard <= 114) {
            $suit2 = array_merge($suit2, [$arrayCard]);
        } else if($arrayCard >= 202 and $arrayCard <= 214) {
            $suit3 = array_merge($suit3, [$arrayCard]);
        } else {
            $suit4 = array_merge($suit4, [$arrayCard]);
        }}
    if(count($suit1) >= 5) {  //если количество карт первой масти больше или равно 5
        $result = straightFlush($suit1); //проверяю не образует ли данная комбинация стрит флеш
        if(0 == $result) {//если стрит флеша нет
            foreach ($suit1 as $key1 => $s1) {//выбираю остатки от деления на 100
                $suit1[$key1] = $s1 % 100;
            }
            rsort($suit1); //сортирую массив по убыванию
            $result = $suit1[0] * 1e+10 + $suit1[1] * 1e+8 + $suit1[2] * 1e+6 + $suit1[3] * 1e+4 + $suit1[4] * 1e+2;
        }
    } else if (count($suit2) >= 5) {   //если количество карт второй масти больше или равно 5
        $result = straightFlush($suit2);
        if(0 == $result) {
            foreach ($suit2 as $key2 => $s2) {
                $suit2[$key2] = $s2 % 100;
            }
            rsort($suit2);
            $result = $suit2[0] * 1e+10 + $suit2[1] * 1e+8 + $suit2[2] * 1e+6 + $suit2[3] * 1e+4 + $suit2[4] * 1e+2;
        }
    } else if (count($suit3) >= 5) { //если количество карт третьей масти больше или равно 5
        $result = straightFlush($suit3);
        if(0 == $result) {
            foreach ($suit3 as $key3 => $s3) {
                $suit3[$key3] = $s3 % 100;
            }
            rsort($suit3);
            $result = $suit3[0] * 1e+10 + $suit3[1] * 1e+8 + $suit3[2] * 1e+6 + $suit3[3] * 1e+4 + $suit3[4] * 1e+2;
        }
    } else if (count($suit4) >= 5) {     //если количество карт четвертой масти больше или равно 5
        $result = straightFlush($suit4);
        if(0 == $result) {
            foreach ($suit4 as $key4 => $s4) {
                $suit4[$key4] = $s4 % 100;
            }
            rsort($suit4);
            $result = $suit4[0] * 1e+10 + $suit4[1] * 1e+8 + $suit4[2] * 1e+6 + $suit4[3] * 1e+4 + $suit4[4] * 1e+2;
        }
    } else {
        $result = 0;
    }
    return $result;
}

Функция для определения Стрит Флеша (аналогична Стриту, только нужно учесть, что количество элементов массива может быть не только 7, но еще и 5, и 6, и что возвращаемый результат этой функции должен быть больше):
function straightFlush(array $arrayCards) {
    $newArrayCards = [];
    $ace = false;
    foreach ($arrayCards as $arrayCard) {
        $newArrayCards = array_merge($newArrayCards, [$arrayCard % 100]);  
        if (14 == $arrayCard % 100) {
            $ace = true;
        }
    }
    if ($ace == true) {
        $newArrayCards = array_merge($newArrayCards, [1]);
    }
    rsort($newArrayCards); 
    $count = 0; 
    $length = 4; 
    $result = 0; 
    $begin = 0; 
    for ($i = 1; $i <= $length; $i++) {
        if (-1 == ($newArrayCards[$i] - $newArrayCards[$i - 1])) {
            $count++;
        }
        if ($length == $i) {
            if (4 == $count) { 
                $result = $newArrayCards[$begin] * 1e+16;
            } else if ((7 == count($arrayCards) and ($length < 6 or (6 == $length and $ace == true))) or
                (6 == count($arrayCards) and ($length < 5 or (5 == $length and $ace == true))) or //если число элементов исходного массива = 6
                (5 == count($arrayCards) and (5 == $length and $ace == true))) { //если число элементов исходного массива = 5
                $length++; 
                $begin++; 
                $i = $begin;
                $count = 0;
            }
        }
    }
    return $result;
}

Определение и расчет парных комбинаций

Существует пять парных комбинаций: каре (4 карты одного достоинства), фулл хаус (3 карты одного достоинства и 2 карты другого достоинства), тройка (3 карты одного достоинства), две пары (2 карты одного достоинства и 2 карты другого достоинства), пара (2 карты одного достоинства).

Важно при расчете помнить, что в комбинации фулл хаус главным является сочетание из трех карт, а из двух второстепенным, т.е., например, комбинацию из 3-х двоек и 2-х десяток будем считать, как 3*1e+12 + 10*1e+10, а не по достоинству карт.

А также при расчете парных комбинаций нужно учитывать проблемы:

1. То, что более 5 карт образуют комбинацию. 2. То, что у нас может возникнуть ситуация, при которой комбинаций более одной.

Для подсчета совпадения карт буду использовать три счетчика, которые будут считать парные карты в колоде. Там, где счетчик равен 1 — участвуют 2 карты в комбинации, где счетчик 2 — 3 карты, где 3 — 4 карты.

Рассмотрим все возможные варианты сочетания счетчиков:

100 — пара 110 — 2 пары 111 — 3 пары (нужно преобразовать к виду 110 (две пары)) 200 — тройка 210 — фулл хаус 120 — фулл хаус (нужно преобразовать к виду 210 (фулл хаус)) 211 (121, 112) — фулл хаус + пара (нужно преобразовать к виду 210 (фулл хаус)) 220 — 2 тройки или фулл хаус + 1 карта (нужно преобразовать к виду 210 (фулл хаус)) 300 — каре 310 (130) — каре + пара (нужно преобразовать к виду 300 (каре)) 320 (230) — каре + тройка (нужно преобразовать к виду 300 (каре)) Далее произведу окончательный расчет парных комбинаций.

function couple(array  $arrayCards) {
    $newArrayCards = [];
    foreach ($arrayCards as $arrayCard) {
        $newArrayCards = array_merge($newArrayCards, [$arrayCard % 100]); //создаю массив с остатками от деления на 100
    }
    rsort($newArrayCards); //сортирую массив с числами в порядке убывания

    $count1 = 0; //счетчик для первой пары
    $count2 = 0; //счетчик для второй пары
    $count3 = 0; //счетчик для третьей пары

    $match1 = 0; //сюда положу значение первой пары
    $match2 = 0; //сюда положу значение второй пары
    $match3 = 0; //сюда положу значение третьей пары


    for($i = 1; $i < count($newArrayCards); $i++) {

        if ($newArrayCards[$i] == $match1 or $match1 == 0) { //первое парное сочетание
            if ($newArrayCards[$i] == $newArrayCards[$i - 1]) {
                $match1 = $newArrayCards[$i];
                $count1++;
            }
        } else if ($newArrayCards[$i] == $match2 or $match2 == 0) { //второе парное сочетание
            if ($newArrayCards[$i] == $newArrayCards[$i - 1]) {
                $match2 = $newArrayCards[$i];
                $count2++;
            }
        } else if ($newArrayCards[$i] == $match3 or $match3 == 0) { //третье парное сочетание
            if ($newArrayCards[$i] == $newArrayCards[$i - 1]) {
                $match3 = $newArrayCards[$i];
                $count3++;
            }
        }
    }

    //здесь я преобразую 111 к 110 (2 пары) и 211 к 210 (фулл хаус)
    if(($count1 == 1 or $count1 == 2) and $count2 == 1 and $count3 == 1) {
        $count3 = 0;
    }
    //здесь я преобразую 121 сначала к 211 для простоты вычислений, а затем к 210 (фулл хаус)
    else if($count2 == 2 and $count1 == 1 and $count3 == 1) {
        $support = $match2;
        $match2 = $match1;
        $match1 = $support;
        $count1 = 2;
        $count2 = 1;
        $count3 = 0;
    }
    //здесь я преобразую 112 сначала к 211 для простоты вычислений, а затем к 210 (фулл хаус)
    else if($count3 == 2 and $count1 == 1 and $count2 == 1) {
        $support = $match3;
        $match2 = $match1;
        $match1 = $support;
        $count1 = 2;
        $count3 = 0;
    }
    //здесь я преобразую 220 к 210 (фулл хаус)
    else if($count1 == 2 and $count2 == 2 and $count3 == 0) {
        $count2 = 1;
    }
    //здесь я преобразую 120 к 210 (фулл хаус)
    else if ($count1 == 1 and $count2 == 2 and $count3 == 0) {
        $support = $match1;
        $match1 = $match2;
        $match2 = $support;
        $count1 = 2;
        $count2 = 1;
    }
    //320 к 300 и 310 к 300
    else if($count1 == 3 and ($count2 == 2 or $count2 == 1)) {
        $count2 = 0;
    }
    //230 к 320 и затем к 300 и 130 к 310 и затем к 300
    else if($count2 == 3 and($count1 == 2 or $count1 == 1)) {
        $support = $match2;
        $match2 = $match1;
        $match1 = $support;
        $count1 = 3;
        $count2 = 0;
    }

    //каре
    if ($count1 == 3) {
        $count1 = 1e+14;
    }
    //фулл хаус
    else if ($count1 == 2 and $count2 == 1) {
        $count1 = 1e+12;
        $count2 = 1e+10;
    }
    //тройка
    else if ($count1 == 2 and $count2 == 0) {
        $count1 = 1e+6;
    }
    //2 пары
    else if ($count1 == 1 and $count2 == 1) {
        $count1 = 1e+4;
        $count2 = 1e+2;
    }
    //пара
    else if ($count1 == 1 and $count2 == 0) {
        $count1 = 1e+2;
    }

    $result = $match1 * $count1 + $match2 * $count2;// $match1 и $match2 будут равны 0, если совпадений не было
    return $result;
}

Окончательный расчет

Теперь у нас имеются функции, которые проверяют массив карт на стрит, флеш, стрит флеш и парные комбинации. Вызову их вместе и посчитаю окончательный результат, не забываю также и про расчет старшей карты.

Расчет старшей карты:

1. Определю наибольшую и наименьшие по приоритетности карманные карты. Для этого сравню их остатками от деления на 100 (%100). 2. К наибольшей по приоритетности карте добавлю наименьшую по приоритетности деленную на 100.

function priority(array  $arrayCards) {
    //здесь я определяю старшую карту
    if($arrayCards[5] % 100 > $arrayCards[6] % 100) { //условились, что две последние карты массива - карманные
        $highCard1 = $arrayCards[5] % 100;
        $highCard2 = $arrayCards[6] % 100;
    } else {
        $highCard1 = $arrayCards[6] % 100;
        $highCard2 = $arrayCards[5] % 100;
    }


    $flush = pokerFlush($arrayCards); //вызываю функцию для расчета флеша
    $straight = straight($arrayCards); //вызываю функцию для расчета стрита
    $couple = couple($arrayCards); //вызываю функцию для расчета пар

    //далее определяю результат согласно приоритету комбинаций
    if($flush >= 1e+16) {
        $result = $flush; //стрит флеш
    } else if($couple >= 1e+14 and $couple < 1e+16) {
        $result = $couple; //каре
    } else if($couple >= 1e+12 and $couple < 1e+14) {
        $result = $couple; //фулл хаус
    } else if($flush >= 1e+10) {
        $result = $flush; //флеш
    } else if($straight >= 1e+8 and $straight < 1e+10) {
        $result = $straight; //стрит
    } else if($couple >= 1e+6 and $couple < 1e+8) {
        $result = $couple; //тройка
    } else if($couple >= 1e+4 and $couple < 1e+6) {
        $result = $couple; //две пары
    } else if($couple >= 1e+2 and $couple < 1e+4) {
        $result = $couple; //пара
    } else {
        $result = $highCard1 + $highCard2 * 1e-2; //старшая карта
    }
    return $result;
}

О выводе на экран

Выводила я результат в браузер, используя вот такой спрайт (брала картинку с сайта: fondhristianin.ru/?p=2941), как background-image для div.

Ширину и высоту для div задавала равными размеру одной карты, т.е. если размер изображения 572px*328px (как в данном случае), а в ширину количество карт равно 13, в высоту — 5, ширину и высоту задавала 44px*65.6px. Далее меняла background-position с учетом ранее сформированного массива карта.

Расчет background-position для любого из divов по оси х:

100/12 * ($arrayCards[$i] % 100 - 2)

где $arrayCards — ранее сформированный массив. $i — порядковый номер карты.

Пояснения к расчету: В ряду 13 карт, начало 1-ой карты — 0%, начало 13-ой карты — 100%, поэтому разница позиций — 100% / 12 (а не 13). Приводим числа карт из вида [2-14, 102-114, 202-214, 302-314] к [0-12]. Для этого возьмет остаток от числа карты и отнимем от остатка 2. Умножим полученное число на разницу позиций.

Расчет background-position для любого из divов по оси y:

100/4 * floor($arrayCards[$i] / 100)

Пояснение к расчету: В ряду 5 карт, начало 1-ой карты — 0%, начало 5-ой карты — 100%, поэтому разница позиций — 100% / 4 (а не 5). Приводим числа карт из вида [2-14, 102-114, 202-214, 302-314] к [0-3]. Для этого разделим число карты на 100 и округлим в меньшую сторону (можно использовать и простую операцию округления, она сработает аналогичным образом). Умножим полученное число на разницу позиций.