Pythonリスクゲーム

Sep 12 2020

リスクゲームの質問を解決しました(http://www.pyschools.com/quiz/view_question/s3-q12)2つの方法がありますが、現在Pythonを学習しているので、この質問を解決する方法をどのように評価するのか疑問に思っています。これらの方法のどれが、この質問を解決するための本当に優れた、効果的で効率的な方法ですか?それとも、私がやったよりも良いアプローチがあるのでしょうか?

これはゲームの指示です。「リスクボードゲームでは、攻撃者が3個のサイコロを振り、防御側が2個のサイコロを振る状況があります。結果を判断するために、各プレーヤーの最高のサイコロが比較され、次に高いサイコロが続きます。いずれの場合も、攻撃者のダイスは防御者のダイスよりも高くなければなりません。敗者はいずれの場合も1つの軍隊を失います。」

これは、関数と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.' 

方法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)を使用します。1つの表記のみを使用することをお勧めします。一般に、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)
    #...
    

どちらの方法がより効率的ですか?

入力リストに最大3つの要素が必要な場合、パフォーマンスに関する問題はありません。実際には、@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

あなたはすでにいくつかの良い答えを持っています。コードの詳細に焦点を合わせるのではなく、完全なプログラムの設計にアプローチする方法についていくつかコメントします。あなたの正確な状況はわかりませんが、あなたが学んでいるとき、コマンドラインプログラムはさまざまな実用的な理由から良い焦点であるため、それを例として使用します。

関数型コアと命令型シェル。可能な限り、プログラムを2つのタイプの関数に厳密に分けてください。(A)データを取得してデータを返すだけの関数(コア)。(B)副作用のあるもの(シェル)。副作用として何がカウントされますか?多くのことがありますが、印刷と例外は最も一般的な2つです。ここでの中心的な考え方は、プログラムのほとんど(特にそのアルゴリズムや計算の詳細)が機能コアに存在する必要があるということです。印刷と双方向性に関係する外殻は、できるだけ薄く、非アルゴリズム的である必要があります。以下の例でmain()は、は外殻であり、アルゴリズムの観点からはちっぽけです-些細なことif-elseです。これらのアイデアについてもっと知りたい場合は、このコアシェルの分離について私が見た中で最高の話は、GaryBernhardtからです。

それを分解します。その基本的な分離を念頭に置いたら、必要な計算を適度に小さな部分に分解し始めます。この取り組みは、芸術の一部であり、科学の一部です。分解をどこまで追求すべきかは、状況によって異なります。以下の例はかなり遠いです。プログラムが複雑で、賭け金が高いほど、分解の努力を真剣に受け止めたいと思うでしょう。より単純な状況では、さまざまなショートカットで問題ありません。しかし、プログラムが重要な場合は、そのためのテストを作成する必要があり、テスト容易性の要求により、多くの場合、より多くの分解が必要になります(一度に多くのことを実行する関数をテストすることは困難であり、テストするのは大きな頭痛の種です主要な副作用がある機能)。

出現する単純さに注意してください。関数は小さく、理解しやすく、読者へのコメントですばやく説明できます。ほとんどの場合、これらの利点は、物事を分解するために余分な時間をかけるという余分なコストを(多くの場合大幅に)上回ります。

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

これが実際のPRである場合、*loose変数の1つを強調表示して、これを読む必要があるloseかどうかを尋ねます。

私はあなたの2番目のアプローチが本当に好きです。それを読んでいる間、次のコード行は私が期待していたことを正確に実行しているように感じました。最初のアプローチのように読んでいる間、精神的な記憶をあまり使用する必要はなく、それらすべてを追跡していました。ゼロに設定された変数。改善を検討する1つの方法は、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 ================================================================

ハッピーテスト!