Game of Life Optimizations

Uses more memory to achieve much higher framerates on large setups. Neighbor counts are stored instead of constantly recalculated. CRC is no longer used for repeat detection so false positives are no longer possible.
This commit is contained in:
Brandon502
2024-11-09 15:23:08 -05:00
parent 537638e5a2
commit 549380a2bf

View File

@@ -5228,8 +5228,276 @@ static const char _data_FX_MODE_2DFRIZZLES[] PROGMEM = "Frizzles@X frequency,Y f
///////////////////////////////////////////
// 2D Cellular Automata Game of life //
// 2D Cellular Automata Game of Life //
///////////////////////////////////////////
typedef struct Cell {
uint8_t currentStatus : 1, currentNeighborCount : 4, oscillatorCheck : 1, spaceshipCheck : 1, unused : 1; // current data + repeated detection
uint8_t futureStatus : 1, futureNeighborCount : 4, edgeCell : 1, superDead : 1, hasColor : 1; // future data + opimizations
} Cell;
class GameOfLifeGrid {
private:
Cell* cells;
const int cols, rows, maxIndex;
const int nOffsets[8] = {-cols-1, -cols, -cols+1, -1, 1, cols-1, cols, cols+1}; // Neighbor offsets
const int offsetX[8] = {-1, 0, 1, -1, 1, -1, 0, 1};
const int offsetY[8] = {-1, -1, -1, 0, 0, 1, 1, 1};
public:
GameOfLifeGrid(Cell* data, int c, int r) : cells(data), cols(c), rows(r), maxIndex(r * c) {}
void getNeighborIndexes(unsigned neighbors[9], unsigned cIndex, unsigned x, unsigned y, bool wrap) {
bool edgeCell = cells[cIndex].edgeCell;
unsigned neighborCount = 0;
for (unsigned i = 0; i < 8; ++i) {
unsigned nIndex = cIndex + nOffsets[i];
if (edgeCell) {
int nX = x + offsetX[i], nY = y + offsetY[i];
if (nX < 0) {if (!wrap) continue; nIndex += cols;}
else if (nX >= cols) {if (!wrap) continue; nIndex -= cols;}
if (nY < 0) {if (!wrap) continue; nIndex += maxIndex;}
else if (nY >= rows) {if (!wrap) continue; nIndex -= maxIndex;}
}
neighbors[++neighborCount] = nIndex;
}
neighbors[0] = neighborCount;
}
void setCell(unsigned cIndex, unsigned x, unsigned y, bool alive, bool wrap) {
Cell* cell = &cells[cIndex];
cell->futureStatus = alive;
if (alive == cell->currentStatus) return; // No change
unsigned neighbors[9];
getNeighborIndexes(neighbors, cIndex, x, y, wrap);
int val = alive ? 1 : -1;
for (unsigned i = 1; i <= neighbors[0]; ++i) cells[neighbors[i]].futureNeighborCount += val;
}
void recalculateEdgeNeighbors(bool wrap) {
unsigned cIndex = 0;
for (unsigned y = 0; y < rows; ++y) for (unsigned x = 0; x < cols; ++x, ++cIndex) {
Cell* cell = &cells[cIndex];
if (cell->edgeCell) {
cell->futureNeighborCount = 0;
cell->currentNeighborCount = 0;
cell->superDead = 0;
unsigned neighbors[9];
getNeighborIndexes(neighbors, cIndex, x, y, wrap);
for (unsigned i = 1; i <= neighbors[0]; ++i) {
if (cells[neighbors[i]].currentStatus) { ++cell->futureNeighborCount; ++cell->currentNeighborCount; }
}
}
}
}
void shiftFutureToCurrent() {
for (unsigned i = 0; i < maxIndex; ++i) {
cells[i].currentStatus = cells[i].futureStatus;
cells[i].currentNeighborCount = cells[i].futureNeighborCount;
}
}
};
uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https://natureofcode.com/book/chapter-7-cellular-automata/
// and https://github.com/DougHaber/nlife-color , Modified By: Brandon Butler
if (!strip.isMatrix) return mode_static(); // not a 2D set-up
const uint16_t cols = SEGMENT.virtualWidth();
const uint16_t rows = SEGMENT.virtualHeight();
const size_t dataSize = SEGMENT.length() * sizeof(Cell); // Cell = 2 bytes
const size_t totalSize = dataSize + 5; // 5 bytes for prevRows, prevCols, prevPalette, prevWrap, soloGlider
if (!SEGENV.allocateData(totalSize)) return mode_static(); //allocation failed
uint8_t *prevRows = reinterpret_cast<uint8_t*>(SEGENV.data);
uint8_t *prevCols = reinterpret_cast<uint8_t*>(SEGENV.data + 1);
uint8_t *prevPalette = reinterpret_cast<uint8_t*>(SEGENV.data + 2);
bool *prevWrap = reinterpret_cast<bool*> (SEGENV.data + 3);
bool *soloGlider = reinterpret_cast<bool*> (SEGENV.data + 4);
Cell *cells = reinterpret_cast<Cell*> (SEGENV.data + 5);
uint16_t& generation = SEGENV.aux0; //Rename SEGENV/SEGMENT variables for readability
uint16_t& gliderLength = SEGENV.aux1;
bool allColors = SEGMENT.check1;
bool overlayBG = SEGMENT.check2;
bool wrap = SEGMENT.check3;
bool bgBlendMode = SEGMENT.custom1 > 220 && !overlayBG; // if blur is high and not overlaying, use bg blend mode
byte blur = overlayBG ? 255 : bgBlendMode ? map2(SEGMENT.custom1 - 220, 0, 35, 255, 128) : map2(SEGMENT.custom1, 0, 220, 255, 10);
uint32_t bgColor = SEGCOLOR(1);
GameOfLifeGrid grid(cells, cols, rows);
// If rows or cols change due to mirror/transpose, edges and neighbor counts need to be recalculated. Just reset the game.
bool setup = SEGENV.call == 0 || rows != *prevRows || cols != *prevCols;
if (setup) {
SEGMENT.setUpLeds();
SEGMENT.fill(bgColor); // to make sure that segment buffer and physical leds are aligned initially
SEGENV.step = 0;
*prevRows = rows;
*prevCols = cols;
// Calculate glider length LCM(rows,cols)*4 once
uint8_t a = rows;
uint8_t b = cols;
while (b) {
uint8_t t = b;
b = a % b;
a = t;
}
gliderLength = cols * rows / a * 4;
}
if (abs(long(strip.now) - long(SEGENV.step)) > 2000) SEGENV.step = 0; // Timebase jump fix
bool paused = SEGENV.step > strip.now;
// Setup New Game of Life
if ((!paused && generation == 0) || setup) {
SEGENV.step = strip.now + 1250; // show initial state for 1.25 seconds
paused = true;
generation = 1;
*prevWrap = wrap;
*prevPalette = SEGMENT.palette;
//Setup Grid
memset(cells, 0, dataSize);
#if !ESP32
random16_set_seed(strip.now>>2); //seed the random generator
#else
const uint32_t chance = UINT32_MAX * 0.32;
#endif
unsigned cIndex = 0;
for (unsigned y = 0; y < rows; ++y) for (unsigned x = 0; x < cols; ++x, ++cIndex) {
if (x == 0 || x == cols - 1 || y == 0 || y == rows - 1) cells[cIndex].edgeCell = 1;
#if ESP32
if (esp_random() < chance) grid.setCell(cIndex, x, y, true, wrap); // ~32% chance of being alive
#else
if (random16(100) < 32) grid.setCell(cIndex, x, y, true, wrap);
#endif
else { cells[cIndex].hasColor = 1; cells[cIndex].superDead = 1; }
}
grid.shiftFutureToCurrent(); // Shift future states to current states
}
bool palChanged = SEGMENT.palette != *prevPalette && !allColors;
if (palChanged) *prevPalette = SEGMENT.palette;
// Enter redraw loop if not updating or palette changed.
if (palChanged || paused || (SEGMENT.speed != 255 && strip.now - SEGENV.step < 1000 / map2(SEGMENT.speed,0,254,1,60))) { //(1 - 60) updates/sec 255 is uncapped
// Redraw if paused (remove blur), palette changed, overlaying background if not max speed (avoid flicker)
// Generation 1 draws alive cells randomly and fades dead cells
bool newGame = generation == 1;
if (paused || palChanged || overlayBG) {
unsigned cIndex = 0;
for (unsigned y = 0; y < rows; ++y) for (unsigned x = 0; x < cols; ++x, ++cIndex) {
Cell& cell = cells[cIndex];
if (!newGame && cell.superDead) continue; // Skip super dead cells unless new game
bool alive = cell.currentStatus;
uint32_t cellColor = SEGMENT.getPixelColorXY(x,y);
if (alive) {
if ((!cell.hasColor && !random(10)) || palChanged) {
uint32_t randomColor = allColors ? random16() * random16() : SEGMENT.color_from_palette(random8(), false, PALETTE_SOLID_WRAP, 0);
SEGMENT.setPixelColorXY(x,y, randomColor); // Palette changed or needs initial color
cells[cIndex].hasColor = 1;
}
else if (overlayBG && cell.hasColor) SEGMENT.setPixelColorXY(x,y, cellColor); // Redraw alive cells for overlayBG
} // Dead
else if (paused && !overlayBG) {
uint32_t blended = color_blend(cellColor, bgColor, bgBlendMode ? 16 : blur);
if (blended == cellColor) blended = bgColor; // color_blend fix
if ((bgBlendMode && newGame) || !bgBlendMode) SEGMENT.setPixelColorXY(x, y, blended); // Blur dead cells when paused
}
}
}
return FRAMETIME;
}
uint32_t color = allColors ? random16() * random16() : SEGMENT.color_from_palette(random8(), false, PALETTE_SOLID_WRAP, 0); // Backup color
if (generation <= 8) blur = 255 - (((generation-1) * (255 - blur)) >> 3); // Ramp up blur for first 8 generations
//Update Game of Life
bool disableWrap = !wrap || generation % 1500 == 0 || *soloGlider; // Disable wrap every 1500 generations to prevent undetected repeats
if (*prevWrap != !disableWrap) { grid.recalculateEdgeNeighbors(!disableWrap); *prevWrap = !disableWrap; }
// Repeat detection
unsigned aliveCount = 0; // Detects empty grids and solo gliders (for smaller grids)
bool updateOscillator = generation % 16 == 0;
bool updateSpaceship = gliderLength && generation % gliderLength == 0;
bool repeatingOscillator = true, repeatingSpaceship = true;
//Loop through all cells. Apply rules, setPixel
unsigned cIndex = 0;
for (unsigned y = 0; y < rows; ++y) for (unsigned x = 0; x < cols; ++x, ++cIndex) {
Cell& cell = cells[cIndex];
if (repeatingOscillator && cell.oscillatorCheck != cell.currentStatus) repeatingOscillator = false;
if (repeatingSpaceship && cell.spaceshipCheck != cell.currentStatus) repeatingSpaceship = false;
if (updateOscillator) cell.oscillatorCheck = cell.currentStatus;
if (updateSpaceship) cell.spaceshipCheck = cell.currentStatus;
unsigned neighbors = cell.currentNeighborCount;
if (cell.superDead && neighbors != 3) continue; // Skip super dead cells (bgColor dead cells)
bool cellValue = cell.currentStatus;
uint32_t cellColor = SEGMENT.getPixelColorXY(x, y);
if (cellValue) {
++aliveCount;
if (cellColor != bgColor) color = cellColor; // Update last seen color
if (neighbors < 2 || neighbors > 3) {
// Loneliness or Overpopulation
grid.setCell(cIndex, x, y, false, !disableWrap);
if (!overlayBG) SEGMENT.setPixelColorXY(x,y, blur == 255 ? bgColor : color_blend(cellColor, bgColor, blur));
if (blur == 255) cell.superDead = 1;
}
else SEGMENT.setPixelColorXY(x, y, cellColor == bgColor ? color : cellColor); // Redraw alive
}
else if (neighbors == 3 && !cellValue) {
// Reproduction
grid.setCell(cIndex, x, y, true, !disableWrap);
cell.superDead = 0;
uint32_t birthColor = color;
if (random8() < SEGMENT.intensity) birthColor = allColors ? random16() * random16() : SEGMENT.color_from_palette(random8(), false, PALETTE_SOLID_WRAP, 0);
else {
// Get Colors
uint32_t nColors[8];
unsigned colorCount = 0;
unsigned neighbors[9];
grid.getNeighborIndexes(neighbors, cIndex, x, y, !disableWrap);
for (unsigned i = 1; i <= neighbors[0]; ++i) {
unsigned nIndex = neighbors[i];
if (cells[nIndex].futureStatus) {
uint32_t nColor = SEGMENT.getPixelColorXY(nIndex % cols, nIndex / cols);
if (nColor == bgColor) continue;
nColors[colorCount++] = nColor;
}
}
if (colorCount) { birthColor = nColors[random8(colorCount)]; color = birthColor; }
}
SEGMENT.setPixelColorXY(x,y, birthColor);
}
else { // Already dead
if (blur != 255 && !overlayBG && !bgBlendMode) {
uint32_t blended = color_blend(cellColor, bgColor, blur); // color_blend doesn't always converge to bgColor (this fix needed for fast fps with custom bgColor)
if (blended == cellColor) { blended = bgColor; cell.superDead = 1; }
SEGMENT.setPixelColorXY(x, y, blended);
}
}
}
grid.shiftFutureToCurrent();
if (aliveCount == 5) *soloGlider = true; else *soloGlider = false;
if (repeatingOscillator || repeatingSpaceship || !aliveCount) {
generation = 0; // reset on next call
SEGENV.step += 1000; // pause final generation for 1 second
return FRAMETIME;
}
++generation;
SEGENV.step = strip.now;
return FRAMETIME;
} // mode_2Dgameoflife()
static const char _data_FX_MODE_2DGAMEOFLIFE[] PROGMEM = "Game Of Life@!,Color Mutation ☾,Blur ☾,,,All Colors ☾,Overlay BG ☾,Wrap ☾;!,!;!;2;sx=56,ix=2,c1=128,o1=0,o2=0,o3=1";
/////////////////////////
// 2D SnowFall //
/////////////////////////
static bool getBitValue(const uint8_t* byteArray, size_t n) {
size_t byteIndex = n / 8;
size_t bitIndex = n % 8;
@@ -5244,199 +5512,8 @@ static void setBitValue(uint8_t* byteArray, size_t n, bool value) {
else
byteArray[byteIndex] &= ~(1 << bitIndex);
}
uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https://natureofcode.com/book/chapter-7-cellular-automata/
// and https://github.com/DougHaber/nlife-color , Modified By: Brandon Butler
if (!strip.isMatrix) return mode_static(); // not a 2D set-up
const uint16_t cols = SEGMENT.virtualWidth();
const uint16_t rows = SEGMENT.virtualHeight();
const size_t dataSize = ((SEGMENT.length() + 7) / 8); // round up to nearest byte
const size_t detectionSize = sizeof(uint16_t) * 3 + 1; // 2 CRCs, gliderLength, soloGlider boolean
const size_t totalSize = dataSize * 2 + detectionSize + sizeof(uint8_t); // detectionSize + prevPalette
if (!SEGENV.allocateData(totalSize)) return mode_static(); //allocation failed
byte *cells = reinterpret_cast<byte*>(SEGENV.data);
byte *futureCells = reinterpret_cast<byte*>(SEGENV.data + dataSize);
uint16_t *gliderLength = reinterpret_cast<uint16_t*>(SEGENV.data + dataSize * 2);
uint16_t *oscillatorCRC = reinterpret_cast<uint16_t*>(SEGENV.data + dataSize * 2 + sizeof(uint16_t));
uint16_t *spaceshipCRC = reinterpret_cast<uint16_t*>(SEGENV.data + dataSize * 2 + sizeof(uint16_t) * 2);
bool *soloGlider = reinterpret_cast<bool*>(SEGENV.data + dataSize * 2 + sizeof(uint16_t) * 3);
uint8_t *prevPalette = reinterpret_cast<uint8_t*>(SEGENV.data + dataSize * 2 + detectionSize);
uint16_t &generation = SEGENV.aux0; //Rename SEGENV/SEGMENT variables for readability
bool allColors = SEGMENT.check1;
bool overlayBG = SEGMENT.check2;
bool wrap = SEGMENT.check3;
bool bgBlendMode = SEGMENT.custom1 > 220 && !overlayBG; // if blur is high and not overlaying, use bg blend mode
byte blur = bgBlendMode ? map2(SEGMENT.custom1 - 220, 0, 35, 255, 128) : map2(SEGMENT.custom1, 0, 255, 255, 0);
uint32_t bgColor = SEGCOLOR(1);
uint32_t color = allColors ? random16() * random16() : SEGMENT.color_from_palette(0, false, PALETTE_SOLID_WRAP, 0);
if (SEGENV.call == 0) {
SEGMENT.setUpLeds();
SEGMENT.fill(BLACK); // to make sure that segment buffer and physical leds are aligned initially
SEGENV.step = 0;
}
// fix SEGENV.step in case that timebase jumps
if (abs(long(strip.now) - long(SEGENV.step)) > 2000) SEGENV.step = 0;
// Setup New Game of Life
if ((SEGENV.call == 0 || generation == 0) && SEGENV.step < strip.now) {
SEGENV.step = strip.now + 1250; // show initial state for 1.25 seconds
generation = 1;
*prevPalette = SEGMENT.palette;
random16_set_seed(strip.now>>2); //seed the random generator
//Setup Grid
memset(cells, 0, dataSize);
for (unsigned x = 0; x < cols; x++) for (unsigned y = 0; y < rows; y++) {
if (random8(100) < 32) { // ~32% chance of being alive
setBitValue(cells, y * cols + x, true);
if (overlayBG) SEGMENT.setPixelColorXY(x,y, allColors ? random16() * random16() : SEGMENT.color_from_palette(random8(), false, PALETTE_SOLID_WRAP, 0));
else SEGMENT.setPixelColorXY(x,y, bgColor); // Initial color set in redraw loop
}
}
memcpy(futureCells, cells, dataSize);
//Set CRCs
uint16_t crc = crc16((const unsigned char*)cells, dataSize);
*oscillatorCRC = crc;
*spaceshipCRC = crc;
//Calculate glider length LCM(rows,cols)*4
uint8_t a = rows;
uint8_t b = cols;
while (b) {
uint8_t t = b;
b = a % b;
a = t;
}
*gliderLength = cols * rows / a * 4;
}
bool blurDead = SEGENV.step > strip.now && blur !=255 && !bgBlendMode && !overlayBG;
bool palChanged = SEGMENT.palette != *prevPalette && !allColors;
bool newGame = generation == 1;
if (palChanged) *prevPalette = SEGMENT.palette;
// Redraw Loop
// Redraw if paused (remove blur), palette changed, overlaying background (avoid flicker)
// Generation 1 draws alive cells randomly and fades dead cells
if (blurDead || newGame || palChanged || overlayBG) {
for (unsigned x = 0; x < cols; x++) for (unsigned y = 0; y < rows; y++) {
unsigned cIndex = y * cols + x;
uint32_t cellColor = SEGMENT.getPixelColorXY(x,y);
bool alive = getBitValue(cells, cIndex);
bool aliveBgColor = (!overlayBG && alive && newGame && cellColor == bgColor );
if ( alive && (palChanged || (aliveBgColor && !random(12)))) { // Palette change or spawn initial colors randomly
uint32_t randomColor = allColors ? random16() * random16() : SEGMENT.color_from_palette(random8(), false, PALETTE_SOLID_WRAP, 0);
SEGMENT.setPixelColorXY(x,y, randomColor); // Recolor alive cells
}
else if ( alive && overlayBG && !aliveBgColor) SEGMENT.setPixelColorXY(x,y, cellColor); // Redraw alive cells for overlayBG
if (!alive && palChanged && !overlayBG) SEGMENT.setPixelColorXY(x,y, bgColor); // Remove blurred cells from previous palette
else if (!alive && blurDead) SEGMENT.setPixelColorXY(x,y, color_blend(cellColor, bgColor, blur));// Blur dead cells (paused)
else if (!alive && !overlayBG && generation == 1) SEGMENT.setPixelColorXY(x,y, color_blend(cellColor, bgColor, 16)); // Fade dead cells on generation 1
}
}
if (!SEGMENT.speed || SEGENV.step > strip.now || (SEGMENT.speed != 255 && strip.now - SEGENV.step < 1000 / map2(SEGMENT.speed,0,254,0,60))) return FRAMETIME; //(0 - 60) updates/sec 255 is uncapped
//Update Game of Life
unsigned aliveCount = 0; // Detects dead grids and solo gliders
bool disableWrap = !wrap || (generation % 1500 == 0 || *soloGlider); // Disable wrap every 1500 generations to prevent undetected repeats
//Loop through all cells. Count neighbors, apply rules, setPixel
for (unsigned x = 0; x < cols; x++) for (unsigned y = 0; y < rows; y++) {
unsigned cIndex = y * cols + x;
bool cellValue = getBitValue(cells, cIndex);
uint32_t cellColor = SEGMENT.getPixelColorXY(x, y);
if (cellValue) aliveCount++;
unsigned neighbors = 0, colorCount = 0;
unsigned neighborIndexes[3];
// Count neighbors and store indexes, get neighbor colors later if needed
for (int i = -1; i <= 1; i++) for (int j = -1; j <= 1; j++) { // Iterate through all neighbors
if (i == 0 && j == 0) continue; // Ignore self
if (i == 1 && j == 0 && !cellValue && !neighbors) break; // Cell can't be born with no neighbors and 2 remaining checks
int nX = x + i;
int nY = y + j;
if (nX < 0) {if (disableWrap) continue; nX = cols - 1;}
else if (nX >= cols) {if (disableWrap) continue; nX = 0;}
if (nY < 0) {if (disableWrap) continue; nY = rows - 1;}
else if (nY >= rows) {if (disableWrap) continue; nY = 0;}
unsigned nIndex = nY * cols + nX; // Neighbor cell index
if (getBitValue(cells, nIndex)) {
++neighbors;
if (neighbors > 3) break; // Cell dies, stop neighbor loop
neighborIndexes[neighbors - 1] = nIndex; // Store alive neighbor index
}
}
if (!cellValue && neighbors != 3 && cellColor == bgColor) continue; // Skip dead cells with no neighbors and no color
// Rules of Life
if (cellValue && (neighbors < 2 || neighbors > 3)) {
// Loneliness or Overpopulation
setBitValue(futureCells, cIndex, false);
if (!overlayBG) SEGMENT.setPixelColorXY(x,y, color_blend(cellColor, bgColor, blur));
}
else if (neighbors == 3 && !cellValue) {
// Reproduction
// Get Colors
uint32_t nColors[3];
for (int i = 0; i < 3; i++) {
unsigned nIndex = neighborIndexes[i];
if (!getBitValue(futureCells, nIndex)) continue; // Parent just died, color lost or blended
uint32_t nColor = SEGMENT.getPixelColorXY(nIndex % cols, nIndex / cols);
if (nColor == bgColor) continue;
color = nColor; // Update last seen color
nColors[colorCount++] = nColor;
}
setBitValue(futureCells, cIndex, true);
uint32_t birthColor = colorCount ? nColors[random8(colorCount)] : color; // Uses last seen color if no surviving neighbors
// Mutate color chance
if (random8() < SEGMENT.intensity) birthColor = allColors ? random16() * random16() : SEGMENT.color_from_palette(random8(), false, PALETTE_SOLID_WRAP, 0);
SEGMENT.setPixelColorXY(x,y, birthColor);
}
else { // Blur dead cells and redraw alive cells
if (cellValue) SEGMENT.setPixelColorXY(x, y, cellColor == bgColor ? color : cellColor); // Redraw alive, fixes fading cells
else if (blur != 255 && !overlayBG && !bgBlendMode) SEGMENT.setPixelColorXY(x, y, color_blend(cellColor, bgColor, blur));
}
}
//update cell values
memcpy(cells, futureCells, dataSize);
// Get current crc value
uint16_t crc = crc16((const unsigned char*)cells, dataSize);
bool repetition = false;
if (!aliveCount || crc == *oscillatorCRC || crc == *spaceshipCRC) repetition = true; //check if cell changed this gen and compare previous stored crc values
if (repetition) {
generation = 0; // reset on next call
SEGENV.step += 1000; // pause final generation for 1 second
return FRAMETIME;
}
// Update CRC values
if (generation % 16 == 0) *oscillatorCRC = crc;
if (*gliderLength && generation % *gliderLength == 0) *spaceshipCRC = crc;
if (aliveCount == 5) *soloGlider = true; else *soloGlider = false;
generation++;
SEGENV.step = strip.now;
return FRAMETIME;
} // mode_2Dgameoflife()
static const char _data_FX_MODE_2DGAMEOFLIFE[] PROGMEM = "Game Of Life@!,Color Mutation ☾,Blur ☾,,,All Colors ☾,Overlay BG ☾,Wrap ☾;!,!;!;2;sx=56,ix=2,c1=128,o1=0,o2=0,o3=1";
/////////////////////////
// 2D SnowFall //
/////////////////////////
uint16_t mode_2DSnowFall(void) { // By: Brandon Butler
// Uses Game of Life style bit array to track snow/particles
// Uses bit array to track snow/particles
if (!strip.isMatrix) return mode_static(); // Not a 2D set-up
const uint16_t cols = SEGMENT.virtualWidth();
const uint16_t rows = SEGMENT.virtualHeight();