Проект «Свинья»

Введение

Цель этого проекта — разработать симулятор и несколько стратегий для игры в кости под названием «Свинья». Тебе придётся использовать сочетание управляющих инструкций и функций высшего порядка.

Правила

При игре в «Свинью» два игрока по очереди совершают ходы, стараясь набрать 100 очков. Ход состоит в том, что игрок выбирает некоторое количество бросков костей (не более 10). За свой ход игрок получает сумму выпавших на костях чисел.

Для того чтоб играть было поинтереснее, приправим игру особыми правилами:

  • Обжора. Если хотя бы на одной кости выпадает 1, то игрок за ход получает 1 очко.

    • Пример 1: Игрок бросил семь костей, на пяти из них выпала «единица» — результат хода 1 очко.

    • Пример 2: Игрок бросил четыре кости, на всех выпало «три» — игрок заработал 12 очков.

  • Халявный бекон. Игрок в свой ход может решить вовсе не бросать кости, тогда он получит 10 очков за вычетом минимума от цифр десятков и единиц текущего счёта противника. Если цифры десятков нет, считай, что это ноль.

    • Пример 1: У противника 13 очков, игрок решил не бросать кости. Результат хода 10 - min(1, 3) = 9 очков.

    • Пример 2: У противника 85 очков, игрок решил не бросать кости. Результат хода 10 - min(8, 5) = 5 очков.

    • Пример 3: У противника 7 очков, игрок решил не бросать кости. Результат хода 10 - min(0, 7) = 10 очков.

  • Мохнатые хрюшки. Если выбранное игроком количество костей в текущем ходе отличается на два по сравнению с количеством костей в предыдущем ходе игрока, к результату хода добавляется три очка.

    • Пример 1:

      • Оба игрока начинают с нуля. (0, 0)

      • Игрок 0 бросает 3 кости и получает 1 очко. (1, 0)

      • Игрок 1 бросает 4 кости и получает 1 очко. (1, 1)

      • Игрок 0 бросает 5 костей и получает 1 очко. Поскольку 5 отличается на 2 от 3, к результату прибавляется 3 очка. (5, 1)

      • Игрок 1 бросает 2 кости и получает 1 очко. Поскольку 2 отличается на 2 от 4, к результату прибавляется 3 очка. (5, 5)

      • Игрок 0 бросает 7 костей и получает 1 очко. Поскольку 7 отличается на 2 от 5, к результату прибавляется 3 очка. (9, 5)

      • Игрок 1 бросает не бросает кости и получает 10 очков. Поскольку 0 отличается на 2 от 2, к результату прибавляется 3 очка. (9, 18)

    • Пример 2:

      • Оба игрока начинают с нуля. (0, 0)

      • Игрок 0 бросает 2 кости и получает 3 очка. Поскольку 2 отличается на 2 от 0, к результату прибавляется 3 очка. (6, 0)

  • Свиной переворот. Правило срабатывает после прибавления к очкам игрока результата хода. Если произведение крайних цифр очков игрока равно произведению крайних цифр очков противника, то очки игрока и противника меняются местами. (Считай, что количество очков занимает не более трёх разрядов).

    • Пример 1: После завершения хода счёт игры 2:4. Поскольку 2 * 2 != 4 * 4, очки не переворачиваются.

    • Пример 2: После завершения хода счёт игры 22:4. Поскольку 2 * 2 != 4 * 4, очки не переворачиваются.

    • Пример 3: После завершения хода счёт игры 28:4. Поскольку 2 * 8 == 4 * 4, очки переворачиваются, теперь счёт 4:28.

    • Пример 4: После завершения хода счёт игры 124:2. Поскольку 1 * 4 == 2 * 2, очки переворачиваются, теперь счёт 2:124.

    • Пример 5: После завершения хода счёт игры 44:28. Поскольку 4 * 4 == 2 * 8, очки переворачиваются, теперь счёт 28:44.

    • Пример 6: После завершения хода счёт игры 2:0. Поскольку 2 * 2 != 0 * 0, очки не переворачиваются.

    • Пример 7: После завершения хода счёт игры 10:0. Поскольку 1 * 0 == 0 * 0, очки переворачиваются, теперь счёт 0:10.

Файлы

Проект включает несколько файлов и один директорий с изображениями граней кости:

hog.py

Заготовка игры в Свинью (тебе нужно изменять только этот файл).

dice.py

Функции, имитирующие бросок кости.

hog_gui.py

Графический интерфейс для игры в Свинью.

images/

Директорий с изображениями для hog_gui.py.

calc.py

Алгоритм оценки стратегии.

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

Логистика

Этот проект может принести тебе 22 балла. Строго рекомендуется выполнять проект в парах, однако, если ты чувствуешь силу, то можно попробовать сделать его в одиночку. Делиться кодом внутри пары можно, делиться кодом с остальными нельзя.

Все решения должны быть представлены в файле hog.py. Это единственный файл, который тебе придётся изменять. Остальные трогать не нужно. Однако чтение их кода весьма полезно.

В требующих дополнения функциях может находиться начальный код. Если он тебе не нравится — можешь полностью стереть тело функции и начать с чистого листа. Мы не против, если потребуется, объявления вспомогательных функций.

Однако, пожалуйста не изменяй другие (готовые) функции. Эти действия могут привести к ошибками как в игре, так и в проверке результатов. Также не меняй сигнатуры функций (имя функции, порядок и число аргументов).

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

После завершения работы над каждым вопросом рекомендуется фиксировать изменения (делать коммит), сопровождая их вменяемым описанием сделанного, например:

$ git add .
$ git commit -m "Решение вопроса X"

Так же не возбраняется в любое время проталкивать накопленные локально изменения на GitHub:

$ git push

Начинай как можно раньше. Сколько именно у тебя уйдёт времени для выполнения проекта предугадать нельзя. Не теряй его.

Если что-то не идёт — сразу проси подсказку в чатике.

Графический интерфейс

Для тебя подготовлен графический интерфейс, или кратко GUI (graphical user interface). Сейчас он ещё не работает, поскольку игровая логика частично отсутствует. После завершения работы над функцией play ты сможешь поиграть в Свинью в интерактивном режиме!

Для работы графического интерфейса требуется графическая библиотека Tkinter. Проверь, что она установлена.

Для запуска GUI набери в терминале:

python3 hog_gui.py

После завершения всего проекта ты сможешь поиграть против твоей же финальной стратегии:

python3 hog_gui.py -f

Часть 1: Симулятор

В первой части ты разработаешь симулятор игры в Свинью.

Задача 1 (2 балла)

Дополни функцию roll_dice (бросок костей) в файле hog.py. Она принимает два аргумента: положительное целое число бросков кости num_rolls и функцию без аргументов dice, которая возвращает результат броска отдельной кости.

Функция roll_dice должна возвращать количество очков, получаемое при бросании num_rolls раз кости dice — это может быть либо сумма результатов, либо 1 (смотри правило Обжоры).

Для получения результата броска одной кости вызови dice(). Тебе необходимо сделать в точности num_rolls таких вызовов, даже если ты уже точно знаешь, что сработает правило Обжоры, — не забывай, что ты делаешь симуляцию одновременного броска нескольких костей.

Ты не сможешь на этом этапе реализовать правило Мохнатых хрюшек, поскольку roll_dice «не знает», сколько костей было передано при прошлом вызове. Оставь это на потом.

Для того, чтобы удостовериться, что решение работает, можешь запустить интерпретатор в интерактивном режиме:

python3 -i hog.py

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

>>> roll_dice(4)

Каждый раз это выражение будет приводить к разным результатам, что не очень удобно для тестов. Существует возможность использовать тестовую кость, которая будет всегда возвращать детерминированные значения. Например, бросив дважды тестовую кость с предопределенными результатами 3 и 4 ты получишь 7 очков за бросок.

>>> fixed_dice = make_test_dice(3, 4)
>>> roll_dice(2, dice=fixed_dice)
7
На большинстве компьютеров поддерживается история ввода команд. До предыдущих команд можно доступиться нажав стрелочку вверх.

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

$ python3 -m doctest hog.py
Не забудь сделать коммит.

Задача 2 (1 балл)

Создай вспомогательную функцию free_bacon для правила Халявный бекон, которая должна возвращать количество зарабатываемых очков при броске нуля костей на основании счёта противника score. Считай, что score меньше 100. Для значений меньших 10 бери старший разряд равным 0.

Не забудь проверить корректность работы в интерактивном режиме, добавить доктестов и коммитнуть результат.

Задача 3 (2 балла)

Создай функцию take_turn, которая возвращает количество очков, полученных за один ход заданной костью, заданным числом бросков.

Реализация должна поддерживать правило Халявный бекон. Считай, что величина opponent_score (очки противника) меньше 100.

Должны быть использованы функции roll_dice и free_bacon.

Ничто не мешает проверить работу в интерактивном режиме.

Задача 4 (2 балла)

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

Поиск самой левой цифры можно сделать с помощью большого количества инструкций if, но лучше используй цикл while.

Ты уже знаешь как протестировать и закоммитить. Вперёд!

Задача 5a (2 балла)

Создай функцию play, которая полностью эмулирует игру в Свинью. Игроки последовательно совершают ходы, пока один из них не наберёт goal очков.

Пока что можешь игнорировать правило Мохнатых хрюшек и не использовать аргумент feral_hogs.

Для того, чтобы определить, сколько костей бросать в свой ход, каждый игрок придерживается стратегии (Игрок 0 использует strategy0, а Игрок 1 — strategy1). Стратегия — это функция, в которую передаются текущие очки игроков, на основании которых определяется количество костей для текущего хода. Каждая стратегия должна вызываться только один раз за ход. Думать о создании стратегии сейчас рано, займешься этим в Части 3.

По завершении игры функция play возвращает набранные игроками очки — сначала очки Игрока 0, затем Игрока 1.

Ниже приведены некоторые подсказки:

  1. Используй уже созданные функции! Вызывать take_turn нужно со всеми тремя аргументами.

  2. Вызывай take_turn только один раз за ход.

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

  4. Получать идентификатор противника можно из идентификатора игрока с помощью предоставляемой функции other.

  5. Сейчас можно игнорировать аргумент say функции play. Этот аргумент потребуется в Части 2.

Тесты и коммит!

Задача 5b (1 балл)

Теперь настало время учесть правило Мохнатых хрюшек. Если при вызове play аргумент feral_hogs равен True, то правило должно включаться. Если же feral_hogs равен False, то правило должно быть проигнорировано. (Это механизм независимого тестирования решения 5a от 5b).

Тест этого вопроса предполагает прогон множества игр. Если эта задача не проходит проверку, возможно следует вернуться в предыдущим задачам и проверить всё ещё раз.

После решения этой задачи ты можешь запустить графическую версию игры. Среди прочих файлов есть hog_gui.py, который можно запустить так:

python3 hog_gui.py

Графический интерфейс требует наличия установленной библиотеки Tkinter. Если она отсутствует, её следует поставить.

Графическая версия игры базируется на твоей реализации, то есть если у тебя есть ошибки в коде, то они будут заметны и в графическом интерфейсе. Стало быть, ты можешь использовать GUI для отладки ошибок, однако всё же лучше использовать доктесты.

Поздравляю! Первая часть Проекта № 1 завершена! Коммит и пуш!

Часть 2: Комментарии

Во второй части ты будешь разрабатывать функции комментирования игры, задача которых — сообщать после каждого хода о результатах, например «22 очка(ов)! Лучший результат для Игрока 1».

Функция комментирования принимает два аргумента: текущий счет Игрока 0 и текущий счет Игрока 1. Она может распечатать комментарий на основании этих данных, а, возможно, даже и на предыдущих. Поскольку в зависимости от текущей ситуации со счётом в игре комментарий может отличаться от хода к ходу, функция комментирования возвращает другую функция комментирования, которая будет вызвана на следующем ходу. Единственным побочным действием функции комментирования должен быть вывод на печать print.

Примеры

Функция say_scores из hog.py — пример функции комментирования, которая просто сообщает текущий счёт обоих игроков. Заметь, что функция say_scores возвращает саму себя. Это означает, что на следующем ходу будет вызвана она же.

def say_scores(score0, score1):
    """Сообщает текущий счёт каждого игрока."""
    print("Игрок 0 набрал", score0, "очков, а Игрок 1 набрал", score1)
    return say_scores

Функция announce_lead_changes — пример функции высшего порядка, которая возвращает функцию комментирования, сообщающую об изменении лидера в игре.

def announce_lead_changes(previous_leader=None):
    """Возвращает функцию, которая сообщает о смене лидера.

    # >>> f0 = announce_lead_changes()
    # >>> f1 = f0(5, 0)
    # Игрок 0 вырвался вперёд на 5
    # >>> f2 = f1(5, 12)
    # Игрок 1 вырвался вперёд на 7
    # >>> f3 = f2(8, 12)
    # >>> f4 = f3(8, 13)
    # >>> f5 = f4(15, 13)
    # Игрок 0 вырвался вперёд на 2
    """
    def say(score0, score1):
        if score0 > score1:
            leader = 0
        elif score1 > score0:
            leader = 1
        else:
            leader = None
        if leader != None and leader != previous_leader:
            print('Игрок', leader, 'вырвался вперёд на', abs(score0 - score1))
        return announce_lead_changes(leader)
    return say

Также тебе надо подразобраться с функцией both, которая принимает две функции комментирования f и g и возвращает новую функцию комментирования. Эта возвращаемая функция комментирования возвращает другую функцию комментирования, которая вызывает функции, возвращаемые из f и g по порядку.

def both(f, g):
    """Выводит два сообщения — первое с помощью f, второе с помощью g.

    NOTE: Следующие примеры не могут иметь место в реальной игре, это
    просто доктесты.

    # >>> h0 = both(say_scores, announce_lead_changes())
    # >>> h1 = h0(10, 0)
    # Игрок 0 набрал 10 очков, а Игрок 1 набрал 0
    # Игрок 0 вырвался вперёд на 10
    # >>> h2 = h1(10, 6)
    # Игрок 0 набрал 10 очков, а Игрок 1 набрал 6
    # >>> h3 = h2(6, 17)
    # Игрок 0 набрал 6 очков, а Игрок 1 набрал 17
    # Игрок 1 вырвался вперёд на 11
    """
    def say(score0, score1):
        return both(f(score0, score1), g(score0, score1))
    return say

Задача 6 (2 балла)

Дополни функцию play так, чтобы функция комментирования вызывалась в конце каждого хода. Возвращаемое значение функции комментирования — это функция комментирования, которую нужно вызывать в следующий ход.

Например, функция say_score(score0, score1) должна вызываться в конце первого хода. Её возвращаемое значение (другая функция комментирования) должна быть вызвана после второго хода. Каждый ход должна вызываться функция комментирования, полученная на предыдущем ходе.

Стоит ли говорить о тестах и коммитах?

Задача 7 (3 балла)

Создай функцию высшего порядка announce_highest, которая возвращает функцию комментирования. В случае, когда игрок за ход набирает наибольшее количество очков за все свои ходы, эта функция комментирования объявляет об этом факте. Чтобы вычислить количество полученных за ход очков, она должна сравнить счет игрока после последнего хода со счетом этого же игрока после текущего хода. Игрок «закодирован» аргументом who. Эта функция также должна отслеживать наибольший выигрыш за ход для игрока.

Способ, которым announce_highest сообщает о событии весьма специфичен. Так что твой код должен как-то пройти представленные доктесты. Беспокоиться о склонении числительных не нужно, считай, что «2 очков» нормально.

Внимательно изучи функцию announce_lead_changes. Она приведена для примера того, как следует хранить информацию в функциях комментирования. Если решение не приходит в голову, проверь, что точно понимаешь, как работает announce_lead_changes.
Доктесты для функций both и announce_highest описывают игру, которая противоречит правилам. Это неважно, поскольку функции комментирования никак не влияют на логику игры.

Если ты получаешь ошибку local variable [var] reference before assignment, то это происходит от того, что Python не может изменять значения, связанные с именами из старших фреймов. При присвоении существующему в старшем фрейме имени интерпретатор считает, что это не присвоение, а создание локального имени в текущем фрейме, что ему, конечно, не нравится. Через несколько недель ты узнаешь, как делать такие присвоения, а пока что попробуй вот что:

  1. Вместо переприсвоения переменной [var] нового значения создай другую переменную и храни значение в ней для дальнейших вычислений.

  2. Конкренто для этой задачи — избегай такой ситуации, то есть не изменяй/добавляй переменные, а вместо этого используй встроенную функцию для вычисления желаемого значения прямо при создании новой функции комментирования.

После завершения работы ты увидишь комментарии к игре в графическом интерфейсе.

python3 hog_gui.py

Комментарии в графическом интерфейсе формируются с помощью вот такой составной функции комментирования:

both(announce_highest(0), both(announce_highest(1), announce_lead_changes()))

Отличная работа! Часть 2 закончена! Коммит!

Часть 3

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

Задача 8 (2 балла)

Разработай функцию высшего порядка make_averaged, которая принимает функцию fn в качестве аргумента и возвращает другую функцию, принимающую такое же количество аргументов, как и fn. Эта возвращённая функция отличается от исходной тем, что она возвращает среднее значение результата многократного вызова fn с одними и теми же аргументами. Усреднение проводится по num_samples вызовам fn.

Для создания такой функции надо познакомиться с новой и тебе ещё незнакомой синтаксической конструкцией Python! Ведь нужно написать функцию, которая принимает произвольное количество аргументов, а затем вызывает другую функцию с этим же произвольным набором аргументов. Вот как это работает.

Вместо перечисления всех формальных параметров через запятую ты можешь написать *args. Для вызова другой функции с тем же набором аргументов ты можешь сделать вызов с *args. Например:

>>> def printed(fn):
...     def print_and_return(*args):
...         result = fn(*args)
...         print('Результат:', result)
...         return result
...     return print_and_return
>>> printed_pow = printed(pow)
>>> printed_pow(2, 8)
Результат: 256
256
>>> printed_abs = printed(abs)
>>> printed_abs(-10)
Результат: 10
10

Внимательно прочитай докстринг функции make_averaged, чтоб разобраться в её поведении.

Тесты! Код! Тесты! Код! Тесты! Коммит! Пуш!

Задача 9 (2 балла)

Создай функцию max_scoring_num_rolls, которая запускает эксперимент по определению числа бросков (от 1 до 10), которое приведет в среднем к максимальному количеству очков за ход. Тебе нужно использовать функции make_averaged и roll_dice.

Если найдены два числа бросков, которые приводят к максимальному среднему количеству баллов за ход, возвращай меньшее. То есть, если и 3 броска, и 6 бросков позволяют в среднем достигнуть максимального количества очков, то надо вернуть 3.

Не забудь протестировать и закоммитить решение.

Для запуска этого эксперимента с честной (случайной) костью, нужно вызвать run_experiments с помощью опции -r:

python3 hog.py -r

Выполнение экспериментов. При дальнейшем выполнении проекта ты можешь изменять код функции run_experiments как хочешь. Вызывая average_win_rate ты можешь оценить различные стратегии игры в «Свинью». Например, измени первый if False: на if True:, чтобы сравнить стратегию always_roll(6) со стратегией always_roll(4).

Некотоые эксперименты могут длиться довольно долго (до минуты). Ты можешь уменьшить заданное количество экспериментов в функции make_averaged.

Задача 10 (1 балл)

Стратегия может получить преимущество Халявного бекона, выбирая ноль бросков, когда это наиболее выгодно.

Реализуй стратегию bacon_strategy, которая возвращает 0, если такой выбор даст по крайней мере margin очков, в противном случае возвращает num_rolls.

Не забудь проверить своё решение с помощью тестов.

После создания стратегии измени run_experiments для оценки новой стратегии против базовой (baseline). Ну как, лучше она, чем просто выбрасывать по 4 кости?

Задача 11 (2 балла)

Стратегия может также использовать преимущества правила Свиного переворота. Функция swap_strategy всегда возвращает ноль бросков, если сработает переворот и он будет выгодным. Избегает нуля, если переворот будет невыгодным. В других случаях выбрасывает ноль, если это принесёт по крайней мере margin очков. Иначе возвращает num_rolls.

В случае, если переворот происходит в состоянии равенства очков, то он и не выгодный, и не невыгодный.

Проверка тестами — это просто и полезно.

После создания стратегии swap_strategy измени run_experiments для сравнения её с базовой (baseline) стратегией. Вероятно ты обнаружишь, что она существенно превосходит always_roll(4).

Задача 12 (0 баллов)

Создай свою стратегию final_strategy, которая сочетает все предложеные и любые другие идеи превосходства. Добейся, чтобы доля побед была устойчиво больше базовой стратегии always_roll(4).

Некоторые идеи:

  • Стратегия swap_strategy хорошая исходная стратегия.

  • Тебе нужно всего 100 очков. Если победа близка, ты можешь победить с помощью 0, 1 или 2 костей.

  • Если ты лидируешь, принимай меньшие риски.

  • Попробуй использовать выгодный переворот, бросив больше 0 костей.

  • Аккуратно выбирай num_rolls и margin.

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

python3 calc.py

Поздравляю! Проект закончен, теперь можешь расслабиться и насладиться игрой в Свинью с твоим новым другом — стратегией final_strategy:

python3 hog_gui.py -f

Коммит и пуш!