Public Access
A new page element runs Conway's Game of Life in an area (the whole display by default), one per page. Start: random dots, one pattern, or the icons whose names start with a text in turn or in random order. Edges: wrap around, dead, or a hidden margin where gliders fly out of view (leftovers that stay put in the margin are cleared, so a glider gun runs cleanly). It restarts when the board is still or repeats within 16 generations and/or after a number of generations, after a hold time (an empty board at once). logic/life holds the board and timing (tested), display/life_player runs the board of the shown page, ticked before each frame; frames are paced like scrolling text. The element reuses existing fields, so the page format stays the same. The Icons tab has a library of 17 LifeWiki patterns (checked with a simulation), and the built-in page "Game of Life" shuffles through the life_ icons. About 12 KB flash, 8 KB RAM. Version 2.17.0, tested on the display over OTA. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
150 lines
5.8 KiB
C++
150 lines
5.8 KiB
C++
#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);
|