#pragma once #include #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);