המשחק Quinto והגדרת המחלקה SBECell
איך בונים צעד-אחר-צעד יישום גרפי חי ב-Squeak תוך הפרדת רשויות בין לוח המשחק (SBEGame) לבין תא בודד (SBECell) היורש ממתג גרפי דו-מצבי (SimpleSwitchMorph).
מודל מנטלי ועיקרון מנחה: חלוקת אחריות בין עצמים ויצירת מחלקות על ידי שליחת הודעה!
תחשוב על מערכת תאורה חכמה בבניין: כל מתג על הקיר (SBECell) יודע רק להידלק, להיכבות ולהודיע שלחצו עליו, בעוד ארון הפיקוד המרכזי (SBEGame) הוא היחיד שמחזיק את מפת החדרים ויודע אילו 4 שכנים להדליק או לכבות!
בשפת Smalltalk אפילו הגדרת מחלקה חדשה אינה מילת מפתח תחבירית מיוחדת — אלא שליחת הודעת מילת-מפתח (subclass:instanceVariableNames:...) אל מחלקת-העל שממנה אנחנו רוצים לרשת!
אנחנו בונים את האפליקציה באופן הדרגתי ואינטואיטיבי (ירושה, אתחול, משתני מופע מול מקומיים, אירועים, בלוקים ודיבוג) עוד לפני הדיון הפורמלי בכללי התחביר שבפרק 6!
1. מבוא לפרק וסעיף 5.1: להכיר את המשחק Quinto ואת ארכיטקטורת התוכנה שלנו
איך אנחנו בונים אפליקציה גרפית שלמה מתחילתה ועד סופה בתוך סביבת העבודה Squeak
עכשיו הגענו לאחד הרגעים הכי מרגשים בלמידה שלנו: אנחנו הולכים לפתח יחד, צעד אחר צעד, אפליקציה ראשונה שלמה בעלת ממשק גרפי מתחילתה ועד סופה — המשחק Quinto (המבוסס על המשחק הלוגי המוכר Lights Out). המטרה שלי איתך בפרק הזה היא שתרגיש בידיים איך הקוד והמחלקות נבנים בצורה הדרגתית בתוך סביבת העבודה Squeak, תוך שימוש אינטואיטיבי באלמנטים המרכזיים של שפת Smalltalk: הגדרת מחלקות וירושה, מתודות אתחול, משתני מופע מול משתנים מקומיים, טיפול באירועים גרפיים, שרשור הודעות, בלוקים, איתור באגים וייצוא קוד — וכל זאת עוד בטרם נערוך את הדיון הפורמלי בכללי התחביר שיוצג בפרק 6!
בוא נבין תחילה איך מתנהל המשחק Quinto ומהם חוקיו המדויקים:
🟨 חוקי המשחק והממשק של Quinto
- הלוח והמצבים: המשחק משוחק על גבי לוח משובץ (מטריצה ריבועית של משבצות) המורכב ממשבצות בצבע אפור (מצב כבוי – המצב ההתחלתי) או צהוב (מצב דלוק/פעיל).
- מכניקת הלחיצה (Toggle לשכנים): לחיצה על משבצת כלשהי בלוח גורמת לשינוי צבען ומצבן (Toggle) של ארבע המשבצות השכנות הצמודות לה בלבד: המשבצת שמעליה (למעלה), מתחתיה (למטה), מימינה ומשמאלה — ללא האלכסונים!
- הפיכת מצב והפיכות הפעולה: משבצת שכנה שהייתה אפורה תהפוך לצהובה, ומשבצת שהייתה צהובה תהפוך לאפורה. לחיצה נוספת על אותה המשבצת תחזיר את המשבצות השכנות לצבען הקודם.
- מטרת המשחק: לגרום לכך שכל משבצות הלוח תהפוכנה לצהובות!
🏗️ ארכיטקטורת התוכנה ויצירת הקטגוריה SBE-Quinto
כדי למדל את המשחק בחשיבה מונחית עצמים נקייה, נפריד את התוכנית שלנו לשתי מחלקות עיקריות:
SBEGame: תייצג את לוח המשחק השלם, תנהל את לוגיקת המשחק ותחזיק את מטריצת המשבצות.SBECell: תייצג משבצת (תא) בודדת על גבי הלוח.
SBE-Quinto (ראשי תיבות: Squeak By Example - Quinto). הוספת הקטגוריה מתבצעת מתוך תפריט ההקשר (לחיצה ימנית / כפתור צהוב) בחלונית הקטגוריות השמאלית ביותר של ה-System Browser ← בחירה באפשרות add item… ← הזנת השם SBE-Quinto ולחיצה על אישור.
2. סעיף 5.2: הגדרת המחלקה SBECell ב-System Browser
איך תבנית יצירת המחלקה ב-Smalltalk שולחת הודעת מילת-מפתח למחלקת-העל SimpleSwitchMorph
ברגע שאנחנו בוחרים בקטגוריה החדשה שלנו SBE-Quinto ב-System Browser, חלונית העריכה המרכזית מציגה לנו תבנית בסיסית ליצירת מחלקה חדשה:
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'SBE-Quinto'
עצור לרגע ותתבונן בביטוי הזה! בשפות כמו Java אנחנו כותבים מילים שמורות מיוחדות של המהדר (public class SBECell extends ...). אבל ב-Smalltalk הכל הוא אובייקט וכל דבר קורה על ידי שליחת הודעה!
הביטוי הזה מורכב משליחת הודעת מילת-מפתח ארוכה בת 5 חלקים:
subclass:instanceVariableNames:classVariableNames:poolDictionaries:category:
הנשלחת ישירות אל העצם שמייצג את מחלקת-העל (בתבנית זוהי המחלקה Object).
הארגומנטים של ההודעה כוללים:
- סימבול שם המחלקה (
#NameOfSubclass): נרשם עם הסימן סולמית#בראשו ומציין את שם המחלקה החדשה. instanceVariableNames:מחרוזת המכילה את רשימת משתני המופע (מופרדים ברווח).classVariableNames:מחרוזת המכילה את רשימת משתני המחלקה.poolDictionaries:מחרוזת המכילה את רשימת המילונים המשותפים.category:שם הקטגוריה שבה תסווג המחלקה ('SBE-Quinto').
כעת נשנה את זהות מקבל ההודעה (מחלקת-העל) וכן חלק מהארגומנטים, כך שההודעה תתאים ליצירת המחלקה SBECell שלנו:
instanceVariableNames: 'mouseAction'
classVariableNames: ''
poolDictionaries: ''
category: 'SBE-Quinto'
SimpleSwitchMorph?
במקום לרשת מ-Object ולבנות גרפיקה מאפס, ההודעה נשלחת למחלקה SimpleSwitchMorph — מחלקה קיימת שמוגדרת בקטגוריה MorphicExtras-Widgets ומייצגת כפתור/מתג גרפי דו-מצבי (כבוי ודלוק)! בנוסף, אנו מגדירים משתנה מופע אחד בשם mouseAction שישמור את פעולת התגובה ללחיצת עכבר.
accept (s) ותיעוד (Class Comment)
שומרים את השינויים על ידי בחירת האפשרות accept (s) מתוך תפריט ההקשר (או Ctrl+s). כתוצאה מהשמירה נוצרת המחלקה SBECell ושמה מוצג בחלונית המחלקות! בחלונית התחתונה אפשר להוסיף תיעוד קצר (Class Comment) המתאר את מטרת המחלקה ולשמור שוב.
הפיכת 4 השכנים הצמודים (ללא אלכסונים) וירושה מ-SimpleSwitchMorph
מיני-סימולציה 5.1: מנתח הודעת יצירת המחלקה SBECell ב-System Browser
מעבדת המשחק Quinto החיה: התנסה בחוקי הפיכת 4 השכנים (ללא אלכסונים)!
לחץ על כל משבצת בלוח כדי לראות כיצד ארבע המשבצות השכנות הצמודות לה בלבד (למעלה, למטה, ימין, שמאל – ללא אלכסונים וללא המשבצת הנלחצת עצמה!) הופכות את צבען מאפור לצהוב ולהפך. נסה להגיע למצב שבו כל משבצות הלוח צהובות!
🎛️ בקרת לוח המשחק ומעקב הודעות חי:
הגשר הנרטיבי: הגדרנו את המחלקה SBECell – אבל איך מאתחלים את הצבע, המסגרת והגודל שלה, ואיך בוחנים אותה חיה על המסך?
יצרנו את שלד המחלקה SBECell שיורשת מ-SimpleSwitchMorph. אבל כרגע התא שלנו עדיין משתמש במראה ברירת המחדל של מתג רגיל ב-Morphic!
בלשונית הבאה נכתוב את מתודת האתחול initialize (ונבין למה חובה לפתוח אותה ב-super initialize!), ולאחר מכן ניצור מופע חי של התא, נחקור ונשנה את משתני המופע שלו מתוך ה-Inspector, ונציג אותו על המסך עם openInWorld ועם הילת ה-Morphic Halo!
הוספת מתודות למחלקה SBECell ובחינתה באמצעות ה-Inspector
מוסכמת הקריאה האוטומטית ל-initialize, חשיבות הקריאה ל-super initialize, אתחול נקודות ומלבנים (0@0 corner: 16@16), וחקירה חיה עם inspect it, openInWorld ו-Morphic Halo.
מודל מנטלי ועיקרון מנחה: קודם מאתחלים את היסודות שנורשו (super initialize), ואז מתאימים אישית!
כשאתה מקבל דירה חדשה מקבלן (מחלקת-העל SimpleSwitchMorph), אתה קודם כל נותן לקבלן לחבר את החשמל והצנרת הבסיסיים (super initialize), ורק אז צובע את הקירות בצהוב חיוור וכחול כהה לפי טעמך!
לפי מוסכמות Smalltalk, מיד לאחר יצירת עצם חדש באמצעות new נשלחת אליו אוטומטית ההודעה initialize. מכיוון שהעצם יורש משתני מופע ממחלקת-האב, מתודת initialize חייבת לפתוח ב-super initialize!
ב-Squeak אנחנו לא צריכים להמתין לסיום פיתוח כל המשחק כדי לבדוק תא בודד: בעזרת ה-Inspector, openInWorld וה-Morphic Halo אנחנו בודקים ומשנים את העצם חי בזמן אמת!
1. סעיף 5.3: הוספת מתודת האתחול initialize למחלקה SBECell
איך פועלת מוסכמת האתחול האוטומטי ב-Smalltalk, וניתוח שורה-אחר-שורה של קוד האתחול
עכשיו כשיש לנו את המחלקה SBECell, אנחנו רוצים שכל תא חדש שייווצר יקבל מיד את המראה המדויק של משבצת במשחק Quinto.
לפי המוסכמות של שפת Smalltalk, מיד לאחר יצירת עצם חדש (באמצעות שליחת ההודעה new למחלקה), נשלחת אל העצם החדש באופן אוטומטי ההודעה initialize המיועדת לאתחול העצם.
מה תפקידן של מתודות אתחול? לאתחל את משתני המופע של העצם! אבל שים לב לנקודה קריטית: משתני המופע של עצם כוללים גם את כל המשתנים שהעצם ירש ממחלקת-האב (במקרה שלנו מ-SimpleSwitchMorph וממחלקות-העל שלה), ולא רק את המשתנה mouseAction שהגדרנו בהודעה שיצרה את המחלקה SBECell. לכן המתודה initialize חייבת לדאוג לאתחל גם אותם!
כשנבחר בפרוטוקול --all-- בחלונית הפרוטוקולים של ה-System Browser, תוצג לנו בחלון העריכה המרכזי התבנית הכללית ליצירת מתודה:
"comment stating purpose of message"
| temporary variable names |
statements
נחליף את התבנית הזו במתודה initialize של המחלקה SBECell ונשמור באמצעות accept (s):
super initialize.
self label: ''.
self borderWidth: 2.
bounds := 0@0 corner: 16@16.
offColor := Color paleYellow.
onColor := Color paleBlue darker darker.
self useSquareCorners.
self turnOff
🔬 בוא ננתח יחד כל שורה במתודת ה-initialize:
- 1.
super initialize.: המשפט הראשון שולח ל-superאת ההודעהinitialize. המילהsuperמתייחסת לחלק העצם שהתקבל בירושה ממחלקת-העל (SimpleSwitchMorph). הודעה זו מפעילה את המתודהinitializeשהוגדרה במחלקת-העל כדי שזו תאתחל את כל משתני המופע שהוגדרו אצלה! - 2.
self label: ''.ו-self borderWidth: 2.: המתודה שולחת לעצמה (self) את ההודעותlabel:(איפוס התווית למחרוזת ריקה כך שלא יופיע טקסט על התא) ו-borderWidth:(קביעת עובי מסגרת של 2 פיקסלים). הודעות אלה מפעילות את המתודות המתאימות שהתקבלו בירושה ממחלקת-העל. - 3.
bounds := 0@0 corner: 16@16.: השמה ישירה למשתנה המופעboundsשהתקבל בירושה ממחלקת-העל. המשתנה מאותחל כמלבן (Rectangle) שפינתו השמאלית-עליונה בנקודה(0,0)ופינתו הימנית-תחתונה בנקודה(16,16). איך נוצר המלבן? על ידי שליחת ההודעה@עם ארגומנט מספרי למספר שלם שיוצרת עצם מסוג נקודה (Point), ושליחת ההודעהcorner:לעצם ה-Pointעם נקודת הפינה הנגדית! - 4.
offColor := Color paleYellow.ו-onColor := Color paleBlue darker darker.: משפטי השמה למשתני המופעoffColorו-onColorשהתקבלו בירושה ממחלקת-העל.offColorמאותחל כצהוב חיוור, ו-onColorמאותחל בגרסה כהה פעמיים של כחול חיוור (על ידי שרשור שתי הודעות אונריותdarkerלעצם הצבע שחזר מ-Color paleBlue). - 5.
self useSquareCorners.ו-self turnOff: לבסוף, המתודה שולחת לעצמה הודעות להשתמש בפינות מרובעות וישרות (במקום מעוגלות) ולכבות את עצמה כמצב התחלתי. גם הודעות אלה מפעילות מתודות שנורשו ממחלקת-העל.
2. סעיף 5.4: יצירת עצם ובחינתו באמצעות ה-Inspector וה-Morphic Halo
איך אנחנו בודקים את התא שיצרנו בעולם החי של Squeak עוד לפני שבנינו את לוח המשחק
אחד הדברים הכי יפים בעבודה בסביבת Squeak הוא שאנחנו לא צריכים לחכות לסיום כתיבת התוכנית כולה כדי לבדוק אם התא שלנו עובד! בוא נעבור לחלון העבודה (Workspace), ניצור עצם חדש מסוג SBECell באמצעות שליחת ההודעה new למחלקה, ונפעיל עליו את המפקח (Inspector) באמצעות הפקודה inspect it (i) (או Ctrl+i):
bounds
בחלונית השמאלית של ה-Inspector מוצגים כל משתני המופע של העצם (כולל אלו שנורשו!), ובחלונית הימנית אפשר לראות ולשנות את ערכו של המשתנה הנבחר. למשל, נבחר במשתנה bounds ונשנה את ערכו כך שפינתו הימנית-תחתונה של המלבן תהיה בנקודה (100,100).
self openInWorld
בחלונית התחתונה של ה-Inspector (שבה המשתנה self מתייחס ישירות לתא הנבדק!) נשלח לעצם את ההודעה self openInWorld ונבצע אותה באמצעות do it (d). מיד נראה את התא מופיע בפינה השמאלית-עליונה של סביבת העבודה, ולחיצה עליו בכפתור שמאלי של העכבר תראה איך הוא מגיב ומחליף צבע!
x
לחיצה על התא באמצעות הכפתור האמצעי של העכבר תציג סביבו את ה-morphic halo: כעת נוכל להזיז את העצם ולשנות את גודלו על המסך, ולראות ב-Inspector כיצד משתני המופע מתעדכנים מיד לפי מצבו החדש! לבסוף, לחיצה על ה-x בפינה השמאלית-עליונה של ה-morphic halo תוציא את העצם מסביבת העבודה.
מנקודות ומלבנים (0@0 corner: 16@16) ועד מניפולציה חיה עם Morphic Halo
מיני-סימולציה 5.2: הרצה צעד-אחר-צעד של 8 השורות במתודה SBECell>>initialize
מעבדת ה-Inspector וה-Morphic Halo החיה: בחן ושנה עצם מסוג SBECell בזמן אמת!
בצע SBECell new ב-Inspector: בחר משתני מופע בחלונית השמאלית, שנה את bounds ל-0@0 corner: 100@100, הרץ self openInWorld בחלונית התחתונה, לחץ על התא כדי להחליף בין כבוי לדלוק, והפעל את ה-Morphic Halo (כולל כפתור ה-x השחור להסרת העצם מהעולם)!
self = התא הנבדק):
Ctrl+d / do it (d)
self openInWorld
הגשר הנרטיבי: התא הבודד SBECell עובד נהדר – עכשיו איך בונים את לוח המשחק השלם SBEGame ומחברים ביניהם?
ראינו איך ליצור, לאתחל ולבחון תא בודד מסוג SBECell. עכשיו הגיע הזמן לבנות את המחלקה השנייה שלנו: SBEGame!
איך יוצרים לוח שמכיל מטריצה של $10 \times 10$ תאים באמצעות Matrix new: n tabulate:? ואיך גורמים לתא בודד (שלא מכיר את לוח המשחק!) להפעיל בלחיצת עכבר את הפיכת השכנים בלוח באמצעות בלוק קוד (mouseAction) וההודעה value? בוא נעבור ליחידה 5.5!
הגדרת המחלקה SBEGame והשלמת המעגל במחלקה SBECell
ירושה מ-BorderedMorph, משתנים מקומיים מול משתנה המופע cells, יצירת המטריצה עם Matrix new: n tabulate:, חישוב מיקומי התאים, בדיקת תנאי שפה ב-toggleNeighboursOfCellAt:at:, והעברת בלוק ל-mouseAction: המופעל ב-mouseUp: על ידי mouseAction value.
מודל מנטלי ועיקרון מנחה: שימוש בבלוק קוד (Closure) לשמירה על הפרדת רשויות ופרטיות משתני מופע!
במקום שכל כפתור תאורה (SBECell) יצטרך להכיר את תוכניות הבניין כולו, מנהל הלוח (SBEGame) מפקיד בידי כל תא "מעטפה סגורה עם הוראה מוכנה" (בלוק הקוד [self toggleNeighboursOfCellAt: i at: j]). כשהמשתמש לוחץ על התא, התא רק פותח את המעטפה ומפעיל אותה עם mouseAction value!
כלל החזרה ב-Smalltalk: מתודה מחזירה ערך באמצעות החץ העליון ^ (כמו ^ 10 ב-cellsPerSide). אם מתודה אינה כוללת ^, ברירת המחדל בשפה היא להחזיר את self!
כל משתני המופע ב-Smalltalk הם פרטיים! מתודות גישה אינן משתמשות בקידומת get/set: מתודת קריאה נקראת כשם המשתנה (mouseAction), ומתודת עדכון נקראת כשם המשתנה עם נקודתיים (mouseAction:).
1. הגדרת המחלקה SBEGame ומתודת ה-initialize של הלוח
ירושה מ-BorderedMorph, משתנים מקומיים זמניים, חישוב גודל הלוח ב-extent: ויצירת מטריצת התאים
המחלקה SBEGame מייצגת את לוח המשחק הכולל, והיא תכיל מטריצה ריבועית של עצמים מסוג SBECell. מכיוון שאנחנו רוצים שללוח המשחק שלנו תהיה מסגרת גרפית יפה עם שוליים, נגדיר את SBEGame כתת-מחלקה של BorderedMorph (צורה גרפית בעלת מסגרת ושוליים) תחת הקטגוריה SBE-Quinto:
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'SBE-Quinto'
שים לב שבשלב הראשוני השארנו את רשימת משתני המופע ריקה (''), ומיד נראה כיצד סביבת Squeak תעזור לנו להוסיף לשם את משתנה המופע cells באופן אוטומטי כשנשמור את מתודת האתחול!
בוא נכתוב כעת את המתודה initialize של SBEGame (בפרוטוקול --all--):
| sampleCell width height n |
super initialize.
n := self cellsPerSide.
sampleCell := SBECell new.
width := sampleCell width.
height := sampleCell height.
self bounds: (5@5 extent: ((width*n) @ (height*n)) + (2 * self borderWidth)).
cells := Matrix new: n tabulate: [ :i :j | self newCellAt: i at: j].
🔍 ניתוח מעמיק של מתודת SBEGame>>initialize:
- משתנים מקומיים
| sampleCell width height n |: בשורה הראשונה מוגדרים 4 משתנים מקומיים זמניים (בין קווים אנכיים| ... |). הם משמשים אותנו לחישוב בזמן ריצת המתודה בלבד, ואינם נשמרים כחלק ממצב העצם לאחריה! super initialize.: מפעילה את מתודת ה-initializeשל מחלקת-העל (BorderedMorph).- מדידת תא דוגמה:
nמאותחל למספר התאים בצלע הלוח באמצעותself cellsPerSide. לאחר מכן אנו יוצרים תא זמניsampleCell := SBECell newרק כדי לשאול אותו מהם הרוחב (width) והגובה (height) של תא בודד! - קביעת גבולות הלוח עם
bounds:ו-extent:: אנו שולחים לעצמנו את ההודעהbounds:(שנורשה) ומעבירים לה מלבן שנוצר על ידי שליחת ההודעהextent:לנקודת הפינה השמאלית-עליונה5@5. ההודעהextent:מקבלת נקודה המייצגת את גודל המלבן (רוחב וגובה כוללים): מכפלת רוחב וגובה תא בודד ב-n, בתוספת כפליים עובי המסגרת של הלוח (2 * self borderWidth– עבור השוליים משני הצדדים!). - יצירת המטריצה עם
Matrix new: n tabulate:: השורה האחרונה מאתחלת את משתנה המופעcellsכמטריצה בגודל $n \times n$. ההודעהnew:tabulate:נשלחת למחלקהMatrixומקבלת את הגודלnובלוק קוד[ :i :j | self newCellAt: i at: j]שמתבצע עבור כל שורהiועמודהjומייצר את התא המתאים! - שמירה, הגדרת
cellsכמשתנה מופע וסיווג אוטומטי: כשנשמור (accept) את המתודה, המערכת תתריע כי המשתנהcellsוהמתודות החדשות טרם הוגדרו. נבחר עבורcellsבאפשרות יצירת משתנה מופע (declare instance variable)! תחילה המתודה תופיע תחת הפרוטוקול"as yet unclassified". נבחר בפעולה"categorize all uncategorized"מתפריט ההקשר של חלונית הפרוטוקולים — והמערכת תיצור אוטומטית את הפרוטוקולinitializationותשייך אליו אתinitialize(מכיוון שגם במחלקת-העל המתודהinitializeמשויכת לפרוטוקולinitialization!).
2. השלמת מתודות הלוח SBEGame והשלמת המעגל במחלקה SBECell
המתודות cellsPerSide, newCellAt:at:, toggleNeighboursOfCellAt:at: ומתודות העכבר mouseAction: ו-mouseUp:
כעת נשלים את שלוש המתודות החסרות במחלקה SBEGame:
המתודה cellsPerSide וכלל ברירת המחדל self
מחזירה את מספר התאים בכל שורה ועמודה של המטריצה (10) באמצעות הסימן ^. זכור כלל יסוד בשפה: אם מתודה אינה כוללת ^, ברירת המחדל היא שהיא מחזירה את self!
"The number of cells in each row and column"
^ 10
המתודה toggleNeighboursOfCellAt: i at: j
בודקת תנאי שפה (כדי לא לחרוג מגבולות המטריצה), שולפת כל שכן מ-cells עם at:at: ושולחת לו toggleState. (נסווג בפרוטוקול חדש game logic באמצעות new category...).
"toggle Neighbours Of Cell ij"
(i > 1) ifTrue: [(cells at: i-1 at: j) toggleState].
(i < self cellsPerSide) ifTrue: [(cells at: i+1 at: j) toggleState].
(j > 1) ifTrue: [(cells at: i at: j-1) toggleState].
(j < self cellsPerSide) ifTrue: [(cells at: i at: j+1) toggleState].
המתודה newCellAt: i at: j – יצירת התא, מיקומו בלוח והגדרת פעולת הלחיצה
מתודה זו מקבלת מספר שורה i ועמודה j, יוצרת תא חדש c := SBECell new, מאתרת את נקודת המוצא הפנימית של הלוח (origin := self innerBounds origin), מוסיפה את התא כרכיב גרפי מוכל בלוח (self addMorph: c), ממקמת אותו לפי קואורדינטות השורה והעמודה שלו, ובשורה האחרונה שולחת לתא את ההודעה mouseAction: עם בלוק קוד שירוץ בעת לחיצת עכבר:
"create a cell for position (i,j) and add it to the game at appropriate position.
return the new cell"
| c origin |
c := SBECell new.
origin := self innerBounds origin.
self addMorph: c.
c position: ((i-1) * c width) @ ((j-1) * c height) + origin.
c mouseAction: [self toggleNeighboursOfCellAt: i at: j].
🔄 השלמת המעגל במחלקה SBECell: מתודת הגישה mouseAction: ומתודת האירוע mouseUp:
כל משתני המופע בשפת Smalltalk הם פרטיים (Private), והדרך היחידה לגשת אליהם מחוץ לעצם עצמו היא דרך מתודות גישה!
בניגוד לשפות אחרות, מתודות גישה ב-Smalltalk אינן משתמשות בקידומת get או set: שמה של מתודת הגישה להחזרת ערכו של משתנה זהה לשם המשתנה (mouseAction), ושמה של מתודת העדכון זהה לשם המשתנה בתוספת נקודתיים (mouseAction:).
נוסיף למחלקה SBECell שתי מתודות:
"set mouseAction to the block argument"
mouseAction := aBlock
"execute mouseAction code"
mouseAction value
המתודה mouseUp: מופעלת באופן אוטומטי על ידי סביבת העבודה Squeak (תשתית Morphic) ברגע שהמשתמש משחרר את כפתור העכבר מעל התא! שליחת ההודעה value לעצם הבלוק השמור ב-mouseAction גורמת לביצוע הקוד של הבלוק — ומפעילה את self toggleNeighboursOfCellAt: i at: j בלוח המשחק!
כיצד SBEGame מפקיד בלוק ב-mouseAction, וכיצד mouseUp: מפעיל אותו עם ההודעה value
מיני-סימולציה 5.3: מחשבון מיקום תא (position:) ובדיקת תנאי שפה (toggleNeighboursOfCellAt:at:)
מעבדת זרימת ההודעות והבלוק: עקוב צעד-אחר-צעד אחר הקשר שבין SBEGame ל-SBECell
צעד דרך 5 השלבים בשרשרת החיים של המשחק: מאתחול המטריצה Matrix new: n tabulate: והפקדת הבלוק ב-mouseAction:, ועד שחרור העכבר ב-mouseUp: והפעלת mouseAction value!
הגשר הנרטיבי: כתבנו את כל המחלקות והמתודות – אבל למה כשמריצים ולוחצים על תא קופצת שגיאת MessageNotUnderstood?!
על פניו סיימנו לכתוב את כל הקוד של SBECell ושל SBEGame! אבל ברגע שנריץ בחלון העבודה SBEGame new openInWorld ונלחץ על משבצת בלוח, יקפוץ מולנו חלון ה-Debugger עם השגיאה המפורסמת:
MessageNotUnderstood: SBEGame>>toggleState!
למה לוח המשחק קיבל את ההודעה toggleState במקום התא? איך חוקרים ומתקנים את הבאג הזה בדקה אחת, ואיך שומרים את הקוד לקובץ חיצוני (fileOut)? בוא נעבור ליחידה 5.6–5.7!
הפעלת המשחק SBEGame, פיצוח הבאג ב-Debugger ושמירת התכנית (fileOut / fileIn)
הרצת SBEGame new openInWorld, תחקור השגיאה MessageNotUnderstood: SBEGame>>toggleState במחסנית הקריאות, הבנת תוצאת החזרת self כברירת מחדל ב-newCellAt:at:, התיקון ^ c, וייצוא/ייבוא של הקטגוריה לקובץ SBE-Quinto.st.
מודל מנטלי ועיקרון מנחה: כששוכחים להחזיר ערך מפורש (^), המתודה מחזירה את self!
ביקשת מקבלן השיפוצים (SBEGame) לייצר 100 מנורות (SBECell) ולרשום בטבלה איזו מנורה נמצאת בכל חדר. הקבלן אומנם הרכיב את המנורות על התקרה (self addMorph: c), אבל בטבלה (cells) הוא רשם בכל 100 המשבצות את השם של עצמו (self)! כשניסית להדליק מנורה דרך הטבלה, שלחת פקודת "הידלק" לקבלן עצמו!
השגיאה MessageNotUnderstood: SBEGame>>toggleState התרחשה משום שב-newCellAt:at: נשכחה השורה ^ c, ולכן המתודה החזירה כברירת מחדל את self (עצם ה-SBEGame) לתוך כל תאי המטריצה cells!
הקוד שלנו נשמר בתוך סביבת העבודה, אך כדי לייצא, לגבות או להעביר אותו לסביבה אחרת אנו משתמשים ב-fileOut (היוצר את הקובץ הטקסטואלי SBE-Quinto.st) וב-fileIn entire file לייבוא חזרה.
1. סעיף 5.6: הפעלת המשחק SBEGame ותחקור השגיאה ב-Debugger
איך אנחנו מריצים את המשחק, ולמה לחיצה על תא מקפיצה את MessageNotUnderstood: SBEGame>>toggleState?
הרצת התוכנית שלנו מתבצעת על ידי יצירת עצם חדש מסוג SBEGame ושליחת ההודעה openInWorld לעצם שנוצר (הגורמת לעצם להציג את עצמו בסביבת העבודה). נרשום בחלון העבודה (Workspace) את הביטוי הבא ונבצע אותו באמצעות do it (d):
לפנינו נפתח על המסך לוח המשחק המורכב ממטריצה יפהפייה של $10 \times 10$ תאים מסוג SBECell!
אבל ברגע שנלחץ עם העכבר על אחד התאים בלוח כדי לבדוק אם השכנים שלו מחליפים צבע... בום! קופץ מולנו חלון השגיאות (ה-Debugger) ומודיע שההודעה toggleState שנשלחה לעצם מסוג SBEGame אינה מובנת:
🕵️♂️ בוא נחקור יחד את השגיאה כמו בלשים בתוך ה-Debugger:
- הקשר הביצוע (Stack Trace): חלון ה-Debugger מציג לנו את מחסנית הקריאות בעת שהבעיה התעוררה. אנו רואים שהבעיה התעוררה מתוך המתודה
toggleNeighboursOfCellAt:at:המוגדרת במחלקהSBEGame, בעת ניסיון לשלוח את ההודעהtoggleStateלתאים השכנים. - השאלה המתבקשת: המתודה
toggleStateמוגדרת עבור תא (SBECell, שירש אותה מ-SimpleSwitchMorph). מדוע ההודעהtoggleStateנשלחה לעצם מסוגSBEGameולא לעצם מסוגSBECell?! - מציאת העדות המפלילה: בפקודה
(cells at: i-1 at: j) toggleState, מי שמקבל את ההודעהtoggleStateהוא האיבר שחזר מההודעהat:at:שנשלחה למשתנה המופעcells! אם חזר משם עצם מסוגSBEGame, פירוש הדבר שהמטריצהcellsמכילה את לוח המשחק עצמו במקום את התאים! - שורש הבאג ב-
newCellAt:at:: איך אותחלה המטריצהcellsבמתודהinitializeשלSBEGame? על ידי השורה:cells := Matrix new: n tabulate: [ :i :j | self newCellAt: i at: j].
כלומר, בכל תא במטריצהcellsהושם הערך המוחזר מההודעהnewCellAt:at:! כשנסתכל שוב במתודהnewCellAt:at:שכתבנו בסעיף 5.5, נגלה שהיא יוצרת תאc, מוסיפה אותו ללוח הגרפי ומגדירה לו מיקום ופעולה — אולם שכחנו להחזיר בה את התאcשנוצר!
ומכיוון שברירת המחדל של מתודה ב-Smalltalk ללא^היא להחזיר אתself, המתודהnewCellAt:at:החזירה אתself— שהוא עצם ה-SBEGameשהמתודה שייכת לו!
✅ התיקון המדויק ב-SBEGame>>newCellAt:at:: הוספת ^ c
כדי לפתור את הבעיה לחלוטין, נוסיף בסוף המתודה newCellAt: i at: j את השורה המפורשת ^ c ונשמור באמצעות accept (s):
"create a cell for position (i,j) and add it to the game at appropriate position.
return the new cell"
| c origin |
c := SBECell new.
origin := self innerBounds origin.
self addMorph: c.
c position: ((i-1) * c width) @ ((j-1) * c height) + origin.
c mouseAction: [self toggleNeighboursOfCellAt: i at: j].
^ c
כעת נריץ שוב SBEGame new openInWorld — והמשחק פועל חלק לחלוטין! כל לחיצה משנה את צבע ארבעת השכנים ומטפלת כהלכה גם במקרי הקצה (פינות ושוליים). כשנרצה לסיים ולסגור את המשחק, נציג את הפקדים של ה-morphic halo ונלחץ על ה-x בפינה השמאלית-עליונה כדי להוציא את המשחק מסביבת העבודה.
2. סעיף 5.7: שמירת התכנית וייצוא/ייבוא קוד (fileOut ו-fileIn)
איך מוציאים את הקוד מתוך סביבת העבודה לקובץ SBE-Quinto.st ואיך מייבאים אותו לסביבה אחרת
הקוד שכתבנו שמור בתוך סביבת העבודה של Squeak (בקובצי ה-.image וה-.changes). אבל מה אם נרצה לייצא אותו, לגבות אותו או להעביר אותו לסביבת עבודה אחרת?
📤 ייצוא הקוד החוצה באמצעות fileOut
בחלונית הקטגוריות של ה-System Browser נבחר בקטגוריה SBE-Quinto, נפתח את תפריט ההקשר (כפתור ימני) ונבחר בפעולה fileOut.
כתוצאה מכך יישמר קובץ בשם SBE-Quinto.st בספרייה שממנה הפעלתם את סביבת העבודה! הקובץ הזה הוא קובץ טקסטואלי המכיל את כל הגדרות המחלקות והמתודות שלנו (אם כי הוא מיועד לקריאה ע"י המערכת ואינו מפורמט בצורה נוחה לעיון אנושי ישיר).
📥 ייבוא הקוד פנימה באמצעות fileIn entire file
כדי לייבא קוד שנשמר באמצעות הפעולה fileOut אל תוך סביבת עבודה אחרת של Squeak, כל מה שצריך לעשות הוא לגרור את הקובץ SBE-Quinto.st ישירות אל תוך חלון סביבת העבודה (או לבחור אותו דרך כלי ה-File List), ולבחור באופציה fileIn entire file. הקוד ייטען ויהודר מיד לתוך המערכת!
מדוע חוסר ב-^ c מילא את המטריצה בלוח המשחק עצמו וגרם ל-MessageNotUnderstood
מיני-סימולציה 5.4: סייר שמירת התכנית – fileOut ו-fileIn entire file
מעבדת ה-Debugger החיה: שחזר את הבאג MessageNotUnderstood ותקן אותו עם ^ c!
נסה להריץ את המשחק במצב הראשוני (ללא ^ c בסוף newCellAt:at:) וללחוץ על תא כדי לראות את חלון ה-Debugger ואת תוכן המטריצה cells. לאחר מכן לחץ על "הוסף ^ c ושמור ב-accept (s)" וראה כיצד המטריצה והמשחק מתוקנים מיד!
SBEGame>>newCellAt:at::
cells בזיכרון כרגע:SBEGame new openInWorld):
הגשר הנרטיבי: השלמנו את בניית המשחק Quinto, תיקנו את הבאג ושמרנו ב-fileOut – בוא נסכם את פרק 5!
איזה מסע עשינו יחד בפרק הזה! בנינו מאפס אפליקציה גרפית עובדת ב-Smalltalk וב-Squeak, ראינו איך שתי מחלקות (SBECell ו-SBEGame) משתפות פעולה דרך בלוקים והודעות, ופיצחנו את הבאג המפורסם של החזרת self כברירת מחדל.
בלשונית המסכמת הבאה נרכז את כל הידע של פרק 5 בכרטיסיות זיכרון תלת-ממדיות (3D Flashcards) ונתכונן למעבר לחלק IV (פרק 6: תחביר)!
מילון מושגים אינטראקטיבי וסיכום פרק 5 (הרצת תכנית ראשונה)
סיכום שלם של כל שלבי בניית המשחק Quinto (סעיפים 5.1 עד 5.7), בוחן אבחנה אדריכלי מהיר, ו-12 כרטיסיות זיכרון תלת-ממדיות במצב בחינה ומצב למידה.
מודל מנטלי ועיקרון מנחה: שלושת עמודי התווך של בניית היישום הראשון שלנו ב-Smalltalk
SBECell יורש מ-SimpleSwitchMorph ו-SBEGame יורש מ-BorderedMorph. כל עצם חדש מקבל אוטומטית את ההודעה initialize, שחייבת לפתוח ב-super initialize.
הלוח יוצר מטריצה עם Matrix new: n tabulate: ומפקיד בכל תא בלוק קוד דרך mouseAction:. בשחרור עכבר (mouseUp:) התא מפעיל את הבלוק באמצעות mouseAction value.
מתודה ללא ^ מחזירה את self כברירת מחדל — ולכן חובה לכתוב ^ c ב-newCellAt:at:! את הקוד מייצאים ב-fileOut לקובץ SBE-Quinto.st ומייבאים ב-fileIn entire file.
מחוקי המשחק Quinto והגדרת המחלקות ועד לדיבוג ב-Debugger וייצוא הקוד ב-fileOut
מיני-סימולציה 5.5: בוחן אבחנה מהיר – שלוש שאלות המפתח של פרק 5
כרטיסיות זיכרון תלת-ממדיות (3D Flashcards) לכל 12 מושגי פרק 5
לחץ על כל כרטיסייה כדי להפוך אותה ולבחון את עצמך על כל אחד מ-12 מושגי היסוד של פרק 5:
נחשפו: 0 / 12סיימנו במלואו את פרק 5 ואת חלק III (מבוא לשפת סמולטוק)! מה מחכה לנו בחלק IV?
בפרק 5 חווינו הלכה למעשה איך בונים יישום גרפי מונחה עצמים שלם ב-Smalltalk וב-Squeak. עכשיו, אחרי שראית את האלמנטים האלה בפעולה חיה בתוך המשחק Quinto, החוקים הפורמליים של השפה ייראו לך טבעיים וברורים מאי פעם!
self, super, nil, true, false, thisContext), בלוקים ומבני בקרה!