Search code examples
swinguser-interfaceclojureminesweeperseesaw

How to accelerate a lagging Swing minesweeper board?


I recently created a little minesweeper UI using the seesaw framework which is basically a nifty clojure wrapper around swing. The related code can be found here.

Minesweeper board

Basically everything works fine so far, the only problem is that the user experience is quite bad when you choose to play on expert level. The reason is that on every click on a cell the whole ui is repainted and this takes quite long (on average 850 millis).

The code that is responsible for the repainting is the following:

(defn- update-fields
  [cell-states]
  (doseq [[idx state] (map-indexed vector cell-states)
        :let [field (select-field idx)]]
    (config! field :icon (icons/cell-icons state))))

(defn- update-board
  [snapshot face]
  (do
    (change-smiley face)
    (update-fields (:cells snapshot))
    (repaint! ui)))

The code for the icon handling looks the following

(ns minesweeper.icons
  (:require
    [clojure.java.io :as io]
    [clojure.string  :as str]
    [seesaw.icon :as icon]))

(def ^:private cell-icons-path "minesweeper/icons/cell")
(def ^:private face-icons-path "minesweeper/icons/face")

(defn- file-name
  [file]
  (str/replace-first
   (.getName file) #"\.[^.]+$" ""))

(def ^:private init-icons
  (memoize
   (fn [res]
     (let [parent (rest (file-seq (io/file (io/resource res))))]
       (reduce
        #(assoc %1 (keyword (file-name %2)) (icon/icon %2))
        {}
        parent)))))

(defn cell-icons
  [id]
  (let [icons (init-icons cell-icons-path)]
    (get icons id)))

(defn face-icons
  [id]
  (let [icons (init-icons face-icons-path)]
    (get icons id)))

So my question is, how to approach this more efficiently? I thought about only updating the cells (which are represented by JButtons) that are affected by a click but in case auto-clear opens a lot of adjacent cells this may also take quite a while.

Is it in general a reasonable choice to use a mig layout with buttons to represent the board?


Solution

  • By using clojure.core/time I found that the bottleneck in your UI logic is lookup of your buttons using (select ui [(keyword (str "#field_" idx))]) as seesaw has to do a name search by filtering all the components in the hierarchy each time you update board.

    The quickest fix would be to wrap your select-field function into memoize but it won't work when you restart the game (new buttons will be created so memoized select-field would return buttons from the previous game).

    Another possible solution is to put all your buttons into a vector and keep it in a global atom:

    (def items (atom []))
    (defn select-field
      [idx]
      (@items idx))
    

    And change how you create a board:

    (defn- make-board-panel
      [snapshot]
      (let [bg    (button-group)
            [n m] (:dimension snapshot)
            buttons (into [] (for [idx (range (* n m))]
                           (make-button idx bg)))]
        (reset! items buttons)
        (mig-panel
         :constraints [(str "gap 0, wrap" n) "[]" "[]" ]
         :items       (map #(vector % "w 24px!, h 24px!") buttons))))
    

    Tests

    I have wrapped your update-board body in clojure.core/time and got following results by playing 10 times (new-game 50 50 1):

    Before the fix

    "Elapsed time: 7020.756206 msecs"
    "Elapsed time: 6766.130362 msecs"
    "Elapsed time: 6616.715565 msecs"
    "Elapsed time: 6628.383521 msecs"
    "Elapsed time: 6657.386279 msecs"
    "Elapsed time: 6588.50692 msecs"
    "Elapsed time: 6554.704587 msecs"
    "Elapsed time: 6650.864132 msecs"
    "Elapsed time: 6610.557065 msecs"
    "Elapsed time: 6671.02469 msecs"
    

    After the fix

    "Elapsed time: 92.491489 msecs"
    "Elapsed time: 60.236867 msecs"
    "Elapsed time: 32.254729 msecs"
    "Elapsed time: 29.551383 msecs"
    "Elapsed time: 29.383067 msecs"
    "Elapsed time: 25.768517 msecs"
    "Elapsed time: 25.724915 msecs"
    "Elapsed time: 45.869723 msecs"
    "Elapsed time: 25.898016 msecs"
    "Elapsed time: 26.254874 msecs"