First commit
This commit is contained in:
commit
61c37de884
171 files changed
+9201
No files matched your search
@@ -0,0 +1,647 @@
|
||||
#!/usr/bin/env python3
|
||||
# Generates base/screens/snake.yaml (modular carousel screen, base id 9). Run from anywhere:
|
||||
# python scripts/gen_snake.py
|
||||
#
|
||||
# Smooth-steering 360-degree snake. The head glides every tick (knob rotates its heading); the body is a TRAIL
|
||||
# of dots the head lays down every SEG_DIST of travel. Dots do NOT move once placed - each tick only
|
||||
# the head moves and (when the head advanced one spacing) one dot is added at the head / dropped at
|
||||
# the tail. Per-tick render cost is constant regardless of length, which keeps the main loop fast
|
||||
# enough not to starve the knob input (a chain-follow body that moved every segment every tick made
|
||||
# turning degrade as the snake grew).
|
||||
#
|
||||
# IMPORTANT (why segments are moved from a lambda, not YAML actions): ESPHome plays a sequence of
|
||||
# synchronous actions by recursion, so a long YAML action list (e.g. a 56-way "if slot==k" dispatch
|
||||
# or a 56-widget redraw) becomes ~56 nested calls and overflows the task stack. So all body/head
|
||||
# positioning is done inside ONE lambda via lv_obj_set_pos on an array of lv_obj_t* (indexed at
|
||||
# runtime). Hidden = parked off-screen (pos 400,400) - avoids any LVGL flag-API version differences.
|
||||
# Coords: a centre-offset (cx,cy) maps to a top-left lv_obj_set_pos of (180+cx-SEG/2, 180+cy-SEG/2).
|
||||
import os
|
||||
|
||||
POOL = 100 # max body dots = number of body widgets (max snake length = POOL-1).
|
||||
# Render is O(1) per tick (trail model), so a big pool only costs RAM (obj
|
||||
# widgets, allocated in PSRAM) - not per-tick time.
|
||||
SEG = 9 # dot square (px)
|
||||
SEGR = 4 # dot corner radius
|
||||
HALF = SEG // 2 # 4
|
||||
SEG_DIST = 8.0 # spacing between trail dots (px)
|
||||
R = 168.0 # max head distance from centre (inside the ring)
|
||||
RING_D = 344 # ring diameter (radius 172) -> just inside the 360 round display
|
||||
SPEED0 = 3.2 # head speed px/tick (50ms tick)
|
||||
TURN = 0.26 # radians turned per knob detent
|
||||
EAT = 12.0 # head-to-fruit distance that counts as eating
|
||||
SKHIT = 13.0 # head-to-skull distance that kills
|
||||
SELF_SKIP = 6 # newest body dots skipped in self-collision (the neck)
|
||||
SELF_DIST = 8.0 # head-to-body distance that kills (~ the two 9px sprites touching)
|
||||
GROW = 3 # body dots added per fruit eaten
|
||||
START_LEN = 5
|
||||
|
||||
NFRUIT = 12 # fruit sprites in the rotation
|
||||
NFSLOT = 3 # max fruit on the board
|
||||
NSKULL = 2 # max skulls
|
||||
FRUIT_WAIT = 90 # ticks between fruit top-ups (~4.5s); halved when only 1 fruit is left
|
||||
SKULL_WAIT = 200 # ticks between skull spawns (~10s)
|
||||
SKULL_LIFE = 220 # ticks a skull stays (~11s)
|
||||
|
||||
BASE = "https://raw.githubusercontent.com/MichalZaniewicz/esphome-guition-jc3636k718c-va/main/assets/sprites/snake/"
|
||||
|
||||
PAGE_BG = "0x0D1E15"
|
||||
RING = "0x3A8C4E"
|
||||
HEAD = "0xB8E86A"
|
||||
BODY = "0x52A049"
|
||||
ACCENT = "0xA6D85F"
|
||||
ACCENT_TXT = "0x12241A"
|
||||
BTN2 = "0x244A30"
|
||||
BTN2_TXT = "0xCFE6C4"
|
||||
GRAY = "0x3A3A3D"
|
||||
TXT = "0xC6DCC0"
|
||||
BADGE = "0x6FB23F"
|
||||
HUD = "0xCFEAE4"
|
||||
WORDMARK = "0x12212D"
|
||||
|
||||
M = POOL * 4 # safe positive-modulo offset
|
||||
# C++ array of the body-segment lv_obj_t* (built locally in each rendering lambda)
|
||||
SEGINIT = "lv_obj_t *SS[" + str(POOL) + "] = { " + ", ".join(f"id(sn_seg{k})" for k in range(POOL)) + " };"
|
||||
|
||||
|
||||
def body_widgets():
|
||||
# no align (top-left origin); parked off-screen until positioned by a lambda
|
||||
return "\n".join(
|
||||
f" - obj: {{ id: sn_seg{k}, x: 400, y: 400, width: {SEG}, height: {SEG}, "
|
||||
f"radius: {SEGR}, border_width: 0, bg_color: {BODY}, bg_opa: COVER }}"
|
||||
for k in range(POOL))
|
||||
|
||||
|
||||
def fruit_widgets():
|
||||
return "\n".join(
|
||||
f" - image: {{ id: sn_fruit{s}, align: CENTER, x: 0, y: 0, src: img_sn_fruit0, hidden: true }}"
|
||||
for s in range(NFSLOT))
|
||||
|
||||
|
||||
def skull_widgets():
|
||||
return "\n".join(
|
||||
f" - image: {{ id: sn_skull{s}, align: CENTER, x: 0, y: 0, src: img_sn_skull, hidden: true }}"
|
||||
for s in range(NSKULL))
|
||||
|
||||
|
||||
def fspr_dispatch():
|
||||
return "\n".join(
|
||||
f" - if: {{ condition: {{ lambda: 'return id(g_sn_newfruit) == {s};' }}, "
|
||||
f"then: [ script.execute: sn_fspr{s} ] }}"
|
||||
for s in range(NFSLOT))
|
||||
|
||||
|
||||
def skull_dispatch():
|
||||
return "\n".join(
|
||||
f" - if: {{ condition: {{ lambda: 'return id(g_sn_newskull) == {s};' }}, "
|
||||
f"then: [ lvgl.widget.update: {{ id: sn_skull{s}, hidden: false, "
|
||||
f"x: !lambda 'return (int) lroundf(id(g_sn_skx)[{s}]);', "
|
||||
f"y: !lambda 'return (int) lroundf(id(g_sn_sky)[{s}]);' }} ] }}"
|
||||
for s in range(NSKULL))
|
||||
|
||||
|
||||
def items_hidden():
|
||||
out = []
|
||||
for s in range(NFSLOT):
|
||||
out.append(f" - lvgl.widget.update: {{ id: sn_fruit{s}, hidden: !lambda 'return !id(g_sn_fon)[{s}];' }}")
|
||||
for s in range(NSKULL):
|
||||
out.append(f" - lvgl.widget.update: {{ id: sn_skull{s}, hidden: !lambda 'return !id(g_sn_skon)[{s}];' }}")
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def item_hides():
|
||||
out = [f" - lvgl.widget.hide: sn_fruit{s}" for s in range(NFSLOT)]
|
||||
out += [f" - lvgl.widget.hide: sn_skull{s}" for s in range(NSKULL)]
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def fruit_setters():
|
||||
blocks = []
|
||||
for s in range(NFSLOT):
|
||||
lines = [f" - id: sn_fspr{s}", " then:",
|
||||
f" - lvgl.widget.update: {{ id: sn_fruit{s}, hidden: false, "
|
||||
f"x: !lambda 'return (int) lroundf(id(g_sn_fx)[{s}]);', "
|
||||
f"y: !lambda 'return (int) lroundf(id(g_sn_fy)[{s}]);' }}"]
|
||||
for k in range(NFRUIT):
|
||||
lines.append(
|
||||
f" - if: {{ condition: {{ lambda: 'return id(g_sn_fi)[{s}] == {k};' }}, "
|
||||
f"then: [ lvgl.image.update: {{ id: sn_fruit{s}, src: img_sn_fruit{k} }} ] }}")
|
||||
blocks.append("\n".join(lines))
|
||||
return "\n".join(blocks)
|
||||
|
||||
|
||||
def fruit_images():
|
||||
return "\n".join(
|
||||
f""" - id: img_sn_fruit{k}
|
||||
file: "{BASE}fruit{k}.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel""" for k in range(NFRUIT))
|
||||
|
||||
|
||||
YAML = f"""# Snake - optional carousel screen (base id 9). Smooth-steering 360-degree snake steered by the knob.
|
||||
# Remove it: drop this file from your config's package "files:" list. Nothing else to edit.
|
||||
# Scores (g_sn_top / g_sn_best) + the "Reset scores" settings action live here - fully self-contained.
|
||||
#
|
||||
# GENERATED by scripts/gen_snake.py - edit the generator, not the blocks. Body = a trail of dots
|
||||
# (ring buffer) rendered from a lambda (lv_obj_set_pos), so per-tick cost is constant + shallow.
|
||||
|
||||
esphome:
|
||||
on_boot:
|
||||
- priority: 655
|
||||
then:
|
||||
- lambda: 'id(g_present)[9] = true;'
|
||||
- priority: 650
|
||||
then:
|
||||
- lambda: |-
|
||||
int g = id(g_wgrp_n)++;
|
||||
id(g_wgrp_labels) += (g ? "\\n" : ""); id(g_wgrp_labels) += "Snake";
|
||||
int o = id(g_wopt_n)++;
|
||||
id(g_wopt_group)[o] = g; id(g_wopt_label)[o] = "Reset scores"; id(g_wopt_kind)[o] = 1; id(g_wopt_ptr)[o] = &id(g_sn_reset);
|
||||
|
||||
globals:
|
||||
- id: g_sn_best
|
||||
type: int
|
||||
restore_value: yes
|
||||
initial_value: '0'
|
||||
- id: g_sn_top
|
||||
type: int[10]
|
||||
restore_value: yes
|
||||
- id: g_sn_reset
|
||||
type: bool
|
||||
restore_value: no
|
||||
initial_value: 'false'
|
||||
- id: g_sn_state # 0=menu 1=playing 2=game over 3=how-to 4=scores
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
- id: g_sn_len # body dots in the snake
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '{START_LEN}'
|
||||
- id: g_sn_score
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
- id: g_sn_tx # body trail dots X (ring buffer; centre coords px)
|
||||
type: float[{POOL}]
|
||||
restore_value: no
|
||||
- id: g_sn_ty
|
||||
type: float[{POOL}]
|
||||
restore_value: no
|
||||
- id: g_sn_head # ring index of the newest dot
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
- id: g_sn_hx # smooth head position (leads the newest dot)
|
||||
type: float
|
||||
restore_value: no
|
||||
initial_value: '0.0'
|
||||
- id: g_sn_hy
|
||||
type: float
|
||||
restore_value: no
|
||||
initial_value: '0.0'
|
||||
- id: g_sn_ang # heading (radians)
|
||||
type: float
|
||||
restore_value: no
|
||||
initial_value: '0.0'
|
||||
- id: g_sn_grow # pending growth (appends that won't drop the tail)
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
- id: g_sn_dead
|
||||
type: bool
|
||||
restore_value: no
|
||||
initial_value: 'false'
|
||||
- id: g_sn_add # ring slot to show/position this tick (-1 none)
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '-1'
|
||||
- id: g_sn_del # ring slot to park off-screen this tick (-1 none)
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '-1'
|
||||
- id: g_sn_fx
|
||||
type: float[{NFSLOT}]
|
||||
restore_value: no
|
||||
- id: g_sn_fy
|
||||
type: float[{NFSLOT}]
|
||||
restore_value: no
|
||||
- id: g_sn_fi
|
||||
type: int[{NFSLOT}]
|
||||
restore_value: no
|
||||
- id: g_sn_fon
|
||||
type: bool[{NFSLOT}]
|
||||
restore_value: no
|
||||
- id: g_sn_fclk
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
- id: g_sn_newfruit
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '-1'
|
||||
- id: g_sn_skx
|
||||
type: float[{NSKULL}]
|
||||
restore_value: no
|
||||
- id: g_sn_sky
|
||||
type: float[{NSKULL}]
|
||||
restore_value: no
|
||||
- id: g_sn_skon
|
||||
type: bool[{NSKULL}]
|
||||
restore_value: no
|
||||
- id: g_sn_skage
|
||||
type: int[{NSKULL}]
|
||||
restore_value: no
|
||||
- id: g_sn_skclk
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '0'
|
||||
- id: g_sn_newskull
|
||||
type: int
|
||||
restore_value: no
|
||||
initial_value: '-1'
|
||||
|
||||
script:
|
||||
{fruit_setters()}
|
||||
- id: sn_hud
|
||||
then:
|
||||
- lvgl.label.update: {{ id: lbl_sn_score, text: !lambda 'char b[12]; snprintf(b,sizeof(b),"%d",id(g_sn_score)); return std::string(b);' }}
|
||||
- id: sn_items
|
||||
then:
|
||||
{items_hidden()}
|
||||
# full body redraw - used at start / when (re)entering the play screen (one lambda, not per tick)
|
||||
- id: sn_drawfull
|
||||
then:
|
||||
- lambda: |-
|
||||
{SEGINIT}
|
||||
for (int k = 0; k < {POOL}; k++) {{
|
||||
int d = (id(g_sn_head) - k + {M}) % {POOL};
|
||||
if (d < id(g_sn_len)) lv_obj_set_pos(SS[k], 180 + (int) lroundf(id(g_sn_tx)[k]) - {HALF}, 180 + (int) lroundf(id(g_sn_ty)[k]) - {HALF});
|
||||
else lv_obj_set_pos(SS[k], 400, 400);
|
||||
}}
|
||||
lv_obj_set_pos(id(sn_head), 180 + (int) lroundf(id(g_sn_hx)) - {HALF}, 180 + (int) lroundf(id(g_sn_hy)) - {HALF});
|
||||
- script.execute: sn_items
|
||||
# hide every gameplay widget (park body off-screen + hide fruit/skulls)
|
||||
- id: sn_clear
|
||||
then:
|
||||
- lambda: |-
|
||||
{SEGINIT}
|
||||
for (int k = 0; k < {POOL}; k++) lv_obj_set_pos(SS[k], 400, 400);
|
||||
lv_obj_set_pos(id(sn_head), 400, 400);
|
||||
{item_hides()}
|
||||
- id: sn_start
|
||||
then:
|
||||
- lambda: |-
|
||||
id(g_sn_state) = 1; id(g_sn_score) = 0; id(g_sn_dead) = false;
|
||||
id(g_sn_ang) = -1.5708f; id(g_sn_grow) = 0; // heading up
|
||||
id(g_sn_len) = {START_LEN}; id(g_sn_head) = {START_LEN - 1};
|
||||
for (int i = 0; i < {POOL}; i++) {{ id(g_sn_tx)[i] = 0.0f; id(g_sn_ty)[i] = 0.0f; }}
|
||||
for (int k = 0; k < {START_LEN}; k++) {{ id(g_sn_tx)[k] = 0.0f; id(g_sn_ty)[k] = ({START_LEN - 1} - k) * {SEG_DIST}f; }}
|
||||
id(g_sn_hx) = 0.0f; id(g_sn_hy) = 0.0f;
|
||||
for (int s = 0; s < {NFSLOT}; s++) id(g_sn_fon)[s] = false;
|
||||
for (int s = 0; s < {NSKULL}; s++) {{ id(g_sn_skon)[s] = false; id(g_sn_skage)[s] = 0; }}
|
||||
id(g_sn_fclk) = 0; id(g_sn_skclk) = 0;
|
||||
id(g_sn_newfruit) = -1; id(g_sn_newskull) = -1; id(g_sn_add) = -1; id(g_sn_del) = -1;
|
||||
id(g_knob_capture) = true; id(g_knob_delta) = 0;
|
||||
float a = (esp_random() % 62832) / 10000.0f; float rad = 60.0f + (esp_random() % 80);
|
||||
id(g_sn_fx)[0] = cosf(a)*rad; id(g_sn_fy)[0] = sinf(a)*rad;
|
||||
id(g_sn_fi)[0] = esp_random() % {NFRUIT}; id(g_sn_fon)[0] = true;
|
||||
- script.execute: sn_fspr0
|
||||
- script.execute: sn_screen
|
||||
- script.execute: sn_drawfull
|
||||
- script.execute: sn_hud
|
||||
- id: sn_over
|
||||
then:
|
||||
- lambda: |-
|
||||
id(g_sn_state) = 2;
|
||||
int s = id(g_sn_score);
|
||||
if (s > id(g_sn_best)) id(g_sn_best) = s;
|
||||
for (int i = 0; i < 10; i++) {{
|
||||
if (s > id(g_sn_top)[i]) {{
|
||||
for (int j = 9; j > i; j--) id(g_sn_top)[j] = id(g_sn_top)[j-1];
|
||||
id(g_sn_top)[i] = s;
|
||||
break;
|
||||
}}
|
||||
}}
|
||||
- script.execute: sn_screen
|
||||
# screen state + the single knob-capture authority (captured only in play + game over)
|
||||
- id: sn_screen
|
||||
then:
|
||||
- lambda: 'id(g_knob_capture) = (id(g_sn_state) == 1 || id(g_sn_state) == 2);'
|
||||
- lvgl.widget.hide: sn_ring
|
||||
- script.execute: sn_clear
|
||||
- lvgl.widget.hide: lbl_sn_score
|
||||
- lvgl.widget.hide: sn_logo
|
||||
- lvgl.widget.hide: btn_sn_start
|
||||
- lvgl.widget.hide: btn_sn_howto
|
||||
- lvgl.widget.hide: btn_sn_scores
|
||||
- lvgl.widget.hide: lbl_sn_over
|
||||
- lvgl.widget.hide: lbl_sn_rec
|
||||
- lvgl.widget.hide: btn_sn_replay
|
||||
- lvgl.widget.hide: btn_sn_exit
|
||||
- lvgl.widget.hide: lbl_sn_howto_t
|
||||
- lvgl.widget.hide: sn_b1
|
||||
- lvgl.widget.hide: sn_b2
|
||||
- lvgl.widget.hide: sn_b3
|
||||
- lvgl.widget.hide: sn_b4
|
||||
- lvgl.widget.hide: lbl_sn_howto1
|
||||
- lvgl.widget.hide: lbl_sn_howto2
|
||||
- lvgl.widget.hide: lbl_sn_howto3
|
||||
- lvgl.widget.hide: lbl_sn_howto4
|
||||
- lvgl.widget.hide: lbl_sn_scores_t
|
||||
- lvgl.widget.hide: lbl_sn_scores1
|
||||
- lvgl.widget.hide: lbl_sn_scores2
|
||||
- lvgl.widget.hide: btn_sn_back
|
||||
- if: # MENU
|
||||
condition: {{ lambda: 'return id(g_sn_state) == 0;' }}
|
||||
then:
|
||||
- lvgl.widget.show: sn_logo
|
||||
- lvgl.widget.show: btn_sn_start
|
||||
- lvgl.widget.show: btn_sn_howto
|
||||
- lvgl.widget.show: btn_sn_scores
|
||||
- if: # PLAY
|
||||
condition: {{ lambda: 'return id(g_sn_state) == 1;' }}
|
||||
then:
|
||||
- lvgl.widget.show: sn_ring
|
||||
- lvgl.widget.show: lbl_sn_score
|
||||
- script.execute: sn_drawfull
|
||||
- if: # GAME OVER
|
||||
condition: {{ lambda: 'return id(g_sn_state) == 2;' }}
|
||||
then:
|
||||
- lvgl.widget.show: sn_logo
|
||||
- lvgl.widget.show: lbl_sn_over
|
||||
- lvgl.widget.show: lbl_sn_rec
|
||||
- lvgl.widget.show: btn_sn_replay
|
||||
- lvgl.widget.show: btn_sn_exit
|
||||
- lvgl.label.update: {{ id: lbl_sn_over, text: !lambda 'char b[24]; snprintf(b,sizeof(b),"Score: %d", id(g_sn_score)); return std::string(b);' }}
|
||||
- lvgl.label.update: {{ id: lbl_sn_rec, text: !lambda 'char b[24]; snprintf(b,sizeof(b),"Best: %d", id(g_sn_best)); return std::string(b);' }}
|
||||
- if: # HOW TO PLAY
|
||||
condition: {{ lambda: 'return id(g_sn_state) == 3;' }}
|
||||
then:
|
||||
- lvgl.widget.show: lbl_sn_howto_t
|
||||
- lvgl.widget.show: sn_b1
|
||||
- lvgl.widget.show: sn_b2
|
||||
- lvgl.widget.show: sn_b3
|
||||
- lvgl.widget.show: sn_b4
|
||||
- lvgl.widget.show: lbl_sn_howto1
|
||||
- lvgl.widget.show: lbl_sn_howto2
|
||||
- lvgl.widget.show: lbl_sn_howto3
|
||||
- lvgl.widget.show: lbl_sn_howto4
|
||||
- lvgl.widget.show: btn_sn_back
|
||||
- if: # SCORES (top 10)
|
||||
condition: {{ lambda: 'return id(g_sn_state) == 4;' }}
|
||||
then:
|
||||
- lvgl.widget.show: lbl_sn_scores_t
|
||||
- lvgl.widget.show: lbl_sn_scores1
|
||||
- lvgl.widget.show: lbl_sn_scores2
|
||||
- lvgl.widget.show: btn_sn_back
|
||||
- lvgl.label.update:
|
||||
id: lbl_sn_scores1
|
||||
text: !lambda |-
|
||||
std::string s; char b[24];
|
||||
for (int i = 0; i < 5; i++) {{ if(i) s += "\\n"; if(id(g_sn_top)[i]>0){{ snprintf(b,sizeof(b),"%d. %d",i+1,id(g_sn_top)[i]); }} else {{ snprintf(b,sizeof(b),"%d. -",i+1); }} s += b; }}
|
||||
return s;
|
||||
- lvgl.label.update:
|
||||
id: lbl_sn_scores2
|
||||
text: !lambda |-
|
||||
std::string s; char b[24];
|
||||
for (int i = 5; i < 10; i++) {{ if(i>5) s += "\\n"; if(id(g_sn_top)[i]>0){{ snprintf(b,sizeof(b),"%d. %d",i+1,id(g_sn_top)[i]); }} else {{ snprintf(b,sizeof(b),"%d. -",i+1); }} s += b; }}
|
||||
return s;
|
||||
|
||||
interval:
|
||||
- interval: 250ms
|
||||
then:
|
||||
- if:
|
||||
condition: {{ lambda: 'return id(g_sn_reset) || id(g_factory_reset);' }}
|
||||
then:
|
||||
- lambda: 'for (int i = 0; i < 10; i++) id(g_sn_top)[i] = 0; id(g_sn_best) = 0; id(g_sn_reset) = false;'
|
||||
- interval: 50ms
|
||||
then:
|
||||
- if:
|
||||
condition: {{ lambda: 'return id(g_nav_req) && id(g_base)==9;' }}
|
||||
then:
|
||||
- lambda: 'id(g_nav_req) = false; id(g_sn_state) = 0; id(g_knob_delta) = 0;'
|
||||
- if: {{ condition: {{ lambda: 'return id(g_nav_anim)==0;' }}, then: [lvgl.page.show: {{ id: page_snake, animation: move_left, time: 250ms }}] }}
|
||||
- if: {{ condition: {{ lambda: 'return id(g_nav_anim)==1;' }}, then: [lvgl.page.show: {{ id: page_snake, animation: move_right, time: 250ms }}] }}
|
||||
- if: {{ condition: {{ lambda: 'return id(g_nav_anim)==2;' }}, then: [lvgl.page.show: {{ id: page_snake, animation: move_top, time: 250ms }}] }}
|
||||
- if: {{ condition: {{ lambda: 'return id(g_nav_anim)==3;' }}, then: [lvgl.page.show: {{ id: page_snake, animation: move_bottom, time: 250ms }}] }}
|
||||
- script.execute: sn_screen
|
||||
# Game tick: steer + glide head; lay/drop one trail dot when the head advanced SEG_DIST.
|
||||
# All body/head positioning is done IN this lambda (lv_obj_set_pos) to keep the action chain shallow.
|
||||
- interval: 50ms
|
||||
then:
|
||||
- if:
|
||||
condition: {{ lambda: 'return id(g_base) == 9 && id(g_sn_state) == 1;' }}
|
||||
then:
|
||||
- lambda: |-
|
||||
id(g_sn_add) = -1; id(g_sn_del) = -1; id(g_sn_newfruit) = -1; id(g_sn_newskull) = -1;
|
||||
int kd = id(g_knob_delta); id(g_knob_delta) = 0;
|
||||
id(g_sn_ang) += kd * {TURN}f;
|
||||
float spd = {SPEED0}f + id(g_sn_score) / 400.0f; if (spd > 5.5f) spd = 5.5f;
|
||||
float hx = id(g_sn_hx) + cosf(id(g_sn_ang)) * spd;
|
||||
float hy = id(g_sn_hy) + sinf(id(g_sn_ang)) * spd;
|
||||
if (hx*hx + hy*hy > {R}f * {R}f) {{ id(g_sn_dead) = true; }} // hit the ring
|
||||
else {{
|
||||
id(g_sn_hx) = hx; id(g_sn_hy) = hy;
|
||||
int len = id(g_sn_len), head = id(g_sn_head);
|
||||
for (int i = {SELF_SKIP}; i < len; i++) {{
|
||||
int sl = (head - i + {M}) % {POOL};
|
||||
float dx = hx - id(g_sn_tx)[sl], dy = hy - id(g_sn_ty)[sl];
|
||||
if (dx*dx + dy*dy < {SELF_DIST}f * {SELF_DIST}f) {{ id(g_sn_dead) = true; break; }}
|
||||
}}
|
||||
for (int s = 0; s < {NSKULL}; s++) if (id(g_sn_skon)[s]) {{
|
||||
float dx = hx - id(g_sn_skx)[s], dy = hy - id(g_sn_sky)[s];
|
||||
if (dx*dx + dy*dy < {SKHIT}f * {SKHIT}f) {{ id(g_sn_dead) = true; }}
|
||||
}}
|
||||
for (int s = 0; s < {NFSLOT} && !id(g_sn_dead); s++) if (id(g_sn_fon)[s]) {{
|
||||
float dx = hx - id(g_sn_fx)[s], dy = hy - id(g_sn_fy)[s];
|
||||
if (dx*dx + dy*dy < {EAT}f * {EAT}f) {{ id(g_sn_fon)[s] = false; id(g_sn_score) += 10; id(g_sn_grow) += {GROW}; }}
|
||||
}}
|
||||
if (!id(g_sn_dead)) {{
|
||||
float dx = hx - id(g_sn_tx)[head], dy = hy - id(g_sn_ty)[head];
|
||||
if (dx*dx + dy*dy >= {SEG_DIST}f * {SEG_DIST}f) {{
|
||||
int nh = (head + 1) % {POOL};
|
||||
id(g_sn_tx)[nh] = hx; id(g_sn_ty)[nh] = hy;
|
||||
id(g_sn_head) = nh; id(g_sn_add) = nh;
|
||||
if (id(g_sn_grow) > 0 && id(g_sn_len) < {POOL - 1}) {{ id(g_sn_grow)--; id(g_sn_len)++; id(g_sn_del) = -1; }} // cap at POOL-1: the ring needs one free slot or add/drop collide
|
||||
else {{ if (id(g_sn_grow) > 0) id(g_sn_grow)--; id(g_sn_del) = (head - id(g_sn_len) + 1 + {M}) % {POOL}; }}
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
if (!id(g_sn_dead)) {{
|
||||
int fc = 0; for (int s = 0; s < {NFSLOT}; s++) if (id(g_sn_fon)[s]) fc++;
|
||||
id(g_sn_fclk)++;
|
||||
int wait = (fc <= 1) ? {FRUIT_WAIT // 2} : {FRUIT_WAIT};
|
||||
if (fc < {NFSLOT} && id(g_sn_fclk) >= wait) {{
|
||||
id(g_sn_fclk) = 0;
|
||||
int sl = -1; for (int s = 0; s < {NFSLOT}; s++) if (!id(g_sn_fon)[s]) {{ sl = s; break; }}
|
||||
if (sl >= 0) {{
|
||||
float a = (esp_random() % 62832) / 10000.0f; float rad = 25.0f + (esp_random() % 140);
|
||||
id(g_sn_fx)[sl] = cosf(a)*rad; id(g_sn_fy)[sl] = sinf(a)*rad;
|
||||
id(g_sn_fi)[sl] = esp_random() % {NFRUIT}; id(g_sn_fon)[sl] = true; id(g_sn_newfruit) = sl;
|
||||
}}
|
||||
}}
|
||||
int sc = 0; for (int s = 0; s < {NSKULL}; s++) if (id(g_sn_skon)[s]) sc++;
|
||||
id(g_sn_skclk)++;
|
||||
if (sc < {NSKULL} && id(g_sn_skclk) >= {SKULL_WAIT}) {{
|
||||
id(g_sn_skclk) = 0;
|
||||
int sl = -1; for (int s = 0; s < {NSKULL}; s++) if (!id(g_sn_skon)[s]) {{ sl = s; break; }}
|
||||
if (sl >= 0) {{
|
||||
float a = (esp_random() % 62832) / 10000.0f; float rad = 30.0f + (esp_random() % 130);
|
||||
id(g_sn_skx)[sl] = cosf(a)*rad; id(g_sn_sky)[sl] = sinf(a)*rad;
|
||||
id(g_sn_skon)[sl] = true; id(g_sn_skage)[sl] = 0; id(g_sn_newskull) = sl;
|
||||
}}
|
||||
}}
|
||||
for (int s = 0; s < {NSKULL}; s++) if (id(g_sn_skon)[s]) {{ id(g_sn_skage)[s]++; if (id(g_sn_skage)[s] >= {SKULL_LIFE}) id(g_sn_skon)[s] = false; }}
|
||||
// render (constant cost): head every tick + the one added/dropped dot
|
||||
lv_obj_set_pos(id(sn_head), 180 + (int) lroundf(id(g_sn_hx)) - {HALF}, 180 + (int) lroundf(id(g_sn_hy)) - {HALF});
|
||||
if (id(g_sn_add) >= 0 || id(g_sn_del) >= 0) {{
|
||||
{SEGINIT}
|
||||
if (id(g_sn_add) >= 0) lv_obj_set_pos(SS[id(g_sn_add)], 180 + (int) lroundf(id(g_sn_tx)[id(g_sn_add)]) - {HALF}, 180 + (int) lroundf(id(g_sn_ty)[id(g_sn_add)]) - {HALF});
|
||||
if (id(g_sn_del) >= 0) lv_obj_set_pos(SS[id(g_sn_del)], 400, 400);
|
||||
}}
|
||||
}}
|
||||
- if:
|
||||
condition: {{ lambda: 'return id(g_sn_dead);' }}
|
||||
then:
|
||||
- script.execute: sn_over
|
||||
else:
|
||||
- if:
|
||||
condition: {{ lambda: 'return id(g_sn_newfruit) >= 0;' }}
|
||||
then:
|
||||
{fspr_dispatch()}
|
||||
- if:
|
||||
condition: {{ lambda: 'return id(g_sn_newskull) >= 0;' }}
|
||||
then:
|
||||
{skull_dispatch()}
|
||||
- script.execute: sn_items
|
||||
- script.execute: sn_hud
|
||||
|
||||
image:
|
||||
- id: img_sn_logo
|
||||
file: "{BASE}snake-logo.png"
|
||||
type: RGB565
|
||||
{fruit_images()}
|
||||
- id: img_sn_skull
|
||||
file: "{BASE}skull.png"
|
||||
type: RGB565
|
||||
transparency: alpha_channel
|
||||
|
||||
lvgl:
|
||||
pages:
|
||||
- id: page_snake
|
||||
bg_color: {PAGE_BG}
|
||||
scrollbar_mode: "off"
|
||||
scrollable: false
|
||||
widgets:
|
||||
- obj: {{ id: sn_ring, align: CENTER, x: 0, y: 0, width: {RING_D}, height: {RING_D}, radius: {RING_D // 2}, bg_opa: TRANSP, border_width: 4, border_color: {RING}, hidden: true }}
|
||||
{fruit_widgets()}
|
||||
{skull_widgets()}
|
||||
{body_widgets()}
|
||||
- obj: {{ id: sn_head, x: 400, y: 400, width: {SEG}, height: {SEG}, radius: {SEGR}, border_width: 0, bg_color: {HEAD}, bg_opa: COVER }}
|
||||
- label: {{ id: lbl_sn_score, align: CENTER, x: 0, y: -150, text: "0", text_font: font_med, text_color: {HUD}, hidden: true }}
|
||||
- image: {{ id: sn_logo, align: CENTER, x: 0, y: 0, src: img_sn_logo, hidden: true }}
|
||||
- button:
|
||||
id: btn_sn_start
|
||||
align: CENTER
|
||||
y: 4
|
||||
width: 150
|
||||
height: 44
|
||||
radius: 22
|
||||
bg_color: {ACCENT}
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
on_click: [ script.execute: sn_start ]
|
||||
widgets:
|
||||
- label: {{ align: CENTER, text: "Start", text_font: font_med, text_color: {ACCENT_TXT} }}
|
||||
- button:
|
||||
id: btn_sn_howto
|
||||
align: CENTER
|
||||
y: 54
|
||||
width: 150
|
||||
height: 44
|
||||
radius: 22
|
||||
bg_color: {BTN2}
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
on_click: [ lambda: 'id(g_sn_state) = 3;', script.execute: sn_screen ]
|
||||
widgets:
|
||||
- label: {{ align: CENTER, text: "How to play?", text_font: font_med, text_color: {BTN2_TXT} }}
|
||||
- button:
|
||||
id: btn_sn_scores
|
||||
align: CENTER
|
||||
y: 104
|
||||
width: 150
|
||||
height: 44
|
||||
radius: 22
|
||||
bg_color: {BTN2}
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
on_click: [ lambda: 'id(g_sn_state) = 4;', script.execute: sn_screen ]
|
||||
widgets:
|
||||
- label: {{ align: CENTER, text: "Scores", text_font: font_med, text_color: {BTN2_TXT} }}
|
||||
- label: {{ id: lbl_sn_over, align: CENTER, x: 0, y: -10, text_align: CENTER, text: "", text_font: font_sel, text_color: {WORDMARK}, hidden: true }}
|
||||
- label: {{ id: lbl_sn_rec, align: CENTER, x: 0, y: 22, text_align: CENTER, text: "", text_font: font_artist, text_color: {WORDMARK}, hidden: true }}
|
||||
- button:
|
||||
id: btn_sn_replay
|
||||
align: CENTER
|
||||
x: -66
|
||||
y: 62
|
||||
width: 112
|
||||
height: 48
|
||||
radius: 24
|
||||
bg_color: {ACCENT}
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
hidden: true
|
||||
on_click: [ script.execute: sn_start ]
|
||||
widgets:
|
||||
- label: {{ align: CENTER, text: "Play", text_font: font_med, text_color: {ACCENT_TXT} }}
|
||||
- button:
|
||||
id: btn_sn_exit
|
||||
align: CENTER
|
||||
x: 66
|
||||
y: 62
|
||||
width: 112
|
||||
height: 48
|
||||
radius: 24
|
||||
bg_color: {GRAY}
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
hidden: true
|
||||
on_click: [ lambda: 'id(g_sn_state) = 0;', script.execute: sn_screen ]
|
||||
widgets:
|
||||
- label: {{ align: CENTER, text: "Exit", text_font: font_med, text_color: 0xFFFFFF }}
|
||||
- label: {{ id: lbl_sn_howto_t, align: CENTER, y: -104, text: "HOW TO PLAY?", text_font: font_sel, text_color: {ACCENT}, hidden: true }}
|
||||
- obj: {{ id: sn_b1, align: CENTER, x: -96, y: -52, width: 24, height: 24, radius: 12, bg_color: {BADGE}, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: {{ align: CENTER, text: "1", text_font: font_artist, text_color: 0xFFFFFF }} ] }}
|
||||
- obj: {{ id: sn_b2, align: CENTER, x: -96, y: -16, width: 24, height: 24, radius: 12, bg_color: {BADGE}, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: {{ align: CENTER, text: "2", text_font: font_artist, text_color: 0xFFFFFF }} ] }}
|
||||
- obj: {{ id: sn_b3, align: CENTER, x: -96, y: 20, width: 24, height: 24, radius: 12, bg_color: {BADGE}, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: {{ align: CENTER, text: "3", text_font: font_artist, text_color: 0xFFFFFF }} ] }}
|
||||
- obj: {{ id: sn_b4, align: CENTER, x: -96, y: 56, width: 24, height: 24, radius: 12, bg_color: 0xE05A4A, bg_opa: COVER, border_width: 0, pad_all: 0, hidden: true, widgets: [ label: {{ align: CENTER, text: "!", text_font: font_artist, text_color: 0xFFFFFF }} ] }}
|
||||
- label: {{ id: lbl_sn_howto1, align_to: {{ id: sn_b1, align: OUT_RIGHT_MID, x: 8 }}, text: "Turn knob to steer", text_font: font_med, text_color: {TXT}, hidden: true }}
|
||||
- label: {{ id: lbl_sn_howto2, align_to: {{ id: sn_b2, align: OUT_RIGHT_MID, x: 8 }}, text: "Eat fruit to grow", text_font: font_med, text_color: {TXT}, hidden: true }}
|
||||
- label: {{ id: lbl_sn_howto3, align_to: {{ id: sn_b3, align: OUT_RIGHT_MID, x: 8 }}, text: "Avoid the ring & tail", text_font: font_med, text_color: {TXT}, hidden: true }}
|
||||
- label: {{ id: lbl_sn_howto4, align_to: {{ id: sn_b4, align: OUT_RIGHT_MID, x: 8 }}, text: "Skulls kill - avoid!", text_font: font_med, text_color: {TXT}, hidden: true }}
|
||||
- label: {{ id: lbl_sn_scores_t, align: CENTER, y: -116, text: "SCORES", text_font: font_sel, text_color: {ACCENT}, hidden: true }}
|
||||
- label: {{ id: lbl_sn_scores1, align: CENTER, x: -52, y: 0, text: "", text_align: LEFT, text_font: font_artist, text_color: {TXT}, hidden: true }}
|
||||
- label: {{ id: lbl_sn_scores2, align: CENTER, x: 56, y: 0, text: "", text_align: LEFT, text_font: font_artist, text_color: {TXT}, hidden: true }}
|
||||
- button:
|
||||
id: btn_sn_back
|
||||
align: CENTER
|
||||
y: 120
|
||||
width: 130
|
||||
height: 42
|
||||
radius: 21
|
||||
bg_color: {GRAY}
|
||||
bg_opa: COVER
|
||||
border_width: 0
|
||||
hidden: true
|
||||
on_click: [ lambda: 'id(g_sn_state) = 0;', script.execute: sn_screen ]
|
||||
widgets:
|
||||
- label: {{ align: CENTER, text: "Back", text_font: font_med, text_color: 0xFFFFFF }}
|
||||
"""
|
||||
|
||||
here = os.path.dirname(os.path.abspath(__file__))
|
||||
out = os.path.join(here, "..", "base", "screens", "snake.yaml")
|
||||
out = os.path.normpath(out)
|
||||
with open(out, "w", encoding="utf-8", newline="\n") as f:
|
||||
f.write(YAML)
|
||||
print("wrote", out, "(", len(YAML), "bytes )")
|
||||
Reference in new issue
Block a user