Invaders on a single WS2812B strip?

Game

I’m calling the game LED Ladder. A couple of days ago I saw a YouTube video from a guy who made a game gadget with some code using a ws2812b strip, an ESP32 – and some real buttons… arcade-style. No code available – then my friend Antonio spotted another video with code….

That triggered me off – half an hour with one of the most powerful AI engines in the world, RIGHT? WRONG! Well over a DAY including time spent discovering that ChatGPT doesn’t always actually LISTEN to what I tell it no matter how clearly explained – but that’s ANOTHER story…

So here’s the idea.. no idea if it matches what the other guys were working on – so – 100-LED strip.. I’m at one end with 10 soldiers (10 green LEDs). The attacking enemy is at the other end with 20 soldiers (red LEDs).

When the game starts, the enemy starts sending soldiers down to me – and their colour changes randomly in the range red, yellow, green, blue as soon as they leave leave their base at the other end of the LED strip. If I do nothing, they will, one by one, kill my soldiers starting at soldier number number 10 and working backwards until I have none left.

Game

If I select one of the 4 available colours (red, yellow, green, blue) with buttons (up to now on-screen but ultimately real arcade buttons – (red, green, yellow, blue and START)) my soldier at the end of my base (starting at LED position 10) will start to fly off to the other end of the board and if his/her colour is the same as the leading travelling enemy colour, both will be annihilated and my soldier will magically reappear at base (back in green).

If I pick the wrong colour I’ll lose the soldier. If I do nothing, the enemy will eventually reach my base and start killing my people one after another.

It doesn’t get any simpler. As the game progresses, speed increases. The timing can be changed – I’m ancient and slow, kids would be much faster.

All that’s needed is an ESP32-C3 board (the common mini ones) and a 100-LED ws2812b strip. The LED control pin is defined in the code. All of this could be changed.

That half an hour trivia project turned into an all day and more nightmare – but now it works – quite exciting really. The board is USB-C powered as usual – my strip connects to the 5v pin on the board as well as signal and ground.

I used strip with 25mm spacing as it’s cheap, better to use the more expensive really close up stuff.

Ok, you know the hardware and now you have the ESPHOME code. Use the WebUI that appears in your browser or change the code to use real buttons (I likely will)… ESP32-C3 mini, buttons, box – all in should be cheap. Maybe this link –rip off EU duty permitting – as well as the 4 colours I use white for game start.. or even from Amazon if you like paying markup. Oh and a little box. No idea why I called it “Match 3”.

LED Ladder Game - Peter Scargill

Right now my desk is full of crap including the board and strip. I’ll put a video in here when I get a few minutes to clean up the project … but I’ve included a VERY preliminary video in here.

I THINK the game logic is now perfect, I very occasionally see a game-harmless LED glitch – I’ll fix that and the code will be updated soon but it’s not a show-stopper – this morning I played several test games in a row – all worked.

Ok, here’s an extremely preliminary video… the subtitle that says “ASP” is not mine – thats all down to YouTube. It should say ESP – and COLOR should be COLOUR.. Enjoy.

In the process of developing this with help from ChatGPT I came to realise the importance of ensuring that ChatGPT always works from the latest code – and we keep comments at the top of the YAML code while debugging back and forth (You may prefer other AIs – mine knows me well so I’m sticking with it).

# ============================================================
# LED LADDER GAME 1.0 - Peter Scargill - August 2026
#
# PURPOSE
# A two-player-style LED strip game: you control the GREEN men
# at one end of a 100-LED strip; the enemy starts RED at the
# opposite end. Coloured shots travel towards the enemy and a
# correctly coloured hit restores one of your men (up to 10).
#
# This is a WORKING VERSION. Comments have been added only to
# document the existing code - GAME LOGIC HAS NOT BEEN CHANGED.
#
# HARDWARE
# - ESP32-C3
# - 100 x WS2812 addressable RGB LEDs
# - LED data on GPIO7
# - Four player fire buttons: RED, YELLOW, GREEN, BLUE
# - START button
# - The strip is numbered 0..99 in software:
#       LEDs 0..9   = player/home area
#       LEDs 10..79 = main game area
#       LEDs 80..99 = enemy/home area
# - Keep the strip power supply adequate for 100 WS2812 LEDs.
#
# IMPORTANT FOR FUTURE MODIFICATIONS
# - PLAYER_END = 9, GAME_START = 10 and GAME_END = 79 are
#   deliberate boundaries. Changing them affects game logic.
# - There are 10 target slots and 10 shot slots.
# - A player's displayed men occupy LEDs 0..lives-1.
# - A fired man is removed from lives immediately and its shot
#   starts at the new lives position. This is why firing with
#   fewer than 10 men still starts at the correct LED.
# - The enemy launch position is deliberately tracked separately
#   so successive enemy men can emerge from the correct place.
# - The 20ms addressable-light effect is the display refresh.
#   Do not confuse this with the game movement timers.
#
# CURRENT KNOWN NOTE
# Aside from an occasional tiny harmless LED glitch, this version
# has been tested and appears to work correctly.
# ============================================================

esphome:
  name: game
  friendly_name: LED Ladder Game

  on_boot:
    priority: -100
    then:
      - light.turn_off: main_strip


esp32:
  variant: esp32c3
  framework:
    type: esp-idf


logger:


api:
  encryption:
    key: "pRVx8Tg6my+g3VrPDhVzIKDZ3xBIns1s1855+pDvBYk="


ota:
  - platform: esphome


wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  ap:
    ssid: "game Fallback Hotspot"
    password: "J0Cg2NkuvrfZ"


captive_portal:


web_server:
  version: 3
  log: false


# ============================================================
# GAME STATE / WORKING VARIABLES
# ============================================================
# Most of the game is deliberately kept in globals so the 20ms
# display effect and the game-control interval can share state.
#
# game_state:
#   0 = READY
#   1 = STARTING
#   2 = PLAYING
#   3 = GAME OVER
#   4 = YOU WIN
#
# lives      = number of GREEN men currently at the player's end
# score      = successful coloured hits
# supply     = enemy men still waiting to be released
#
# target_*   = up to 10 moving enemy men
# shot_*     = up to 10 moving player shots
#
# last_* and *_time values are millisecond timers.
# ============================================================

globals:

  - id: game_state
    type: int
    restore_value: no
    initial_value: '0'

  - id: lives
    type: int
    restore_value: no
    initial_value: '10'

  - id: score
    type: int
    restore_value: no
    initial_value: '0'

  - id: supply
    type: int
    restore_value: no
    initial_value: '20'

  - id: target_active
    type: bool[10]
    restore_value: no

  - id: target_pos
    type: int[10]
    restore_value: no

  - id: target_colour
    type: int[10]
    restore_value: no

  - id: shot_active
    type: bool[10]
    restore_value: no

  - id: shot_pos
    type: int[10]
    restore_value: no

  - id: shot_colour
    type: int[10]
    restore_value: no

  - id: last_move
    type: uint32_t
    restore_value: no
    initial_value: '0'

  - id: last_release
    type: uint32_t
    restore_value: no
    initial_value: '0'

  - id: last_shot_move
    type: uint32_t
    restore_value: no
    initial_value: '0'

  - id: start_time
    type: uint32_t
    restore_value: no
    initial_value: '0'

  - id: game_over_time
    type: uint32_t
    restore_value: no
    initial_value: '0'

  - id: win_time
    type: uint32_t
    restore_value: no
    initial_value: '0'

  - id: move_interval
    type: int
    restore_value: no
    initial_value: '450'

  - id: release_interval
    type: int
    restore_value: no
    initial_value: '3000'

  - id: shot_interval
    type: int
    restore_value: no
    initial_value: '70'

  - id: flash_colour
    type: int
    restore_value: no
    initial_value: '0'

  - id: flash_pos
    type: int
    restore_value: no
    initial_value: '-1'

  - id: flash_until
    type: uint32_t
    restore_value: no
    initial_value: '0'

  - id: enemy_launch_pos
    type: int
    restore_value: no
    initial_value: '80'


# ============================================================
# USER-ADJUSTABLE CONTROLS
# Game Speed:
#   1 = slowest, 10 = fastest.
#   5 is the original/default game speed.
#
# The speed control changes ENEMY MOVEMENT speed only. The shot
# animation remains at its established 70ms timing.
#
# Game Brightness:
#   0..255. This is the actual LED brightness scaling used by
#   the display effect. It is not a percentage.
# ============================================================

number:

  - platform: template
    name: "Game Speed"
    id: game_speed

    min_value: 1
    max_value: 10
    step: 1

    initial_value: 5

    optimistic: true
    restore_value: false

    mode: slider

    web_server:
      sorting_weight: 65


  - platform: template
    name: "Game Brightness"
    id: game_brightness

    min_value: 0
    max_value: 255
    step: 5

    initial_value: 64

    optimistic: true
    restore_value: true

    mode: slider

    unit_of_measurement: ""

    web_server:
      sorting_weight: 70


# ============================================================
# LED STRIP HARDWARE
# ============================================================
# 100 WS2812 LEDs, GRB order, data on GPIO7.
# The strip is kept INTERNAL because the game controls every LED
# through the addressable effect rather than normal HA light
# commands.
#
# max_refresh_rate and the 20ms effect update are deliberately
# separate from the game's movement intervals.
# ============================================================

light:

  - platform: esp32_rmt_led_strip
    id: main_strip
    internal: true

    pin: GPIO7

    num_leds: 100

    rgb_order: GRB
    chipset: WS2812

    rmt_symbols: 96

    max_refresh_rate: 30ms

    default_transition_length: 0s

    effects:

      - addressable_lambda:
          name: "MATCH 3 GAME"

          update_interval: 20ms

          lambda: |-

            // ==================================================
            // DISPLAY LAYOUT CONSTANTS
            // ==================================================
            // Software LED numbering is 0..99.
            //
            // Player/home:  0..9
            // Game area:   10..79
            // Enemy/home:  80..99
            //
            // IMPORTANT: these boundaries are used by both the
            // display and the game logic. Change with care.
            // ==================================================

            const int NUM_LEDS = 100;
            const int PLAYER_END = 9;
            const int GAME_START = 10;
            const int GAME_END = 79;
            const int MAX_TARGETS = 10;
            /* shot arrays use 10 slots in this version */

            uint32_t now = millis();

            // Convert the 0..255 brightness control into a
            // 0.0..1.0 multiplier used for all game colours.
            float b = id(game_brightness).state / 255.0f;

            if (b < 0.0f) {
              b = 0.0f;
            }

            if (b > 1.0f) {
              b = 1.0f;
            }

            uint8_t C = (uint8_t)(255.0f * b);
            uint8_t Y = (uint8_t)(150.0f * b);

            Color RED = Color(C, 0, 0);
            Color YELLOW = Color(C, Y, 0);
            Color GREEN = Color(0, C, 0);
            Color BLUE = Color(0, 0, C);
            Color WHITE = Color(C, C, C);
            Color OFF = Color(0, 0, 0);

            // Every refresh starts with a completely blank strip.
            // The game then redraws every currently active object.
            // This is important: do not remove this clearing step
            // when modifying the display code, as it prevents stale
            // pixels being left behind.
            for (int n = 0; n < NUM_LEDS; n++) {
              it[n] = OFF;
            }

            if (id(game_state) == 0) {
              return;
            }

            if (id(game_state) == 1) {

              if (((now / 200) % 2) == 0) {
                for (int n = 0; n <= PLAYER_END; n++) {
                  it[n] = Color(C, 0, C);
                }
              }

              for (int n = 0; n < 20; n++) {
                it[99 - n] = RED;
              }

              return;
            }

            if (id(game_state) == 3) {

              if (now - id(game_over_time) < 4000) {

                if (((now / 250) % 2) == 0) {
                  for (int n = 0; n <= PLAYER_END; n++) {
                    it[n] = RED;
                  }
                }
              }

              return;
            }

            if (id(game_state) == 4) {

              if (now - id(win_time) < 4000) {

                if (((now / 200) % 2) == 0) {
                  for (int n = 0; n <= PLAYER_END; n++) {
                    it[n] = GREEN;
                  }
                }
              }

              return;
            }

            // Draw the player's remaining men.
            // If lives == 3, LEDs 0,1,2 are GREEN.
            for (int n = 0; n < id(lives); n++) {
              it[n] = GREEN;
            }

            for (int n = 0; n < id(supply); n++) {
              it[99 - n] = RED;
            }

            // Draw every active enemy from its target slot.
            // target_pos[] contains its current physical LED.
            for (int n = 0; n < MAX_TARGETS; n++) {

              if (!id(target_active)[n]) {
                continue;
              }

              int p = id(target_pos)[n];

              if (p < 0 || p > 99) {
                continue;
              }

              if (id(target_colour)[n] == 0) {
                it[p] = RED;
              }
              else if (id(target_colour)[n] == 1) {
                it[p] = YELLOW;
              }
              else if (id(target_colour)[n] == 2) {
                it[p] = GREEN;
              }
              else {
                it[p] = BLUE;
              }
            }

            // Draw every active player shot.
            // Each shot has its own slot so multiple shots can be
            // travelling at the same time.
            for (int n = 0; n < 10; n++) {

              if (!id(shot_active)[n]) {
                continue;
              }

              int p = id(shot_pos)[n];

              if (p < 0 || p > 79) {
                continue;
              }

              if (id(shot_colour)[n] == 0) {
                it[p] = RED;
              }
              else if (id(shot_colour)[n] == 1) {
                it[p] = YELLOW;
              }
              else if (id(shot_colour)[n] == 2) {
                it[p] = GREEN;
              }
              else {
                it[p] = BLUE;
              }
            }

            if (now < id(flash_until)) {

              int p = id(flash_pos);

              if (p >= 0 && p < NUM_LEDS) {

                if (id(flash_colour) == 1) {
                  it[p] = GREEN;
                }
                else {
                  it[p] = RED;
                }
              }
            }


# ============================================================
# MAIN GAME ENGINE
# ============================================================
# This runs every 20ms. It handles:
#   - start-up sequence
#   - player shots and collisions
#   - enemy releases
#   - enemy movement
#   - win/game-over detection
#
# IMPORTANT:
# 20ms is the engine tick, NOT the speed of the game.
# Movement is controlled by move_interval / game_speed.
# Shots use shot_interval (70ms in this version).
# ============================================================

interval:

  - interval: 20ms

    then:

      - lambda: |-

          uint32_t now = millis();

          if (id(game_state) == 1) {

            if (now - id(start_time) >= 2000) {

              id(game_state) = 2;

              id(lives) = 10;
              id(supply) = 20;
              id(score) = 0;
              id(enemy_launch_pos) = 80;

              id(move_interval) = 450;
              id(release_interval) = 3000;
              id(shot_interval) = 70;

              id(last_move) = now;
              id(last_release) = now;
              id(last_shot_move) = now;

              id(flash_until) = 0;
              id(flash_pos) = -1;

              for (int n = 0; n < 10; n++) {

                id(target_active)[n] = false;
                id(shot_active)[n] = false;

                id(target_pos)[n] = 0;
                id(target_colour)[n] = 0;

                id(shot_pos)[n] = 0;
                id(shot_colour)[n] = 0;
              }
            }

            return;
          }

          if (id(game_state) == 0) {
            return;
          }

          if (id(game_state) == 3) {

            if (now - id(game_over_time) >= 4000) {

              id(game_state) = 0;

              id(lives) = 10;
              id(supply) = 20;
              id(score) = 0;

              id(flash_until) = 0;
              id(flash_pos) = -1;

              for (int n = 0; n < 10; n++) {
                id(target_active)[n] = false;
                id(shot_active)[n] = false;
              }

              auto call = id(main_strip).turn_off();
              call.perform();
            }

            return;
          }

          if (id(game_state) == 4) {

            if (now - id(win_time) >= 4000) {

              id(game_state) = 0;

              id(lives) = 10;
              id(supply) = 20;
              id(score) = 0;

              id(flash_until) = 0;
              id(flash_pos) = -1;

              for (int n = 0; n < 10; n++) {
                id(target_active)[n] = false;
                id(shot_active)[n] = false;
              }

              auto call = id(main_strip).turn_off();
              call.perform();
            }

            return;
          }

          // --------------------------------------------------
          // PLAYER SHOT MOVEMENT / COLLISION
          // shot_interval is deliberately independent of the
          // overall Game Speed control.
          // --------------------------------------------------
          if (now - id(last_shot_move) >=
              (uint32_t)id(shot_interval)) {

            id(last_shot_move) = now;

            for (int s = 0; s < 10; s++) {

              if (!id(shot_active)[s]) {
                continue;
              }

              id(shot_pos)[s]++;

              int hit_target = -1;

              for (int t = 0; t < 10; t++) {

                if (!id(target_active)[t]) {
                  continue;
                }

                if (id(shot_pos)[s] >= id(target_pos)[t]) {
                  hit_target = t;
                  break;
                }
              }

              if (hit_target >= 0) {

                int collision_pos =
                  id(target_pos)[hit_target];

                bool match =
                  id(shot_colour)[s] ==
                  id(target_colour)[hit_target];

                id(flash_pos) = collision_pos;

                if (match) {

                  id(score)++;

                  if (id(lives) < 10) {
                    id(lives)++;
                  }

                  id(flash_colour) = 1;
                  id(flash_until) = now + 180;
                }
                else {

                  id(flash_colour) = 0;
                  id(flash_until) = now + 500;

                  if (id(lives) <= 0) {

                    id(lives) = 0;

                    id(game_state) = 3;
                    id(game_over_time) = now;

                    id(target_active)[hit_target] = false;
                    id(shot_active)[s] = false;

                    return;
                  }
                }

                id(target_active)[hit_target] = false;
                id(shot_active)[s] = false;
                id(shot_pos)[s] = -1;

                if (match &&
                    id(score) % 3 == 0 &&
                    id(move_interval) > 180) {

                  id(move_interval) -= 15;
                }

                continue;
              }

              if (id(shot_pos)[s] > 79) {
                id(shot_active)[s] = false;
                id(shot_pos)[s] = -1;
              }
            }
          }

          // --------------------------------------------------
          // ENEMY RELEASE
          // enemy_launch_pos records where the next enemy is
          // allowed to emerge. It advances as enemies are
          // released so they do not all appear at one fixed LED.
          // --------------------------------------------------
          if (id(supply) > 0 &&
              now - id(last_release) >=
              (uint32_t)id(release_interval)) {

            int free_slot = -1;

            for (int n = 0; n < 10; n++) {

              if (!id(target_active)[n]) {
                free_slot = n;
                break;
              }
            }

            if (free_slot >= 0) {

              bool enough_space = true;

              for (int n = 0; n < 10; n++) {

                if (!id(target_active)[n]) {
                  continue;
                }

                if (id(target_pos)[n] > 72) {
                  enough_space = false;
                  break;
                }
              }

              if (enough_space) {

                id(target_active)[free_slot] = true;
                id(target_pos)[free_slot] = id(enemy_launch_pos);
                id(target_colour)[free_slot] =
                  random_uint32() % 4;

                id(supply)--;

                if (id(enemy_launch_pos) < 99) {
                  id(enemy_launch_pos)++;
                }

                id(last_release) = now;

                int released = 20 - id(supply);

                if (released > 0 &&
                    released % 4 == 0) {

                  if (id(move_interval) > 180) {
                    id(move_interval) -= 25;
                  }

                  if (id(release_interval) > 1400) {
                    id(release_interval) -= 200;
                  }
                }
              }
            }
          }

          // Overall enemy movement speed.
          // Speed 5 produces the original 450ms movement interval.
          // The value is deliberately clamped to 1..10.
          int speed = (int)id(game_speed).state;

          if (speed < 1) {
            speed = 1;
          }

          if (speed > 10) {
            speed = 10;
          }

          // Speed 5 = original game speed.
          // Speed 1 = slower; speed 10 = faster.
          int effective_move_interval =
              2250 / speed;

          if (effective_move_interval < 225) {
            effective_move_interval = 225;
          }

          // --------------------------------------------------
          // ENEMY MOVEMENT
          // The internal move_interval still accelerates as the
          // player scores/releases enemies. Game Speed scales the
          // resulting movement interval without changing the
          // underlying progression logic.
          // --------------------------------------------------
          if (now - id(last_move) >=
              (uint32_t)effective_move_interval) {

            id(last_move) = now;

            for (int n = 0; n < 10; n++) {

              if (!id(target_active)[n]) {
                continue;
              }

              id(target_pos)[n]--;
              
              if (id(lives) > 0 &&
                  id(target_pos)[n] <= (id(lives) - 1)) {
              
                // The man being killed is the last currently displayed man.
                id(flash_pos) = id(lives) - 1;
              
                id(flash_colour) = 0;
                id(flash_until) = now + 500;
              
                id(target_active)[n] = false;
              
                id(lives)--;
              
                if (id(lives) <= 0) {
                  id(lives) = 0;
              
                  id(game_state) = 3;
              
                  id(game_over_time) = now;
              
                  return;
                }
              }
            }
          }

          // --------------------------------------------------
          // WIN CONDITION
          // Once no enemy supply remains, the game waits until
          // all active enemies AND all active player shots are
          // gone before declaring the win.
          // --------------------------------------------------
          if (id(supply) == 0) {

            bool targets_left = false;
            bool shots_left = false;

            for (int n = 0; n < 10; n++) {

              if (id(target_active)[n]) {
                targets_left = true;
              }

              if (id(shot_active)[n]) {
                shots_left = true;
              }
            }

            if (!targets_left && !shots_left) {

              id(game_state) = 4;
              id(win_time) = now;
            }
          }


# ============================================================
# PLAYER CONTROLS
# ============================================================
# Each colour button fires one man.
#
# IMPORTANT:
# Firing immediately decrements 'lives'. The fired man's position
# is then set to the new lives value. Therefore:
#   10 men -> fired shot starts at LED 9
#    3 men -> fired shot starts at LED 2
#
# A successful colour match later restores exactly one man, up to
# the maximum of 10.
# ============================================================

button:

  - platform: template
    name: "RED"

    web_server:
      sorting_weight: 10

    on_press:
      - lambda: |-

          if (id(game_state) != 2) {
            return;
          }

          if (id(lives) <= 0) {
            return;
          }

          for (int n = 0; n < 10; n++) {

            if (!id(shot_active)[n]) {

              id(shot_active)[n] = true;

              id(lives)--;
              id(shot_pos)[n] = id(lives);
              id(shot_colour)[n] = 0;

              break;
            }
          }


  - platform: template
    name: "YELLOW"

    web_server:
      sorting_weight: 20

    on_press:
      - lambda: |-

          if (id(game_state) != 2) {
            return;
          }

          if (id(lives) <= 0) {
            return;
          }

          for (int n = 0; n < 10; n++) {

            if (!id(shot_active)[n]) {

              id(shot_active)[n] = true;

              id(lives)--;
              id(shot_pos)[n] = id(lives);
              id(shot_colour)[n] = 1;

              break;
            }
          }


  - platform: template
    name: "GREEN"

    web_server:
      sorting_weight: 30

    on_press:
      - lambda: |-

          if (id(game_state) != 2) {
            return;
          }

          if (id(lives) <= 0) {
            return;
          }

          for (int n = 0; n < 10; n++) {

            if (!id(shot_active)[n]) {

              id(shot_active)[n] = true;

              id(lives)--;
              id(shot_pos)[n] = id(lives);
              id(shot_colour)[n] = 2;

              break;
            }
          }


  - platform: template
    name: "BLUE"

    web_server:
      sorting_weight: 40

    on_press:
      - lambda: |-

          if (id(game_state) != 2) {
            return;
          }

          if (id(lives) <= 0) {
            return;
          }

          for (int n = 0; n < 10; n++) {

            if (!id(shot_active)[n]) {

              id(shot_active)[n] = true;

              id(lives)--;
              id(shot_pos)[n] = id(lives);
              id(shot_colour)[n] = 3;

              break;
            }
          }


  # ----------------------------------------------------------
  # START
  # Resets the complete game and begins the purple start flash.
  # The actual PLAYING state begins after the two-second start
  # sequence in the main 20ms game engine.
  # ----------------------------------------------------------
  - platform: template
    name: "START"

    web_server:
      sorting_weight: 50

    on_press:

      - lambda: |-

          id(game_state) = 1;

          id(lives) = 10;
          id(supply) = 20;
          id(score) = 0;
          id(enemy_launch_pos) = 80;

          id(move_interval) = 450;
          id(release_interval) = 3000;
          id(shot_interval) = 70;

          id(start_time) = millis();
          id(last_move) = millis();
          id(last_release) = millis();
          id(last_shot_move) = millis();

          id(flash_until) = 0;
          id(flash_pos) = -1;

          for (int n = 0; n < 10; n++) {

            id(target_active)[n] = false;
            id(shot_active)[n] = false;

            id(target_pos)[n] = 0;
            id(target_colour)[n] = 0;

            id(shot_pos)[n] = -1;
            id(shot_colour)[n] = 0;
          }

      - light.turn_on:
          id: main_strip
          effect: "MATCH 3 GAME"


# ============================================================
# WEB UI / HOME ASSISTANT STATUS SENSORS
# ============================================================

sensor:

  # Current successful-hit score.
  - platform: template
    name: "Score"
    update_interval: 500ms

    web_server:
      sorting_weight: 80

    lambda: |-
      return id(score);


  - platform: template
    name: "Lives"
    update_interval: 500ms

    web_server:
      sorting_weight: 90

    lambda: |-
      return id(lives);


  - platform: template
    name: "Targets Remaining"
    update_interval: 500ms

    web_server:
      sorting_weight: 100

    lambda: |-
      return id(supply);


  # Actual enemy movement interval after the Game Speed slider
  # is applied. Lower milliseconds = faster game.
  - platform: template
    name: "Effective Game Speed"
    unit_of_measurement: "ms"
    update_interval: 500ms

    web_server:
      sorting_weight: 110

    lambda: |-
      int speed = (int)id(game_speed).state;

      if (speed < 1) {
        speed = 1;
      }

      if (speed > 10) {
        speed = 10;
      }

      return 2250 / speed;


# Human-readable game state for the Web UI / Home Assistant.
text_sensor:

  - platform: template
    name: "Game Status"
    update_interval: 500ms

    web_server:
      sorting_weight: 120

    lambda: |-

      if (id(game_state) == 0) {
        return {"READY"};
      }

      if (id(game_state) == 1) {
        return {"STARTING"};
      }

      if (id(game_state) == 2) {
        return {"PLAYING"};
      }

      if (id(game_state) == 3) {
        return {"GAME OVER"};
      }

      return {"YOU WIN"};
      

Leave a Reply

Your email address will not be published. Required fields are marked *

Leave the field below empty!

Are you human? Please solve:Captcha


The maximum upload file size: 512 MB. You can upload: image, audio, video, document, spreadsheet, interactive, text, archive, code, other. Links to YouTube, Facebook, Twitter and other services inserted in the comment text will be automatically embedded. Drop file here