פרויקט- אבני דומינו
הגיע הזמן לעשות חזרה, במסגרת מיני פרויקט, המיישם חלק גדול מהחומר שנלמד כאן. וגם להבין את השלבים של התהליך המחשבתי בזמן בניית תוכנית מורכבת. כבר אמרנו שתוכנית היא מודל של המציאות ובפרויקט הזה אנו רוצים לערוך מעין מודל ראשוני של משחק הדומינו הידוע. משחק הדומינו כולל 28 אבנים, לכל אבן שני צדדים, לכל צד יש שבע אפשרויות – הוא יכול להיות חלק או בעל 1-6 נקודות. הדבר הראשון שנרצה לעשות הוא לייצר מודל של אבני דומינו. אנו יודעים שכל אבן כוללת שני מספרים, שכל אחד מהם יכול להיות בין 0 ל-6, לכן כל מופע (שזה בעצם אבן דומינו שנבנה) יכלול את שני המספרים האלה. בתוכנית למטה המספרים מיוצגים על ידי x ו- y -
class Domino: def __init__(self,x,y): self.domino=[x,y] def __repr__(self): return f"{self.domino}"
החלטתי לייצור אבני דומינו באמצעות מחלקה שקראתי לה Domino (מחלקות אני מזכיר מתחילות על פי המוסכמה באות גדולה) הכוללת כרגע שתי פונקציות מיוחדות (special methods) הראשונה היא __init__ המגדירה את המופע שלנו (והיא הפונקציה/מתודה הראשונה שהמחשב קורא לה כאשר יוצרים מופע חדש באמצעות המחלקה), בהמשך, ייצוג אבן הדומינו יהיה באמצעות רשימה המכילה את שני המספרים. למשל משהוא כזה [0,0] ישמש כדי לתאר אבן עם שני צדדים חלקים. על מנת שאוכל להדפיס משהו עם משמעות, כאשר אבקש להדפיס את המופע, הגדרתי במחלקה גם פונקציה מיוחדת נוספת בשם __repr__ שתדפיס לי מחרוזת שאני יכול להבין שנראית כמו [3,2] .
עכשיו ננסה לייצר דומינו ונדגים את תהליך הניסוי והטעיה, תחיל מהקוד ואחר כך ההסבר -
tooManyDominos=[[x,y] for x in range(7) for y in range(7)] dominos=[] for item in tooManyDominos: if sorted(item) not in dominos: dominos.append(item) allDominos=[Domino(i[0],i[1]) for i in dominos]
בהתחלה ניסיתי ליצור, בטכניקה של list comprehension, אבן דומינו מהסוג [x,y] באמצעות ה"איטרטור העצל" range שבעצם עובר על המספרים מאפס עד שש ולכל תחנה כזאת הוא עובר שוב מאפס עד שש עבור הצד השני של האבן, כך שיש לנו אפס עם כל המספרים, אחד עם כל המספרים וכו'. אבל מיד התברר לי שהתהליך מייצר יותר מידי אבני דומינו, 49 במקום 28, הוא מייצר גם את [6,1] וגם את [1,6] מה שמופיע רק פעם אחת בלבד בדומינו. לכן הרשימה הזאת קיבלה בדיעבד את השם tooManyDominos. אז איך מצמצמים את הרשימה? יצרתי רשימה ריקה בשם dominos ובאמצעות לולאת for עברתי על כל פרטי הרשימה tooManyDominos וביקשתי לקחת כל אבן ולעשות עליה את הפעולה ()sorted שבעצם מסדרת את האבן מהמספר הקטן לגדול כך שלא יהיה יותר [6,2] אלא רק [2,6]. אבל זה לא מספיק, כי עכשיו צפויות להיות לי שתי אבנים שנראות כמו [2,6] ולכן ביקשתי מפייתון, שאם האבן (בתכנית למעלה נקראת item) כבר נמצאת ברשימה dominos שלא תכנס פעם שנייה.
טוב, עכשיו יש לי רשימה שנקראת dominos הכוללת בדיוק 28 רשימות קטנות (אבני הדומינו) שכל אחת מהן נראה כך [2,5] בשינוי של מספר או שניהם.
יאללה, עכשיו משתמשים במחלקה Domino שבנינו מבעוד מועד, וגם ב- list comprehension כדי לייצר סט מושלם של אבני דומינו, שהם אובייקטים מסוג class, שיאופסנו בתוך רשימה שנקראת allDominos. היות שאתx ו- y הדרושים למימוש מופע (instance) של המחלקה אני לוקח מתוך הרשימה dominosהכוללת מיני רשימות שכל אחת מהן מכילה שני מספרים, אני צריך לפרק כל מיני רשימה כזאת לשני מרכיביה (כי על מנת לייצר מופע אני צריך מספר ולא רשימה שלימה) [i[0 זה המספר השמאלי ו- [i[1 הוא המספר הימיני, וכך עוברים אבן אבן ברשימה dominos והופכים אותה למופע (אובייקט של אבן דומינו) במחלקה Domino.
השלב הבא – לערבב את האבנים באופן רנדומלי
Import random
...
...
random.Random(4).shuffle(allDominos)
כאן אני מדגים שימוש במודול (ספריה) שנקראת random, אותה מייבאים לתוכנית באמצעות פקודת import, והיא כוללת את הפונקציה השימושית random.suffle(x) שלוקחת אובייקט כמו רשימה ומחזירה אותו מעורבב. מגניב. אבל הדבר יצר לי בעיה, כי כל פעם שהתחלתי לשחק עם האבנים, מצאתי את עצמי עם ערבוב חדש. אני צריך ערבוב פעם אחת ושלא ישגע אותי ויערבב כל פעם שאני מניח אבן על השולחן. יש לזה כמה פתרונות, אני בחרתי בפתרון שיש במודל random והוא נותן ערבוב קבוע לבחירתך וזה נראה כמו בתוכנית למעלה.
עכשיו allDominos מעורבבת.
השלב הבא – לחלק לכל שחקן שבע אבני דומינו
המשתנה myHand יהפוך עד מהרה לרשימה עם שבע אבנים, באמצעות מתודה נחמדה שנלמדה קודם לכן ומבוצעת גם עם רשימות, היא נקראת ()pop והיא עושה שתי פעולות, גם מוציאה אבן מהרשימה allDominos וגם מעבירה אותה ליד שלי myHand. בשלב הזה ברור לנו שבסופו של דבר אחרי שנחלק אבנים לכל השחקנים, allDominos תהיה הקופה, ממנה מושכים אבן כשלמישהו נגמרות האפשרויות, (אלא אם יש ארבעה שחקנים ואז אין קופה).
myHand=[allDominos.pop() for i in range(7)]
computerHand=[allDominos.pop() for n in range(7)]
התפקיד של i in range (7) הוא פשוט, לספור כמה פעמים תתבצע המתודה pop() – שבע פעמים (זו למעשה ספירה מאפס עד 6 כולל 6 ולא כולל 7- סה"כ 7 פעמים)
השלב הבא- לייצר שולחן עליו מניחים את אבני הדומינו –
לשני השחקנים שלנו יש 7 אבנים שונות, עכשיו אנו רוצים ייצוג לשולחן דומינו, כלומר, רשימה, שבתוכה נסדר את האבנים.
היות שלפעמים רוצים להניח אבן בצד הימיני של הרשימה ולפעמים בצד השמאלי שלה, נוח יותר להשתמש במתודה מתוך ספריית collections שנקראת: ()collections.deque והיא נותנת לנו מעין רשימה משוכללת שאפשר לצרף אליה בקלות איברים משני הצדדים.
לשולחן שלנו אנו קוראים dominoTable וזה נראה בתוכנית כך -
import collections
dominoTable =collections.deque()
השלב הבא – לאפשר לשחקן להוציא אבן דומינו מהיד שלו ולהניח על השולחן בצד הנכון
בשביל זה אנו חוזרים למחלקה שלנו Domino ומוסיפים לה פונקציונאליות play ומקשרים את הפעולות למשתנים מחוץ למחלקה באמצעות הביטוי global (כך שגם השולחן, גם היד של המחשב וגם היד שלי מושפעים כאשר אני מוציא אבן מהיד של שחקן כלשהו).
לשם כך בנינו את המתודה play הכוללת ארבעה מצבים שמחייבים הסבר – נניח שאחת מאבני הדומינו ביד שלי נראית כך – [1,2] ונניח שי אבן אחת על השולחן שנראית כך – [6,2] (זה אפשרי למרות שבהתחלה עשינו sorted והיא אמורה להיות מהמספר הקטן בשמאל לגדול בימין), זה מעולה כי יש לי את המספר 2 אבל אם אני אניח את האבן בצד ימין בלי לסובב אותה, השולחן יראה כך [6,2],[1,2] שזה לא טוב, כל הרעיון של אבן דומינו הוא שאפשר לסובב אותה. מכאן חילקתי את האפשרויות שעומדות בפני השחקן לארבעה מצבים כלהלן –
1 – מניחים אבן בצד ימין בלי לסובב אותה (פקודת append פשוטה, מצרפת איבר לצד ימין של הרשימה או ה- deque במקרה שלנו)
2- מניחים אבן בצד ימין עם לסובב (מצרפים רשימה הפוכה - [self.domino[1], self.domino[0] היא הפוכה כי 0 ברשימה הוא השמאלי ביותר וכאן אנו מבקשים ש-1 יהיה בשמאל. כלומר במקום [1,2] נצרף [2,1] וזה מדמה מצב של סיבוב אבן הדומינו לפני שמניחים אותה.
3- מניחים בצד שמאל בלי לסובב - דבר שמתאפשר ב deque באמצעות appendleft.
4- מניחים בצד שמאל עם לסובב.
class Domino: def __init__(self,x,y): global dominoTable ,myHand,computerHand self.domino=[x,y] def __repr__(self): return f"{self.domino}" def play(self,x): if x==1: dominoTable.append(self.domino) if x==2: dominoTable.append([self.domino[1],self.domino[0]]) if x==3: dominoTable.appendleft(self.domino) if x==4: dominoTable.appendleft([self.domino[1], self.domino[0]]) if self in myHand: myHand.remove(self) if self in computerHand: computerHand.remove(self)
דאגנו שהמתודה play מגיעה עם פרמטר מספרי 1-4 שיקבע לנו איזה אפשרות תבחר. אם למשל נבחר (play(3 האבן תונח בצד השמאלי בלי סיבוב. כמובן שאחרי משחק צריך להוסיף את האבן לשולחן ולגרוע אותה מהיד של מי ששיחק הרגע. לשם כך השתמשנו במתודה remove במקרה הזה של self האובייקט (אבן הדומינו שלנו) מתוך אחת הידיים שהוא היה בה קודם לכן.
לשים לב שבפקודה remove אם מנסים להסיר משהו שאינו קיים ברשימה, מקבלים הודעת שגיאה.
השלב הבא – לבדוק מה קורה לשולחן בזמן שמשחקים
כך נראים מהלכי המשחק – המספר השמאלי, למשל ב- [myHand[1, מייצג את הבחירה שלנו באבן הדומינו מתוך רשימת האבנים שביד שלנו – 0 היא האבן השמאלית, 1 אחריה וכך הלאה כמו ב- list כי myHand היא באמת list. והמספר שאחרי המתודה play בסוגריים עגולים, (play(4 למשל, מייצג באיזה אופן אנו רוצים לשחק עם האבן, לפי ארבעת המצבים, שמאל ימין עם או בלי סיבוב.
myHand[1].play(1) computerHand[1].play(1) myHand[0].play(3) computerHand[5].play(1) myHand[2].play(4) computerHand[2].play(3) print("my hand:" ,myHand) print("dominoTable:",list(dominoTable)) print("computerHand:",computerHand)
כמובן שניתן לערוך את הדברים בקלות באופן אוטומטי, כלומר, לבחור רק אבן מתוך היד שלי וצד בשולחן, והתוכנית תבצע במידת הצורך סיבוב של האבן. איך עושים ? כותבים תנאי פשוט שלגבי הצד שבחרנו בשולחן, אם המספר האחרון בקצה שווה למספר באבן שלנו כאשר מניחים אותה בלי סיבוב, אזי לאפשר צירוף בלי סיבוב, ואם זה שווה למספר בצד השני של האבן, לבצע סיבוב ואז לצרף. לא מסובך, אבל לצורך הלימוד מאריך מידי את הדברים.
לפני שנראה איך הדברים נראים אחרי כל המהלכים שעשינו, כדאי לראות איך נראה כל הקוד (עד לשלב הזה) ביחד –
import random import collections dominoTable =collections.deque() class Domino: def __init__(self,x,y): global dominoTable ,myHand,computerHand self.domino=[x,y] def __repr__(self): return f"{self.domino}" def play(self,x): if x==1: dominoTable.append(self.domino) if x==2: dominoTable.append([self.domino[1],self.domino[0]]) if x==3: dominoTable.appendleft(self.domino) if x==4: dominoTable.appendleft([self.domino[1], self.domino[0]]) if self in myHand: myHand.remove(self) if self in computerHand: computerHand.remove(self) tooManyDominos=[[x,y] for x in range(7) for y in range(7)] dominos=[] for item in tooManyDominos: if sorted(item) not in dominos: dominos.append(item) allDominos=[Domino(i[0],i[1]) for i in dominos] random.Random(4).shuffle(allDominos) myHand=[allDominos.pop() for i in range(7)] computerHand=[allDominos.pop() for n in range(7)] myHand[1].play(1) computerHand[1].play(1) myHand[0].play(3) computerHand[5].play(1) myHand[2].play(4) computerHand[2].play(3) print("my hand:" ,myHand) print("dominoTable:",list(dominoTable)) print("computerHand:",computerHand)
תמיד יש אפשרות לעשות את הכל בארבע שורות אבל קשה לעקוב אחרי הלוגיקה בקודים מקוצרים מידי.
כך נראה השולחן והידיים של השחקנים אחרי סיבובי המשחק שלמעלה, בהם כל שחקן שיחק שלוש פעמים ונותר עם ארבע אבנים –
my hand: [[0, 3], [4, 5], [2, 4], [0, 4]]
dominoTable: [[4, 6], [6, 1], [1, 1], [1, 3], [3, 5], [5, 5]]
computerHand: [[0, 2], [0, 0], [5, 6], [0, 1]]
ביד שלי וביד של המחשב נותרו ארבע אבנים, האבנים בשולחן מסודרים בשרשרת נחמדה כמו שאבני דומינו צריכים להיות מונחים. בתור הבא יהיו לי כמה אפשרויות טובות, משום שהאבנים בשולחן מסודרות כך ש- 4 ו- 5 בקצוות ויש לי שלוש רביעיות וחמש אחד.
השלבים הבאים בפרויקט – (אותם תוכלו לעשות בעצמכם)
לאפשר משיכה של אבן מהקופה ליד של השחקן שאין לו אפשרות במסגרת האבנים שבידיו
להגדיר ניצחון
להפוך את המשחק של המחשב לאוטומטי
להפוך את המשחק של המחשב לחכם (אם יש לו כמה אפשרויות משחק אולי כדאי להיפתר קודם ממספרים שיש לו כמותם בשפע באבנים אחרות)
לשפר את הנראות של המשחק
לתפוס שגיאות של שחקנים שמנסים לשחק עם אבן שלא ברשימה, למשל, לשחק עם אבן מספר 5 כאשר נותרו להם רק 3 אבנים ביד. או לתקן שגיאה של מהלך שהוא לא חוקי, הצמדת המספר 2 באבן אחת למספר 6 באבן אחרת.
אולי לאפשר לשחקן להתחרט ולהחזיר את המהלך לאחור (אני בעד נגעת נסעת) –
להלביש על התוכנה ממשק גרפי צבעוני שהשולחן יראה כמו שולחן והאבן כמו אבן דומינו (טוב זה עדיין רחוק – אבל מי שרוצה...)