Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
422 Newsletter

Knight move

Write a function that takes a chess position as a tuple (such as [:A 4]). If there were a knight there, where would it be able to move? Have the function return a collection of all possible moves for that knight. Assume there are no other pieces on the board. Be sure the knight doesn't leave the board.

Knight Move diagram


(knight-move [:D 3]) ;=> [[:B 2] [:C 1] [:B 4] [:C 5] [:E 5] [:F 4] [:E 1] [:F 2]]
(knight-move [:A 1]) ;=> [[:B 3] [:C 2]]

Thanks to this site for the problem idea, where it is rated Hard in JavaScript. The problem has been modified.

Please submit your solutions as comments on this gist.

To subscribe:

Copy link

nwjsmith commented Apr 19, 2021

(def board-coordinates
  (for [x (range 1 9)
        y (range 1 9)]
    [x y]))

(def offsets
  [[-2 -1]
   [-1 -2]
   [-2 1]
   [-1 2]
   [1 2]
   [2 1]
   [1 -2]
   [2 -1]])

(defn coordinate
  [[column row]]
  [(inc (.indexOf [:A :B :C :D :E :F :G :H] column)) row])

(defn position
  [[x y]]
  [(get [:A :B :C :D :E :F :G :H] (dec x)) y])

(defn knight-move
  (let [[x y] (coordinate pos)]
    (->> (for [[x-offset y-offset] offsets] [(+ x x-offset) (+ y y-offset)])
         (map (fn [candidate] (some #{candidate} board-coordinates)))
         (remove nil?)
         (map position))))

Copy link

KingCode commented Apr 20, 2021

I have added a solution to @stefan-westcott's shortest-paths function:

(def move-fns (let [half (->> (for [f [inc dec] 
                                     g [#(+ % 2) #(- % 2)]]
                                 [f g]))]
                (->> half (map reverse) (into half)
                     (map (fn [[row-fn col-fn]]
                            (fn [[r c]]
                              [(row-fn r) (col-fn c)]))))))

(defn neighbours [rc]
  (->> move-fns (map #(% rc))))

(defn in-bounds [rc]
  (every? #(< 0 % 9) rc))

(defn moves [rc]
  (->> rc neighbours (filter in-bounds)))

;; de/formatting
(def kw<->dig (let [l->d (->> "ABCDEFGH" (map str) (map keyword) 
                              (zipmap (range 1 9)))]
                (into l->d (zipmap (vals l->d) (keys l->d)))))

(defn ->rc [[kw rank]]
  (->> kw kw<->dig (vector rank)))

(defn ->kw+rank [[row col]]
  (-> col kw<->dig (vector row)))

(defn knight-move [file+rank]
  (->> file+rank ->rc 
       (map ->kw+rank))) 

;; shortest-paths challenge 
(defn new-paths [[from {:keys [paths]}] step seen]
  (->> from moves (remove seen)
       (map #(vector % {:paths (->> paths 
                                    (mapv (fn [p] 
                                            (conj p %))))
                        :distance (inc step)}))))

(defn new-entries [pinfo step visited]
  (->> pinfo
       (group-by #(-> % val :distance))
       (#(% step))
       (mapcat #(new-paths % step visited))))

(defn merge-into [pinfo new-entries]
  (->> new-entries
       (reduce (fn [acc [loc {:keys [paths distance]}]]
                 (update acc loc (fn [{paths' :paths :or {paths' []}}]
                                   {:paths (concat paths paths')
                                    :distance distance})))

(defn expand [from to]
  (loop [step 0 
         pinfo {from {:distance step
                      :paths [[from]]}}
         seen #{from}]
    (if (some #{to} (keys pinfo))
      (let [added (new-entries pinfo step seen)]
        (recur (inc step)
               (merge-into pinfo 
               (into seen (map first added)))))))

(defn shortest-paths [from to]
  (let [to (->rc to)
        paths  (-> (expand (->rc from) to)
                   (get to)
    (->> paths (map #(->> % (map ->kw+rank))))))

Copy link

sztamas commented Apr 20, 2021

(def ord (comp int first name))

(defn file-idx [file]
  (- (ord file) (ord :A)))

(defn index->file [idx]
  (-> (+ (ord :A) idx) char str keyword))

(def valid-file? #(<= 0 (file-idx %) 7))

(def valid-rank? #(<= 1 % 8))

(defn valid-square? [[file rank]]
  (and (valid-file? file) (valid-rank? rank)))

(def knight-move-diffs
  (->> (for [two-squares [-2 2]
             one-square  [-1 1]]
         [[two-squares one-square]
          [one-square  two-squares]])
       (reduce concat)))

(defn move [[file rank] [file-diff rank-diff]]
  [(index->file (+ (file-idx file) file-diff))
   (+ rank rank-diff)])

(defn knight-move [coord]
  (->> (map move (repeat coord) knight-move-diffs)
       (filter valid-square?)))

Copy link

KingCode commented Apr 22, 2021

Thank you @stefan-westcott for the extra challenge of computing all shortest-paths, and @sztamas for the nice web app! I used it to test my solution (my edited submission above).

If anyone wants to try it, another interesting (but much harder) related challenge is the Knight's Tour for various side lengths, yielding all paths starting from a corner of the board and visiting each square exactly once, e.g. a solution for side length 5 could start with:

(knight-tour 5)
;;=> (([:A 1] [:C 2] [:E 3] [:D 5] [:B 4] [:A 2] [:C 3] [:D 1] [:B 2] [:A 4] [:C 5] [:E 4] [:D 2] [:B 1] [:A 3] [:B 5] [:D 4] [:E 2] [:C 1] [:B 3] [:A 5][:C 4] [:E 5] [:D 3] [:E 1]) ...)

I haven't been able to compute even a single path in reasonable time for solutions for larger than boards of side length 5 using brute force depth-first search.. There is a neat article here referencing faster methods.

Copy link

steffan-westcott commented Apr 23, 2021

Here's my answer to the extra challenge of finding all shortest paths:

(defn extend-path [path]
  (map #(conj path %) (knight-move (peek path))))

(defn shortest-paths [src dest]
  (loop [reached #{src} paths [[src]]]
    (if (reached dest)
      (filter #(= dest (peek %)) paths)
      (let [paths' (remove (comp reached peek) (mapcat extend-path paths))]
        (recur (into reached (map peek paths')) paths')))))

Copy link

vmpj commented Apr 23, 2021

(def file2int {:A 1 :B 2 :C 3 :D 4 :E 5 :F 6 :G 7 :H 8})
(def int2file (clojure.set/map-invert file2int))

(defn valid-move? [move]
  (every? #(and (> % 0) (< % 9)) move))

(defn friendly-format [[f r]] [(int2file f) r])

(defn knight-move [[file rank]]
  (let [f (file2int file) r rank]
    (as-> '(2 2 -2 -2 1 -1 1 -1) l
          (map #(vector (+ f %1) (+ r %2)) l (reverse l))
          (filter valid-move? l)
          (map friendly-format l))))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment