בעיה ז'

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

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

while age != ‘-1’:

    print(age)

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

 

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

1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6, 1, 2, 3 . . .

בכל סיבוב שתי השחקניות מטילות כל אחת זוג קוביות. הפאות של כל קוביה ממוספרות במספרים 1–6. 

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

• מקרה א’ – אם המספרים שעלו בכל אחת משתי הקוביות שווים למספר התור – השחקנית מקבלת 21 נקודות. לדוגמה אם בתור שמספרו 3 התקבל המספר 3 בכל אחת משתי הקוביות, מספר הנקודות של השחקנית יגדל ב-21. 

• מקרה ב’ – אם רק בקוביה אחת עלה מספר השווה למספר התור – מספר הנקודות של השחקנית יגדל במספר השווה למספר התור. לדוגמה אם בתור שמספרו 3 התקבל המספר 3 בקוביה אחת בלבד, מספר הנקודות של השחקנית יגדל ב-3.

• מקרה ג’ – אם המספרים שעלו בכל אחת משתי הקוביות שונים ממספר התור, הניקוד של השחקנית לא משתנה. 

השחקנית המנצחת היא זו הצוברת ראשונה יותר מ-100 נקודות. 

כתבו פונקציה המממשת את המשחק “לא עוצרות לפני 100”. שם הפונקציה: game100 –

def game100(player1, player2):

לפונקציה שני פרמטרים: player1 ו-player2, שמות שתי השחקניות המשתתפות במשחק. 

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

פתרון

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

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

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

(1) מוטלות הקוביות שלה 

(2) מוחלט מה הניקוד שהיא קבלה בסיבוב זה 

(3) מעודכן הניקוד הנוכחי שלה 

(4) נבדק אם הניקוד החדש גדול מ-100 

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

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

import random

cube1 = random.randint(1, 6)

cube2 = random.randint(1, 6)

if CASE1 :

    UPDATE CURRENT PLAYER’S POINTS

elif CASE2: 

    UPDATE CURRENT PLAYER’S POINTS

while THE PLAYER WHO HAS JUST PLAYED DOES NOT HAVE MORE THAN 100 POINTS:        

    cube1 = random.randint(1, 6)

    cube2 = random.randint(1, 6)

    if CASE1:

        UPDATE CURRENT PLAYER’S POINTS 

    elif CASE2: 

        UPDATE CURRENT PLAYER’S POINTS

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

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

while THE PLAYER WHO HAS JUST PLAYED DOES NOT HAVE MORE THAN 100 POINTS:        

    cube1 = random.randint(1, 6)

    cube2 = random.randint(1, 6)

    if CASE1:

        UPDATE CURRENT PLAYER’S POINTS 

    elif CASE2: 

        UPDATE CURRENT PLAYER’S POINTS

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

curPlayer = player1

while THE PLAYER WHO HAS JUST PLAYED DOES NOT HAVE MORE THAN 100 POINTS:        

    cube1 = random.randint(1, 6)

    cube2 = random.randint(1, 6)

    if CASE1:

        UPDATE CURRENT PLAYER’S POINTS 

    elif CASE2: 

        UPDATE CURRENT PLAYER’S POINTS

    if curPlayer == player1:

        curPlayer = player2

    else: 

        curPlayer = player1

בתחילת הקוד הגדרנו משתנה בשם curPlayer, ואתחלנו אותו לשם השחקנית הראשונה שהועבר לפונקציה. בסוף המשחק עלינו לקבוע את זהות השחקנית שתשחק בסיבוב הבא. כיצד נעשה זאת? דרך אחת מוצגת בקוד האחרון: שימוש במבנה if…else כדי לקבוע מי השחקנית שסיימה לשחק זה עתה ולפי זה לקבוע מי השחקנית שתשחק בסיבוב הבא של הלולאה. זה מסבך מעט את המימוש ויסבכו אף יותר כשנגיע לתנאי הלולאה: בתנאי הלולאה עלינו לבדוק את הניקוד של השחקנית ששמה כבר אינו במשתנה curPlayer, זו ששחקה בסיבוב הקודם של הלולאה. נוכל להקצות משתנים נוספים לטיפול בעניין זה, אך נעצור כאן את ההתבוננות בכיוון זה ונעיין בכיוון אחר. עיינו בקוד זה –

curPlayer = player1

nextPlayer = player2

while nextPlayer DOES NOT HAVE MORE THAN 100 POINTS:        

    cube1 = random.randint(1, 6)

    cube2 = random.randint(1, 6)

    if CASE1:

        UPDATE CURRENT PLAYER’S POINTS 

    elif CASE2: 

        UPDATE CURRENT PLAYER’S POINTS

    curPlayer, nextPlayer = nextPlayer, curPlayer

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

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

curPlayer = player1

nextPlayer = player2

roundNum = 1

while nextPlayer does not have MORE THAN 100 POINTS:        

    cube1 = random.randint(1, 6)

    cube2 = random.randint(1, 6)

    if (cube1 == roundNum and cube2 == roundNum):

        UPDATE CURRENT PLAYER’S POINTS 

    elif (cube1 == roundNum or cube2 == roundNum): 

        UPDATE CURRENT PLAYER’S POINTS

    curPlayer, nextPlayer = nextPlayer, curPlayer

    roundNum = (roundNum + 1) % 7

כאן הוספנו לפני הלולאה משתנה בשם roundNum. בכל סיבוב של המשחק הוא יכיל את מספר הסיבוב. המשתנה מאותחל ל-1, כיוון שזה המספר של הסיבוב הראשון. בסוף גוף הלולאה, אם התברר שיש לה סיבוב נוסף, אנו מגדילים את מספר הסיבוב ב-1. לפי המוסבר בשאלה לאחר סיבוב שמספרו 6 אנו עוברים לסיבוב של מספרו 1. מכאן התוספת קבלת השארית מחלוקה ב-7. 

נשאר עניין בלתי פתור אחרון: אופן שמירת הניקוד לאורך המשחק. נבחן כמה דרכים. דרך אחת היא להשתמש בשני משתנים שיאותחלו ל-0 בתחילת המשחק. הנה כך יכול להראות הקוד – 

curPlayer = player1

nextPlayer = player2

player1Points = 0 

player2Points = 0

roundNum = 1

while nextPlayer DOES NOTH HAVE MORE THAN 100 POINTS:        

    cube1 = random.randint(1, 6)

    cube2 = random.randint(1, 6)

    if (cube1 == roundNum and cube2 == roundNum):

        UPDATE CURRENT PLAYER’S POINTS

    elif (cube1 == roundNum or cube2 == roundNum): 

        UPDATE CURRENT PLAYER’S POINTS

    curPlayer, nextPlayer = nextPlayer, curPlayer

    roundNum = (roundNum + 1) % 7

אם נבחר בגישה זאת תתעורר בעיה: כשנבוא לשנות את הניקוד במבנה ה-if…else בגוף הלולאה, יהיה עלינו להבין היכן נמצא הניקוד שעלינו לעדכן, ב-player1Points או ב-player2Points? אפשר להוסיף התניות שיבדקו מה מכיל המשתנה curPlayer – את שמה של השחקנית הראשונה או את שמה של השחקנית השניה – ולפי זה לדעת אם לשנות את לשנות, player1Points או את player2Points. נוכל לעשות זאת, אך נסבך את הקוד אם ננהג כך. סיבה אחרת לא לעשות זאת נובעת מאופן ההסתכלות שלנו על נתוני הניקוד. נתוני הניקוד הם למעשה אוסף. בשימוש באוסף בתכנית נעדיף לא לשמור כל נתון באוסף במשתנה נפרד, אלא לשמור את כל הנתונים כמכלול, באמצעות מבנה נתונים שהשפה מעמידה לרשותנו. אמנם כאן האוסף יכיל רק שני ערכים – הניקוד לשחקנית האחת והניקוד לשחקנית האחרת – ובכל זאת עלינו להסתכל על התכנית במבט כללי ולשאול את עצמנו: מה נעשה אם נרצה להרחיב את המשחק ולהפכו למשחק ל-4 שחקניות, ואולי ל-6, או ל-12? מובן שלא נחזיק בתכנית משתנה נפרד לניקוד של כל אחת ואחת מהן!

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

def game100(player1, player2):

    import random 

    roundNum = 1

    curPlayer = player1 

    nextPlayer = player2

    points = {player1:0, player2:0} 

    while points[nextPlayer] <= 100:        

        cube1 = random.randint(1, 6)

        cube2 = random.randint(1, 6)

        if (cube1 == roundNum and cube2 == roundNum):

            points[curPlayer] += 21

        elif (cube1 == roundNum or cube2 == roundNum):

            points[curPlayer] += playNum

        curPlayer, nextPlayer = nextPlayer, curPlayer

        roundNum = (roundNum + 1) % 7

    return [points[curPlayer], curPlayer]

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