Python-Risikospiel

Sep 12 2020

Ich habe eine Risikospielfrage gelöst (http://www.pyschools.com/quiz/view_question/s3-q12) auf zwei Arten, aber da ich gerade Python lerne, frage ich mich nur, wie ich meine Methoden zur Lösung dieser Frage bewerten soll. Wie ist eine dieser Möglichkeiten wirklich eine bessere, effektivere und effizientere Methode, um diese Frage zu lösen? Oder gibt es einen besseren Ansatz als den, den ich gemacht habe?

Dies ist die Spielanweisung: "Im Risiko-Brettspiel gibt es die Situation, in der der Angreifer 3 Würfel würfelt, während der Verteidiger 2 Würfel würfelt. Um das Ergebnis zu bestimmen, wird der höchste Würfel jedes Spielers verglichen, gefolgt vom nächsthöheren Würfel Für jeden Fall muss der Würfel des Angreifers höher sein als der des Verteidigers, um zu gewinnen. Der Verlierer verliert jeweils 1 Armee. "

Dies ist das Beispiel der Funktion und der return-Anweisung:

    >>> 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.' 

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

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

Antworten

13 Marc Sep 12 2020 at 18:40

Gute Implementierung für beide Methoden, wenige Vorschläge:

  • Kamel Fall oder Unterstriche. Die Funktion RiskGameverwendet die Kamelschreibweise, die Variablen jedoch die Unterstrichnotation ( a_score). Verwenden Sie besser nur eine Notation. Im Allgemeinen werden in Python Unterstriche bevorzugt.
  • Nicht verwendete Variablen : a_scoreund d_scorein Methode 1.
  • Variablennamen können verbessert werden : a_scrkönnen umbenannt werden in attacker_score. Diese Aussage a=max(attacker)könnte attacker_max_number=max(attacker)oder ähnlich sein. Auch wenn es länger ist, ist der Code leichter zu lesen.
  • Geben Sie das Ergebnis anstelle einer für Menschen lesbaren Zeichenfolge zurück. Es ist einfacher, es wiederzuverwenden und zu testen. Zum Beispiel anstelle von:
    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)
    
    Geben Sie das Ergebnis direkt zurück:
    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)
    #...
    

Welche Methode ist effizienter?

Angesichts der Anforderungen von maximal drei Elementen für die Eingabeliste gibt es keine Leistungsprobleme. In der Tat, wie @Jasmijn in den Kommentaren hervorhebt, unter der Bedingung, dass 1 <= len(attacker) <= 3und 1 <= len(defender) <= 2die zeitliche Komplexität beider Methoden \ ist$O(1)\$.

Wenn die Eingabelisten viele Elemente enthalten und die Liste des Angreifers größer ist als die Liste des Verteidigers, würde ich sagen, dass Methode 2 schneller ist. Die zeitliche Komplexität von Methode 1 wäre \$O(d*a)\$wo \$d\$ist die Länge der Liste des Verteidigers und \$a\$ist die Länge der Liste des Angreifers. Selbst wenn die Listen bei jeder Iteration kleiner werden, können wir dies für große Eingaben sagen. Methode 2 wäre \$O(a*log(a))\$unter der Annahme, dass die zip()Funktion in \ ausgeführt wird$O(d)\$und das Sortieren der Liste des Angreifers dauert \$O(a*log(a))\$.

7 FMc Sep 13 2020 at 01:46

Sie haben bereits einige gute Antworten. Anstatt mich auf die Details Ihres Codes zu konzentrieren, werde ich einige Kommentare dazu abgeben, wie Sie sich dem Design eines vollständigen Programms nähern können. Ich kenne Ihre genaue Situation nicht, aber wenn Sie lernen, sind Befehlszeilenprogramme aus verschiedenen praktischen Gründen ein guter Schwerpunkt, daher werde ich dies als Illustration verwenden.

Funktionskern und Imperativschale . Trennen Sie Ihr Programm so weit wie möglich streng in zwei Arten von Funktionen: (A) diejenigen, die nichts anderes tun, als Daten zu nehmen und Daten zurückzugeben (der Kern); und (B) diejenigen, die Nebenwirkungen haben (die Schale). Was zählt als Nebenwirkung? Viele Dinge, aber Drucken und Ausnahmen sind zwei der häufigsten. Die Kernidee hier ist, dass sich der größte Teil Ihres Programms (und insbesondere seine algorithmischen, rechnerischen Details) im Funktionskern befinden sollte. Die äußere Hülle, die sich mit Drucken und Interaktivität befasst, sollte so dünn und nicht algorithmisch wie möglich sein. Im folgenden Beispiel main()handelt es sich um die äußere Hülle, die aus algorithmischer Sicht mickrig ist - nichts weiter als eine Kleinigkeit if-else. Wenn Sie mehr über diese Ideen erfahren möchten, ist das beste Gespräch, das ich über diese Kern-Schale-Trennung gesehen habe, von Gary Bernhardt .

Mach es kaputt . Sobald Sie diese grundlegende Trennung im Auge haben, beginnen Sie, die erforderlichen Berechnungen in relativ kleine Teile zu zerlegen. Diese Anstrengung ist Teil Kunst, Teil Wissenschaft. Wie weit Sie die Zersetzung verfolgen sollten, hängt vom Kontext ab. Das folgende Beispiel geht ziemlich weit. Je komplexer das Programm und je höher der Einsatz, desto ernsthafter möchten Sie den Zerlegungsaufwand nehmen. In einfacheren Situationen sind verschiedene Verknüpfungen in Ordnung. Aber wenn das Programm wichtig ist, müssen Sie Tests dafür schreiben, und die Anforderungen an die Testbarkeit führen häufig dazu, dass Sie sich mehr zersetzen (es kann schwierig sein, Funktionen zu testen, die zu viele Dinge gleichzeitig ausführen, und es ist ein großes Problem, sie zu testen Funktionen, die schwerwiegende Nebenwirkungen haben).

Beachten Sie die Einfachheit, die sich ergibt . Die Funktionen sind klein, leicht zu verstehen und in einem Kommentar für den Leser schnell zu beschreiben. In den meisten Situationen überwiegen diese Vorteile (oftmals erheblich) die zusätzlichen Kosten für die zusätzliche Zeit, um die Dinge auseinanderzubrechen.

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

Der erste ändert die Eingabe, was schlecht ist, es sei denn, es soll. Sie können dies beheben, indem Sie Kopien der angegebenen Listen erstellen und stattdessen daran arbeiten.

Die String - Formatierung ist nicht erforderlich, da die Werte immer 2, 2oder (1, 1)ist. Gibt uns auch die Möglichkeit für einen guten Schreibstil, indem wir die Zahlen als Wörter und nicht als Ziffern schreiben. Ja, mir ist klar, dass der Problemsteller Ziffern benötigt, also ist es ihre Schuld, nicht deine. Ich sage nur. Sie könnten argumentieren, dass Ihre allgemeiner sind, falls mehr Würfel verwendet wurden, aber dann sollten Sie auch Singular / Plural entsprechend verwenden, um zu vermeiden, etwas wie "Angreifer verliert 5 Armee" zu sagen.

Schließlich würde ich eine Seite auswählen. Das heißt, zählen Sie nur die Statistik einer Seite. Ich werde mit dem Angreifer gehen, da dies die "aktive" Partei ist (im Gegensatz dazu, dass Verteidigung eine Reaktion ist ).

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

Wenn dies eine echte PR wäre, würde ich eine der *looseVariablen hervorheben und fragen, ob dies lesen solllose .

Ihr zweiter Ansatz gefällt mir sehr gut. Während ich es las, hatte ich das Gefühl, dass die nächste Codezeile genau das tat, was ich erwartet hatte, und ich musste beim Lesen nicht so viel mentales Gedächtnis verwenden wie bei Ihrem ersten Ansatz, um all diese zu verfolgen Variablen, die auf Null gesetzt wurden. Ein Weg , man bedenkt , könnte die Verbesserung der es ist , die zu entfernen a_scrund d_scrInitialisierungen und verwenden sum()sie zu berechnen, wie zum Beispiel:

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

Ich habe ähnliche Vorbehalte gegen das Pascal-Gehäuse der Funktionen und die Rückgabe von Zeichenfolgen anstelle von Werten, aber diese werden in den anderen Antworten gut behandelt.

Bearbeiten: Demonstrieren Sie die Verwendung von Tests

Dieser Teil behandelt eine weitere Möglichkeit, um sicherzustellen, dass Ihre Beispielschlachten mithilfe von pytest systematisch korrekt ausgeführt werden. Es zeigt, warum einige andere Vorschläge der anderen Antworten zum Drucken der Funktion das Codieren erleichtern.

Schritt 1.

Lassen Sie Ihre Hauptfunktion einfach a_scrund zurückgeben d_scrund die Drucklogik in das Hauptskript einfügen:

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

Wenn Sie das Skript ausführen, erhalten Sie das gleiche Verhalten wie zuvor:

$ python riskgame.py 
Attacker loses 2 armies.

Schritt 2.

Erstellen test_wins_and_losses.pyund erstellen Sie im selben Verzeichnis einige 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

Beachten Sie, dass ich dieselben Werte verwendet habe, die Sie in den ursprünglichen Beitrag eingegeben haben, aber Sie könnten dort alle gewünschten Beispielspiele haben. Im Idealfall hätten Sie viele, die so viele Anwendungsfälle wie möglich abdecken.

Schritt 3.

Installieren Sie pytest, falls Sie dies noch nicht getan haben.

$ pip install pytest

Schritt 4.

Starte es!

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

Die Idee ist jetzt, dass Sie Ihren Code ändern können und jedes Mal, wenn Sie dies tun, einfach pytestin die Befehlszeile eingeben können, um zu bestätigen, dass alles noch so funktioniert, wie Sie es erwarten . Zum Beispiel, wenn wir den Fehler machen, den ich zuvor gemacht habe, und die Zeile in ändern

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

und führen Sie die Tests durch, wir erhalten:

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

Viel Spaß beim Testen!