4 x 4 Magic Square solutions

Setting aside some frustrating negative aspects, the recent discussion about a 4 x4 magic square captured my interest. Lengthy browsing got me to this site.

https://www.hackster.io/Klausj/r3-r4-compare-using-magic-square-6d7e7f

Did a bit of Google Translate work so that I had some faint idea what his/her code was doing. But as I dont have a 1.8" TFT or anything similar the hard part was getting it to deliver printed output instead. A few sessions with AI and I finally had a working sketch.

There are actually 880 solutions. But I asked it to include mirroring and rotation for the hell of it, which delivered the full 7040, in about 16 mins. About 42,000 lines so won't attempt to paste it. If anyone wants it pleased PM me with an email address.

Working sketch below:

const byte cols = 4;
const byte maxi = cols * cols;
const byte sum = (maxi + 1) * maxi / 2 / cols;
byte p[maxi]; // Holds the current square
const byte seq[] = {
  0, 1, 2, 3,
  4, 5, 6, 7,
  8, 10, 12, 13,
  9, 11, 14, 15
};

int count = 0;
unsigned long startTime;

void setup() {
  Serial.begin(115200);
  delay(1000); // Give serial monitor time to connect
  Serial.println(F("Generating 4x4 magic squares...\n"));
  startTime = millis();
  place(0, 0xFFFF);
  Serial.print(F("\nDone. Total solutions: "));
  Serial.println(count);
}

void loop() {}

void place(byte pos, int avail) {
  byte i;
  switch (pos) {
    case 0: case 1: case 2: case 4: case 5: case 6: case 8: case 10:
      for (i = 1; i <= maxi; i++) Try(pos, i, avail); break;
    case 3:  i = rem3(0, 1, 2); Try(pos, i, avail); break;
    case 7:  i = rem3(4, 5, 6); Try(pos, i, avail); break;
    case 9:  i = rem3(0, 4, 8); Try(pos, i, avail); break;
    case 11: i = rem3(1, 5, 10); Try(pos, i, avail); break;
    case 12: i = rem3(5, 6, 10); Try(pos, i, avail); break;
    case 13: i = rem3(8, 10, 12); Try(pos, i, avail); break;
    case 14: i = rem3(2, 6, 12); Try(pos, i, avail); break;
    case 15:
      i = rem3(3, 7, 13);
      Try(pos, i, avail);
      break;
    case 16:
      if (rem3(0, 5, 12) == p[15]) {
        count++;
        unsigned long elapsed = (millis() - startTime) / 1000;
        Serial.print(F("Solution #"));
        Serial.print(count);
        Serial.print(F(" ["));
        Serial.print(elapsed);
        Serial.println(F(" sec]:"));
        printSquare();
        delay(10); // optional: slow output if needed
      }
  }
}

void Try(byte pos, byte value, int avail) {
  if ((value < 1) || (value > maxi)) return;
  int mask = 1 << (value - 1);
  if (avail & mask) {
    p[pos] = value;
    place(pos + 1, avail & ~mask);
    p[pos] = 0;
  }
}

byte rem3(byte a, byte b, byte c) {
  return sum - p[a] - p[b] - p[c];
}

void printSquare() {
  for (byte i = 0; i < 16; i++) {
    Serial.print(p[i] < 10 ? " " : "");
    Serial.print(p[i]);
    Serial.print(" ");
    if ((i + 1) % 4 == 0) Serial.println();
  }
  Serial.println();
}

1 Like

Here is the output for solution number 1

Solution #1 [0 sec]:
 1  2 15 16 
12 14  3  5 
13  8  7 11 
10  4  6  9 

What should the sum of each of the columns be ?

1 Like

Thanks, does indeed look very wrong. I should have checked more carefully. Totals should all be 34. (Generally, N*(N*N +1)/2). I'll take a closer look at the original article in the morning, but meanwhile I reckon the sketch must be to generate possible combinations not solutions.

Meanwhile maybe someone (in an earlier time zone?) can add a few extra lines to check if any of the 880 deliver 34! Or spot any errors in the author's code?

It looks like there is a certain solution/insertion order defined in seq[] and it differs from the printing layout. Perhaps you need to pay attention to seq[] array in printing?

Code from ZIP file:

/*
  Magische Quadrate 4x4, rekursiv

  Versuch mit library TFT18_R4.h
*/
// pin definition for the Uno
#define cs   10
#define dc   9
#define rst  8

/*
  select the library you want to use:
  (you also can use the NEW library with the OLD UNO R3)
*/

#ifdef __AVR__
// R3:
#include <TFT.h>
TFT tft = TFT(cs, dc, rst);
#else

// R4:
#include <TFT18_R4.h>
TFT18_R4 tft = TFT18_R4(cs, dc, rst);
#endif

struct cell {
  int left;
  int top;
};

int w, h;
const byte cols = 4;
const byte maxi = cols * cols;
const byte sum = (maxi + 1) * maxi / 2 / cols;
byte p[maxi];
int colour[maxi + 1];

// Reihenfolge, in der die Werte gesetzt werden:
const byte seq[] = {
  0,  1,  2,  3,
  4,  5,  6,  7,
  8, 10, 12, 13,
  9, 11, 14, 15
};

/* die Positionen:
  _0 _1 _2  3    alle Postionen ohne "_"
  _4 _5 _6  7    koennen berechnet werden!
  _8 _9 10 11
  12 13 14 15
*/

cell disp[maxi];
char str[2 * maxi + 2];
int count = 0;
const byte charW = 13;
const byte charH = 13;
const byte left = 39;
const byte top =  59;
const byte p0 = 2;
const byte p1 = 102;

void setup() {
  Serial.begin(9600);
  Serial.println(F(__FILE__));
  // TFT:
  tft.begin();
  tft.setRotation(0);
  h = tft.height();
  tft.fillScreen(tft.Color565(0, 0, 16));
  tft.drawRect(left - 2, top - 5, 58, 58, ST7735_WHITE);
  // berechne die Positionen der 16 Zellen
  for (int i = 0; i < maxi; i++) {
    int j = seq[i];
    disp[j].left =  left + 1 + (i & 3) * charW;
    disp[j].top  = top + 2 + (i / 4) * charH;
  }
  /*
    erzeuge die Strings, um die Zahlen
    schneller anzeigen zu koennen
  */
  for (int i = 1; i <= maxi; i++) {
    colour[i] = hue(i);
    str[i * 2] = i < 10 ? ' ' : '1';
    str[i * 2 + 1] = '0' + i % 10;
    // Vorbelegung: schwarz
    dispNumAtpos(0, i);
  }
  //======================
  tft.setCursor(p0, h - 20);
  tft.print("time/hms");
  tft.setCursor(78, h - 20);
  tft.print("solution");
  showValue(0, p0, true);
  showValue(count, p1, false);
  // starte die Rekursion:
  place(0, 0xFFFF);
  // finished and ready.
}

void loop() {}

void place(byte pos, int avail) {
  // showValue(millis() / 1000, p0, true);
  byte i;
  switch (pos) {
    case  0: case 1: case 2: case 4: case 5: case 6: case 8: case 10:
      for (i = 1; i <= maxi; i++) Try(pos, i, avail); break;
    case  3: i = rem3(0,  1,  2); // if (p[0] > i)
      Try(pos, i, avail); break;
    // Zeile 1, gespiegelt an der senkrechten Achse
    case  7: i = rem3(4,  5,  6); Try(pos, i, avail); break;   // Zeile 2
    case  9: i = rem3(0,  4,  8); // if ((p[0] > i) && (p[3] > i))
      Try(pos, i, avail); break;   // Spalte 1
    case 11: i = rem3(1,  5, 10); Try(pos, i, avail); break;   // Spalte 2
    case 12: i = rem3(5,  6, 10); Try(pos, i, avail); break;   // mittlerer Block
    case 13: i = rem3(8, 10, 12); Try(pos, i, avail); break;   // Zeile 3
    case 14: i = rem3(2,  6, 12); Try(pos, i, avail); break;   // Spalte 3
    case 15: i = rem3(3,  7, 13); // if (p[0] > i)
      Try(pos, i, avail); break;
    // Spalte 4, gespiegelt an der Haupdiagonale
    case 16: if (rem3(0, 5, 12) == p[15]) {
        showValue(millis() / 1000, p0, true);
        showValue(++count, p1, false);
        // signalisiere die aktuelle Loesung
        tone(A0, 1000, 100);
        delay(5000);
      }
  }
}

void Try(byte pos, byte value, int avail) {
  if ((value < 1) || (value > maxi)) return;  // ungueltige Zahl
  int mask = 1 << (value - 1);
  if (avail & mask) {
    p[pos] = value;
    dispNumAtpos(value, pos);
    place(pos + 1, avail & ~mask);
    dispNumAtpos(0, pos); // loesche Zelle
    p[pos] = 0;
  }
}

byte rem3(byte a, byte b, byte c) {  // gib den Rest zurueck
  return sum - p[a] - p[b] - p[c];
}

void dispNumAtpos(byte number, byte position) {
  if (number != 0) {
    tft.setCursor(disp[position].left, disp[position].top);
    int farbe = colour[number];
    tft.setTextColor(farbe);
    number = number * 2;
    tft.print(str[number++]);
    tft.print(str[number]);
  }
  else
    // loesche Zahl
    tft.fillRect(disp[position].left, disp[position].top - 1, 11, 8, ST7735_BLACK);
}

// convert seconds to HH:MM:SS
char ch[9];
long val;
void setDigit(byte pos, long quot) {
  byte tmp = val / quot;
  ch[pos] = ch[pos] + tmp;
  val = val % quot;
}

// nur fuer millis() und count:
void showValue(long value, byte xPos, boolean hms)  {
  byte br = 4;
  if (hms) {
    strcpy(ch, "00:00:00");
    val = value;
    setDigit(0, 36000);
    setDigit(1, 3600);
    setDigit(3, 600);
    setDigit(4, 60);
    setDigit(6, 10);
    setDigit(7, 1);
    br = 8;
  } else
    ltoa(value, ch, 10);
  // clear the old entry:
  tft.fillRect(xPos - 1, h - 11, br * 6 + 1, 9, ST7735_BLACK);
  // right-align count:
  byte b = (br - strlen(ch)) * 6;
  tft.setTextColor(0x07FF);
  tft.setCursor(xPos + b, h - 10);
  tft.print(ch);
}

word hue(float z) {
  int M = 127;
  float h = z * M * 6 / 17;
  int g = max(-abs(h - 2 * M) + 3 * M - M, 0);
  int r = max(+abs(h - 3 * M) + 0 * M - M, 0);
  int b = max(-abs(h - 4 * M) + 3 * M - M, 0);
  return tft.Color565(b, g, r);
}

... and maybe there is a confusion about the constraints in insertion-order-space versus print-order-space?

I think that’s the most likely cause. ideally what we need next is someone to run the sketch with the TFT module or one compatible with it,

I asked ChatGPT to convert everything displayed to print. But my check of the first few results, whose first couple of rows summed to 34, was hasty and inadequate.

Not all the same if that's a solution!

1 + 12 + 13 + 10 = 36
2 + 14 + 8 + 4 = 28
15 + 3 + 7 + 6 = 31
16 + 5 + 11 + 9 = 41
1 + 2 + 15 + 16 = 34
12 + 14 + 3 + 5 = 34
13 + 8 + 7 + 11 = 39
10 + 4 + 6 + 9 = 29
1 + 14 + 7 + 9 = 31
16 + 3 + 8 + 10 = 37

For which constraints? Solutions to the 4x4 puzzle can have different totals, but all column, row and diagonal sums have to be equal.

Those of the recent discussion mentioned in my post, which was about the classic traditional 4x4, using the digits 1 to 16.

For the first couple rows, the insertion/solution order exactly matches the print order, but after the 9th insertion, the 1st column constraint has to work and set print position 13 (0-index 12). And the the down-left diagonal constraint defines position 10(0-index 9).

I got lost with the solution order versus German constraint name and printing order. It might even be solving a different problem, trading the down-left diagonal constraint for a middle-block constraint.

But first, you need your code to reorder your solution order into print order, and I think that may some sort of inverse of seq[].

Reordering the solution with the seq array does produce the correct output.

Only change needed in the code is to print p[seq[i]] instead of p[i] in the printSquare function.

Here is the original code modified to run on an ILI9341 display, with code added to print the solutions to the serial monitor. Much slower because of the display, @Terrypin code running on an UNO R4 Wifi takes 242 seconds to complete.

/*
  Magische Quadrate 4x4, rekursiv

  Versuch mit library TFT18_R4.h
*/

#include "SPI.h"
#include "Adafruit_GFX.h"
#include "Adafruit_ILI9341.h"

// For the Adafruit shield, these are the default.
#define TFT_DC 9
#define TFT_CS 10

Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC);


struct cell {
  int left;
  int top;
};

int w, h;
const byte cols = 4;
const byte maxi = cols * cols;
const byte sum = (maxi + 1) * maxi / 2 / cols;
byte p[maxi];
int colour[maxi + 1];

// Reihenfolge, in der die Werte gesetzt werden:
const byte seq[] = {
  0,  1,  2,  3,
  4,  5,  6,  7,
  8, 10, 12, 13,
  9, 11, 14, 15
};

/* die Positionen:
  _0 _1 _2  3    alle Postionen ohne "_"
  _4 _5 _6  7    koennen berechnet werden!
  _8 _9 10 11
  12 13 14 15
*/

cell disp[maxi];
char str[2 * maxi + 2];
int count = 0;
const byte charW = 13;
const byte charH = 13;
const byte left = 39;
const byte top =  59;
const byte p0 = 2;
const byte p1 = 102;
byte solution[maxi + 1] = {0};

void setup() {
  Serial.begin(115200);
  Serial.println(F(__FILE__));
  // TFT:
  tft.begin();
  tft.cp437(true);
  tft.setRotation(0);
  h = tft.height();
  tft.fillScreen(tft.color565(0, 0, 14));
  tft.drawRect(left - 2, top - 5, 58, 58, ILI9341_WHITE);
  // calculate the positions of the 16 cells
  for (int i = 0; i < maxi; i++) {
    int j = seq[i];
    disp[j].left =  left + 1 + (i & 3) * charW;
    disp[j].top  = top + 2 + (i / 4) * charH;
  }
  // Generate the strings to display the numbers faster
  str[0] = 0xDB;
  str[1] = 0xDB;
  colour[0] = ILI9341_BLACK;
  for (int i = 1; i <= maxi; i++) {
    colour[i] = hue(i);
    str[i * 2] = i < 10 ? ' ' : '1';
    str[i * 2 + 1] = '0' + i % 10;
    // Default: black
    dispNumAtpos(0, i);
  }
  //======================
  tft.setCursor(p0, h - 20);
  tft.print("time/hms");
  tft.setCursor(78, h - 20);
  tft.print("solution");
  showValue(0, p0, true);
  showValue(count, p1, false);
  // start the recursion:
  place(0, 0xFFFF);
  // finished and ready.
}

void loop() {}

void place(byte pos, int avail) {
  // showValue(millis() / 1000, p0, true);
  byte i;
  switch (pos) {
    case  0: case 1: case 2: case 4: case 5: case 6: case 8: case 10:
      for (i = 1; i <= maxi; i++) Try(pos, i, avail); break;
    case  3: i = rem3(0,  1,  2); // if (p[0] > i)
      Try(pos, i, avail); break;
    // Row 1, mirrored on the vertical axis
    case  7: i = rem3(4,  5,  6); Try(pos, i, avail); break;   // Zeile 2
    case  9: i = rem3(0,  4,  8); // if ((p[0] > i) && (p[3] > i))
      Try(pos, i, avail); break;   // Spalte 1
    case 11: i = rem3(1,  5, 10); Try(pos, i, avail); break;   // Spalte 2
    case 12: i = rem3(5,  6, 10); Try(pos, i, avail); break;   // mittlerer Block
    case 13: i = rem3(8, 10, 12); Try(pos, i, avail); break;   // Zeile 3
    case 14: i = rem3(2,  6, 12); Try(pos, i, avail); break;   // Spalte 3
    case 15: i = rem3(3,  7, 13); // if (p[0] > i)
      Try(pos, i, avail); break;
    // Column 4, mirrored on the main diagonal
    case 16: if (rem3(0, 5, 12) == p[15]) {
        showValue(millis() / 1000, p0, true);
        showValue(++count, p1, false);
        // signal the current solution
        Serial.print(F("Solution #"));
        Serial.print(count);
        Serial.print(F(" ["));
        Serial.print(millis() / 1000);
        Serial.println(F(" sec]:"));
        for (size_t pr = 0; pr < maxi; pr++) {
          Serial.print(str[solution[seq[pr]] * 2]);
          Serial.print(str[solution[seq[pr]] * 2 + 1]);
          Serial.print(' ');
          if (((pr + 1) % cols) == 0) {
            Serial.println();
          }
        }
        Serial.println();
        tone(A0, 1000, 100);
        delay(5000);
      }
  }
}

void Try(byte pos, byte value, int avail) {
  if ((value < 1) || (value > maxi)) return;  // ungueltige Zahl
  int mask = 1 << (value - 1);
  if (avail & mask) {
    p[pos] = value;
    dispNumAtpos(value, pos);
    place(pos + 1, avail & ~mask);
    dispNumAtpos(0, pos); // loesche Zelle
    p[pos] = 0;
  }
}

byte rem3(byte a, byte b, byte c) {  // gib den Rest zurueck
  return sum - p[a] - p[b] - p[c];
}

void dispNumAtpos(byte number, byte position) {
  solution[position] = number;
  //if (number != 0) {
  tft.setCursor(disp[position].left, disp[position].top);
  int farbe = colour[number];
  tft.setTextColor(farbe);
  number = number * 2;
  tft.print(str[number++]);
  tft.print(str[number]);
  //}
  //else
  //  // delete number
  //  tft.fillRect(disp[position].left, disp[position].top - 1, 11, 8, ILI9341_BLACK);
}

// convert seconds to HH:MM:SS
char ch[9];
long val;
void setDigit(byte pos, long quot) {
  byte tmp = val / quot;
  ch[pos] = ch[pos] + tmp;
  val = val % quot;
}

// nur fuer millis() und count:
void showValue(long value, byte xPos, boolean hms)  {
  byte br = 4;
  if (hms) {
    strcpy(ch, "00:00:00");
    val = value;
    setDigit(0, 36000);
    setDigit(1, 3600);
    setDigit(3, 600);
    setDigit(4, 60);
    setDigit(6, 10);
    setDigit(7, 1);
    br = 8;
  } else
    ltoa(value, ch, 10);
  // clear the old entry:
  tft.fillRect(xPos - 1, h - 11, br * 6 + 1, 9, ILI9341_BLACK);
  // right-align count:
  byte b = (br - strlen(ch)) * 6;
  tft.setTextColor(0x07FF);
  tft.setCursor(xPos + b, h - 10);
  tft.print(ch);
}

word hue(float z) {
  int M = 127;
  float h = z * M * 6 / 17;
  int g = max(-abs(h - 2 * M) + 3 * M - M, 0);
  int r = max(+abs(h - 3 * M) + 0 * M - M, 0);
  int b = max(-abs(h - 4 * M) + 3 * M - M, 0);
  return tft.color565(b, g, r);
}
1 Like

Cool.

modded code
const byte cols = 4;
const byte maxi = cols * cols;
const byte sum = (maxi + 1) * maxi / 2 / cols;
byte p[maxi]; // Holds the current square
const byte seq[] = {
  0, 1, 2, 3,
  4, 5, 6, 7,
  8, 10, 12, 13,
  9, 11, 14, 15
};

int count = 0;
unsigned long startTime;

void setup() {
  Serial.begin(115200);
  delay(1000); // Give serial monitor time to connect
  Serial.println(F("Generating 4x4 magic squares...\n"));
  startTime = millis();
  place(0, 0xFFFF);
  Serial.print(F("\nDone. Total solutions: "));
  Serial.println(count);
}

void loop() {}

void place(byte pos, int avail) {
  byte i;
  switch (pos) {
    case 0: case 1: case 2: case 4: case 5: case 6: case 8: case 10:
      for (i = 1; i <= maxi; i++) Try(pos, i, avail); break;
    case 3:  i = rem3(0, 1, 2); Try(pos, i, avail); break;
    case 7:  i = rem3(4, 5, 6); Try(pos, i, avail); break;
    case 9:  i = rem3(0, 4, 8); Try(pos, i, avail); break;
    case 11: i = rem3(1, 5, 10); Try(pos, i, avail); break;
    case 12: i = rem3(5, 6, 10); Try(pos, i, avail); break;
    case 13: i = rem3(8, 10, 12); Try(pos, i, avail); break;
    case 14: i = rem3(2, 6, 12); Try(pos, i, avail); break;
    case 15:
      i = rem3(3, 7, 13);
      Try(pos, i, avail);
      break;
    case 16:
      if (rem3(0, 5, 12) == p[15]) {
        count++;
        unsigned long elapsed = (millis() - startTime) / 1000;
        Serial.print(F("Solution #"));
        Serial.print(count);
        Serial.print(F(" ["));
        Serial.print(elapsed);
        Serial.println(F(" sec]:"));
        printSquare();
        delay(10); // optional: slow output if needed
      }
  }
}

void Try(byte pos, byte value, int avail) {
  if ((value < 1) || (value > maxi)) return;
  int mask = 1 << (value - 1);
  if (avail & mask) {
    p[pos] = value;
    place(pos + 1, avail & ~mask);
    p[pos] = 0;
  }
}

byte rem3(byte a, byte b, byte c) {
  return sum - p[a] - p[b] - p[c];
}

void printSquare() {
  for (byte i = 0; i < 16; i++) {
    Serial.print(p[seq[i]] < 10 ? " " : "");
    Serial.print(p[seq[i]]);
    Serial.print(" ");
    if ((i + 1) % 4 == 0) Serial.println();
  }
  Serial.println();
}

Running on an Uno (or Mega) seemed to have some memory problems and resets after 10 (or 30) seconds.

...
Solution #248 [30 sec]:
 1 12 15  6 
14  9  4  7 
 3  8 13 10 
16  5  2 11 

Generating 4x4 magic squares...

Solution #1 [0 sec]:
 1  2 15 16 
12 14  3  5 
13  7 10  4 
 8 11  6  9 
...

And on an ESP32:

...
Solution #7038 [500 sec]:
16 15  1  2 
 4  3 13 14 
 5 10  8 11 
 9  6 12  7 

Solution #7039 [500 sec]:
16 15  2  1 
 4  3 14 13 
 5 10  7 12 
 9  6 11  8 

Solution #7040 [500 sec]:
16 15  2  1 
 5  3 14 12 
 4 10  7 13 
 9  6 11  8 


Done. Total solutions: 7040

Storing the data in solution order rather than print order makes the code for the later constraints rather difficult to understand.

An interesting little diversion! The code is indeed difficult to follow.

Here is a non-optimized serial only version of the above. Running on a 3.3V 8 MHz Pro Mini powerhouse, it finds about four solutions per second.

/*
  Magische Quadrate 4x4, rekursiv
Serial only version
*/

struct cell {
  int left;
  int top;
};

int w, h;
const byte cols = 4;
const byte maxi = cols * cols;
const byte sum = (maxi + 1) * maxi / 2 / cols;
byte p[maxi];
int colour[maxi + 1];

// Reihenfolge, in der die Werte gesetzt werden:
const byte seq[] = {
  0,  1,  2,  3,
  4,  5,  6,  7,
  8, 10, 12, 13,
  9, 11, 14, 15
};

/* die Positionen:
  _0 _1 _2  3    alle Postionen ohne "_"
  _4 _5 _6  7    koennen berechnet werden!
  _8 _9 10 11
  12 13 14 15
*/

cell disp[maxi];
char str[2 * maxi + 2];
int count = 0;
const byte charW = 13;
const byte charH = 13;
const byte left = 39;
const byte top =  59;
const byte p0 = 2;
const byte p1 = 102;
byte solution[maxi + 1] = {0};

void setup() {
  Serial.begin(115200);
  Serial.println(F(__FILE__));
  // calculate the positions of the 16 cells
  for (int i = 0; i < maxi; i++) {
    int j = seq[i];
    disp[j].left =  left + 1 + (i & 3) * charW;
    disp[j].top  = top + 2 + (i / 4) * charH;
  }
  // Generate the strings to display the numbers faster
  str[0] = 0xDB;
  str[1] = 0xDB;
  for (int i = 1; i <= maxi; i++) {
//    colour[i] = hue(i);
    str[i * 2] = i < 10 ? ' ' : '1';
    str[i * 2 + 1] = '0' + i % 10;
    dispNumAtpos(0, i);
  }

  // start the recursion:
  place(0, 0xFFFF);
  // finished and ready.
}

void loop() {}

void place(byte pos, int avail) {
  byte i;
  switch (pos) {
    case  0: case 1: case 2: case 4: case 5: case 6: case 8: case 10:
      for (i = 1; i <= maxi; i++) Try(pos, i, avail); break;
    case  3: i = rem3(0,  1,  2); // if (p[0] > i)
      Try(pos, i, avail); break;
    // Row 1, mirrored on the vertical axis
    case  7: i = rem3(4,  5,  6); Try(pos, i, avail); break;   // Zeile 2
    case  9: i = rem3(0,  4,  8); // if ((p[0] > i) && (p[3] > i))
      Try(pos, i, avail); break;   // Spalte 1
    case 11: i = rem3(1,  5, 10); Try(pos, i, avail); break;   // Spalte 2
    case 12: i = rem3(5,  6, 10); Try(pos, i, avail); break;   // mittlerer Block
    case 13: i = rem3(8, 10, 12); Try(pos, i, avail); break;   // Zeile 3
    case 14: i = rem3(2,  6, 12); Try(pos, i, avail); break;   // Spalte 3
    case 15: i = rem3(3,  7, 13); // if (p[0] > i)
      Try(pos, i, avail); break;
    // Column 4, mirrored on the main diagonal
    case 16: if (rem3(0, 5, 12) == p[15]) {
        // signal the current solution
        Serial.print(F("Solution #"));
        Serial.print(++count);
        Serial.print(F(" ["));
        Serial.print(millis() / 1000);
        Serial.println(F(" sec]:"));
        for (size_t pr = 0; pr < maxi; pr++) {
          Serial.print(str[solution[seq[pr]] * 2]);
          Serial.print(str[solution[seq[pr]] * 2 + 1]);
          Serial.print(' ');
          if (((pr + 1) % cols) == 0) {
            Serial.println();
          }
        }
        Serial.println();
      }
  }
}

void Try(byte pos, byte value, int avail) {
  if ((value < 1) || (value > maxi)) return;  // ungueltige Zahl
  int mask = 1 << (value - 1);
  if (avail & mask) {
    p[pos] = value;
    dispNumAtpos(value, pos);
    place(pos + 1, avail & ~mask);
    dispNumAtpos(0, pos); // loesche Zelle
    p[pos] = 0;
  }
}

byte rem3(byte a, byte b, byte c) {  // gib den Rest zurueck
  return sum - p[a] - p[b] - p[c];
}

void dispNumAtpos(byte number, byte position) {
  solution[position] = number;
}

Snapshot:

Solution #1942 [490 sec]:
 5 11  6 12 
10  7 16  1 
15  2  9  8 
 4 14  3 13 

Solution #1943 [490 sec]:
 5 11  6 12 
10 13  4  7 
 3  2 15 14 
16  8  9  1 

1832 seconds total to find 7040 solutions.

1 Like

I'm not familiar with the algorithm for generating the solutions, but if it were faster to generate just the 880 primary solutions, the mirroring and rotation could be done by rearranging the print order.

Brilliant, thank you! Running now and delivered the first thousand in 122s with a Uno R3.

Posting the best I've achieved below. That's after (varying quality) collaboration with ChatGPT on display-to-print conversion of my original source. But it does not meet the 'diagonals constraint'.

/*
  Magic Squares 4x4, Recursive — Serial Monitor version
  Based on original sketch using TFT display from KlausJ (& Bruce Hall?)
  https://www.hackster.io/Klausj/r3-r4-compare-using-magic-square-6d7e7f
  Unsure whether that includes the 'diagonals constraint', because mine
  adds only rows and cols to 34, but not diags.
*/

const byte cols = 4;
const byte maxi = cols * cols;
const byte sum = (maxi + 1) * maxi / 2 / cols; // 34
byte p[maxi];
const byte seq[] =
{
  0,  1,  2,  3,
  4,  5,  6,  7,
  8, 10, 12, 13,
  9, 11, 14, 15
};
int count = 0;

void setup()
{
  Serial.begin(115200);
  delay(1000);
  Serial.println(F("Generating 4x4 magic squares..."));
  place(0, 0xFFFF);
  Serial.println(F("Done."));
  Serial.print(F("Total solutions: "));
  Serial.println(count);
}

void loop() {}

void place(byte pos, int avail)
{
  byte i;
  switch (pos)
  {
    case  0: case 1: case 2: case 4:
    case 5: case 6: case 8: case 10:
      for (i = 1; i <= maxi; i++) Try(pos, i, avail);
      break;

    case  3:  i = rem3(0, 1, 2);    Try(pos, i, avail); break;
    case  7:  i = rem3(4, 5, 6);    Try(pos, i, avail); break;
    case  9:  i = rem3(0, 4, 8);    Try(pos, i, avail); break;
    case 11:  i = rem3(1, 5, 10);   Try(pos, i, avail); break;
    case 12:  i = rem3(5, 6, 10);   Try(pos, i, avail); break;
    case 13:  i = rem3(8, 10, 12);  Try(pos, i, avail); break;
    case 14:  i = rem3(2, 6, 12);   Try(pos, i, avail); break;
    case 15:
      i = rem3(3, 7, 13);
      if (isAvailable(i, avail))
      {
        p[pos] = i;
        printSolution(++count);
        p[pos] = 0;
      }
      break;
  }
}

void Try(byte pos, byte value, int avail)
{
  if ((value < 1) || (value > maxi)) return;
  int mask = 1 << (value - 1);
  if (avail & mask)
  {
    p[pos] = value;
    place(pos + 1, avail & ~mask);
    p[pos] = 0;
  }
}

byte rem3(byte a, byte b, byte c)
{
  return sum - p[a] - p[b] - p[c];
}

bool isAvailable(byte value, int avail)
{
  if (value < 1 || value > maxi) return false;
  return avail & (1 << (value - 1));
}

void printSolution(int num)
{
  Serial.print(F("Solution #"));
  Serial.println(num);
  for (byte row = 0; row < 4; row++)
  {
    for (byte col = 0; col < 4; col++)
    {
      byte i = row * 4 + col;
      byte val = p[seq[i]];
      if (val < 10) Serial.print(" ");
      Serial.print(val);
      Serial.print(" ");
    }
    Serial.println();
  }
  Serial.println();
}

EDIT: "1832 seconds total to find 7040 solutions."
923s here.

You were right. See my partially successful version in post 14.

I'm pretty sure that would be faster than waiting patiently for the other 6,160.

On my Uno yours running for 5 mins now. First thousand in 135 s.

No obvious competition for that port or CPU during that Uno run of yours?

EDIT:
12:01:12.742 -> Solution #7040 [1013 sec]:
12:01:12.742 -> 16 15 2 1
12:01:12.742 -> 5 3 14 12
12:01:12.742 -> 4 10 7 13
12:01:12.742 -> 9 6 11 8

Yes. I think that for clarity of the constraints I might store the values in the final order, but that costs more indirection in the loop. Knowing you need to specify the constraints with their solution-sequence order indices is easy enough:

// Reihenfolge, in der die Werte gesetzt werden:
const byte seq[] = {
  0,  1,  2,  3,
  4,  5,  6,  7,
  8, 10, 12, 13,
  9, 11, 14, 15
};

And I’m still not sure about enforcing the middle-4 constraint instead of the forward slash diagonal. Maybe they are redundant?

Oh- you can use the redundant constraints to shorten the search. For instance, one could calculate case 10 from the 3+6+9 diagonal constraint rather than search it.

Edit: Calculating case 10 rather than exhaustively searching it speeds it up appreciably:

@jremington code plus /-diagonal calculation
/*
  Magische Quadrate 4x4, rekursiv
Serial only version
for https://forum.arduino.cc/t/4-x-4-magic-square-solutions/1386274/18
*/

struct cell {
  int left;
  int top;
};

int w, h;
const byte cols = 4;
const byte maxi = cols * cols;
const byte sum = (maxi + 1) * maxi / 2 / cols;
byte p[maxi];
int colour[maxi + 1];

// Reihenfolge, in der die Werte gesetzt werden:
const byte seq[] = {
  0,  1,  2,  3,
  4,  5,  6,  7,
  8, 10, 12, 13,
  9, 11, 14, 15
};

/* die Positionen:
  _0 _1 _2  3    alle Postionen ohne "_"
  _4 _5 _6  7    koennen berechnet werden!
  _8 _9 10 11
  12 13 14 15
*/

cell disp[maxi];
char str[2 * maxi + 2];
int count = 0;
const byte charW = 13;
const byte charH = 13;
const byte left = 39;
const byte top =  59;
const byte p0 = 2;
const byte p1 = 102;
byte solution[maxi + 1] = {0};

void setup() {
  Serial.begin(115200);
  Serial.println(F(__FILE__));
  // calculate the positions of the 16 cells
  for (int i = 0; i < maxi; i++) {
    int j = seq[i];
    disp[j].left =  left + 1 + (i & 3) * charW;
    disp[j].top  = top + 2 + (i / 4) * charH;
  }
  // Generate the strings to display the numbers faster
  str[0] = 0xDB;
  str[1] = 0xDB;
  for (int i = 1; i <= maxi; i++) {
//    colour[i] = hue(i);
    str[i * 2] = i < 10 ? ' ' : '1';
    str[i * 2 + 1] = '0' + i % 10;
    dispNumAtpos(0, i);
  }

  // start the recursion:
  place(0, 0xFFFF);
  // finished and ready.
}

void loop() {}

void place(byte pos, int avail) {
  byte i;
  switch (pos) {
    case  0: case 1: case 2: case 4: case 5: case 6: case 8: 
      for (i = 1; i <= maxi; i++) Try(pos, i, avail); break;
    case  3: i = rem3(0,  1,  2); // if (p[0] > i)
      Try(pos, i, avail); break;
    // Row 1, mirrored on the vertical axis
    case  7: i = rem3(4,  5,  6); Try(pos, i, avail); break;   // Zeile 2
    case  9: i = rem3(0,  4,  8); // if ((p[0] > i) && (p[3] > i))
      Try(pos, i, avail); break;   // Spalte 1
    case 10: i = rem3(9,  6, 3); Try(pos, i, avail); break;   // /-diagonal
    case 11: i = rem3(1,  5, 10); Try(pos, i, avail); break;   // Spalte 2
    case 12: i = rem3(5,  6, 10); Try(pos, i, avail); break;   // mittlerer Block
    case 13: i = rem3(8, 10, 12); Try(pos, i, avail); break;   // Zeile 3
    case 14: i = rem3(2,  6, 12); Try(pos, i, avail); break;   // Spalte 3
    case 15: i = rem3(3,  7, 13); // if (p[0] > i)
      Try(pos, i, avail); break;
    // Column 4, mirrored on the main diagonal
    case 16: if (rem3(0, 5, 12) == p[15]) {
        // signal the current solution
        Serial.print(F("Solution #"));
        Serial.print(++count);
        Serial.print(F(" ["));
        Serial.print(millis() / 1000);
        Serial.println(F(" sec]:"));
        for (size_t pr = 0; pr < maxi; pr++) {
          Serial.print(str[solution[seq[pr]] * 2]);
          Serial.print(str[solution[seq[pr]] * 2 + 1]);
          Serial.print(' ');
          if (((pr + 1) % cols) == 0) {
            Serial.println();
          }
        }
        Serial.println();
      }
  }
}

void Try(byte pos, byte value, int avail) {
  if ((value < 1) || (value > maxi)) return;  // ungueltige Zahl
  int mask = 1 << (value - 1);
  if (avail & mask) {
    p[pos] = value;
    dispNumAtpos(value, pos);
    place(pos + 1, avail & ~mask);
    dispNumAtpos(0, pos); // loesche Zelle
    p[pos] = 0;
  }
}

byte rem3(byte a, byte b, byte c) {  // gib den Rest zurueck
  return sum - p[a] - p[b] - p[c];
}

void dispNumAtpos(byte number, byte position) {
  solution[position] = number;
}

On an Uno R3 in Wokwi:

...
Solution #999 [54 sec]:
 3 10  7 14 
 9  8 13  4 
16  1 12  5 
 6 15  2 11 

Solution #1000 [54 sec]:
 3 10  7 14 
13  4  9  8 
12  5 16  1 
 6 15  2 11 

Solution #1001 [54 sec]:
 3 10  8 13 
 5 16  2 11 
14  7  9  4 
12  1 15  6 
...
Solution #7039 [383 sec]:
16 15  2  1 
 4  3 14 13 
 5 10  7 12 
 9  6 11  8 

Solution #7040 [383 sec]:
16 15  2  1 
 5  3 14 12 
 4 10  7 13 
 9  6 11  8 

I figured 440 at most, because each round starts by placing something in the upper left corner.

Counting four rotations times one horizontal and one vertical reflection, there should be at least 16 symmetry related solutions. 7040/16 = 440.

Am I overlooking something?

This is @DaveX's adjustment to @jremington's code from #12, I think. Halfway converted to C++ for real machines.

Currently it prints 0 - 7039 as fast as it can print.

/*
  Magische Quadrate 4x4, rekursiv
Serial only version
for https://forum.arduino.cc/t/4-x-4-magic-square-solutions/1386274/18
*/

# include <stdio.h>

typedef unsigned char byte;

struct cell {
  int left;
  int top;
};

int w, h;
const byte cols = 4;
const byte maxi = cols * cols;
const byte sum = (maxi + 1) * maxi / 2 / cols;
byte p[maxi];
int colour[maxi + 1];

// Reihenfolge, in der die Werte gesetzt werden:
const byte seq[] = {
  0,  1,  2,  3,
  4,  5,  6,  7,
  8, 10, 12, 13,
  9, 11, 14, 15
};

/* die Positionen:
  _0 _1 _2  3    alle Postionen ohne "_"
  _4 _5 _6  7    koennen berechnet werden!
  _8 _9 10 11
  12 13 14 15
*/

cell disp[maxi];
char str[2 * maxi + 2];
int count = 0;
const byte charW = 13;
const byte charH = 13;
const byte left = 39;
const byte top =  59;
const byte p0 = 2;
const byte p1 = 102;
byte solution[maxi + 1] = {0};

void place(byte, int);

void dispNumAtpos(byte number, byte position) {
  solution[position] = number;
}

void Try(byte pos, byte value, int avail) {
  if ((value < 1) || (value > maxi)) return;  // ungueltige Zahl
  int mask = 1 << (value - 1);
  if (avail & mask) {
    p[pos] = value;
    dispNumAtpos(value, pos);
    place(pos + 1, avail & ~mask);
    dispNumAtpos(0, pos); // loesche Zelle
    p[pos] = 0;
  }
}

byte rem3(byte a, byte b, byte c) {  // gib den Rest zurueck
  return sum - p[a] - p[b] - p[c];
}

int main() {
  // Serial.begin(115200);
  // Serial.println(F(__FILE__));
  // calculate the positions of the 16 cells
  for (int i = 0; i < maxi; i++) {
    int j = seq[i];
    disp[j].left =  left + 1 + (i & 3) * charW;
    disp[j].top  = top + 2 + (i / 4) * charH;
  }
  // Generate the strings to display the numbers faster
  str[0] = 0xDB;
  str[1] = 0xDB;
  for (int i = 1; i <= maxi; i++) {
//    colour[i] = hue(i);
    str[i * 2] = i < 10 ? ' ' : '1';
    str[i * 2 + 1] = '0' + i % 10;
    dispNumAtpos(0, i);
  }

  // start the recursion:
  place(0, 0xFFFF);
  // finished and ready.
  
  return 0;
}

void place(byte pos, int avail) {
  byte i;
  switch (pos) {
    case  0: case 1: case 2: case 4: case 5: case 6: case 8: 
      for (i = 1; i <= maxi; i++) Try(pos, i, avail); break;
    case  3: i = rem3(0,  1,  2); // if (p[0] > i)
      Try(pos, i, avail); break;
    // Row 1, mirrored on the vertical axis
    case  7: i = rem3(4,  5,  6); Try(pos, i, avail); break;   // Zeile 2
    case  9: i = rem3(0,  4,  8); // if ((p[0] > i) && (p[3] > i))
      Try(pos, i, avail); break;   // Spalte 1
    case 10: i = rem3(9,  6, 3); Try(pos, i, avail); break;   // /-diagonal
    case 11: i = rem3(1,  5, 10); Try(pos, i, avail); break;   // Spalte 2
    case 12: i = rem3(5,  6, 10); Try(pos, i, avail); break;   // mittlerer Block
    case 13: i = rem3(8, 10, 12); Try(pos, i, avail); break;   // Zeile 3
    case 14: i = rem3(2,  6, 12); Try(pos, i, avail); break;   // Spalte 3
    case 15: i = rem3(3,  7, 13); // if (p[0] > i)
      Try(pos, i, avail); break;
    // Column 4, mirrored on the main diagonal
    case 16: if (rem3(0, 5, 12) == p[15]) {
        // signal the current solution
        // Serial.print(F("Solution #"));
        printf("%d\n", count);
        count++;
        // Serial.print(F(" ["));
        // Serial.print(millis() / 1000);
        // Serial.println(F(" sec]:"));
        for (int pr = 0; pr < maxi; pr++) {
          // Serial.print(str[solution[seq[pr]] * 2]);
          // Serial.print(str[solution[seq[pr]] * 2 + 1]);
          // Serial.print(' ');
          if (((pr + 1) % cols) == 0) {
            // Serial.println();
          }
        }
        // Serial.println();
      }
  }
}

If the algorithm is the interesting part, you can save some time. If you are going to spend all day playing with this, write portable guts so it can be run later on a dinky microprocessor assuming Arduino and Serial.print.

a7