Public Access
Game of Life page element and built-in page
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>
This commit is contained in:
1 parent
948087077d
commit
a91d2550d7
29 files changed
+1402
-31
No files matched your search
@@ -3,8 +3,9 @@
|
||||
#include <string.h>
|
||||
|
||||
#include "logic/builtin_fonts.h"
|
||||
#include "logic/life.h"
|
||||
|
||||
static const char *const NAMES[] = {BUILTIN_PAGE_CLOCK};
|
||||
static const char *const NAMES[] = {BUILTIN_PAGE_CLOCK, BUILTIN_PAGE_LIFE};
|
||||
|
||||
int builtinPageCount()
|
||||
{
|
||||
@@ -42,6 +43,26 @@ static void clockPage(DynamicPage &page)
|
||||
strcpy(clock.font, FONT_NORMAL_NAME);
|
||||
}
|
||||
|
||||
// The whole display, the gliders of the glider gun fly out of view. The gun never repeats
|
||||
// (its period is 30), so a run ends after 600 generations (2.5 minutes).
|
||||
static void lifePage(DynamicPage &page)
|
||||
{
|
||||
clearPage(page);
|
||||
page.element_count = 1;
|
||||
PageElement &life = page.elements[0];
|
||||
life.type = ElementType::Life;
|
||||
life.bar_width = BITMAP_WIDTH;
|
||||
life.bar_height = BITMAP_HEIGHT;
|
||||
life.format = LIFE_EDGES_MARGIN;
|
||||
life.input_unit = LIFE_START_SHUFFLE;
|
||||
strcpy(life.text, "life_");
|
||||
life.factor = 30; // random dots, when there are no patterns
|
||||
life.scroll_speed = 4;
|
||||
life.auto_unit = true;
|
||||
life.target = 600;
|
||||
life.bar_max = 5;
|
||||
}
|
||||
|
||||
bool builtinPage(const char *name, DynamicPage &page)
|
||||
{
|
||||
if (name && sameName(name, BUILTIN_PAGE_CLOCK))
|
||||
@@ -49,5 +70,10 @@ bool builtinPage(const char *name, DynamicPage &page)
|
||||
clockPage(page);
|
||||
return true;
|
||||
}
|
||||
if (name && sameName(name, BUILTIN_PAGE_LIFE))
|
||||
{
|
||||
lifePage(page);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -5,10 +5,13 @@
|
||||
// The original dynamic pages. display/page_store stores a copy of each at boot that can be
|
||||
// edited; the original is the fallback (deleting the copy resets the page).
|
||||
//
|
||||
// Clock Pikachu with the time in a thought bubble, yawning from 17:00 (the clock of
|
||||
// the old firmware; also the default page)
|
||||
// Clock Pikachu with the time in a thought bubble, yawning from 17:00 (the clock
|
||||
// of the old firmware; also the default page)
|
||||
// Game of Life Conway's Game of Life on the whole display: the life_ icons (the
|
||||
// patterns of the Icons tab) in random order, random dots without them
|
||||
|
||||
#define BUILTIN_PAGE_CLOCK "Clock"
|
||||
#define BUILTIN_PAGE_LIFE "Game of Life"
|
||||
|
||||
int builtinPageCount();
|
||||
const char *builtinPageName(int index);
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
#include "logic/life.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
uint32_t lifeRandom(uint32_t &state)
|
||||
{
|
||||
if (state == 0)
|
||||
state = 0x9E3779B9;
|
||||
state ^= state << 13;
|
||||
state ^= state >> 17;
|
||||
state ^= state << 5;
|
||||
return state;
|
||||
}
|
||||
|
||||
void LifeBoard::begin(int width, int height, uint8_t edges)
|
||||
{
|
||||
if (width < 1)
|
||||
width = 1;
|
||||
if (width > BITMAP_WIDTH)
|
||||
width = BITMAP_WIDTH;
|
||||
if (height < 1)
|
||||
height = 1;
|
||||
if (height > BITMAP_HEIGHT)
|
||||
height = BITMAP_HEIGHT;
|
||||
_edges = edges < LIFE_EDGES_COUNT ? edges : LIFE_EDGES_WRAP;
|
||||
_width = width;
|
||||
_height = height;
|
||||
_margin = _edges == LIFE_EDGES_MARGIN ? LIFE_MARGIN : 0;
|
||||
_world_width = width + 2 * _margin;
|
||||
_world_height = height + 2 * _margin;
|
||||
clear();
|
||||
}
|
||||
|
||||
void LifeBoard::clear()
|
||||
{
|
||||
memset(_cells, 0, sizeof(_cells));
|
||||
memset(_age, 0, sizeof(_age));
|
||||
_generation = 0;
|
||||
_startHistory();
|
||||
}
|
||||
|
||||
bool LifeBoard::cell(int x, int y) const
|
||||
{
|
||||
if (x < 0 || y < 0 || x >= _width || y >= _height)
|
||||
return false;
|
||||
return _world(x + _margin, y + _margin);
|
||||
}
|
||||
|
||||
void LifeBoard::setCell(int x, int y, bool alive)
|
||||
{
|
||||
if (x < 0 || y < 0 || x >= _width || y >= _height)
|
||||
return;
|
||||
_setWorld(x + _margin, y + _margin, alive);
|
||||
_startHistory();
|
||||
}
|
||||
|
||||
void LifeBoard::randomize(int percent, uint32_t &random)
|
||||
{
|
||||
clear();
|
||||
for (int y = 0; y < _height; y++)
|
||||
for (int x = 0; x < _width; x++)
|
||||
if ((int)(lifeRandom(random) % 100) < percent)
|
||||
_setWorld(x + _margin, y + _margin, true);
|
||||
_startHistory();
|
||||
}
|
||||
|
||||
bool lifePlacement(const uint8_t *pattern, int width, int height, bool random_place, uint32_t &random, int &dx,
|
||||
int &dy)
|
||||
{
|
||||
int left = BITMAP_WIDTH, top = BITMAP_HEIGHT, right = -1, bottom = -1;
|
||||
for (int y = 0; y < BITMAP_HEIGHT; y++)
|
||||
for (int x = 0; x < BITMAP_WIDTH; x++)
|
||||
if (bitmapPixel(pattern, x, y))
|
||||
{
|
||||
if (x < left)
|
||||
left = x;
|
||||
if (x > right)
|
||||
right = x;
|
||||
if (y < top)
|
||||
top = y;
|
||||
if (y > bottom)
|
||||
bottom = y;
|
||||
}
|
||||
if (right < 0)
|
||||
return false;
|
||||
int pattern_width = right - left + 1;
|
||||
int pattern_height = bottom - top + 1;
|
||||
int x0 = (width - pattern_width) / 2;
|
||||
int y0 = (height - pattern_height) / 2;
|
||||
if (random_place)
|
||||
{
|
||||
if (pattern_width <= width)
|
||||
x0 = lifeRandom(random) % (width - pattern_width + 1);
|
||||
if (pattern_height <= height)
|
||||
y0 = lifeRandom(random) % (height - pattern_height + 1);
|
||||
}
|
||||
dx = x0 - left;
|
||||
dy = y0 - top;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LifeBoard::place(const uint8_t *pattern, bool random_place, uint32_t &random)
|
||||
{
|
||||
clear();
|
||||
int dx, dy;
|
||||
if (!lifePlacement(pattern, _width, _height, random_place, random, dx, dy))
|
||||
return false;
|
||||
for (int y = 0; y < BITMAP_HEIGHT; y++)
|
||||
for (int x = 0; x < BITMAP_WIDTH; x++)
|
||||
if (bitmapPixel(pattern, x, y))
|
||||
{
|
||||
int bx = x + dx, by = y + dy;
|
||||
if (bx >= 0 && by >= 0 && bx < _width && by < _height)
|
||||
_setWorld(bx + _margin, by + _margin, true);
|
||||
}
|
||||
_startHistory();
|
||||
return true;
|
||||
}
|
||||
|
||||
void LifeBoard::step()
|
||||
{
|
||||
memset(_next, 0, sizeof(_next));
|
||||
for (int y = 0; y < _world_height; y++)
|
||||
for (int x = 0; x < _world_width; x++)
|
||||
{
|
||||
int neighbours = 0;
|
||||
for (int dy = -1; dy <= 1; dy++)
|
||||
for (int dx = -1; dx <= 1; dx++)
|
||||
if ((dx || dy) && _world(x + dx, y + dy))
|
||||
neighbours++;
|
||||
if (neighbours == 3 || (neighbours == 2 && _world(x, y)))
|
||||
_next[y][x / 8] |= 0x80 >> (x % 8);
|
||||
}
|
||||
memcpy(_cells, _next, sizeof(_cells));
|
||||
if (_margin > 0)
|
||||
_clearMargin();
|
||||
_generation++;
|
||||
|
||||
uint32_t hash = _hash();
|
||||
_repeating = false;
|
||||
for (int i = 0; i < _history_count; i++)
|
||||
if (_history[i] == hash)
|
||||
_repeating = true;
|
||||
_history[_history_next] = hash;
|
||||
_history_next = (_history_next + 1) % LIFE_MAX_PERIOD;
|
||||
if (_history_count < LIFE_MAX_PERIOD)
|
||||
_history_count++;
|
||||
}
|
||||
|
||||
int LifeBoard::population() const
|
||||
{
|
||||
int count = 0;
|
||||
for (int y = 0; y < _world_height; y++)
|
||||
for (int x = 0; x < _world_width; x++)
|
||||
count += _world(x, y);
|
||||
return count;
|
||||
}
|
||||
|
||||
bool LifeBoard::_world(int x, int y) const
|
||||
{
|
||||
if (_edges == LIFE_EDGES_WRAP)
|
||||
{
|
||||
x = (x + _world_width) % _world_width;
|
||||
y = (y + _world_height) % _world_height;
|
||||
}
|
||||
else if (x < 0 || y < 0 || x >= _world_width || y >= _world_height)
|
||||
return false;
|
||||
return _cells[y][x / 8] & (0x80 >> (x % 8));
|
||||
}
|
||||
|
||||
void LifeBoard::_setWorld(int x, int y, bool alive)
|
||||
{
|
||||
if (alive)
|
||||
_cells[y][x / 8] |= 0x80 >> (x % 8);
|
||||
else
|
||||
_cells[y][x / 8] &= ~(0x80 >> (x % 8));
|
||||
}
|
||||
|
||||
void LifeBoard::_clearMargin()
|
||||
{
|
||||
for (int y = 0; y < _world_height; y++)
|
||||
for (int x = 0; x < _world_width; x++)
|
||||
{
|
||||
if (!_world(x, y))
|
||||
{
|
||||
_age[y][x] = 0;
|
||||
continue;
|
||||
}
|
||||
if (_age[y][x] < 255)
|
||||
_age[y][x]++;
|
||||
bool outer = x < LIFE_MARGIN_KILL || y < LIFE_MARGIN_KILL || x >= _world_width - LIFE_MARGIN_KILL ||
|
||||
y >= _world_height - LIFE_MARGIN_KILL;
|
||||
if (outer)
|
||||
{
|
||||
_setWorld(x, y, false);
|
||||
_age[y][x] = 0;
|
||||
}
|
||||
else if (_age[y][x] >= LIFE_MARGIN_STILL && _far(x, y))
|
||||
for (int dy = -2; dy <= 2; dy++)
|
||||
for (int dx = -2; dx <= 2; dx++)
|
||||
{
|
||||
int nx = x + dx, ny = y + dy;
|
||||
if (nx >= 0 && ny >= 0 && nx < _world_width && ny < _world_height && _far(nx, ny))
|
||||
{
|
||||
_setWorld(nx, ny, false);
|
||||
_age[ny][nx] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool LifeBoard::_far(int x, int y) const
|
||||
{
|
||||
return x < _margin - LIFE_MARGIN_KEEP || y < _margin - LIFE_MARGIN_KEEP ||
|
||||
x >= _margin + _width + LIFE_MARGIN_KEEP || y >= _margin + _height + LIFE_MARGIN_KEEP;
|
||||
}
|
||||
|
||||
// FNV-1a over the used part of the world
|
||||
uint32_t LifeBoard::_hash() const
|
||||
{
|
||||
uint32_t hash = 2166136261u;
|
||||
int row_bytes = (_world_width + 7) / 8;
|
||||
for (int y = 0; y < _world_height; y++)
|
||||
for (int i = 0; i < row_bytes; i++)
|
||||
{
|
||||
hash ^= _cells[y][i];
|
||||
hash *= 16777619u;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
void LifeBoard::_startHistory()
|
||||
{
|
||||
_history[0] = _hash();
|
||||
_history_count = 1;
|
||||
_history_next = 1;
|
||||
_repeating = false;
|
||||
}
|
||||
|
||||
void LifeRunner::started(uint32_t now)
|
||||
{
|
||||
_last_step = now;
|
||||
_ending = false;
|
||||
}
|
||||
|
||||
LifeTick LifeRunner::tick(uint32_t now)
|
||||
{
|
||||
// An empty board shows nothing: no hold time
|
||||
if (_ending && (now - _ended_at >= _timing.hold_ms || board.population() == 0))
|
||||
return LifeTick::Restart;
|
||||
if (now - _last_step < _interval())
|
||||
return LifeTick::Wait;
|
||||
_last_step = now;
|
||||
board.step();
|
||||
if (!_ending && ((_timing.restart_when_repeating && board.repeating()) ||
|
||||
(_timing.max_generations > 0 && board.generation() >= _timing.max_generations)))
|
||||
{
|
||||
_ending = true;
|
||||
_ended_at = now;
|
||||
}
|
||||
return LifeTick::Stepped;
|
||||
}
|
||||
|
||||
uint32_t LifeRunner::msUntilNext(uint32_t now) const
|
||||
{
|
||||
uint32_t passed = now - _last_step;
|
||||
uint32_t interval = _interval();
|
||||
uint32_t wait = passed < interval ? interval - passed : 1;
|
||||
if (_ending)
|
||||
{
|
||||
uint32_t held = now - _ended_at;
|
||||
uint32_t hold_left = held < _timing.hold_ms ? _timing.hold_ms - held : 1;
|
||||
if (hold_left < wait)
|
||||
wait = hold_left;
|
||||
}
|
||||
return wait > 0 ? wait : 1;
|
||||
}
|
||||
|
||||
uint32_t LifeRunner::_interval() const
|
||||
{
|
||||
if (!(_timing.speed > 0))
|
||||
return 1000;
|
||||
float ms = 1000 / _timing.speed;
|
||||
return ms < 1 ? 1 : (uint32_t)ms;
|
||||
}
|
||||
|
||||
int lifeNextPattern(uint8_t start, int count, int current, uint32_t &random)
|
||||
{
|
||||
if (count <= 0)
|
||||
return -1;
|
||||
if (start == LIFE_START_SHUFFLE)
|
||||
{
|
||||
if (current < 0 || current >= count)
|
||||
return lifeRandom(random) % count;
|
||||
if (count == 1)
|
||||
return 0;
|
||||
int next = lifeRandom(random) % (count - 1); // any other than current
|
||||
return next >= current ? next + 1 : next;
|
||||
}
|
||||
if (start == LIFE_START_ROTATE)
|
||||
return current < 0 || current >= count - 1 ? 0 : current + 1;
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
#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);
|
||||
@@ -7,6 +7,8 @@
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
|
||||
#include "logic/life.h"
|
||||
|
||||
static const uint16_t MINUTES_PER_DAY = 24 * 60;
|
||||
static const uint32_t MAX_TARGET = 36525UL * MINUTES_PER_DAY; // 2100-01-01
|
||||
|
||||
@@ -71,6 +73,13 @@ bool elementUsesHa(const PageElement &element)
|
||||
}
|
||||
}
|
||||
|
||||
bool sameLifeStart(const PageElement &a, const PageElement &b)
|
||||
{
|
||||
return a.type == ElementType::Life && b.type == ElementType::Life && a.bar_width == b.bar_width &&
|
||||
a.bar_height == b.bar_height && a.format == b.format && a.input_unit == b.input_unit &&
|
||||
strcasecmp(a.text, b.text) == 0 && a.factor == b.factor && a.absolute == b.absolute;
|
||||
}
|
||||
|
||||
bool pageUsesImage(const DynamicPage &page, const char *name, bool by_state)
|
||||
{
|
||||
for (int i = 0; i < page.slot_count; i++)
|
||||
@@ -79,6 +88,8 @@ bool pageUsesImage(const DynamicPage &page, const char *name, bool by_state)
|
||||
for (int i = 0; i < page.element_count; i++)
|
||||
{
|
||||
const PageElement &e = page.elements[i];
|
||||
if (e.type == ElementType::Life && e.input_unit == LIFE_START_PATTERN && sameName(e.text, name))
|
||||
return true; // patterns to rotate through can be deleted (the list gets shorter)
|
||||
if (e.type != ElementType::Image)
|
||||
continue;
|
||||
if (!e.entity[0])
|
||||
@@ -152,11 +163,39 @@ static uint8_t formatCount(const PageElement &e)
|
||||
return STOPWATCH_FORMAT_COUNT;
|
||||
case ElementType::Bar:
|
||||
return BAR_FORMAT_COUNT;
|
||||
case ElementType::Life:
|
||||
return LIFE_EDGES_COUNT;
|
||||
default:
|
||||
return 255; // not used
|
||||
}
|
||||
}
|
||||
|
||||
static const char *validateLife(const PageElement &e)
|
||||
{
|
||||
if (e.bar_width < 1 || e.bar_width > BITMAP_WIDTH || e.bar_height < 1 || e.bar_height > BITMAP_HEIGHT)
|
||||
return "Game of Life: width 1-84, height 1-19 dots";
|
||||
if (e.input_unit >= LIFE_START_COUNT)
|
||||
return "Game of Life: unknown start";
|
||||
if (e.input_unit == LIFE_START_RANDOM && !(e.factor >= 1 && e.factor <= 100))
|
||||
return "Game of Life: the density must be 1-100 %";
|
||||
if (e.input_unit == LIFE_START_PATTERN && !validBitmapName(e.text))
|
||||
return "Game of Life: choose a pattern";
|
||||
if ((e.input_unit == LIFE_START_ROTATE || e.input_unit == LIFE_START_SHUFFLE) && e.text[0])
|
||||
{
|
||||
char name[ELEMENT_TEXT_SIZE + 1];
|
||||
snprintf(name, sizeof(name), "%sx", e.text); // the start of a valid name
|
||||
if (e.text[0] == ' ' || !validBitmapName(name))
|
||||
return "Game of Life: invalid start of the pattern names";
|
||||
}
|
||||
if (!(e.scroll_speed >= LIFE_SPEED_MIN && e.scroll_speed <= LIFE_SPEED_MAX))
|
||||
return "Game of Life: speed 0.1-20 generations per second";
|
||||
if (e.target > LIFE_GENERATIONS_MAX)
|
||||
return "Game of Life: at most 1000000 generations";
|
||||
if (!(e.bar_max >= 0 && e.bar_max <= LIFE_HOLD_MAX))
|
||||
return "Game of Life: hold 0-3600 seconds";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char *validatePage(const DynamicPage &page)
|
||||
{
|
||||
if (page.slot_count > PAGE_SLOTS)
|
||||
@@ -194,7 +233,7 @@ const char *validatePage(const DynamicPage &page)
|
||||
return "Invalid entity or attribute";
|
||||
if (e.decimals < DECIMALS_AS_IS || e.decimals > DECIMALS_MAX)
|
||||
return "Decimals must be 0-4, or as received";
|
||||
if (e.show >= HaShow::Count || e.input_unit >= UNIT_COUNT)
|
||||
if (e.show >= HaShow::Count || (e.type != ElementType::Life && e.input_unit >= UNIT_COUNT))
|
||||
return "Unknown Home Assistant value setting";
|
||||
if (!printable(e.map, sizeof(e.map)))
|
||||
return "The list of texts can only contain plain characters, up to 63";
|
||||
@@ -218,6 +257,15 @@ const char *validatePage(const DynamicPage &page)
|
||||
if (!isfinite(e.bar_max) || e.bar_max <= 0)
|
||||
return "Progress bar: the full value must be a number above 0";
|
||||
}
|
||||
if (e.type == ElementType::Life)
|
||||
{
|
||||
const char *message = validateLife(e);
|
||||
if (message)
|
||||
return message;
|
||||
for (int j = 0; j < i; j++)
|
||||
if (page.elements[j].type == ElementType::Life)
|
||||
return "A page can have one Game of Life";
|
||||
}
|
||||
if (e.type == ElementType::Countdown && e.target >= MAX_TARGET)
|
||||
return "The countdown date must be before 2100";
|
||||
if (e.type == ElementType::Image)
|
||||
@@ -768,6 +816,7 @@ void elementText(const PageElement &e, const LocalTime &t, const ElementValues &
|
||||
break;
|
||||
case ElementType::Image:
|
||||
case ElementType::Bar:
|
||||
case ElementType::Life:
|
||||
out[0] = '\0';
|
||||
break;
|
||||
default:
|
||||
|
||||
+27
-2
@@ -8,8 +8,8 @@
|
||||
#include "logic/local_time.h"
|
||||
|
||||
// Dynamic pages: a background bitmap that depends on the time of day, with elements
|
||||
// (clock, date, Home Assistant value, text, countdown, stopwatch, image, progress bar)
|
||||
// drawn on top.
|
||||
// (clock, date, Home Assistant value, text, countdown, stopwatch, image, progress bar,
|
||||
// Game of Life) drawn on top.
|
||||
// Stored as a binary struct on LittleFS (display/page_store), edited in the web UI. Pure
|
||||
// logic, tested in test/test_esp32.
|
||||
//
|
||||
@@ -38,6 +38,7 @@ enum class ElementType : uint8_t
|
||||
Stopwatch, // the stopwatch (web UI and Home Assistant start, stop and reset it)
|
||||
Image, // a bitmap, fixed or chosen by a Home Assistant state
|
||||
Bar, // a progress bar filled by a Home Assistant number (e.g. 0-100 %)
|
||||
Life, // Conway's Game of Life in an area (logic/life), at most one per page
|
||||
Count,
|
||||
};
|
||||
|
||||
@@ -137,6 +138,25 @@ const uint8_t TIMESTAMP_FORMAT_COUNT = 4;
|
||||
const int8_t DECIMALS_AS_IS = -1;
|
||||
const int8_t DECIMALS_MAX = 4;
|
||||
|
||||
// Game of Life elements use these fields (no fields of their own, so the page format
|
||||
// stayed the same):
|
||||
// x, y, align the area, like a progress bar (x is its left, middle or right)
|
||||
// bar_width/bar_height the size of the area
|
||||
// format LIFE_EDGES_* (logic/life.h)
|
||||
// input_unit LIFE_START_*: random, one pattern, or rotate/shuffle through patterns
|
||||
// text the pattern (an icon or bitmap), or the start of the names to rotate
|
||||
// through (e.g. "life_"); not used for random
|
||||
// factor random: the density in % (1-100)
|
||||
// absolute patterns at a random place in the area (otherwise in the middle)
|
||||
// scroll_speed generations per second (LIFE_SPEED_MIN-LIFE_SPEED_MAX)
|
||||
// auto_unit restart when it is still or repeating
|
||||
// target restart after this many generations (0: no limit)
|
||||
// bar_max after the end it keeps running this many seconds, then restarts
|
||||
const float LIFE_SPEED_MIN = 0.1f;
|
||||
const float LIFE_SPEED_MAX = 20;
|
||||
const uint32_t LIFE_GENERATIONS_MAX = 1000000;
|
||||
const float LIFE_HOLD_MAX = 3600;
|
||||
|
||||
// Scrolling: dots per second, and the empty dots between the end and the next start
|
||||
const float SCROLL_SPEED_MIN = 0.05f; // one dot every 20 s
|
||||
const float SCROLL_SPEED_MAX = 30;
|
||||
@@ -162,6 +182,7 @@ struct PageElement
|
||||
int8_t decimals = DECIMALS_AS_IS;
|
||||
// Text: the text; HaValue, Countdown, Stopwatch: shown before the value; Image: the
|
||||
// bitmap name, or with an entity the start of it (the state is added)
|
||||
// Life: the pattern, or the start of the pattern names
|
||||
char text[ELEMENT_TEXT_SIZE] = "";
|
||||
// HaValue, Stopwatch: shown after the value; Countdown: shown when the time has passed
|
||||
char suffix[ELEMENT_SUFFIX_SIZE] = "";
|
||||
@@ -246,6 +267,10 @@ bool expandEntity(const char *entity, const char *user, char *out, size_t out_si
|
||||
// A user name the way Home Assistant puts it in entity ids: "Manga Valk" -> "manga_valk"
|
||||
void plexUserSlug(const char *name, char *out, size_t out_size);
|
||||
|
||||
// Two Game of Life elements start the same way (area, edges, start, pattern, density,
|
||||
// place); the timing settings may differ. A running board can then go on.
|
||||
bool sameLifeStart(const PageElement &a, const PageElement &b);
|
||||
|
||||
// The element shows a Home Assistant value (HaValue; Image, Bar with an entity)
|
||||
bool elementUsesHa(const PageElement &element);
|
||||
|
||||
|
||||
@@ -156,6 +156,62 @@ static void drawBar(const PageElement &e, PageResources &resources, uint8_t *bit
|
||||
drawBarPercent(e, resources, bitmap, left + inner, e.y + inner, w - 2 * inner, h - 2 * inner);
|
||||
}
|
||||
|
||||
// Life without a running board (a preview of a page that is not shown): the start, the
|
||||
// pattern in the middle or a random fill that is always the same, in area coordinates
|
||||
static void lifeStart(const PageElement &e, int w, int h, PageResources &resources, uint8_t *start)
|
||||
{
|
||||
memset(start, 0, BITMAP_BYTES);
|
||||
uint32_t random = 1;
|
||||
uint8_t pattern[BITMAP_BYTES];
|
||||
int dx, dy;
|
||||
if (e.input_unit == LIFE_START_PATTERN)
|
||||
{
|
||||
if (!resources.background(e.text, pattern) || !lifePlacement(pattern, w, h, false, random, dx, dy))
|
||||
return;
|
||||
for (int y = 0; y < BITMAP_HEIGHT; y++)
|
||||
for (int x = 0; x < BITMAP_WIDTH; x++)
|
||||
if (bitmapPixel(pattern, x, y))
|
||||
setBitmapPixel(start, x + dx, y + dy, true);
|
||||
return;
|
||||
}
|
||||
int percent = e.input_unit == LIFE_START_RANDOM ? (int)e.factor : 30;
|
||||
for (int y = 0; y < h; y++)
|
||||
for (int x = 0; x < w; x++)
|
||||
setBitmapPixel(start, x, y, (int)(lifeRandom(random) % 100) < percent);
|
||||
}
|
||||
|
||||
// Life: the running board of the shown page; returns the ms until its next generation
|
||||
static uint32_t drawLife(const PageElement &e, uint32_t now_ms, PageResources &resources, uint8_t *bitmap,
|
||||
ElementBox &box)
|
||||
{
|
||||
int w = e.bar_width, h = e.bar_height;
|
||||
int left = e.x;
|
||||
if (e.align == Align::Center)
|
||||
left -= w / 2;
|
||||
else if (e.align == Align::Right)
|
||||
left -= w;
|
||||
box.x = left;
|
||||
box.y = e.y;
|
||||
box.width = w;
|
||||
box.height = h;
|
||||
if (e.overlay == Overlay::Box)
|
||||
clearBox(bitmap, left - 1, e.y - 1, w + 2, h + 2);
|
||||
|
||||
const LifeRunner *runner = resources.life(e);
|
||||
uint8_t start[BITMAP_BYTES];
|
||||
if (!runner)
|
||||
lifeStart(e, w, h, resources, start);
|
||||
for (int y = 0; y < h; y++)
|
||||
for (int x = 0; x < w; x++)
|
||||
{
|
||||
if (!(runner ? runner->board.cell(x, y) : bitmapPixel(start, x, y)))
|
||||
continue;
|
||||
int bx = left + x, by = e.y + y;
|
||||
setBitmapPixel(bitmap, bx, by, e.overlay == Overlay::Invert ? !bitmapPixel(bitmap, bx, by) : true);
|
||||
}
|
||||
return runner ? runner->msUntilNext(now_ms) : 0;
|
||||
}
|
||||
|
||||
// A text element; returns the ms until it scrolls on (0: it doesn't scroll)
|
||||
static uint32_t drawTextElement(const PageElement &e, const char *text, uint32_t now_ms, PageResources &resources,
|
||||
uint8_t *bitmap, ElementBox &box)
|
||||
@@ -223,6 +279,8 @@ uint32_t renderPage(const DynamicPage &page, const LocalTime &time, uint32_t now
|
||||
drawImage(e, resources, bitmap, box);
|
||||
else if (e.type == ElementType::Bar)
|
||||
drawBar(e, resources, bitmap, box);
|
||||
else if (e.type == ElementType::Life)
|
||||
next = sooner(next, drawLife(e, now_ms, resources, bitmap, box));
|
||||
else
|
||||
{
|
||||
ElementValues values;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <stdint.h>
|
||||
|
||||
#include "logic/font.h"
|
||||
#include "logic/life.h"
|
||||
#include "logic/local_time.h"
|
||||
#include "logic/page.h"
|
||||
|
||||
@@ -21,6 +22,9 @@ public:
|
||||
virtual const char *haValue(const PageElement &element) = 0;
|
||||
virtual uint32_t stopwatchMs() { return 0; }
|
||||
virtual bool stopwatchRunning() { return false; }
|
||||
// The running Game of Life of the shown page when this element starts like it
|
||||
// (sameLifeStart); nullptr otherwise (the start is drawn then)
|
||||
virtual const LifeRunner *life(const PageElement &) { return nullptr; }
|
||||
};
|
||||
|
||||
// The area an element covers on the screen (for the editor), may be partly off screen.
|
||||
@@ -36,7 +40,7 @@ struct ElementBox
|
||||
// now_ms: a running millisecond counter (millis()), for scrolling. boxes: nullptr, or room
|
||||
// for page.element_count boxes.
|
||||
// Returns the ms until the page looks different again (scrolling, seconds, a running
|
||||
// stopwatch), or 0 when it only changes with the minute or its content (Home Assistant
|
||||
// stopwatch, the next generation of a Game of Life), or 0 when it only changes with the minute or its content (Home Assistant
|
||||
// values, the stopwatch being started or stopped).
|
||||
uint32_t renderPage(const DynamicPage &page, const LocalTime &time, uint32_t now_ms, PageResources &resources,
|
||||
uint8_t *bitmap, ElementBox *boxes = nullptr);
|
||||
Reference in new issue
Block a user