איקס עיגול בClojure – חלק א׳ לוגיקה

איקס עיגול בClojure – חלק א׳ לוגיקה

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

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

על מנת לשמור על האתגר קטן בחרתי להקל על עצמי בדברים הבאים:

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

וכעת ללא הקדמות נוספות נעבור למימוש.

חלק א: לוח, מהלך וסיום משחק

הפונקציה הראשונה יוצרת לוח משחק בסיסי:

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

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

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

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

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

שימו לב שאנחנו פונים לאיבר בלוח ע״י הוצאת הנקודות מהווקטור p* ושימוש בהם על הווקטור של הווקטורים board, כמו כן שימו לב לכתיב הפולני של ההשוואה. הפונקציה if בודקת את הפרמטר הראשון ואם הוא חיובי אזי היא מחזירה את הפרמטר השני (במקרה שלנו מה שיש בלוח במיקום p0) אחרת היא מחזירה את הפרמטר השלישי (האיבר הריק).

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

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

כאן אנחנו יוצרים ווקטור חדש גדול מכל הווקטורים הקטנים ובודקים האם לא קיים שם איבר ריק.

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

חלק ב: האלגוריתם הטיפש

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

הפונקציה int_to_cord מקבלת מספר ומחזירה לנו וקטור עם המיקום של התא בלוח – כך שאפשר להשתמש בפונקציה של range כדי לייצר מערך של כל המספרים מ0 עד 9 ואז בעזרת map אנחנו מקבלים את מערך של ווקטורים עם כל המיקומים על הלוח. והפונקציה is_cord_empty מקבלת את הלוח והמיקום ומחזירה האם הוא ריק. 

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

הפונקציה partial מקבעת את המשתנה הראשון (כמו bind בjavaScript) ומחזירה פונקציה שמקבלת משתנה אחד.

נסיים בהוספה של אלגוריתם מתוחכם שבוחר מהלך רנדומלי מכל המהלכים האפשריים:

חלק ג (ואחרון): לולאת המשחק

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

נתחיל בפונקציה שמזהה תור איזה שחקן, על פי בדיקה האם מספר הX על הלוח שווה למספר הO על הלוח:

נוסיף פונקציית דיבאג שמדפיסה יפה את הלוח (אחרת איך נדע שמשהו באמת קרה…):

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

זהו סיימנו, שמתם לב שהצלחנו לעשות את כל זה בלי להגדיר משתנה אחד?

מחשבות לעתיד
  • לכתוב שרת שמתחבר ללוגיקה כך שיהיה אפשר לשחק מול המחשב דרך קריאות API.
  • להוסיף את הקוד לgithub.
  • לכתוב אלגוריתם יותר חכם.
  • לכתוב עוד פוסטים…

כתיבת תגובה

האימייל לא יוצג באתר. שדות החובה מסומנים *

*