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
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user