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

בתקופה האחרונה השתעשעתי בללמוד Clojure שהיא השפת תכנות הפונקציונאלי הראשונה שיוצא לי להתעסק איתה ברצינות. כמתכנת עם קצת פז״ם חטפתי שוק רציני, מלנסות לכתוב דברים פשוטים בפרדיגמה השונה ולחשוב על הכול במובנים של פונקציות ולא להשתמש במשתנים.
על מנת לאתגר את עצמי החלטתי לכתוב משחק איקס-עיגול קטן ופשוט בשפה. בפוסט הזה אציג את פונקציות הלוגיקה הבסיסיות של המשחק שכתבתי, עם הסברים קצרים כך שכל המעוניין יוכל לטעום קצת את השפה. העדפתי לא לכתוב על הפילוסופיה של השפה או מדריך בסיסי מכיוון שאני עדיין לא שולט/ מבין ברמה מספקת למדריך מסוג כזה. במקום זה אני פשוט מראה איך אני כמתכנת שלומד שפה ופרדיגמה חדשה מתמודד עם האתגר החדש.
על מנת לשמור על האתגר קטן בחרתי להקל על עצמי בדברים הבאים:
- אסטרטגיית משחק טיפשית – המחשב בוחר באופן רנדומלי מהלך מתוך כל המהלכים החוקיים.
- משחק עם גודל לוח קבוע (של שלוש על שלוש).
- המחשב משחק רק כנגד עצמו.
וכעת ללא הקדמות נוספות נעבור למימוש.
חלק א: לוח, מהלך וסיום משחק
הפונקציה הראשונה יוצרת לוח משחק בסיסי:
1 2 3 4 5 6 |
; get initial board (defn get_initial_board [] [[nil,nil,nil], [nil,nil,nil], [nil,nil,nil]]) (get_initial_board) ; => [[nil nil nil] [nil nil nil] [nil nil nil]] |
השורה הראשונה זה הדרך בא אנחנו כותבים הערות בקוד. השורה השנייה מגדירה את הפונקציה (המילה השמורה defn) אח״כ מגיע השם של הפונקציה ובסוף מגיעה ווקטור שמכיל את הפרמטרים. ב Clojure השורה האחרונה בפונקציה זה הערך שחוזר למי שקורא לפונקציה, וכאן אנחנו פשוט מחזירים וקטור שמכיל שלושה וקטורים שבכל אחד מהם יש שלוש ערכים ריקים (null).
בשורה לפני האחרונה אנחנו קוראים לפונקציה כאשר הקריאה מתבצעת ע״י סוגריים, שם הפונקציה ולאחר מכן הפרמטרים. ובשורה האחרונה העתקתי את הפלט של הפונקציה בהערה. כפי שאתם רואים בווקטור שחזר מהפונקציה – אין צורך לסמן פסיקים בין האיברים השונים בווקטור (אבל אני מניח שיהיה לי קשה להיפרד מההרגל הזה).
הפונקציה הבאה היא פונקציה שמבצעת מהלך על הלוח – כאשר כיאה לשפת תכנות פונקציונאלית (וגם כי אין דרך אחרת) אנחנו לא משנים את הלוח המקורי אלא מחזירים לוח חדש עם המהלך שביצענו.
1 2 3 4 5 6 7 |
; place player move (borad, player, place) => board (defn move [board, player, place] (assoc board (place 0) (assoc (board (place 0)) (place 1) player))) (move (get_initial_board) "X" [1 1]) ; => [[nil nil nil] [nil "X" nil] [nil nil nil]] |
הפונקציה מקבלת את הלוח הראשוני, הסימן של השחקן – ווקטור אם המיקום של המהלך שהוא רוצה לבצע. הפקודה assoc יוצרת שכפול של הווקטור כאשר היא מקבלת שלושה פרמטרים: הווקטור המקורי, האינדקס אותו אנחנו מחליפים והפרמטר האחרון הוא במה אנחנו מחליפים אותו. שימו לב שאנחנו ניגשים לאיבר בתוך הווקטור ע״י קריאה לווקטור כמו לפונקציה עם האינדקס אליו אנחנו פונים. בתכל׳ס הפונקציה מחליפה את השורה בלוח (החדש) בשורה שבה אנחנו מחליפים את העמודה (החדשה) בסימן של השחקן.
הדבר הבא שבחרתי לממש זה חוקיות שבודקת האם יש שחקן שניצח במשחק. לצורך קח מימשתי פונקציית עזר קטנה שמקבלת את הלוח ושלוש מיקומים והיא בודקת האם המיקום הראשון לא ריק והאם בשני המיקומים האחרים יש את אותו הסימן ואם כן היא מחזירה את הסימן – אחרת היא מחזירה null.
1 2 3 4 5 6 7 8 9 10 11 |
(defn check_winner_cord [board, p0, p1, p2] (if (and (not (nil? ((board (p0 0)) (p0 1)) )) (= ((board (p0 0)) (p0 1)) ((board (p1 0)) (p1 1))) (= ((board (p0 0)) (p0 1)) ((board (p2 0)) (p2 1)))) ((board (p0 0)) (p0 1)) nil)) (check_winner_cord (get_initial_board), [0 0], [0 1], [0 2]) ; => nil (check_winner_cord [[nil,nil,nil], ["X","X","X"], [nil,nil,nil]], [1 0], [1 1], [1 2]) ; => "X" |
שימו לב שאנחנו פונים לאיבר בלוח ע״י הוצאת הנקודות מהווקטור p* ושימוש בהם על הווקטור של הווקטורים board, כמו כן שימו לב לכתיב הפולני של ההשוואה. הפונקציה if בודקת את הפרמטר הראשון ואם הוא חיובי אזי היא מחזירה את הפרמטר השני (במקרה שלנו מה שיש בלוח במיקום p0) אחרת היא מחזירה את הפרמטר השלישי (האיבר הריק).
בעזרת הפונקציה הזאת (ועם הכנסה ידנית של כל האפשרויות ליצור רצף) ניתן להגדיר פונקציה שבודקת מי ניצח בלוח:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
(defn check_winner [board] (or (check_winner_cord board, [0 0], [0 1], [0 2]) (check_winner_cord board, [1 0], [1 1], [1 2]) (check_winner_cord board, [2 0], [2 1], [2 2]) (check_winner_cord board, [0 0], [1 0], [2 0]) (check_winner_cord board, [0 1], [1 1], [2 1]) (check_winner_cord board, [0 2], [1 2], [2 2]) (check_winner_cord board, [0 0], [1 1], [2 2]) (check_winner_cord board, [0 2], [1 1], [2 0]))) (check_winner [["X","O",nil], [nil,"X","X"], [nil,"X","O"]] ; => nil (check_winner [["X","O","O"], [nil,"O","X"], ["O","X","O"]]) ; => "O" |
משחק יכול להסתיים גם בתיקו כאשר הלוח מתמלא, ניתן לבדוק זאת בעזרת הפונקציה:
1 2 3 4 5 6 7 8 |
(defn is_board_full [board] (not (some nil? (apply concat board)))) (is_board_full [["X","O","O"], ["X","O","X"], ["O","X","O"]]) ; => true (is_board_full [["X","O","O"], ["X","O","X"], ["O",nil,"O"]]) ; => false |
כאן אנחנו יוצרים ווקטור חדש גדול מכל הווקטורים הקטנים ובודקים האם לא קיים שם איבר ריק.
וכעת אפשר לממש פונקציה שבודקת האם המשחק נגמר ע״י בדיקה האם הלוח מלא או שיש מנצח:
1 2 3 4 5 6 7 8 9 10 11 12 |
(defn is_game_ended [board] (or (is_board_full board) (not (= nil (check_winner board))))) (is_game_ended [["X","O",nil], [nil,"X","X"], [nil,"X","O"]]) ; => false (is_game_ended [["X","X","X"], [nil,nil,nil], [nil,nil,nil]]) ; => true (is_game_ended [["X","X","O"], ["X","X","X"], ["O","X","O"]]) ; => true (is_game_ended [["1","2","3"], ["4","5","6"], ["7","8","9"]]) ; => true |
חלק ב: האלגוריתם הטיפש
כעת נעבור למימוש של האלגוריתם של המשחק. בשלב הראשון אני מעוניין לקבל רשימה של מהלכים אפשרים (המיקומים שעדיין ריקים על הלוח) מימשתי את זה בעזרת שתי פונקציות עזר קטנות:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
(defn int_to_cord [x] [(int (/ x 3)) (mod x 3)]) (int_to_cord 4) ; => [1 1] (map int_to_cord (range 9)) ; => ([0 0] [0 1] [0 2] [1 0] [1 1] [1 2] [2 0] [2 1] [2 2]) (defn is_cord_empty [board, place] (nil? ((board (place 0)) (place 1)))) (is_cord_empty [[nil,nil,nil], [nil,nil,nil], [nil,nil,nil]] [0 0]) ; => true (is_cord_empty [["X",nil,nil], [nil,nil,nil], [nil,nil,nil]] [0 0]) ; => false |
הפונקציה int_to_cord מקבלת מספר ומחזירה לנו וקטור עם המיקום של התא בלוח – כך שאפשר להשתמש בפונקציה של range כדי לייצר מערך של כל המספרים מ0 עד 9 ואז בעזרת map אנחנו מקבלים את מערך של ווקטורים עם כל המיקומים על הלוח. והפונקציה is_cord_empty מקבלת את הלוח והמיקום ומחזירה האם הוא ריק.
כעת כל מה שנותר זה לפלטר מכל המיקומים את כל המיקומים שעדיין לא נתפסו.
1 2 3 4 5 6 |
(defn get_all_posible_moves [board] (filter (partial is_cord_empty board) (map int_to_cord (range 9)))) (get_all_posible_moves [["X","O",nil], [nil,"X","X"], [nil,"X","O"]]) ; => ([0 2] [1 0] [2 0]) |
הפונקציה partial מקבעת את המשתנה הראשון (כמו bind בjavaScript) ומחזירה פונקציה שמקבלת משתנה אחד.
נסיים בהוספה של אלגוריתם מתוחכם שבוחר מהלך רנדומלי מכל המהלכים האפשריים:
1 2 3 4 5 6 |
(defn get_random_move [board] (rand-nth (get_all_posible_moves board))) (get_random_move (get_initial_board)) ; => [0 0] (get_random_move (get_initial_board)) ; => [1 2] |
חלק ג (ואחרון): לולאת המשחק
ועכשיו כל מה שנשאר זה לכתוב פונקציונליות שתתחיל משחק על לוח ריק תיתן למחשב לשחק את שני הצדדים עד שהמשחק מסתיים ותדווח מי ניצח.
נתחיל בפונקציה שמזהה תור איזה שחקן, על פי בדיקה האם מספר הX על הלוח שווה למספר הO על הלוח:
1 2 3 4 5 6 7 8 9 |
(defn get_current_player [board] (if (= (get (frequencies (apply concat board)) "X" 0) (get (frequencies (apply concat board)) "O" 0)) "X" "O")) (get_current_player [[nil,nil,nil], [nil,nil,nil], [nil,"X",nil]]) ; => "O" (get_current_player [[nil,nil,nil], [nil,"O",nil], ["X",nil,nil]]) ; => "X" |
נוסיף פונקציית דיבאג שמדפיסה יפה את הלוח (אחרת איך נדע שמשהו באמת קרה…):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
(defn print_cell [cell] (print " ") (if (nil? cell) (print " ") (print cell)) (print " ")) (defn print_row [row] (print_cell (row 0)) (print "|") (print_cell (row 1)) (print "|") (print_cell (row 2)) (print "\n")) ; print board (defn print_board [board] (print_row [nil,nil,nil]) (print_row (board 0)) (print "_____|_____|_____\n") (print_row [nil,nil,nil]) (print_row (board 1)) (print "_____|_____|_____\n") (print_row [nil,nil,nil]) (print_row (board 2)) (print_row [nil,nil,nil])) (print_board [[nil,nil,nil], [nil,nil,nil], ["X","X","O"]]) ; => ; | | ; | | ; _____|_____|_____ ; | | ; | | ; _____|_____|_____ ; | | ; X | X | O ; | | |
וכעת בעזרת רקורסיה נממש את הפונקציה הראשית של המשחק: אנחנו בודקים האם המשחק נגמר – במידה וכן אנחנו מכריזים על המנצח – אחרת אנחנו יוצרים לוח עם המהלך הבא ושולחים אותו ברקורסיה לאותה הפונקציה.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
; Game Loop: ; print board ; if game ended print winner ; else play game on board that you make move on him (defn play [board] (print_board board) (if (is_game_ended board) (print "The winner is:" (check_winner board)) (play (move board (get_current_player board) (get_random_move board))))) (play (get_initial_board)) ; | | ; | | ; _____|_____|_____ ; | | ; | | ; _____|_____|_____ ; | | ; | | ; | | ; | | ; | | ; _____|_____|_____ ; | | ; X | | ; _____|_____|_____ ; | | ; | | ; | | ; | | ; | | ; _____|_____|_____ ; | | ; X | | ; _____|_____|_____ ; | | ; O | | ; | | ; | | ; X | | ; _____|_____|_____ ; | | ; X | | ; _____|_____|_____ ; | | ; O | | ; | | ; | | ; X | | ; _____|_____|_____ ; | | ; X | | O ; _____|_____|_____ ; | | ; O | | ; | | ; | | ; X | X | ; _____|_____|_____ ; | | ; X | | O ; _____|_____|_____ ; | | ; O | | ; | | ; | | ; X | X | ; _____|_____|_____ ; | | ; X | O | O ; _____|_____|_____ ; | | ; O | | ; | | ; | | ; X | X | X ; _____|_____|_____ ; | | ; X | O | O ; _____|_____|_____ ; | | ; O | | ; | | ; The winner is: X |
זהו סיימנו, שמתם לב שהצלחנו לעשות את כל זה בלי להגדיר משתנה אחד?
מחשבות לעתיד
- לכתוב שרת שמתחבר ללוגיקה כך שיהיה אפשר לשחק מול המחשב דרך קריאות API.
- להוסיף את הקוד לgithub.
- לכתוב אלגוריתם יותר חכם.
- לכתוב עוד פוסטים…
כתיבת תגובה