חלק III: מבוא לסמולטוק • פרק 5 • סעיפים 5.1–5.2 פיתוח אפליקציה ראשונה מתחילתה ועד סופה

המשחק 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

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

  1. SBEGame: תייצג את לוח המשחק השלם, תנהל את לוגיקת המשחק ותחזיק את מטריצת המשבצות.
  2. SBECell: תייצג משבצת (תא) בודדת על גבי הלוח.
📂 יצירת הקטגוריה ב-System Browser: את שתי המחלקות נגדיר תחת קטגוריה חדשה בשם SBE-Quinto (ראשי תיבות: Squeak By Example - Quinto). הוספת הקטגוריה מתבצעת מתוך תפריט ההקשר (לחיצה ימנית / כפתור צהוב) בחלונית הקטגוריות השמאלית ביותר של ה-System Browser ← בחירה באפשרות add item… ← הזנת השם SBE-Quinto ולחיצה על אישור.

2. סעיף 5.2: הגדרת המחלקה SBECell ב-System Browser

איך תבנית יצירת המחלקה ב-Smalltalk שולחת הודעת מילת-מפתח למחלקת-העל SimpleSwitchMorph

ברגע שאנחנו בוחרים בקטגוריה החדשה שלנו SBE-Quinto ב-System Browser, חלונית העריכה המרכזית מציגה לנו תבנית בסיסית ליצירת מחלקה חדשה:

Object subclass: #NameOfSubclass
    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 שלנו:

SimpleSwitchMorph subclass: #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) המתאר את מטרת המחלקה ולשמור שוב.

עזר גרפי 5.1 • מכניקת המשחק Quinto ואנטומיית הגדרת המחלקה SBECell

הפיכת 4 השכנים הצמודים (ללא אלכסונים) וירושה מ-SimpleSwitchMorph

SBE-Quinto Architecture
מכניקת Quinto: לחיצה הופכת 4 שכנים צמודים אלכסון (לא) למעלה Toggle! אלכסון (לא) שמאל Toggle! לחיצה 🖱️ (i, j) ימין Toggle! אלכסון (לא) למטה Toggle! אלכסון (לא) ארכיטקטורת הקטגוריה SBE-Quinto ב-System Browser מחלקת-על: SimpleSwitchMorph (מתג גרפי דו-מצבי ב-Morphic) ⬇ ירושה (subclass: #SBECell) המחלקה SBECell (תא בודד) משתנה מופע: 'mouseAction' שמירה: accept (s) + תיעוד מחלקת-על: BorderedMorph (צורה גרפית בעלת מסגרת ושוליים) ⬇ ירושה (subclass: #SBEGame) המחלקה SBEGame (לוח המשחק) משתנה מופע: 'cells' (Matrix) מנהלת את 10×10 תאי SBECell
🔍

מיני-סימולציה 5.1: מנתח הודעת יצירת המחלקה SBECell ב-System Browser

לחץ על כל רכיב בהגדרת המחלקה כדי שאסביר לך את תפקידו
🕹️
סימולציה אינטראקטיבית גדולה 5.1

מעבדת המשחק Quinto החיה: התנסה בחוקי הפיכת 4 השכנים (ללא אלכסונים)!

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

צהובות: 0 / 25 צעדים: 0
🎛️ בקרת לוח המשחק ומעקב הודעות חי:
⚡

הגשר הנרטיבי: הגדרנו את המחלקה SBECell – אבל איך מאתחלים את הצבע, המסגרת והגודל שלה, ואיך בוחנים אותה חיה על המסך?

יצרנו את שלד המחלקה SBECell שיורשת מ-SimpleSwitchMorph. אבל כרגע התא שלנו עדיין משתמש במראה ברירת המחדל של מתג רגיל ב-Morphic! בלשונית הבאה נכתוב את מתודת האתחול initialize (ונבין למה חובה לפתוח אותה ב-super initialize!), ולאחר מכן ניצור מופע חי של התא, נחקור ונשנה את משתני המופע שלו מתוך ה-Inspector, ונציג אותו על המסך עם openInWorld ועם הילת ה-Morphic Halo!

סוף יחידה 5.1–5.2 • המשחק Quinto והגדרת SBECell
חלק III: מבוא לסמולטוק • פרק 5 • סעיפים 5.3–5.4 אתחול העצם וחקירה חיה

הוספת מתודות למחלקה 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, תוצג לנו בחלון העריכה המרכזי התבנית הכללית ליצירת מתודה:

messageSelectorAndArgumentNames
    "comment stating purpose of message"
    | temporary variable names |
    statements

נחליף את התבנית הזו במתודה initialize של המחלקה SBECell ונשמור באמצעות accept (s):

initialize
    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):

SBECell new    "סמן ובצע inspect it (i)"
🔍 1. חלוניות ה-Inspector ושינוי bounds

בחלונית השמאלית של ה-Inspector מוצגים כל משתני המופע של העצם (כולל אלו שנורשו!), ובחלונית הימנית אפשר לראות ולשנות את ערכו של המשתנה הנבחר. למשל, נבחר במשתנה bounds ונשנה את ערכו כך שפינתו הימנית-תחתונה של המלבן תהיה בנקודה (100,100).

🌍 2. הצגה בעולם החי עם self openInWorld

בחלונית התחתונה של ה-Inspector (שבה המשתנה self מתייחס ישירות לתא הנבדק!) נשלח לעצם את ההודעה self openInWorld ונבצע אותה באמצעות do it (d). מיד נראה את התא מופיע בפינה השמאלית-עליונה של סביבת העבודה, ולחיצה עליו בכפתור שמאלי של העכבר תראה איך הוא מגיב ומחליף צבע!

✨ 3. הילת Morphic Halo והסרה ב-x

לחיצה על התא באמצעות הכפתור האמצעי של העכבר תציג סביבו את ה-morphic halo: כעת נוכל להזיז את העצם ולשנות את גודלו על המסך, ולראות ב-Inspector כיצד משתני המופע מתעדכנים מיד לפי מצבו החדש! לבסוף, לחיצה על ה-x בפינה השמאלית-עליונה של ה-morphic halo תוציא את העצם מסביבת העבודה.

עזר גרפי 5.2 • תהליך האתחול ב-initialize והקשר הדו-כיווני עם ה-Inspector

מנקודות ומלבנים (0@0 corner: 16@16) ועד מניפולציה חיה עם Morphic Halo

Initialize & Inspector
1. יצירת מלבן הגבולות (bounds) bounds := 0@0 corner: 16@16 0@0 (Point) SBECell (16×16) offColor: paleYellow 16@16 (או 100@100) פינות ישרות (useSquareCorners) + borderWidth: 2 2. סנכרון דו-כיווני חי: חלון ה-Inspector ⇄ העצם ב-World עם Morphic Halo Inspector: a SBECell self bounds offColor onColor 0@0 corner: 100@100 self openInWorld (do it) ⇄ Morphic Halo (כפתור אמצעי) x a SBECell חי! לחיצה על x מסירה מהעולם
🔬

מיני-סימולציה 5.2: הרצה צעד-אחר-צעד של 8 השורות במתודה SBECell>>initialize

לחץ על כל שורת קוד כדי לראות איך היא מעצבת את התא
🔎
סימולציה אינטראקטיבית גדולה 5.2

מעבדת ה-Inspector וה-Morphic Halo החיה: בחן ושנה עצם מסוג SBECell בזמן אמת!

בצע SBECell new ב-Inspector: בחר משתני מופע בחלונית השמאלית, שנה את bounds ל-0@0 corner: 100@100, הרץ self openInWorld בחלונית התחתונה, לחץ על התא כדי להחליף בין כבוי לדלוק, והפעל את ה-Morphic Halo (כולל כפתור ה-x השחור להסרת העצם מהעולם)!

🔍 Inspector: a SBECell SBECell new (inspect it)
משתני מופע (בחר):
ערך המשתנה הנבחר (חלונית ימנית):
a SBECell
חלונית עבודה תחתונה ב-Inspector (self = התא הנבדק): Ctrl+d / do it (d)
self openInWorld
🌍 Squeak World (פינה שמאלית-עליונה)
לחץ על "do it (d)" כדי להריץ self openInWorld ולהציג את העצם בעולם!
⚡

הגשר הנרטיבי: התא הבודד SBECell עובד נהדר – עכשיו איך בונים את לוח המשחק השלם SBEGame ומחברים ביניהם?

ראינו איך ליצור, לאתחל ולבחון תא בודד מסוג SBECell. עכשיו הגיע הזמן לבנות את המחלקה השנייה שלנו: SBEGame! איך יוצרים לוח שמכיל מטריצה של $10 \times 10$ תאים באמצעות Matrix new: n tabulate:? ואיך גורמים לתא בודד (שלא מכיר את לוח המשחק!) להפעיל בלחיצת עכבר את הפיכת השכנים בלוח באמצעות בלוק קוד (mouseAction) וההודעה value? בוא נעבור ליחידה 5.5!

חלק III: מבוא לסמולטוק • פרק 5 • סעיף 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:

BorderedMorph subclass: #SBEGame
    instanceVariableNames: ''
    classVariableNames: ''
    poolDictionaries: ''
    category: 'SBE-Quinto'

שים לב שבשלב הראשוני השארנו את רשימת משתני המופע ריקה (''), ומיד נראה כיצד סביבת Squeak תעזור לנו להוסיף לשם את משתנה המופע cells באופן אוטומטי כשנשמור את מתודת האתחול! בוא נכתוב כעת את המתודה initialize של SBEGame (בפרוטוקול --all--):

initialize
    | 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:

1. פרוטוקול initialization ב-SBEGame

המתודה cellsPerSide וכלל ברירת המחדל self

מחזירה את מספר התאים בכל שורה ועמודה של המטריצה (10) באמצעות הסימן ^. זכור כלל יסוד בשפה: אם מתודה אינה כוללת ^, ברירת המחדל היא שהיא מחזירה את self!

cellsPerSide
    "The number of cells in each row and column"
    ^ 10
2. פרוטוקול חדש game logic ב-SBEGame

המתודה toggleNeighboursOfCellAt: i at: j

בודקת תנאי שפה (כדי לא לחרוג מגבולות המטריצה), שולפת כל שכן מ-cells עם at:at: ושולחת לו toggleState. (נסווג בפרוטוקול חדש game logic באמצעות new category...).

toggleNeighboursOfCellAt: i at: j
    "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].
3. פרוטוקול initialization ב-SBEGame (הגרסה הראשונית בסעיף 5.5) שים לב לשורה האחרונה שמעבירה בלוק קוד לתא!

המתודה newCellAt: i at: j – יצירת התא, מיקומו בלוח והגדרת פעולת הלחיצה

מתודה זו מקבלת מספר שורה i ועמודה j, יוצרת תא חדש c := SBECell new, מאתרת את נקודת המוצא הפנימית של הלוח (origin := self innerBounds origin), מוסיפה את התא כרכיב גרפי מוכל בלוח (self addMorph: c), ממקמת אותו לפי קואורדינטות השורה והעמודה שלו, ובשורה האחרונה שולחת לתא את ההודעה mouseAction: עם בלוק קוד שירוץ בעת לחיצת עכבר:

newCellAt: i at: j
    "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 שתי מתודות:

1. מתודת השמה בפרוטוקול accessing:
mouseAction: aBlock
    "set mouseAction to the block argument"
    mouseAction := aBlock
2. מתודת אירוע עכבר בפרוטוקול event handling:
mouseUp: anEvent
    "execute mouseAction code"
    mouseAction value

המתודה mouseUp: מופעלת באופן אוטומטי על ידי סביבת העבודה Squeak (תשתית Morphic) ברגע שהמשתמש משחרר את כפתור העכבר מעל התא! שליחת ההודעה value לעצם הבלוק השמור ב-mouseAction גורמת לביצוע הקוד של הבלוק — ומפעילה את self toggleNeighboursOfCellAt: i at: j בלוח המשחק!

עזר גרפי 5.3 • מעגל התקשורת בין הלוח לתא באמצעות בלוק קוד (Closure)

כיצד SBEGame מפקיד בלוק ב-mouseAction, וכיצד mouseUp: מפעיל אותו עם ההודעה value

Block Callback Cycle
1. לוח המשחק (a SBEGame) בעת יצירת תא ב-newCellAt: i at: j: c mouseAction: [self toggleNeighboursOfCellAt: i at: j] 3. כשהבלוק מופעל, הלוח מבצע: (cells at: ... at: ...) toggleState בודק תנאי שפה (i>1, i<10, j>1, j<10) שלב א': מפקיד בלוק דרך mouseAction: ⟵ ⟶ שלב ב': mouseAction value מריץ את הבלוק! 2. התא הבודד (a SBECell) משתנה מופע פרטי: mouseAction mouseAction: aBlock mouseAction := aBlock 🖱️ בעת שחרור עכבר (Morphic): mouseUp: anEvent mouseAction value
📐

מיני-סימולציה 5.3: מחשבון מיקום תא (position:) ובדיקת תנאי שפה (toggleNeighboursOfCellAt:at:)

בחר אינדקסים (i, j) כדי לראות אילו תנאי ifTrue: מתקיימים!
⚡
סימולציה אינטראקטיבית גדולה 5.3

מעבדת זרימת ההודעות והבלוק: עקוב צעד-אחר-צעד אחר הקשר שבין 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!

חלק III: מבוא לסמולטוק • פרק 5 • סעיפים 5.6–5.7 הרצה, איתור באגים ב-Debugger וייצוא קוד

הפעלת המשחק 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):

SBEGame new openInWorld    "סמן ובצע do it (d)"

לפנינו נפתח על המסך לוח המשחק המורכב ממטריצה יפהפייה של $10 \times 10$ תאים מסוג SBECell! אבל ברגע שנלחץ עם העכבר על אחד התאים בלוח כדי לבדוק אם השכנים שלו מחליפים צבע... בום! קופץ מולנו חלון השגיאות (ה-Debugger) ומודיע שההודעה toggleState שנשלחה לעצם מסוג SBEGame אינה מובנת:

MessageNotUnderstood: SBEGame>>toggleState

🕵️‍♂️ בוא נחקור יחד את השגיאה כמו בלשים בתוך ה-Debugger:

  1. הקשר הביצוע (Stack Trace): חלון ה-Debugger מציג לנו את מחסנית הקריאות בעת שהבעיה התעוררה. אנו רואים שהבעיה התעוררה מתוך המתודה toggleNeighboursOfCellAt:at: המוגדרת במחלקה SBEGame, בעת ניסיון לשלוח את ההודעה toggleState לתאים השכנים.
  2. השאלה המתבקשת: המתודה toggleState מוגדרת עבור תא (SBECell, שירש אותה מ-SimpleSwitchMorph). מדוע ההודעה toggleState נשלחה לעצם מסוג SBEGame ולא לעצם מסוג SBECell?!
  3. מציאת העדות המפלילה: בפקודה (cells at: i-1 at: j) toggleState, מי שמקבל את ההודעה toggleState הוא האיבר שחזר מההודעה at:at: שנשלחה למשתנה המופע cells! אם חזר משם עצם מסוג SBEGame, פירוש הדבר שהמטריצה cells מכילה את לוח המשחק עצמו במקום את התאים!
  4. שורש הבאג ב-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):

newCellAt: i at: j
    "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. הקוד ייטען ויהודר מיד לתוך המערכת!

עזר גרפי 5.4 • אנטומיית הבאג במטריצה cells: החזרת self לעומת החזרת ^ c

מדוע חוסר ב-^ c מילא את המטריצה בלוח המשחק עצמו וגרם ל-MessageNotUnderstood

Default Return ^self Bug
❌ לפני התיקון: שכחנו ^c בסוף newCellAt:at: ברירת המחדל מחזירה את self (= a SBEGame)! cells(1,1) = a SBEGame! cells(1,2) = a SBEGame! cells(i,j) = a SBEGame! (cells at: i-1 at: j) toggleState 💥 MessageNotUnderstood: SBEGame>>toggleState ✅ אחרי התיקון: הוספנו ^ c בסוף newCellAt:at: כל תא במטריצה cells מחזיק את ה-SBECell שלו! cells(1,1) = a SBECell cells(1,2) = a SBECell cells(i,j) = a SBECell (cells at: i-1 at: j) toggleState 🎉 התא השכן מבין toggleState ומחליף את צבעו!
💾

מיני-סימולציה 5.4: סייר שמירת התכנית – fileOut ו-fileIn entire file

לחץ על כל פעולה כדי לראות כיצד הקוד נשמר ונטען מחדש
🐞
סימולציה אינטראקטיבית גדולה 5.4

מעבדת ה-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: תחביר)!

חלק III: מבוא לסמולטוק • פרק 5 • יחידה 5.8 אינטגרציה ואינדקס מושגים

מילון מושגים אינטראקטיבי וסיכום פרק 5 (הרצת תכנית ראשונה)

סיכום שלם של כל שלבי בניית המשחק Quinto (סעיפים 5.1 עד 5.7), בוחן אבחנה אדריכלי מהיר, ו-12 כרטיסיות זיכרון תלת-ממדיות במצב בחינה ומצב למידה.

🏆
🧠

מודל מנטלי ועיקרון מנחה: שלושת עמודי התווך של בניית היישום הראשון שלנו ב-Smalltalk

💡 1. ירושה ואתחול (5.1–5.4)

SBECell יורש מ-SimpleSwitchMorph ו-SBEGame יורש מ-BorderedMorph. כל עצם חדש מקבל אוטומטית את ההודעה initialize, שחייבת לפתוח ב-super initialize.

⚖️ 2. מטריצה, בלוקים ואירועים (5.5)

הלוח יוצר מטריצה עם Matrix new: n tabulate: ומפקיד בכל תא בלוק קוד דרך mouseAction:. בשחרור עכבר (mouseUp:) התא מפעיל את הבלוק באמצעות mouseAction value.

🎯 3. החזרת ערך, דיבוג וייצוא (5.6–5.7)

מתודה ללא ^ מחזירה את self כברירת מחדל — ולכן חובה לכתוב ^ c ב-newCellAt:at:! את הקוד מייצאים ב-fileOut לקובץ SBE-Quinto.st ומייבאים ב-fileIn entire file.

עזר גרפי 5.5 • מפת הדרכים המלאה של פרק 5 (סעיפים 5.1 עד 5.7)

מחוקי המשחק Quinto והגדרת המחלקות ועד לדיבוג ב-Debugger וייצוא הקוד ב-fileOut

Chapter 5 Synthesis
1. התא SBECell (5.1–5.4) • יורש מ-SimpleSwitchMorph • משתנה מופע: mouseAction • initialize + super initialize • 0@0 corner: 16@16 • Inspector + Morphic Halo ⟵ 2. הלוח SBEGame (5.5) • יורש מ-BorderedMorph • cells := Matrix new: n tabulate: • cellsPerSide (^ 10) • newCellAt:at: + position: • toggleNeighboursOfCellAt:at: ⟵ 3. אירועים ודיבוג (5.5–5.6) • mouseAction: (accessing) • mouseUp: ➔ mouseAction value • באג: החזרת self ללא ^ • SBEGame>>toggleState • התיקון: הוספת ^ c! ⟵ 4. שמירה וייבוא (5.7) • קטגוריה: SBE-Quinto • ייצוא: fileOut • קובץ: SBE-Quinto.st • ייבוא: fileIn entire file
⚡

מיני-סימולציה 5.5: בוחן אבחנה מהיר – שלוש שאלות המפתח של פרק 5

לחץ על כל שאלה לבדיקת הבנה
🃏
סימולציה אינטראקטיבית גדולה 5.5

כרטיסיות זיכרון תלת-ממדיות (3D Flashcards) לכל 12 מושגי פרק 5

לחץ על כל כרטיסייה כדי להפוך אותה ולבחון את עצמך על כל אחד מ-12 מושגי היסוד של פרק 5:

נחשפו: 0 / 12
🚀

סיימנו במלואו את פרק 5 ואת חלק III (מבוא לשפת סמולטוק)! מה מחכה לנו בחלק IV?

בפרק 5 חווינו הלכה למעשה איך בונים יישום גרפי מונחה עצמים שלם ב-Smalltalk וב-Squeak. עכשיו, אחרי שראית את האלמנטים האלה בפעולה חיה בתוך המשחק Quinto, החוקים הפורמליים של השפה ייראו לך טבעיים וברורים מאי פעם!

🎯 התחנה הבאה שלנו – חלק IV: פיתוח תוכנה בשפת סמולטוק, פרק 6: תחביר: בפרק 6 נלמד באופן שיטתי ומעמיק את כללי התחביר של Smalltalk: ליטרלים, תווים, סימבולים, משתנים-פסאודו (self, super, nil, true, false, thisContext), בלוקים ומבני בקרה!
🕹️

מעבדת חקר אינטראקטיבית

❓

5 שאלות פתוחות מדורגות + 5 שאלות אמריקאיות עם הסברים מלאים לכל יחידה

🚀 התחנה הבאה שלנו: חלק IV – פרק 6: תחביר

סיימנו במלואו את פרק 5: הרצת תכנית ראשונה (המשחק Quinto) ואת חלק III (מבוא לשפת סמולטוק)! החלק הבא שלנו הוא חלק IV: פיתוח תוכנה בשפת סמולטוק, הפותח בפרק 6: תחביר: