LOLIN/WeMos/NodeMCU D1 R1 Adafruit Lib Projects

Hello everyone.
This will be my single unified thread to showcase all and any D1 R1 board projects using various display setup but with Adafruit Libraries. Only wet test passed firmware and projects will be uploaded here.

For the first one, I present:

1.44 TFT Hyperclock Firmware Raycaster

A LOLIN/Wemos/NodeMCU D1 R1 ESP8266 1.44 SPI TFT 128*128 ST7735 (GREENTAB) Firmware which uses dedicated text and graphics display buffer to procedurally render a ray casted 2.5D visual as the clock animation loop.

Tested to run on D1 R1 for days uninterrupted without any issues.

If the breadboard only contains the display and nothing else, the limiting resistor and the decoupling capacitor of the circuit/breadboard can be removed or not used. Ceramic capacitors are always recommended, use electrolytics only on unavailability of ceramics.
Alternate schematic is included in the commented header of the main sketch.

PS: The board present in the above Fritzing diagram is R2. But I have kept the physical positions of the Pin relevant to R1, which is the board on which the firmware has been tested. Kindly check the GPIO relevance if you have R2 version of the board.
The TFT shown here (in the fritzing export) is 128x160, the one I have used is 128*128 but by the same manufacturer. (The file itself has been provided by the manufacturer). So the pin definitions match perfectly (personally double checked). You can safely follow them.
Device info link:
1.44 128*128 SPI TFT ST7735

If your board is R1, follow the physical positions along with the red helper labels to be sure, and you are golden.
If your board is R2, refer to your datasheet for appropriate MOSI and SCK female slots.
Here are the R1 and R2 side by side for verification and reference. I will go ahead in the rest of the article assuming my hardware, which is R1.

Image source thread:
Image by Costas from blynk community.

Main sketch:

/**
 * Core1D Automation Labs Present:
 * โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—
 * โ•‘                                                                             โ•‘
 * โ–ˆโ–ˆโ•—  โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•—   โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—  โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•—      โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—  โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•—
 * โ–ˆโ–ˆโ•‘  โ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ•‘     โ–ˆโ–ˆโ•”โ•โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘
 * โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘ โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ• โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—  โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•‘     โ–ˆโ–ˆโ•‘     โ–ˆโ–ˆโ•‘   โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘     โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•
 * โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•‘  โ•šโ–ˆโ–ˆโ•”โ•  โ–ˆโ–ˆโ•”โ•โ•โ•โ• โ–ˆโ–ˆโ•”โ•โ•โ•  โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘     โ–ˆโ–ˆโ•‘     โ–ˆโ–ˆโ•‘   โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘     โ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•—
 * โ–ˆโ–ˆโ•‘  โ–ˆโ–ˆโ•‘   โ–ˆโ–ˆโ•‘   โ–ˆโ–ˆโ•‘     โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘  โ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘ โ•‘โ–ˆโ–ˆโ•—
 * โ•šโ•โ•  โ•šโ•โ•   โ•šโ•โ•   โ•šโ•โ•     โ•šโ•โ•โ•โ•โ•โ•โ•โ•šโ•โ•  โ•šโ•โ• โ•šโ•โ•โ•โ•โ•โ•โ•šโ•โ•โ•โ•โ•โ•โ• โ•šโ•โ•โ•โ•โ•โ•  โ•šโ•โ•โ•โ•โ•โ•โ•šโ•โ• โ•‘โ•šโ•โ•
 * โ•‘                                    ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,    โ•‘
 * โ•‘  โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—      โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—      โ–ˆโ–ˆโ–ˆโ–ˆ\                 /โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ    โ•‘
 * โ•‘  โ•šโ•โ•โ•โ•โ–ˆโ–ˆโ•—     โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—     โ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ–ˆโ–ˆ\    /โ•โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ    โ•‘
 * โ•‘   โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•     โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘  โ–ˆโ–ˆโ•‘     โ–ˆโ–ˆโ–ˆโ–ˆโ•”โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ    โ•‘   
 * โ•‘  โ–ˆโ–ˆโ•”โ•โ•โ•โ•      โ•šโ•โ•โ•โ•โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘  โ–ˆโ–ˆโ•‘     โ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆ/    \โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ    โ•‘
 * โ•‘  โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•     โ–ˆโ–ˆโ–ˆโ–ˆ/โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•      \โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ    โ•‘   
 * โ•‘  โ•šโ•โ•โ•โ•โ•โ•โ• โ•šโ•โ• โ•šโ•โ•โ•โ•โ•โ•โ•โ•šโ•โ•โ•โ•โ•โ•      โ•โ•โ•โ•                   โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•    โ•‘
 * โ•‘                                    """""""""""""""""""""""""""""""""""""    โ•‘
 * โ•‘        โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—  โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•—   โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— 
 * โ•‘        โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ•šโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ•šโ•โ•โ–ˆโ–ˆโ•”โ•โ•โ•โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—
 * โ•‘        โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘ โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ• โ–ˆโ–ˆโ•‘     โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—   โ–ˆโ–ˆโ•‘   โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—  โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•
 * โ•‘        โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•‘  โ•šโ–ˆโ–ˆโ•”โ•  โ–ˆโ–ˆโ•‘     โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•‘โ•šโ•โ•โ•โ•โ–ˆโ–ˆโ•‘   โ–ˆโ–ˆโ•‘   โ–ˆโ–ˆโ•”โ•โ•โ•  โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—
 * โ•‘        โ–ˆโ–ˆโ•‘  โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘  โ–ˆโ–ˆโ•‘   โ–ˆโ–ˆโ•‘   โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘  โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘   โ–ˆโ–ˆโ•‘   โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘โ•‘ โ•šโ–ˆโ–ˆโ•‘
 * โ•‘        โ•šโ•โ•  โ•šโ•โ•โ•šโ•โ•  โ•šโ•โ•   โ•šโ•โ•    โ•šโ•โ•โ•โ•โ•โ•โ•šโ•โ•  โ•šโ•โ•โ•šโ•โ•โ•โ•โ•โ•โ•   โ•šโ•โ•   โ•šโ•โ•โ•โ•โ•โ•โ•โ•šโ•โ•โ•‘  โ•šโ•โ•
 * โ•‘                             ESP8266 ST77xx Firmware Version  V: 2.5.2       โ•‘
 * โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
 *
 * ESP8266 2.5D Raycasting Real-Time Clock with Weather Integration
 * Advanced Pseudo-3D Graphics Engine with Optimized Memory Management
 * 
 * Version: 2.5 Polished Edition Revision 2
 * Author: Sir Ronnie from Core1D Automation Labs
 * License: MIT License
 * Target: ESP8266 WeMos D1 R1 with 1.44" ST7735 SPI TFT Display (128x128)
 * 
 * HARDWARE CONNECTIONS:
 * =====================
 * 
 * ST7735 1.44" TFT Display (128x128):
 * โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
 * โ”‚  ST7735 Pin  โ”‚  WeMos D1 Pin   โ”‚
 * โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚
 * โ”‚  VCC         โ”‚  3V3            โ”‚
 * โ”‚  GND         โ”‚  GND            โ”‚
 * โ”‚  CS          โ”‚  D2 (GPIO4)     โ”‚
 * โ”‚  RESET       โ”‚  D3 (GPIO0)     โ”‚
 * โ”‚  A0/DC       โ”‚  D4 (GPIO2)     โ”‚
 * โ”‚  SDA/MOSI    โ”‚  D7 (GPIO13)    โ”‚
 * โ”‚  SCK/CLK     โ”‚  D5 (GPIO14)    โ”‚
 * โ”‚  LED         โ”‚  3V3 (Optional) โ”‚
 * โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
 * 
 * HARDWARE ASCII SCHEMATIC:
 * ========================
 * 
 *      WeMos D1 R1 (ESP8266)          ST7735 1.44" TFT
 *    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
 *    โ”‚                     โ”‚         โ”‚                 โ”‚
 *    โ”‚  [USB-C]       3V3 โ—โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”คโ— VCC            โ”‚
 *    โ”‚                     โ”‚         โ”‚                 โ”‚
 *    โ”‚  [WIFI ANTENNA]     โ”‚         โ”‚                 โ”‚
 *    โ”‚                     โ”‚         โ”‚                 โ”‚
 *    โ”‚              D7/13 โ—โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”คโ— SDA (MOSI)     โ”‚
 *    โ”‚              D5/14 โ—โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”คโ— SCK (CLK)      โ”‚
 *    โ”‚               D2/4 โ—โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”คโ— CS             โ”‚
 *    โ”‚               D3/0 โ—โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”คโ— RST            โ”‚
 *    โ”‚               D4/2 โ—โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”คโ— DC (A0)        โ”‚
 *    โ”‚                     โ”‚         โ”‚                 โ”‚
 *    โ”‚                GND โ—โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”คโ— GND            โ”‚
 *    โ”‚                     โ”‚         โ”‚                 โ”‚
 *    โ”‚    [ESP8266-12E]    โ”‚         โ”‚ [128x128 LCD]   โ”‚
 *    โ”‚    [80MHz/160MHz]   โ”‚         โ”‚ [65K Colors]    โ”‚
 *    โ”‚    [4MB Flash]      โ”‚         โ”‚ [SPI Interface] โ”‚
 *    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
 *               โ”‚                              โ”‚
 *               โ””โ”€โ”€โ”€โ”€ [ Xtensa LX106 Core ] โ”€โ”€โ”€โ”˜
 *                     [ 2.4GHz WiFi Radio ]
 * 
 * Principles Applied:
 * ==================
 * โ€ข Real-time 2.5D raycasting engine with wall collision detection
 * โ€ข Dynamic color-coded walls based on current hour (12-hour cycle)
 * โ€ข Smooth camera movement with physics-based collision response
 * โ€ข Real-time weather data integration via OpenWeatherMap API
 * โ€ข NTP time synchronization with automatic timezone handling
 * โ€ข Optimized memory management with PROGMEM usage
 * โ€ข Split-screen rendering: 3D viewport + 2D info display
 * โ€ข Custom degree symbol rendering for temperature display
 * โ€ข WiFi connection management with visual feedback
 * โ€ข Efficient frame rate optimization (~25 FPS target)
 * 
 * PERFORMANCE SPECS:
 * ==========================
 * โ€ข Render Resolution: 128x68 pixels (2.5D viewport)
 * โ€ข Info Display Area: 128x60 pixels (2D text/data)
 * โ€ข Map Size: 16x16 grid world
 * โ€ข Wall Height: 48 pixels (variable based on distance)
 * โ€ข Field of View: 90 degrees
 * โ€ข Maximum Draw Distance: 12.0 units
 * โ€ข Z-Buffer Depth Testing: 16-bit precision
 * โ€ข Memory Usage: <32KB RAM, Flash optimized with PROGMEM
 */

#include <Arduino.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClient.h>
#include <time.h>
#include <ArduinoJson.h>

// Hard SPI pins for WeMos D1 R1
#define TFT_CS  D2   // GPIO4
#define TFT_RST D3   // GPIO0  
#define TFT_DC  D4   // GPIO2
#define TFT_LED -1   // Disable pin if connected to 3v3

// WiFi credentials
static constexpr const char* const SSID     = "YourSSID";
static constexpr const char* const PASSWORD = "yourPassword";

// Weather API configuration for performance optimization
static constexpr const char* const WEATHER_API_KEY  = "cb840413161e7ee08e831af35dfb9c53";
static constexpr const char* const WEATHER_BASE_URL = "http://api.openweathermap.org/data/2.5/weather?id=";
static constexpr const char* const MUMBAI_CITY_ID   = "1275339"; // Mumbai, India city ID
 /*
// To change cities, simply use your City ID from OpenWeatherMap
static constexpr const char* const NEW_YORK_CITY_ID = "5128581"; // New York City, USA
static constexpr const char* const LONDON_CITY_ID   = "2643743"; // London, UK
static constexpr const char* const TOKYO_CITY_ID    = "1850147"; // Tokyo, Japan
*/

// Display setup
Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST);

// 2.5D Raycasting constants for compile-time optimization
static constexpr uint8_t RENDER_WIDTH  = 128;
static constexpr uint8_t RENDER_HEIGHT = 68; 
static constexpr uint8_t MAP_SIZE      = 16;
static constexpr uint8_t FOV_ANGLE     = 90;
static constexpr uint8_t WALL_HEIGHT   = 48;
static constexpr float   MAX_DISTANCE  = 12.0f;

// Fixed perspective constants for Camera
static constexpr float CAMERA_HEIGHT    = 0.5f;
static constexpr float PROJ_PLANE_DIST  = 1.0f;
static constexpr float COLLISION_RADIUS = 0.4f;
static constexpr float MOVEMENT_SPEED   = 0.008f;
static constexpr float BOUNCE_DAMPING   = 0.8f;
static constexpr float MIN_VELOCITY     = 0.002f;

// Degree symbol bitmap (5x5 pixels) - PROGMEM for flash storage
static const uint8_t PROGMEM degree_bitmap[5] = {
  0b01100000, // .##.....
  0b10010000, // #..#....
  0b10010000, // #..#....
  0b10010000, // #..#....
  0b01100000  // .##.....
};

// Day names array for performance
static constexpr const char* const DAY_NAMES[7] = {
  "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
};

// Wall colors hourly scheme
static constexpr uint16_t WALL_COLORS[12] = {
  ST7735_RED,     // 0-1 or 12-1
  0xFD20,         // 1-2 (Orange)
  ST7735_YELLOW,  // 2-3
  ST7735_GREEN,   // 3-4
  ST7735_CYAN,    // 4-5
  ST7735_BLUE,    // 5-6
  ST7735_MAGENTA, // 6-7
  0xF81F,         // 7-8 (Pink)
  0x07E0,         // 8-9 (Bright Green)
  0x001F,         // 9-10 (Bright Blue)
  0xFFE0,         // 10-11 (Bright Yellow)
  0x7BEF          // 11-12 (Light Gray)
};

// Game world structure
struct World {
  uint8_t map[MAP_SIZE][MAP_SIZE];
  float cameraX, cameraY;
  float cameraAngle;
  float cameraDirX, cameraDirY;
  float velocityX, velocityY; // For bounce physics
  uint8_t columns[MAP_SIZE * MAP_SIZE];
  uint32_t lastMoveTime;
} world;

// Optimized color scheme
struct Colors {
  uint16_t wall;
  uint16_t background; // Single background color for non-wall areas
  uint16_t column;
} colors;

// Weather and time data
struct TimeData {
  int hour, minute, second;
  int day, month, year;
  const char* dayName;
  float temperature;
  const char* weatherDesc;
  int humidity;
  float pressure;
  float windSpeed;
  const char* cityName;
  const char* ipAddress;
  int wifiStrength;
  uint32_t lastWeatherUpdate;
} timeData;

// Z-buffer for depth testing (optimized for walls only)
uint16_t zbuffer[RENDER_WIDTH];

void setup() {
  // Initialize display
  tft.initR(INITR_144GREENTAB);
/*
If having any issues, try these alternatives one at a time:
  tft.initR(INITR_GREENTAB);   // Alternative 1
  tft.initR(INITR_REDTAB);    // Alternative 2
  tft.initR(INITR_BLACKTAB); // Alternative 3 (Creates dirty region/line on the screen bottom with the tested screen)
                            // Check Footer Notes (Technical Documentation) 
                           //  for a detailed brief on the ST7735 and it's functioning regarding this firmware sketch.
*/

  tft.setRotation(3); // Landscape mode
  tft.setAddrWindow(0, 0, 128, 128);
  tft.fillScreen(ST7735_BLACK);
  
  // Show startup logo
  showStartupScreen();
  
  // Initialize WiFi
  connectToWiFi();
  
  // Configure time
  configTime(5.5 * 3600, 0, "pool.ntp.org", "time.nist.gov");
  
  // Initialize world
  initializeWorld();
}

void loop() {
  updateTime();
  updateWorld();
  renderFrame();
  displayInfo();
  
  // Update weather every 10 minutes
  if (millis() - timeData.lastWeatherUpdate > 600000UL) {
    updateWeather();
  }
//delay(40);  // ~More delay = Low FPS for stability, power saving (Stress Tested)
  delay(16);  // ~Less delay = Higher FPS, also more power hungry (Stress Test under progress)
}

void showStartupScreen() {
  tft.fillScreen(ST7735_BLACK);
  
  // Text area configuration
  const int textAreaStart  = RENDER_HEIGHT;
  const int textAreaHeight = 128 - RENDER_HEIGHT;
  const int textAreaCenter = textAreaStart + (textAreaHeight / 2);
  
  // Display title text in text buffer area
  tft.setTextColor(ST7735_CYAN);
  tft.setTextSize(1);
  tft.setCursor(20, textAreaCenter - 20);
  tft.println(F("CORE1D LABS"));
  
  tft.setTextColor(ST7735_YELLOW);
  tft.setCursor(15, textAreaCenter - 10);
  tft.println(F("HyperClock 2.5D"));
  
  tft.setTextColor(ST7735_WHITE);
  tft.setCursor(25, textAreaCenter + 5);
  tft.println(F("ESP8266 Tested"));
  
  // 3D tunnel effect in render area
  const int effectLayers = 25;
  const int maxBoxWidth  = RENDER_WIDTH;
  const int maxBoxHeight = RENDER_HEIGHT;
  
  // Rainbow gradient tunnel effect
  for (int i = 0; i < effectLayers; i++) {
    const float hueStep    = 360.0f / effectLayers;
    const float currentHue = i * hueStep;
    
    // HSV to RGB conversion for vibrant colors
    uint16_t color;
    if (currentHue < 60) {
      const uint8_t r = 31;
      const uint8_t g = (currentHue / 60.0f) * 63;
      const uint8_t b = 0;
      color = (r << 11) | (g << 5) | b;
    } else if (currentHue < 120) {
      const uint8_t r = ((120 - currentHue) / 60.0f) * 31;
      const uint8_t g = 63;
      const uint8_t b = 0;
      color = (r << 11) | (g << 5) | b;
    } else if (currentHue < 180) {
      const uint8_t r = 0;
      const uint8_t g = 63;
      const uint8_t b = ((currentHue - 120) / 60.0f) * 31;
      color = (r << 11) | (g << 5) | b;
    } else if (currentHue < 240) {
      const uint8_t r = 0;
      const uint8_t g = ((240 - currentHue) / 60.0f) * 63;
      const uint8_t b = 31;
      color = (r << 11) | (g << 5) | b;
    } else if (currentHue < 300) {
      const uint8_t r = ((currentHue - 240) / 60.0f) * 31;
      const uint8_t g = 0;
      const uint8_t b = 31;
      color = (r << 11) | (g << 5) | b;
    } else {
      const uint8_t r = 31;
      const uint8_t g = 0;
      const uint8_t b = ((360 - currentHue) / 60.0f) * 31;
      color = (r << 11) | (g << 5) | b;
    }
    
    // Calculate layer dimensions
    const int layerX      = i * 2;
    const int layerY      = i * 1;
    const int layerWidth  = maxBoxWidth  - (i * 4);
    const int layerHeight = maxBoxHeight - (i * 2);
    
    // Bounds checking
    if (layerX >= 0 && layerY >= 0 && 
        layerX + layerWidth < RENDER_WIDTH && 
        layerY + layerHeight < RENDER_HEIGHT &&
        layerWidth > 0 && layerHeight > 0) {
      
      tft.drawRect(layerX, layerY, layerWidth, layerHeight, color);
      
      // Inner glow effect
      if (i > 0 && layerWidth > 4 && layerHeight > 4) {
        const uint16_t glowColor = color | 0x2104;
        tft.drawRect(layerX + 1, layerY + 1, layerWidth - 2, layerHeight - 2, glowColor);
      }
    }
  }
  
  // Pulsing center core
  const int renderCenterX = RENDER_WIDTH  / 2;
  const int renderCenterY = RENDER_HEIGHT / 2;
  const int coreSize      = 12;
  const int coreX         = renderCenterX - (coreSize / 2);
  const int coreY         = renderCenterY - (coreSize / 2);
  
  if (coreX >= 0 && coreY >= 0 && 
      coreX + coreSize < RENDER_WIDTH && 
      coreY + coreSize < RENDER_HEIGHT) {
    
    tft.fillRect(coreX + 2, coreY + 2, coreSize - 4, coreSize - 4, ST7735_WHITE);
    tft.drawRect(coreX + 1, coreY + 1, coreSize - 2, coreSize - 2, ST7735_YELLOW);
    tft.drawRect(coreX,     coreY,     coreSize,     coreSize,     ST7735_CYAN);
  }
  
  delay(4000);
}

void connectToWiFi() {
  tft.fillScreen(ST7735_BLACK);
  tft.setTextColor(ST7735_WHITE);
  tft.setCursor(10, RENDER_HEIGHT / 2 - 10);
  tft.println(F("Connecting WiFi..."));
  
  WiFi.begin(SSID, PASSWORD);
  uint8_t attempts = 0;
  
  while (WiFi.status() != WL_CONNECTED && attempts < 20) {
    delay(500);
    tft.print(F("."));
    attempts++;
  }
  
  if (WiFi.status() == WL_CONNECTED) {
    tft.setCursor(10, RENDER_HEIGHT / 2 + 10);
    tft.setTextColor(ST7735_GREEN);
    tft.println(F("Connected!"));
  } else {
    tft.setCursor(10, RENDER_HEIGHT / 2 + 10);
    tft.setTextColor(ST7735_RED);
    tft.println(F("Connection failed"));
  }
  
  delay(1000);
}

void initializeWorld() {
  // Initialize room layout (walls around perimeter)
  memset(world.map, 0, sizeof(world.map));
  
  // Create room walls
  for (uint8_t x = 0; x < MAP_SIZE; x++) {
    world.map[x][0] = 1;
    world.map[x][MAP_SIZE-1] = 1;
  }
  for (uint8_t y = 0; y < MAP_SIZE; y++) {
    world.map[0][y] = 1;
    world.map[MAP_SIZE-1][y] = 1;
  }
  
  // Add interior walls
  world.map[8][4]  = 1; world.map[8][5]  = 1; world.map[8][6] = 1;
  world.map[4][8]  = 1; world.map[5][8]  = 1; world.map[6][8] = 1;
  world.map[12][8] = 1; world.map[12][9] = 1; world.map[12][10] = 1;
  
  // Initialize camera in safe starting position
  world.cameraX     = 8.0f;
  world.cameraY     = 8.0f;
  world.cameraAngle = 0.0f;
  world.cameraDirX  = 1.0f;
  world.cameraDirY  = 0.0f;
  world.velocityX   = 0.0f;
  world.velocityY   = 0.0f;
  
  // Initialize destructible columns
  memset(world.columns, 0, sizeof(world.columns));
  respawnColumns();
  
  world.lastMoveTime = millis();
}

void respawnColumns() {
  // Column positions array
  static constexpr uint8_t columnPositions[][2] = {
    {3, 3},  {5, 3},  {7, 3},  {9, 3},  {11, 3},
    {3, 7},  {5, 7},  {7, 7},  {9, 7},  {11, 7},
    {3, 11}, {5, 11}, {7, 11}, {9, 11}, {11, 11}
  };
  
  for (uint8_t i = 0; i < 15; i++) {
    const uint8_t x = columnPositions[i][0];
    const uint8_t y = columnPositions[i][1];
    world.columns[y * MAP_SIZE + x] = 1;
  }
}

// General collision detection
inline bool checkCollision(float x, float y) {
  const int mapX = (int)x;
  const int mapY = (int)y;
  
  // Bounds check with safety margin
  if (mapX < 1 || mapX >= MAP_SIZE - 1 || mapY < 1 || mapY >= MAP_SIZE - 1) {
    return true;
  }
  
  // Wall check
  if (world.map[mapX][mapY] != 0) {
    return true;
  }
  
  // Column check
  if (world.columns[mapY * MAP_SIZE + mapX] != 0) {
    return true;
  }
  
  return false;
}

// Multi point sampling collision detection for camera movement
inline bool checkCameraCollision(float x, float y, float radius = COLLISION_RADIUS) {
  // Check center point
  if (checkCollision(x, y)) return true;
  
  // Check 8 points around the camera in a circle
  for (uint8_t i = 0; i < 8; i++) {
    const float angle = i * PI / 4.0f;
    const float testX = x + cos(angle) * radius;
    const float testY = y + sin(angle) * radius;
    if (checkCollision(testX, testY)) return true;
  }
  
  return false;
}

void updateTime() {
  const time_t now                = time(nullptr);
  const struct tm* const timeinfo = localtime(&now);
  
  timeData.hour   = timeinfo->tm_hour;
  timeData.minute = timeinfo->tm_min;
  timeData.second = timeinfo->tm_sec;
  timeData.day    = timeinfo->tm_mday;
  timeData.month  = timeinfo->tm_mon + 1;
  timeData.year   = timeinfo->tm_year + 1900;
  
  // Update colors based on hour
  const uint8_t hourIndex = timeData.hour % 12;
  colors.wall = WALL_COLORS[hourIndex];
  
  // Set background and column colors
  colors.background = ST7735_BLACK;  // Black background for non-wall areas
  colors.column     = 0xFD20;        // Orange for columns
  
  // Day name
  timeData.dayName = DAY_NAMES[timeinfo->tm_wday];
  
  // Get WiFi info
  timeData.wifiStrength = WiFi.RSSI();
  static String ipStr   = WiFi.localIP().toString();
  timeData.ipAddress    = ipStr.c_str();
}

void updateWorld() {
  const uint32_t currentTime = millis();
  
  // Camera movement with enhanced collision detection
  // Use "40UL" for sower but more stable movement if this settings is somehow unstable
  if (currentTime - world.lastMoveTime > 16UL) {
    
    // Time scaling for movement calculations
    const uint16_t timeScaleInt = (currentTime >> 8) & 0xFFFF;
    const float timeScale = timeScaleInt * 0.001f;
    
    // Primary orbital motion
    const float actualMovementSpeed = MOVEMENT_SPEED + 0.005f;
    world.cameraAngle += actualMovementSpeed;
    
    // Angle normalization
    if (world.cameraAngle > 6.28318f) world.cameraAngle -= 6.28318f;
    
    // Bounded radius calculation
    const float currentRadius = constrain(4.0f + sin(timeScale) * 1.2f, 2.8f, 5.5f);
    
    // Center with controlled drift
    const float centerX = constrain(8.0f + sin(timeScale * 0.3f) * 0.6f, 4.5f, 11.5f);
    const float centerY = constrain(8.0f + cos(timeScale * 0.3f) * 0.6f, 4.5f, 11.5f);
    
    // Calculate target position with orbital motion
    const float primaryOrbit = world.cameraAngle;
    float targetX = centerX + currentRadius * cos(primaryOrbit);
    float targetY = centerY + currentRadius * sin(primaryOrbit);
    
    // Add secondary motion
    targetX += 0.3f * cos(timeScale * 0.8f);
    targetY += 0.3f * sin(timeScale * 0.8f);
    
    // Enhanced boundary enforcement (reduced safety margin for more dynamic movement)
    const float SAFE_MARGIN = 0.9f;
    targetX = constrain(targetX, SAFE_MARGIN, MAP_SIZE - SAFE_MARGIN);
    targetY = constrain(targetY, SAFE_MARGIN, MAP_SIZE - SAFE_MARGIN);
    
    // Pre-check if target position is valid before movement
    if (checkCameraCollision(targetX, targetY)) {
      // Target is blocked, try alternative movement
      const float escapeAngle = world.cameraAngle + PI / 2.0f;
      targetX = world.cameraX + cos(escapeAngle) * 0.1f;
      targetY = world.cameraY + sin(escapeAngle) * 0.1f;
      
      // Ensure escape position is also safe
      targetX = constrain(targetX, SAFE_MARGIN, MAP_SIZE - SAFE_MARGIN);
      targetY = constrain(targetY, SAFE_MARGIN, MAP_SIZE - SAFE_MARGIN);
    }
    
    // Movement vector calculation with smaller steps
    const float deltaX = targetX - world.cameraX;
    const float deltaY = targetY - world.cameraY;
    
    const float moveLengthSq = deltaX * deltaX + deltaY * deltaY;
    float moveX = 0.0f, moveY = 0.0f;
    
    if (moveLengthSq > 0.000001f) {
      const float moveLength = sqrt(moveLengthSq);
      const float moveSpeed = min(0.08f, moveLength * 0.1f); // Restored original speeds
      
      moveX = (deltaX / moveLength) * moveSpeed;
      moveY = (deltaY / moveLength) * moveSpeed;
    } else {
      // Emergency movement when stuck - keep aggressive
      moveX = cos(timeScale * 2.0f) * 0.08f;
      moveY = sin(timeScale * 2.0f) * 0.08f;
    }
    
    // Collision detection with gradual movement
    float newX = world.cameraX + moveX;
    float newY = world.cameraY + moveY;
    
    // Try full movement first
    if (!checkCameraCollision(newX, newY)) {
      world.cameraX = newX;
      world.cameraY = newY;
    } else {
      // Try X movement only
      if (!checkCameraCollision(world.cameraX + moveX, world.cameraY)) {
        world.cameraX += moveX;
        world.cameraAngle += 0.1f; // Slight angle adjustment
      }
      // Try Y movement only
      else if (!checkCameraCollision(world.cameraX, world.cameraY + moveY)) {
        world.cameraY += moveY;
        world.cameraAngle += 0.1f; // Slight angle adjustment
      }
      // Try smaller movements
      else {
        const float smallMoveX = moveX * 0.3f;
        const float smallMoveY = moveY * 0.3f;
        
        if (!checkCameraCollision(world.cameraX + smallMoveX, world.cameraY + smallMoveY)) {
          world.cameraX += smallMoveX;
          world.cameraY += smallMoveY;
        } else {
          // Last resort - force movement away from nearest wall
          float escapeX = 0.0f, escapeY = 0.0f;
          uint8_t openDirections = 0;
          
          // Check all 8 directions and find open ones
          for (uint8_t dir = 0; dir < 8; dir++) {
            const float testAngle = dir * PI / 4.0f;
            const float testX = world.cameraX + cos(testAngle) * 0.15f;
            const float testY = world.cameraY + sin(testAngle) * 0.15f;
            
            if (!checkCameraCollision(testX, testY)) {
              escapeX += cos(testAngle);
              escapeY += sin(testAngle);
              openDirections++;
            }
          }
          
          if (openDirections > 0) {
            escapeX /= openDirections;
            escapeY /= openDirections;
            
            const float escapeLength = sqrt(escapeX * escapeX + escapeY * escapeY);
            if (escapeLength > 0.1f) {
              world.cameraX += (escapeX / escapeLength) * 0.1f;
              world.cameraY += (escapeY / escapeLength) * 0.1f;
            }
          }
          
          world.cameraAngle += 0.3f; // Larger angle change when stuck
        }
      }
    }
    
    // Final boundary enforcement
    const float FINAL_MARGIN = 0.8f;
    world.cameraX = constrain(world.cameraX, FINAL_MARGIN, MAP_SIZE - FINAL_MARGIN);
    world.cameraY = constrain(world.cameraY, FINAL_MARGIN, MAP_SIZE - FINAL_MARGIN);
    
    // Enhanced stuck detection with better recovery
    static uint8_t stuckCounter = 0;
    static uint8_t recoveryAttempts = 0;
    static float lastCameraX = world.cameraX;
    static float lastCameraY = world.cameraY;
    static uint32_t lastStuckTime = 0;
    
    const float movementThisFrame = abs(world.cameraX - lastCameraX) + abs(world.cameraY - lastCameraY);
    
    if (movementThisFrame < 0.001f) { 
      if (++stuckCounter > 20) {
        recoveryAttempts++;
        
        if (recoveryAttempts < 3) {
          // Try different recovery strategies
          const float recoveryAngle = timeScale * 4.0f + recoveryAttempts * PI / 3.0f;
          const float recoveryRadius = 1.5f + recoveryAttempts * 0.5f;
          
          float newPosX = 8.0f + cos(recoveryAngle) * recoveryRadius;
          float newPosY = 8.0f + sin(recoveryAngle) * recoveryRadius;
          
          // Ensure recovery position is safe
          newPosX = constrain(newPosX, 2.0f, MAP_SIZE - 2.0f);
          newPosY = constrain(newPosY, 2.0f, MAP_SIZE - 2.0f);
          
          if (!checkCameraCollision(newPosX, newPosY)) {
            world.cameraX = newPosX;
            world.cameraY = newPosY;
            world.cameraAngle += PI / 4.0f; // 45-degree turn
            stuckCounter = 0;
            recoveryAttempts = 0;
          }
        } else {
          // Nuclear option - teleport to guaranteed safe position
          world.cameraX = 8.0f;
          world.cameraY = 8.0f;
          world.cameraAngle = (float)(millis() % 628) / 100.0f;
          stuckCounter = 0;
          recoveryAttempts = 0;
          lastStuckTime = currentTime;
        }
      }
    } else {
      if (stuckCounter > 0) stuckCounter--;
      if (currentTime - lastStuckTime > 30000UL) {
        recoveryAttempts = 0;
      }
    }
    
    lastCameraX = world.cameraX;
    lastCameraY = world.cameraY;
    
    // Camera orientation with smooth interpolation
    const float lookAtX = centerX - world.cameraX;
    const float lookAtY = centerY - world.cameraY;
    const float lookLength = sqrt(lookAtX * lookAtX + lookAtY * lookAtY);
    
    if (lookLength > 0.1f) {
      const float targetDirX = lookAtX / lookLength;
      const float targetDirY = lookAtY / lookLength;
      
      // Smooth camera sway
      const float sway = sin(timeScale * 1.5f) * 0.03f;
      const float swayY = cos(timeScale * 1.1f) * 0.02f;
      
      // Smooth interpolation with adaptive factor
      const float interpFactor = min(0.15f, movementThisFrame * 5.0f + 0.08f);
      world.cameraDirX = world.cameraDirX * (1.0f - interpFactor) + (targetDirX + sway) * interpFactor;
      world.cameraDirY = world.cameraDirY * (1.0f - interpFactor) + (targetDirY + swayY) * interpFactor;
      
      // Normalize camera direction
      const float dirLength = sqrt(world.cameraDirX * world.cameraDirX + world.cameraDirY * world.cameraDirY);
      if (dirLength > 0.1f) {
        world.cameraDirX /= dirLength;
        world.cameraDirY /= dirLength;
      } else {
        world.cameraDirX = cos(world.cameraAngle);
        world.cameraDirY = sin(world.cameraAngle);
      }
    } else {
      world.cameraDirX = cos(world.cameraAngle);
      world.cameraDirY = sin(world.cameraAngle);
    }
    
    // Enhanced NaN safety check with better recovery
    if (world.cameraX != world.cameraX || world.cameraY != world.cameraY || 
        world.cameraDirX != world.cameraDirX || world.cameraDirY != world.cameraDirY ||
        world.cameraX < 0 || world.cameraX >= MAP_SIZE ||
        world.cameraY < 0 || world.cameraY >= MAP_SIZE) {
      // Reset to safe defaults
      world.cameraX = 8.0f;
      world.cameraY = 8.0f;
      world.cameraAngle = 0.0f;
      world.cameraDirX = 1.0f;
      world.cameraDirY = 0.0f;
      world.velocityX = 0.0f;
      world.velocityY = 0.0f;
    }
    
    world.lastMoveTime = currentTime;
  }
}

void renderFrame() {
  // Clear z-buffer
  for (uint8_t i = 0; i < RENDER_WIDTH; i++) {
    zbuffer[i] = (uint16_t)(MAX_DISTANCE * 1000.0f);
  }
  
  // Render walls
  renderRaycastedView();
}

void renderRaycastedView() {
  // Calculate plane vectors for proper FOV
  const float fovRad = FOV_ANGLE * PI / 180.0f;
  const float planeMagnitude = tan(fovRad / 2.0f);
  
  // Camera direction normalization
  const float dirLength = sqrt(world.cameraDirX * world.cameraDirX + world.cameraDirY * world.cameraDirY);
  float normDirX = world.cameraDirX;
  float normDirY = world.cameraDirY;
  
  if (dirLength > 0.1f) {
    normDirX /= dirLength;
    normDirY /= dirLength;
  } else {
    normDirX = cos(world.cameraAngle);
    normDirY = sin(world.cameraAngle);
  }
  
  const float planeX = -normDirY * planeMagnitude;
  const float planeY = normDirX * planeMagnitude;
  
  for (uint8_t x = 0; x < RENDER_WIDTH; x++) {
    // Calculate ray direction with perspective correction
    const float cameraX_ray = 2.0f * x / (float)(RENDER_WIDTH - 1) - 1.0f;
    const float rayDirX = normDirX + planeX * cameraX_ray;
    const float rayDirY = normDirY + planeY * cameraX_ray;
    
    // Ensure ray direction is not zero
    const float rayLength = sqrt(rayDirX * rayDirX + rayDirY * rayDirY);
    if (rayLength < 0.001f) {
      drawVerticalLine(x, RENDER_HEIGHT / 2, RENDER_HEIGHT / 2 + 1, MAX_DISTANCE);
      continue;
    }
    
    // Perform DDA raycasting
    const float distance = castRay(world.cameraX, world.cameraY, rayDirX / rayLength, rayDirY / rayLength);
    
    // Calculate wall height with proper perspective
    const int wallHeight = constrain((int)(WALL_HEIGHT / max(distance, 0.1f)), 2, RENDER_HEIGHT - 8);
    
    // Calculate wall start and end positions
    const int wallStart = constrain((RENDER_HEIGHT / 2) - wallHeight / 2, 0, RENDER_HEIGHT - 2);
    const int wallEnd = constrain((RENDER_HEIGHT / 2) + wallHeight / 2, wallStart + 2, RENDER_HEIGHT);
    
    // Draw vertical line - walls only
    drawVerticalLine(x, wallStart, wallEnd, distance);
    
    zbuffer[x] = (uint16_t)(min(distance, MAX_DISTANCE) * 1000.0f);
  }
}

float castRay(const float startX, const float startY, const float dirX, const float dirY) {
  // DDA algorithm for raycasting
  int mapX = (int)startX;
  int mapY = (int)startY;
  
  // Calculate delta distances
  const float deltaDistX = (dirX == 0) ? 1e30f : abs(1.0f / dirX);
  const float deltaDistY = (dirY == 0) ? 1e30f : abs(1.0f / dirY);
  
  float sideDistX, sideDistY;
  int stepX, stepY;
  uint8_t side; // 0 for NS wall, 1 for EW wall
  
  // Calculate step and initial sideDist
  if (dirX < 0) {
    stepX = -1;
    sideDistX = (startX - mapX) * deltaDistX;
  } else {
    stepX = 1;
    sideDistX = (mapX + 1.0f - startX) * deltaDistX;
  }
  
  if (dirY < 0) {
    stepY = -1;
    sideDistY = (startY - mapY) * deltaDistY;
  } else {
    stepY = 1;
    sideDistY = (mapY + 1.0f - startY) * deltaDistY;
  }
  
  // Perform DDA
  bool hit = false;
  while (!hit) {
    // Jump to next map square
    if (sideDistX < sideDistY) {
      sideDistX += deltaDistX;
      mapX += stepX;
      side = 0;
    } else {
      sideDistY += deltaDistY;
      mapY += stepY;
      side = 1;
    }
    
    // Check if ray has hit a wall or is out of bounds
    if (mapX < 0 || mapX >= MAP_SIZE || mapY < 0 || mapY >= MAP_SIZE) {
      return MAX_DISTANCE;
    }
    
    if (world.map[mapX][mapY] > 0 || world.columns[mapY * MAP_SIZE + mapX] > 0) {
      hit = true;
    }
  }
  
  // Calculate distance
  const float distance = (side == 0) ? 
    (mapX - startX + (1 - stepX) / 2) / dirX :
    (mapY - startY + (1 - stepY) / 2) / dirY;
  
  return constrain(distance, 0.1f, MAX_DISTANCE);
}

// Walls-only vertical line drawing - optimized for performance
void drawVerticalLine(const uint8_t x, const int wallStart, const int wallEnd, const float distance) {
  // Calculate fog intensity for depth perception
  const uint8_t fogLevel = constrain((int)(distance * 4), 0, 15);
  
  // Apply fog to wall color only
  const uint16_t wallColor = applyFog(colors.wall, fogLevel);
  
  // Wall bounds checking
  const int safeWallStart = constrain(wallStart, 0, RENDER_HEIGHT - 1);
  const int safeWallEnd = constrain(wallEnd, safeWallStart + 1, RENDER_HEIGHT);
  
  // Draw background (non-wall areas) - single color for performance
  for (int y = 0; y < safeWallStart; y++) {
    tft.drawPixel(x, y, colors.background);
  }
  
  // Draw wall segment
  for (int y = safeWallStart; y < safeWallEnd; y++) {
    tft.drawPixel(x, y, wallColor);
  }
  
  // Draw background (non-wall areas)
  for (int y = safeWallEnd; y < RENDER_HEIGHT; y++) {
    tft.drawPixel(x, y, colors.background);
  }
}

// Apply fog effect for depth perception
inline uint16_t applyFog(const uint16_t color, const uint8_t fogLevel) {
  if (fogLevel >= 16) return ST7735_BLACK;
  
  // Extract RGB components using bit operations
  const uint8_t r = (color >> 11) & 0x1F;
  const uint8_t g = (color >> 5) & 0x3F;
  const uint8_t b = color & 0x1F;
  
  // Apply fog intensity
  const uint8_t intensity = 16 - fogLevel;
  const uint8_t foggedR   = (r * intensity) >> 4;
  const uint8_t foggedG   = (g * intensity) >> 4;
  const uint8_t foggedB   = (b * intensity) >> 4;
  
  return (foggedR << 11) | (foggedG << 5) | foggedB;
}

// Custom degree symbol drawing function
void drawDegreeSymbol(const int16_t x, const int16_t y, const uint16_t color) {
  for (uint8_t row = 0; row < 5; row++) {
    const uint8_t bitmap_row = pgm_read_byte(&degree_bitmap[row]);
    for (uint8_t col = 0; col < 4; col++) {
      if (bitmap_row & (0x80 >> col)) {
        tft.drawPixel(x + col, y + row, color);
      }
    }
  }
}

void displayInfo() {
  // Update display once per minute to reduce CPU cycles
  static int lastMinute = -1;
  if (timeData.minute == lastMinute) return;
  lastMinute = timeData.minute;
  
  // Clear info area
  const int textAreaHeight = 128 - RENDER_HEIGHT - 2;
  tft.fillRect(0, RENDER_HEIGHT, 160, textAreaHeight, ST7735_BLACK);
  
  // Convert to 12-hour format
  int displayHour = timeData.hour;
  const char* ampm;
  
  if (displayHour == 0) {
    displayHour = 12; // Midnight
    ampm = "AM";
  } else if (displayHour > 12) {
    displayHour -= 12;
    ampm = "PM";
  } else if (displayHour == 12) {
    ampm = "PM"; // Noon
  } else {
    ampm = "AM";
  }
  
  // Display time in 12-hour format
  const int baseY = RENDER_HEIGHT + 1;
  
  tft.setTextColor(ST7735_WHITE);
  tft.setTextSize(2);
  tft.setCursor(10, baseY);
  tft.printf("%02d:%02d", displayHour, timeData.minute);
  
  // Display AM/PM
  tft.setTextSize(1);
  tft.setCursor(90, baseY + 7);
  tft.print(ampm);
  
  // Display date
  tft.setTextSize(1);
  tft.setCursor(10, baseY + 20);
  tft.printf("%s %02d/%02d/%04d", timeData.dayName, 
             timeData.day, timeData.month, timeData.year);
  
  // Display weather with custom degree symbol
  if (timeData.temperature != 0 && (baseY + 30) < (128 - 10)) {
    tft.setCursor(10, baseY + 30);
    tft.printf("%.1f", timeData.temperature);
    
    // Draw custom degree symbol
    drawDegreeSymbol(tft.getCursorX(), tft.getCursorY() - 1, ST7735_WHITE);
    
    // Continue with C and humidity
    tft.setCursor(tft.getCursorX() + 6, tft.getCursorY());
    tft.printf("C %dRH", timeData.humidity);
  }
  
  // Display WiFi strength and pressure
  if ((baseY + 40) < (128 - 8)) {
    tft.setTextColor(ST7735_CYAN);
    tft.setCursor(10, baseY + 40);
    tft.printf("WiFi:%ddBm", timeData.wifiStrength);
    
    // Display pressure if available
    if (timeData.pressure > 0 && (baseY + 40) < (128 - 8)) {
      tft.setCursor(80, baseY + 40);
      tft.printf("%.0fhPa", timeData.pressure);
    }
  }
}

void updateWeather() {
  if (WiFi.status() != WL_CONNECTED) return;
  
  HTTPClient http;
  WiFiClient client;
  
  // Construct URL using efficient string operations
  static char url[200];
  strcpy_P(url, WEATHER_BASE_URL);
  strcat_P(url, MUMBAI_CITY_ID);
  strcat  (url, "&appid=");
  strcat_P(url, WEATHER_API_KEY);
  strcat  (url, "&units=metric");
  
  http.begin(client, url);
  const int httpResponseCode = http.GET();
  
  if (httpResponseCode == 200) {
    const String payload = http.getString();
    
    // Parse JSON for weather data
    DynamicJsonDocument doc(2048);
    deserializeJson(doc, payload);
    
    timeData.temperature = doc["main"]["temp"];
    timeData.humidity    = doc["main"]["humidity"];
    timeData.pressure    = doc["main"]["pressure"];
    
    // Store weather description
    static String weatherDescStr = doc["weather"][0]["description"].as<String>();
    timeData.weatherDesc = weatherDescStr.c_str();
    
    static String cityNameStr = doc["name"].as<String>();
    timeData.cityName = cityNameStr.c_str();
    
    if (doc["wind"]["speed"]) {
      timeData.windSpeed = doc["wind"]["speed"];
    }
    
    timeData.lastWeatherUpdate = millis();
  }
  
  http.end();
}

/**
 * โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—
 * โ•‘                            TECHNICAL DOCUMENTATION                          โ•‘
 * โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
 * 
 * CONSTEXPR IN C++11:
 * ===============================
 * 
 * The 'constexpr' keyword was introduced in C++11 (ISO/IEC 14882:2011) as part of 
 * the language's move toward more compile-time computation capabilities. It serves 
 * multiple purposes in modern C++ development:
 * 
 * 1. COMPILE-TIME CONSTANTS:
 *    - constexpr variables are evaluated at compile time, not runtime
 *    - Values are embedded directly into the binary, eliminating memory loads
 *    - E.g.: constexpr uint8_t RENDER_WIDTH = 128;
 *    
 * 2. COMPILE-TIME FUNCTIONS:
 *    - constexpr functions can be evaluated during compilation if arguments are constant
 *    - Enables template metaprogramming and compile-time calculations
 *    - Reduces runtime overhead significantly in embedded systems
 *    
 * 3. OPTIMIZATION BENEFITS:
 *    - Eliminates runtime variable initialization
 *    - Reduces memory footprint in resource-constrained environments
 *    - Allows aggressive compiler optimizations (loop unrolling, dead code elimination)
 *    - Perfect for embedded systems where every byte and CPU cycle matters
 * 
 * CONSTEXPR IN EMBEDDED C++:
 * ==========================
 * 
 * In microcontroller programming, constexpr provides several critical advantages:
 * 
 * โ€ข MEMORY EFFICIENCY: Values stored in flash (program memory) instead of RAM
 * โ€ข PERFORMANCE:       No runtime initialization or memory access penalties
 * โ€ข PREDICTABILITY:    Compile-time evaluation ensures deterministic behavior
 * โ€ข CODE CLARITY:      Clearly distinguishes between compile-time and runtime data
 * 
 * Traditional const vs constexpr:
 * - const int x = 5;        // May be stored in RAM, initialized at runtime
 * - constexpr int x = 5;    // Guaranteed compile-time constant, stored in flash
 * 
 * RAYCASTING PRINCIPLES AND IMPLEMENTATION:
 * ========================================= 
 * 
 * Raycasting is a rendering technique that creates pseudo-3D graphics by casting
 * rays from the camera through each column of the screen into a 2D world map.
 * 
 * MATHEMATICAL FOUNDATION:
 * 
 * 1. RAY EQUATION:
 *    For each screen column x, calculate ray direction:
 *    rayDirX = cameraDirX + planeX * cameraX_normalized
 *    rayDirY = cameraDirY + planeY * cameraX_normalized
 *    
 *    โˆ€ cameraX_normalized = 2.0 * x / screenWidth - 1.0 (range: -1 to +1)
 * 
 * 2. DDA ALGORITHM (Digital Differential Analyzer):
 *    - Efficiently steps through grid cells along the ray path
 *    - Calculates deltaDistX = |1/rayDirX| and deltaDistY = |1/rayDirY|
 *    - Uses sideDist variables to track distance to next grid line
 *    - Determines step direction (+1 or -1) for X and Y axes
 * 
 * 3. WALL HEIGHT CALCULATION:
 *    wallHeight = WALL_HEIGHT / distance
 *    - Implements perspective projection using 1/distance relationship
 *    - Creates illusion of depth through size variation
 * 
 * 4. PERSPECTIVE CORRECTION:
 *    distance = (side == 0) ? (mapX - startX + (1-stepX)/2) / rayDirX :
 *                             (mapY - startY + (1-stepY)/2) / rayDirY
 *    - Prevents fisheye distortion by using perpendicular distance
 *    - Maintains realistic proportions across field of view
 *
 * ST7735 Display Tab Configuration Guide
 * =====================================
 * 
 * The ST7735 controller comes in different variants, each requiring specific 
 * initialization settings called "tabs". The tab determines memory mapping,
 * color ordering, and display offsets.
 * 
 * TAB TYPES:
 * ----------
 * INITR_BLACKTAB:    - Most common variant
 *                    - Often has 1-2 pixel offset issues on edges
 *                    - RGB color order
 *                    - May show dirty lines on 1.44" displays
 * 
 * INITR_GREENTAB:    - Alternative variant 
 *                    - Different memory mapping
 *                    - Better for some 1.44" displays
 *                    - May have different color calibration
 * 
 * INITR_REDTAB:      - Third variant
 *                    - BGR color order (colors may appear swapped)
 *                    - Different offset configuration
 *                    - Less common for 1.44" displays
 * 
 * INITR_144GREENTAB: - Specific variant for 1.44" ST7735 displays
 *                    - Optimized memory mapping for 128x128 resolution
 *                    - Eliminates edge artifacts and dirty lines
 *                    - Best choice for genuine 1.44" ST7735R displays
 * 
 * WHEN TO USE 144GREENTAB:
 * -----------------------
 * - Use for 1.44" ST7735 displays (128x128 resolution)
 * - When experiencing random pixels or lines at screen edges
 * - When BLACKTAB/GREENTAB show offset or boundary issues
 * - For displays with "ST7735R" controller specifically
 * 
 * WHY 144GREENTAB TO BE PREFERRED for 1.44 Display:
 * ------------------------------------------------
 * - Correct memory window addressing for 1.44" variants
 * - Eliminates/accounts for errors like off-by-one pixel boundary  
 * - Proper hardware tailored RGB color mapping (or more like, the manufacturers follow the said standard).
 * 
 * TROUBLESHOOTING:
 * ---------------
 * If display shows:
 * - Wrong colors        โ†’ Try REDTAB or different tab
 * - Offset image        โ†’ Try GREENTAB or 144GREENTAB  
 * - Edge artifacts      โ†’ Use 144GREENTAB for 1.44" displays
 * - Completely blank    โ†’ Check wiring and tab compatibility
 * 
 * NOTE: Always match the tab to your specific display variant.
 * When in doubt, try 144GREENTAB first for 1.44" displays.
 * 
 * ================================================================================================
 * ADDITIONAL RESOURCES AND REFERENCES:
 * ================================================================================================
 * While in the 21st Century almost everything is available at our fingertips, 
 * here are some specific references and sources that adhere to academic rigour or have proven materials, 
 * including resources for procedural techniques and raycasting techniques used in this firmware sketch:
 * 
 * TECHNICAL REFERENCES:
 * โ€ข Lode Vandevenne's Raycasting Tutorial: Comprehensive guide on implementing raycasting 
 *   for pseudo-3D rendering, covering DDA algorithms and perspective projection.
 *   https://lodev.org/cgtutor/raycasting.html
 * โ€ข Permadi's Ray-Casting Tutorial: Detailed explanation of raycasting mechanics, including 
 *   ray generation, grid traversal, and perspective correction techniques used in the code.
 *   https://permadi.com/1996/05/ray-casting-tutorial/
 * โ€ข ESP8266 Arduino Core Documentation: Official documentation for programming the ESP8266 
 *   with Arduino, detailing memory management and optimization techniques.
 *   https://arduino-esp8266.readthedocs.io/
 * โ€ข ST7735 Display Driver Reference: Technical reference for the ST7735 display driver, 
 *   used for optimized rendering in the raycasting pipeline.
 *   https://github.com/adafruit/Adafruit-ST7735-Library
 * โ€ข NTP Protocol Specification: RFC 5905, detailing Network Time Protocol for time 
 *   synchronization used in the dynamic color system.
 *   https://tools.ietf.org/html/rfc5905
 * โ€ข C++11 constexpr Reference: ISO/IEC 14882:2011 Standard, explaining compile-time 
 *   constants used for performance optimization in the code.
 *   ISO/IEC 14882:2011 Standard
 * โ€ข OpenWeatherMap API Documentation: Reference for integrating real-time environmental 
 *   data, potentially used for dynamic lighting or atmospheric effects.
 *   https://openweathermap.org/api
 * 
 * MATHEMATICAL REFERENCES:
 * โ€ข Computer Graphics: Principles and Practice (Foley, van Dam, Feiner, Hughes): 
 *   Foundational text covering raycasting mathematics, projection techniques, and 3D 
 *   rendering principles.
 * โ€ข Real-Time Rendering (Akenine-Mรถller, Haines, Hoffman): In-depth resource on real-time 
 *   rendering techniques, including optimization strategies for raycasting and perspective 
 *   correction.
 * โ€ข 3D Math Primer for Graphics and Game Development (Dunn, Parberry): Covers vector 
 *   mathematics and camera systems, essential for the orbital motion and direction 
 *   calculations in the code.
 * โ€ข Procedural Content Generation in Games (Shaker, Togelius, Nelson): Explores procedural 
 *   techniques for dynamic camera motion and map generation, as used in the orbital and 
 *   unstuck mechanisms.
 * 
 * ESP8266 DEVELOPMENT RESOURCES:
 * โ€ข ESP8266 Technical Reference: Official Espressif documentation detailing hardware 
 *   constraints and optimization strategies for the ESP8266 microcontroller.
 *   https://www.espressif.com/sites/default/files/documentation/esp8266-technical_reference_en.pdf
 * โ€ข Arduino IDE ESP8266 Package: Source for ESP8266 Arduino integration, providing tools 
 *   for efficient memory and stack management.
 *   https://github.com/esp8266/Arduino
 * โ€ข ESP8266 Community Forum: Community-driven resource for troubleshooting and optimizing 
 *   ESP8266-based projects.
 *   https://www.esp8266.com/
 * 
 * TIME SYNCHRONIZATION RESOURCES:
 * โ€ข NTP Pool Project: Resource for reliable time synchronization servers, used for accurate 
 *   time-based effects in the rendering system.
 *   https://www.ntppool.org/
 * โ€ข NIST Time Services: Official time services for precise synchronization, supporting the 
 *   dynamic color palette cycling.
 *   https://www.nist.gov/pml/time-and-frequency-division/time-services
 * โ€ข Internet Time Synchronization: RFC 1305, foundational specification for NTP, relevant 
 *   for time-driven procedural effects.
 *   https://tools.ietf.org/html/rfc1305
 * 
 * PROCEDURAL TECHNIQUES RESOURCES:
 * โ€ข Procedural Generation Algorithms (RogueBasin): Wiki resource detailing procedural 
 *   movement and map generation techniques, applicable to the camera's orbital motion and 
 *   dynamic center calculations.
 *   http://www.roguebasin.com/index.php?title=Procedural_Generation
 * โ€ข Game Programming Patterns (Nystrom): Book covering procedural design patterns, 
 *   including techniques for organic camera movement and collision avoidance systems.
 *   https://gameprogrammingpatterns.com/
 * โ€ข The Art of Procedural Content Generation (GDC Talks): Collection of GDC presentations 
 *   on procedural techniques, offering insights into dynamic camera behaviors and 
 *   environmental interaction.
 *   https://www.gdcvault.com/
 * 
 * ================================================================================================
 * 
 * Copyright (c) 2025 Core1D Automation Labs
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 * 
 * Designed and Developed by Sir Ronnie
 * Core1D Automation Labs
 * https://core1d.com
 *
 * Adafruit Library License details:
  Adafruit invests time and resources providing this open source code,
  please support Adafruit and open-source hardware by purchasing
  products from Adafruit!

  Written by Limor Fried/Ladyada for Adafruit Industries.
  MIT license, all text above must be included in any redistribution.
 * 
 * โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
 */

More D1 R1 project links:

LOLIN/WeMos/NodeMCU D1 R1 1.44 TFT Hyperclock Firmware Rev 2

LOLIN/WeMos/NodeMCU D1 R1 1.44

LOLIN/WeMos/NodeMCU D1 R1 LCD Keypad Shield Firmware TFT Hyperclock Software

[More coming soon (Under testing and documentation)]

Cheers

Note: 10ฮผF Circuit capacitor and 220 Ohm resistor to VCC is optional if your power supply is from the board itself.
You can control the brightness of the LED display by using the resistor value as your hardware brightness control. Avenues to experiment with variable resistors, or implement a PWM (Pulse Width Modulation) based Backlight control from the microcontroller (by using PWM capable pins and placing the connection end somewhere between the LED+/LED/BL pin and the resistor from the VCC source{3v3 in this case}. Usage of resistor may depend on the board and their GPIO physical capabilities. Refer to official datasheets or datasheet informed source for verification).

Test Update 08/08/25: Approx. 25% battery consumption on 5000 mAh battery on six hours of run.
Patch Update: Added measures to collision detection and world update to prevent camera getting stuck in procedural edge cases,

2 Likes

Hello everyone.
Hope you all had a wonderful week.

This time, I bring another Adafruit project for D1 R1 ESP8266 board, albeit with a different screen.
This time using the commonly available cheap KMR 1.8 SPI TFT 128x160 ST7735 Display.

Was brainstorming on how to make destructible digits at least visually, if actually implementing in an active clock will be too much for an ESP8266. Discovered the following elegant solution involving usage of dirty rendering with particle effects. (Donโ€™t worry, the digits regenerate every minute, so you will not end up with a completely useless clock).

I present:

D1 R1 HyperClock PONG GoL ST7735 Firmware

Hardware physical footprint:

Source Code:

/*
 * Core1D Automation Labs Present:
 * โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—
 * โ•‘                                                                             โ•‘
 * โ–ˆโ–ˆโ•—  โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•—   โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—  โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•—      โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—  โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•—
 * โ–ˆโ–ˆโ•‘  โ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ•‘     โ–ˆโ–ˆโ•”โ•โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘
 * โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘ โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ• โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—  โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•‘     โ–ˆโ–ˆโ•‘     โ–ˆโ–ˆโ•‘   โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘     โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•
 * โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•‘  โ•šโ–ˆโ–ˆโ•”โ•  โ–ˆโ–ˆโ•”โ•โ•โ•โ• โ–ˆโ–ˆโ•”โ•โ•โ•  โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘     โ–ˆโ–ˆโ•‘     โ–ˆโ–ˆโ•‘   โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘     โ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•—
 * โ–ˆโ–ˆโ•‘  โ–ˆโ–ˆโ•‘   โ–ˆโ–ˆโ•‘   โ–ˆโ–ˆโ•‘     โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘  โ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘ โ•‘โ–ˆโ–ˆโ•—
 * โ•šโ•โ•  โ•šโ•โ•   โ•šโ•โ•   โ•šโ•โ•     โ•šโ•โ•โ•โ•โ•โ•โ•โ•šโ•โ•  โ•šโ•โ• โ•šโ•โ•โ•โ•โ•โ•โ•šโ•โ•โ•โ•โ•โ•โ• โ•šโ•โ•โ•โ•โ•โ•  โ•šโ•โ•โ•โ•โ•โ•โ•šโ•โ• โ•‘โ•šโ•โ•
 * โ•‘                                                                             โ•‘
 * โ•‘                                                                             โ•‘
 * โ•‘      โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—  โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ•—   โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—       โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—  โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•—         โ•‘
 * โ•‘      โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ•—  โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•      โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ• โ–ˆโ–ˆโ•”โ•โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘         โ•‘
 * โ•‘      โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•‘   โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘  โ–ˆโ–ˆโ–ˆโ•—     โ–ˆโ–ˆโ•‘  โ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘   โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘         โ•‘
 * โ•‘      โ–ˆโ–ˆโ•”โ•โ•โ•โ• โ–ˆโ–ˆโ•‘   โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘   โ–ˆโ–ˆโ•‘     โ–ˆโ–ˆโ•‘   โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘   โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘         โ•‘
 * โ•‘      โ–ˆโ–ˆโ•‘     โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•‘ โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•     โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—    โ•‘
 * โ•‘      โ•šโ•โ•      โ•šโ•โ•โ•โ•โ•โ• โ•šโ•โ•  โ•šโ•โ•โ•โ• โ•šโ•โ•โ•โ•โ•โ•       โ•šโ•โ•โ•โ•โ•โ•  โ•šโ•โ•โ•โ•โ•โ• โ•šโ•โ•โ•โ•โ•โ•โ•    โ•‘
 * โ•‘                                                                             โ•‘
 * โ•‘              โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”            โ•‘
 * โ•‘              โ”‚  โ— โ— โ—    CONWAY'S GAME OF LIFE    โ— โ— โ—        โ”‚            โ•‘
 * โ•‘              โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”  +  โ”Œโ”€โ”€โ”€โ”€โ”€โ”  +  โ”Œโ”€โ”€โ”€โ”€โ”€โ”                โ”‚            โ•‘
 * โ•‘              โ”‚  โ”‚โ–ˆ โ–ˆ โ–ˆโ”‚     โ”‚ โ–ˆ โ–ˆ โ”‚     โ”‚โ–ˆ   โ–ˆโ”‚  CELLULAR      โ”‚            โ•‘
 * โ•‘              โ”‚  โ”‚ โ–ˆ โ–ˆ โ”‚     โ”‚โ–ˆ โ–ˆ โ–ˆโ”‚     โ”‚ โ–ˆ โ–ˆ โ”‚  AUTOMATON     โ”‚            โ•‘
 * โ•‘              โ”‚  โ”‚โ–ˆ โ–ˆ โ–ˆโ”‚     โ”‚ โ–ˆ โ–ˆ โ”‚     โ”‚โ–ˆ   โ–ˆโ”‚                โ”‚            โ•‘
 * โ•‘              โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”€โ”˜                โ”‚            โ•‘
 * โ•‘              โ”‚                                                 โ”‚            โ•‘
 * โ•‘              โ”‚                                                 โ”‚            โ•‘
 * โ•‘              โ”‚           PONG GAME                             โ”‚            โ•‘
 * โ•‘              โ”‚         โ”‚โ–Œ      โ—‹   โ–โ”‚                          โ”‚            โ•‘
 * โ•‘              โ”‚         โ”‚โ–Œ    โ—     โ–โ”‚                          โ”‚            โ•‘
 * โ•‘              โ”‚         โ”‚โ–Œ  โ—       โ–โ”‚                          โ”‚            โ•‘
 * โ•‘              โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜            โ•‘
 * โ•‘                            ESP8266 ST7735 Firmware Version  V: 3.1.2        โ•‘
 * โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
 *
 * ESP8266 HyperClock Pong GoL - Conway's Game of Life + Classic Pong Clock
 * Adaptive Visual Clock with Cellular Automaton Background & Classic Gaming
 * 
 * Version:     3.1 Living Edition
 * Designed by: Sir Ronnie from Core1D Automation Labs
 * License:     MIT License
 * Target:      ESP8266 WeMos D1 R1 with 1.8" ST7735 SPI TFT Display (128x160)
 * 
 * HARDWARE CONNECTIONS:
 * =====================
 * 
 * ST7735 1.8" TFT Display (128x160):
 * โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
 * โ”‚  ST7735 Pin  โ”‚  WeMos D1 Pin   โ”‚
 * โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚
 * โ”‚  VCC         โ”‚  3V3            โ”‚
 * โ”‚  GND         โ”‚  GND            โ”‚
 * โ”‚  CS          โ”‚  D2 (GPIO4)     โ”‚
 * โ”‚  RESET       โ”‚  D3 (GPIO0)     โ”‚
 * โ”‚  A0/DC       โ”‚  D4 (GPIO2)     โ”‚
 * โ”‚  SDA/MOSI    โ”‚  D7 (GPIO13)    โ”‚
 * โ”‚  SCK/CLK     โ”‚  D5 (GPIO14)    โ”‚
 * โ”‚  LED         โ”‚  3V3 (Optional) โ”‚
 * โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
 * 
 * HARDWARE ASCII SCHEMATIC:
 * ========================
 * 
 *      WeMos D1 R1 (ESP8266)          ST7735 1.8" TFT
 *    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
 *    โ”‚                     โ”‚         โ”‚                 โ”‚
 *    โ”‚  [MICRO-USB]   3V3 โ—โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”คโ— VCC            โ”‚
 *    โ”‚                     โ”‚         โ”‚                 โ”‚
 *    โ”‚  [WIFI ANTENNA]     โ”‚         โ”‚                 โ”‚
 *    โ”‚                     โ”‚         โ”‚                 โ”‚
 *    โ”‚              D7/13 โ—โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”คโ— SDA (MOSI)     โ”‚
 *    โ”‚              D5/14 โ—โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”คโ— SCK (CLK)      โ”‚
 *    โ”‚               D2/4 โ—โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”คโ— CS             โ”‚
 *    โ”‚               D3/0 โ—โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”คโ— RST            โ”‚
 *    โ”‚               D4/2 โ—โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”คโ— DC (A0)        โ”‚
 *    โ”‚                     โ”‚         โ”‚                 โ”‚
 *    โ”‚                GND โ—โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”คโ— GND            โ”‚
 *    โ”‚                     โ”‚         โ”‚                 โ”‚
 *    โ”‚    [ESP8266-12E]    โ”‚         โ”‚ [128x160 LCD]   โ”‚
 *    โ”‚    [80MHz/160MHz]   โ”‚         โ”‚ [65K Colors]    โ”‚
 *    โ”‚    [4MB Flash]      โ”‚         โ”‚ [SPI Interface] โ”‚
 *    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
 *               โ”‚                              โ”‚
 *               โ””โ”€โ”€โ”€โ”€ [ Xtensa LX106 Core ] โ”€โ”€โ”€โ”˜
 *                     [ 2.4GHz WiFi Radio ]
 *
 * FEATURES:
 * =========
 * โ–ธ Conway's Game of Life cellular automaton background
 * โ–ธ Classic Pong game with AI paddles, particle effects, and a unique destructible digit implementation
 * โ–ธ Hourly automatic mode switching (GOL โ†” Pong)
 * โ–ธ Day/Night theme adaptation based on time (6AM-6PM)
 * โ–ธ NTP-synchronized real-time clock with WiFi connectivity
 * โ–ธ Optimized 2x scaled rendering for smooth 50 FPS performance
 * โ–ธ Memory management with automatic cleanup and monitoring
 * โ–ธ Visual effects: color cycling, particle systems, collision detection
 * โ–ธ Resilient offline operation with fallback time keeping
 * โ–ธ Advanced Pong AI with predictive ball tracking
 * โ–ธ Self-reseeding GOL patterns to prevent stagnation
 * โ–ธ WiFi status indication via colon color (white(night)/black(day) = connected, red = offline)
 *
 * OPERATING MODES:
 * ===============
 * โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
 * โ”‚ GAME OF LIFE MODE (Even Hours: 0, 2, 4, 6, 8, 10, 12, 14, 16...) โ”‚
 * โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
 * โ”‚ โ”‚ โ—  โ—     โ—โ—              12:34           โ—   โ—โ—   โ—โ—  โ—    โ”‚   โ”‚
 * โ”‚ โ”‚  xโ—โ—    โ—xxโ—                             โ—โ—  โ—xโ—  โ—  โ—โ—    โ”‚   โ”‚
 * โ”‚ โ”‚ โ—xโ—     โ—xโ—              <WiFi           โ— xx โ— x โ—  โ—     โ”‚   โ”‚
 * โ”‚ โ”‚  xโ—โ—     โ—              Status>           โ—โ—   โ—โ—   โ—xโ—    โ”‚   โ”‚
 * โ”‚ โ”‚   โ—                        :              โ—    โ—    โ—โ—     โ”‚   โ”‚
 * โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
 * โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
 * 
 * โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
 * โ”‚ PONG GAME MODE (Odd Hours: 1, 3, 5, 7, 9, 11, 13, 15, 17, 19...) โ”‚
 * โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
 * โ”‚ โ”‚                                                          โ–ˆ โ”‚   โ”‚
 * โ”‚ โ”‚             โ—              11:34                         โ–ˆ โ”‚   โ”‚
 * โ”‚ โ”‚                            <WiFi                           โ”‚   โ”‚
 * โ”‚ โ”‚ โ–ˆ    *  โ—                 Status>         โ—                โ”‚   โ”‚
 * โ”‚ โ”‚ โ–ˆ      *                     :              *  *           โ”‚   โ”‚
 * โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
 * โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
 *
 * MEMORY ARCHITECTURE:
 * ===================
 * Flash Memory (4MB):        Dynamic Memory (Heap):
 * โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
 * โ”‚ Program Code    โ”‚        โ”‚ WiFi Buffers    โ”‚
 * โ”‚ Constants       โ”‚        โ”‚ Display Buffer  โ”‚
 * โ”‚ String Literals โ”‚        โ”‚ Game States     โ”‚
 * โ”‚ PROGMEM Data    โ”‚        โ”‚ Particle System โ”‚
 * โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜        โ”‚ Stack Growth    โ”‚
 *                            โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
 *
 * Performance Optimizations:
 * โ–ธ 160MHz CPU overclock for enhanced responsiveness
 * โ–ธ Half-resolution internal rendering (64x80) with 2x scaling
 * โ–ธ Efficient differential updates (render only changes)
 * โ–ธ Memory-aware particle system with dynamic allocation
 * โ–ธ PROGMEM usage for static strings and constants
 * โ–ธ Minimal clock area protection in cellular automaton
 *
 * โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—
 * โ•‘                         ARDUINO IDE OPTIMIZATION NOTES                        โ•‘
 * โ• โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ฃ
 * โ•‘ This sketch leverages Arduino IDE's automatic PROGMEM handling for ESP8266:   โ•‘
 * โ•‘                                                                               โ•‘
 * โ•‘ โ€ข F() macro moves string literals to flash memory automatically               โ•‘
 * โ•‘ โ€ข FPSTR() macro accesses PROGMEM strings efficiently                          โ•‘
 * โ•‘ โ€ข Static const arrays declared with PROGMEM stay in flash                     โ•‘
 * โ•‘ โ€ข Reduces heap fragmentation by ~2KB for better stability                     โ•‘
 * โ•‘                                                                               โ•‘
 * โ•‘ Arduino IDE Tools โ†’ ESP8266 โ†’ Flash Size: "4MB (FS:2MB OTA:~1019KB)"          โ•‘
 * โ• โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ฃ
 * โ•‘                           LIBRARIES & CREDITS                                 โ•‘
 * โ• โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•ฃ
 * โ•‘                                                                               โ•‘
 * โ•‘ 1. Adafruit_GFX by Adafruit Industries (Limor Fried & contributors)           โ•‘
 * โ•‘    - Core graphics library providing primitives for displays and LEDs         โ•‘
 * โ•‘    - https://github.com/adafruit/Adafruit-GFX-Library                         โ•‘
 * โ•‘                                                                               โ•‘
 * โ•‘ 2. Adafruit_ST7735 by Adafruit Industries (Limor Fried & contributors)        โ•‘
 * โ•‘    - Hardware-specific display driver for ST7735-based TFT displays           โ•‘
 * โ•‘    - https://github.com/adafruit/Adafruit-ST7735-Library                      โ•‘
 * โ•‘                                                                               โ•‘
 * โ•‘ 3. SPI by Arduino                                                             โ•‘
 * โ•‘    - Core SPI communication library for high-speed device interfacing         โ•‘
 * โ•‘    - https://www.arduino.cc/en/reference/SPI                                  โ•‘
 * โ•‘                                                                               โ•‘
 * โ•‘ 4. ESP8266WiFi by ESP8266 Community (Ivan Grokhotkov & contributors)          โ•‘
 * โ•‘    - WiFi connectivity library for ESP8266 microcontroller                    โ•‘
 * โ•‘    - https://github.com/esp8266/Arduino/tree/master/libraries/ESP8266WiFi     โ•‘
 * โ•‘                                                                               โ•‘
 * โ•‘ 5. WiFiUdp by ESP8266 Community (Ivan Grokhotkov & contributors)              โ•‘
 * โ•‘    - UDP protocol support for ESP8266 WiFi networking                         โ•‘
 * โ•‘    - https://github.com/esp8266/Arduino/tree/master/libraries/ESP8266WiFi     โ•‘
 * โ•‘                                                                               โ•‘
 * โ•‘ 6. NTPClient by Fabrice Weinberg                                              โ•‘
 * โ•‘    - Network Time Protocol client for accurate time synchronization           โ•‘
 * โ•‘    - https://github.com/arduino-libraries/NTPClient                           โ•‘
 * โ•‘                                                                               โ•‘
 * โ•‘ 7. ESP8266 pgmspace (part of ESP8266 Arduino Core)                            โ•‘
 * โ•‘    - PROGMEM utilities for storing constants in flash memory on ESP8266       โ•‘
 * โ•‘    - https://github.com/esp8266/Arduino/tree/master/cores/esp8266             โ•‘
 * โ•‘                                                                               โ•‘
 * โ•‘ 8. user_interface by Espressif Systems                                        โ•‘
 * โ•‘    - ESP8266 system-level functions including CPU frequency control           โ•‘
 * โ•‘    - Part of ESP8266 Non-OS SDK                                               โ•‘
 * โ•‘                                                                               โ•‘
 * โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
 *
 */

/*
Hardware Check:
LOLIN/WeMos/NodeMCU D1 R1 + KMR 1.8inch ST7735 LCD Module (128x160)
*/

#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>  // For ST7735 display
#include <SPI.h>
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include <NTPClient.h>
#include <pgmspace.h>  // ESP8266 PROGMEM support for PSTR(), FPSTR(), F() macros and _P functions
#include <user_interface.h>  // For system_update_cpu_freq

// WiFi credentials
const char* ssid     = "YourWiFiSSID";
const char* password = "YourPassword";

// Game of Life variables (exposed for manual setting)
constexpr int16_t RENDER_WIDTH         = 64;   // Internal render resolution (half for performance)
constexpr int16_t RENDER_HEIGHT        = 80;
constexpr uint32_t GENERATION_INTERVAL = 100;  // ms between generations
constexpr uint8_t INITIAL_ALIVE_CHANCE = 25;   // % chance for cell to be alive initially
constexpr int16_t MIN_ALIVE_CELLS      = 50;   // Minimum alive cells before reseeding

// Pin definitions for WeMos D1 R1
constexpr uint8_t TFT_CS   = D2;  // GPIO4
constexpr uint8_t TFT_RST  = D3;  // GPIO0
constexpr uint8_t TFT_DC   = D4;  // GPIO2
constexpr uint8_t TFT_MOSI = D7;  // GPIO13
constexpr uint8_t TFT_SCLK = D5;  // GPIO14

// Color definitions (16-bit RGB565)
constexpr uint16_t BLACK   = 0x0000;
constexpr uint16_t WHITE   = 0xFFFF;
constexpr uint16_t RED     = 0xF800;
constexpr uint16_t YELLOW  = 0xFFE0;
constexpr uint16_t ORANGE  = 0xFBE0;
constexpr uint16_t CYAN    = 0x07FF;
constexpr uint16_t GREEN   = 0x07E0;
constexpr uint16_t BLUE    = 0x001F;
constexpr uint16_t MAGENTA = 0xF81F;

// Day/Night theme colors
constexpr uint16_t NIGHT_BG = BLACK;
constexpr uint16_t NIGHT_FG = WHITE;
constexpr uint16_t DAY_BG   = WHITE;
constexpr uint16_t DAY_FG   = BLACK;

// Background color cycle for night theme (dark mode)
constexpr uint16_t NIGHT_BG_COLORS[] = {BLACK, 0x0841, 0x1082, 0x0020, 0x4000, 0x2000};
// Background color cycle for day theme (light mode)
constexpr uint16_t DAY_BG_COLORS[]   = {WHITE, 0xF7BE, 0xEF7D, 0xF7DF, 0xFBFF, 0xFDFF};
constexpr uint8_t  BG_COLOR_COUNT    = sizeof(NIGHT_BG_COLORS) / sizeof(NIGHT_BG_COLORS[0]);

// Alive cell color cycle (for GOL mode)
constexpr uint16_t ALIVE_COLORS[]   = {RED, YELLOW, ORANGE, CYAN, GREEN, BLUE, MAGENTA};
constexpr uint8_t ALIVE_COLOR_COUNT = sizeof(ALIVE_COLORS) / sizeof(ALIVE_COLORS[0]);

// Day hours definition (6 AM to 6 PM)
constexpr uint8_t DAY_START_HOUR = 6;
constexpr uint8_t DAY_END_HOUR   = 18;

// Display constants - Half resolution internal, 2x scaling
constexpr int16_t SCREEN_WIDTH  = 128;   // Actual screen size
constexpr int16_t SCREEN_HEIGHT = 160;
constexpr int16_t SCALE_FACTOR  = 2;     // 2x scaling
constexpr int16_t OFFSET        = (SCREEN_WIDTH - RENDER_WIDTH * SCALE_FACTOR) / 2;  // Center the rendered area (0 for native)

constexpr int16_t CLOCK_Y       = 10;         // Scaled coordinates

// Memory monitoring constants
constexpr uint32_t MEMORY_CHECK_INTERVAL = 30000; // Check every 30 seconds
constexpr uint16_t MIN_FREE_HEAP         = 1024;  // Minimum free heap before restart

// NTP and timing constants
constexpr uint32_t NTP_UPDATE_INTERVAL     = 600000;  // 10 minutes
constexpr uint32_t WIFI_CHECK_INTERVAL     = 60000;   // 60 seconds
constexpr uint32_t FRAME_DELAY             = 20;      // Balanced ~50 FPS
constexpr uint8_t  MAX_WIFI_ATTEMPTS       = 40;      // WiFi connection attempts
constexpr uint8_t  MAX_WIFI_RETRIES        = 5;       // WiFi retry limit before restart
constexpr uint16_t WIFI_ATTEMPT_DELAY      = 500;     // Delay between WiFi attempts
constexpr int16_t  WIFI_CONNECTION_TIMEOUT = 10;  // Seconds to wait for WiFi connection

// NTP server configuration - using PROGMEM to store in flash
static const char NTP_SERVER[] PROGMEM = "pool.ntp.org";
constexpr long NTP_OFFSET              = 19800;  // IST offset in seconds

// Memory thresholds
constexpr uint16_t LOW_MEMORY_THRESHOLD = 2048;   // Threshold for cleanup

// Default time constants
constexpr unsigned long DEFAULT_TIME_SECONDS = 43200; // 12:00 in seconds since midnight
constexpr unsigned long SECONDS_PER_DAY      = 86400L;
constexpr unsigned long SECONDS_PER_HOUR     = 3600;
constexpr unsigned long SECONDS_PER_MINUTE   = 60;

// Theme color mapping constants
constexpr uint16_t DARK_CYAN    = 0x03F8;
constexpr uint16_t DARK_MAGENTA = 0x780F;
constexpr uint16_t DARK_YELLOW  = 0x8400;
constexpr uint16_t DARK_RED     = 0x7800;
constexpr uint16_t DARK_ORANGE  = 0x7B00;
constexpr uint16_t DARK_GREEN   = 0x03E0;
constexpr uint16_t DARK_BLUE    = 0x000F;

// Pong game constants - All in scaled coordinates, proportionality maintained
constexpr int16_t PADDLE_HEIGHT = 16;  // Scaled down proportionally for half res (original native 32/160=0.2)
constexpr int16_t PADDLE_WIDTH  = 2;   // Scaled down (original 4/128~0.031)
constexpr int16_t BALL_SIZE     = 2;   // Scaled down (original 4)
constexpr int16_t MAX_PARTICLES = 8;   // Reduced for performance

// Ball physics constants
constexpr float MIN_BALL_VELOCITY       = 0.3f;
constexpr float MAX_BALL_VELOCITY       = 2.0f;
constexpr float MIN_AXIS_VELOCITY       = 0.5f;
constexpr float BALL_VELOCITY_VARIATION = 0.1f;  // Max velocity change on collision
constexpr float GRAVITY                 = 0.05f;

// Paddle AI constants
constexpr float AI_DISTANCE_THRESHOLD = 0.7f;  // Fraction of screen width for tracking
constexpr float MIN_PREDICTION_TIME   = 0.1f;
constexpr int16_t PADDLE_SPEED        = 3;

// Particle system constants
constexpr uint8_t PARTICLES_PER_COLLISION    = 4;
constexpr uint8_t MIN_PARTICLE_LIFE          = 10;
constexpr uint8_t MAX_PARTICLE_LIFE          = 20;
constexpr uint8_t PARTICLE_FADE_THRESHOLD    = 5;
constexpr uint16_t PARTICLE_MEMORY_THRESHOLD = 1500; // Minimum memory for particle creation

// Individual digit structure
struct ClockDigit {
  uint8_t value;
  uint8_t old_value;
  int16_t x, y;
  bool needs_update;
};

// Particle system
struct Particle {
  float x, y;
  float vx, vy;
  uint16_t color;
  uint8_t life;
  bool active;
};

// GOL specific state
struct GolSpecific {
  // Grid for Game of Life
  bool current_grid[RENDER_HEIGHT][RENDER_WIDTH];
  bool next_grid[RENDER_HEIGHT][RENDER_WIDTH];
  
  // Clock area bounds (in render coordinates)
  int16_t clock_min_x;
  int16_t clock_max_x;
  int16_t clock_min_y;
  int16_t clock_max_y;
  
  // Visual effects
  uint8_t alive_color_index = 0;
  
  // GoL timing
  uint32_t last_generation  = 0;
  
  // Rendering
  bool force_clock_redraw   = false;
};

// Pong specific state
struct PongSpecific {
  // Ball
  float ball_x       = RENDER_WIDTH / 2;
  float ball_y       = RENDER_HEIGHT / 2;
  float ball_vx      = 1.0f;
  float ball_vy      = 0.8f;
  int16_t old_ball_x = 0;
  int16_t old_ball_y = 0;
  
  // Paddles
  int16_t left_paddle_y       = RENDER_HEIGHT / 2;
  int16_t right_paddle_y      = RENDER_HEIGHT / 2;
  int16_t left_paddle_target  = RENDER_HEIGHT / 2;
  int16_t right_paddle_target = RENDER_HEIGHT / 2;
  int16_t old_left_paddle_y   = RENDER_HEIGHT / 2;
  int16_t old_right_paddle_y  = RENDER_HEIGHT / 2;
  
  // Visual effects
  Particle particles[MAX_PARTICLES];
};

// Common game state
struct CommonState {
  // Visual effects
  uint8_t bg_color_index = 0;
  
  // Clock digits (H1 H2 : M1 M2)
  ClockDigit digits[4];
  bool colon_visible          = true;
  bool old_colon_visible      = true;
  uint32_t last_second_update = 0;
  uint32_t last_ntp_update    = 0;
  uint32_t last_minute_update = 0;  // Track last minute for digit repair
  uint8_t last_hour           = 255; // Track last hour for mode switch
  
  // Theme state
  bool is_day_theme     = false;
  bool old_is_day_theme = true; // Force initial theme update
  
  // Memory management
  uint32_t last_memory_check = 0;
  uint16_t min_heap_seen     = 65535;
  
  // WiFi stability
  uint32_t last_wifi_check = 0;
  uint8_t wifi_retry_count = 0;
  bool ntp_sync_failed     = false;
  bool wifi_connected      = false;
};

// Initialize display
Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST);

WiFiUDP ntpUDP;
// NTPClient constructor: need to copy NTP_SERVER from PROGMEM to RAM for constructor
char ntp_server_buffer[20];
NTPClient timeClient(ntpUDP, "", NTP_OFFSET, NTP_UPDATE_INTERVAL); // Will set server in setup()

CommonState common;
GolSpecific gol;
PongSpecific pong;
bool is_gol_mode = true;  // Start with GOL

// Digit collision bounds and spacing (scaled coordinates)
// Adjusted for 2x scaling (text size 4 effective, digits ~12 wide)
constexpr uint8_t DIGIT_WIDTH       = 12;
constexpr uint8_t DIGIT_HEIGHT      = 16;
constexpr uint8_t DIGIT_SPACING     = 12;
constexpr uint8_t COLON_EXTRA_SPACE = 8; // Extra space after colon

// Text size calculation constants
constexpr uint8_t TEXT_SIZE_MULTIPLIER = 2; // Base text size multiplier (2 for 2x scale, effective size 4)
constexpr uint8_t TEXT_SIZE            = TEXT_SIZE_MULTIPLIER * SCALE_FACTOR;
constexpr int16_t DOT_SIZE             = TEXT_SIZE;
constexpr int16_t BOTTOM_DOT_OFFSET    = 2 * TEXT_SIZE;
constexpr int16_t TOP_DOT_OFFSET       = 5 * TEXT_SIZE;

// Serial and display configuration constants
constexpr uint32_t SERIAL_BAUD_RATE = 115200;
constexpr uint8_t DISPLAY_ROTATION  = 0; // Default portrait for ST7735 1.8"
constexpr uint8_t DISPLAY_WIDTH     = 128;
constexpr uint8_t DISPLAY_HEIGHT    = 160;

// PROGMEM String constants for ESP8266
static const char MSG_CONNECTING_WIFI[]   PROGMEM = "Connecting WiFi...";
static const char MSG_WIFI_CONNECTED[]    PROGMEM = "WiFi connected!";
static const char MSG_WIFI_FAILED[]       PROGMEM = "WiFi connection failed - continuing offline";
static const char MSG_NTP_FAILED[]        PROGMEM = "Initial NTP sync failed";
static const char MSG_NTP_UPDATE_FAILED[] PROGMEM = "NTP update failed";
static const char MSG_CRITICAL_MEMORY[]   PROGMEM = "Critical memory low - restarting";
static const char MSG_WIFI_RESTART[]      PROGMEM = "WiFi connection failed - restarting";
static const char MSG_INITIAL_HEAP[]      PROGMEM = "Initial free heap: ";
static const char MSG_DOT[]               PROGMEM = ".";

// Theme helper functions
uint16_t getCurrentBgColor() {
  return common.is_day_theme ? DAY_BG_COLORS[common.bg_color_index] : NIGHT_BG_COLORS[common.bg_color_index];
}

uint16_t getCurrentFgColor() {
  return common.is_day_theme ? DAY_FG : NIGHT_FG;
}

uint16_t getThemeColor(uint16_t night_color) {
  if (!common.is_day_theme) return night_color;
  
  // Convert night colors to day equivalents
  if (night_color == WHITE)   return BLACK;
  if (night_color == CYAN)    return DARK_CYAN;
  if (night_color == MAGENTA) return DARK_MAGENTA;
  if (night_color == YELLOW)  return DARK_YELLOW;
  if (night_color == RED)     return DARK_RED;
  if (night_color == ORANGE)  return DARK_ORANGE;
  if (night_color == GREEN)   return DARK_GREEN;
  if (night_color == BLUE)    return DARK_BLUE;
  
  return night_color; // Keep other colors as-is
}

bool isDayTime(uint8_t hour) {
  return (hour >= DAY_START_HOUR && hour < DAY_END_HOUR);
}

uint16_t getAliveColor() {
  return getThemeColor(ALIVE_COLORS[gol.alive_color_index]);
}

// Memory management functions
void checkMemoryHealth() {
  uint16_t free_heap = ESP.getFreeHeap();
  
  if (free_heap < common.min_heap_seen) {
    common.min_heap_seen = free_heap;
  }
  
  // Force restart if memory is critically low
  if (free_heap < MIN_FREE_HEAP) {
    Serial.println(F("Critical memory low - restarting"));
    ESP.restart();
  }
  
  // Periodic cleanup if needed
  if (free_heap < LOW_MEMORY_THRESHOLD) {
    if (!is_gol_mode) {
      // Clear all particles in Pong mode
      for (int i = 0; i < MAX_PARTICLES; i++) {
        pong.particles[i].active = false;
      }
    }
  }
}

// WiFi stability functions
bool checkWiFiConnection() {
  if (WiFi.status() != WL_CONNECTED) {
    common.wifi_retry_count++;
    
    if (common.wifi_retry_count > MAX_WIFI_RETRIES) {
      Serial.println(FPSTR(MSG_WIFI_RESTART));
      ESP.restart();
    }
    
    WiFi.disconnect();
    WiFi.begin(ssid, password);
    
    int attempts = 0;
    while (WiFi.status() != WL_CONNECTED && attempts < (WIFI_CONNECTION_TIMEOUT * 2)) {
      delay(WIFI_ATTEMPT_DELAY);
      attempts++;
    }
    
    if (WiFi.status() == WL_CONNECTED) {
      common.wifi_retry_count = 0;
      return true;
    }
    return false;
  }
  
  common.wifi_retry_count = 0;
  return true;
}

void setup() {
  system_update_cpu_freq(SYS_CPU_160MHZ);  // Overclock to 160MHz for better performance
  
  Serial.begin(SERIAL_BAUD_RATE);
  
  strcpy_P(ntp_server_buffer, NTP_SERVER);
  timeClient.setPoolServerName(ntp_server_buffer);
  
  tft.initR(INITR_BLACKTAB);  // Initialize ST7735 with black tab (for KMR 1.8)
  tft.setRotation(DISPLAY_ROTATION);
  
  // Start with night theme for initial screen
  common.is_day_theme = false;
  
  // Proceed to WiFi connection
  tft.fillScreen(getCurrentBgColor());
  tft.setTextColor(getCurrentFgColor());
  tft.setTextSize(1);
  tft.setCursor(5, 50);
  tft.print(FPSTR(MSG_CONNECTING_WIFI));
  
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  
  int wifi_attempts = 0;
  while (WiFi.status() != WL_CONNECTED && wifi_attempts < MAX_WIFI_ATTEMPTS) {
    delay(WIFI_ATTEMPT_DELAY);
    wifi_attempts++;
    Serial.print(FPSTR(MSG_DOT));
  }
  
  if (WiFi.status() == WL_CONNECTED) {
    Serial.println(FPSTR(MSG_WIFI_CONNECTED));
    timeClient.begin();
    if (!timeClient.update()) {
      Serial.println(FPSTR(MSG_NTP_FAILED));
      common.ntp_sync_failed = true;
    }
    common.wifi_connected = true;
  } else {
    Serial.println(FPSTR(MSG_WIFI_FAILED));
    common.wifi_connected = false;
  }
  
  initializeCommon();
  initializeDigits();
  initializeGol();  // Start with GOL
  
  tft.fillScreen(getCurrentBgColor());
  renderFullGol();
  
  randomSeed(WiFi.RSSI() * analogRead(A0));
  
  Serial.print(FPSTR(MSG_INITIAL_HEAP));
  Serial.println(ESP.getFreeHeap());
}

void loop() {
  uint32_t now = millis();
  
  if (now - common.last_memory_check > MEMORY_CHECK_INTERVAL) {
    common.last_memory_check = now;
    checkMemoryHealth();
  }
  
  if (now - common.last_wifi_check > WIFI_CHECK_INTERVAL) {
    common.last_wifi_check = now;
    checkWiFiConnection();
    common.wifi_connected = (WiFi.status() == WL_CONNECTED);
  }
  
  updateTime(now);
  
  if (is_gol_mode) {
    gol.force_clock_redraw = false;
    if (now - gol.last_generation >= GENERATION_INTERVAL) {
      computeGol();
      gol.last_generation = now;
      if (countAlive() < MIN_ALIVE_CELLS) {
        initializeGol();
        renderFullGol();
      }
    }
  } else {
    updateBall();
    updatePaddles();
    updateParticles();
  }
  
  renderClock();
  if (is_gol_mode) {
    // No additional render needed as computeGol handles changes
  } else {
    renderGamePong();
  }
  
  delay(FRAME_DELAY);
  
  yield();
}

void initializeCommon() {
  // Common initialization
  common.bg_color_index = 0;
  common.colon_visible = true;
  common.old_colon_visible = true;
  common.last_second_update = 0;
  common.last_ntp_update = 0;
  common.last_minute_update = 0;
  common.last_hour = 255;
  common.is_day_theme = false;
  common.old_is_day_theme = true;
  common.last_memory_check = 0;
  common.min_heap_seen = 65535;
  common.last_wifi_check = 0;
  common.wifi_retry_count = 0;
  common.ntp_sync_failed = false;
  common.wifi_connected = false;
}

void addGlider(int y, int x) {
  if (x < 0 || x >= RENDER_WIDTH - 2 || y < 0 || y >= RENDER_HEIGHT - 2) return;
  gol.current_grid[y][x+1] = true;
  gol.current_grid[y+1][x+2] = true;
  gol.current_grid[y+2][x] = true;
  gol.current_grid[y+2][x+1] = true;
  gol.current_grid[y+2][x+2] = true;
}

void addBlinker(int y, int x) {
  if (x < 0 || x >= RENDER_WIDTH - 2 || y < 0 || y >= RENDER_HEIGHT) return;
  gol.current_grid[y][x] = true;
  gol.current_grid[y][x+1] = true;
  gol.current_grid[y][x+2] = true;
}

void initializeGol() {
  // Clear grid
  memset(gol.current_grid, 0, sizeof(gol.current_grid));

  // Add random patterns for perpetual interest
  for (int i = 0; i < 10; i++) {
    int rx = random(0, RENDER_WIDTH - 3);
    int ry = random(0, RENDER_HEIGHT - 3);
    addGlider(ry, rx);
  }
  for (int i = 0; i < 5; i++) {
    int rx = random(0, RENDER_WIDTH - 3);
    int ry = random(0, RENDER_HEIGHT - 1);
    addBlinker(ry, rx);
  }

  // Add some random cells for variety
  for (int y = 0; y < RENDER_HEIGHT; y++) {
    for (int x = 0; x < RENDER_WIDTH; x++) {
      if (random(0, 100) < INITIAL_ALIVE_CHANCE / 2) {  // Reduced random density since patterns added
        gol.current_grid[y][x] = true;
      }
    }
  }
  gol.alive_color_index = 0;
  gol.last_generation = 0;
  gol.force_clock_redraw = false;
  
  // Clock bounds
  gol.clock_min_x = common.digits[0].x - 1;
  gol.clock_max_x = common.digits[3].x + DIGIT_WIDTH + 1;
  gol.clock_min_y = CLOCK_Y - 1;
  gol.clock_max_y = CLOCK_Y + DIGIT_HEIGHT + 1;
}

void initializePong() {
  // Initialize ball with momentum
  pong.ball_x = RENDER_WIDTH / 2;
  pong.ball_y = RENDER_HEIGHT / 2;
  pong.ball_vx = random(-100, 100) / 100.0f;
  pong.ball_vy = random(50, 150) / 100.0f;
  pong.old_ball_x = 0;
  pong.old_ball_y = 0;

  // Initialize paddles
  pong.left_paddle_y = RENDER_HEIGHT / 2;
  pong.right_paddle_y = RENDER_HEIGHT / 2;
  pong.left_paddle_target = RENDER_HEIGHT / 2;
  pong.right_paddle_target = RENDER_HEIGHT / 2;
  pong.old_left_paddle_y = RENDER_HEIGHT / 2;
  pong.old_right_paddle_y = RENDER_HEIGHT / 2;
  
  // Clear particles
  for (int i = 0; i < MAX_PARTICLES; i++) {
    pong.particles[i].active = false;
  }
}

void initializeDigits() {
  constexpr int16_t total_clock_width = 3 * DIGIT_SPACING + DIGIT_WIDTH + COLON_EXTRA_SPACE;
  constexpr int16_t start_x = (RENDER_WIDTH - total_clock_width) / 2;
  
  for (int i = 0; i < 4; i++) {
    common.digits[i].x = start_x + i * DIGIT_SPACING + (i >= 2 ? COLON_EXTRA_SPACE : 0);
    common.digits[i].y = CLOCK_Y;
    common.digits[i].value = 255; // Force initial update
    common.digits[i].old_value = 0;
    common.digits[i].needs_update = true;
  }
}

void updateTime(uint32_t now) {
  if (now - common.last_ntp_update > NTP_UPDATE_INTERVAL) {
    common.last_ntp_update = now;
    
    if (WiFi.status() == WL_CONNECTED) {
      if (!timeClient.update()) {
        Serial.println(F("NTP update failed"));
        common.ntp_sync_failed = true;
      } else {
        common.ntp_sync_failed = false;
      }
    }
  }
  
  if (now - common.last_second_update >= 1000) {
    common.last_second_update = now;
    
    common.wifi_connected = (WiFi.status() == WL_CONNECTED);
    
    static unsigned long last_known_tod = DEFAULT_TIME_SECONDS;
    static unsigned long last_sync_millis = 0;
    
    unsigned long epochTime;
    bool use_fallback = common.ntp_sync_failed || !common.wifi_connected;
    if (use_fallback) {
      if (last_sync_millis == 0) {
        last_sync_millis = millis();
      }
      unsigned long elapsed_seconds = (millis() - last_sync_millis) / 1000;
      epochTime = last_known_tod + elapsed_seconds;
      epochTime %= SECONDS_PER_DAY;
    } else {
      epochTime = timeClient.getEpochTime();
      last_known_tod = epochTime % SECONDS_PER_DAY;
      last_sync_millis = millis();
      epochTime = last_known_tod;
    }
    
    uint8_t hours = (epochTime % SECONDS_PER_DAY) / SECONDS_PER_HOUR;
    uint8_t minutes = (epochTime % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE;
    
    // Check for hour change - trigger mode switch
    if (hours != common.last_hour) {
      common.last_hour = hours;
      is_gol_mode = !is_gol_mode;  // Toggle mode
      tft.fillScreen(getCurrentBgColor());  // Clear screen for new mode
      if (is_gol_mode) {
        initializeGol();
        renderFullGol();
      } else {
        initializePong();
        renderFullPong();
      }
      // Force clock redraw after mode switch
      for (int i = 0; i < 4; i++) {
        common.digits[i].needs_update = true;
      }
      common.old_colon_visible = !common.colon_visible;
    }
    
    // Force refresh all digits every minute
    uint32_t current_minute = epochTime / SECONDS_PER_MINUTE;
    if (current_minute != common.last_minute_update) {
      common.last_minute_update = current_minute;
      for (int i = 0; i < 4; i++) {
        common.digits[i].needs_update = true;
      }
    }
    
    // Update theme based on time
    bool new_is_day = isDayTime(hours);
    if (new_is_day != common.is_day_theme) {
      common.old_is_day_theme = common.is_day_theme;
      common.is_day_theme = new_is_day;
      tft.fillScreen(getCurrentBgColor());
      if (is_gol_mode) {
        renderFullGol();
      } else {
        renderFullPong();
      }
      for (int i = 0; i < 4; i++) {
        common.digits[i].needs_update = true;
      }
    }
    
    // Update individual digits only if changed
    updateDigit(0, hours / 10);
    updateDigit(1, hours % 10);
    updateDigit(2, minutes / 10);
    updateDigit(3, minutes % 10);
    
    // Update colon
    common.old_colon_visible = common.colon_visible;
    common.colon_visible = !common.colon_visible;
  }
}

void updateDigit(uint8_t index, uint8_t new_value) {
  if (common.digits[index].value != new_value) {
    common.digits[index].old_value = common.digits[index].value;
    common.digits[index].value = new_value;
    common.digits[index].needs_update = true;
  }
}

int countAlive() {
  int count = 0;
  for (int y = 0; y < RENDER_HEIGHT; y++) {
    for (int x = 0; x < RENDER_WIDTH; x++) {
      if (gol.current_grid[y][x]) count++;
    }
  }
  return count;
}

int countNeighbors(int y, int x) {
  int count = 0;
  for (int dy = -1; dy <= 1; dy++) {
    for (int dx = -1; dx <= 1; dx++) {
      if (dy == 0 && dx == 0) continue;
      int ny = (y + dy + RENDER_HEIGHT) % RENDER_HEIGHT;
      int nx = (x + dx + RENDER_WIDTH) % RENDER_WIDTH;
      if (gol.current_grid[ny][nx]) count++;
    }
  }
  return count;
}

void computeGol() {
  for (int y = 0; y < RENDER_HEIGHT; y++) {
    for (int x = 0; x < RENDER_WIDTH; x++) {
      int neighbors = countNeighbors(y, x);
      bool alive = gol.current_grid[y][x];
      gol.next_grid[y][x] = (alive && (neighbors == 2 || neighbors == 3)) || (!alive && neighbors == 3);
    }
  }
  
  uint16_t bg_color = getCurrentBgColor();
  uint16_t alive_color = getAliveColor();
  
  for (int y = 0; y < RENDER_HEIGHT; y++) {
    for (int x = 0; x < RENDER_WIDTH; x++) {
      if (gol.next_grid[y][x] != gol.current_grid[y][x]) {
        // Skip drawing in clock area to avoid overwriting clock
        if (y >= gol.clock_min_y && y < gol.clock_max_y &&
            x >= gol.clock_min_x && x < gol.clock_max_x) continue;
        
        uint16_t color = gol.next_grid[y][x] ? alive_color : bg_color;
        tft.fillRect(OFFSET + x * SCALE_FACTOR, OFFSET + y * SCALE_FACTOR, SCALE_FACTOR, SCALE_FACTOR, color);
      }
    }
  }
  
  memcpy(gol.current_grid, gol.next_grid, sizeof(gol.next_grid));
}

void renderFullGol() {
  uint16_t bg_color = getCurrentBgColor();
  uint16_t alive_color = getAliveColor();
  
  for (int y = 0; y < RENDER_HEIGHT; y++) {
    for (int x = 0; x < RENDER_WIDTH; x++) {
      // Skip drawing in clock area to avoid overwriting clock
      if (y >= gol.clock_min_y && y < gol.clock_max_y &&
          x >= gol.clock_min_x && x < gol.clock_max_x) continue;
      
      uint16_t color = gol.current_grid[y][x] ? alive_color : bg_color;
      tft.fillRect(OFFSET + x * SCALE_FACTOR, OFFSET + y * SCALE_FACTOR, SCALE_FACTOR, SCALE_FACTOR, color);
    }
  }
}

void updateBall() {
  // Store old position
  pong.old_ball_x = (int16_t)pong.ball_x;
  pong.old_ball_y = (int16_t)pong.ball_y;
  
  // Update position
  pong.ball_x += pong.ball_vx;
  pong.ball_y += pong.ball_vy;
  
  // Screen boundary collisions
  if (pong.ball_x <= 0 || pong.ball_x >= RENDER_WIDTH - BALL_SIZE) {
    pong.ball_vx = -pong.ball_vx;
    pong.ball_x = constrain(pong.ball_x, 0, RENDER_WIDTH - BALL_SIZE);
    createParticles(pong.ball_x, pong.ball_y, getThemeColor(WHITE));
    varyBallBehavior();
  }
  
  if (pong.ball_y <= 0 || pong.ball_y >= RENDER_HEIGHT - BALL_SIZE) {
    pong.ball_vy = -pong.ball_vy;
    pong.ball_y = constrain(pong.ball_y, 0, RENDER_HEIGHT - BALL_SIZE);
    createParticles(pong.ball_x, pong.ball_y, getThemeColor(WHITE));
    varyBallBehavior();
  }
  
  // Paddle collisions
  checkPaddleCollisions();
  
  // Clock digit collisions
  checkDigitCollisions();
  
  // Reset if ball gets stuck
  if (abs(pong.ball_vx) < MIN_BALL_VELOCITY && abs(pong.ball_vy) < MIN_BALL_VELOCITY) {
    resetBall();
  }
}

void checkPaddleCollisions() {
  // Left paddle
  if (pong.ball_x <= PADDLE_WIDTH && 
      pong.ball_y + BALL_SIZE >= pong.left_paddle_y && 
      pong.ball_y <= pong.left_paddle_y + PADDLE_HEIGHT) {
    pong.ball_vx = abs(pong.ball_vx);
    pong.ball_x = PADDLE_WIDTH;
    createParticles(pong.ball_x, pong.ball_y, getThemeColor(CYAN));
    varyBallBehavior();
  }
  
  // Right paddle
  if (pong.ball_x + BALL_SIZE >= RENDER_WIDTH - PADDLE_WIDTH && 
      pong.ball_y + BALL_SIZE >= pong.right_paddle_y && 
      pong.ball_y <= pong.right_paddle_y + PADDLE_HEIGHT) {
    pong.ball_vx = -abs(pong.ball_vx);
    pong.ball_x = RENDER_WIDTH - PADDLE_WIDTH - BALL_SIZE;
    createParticles(pong.ball_x, pong.ball_y, getThemeColor(MAGENTA));
    varyBallBehavior();
  }
}

void checkDigitCollisions() {
  for (int i = 0; i < 4; i++) {
    if (pong.ball_x + BALL_SIZE >= common.digits[i].x && 
        pong.ball_x <= common.digits[i].x + DIGIT_WIDTH &&
        pong.ball_y + BALL_SIZE >= common.digits[i].y && 
        pong.ball_y <= common.digits[i].y + DIGIT_HEIGHT) {
      
      // Determine collision side
      float center_x = common.digits[i].x + DIGIT_WIDTH / 2;
      float center_y = common.digits[i].y + DIGIT_HEIGHT / 2;
      
      if (abs(pong.ball_x + BALL_SIZE/2 - center_x) > abs(pong.ball_y + BALL_SIZE/2 - center_y)) {
        pong.ball_vx = -pong.ball_vx;
      } else {
        pong.ball_vy = -pong.ball_vy;
      }
      
      createParticles(pong.ball_x, pong.ball_y, getThemeColor(YELLOW));
      varyBallBehavior();
      break;
    }
  }
}

void updatePaddles() {
  // Store old positions
  pong.old_left_paddle_y = pong.left_paddle_y;
  pong.old_right_paddle_y = pong.right_paddle_y;
  
  // Improved AI - more aggressive tracking and prediction
  float ball_center_x = pong.ball_x + BALL_SIZE / 2;
  float ball_center_y = pong.ball_y + BALL_SIZE / 2;
  float distance_threshold = RENDER_WIDTH * AI_DISTANCE_THRESHOLD;
  
  // Left paddle AI
  if (pong.ball_vx < 0 && ball_center_x < distance_threshold) {
    float time_to_paddle = max(MIN_PREDICTION_TIME, (ball_center_x - PADDLE_WIDTH) / abs(pong.ball_vx));
    float predicted_y = pong.ball_y + pong.ball_vy * time_to_paddle;
    if (predicted_y < 0) predicted_y = -predicted_y;
    else if (predicted_y > RENDER_HEIGHT) predicted_y = 2 * RENDER_HEIGHT - predicted_y;
    pong.left_paddle_target = constrain((int16_t)predicted_y - PADDLE_HEIGHT/2, 0, RENDER_HEIGHT - PADDLE_HEIGHT);
  } else {
    pong.left_paddle_target = constrain((int16_t)ball_center_y - PADDLE_HEIGHT/2, 0, RENDER_HEIGHT - PADDLE_HEIGHT);
  }
  
  // Right paddle AI
  if (pong.ball_vx > 0 && ball_center_x > RENDER_WIDTH - distance_threshold) {
    float time_to_paddle = max(MIN_PREDICTION_TIME, (RENDER_WIDTH - PADDLE_WIDTH - ball_center_x) / abs(pong.ball_vx));
    float predicted_y = pong.ball_y + pong.ball_vy * time_to_paddle;
    if (predicted_y < 0) predicted_y = -predicted_y;
    else if (predicted_y > RENDER_HEIGHT) predicted_y = 2 * RENDER_HEIGHT - predicted_y;
    pong.right_paddle_target = constrain((int16_t)predicted_y - PADDLE_HEIGHT/2, 0, RENDER_HEIGHT - PADDLE_HEIGHT);
  } else {
    pong.right_paddle_target = constrain((int16_t)ball_center_y - PADDLE_HEIGHT/2, 0, RENDER_HEIGHT - PADDLE_HEIGHT);
  }
  
  // Paddle movement
  if (pong.left_paddle_y < pong.left_paddle_target) {
    pong.left_paddle_y = min((int16_t)(pong.left_paddle_y + PADDLE_SPEED), pong.left_paddle_target);
  } else if (pong.left_paddle_y > pong.left_paddle_target) {
    pong.left_paddle_y = max((int16_t)(pong.left_paddle_y - PADDLE_SPEED), pong.left_paddle_target);
  }
  
  if (pong.right_paddle_y < pong.right_paddle_target) {
    pong.right_paddle_y = min((int16_t)(pong.right_paddle_y + PADDLE_SPEED), pong.right_paddle_target);
  } else if (pong.right_paddle_y > pong.right_paddle_target) {
    pong.right_paddle_y = max((int16_t)(pong.right_paddle_y - PADDLE_SPEED), pong.right_paddle_target);
  }
}

void updateParticles() {
  for (int i = 0; i < MAX_PARTICLES; i++) {
    if (pong.particles[i].active) {
      // Clear old position
      tft.fillRect((int16_t)pong.particles[i].x * SCALE_FACTOR, 
                   (int16_t)pong.particles[i].y * SCALE_FACTOR, 
                   SCALE_FACTOR, SCALE_FACTOR, getCurrentBgColor());
      
      // Update position
      pong.particles[i].x += pong.particles[i].vx;
      pong.particles[i].y += pong.particles[i].vy;
      pong.particles[i].vy += GRAVITY;
      pong.particles[i].life--;
      
      // Check bounds and lifetime
      if (pong.particles[i].life <= 0 || 
          pong.particles[i].x < 0 || pong.particles[i].x >= RENDER_WIDTH ||
          pong.particles[i].y < 0 || pong.particles[i].y >= RENDER_HEIGHT) {
        pong.particles[i].active = false;
      } else {
        // Draw particle with fade
        uint16_t alpha_color = pong.particles[i].color;
        if (pong.particles[i].life < PARTICLE_FADE_THRESHOLD) {
          alpha_color = (alpha_color & 0xE79C) >> 1;
        }
        tft.fillRect((int16_t)pong.particles[i].x * SCALE_FACTOR, 
                     (int16_t)pong.particles[i].y * SCALE_FACTOR, 
                     SCALE_FACTOR, SCALE_FACTOR, alpha_color);
      }
    }
  }
}

void createParticles(float x, float y, uint16_t color) {
  if (ESP.getFreeHeap() < PARTICLE_MEMORY_THRESHOLD) return;
  
  for (int i = 0; i < PARTICLES_PER_COLLISION; i++) {
    for (int j = 0; j < MAX_PARTICLES; j++) {
      if (!pong.particles[j].active) {
        pong.particles[j].x = x + BALL_SIZE/2;
        pong.particles[j].y = y + BALL_SIZE/2;
        pong.particles[j].vx = (random(-100, 100) / 100.0f);
        pong.particles[j].vy = (random(-100, 50) / 100.0f);
        pong.particles[j].color = color;
        pong.particles[j].life = random(MIN_PARTICLE_LIFE, MAX_PARTICLE_LIFE);
        pong.particles[j].active = true;
        break;
      }
    }
  }
}

void varyBallBehavior() {
  pong.ball_vx += (random(-10, 10) / 100.0f) * BALL_VELOCITY_VARIATION;
  pong.ball_vy += (random(-10, 10) / 100.0f) * BALL_VELOCITY_VARIATION;
  
  pong.ball_vx = constrain(pong.ball_vx, -MAX_BALL_VELOCITY, MAX_BALL_VELOCITY);
  pong.ball_vy = constrain(pong.ball_vy, -MAX_BALL_VELOCITY, MAX_BALL_VELOCITY);
  
  if (abs(pong.ball_vx) < MIN_AXIS_VELOCITY) pong.ball_vx = pong.ball_vx > 0 ? MIN_AXIS_VELOCITY : -MIN_AXIS_VELOCITY;
  if (abs(pong.ball_vy) < MIN_AXIS_VELOCITY) pong.ball_vy = pong.ball_vy > 0 ? MIN_AXIS_VELOCITY : -MIN_AXIS_VELOCITY;
}

void resetBall() {
  initializePong();
  common.bg_color_index = (common.bg_color_index + 1) % BG_COLOR_COUNT;
  tft.fillScreen(getCurrentBgColor());
  
  // Force redraw all digits
  for (int i = 0; i < 4; i++) {
    common.digits[i].needs_update = true;
  }
}

void renderFullPong() {
  // For Pong, full render is just background + initial elements (handled in renderGamePong after init)
  renderGamePong();
}

void renderGamePong() {
  // Clear old ball position
  if (pong.old_ball_x != (int16_t)pong.ball_x || pong.old_ball_y != (int16_t)pong.ball_y) {
    tft.fillRect(pong.old_ball_x * SCALE_FACTOR, pong.old_ball_y * SCALE_FACTOR, 
                 BALL_SIZE * SCALE_FACTOR, BALL_SIZE * SCALE_FACTOR, 
                 getCurrentBgColor());
    
    tft.fillRect((int16_t)pong.ball_x * SCALE_FACTOR, (int16_t)pong.ball_y * SCALE_FACTOR, 
                 BALL_SIZE * SCALE_FACTOR, BALL_SIZE * SCALE_FACTOR, getCurrentFgColor());
  }
  
  // Update left paddle if moved - clear only the differing parts to reduce flicker
  if (pong.old_left_paddle_y != pong.left_paddle_y) {
    if (pong.left_paddle_y > pong.old_left_paddle_y) {
      // Clear top part
      tft.fillRect(0, pong.old_left_paddle_y * SCALE_FACTOR, 
                   PADDLE_WIDTH * SCALE_FACTOR, (pong.left_paddle_y - pong.old_left_paddle_y) * SCALE_FACTOR, 
                   getCurrentBgColor());
      // Draw bottom extension
      tft.fillRect(0, (pong.old_left_paddle_y + PADDLE_HEIGHT) * SCALE_FACTOR, 
                   PADDLE_WIDTH * SCALE_FACTOR, (pong.left_paddle_y - pong.old_left_paddle_y) * SCALE_FACTOR, 
                   getThemeColor(CYAN));
    } else {
      // Clear bottom part
      tft.fillRect(0, pong.left_paddle_y * SCALE_FACTOR + PADDLE_HEIGHT * SCALE_FACTOR, 
                   PADDLE_WIDTH * SCALE_FACTOR, (pong.old_left_paddle_y - pong.left_paddle_y) * SCALE_FACTOR, 
                   getCurrentBgColor());
      // Draw top extension
      tft.fillRect(0, pong.left_paddle_y * SCALE_FACTOR, 
                   PADDLE_WIDTH * SCALE_FACTOR, (pong.old_left_paddle_y - pong.left_paddle_y) * SCALE_FACTOR, 
                   getThemeColor(CYAN));
    }
  }
  
  // Update right paddle if moved - clear only the differing parts to reduce flicker
  if (pong.old_right_paddle_y != pong.right_paddle_y) {
    if (pong.right_paddle_y > pong.old_right_paddle_y) {
      // Clear top part
      tft.fillRect((RENDER_WIDTH - PADDLE_WIDTH) * SCALE_FACTOR, 
                   pong.old_right_paddle_y * SCALE_FACTOR, 
                   PADDLE_WIDTH * SCALE_FACTOR, (pong.right_paddle_y - pong.old_right_paddle_y) * SCALE_FACTOR, 
                   getCurrentBgColor());
      // Draw bottom extension
      tft.fillRect((RENDER_WIDTH - PADDLE_WIDTH) * SCALE_FACTOR, 
                   (pong.old_right_paddle_y + PADDLE_HEIGHT) * SCALE_FACTOR, 
                   PADDLE_WIDTH * SCALE_FACTOR, (pong.right_paddle_y - pong.old_right_paddle_y) * SCALE_FACTOR, 
                   getThemeColor(MAGENTA));
    } else {
      // Clear bottom part
      tft.fillRect((RENDER_WIDTH - PADDLE_WIDTH) * SCALE_FACTOR, 
                   pong.right_paddle_y * SCALE_FACTOR + PADDLE_HEIGHT * SCALE_FACTOR, 
                   PADDLE_WIDTH * SCALE_FACTOR, (pong.old_right_paddle_y - pong.right_paddle_y) * SCALE_FACTOR, 
                   getCurrentBgColor());
      // Draw top extension
      tft.fillRect((RENDER_WIDTH - PADDLE_WIDTH) * SCALE_FACTOR, 
                   pong.right_paddle_y * SCALE_FACTOR, 
                   PADDLE_WIDTH * SCALE_FACTOR, (pong.old_right_paddle_y - pong.right_paddle_y) * SCALE_FACTOR, 
                   getThemeColor(MAGENTA));
    }
  }
}

void renderClock() {
  if (is_gol_mode && gol.force_clock_redraw) {
    for (int i = 0; i < 4; i++) {
      common.digits[i].needs_update = true;
    }
    common.old_colon_visible = !common.colon_visible; // Force colon redraw
  }
  
  // Update digits that need it
  for (int i = 0; i < 4; i++) {
    if (common.digits[i].needs_update) {
      tft.fillRect(OFFSET + common.digits[i].x * SCALE_FACTOR, 
                   OFFSET + common.digits[i].y * SCALE_FACTOR, 
                   DIGIT_WIDTH * SCALE_FACTOR, 
                   DIGIT_HEIGHT * SCALE_FACTOR, 
                   getCurrentBgColor());
      
      tft.setTextColor(getCurrentFgColor());
      tft.setTextSize(TEXT_SIZE);
      tft.setCursor(OFFSET + common.digits[i].x * SCALE_FACTOR, OFFSET + common.digits[i].y * SCALE_FACTOR);
      tft.print(common.digits[i].value);
      
      common.digits[i].needs_update = false;
    }
  }
  
  // Update colon if changed or forced
  if (common.colon_visible != common.old_colon_visible) {
    uint16_t colon_color = common.colon_visible ? (common.wifi_connected ? getCurrentFgColor() : RED) : getCurrentBgColor();

    int16_t digit_y_pixel = OFFSET + common.digits[0].y * SCALE_FACTOR;
    int16_t colon_center_x_pixel = ((common.digits[1].x + DIGIT_WIDTH) * SCALE_FACTOR + common.digits[2].x * SCALE_FACTOR) / 2;
    int16_t dot_x_pixel = colon_center_x_pixel - (DOT_SIZE / 2);
    int16_t bottom_dot_y_pixel = digit_y_pixel + BOTTOM_DOT_OFFSET;
    int16_t top_dot_y_pixel = digit_y_pixel + TOP_DOT_OFFSET;

    tft.fillRect(dot_x_pixel, bottom_dot_y_pixel, DOT_SIZE, DOT_SIZE, colon_color);
    tft.fillRect(dot_x_pixel, top_dot_y_pixel, DOT_SIZE, DOT_SIZE, colon_color);

    common.old_colon_visible = common.colon_visible;
  }
}

27/08/25 Edit:
Here is the 1.4 128x128 SPI TFT GREENTAB standard version of the same sketch:

#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>  // For ST7735 BLACKTAB display
#include <SPI.h>
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include <NTPClient.h>
#include <pgmspace.h>  // ESP8266 PROGMEM support for PSTR(), FPSTR(), F() macros and _P functions
#include <user_interface.h>  // For system_update_cpu_freq

// WiFi credentials
const char* ssid     = "YourWifiSSID";
const char* password = "YourPassword";

// Game of Life variables - Optimized for 128x128 with native resolution
constexpr int16_t RENDER_WIDTH         = 128;  // Native resolution for better performance
constexpr int16_t RENDER_HEIGHT        = 128;  // Square display
constexpr uint32_t GENERATION_INTERVAL = 80;   // Faster generations for smoother animation
constexpr uint8_t INITIAL_ALIVE_CHANCE = 20;   // Reduced for better performance
constexpr int16_t MIN_ALIVE_CELLS      = 40;   // Adjusted for smaller grid

// Pin definitions for WeMos D1 R1
constexpr uint8_t TFT_CS   = D2;  // GPIO4
constexpr uint8_t TFT_RST  = D3;  // GPIO0
constexpr uint8_t TFT_DC   = D4;  // GPIO2

// Color definitions (16-bit RGB565)
constexpr uint16_t BLACK   = 0x0000;
constexpr uint16_t WHITE   = 0xFFFF;
constexpr uint16_t RED     = 0xF800;
constexpr uint16_t YELLOW  = 0xFFE0;
constexpr uint16_t ORANGE  = 0xFBE0;
constexpr uint16_t CYAN    = 0x07FF;
constexpr uint16_t GREEN   = 0x07E0;
constexpr uint16_t BLUE    = 0x001F;
constexpr uint16_t MAGENTA = 0xF81F;

// Day/Night theme colors
constexpr uint16_t NIGHT_BG = BLACK;
constexpr uint16_t NIGHT_FG = WHITE;
constexpr uint16_t DAY_BG   = WHITE;
constexpr uint16_t DAY_FG   = BLACK;

// Background color cycle for night theme
constexpr uint16_t NIGHT_BG_COLORS[] = {BLACK, 0x0841, 0x1082, 0x0020, 0x4000, 0x2000};
// Background color cycle for day theme (light variants)
constexpr uint16_t DAY_BG_COLORS[]   = {WHITE, 0xF7BE, 0xEF7D, 0xF7DF, 0xFBFF, 0xFDFF};
constexpr uint8_t  BG_COLOR_COUNT    = sizeof(NIGHT_BG_COLORS) / sizeof(NIGHT_BG_COLORS[0]);

// Alive cell color cycle (for GOL mode)
constexpr uint16_t ALIVE_COLORS[]   = {RED, YELLOW, ORANGE, CYAN, GREEN, BLUE, MAGENTA};
constexpr uint8_t ALIVE_COLOR_COUNT = sizeof(ALIVE_COLORS) / sizeof(ALIVE_COLORS[0]);

// Day hours definition (6 AM to 6 PM)
constexpr uint8_t DAY_START_HOUR = 6;
constexpr uint8_t DAY_END_HOUR   = 18;

// Display constants - Native resolution, no scaling
constexpr int16_t SCREEN_WIDTH  = 128;   // Native screen size
constexpr int16_t SCREEN_HEIGHT = 128;   // Native screen size (square)
constexpr int16_t SCALE_FACTOR  = 1;     // No scaling for maximum performance
constexpr int16_t OFFSET        = 0;     // No offset needed

constexpr int16_t CLOCK_Y       = 10;    // Clock position

// Memory monitoring constants
constexpr uint32_t MEMORY_CHECK_INTERVAL = 30000; // Check every 30 seconds
constexpr uint16_t MIN_FREE_HEAP         = 1024;  // Minimum free heap before restart

// NTP and timing constants
constexpr uint32_t NTP_UPDATE_INTERVAL     = 600000;  // 10 minutes
constexpr uint32_t WIFI_CHECK_INTERVAL     = 60000;   // 60 seconds
constexpr uint32_t FRAME_DELAY             = 16;      // ~60 FPS for smoother animation
constexpr uint8_t  MAX_WIFI_ATTEMPTS       = 40;      // WiFi connection attempts
constexpr uint8_t  MAX_WIFI_RETRIES        = 5;       // WiFi retry limit before restart
constexpr uint16_t WIFI_ATTEMPT_DELAY      = 500;     // Delay between WiFi attempts
constexpr int16_t  WIFI_CONNECTION_TIMEOUT = 10;      // Seconds to wait for WiFi connection

// NTP server configuration - using PROGMEM to store in flash
static const char NTP_SERVER[] PROGMEM = "pool.ntp.org";
constexpr long NTP_OFFSET              = 19800;  // IST offset in seconds

// Memory thresholds
constexpr uint16_t LOW_MEMORY_THRESHOLD = 2048;   // Threshold for cleanup

// Default time constants
constexpr unsigned long DEFAULT_TIME_SECONDS = 43200; // 12:00 in seconds since midnight
constexpr unsigned long SECONDS_PER_DAY      = 86400L;
constexpr unsigned long SECONDS_PER_HOUR     = 3600;
constexpr unsigned long SECONDS_PER_MINUTE   = 60;

// Theme color mapping constants
constexpr uint16_t DARK_CYAN    = 0x03F8;
constexpr uint16_t DARK_MAGENTA = 0x780F;
constexpr uint16_t DARK_YELLOW  = 0x8400;
constexpr uint16_t DARK_RED     = 0x7800;
constexpr uint16_t DARK_ORANGE  = 0x7B00;
constexpr uint16_t DARK_GREEN   = 0x03E0;
constexpr uint16_t DARK_BLUE    = 0x000F;

// Pong game constants - Optimized for 128x128 native resolution
constexpr int16_t PADDLE_HEIGHT = 20;  // Proportional to screen
constexpr int16_t PADDLE_WIDTH  = 3;   // Slightly wider for visibility
constexpr int16_t BALL_SIZE     = 2;   // Small ball for smooth movement
constexpr int16_t MAX_PARTICLES = 6;   // Reduced for better performance

// Ball physics constants - Tuned for smooth animation
constexpr float MIN_BALL_VELOCITY       = 0.8f;  // Faster minimum speed
constexpr float MAX_BALL_VELOCITY       = 2.5f;  // Higher max speed
constexpr float MIN_AXIS_VELOCITY       = 0.6f;  // Minimum per-axis velocity
constexpr float BALL_VELOCITY_VARIATION = 0.15f; // More variation
constexpr float GRAVITY                 = 0.08f; // Slightly stronger gravity

// Paddle AI constants
constexpr float AI_DISTANCE_THRESHOLD = 0.65f;  // Adjusted for square screen
constexpr float MIN_PREDICTION_TIME   = 0.12f;
constexpr int16_t PADDLE_SPEED        = 2;     // Smooth paddle movement

// Particle system constants
constexpr uint8_t PARTICLES_PER_COLLISION    = 3;  // Fewer particles for performance
constexpr uint8_t MIN_PARTICLE_LIFE          = 8;
constexpr uint8_t MAX_PARTICLE_LIFE          = 15;
constexpr uint8_t PARTICLE_FADE_THRESHOLD    = 4;
constexpr uint16_t PARTICLE_MEMORY_THRESHOLD = 1500;

// Optimized grid storage using bitwise operations
constexpr int16_t GRID_BYTES = (RENDER_WIDTH * RENDER_HEIGHT + 7) / 8;  // Bits packed into bytes

// Individual digit structure
struct ClockDigit {
  uint8_t value;
  uint8_t old_value;
  int16_t x, y;
  bool needs_update;
};

// Particle system
struct Particle {
  float x, y;
  float vx, vy;
  uint16_t color;
  uint8_t life;
  bool active;
};

// GOL specific state - Using bitwise storage for memory efficiency
struct GolSpecific {
  uint8_t current_grid[GRID_BYTES];  // Bitwise storage
  uint8_t next_grid[GRID_BYTES];     // Bitwise storage
  
  // Clock area bounds
  int16_t clock_min_x;
  int16_t clock_max_x;
  int16_t clock_min_y;
  int16_t clock_max_y;
  
  // Visual effects
  uint8_t alive_color_index = 0;
  
  // GoL timing
  uint32_t last_generation  = 0;
  
  // Rendering optimization
  bool force_full_redraw = false;
};

// Pong specific state
struct PongSpecific {
  // Ball
  float ball_x       = RENDER_WIDTH / 2;
  float ball_y       = RENDER_HEIGHT / 2;
  float ball_vx      = 1.5f;  // Faster initial velocity
  float ball_vy      = 1.0f;
  int16_t old_ball_x = 0;
  int16_t old_ball_y = 0;
  
  // Paddles
  int16_t left_paddle_y       = RENDER_HEIGHT / 2;
  int16_t right_paddle_y      = RENDER_HEIGHT / 2;
  int16_t left_paddle_target  = RENDER_HEIGHT / 2;
  int16_t right_paddle_target = RENDER_HEIGHT / 2;
  int16_t old_left_paddle_y   = RENDER_HEIGHT / 2;
  int16_t old_right_paddle_y  = RENDER_HEIGHT / 2;
  
  // Visual effects
  Particle particles[MAX_PARTICLES];
};

// Common game state
struct CommonState {
  // Visual effects
  uint8_t bg_color_index = 0;
  
  // Clock digits (H1 H2 : M1 M2)
  ClockDigit digits[4];
  bool colon_visible          = true;
  bool old_colon_visible      = true;
  uint32_t last_second_update = 0;
  uint32_t last_ntp_update    = 0;
  uint32_t last_minute_update = 0;
  uint8_t last_hour           = 255;
  
  // Theme state
  bool is_day_theme     = false;
  bool old_is_day_theme = true;
  
  // Memory management
  uint32_t last_memory_check = 0;
  uint16_t min_heap_seen     = 65535;
  
  // WiFi stability
  uint32_t last_wifi_check = 0;
  uint8_t wifi_retry_count = 0;
  bool ntp_sync_failed     = false;
  bool wifi_connected      = false;
};

// Initialize display
Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST);

WiFiUDP ntpUDP;
char ntp_server_buffer[20];
NTPClient timeClient(ntpUDP, "", NTP_OFFSET, NTP_UPDATE_INTERVAL);

CommonState common;
GolSpecific gol;
PongSpecific pong;
bool is_gol_mode = true;  // Start with GOL

// Digit collision bounds and spacing - Optimized for 128px width
constexpr uint8_t DIGIT_WIDTH       = 20;  // Smaller digits for native res
constexpr uint8_t DIGIT_HEIGHT      = 28;
constexpr uint8_t DIGIT_SPACING     = 24;  // Increased spacing
constexpr uint8_t COLON_EXTRA_SPACE = 15;

// Text size constant
constexpr uint8_t TEXT_SIZE      = 4;    // Native text size
constexpr int16_t DOT_SIZE       = 4;    // Increased to match digit thickness
constexpr int16_t BOTTOM_DOT_OFFSET = 6; // Adjusted to center vertically
constexpr int16_t TOP_DOT_OFFSET    = 14; // Adjusted to center vertically

// Serial and display configuration constants
constexpr uint32_t SERIAL_BAUD_RATE = 115200;
constexpr uint8_t DISPLAY_ROTATION  = 2;     // Rotated 180 degrees as requested
constexpr uint8_t DISPLAY_WIDTH     = 128;
constexpr uint8_t DISPLAY_HEIGHT    = 128;   // Square display

// PROGMEM String constants
static const char MSG_CONNECTING_WIFI[]   PROGMEM = "Connecting WiFi...";
static const char MSG_WIFI_CONNECTED[]    PROGMEM = "WiFi connected!";
static const char MSG_WIFI_FAILED[]       PROGMEM = "WiFi connection failed - continuing offline";
static const char MSG_NTP_FAILED[]        PROGMEM = "Initial NTP sync failed";
static const char MSG_NTP_UPDATE_FAILED[] PROGMEM = "NTP update failed";
static const char MSG_CRITICAL_MEMORY[]   PROGMEM = "Critical memory low - restarting";
static const char MSG_WIFI_RESTART[]      PROGMEM = "WiFi connection failed - restarting";
static const char MSG_INITIAL_HEAP[]      PROGMEM = "Initial free heap: ";
static const char MSG_DOT[]               PROGMEM = ".";

// Bitwise GOL helper functions for memory efficiency
inline bool getCell(const uint8_t* grid, int16_t x, int16_t y) {
  if (x < 0 || x >= RENDER_WIDTH || y < 0 || y >= RENDER_HEIGHT) return false;
  int16_t index = y * RENDER_WIDTH + x;
  int16_t byte_index = index / 8;
  int16_t bit_index = index % 8;
  return (grid[byte_index] >> bit_index) & 1;
}

inline void setCell(uint8_t* grid, int16_t x, int16_t y, bool alive) {
  if (x < 0 || x >= RENDER_WIDTH || y < 0 || y >= RENDER_HEIGHT) return;
  int16_t index = y * RENDER_WIDTH + x;
  int16_t byte_index = index / 8;
  int16_t bit_index = index % 8;
  if (alive) {
    grid[byte_index] |= (1 << bit_index);
  } else {
    grid[byte_index] &= ~(1 << bit_index);
  }
}

// Theme helper functions
uint16_t getCurrentBgColor() {
  return common.is_day_theme ? DAY_BG_COLORS[common.bg_color_index] : NIGHT_BG_COLORS[common.bg_color_index];
}

uint16_t getCurrentFgColor() {
  return common.is_day_theme ? DAY_FG : NIGHT_FG;
}

uint16_t getThemeColor(uint16_t night_color) {
  if (!common.is_day_theme) return night_color;
  
  // Convert night colors to day equivalents
  if (night_color == WHITE)   return BLACK;
  if (night_color == CYAN)    return DARK_CYAN;
  if (night_color == MAGENTA) return DARK_MAGENTA;
  if (night_color == YELLOW)  return DARK_YELLOW;
  if (night_color == RED)     return DARK_RED;
  if (night_color == ORANGE)  return DARK_ORANGE;
  if (night_color == GREEN)   return DARK_GREEN;
  if (night_color == BLUE)    return DARK_BLUE;
  
  return night_color;
}

bool isDayTime(uint8_t hour) {
  return (hour >= DAY_START_HOUR && hour < DAY_END_HOUR);
}

uint16_t getAliveColor() {
  return getThemeColor(ALIVE_COLORS[gol.alive_color_index]);
}

// Memory management functions
void checkMemoryHealth() {
  uint16_t free_heap = ESP.getFreeHeap();
  
  if (free_heap < common.min_heap_seen) {
    common.min_heap_seen = free_heap;
  }
  
  if (free_heap < MIN_FREE_HEAP) {
    Serial.println(FPSTR(MSG_CRITICAL_MEMORY));
    ESP.restart();
  }
  
  if (free_heap < LOW_MEMORY_THRESHOLD) {
    if (!is_gol_mode) {
      for (int i = 0; i < MAX_PARTICLES; i++) {
        pong.particles[i].active = false;
      }
    }
  }
}

// WiFi stability functions
bool checkWiFiConnection() {
  if (WiFi.status() != WL_CONNECTED) {
    common.wifi_retry_count++;
    
    if (common.wifi_retry_count > MAX_WIFI_RETRIES) {
      Serial.println(FPSTR(MSG_WIFI_RESTART));
      ESP.restart();
    }
    
    WiFi.disconnect();
    WiFi.begin(ssid, password);
    
    int attempts = 0;
    while (WiFi.status() != WL_CONNECTED && attempts < (WIFI_CONNECTION_TIMEOUT * 2)) {
      delay(WIFI_ATTEMPT_DELAY);
      attempts++;
    }
    
    if (WiFi.status() == WL_CONNECTED) {
      common.wifi_retry_count = 0;
      return true;
    }
    return false;
  }
  
  common.wifi_retry_count = 0;
  return true;
}

void setup() {
  system_update_cpu_freq(SYS_CPU_160MHZ);  // Overclock for performance
  
  Serial.begin(SERIAL_BAUD_RATE);
  
  strcpy_P(ntp_server_buffer, NTP_SERVER);
  timeClient.setPoolServerName(ntp_server_buffer);
  
  tft.initR(INITR_BLACKTAB);
  tft.setRotation(DISPLAY_ROTATION);  // Rotation 2 as requested
  
  // Start with night theme
  common.is_day_theme = false;
  
  // WiFi connection with visual feedback
  tft.fillScreen(getCurrentBgColor());
  tft.setTextColor(getCurrentFgColor());
  tft.setTextSize(1);
  tft.setCursor(5, 40);
  tft.print(FPSTR(MSG_CONNECTING_WIFI));
  
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  
  int wifi_attempts = 0;
  while (WiFi.status() != WL_CONNECTED && wifi_attempts < MAX_WIFI_ATTEMPTS) {
    delay(WIFI_ATTEMPT_DELAY);
    wifi_attempts++;
    Serial.print(FPSTR(MSG_DOT));
  }
  
  if (WiFi.status() == WL_CONNECTED) {
    Serial.println(FPSTR(MSG_WIFI_CONNECTED));
    timeClient.begin();
    if (!timeClient.update()) {
      Serial.println(FPSTR(MSG_NTP_FAILED));
      common.ntp_sync_failed = true;
    }
    common.wifi_connected = true;
  } else {
    Serial.println(FPSTR(MSG_WIFI_FAILED));
    common.wifi_connected = false;
  }
  
  initializeCommon();
  initializeDigits();
  initializeGol();
  
  tft.fillScreen(getCurrentBgColor());
  renderFullGol();
  
  randomSeed(WiFi.RSSI() * analogRead(A0));
  
  Serial.print(FPSTR(MSG_INITIAL_HEAP));
  Serial.println(ESP.getFreeHeap());
}

void loop() {
  uint32_t now = millis();
  
  // Memory and WiFi health checks
  if (now - common.last_memory_check > MEMORY_CHECK_INTERVAL) {
    common.last_memory_check = now;
    checkMemoryHealth();
  }
  
  if (now - common.last_wifi_check > WIFI_CHECK_INTERVAL) {
    common.last_wifi_check = now;
    checkWiFiConnection();
    common.wifi_connected = (WiFi.status() == WL_CONNECTED);
  }
  
  updateTime(now);
  
  if (is_gol_mode) {
    if (now - gol.last_generation >= GENERATION_INTERVAL) {
      computeGol();
      gol.last_generation = now;
      if (countAlive() < MIN_ALIVE_CELLS) {
        initializeGol();
        renderFullGol();
      }
    }
  } else {
    updateBall();
    updatePaddles();
    updateParticles();
    renderGamePong();
  }
  
  renderClock();
  
  delay(FRAME_DELAY);
  yield();
}

void initializeCommon() {
  common.bg_color_index = 0;
  common.colon_visible = true;
  common.old_colon_visible = true;
  common.last_second_update = 0;
  common.last_ntp_update = 0;
  common.last_minute_update = 0;
  common.last_hour = 255;
  common.is_day_theme = false;
  common.old_is_day_theme = true;
  common.last_memory_check = 0;
  common.min_heap_seen = 65535;
  common.last_wifi_check = 0;
  common.wifi_retry_count = 0;
  common.ntp_sync_failed = false;
  common.wifi_connected = false;
}

void addGlider(int16_t y, int16_t x) {
  if (x < 0 || x >= RENDER_WIDTH - 2 || y < 0 || y >= RENDER_HEIGHT - 2) return;
  setCell(gol.current_grid, x+1, y, true);
  setCell(gol.current_grid, x+2, y+1, true);
  setCell(gol.current_grid, x, y+2, true);
  setCell(gol.current_grid, x+1, y+2, true);
  setCell(gol.current_grid, x+2, y+2, true);
}

void addBlinker(int16_t y, int16_t x) {
  if (x < 0 || x >= RENDER_WIDTH - 2 || y < 0 || y >= RENDER_HEIGHT) return;
  setCell(gol.current_grid, x, y, true);
  setCell(gol.current_grid, x+1, y, true);
  setCell(gol.current_grid, x+2, y, true);
}

void initializeGol() {
  // Clear grid
  memset(gol.current_grid, 0, sizeof(gol.current_grid));

  // Add interesting patterns
  for (int i = 0; i < 8; i++) {  // Fewer patterns for better performance
    int16_t rx = random(0, RENDER_WIDTH - 3);
    int16_t ry = random(0, RENDER_HEIGHT - 3);
    addGlider(ry, rx);
  }
  for (int i = 0; i < 4; i++) {
    int16_t rx = random(0, RENDER_WIDTH - 3);
    int16_t ry = random(0, RENDER_HEIGHT - 1);
    addBlinker(ry, rx);
  }

  // Add some random cells
  for (int16_t y = 0; y < RENDER_HEIGHT; y++) {
    for (int16_t x = 0; x < RENDER_WIDTH; x++) {
      if (random(0, 100) < INITIAL_ALIVE_CHANCE / 3) {  // Reduced density
        setCell(gol.current_grid, x, y, true);
      }
    }
  }
  
  gol.alive_color_index = 0;
  gol.last_generation = 0;
  gol.force_full_redraw = false;
  
  // Clock bounds
  gol.clock_min_x = common.digits[0].x - 1;
  gol.clock_max_x = common.digits[3].x + DIGIT_WIDTH + 1;
  gol.clock_min_y = CLOCK_Y - 1;
  gol.clock_max_y = CLOCK_Y + DIGIT_HEIGHT + 1;
}

void initializePong() {
  pong.ball_x = RENDER_WIDTH / 2;
  pong.ball_y = RENDER_HEIGHT / 2;
  pong.ball_vx = random(-120, 120) / 100.0f;
  pong.ball_vy = random(80, 160) / 100.0f;
  pong.old_ball_x = 0;
  pong.old_ball_y = 0;

  pong.left_paddle_y = RENDER_HEIGHT / 2;
  pong.right_paddle_y = RENDER_HEIGHT / 2;
  pong.left_paddle_target = RENDER_HEIGHT / 2;
  pong.right_paddle_target = RENDER_HEIGHT / 2;
  pong.old_left_paddle_y = RENDER_HEIGHT / 2;
  pong.old_right_paddle_y = RENDER_HEIGHT / 2;
  
  for (int i = 0; i < MAX_PARTICLES; i++) {
    pong.particles[i].active = false;
  }
}

void initializeDigits() {
  // Center the clock on 128px width with increased spacing
  constexpr int16_t total_clock_width = 3 * DIGIT_SPACING + DIGIT_WIDTH + COLON_EXTRA_SPACE;
  constexpr int16_t start_x = (RENDER_WIDTH - total_clock_width) / 2;
  
  for (int i = 0; i < 4; i++) {
    common.digits[i].x = start_x + i * DIGIT_SPACING + (i >= 2 ? COLON_EXTRA_SPACE : 0);
    common.digits[i].y = CLOCK_Y;
    common.digits[i].value = 255;
    common.digits[i].old_value = 0;
    common.digits[i].needs_update = true;
  }
}

void updateTime(uint32_t now) {
  if (now - common.last_ntp_update > NTP_UPDATE_INTERVAL) {
    common.last_ntp_update = now;
    
    if (WiFi.status() == WL_CONNECTED) {
      if (!timeClient.update()) {
        Serial.println(FPSTR(MSG_NTP_UPDATE_FAILED));
        common.ntp_sync_failed = true;
      } else {
        common.ntp_sync_failed = false;
      }
    }
  }
  
  if (now - common.last_second_update >= 1000) {
    common.last_second_update = now;
    
    common.wifi_connected = (WiFi.status() == WL_CONNECTED);
    
    static unsigned long last_known_tod = DEFAULT_TIME_SECONDS;
    static unsigned long last_sync_millis = 0;
    
    unsigned long epochTime;
    bool use_fallback = common.ntp_sync_failed || !common.wifi_connected;
    if (use_fallback) {
      if (last_sync_millis == 0) {
        last_sync_millis = millis();
      }
      unsigned long elapsed_seconds = (millis() - last_sync_millis) / 1000;
      epochTime = last_known_tod + elapsed_seconds;
      epochTime %= SECONDS_PER_DAY;
    } else {
      epochTime = timeClient.getEpochTime();
      last_known_tod = epochTime % SECONDS_PER_DAY;
      last_sync_millis = millis();
      epochTime = last_known_tod;
    }
    
    uint8_t hours = (epochTime % SECONDS_PER_DAY) / SECONDS_PER_HOUR;
    uint8_t minutes = (epochTime % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE;
    
    // Check for hour change - trigger mode switch
    if (hours != common.last_hour) {
      common.last_hour = hours;
      is_gol_mode = !is_gol_mode;
      tft.fillScreen(getCurrentBgColor());
      if (is_gol_mode) {
        initializeGol();
        renderFullGol();
      } else {
        initializePong();
        renderFullPong();
      }
      for (int i = 0; i < 4; i++) {
        common.digits[i].needs_update = true;
      }
      common.old_colon_visible = !common.colon_visible;
    }
    
    // Force refresh all digits every minute
    uint32_t current_minute = epochTime / SECONDS_PER_MINUTE;
    if (current_minute != common.last_minute_update) {
      common.last_minute_update = current_minute;
      for (int i = 0; i < 4; i++) {
        common.digits[i].needs_update = true;
      }
    }
    
    // Update theme based on time
    bool new_is_day = isDayTime(hours);
    if (new_is_day != common.is_day_theme) {
      common.old_is_day_theme = common.is_day_theme;
      common.is_day_theme = new_is_day;
      tft.fillScreen(getCurrentBgColor());
      if (is_gol_mode) {
        renderFullGol();
      } else {
        renderFullPong();
      }
      for (int i = 0; i < 4; i++) {
        common.digits[i].needs_update = true;
      }
    }
    
    updateDigit(0, hours / 10);
    updateDigit(1, hours % 10);
    updateDigit(2, minutes / 10);
    updateDigit(3, minutes % 10);
    
    // Update colon
    common.old_colon_visible = common.colon_visible;
    common.colon_visible = !common.colon_visible;
  }
}

void updateDigit(uint8_t index, uint8_t new_value) {
  if (common.digits[index].value != new_value) {
    common.digits[index].old_value = common.digits[index].value;
    common.digits[index].value = new_value;
    common.digits[index].needs_update = true;
  }
}

int countAlive() {
  int count = 0;
  for (int16_t y = 0; y < RENDER_HEIGHT; y++) {
    for (int16_t x = 0; x < RENDER_WIDTH; x++) {
      if (getCell(gol.current_grid, x, y)) count++;
    }
  }
  return count;
}

int countNeighbors(int16_t x, int16_t y) {
  int count = 0;
  for (int8_t dx = -1; dx <= 1; dx++) {
    for (int8_t dy = -1; dy <= 1; dy++) {
      if (dx == 0 && dy == 0) continue;
      int16_t nx = (x + dx + RENDER_WIDTH) % RENDER_WIDTH;
      int16_t ny = (y + dy + RENDER_HEIGHT) % RENDER_HEIGHT;
      if (getCell(gol.current_grid, nx, ny)) count++;
    }
  }
  return count;
}

void computeGol() {
  // Clear next grid
  memset(gol.next_grid, 0, sizeof(gol.next_grid));
  
  // Compute next generation
  for (int16_t y = 0; y < RENDER_HEIGHT; y++) {
    for (int16_t x = 0; x < RENDER_WIDTH; x++) {
      int neighbors = countNeighbors(x, y);
      bool alive = getCell(gol.current_grid, x, y);
      bool next_alive = (alive && (neighbors == 2 || neighbors == 3)) || (!alive && neighbors == 3);
      setCell(gol.next_grid, x, y, next_alive);
    }
  }
  
  uint16_t bg_color = getCurrentBgColor();
  uint16_t alive_color = getAliveColor();
  
  // Render changes efficiently
  for (int16_t y = 0; y < RENDER_HEIGHT; y++) {
    for (int16_t x = 0; x < RENDER_WIDTH; x++) {
      bool current_state = getCell(gol.current_grid, x, y);
      bool next_state = getCell(gol.next_grid, x, y);
      
      if (next_state != current_state) {
        // Skip drawing in clock area
        if (y >= gol.clock_min_y && y < gol.clock_max_y &&
            x >= gol.clock_min_x && x < gol.clock_max_x) continue;
        
        uint16_t color = next_state ? alive_color : bg_color;
        tft.drawPixel(x, y, color);  // Single pixel for native resolution
      }
    }
  }
  
  // Swap grids
  memcpy(gol.current_grid, gol.next_grid, sizeof(gol.next_grid));
}

void renderFullGol() {
  uint16_t bg_color = getCurrentBgColor();
  uint16_t alive_color = getAliveColor();
  
  for (int16_t y = 0; y < RENDER_HEIGHT; y++) {
    for (int16_t x = 0; x < RENDER_WIDTH; x++) {
      // Skip clock area
      if (y >= gol.clock_min_y && y < gol.clock_max_y &&
          x >= gol.clock_min_x && x < gol.clock_max_x) continue;
      
      uint16_t color = getCell(gol.current_grid, x, y) ? alive_color : bg_color;
      tft.drawPixel(x, y, color);
    }
  }
}

void updateBall() {
  // Store old position
  pong.old_ball_x = (int16_t)pong.ball_x;
  pong.old_ball_y = (int16_t)pong.ball_y;
  
  // Update position
  pong.ball_x += pong.ball_vx;
  pong.ball_y += pong.ball_vy;
  
  // Screen boundary collisions
  if (pong.ball_x <= 0 || pong.ball_x >= RENDER_WIDTH - BALL_SIZE) {
    pong.ball_vx = -pong.ball_vx;
    pong.ball_x = constrain(pong.ball_x, 0, RENDER_WIDTH - BALL_SIZE);
    createParticles(pong.ball_x, pong.ball_y, getThemeColor(WHITE));
    varyBallBehavior();
  }
  
  if (pong.ball_y <= 0 || pong.ball_y >= RENDER_HEIGHT - BALL_SIZE) {
    pong.ball_vy = -pong.ball_vy;
    pong.ball_y = constrain(pong.ball_y, 0, RENDER_HEIGHT - BALL_SIZE);
    createParticles(pong.ball_x, pong.ball_y, getThemeColor(WHITE));
    varyBallBehavior();
  }
  
  // Paddle collisions
  checkPaddleCollisions();
  
  // Clock digit collisions
  checkDigitCollisions();
  
  // Reset if ball gets stuck
  if (abs(pong.ball_vx) < MIN_BALL_VELOCITY && abs(pong.ball_vy) < MIN_BALL_VELOCITY) {
    resetBall();
  }
}

void checkPaddleCollisions() {
  // Left paddle collision - adjusted for x=1 position
  if (pong.ball_x <= PADDLE_WIDTH + 1 && 
      pong.ball_y + BALL_SIZE >= pong.left_paddle_y && 
      pong.ball_y <= pong.left_paddle_y + PADDLE_HEIGHT) {
    pong.ball_vx = abs(pong.ball_vx);
    pong.ball_x = PADDLE_WIDTH + 1;
    createParticles(pong.ball_x, pong.ball_y, getThemeColor(CYAN));
    varyBallBehavior();
  }
  
  // Right paddle collision - kept at original position
  if (pong.ball_x + BALL_SIZE >= RENDER_WIDTH - PADDLE_WIDTH && 
      pong.ball_y + BALL_SIZE >= pong.right_paddle_y && 
      pong.ball_y <= pong.right_paddle_y + PADDLE_HEIGHT) {
    pong.ball_vx = -abs(pong.ball_vx);
    pong.ball_x = RENDER_WIDTH - PADDLE_WIDTH - BALL_SIZE;
    createParticles(pong.ball_x, pong.ball_y, getThemeColor(MAGENTA));
    varyBallBehavior();
  }
}

void checkDigitCollisions() {
  for (int i = 0; i < 4; i++) {
    if (pong.ball_x + BALL_SIZE >= common.digits[i].x && 
        pong.ball_x <= common.digits[i].x + DIGIT_WIDTH &&
        pong.ball_y + BALL_SIZE >= common.digits[i].y && 
        pong.ball_y <= common.digits[i].y + DIGIT_HEIGHT) {
      
      // Determine collision side
      float center_x = common.digits[i].x + DIGIT_WIDTH / 2;
      float center_y = common.digits[i].y + DIGIT_HEIGHT / 2;
      
      if (abs(pong.ball_x + BALL_SIZE/2 - center_x) > abs(pong.ball_y + BALL_SIZE/2 - center_y)) {
        pong.ball_vx = -pong.ball_vx;
      } else {
        pong.ball_vy = -pong.ball_vy;
      }
      
      createParticles(pong.ball_x, pong.ball_y, getThemeColor(YELLOW));
      varyBallBehavior();
      break;
    }
  }
}

void updatePaddles() {
  // Store old positions
  pong.old_left_paddle_y = pong.left_paddle_y;
  pong.old_right_paddle_y = pong.right_paddle_y;
  
  // AI tracking with prediction
  float ball_center_x = pong.ball_x + BALL_SIZE / 2;
  float ball_center_y = pong.ball_y + BALL_SIZE / 2;
  float distance_threshold = RENDER_WIDTH * AI_DISTANCE_THRESHOLD;
  
  // Left paddle AI
  if (pong.ball_vx < 0 && ball_center_x < distance_threshold) {
    float time_to_paddle = max(MIN_PREDICTION_TIME, (ball_center_x - PADDLE_WIDTH) / abs(pong.ball_vx));
    float predicted_y = pong.ball_y + pong.ball_vy * time_to_paddle;
    if (predicted_y < 0) predicted_y = -predicted_y;
    else if (predicted_y > RENDER_HEIGHT) predicted_y = 2 * RENDER_HEIGHT - predicted_y;
    pong.left_paddle_target = constrain((int16_t)predicted_y - PADDLE_HEIGHT/2, 0, RENDER_HEIGHT - PADDLE_HEIGHT);
  } else {
    pong.left_paddle_target = constrain((int16_t)ball_center_y - PADDLE_HEIGHT/2, 0, RENDER_HEIGHT - PADDLE_HEIGHT);
  }
  
  // Right paddle AI
  if (pong.ball_vx > 0 && ball_center_x > RENDER_WIDTH - distance_threshold) {
    float time_to_paddle = max(MIN_PREDICTION_TIME, (RENDER_WIDTH - PADDLE_WIDTH - ball_center_x) / abs(pong.ball_vx));
    float predicted_y = pong.ball_y + pong.ball_vy * time_to_paddle;
    if (predicted_y < 0) predicted_y = -predicted_y;
    else if (predicted_y > RENDER_HEIGHT) predicted_y = 2 * RENDER_HEIGHT - predicted_y;
    pong.right_paddle_target = constrain((int16_t)predicted_y - PADDLE_HEIGHT/2, 0, RENDER_HEIGHT - PADDLE_HEIGHT);
  } else {
    pong.right_paddle_target = constrain((int16_t)ball_center_y - PADDLE_HEIGHT/2, 0, RENDER_HEIGHT - PADDLE_HEIGHT);
  }
  
  // Smooth paddle movement
  if (pong.left_paddle_y < pong.left_paddle_target) {
    pong.left_paddle_y = min((int16_t)(pong.left_paddle_y + PADDLE_SPEED), pong.left_paddle_target);
  } else if (pong.left_paddle_y > pong.left_paddle_target) {
    pong.left_paddle_y = max((int16_t)(pong.left_paddle_y - PADDLE_SPEED), pong.left_paddle_target);
  }
  
  if (pong.right_paddle_y < pong.right_paddle_target) {
    pong.right_paddle_y = min((int16_t)(pong.right_paddle_y + PADDLE_SPEED), pong.right_paddle_target);
  } else if (pong.right_paddle_y > pong.right_paddle_target) {
    pong.right_paddle_y = max((int16_t)(pong.right_paddle_y - PADDLE_SPEED), pong.right_paddle_target);
  }
}

void updateParticles() {
  for (int i = 0; i < MAX_PARTICLES; i++) {
    if (pong.particles[i].active) {
      // Clear old position
      tft.fillRect((int16_t)pong.particles[i].x, (int16_t)pong.particles[i].y, 
                   1, 1, getCurrentBgColor());
      
      // Update position
      pong.particles[i].x += pong.particles[i].vx;
      pong.particles[i].y += pong.particles[i].vy;
      pong.particles[i].vy += GRAVITY;
      pong.particles[i].life--;
      
      // Check bounds and lifetime
      if (pong.particles[i].life <= 0 || 
          pong.particles[i].x < 0 || pong.particles[i].x >= RENDER_WIDTH ||
          pong.particles[i].y < 0 || pong.particles[i].y >= RENDER_HEIGHT) {
        pong.particles[i].active = false;
      } else {
        // Draw particle with fade
        uint16_t alpha_color = pong.particles[i].color;
        if (pong.particles[i].life < PARTICLE_FADE_THRESHOLD) {
          alpha_color = (alpha_color & 0xE79C) >> 1;
        }
        tft.drawPixel((int16_t)pong.particles[i].x, (int16_t)pong.particles[i].y, alpha_color);
      }
    }
  }
}

void createParticles(float x, float y, uint16_t color) {
  if (ESP.getFreeHeap() < PARTICLE_MEMORY_THRESHOLD) return;
  
  for (int i = 0; i < PARTICLES_PER_COLLISION; i++) {
    for (int j = 0; j < MAX_PARTICLES; j++) {
      if (!pong.particles[j].active) {
        pong.particles[j].x = x + BALL_SIZE/2;
        pong.particles[j].y = y + BALL_SIZE/2;
        pong.particles[j].vx = (random(-150, 150) / 100.0f);
        pong.particles[j].vy = (random(-150, 75) / 100.0f);
        pong.particles[j].color = color;
        pong.particles[j].life = random(MIN_PARTICLE_LIFE, MAX_PARTICLE_LIFE);
        pong.particles[j].active = true;
        break;
      }
    }
  }
}

void varyBallBehavior() {
  pong.ball_vx += (random(-15, 15) / 100.0f) * BALL_VELOCITY_VARIATION;
  pong.ball_vy += (random(-15, 15) / 100.0f) * BALL_VELOCITY_VARIATION;
  
  pong.ball_vx = constrain(pong.ball_vx, -MAX_BALL_VELOCITY, MAX_BALL_VELOCITY);
  pong.ball_vy = constrain(pong.ball_vy, -MAX_BALL_VELOCITY, MAX_BALL_VELOCITY);
  
  if (abs(pong.ball_vx) < MIN_AXIS_VELOCITY) pong.ball_vx = pong.ball_vx > 0 ? MIN_AXIS_VELOCITY : -MIN_AXIS_VELOCITY;
  if (abs(pong.ball_vy) < MIN_AXIS_VELOCITY) pong.ball_vy = pong.ball_vy > 0 ? MIN_AXIS_VELOCITY : -MIN_AXIS_VELOCITY;
}

void resetBall() {
  initializePong();
  common.bg_color_index = (common.bg_color_index + 1) % BG_COLOR_COUNT;
  tft.fillScreen(getCurrentBgColor());
  
  // Force redraw all digits
  for (int i = 0; i < 4; i++) {
    common.digits[i].needs_update = true;
  }
}

void renderFullPong() {
  // Initial render - left paddle 1 pixel from edge, right paddle flush
  tft.fillRect(1, pong.left_paddle_y, PADDLE_WIDTH, PADDLE_HEIGHT, getThemeColor(CYAN));
  tft.fillRect(RENDER_WIDTH - PADDLE_WIDTH, pong.right_paddle_y, PADDLE_WIDTH, PADDLE_HEIGHT, getThemeColor(MAGENTA));
  tft.fillRect((int16_t)pong.ball_x, (int16_t)pong.ball_y, BALL_SIZE, BALL_SIZE, getCurrentFgColor());
}

void renderGamePong() {
  // Clear old ball position and draw new
  if (pong.old_ball_x != (int16_t)pong.ball_x || pong.old_ball_y != (int16_t)pong.ball_y) {
    tft.fillRect(pong.old_ball_x, pong.old_ball_y, BALL_SIZE, BALL_SIZE, getCurrentBgColor());
    tft.fillRect((int16_t)pong.ball_x, (int16_t)pong.ball_y, BALL_SIZE, BALL_SIZE, getCurrentFgColor());
  }
  
  // Update left paddle efficiently - moved 1 pixel from edge to prevent clipping
  if (pong.old_left_paddle_y != pong.left_paddle_y) {
    int16_t diff = pong.left_paddle_y - pong.old_left_paddle_y;
    int16_t left_x = 1; // 1 pixel from edge to prevent display clipping
    
    if (diff > 0) {
      // Moving down: only clear/draw the non-overlapping areas
      tft.fillRect(left_x, pong.old_left_paddle_y, PADDLE_WIDTH, diff, getCurrentBgColor());
      tft.fillRect(left_x, pong.old_left_paddle_y + PADDLE_HEIGHT, PADDLE_WIDTH, diff, getThemeColor(CYAN));
    } else if (diff < 0) {
      // Moving up: only clear/draw the non-overlapping areas  
      tft.fillRect(left_x, pong.left_paddle_y + PADDLE_HEIGHT, PADDLE_WIDTH, -diff, getCurrentBgColor());
      tft.fillRect(left_x, pong.left_paddle_y, PADDLE_WIDTH, -diff, getThemeColor(CYAN));
    }
  }
  
  // Update right paddle efficiently - kept at edge for no gap
  if (pong.old_right_paddle_y != pong.right_paddle_y) {
    int16_t diff = pong.right_paddle_y - pong.old_right_paddle_y;
    int16_t right_x = RENDER_WIDTH - PADDLE_WIDTH; // Flush to edge
    
    if (diff > 0) {
      // Moving down: only clear/draw the non-overlapping areas
      tft.fillRect(right_x, pong.old_right_paddle_y, PADDLE_WIDTH, diff, getCurrentBgColor());
      tft.fillRect(right_x, pong.old_right_paddle_y + PADDLE_HEIGHT, PADDLE_WIDTH, diff, getThemeColor(MAGENTA));
    } else if (diff < 0) {
      // Moving up: only clear/draw the non-overlapping areas
      tft.fillRect(right_x, pong.right_paddle_y + PADDLE_HEIGHT, PADDLE_WIDTH, -diff, getCurrentBgColor());
      tft.fillRect(right_x, pong.right_paddle_y, PADDLE_WIDTH, -diff, getThemeColor(MAGENTA));
    }
  }
}

void renderClock() {
  // Update digits that need it
  for (int i = 0; i < 4; i++) {
    if (common.digits[i].needs_update) {
      tft.fillRect(common.digits[i].x, common.digits[i].y, DIGIT_WIDTH, DIGIT_HEIGHT, getCurrentBgColor());
      
      tft.setTextColor(getCurrentFgColor());
      tft.setTextSize(TEXT_SIZE);
      tft.setCursor(common.digits[i].x, common.digits[i].y);
      tft.print(common.digits[i].value);
      
      common.digits[i].needs_update = false;
    }
  }
  
  // Update colon if changed
  if (common.colon_visible != common.old_colon_visible) {
    uint16_t colon_color = common.colon_visible ? (common.wifi_connected ? getCurrentFgColor() : RED) : getCurrentBgColor();

    int16_t colon_center_x = (common.digits[1].x + DIGIT_WIDTH + common.digits[2].x) / 2;
    int16_t dot_x = colon_center_x - (DOT_SIZE / 2);
    int16_t bottom_dot_y = common.digits[0].y + BOTTOM_DOT_OFFSET;
    int16_t top_dot_y = common.digits[0].y + TOP_DOT_OFFSET;

    tft.fillRect(dot_x, bottom_dot_y, DOT_SIZE, DOT_SIZE, colon_color);
    tft.fillRect(dot_x, top_dot_y, DOT_SIZE, DOT_SIZE, colon_color);

    common.old_colon_visible = common.colon_visible;
  }
}

Have a nice week ahead!
As always, any corrections or improvement suggestions, feel free to add, and if significant, will definitely get added in the project in itโ€™s next revision.

2 Likes

Nice job, thanks for sharing:-)

1 Like

Pixel Wash D1 R1 1.44 ST7735 SPI ESP8266 Firmware

An ESP8266 refactoring of the SPI GFX sketch written for Arduino MEGA 2560 LCD Keypad Shield KMR 1.8 SPI Firmware Rev 2.

Just a basic GFX testing suite containing various well known animations and algorithms to test out various libraries or hardware integrity. Presented Source Code is powered by Adafruit GFX Libraries.

Refactored for 1.44 128x128 SPI TFT ST7735 GREENTAB.
Uses hardware SPI bus.

       WeMos D1 R1 (ESP8266)          ST7735 1.44" TFT GREENTAB
     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
     โ”‚                     โ”‚         โ”‚                 โ”‚
     โ”‚  [USB-C]       3V3 โ—โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”คโ— VCC            โ”‚
     โ”‚                     โ”‚         โ”‚                 โ”‚
     โ”‚  [WIFI ANTENNA]     โ”‚         โ”‚                 โ”‚
     โ”‚                     โ”‚         โ”‚                 โ”‚
     โ”‚              D7/13 โ—โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”คโ— SDA (MOSI)     โ”‚
     โ”‚              D5/14 โ—โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”คโ— SCK (CLK)      โ”‚
     โ”‚               D2/4 โ—โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”คโ— CS             โ”‚
     โ”‚               D3/0 โ—โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”คโ— RST            โ”‚
     โ”‚               D4/2 โ—โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”คโ— DC (A0)        โ”‚
     โ”‚                     โ”‚         โ”‚                 โ”‚
     โ”‚                GND โ—โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”คโ— GND            โ”‚
     โ”‚                     โ”‚         โ”‚                 โ”‚
     โ”‚    [ESP8266-12E]    โ”‚         โ”‚ [128x128 LCD]   โ”‚
     โ”‚    [80MHz/160MHz]   โ”‚         โ”‚ [65K Colors]    โ”‚
     โ”‚    [4MB Flash]      โ”‚         โ”‚ [SPI Interface] โ”‚
     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                โ”‚                              โ”‚
                โ””โ”€โ”€โ”€โ”€ [ Xtensa LX106 Core ] โ”€โ”€โ”€โ”˜
                      [ 2.4GHz WiFi Radio ]

Source Code:

/*****************************************************************************
 * PIXELWASH ESP8266 - GFX Demo Suite v2.8
 * 
 * A multi-mode visual effects system designed for ESP8266 
 * microcontrollers with 128x128 SPI TFT displays. Featuring multiple
 * modes including Conway's Game of Life (CA), fire simulation, water effects, 
 * wind patterns, portal effects, and wormhole & cyclone abstract visualizations/effects.
 *
 * Hardware Requirements:
 * - WeMos D1 R1 (ESP8266-based board)
 * - 128x128 ST7735 SPI TFT Display
 * - Standard hard SPI connections (CS: D2/GPIO4, DC: D4/GPIO2, RST: D3/GPIO0)
 *
 * Designed by: Sir Ronnie
 * Organization: Core1D Automation Labs
 * Version: 2.8
 * Target Platform: ESP8266 (WeMos D1 R1)
 * Display: ST7735 128x128 SPI TFT
 *
 * Dependencies:
 * - Adafruit GFX Library
 * - Adafruit ST7735 Library  
 * - Arduino SPI Library
 *
 * ADAFRUIT SOFTWARE LICENSE:
 * 
 * Software License Agreement (BSD License)
 * 
 * Copyright (c) 2012 Adafruit Industries.  All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * - Redistributions of source code must retain the above copyright notice,
 *   this list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 * - Neither the name of the Adafruit Industries nor the names of its
 *   contributors may be used to endorse or promote products derived from this
 *   software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * PIXELWASH PROJECT LICENSE:
 *
 * MIT License
 *
 * Copyright (c) 2024 Sir Ronnie, Core1D Automation Labs
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 * Acknowledgments:
 * - Adafruit Industries for graphics and display libraries
 * - The Arduino community for the ESP8266 core and development tools
 * - John Conway for the Game of Life cellular automaton
 * - The open source community and academia for algorithmic foundations
 *
 *****************************************************************************/


#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include <SPI.h>

// Pin definitions for WeMos D1 R1
constexpr uint8_t TFT_CS  = D2; // GPIO4
constexpr uint8_t TFT_RST = D3; // GPIO0
constexpr uint8_t TFT_DC  = D4; // GPIO2
// D1R1 Hard connections: SCK = D5, MOSI = D7.

// Display initialization for ST7735 128x128
Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST);

// Display dimensions for 128x128 screen
constexpr uint16_t DISPLAY_WIDTH  = 128;
constexpr uint16_t DISPLAY_HEIGHT = 128;

// Grid dimensions config optimized for 128x128 ESP8266 performance
// 16x16 grid with 8px cells = 128x128 - Balanced Performance
constexpr uint8_t GRID_WIDTH  = 16;
constexpr uint8_t GRID_HEIGHT = 16;
constexpr uint8_t CELL_SIZE   = 8;

// Calculate offsets to center the grid
constexpr uint8_t X_OFFSET = 0;  // 128/16 = 8 exactly
constexpr uint8_t Y_OFFSET = 0;  // 128/16 = 8 exactly

// Dirty rendering tile system - optimized for ESP8266
constexpr uint8_t     TILE_SIZE   = 4;
constexpr uint8_t     TILES_X     = (GRID_WIDTH + TILE_SIZE - 1) / TILE_SIZE;
constexpr uint8_t     TILES_Y     = (GRID_HEIGHT + TILE_SIZE - 1) / TILE_SIZE;
constexpr uint8_t     TOTAL_TILES = TILES_X * TILES_Y;
constexpr uint8_t     TILES_TOTAL = TILES_X * TILES_Y;
constexpr uint8_t CELL_SIZE_SHIFT = 3;  // If CELL_SIZE is 8, use bit shift (8 = 2^3)
/*
constexpr uint8_t TILE_SIZE_SHIFT = 2;  // Only if TILE_SIZE is 4 (4 = 2^2)
*/
constexpr uint16_t GRID_AREA = GRID_WIDTH * GRID_HEIGHT;

// Duration for each mode in milliseconds
constexpr uint32_t MODE_CHANGE_INTERVAL = 60000; // 60 seconds

// Color change threshold for dirty marking
constexpr uint8_t COLOR_CHANGE_THRESHOLD = 8;



// Optimized coordinate functions
inline constexpr uint16_t coordToBitFast(const uint16_t x, const uint16_t y) {
  return (y << 4) + x;  // Assumes GRID_WIDTH is 16, use bit shift
}

inline constexpr uint16_t coordToIndexFast(const uint8_t x, const uint8_t y) {
  return (y << 4) + x;  // Same optimization
}

// Visual modes - including old test versions
enum VisualMode {
  MODE_CONWAY = 0,
  MODE_BRIGHT,
  MODE_FIRE,
  MODE_WATER,
  MODE_WIND,
  MODE_CYCLONE,
  MODE_PORTAL,
  MODE_WORMHOLE,
  MODE_PORTAL_OLD,    // Old test version
  MODE_WORMHOLE_OLD,  // Old test version
  MODE_WATER_OLD,     // Old test version
  MODE_WIND_OLD,      // Old test version
  MODE_COUNT
};

// Forward declarations
void     addConwayPatterns();
void     showModeTransition();
void     updateConway();
void     updateBright();
void     updateFire();
void     updateWater();
void     updateWind();
void     updateCyclone();
void     updatePortal();
void     updateWormhole();
void     updatePortalOld();
void     updateWormholeOld();
void     updateWaterOld();
void     updateWindOld();
void     initializeGrid();
void     showTitleScreen();

// Global state
static uint32_t rngState = 1;
VisualMode currentMode   = MODE_CONWAY;
uint32_t lastModeChange  = 0;

// OPTIMIZED BIT MANIPULATION FUNCTIONS
inline void setBit(uint8_t* array, const uint16_t bit) {
  const uint16_t byteIndex = bit >> 3;
  const uint8_t bitIndex   = bit & 7;
  array[byteIndex] |= (1 << bitIndex);
}

inline void clrBit(uint8_t* array, const uint16_t bit) {
  const uint16_t byteIndex = bit >> 3;
  const uint8_t bitIndex   = bit & 7;
  array[byteIndex] &= ~(1 << bitIndex);
}

inline bool getBit(const uint8_t* array, const uint16_t bit) {
  const uint16_t byteIndex = bit >> 3;
  const uint8_t bitIndex   = bit & 7;
  return (array[byteIndex] >> bitIndex) & 1;
}

// Memory arrays - optimized for ESP8266's limited RAM
constexpr uint16_t GRID_BYTES = (GRID_WIDTH * GRID_HEIGHT + 7) / 8;
uint8_t currentGrid[GRID_BYTES];
uint8_t nextGrid[GRID_BYTES];
constexpr uint8_t DIRTY_BYTES = (TOTAL_TILES + 7) / 8;
uint8_t dirtyTiles[DIRTY_BYTES];
uint8_t lastGrid[GRID_BYTES];

// Intensity grid for complex modes (0-255 per cell)
uint8_t  intensityGrid[GRID_WIDTH * GRID_HEIGHT];

// Color buffer for optimization - track last rendered colors
uint16_t lastColorBuffer[GRID_WIDTH * GRID_HEIGHT];

// Shared buffer union to save RAM
union {
  uint8_t lastIntensityBuffer[GRID_WIDTH * GRID_HEIGHT];
  uint8_t neighborCount[GRID_WIDTH * GRID_HEIGHT];
} sharedBuffer;

// Color palettes
const uint16_t firePalette[] = {
  0x0000, // Black (no fire)
  0x8000, // Dark red (ember glow)
  0x8800, // Red (low flame)
  0xC800, // Bright red (medium flame)
  0xE800, // Red-orange (hot flame)
  0xF800, // Orange (flame body)
  0xFC00, // Orange-yellow (flame tip)
  0xFE00, // Yellow-orange (hot flame tip)
  0xFF00, // Yellow (flame core)
  0xFF80, // Light yellow (bright flame)
  0xFFC0, // Pale yellow (hottest part)
  0xFFE0, // Near white-yellow (flame peak)
  0xFFFF  // White (flame core/spark)
};

const uint16_t waterPalette[] = {
  0x0000, // Black (deep water/no water)
  0x0008, // Very dark blue (deep ocean)
  0x0010, // Dark blue (deep water)
  0x0018, // Medium dark blue (underwater)
  0x001F, // Pure blue (water body)
  0x041F, // Blue with hint of green (shallow water)
  0x081F, // Blue-green (water surface)
  0x0C1F, // Light blue-green (shallow water)
  0x101F, // Cyan-blue (water surface)
  0x141F, // Light cyan-blue (water highlights)
  0x181F, // Bright cyan-blue (surface reflection)
  0x1C1F, // Very bright cyan-blue (water sparkle)
  0x07FF  // Pure cyan (water surface highlight)
};

const uint16_t windPalette[] = {
  0x0000, // Black (no wind/still air)
  0x0410, // Dark gray-blue (light breeze)
  0x0820, // Gray-blue (gentle wind)
  0x0C30, // Medium gray-blue (moderate wind)
  0x1040, // Blue-gray (strong breeze)
  0x1450, // Light blue-gray (strong wind)
  0x1860, // Medium blue-gray (high wind)
  0x1C70, // Bright blue-gray (very strong wind)
  0x2080, // Light blue (gale force)
  0x2490, // Brighter blue (storm wind)
  0x28A0, // Sky blue (hurricane force)
  0x2CB0, // Light sky blue (extreme wind)
  0x30C0  // Pale blue (maximum wind intensity)
};

const uint16_t cyclonePalette[] = {
  0x0000, // Black (calm/eye of storm)
  0x4000, // Dark red (storm formation)
  0x8000, // Red (developing storm)
  0xC000, // Bright red (strong storm)
  0xF800, // Orange-red (severe storm)
  0xF810, // Red-orange (hurricane cat 1)
  0xF820, // Orange (hurricane cat 2)
  0xF830, // Bright orange (hurricane cat 3)
  0xF840, // Yellow-orange (hurricane cat 4)
  0xF850, // Light orange (hurricane cat 5)
  0xF860, // Pale orange (extreme hurricane)
  0xF870, // Very pale orange (super hurricane)
  0xFFFF  // White (maximum storm intensity)
};

const uint16_t portalPalette[] = {
  0x0000, // Black (no portal energy)
  0x8010, // Dark purple (portal formation)
  0x8020, // Purple (weak portal energy)
  0x8030, // Medium purple (building energy)
  0x8810, // Purple-red (active portal)
  0x9010, // Red-purple (strong portal)
  0x9820, // Purple-magenta (high energy)
  0xA030, // Magenta-purple (very active)
  0xA810, // Red-magenta (portal peak)
  0xB010, // Bright magenta (maximum energy)
  0xB820, // Light magenta (portal overflow)
  0xC030, // Very bright magenta (energy burst)
  0xF81F  // Pure magenta (portal core)
};

const uint16_t brightPalette[] = {
  0x0000, // Black (off/background)
  0x001F, // Pure blue (cool accent)
  0x07E0, // Pure green (nature accent)
  0x07FF, // Cyan (water accent)
  0xF800, // Pure red (fire accent)
  0xF81F, // Magenta (energy accent)
  0xFFE0, // Yellow (sun accent)
  0xFFFF, // White (maximum brightness)
  0x8410, // Gray (neutral tone)
  0xFD20, // Orange (warm accent)
  0x8000, // Dark red (deep accent)
  0x0400, // Dark green (forest accent)
  0xFFFF  // White (secondary maximum)
};

const uint16_t wormholePalette[] = {
  0x0000, // Black (empty space)
  0x0010, // Very dark blue (space edge)
  0x0030, // Dark blue (space distortion)
  0x0050, // Medium blue (mild distortion)
  0x0070, // Blue (space warp)
  0x4010, // Blue-purple (wormhole edge)
  0x8030, // Purple-blue (wormhole entrance)
  0xC050, // Purple (wormhole tunnel)
  0xF070, // Red-purple (high energy warp)
  0xF890, // Orange-purple (extreme warp)
  0xFCB0, // Yellow-orange (wormhole core)
  0xFED0, // Light yellow (energy peak)
  0xFFFF  // White (wormhole center/exit)
};

// Mode names stored in PROGMEM
const char  mode0[] = "Dirty GoLife";
const char  mode1[] = "Dynamic Color";
const char  mode2[] = "Fire";
const char  mode3[] = "Water";
const char  mode4[] = "Wind";
const char  mode5[] = "Cyclone";
const char  mode6[] = "Portal";
const char  mode7[] = "Wormhole";
const char  mode8[] = "Portal Classic";
const char  mode9[] = "Wormhole Classic";
const char mode10[] = "Water Classic";
const char mode11[] = "Wind Classic";

const char* const modeNames[] = {
  mode0, mode1, mode2, mode3, mode4, mode5, mode6, mode7, mode8, mode9, mode10, mode11
};

// OPTIMIZED MATH FUNCTIONS - Using lookup tables for ESP8266
constexpr uint8_t SIN_TABLE_SIZE = 64;
const int8_t sinTable[SIN_TABLE_SIZE] = {
  0, 6, 12, 18, 25, 31, 37, 43, 49, 54, 60, 65, 71, 76, 81, 85,
  90, 94, 98, 102, 106, 109, 112, 115, 117, 120, 122, 124, 126, 127, 128, 129,
  129, 129, 128, 127, 126, 124, 122, 120, 117, 115, 112, 109, 106, 102, 98, 94,
  90, 85, 81, 76, 71, 65, 60, 54, 49, 43, 37, 31, 25, 18, 12, 6
};

inline int8_t fastSin8(uint8_t angle) {
  return pgm_read_byte(&sinTable[angle & (SIN_TABLE_SIZE - 1)]);
}

inline int8_t fastCos8(uint8_t angle) {
  return fastSin8(angle + (SIN_TABLE_SIZE >> 2));
}

// Fast integer square root
inline uint8_t fastSqrt8(uint16_t value) {
  if (value < 4) return value >> 1;
  uint8_t result = 0;
  uint8_t bit = 64; // 2^6
  while (bit > value) bit >>= 2;
  while (bit != 0) {
    if (value >= result + bit) {
      value -= result + bit;
      result = (result >> 1) + bit;
    } else {
      result >>= 1;
    }
    bit >>= 2;
  }
  return result;
}

// Fast random number generator (xorshift)
inline uint32_t fastRandom() {
  rngState ^= rngState << 13;
  rngState ^= rngState >> 17;
  rngState ^= rngState << 5;
  return rngState;
}

void initRandomSeed() {
  uint32_t seed = 0;
  for (uint8_t i = 0; i < 32; i++) {
    seed = (seed << 1) | (analogRead(A0) & 1);
    delay(1);
  }
  rngState = seed ? seed : 12345;
}

// Grid functions
inline uint16_t coordToBit(const uint16_t x, const uint16_t y) {
  return y * GRID_WIDTH + x;
}

inline uint16_t coordToIndex(const uint8_t x, const uint8_t y) {
  return y * GRID_WIDTH + x;
}

inline bool getCellState(const uint8_t* grid, const uint8_t x, const uint8_t y) {
  if (x >= GRID_WIDTH || y >= GRID_HEIGHT) return false;
  return getBit(grid, coordToBit(x, y));
}

inline void setCellState(uint8_t* grid, const uint8_t x, const uint8_t y, const bool alive) {
  if (x >= GRID_WIDTH || y >= GRID_HEIGHT) return;
  const uint16_t bit = coordToBit(x, y);
  if (alive) setBit(grid, bit);
  else clrBit(grid, bit);
}

inline uint8_t getIntensity(const uint8_t x, const uint8_t y) {
  if (x >= GRID_WIDTH || y >= GRID_HEIGHT) return 0;
  return intensityGrid[coordToIndex(x, y)];
}

inline void setIntensity(const uint8_t x, const uint8_t y, const uint8_t intensity) {
  if (x >= GRID_WIDTH || y >= GRID_HEIGHT) return;
  intensityGrid[coordToIndex(x, y)] = intensity;
}

// Color buffer functions for optimization
inline uint16_t getLastColor(const uint8_t x, const uint8_t y) {
  if (x >= GRID_WIDTH || y >= GRID_HEIGHT) return 0;
  return lastColorBuffer[coordToIndex(x, y)];
}

inline void setLastColor(const uint8_t x, const uint8_t y, const uint16_t color) {
  if (x >= GRID_WIDTH || y >= GRID_HEIGHT) return;
  lastColorBuffer[coordToIndex(x, y)] = color;
}

inline uint8_t getLastIntensity(const uint8_t x, const uint8_t y) {
  if (x >= GRID_WIDTH || y >= GRID_HEIGHT) return 0;
  return sharedBuffer.lastIntensityBuffer[coordToIndex(x, y)];
}

inline void setLastIntensity(const uint8_t x, const uint8_t y, const uint8_t intensity) {
  if (x >= GRID_WIDTH || y >= GRID_HEIGHT) return;
  sharedBuffer.lastIntensityBuffer[coordToIndex(x, y)] = intensity;
}

// Optimized color distance calculation
inline uint8_t colorDistance(const uint16_t color1, const uint16_t color2) {
  if (color1 == color2) return 0;
  
  const uint8_t r1 = (color1 >> 11) & 0x1F;
  const uint8_t g1 = (color1 >> 5) & 0x3F;
  const uint8_t b1 = color1 & 0x1F;
  
  const uint8_t r2 = (color2 >> 11) & 0x1F;
  const uint8_t g2 = (color2 >> 5) & 0x3F;
  const uint8_t b2 = color2 & 0x1F;
  
  const uint8_t dr = (r1 > r2) ? (r1 - r2) : (r2 - r1);
  const uint8_t dg = (g1 > g2) ? (g1 - g2) : (g2 - g1);
  const uint8_t db = (b1 > b2) ? (b1 - b2) : (b2 - b1);
  
  return dr + (dg >> 1) + db;
}

// Dirty rendering functions
inline uint8_t getTileIndex(const uint8_t tileX, const uint8_t tileY) {
  return tileY * TILES_X + tileX;
}

inline void markTileDirty(const uint8_t tileX, const uint8_t tileY) {
  if (tileX >= TILES_X || tileY >= TILES_Y) return;
  setBit(dirtyTiles, getTileIndex(tileX, tileY));
}

inline bool isTileDirty(const uint8_t tileX, const uint8_t tileY) {
  if (tileX >= TILES_X || tileY >= TILES_Y) return false;
  return getBit(dirtyTiles, getTileIndex(tileX, tileY));
}

inline void clearTileDirty(const uint8_t tileX, const uint8_t tileY) {
  if (tileX >= TILES_X || tileY >= TILES_Y) return;
  clrBit(dirtyTiles, getTileIndex(tileX, tileY));
}

void clearAllDirtyTiles() {
  memset(dirtyTiles, 0, DIRTY_BYTES);
}

void markAllTilesDirty() {
  memset(dirtyTiles, 0xFF, DIRTY_BYTES);
}

void markCellDirty(const uint8_t x, const uint8_t y) {
  markTileDirty(x / TILE_SIZE, y / TILE_SIZE);
}

// Auto mode cycling function
void updateModeRotation() {
  const uint32_t currentTime = millis();
  
  if (currentTime - lastModeChange >= MODE_CHANGE_INTERVAL) {
    const VisualMode previousMode = currentMode;
    currentMode    = (VisualMode)((currentMode + 1) % MODE_COUNT);
    lastModeChange = currentTime;
    
    // Handle Conway's dirty effect specially
    if (previousMode == MODE_CONWAY && currentMode != MODE_CONWAY) {
      tft.fillScreen(ST77XX_BLACK);
      memset(lastColorBuffer, 0, sizeof(lastColorBuffer));
    } else if (currentMode == MODE_CONWAY && previousMode != MODE_CONWAY) {
      // Preserve screen for Conway's dirty effect
    } else if (currentMode != MODE_CONWAY) {
      tft.fillScreen(ST77XX_BLACK);
      memset(lastColorBuffer, 0, sizeof(lastColorBuffer));
    }
    
    // Clear data structures
    memset(currentGrid, 0, GRID_BYTES);
    memset(lastGrid, 0, GRID_BYTES);
    memset(intensityGrid, 0, sizeof(intensityGrid));
    memset(&sharedBuffer, 0, sizeof(sharedBuffer));
    
    markAllTilesDirty();
    initializeGrid();
    showModeTransition();
  }
}

// Show mode transition with text overlay
void showModeTransition() {
  tft.fillRect(10, DISPLAY_HEIGHT/2 - 15, DISPLAY_WIDTH - 20, 30, ST77XX_BLUE);
  tft.setTextColor(ST77XX_WHITE);
  tft.setTextSize(1);
  
  char buffer[20];
  strcpy_P(buffer, (char*)pgm_read_ptr(&(modeNames[currentMode])));
  
  const uint16_t textWidth = strlen(buffer) * 6;
  const uint16_t x = (DISPLAY_WIDTH - textWidth) / 2;
  
  tft.setCursor(x, DISPLAY_HEIGHT/2 - 4);
  tft.print(buffer);
  delay(1500);
}

// Add Conway patterns
void addConwayPatterns() {
  const uint8_t patternType = fastRandom() % 5;
  const uint8_t startX      = fastRandom() % (GRID_WIDTH - 6);
  const uint8_t startY      = fastRandom() % (GRID_HEIGHT - 6);
  
  switch (patternType) {
    case 0: // Glider
      setCellState(currentGrid, startX + 1, startY,     true);
      setCellState(currentGrid, startX + 2, startY + 1, true);
      setCellState(currentGrid, startX,     startY + 2, true);
      setCellState(currentGrid, startX + 1, startY + 2, true);
      setCellState(currentGrid, startX + 2, startY + 2, true);
      break;
    case 1: // Blinker
      setCellState(currentGrid, startX,     startY + 1, true);
      setCellState(currentGrid, startX + 1, startY + 1, true);
      setCellState(currentGrid, startX + 2, startY + 1, true);
      break;
    case 2: // Block
      setCellState(currentGrid, startX,     startY,     true);
      setCellState(currentGrid, startX + 1, startY,     true);
      setCellState(currentGrid, startX,     startY + 1, true);
      setCellState(currentGrid, startX + 1, startY + 1, true);
      break;
    case 3: // Toad
      setCellState(currentGrid, startX + 1, startY,     true);
      setCellState(currentGrid, startX + 2, startY,     true);
      setCellState(currentGrid, startX + 3, startY,     true);
      setCellState(currentGrid, startX,     startY + 1, true);
      setCellState(currentGrid, startX + 1, startY + 1, true);
      setCellState(currentGrid, startX + 2, startY + 1, true);
      break;
    case 4: // Random cluster
      for (uint8_t i = 0; i < 6; i++) {
        const uint8_t x = startX + (fastRandom() % 4);
        const uint8_t y = startY + (fastRandom() % 4);
        setCellState(currentGrid, x, y, true);
      }
      break;
  }
}

// Conway's Game of Life
void updateConway() {
  static uint16_t stagnationCounter = 0;
  static uint16_t lastLiveCells = 0;
  
  // Pre-compute neighbor counts
  memset(&sharedBuffer, 0, sizeof(sharedBuffer));
  
  for (uint8_t y = 0; y < GRID_HEIGHT; y++) {
    for (uint8_t x = 0; x < GRID_WIDTH; x++) {
      if (getCellState(currentGrid, x, y)) {
        for (int8_t dy = -1; dy <= 1; dy++) {
          for (int8_t dx = -1; dx <= 1; dx++) {
            if (dx == 0 && dy == 0) continue;
            const uint8_t nx = (x + dx + GRID_WIDTH) % GRID_WIDTH;
            const uint8_t ny = (y + dy + GRID_HEIGHT) % GRID_HEIGHT;
            sharedBuffer.neighborCount[coordToIndex(nx, ny)]++;
          }
        }
      }
    }
  }
  
  // Apply Conway's rules
  memset(nextGrid, 0, GRID_BYTES);
  uint16_t liveCells = 0;
  
  for (uint8_t y = 0; y < GRID_HEIGHT; y++) {
    for (uint8_t x = 0; x < GRID_WIDTH; x++) {
      const uint8_t neighbors = sharedBuffer.neighborCount[coordToIndex(x, y)];
      const     bool    alive = getCellState(currentGrid, x, y);
      const     bool newState = alive ? (neighbors == 2 || neighbors == 3) : (neighbors == 3);
      
      if (newState) {
        setCellState(nextGrid, x, y, true);
        liveCells++;
      }
    }
  }
  
  // Check for stagnation
  if (liveCells == lastLiveCells) {
    stagnationCounter++;
  } else {
    stagnationCounter = 0;
  }
  
  if (stagnationCounter > 8 || liveCells < 5) {
    addConwayPatterns();
    stagnationCounter = 0;
  }
  
  lastLiveCells = liveCells;
  memcpy(currentGrid, nextGrid, GRID_BYTES);
}

// Enhanced dynamic bright colors
void updateBright() {
  static uint8_t pulsePhase = 0;
  static uint8_t colorCycle = 0;
  static uint8_t patternMode = 0;
  static uint16_t modeCounter = 0;
  static uint8_t subPattern = 0;
  
  pulsePhase++;
  colorCycle++;
  modeCounter++;
  
  // constexpr compile-time constants
  constexpr uint16_t patternInterval    = 180;
  constexpr uint16_t subPatternInterval = 60;
  constexpr uint8_t  maxPatterns        = 6;
  constexpr uint8_t  maxSubPatterns     = 3;
  constexpr uint8_t  gridCenterX        = GRID_WIDTH >> 1;
  constexpr uint8_t  gridCenterY        = GRID_HEIGHT >> 1;
  constexpr uint16_t minIntensity       = 50;
  constexpr uint16_t maxIntensity       = 255;
  constexpr uint8_t  aliveThreshold     = 60;
  
  // Pattern mode cycling
  if ((modeCounter & (patternInterval - 1)) == 0) [[unlikely]] {
    patternMode = (patternMode + 1) % maxPatterns;
    subPattern = 0;
  }
  
  if ((modeCounter & (subPatternInterval - 1)) == 0) [[unlikely]] {
    subPattern = (subPattern + 1) % maxSubPatterns;
  }
  
  // Pre-calculate pulse values once per frame
  const int16_t globalPulse       = fastSin8(pulsePhase >> 2) + 128;
  const int16_t secondaryPulse    = fastCos8((pulsePhase >> 1) + 64) + 128;
  const uint8_t colorCycleShifted = colorCycle >> 1;
  
  for (uint8_t y = 0; y < GRID_HEIGHT; y++) {
    // Cache row-specific calculations
    const int8_t       dy = y - gridCenterY;
    const uint8_t yShift2 = y << 2;
    const uint8_t yShift1 = y << 1;
    const uint8_t   cellY = y >> 1;
    
    for (uint8_t x = 0; x < GRID_WIDTH; x++) {
      const int8_t       dx = x - gridCenterX;
      const uint8_t xShift2 = x << 2;
      const uint8_t xShift1 = x << 1;
      const uint8_t   cellX = x >> 1;
      
      uint16_t intensity = 0;
      
      switch (patternMode) {
        case 0: { // Enhanced radial pulse
          const uint8_t distance   = fastSqrt8(dx*dx + dy*dy);
          const int16_t radialWave = fastSin8(distance - (pulsePhase >> 1)) + 128;
          const int16_t harmonic   = fastSin8((distance << 1) - pulsePhase) + 128;
          /*uint16_t*/  intensity  = (globalPulse + radialWave + (harmonic >> 1)) / 3;
          break;
        }
        
        case 1: { // Interference patterns
          const int16_t        wave1 = fastSin8(xShift2 + pulsePhase) + 128;
          const int16_t        wave2 = fastCos8(yShift2 + (pulsePhase >> 1)) + 128;
          const int16_t interference = (wave1 * wave2) >> 8;
          /*uint16_t*/     intensity = (globalPulse + interference) >> 1;
          break;
        }
        
        case 2: { // Spiral pattern
          const uint8_t distance = fastSqrt8(dx*dx + dy*dy);
          uint8_t angle = 0;
          
          if (abs(dx) > abs(dy)) {
            angle = (dx > 0) ? (32 + ((dy << 2) / dx)) : (96 - ((dy << 2) / dx));
          } else if (dy != 0) {
            angle = (dy > 0) ? (64 - ((dx << 2) / dy)) : (128 + ((dx << 2) / dy));
          }
          
          const int16_t   spiral = fastSin8(angle + (distance << 2) + pulsePhase) + 128;
          /*uint16_t*/ intensity = (globalPulse + spiral) >> 1;
          break;
        }
        
        case 3: { // Plasma effect
          const int16_t plasma = 
            fastSin8(xShift1 + pulsePhase) +
            fastSin8(yShift1 + (pulsePhase >> 1)) +
            fastSin8(((x + y) << 1) + (pulsePhase >> 2)) +
            fastSin8(fastSqrt8(dx*dx + dy*dy) + pulsePhase);
          /*uint16_t*/ intensity = (plasma >> 2) + 128;
          break;
        }
        
        case 4: { // Cellular pattern
          const uint8_t cellState = ((cellX + cellY + (pulsePhase >> 3)) & 0x07);
          const int16_t  cellWave = fastSin8(cellState << 5) + 128;
          /*uint16_t*/  intensity = (globalPulse + cellWave + secondaryPulse) / 3;
          break;
        }
        
        case 5: { // Mandala pattern
          const uint8_t distance = fastSqrt8(dx*dx + dy*dy);
          const int16_t  mandala = 
            fastSin8((distance << 3) + pulsePhase) +
            fastCos8((distance << 2) + (pulsePhase >> 1)) +
            fastSin8((distance << 4) + (pulsePhase << 1));
          /*uint16_t*/ intensity = ((mandala >> 2) + 128 + globalPulse) >> 1;
          break;
        }
      }
      
      // Sub-pattern modulation
      if (subPattern == 1) [[unlikely]] {
        const int16_t modulation = fastSin8((x + y + colorCycle) >> 1) + 128;
          /*uint16_t*/ intensity = (intensity * modulation) >> 8;
      } else if (subPattern == 2) [[unlikely]] {
         /*uint16_t*/ intensity += (fastSin8((x ^ y) + colorCycle) >> 3);
      }
      
      // Fixed clamping and threshold operation with proper type casting
      if (intensity < minIntensity) {
        /*uint16_t*/ intensity = 0;
      } else {
        /*uint16_t*/ intensity = min(intensity, maxIntensity);
      }
      
      setIntensity(x, y, (uint8_t)intensity);
      setCellState(currentGrid, x, y, intensity > aliveThreshold);
    }
  }
}

// Fire Animation
void updateFire() {
    static uint8_t windOffset = 0;
    static uint32_t cachedRandom = 0;
    static uint8_t randomCounter = 0;
    
    windOffset++;
    
    // Cache random values for multiple uses
    if ((randomCounter & 0x07) == 0) {
        cachedRandom = fastRandom();
    }
    randomCounter++;
    
    // Single big wick spanning most of bottom row
    constexpr uint8_t wickStart = 4;
    constexpr uint8_t wickEnd = GRID_WIDTH - 5;
    constexpr uint8_t wickIntensity = 255;
    
    // Bottom row - big continuous wick
    for (uint8_t x = 0; x < GRID_WIDTH; x++) {
        if (x >= wickStart && x <= wickEnd) {
            // Main wick area - always burning
            setIntensity(x, GRID_HEIGHT - 1, wickIntensity);
            setCellState(currentGrid, x, GRID_HEIGHT - 1, true);
        } else {
            // Edges - occasional sparks
            if (((cachedRandom >> (x & 7)) & 0x07) == 0) {
                setIntensity(x, GRID_HEIGHT - 1, 180);
                setCellState(currentGrid, x, GRID_HEIGHT - 1, true);
            } else {
                setIntensity(x, GRID_HEIGHT - 1, 0);
                setCellState(currentGrid, x, GRID_HEIGHT - 1, false);
            }
        }
    }
    
    // Pre-calculate wind effect once per frame
    const int8_t baseWind = fastSin8(windOffset) >> 6;
    
    // Optimized heat propagation
    for (uint8_t y = 0; y < GRID_HEIGHT - 1; y++) {
        const uint8_t nextRow = y + 1;
        const int8_t rowWind = baseWind + ((int8_t)(fastSin8(windOffset + y) >> 7));
        
        for (uint8_t x = 0; x < GRID_WIDTH; x++) {
            uint16_t newIntensity = 0;
            uint8_t samples = 0;
            
            // Heat from below (primary)
            const uint8_t belowHeat = getIntensity(x, nextRow);
            newIntensity += belowHeat + (belowHeat << 1); // belowHeat * 3
            samples += 3;
            
            // Side heat
            if (x > 0) {
                newIntensity += getIntensity(x - 1, y);
                samples++;
            }
            if (x < GRID_WIDTH - 1) {
                newIntensity += getIntensity(x + 1, y);
                samples++;
            }
            
            // Average
            if (samples > 0) {
                newIntensity /= samples;
            }
            
            // Decay and randomness
            newIntensity = (newIntensity * 220) >> 8; // ~86% retention
            
            // Optimized randomness using cached value
            const uint8_t randomBits = (cachedRandom >> ((x + y) & 0x0F)) & 0x0F;
            newIntensity += randomBits - 8;
            
            // Wind effect
            if (rowWind != 0) {
                const int16_t windX = x + rowWind;
                if (windX >= 0 && windX < GRID_WIDTH) {
                    const uint8_t windHeat = getIntensity(windX, nextRow);
                    newIntensity += windHeat >> 3;
                }
            }
            
            // Clamp
            if (newIntensity > 255) newIntensity = 255;
            
            setIntensity(x, y, newIntensity);
            setCellState(currentGrid, x, y, newIntensity > 30);
        }
    }
}

// Water effect
void updateWater() {
  static uint8_t wavePhase = 0;
  static uint8_t rippleTimer = 0;
  static uint8_t rippleCenterX = GRID_WIDTH / 2;
  static uint8_t rippleCenterY = GRID_HEIGHT / 2;
  static uint8_t secondaryRippleX = GRID_WIDTH / 3;
  static uint8_t secondaryRippleY = 2 * GRID_HEIGHT / 3;
  
  wavePhase++;
  rippleTimer++;
  
  // constexpr optimizations
  constexpr uint8_t rippleInterval1 = 140;
  constexpr uint8_t rippleInterval2 = 190;
  constexpr uint8_t rippleOffset2 = 50;
  constexpr uint8_t maxRippleDist1 = 12;
  constexpr uint8_t maxRippleDist2 = 8;
  constexpr uint16_t baseWaterLevel = 140;
  constexpr uint8_t minWaterLevel = 60;
  constexpr uint8_t maxWaterLevel = 255;
  constexpr uint8_t aliveThreshold = 80;
  
  // Dynamic ripple sources
  if (rippleTimer % rippleInterval1 == 0) {
    rippleCenterX = (GRID_WIDTH / 4) + (fastRandom() % (GRID_WIDTH / 2));
    rippleCenterY = (GRID_HEIGHT / 4) + (fastRandom() % (GRID_HEIGHT / 2));
  }
  
  if (rippleTimer % rippleInterval2 == rippleOffset2) {
    secondaryRippleX = (GRID_WIDTH / 6) + (fastRandom() % (2 * GRID_WIDTH / 3));
    secondaryRippleY = (GRID_HEIGHT / 6) + (fastRandom() % (2 * GRID_HEIGHT / 3));
  }
  
  // Pre-calculate wave phases
  const uint8_t wavePhaseShifted1 = wavePhase >> 1;
  
  for (uint8_t y = 0; y < GRID_HEIGHT; y++) {
    for (uint8_t x = 0; x < GRID_WIDTH; x++) {
      
      // Base wave patterns - FIXED coordinate usage
      const int16_t horizontalWave = fastSin8((x * 4 + wavePhase) >> 2);
      const int16_t verticalWave = fastCos8((y * 3 + wavePhaseShifted1) >> 2);
      const int16_t diagonalWave = fastSin8(((x + y) * 2 + wavePhase) >> 3);
      
      // Primary ripple - CORRECTED coordinate system
      const int8_t dx1 = x - rippleCenterX; // X stays X
      const int8_t dy1 = y - rippleCenterY; // Y stays Y
      const uint8_t dist1 = fastSqrt8(dx1*dx1 + dy1*dy1);
      
      int16_t ripple1 = 0;
      if (dist1 < maxRippleDist1) {
        ripple1 = (fastSin8((dist1 << 3) - wavePhaseShifted1) * (maxRippleDist1 - dist1)) >> 3;
      }
      
      // Secondary ripple - CORRECTED coordinate system
      const int8_t dx2 = x - secondaryRippleX; // X stays X
      const int8_t dy2 = y - secondaryRippleY; // Y stays Y
      const uint8_t dist2 = fastSqrt8(dx2*dx2 + dy2*dy2);
      
      int16_t ripple2 = 0;
      if (dist2 < maxRippleDist2) {
        ripple2 = (fastCos8((dist2 << 4) - wavePhase) * (maxRippleDist2 - dist2)) >> 3;
      }
      
      // Standing wave pattern
      const int16_t standingWave = (fastSin8((x << 2) + wavePhase) * 
                                   fastCos8((y << 2) + (wavePhase >> 2))) >> 8;
      
      // Wave combination
      int16_t intensity = baseWaterLevel;
      intensity += (horizontalWave * 25) >> 8;
      intensity += (verticalWave * 30) >> 8;
      intensity += (diagonalWave * 20) >> 8;
      intensity += ripple1 + ripple2 + standingWave;
      
      // Surface tension simulation (smoothing)
      if (x > 0 || x < GRID_WIDTH-1 || y > 0 || y < GRID_HEIGHT-1) {
        const uint16_t smoothing = 
          (x > 0 ? getIntensity(x-1, y) : intensity) +
          (x < GRID_WIDTH-1 ? getIntensity(x+1, y) : intensity) +
          (y > 0 ? getIntensity(x, y-1) : intensity) +
          (y < GRID_HEIGHT-1 ? getIntensity(x, y+1) : intensity);
        
        intensity = (intensity * 3 + (smoothing >> 2)) >> 2;
      }
      
      // Clamp values
      if (intensity < minWaterLevel) intensity = minWaterLevel;
      if (intensity > maxWaterLevel) intensity = maxWaterLevel;
      
      setIntensity(x, y, intensity);
      setCellState(currentGrid, x, y, intensity > aliveThreshold);
    }
  }
}

// Wind effect
void updateWind() {
  static uint8_t windPhase    = 0;
  static uint8_t gustTimer    = 0;
  static uint8_t gustCenterX  = GRID_WIDTH / 2;
  static uint8_t gustStrength = 0;
  static uint8_t eddyPhase    = 0;
  
  windPhase++;
  gustTimer++;
  eddyPhase++;
  
  // constexpr compile-time constants
  constexpr  uint8_t gustInterval     = 90;
  constexpr  uint8_t gustDecay        = 45;
  constexpr uint16_t baseWindLevel    = 90;
  constexpr uint16_t minWindLevel     = 50;
  constexpr uint16_t maxWindLevel     = 255;
  constexpr  uint8_t aliveThreshold   = 110;
  constexpr  int16_t windMultiplier1  = 40;
  constexpr  int16_t windMultiplier2  = 25;
  constexpr  int16_t windMultiplier3  = 30;
  constexpr  uint8_t gustRadius       = 6;
  constexpr  uint8_t eddyRadius       = 5;
  constexpr  uint8_t eddyMinRadius    = 1;
  constexpr  uint8_t streamMask       = 4; // 5 - 1 for bit masking
  constexpr  uint8_t streamThreshold  = 2;
  constexpr  int16_t streamMultiplier = 20;
  constexpr  uint8_t randomMask       = 0x0F;
  constexpr   int8_t randomOffset     = 8;
  constexpr  uint8_t eddyCenterX      = (GRID_WIDTH * 2) / 3;
  constexpr  uint8_t eddyCenterY      = GRID_HEIGHT / 2;
  
  // Dynamic gust system with bit operations
  if ((gustTimer & (gustInterval - 1)) == 0) [[unlikely]] {
    gustCenterX = (GRID_WIDTH >> 2) + (fastRandom() & (GRID_WIDTH >> 1));
    gustStrength = 40 + (fastRandom() & 0x3F);
  } else if ((gustTimer & (gustDecay - 1)) == 0) [[unlikely]] {
    gustStrength >>= 1;
  }
  
  // Pre-calculate common values
  const uint8_t windPhaseShifted1 = windPhase >> 1;
  const uint8_t windPhaseShifted2 = windPhase >> 2;
  const bool hasGust = gustStrength > 0;
  
  for (uint8_t y = 0; y < GRID_HEIGHT; y++) {
    // Cache row-specific calculations
    const uint8_t yMult2 = y << 1;
    const uint8_t streamY = (y + windPhaseShifted2) & streamMask;
    const bool inStream = streamY < streamThreshold;
    const int8_t eddyDy = y - eddyCenterY;
    
    for (uint8_t x = 0; x < GRID_WIDTH; x++) {
      uint16_t intensity = baseWindLevel;
      
      // Primary horizontal wind
      const int16_t primaryWind = fastSin8((x + windPhaseShifted1) >> 1);
      intensity += (primaryWind * windMultiplier1) >> 8;
      
      // Vertical wind shear
      const int16_t windShear = fastCos8((yMult2 + windPhase) >> 2);
      intensity += (windShear * windMultiplier2) >> 8;
      
      // Turbulence
      const int16_t turbulence = fastSin8(((x * 3 + yMult2) ^ windPhase) >> 2);
      intensity += (turbulence * windMultiplier3) >> 8;
      
      // Optimized gust effects
      if (hasGust) [[unlikely]] {
        const uint8_t distToGust = abs((int)x - (int)gustCenterX);
        if (distToGust < gustRadius) [[likely]] {
          const uint16_t gustEffect = (gustStrength * (gustRadius - distToGust) * 
                                     (fastSin8(windPhaseShifted1 + (distToGust << 4)) + 128)) >> 12;
          intensity += gustEffect;
        }
      }
      
      // Optimized eddy calculation
      const int8_t eddyDx = x - eddyCenterX;
      const uint8_t eddyDist = fastSqrt8(eddyDx*eddyDx + eddyDy*eddyDy);
      
      if (eddyDist < eddyRadius && eddyDist > eddyMinRadius) [[unlikely]] {
        const uint8_t eddyAngle = eddyPhase + (eddyDist << 4);
        const int16_t eddyIntensity = (fastSin8(eddyAngle) * (eddyRadius - eddyDist)) >> 3;
        intensity += eddyIntensity;
      }
      
      // Wind streaks
      if (inStream) [[likely]] {
        const int16_t streamIntensity = fastSin8((x << 1) + windPhase);
        intensity += (streamIntensity * streamMultiplier) >> 8;
      }
      
      // Optimized randomness
      intensity += (fastRandom() & randomMask) - randomOffset;
      
      // Fixed clamping with proper type casting
      intensity = max((uint16_t)minWindLevel, min(intensity, (uint16_t)maxWindLevel));
      
      setIntensity(x, y, (uint8_t)intensity);
      setCellState(currentGrid, x, y, intensity > aliveThreshold);
    }
  }
}

// Cyclone effect
void updateCyclone() {
  static uint8_t rotationAngle = 0;
  static uint8_t eyeSize       = 2;
  static    bool eyeGrowing    = true;
  
  rotationAngle += 2;
  
  if (eyeGrowing) {
    eyeSize++;
    if (eyeSize > 4) eyeGrowing = false;
  } else {
    eyeSize--;
    if (eyeSize < 1) eyeGrowing = true;
  }
  
  constexpr uint8_t centerX = GRID_WIDTH / 2;
  constexpr uint8_t centerY = GRID_HEIGHT / 2;
  
  for (uint8_t y = 0; y < GRID_HEIGHT; y++) {
    for (uint8_t x = 0; x < GRID_WIDTH; x++) {
      const int8_t        dx = x - centerX;
      const int8_t        dy = y - centerY;
      const uint8_t distance = fastSqrt8(dx*dx + dy*dy);
      
      uint16_t intensity = 0;
      
      if (distance <= eyeSize) {
        intensity = 30 + (fastSin8(rotationAngle >> 2) >> 3);
      } else {
        uint8_t angle = rotationAngle + (distance << 1);
        
        if (dx > 0 && dy >= 0) angle += 0;
        else if (dx <= 0 && dy > 0) angle += 16;
        else if (dx < 0 && dy <= 0) angle += 32;
        else angle += 48;
        
        const int16_t  spiralIntensity = fastSin8(angle) + 128;
        const uint16_t windSpeed       = (distance < 9) ? (255 - (distance * 20)) : 50;
        
        intensity = (spiralIntensity * windSpeed) >> 9;
        
        const uint8_t armAngle = (angle >> 2) & 0x3F;
        if (armAngle < 16 || (armAngle > 32 && armAngle < 48)) {
          intensity += 50;
        }
      }
      
      if (intensity > 255) intensity = 255;
      
      setIntensity(x, y, intensity);
      setCellState(currentGrid, x, y, intensity > 50);
    }
  }
}

// Portal effect
void updatePortal() {
  static uint8_t ringPhase      = 0;
  static uint8_t secondaryPhase = 0;
  static uint8_t energyPulse    = 0;
  
  ringPhase++;
  secondaryPhase++;
  energyPulse++;
  
  constexpr uint8_t centerX = GRID_WIDTH / 2;
  constexpr uint8_t centerY = GRID_HEIGHT / 2;
  
  for (uint8_t y = 0; y < GRID_HEIGHT; y++) {
    for (uint8_t x = 0; x < GRID_WIDTH; x++) {
      const int8_t dx        = x - centerX;
      const int8_t dy        = y - centerY;
      const uint8_t distance = fastSqrt8(dx*dx + dy*dy);
      
      uint16_t intensity = 0;
      
      const uint8_t ring1 = (ringPhase >> 2) & 0x0F;
      const uint8_t ring2 = (ringPhase >> 1) & 0x0F;
      const uint8_t ring3 = ringPhase & 0x0F;
      
      bool onRing           = false;
      uint8_t ringIntensity = 0;
      
      if (abs(distance - ring1) <= 1) {
        onRing        = true;
        ringIntensity = 180;
      }
      if (abs(distance - ring2) <= 1) {
        onRing        = true;
        ringIntensity = max(ringIntensity, (uint8_t)220);
      }
      if (abs(distance - ring3) <= 1) {
        onRing        = true;
        ringIntensity = max(ringIntensity, (uint8_t)255);
      }
      
      if (onRing) {
        intensity = ringIntensity;
        const uint8_t shimmerAngle = (dx + dy + energyPulse) & 0x3F;
        const int16_t shimmer = (fastSin8(shimmerAngle) >> 3) + 16;
        intensity = (intensity * shimmer) >> 4;
      }
      
      if (distance <= 2) {
        const uint16_t centerEnergy = 150 + ((fastSin8(energyPulse >> 1) + 128) >> 2);
        intensity = max(intensity, centerEnergy);
      }
      
      if (distance > 2 && distance < 10 && !onRing) {
        const uint8_t swirlAngle = (distance * 8) + secondaryPhase;
        const int16_t swirl = fastSin8(swirlAngle) + 128;
        if (swirl > 200) {
          intensity = max(intensity, (uint16_t)100);
        }
      }
      
      if (intensity > 255) intensity = 255;
      
      setIntensity(x, y, intensity);
      setCellState(currentGrid, x, y, intensity > 60);
    }
  }
}

// Wormhole effect
void updateWormhole() {
  static uint8_t tunnelPhase   = 0;
  static uint8_t rotationAngle = 0;
  static uint8_t pulsePhase    = 0;
  static uint8_t depthCycle    = 0;
  
  tunnelPhase += 2;
  rotationAngle++;
  pulsePhase++;
  depthCycle++;
  
  constexpr uint8_t centerX = GRID_WIDTH / 2;
  constexpr uint8_t centerY = GRID_HEIGHT / 2;
  
  for (uint8_t y = 0; y < GRID_HEIGHT; y++) {
    for (uint8_t x = 0; x < GRID_WIDTH; x++) {
      const int8_t dx = x - centerX;
      const int8_t dy = y - centerY;
      const uint8_t distance = fastSqrt8(dx*dx + dy*dy);
      
      uint16_t intensity = 0;
      
      if (distance > 0) {
        const uint16_t tunnelDepth = tunnelPhase + (200 / (distance + 4));
        uint8_t spiralAngle = rotationAngle + (tunnelDepth >> 3);
        
        if (dx >= 0 && dy >= 0) spiralAngle += 0;
        else if (dx < 0 && dy >= 0) spiralAngle += 16;
        else if (dx < 0 && dy < 0) spiralAngle += 32;
        else spiralAngle += 48;
        
        const int16_t primarySpiral   = fastSin8(spiralAngle) + 128;
        const int16_t secondarySpiral = fastCos8((spiralAngle >> 1) + 16) + 128;
        const int16_t depthRings      = fastSin8((tunnelDepth >> 2) + pulsePhase) + 128;
        
        intensity = (primarySpiral * 50 + secondarySpiral * 30 + depthRings * 40) >> 7;
        
        if (distance > 3) {
          intensity = (intensity * (15 - min((uint8_t)distance, (uint8_t)12))) / 15;
        }
        
        if (distance <= 2) {
          const uint16_t vortexEnergy = 200 + ((fastSin8(depthCycle + tunnelPhase) + 128) >> 3);
          intensity = max(intensity, vortexEnergy);
        }
        
        if (distance >= 4 && distance <= 8) {
          const uint8_t wallPattern = fastSin8(tunnelDepth >> 1) + 128;
          if (wallPattern > 180) {
            intensity += 80;
          }
        }
        
        if (distance > 8 && (fastRandom() & 0x7F) == 0) {
          intensity = 255;
        }
        
        if (intensity > 255) intensity = 255;
        if (intensity < 40) intensity = 0;
      }
      
      setIntensity(x, y, intensity);
      setCellState(currentGrid, x, y, intensity > 40);
    }
  }
}

// Old Portal test version
void updatePortalOld() {
  static    bool expanding         = true;
  static uint8_t portalSize        = 3;
  static uint8_t lastPortalSize    = 3;
  static uint8_t ringThickness     = 1;
  static    bool backgroundCleared = false;
  
  if (expanding) {
    portalSize++;
    if (portalSize > 10) {
      expanding = false;
    }
  } else {
    portalSize--;
    if (portalSize < 2) {
      expanding = true;
      backgroundCleared = false;
    }
  }
  
  uint8_t centerX = GRID_WIDTH / 2;
  uint8_t centerY = GRID_HEIGHT / 2;
  
  if (!expanding && !backgroundCleared) {
    for (uint8_t y = 0; y < GRID_HEIGHT; y++) {
      for (uint8_t x = 0; x < GRID_WIDTH; x++) {
        setIntensity(x, y, 0);
        setCellState(currentGrid, x, y, false);
      }
    }
    backgroundCleared = true;
  }
  
  if (expanding && portalSize > lastPortalSize) {
    for (uint8_t y = 0; y < GRID_HEIGHT; y++) {
      for (uint8_t x = 0; x < GRID_WIDTH; x++) {
        int8_t dx = x - centerX;
        int8_t dy = y - centerY;
        uint8_t distance = fastSqrt8(dx*dx + dy*dy);
        
        if (distance > lastPortalSize - ringThickness && distance <= portalSize - ringThickness) {
          if ((x + y) % 3 == 0) {
            setIntensity(x, y, 120);
            setCellState(currentGrid, x, y, true);
          }
        }
      }
    }
  }
  
  if (!expanding && portalSize < lastPortalSize) {
    for (uint8_t y = 0; y < GRID_HEIGHT; y++) {
      for (uint8_t x = 0; x < GRID_WIDTH; x++) {
        int8_t dx = x - centerX;
        int8_t dy = y - centerY;
        uint8_t distance = fastSqrt8(dx*dx + dy*dy);
        
        if (distance <= lastPortalSize + ringThickness && distance >= portalSize + ringThickness) {
          setIntensity(x, y, 0);
          setCellState(currentGrid, x, y, false);
        }
      }
    }
  }
  
  // Draw portal ring
  for (uint8_t y = 0; y < GRID_HEIGHT; y++) {
    for (uint8_t x = 0; x < GRID_WIDTH; x++) {
      int8_t dx = x - centerX;
      int8_t dy = y - centerY;
      uint8_t distance = fastSqrt8(dx*dx + dy*dy);
      
      if (distance >= (portalSize - ringThickness) && distance <= (portalSize + ringThickness)) {
        uint8_t distanceFromRing = abs((int)distance - (int)portalSize);
        
        if (distanceFromRing <= ringThickness) {
          uint8_t intensity = 255 - (distanceFromRing * 100 / (ringThickness + 1));
          
          // Shimmer effect
          uint8_t shimmerAngle = (dx + dy + (millis() >> 4)) & 0x3F;
          int16_t shimmer = (fastSin8(shimmerAngle) >> 2) + 32;
          intensity = (intensity * shimmer) >> 5;
          
          if (intensity > 255) intensity = 255;
          if (intensity < 100) intensity = 100;
          
          setIntensity(x, y, intensity);
          setCellState(currentGrid, x, y, true);
        }
      }
    }
  }
  
  lastPortalSize = portalSize;
}

// Old Wormhole test version
void updateWormholeOld() {
  static uint16_t  tunnelDepth = 0;
  static uint8_t rotationAngle = 0;
  
  tunnelDepth += 3;
  rotationAngle++;
  
  uint8_t centerX = GRID_WIDTH / 2;
  uint8_t centerY = GRID_HEIGHT / 2;
  
  uint8_t maxDistance = fastSqrt8((GRID_WIDTH/2) * (GRID_WIDTH/2) + (GRID_HEIGHT/2) * (GRID_HEIGHT/2));
  
  for (uint8_t y = 0; y < GRID_HEIGHT; y++) {
    for (uint8_t x = 0; x < GRID_WIDTH; x++) {
      int8_t dx = x - centerX;
      int8_t dy = y - centerY;
      uint8_t distance = fastSqrt8(dx * dx + dy * dy);
      
      uint8_t intensity = 0;
      
      if (distance > 0) {
        uint16_t depth = tunnelDepth + (100 / (distance + 1));
        
        uint8_t angle = rotationAngle + (depth >> 2);
        if (dx > 0 && dy >= 0) angle += 0;
        else if (dx <= 0 && dy > 0) angle += 16;
        else if (dx < 0 && dy <= 0) angle += 32;
        else angle += 48;
        
        int16_t spiralPattern = fastSin8(angle * 3);
        int16_t ringPattern = fastSin8(depth >> 1);
        
        int16_t combinedPattern = (spiralPattern + ringPattern) >> 1;
        
        if (combinedPattern > 20) {
          intensity = (combinedPattern + 128) >> 1;
          
          uint8_t falloff = 255;
          if (distance > 3) {
            falloff = 255 - ((distance - 3) * 255 / (maxDistance - 3));
          }
          
          intensity = (intensity * falloff) >> 8;
          
          if (intensity > 0 && intensity < 30) {
            intensity = 30;
          }
          
          if (intensity > 255) intensity = 255;
        }
      } else {
        intensity = 255;
      }
      
      setIntensity(x, y, intensity);
      setCellState(currentGrid, x, y, intensity > 0);
    }
  }
}

// Old Water test version
void updateWaterOld() {
  static uint16_t wavePhase = 0;
  wavePhase += 3;
  
  for (uint8_t y = 0; y < GRID_HEIGHT; y++) {
    for (uint8_t x = 0; x < GRID_WIDTH; x++) {
      int16_t wave = fastSin8((x + y + wavePhase) >> 2) + 128;
      wave += (fastRandom() % 20) - 10;
      
      if (wave < 0) wave = 0;
      if (wave > 255) wave = 255;
      
      setIntensity(x, y, wave);
      setCellState(currentGrid, x, y, wave > 100);
    }
  }
}

// Old Wind test version  
void updateWindOld() {
  static uint8_t windOffset = 0;
  windOffset++;
  
  for (uint8_t y = 0; y < GRID_HEIGHT; y++) {
    for (uint8_t x = 0; x < GRID_WIDTH; x++) {
      uint8_t pattern = ((x + windOffset) % 6) + ((y + windOffset/2) % 3) * 2;
      uint8_t intensity = (pattern * 40) + (fastRandom() % 40);
      if (intensity > 255) intensity = 255;
      
      setIntensity(x, y, intensity);
      setCellState(currentGrid, x, y, intensity > 80);
    }
  }
}

// Get color based on current mode
uint16_t getModeColor(const uint8_t x, const uint8_t y, const uint16_t generation, const bool alive) {
  const uint16_t* palette;
  uint8_t paletteSize;
  
  switch (currentMode) {
    case MODE_FIRE:
      palette     = firePalette;
      paletteSize = 13;
      break;
    case MODE_WATER:
    case MODE_WATER_OLD:
      palette     = waterPalette;
      paletteSize = 13;
      break;
    case MODE_WIND:
    case MODE_WIND_OLD:
      palette     = windPalette;
      paletteSize = 13;
      break;
    case MODE_CYCLONE:
      palette     = cyclonePalette;
      paletteSize = 13;
      break;
    case MODE_PORTAL:
    case MODE_PORTAL_OLD:
      palette     = portalPalette;
      paletteSize = 13;
      break;
    case MODE_WORMHOLE:
    case MODE_WORMHOLE_OLD:
      palette     = wormholePalette;
      paletteSize = 13;
      break;
    case MODE_BRIGHT:
      palette     = brightPalette;
      paletteSize = 13;
      break;
    default: // Conway
      palette     = brightPalette;
      paletteSize = 8;
      break;
  }
  
  uint8_t colorIndex;
  if (currentMode == MODE_CONWAY) {
    if (!alive) {
      return getLastColor(x, y);
    }
    colorIndex = ((x + y + generation) % 7) + 1;
  } else {
    const uint8_t intensity = getIntensity(x, y);
    if (intensity == 0) return ST77XX_BLACK;
    colorIndex = (intensity * (paletteSize - 1)) / 255;
  }
  
  return pgm_read_word(&palette[colorIndex]);
}

// Smart change detection with color and intensity comparison
void detectChangesAndMarkDirty() {
  for (uint8_t y = 0; y < GRID_HEIGHT; y++) {
    for (uint8_t x = 0; x < GRID_WIDTH; x++) {
      const bool currentState = getCellState(currentGrid, x, y);
      const bool lastState = getBit(lastGrid, coordToBit(x, y));
      
      bool needsUpdate = false;
      
      if (currentMode == MODE_CONWAY) {
        if (currentState != lastState) {
          needsUpdate = true;
        }
      } else {
        const uint8_t currentIntensity = getIntensity(x, y);
        const uint8_t lastIntensity = getLastIntensity(x, y);
        
        const uint8_t intensityDiff = (currentIntensity > lastIntensity) ? 
                                     (currentIntensity - lastIntensity) : 
                                     (lastIntensity - currentIntensity);
        
        if (intensityDiff >= COLOR_CHANGE_THRESHOLD) {
          needsUpdate = true;
        }
        
        const bool wasVisible = lastIntensity > 30;
        const bool isVisible = currentIntensity > 30;
        if (wasVisible != isVisible) {
          needsUpdate = true;
        }
        
        setLastIntensity(x, y, currentIntensity);
      }
      
      if (needsUpdate) {
        markCellDirty(x, y);
      }
      
      if (currentState) {
        setBit(lastGrid, coordToBit(x, y));
      } else {
        clrBit(lastGrid, coordToBit(x, y));
      }
    }
  }
}

// Optimized tile rendering with SPI burst
inline void renderTile(const uint8_t tileX, const uint8_t tileY, const uint16_t generation) {
  constexpr uint8_t tileSize = TILE_SIZE;
  const uint8_t startX = tileX << 2;  // Multiply by 4 using bit shift
  const uint8_t startY = tileY << 2;
  // Fixed type casting for min() function
  const uint8_t endX = min((uint8_t)(startX + tileSize), GRID_WIDTH);
  const uint8_t endY = min((uint8_t)(startY + tileSize), GRID_HEIGHT);
  
  // Process in memory-friendly order
  for (uint8_t y = startY; y < endY; y++) {
    const uint16_t baseScreenY = Y_OFFSET + (y << CELL_SIZE_SHIFT);
    
    for (uint8_t x = startX; x < endX; x++) {
      const bool currentState = getCellState(currentGrid, x, y);
      const uint16_t newColor = getModeColor(x, y, generation, currentState);
      const uint16_t lastColor = getLastColor(x, y);
      
      // Optimized color comparison
      if (newColor != lastColor && colorDistance(newColor, lastColor) >= COLOR_CHANGE_THRESHOLD) {
        const uint16_t screenX = X_OFFSET + (x << CELL_SIZE_SHIFT);
        tft.fillRect(screenX, baseScreenY, CELL_SIZE, CELL_SIZE, newColor);
        setLastColor(x, y, newColor);
      }
    }
  }
}

// Update display with enhanced change detection
void updateDisplay(const uint16_t generation) {
  static bool firstRun = true;
  
  if (firstRun) {
    tft.fillScreen(ST77XX_BLACK);
    memset(lastColorBuffer, 0, sizeof(lastColorBuffer));
    memset(&sharedBuffer, 0, sizeof(sharedBuffer));
    firstRun = false;
  }
  
  detectChangesAndMarkDirty();
  
  for (uint8_t tileY = 0; tileY < TILES_Y; tileY++) {
    for (uint8_t tileX = 0; tileX < TILES_X; tileX++) {
      if (isTileDirty(tileX, tileY)) {
        renderTile(tileX, tileY, generation);
        clearTileDirty(tileX, tileY);
      }
    }
  }
}

// Initialize Conway mode with intentional dirty setup
void initializeConwayMode() {
  if (currentMode == MODE_CONWAY) {
    memset(currentGrid, 0, GRID_BYTES);
    memset(lastGrid,    0, GRID_BYTES);
    
    addConwayPatterns();
    addConwayPatterns();
    
    for (uint8_t i = 0; i < 8; i++) {
      const uint8_t x = fastRandom() % GRID_WIDTH;
      const uint8_t y = fastRandom() % GRID_HEIGHT;
      setCellState(currentGrid, x, y, true);
    }
    
    markAllTilesDirty();
  }
}

// Initialize grid based on mode
void initializeGrid() {
  if (currentMode == MODE_CONWAY) {
    initializeConwayMode();
  } else {
    memset(currentGrid,     0, GRID_BYTES);
    memset(lastGrid,        0, GRID_BYTES);
    memset(intensityGrid,   0, sizeof(intensityGrid));
    memset(&sharedBuffer,   0, sizeof(sharedBuffer));
    memset(lastColorBuffer, 0, sizeof(lastColorBuffer));
    markAllTilesDirty();
  }
}

// Enhanced title screen
void showTitleScreen() {
  tft.fillScreen(ST77XX_BLACK);
  
  for (uint8_t frame = 0; frame < 120; frame++) {
    
    tft.setTextSize(1);
    if (frame >= 15) {
      tft.setTextColor(ST77XX_CYAN);
      if (frame >= 20) { tft.setCursor(20, 30); tft.print(F("Core1D")); }
      if (frame >= 25) { tft.setCursor(20, 42); tft.print(F("Labs")); }
    }
    
    if (frame >= 40) {
      tft.setTextColor(ST77XX_MAGENTA);
      tft.setTextSize(2);
      if (frame >= 45) { tft.setCursor(15, 60); tft.print(F("PIXEL")); }
      if (frame >= 50) { tft.setCursor(25, 80); tft.print(F("WASH")); }
      if (frame >= 55) { 
        tft.setTextSize(1);
        tft.setCursor(20, 100); 
        tft.print(F("v2.8 ESP8266"));
      }
    }
    
    if (frame >= 70) {
      for (uint8_t i = 0; i < 15; i++) {
        const uint16_t x = fastRandom() % DISPLAY_WIDTH;
        const uint16_t y = fastRandom() % DISPLAY_HEIGHT;
        
        if ((y >= 25 && y <= 110)) {
          if (x >= 15 && x <= 113) continue;
        }
        
        const uint16_t color = pgm_read_word(&brightPalette[(fastRandom() % 7) + 1]);
        tft.drawPixel(x, y, color);
      }
      
      if (frame % 3 == 0) {
        for (uint8_t i = 0; i < 20; i++) {
          const uint16_t x = fastRandom() % DISPLAY_WIDTH;
          const uint16_t y = fastRandom() % DISPLAY_HEIGHT;
          
          if ((y >= 25 && y <= 110)) {
            if (x >= 15 && x <= 113) continue;
          }
          
          tft.drawPixel(x, y, ST77XX_BLACK);
        }
      }
    }
    
    delay(25);
  }
  
  delay(1500);
  tft.fillScreen(ST77XX_BLACK);
}

void setup() {
  Serial.begin(115200);
  
  initRandomSeed();
  
  // Initialize SPI TFT display - FIXED INITR CONSTANT
  // Possible values: INITR_GREENTAB, INITR_REDTAB, and INITR_BLACKTAB
  // They represent the different revisions of the ST7735 Chip, with the different hardware standards followed by the manufacturers
  tft.initR(INITR_GREENTAB);  // Use INITR_GREENTAB for 128x128 displays
  tft.setRotation(2);
  
  tft.fillScreen(ST77XX_BLUE);
  delay(300);
  tft.fillScreen(ST77XX_BLACK);
  
  showTitleScreen();
  
  clearAllDirtyTiles();
  initializeGrid();
  
  lastModeChange = millis();
}

void loop() {
  static uint16_t generation      = 0;
  static uint32_t lastUpdate      = 0;
  static uint32_t lastConwayBoost = 0;
  
  const uint32_t currentTime = millis();
  
  updateModeRotation();
  
  // Conway runs slower for better visual effect
  const uint8_t updateInterval = (currentMode == MODE_CONWAY) ? 120 : 50;
  
  if (currentTime - lastUpdate >= updateInterval) {
    lastUpdate = currentTime;
    
    switch (currentMode) {
      case MODE_CONWAY: 
        updateConway(); 
        // Periodic pattern injection for Conway
        if (currentTime - lastConwayBoost > 6000) {
          if ((fastRandom() & 0x0F) == 0) { // 1 in 16 chance
            addConwayPatterns();
            lastConwayBoost = currentTime;
          }
        }
        break;
      case MODE_BRIGHT:        updateBright();        break;
      case MODE_FIRE:          updateFire();          break;
      case MODE_WATER:         updateWater();         break;
      case MODE_WIND:          updateWind();          break;
      case MODE_CYCLONE:       updateCyclone();       break;
      case MODE_PORTAL:        updatePortal();        break;
      case MODE_WORMHOLE:      updateWormhole();      break;
      case MODE_PORTAL_OLD:    updatePortalOld();     break;
      case MODE_WORMHOLE_OLD:  updateWormholeOld();   break;
      case MODE_WATER_OLD:     updateWaterOld();      break;
      case MODE_WIND_OLD:      updateWindOld();       break;
    }
    
    updateDisplay(generation);
    generation++;
    
    if (generation > 30000) generation = 1000;
  }
  
  // Yield to prevent watchdog reset
  yield();
}

Next part I will cover a Weather Clock using some of these GFX functions.

Good morning.

Following is a working and tested WeatherClock Module with the same D1R1 ESP8266 with GREENTAB 1.44 SPI TFT hardware setup, using some of the previous animation functions from before (slightly modified and improved), while keeping stubs for rest for any custom animation integration.

Pixel Wash D1 R1 1.44 ST7735 SPI ESP8266 Weather Clock

To test and switch between animation modes, you can type โ€œnextโ€œ in the Serial Monitor of Arduino IDE.
To test all the animations, type โ€œtestโ€œ.

WeMos D1 R1 (ESP8266)          ST7735 1.44" TFT
     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
     โ”‚                     โ”‚         โ”‚                 โ”‚
     โ”‚  [USB-C]       3V3 โ—โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”คโ— VCC            โ”‚
     โ”‚                     โ”‚         โ”‚                 โ”‚
     โ”‚  [WIFI ANTENNA]     โ”‚         โ”‚                 โ”‚
     โ”‚                     โ”‚         โ”‚                 โ”‚
     โ”‚              D7/13 โ—โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”คโ— SDA (MOSI)     โ”‚
     โ”‚              D5/14 โ—โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”คโ— SCK (CLK)      โ”‚
     โ”‚               D2/4 โ—โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”คโ— CS             โ”‚
     โ”‚               D3/0 โ—โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”คโ— RST            โ”‚
     โ”‚               D4/2 โ—โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”คโ— DC (A0)        โ”‚
     โ”‚                     โ”‚         โ”‚                 โ”‚
     โ”‚                GND โ—โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”คโ— GND            โ”‚
     โ”‚                     โ”‚         โ”‚                 โ”‚
     โ”‚    [ESP8266-12E]    โ”‚         โ”‚ [128x128 LCD]   โ”‚
     โ”‚    [80MHz/160MHz]   โ”‚         โ”‚ [65K Colors]    โ”‚
     โ”‚    [4MB Flash]      โ”‚         โ”‚ [SPI Interface] โ”‚
     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                โ”‚                              โ”‚
                โ””โ”€โ”€โ”€โ”€ [ Xtensa LX106 Core ] โ”€โ”€โ”€โ”˜
                      [ 2.4GHz WiFi Radio ]

Source Code:

/**************************************************************
* Pixel Wash Weather HyprClock - ESP8266 WeMos D1R1 + 128x128 ST7735
* Core1D Automation Labs - Weather Clock with Pixel Wash Animations
* 
* Features:
* - Top Half: Dynamic animations (cycles hourly)
* - Bottom Half: Time, date, weather info
* - Serial "test" command for animation debugging
* - Optimized for performance and memory safety
**************************************************************/

#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <ArduinoJson.h>
#include <time.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include <SPI.h>

// === CONFIGURATION ===
const char*             SSID = "YourWiFiSSID";
const char*         PASSWORD = "YourWiFiPassword";
const char*  WEATHER_API_KEY = "YourFreeAPIKey";  // Obtain your free API key from the website
const char* WEATHER_BASE_URL = "http://api.openweathermap.org/data/2.5/weather?lat=23.8315&lon=91.2868&appid=";

// === HARDWARE PINS ===
#define TFT_CS  D2  // GPIO4
#define TFT_DC  D4  // GPIO2  
#define TFT_RST D3  // GPIO0

Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST);

// === DISPLAY LAYOUT ===
constexpr uint16_t  SCREEN_WIDTH = 128;
constexpr uint16_t SCREEN_HEIGHT = 128;
constexpr uint16_t   ANIM_HEIGHT = 64;     // Top half for animations
constexpr uint16_t   INFO_HEIGHT = 64;     // Bottom half for info
constexpr uint16_t  INFO_START_Y = 64;

// === ANIMATION GRID Performance === 
constexpr uint8_t GRID_WIDTH  = 16;       
constexpr uint8_t GRID_HEIGHT = 8;      // Covers top half exactly
constexpr uint8_t CELL_SIZE   = 8;         // 16*8 = 128px width, 8*8 = 64px height
// The animations have been designed based on Balanced Config. Visual Signatures may vary in other settings.
/*
// === ANIMATION GRID Balannced === 
constexpr uint8_t GRID_WIDTH  = 32;       
constexpr uint8_t GRID_HEIGHT = 16;      // Covers top half exactly
constexpr uint8_t CELL_SIZE   = 4;         // 32*4 = 128px width, 16*4 = 64px height

// === ANIMATION GRID Performance === 
constexpr uint8_t GRID_WIDTH  = 16;       
constexpr uint8_t GRID_HEIGHT = 8;      // Covers top half exactly
constexpr uint8_t CELL_SIZE   = 8;         // 16*8 = 128px width, 8*8 = 64px height

// === ANIMATION GRID Quality === 
constexpr uint8_t GRID_WIDTH  = 64;       
constexpr uint8_t GRID_HEIGHT = 32;      // Covers top half exactly
constexpr uint8_t CELL_SIZE   = 2;         // 64*2 = 128px width, 32*2 = 64px height
*/

// === ANIMATION MODES ===
enum VisualMode {
    MODE_CONWAY = 0,
    MODE_BRIGHT,
    MODE_FIRE, 
    MODE_WATER,
    MODE_WIND,
    MODE_CYCLONE,
    MODE_PORTAL,
    MODE_WORMHOLE,
    MODE_COUNT
};

const char* modeNames[] = {
    "Conway GoL", 
    "Bright Pulse", 
    "Fire Effect", 
    "Water Waves", 
    "RGB Pixel Rain",
    "Cyclone Spin", 
    "Portal Rings", 
    "Wormhole Tunnel"
};

// === GLOBAL STATE ===
VisualMode currentMode = MODE_CONWAY;
uint32_t      rngState = 1;

// === TIMING VARIABLES ===
unsigned long lastModeChange    = 0;
unsigned long lastWeatherUpdate = 0; 
unsigned long lastTimeUpdate    = 0;
unsigned long lastAnimUpdate    = 0;
unsigned long lastInfoUpdate    = 0;

// === SERIAL TEST MODE ===
bool                        serialTestActive = false;
unsigned long                serialTestStart = 0;
uint8_t                  serialTestModeIndex = 0;
constexpr unsigned long SERIAL_TEST_DURATION = 30000;

// === MEMORY ARRAYS ===
constexpr uint16_t GRID_BYTES = (GRID_WIDTH * GRID_HEIGHT + 7) / 8;
uint8_t  currentGrid[GRID_BYTES];
uint8_t  nextGrid[GRID_BYTES];
uint8_t  intensityGrid[GRID_WIDTH * GRID_HEIGHT];
uint16_t lastColorBuffer[GRID_WIDTH * GRID_HEIGHT];

// === GoL Dynamic Color Trackers ===
uint8_t cellAge[GRID_WIDTH * GRID_HEIGHT];           // Track cell age
uint8_t cellGeneration[GRID_WIDTH * GRID_HEIGHT];    // Track cell birth time
static uint32_t globalGeneration = 0;

// === DATA STRUCTURES ===
struct WeatherData {
    float  temperature = 25.0f;
    uint8_t   humidity = 50;
    String description = "Clear";
    bool         valid = false;
} weather;

struct TimeData {
    uint8_t hour12 = 12;
    uint8_t minute = 0;
    uint8_t second = 0;
    bool      isPM = false;
    String dayName = "Monday";
    String    date = "Jan 1";
    bool     valid = false;
} timeData;

// === COLOR PALETTES ===

// Fire Effect Palette - Transitions from black through red, orange, yellow to white (hottest)
const uint16_t firePalette[] = {
    0x0000,  // Black - No fire/cold
    0x8000,  // Dark red - Ember glow
    0x8800,  // Deep red-orange - Small flame
    0xC800,  // Red-orange - Growing flame
    0xE800,  // Bright red-orange - Active flame
    0xF800,  // Pure red - Hot flame base
    0xFC00,  // Red-orange - Flame body
    0xFE00,  // Orange - Bright flame
    0xFF00,  // Yellow-orange - Very hot flame
    0xFF80,  // Yellow - Intense heat
    0xFFC0,  // Bright yellow - Max heat
    0xFFE0,  // Pure yellow - White-hot core
    0xFFFF   // White - Hottest flame center
};

// Water Wave Palette - Transitions from black through deep blue to bright cyan
const uint16_t waterPalette[] = {
    0x0000,  // Black - No water/deep void
    0x0008,  // Very dark blue
    0x0010,  // Dark blue 
    0x0018,  // Medium dark blue
    0x001F,  // Pure blue
    0x041F,  // Blue with green tint
    0x081F,  // Blue-cyan 
    0x0C1F,  // Medium blue-cyan
    0x101F,  // Cyan-blue - Wave crests
    0x141F,  // Medium cyan - Foam formation
    0x181F,  // Bright cyan - Wave peaks
    0x1C1F,  // Very bright cyan - Spray/mist
    0x07FF   // Pure cyan - Brightest water highlights
};

// Bright Pulse Palette - Various bright colors for rainbow/pulse effects
const uint16_t brightPalette[] = {
    0x0000,  // Black - Background/off state
    0x001F,  // Pure blue
    0x07E0,  // Pure green 
    0x07FF,  // Cyan 
    0xF800,  // Pure red 
    0xF81F,  // Magenta 
    0xFFE0,  // Yellow
    0xFFFF,  // White - Maximum brightness
    0x8410,  // Gray - Neutral tone
    0xFD20,  // Orange - Warm accent
    0x8000,  // Dark red - Deep accent
    0x0400,  // Dark green 
    0xFFFF   // White - Peak intensity
};

// === FAST MATH LOOKUP TABLES ===
const int8_t sinTable[64] = {
    0, 6, 12, 18, 25, 31, 37, 43, 49, 54, 60, 65, 71, 76, 81, 85,
    90, 94, 98, 102, 106, 109, 112, 115, 117, 120, 122, 124, 126, 127, 128, 129,
    129, 129, 128, 127, 126, 124, 122, 120, 117, 115, 112, 109, 106, 102, 98, 94,
    90, 85, 81, 76, 71, 65, 60, 54, 49, 43, 37, 31, 25, 18, 12, 6
};

// === HELPER FUNCTIONS ===

// Fast sine/cosine using lookup table
inline int8_t fastSin8(uint8_t angle) {
    return pgm_read_byte(&sinTable[angle & 63]);
}

inline int8_t fastCos8(uint8_t angle) {
    return fastSin8(angle + 16);
}

// Fast random number generator
inline uint32_t fastRandom() {
    rngState ^= rngState << 13;
    rngState ^= rngState >> 17;
    rngState ^= rngState << 5;
    return rngState;
}

// Fast square root approximation
inline uint8_t fastSqrt8(uint16_t value) {
    if (value < 4) return value >> 1;
    uint8_t result = 0;
    uint8_t bit = 64;
    while (bit > value) bit >>= 2;
    while (bit != 0) {
        if (value >= result + bit) {
            value -= result + bit;
            result = (result >> 1) + bit;
        } else {
            result >>= 1;
        }
        bit >>= 2;
    }
    return result;
}

void debugStatus() {
    Serial.print(F("Mode: "));
    Serial.print(modeNames[currentMode]);
    Serial.print(F(", Time valid: "));
    Serial.print(timeData.valid ? "Yes" : "No");
    Serial.print(F(", Weather valid: "));
    Serial.print(weather.valid ? "Yes" : "No");
    Serial.print(F(", Free heap: "));
    Serial.println(ESP.getFreeHeap());
}

// === BIT MANIPULATION FOR GRID ===
inline void setBit(uint8_t* array, uint16_t bit) {
    array[bit >> 3] |= (1 << (bit & 7));
}

inline void clrBit(uint8_t* array, uint16_t bit) {
    array[bit >> 3] &= ~(1 << (bit & 7));
}

inline bool getBit(const uint8_t* array, uint16_t bit) {
    return (array[bit >> 3] >> (bit & 7)) & 1;
}

inline uint16_t coordToBit(uint8_t x, uint8_t y) {
    return y * GRID_WIDTH + x;
}

inline bool getCellState(const uint8_t* grid, uint8_t x, uint8_t y) {
    if (x >= GRID_WIDTH || y >= GRID_HEIGHT) return false;
    return getBit(grid, coordToBit(x, y));
}

inline void setCellState(uint8_t* grid, uint8_t x, uint8_t y, bool alive) {
    if (x >= GRID_WIDTH || y >= GRID_HEIGHT) return;
    if (alive) setBit(grid, coordToBit(x, y));
    else clrBit(grid, coordToBit(x, y));
}

inline uint8_t getIntensity(uint8_t x, uint8_t y) {
    if (x >= GRID_WIDTH || y >= GRID_HEIGHT) return 0;
    return intensityGrid[y * GRID_WIDTH + x];
}

inline void setIntensity(uint8_t x, uint8_t y, uint8_t intensity) {
    if (x >= GRID_WIDTH || y >= GRID_HEIGHT) return;
    intensityGrid[y * GRID_WIDTH + x] = intensity;
}

inline uint8_t getCellAge(uint8_t x, uint8_t y) {
    if (x >= GRID_WIDTH || y >= GRID_HEIGHT) return 0;
    return cellAge[y * GRID_WIDTH + x];
}

inline void setCellAge(uint8_t x, uint8_t y, uint8_t age) {
    if (x >= GRID_WIDTH || y >= GRID_HEIGHT) return;
    cellAge[y * GRID_WIDTH + x] = age;
}

inline uint8_t getCellGeneration(uint8_t x, uint8_t y) {
    if (x >= GRID_WIDTH || y >= GRID_HEIGHT) return 0;
    return cellGeneration[y * GRID_WIDTH + x];
}

inline void setCellGeneration(uint8_t x, uint8_t y, uint8_t gen) {
    if (x >= GRID_WIDTH || y >= GRID_HEIGHT) return;
    cellGeneration[y * GRID_WIDTH + x] = gen;
}

// === FUNCTION DECLARATIONS ===
void updateTime();
void fetchWeatherData();
void renderInfoDisplay();
void processSerialTestMode();
void switchToNextMode();
void switchMode(VisualMode newMode);

// === SETUP FUNCTION ===
void setup() {
    Serial.begin(115200);
    Serial.println(F("Pixel Wash Weather HyprClock v2.0"));
    Serial.println(F("Commands:"));
    Serial.println(F("  'test' - Start/stop animation test mode"));
    Serial.println(F("  'next' - Switch to next animation"));
    
    // Initialize random seed from analog noise
    rngState = (analogRead(A0) << 16) + millis() + ESP.getCycleCount();
    
    // Initialize display
    tft.initR(INITR_GREENTAB);
    tft.setRotation(2);
    tft.fillScreen(ST77XX_BLACK);
    
    // Enhanced loading screen with progress tracking
    tft.setTextColor(ST77XX_CYAN);
    tft.setTextSize(1);
    tft.setCursor(20, 20);
    tft.print(F("Pixel Wash"));
    tft.setCursor(15, 35);
    tft.print(F("Weather Clock"));
    tft.setTextColor(ST77XX_WHITE);
    tft.setCursor(30, 50);
    tft.print(F("Initializing..."));
    
    // Progress bar setup
    tft.drawRect(10, 70, 108, 8, ST77XX_WHITE);
    
    // Step 1: WiFi Connection (0-50%)
    tft.setCursor(20, 85);
    tft.print(F("Connecting WiFi..."));
    
    WiFi.begin(SSID, PASSWORD);
    uint8_t wifiAttempts = 0;
    while (WiFi.status() != WL_CONNECTED && wifiAttempts < 30) {
        delay(500);
        Serial.print(".");
        wifiAttempts++;
        
        // Update progress bar
        uint8_t progress = (wifiAttempts * 50) / 30;
        tft.fillRect(12, 72, progress, 4, ST77XX_GREEN);
    }
    
    if (WiFi.status() == WL_CONNECTED) {
        Serial.println(F(" Connected!"));
        tft.fillRect(12, 72, 54, 4, ST77XX_GREEN); // 50% complete
        tft.fillRect(0, 85, 128, 8, ST77XX_BLACK);
        tft.setCursor(25, 85);
        tft.setTextColor(ST77XX_GREEN);
        tft.print(F("WiFi Connected"));
    } else {
        Serial.println(F(" Failed!"));
        tft.fillRect(0, 85, 128, 8, ST77XX_BLACK);
        tft.setCursor(30, 85);
        tft.setTextColor(ST77XX_RED);
        tft.print(F("WiFi Failed"));
    }
    
    delay(500);
    
    // Step 2: Time Synchronization (50-80%) - IMPROVED
    tft.fillRect(0, 85, 128, 8, ST77XX_BLACK);
    tft.setTextColor(ST77XX_CYAN);
    tft.setCursor(25, 85);
    tft.print(F("Syncing Time..."));
    
    // === TIMEZONE CONFIGURATION OPTIONS ===
    // Uncomment ONE line below for your timezone, comment out the current IST line

    // === EUROPE ===
    // configTime(0, 0, "pool.ntp.org");           // UTC/GMT (London, Dublin) UTC+0:00
    // configTime(3600, 0, "pool.ntp.org");        // CET (Paris, Berlin, Rome) UTC+1:00
    // configTime(7200, 0, "pool.ntp.org");        // EET (Athens, Helsinki, Cairo) UTC+2:00
    // configTime(10800, 0, "pool.ntp.org");       // MSK (Moscow, Istanbul) UTC+3:00

    // === NORTH AMERICA ===
    // configTime(-18000, 0, "pool.ntp.org");      // EST (New York, Toronto, Miami) UTC-5:00
    // configTime(-21600, 0, "pool.ntp.org");      // CST (Chicago, Dallas, Mexico City) UTC-6:00
    // configTime(-25200, 0, "pool.ntp.org");      // MST (Denver, Phoenix) UTC-7:00
    // configTime(-28800, 0, "pool.ntp.org");      // PST (Los Angeles, Seattle, Vancouver) UTC-8:00

    // === ASIA-PACIFIC ===
    configTime(19800, 0, "pool.ntp.org");          // IST (Mumbai, Delhi, Kolkata) UTC+5:30 - CURRENT (used for Agartala)
    // configTime(16200, 0, "pool.ntp.org");       // MMT (Yangon, Myanmar) UTC+4:30
    // configTime(18000, 0, "pool.ntp.org");       // PKT (Karachi, Tashkent) UTC+5:00
    // configTime(20700, 0, "pool.ntp.org");       // NPT (Kathmandu, Nepal) UTC+5:45
    // configTime(21600, 0, "pool.ntp.org");       // BST (Dhaka, Almaty) UTC+6:00
    // configTime(25200, 0, "pool.ntp.org");       // ICT (Bangkok, Jakarta, Ho Chi Minh) UTC+7:00
    // configTime(28800, 0, "pool.ntp.org");       // CST (Beijing, Singapore, Manila) UTC+8:00
    // configTime(32400, 0, "pool.ntp.org");       // JST (Tokyo, Seoul) UTC+9:00
    // configTime(34200, 0, "pool.ntp.org");       // ACST (Adelaide, Darwin) UTC+9:30
    // configTime(36000, 0, "pool.ntp.org");       // AEST (Sydney, Melbourne, Brisbane) UTC+10:00

    // === MIDDLE EAST & AFRICA ===
    // configTime(7200, 0, "pool.ntp.org");        // SAST (Johannesburg, Cairo) UTC+2:00
    // configTime(10800, 0, "pool.ntp.org");       // EAT (Nairobi, Baghdad) UTC+3:00
    // configTime(12600, 0, "pool.ntp.org");       // IRST (Tehran, Iran) UTC+3:30
    // configTime(14400, 0, "pool.ntp.org");       // GST (Dubai, Abu Dhabi) UTC+4:00

    // === SOUTH AMERICA ===
    // configTime(-10800, 0, "pool.ntp.org");      // BRT (Sรฃo Paulo, Rio de Janeiro) UTC-3:00
    // configTime(-14400, 0, "pool.ntp.org");      // CLT (Santiago, La Paz) UTC-4:00
    // configTime(-18000, 0, "pool.ntp.org");      // COT (Bogotรก, Lima, Quito) UTC-5:00

    // === OCEANIA ===
    // configTime(43200, 0, "pool.ntp.org");       // NZST (Auckland, Wellington) UTC+12:00
    // configTime(39600, 0, "pool.ntp.org");       // NCT (Noumรฉa, New Caledonia) UTC+11:00

    
    // FIXED: Proper time synchronization with multiple attempts
    bool timeFullySynced = false;
    uint8_t timeAttempts = 0;
    
    while (timeAttempts < 40 && !timeFullySynced) { // Increase attempts to 40 (20 seconds)
        delay(500);
        timeAttempts++;
        
        // Check if time is valid and update timeData
        time_t rawTime;
        struct tm * timeInfo;
        time(&rawTime);
        timeInfo = localtime(&rawTime);
        
        if (timeInfo->tm_year > (2020 - 1900)) {
            // FIXED: Fully populate timeData during setup
            timeData.hour12 = timeInfo->tm_hour;
            if (timeData.hour12 == 0) {
                timeData.hour12 = 12;
                timeData.isPM = false;
            } else if (timeData.hour12 < 12) {
                timeData.isPM = false;
            } else if (timeData.hour12 == 12) {
                timeData.isPM = true;
            } else {
                timeData.hour12 -= 12;
                timeData.isPM = true;
            }
            
            timeData.minute = timeInfo->tm_min;
            timeData.second = timeInfo->tm_sec;
            
            // Day names
            const char* days[] = {"Sunday", "Monday", "Tuesday", "Wednesday", 
                                 "Thursday", "Friday", "Saturday"};
            timeData.dayName = days[timeInfo->tm_wday];
            
            // Date formatting with year
            const char* months[] = {"", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
                                   "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
            timeData.date = String(months[timeInfo->tm_mon + 1]) + " " + 
                           String(timeInfo->tm_mday) + " " +
                           String(timeInfo->tm_year + 1900);
            
            timeData.valid = true;
            timeFullySynced = true;
            
            Serial.println(F("Time fully synchronized!"));
            Serial.print(F("Current time: "));
            Serial.print(timeData.hour12);
            Serial.print(F(":"));
            if (timeData.minute < 10) Serial.print(F("0"));
            Serial.print(timeData.minute);
            Serial.println(timeData.isPM ? F(" PM") : F(" AM"));
        }
        
        // Update progress bar
        uint8_t progress = 54 + (timeAttempts * 26) / 40;
        tft.fillRect(12, 72, progress, 4, ST77XX_GREEN);
    }
    
    if (timeFullySynced) {
        tft.fillRect(0, 85, 128, 8, ST77XX_BLACK);
        tft.setTextColor(ST77XX_GREEN);
        tft.setCursor(30, 85);
        tft.print(F("Time Synced"));
    } else {
        tft.fillRect(0, 85, 128, 8, ST77XX_BLACK);
        tft.setTextColor(ST77XX_YELLOW);
        tft.setCursor(25, 85);
        tft.print(F("Time Timeout"));
        // Set a default time to prevent issues
        timeData.hour12 = 12;
        timeData.minute = 0;
        timeData.second = 0;
        timeData.isPM = false;
        timeData.dayName = "Unknown";
        timeData.date = "Unknown";
        timeData.valid = false;
    }
    
    delay(500);
    
    // Step 3: Weather Data (80-95%)
    tft.fillRect(0, 85, 128, 8, ST77XX_BLACK);
    tft.setTextColor(ST77XX_CYAN);
    tft.setCursor(20, 85);
    tft.print(F("Getting Weather..."));
    
    // Initial weather fetch
    fetchWeatherData();
    tft.fillRect(12, 72, 96, 4, ST77XX_GREEN); // 95% complete
    
    delay(500);
    
    // Step 4: Initialize Animation Data (95-100%)
    tft.fillRect(0, 85, 128, 8, ST77XX_BLACK);
    tft.setTextColor(ST77XX_CYAN);
    tft.setCursor(15, 85);
    tft.print(F("Starting Anims..."));
    
    // Initialize data structures
    memset(currentGrid, 0, GRID_BYTES);
    memset(nextGrid, 0, GRID_BYTES);
    memset(intensityGrid, 0, sizeof(intensityGrid));
    memset(lastColorBuffer, 0, sizeof(lastColorBuffer));
    memset(cellAge, 0, sizeof(cellAge));
    memset(cellGeneration, 0, sizeof(cellGeneration));
    globalGeneration = 0;
    
    // Initialize Conway patterns in memory only
    addConwayPatterns();
    
    // Complete progress bar
    tft.fillRect(12, 72, 104, 4, ST77XX_GREEN); // 100% complete
    
    delay(500);
    
    // Final ready message
    tft.fillRect(0, 85, 128, 8, ST77XX_BLACK);
    tft.setTextColor(ST77XX_GREEN);
    tft.setCursor(45, 85);
    tft.print(F("Ready!"));
    
    delay(1000);
    
    // FIXED: Clear screen and immediately render everything
    tft.fillScreen(ST77XX_BLACK);
    
    // Initialize animations
    updateConway();
    renderAnimation();
    
    // FIXED: Force immediate info display render if time is valid
    if (timeData.valid) {
        renderInfoDisplay();
        Serial.println(F("Info display rendered immediately"));
    }
    
    // FIXED: Set initial timing to prevent immediate retriggering
    lastModeChange    = millis();
    lastWeatherUpdate = millis() - 590000; // Schedule weather update in 10 seconds
    lastTimeUpdate    = millis();           // Current time
    lastInfoUpdate    = millis();           // Current time - prevents immediate retrigger
    lastAnimUpdate    = millis();           // Current time
    
    Serial.println(F("Setup complete! All systems ready."));
    debugStatus();
}

// === MAIN LOOP ===
void loop() {
    static uint32_t      frameCount = 0;
    static uint32_t lastDebugOutput = 0;
    unsigned long       currentTime = millis();
    
    // Handle serial test command (highest priority)
    processSerialTestMode();
    
    // Priority 1: Update time every second for accuracy
    if (currentTime - lastTimeUpdate >= 1000) {
        updateTime();
        lastTimeUpdate = currentTime;
        
        // Debug output every 30 seconds
        if (currentTime - lastDebugOutput >= 30000) {
            debugStatus();
            lastDebugOutput = currentTime;
        }
    }
    
    // Priority 2: Update weather every 10 minutes
    if (currentTime - lastWeatherUpdate >= 600000) {
        fetchWeatherData();
        lastWeatherUpdate = currentTime;
        Serial.println(F("Weather updated"));
    }
    
    // Priority 3: Mode cycling (every hour, unless in serial test)
    if (!serialTestActive && currentTime - lastModeChange >= 3600000) {
        switchToNextMode();
        lastModeChange = currentTime;
    }
    
    // Priority 4: Animation updates with optimized intervals
    uint16_t animInterval = (currentMode == MODE_CONWAY) ? 150 : 33; // 6.7fps vs 30fps
    if (currentTime - lastAnimUpdate >= animInterval) {
        // Update the current animation mode
        updateAnimation();
        
        // Render the animation to screen
        renderAnimation();
        
        lastAnimUpdate = currentTime;
        frameCount++;
        
        // Debug frame rate every 200 frames
        if (frameCount % 200 == 0) {
            Serial.print(F("Frames: "));
            Serial.print(frameCount);
            Serial.print(F(", Mode: "));
            Serial.print(modeNames[currentMode]);
            Serial.print(F(", Free RAM: "));
            Serial.print(ESP.getFreeHeap());
            Serial.println(F(" bytes"));
        }
    }
    
    // Priority 5: Info display updates (every 5 seconds, but only if time is valid)
    if (timeData.valid && currentTime - lastInfoUpdate >= 5000) {
        renderInfoDisplay();
        lastInfoUpdate = currentTime;
    }
    
    // Memory safety check every 60 seconds
    static uint32_t lastMemoryCheck = 0;
    if (currentTime - lastMemoryCheck >= 60000) {
        uint32_t freeHeap = ESP.getFreeHeap();
        if (freeHeap < 8192) { // Warning if less than 8KB free
            Serial.print(F("WARNING: Low memory - "));
            Serial.print(freeHeap);
            Serial.println(F(" bytes free"));
        }
        lastMemoryCheck = currentTime;
    }
    
    // Prevent watchdog timeout
    yield();
    
    // Small delay to prevent overwhelming the CPU
    delayMicroseconds(100);
}


// === SERIAL TEST MODE ===
void processSerialTestMode() {
    // Check for serial input
    if (Serial.available()) {
        String command = Serial.readStringUntil('\n');
        command.trim();
        
        if (command.equalsIgnoreCase("test")) {
            if (!serialTestActive) {
                // Start test mode
                serialTestActive = true;
                serialTestStart = millis();
                serialTestModeIndex = 0;
                switchMode((VisualMode)serialTestModeIndex);
                Serial.println(F("=== SERIAL TEST MODE ACTIVATED ==="));
                Serial.println(F("Testing all 8 animation modes (30s each)"));
                Serial.println(F("NEW: RGB Pixel Rain & Wormhole animations!"));
                Serial.println(F("Send 'test' again to exit early, 'next' to skip current"));
                Serial.print(F("Mode 1/8: "));
                Serial.println(modeNames[serialTestModeIndex]);
            } else {
                // Exit test mode early
                serialTestActive = false;
                switchMode(MODE_CONWAY);
                Serial.println(F("=== SERIAL TEST MODE EXITED ==="));
                Serial.println(F("Returned to Conway GoL"));
                return;
            }
        }
        // "next" command for manual animation switching
        else if (command.equalsIgnoreCase("next")) {
            if (serialTestActive) {
                // In test mode - advance to next test animation
                serialTestModeIndex++;
                if (serialTestModeIndex >= MODE_COUNT) {
                    serialTestActive = false;
                    switchMode(MODE_CONWAY);
                    Serial.println(F("=== SERIAL TEST COMPLETE ==="));
                    Serial.println(F("All modes tested - returned to Conway GoL"));
                } else {
                    switchMode((VisualMode)serialTestModeIndex);
                    serialTestStart = millis(); // Reset timer for new mode
                    Serial.print(F("Mode "));
                    Serial.print(serialTestModeIndex + 1);
                    Serial.print(F("/8: "));
                    Serial.println(modeNames[serialTestModeIndex]);
                    
                    // Special announcements for new modes
                    if (serialTestModeIndex == MODE_WIND) {
                        Serial.println(F("  -> NEW: RGB Pixel Rain with colorful trails!"));
                    } else if (serialTestModeIndex == MODE_WORMHOLE) {
                        Serial.println(F("  -> NEW: Wormhole with pulsing concentric rings!"));
                    }
                }
            } else {
                // Normal mode - switch to next animation and reset hourly timer
                switchToNextMode();
                lastModeChange = millis(); // Reset the hourly timer
                Serial.print(F("Manually switched to: "));
                Serial.println(modeNames[currentMode]);
            }
        }
    }
    
    // Handle test mode progression
    if (serialTestActive) {
        unsigned long elapsed = millis() - serialTestStart;
        if (elapsed >= SERIAL_TEST_DURATION) {
            serialTestModeIndex++;
            if (serialTestModeIndex >= MODE_COUNT) {
                // Test complete, return to normal Conway mode
                serialTestActive = false;
                switchMode(MODE_CONWAY);
                Serial.println(F("=== SERIAL TEST COMPLETE ==="));
                Serial.println(F("All modes tested - returned to Conway GoL"));
            } else {
                // Switch to next test mode
                switchMode((VisualMode)serialTestModeIndex);
                serialTestStart = millis();
                Serial.print(F("Auto-advancing to Mode "));
                Serial.print(serialTestModeIndex + 1);
                Serial.print(F("/8: "));
                Serial.println(modeNames[serialTestModeIndex]);
                
                // Special announcements for new modes
                if (serialTestModeIndex == MODE_WIND) {
                    Serial.println(F("  -> NEW: RGB Pixel Rain with colorful trails!"));
                } else if (serialTestModeIndex == MODE_WORMHOLE) {
                    Serial.println(F("  -> NEW: Wormhole with pulsing concentric rings!"));
                }
            }
        }
        
        // Show progress every 5 seconds
        static uint32_t lastProgressReport = 0;
        if (elapsed - lastProgressReport >= 5000) {
            uint8_t secondsRemaining = (SERIAL_TEST_DURATION - elapsed) / 1000;
            Serial.print(F("Testing "));
            Serial.print(modeNames[serialTestModeIndex]);
            Serial.print(F(" - "));
            Serial.print(secondsRemaining);
            Serial.println(F("s remaining"));
            lastProgressReport = elapsed;
        }
    }
}

// === MODE SWITCHING ===
void switchToNextMode() {
    VisualMode nextMode = (VisualMode)((currentMode + 1) % MODE_COUNT);
    switchMode(nextMode);
}

void switchMode(VisualMode newMode) {
    currentMode = newMode;
    
    // Clear animation area and reset grids
    tft.fillRect(0, 0, SCREEN_WIDTH, ANIM_HEIGHT, ST77XX_BLACK);
    memset(currentGrid,     0, GRID_BYTES             );
    memset(nextGrid,        0, GRID_BYTES             );
    memset(intensityGrid,   0, sizeof(intensityGrid)  );
    memset(lastColorBuffer, 0, sizeof(lastColorBuffer));
    
    // Clear age tracking arrays
    memset(cellAge,        0, sizeof(cellAge)       );
    memset(cellGeneration, 0, sizeof(cellGeneration));
    globalGeneration = 0;
    
    // FIXED: Initialize all animation modes properly
    switch (newMode) {
        case MODE_CONWAY:
            addConwayPatterns();
            break;
            
        case MODE_BRIGHT:
        case MODE_CYCLONE:
        case MODE_PORTAL:
            // Initialize with base pattern for remaining placeholder modes
            for (uint8_t y = 0; y < GRID_HEIGHT; y++) {
                for (uint8_t x = 0; x < GRID_WIDTH; x++) {
                    setIntensity(x, y, 128); // Base intensity
                }
            }
            break;
            
        case MODE_FIRE:
            // Initialize fire with base heat
            for (uint8_t x = 0; x < GRID_WIDTH; x++) {
                setIntensity(x, GRID_HEIGHT - 1, 200); // Bottom row heat source
            }
            break;
            
        case MODE_WATER:
            // Initialize water with base wave pattern
            for (uint8_t y = 0; y < GRID_HEIGHT; y++) {
                for (uint8_t x = 0; x < GRID_WIDTH; x++) {
                    setIntensity(x, y, 140 + ((x + y) % 50)); // Base wave pattern
                }
            }
            break;
            
        case MODE_WIND: // RGB Pixel Rain
            // Initialize with clear sky (low intensity base)
            for (uint8_t y = 0; y < GRID_HEIGHT; y++) {
                for (uint8_t x = 0; x < GRID_WIDTH; x++) {
                    setIntensity(x, y, 10 + (fastRandom() % 20)); // Slight variation
                }
            }
            break;
            
        case MODE_WORMHOLE:
            // Initialize wormhole with center point
            {
                uint8_t centerX = GRID_WIDTH / 2;
                uint8_t centerY = GRID_HEIGHT / 2;
                for (uint8_t y = 0; y < GRID_HEIGHT; y++) {
                    for (uint8_t x = 0; x < GRID_WIDTH; x++) {
                        int8_t dx = x - centerX;
                        int8_t dy = y - centerY;
                        uint8_t distance = fastSqrt8(dx * dx + dy * dy);
                        uint8_t intensity = (distance * 20 < 200) ? (200 - distance * 20) : 50;
                        setIntensity(x, y, intensity);
                    }
                }
            }
            break;
    }
    
    Serial.print(F("Mode: "));
    Serial.println(modeNames[currentMode]);
}

// === CONWAY GAME OF LIFE ===
void updateConway() {
    static uint8_t neighborCount[GRID_WIDTH * GRID_HEIGHT];
    static uint16_t stagnationCounter = 0;
    static uint16_t     lastLiveCells = 0;
    static uint32_t lastPatternInject = 0;
    
    globalGeneration++;
    
    // Calculate neighbor counts with proper wraparound
    memset(neighborCount, 0, sizeof(neighborCount));
    for (uint8_t y = 0; y < GRID_HEIGHT; y++) {
        for (uint8_t x = 0; x < GRID_WIDTH; x++) {
            if (getCellState(currentGrid, x, y)) {
                for (int8_t dy = -1; dy <= 1; dy++) {
                    for (int8_t dx = -1; dx <= 1; dx++) {
                        if (dx == 0 && dy == 0) continue;
                        uint8_t nx = (x + dx + GRID_WIDTH) % GRID_WIDTH;
                        uint8_t ny = (y + dy + GRID_HEIGHT) % GRID_HEIGHT;
                        neighborCount[ny * GRID_WIDTH + nx]++;
                    }
                }
            }
        }
    }
    
    // Apply Conway's rules AND track cell ages
    memset(nextGrid, 0, GRID_BYTES);
    uint16_t liveCells = 0;
    
    for (uint8_t y = 0; y < GRID_HEIGHT; y++) {
        for (uint8_t x = 0; x < GRID_WIDTH; x++) {
            uint8_t  neighbors = neighborCount[y * GRID_WIDTH + x];
            bool         alive = getCellState(currentGrid, x, y);
            uint8_t currentAge = getCellAge(x, y);
            
            // Pure Conway rules
            bool newState = false;
            if (alive && (neighbors == 2 || neighbors == 3)) {
                newState = true; // Survival
            } else if (!alive && neighbors == 3) {
                newState = true; // Birth
            }
            
            setCellState(nextGrid, x, y, newState);
            
            // Handle age and colors
            if (newState) {
                if (alive) {
                    // Cell survived - age it up to maximum of 255
                    uint8_t newAge = (currentAge < 255) ? currentAge + 1 : 255;
                    setCellAge(x, y, newAge);
                } else {
                    // New birth - reset age and set generation
                    setCellAge(x, y, 1);
                    setCellGeneration(x, y, globalGeneration & 0xFF);
                }
                
                // Dynamic intensity based on age and generation (not working properly)
                // FIXED: Dynamic intensity based on age and generation
                uint8_t age = getCellAge(x, y);
                uint8_t birthGen = getCellGeneration(x, y);

                // Balanced intensity calculation with proper bounds checking
                uint16_t baseIntensity = 160 + min(60, age * 2); // Age: 160-220 range
                uint8_t genWave = (fastSin8((birthGen + globalGeneration) * 4) + 128) >> 3; // Gen: 0-31 range  
                uint8_t posVariation = ((x ^ y ^ (globalGeneration & 0xFF)) & 0x0F); // Pos: 0-15 range
        
                uint16_t finalIntensity = baseIntensity + genWave + posVariation;

                // Ensure proper bounds
                finalIntensity = constrain(finalIntensity, 120, 255);

                setIntensity(x, y, finalIntensity);

                liveCells++;
            } else {
                // Dead cell - fade out and reset age
                setCellAge(x, y, 0);
                uint8_t currentIntensity = getIntensity(x, y);
                if (currentIntensity > 15) {
                    setIntensity(x, y, currentIntensity - 12);
                } else {
                    setIntensity(x, y, 0);
                }
            }
        }
    }
    
    // Copy next generation to current
    memcpy(currentGrid, nextGrid, GRID_BYTES);
    
    // Rest of function remains the same...
    if (liveCells == lastLiveCells) {
        stagnationCounter++;
    } else {
        stagnationCounter = 0;
    }
    
    uint32_t currentTime = millis();
    
    if (stagnationCounter > 8 || liveCells < 3 || 
        (currentTime - lastPatternInject > 12000)) {
        
        addConwaySeedPatterns();
        stagnationCounter = 0;
        lastPatternInject = currentTime;
    }
    
    lastLiveCells = liveCells;
}


// Seed patterns that evolve naturally
void addConwaySeedPatterns() {
    // Add fewer, smaller, true seed patterns that will evolve
    uint8_t seedCount = 3 + (fastRandom() % 3); // 3-5 seeds only
    
    for (uint8_t i = 0; i < seedCount; i++) {
        uint8_t seedType = fastRandom() % 5;
        uint8_t startX = 4 + (fastRandom() % (GRID_WIDTH - 8));
        uint8_t startY = 2 + (fastRandom() % (GRID_HEIGHT - 4));
        
        // Clear small area first to prevent interference
        for (int8_t dy = -1; dy <= 1; dy++) {
            for (int8_t dx = -1; dx <= 1; dx++) {
                uint8_t clearX = startX + dx;
                uint8_t clearY = startY + dy;
                if (clearX < GRID_WIDTH && clearY < GRID_HEIGHT) {
                    setCellState(currentGrid, clearX, clearY, false);
                    setIntensity(clearX, clearY, 0);
                }
            }
        }
        
        switch (seedType) {
            case 0: // R-pentomino - evolves for ~1100 generations
                setCellState(currentGrid, startX + 1, startY, true);
                setCellState(currentGrid, startX + 2, startY, true);
                setCellState(currentGrid, startX, startY + 1, true);
                setCellState(currentGrid, startX + 1, startY + 1, true);
                setCellState(currentGrid, startX + 1, startY + 2, true);
                break;
                
            case 1: // Acorn - evolves for 5206 generations
                setCellState(currentGrid, startX + 1, startY, true);
                setCellState(currentGrid, startX + 3, startY + 1, true);
                setCellState(currentGrid, startX, startY + 2, true);
                setCellState(currentGrid, startX + 1, startY + 2, true);
                setCellState(currentGrid, startX + 4, startY + 2, true);
                setCellState(currentGrid, startX + 5, startY + 2, true);
                setCellState(currentGrid, startX + 6, startY + 2, true);
                break;
                
            case 2: // Pi-heptomino - creates interesting patterns
                setCellState(currentGrid, startX, startY, true);
                setCellState(currentGrid, startX + 1, startY, true);
                setCellState(currentGrid, startX + 2, startY, true);
                setCellState(currentGrid, startX, startY + 1, true);
                setCellState(currentGrid, startX + 2, startY + 1, true);
                break;
                
            case 3: // Simple glider gun component
                setCellState(currentGrid, startX, startY, true);
                setCellState(currentGrid, startX + 1, startY, true);
                setCellState(currentGrid, startX, startY + 1, true);
                setCellState(currentGrid, startX + 2, startY + 2, true);
                setCellState(currentGrid, startX + 3, startY + 3, true);
                setCellState(currentGrid, startX + 4, startY + 3, true);
                break;
                
            case 4: // Random sparse seed
                for (uint8_t j = 0; j < 4; j++) {
                    uint8_t x = startX + (fastRandom() % 3);
                    uint8_t y = startY + (fastRandom() % 2);
                    setCellState(currentGrid, x, y, true);
                }
                break;
        }
    }
}

// Replace the old addConwayPatterns function call
void addConwayPatterns() {
    addConwaySeedPatterns();
}

// === OTHER ANIMATION UPDATES ===
void updateBright() {
    static uint8_t phase = 0;
    phase++;
    
    uint8_t centerX = GRID_WIDTH / 2;
    uint8_t centerY = GRID_HEIGHT / 2;
    
    for (uint8_t y = 0; y < GRID_HEIGHT; y++) {
        for (uint8_t x = 0; x < GRID_WIDTH; x++) {
            int8_t dx = x - centerX;
            int8_t dy = y - centerY;
            uint8_t distance = fastSqrt8(dx * dx + dy * dy);
            
            int16_t intensity = fastSin8(distance * 8 - phase) + 128;
            intensity += fastCos8((x + y + phase) >> 1) >> 2;
            
            if (intensity > 255) intensity = 255;
            if (intensity < 0) intensity = 0;
            
            setIntensity(x, y, intensity);
        }
    }
}

void updateFire() {
    static uint8_t windOffset = 0;
    static uint32_t cachedRandom = 0;
    static uint8_t randomCounter = 0;
    
    windOffset++;
    
    // Cache random values for performance
    if ((randomCounter & 0x07) == 0) {
        cachedRandom = fastRandom();
    }
    randomCounter++;
    
    // Enhanced fire source - wider base for larger display area
    constexpr uint8_t wickStart = 6;
    constexpr uint8_t wickEnd = GRID_WIDTH - 7;
    
    // Bottom row - continuous fire source
    for (uint8_t x = 0; x < GRID_WIDTH; x++) {
        if (x >= wickStart && x <= wickEnd) {
            // Main wick area - always burning hot
            setIntensity(x, GRID_HEIGHT - 1, 255);
        } else {
            // Edge sparks with controlled randomness
            if (((cachedRandom >> (x & 7)) & 0x07) == 0) {
                setIntensity(x, GRID_HEIGHT - 1, 180 + (fastRandom() & 31));
            } else {
                setIntensity(x, GRID_HEIGHT - 1, 40 + (fastRandom() & 15));
            }
        }
    }
    
    // Pre-calculate wind effect for entire frame
    const int8_t baseWind = fastSin8(windOffset) >> 6;
    
    // Enhanced heat propagation with better flame reach
    for (int16_t y = GRID_HEIGHT - 2; y >= 0; y--) {
        const uint8_t nextRow = y + 1;
        const int8_t rowWind = baseWind + ((int8_t)(fastSin8(windOffset + y) >> 7));
        
        for (uint8_t x = 0; x < GRID_WIDTH; x++) {
            uint16_t newIntensity = 0;
            uint8_t samples = 0;
            
            // Primary heat from below (stronger for taller flames)
            const uint8_t belowHeat = getIntensity(x, nextRow);
            newIntensity += belowHeat * 3; // Triple weight for upward heat
            samples += 3;
            
            // Side heat diffusion
            if (x > 0) {
                newIntensity += getIntensity(x - 1, nextRow);
                samples++;
            }
            if (x < GRID_WIDTH - 1) {
                newIntensity += getIntensity(x + 1, nextRow);
                samples++;
            }
            
            // Diagonal heat for more realistic spread
            if (x > 0 && y < GRID_HEIGHT - 1) {
                newIntensity += getIntensity(x - 1, nextRow) >> 1;
                samples++;
            }
            if (x < GRID_WIDTH - 1 && y < GRID_HEIGHT - 1) {
                newIntensity += getIntensity(x + 1, nextRow) >> 1;
                samples++;
            }
            
            // Average and apply decay
            if (samples > 0) {
                newIntensity /= samples;
            }
            
            // Enhanced decay model for taller flames
            newIntensity = (newIntensity * 235) >> 8; // ~92% retention for better reach
            
            // Wind effect with position-based variation
            if (rowWind != 0) {
                const int16_t windX = x + rowWind;
                if (windX >= 0 && windX < GRID_WIDTH) {
                    const uint8_t windHeat = getIntensity(windX, nextRow);
                    newIntensity += windHeat >> 2;
                }
            }
            
            // Controlled randomness using cached values
            const uint8_t randomBits = (cachedRandom >> ((x * y) & 0x0F)) & 0x0F;
            newIntensity += randomBits - 8;
            
            // Enhanced intensity boost for middle columns (core flame)
            if (x >= wickStart && x <= wickEnd && y >= GRID_HEIGHT - 6) {
                newIntensity += 20;
            }
            
            // Clamp values
            if (newIntensity > 255) newIntensity = 255;
            setIntensity(x, y, newIntensity);
        }
    }
}

/*
// No wind version ultra performance
void updateFire() {
    static uint8_t heatPhase = 0;
    heatPhase++;
    
    // Stable fire source
    constexpr uint8_t wickStart = 6;
    constexpr uint8_t wickEnd = GRID_WIDTH - 7;
    
    // Bottom row - completely stable base with gentle flicker
    for (uint8_t x = 0; x < GRID_WIDTH; x++) {
        if (x >= wickStart && x <= wickEnd) {
            // Main wick - gentle sine wave flicker only
            uint8_t flicker = fastSin8((x << 2) + heatPhase) >> 5; // Very gentle
            setIntensity(x, GRID_HEIGHT - 1, 255 - (flicker & 0x0F));
        } else {
            // Smooth edge falloff
            uint8_t distance = (x < wickStart) ? (wickStart - x) : (x - wickEnd);
            setIntensity(x, GRID_HEIGHT - 1, max(50, 200 - (distance * 25)));
        }
    }
    
    // Pure upward heat propagation - no wind at all
    for (int16_t y = GRID_HEIGHT - 2; y >= 0; y--) {
        for (uint8_t x = 0; x < GRID_WIDTH; x++) {
            uint16_t newIntensity = 0;
            uint8_t samples = 0;
            
            // Heat from directly below (primary)
            const uint8_t belowHeat = getIntensity(x, y + 1);
            newIntensity += belowHeat * 4;
            samples += 4;
            
            // Gentle side blending for smooth flames
            if (x > 0) {
                newIntensity += getIntensity(x - 1, y + 1);
                samples++;
            }
            if (x < GRID_WIDTH - 1) {
                newIntensity += getIntensity(x + 1, y + 1);
                samples++;
            }
            
            // Average
            newIntensity /= samples;
            
            // Smooth, predictable decay
            newIntensity = (newIntensity * 245) >> 8; // ~96% retention
            
            // Minimal variation using position-based pattern
            uint8_t posVariation = (x + y + heatPhase) & 0x03; // 0-3 range
            newIntensity += posVariation - 1; // ยฑ1 variation
            
            // Core flame boost
            if (x >= wickStart && x <= wickEnd && y >= GRID_HEIGHT - 10) {
                newIntensity += 15;
            }
            
            // Clamp
            if (newIntensity > 255) newIntensity = 255;
            setIntensity(x, y, newIntensity);
        }
    }
}
*/

void updateWater() {
    static uint8_t wavePhase = 0;
    wavePhase += 2;
    
    for (uint8_t y = 0; y < GRID_HEIGHT; y++) {
        for (uint8_t x = 0; x < GRID_WIDTH; x++) {
            int16_t wave1 = fastSin8((x * 4 + wavePhase) >> 2);
            int16_t wave2 = fastCos8((y * 3 + wavePhase) >> 2);
            int16_t wave3 = fastSin8(((x + y) * 2 + wavePhase) >> 3);
            
            int16_t intensity = 140 + (wave1 >> 2) + (wave2 >> 2) + (wave3 >> 3);
            
            if (intensity > 255) intensity = 255;
            if (intensity < 50) intensity = 50;
            
            setIntensity(x, y, intensity);
        }
    }
}

void updateRGBRain() {
    static uint8_t rainPhase = 0;
    static uint8_t dropPositions[GRID_WIDTH];
    static uint8_t dropColors[GRID_WIDTH];
    static bool dropActive[GRID_WIDTH];
    static bool initialized = false;
    
    if (!initialized) {
        // Initialize rain drops
        for (uint8_t x = 0; x < GRID_WIDTH; x++) {
            dropPositions[x] = fastRandom() % GRID_HEIGHT;
            dropColors[x] = fastRandom() % 7; // 7 different colors
            dropActive[x] = (fastRandom() % 3) == 0; // Random activation
        }
        initialized = true;
    }
    
    rainPhase++;
    
    // Clear previous frame with fade effect
    for (uint8_t y = 0; y < GRID_HEIGHT; y++) {
        for (uint8_t x = 0; x < GRID_WIDTH; x++) {
            uint8_t currentIntensity = getIntensity(x, y);
            if (currentIntensity > 20) {
                setIntensity(x, y, currentIntensity - 15); // Fade trail
            } else {
                setIntensity(x, y, 0);
            }
        }
    }
    
    // Update each rain column
    for (uint8_t x = 0; x < GRID_WIDTH; x++) {
        if (dropActive[x]) {
            // Set bright pixel at drop position
            setIntensity(x, dropPositions[x], 255);
            
            // Move drop down
            dropPositions[x]++;
            
            // Reset drop when it reaches bottom
            if (dropPositions[x] >= GRID_HEIGHT) {
                dropPositions[x] = 0;
                dropColors[x] = fastRandom() % 7;
                dropActive[x] = (fastRandom() % 4) != 0; // 75% chance to stay active
            }
        } else {
            // Randomly reactivate drops
            if ((fastRandom() % 20) == 0) {
                dropActive[x] = true;
                dropPositions[x] = 0;
                dropColors[x] = fastRandom() % 7;
            }
        }
    }
    
    // Add some sparkle effects
    if ((rainPhase % 8) == 0) {
        uint8_t sparkleX = fastRandom() % GRID_WIDTH;
        uint8_t sparkleY = fastRandom() % GRID_HEIGHT;
        uint8_t currentIntensity = getIntensity(sparkleX, sparkleY);
        if (currentIntensity > 100) {
            setIntensity(sparkleX, sparkleY, 255); // Enhance existing pixels
        }
    }
}

void updateWormhole() {
    static uint8_t wormholePhase = 0;
    static int16_t ringRadius[8]; // Multiple concentric rings
    static uint8_t ringIntensity[8];
    static bool initialized = false;
    
    if (!initialized) {
        // Initialize rings at different radii
        for (uint8_t i = 0; i < 8; i++) {
            ringRadius[i] = i * 3;
            ringIntensity[i] = 255 - (i * 25);
        }
        initialized = true;
    }
    
    wormholePhase += 3;
    
    uint8_t centerX = GRID_WIDTH / 2;
    uint8_t centerY = GRID_HEIGHT / 2;
    
    // Clear grid first
    for (uint8_t y = 0; y < GRID_HEIGHT; y++) {
        for (uint8_t x = 0; x < GRID_WIDTH; x++) {
            setIntensity(x, y, 0);
        }
    }
    
    // Draw concentric rings with pulsing effect
    for (uint8_t ringIndex = 0; ringIndex < 8; ringIndex++) {
        // Update ring radius (expanding outward)
        ringRadius[ringIndex]++;
        if (ringRadius[ringIndex] > 20) {
            ringRadius[ringIndex] = 0; // Reset to center
            ringIntensity[ringIndex] = 255 - (ringIndex * 25);
        }
        
        // Calculate ring intensity with pulsing
        uint16_t pulseIntensity = ringIntensity[ringIndex];
        pulseIntensity += fastSin8(wormholePhase + ringIndex * 16) >> 2;
        if (pulseIntensity > 255) pulseIntensity = 255;
        
        // Draw ring pixels
        for (uint8_t y = 0; y < GRID_HEIGHT; y++) {
            for (uint8_t x = 0; x < GRID_WIDTH; x++) {
                int8_t dx = x - centerX;
                int8_t dy = y - centerY;
                uint8_t pixelRadius = fastSqrt8(dx * dx + dy * dy);
                
                // Check if pixel is close to ring radius
                if (abs(pixelRadius - (ringRadius[ringIndex] >> 2)) <= 1) {
                    uint8_t currentIntensity = getIntensity(x, y);
                    uint8_t newIntensity = (currentIntensity > (pulseIntensity >> 1)) ? currentIntensity : (pulseIntensity >> 1);
                    
                    // Add swirl effect
                    uint8_t angle = (fastCos8((x * y + wormholePhase) >> 2) + 128) >> 3;
                    newIntensity += angle;
                    if (newIntensity > 255) newIntensity = 255;
                    
                    setIntensity(x, y, newIntensity);
                }
            }
        }
    }
    
    // Add central bright spot that pulses
    uint8_t centerBrightness = 200 + ((fastSin8(wormholePhase << 1) + 128) >> 3);
    setIntensity(centerX, centerY, centerBrightness);
    
    // Add some neighboring center pixels
    if (centerX > 0) setIntensity(centerX - 1, centerY, centerBrightness >> 1);
    if (centerX < GRID_WIDTH - 1) setIntensity(centerX + 1, centerY, centerBrightness >> 1);
    if (centerY > 0) setIntensity(centerX, centerY - 1, centerBrightness >> 1);
    if (centerY < GRID_HEIGHT - 1) setIntensity(centerX, centerY + 1, centerBrightness >> 1);
}

// === ANIMATION DISPATCHER ===
void updateAnimation() {
    switch (currentMode) {
        case MODE_CONWAY:
            updateConway();
            break;
        case MODE_BRIGHT:
            updateBright();
            break;
        case MODE_FIRE:
            updateFire();
            break;
        case MODE_WATER:
            updateWater();
            break;
        case MODE_WIND:
            updateRGBRain(); // RGB Pixel Rain
            break;
        case MODE_CYCLONE:
            updateBright(); // Placeholder - use bright for now
            break;
        case MODE_PORTAL:
            updateBright(); // Placeholder - use bright for now
            break;
        case MODE_WORMHOLE:
            updateWormhole(); // Wormhole animation
            break;
    }
}

// === ANIMATION RENDERING ===
uint16_t getModeColor(uint8_t x, uint8_t y) {
    const uint16_t* palette;
    uint8_t paletteSize;
    
    switch (currentMode) {
        case MODE_FIRE:
            palette = firePalette;
            paletteSize = 13;
            break;
            
        case MODE_WATER:
            palette = waterPalette;
            paletteSize = 13;
            break;
            
        case MODE_WIND: // RGB Pixel Rain
        {
            uint8_t intensity = getIntensity(x, y);
            if (intensity == 0) return ST77XX_BLACK;
            
            // Create rainbow colors based on position and time
            uint8_t colorIndex = (x + (globalGeneration >> 2)) % 7;
            uint16_t baseColor;
            switch (colorIndex) {
                case 0: baseColor = ST77XX_RED; break;
                case 1: baseColor = ST77XX_GREEN; break;
                case 2: baseColor = ST77XX_BLUE; break;
                case 3: baseColor = ST77XX_CYAN; break;
                case 4: baseColor = ST77XX_MAGENTA; break;
                case 5: baseColor = ST77XX_YELLOW; break;
                case 6: baseColor = ST77XX_WHITE; break;
                default: baseColor = ST77XX_WHITE; break;
            }
            
            // Scale intensity
            if (intensity >= 200) return baseColor;
            
            uint8_t scale = (intensity * 255) / 200;
            uint8_t r = ((baseColor >> 11) * scale) >> 8;
            uint8_t g = (((baseColor >> 5) & 0x3F) * scale) >> 8;
            uint8_t b = ((baseColor & 0x1F) * scale) >> 8;
            return (r << 11) | (g << 5) | b;
        }
        
        case MODE_WORMHOLE: // Wormhole colors
        {
            uint8_t intensity = getIntensity(x, y);
            if (intensity == 0) return ST77XX_BLACK;
            
            // Create purple/blue wormhole colors with some variation
            uint8_t colorVariation = (x + y + (globalGeneration >> 1)) % 4;
            uint16_t baseColor;
            switch (colorVariation) {
                case 0: baseColor = ST77XX_BLUE; break;
                case 1: baseColor = ST77XX_MAGENTA; break;
                case 2: baseColor = ST77XX_CYAN; break;
                case 3: baseColor = 0x801F; break; // Purple
                default: baseColor = ST77XX_BLUE; break;
            }
            
            // Scale intensity
            if (intensity >= 240) return baseColor;
            
            uint8_t scale = (intensity * 255) / 240;
            uint8_t r = ((baseColor >> 11) * scale) >> 8;
            uint8_t g = (((baseColor >> 5) & 0x3F) * scale) >> 8;
            uint8_t b = ((baseColor & 0x1F) * scale) >> 8;
            return (r << 11) | (g << 5) | b;
        }
        
        // Dynamic Intensity Conway GoL
        case MODE_CONWAY:
        {
            uint8_t intensity = getIntensity(x, y);
            if (intensity == 0) return ST77XX_BLACK;

            uint8_t age = getCellAge(x, y);
            uint8_t birthGen = getCellGeneration(x, y);
            uint8_t colorIndex = (age + birthGen + globalGeneration) % 7;

            // Apply calculated intensity to the base color
            uint16_t baseColor;
            switch (colorIndex) {
                case 0:  baseColor = ST77XX_WHITE;   break;
                case 1:  baseColor = ST77XX_CYAN;    break;
                case 2:  baseColor = ST77XX_BLUE;    break;
                case 3:  baseColor = ST77XX_MAGENTA; break;
                case 4:  baseColor = ST77XX_RED;     break;
                case 5:  baseColor = ST77XX_YELLOW;  break;
                case 6:  baseColor = ST77XX_GREEN;   break;
                default: baseColor = ST77XX_WHITE;   break;
            }

            // Handle maximum intensity as special case for full brightness
            if (intensity >= 255) {
                return baseColor; // Return full color at maximum intensity
            }
    
            // Scale color based on intensity (120-255 maps to 50%-100% brightness)
            if (intensity >= 120) {
                uint8_t scale = map(intensity, 120, 255, 128, 255); // 50%-100% scaling
                uint8_t r = ((baseColor >> 11) * scale) >> 8;
                uint8_t g = (((baseColor >> 5) & 0x3F) * scale) >> 8;
                uint8_t b = ((baseColor & 0x1F) * scale) >> 8;
                return (r << 11) | (g << 5) | b;
            }
    
            return baseColor;
        }

        case MODE_BRIGHT:   // Stub for additional effects
        case MODE_CYCLONE:  // Stub for additional effects
        case MODE_PORTAL:
        default:
            palette = brightPalette;
            paletteSize = 13;
            break;
    }
    
    // For palette-based modes (FIRE, WATER, BRIGHT, etc.)
    uint8_t intensity = getIntensity(x, y);
    if (intensity == 0) return ST77XX_BLACK;
    
    uint8_t colorIndex = (intensity * (paletteSize - 1)) / 255;
    return pgm_read_word(&palette[colorIndex]);
}

void renderAnimation() {
    // Optimized rendering - only update changed pixels
    for (uint8_t y = 0; y < GRID_HEIGHT; y++) {
        for (uint8_t x = 0; x < GRID_WIDTH; x++) {
            uint16_t color = getModeColor(x, y);
            uint16_t lastColor = lastColorBuffer[y * GRID_WIDTH + x];
            
            if (color != lastColor) {
                uint16_t screenX = x * CELL_SIZE;
                uint16_t screenY = y * CELL_SIZE;
                tft.fillRect(screenX, screenY, CELL_SIZE, CELL_SIZE, color);
                lastColorBuffer[y * GRID_WIDTH + x] = color;
            }
        }
    }
}

// === INFO DISPLAY ===
void renderInfoDisplay() {
    if (!timeData.valid) return;
    
    // FIXED: Correct day/night logic with debugging
    int hour24 = timeData.hour12;
    if (timeData.isPM && timeData.hour12 != 12) hour24 += 12;
    if (!timeData.isPM && timeData.hour12 == 12) hour24 = 0;
    
    bool isDay = (hour24 >= 6 && hour24 < 18); // 6 AM to 6 PM
    
    // FIXED: Initialize static variables properly
    static bool lastIsDay = !isDay; // Force initial clear by making it different
    static String lastTime = "";
    static String lastDate = "";
    static String lastWeather = "";
    static bool firstRun = true;
    
    // Debug output (remove after testing)
    if (firstRun) {
        Serial.print(F("DEBUG: hour12="));
        Serial.print(timeData.hour12);
        Serial.print(F(", isPM="));
        Serial.print(timeData.isPM);
        Serial.print(F(", hour24="));
        Serial.print(hour24);
        Serial.print(F(", isDay="));
        Serial.println(isDay);
        firstRun = false;
    }
    
    uint16_t bgColor = isDay ? ST77XX_WHITE : ST77XX_BLACK;
    uint16_t textColor = isDay ? ST77XX_BLACK : ST77XX_WHITE;
    uint16_t accentColor = isDay ? 0x8000 : ST77XX_GREEN;
    
    // FIXED: Always clear and redraw on day/night change OR first run
    if (isDay != lastIsDay || lastTime == "") {
        // Clear entire info area
        tft.fillRect(0, INFO_START_Y, SCREEN_WIDTH, INFO_HEIGHT, bgColor);
        lastIsDay = isDay;
        lastTime = "";
        lastDate = "";
        lastWeather = "";
        
        Serial.print(F("INFO: Switched to "));
        Serial.print(isDay ? "DAY" : "NIGHT");
        Serial.println(F(" mode"));
    }
    
    // Time display - ALWAYS update if different
    String timeStr = String(timeData.hour12) + ":" + 
                    (timeData.minute < 10 ? "0" : "") + String(timeData.minute) +
                    (timeData.isPM ? " PM" : " AM");
    
    if (timeStr != lastTime || lastTime == "") {
        // Clear time area with background color
        tft.fillRect(0, INFO_START_Y, SCREEN_WIDTH, 25, bgColor);
        
        // Set text color and draw
        tft.setTextColor(textColor);
        tft.setTextSize(2);
        uint16_t timeWidth = timeStr.length() * 12;
        uint16_t timeX = (SCREEN_WIDTH - timeWidth) / 2;
        tft.setCursor(timeX, INFO_START_Y + 5);
        tft.print(timeStr);
        
        lastTime = timeStr;
        
        // Debug output
        Serial.print(F("Time updated: "));
        Serial.print(timeStr);
        Serial.print(F(" (bg="));
        Serial.print(bgColor, HEX);
        Serial.print(F(", text="));
        Serial.print(textColor, HEX);
        Serial.println(F(")"));
    }
    
    // Date display with year
    String dateStr = timeData.dayName + " " + timeData.date;
    if (dateStr != lastDate || lastDate == "") {
        tft.fillRect(0, INFO_START_Y + 25, SCREEN_WIDTH, 12, bgColor);
        tft.setTextColor(accentColor);
        tft.setTextSize(1);
        uint16_t dateWidth = dateStr.length() * 6;
        uint16_t dateX = (SCREEN_WIDTH - dateWidth) / 2;
        tft.setCursor(dateX, INFO_START_Y + 27);
        tft.print(dateStr);
        lastDate = dateStr;
    }
    
    // Weather display
    if (weather.valid) {
        String weatherStr = String(weather.temperature, 1) + "C " + weather.description;
        if (weatherStr.length() > 21) weatherStr = weatherStr.substring(0, 21);
        
        if (weatherStr != lastWeather || lastWeather == "") {
            // Clear entire remaining area
            tft.fillRect(0, INFO_START_Y + 37, SCREEN_WIDTH, INFO_HEIGHT - 37, bgColor);
            
            tft.setTextColor(textColor);
            tft.setTextSize(1);
            uint16_t weatherWidth = weatherStr.length() * 6;
            uint16_t weatherX = (SCREEN_WIDTH - weatherWidth) / 2;
            tft.setCursor(weatherX, INFO_START_Y + 40);
            tft.print(weatherStr);
            
            // City name
            String city = "Agartala";   // Enter your City Name
            uint16_t cityWidth = city.length() * 6;
            uint16_t cityX = (SCREEN_WIDTH - cityWidth) / 2;
            tft.setCursor(cityX, INFO_START_Y + 50);
            tft.setTextColor(accentColor);
            tft.print(city);
            
            lastWeather = weatherStr;
        }
    } else {
        // Clear weather area if no valid weather data
        tft.fillRect(0, INFO_START_Y + 37, SCREEN_WIDTH, INFO_HEIGHT - 37, bgColor);
    }
}

// === TIME FUNCTIONS ===
void updateTime() {
    time_t rawTime;
    struct tm * timeInfo;
    
    time(&rawTime);
    timeInfo = localtime(&rawTime);
    
    if (timeInfo->tm_year > (2020 - 1900)) {
        // Convert to 12-hour format
        timeData.hour12 = timeInfo->tm_hour;
        if (timeData.hour12 == 0) {
            timeData.hour12 = 12;
            timeData.isPM = false;
        } else if (timeData.hour12 < 12) {
            timeData.isPM = false;
        } else if (timeData.hour12 == 12) {
            timeData.isPM = true;
        } else {
            timeData.hour12 -= 12;
            timeData.isPM = true;
        }
        
        timeData.minute = timeInfo->tm_min;
        timeData.second = timeInfo->tm_sec;
        
        // Day names
        const char* days[] = {"Sunday", "Monday", "Tuesday", "Wednesday", 
                             "Thursday", "Friday", "Saturday"};
        timeData.dayName = days[timeInfo->tm_wday];
        
        // FIXED: Date formatting with year
        const char* months[] = {"", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
                               "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
        timeData.date = String(months[timeInfo->tm_mon + 1]) + " " + 
                       String(timeInfo->tm_mday) + " " +
                       String(timeInfo->tm_year + 1900); // Add year
        
        timeData.valid = true;
    }
}


// === WEATHER FUNCTIONS ===
void fetchWeatherData() {
    if (WiFi.status() != WL_CONNECTED) return;
    
    HTTPClient http;
    WiFiClient client;
    String url = String(WEATHER_BASE_URL) + WEATHER_API_KEY + "&units=metric";
    
    http.begin(client, url);
    http.setTimeout(8000);
    
    int httpResponseCode = http.GET();
    if (httpResponseCode == 200) {
        String payload = http.getString();
        
        DynamicJsonDocument doc(1024);
        if (deserializeJson(doc, payload) == DeserializationError::Ok) {
            weather.temperature = doc["main"]["temp"];
            weather.humidity = doc["main"]["humidity"];
            weather.description = doc["weather"][0]["description"].as<String>();
            
            // Capitalize first letter
            if (weather.description.length() > 0) {
                weather.description[0] = toupper(weather.description[0]);
            }
            
            weather.valid = true;
        }
    }
    
    http.end();
}

Any feedback, suggestion, or test report, feel free to update on this thread.

Cheers.