Safe spaces

In checkers, the red pieces move down the board and the black pieces move up it, attacking foes diagonally along the way. I realize that "up" and "down" are completely relative, but work with me here.

In a local situation like the following one, where R is a red piece and B is a black piece, either player is at risk of being attacked, since red and black have met one another diagonally.

R _
_ B

The next situation, however, is a peaceful one. The two pieces have already passed one another and cannot turn backwards to attack.

B _
_ R

What would it take to write a function that scans the entire board and determines whether or not all its pieces are safe?

Representation

The board can be thought of as a non-toroidal matrix of any dimensions, but I'm using a square one here.

An enum is a decent way to differentiate between empty spaces (0), red pieces (3), and black ones (1). For any 2x2 subsection of the board

A B
C D

the program should check the values of

A - D
B - C

If either of these subtractions yields a value of 2, it means that a red piece was detected above a black piece, and that a valid attack can be formed. The board is in danger. Any other value suggests a safe arrangement.

Manipulation

I wrote this solution in Q rather than J. I've been reading Q for Mortals during my lunch breaks and wanted to test my knowledge. I suspect that the code here is still not quite idiomatic.

I started out with four 4x4 test boards: two unsafe ones in opposite directions, and two safe ones—the last of which is quite crowded.

unsafe1:4 4#0 3 0 0 0 0 1 0
unsafe2:4 4#0 3 0 0 1 0 0 0
safe1:4 4#0 1 0 0 0 0 3 0 0 0 0 3 0 0 0 1
safe2:4 4#0 1 0 0 3 0 3 0 0 0 1 0 1 3 1 1

unsafe1

0 3 0 0
0 0 1 0
0 3 0 0
0 0 1 0

unsafe2

0 3 0 0
1 0 0 0
0 3 0 0
1 0 0 0

safe1

0 1 0 0
0 0 3 0
0 0 0 3
0 0 0 1

safe2

0 1 0 0
3 0 3 0
0 0 1 0
1 3 1 1

The easiest way to compare two pieces is to have them right next to one another. If all diagonal stretches were isolated into rows, then it'd be easy to perform the aforementioned subtractions across them in a simple fold operation. My first instinct was to rotate each row n of the matrix n times to coerce the diagonals into columns, but this had the effect of wrapping attacks around the board, like a game of Asteroids. The solution wound up being a bit more complex.

The cartesian product of a matrix's top-level shape contains the indices of each one of its values.

{c:n cross n:til count x}[unsafe2]

0 0
0 1
0 2
0 3
1 0
1 1
1 2
1 3
2 0
2 1
2 2
2 3
3 0
3 1
3 2
3 3

Summing these addresses reveals which diagonal row they are in. A 4x4 matrix has 7 diagonals.

{(sum')c:n cross n:til count x}[unsafe2]

0 1 2 3 1 2 3 4 2 3 4 5 3 4 5 6

These indices can be grouped together, then released as a two-dimensional list.

{value group(sum')c:n cross n:til count x}[unsafe2]

,0
1 4
2 5 8
3 6 9 12
7 10 13
11 14
,15

Mapping the cartesian product back across this list gives the 2D indices needed to address each diagonal stretch in the original matrix.

diagonal:{c value group(sum')c:n cross n:til count x}
diagonal[unsafe2]

,0 0
(0 1;1 0)
(0 2;1 1;2 0)
(0 3;1 2;2 1;3 0)
(1 3;2 2;3 1)
(2 3;3 2)
,3 3

Applying the matrix to this diagonal map requires a nested each right operation.

unsafe2 ./:/:diagonal[unsafe2]

,0
3 1
0 0 0
0 0 3 1
0 0 0
0 0
,0

Calculation

For sequential positions x and y, just check for 2=x-y. If x itself is 2, that means a dangerous situation has already been encountered and should be propagated.

danger:{$[2=x;x;$[2=x-y;2;y]]}

In action

(danger/) 0 0 1 3 0 1 3

3

(danger/) 0 0 3 1 0 1 3

2

Checking the whole board means

  1. Getting the diagonal runs of the board.
  2. Reversing every row of the board, then getting its diagonals. This checks both diagonal directions.
  3. Appending both lists of diagonals.
  4. Folding danger over every diagonal row.
  5. Checking if any value in the result equals 2.
  6. A board is safe if no 2 is found.

This is expressed in the following function.

safe:{not any 2=(danger/')((reverse each x)./:/:d),x ./:/:d:diagonal x}

Notice how unsafe1 appears safe if only its primary diagonals are isolated.

{x ./:/:diagonal x}[unsafe1]

,0
3 0
0 0 0
0 1 3 0
0 0 0
0 1
,0

But danger rears its head when the rows are reversed.

{((reverse each x)./:/:d),x ./:/:d:diagonal x}[unsafe1]

,0
0 0
3 1 0
0 0 0 0
0 3 1
0 0
,0
,0
3 0
0 0 0
0 1 3 0
0 0 0
0 1
,0

Folding danger over each row gives an array of threatened diagonals.

{(danger/')((reverse each x)./:/:d),x ./:/:d:diagonal x}[unsafe1]

0 0 2 0 2 0 0 0 0 0 0 0 1 0

And the board is unsafe, of course, because a 2 exists in this array.

{not any 2=(danger/')((reverse each x)./:/:d),x ./:/:d:diagonal x}[unsafe1]

0b

We can see that the other boards return their expected results.

safe unsafe2

0b

safe safe1

1b

safe safe2

1b

Writing this solution in Q was fun. Since I couldn't use any of J's funky combinators I might actually be able to read this a week from now.