เกม Python Risk

Sep 12 2020

ฉันแก้ไขคำถามเกี่ยวกับเกมความเสี่ยง (http://www.pyschools.com/quiz/view_question/s3-q12) ได้สองวิธี แต่ในขณะที่ฉันกำลังเรียนรู้ Python ฉันแค่สงสัยว่าจะให้คะแนนวิธีการแก้คำถามนี้อย่างไร เช่นเดียวกับวิธีใดต่อไปนี้เป็นวิธีที่ดีกว่ามีประสิทธิภาพและประสิทธิผลในการแก้ปัญหานี้ หรืออาจจะมีแนวทางที่ดีกว่าที่เคยทำ?

นี่คือคำแนะนำของเกม: "ในเกมกระดาน Risk มีสถานการณ์ที่ผู้โจมตีทอยลูกเต๋า 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) ดีกว่าที่จะใช้สัญกรณ์เดียว โดยทั่วไปควรใช้ขีดล่างใน 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))\$.

7 FMc Sep 13 2020 at 01:46

คุณมีคำตอบที่ดีอยู่แล้ว แทนที่จะเน้นที่รายละเอียดของโค้ดของคุณฉันจะเสนอความคิดเห็นเกี่ยวกับวิธีการออกแบบโปรแกรมเต็มรูปแบบ ฉันไม่ทราบสถานการณ์ที่แน่นอนของคุณ แต่เมื่อคุณกำลังเรียนรู้โปรแกรมบรรทัดคำสั่งเป็นจุดโฟกัสที่ดีด้วยเหตุผลเชิงปฏิบัติหลายประการดังนั้นฉันจะใช้เป็นภาพประกอบ

หลักในการทำงานและเปลือกความจำเป็น ให้มากที่สุดเท่าที่จะเป็นไปได้ให้แยกโปรแกรมของคุณออกเป็นฟังก์ชันสองประเภทอย่างเคร่งครัด: (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:])
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ตัวแปรตัวใดตัวหนึ่งและถามว่าควรอ่านloseหรือไม่

ฉันชอบแนวทางที่สองของคุณมาก ในขณะที่อ่านมันฉันรู้สึกว่าโค้ดบรรทัดถัดไปกำลังทำในสิ่งที่ฉันคาดหวังว่ามันจะเป็นจริงและฉันไม่จำเป็นต้องใช้ความจำทางจิตมากนักในขณะที่อ่านมันเหมือนกับที่ฉันทำตามแนวทางแรกของคุณคอยติดตามสิ่งเหล่านั้นทั้งหมด ตัวแปรที่ตั้งค่าเป็นศูนย์ วิธีหนึ่งที่คุณอาจพิจารณาปรับปรุงได้คือการลบa_scrและการd_scrเริ่มต้นและใช้ a 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 ================================================================

ขอให้สนุกกับการทดสอบ!