diff --git a/project/mode_snake.c b/project/mode_snake.c index 93be735..8426510 100644 --- a/project/mode_snake.c +++ b/project/mode_snake.c @@ -24,7 +24,8 @@ typedef enum { } CellType; /** Board cell */ -typedef struct __attribute__((packed)) { +typedef struct __attribute__((packed)) +{ CellType type : 2; // Cell type Direction dir : 2; // direction the head moved to from this tile, if body = 1 } Cell; @@ -47,8 +48,9 @@ static Direction head_dir; /** blinking to visually change 'color' */ -typedef struct { - bool pwm_bit; +typedef struct __attribute__((packed)) { + bool pwm_bit : 1; + bool offtask_pending : 1; task_pid_t on_task; // periodic task_pid_t off_task; // scheduled ms_time_t interval_ms; @@ -56,6 +58,7 @@ typedef struct { } snake_pwm; +static bool alive = false; static bool moving = false; static task_pid_t task_snake_move; @@ -71,13 +74,117 @@ static snake_pwm head_pwm = { .offtime_ms = 90 }; -static ms_time_t move_interval = 500; +#define MIN_MOVE_INTERVAL 64 +#define POINTS_TO_LEVEL_UP 5 +#define PIECES_REMOVED_ON_LEVEL_UP 3 + +#define PREDEFINED_LEVELS 10 +static const ms_time_t speeds_for_levels[PREDEFINED_LEVELS] = { + 320, 245, 190, 150, 120, 100, 80, 60, 50, 40 +}; + +static ms_time_t move_interval; + +static uint32_t score; +static uint32_t level_up_score; // counter +static uint32_t level; + + +static Cell *cell_at_xy(int x, int y); + + +// --- + +static void show_board(void) +{ + if (!snake_active) return; + + dmtx_clear(dmtx); + + for (int x = 0; x < BOARD_W; x++) { + for (int y = 0; y < BOARD_H; y++) { + Cell *cell = cell_at_xy(x, y); + + bool set = 0; + + switch (cell->type) { + case CELL_EMPTY: set = 0; break; + case CELL_BODY: set = 1; break; + case CELL_FOOD: + set = food_pwm.pwm_bit; + break; + + case CELL_WALL: set = 1; break; + } + + if (x == head.x && y == head.y) set = head_pwm.pwm_bit; + + dmtx_set(dmtx, x, y, set); + } + } + + dmtx_show(dmtx); +} + + +// --- Snake PWM --- + +/** Turn off a PWM bit (scheduled callback) */ +static void task_pwm_off_cb(void *ptr) +{ + if (!snake_active) return; + snake_pwm* pwm = ptr; + + if (!pwm->offtask_pending) return; + pwm->offtask_pending = false; + + pwm->pwm_bit = 0; + show_board(); +} + +/** Turn on a PWM bit and schedule the turn-off task (periodic callback) */ +static void task_pwm_on_cb(void *ptr) +{ + if (!snake_active) return; + snake_pwm* pwm = ptr; + + pwm->pwm_bit = 1; + show_board(); + + pwm->offtask_pending = true; + schedule_task(task_pwm_off_cb, ptr, pwm->offtime_ms, true); +} + +/** Initialize a snake PWM channel */ +static void snake_pwm_init(snake_pwm *pwm) +{ + pwm->on_task = add_periodic_task(task_pwm_on_cb, pwm, pwm->interval_ms, true); + enable_periodic_task(pwm->on_task, false); +} + +/** Clear & start a snake PWM channel */ +static void snake_pwm_start(snake_pwm *pwm) +{ + pwm->pwm_bit = 1; + pwm->offtask_pending = false; + reset_periodic_task(pwm->on_task); + enable_periodic_task(pwm->on_task, true); +} + +/** Stop a snake PWM channel */ +static void snake_pwm_stop(snake_pwm *pwm) +{ + pwm->offtask_pending = false; + enable_periodic_task(pwm->on_task, false); + abort_scheduled_task(pwm->off_task); +} + /** Get board cell at coord (x,y) */ static Cell *cell_at_xy(int x, int y) { - if (x < 0 || x >= BOARD_W || y < 0 || y >= BOARD_H ) { + if (x < 0 || x >= BOARD_W || y < 0 || y >= BOARD_H) { return &WALL_TILE; // discards const - OK } @@ -104,15 +211,33 @@ static void place_food(void) if (cell->type == CELL_EMPTY) { cell->type = CELL_FOOD; - dbg("Food at [%d, %d]", food.x, food.y); + //dbg("Food at [%d, %d]", food.x, food.y); break; } } + + // avoid "doubleblink" glitch + reset_periodic_task(food_pwm.on_task); + food_pwm.pwm_bit = 1; + food_pwm.offtask_pending = 0; } /** Clear the board and prepare for a new game */ static void new_game(void) { + // Stop snake (make sure it's stopped) + reset_periodic_task(task_snake_move); + enable_periodic_task(task_snake_move, false); + + moving = false; + alive = true; + + level = 0; + score = 0; + move_interval = speeds_for_levels[level]; + level_up_score = 0; + set_periodic_task_interval(task_snake_move, move_interval); + // randomize (we can assume it takes random time before the user switches to the Snake mode) srand(SysTick->VAL); @@ -121,8 +246,8 @@ static void new_game(void) // Place initial snake - tail.x = BOARD_W/2-5; - tail.y = BOARD_H/2; + tail.x = BOARD_W / 2 - 5; + tail.y = BOARD_H / 2; head.x = tail.x + 4; head.y = tail.y; @@ -131,93 +256,129 @@ static void new_game(void) for (int x = tail.x; x < head.x; x++) { board[tail.y][x].type = CELL_BODY; + board[tail.y][x].dir = EAST; } place_food(); -} -static void show_board(void) -{ - if (!snake_active) return; - - dmtx_clear(dmtx); + snake_pwm_start(&head_pwm); + snake_pwm_start(&food_pwm); - for (int x = 0; x < BOARD_W; x++) { - for (int y = 0; y < BOARD_H; y++) { - Cell *cell = cell_at_xy(x, y); + show_board(); +} - bool set = 0; +static void snake_died(void) +{ + dbg("R.I.P. Snake"); - switch (cell->type) { - case CELL_EMPTY: set = 0; break; - case CELL_BODY: set = 1; break; - case CELL_FOOD: - set = food_pwm.pwm_bit; - break; + moving = false; + alive = false; + enable_periodic_task(task_snake_move, false); - case CELL_WALL: set = 1; break; - } + // stop blinky animation of the snake head. + snake_pwm_stop(&head_pwm); - if (x == head.x && y == head.y) set = head_pwm.pwm_bit; + // TODO death animation + // TODO show score +} - dmtx_set(dmtx, x, y, set); - } +static void move_coord_by_dir(Coord *coord, Direction dir) +{ + switch (dir) { + case NORTH: coord->y++; break; + case SOUTH: coord->y--; break; + case EAST: coord->x++; break; + case WEST: coord->x--; break; } - dmtx_show(dmtx); + // Wrap-around + if (coord->x < 0) coord->x = BOARD_W - 1; + if (coord->y < 0) coord->y = BOARD_H - 1; + if (coord->x >= BOARD_W) coord->x = 0; + if (coord->y >= BOARD_H) coord->y = 0; +} + +static void move_tail(void) +{ + Cell *tail_cell = cell_at(&tail); + tail_cell->type = CELL_EMPTY; + move_coord_by_dir(&tail, tail_cell->dir); } static void snake_move_cb(void *unused) { (void)unused; - dbg("Move."); -} + bool want_new_food = false; -// --- Snake PWM --- + Coord shadowhead = head; -/** Turn off a PWM bit (scheduled callback) */ -static void task_pwm_off_cb(void *ptr) -{ - if (!snake_active) return; + move_coord_by_dir(&shadowhead, head_dir); - ((snake_pwm*)ptr)->pwm_bit = 0; - show_board(); -} + Cell *head_cell = cell_at(&head); + Cell *future_head_cell = cell_at(&shadowhead); -/** Turn on a PWM bit and schedule the turn-off task (periodic callback) */ -static void task_pwm_on_cb(void *ptr) -{ - if (!snake_active) return; + switch (future_head_cell->type) { + case CELL_BODY: + case CELL_WALL: + // R.I.P. + snake_died(); + return; + + case CELL_FOOD: + // grow - no head move + score++; + want_new_food = 1; + level_up_score++; + break; - ((snake_pwm*)ptr)->pwm_bit = 1; + case CELL_EMPTY: + // move tail + move_tail(); + break; + } + + // Move head + // (if died, already returned) + head_cell->dir = head_dir; + head_cell->type = CELL_BODY; + + future_head_cell->type = CELL_BODY; + + // apply movement + head = shadowhead; + + if (want_new_food) { + place_food(); + } + + // render show_board(); - schedule_task(task_pwm_off_cb, ptr, ((snake_pwm*)ptr)->offtime_ms, true); -} + // level up + if (level_up_score == POINTS_TO_LEVEL_UP) { + enable_periodic_task(task_snake_move, false); -/** Initialize a snake PWM channel */ -static void snake_pwm_init(snake_pwm *ptr) -{ - ptr->on_task = add_periodic_task(task_pwm_on_cb, ptr, ptr->interval_ms, true); - enable_periodic_task(ptr->on_task, false); -} + // remove some pieces + for (int i = 0; i < PIECES_REMOVED_ON_LEVEL_UP; i++) { + move_tail(); + until_timeout(move_interval/2) { + tq_poll(); // take care of periodic tasks (anim) + } + } -/** Clear & start a snake PWM channel */ -static void snake_pwm_start(snake_pwm *ptr) -{ - ptr->pwm_bit = 1; - reset_periodic_task(ptr->on_task); - enable_periodic_task(ptr->on_task, true); -} + level++; + level_up_score = 0; -/** Stop a snake PWM channel */ -static void snake_pwm_stop(snake_pwm *ptr) -{ - enable_periodic_task(ptr->on_task, false); - abort_scheduled_task(ptr->off_task); -} + // go faster if not too fast yet + if (level < PREDEFINED_LEVELS) { + move_interval = speeds_for_levels[level]; + } + set_periodic_task_interval(task_snake_move, move_interval); + enable_periodic_task(task_snake_move, true); + } +} /** INIT snake */ @@ -238,10 +399,6 @@ void mode_snake_start(void) snake_pwm_start(&head_pwm); snake_pwm_start(&food_pwm); - // Stop snake (make sure it's stopped) - enable_periodic_task(task_snake_move, false); - moving = false; - new_game(); } @@ -256,30 +413,51 @@ void mode_snake_stop(void) enable_periodic_task(task_snake_move, false); } +/** Change dir (safely) */ +static void change_direction(Direction dir) +{ + // This is a compensation for shitty gamepads + // with accidental arrow hits + + Coord shadowhead = head; + move_coord_by_dir(&shadowhead, dir); + + Cell *target = cell_at(&shadowhead); + + if (target->type == CELL_BODY) return; + + head_dir = dir; +} + /** User button */ void mode_snake_btn(char key) { switch (key) { - case 'U': head_dir = NORTH; break; - case 'D': head_dir = SOUTH; break; - case 'L': head_dir = WEST; break; - case 'R': head_dir = EAST; break; + case 'U': change_direction(NORTH); break; + case 'D': change_direction(SOUTH); break; + case 'L': change_direction(WEST); break; + case 'R': change_direction(EAST); break; + + case 'J': + if (alive) break; + // dead - reset by pressing 'start' case 'K': // clear // TODO reset animation - mode_snake_stop(); - mode_snake_start(); - break; + new_game(); + return; } - if (!moving && (key == 'U' || key == 'D' || key == 'L' || key == 'R' || key == 'J')) { + if (!moving && alive && (key == 'U' || key == 'D' || key == 'L' || key == 'R' || key == 'J')) { // start moving + moving = true; reset_periodic_task(task_snake_move); enable_periodic_task(task_snake_move, true); } // running + start -> 'pause' - if (moving && key == 'J') { + if (alive && moving && key == 'J') { + moving = false; enable_periodic_task(task_snake_move, false); } }