בעיה י"ב

הוראות המופיעות בגוף של מבנה while ומתבצעות כל עוד תנאי הלולאה מתקיים צריכות להיות מוזזות ימינה ביחס לשורה הראשונה במבנה. הקוד התקין:

age = input(‘Insert age; -1 to stop: ‘)

while age != ‘-1’:

    print(age)

    age = input(‘Insert age; -1 to stop: ‘)

 

כתבו את הפונקציה game –

def game():

לפונקציה אין פרמטרים.  

הפונקציה תממש גרסה של המשחק “אבן, נייר ומספריים”. במימוש זה המחשב משחק מול שחקנית אחת או יותר. 

בכל סיבוב וסיבוב המחשב וגם שחקנית אחת בוחרים חפץ אחד משלשת חפצים זו: אבן, נייר או מספריים. החפצים מיוצגים בפונקציה באמצעות התווים ‘e’, ‘n’ ו-‘m’:  התו ‘e’ מייצג אבן, התו ‘n’ מייצג נייר, והתו ‘m’ מייצג מספריים.

כללי המשחק: נייר גובר על אבן, אבן גוברת על מספריים, מספריים גוברים על נייר. בחירה בחפץ זהה פירושה תיקו. 

לכל סיבוב של המשחק יש ארבעה שלבים, לפי סדר זה –

(א) השחקנית תתבקש להכניס את שמה ותעשה זאת. אם השם הוא ‘end’ המשחק מסתיים, ואם הוא אינו ‘end’ הסיבוב ממשיך לשלביו הבאים. 

(ב) השחקנית תתבקש לבחור חפץ באמצעות הכנסת אחד מהתווים ‘e’, ‘n’ או ‘m’, ותעשה זאת. 

(ג) המחשב יבחר חפץ. לצורך הבחירה מוגרל באופן אקראי מספר: 0, 1, או 2, והמספר שהוגרל משמש בתור אינדקס אקראי של תו במחרוזת “enm” – תו זה הוא החפץ של המחשב. לדוגמה אם הוגרל המספר 2, אז החפץ של המחשב הוא ‘m’ (התו באינדקס 2 במחרוזת “enm”), כלומר מספריים. 

(ד) לבסוף ייקבע מי ניצח. אפשר להניח שהמחשב ניצח אלא אם כן בדיקה מגלה שהשחקנית היא שניצחה. 

כל שחקנית יכולה להשתתף ביותר מסיבוב אחד של המשחק. אפשר להניח שאין שתי שחקניות ששמן זהה. 

כאמור המשחק מסתיים כאשר שחקנית מכניסה בתור שמה את המחרוזת “end”.

בסוף המשחק הפונקציה תחזיר מילון. במילון המוחזר כל זוג יכיל נתונים בנוגע לשחקנית אחת ששחקה במשחק, ויהיה בנוי כך: מפתח: שם השחקנית; ערך: רשימה שיש בה שני ערכים אלה: (1) מספר המשחקים שהשחקנית שיחקה בהם, (2) מספר המשחקים שנצחה. 

דוגמת הרצה:

Please enter name: Dana

Please enter item: e

Please enter name: Dana

Please enter item: n

Please enter name: Dana

Please enter item: n

Please enter name: Yona

Please enter item: e

Please enter name: end

לפי הרצה זו המילון המוחזר מהפונקציה game הוא –

{‘Dana’: [3, 1], ‘Yona’: [1, 0]}

כלומר: דנה שיחקה ב-3 משחקים, ונצחה ב-1 מהם. יונה השתתפה במשחק 1, והיא לא נצחה את המחשב.

פתרון

בבעיה ז’ כתבנו פונקציה המממשת משחק שבו שחקניות משחקות זו עם זו. הפונקציה שיש לכתוב בפתרון הבעיה הנוכחית מממשת משחק שבו שחקניות משחקות מול המחשב. כפי שנראה במימוש המשחק משולבות שתי פעולות שדנו בהם בפתרונות לבעיות קודמות: יצירת מילון שכיחויות ויצירת מילון שבו כל ערך הוא בעצמו אוסף ועדכון הערכים-האוספים האלה. עם זה הפעם העדכון של הערכים-האוספים יהיה מעט שונה מזה שראינו. 

כתיבת הפתרון לבעיה זו מורכבת משני היבטים: טיפול בניהול המשחק, ויצירת המילון המכיל את תוצאות המשחק. נתחיל בניהול המשחק. מובן כי תהיה כאן לולאה – הרי למשחק יש סיבובים שמהלכם זהה. בנוסף טבעי שהקוד יממש את הלולאה באמצעות מבנה while, כיוון שבבעיה צוין במפורש שהמשחק ממשיך כל עוד אין מוכנסת המחרוזת “end” בשלב הכנסת שם השחקנית. לפי תובנות אלו נוכל להציע שלד לקוד (עדיין לא בתוך פונקציה), כדלקמן –

name = input(“Please enter name: “)

while (name != “end”):

. . . 

     name = input(“Please enter name: “)

לולאה זו תמשיך להסתובב כל עוד בתחילת הסיבוב השחקנית אינה מכניסה את המחרוזת ‘end’. נעיר כי לפי המבנה הזה של לולאת ה-while הכנסת השם הבא נעשית בסוף הסיבוב ולא בתחילתו כפי שהוסבר בתיאור המשחק. לפי שעה נשאיר את הקוד כפי שהוא ובסוף הדיון נראה כיצד לקלוט את השם תמיד בתחילת הסיבוב. 

בגוף הלולאה עלינו לטפל בבחירה של המחשב – אבן, נייר, או מספריים, וכן בבחירה של השחקנית המשחקת מול המחשב בסיבוב הנוכחי. בחירתה של השחקנית תיקלט מה-console בתור מחרוזת, למשל כך –

player = input(“Please enter item: “)

לפי ההסבר בשאלה, הבחירה של המחשב יכולה להיות ממומשת כך –

computer = “enm”[random.randint(0, 2)]

על המחרוזת “enm” הפעלנו את האופרטור [ ] והנחנו בין הסוגריים המרובעים אינדקס שנבחר אקראית מהטווח 0 עד 2 (כולל שניהם). כך במשתנה computer הוצבה באופן אקראי המחרוזת ‘e’ או המחרוזת ‘n’ או המחרוזת ‘m’. שימו לב שהשימוש בפונקציה randint של המודול random מחייב להקדים ולייבא את המודול הזה לתכנית, באמצעות הוראת import (כך ייעשה בקוד המלא של הפונקציה שיוצג להלן). 

לאחר שידועות הבחירה של המחשב והבחירה של המשתמשת, עלינו להחליט, לפי כללי המשחק המנוסחים בשאלה, מי ניצח בסיבוב, המחשב או השחקנית. בשאלה הוסבר כי אפשר להניח שהמחשב ניצח אלא אם כן נמצא שהמשתמשת נצחה. אם כן נוכל להציע את המימוש הזה –

winner = 0

if ((player == “n”) & (computer == “e”)

    |

    (player == “e”) & (computer == “m”)

    |

    (player == “m”) & (computer == “n”)

    ):

    winner = 1

שימו לב: אין צורך להפריד לשורות את תנאי ה-if וההפרדה נעשתה כאן רק לצורך הבהרת הקוד.

הקוד משתמש במשתנה עזר בשם winner. זה משתנה דגל (flag), כלומר הוא בא לאותת לנו על קיומו או על אי קיומו של מצב עניינים מסוים (ראו גם לעיל, בעיה א’). כאן הוא מאותחל ל-0, ואתחול זה בא לציין שהנחת היסוד היא שהמחשב ניצח. מבנה ה-if בודק אם בכל זאת השחקנית נצחה – יש שלושה מצבים כאלה, לפי המתואר בשאלה: לשחקנית יש נייר ולמחשב אבן, או שלשחקנית יש אבן ולמחשב מספריים, או שלשחקנית יש מספריים ולמחשב נייר. אם השחקנית נצחה מוחלף ערכו של המשתנה winner, כלומר 0, ב-1. הערכים המסמנים כל אחד משני המצבים – המחשב ניצח או השחקנית נצחה – אינם חייבים להיות 0 ו-1, ואינטואטיבית אולי נחשוב שרצוי יותר להשתמש ב-True וב-False. כאן השתמשנו ב-0 וב-1 כיוון שהדבר יועיל להמשך המימוש של הפונקציה, כפי שיתברר מיד. 

אם כן הנה הפונקציה במימושה עד כה –

def game():    

    name = input(“Please enter name: “)

    while name != ‘end’:

        computer = “enm”[random.randint(0, 2)]

        player = input(“Please enter item: “)

        winner = 0

        if ((player == “n”) & (computer == “e”)

            |

            (player == “e”) & (computer == “m”)

            |

            (player == “m”) & (computer == “n”)

            ):

            winner = 1

        name = input(“Please enter name: “)

עד כאן הטיפול בניהול המשחק. עכשיו נפנה לטפל ביצירת המילון שהפונקציה צריכה להחזיר. נשים לב כי מהשאלה עולה כי עלינו לבנות מילון שכיחויות (ראו לעיל, בעיה ח’): יש כאן מספר מסוים של מקרים – ספציפית: מספר מסוים של שחקניות – ועלינו למצוא את השכיחות של כל מקרה – ספציפית: בכמה משחקים כל שחקנית נצחה). מכאן שמפתחות המילון יהיו שמות השחקניות. אמנם הערכים שהמפתחות (השמות) ימופו אליהם אינם שכיחויות (מספרים) כשלעצמן, אלא רשימה המכילה את השכיחויות. בשאלה צוין כי לכל שחקנית עלינו לשמור הן את מספר המשחקים ששחקה בהם הן את מספר המשחקים שנצחה בהם, וגם ניתנה דוגמה למילון שיכול להיות מוחזר מהפונקציה –

{‘Dana’: [3, 1], ‘Yona’: [1, 0]}

מכאן שיש לפנינו בעיה דומה לזו שראינו בבעיה הקודמת (בעיה י”א): יצירת מילון שבו כל ערך הוא בעצמו אוסף ועדכון הערכים-האוספים האלה. 

הנה הגרסה הסופית של הפונקציה, והיא גם כוללת את הקוד המממש את יצירת המילון שהפונקציה מחזירה –

import random

def game():

    d = {}     

    name = input(“Please enter name: “)

    while (name != “end”):

        computer = “enm”[random.randint(0, 2)]

        player = input(“Please enter item: “)

        winner = 0

        if ((player == “n”) & (computer == “e”)

            |

            (player == “e”) & (computer == “m”)

            |

            (player == “m”) & (computer == “n”)

            ):

            winner = 1

        if name not in d.keys():

            d[name] = [1, winner]

        else:                        

            d[name][0] += 1

            d[name][1] += winner

        name = input(“Please enter name: “)

    return d

לפני הלולאה הגדרנו מילון ריק. בגוף הלולאה ובתום הסיבוב הנוכחי של המשחק, לאחר שהתבררה זהות המנצח – השחקנית או המחשב – אנו מכניסים לתוך המילון נתונים לפי תוצאות הסיבוב. כרגיל ביצירת מילון שכיחויות אנו מפרידים בין שני מקרים – 

(1) המקרה האחד הוא שהמפתח עדיין אינו במילון, וכאן: שם השחקנית ששחקה בסיבוב הנוכחי עדיין אינו מופיע בתור מפתח במילון (זה הסיבוב הראשון שבו שיחקה שחקנית זו). אם זה המצב מבוצעת הוראה זו – 

d[name] = [1, winner]

 

ההוראה מוסיפה למילון זוג שהמפתח בו הוא שם השחקנית והערך הוא רשימה המכילה שני ערכים, בסדר זה: 1 – כי זה הסיבוב הראשון שהשחקנית משחקת בו, ותוכן המשתנה winner, כלומר 1 אם בסיבוב זה היא נצחה, או 0 אם לא נצחה. 

(4) המקרה האחר הוא שהמפתח כבר במילון, כלומר שיש במילון זוג שהמפתח בו הוא שם השחקנית ששחקה בסיבוב הנוכחי (דהיינו זה לא הסיבוב הראשון שהיא משחקת). נניח למשל שהשחקנית ששיחקה בסיבוב הנוכחי היא Dana ושהנתונים הנשמרים במילון על אודות השתתפותה במשחק עד כה הם אלה –

“Dana” : [5, 3] 

 

כלומר דנה שחקה בחמישה סיבובים של המשחק ונצחה את המחשב בשלושה מהם. עכשיו עלינו להגדיל את 5 ל-6, ובנוסף להגדיל את 3 ל-4 אך ורק אם היא נצחה במשחק הנוכחי. ההוראות האלה המופיעות בפתרון מבצעות בדיוק את המבוקש – 

d[name][0] += 1

d[name][1] += winner

 

בשתי הוראות אלו ההפעלה הראשונה של האופרטור [ ] היא על מילון, והיא מחזירה את הרשימה שאליה ממופה השם name. ההעלה השניה של האופרטור [ ] היא על הרשימה הזאת: בין הסוגריים המרובעים מונח אינדקס המציין את האינדקס של הערך ברשימה שברצוננו לשנות: באינדקס 0 נמצא מספר המשחקים שהשחקנית שיחקה עד כה, ואותו יש להגדיל ב-1; ובאינדקס 1 נמצא מספר המשחקנים שהשחקנית נצחה בהם עד כה, ואותו יש להגדיל ב-1 או לא לשנותו כלל. כאן תועיל לנו בחירת הערכים שיוצבו במשתנה winner: הואיל ו-winner מכיל 1 אם המשתתפת נצחה בסיבוב הנוכחי ו-0 אם הפסידה למחשב, התוספת של תוכן המשתנה הזה למספר ברשימה המציין את מספר המשחקים שהשחקנית נצחה בהם עד כה (נמצא באינדקס 1 בברשימה) תגדיל ב-1 את המספר הזה או תשאיר אותו כפי שהוא, ממש לפי הנדרש. 

לסיום נשוב להערה שהערנו בהסבר לולאת ה-while המופיעה בפתרון. הנה שוב שלד הלולאה – 

name = input(“Please enter name: “)

while (name != “end”):

. . . 

     name = input(“Please enter name: “)

בגרסה זו של הקוד, הפעם הראשונה ששם השחקנית (או המחרוזת ‘end’) נקלט הוא לפני הלולאה. בכל הסיבובים של הלולאה הקליטה נעשית בסוף הסיבוב. מבנה זה של לולאת while מכונה “קליטה לפני לולאה”. בהקשר הנוכחי השימוש בו במימוש יוצר אי התאמה מסוימת בין הפתרון לניסוח השאלה: הרי לפי פתרון זה, החל מהסיבוב השני של המשחק שמה של השחקנית בסיבוב הנוכחי נקלט בסוף הסיבוב הקודם ולא בתחילת הסיבוב הנוכחי כפי שמתואר בשאלה. נוכל לשכתב את המבנה של לולאת ה-while באופן שקליטת השם תעשה תמיד בתחילת הסיבוב, אם נשתמש במבנה “קליטה לפני לולאה”, למשל כך – 

stop = False

d = {}

while not stop:

    name = input(“Please enter name: “)

    if (name == “end”):

        stop = True

        continue

    . . . 

כפי שאפשר לראות לפני הלולאה לא קלטנו דבר. הפעם הראשונה שנקלט שם של שחקנית היא בהוראה הראשונה בגוף הלולאה. לאור זאת אין אנו יכולים להשתמש בשם שהשחקנית הכניסה כדי לקבוע אם הלולאה צריכה להמשיך להסתובב, כפי שעשינו במימוש הקוד לפי המבנה “קליטה לפני לולאה”. במצב עניינים זה נשתמש במשתנה דגל: כאן שמו stop והוא מאותחל ל-False. הלולאה תמשיך להסתובב כל עוד הוא מכיל False, או, לשם דיוק כל עוד שלילתו היא True. ערכו ישתנה ל-True (ושלילתו ל-False) אם בתחילת הסיבוב יתברר שהשחקנית הכניסה את המחרוזת ‘end’: או אז, נוסף לשינוי ערכו של המשתנה stop, תבוצע ההוראה continue, כל ההוראות הבאות בגוף הלולאה לא תתבצענה, תנאי הלולאה ייבדק ואז יתברר שיש להפסיק את הלולאה. הנה אפוא גרסה של הפתרון המלא במבנה של “לולאה לפני קליטה” –

import random

def game():

    d = {}

    stop = False

    while not stop:

        name = input(“Please enter name: “)

        if (name == “end”):

            stop = True

            continue

        computer = “enm”[random.randint(0, 2)]

        player = input(“Please enter item: “)

        winner = 0

        if ((player == “n”) & (computer == “e”)

            |

            (player == “e”) & (computer == “m”)

            |

            (player == “m”) & (computer == “n”)

            ):

            winner = 1

        if name not in d.keys():

            d[name] = [1, winner]

        else:                        

            d[name][0] += 1

            d[name][1] += winner

    return d

לא מעט מתכנתות ומתכנתים מעדיפים את המבנה “לולאה לפני קליטה” כיוון שהוא אלגנטי יותר בעיניהם ונאמן יותר להגדרה של לולאה, בייחוד כיוון שכל קטע הגוף המורץ בלולאה נמצא אך ורק בגוף הלולאה.