משחק זיכרון ב ClojureScript

18/02/2022

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

1. איך מתחילים

בשביל לקבל סטארטר לכתיבת אפליקציית ריאייג'נט (שזה הריאקט של קלוז'רסקריפט) אתם צריכים להתקין כלי בשם lein מכאן: https://leiningen.org/

ואחר כך מהמסוף כותבים:

$ lein new reagent my-reagent-app

זה נותן תבנית לפרויקט עם מספר דפים. קובץ הממשק נקרא src/cljs/my_reagent_app/core.cljs. בהתחלה יש בו הרבה תוכן לא רלוונטי ואפשר למחוק את הרוב ולהישאר עם הקוד הזה בשביל לקבל יישום פשוט:

(ns my-reagent-app.core
  (:require
   [reagent.core :as reagent :refer [atom]]
   [reagent.dom :as rdom]
   [my-reagent-app.util :as util]
   [my-reagent-app.lib.cards :as cards]))

(defn home-page []
  (fn []
    [:span.main
     [:h1 "Hello World"]]))

(defn mount-root []
  (rdom/render [home-page] (.getElementById js/document "app")))

(defn init! []
  (mount-root))

מתיקיית הפרויקט שנוצר מפעילים:

$ npm install
$ npx shadow-cljs watch app

ואז אפשר להיכנס בדפדפן לפורט 3000 על localhost כדי לראות את הדף הראשון והפשוט מהאפליקציה שבסך הכל כותב Hello World.

2. קוד למשחק זיכרון

אחרי שהיתה לי את התבנית המשכתי לכתוב את קוד הקלוז'ר למשחק ושמרתי אותו בקובץ src/cljs/my_reagent_app/lib/cards.cljs.

משחק זיכרון כולל חבילה של קלפים, כך שכל קלף הוא אוביקט עם המפתחות id, value, found ו visible. המשמעות של value היא המספר שכתוב על הקלף (במשחק זיכרון יש זוגות של קלפים עם אותו מספר), המפתח visible מתאר האם עכשיו הקלף גלוי או מוחבא, המפתח found מתאר האם מצאתי את הקלף הזה ואת הזוג שלו והמפתח id כולל מזהה ייחודי לקלף.

כל הקלפים מתחילים מוסתרים, לחיצה על קלף מציגה אותו, לחיצה על קלף נוסף מציגה גם אותו, ואז אם שני הקלפים זהים הם יישארו גלויים וה found של שניהם יוגדר ל true, ואם הם שונים אז שניהם יוסתרו חזרה אחרי שתי שניות.

זה הקוד בקלוז'ר למשחק:

(ns my-reagent-app.lib.cards
  (:require [reagent.core :as r]))

(def cards (r/atom (shuffle 
                     (map-indexed (fn [id v]
                                    {
                                     :id id
                                     :value v
                                     :found false
                                     :visible false})
                          (flatten (take 2 (repeat (range 5))))))))

(def timeout (r/atom nil))

(defn show [id]
  (fn [item]
    (if (== (:id item) id)
      (conj item {:visible true})
      item)))

(defn new-turn [cards]
  (map #(conj % {:visible false}) cards))

(defn reveal [card1 card2]
  (fn [cards]
    (map #(if (or
                (== (:id %) (:id card1))
                (== (:id %) (:id card2)))
            (conj % {:found true :visible false})
            %) cards)))


(defn check-pairs [id]
  (let [other (first (filter :visible @cards))
        this  (first (filter #(== (:id %) id) @cards))]
    (if (== (:value  other) (:value this))
      (swap! cards (reveal other this))
      (do
        (swap! cards #(map (show id) %))
        (reset! timeout (js/setTimeout #(swap! cards new-turn) 2000))))))

(defn click-on [id]
    (if (== 2 (count (filter :visible @cards))) (do
                     (js/clearTimeout @timeout)
                     (swap! cards new-turn)))

    (let [card (first (filter #(== (:id %) id) @cards))
          in-turn (some :visible @cards)]
      (cond
        in-turn (check-pairs id)
        (:found card) false
        (:visible card) false
        :else (swap! cards #(map (show id) %)))))

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

  1. הפקודה r/atom יוצרת אטום, שזה דבר ששומר מידע שיכול להשתנות. רוב המידע בקלוז'ר הוא Immutable, ובדרך כלל יהיה לנו אטום גדול או מספר אטומים כדי להחזיק את המידע המשתנה של היישום. אפשר לחשוב על האטום כמו Store ברידאקס או מובאקס.

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

  3. הפונקציה click-on, שכנראה היתה צריכה להיקרא handle-click, היא נקודת הכניסה לקוד והיא נקראת כשמשתמש לוחץ על קלף. היא מקבלת את ה id של הקלף, בודקת מה מצב העניינים במשחק ומעדכנת את מאפייני הקלפים. היא נעזרת בפונקציות העזר האחרות שכתובות בקובץ.

בקובץ ה UI עדכנתי את הקוד ל home-page כך שיציג את רשימת הקלפים ואחרי השינוי הוא נראה כך:

(defn home-page []
  (fn []
    [:span.main
     [:h1 "Welcome to my-reagent-app"]
     [:ul {:class "cards"}
      (for [item @cards/cards]
        ^{:key (:id item)}
        [:li {
              :class [(if (:visible item) "visible" "hidden")
                      (when (:found item) "found")]
              :on-click #(cards/click-on (:id item))
              } (:value item)])]]))

אני מאוד אהבתי את הכתיב של reagent. אני חושב שהוא הרבה יותר נקי וקל לתחזוקה בהשוואה ל jsx. כל קומפוננטה מיוצגת על ידי רשימה שמתחילה ב keyword כלשהו, אחר כך hash-map של מאפיינים ובסוף הטקסט או הילדים של האלמנט.

3. מה הלאה

אתם יכולים למצוא את משחק הזיכרון המלא שלי בגיטהאב בקישור: https://github.com/ynonp/cljs-memory-game.

ואם אתם רוצים לדעת יותר על ריאייג'נט שווה לקרוא את המדריך הקצר שלהם בקישור: https://reagent-project.github.io/.