Juego de Python Risk

Sep 12 2020

Resolví una pregunta del juego de riesgo (http://www.pyschools.com/quiz/view_question/s3-q12) de dos maneras, pero como actualmente estoy aprendiendo Python, me pregunto cómo calificar mis métodos para resolver esta pregunta. ¿Cuál de estas formas es realmente un método mejor, efectivo y eficiente para resolver esta pregunta? ¿O tal vez hay un enfoque mejor que el que hice?

Esta es la instrucción del juego: "En el juego de mesa de Riesgo, existe la situación en la que el atacante lanza 3 dados mientras el defensor lanza 2 dados. Para determinar el resultado, se compara el dado más alto de cada jugador, seguido del siguiente dado más alto . Para cada caso, el dado del atacante tiene que ser más alto que el del defensor para ganar. El perdedor perderá 1 ejército en cada caso ".

Esta es la muestra de la función y la declaración de retorno:

    >>> 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étodo 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étodo 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])

Respuestas

13 Marc Sep 12 2020 at 18:40

Buena implementación para ambos métodos, algunas sugerencias:

  • Caso camel o guiones bajos. La función RiskGameusa notación de mayúsculas y minúsculas, pero las variables usan notación de subrayado ( a_score). Es mejor usar solo una notación. Generalmente, se prefieren los guiones bajos en Python.
  • Variables no utilizadas : a_scorey d_scoreen el Método 1.
  • Los nombres de las variables se pueden mejorar : se a_scrpuede cambiar el nombre a attacker_score. Esta declaración a=max(attacker)podría ser attacker_max_number=max(attacker)o similar. Incluso si es más largo, hace que el código sea más fácil de leer.
  • Devuelve el resultado en lugar de una cadena legible por humanos, es más fácil de reutilizar y probar. Por ejemplo en lugar de:
    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)
    
    devuelve el resultado directamente:
    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)
    #...
    

¿Qué método es más eficaz?

No hay problemas con las actuaciones dados los requisitos de un máximo de tres elementos para la lista de entrada. De hecho, como @Jasmijn señala en los comentarios, con la condición de que 1 <= len(attacker) <= 3y 1 <= len(defender) <= 2, la complejidad temporal de ambos métodos sea \$O(1)\$.

Si las listas de entrada tienen muchos elementos y la lista del atacante es más grande que la lista del defensor, yo diría que el Método 2 es más rápido. La complejidad de tiempo del Método 1 sería \$O(d*a)\$donde \$d\$es la longitud de la lista de defensores y \$a\$es la longitud de la lista de atacantes. Incluso si las listas se reducen en cada iteración, podemos decir que para grandes entradas. El método 2 sería \$O(a*log(a))\$, asumiendo que la zip()función se ejecuta en \$O(d)\$y ordenar la lista de atacantes lleva \$O(a*log(a))\$.

7 FMc Sep 13 2020 at 01:46

Ya tienes algunas buenas respuestas. En lugar de centrarme en los detalles de su código, ofreceré algunos comentarios sobre cómo abordar el diseño de un programa completo. No conozco su situación exacta, pero cuando está aprendiendo, los programas de línea de comandos son un buen punto focal por varias razones prácticas, así que lo usaré como ilustración.

Núcleo funcional y caparazón imperativo . Tanto como sea posible, separe estrictamente su programa en dos tipos de funciones: (A) aquellas que no hacen nada más que tomar datos y devolver datos (el núcleo); y (B) los que tienen efectos secundarios (el caparazón). ¿Qué cuenta como efecto secundario? Muchas cosas, pero la impresión y las excepciones son dos de las más comunes. La idea central aquí es que la mayor parte de su programa (y especialmente sus detalles algorítmicos y computacionales) deben residir en el núcleo funcional. La capa externa relacionada con la impresión y la interactividad debe ser lo más delgada y no algorítmica posible. En el siguiente ejemplo, main()es la capa exterior y es insignificante desde una perspectiva algorítmica, nada más que trivial if-else. Si desea obtener más información sobre estas ideas, la mejor charla que he visto sobre esta separación núcleo-capa es de Gary Bernhardt .

Romperlo . Una vez que tenga en mente esa separación fundamental, comience a descomponer los cálculos necesarios en partes razonablemente pequeñas. Este esfuerzo es en parte arte, en parte ciencia. Hasta dónde debe buscar la descomposición depende del contexto. El siguiente ejemplo lo lleva bastante lejos. Cuanto más complejo sea el programa y cuanto más en juego, más en serio querrá tomar el esfuerzo de descomposición. En situaciones más simples, varios atajos están bien. Pero cuando el programa es importante, necesita escribir pruebas para él, y las demandas de capacidad de prueba a menudo lo llevarán a descomponer más (puede ser difícil probar funciones que hacen demasiadas cosas a la vez, y es un gran dolor de cabeza probar funciones que tienen efectos secundarios importantes).

Note la simplicidad que emerge . Las funciones acaban siendo pequeñas, fáciles de entender, rápidas de describir en un comentario para el lector. En la mayoría de las situaciones, esos beneficios superan (a menudo significativamente) los costos adicionales de tomarse el tiempo adicional para separar las cosas.

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

El primero cambia la entrada, lo cual es malo a menos que se suponga que lo haga. Puede solucionarlo haciendo copias de las listas dadas y trabajando en ellas.

El formato de cadenas es innecesaria, ya que los valores son siempre 2, 2o (1, 1), respectivamente. También nos da la oportunidad de tener un buen estilo de escritura, escribiendo los números como palabras, no como dígitos. Sí, me doy cuenta de que el autor del problema requiere los dígitos, por lo que es su culpa, no la suya. Solo digo. Podrías argumentar que los tuyos son más generales, en caso de que se usen más dados, pero también debes usar singular / plural de manera apropiada, para evitar decir algo como "El atacante pierde 5 ejércitos".

Finalmente, elegiría un lado. Es decir, solo cuente la estadística de un lado. Iré con el atacante, ya que esa es la parte "activa" (en lugar de que la defensa sea una reacción ).

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

Si esto fuera un PR real, destacaría una de las *loosevariables y preguntaría ¿Debería leerse estolose ?

Realmente me gusta tu segundo enfoque. Mientras lo leía, sentí que la siguiente línea de código estaba haciendo exactamente lo que estaba anticipando que haría, y no tuve que usar tanta memoria mental mientras lo leía como lo hice en su primer enfoque, haciendo un seguimiento de todos esos variables que se establecieron en cero. Una forma es posible considerar que la mejora es eliminar los a_scry d_scrlas inicializaciones y utilizar un sum()calcularlos, tales como:

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])

Tengo reservas similares sobre el uso de mayúsculas y minúsculas en Pascal de las funciones y la devolución de cadenas en lugar de valores, pero estas se tratan muy bien en las otras respuestas.

Editar: demuestre cómo usar las pruebas

Esta parte cubre otra forma de garantizar que tus batallas de ejemplo se ejecuten correctamente de manera sistemática usando pytest. Ilustra por qué algunas otras sugerencias de las otras respuestas para obtener la impresión de la función facilitan la codificación.

Paso 1.

Haga que su función principal simplemente regrese a_scry d_scr, y coloque la lógica de impresión en el 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))

Cuando ejecuta el script, obtiene el mismo comportamiento que antes:

$ python riskgame.py 
Attacker loses 2 armies.

Paso 2.

En el mismo directorio crea test_wins_and_losses.pyy crea algunas pruebas:

# 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

Observe que usé los mismos valores que puso en la publicación original, pero podría tener cualquier juego de ejemplo que desee allí. Idealmente, tendría muchos, cubriendo tantos casos de uso como sea posible.

Paso 3.

Instale pytest si aún no lo ha hecho.

$ pip install pytest

Etapa 4.

¡Ejecutarlo!

$ 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 ===============================

La idea es que ahora puede cambiar su código, y cada vez que lo haga, simplemente escriba pytesten la línea de comandos para confirmar que todo sigue funcionando como espera . Por ejemplo, si cometemos el error que cometí antes y cambiamos la línea a

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

y ejecutamos las pruebas, obtenemos:

$ 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 ================================================================

¡Feliz prueba!