Игра Python Risk

Sep 12 2020

Решил вопрос о риск-игре (http://www.pyschools.com/quiz/view_question/s3-q12) двумя способами, но поскольку я сейчас изучаю Python, мне просто интересно, как оценить мои методы решения этого вопроса. Например, какой из этих способов действительно является лучшим, эффективным и действенным методом решения этого вопроса? Или, может быть, есть подход лучше, чем я?

Это инструкция к игре: «В настольной игре« Риск »есть ситуация, когда атакующий бросает 3 кубика, а защитник бросает 2 кубика. Чтобы определить результат, сравнивается самый высокий кубик каждого игрока, за которым следует следующий самый высокий кубик. . В каждом случае для победы кубик атакующего должен быть больше, чем кубик защищающегося. Проигравший потеряет 1 армию в каждом случае ».

Это образец функции и оператора возврата:

    >>> RiskGame([6,2,6], [6, 6])
    'Attacker loses 2 armies.'
    >>> RiskGame([1,4,1], [1, 2])
    'Attacker loses 1 army and defender loses 1 army.' 

Способ 1

def RiskGame(attacker, defender):
    a_score = 0
    a_loose = 0
    d_score = 0
    d_loose = 0
    for e in range(len(defender)):
        a= max(attacker)
        d= max(defender)
        
        if a>d:
            a_score +=1
            d_loose +=1
        
        else:
            d_score +=1
            a_loose +=1
        attacker.remove(a)
        defender.remove(d)
    if a_loose == 0:
        return 'Defender Loses %i armies.' %d_loose
    elif d_loose == 0:
        return 'Attacker loses %i armies.' %a_loose
    else:
        return 'Attacker loses %i army and defender loses %i army.' %(a_loose, d_loose)
            
RiskGame([1,2,6], [1, 5])
RiskGame([1,4,1], [1, 2])
RiskGame([6,2,6], [6, 6])

Способ 2

def RiskGame(attacker, defender):
    a = sorted(attacker, reverse=True)
    b = sorted(defender, reverse=True)
    
    a_scr =0
    d_scr =0
    pairs = zip(a,b)
    for i,j in pairs:
        if i>j:
            a_scr +=1
        else:
            d_scr +=1
    if d_scr == 0:
        return 'Defender loses %i armies.' %a_scr
    elif a_scr == 0:
        return 'Attacker loses %i armies.' %d_scr
    else:
        return 'Attacker loses %i army and defender loses %i army.' %(a_scr, d_scr)

RiskGame([1,2,6], [1, 5])
RiskGame([1,4,1], [1, 2])
RiskGame([6,2,6], [6, 6])

Ответы

13 Marc Sep 12 2020 at 18:40

Хорошая реализация для обоих методов, несколько предложений:

  • Верблюжий регистр или символы подчеркивания. Функция RiskGameиспользует обозначение верблюжьего регистра, но переменные используют обозначение подчеркивания ( a_score). Лучше использовать только одно обозначение. Как правило, в Python предпочтительнее подчеркивание.
  • Неиспользуемые переменные : a_scoreи d_scoreв методе 1.
  • Имена переменных можно улучшить : a_scrможно переименовать в attacker_score. Это утверждение a=max(attacker)могло быть attacker_max_number=max(attacker)или похожим. Даже если он длиннее, код будет легче читать.
  • Вернуть результат вместо удобочитаемой строки, это проще использовать повторно и тестировать. Например вместо:
    def RiskGame(attacker, defender):
        #...
        if d_scr == 0:
            return 'Defender loses %i armies.' %a_scr
        elif a_scr == 0:
            return 'Attacker loses %i armies.' %d_scr
        else:
            return 'Attacker loses %i army and defender loses %i army.' %(a_scr, d_scr)
    
    вернуть результат напрямую:
    def RiskGame(attacker, defender):
        #...
        return attacker_score, defender_score
    
    attacker_score, defender_score = RiskGame([1,2,6], [1, 5])
    if defender_score == 0:
        print('Defender Loses %i armies.' %attacker_score)
    #...
    

Какой метод более эффективен?

Нет проблем с производительностью, учитывая требования не более трех элементов для входного списка. Фактически, как отмечает @Jasmijn в комментариях, при условии, что 1 <= len(attacker) <= 3и 1 <= len(defender) <= 2, временная сложность обоих методов равна \$O(1)\$.

Если входные списки содержат много элементов и список злоумышленника больше, чем список защитника, я бы сказал, что метод 2 быстрее. Временная сложность метода 1 будет \$O(d*a)\$где \$d\$длина списка защитников и \$a\$длина списка злоумышленников. Даже если списки сжимаются на каждой итерации, мы можем сказать это для больших входных данных. Метод 2 будет \$O(a*log(a))\$, предполагая, что zip()функция работает в \$O(d)\$и сортировка списка злоумышленников занимает \$O(a*log(a))\$.

7 FMc Sep 13 2020 at 01:46

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

Функциональное ядро ​​и императивная оболочка . Насколько это возможно, строго разделите вашу программу на два типа функций: (A) те, которые ничего не делают, кроме приема данных и возврата данных (ядро); и (B) те, которые имеют побочные эффекты (оболочка). Что считается побочным эффектом? Многое, но наиболее распространенными являются печать и исключения. Основная идея здесь заключается в том, что большая часть вашей программы (и особенно ее алгоритмические, вычислительные детали) должны находиться в функциональном ядре. Внешняя оболочка, связанная с печатью и интерактивностью, должна быть как можно более тонкой и неалгоритмической. В приведенном ниже примере main()это внешняя оболочка, и она ничтожна с алгоритмической точки зрения - не более чем тривиально if-else. Если вы хотите узнать больше об этих идеях, лучший доклад, который я видел об этом разделении ядра и оболочки, принадлежит Гэри Бернхардту .

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

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

import sys

def main(args):
    # Command-line usage example: `python risk_game.py 3,4,6 3,5`
    attacker, defender, error = parse_entries(args)
    if error:
        print(error)
        sys.exit(1)
    else:
        message = risk_game(attacker, defender)
        print(message)

def parse_entries(entries):
    # Takes attacker and defender entries. Returns a 3-tuple: (ATTACKER-ROLLS,
    # DEFENDER-ROLLS, ERROR-MESSAGE). There are more featureful and robust ways
    # to handle this; adjust as needed.
    try:
        return (parse_entry(entries[0]), parse_entry(entries[1]), None)
    except Exception as e:
        return (None, None, 'Invalid entry')

def parse_entry(entry):
    # Parses a single entry and returns a list of dice rolls.
    return [int(val) for val in entry.split(',')]

def risk_game(attacker, defender):
    # Takes two lists of dice rolls. Returns a message describing the outcome.
    score = compute_battle_score(attacker, defender)
    return generate_message(attacker, defender, score)

def compute_battle_score(attacker, defender):
    # Takes two lists of dice rolls. Returns a battle score.
    atts = sorted(attacker, reverse = True)
    defs = sorted(defender, reverse = True)
    return sum(1 if a > d else -1 for a, d in zip(atts, defs))

    # Or if you need to know N of victories for each combatant.
    return collections.Counter(a > d for a, d in zip(atts, defs))

def generate_message(attacker, defender, score):
    # Make it as fancy as you want.
    return f'Attacker score: {score}'

if __name__ == '__main__':
    main(sys.argv[1:])
5 superbrain Sep 12 2020 at 21:06

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

В форматировании строки нет необходимости, поскольку значения всегда равны 2, 2или (1, 1)соответственно. Также дает нам возможность хорошо писать, записывая числа словами, а не цифрами. Да, я понимаю, что разработчикам задач требуются цифры, так что это их вина, а не ваша. Просто говорю. Вы можете возразить, что ваши более общие, если было использовано больше кубиков, но тогда вы также должны использовать единственное / множественное число соответственно, чтобы не говорить что-то вроде «Атакующий теряет 5 армий».

Наконец, я бы выбрал сторону. То есть учитывайте статистику только одной стороны. Я пойду с атакующим, так как это «активная» сторона (в отличие от защиты, являющейся реакцией ).

def RiskGame(attacker, defender):
    _, a1, a2 = sorted(attacker)
    d1, d2 = sorted(defender)
    wins = (a2 > d2) + (a1 > d1)
    if wins == 2:
        return 'Defender loses two armies.'
    elif wins == 0:
        return 'Attacker loses two armies.'
    else:
        return 'Attacker loses one army and defender loses one army.'
2 user1717828 Sep 13 2020 at 08:06

Если бы это был настоящий пиар, я бы выделил одну из *looseпеременных и спросил, следует ли это читатьlose ?

Мне очень нравится твой второй подход. Читая его, я чувствовал, что следующая строка кода делает именно то, что я ожидал, и мне не нужно было использовать столько умственной памяти при чтении, как я сделал ваш первый подход, отслеживая все эти переменные, которые были установлены в ноль. Один из способов , вы могли бы рассмотреть вопрос об улучшении это удалить a_scrи d_scrинициализацию и использовать sum()для их вычислений, таких как:

def RiskGame(attacker, defender):
    a = sorted(attacker, reverse=True)
    b = sorted(defender, reverse=True)
    
    a_scr = sum([i > j for i, j in zip(a,b)])
    d_scr = sum([i < j for i, j in zip(a,b)])

    if d_scr == 0:
        return 'Defender loses %i armies.' %a_scr
    elif a_scr == 0:
        return 'Attacker loses %i armies.' %d_scr
    else:
        return 'Attacker loses %i army and defender loses %i army.' %(a_scr, d_scr)

RiskGame([1,2,6], [1, 5])

У меня есть аналогичные оговорки в отношении оболочки Pascal для функций и возврата строк вместо значений, но они хорошо освещены в других ответах.

Изменить: продемонстрировать, как использовать тесты

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

Шаг 1.

Попросите вашу основную функцию просто вернуть a_scrи d_scrпоместить логику печати в основной скрипт:

# riskgame.py
def risk_game(attacker, defender):
    a = sorted(attacker, reverse=True)
    b = sorted(defender, reverse=True)

    a_scr = sum([i > j for i, j in zip(a,b)])
    d_scr = sum([i <= j for i, j in zip(a,b)])

    return a_scr, d_scr

if __name__ == "__main__":
    a_scr, d_scr = risk_game([6,2,6], [6, 6])

    if d_scr == 0:
        print('Defender loses %i armies.' %a_scr)
    elif a_scr == 0:
        print('Attacker loses %i armies.' %d_scr)
    else:
        print('Attacker loses %i army and defender loses %i army.' %(a_scr, d_scr))

Когда вы выполняете сценарий, вы получаете то же поведение, что и раньше:

$ python riskgame.py 
Attacker loses 2 armies.

Шаг 2.

В этом же каталоге создайте test_wins_and_losses.pyи создайте несколько тестов:

# test_wins_and_losses.py
from riskgame import risk_game
  
def test_d_wins():
    a_scr, d_scr = risk_game([1,2,6], [1, 5])
    assert a_scr == 2
    assert d_scr == 0

def test_a_wins():
    a_scr, d_scr = risk_game([6,2,6], [6, 6])
    assert a_scr == 0
    assert d_scr == 2

def test_equal():
    a_scr, d_scr = risk_game([1,4,1], [1, 2])
    assert a_scr == 1
    assert d_scr == 1

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

Шаг 3.

Установите pytest, если вы еще этого не сделали.

$ pip install pytest

Шаг 4.

Запустить его!

$ pytest
============================= test session starts ==============================
platform linux -- Python 3.7.4, pytest-6.0.2, py-1.9.0, pluggy-0.13.1
rootdir: /tmp/risk_game
collected 3 items                                                              

test_wins_and_losses.py ...                                              [100%]

============================== 3 passed in 0.02s ===============================

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

d_scr = sum([i < j for i, j in zip(a,b)])

и запускаем тесты, получаем:

$ pytest
==================================================================== test session starts =====================================================================
platform linux -- Python 3.7.4, pytest-6.0.2, py-1.9.0, pluggy-0.13.1
rootdir: /tmp/risk_game
collected 3 items                                                                                                                                            

test_wins_and_losses.py .FF                                                                                                                            [100%]

========================================================================== FAILURES ==========================================================================
________________________________________________________________________ test_a_wins _________________________________________________________________________

    def test_a_wins():
        a_scr, d_scr = risk_game([6,2,6], [6, 6])
        assert a_scr == 0
>       assert d_scr == 2
E       assert 0 == 2

test_wins_and_losses.py:11: AssertionError
_________________________________________________________________________ test_equal _________________________________________________________________________

    def test_equal():
        a_scr, d_scr = risk_game([1,4,1], [1, 2])
        assert a_scr == 1
>       assert d_scr == 1
E       assert 0 == 1

test_wins_and_losses.py:16: AssertionError
================================================================== short test summary info ===================================================================
FAILED test_wins_and_losses.py::test_a_wins - assert 0 == 2
FAILED test_wins_and_losses.py::test_equal - assert 0 == 1
================================================================ 2 failed, 1 passed in 0.09s ================================================================

Удачного тестирования!