Mystify Demo, TwoEleven updates & Snake (#21)
* **New Features** * Added Mystify screensaver demo with animated polygons and trails * Added Snake game with multiple difficulties, high-score persistence, and multi-input (touch/keyboard) support * Added CLI tool for building, packaging, and deploying apps (end-to-end build/install/run workflow) * Per-grid-size high-score persistence added to 2048 app; expanded keyboard controls (WASD and device-specific mappings) * **Documentation** * Added Snake README with gameplay, controls, and usage instructions
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
/**
|
||||
* @file SnakeLogic.c
|
||||
* @brief Pure game logic for the Snake game
|
||||
*/
|
||||
#include "SnakeLogic.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/**
|
||||
* @brief Initialize the snake body as a linked list
|
||||
*/
|
||||
snake_segment_t* snake_init_body(uint16_t length, int16_t start_x, int16_t start_y) {
|
||||
if (length == 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Create head segment
|
||||
snake_segment_t* head = (snake_segment_t*)lv_malloc(sizeof(snake_segment_t));
|
||||
if (!head) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
head->x = start_x;
|
||||
head->y = start_y;
|
||||
head->obj = NULL;
|
||||
head->prior = NULL;
|
||||
head->next = NULL;
|
||||
|
||||
// Create remaining segments (body extends to the left of head)
|
||||
snake_segment_t* current = head;
|
||||
for (uint16_t i = 1; i < length; i++) {
|
||||
snake_segment_t* segment = (snake_segment_t*)lv_malloc(sizeof(snake_segment_t));
|
||||
if (!segment) {
|
||||
// Clean up already allocated segments
|
||||
snake_free_body(head);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
segment->x = start_x - i; // Body extends left from head
|
||||
segment->y = start_y;
|
||||
segment->obj = NULL;
|
||||
segment->prior = current;
|
||||
segment->next = NULL;
|
||||
|
||||
current->next = segment;
|
||||
current = segment;
|
||||
}
|
||||
|
||||
return head;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Free all memory used by the snake body
|
||||
*/
|
||||
void snake_free_body(snake_segment_t* head) {
|
||||
snake_segment_t* current = head;
|
||||
while (current != NULL) {
|
||||
snake_segment_t* next = current->next;
|
||||
// Note: LVGL objects must be deleted separately by the UI layer
|
||||
lv_free(current);
|
||||
current = next;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Add a new segment to the tail of the snake
|
||||
*/
|
||||
bool snake_grow(snake_segment_t* head) {
|
||||
if (!head) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find the tail
|
||||
snake_segment_t* tail = head;
|
||||
while (tail->next != NULL) {
|
||||
tail = tail->next;
|
||||
}
|
||||
|
||||
// Create new segment at tail position (will be updated on next move)
|
||||
snake_segment_t* new_segment = (snake_segment_t*)lv_malloc(sizeof(snake_segment_t));
|
||||
if (!new_segment) {
|
||||
return false;
|
||||
}
|
||||
|
||||
new_segment->x = tail->x;
|
||||
new_segment->y = tail->y;
|
||||
new_segment->obj = NULL;
|
||||
new_segment->prior = tail;
|
||||
new_segment->next = NULL;
|
||||
|
||||
tail->next = new_segment;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Move the snake one step in the current direction
|
||||
*/
|
||||
bool snake_move(snake_game_t* game) {
|
||||
if (!game || !game->head || game->game_over) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Apply buffered direction
|
||||
game->direction = game->next_direction;
|
||||
|
||||
// Calculate new head position
|
||||
int16_t new_x = game->head->x;
|
||||
int16_t new_y = game->head->y;
|
||||
|
||||
switch (game->direction) {
|
||||
case SNAKE_DIR_UP:
|
||||
new_y--;
|
||||
break;
|
||||
case SNAKE_DIR_DOWN:
|
||||
new_y++;
|
||||
break;
|
||||
case SNAKE_DIR_LEFT:
|
||||
new_x--;
|
||||
break;
|
||||
case SNAKE_DIR_RIGHT:
|
||||
new_x++;
|
||||
break;
|
||||
}
|
||||
|
||||
// Handle wall collision or wrap-around
|
||||
if (game->wall_collision_enabled) {
|
||||
// Wall collision mode - hitting walls = game over
|
||||
if (new_x < 0 || new_x >= game->grid_width ||
|
||||
new_y < 0 || new_y >= game->grid_height) {
|
||||
game->game_over = true;
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// Wrap around walls
|
||||
if (new_x < 0) new_x = game->grid_width - 1;
|
||||
else if (new_x >= game->grid_width) new_x = 0;
|
||||
if (new_y < 0) new_y = game->grid_height - 1;
|
||||
else if (new_y >= game->grid_height) new_y = 0;
|
||||
}
|
||||
|
||||
// Check for self collision BEFORE moving (so tail hasn't vacated yet)
|
||||
// Skip the tail segment since it will move out of the way
|
||||
snake_segment_t* segment = game->head->next;
|
||||
while (segment != NULL && segment->next != NULL) { // Stop before tail
|
||||
if (segment->x == new_x && segment->y == new_y) {
|
||||
game->game_over = true;
|
||||
return false;
|
||||
}
|
||||
segment = segment->next;
|
||||
}
|
||||
// Also check the tail - it will move, so new head CAN go there
|
||||
// (This is intentional - allows snake to "chase its tail")
|
||||
|
||||
// Move body segments (from tail to head, each takes position of previous)
|
||||
snake_segment_t* tail = game->head;
|
||||
while (tail->next != NULL) {
|
||||
tail = tail->next;
|
||||
}
|
||||
|
||||
// Move from tail towards head
|
||||
while (tail->prior != NULL) {
|
||||
tail->x = tail->prior->x;
|
||||
tail->y = tail->prior->y;
|
||||
tail = tail->prior;
|
||||
}
|
||||
|
||||
// Move head to new position
|
||||
game->head->x = new_x;
|
||||
game->head->y = new_y;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set the snake's next direction (with 180° reversal prevention)
|
||||
*/
|
||||
bool snake_set_direction(snake_game_t* game, snake_direction_t dir) {
|
||||
if (!game) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prevent 180° reversal - check against BUFFERED direction to handle rapid inputs
|
||||
snake_direction_t current = game->next_direction;
|
||||
|
||||
if ((current == SNAKE_DIR_UP && dir == SNAKE_DIR_DOWN) ||
|
||||
(current == SNAKE_DIR_DOWN && dir == SNAKE_DIR_UP) ||
|
||||
(current == SNAKE_DIR_LEFT && dir == SNAKE_DIR_RIGHT) ||
|
||||
(current == SNAKE_DIR_RIGHT && dir == SNAKE_DIR_LEFT)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
game->next_direction = dir;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if a position is occupied by the snake body
|
||||
*/
|
||||
static bool is_position_on_snake(snake_segment_t* head, int16_t x, int16_t y) {
|
||||
snake_segment_t* current = head;
|
||||
while (current != NULL) {
|
||||
if (current->x == x && current->y == y) {
|
||||
return true;
|
||||
}
|
||||
current = current->next;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Spawn food at a random location not occupied by snake
|
||||
* @return true if food was placed, false if grid is full (win condition)
|
||||
*/
|
||||
bool snake_spawn_food(snake_game_t* game) {
|
||||
if (!game || game->grid_width == 0 || game->grid_height == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int16_t x, y;
|
||||
const int32_t grid_size = (int32_t)game->grid_width * game->grid_height;
|
||||
uint16_t snake_len = snake_count_segments(game->head);
|
||||
|
||||
// Use random sampling for sparse grids, deterministic search for dense grids
|
||||
if (snake_len < (grid_size * 3 / 4)) {
|
||||
// Random sampling - efficient for sparse grids
|
||||
int attempts = 0;
|
||||
do {
|
||||
x = rand() % game->grid_width;
|
||||
y = rand() % game->grid_height;
|
||||
attempts++;
|
||||
} while (is_position_on_snake(game->head, x, y) && attempts < grid_size);
|
||||
|
||||
if (!is_position_on_snake(game->head, x, y)) {
|
||||
game->food_x = x;
|
||||
game->food_y = y;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Deterministic search - guaranteed to find free cell if one exists
|
||||
int16_t start_y = rand() % game->grid_height;
|
||||
int16_t start_x = rand() % game->grid_width;
|
||||
for (int16_t i = 0; i < game->grid_height; i++) {
|
||||
int16_t gy = (start_y + i) % game->grid_height;
|
||||
for (int16_t j = 0; j < game->grid_width; j++) {
|
||||
int16_t gx = (start_x + j) % game->grid_width;
|
||||
if (!is_position_on_snake(game->head, gx, gy)) {
|
||||
game->food_x = gx;
|
||||
game->food_y = gy;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Grid is truly full - win condition
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if snake head collides with walls (unused - wrap-around enabled)
|
||||
*/
|
||||
bool snake_check_wall_collision(snake_game_t* game) {
|
||||
if (!game || !game->head) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int16_t x = game->head->x;
|
||||
int16_t y = game->head->y;
|
||||
|
||||
return (x < 0 || x >= game->grid_width || y < 0 || y >= game->grid_height);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if snake head collides with its own body
|
||||
*/
|
||||
bool snake_check_self_collision(snake_game_t* game) {
|
||||
if (!game || !game->head) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int16_t head_x = game->head->x;
|
||||
int16_t head_y = game->head->y;
|
||||
|
||||
// Check collision with body segments (skip head itself)
|
||||
snake_segment_t* current = game->head->next;
|
||||
while (current != NULL) {
|
||||
if (current->x == head_x && current->y == head_y) {
|
||||
return true;
|
||||
}
|
||||
current = current->next;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Check if snake head collides with food
|
||||
*/
|
||||
bool snake_check_food_collision(snake_game_t* game) {
|
||||
if (!game || !game->head) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (game->head->x == game->food_x && game->head->y == game->food_y);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Count the number of segments in the snake
|
||||
*/
|
||||
uint16_t snake_count_segments(snake_segment_t* head) {
|
||||
uint16_t length = 0;
|
||||
snake_segment_t* current = head;
|
||||
|
||||
while (current != NULL) {
|
||||
length++;
|
||||
current = current->next;
|
||||
}
|
||||
|
||||
return length;
|
||||
}
|
||||
Reference in New Issue
Block a user