Today I was working on a 3-Dimensional Tic-Tac-Toe game and needed to find a way to map out every possible path that can result in a win.

Structural Overview

In a 3x3 board, the first cell would be represented by [0 0] and the last cell [2 2]. In a 3x3x3 board, this would be [0 0 0] and [2 2 2] respectively–each position in a key equates a dimension in the board.

In a 3x3, one possible path would be cells for the second row of the board, in no particular order:

  • [1 0] [1 1] [1 2]

And in a 3x3x3, this would be the bottom row of the second dimension:

  • [1 2 0] [1 2 1] [1 2 2]

In order to find every possible path, the path must follow these criteria:

  • The number of cells must be equal to the size of the board (3 in a 3x3 4 in a 4x4).
  • Each cell in the path must differ by:
    • Exactly 1 Position
    • Exactly 2 Positions
    • Exactly D Positions (D is the number of dimensions in the board)

This seems simple at first, but it took me a bit to figure out how this would actually look.

Finding the Differences

I thought it easiest to start out with checking when every position differs and only comparing two cells.

(defn differs-by-3? [[z1 y1 x1] [z2 y2 x2]]
  (and (not= z1 z2) 
       (not= y1 y2) 
       (not= x1 x2)))

That was easy enough. Now to checking for two and one position. This should be doable just by checking for equivalence of one item and the “inequivalence” of one item, for each item.

(defn differs-by-2? [[z1 y1 x1] [z2 y2 x2]]
  (or (and (= z1 z2)
           (not= y1 y2)
           (not= x1 x2))
      (and (not= z1 z2)
           (= y1 y2)
           (not= x1 x2))
      (and (not= z1 z2)
           (not= y1 y2)
           (= x1 x2))))

(defn differs-by-1? [[z1 y1 x1] [z2 y2 x2]]
  (or (and (not= z1 z2)
           (= y1 y2)
           (= x1 x2))
      (and (= z1 z2)
           (not= y1 y2)
           (= x1 x2))
      (and (= z1 z2)
           (= y1 y2)
           (not= x1 x2))))

These two functions look oddly similar. In fact, one might say that the differs-by-3? function is a subset of the other two. It’s not quite clear yet how these can all be put into a single function, so I’ll move on to fixing the parameter issues.

Variable Parameters

Ideally, this would work for any board size, so we should be able to pass in 1-N number of cells. We can do this using a rest parameter, & cells. Since this will take away our parameter destructuring, it will need to be destructured in the function body.

(defn map-3d-positions [cells]
  [(map first cells)
   (map second cells)
   (map last cells)])

(defn differs-by-...? [& cells]
  (let [[zs ys xs] (map-3d-positions cells)]
       ;...
       ))

Now that we’ve fixed the parameter, we can apply = or not= to all the Xs, Ys, and Zs to find what we need.

(defn differs-by-1? [& cells]
  (let [[zs ys xs] (map-3d-positions cells)]
    (or (and (apply not= zs)
             (apply = ys)
             (apply = xs))
        (and (apply = zs)
             (apply not= ys)
             (apply = xs))
        (and (apply = zs)
             (apply = ys)
             (apply not= xs)))))

The other two functions will look very similar, but we still need to find out how to make these functions more generic.

Consolidating the Functions

I don’t know why I didn’t see it before, but if we simply count the number of positions that differ, then that will tell us the number of positions the cells differ by.

(defn differs-by-1? [& cells]
  (= 1 (count (filter (partial apply not=) (map-3d-positions cells)))))
(defn differs-by-2? [& cells]
  (= 2 (count (filter (partial apply not=) (map-3d-positions cells)))))
(defn differs-by-3? [& cells]
  (= 3 (count (filter (partial apply not=) (map-3d-positions cells)))))

This change collapses all of these larger functions into one generic function.

(defn differs-by-n? [n & cells]
  (= n (count (filter (partial apply not=) (map-3d-positions cells)))))

Now that I have this taken care of, next is to figure out how to actually get each possible path.