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:
mangavalkandClaude Opus 5.5 committed 2026-10-20 17:52:25 +02:00
1 parent 948087077d
commit a91d2550d7
29 files changed
+1402 -31

No files matched your search

+50 -1
View File
@@ -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: