1
votes

I want to represent a graph of 2D positions + neighbors in Clojure for a board game. I am using a map that maps a position to a vector of neighbors:

{[0 0] [[0 1] [1 0] [1 1]]}

I have written some functions that can generate the neighbor graph for any sized board:

(defn positions [size]
  (for [x (range 0 size) y (range 0 size)] [x y]))

(defn neighbors [size [x y]]
  (filter (fn [[x y]]
        (and (>= x 0) (< x size) (>= y 0) (< y size)))
      (-> []
          (conj [(inc x) y])
          (conj [(dec x) y])
          (conj [x (inc y)])
          (conj [x (dec y)])
          (conj [(inc x) (inc y)])
          (conj [(dec x) (dec x)]))))

(defn board-graph
  [size]
  (reduce (fn [map position] (assoc map
                                    position
                                    (neighbors size position)))
          {}
          (positions size)))

This works fine:

(board-graph 2)
=> {[0 0] ([1 0] [0 1] [1 1]), [0 1] ([1 1] [0 0]), [1 0] ([0 0] [1 1] [0 0]), [1 1] ([0 1] [1 0] [0 0])}

However I now want to add to this additional 'virtual neighbors' to each of the board positions that are on the edge of the board, .e.g :TOP, :BOTTOM, :LEFT, :RIGHT. So I'd like:

(board-graph 2)
=> {[0 0] (:LEFT :TOP [1 0] [0 1] [1 1]), [0 1] (:LEFT :BOTTOM [1 1] [0 0]), [1 0] (:RIGHT :TOP [0 0] [1 1] [0 0]), [1 1] (:RIGHT :BOTTOM [0 1] [1 0] [0 0])}

Here is my attempt so far, but it doesn't quite work right and it seems really over-complicated:

(defn- filter-keys
  [pred map]
  (into {}
        (filter (fn [[k v]] (pred k)) map)))


(defn board-graph
  [size]
  (let [g (reduce (fn [map position] (assoc map
                                            position
                                            (neighbors size position)))
                  {}
                  (positions size))]
    (merge g
           (reduce-kv #(assoc %1 %2 (conj %3 :TOP)) {}
                      (filter-keys (fn [[x y]] (= y 0)) g))
           (reduce-kv #(assoc %1 %2 (conj %3 :BOTTOM)) {}
                      (filter-keys (fn [[x y]] (= y (dec size))) g))
           (reduce-kv #(assoc %1 %2 (conj %3 :LEFT)) {}
                      (filter-keys (fn [[x y]] (= x 0)) g))
           (reduce-kv #(assoc %1 %2 (conj %3 :RIGHT)) {}
                      (filter-keys (fn [[x y]] (= x (dec size))) g)))))

I basically want to build my map, go over it again and for certain keys update the value associated there depending on what the key is. I can't find a nice way to do this without resorting to state! Is there a more idiomatic way of doing this?

2
are you familiar with update?RedDeckWins
Thanks. Update only works on a single key though. I basically have 4 lists of keys that I need to call update on. Thinking about that I changed my search and found this: stackoverflow.com/questions/9638271/… This could maybe fix the last part of the code.jimypbr

2 Answers

2
votes

You can just add a few more expressions to your threaded expression where you build the returned vector. To make this read more nicely I switched it from the thread first macro -> to the "thread as" macro as-> which binds the symbol (in this case) c to the value being threaded at each step so I can use it in an a conditional expression on some of the stages:

(defn neighbors [size [x y]]
  (filter (fn [[x y]]
            (and (>= x 0) (< x size) (>= y 0) (< y size)))
          (as-> [] c
              (conj c [(inc x) y])
              (conj c [(dec x) y])
              (conj c [x (inc y)])
              (conj c [x (dec y)])
              (conj c [(inc x) (inc y)])
              (conj c [(dec x) (dec x)])
              (if (zero? x)  (conj c :TOP) c)
              (if (= size x) (conj c :BOTTOM) c)
              (if (zero? y)  (conj c :LEFT) c)
              (if (= size y) (conj c :RIGHT) c))))

each conditional expression either returns an updated version, or passes it through unchanged if the condition was not met.

0
votes

This works for me, but it isn't very close to what you wrote originally.

(defn positions [size]
  (for [x (range 0 size) y (range 0 size)] [x y]))

(defn neighbors [n [x y]]
  (let [left (if (zero? x)
               :LEFT
               [(dec x) y])
        right (if (= x (dec n))
                :RIGHT
                [(inc x) y])
        up (if (zero? y)
             :TOP
             [x (dec y)])
        down (if (= y (dec n))
               :BOTTOM
               [x (inc y)])]
    (list left right up down)))

(defn board-graph [n]
  (let [keys (positions n)
        vals (map (partial neighbors n) keys)]
    (zipmap keys vals)))

then

(clojure-scratch.core/board-graph 2)
=>
{[1 1] ([0 1] :RIGHT [1 0] :BOTTOM),
 [1 0] ([0 0] :RIGHT :TOP [1 1]),
 [0 1] (:LEFT [1 1] [0 0] :BOTTOM),
 [0 0] (:LEFT [1 0] :TOP [0 1])}

EDIT: As Arthur noted, this doesn't handle diagonals, if you want that.