Most of us who got into computer chess or combinatorial algorithms started with the classic 8-Queens Puzzle. Placing 8 queens without a single collision is a legendary milestone, but let's be honest—it has been analyzed to death.
So I wanted to push the boundaries of board saturation and explore a much harder, non-standard challenge:
The Challenge: "The 12-Piece Non-Attacking Army"
Can you place 12 non-attacking pieces on a standard 8x8 chessboard using only the following classic army?
* 4 Rooks (R)
* 4 Bishops (B)
* 4 Knights (N)
Constraint: No piece may attack any other piece.
At first glance, dropping 4 rooks immediately locks down half of the board’s ranks and files. Throwing in 4 bishops creates an intense crossfire of diagonals, leaving seemingly zero safe squares for 4 jumping knights. Human intuition screams that the board is completely saturated!
The Exact Search Results & Discovery
Through a fast bitboard search, the board actually opens up to an elegant set of valid solutions where rooks, bishops, and knights arrange themselves into microscopic non-interfering corridors.
Depending on how bishop color-square constraints are enforced:
1. Unconstrained Bishops (Any 4 bishop squares):
* Over 27,000 valid non-attacking placements across the board.
2. Realistic Color-Balanced Setup (Strictly 2 Light-squared + 2 Dark-squared Bishops):
* Total Board Solutions (including 90°/180°/270° rotations & reflections): Exactly 168 solutions.
* Unique Fundamental Geometries (Symmetry-reduced / Canonical): Exactly 21 distinct solutions.
Here is one valid configuration:
Code: Select all
. . . R . . . .
. . . . . R . .
. B . . . . . B
. . R . . . . .
R . . . . . . .
. . . . N . N .
. B . . . N . .
. . . . B . . N
Here is a minimalist, bug-free Python script utilizing 64-bit integer bitboards and backtracking to compute the solutions:
Code: Select all
import sys
import time
import itertools
def solve_4r_4b_4n():
print("=== SOLVING 12 PIECES: 4R, 4B, 4N ===")
R_MASK, B_MASK, N_MASK = [0]*64, [0]*64, [0]*64
COLOR = [0, 0]
for r in range(8):
for c in range(8):
i = r * 8 + c
COLOR[(r + c) % 2] |= (1 << i)
for x in range(8):
if x != c: R_MASK[i] |= (1 << (r * 8 + x))
if x != r: R_MASK[i] |= (1 << (x * 8 + c))
for dr, dc in [(-1,-1), (-1,1), (1,-1), (1,1)]:
nr, nc = r + dr, c + dc
while 0 <= nr < 8 and 0 <= nc < 8:
B_MASK[i] |= (1 << (nr * 8 + nc))
nr += dr; nc += dc
for dr, dc in [(-2,-1), (-2,1), (-1,-2), (-1,2), (1,-2), (1,2), (2,-1), (2,1)]:
nr, nc = r + dr, c + dc
if 0 <= nr < 8 and 0 <= nc < 8:
N_MASK[i] |= (1 << (nr * 8 + nc))
valid_4r = []
for rows in itertools.combinations(range(8), 4):
for cols in itertools.permutations(range(8), 4):
rm = sum(1 << (rows[k] * 8 + cols[k]) for k in range(4))
valid_4r.append(rm)
solutions_count = 0
def place_knights(count, start_idx, forb, pm):
nonlocal solutions_count
if count == 0:
solutions_count += 1
return
for i in range(start_idx, 64):
bit = 1 << i
if (forb & bit) == 0 and (N_MASK[i] & pm) == 0:
place_knights(count - 1, i + 1, forb | N_MASK[i] | bit, pm | bit)
def place_bishops_dark(count, start_idx, forb, pm):
if count == 0:
place_knights(4, 0, forb, pm)
return
for i in range(start_idx, 64):
bit = 1 << i
if (COLOR[1] & bit) and (forb & bit) == 0 and (B_MASK[i] & pm) == 0:
place_bishops_dark(count - 1, i + 1, forb | B_MASK[i] | bit, pm | bit)
def place_bishops_light(count, start_idx, forb, pm):
if count == 0:
place_bishops_dark(2, 0, forb, pm)
return
for i in range(start_idx, 64):
bit = 1 << i
if (COLOR[0] & bit) and (forb & bit) == 0 and (B_MASK[i] & pm) == 0:
place_bishops_light(count - 1, i + 1, forb | B_MASK[i] | bit, pm | bit)
t0 = time.time()
total_bases = len(valid_4r)
for idx, rm in enumerate(valid_4r, 1):
if idx % 5000 == 0 or idx == total_bases:
sys.stdout.write(f"\rScanning rook bases: {idx}/{total_bases}...")
sys.stdout.flush()
forb = 0
t = rm
while t:
i = (t & -t).bit_length() - 1
forb |= R_MASK[i] | (1 << i)
t &= t - 1
place_bishops_light(2, 0, forb, rm)
print(f"\nDone in {time.time() - t0:.2f}s | Solutions found: {solutions_count}")
if __name__ == "__main__":
solve_4r_4b_4n()
