Jeu Python Risk
J'ai résolu une question sur le jeu de risque (http://www.pyschools.com/quiz/view_question/s3-q12) de deux manières, mais comme j'apprends actuellement Python, je me demande simplement comment évaluer mes méthodes pour résoudre cette question. Par exemple, lequel de ces moyens est vraiment une méthode meilleure, efficace et efficiente pour résoudre cette question? Ou peut-être y a-t-il une meilleure approche que ce que j'ai fait?
Voici l'instruction de jeu: "Dans le jeu de plateau Risk, il y a la situation où l'attaquant lance 3 dés tandis que le défenseur lance 2 dés. Pour déterminer le résultat, le dé le plus élevé de chaque joueur est comparé, suivi du dé le plus élevé suivant. . Pour chaque cas, le dé de l'attaquant doit être plus élevé que celui du défenseur pour gagner. Le perdant perdra 1 armée dans chaque cas. "
Voici l'exemple de la fonction et de l'instruction return:
>>> 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.'
Méthode 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])
Méthode 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])
Réponses
Belle implémentation pour les deux méthodes, quelques suggestions:
- Cas de chameau ou soulignements. La fonction
RiskGameutilise la notation de casse camel mais les variables utilisent la notation de soulignement (a_score). Mieux vaut n'utiliser qu'une seule notation. Généralement, les traits de soulignement sont préférés en Python. - Variables inutilisées :
a_scoreetd_scoredans la méthode 1. - Les noms de variables peuvent être améliorés :
a_scrpeuvent être renommés enattacker_score. Cette déclarationa=max(attacker)pourrait êtreattacker_max_number=max(attacker)ou similaire. Même si c'est plus long, rend le code plus facile à lire. - Renvoyez le résultat au lieu d'une chaîne lisible par l'homme, il est plus facile à réutiliser et à tester. Par exemple au lieu de:
renvoie directement le résultat: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) #...
Quelle méthode est la plus efficace?
Il n'y a aucun problème concernant les performances étant donné les exigences de trois éléments maximum pour la liste d'entrée. En fait, comme le souligne @Jasmijn dans les commentaires, à condition que 1 <= len(attacker) <= 3et 1 <= len(defender) <= 2, la complexité temporelle des deux méthodes soit \$O(1)\$.
Si les listes d'entrée contiennent de nombreux éléments et que la liste de l'attaquant est plus grande que la liste du défenseur, je dirais que la méthode 2 est plus rapide. La complexité temporelle de la méthode 1 serait \$O(d*a)\$où \$d\$est la longueur de la liste des défenseurs et \$a\$est la longueur de la liste de l'attaquant. Même si les listes rétrécissent à chaque itération, on peut dire que pour les gros intrants. La méthode 2 serait \$O(a*log(a))\$, en supposant que la zip()fonction s'exécute dans \$O(d)\$et trier la liste des attaquants prend \$O(a*log(a))\$.
Vous avez déjà de bonnes réponses. Plutôt que de me concentrer sur les détails de votre code, je proposerai quelques commentaires sur la manière d'aborder la conception d'un programme complet. Je ne connais pas votre situation exacte, mais lorsque vous apprenez, les programmes de ligne de commande sont un bon point focal pour diverses raisons pratiques, donc je vais utiliser cela comme une illustration.
Noyau fonctionnel et coque impérative . Autant que possible, séparez strictement votre programme en deux types de fonctions: (A) celles qui ne font que prendre des données et renvoyer des données (le noyau); et (B) ceux qui ont des effets secondaires (la coquille). Qu'est-ce qui compte comme effet secondaire? Beaucoup de choses, mais l'impression et les exceptions sont deux des plus courantes. L'idée centrale ici est que la plupart de votre programme (et en particulier ses détails algorithmiques et de calcul) doivent résider dans le noyau fonctionnel. L'enveloppe externe concernée par l'impression et l'interactivité doit être aussi fine et non algorithmique que possible. Dans l'exemple ci-dessous, main()est la coquille externe et elle est chétive d'un point de vue algorithmique - rien de plus qu'un trivial if-else. Si vous voulez en savoir plus sur ces idées, le meilleur discours que j'ai vu sur cette séparation noyau-coquille est de Gary Bernhardt .
Décomposez-le . Une fois que vous avez cette séparation fondamentale à l'esprit, commencez à décomposer les calculs nécessaires en parties raisonnablement petites. Cet effort fait partie de l'art et de la science. Dans quelle mesure vous devez poursuivre la décomposition dépend du contexte. L'exemple ci-dessous va assez loin. Plus le programme est complexe, et plus les enjeux sont élevés, plus vous voulez prendre l'effort de décomposition au sérieux. Dans des situations plus simples, divers raccourcis conviennent. Mais lorsque le programme est important, vous devez écrire des tests pour lui, et les exigences de testabilité vous pousseront souvent à vous décomposer davantage (il peut être difficile de tester des fonctions qui font trop de choses à la fois, et c'est un gros casse-tête de tester fonctions qui ont des effets secondaires majeurs).
Remarquez la simplicité qui s'en dégage . Les fonctions finissent par être petites, faciles à comprendre, rapides à décrire dans un commentaire pour le lecteur. Dans la plupart des situations, ces avantages l'emportent (souvent de manière significative) sur les coûts supplémentaires liés à la prise de temps supplémentaire pour briser les choses.
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:])
Le premier modifie l'entrée, ce qui est mauvais sauf si c'est censé le faire. Vous pouvez résoudre ce problème en faisant des copies des listes données et en y travaillant à la place.
Le formatage de chaîne n'est pas nécessaire, car les valeurs sont toujours 2, 2ou (1, 1), respectivement. Nous donne également la possibilité d'avoir un bon style d'écriture, en écrivant les nombres sous forme de mots et non de chiffres. Oui, je me rends compte que les chiffres sont requis par le poseur de problèmes, donc c'est sa faute, pas la vôtre. Juste dire. Vous pourriez argumenter que les vôtres sont plus généraux, au cas où plus de dés seraient utilisés, mais alors vous devriez aussi utiliser le singulier / pluriel de manière appropriée, pour éviter de dire quelque chose comme "l'attaquant perd 5 armée".
Enfin, je choisirais un côté. Autrement dit, ne comptez que la statistique d'un côté. J'irai avec l'attaquant, car c'est le parti «actif» (par opposition à la défense étant une réaction ).
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.'
S'il s'agissait d'un vrai PR, je mettrais en évidence l'une des *loosevariables et demanderais si cela devait être lulose ?
J'aime beaucoup votre deuxième approche. En le lisant, j'avais l'impression que la prochaine ligne de code faisait exactement ce que je m'attendais à ce qu'elle fasse, et je n'ai pas eu à utiliser autant de mémoire mentale en la lisant que j'ai fait votre première approche, en gardant une trace de tout cela variables définies sur zéro. Une façon vous pourriez envisager de l' améliorer est d'enlever les a_scret d_scrinitialisations et utiliser sum()pour les calculer, tels que:
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])
J'ai des réserves similaires sur le boîtier Pascal des fonctions et le retour de chaînes au lieu de valeurs, mais celles-ci sont bien couvertes dans les autres réponses.
Edit: montrer comment utiliser les tests
Cette partie couvre une autre façon de garantir que vos exemples de batailles se déroulent correctement de manière systématique à l'aide de pytest. Cela illustre pourquoi certaines autres suggestions des autres réponses pour obtenir l'impression de la fonction facilitent le codage.
Étape 1.
Demandez à votre fonction principale de retourner a_scret d_scr, et de mettre la logique d'impression dans le script principal:
# 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))
Lorsque vous exécutez le script, vous obtenez le même comportement qu'auparavant:
$ python riskgame.py
Attacker loses 2 armies.
Étape 2.
Dans le même répertoire, créez test_wins_and_losses.pyet créez des tests:
# 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
Remarquez que j'ai utilisé les mêmes valeurs que vous avez mises dans le message d'origine, mais vous pouvez avoir tous les exemples de jeux que vous voulez. Idéalement, vous en auriez beaucoup, couvrant autant de cas d'utilisation que possible.
Étape 3.
Installez pytest si vous ne l'avez pas déjà fait.
$ pip install pytest
Étape 4.
Exécuter!
$ 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 ===============================
L'idée est maintenant que vous pouvez changer votre code, et chaque fois que vous le faites, vous pouvez simplement taper pytestsur la ligne de commande pour confirmer que tout fonctionne toujours comme vous le souhaitez . Par exemple, si nous faisons l'erreur que j'ai faite plus tôt et changeons la ligne en
d_scr = sum([i < j for i, j in zip(a,b)])
et exécutez les tests, nous obtenons:
$ 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 ================================================================
Bon test!