Files
Flipdot-display/src/esp32/logic/life.h
T

149 lines
5.8 KiB
C++
Raw Normal View History

2026-10-20 17:52:25 +02:00
#pragma once
#include <stdint.h>
#include "logic/bitmap_format.h"
// Conway's Game of Life for the Life page element: a board with its rules (LifeBoard) and
// the timing on top of it (LifeRunner). Pure logic, tested in test/test_esp32.
//
// Rules: a live cell with 2 or 3 live neighbours stays alive, a dead cell with exactly 3
// becomes alive, everything else dies.
// What happens at the edges of the area
const uint8_t LIFE_EDGES_WRAP = 0; // what leaves on one side comes back on the other
const uint8_t LIFE_EDGES_DEAD = 1; // outside the area everything is dead
const uint8_t LIFE_EDGES_MARGIN = 2; // a hidden margin around the area; gliders fly out of view
const uint8_t LIFE_EDGES_COUNT = 3;
// Hidden margin: dots on every side; its outer LIFE_MARGIN_KILL rings are cleared after
// every step, so what flies out disappears. Gliders that hit those rings leave a block or a
// blinker behind; cells of the margin (further than LIFE_MARGIN_KEEP from the area) that
// stay alive LIFE_MARGIN_STILL generations in a row are cleared too, with the cells around
// them (one at a time, a block grows back). Moving things never keep a cell alive that long.
const int LIFE_MARGIN = 16;
const int LIFE_MARGIN_KILL = 2;
const int LIFE_MARGIN_KEEP = 3;
const uint8_t LIFE_MARGIN_STILL = 12;
const int LIFE_MAX_WIDTH = BITMAP_WIDTH + 2 * LIFE_MARGIN;
const int LIFE_MAX_HEIGHT = BITMAP_HEIGHT + 2 * LIFE_MARGIN;
const int LIFE_MAX_ROW_BYTES = (LIFE_MAX_WIDTH + 7) / 8;
// Cycles up to this period count as "ended" (still lifes, blinkers, pulsars: 3,
// pentadecathlons: 15). A glider gun (30) keeps running.
const int LIFE_MAX_PERIOD = 16;
// Simple random numbers (xorshift32); the state must not be 0
uint32_t lifeRandom(uint32_t &state);
// Where a pattern (drawn anywhere in a full screen bitmap) goes in an area of width x
// height: its dot x, y goes to x + dx, y + dy. In the middle, or at a random place where it
// fits (in the middle in a direction it doesn't fit). False when it has no dots.
bool lifePlacement(const uint8_t *pattern, int width, int height, bool random_place, uint32_t &random, int &dx,
int &dy);
class LifeBoard
{
public:
// An empty board for an area of width x height dots (1-84 x 1-19)
void begin(int width, int height, uint8_t edges);
void clear();
int width() const { return _width; }
int height() const { return _height; }
uint8_t edges() const { return _edges; }
// Cells of the area (0, 0 is its top left); outside it false
bool cell(int x, int y) const;
void setCell(int x, int y, bool alive);
// Every cell of the area alive with this chance (0-100 %)
void randomize(int percent, uint32_t &random);
// Places a pattern drawn at the top left of a full screen bitmap (an icon expanded
// with iconToBitmap): in the middle of the area, or at a random place in it. Parts
// that don't fit are left out. The board is cleared first. False without dots.
bool place(const uint8_t *pattern, bool random_place, uint32_t &random);
// One generation
void step();
uint32_t generation() const { return _generation; }
// Live cells in the area and the hidden margin
int population() const;
// The board looks like one of the last LIFE_MAX_PERIOD generations (still, or
// repeating with a short period); also when it is empty
bool repeating() const { return _repeating; }
private:
uint8_t _edges = LIFE_EDGES_WRAP;
int _width = 0; // the area
int _height = 0;
int _margin = 0; // LIFE_MARGIN with hidden margin
int _world_width = 0;
int _world_height = 0;
uint32_t _generation = 0;
bool _repeating = false;
uint32_t _history[LIFE_MAX_PERIOD] = {};
int _history_count = 0;
int _history_next = 0;
uint8_t _cells[LIFE_MAX_HEIGHT][LIFE_MAX_ROW_BYTES] = {};
uint8_t _next[LIFE_MAX_HEIGHT][LIFE_MAX_ROW_BYTES] = {};
uint8_t _age[LIFE_MAX_HEIGHT][LIFE_MAX_WIDTH] = {}; // hidden margin: generations alive in a row
bool _world(int x, int y) const; // world coordinates, with the edge rule
void _setWorld(int x, int y, bool alive);
void _clearMargin();
bool _far(int x, int y) const; // in the margin, further than LIFE_MARGIN_KEEP from the area
uint32_t _hash() const;
void _startHistory();
};
// When a run ends and how fast it goes
struct LifeTiming
{
float speed = 4; // generations per second
bool restart_when_repeating = true;
uint32_t max_generations = 0; // 0: no limit
uint32_t hold_ms = 5000; // after the end it keeps running this long, then restarts (empty: at once)
};
enum class LifeTick : uint8_t
{
Wait, // nothing changed
Stepped, // a new generation
Restart, // the run is over: start a new one (then call started())
};
class LifeRunner
{
public:
LifeBoard board;
void setTiming(const LifeTiming &timing) { _timing = timing; }
// A new run was put on the board at this time (generation 0 is shown first)
void started(uint32_t now);
// Steps when a generation is due; at most one step per call
LifeTick tick(uint32_t now);
// The ms until the next tick() does something (at least 1)
uint32_t msUntilNext(uint32_t now) const;
// The run ended (repeating or max generations) and is in its hold time
bool ending() const { return _ending; }
private:
LifeTiming _timing;
uint32_t _last_step = 0;
bool _ending = false;
uint32_t _ended_at = 0;
uint32_t _interval() const;
};
// Start modes of the Life element
const uint8_t LIFE_START_RANDOM = 0; // random cells (density %)
const uint8_t LIFE_START_PATTERN = 1; // one pattern (an icon)
const uint8_t LIFE_START_ROTATE = 2; // the patterns whose names start with the text, in order
const uint8_t LIFE_START_SHUFFLE = 3; // the same, in random order
const uint8_t LIFE_START_COUNT = 4;
// The pattern after `current` (-1: none yet) of `count` patterns: the next one (ROTATE) or
// a random other one (SHUFFLE). -1 when count is 0.
int lifeNextPattern(uint8_t start, int count, int current, uint32_t &random);