Invaders on a single WS2812B strip? - Scargill's Tech Blog

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 and an ESP-32 – and some real buttons… arcade-style.No code – then another video with code….That triggered me off – half an hour with one of the most powerful AI engines in the world, I thought.

WRONG.Well over a DAY including time spent discovering that ChatGPT doesn’t always actually LISTEN to what you 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 men (10 green LEDs).The attacking enemy is at the other end with 20 men (red LEDs).When the game starts, the enemy starts sending men down to me – and their colour changes randomly in the range red, yellow, green, blue when they leave base.

If I do nothing, they will, one by one, kill my men starting at number 10 and working backwards until I have no men left.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 man at the end (starting at number 10) will start to fly 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 man will magically reappear at base (back in green).If I pick the wrong colour I’ll lose the man.

If I do nothing, the enemy will eventually reach my base and start killing my men 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 ESP86 C3 board (the common mini ones) and a 100-LED ws2812b strip.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 code tells you what pin to use for the ws2812b serial signal.

The board is USB-C 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 WebIDE that appears or change the code to use real buttons (I likely will)… ESP32-C3 mini, buttons, box – not a problem.Maybe this link – rip off EU duty permitting – white for START GAME..or even from Amazon if you like paying markup.

Oh and a little box.No idea why I called it “Match 3”.Right now my desk is full of crap including the board and strip.

I’ll put a photo or video in here when I get a few minutes.There will likely be the odd bug – I’ll fix them and the code will be updated – tonight I had several test games in a row – all worked – but luck of the devil – when I made a test video – erm, slight problem if I sit and do nothing while the enemy attacks Ok, here’s an extremely preliminary video… the subtitle that says ASP is not mine – thats all down to YouTube.I should say ESP – and COLOR should be COLOUR..

# --------------------------------------------------------- # Match 3 game, men working correctly, almost perfect # needs pin to real button allocation -trivial # for now works in WebIDE for the buttons # --------------------------------------------------------- esphome: name: game friendly_name: Match 3 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 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' number: - 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 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: |- 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(); 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); 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; } int shots_out = 0; for (int n = 0; n < 10; n++) { if (id(shot_active)[n]) { shots_out++; } } int available_men = id(lives) - shots_out; if (available_men < 0) { available_men = 0; } for (int n = 0; n < available_men; n++) { it[n] = GREEN; } for (int n = 0; n < id(supply); n++) { it[99 - n] = RED; } 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; } } for (int n = 0; n < 10; n++) { if (!id(shot_active)[n]) { continue; } int p = id(shot_pos)[n]; if (p < PLAYER_END || 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; } } } 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; } 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 { if (id(lives) > 0) { id(lives)--; } 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; } } } 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; } } } } } if (now - id(last_move) >= (uint32_t)id(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; } } } } 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; } } button: - platform: template name: "RED" web_server: sorting_weight: 10 on_press: - lambda: |- if (id(game_state) != 2) { return; } int shots_out = 0; for (int n = 0; n < 10; n++) { if (id(shot_active)[n]) { shots_out++; } } if (shots_out >= id(lives)) { return; } for (int n = 0; n < 10; n++) { if (!id(shot_active)[n]) { id(shot_active)[n] = true; id(shot_pos)[n] = 9; id(shot_colour)[n] = 0; break; } } - platform: template name: "YELLOW" web_server: sorting_weight: 20 on_press: - lambda: |- if (id(game_state) != 2) { return; } int shots_out = 0; for (int n = 0; n < 10; n++) { if (id(shot_active)[n]) { shots_out++; } } if (shots_out >= id(lives)) { return; } for (int n = 0; n < 10; n++) { if (!id(shot_active)[n]) { id(shot_active)[n] = true; id(shot_pos)[n] = 9; id(shot_colour)[n] = 1; break; } } - platform: template name: "GREEN" web_server: sorting_weight: 30 on_press: - lambda: |- if (id(game_state) != 2) { return; } int shots_out = 0; for (int n = 0; n < 10; n++) { if (id(shot_active)[n]) { shots_out++; } } if (shots_out >= id(lives)) { return; } for (int n = 0; n < 10; n++) { if (!id(shot_active)[n]) { id(shot_active)[n] = true; id(shot_pos)[n] = 9; id(shot_colour)[n] = 2; break; } } - platform: template name: "BLUE" web_server: sorting_weight: 40 on_press: - lambda: |- if (id(game_state) != 2) { return; } int shots_out = 0; for (int n = 0; n < 10; n++) { if (id(shot_active)[n]) { shots_out++; } } if (shots_out >= id(lives)) { return; } for (int n = 0; n < 10; n++) { if (!id(shot_active)[n]) { id(shot_active)[n] = true; id(shot_pos)[n] = 9; id(shot_colour)[n] = 3; break; } } - 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" sensor: - 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); - platform: template name: "Game Speed" unit_of_measurement: "ms" update_interval: 500ms web_server: sorting_weight: 110 lambda: |- return id(move_interval); 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"};

Read More
Related Posts