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
@@ -6,7 +6,8 @@
|
||||
#include "config.h"
|
||||
#include "display/text.h"
|
||||
|
||||
FlipdotFrames::FlipdotFrames(BROSE9323 &display, PageResources &resources) : _display(display), _resources(resources)
|
||||
FlipdotFrames::FlipdotFrames(BROSE9323 &display, PageResources &resources, LifePlayer &life)
|
||||
: _display(display), _resources(resources), _life(life)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -14,6 +15,7 @@ FlipdotFrames::FlipdotFrames(BROSE9323 &display, PageResources &resources) : _di
|
||||
uint32_t FlipdotFrames::dynamicPage(const LocalTime &time, uint32_t now, bool redraw)
|
||||
{
|
||||
uint8_t bitmap[BITMAP_BYTES];
|
||||
_life.tick(now);
|
||||
uint32_t wait = renderPage(_dynamic, time, now, _resources, bitmap);
|
||||
bool moving = wait != 0;
|
||||
// The Nano has no acknowledgement: the next frame waits until it has received this one
|
||||
@@ -84,6 +86,7 @@ void FlipdotFrames::off()
|
||||
void FlipdotFrames::setDynamicPage(const DynamicPage &page)
|
||||
{
|
||||
_dynamic = page;
|
||||
_life.setPage(page);
|
||||
}
|
||||
|
||||
void FlipdotFrames::setPageBitmap(const uint8_t *bitmap)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "BROSE9323.h"
|
||||
#include "display/life_player.h"
|
||||
#include "logic/bitmap_format.h"
|
||||
#include "logic/display_controller.h"
|
||||
#include "logic/page_render.h"
|
||||
@@ -10,9 +11,10 @@
|
||||
class FlipdotFrames : public Frames
|
||||
{
|
||||
public:
|
||||
FlipdotFrames(BROSE9323 &display, PageResources &resources);
|
||||
FlipdotFrames(BROSE9323 &display, PageResources &resources, LifePlayer &life);
|
||||
|
||||
// The page set with setDynamicPage(), drawn for this time. Only the changed dots are
|
||||
// The page set with setDynamicPage(), drawn for this time (its Game of Life is ticked
|
||||
// first). Only the changed dots are
|
||||
// sent (nothing when it looks the same), unless redraw. Moving pages are sent without
|
||||
// the refresh effect, and not faster than the Nano can receive and flip them.
|
||||
uint32_t dynamicPage(const LocalTime &time, uint32_t now, bool redraw) override;
|
||||
@@ -46,6 +48,7 @@ public:
|
||||
private:
|
||||
BROSE9323 &_display;
|
||||
PageResources &_resources;
|
||||
LifePlayer &_life;
|
||||
DynamicPage _dynamic;
|
||||
static const uint8_t EFFECT_UNKNOWN = 254;
|
||||
DriveSettings _drive;
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
#include "display/life_player.h"
|
||||
|
||||
#include <strings.h>
|
||||
|
||||
#include "display/icon_store.h"
|
||||
#include "display/page_resources.h"
|
||||
|
||||
LifePlayer lifePlayer;
|
||||
|
||||
// Random fill when no pattern can be loaded (rotating through no icons, a pattern deleted)
|
||||
static const int FALLBACK_DENSITY = 30;
|
||||
|
||||
static LifeTiming timingOf(const PageElement &e)
|
||||
{
|
||||
LifeTiming timing;
|
||||
timing.speed = e.scroll_speed;
|
||||
timing.restart_when_repeating = e.auto_unit;
|
||||
timing.max_generations = e.target;
|
||||
timing.hold_ms = (uint32_t)(e.bar_max * 1000);
|
||||
return timing;
|
||||
}
|
||||
|
||||
void LifePlayer::setPage(const DynamicPage &page)
|
||||
{
|
||||
const PageElement *life = nullptr;
|
||||
for (int i = 0; i < page.element_count && i < PAGE_ELEMENTS; i++)
|
||||
if (page.elements[i].type == ElementType::Life)
|
||||
life = &page.elements[i];
|
||||
if (!life)
|
||||
{
|
||||
_active = false;
|
||||
return;
|
||||
}
|
||||
_runner.setTiming(timingOf(*life));
|
||||
if (_active && sameLifeStart(_element, *life))
|
||||
{
|
||||
_element = *life;
|
||||
return;
|
||||
}
|
||||
_element = *life;
|
||||
_active = true;
|
||||
_start_needed = true;
|
||||
_pattern = -1;
|
||||
}
|
||||
|
||||
void LifePlayer::tick(uint32_t now)
|
||||
{
|
||||
if (!_active)
|
||||
return;
|
||||
if (_start_needed || _runner.tick(now) == LifeTick::Restart)
|
||||
_start(now);
|
||||
}
|
||||
|
||||
const LifeRunner *LifePlayer::runner(const PageElement &element) const
|
||||
{
|
||||
if (!_active || _start_needed || !sameLifeStart(_element, element))
|
||||
return nullptr;
|
||||
return &_runner;
|
||||
}
|
||||
|
||||
void LifePlayer::_start(uint32_t now)
|
||||
{
|
||||
_start_needed = false;
|
||||
if (_random == 0)
|
||||
_random = esp_random() | 1;
|
||||
const PageElement &e = _element;
|
||||
_runner.board.begin(e.bar_width, e.bar_height, e.format);
|
||||
|
||||
bool placed = false;
|
||||
if (e.input_unit == LIFE_START_PATTERN)
|
||||
placed = _place(e.text);
|
||||
else if (e.input_unit == LIFE_START_ROTATE || e.input_unit == LIFE_START_SHUFFLE)
|
||||
{
|
||||
_loadPatterns();
|
||||
// Skips patterns that can't be loaded (or have no dots), each one tried once
|
||||
for (size_t tries = 0; !placed && tries < _patterns.size(); tries++)
|
||||
{
|
||||
_pattern = lifeNextPattern(e.input_unit, _patterns.size(), _pattern, _random);
|
||||
placed = _place(_patterns[_pattern].c_str());
|
||||
}
|
||||
}
|
||||
if (!placed)
|
||||
{
|
||||
int density = e.input_unit == LIFE_START_RANDOM ? (int)e.factor : FALLBACK_DENSITY;
|
||||
_runner.board.randomize(density, _random);
|
||||
if (e.input_unit != LIFE_START_RANDOM)
|
||||
Serial.printf("Life: no pattern \"%s\", random start\n", e.text);
|
||||
}
|
||||
_runner.started(now);
|
||||
}
|
||||
|
||||
bool LifePlayer::_place(const char *name)
|
||||
{
|
||||
uint8_t pattern[BITMAP_BYTES];
|
||||
if (!pageResources.background(name, pattern))
|
||||
return false;
|
||||
if (!_runner.board.place(pattern, _element.absolute, _random))
|
||||
return false;
|
||||
Serial.printf("Life: %s\n", name);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Listing the icons takes about a second: only when they or the name start changed
|
||||
void LifePlayer::_loadPatterns()
|
||||
{
|
||||
if (_patterns_loaded && _patterns_revision == iconListRevision() && _patterns_start.equalsIgnoreCase(_element.text))
|
||||
return;
|
||||
_patterns.clear();
|
||||
size_t start = strlen(_element.text);
|
||||
for (const String &name : listIcons())
|
||||
if (strncasecmp(name.c_str(), _element.text, start) == 0)
|
||||
_patterns.push_back(name);
|
||||
_patterns_loaded = true;
|
||||
_patterns_revision = iconListRevision();
|
||||
_patterns_start = _element.text;
|
||||
_pattern = -1;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <vector>
|
||||
|
||||
#include "logic/life.h"
|
||||
#include "logic/page.h"
|
||||
|
||||
// Runs the Game of Life element of the shown page (logic/life): FlipdotFrames sets the page
|
||||
// and ticks it before each frame, StoredPageResources gives the board to the renderer.
|
||||
// Patterns are bitmaps or icons (loaded through pageResources); for rotating, the icons
|
||||
// whose names start with the element's text, listed once per change of the icons.
|
||||
class LifePlayer
|
||||
{
|
||||
public:
|
||||
// The shown page changed or was loaded again. A Life element that starts the same way
|
||||
// (sameLifeStart) keeps running with the new timing; otherwise it starts again.
|
||||
void setPage(const DynamicPage &page);
|
||||
// Starts a run when needed, steps when a generation is due, restarts after the hold time
|
||||
void tick(uint32_t now);
|
||||
// The running board when the element starts like the running one, otherwise nullptr
|
||||
const LifeRunner *runner(const PageElement &element) const;
|
||||
|
||||
private:
|
||||
LifeRunner _runner;
|
||||
PageElement _element;
|
||||
bool _active = false;
|
||||
bool _start_needed = false;
|
||||
uint32_t _random = 0;
|
||||
int _pattern = -1; // index in _patterns of the running one
|
||||
std::vector<String> _patterns;
|
||||
bool _patterns_loaded = false;
|
||||
uint32_t _patterns_revision = 0;
|
||||
String _patterns_start;
|
||||
|
||||
void _start(uint32_t now);
|
||||
bool _place(const char *name);
|
||||
void _loadPatterns();
|
||||
};
|
||||
|
||||
extern LifePlayer lifePlayer;
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "display/bitmap_store.h"
|
||||
#include "display/font_store.h"
|
||||
#include "display/icon_store.h"
|
||||
#include "display/life_player.h"
|
||||
#include "net/ha_api.h"
|
||||
|
||||
Stopwatch stopwatch;
|
||||
@@ -40,3 +41,8 @@ bool StoredPageResources::stopwatchRunning()
|
||||
{
|
||||
return _stopwatch.running();
|
||||
}
|
||||
|
||||
const LifeRunner *StoredPageResources::life(const PageElement &element)
|
||||
{
|
||||
return lifePlayer.runner(element);
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
#include "logic/stopwatch.h"
|
||||
|
||||
// Bitmaps and icons from display/bitmap_store and display/icon_store, fonts from display/font_store, Home Assistant values
|
||||
// from net/ha_api and the stopwatch, for drawing dynamic pages on the display and in the
|
||||
// from net/ha_api, the stopwatch and the running Game of Life (display/life_player), for drawing dynamic pages on the display and in the
|
||||
// web UI
|
||||
class StoredPageResources : public PageResources
|
||||
{
|
||||
@@ -16,6 +16,7 @@ public:
|
||||
const char *haValue(const PageElement &element) override;
|
||||
uint32_t stopwatchMs() override;
|
||||
bool stopwatchRunning() override;
|
||||
const LifeRunner *life(const PageElement &element) override;
|
||||
|
||||
private:
|
||||
Stopwatch &_stopwatch;
|
||||
|
||||
Reference in new issue
Block a user