Игра Python Risk
Решил вопрос о риск-игре (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])
Ответы
Хорошая реализация для обоих методов, несколько предложений:
- Верблюжий регистр или символы подчеркивания. Функция
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))\$.
У вас уже есть хорошие ответы. Вместо того, чтобы сосредотачиваться на деталях вашего кода, я предложу несколько комментариев о том, как подходить к проектированию полной программы. Я не знаю вашей конкретной ситуации, но когда вы учитесь, программы командной строки являются хорошим фокусом по разным практическим причинам, поэтому я буду использовать это в качестве иллюстрации.
Функциональное ядро и императивная оболочка . Насколько это возможно, строго разделите вашу программу на два типа функций: (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:])
Первый изменяет ввод, что плохо, если не должно. Вы можете исправить это, сделав копии данных списков и вместо этого работая над ними.
В форматировании строки нет необходимости, поскольку значения всегда равны 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.'
Если бы это был настоящий пиар, я бы выделил одну из *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 ================================================================
Удачного тестирования!