/* * MIT License * * Copyright (c) 2024 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ #include #include #include "endless_runner_face.h" #include "delay.h" typedef enum { JUMPING_FINAL_FRAME = 0, NOT_JUMPING, JUMPING_START, } RunnerJumpState; typedef enum { SCREEN_TITLE = 0, SCREEN_PLAYING, SCREEN_LOSE, SCREEN_TIME, SCREEN_COUNT } RunnerCurrScreen; typedef enum { DIFF_BABY = 0, // FREQ_SLOW FPS; MIN_ZEROES 0's min; Jump is JUMP_FRAMES_EASY frames DIFF_EASY, // FREQ FPS; MIN_ZEROES 0's min; Jump is JUMP_FRAMES_EASY frames DIFF_NORM, // FREQ FPS; MIN_ZEROES 0's min; Jump is JUMP_FRAMES frames DIFF_HARD, // FREQ FPS; MIN_ZEROES_HARD 0's min; jump is JUMP_FRAMES frames DIFF_FUEL, // Mode where the top-right displays the amoount of fuel that you can be above the ground for, dodging obstacles. When on the ground, your fuel recharges. DIFF_FUEL_1, // Same as DIFF_FUEL, but if your fuel is 0, then you won't recharge DIFF_COUNT } RunnerDifficulty; #define NUM_GRID 12 // This the length that the obstacle track can be on #define FREQ 8 // Frequency request for the game #define FREQ_SLOW 4 // Frequency request for baby mode #define JUMP_FRAMES 2 // Wait this many frames on difficulties above EASY before coming down from the jump button pressed #define JUMP_FRAMES_EASY 3 // Wait this many frames on difficulties at or below EASY before coming down from the jump button pressed #define MIN_ZEROES 4 // At minimum, we'll have this many spaces between obstacles #define MIN_ZEROES_HARD 3 // At minimum, we'll have this many spaces between obstacles on hard mode #define MAX_HI_SCORE 9999 // Max hi score to store and display on the title screen. #define MAX_DISP_SCORE 39 // The top-right digits can't properly display above 39 #define JUMP_FRAMES_FUEL 30 // The max fuel that fuel that the fuel mode game will hold #define JUMP_FRAMES_FUEL_RECHARGE 3 // How much fuel each frame on the ground adds #define MAX_DISP_SCORE_FUEL 9 // Since the fuel mode displays the score in the weekday slot, two digits will display wonky data typedef struct { uint32_t obst_pattern; uint16_t obst_indx : 8; uint16_t jump_state : 5; uint16_t sec_before_moves : 3; uint16_t curr_score : 10; uint16_t curr_screen : 4; bool loc_2_on; bool loc_3_on; bool success_jump; bool fuel_mode; uint8_t fuel; } game_state_t; // always-on, left, right, bottom, jump-top, jump-left, jump-right int8_t classic_ball_arr_com[] = {1, 0, 1, 0, 2, 1, 2}; int8_t classic_ball_arr_seg[] = {20, 20, 21, 21, 20, 17, 21}; int8_t custom_ball_arr_com[] = {2, 1, 1, 0, 3, 3, 2}; int8_t custom_ball_arr_seg[] = {15, 15, 14, 15, 14, 15, 14}; // obstacle 0-11 int8_t classic_obstacle_arr_com[] = {0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1}; int8_t classic_obstacle_arr_seg[] = {18, 19, 20, 21, 22, 23, 0, 1, 2, 4, 5, 6}; int8_t custom_obstacle_arr_com[] = {1, 1, 1, 1, 1, 0, 1, 0, 3, 0, 0, 2}; int8_t custom_obstacle_arr_seg[] = {22, 16, 15, 14, 1, 2, 3, 4, 4, 5, 6, 7}; int8_t *ball_arr_com; int8_t *ball_arr_seg; int8_t *obstacle_arr_com; int8_t *obstacle_arr_seg; static game_state_t game_state; static const uint8_t _num_bits_obst_pattern = sizeof(game_state.obst_pattern) * 8; static void print_binary(uint32_t value, int bits) { #if __EMSCRIPTEN__ for (int i = bits - 1; i >= 0; i--) { // Print each bit printf("%u", (value >> i) & 1); // Optional: add a space every 4 bits for readability if (i % 4 == 0 && i != 0) { printf(" "); } } printf("\r\n"); #else (void) value; (void) bits; #endif return; } static uint32_t get_random(uint32_t max) { #if __EMSCRIPTEN__ return rand() % max; #else return arc4random_uniform(max); #endif } static uint32_t get_random_nonzero(uint32_t max) { uint32_t random; do { random = get_random(max); } while (random == 0); return random; } static uint32_t get_random_kinda_nonzero(uint32_t max) { // Returns a number that's between 1 and max, unless max is 0 or 1, then it returns 0 to max. if (max == 0) return 0; else if (max == 1) return get_random(max); return get_random_nonzero(max); } static uint32_t get_random_fuel(uint32_t prev_val) { static uint8_t prev_rand_subset = 0; uint32_t rand; uint8_t max_ones, subset; uint32_t rand_legal = 0; prev_val = prev_val & ~0xFFFF; for (int i = 0; i < 2; i++) { subset = 0; max_ones = 8; if (prev_rand_subset > 4) max_ones -= prev_rand_subset; rand = get_random_kinda_nonzero(max_ones); if (rand > 5 && prev_rand_subset) rand = 5; // The gap of one or two is awkward for (uint32_t j = 0; j < rand; j++) { subset |= (1 << j); } if (prev_rand_subset >= 7) subset = subset << 1; subset &= 0xFF; rand_legal |= subset << (8 * i); prev_rand_subset = rand; } rand_legal = prev_val | rand_legal; print_binary(rand_legal, 32); return rand_legal; } static uint32_t get_random_legal(uint32_t prev_val, uint16_t difficulty) { /** @brief A legal random number starts with the previous number (which should be the 12 bits on the screen). * @param prev_val The previous value to tack onto. The return will have its first NUM_GRID MSBs be the same as prev_val, and the rest be new * @param difficulty To dictate how spread apart the obsticles must be * @return the new random value, where it's first NUM_GRID MSBs are the same as prev_val */ uint8_t min_zeros = (difficulty == DIFF_HARD) ? MIN_ZEROES_HARD : MIN_ZEROES; uint32_t max = (1 << (_num_bits_obst_pattern - NUM_GRID)) - 1; uint32_t rand = get_random_nonzero(max); uint32_t rand_legal = 0; prev_val = prev_val & ~max; for (int i = (NUM_GRID + 1); i <= _num_bits_obst_pattern; i++) { uint32_t mask = 1 << (_num_bits_obst_pattern - i); bool msb = (rand & mask) >> (_num_bits_obst_pattern - i); if (msb) { rand_legal = rand_legal << min_zeros; i+=min_zeros; } rand_legal |= msb; rand_legal = rand_legal << 1; } rand_legal = rand_legal & max; for (int i = 0; i <= min_zeros; i++) { if (prev_val & (1 << (i + _num_bits_obst_pattern - NUM_GRID))){ rand_legal = rand_legal >> (min_zeros - i); break; } } rand_legal = prev_val | rand_legal; print_binary(rand_legal, 32); return rand_legal; } static void display_ball(bool jumping) { if (!jumping) { watch_set_pixel(ball_arr_com[3], ball_arr_seg[3]); watch_set_pixel(ball_arr_com[2], ball_arr_seg[2]); watch_set_pixel(ball_arr_com[1], ball_arr_seg[1]); watch_set_pixel(ball_arr_com[0], ball_arr_seg[0]); watch_clear_pixel(ball_arr_com[6], ball_arr_seg[6]); watch_clear_pixel(ball_arr_com[5], ball_arr_seg[5]); watch_clear_pixel(ball_arr_com[4], ball_arr_seg[4]); } else { watch_clear_pixel(ball_arr_com[3], ball_arr_seg[3]); watch_clear_pixel(ball_arr_com[2], ball_arr_seg[2]); watch_clear_pixel(ball_arr_com[1], ball_arr_seg[1]); watch_set_pixel(ball_arr_com[0], ball_arr_seg[0]); watch_set_pixel(ball_arr_com[6], ball_arr_seg[6]); watch_set_pixel(ball_arr_com[5], ball_arr_seg[5]); watch_set_pixel(ball_arr_com[4], ball_arr_seg[4]); } } static void display_score(uint8_t score) { char buf[3]; if (game_state.fuel_mode) { score %= (MAX_DISP_SCORE_FUEL + 1); sprintf(buf, "%1d", score); watch_display_text(WATCH_POSITION_TOP_LEFT, buf); } else { score %= (MAX_DISP_SCORE + 1); sprintf(buf, "%2d", score); watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); } } static void add_to_score(endless_runner_state_t *state) { if (game_state.curr_score <= MAX_HI_SCORE) { game_state.curr_score++; if (game_state.curr_score > state -> hi_score) state -> hi_score = game_state.curr_score; } game_state.success_jump = true; display_score(game_state.curr_score); } static void display_fuel(uint8_t subsecond, uint8_t difficulty) { char buf[4]; if (difficulty == DIFF_FUEL_1 && game_state.fuel == 0 && subsecond % (FREQ/2) == 0) { watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); // Blink the 0 fuel to show it cannot be refilled. return; } sprintf(buf, "%2d", game_state.fuel); watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); } static void check_and_reset_hi_score(endless_runner_state_t *state) { // Resets the hi score at the beginning of each month. watch_date_time_t date_time = movement_get_local_date_time(); if ((state -> year_last_hi_score != date_time.unit.year) || (state -> month_last_hi_score != date_time.unit.month)) { // The high score resets itself every new month. state -> hi_score = 0; state -> year_last_hi_score = date_time.unit.year; state -> month_last_hi_score = date_time.unit.month; } } static void display_difficulty(uint16_t difficulty) { static const char *labels[] = { [DIFF_BABY] = " b", [DIFF_EASY] = " E", [DIFF_HARD] = " H", [DIFF_FUEL] = " F", [DIFF_FUEL_1] = "1F", [DIFF_NORM] = " N" }; watch_display_text(WATCH_POSITION_TOP_RIGHT, labels[difficulty]); game_state.fuel_mode = difficulty >= DIFF_FUEL && difficulty <= DIFF_FUEL_1; } static void change_difficulty(endless_runner_state_t *state) { state -> difficulty = (state -> difficulty + 1) % DIFF_COUNT; display_difficulty(state -> difficulty); if (state -> soundOn) { if (state -> difficulty == 0) watch_buzzer_play_note(BUZZER_NOTE_B4, 30); else watch_buzzer_play_note(BUZZER_NOTE_C5, 30); } } static void toggle_sound(endless_runner_state_t *state) { state -> soundOn = !state -> soundOn; if (state -> soundOn){ watch_buzzer_play_note(BUZZER_NOTE_C5, 30); watch_set_indicator(WATCH_INDICATOR_BELL); } else { watch_clear_indicator(WATCH_INDICATOR_BELL); } } static void enable_tap_control(endless_runner_state_t *state) { if (!state->tap_control_on) { movement_enable_tap_detection_if_available(); state->tap_control_on = true; } } static void disable_tap_control(endless_runner_state_t *state) { if (state->tap_control_on) { movement_disable_tap_detection_if_available(); state->tap_control_on = false; } } static void display_title(endless_runner_state_t *state) { uint16_t hi_score = state -> hi_score; uint8_t difficulty = state -> difficulty; bool sound_on = state -> soundOn; game_state.curr_screen = SCREEN_TITLE; memset(&game_state, 0, sizeof(game_state)); game_state.sec_before_moves = 1; // The first obstacles will all be 0s, which is about an extra second of delay. if (sound_on) game_state.sec_before_moves--; // Start chime is about 1 second watch_set_colon(); watch_display_text_with_fallback(WATCH_POSITION_TOP, "RUN", "ER"); watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); if (hi_score > MAX_HI_SCORE) { watch_display_text(WATCH_POSITION_BOTTOM, "HS --"); } else { char buf[10]; sprintf(buf, "HS%4d", hi_score); watch_display_text(WATCH_POSITION_BOTTOM, buf); } display_difficulty(difficulty); if (state -> soundOn) watch_set_indicator(WATCH_INDICATOR_BELL); } static void display_time(watch_date_time_t date_time, bool clock_mode_24h) { static watch_date_time_t previous_date_time; char buf[6 + 1]; // If the hour needs updating or it's the first time displaying the time if ((game_state.curr_screen != SCREEN_TIME) || (date_time.unit.hour != previous_date_time.unit.hour)) { uint8_t hour = date_time.unit.hour; game_state.curr_screen = SCREEN_TIME; if (clock_mode_24h) watch_set_indicator(WATCH_INDICATOR_24H); else { if (hour >= 12) watch_set_indicator(WATCH_INDICATOR_PM); hour %= 12; if (hour == 0) hour = 12; } watch_set_colon(); sprintf( buf, "%2d%02d ", hour, date_time.unit.minute); watch_display_text(WATCH_POSITION_BOTTOM, buf); } // If only the minute need updating else { sprintf( buf, "%02d", date_time.unit.minute); watch_display_text(WATCH_POSITION_MINUTES, buf); } previous_date_time.reg = date_time.reg; } int8_t start_tune[] = { BUZZER_NOTE_C5, 15, BUZZER_NOTE_E5, 15, BUZZER_NOTE_G5, 15, 0 }; static void begin_playing(endless_runner_state_t *state) { uint8_t difficulty = state -> difficulty; game_state.curr_screen = SCREEN_PLAYING; watch_clear_colon(); watch_clear_indicator(WATCH_INDICATOR_BELL); movement_request_tick_frequency((state -> difficulty == DIFF_BABY) ? FREQ_SLOW : FREQ); if (game_state.fuel_mode) { watch_clear_display(); game_state.obst_pattern = get_random_fuel(0); if ((16 * JUMP_FRAMES_FUEL_RECHARGE) < JUMP_FRAMES_FUEL) // 16 frames of zeros at the start of a level game_state.fuel = JUMP_FRAMES_FUEL - (16 * JUMP_FRAMES_FUEL_RECHARGE); // Have it below its max to show it counting up when starting. if (game_state.fuel < JUMP_FRAMES_FUEL_RECHARGE) game_state.fuel = JUMP_FRAMES_FUEL_RECHARGE; } else { watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); watch_display_text(WATCH_POSITION_BOTTOM, " "); game_state.obst_pattern = get_random_legal(0, difficulty); } game_state.jump_state = NOT_JUMPING; display_ball(game_state.jump_state != NOT_JUMPING); display_score( game_state.curr_score); if (state -> soundOn){ watch_buzzer_play_sequence(start_tune, NULL); } } static void display_lose_screen(endless_runner_state_t *state) { game_state.curr_screen = SCREEN_LOSE; game_state.curr_score = 0; watch_clear_display(); watch_display_text(WATCH_POSITION_BOTTOM, " LOSE "); if (state -> soundOn) watch_buzzer_play_note(BUZZER_NOTE_A1, 600); else delay_ms(600); } static void display_obstacle(bool obstacle, int grid_loc, endless_runner_state_t *state) { static bool prev_obst_pos_two = 0; switch (grid_loc) { case 2: game_state.loc_2_on = obstacle; if (obstacle) watch_set_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]); else if (game_state.jump_state != NOT_JUMPING) { watch_clear_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]); if (game_state.fuel_mode && prev_obst_pos_two) add_to_score(state); } prev_obst_pos_two = obstacle; break; case 3: game_state.loc_3_on = obstacle; if (obstacle) watch_set_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]); else if (game_state.jump_state != NOT_JUMPING) watch_clear_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]); break; case 1: if (!game_state.fuel_mode && obstacle) // If an obstacle is here, it means the ball cleared it add_to_score(state); //fall through default: if (obstacle) watch_set_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]); else watch_clear_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]); break; } } static void stop_jumping(endless_runner_state_t *state) { game_state.jump_state = NOT_JUMPING; display_ball(game_state.jump_state != NOT_JUMPING); if (state -> soundOn){ if (game_state.success_jump) watch_buzzer_play_note(BUZZER_NOTE_C5, 60); else watch_buzzer_play_note(BUZZER_NOTE_C3, 60); } game_state.success_jump = false; } static void display_obstacles(endless_runner_state_t *state) { for (int i = 0; i < NUM_GRID; i++) { // Use a bitmask to isolate each bit and shift it to the least significant position uint32_t mask = 1 << ((_num_bits_obst_pattern - 1) - i); bool obstacle = (game_state.obst_pattern & mask) >> ((_num_bits_obst_pattern - 1) - i); display_obstacle(obstacle, i, state); } game_state.obst_pattern = game_state.obst_pattern << 1; game_state.obst_indx++; if (game_state.fuel_mode) { if (game_state.obst_indx >= (_num_bits_obst_pattern / 2)) { game_state.obst_indx = 0; game_state.obst_pattern = get_random_fuel(game_state.obst_pattern); } } else if (game_state.obst_indx >= _num_bits_obst_pattern - NUM_GRID) { game_state.obst_indx = 0; game_state.obst_pattern = get_random_legal(game_state.obst_pattern, state -> difficulty); } } static void update_game(endless_runner_state_t *state, uint8_t subsecond) { uint8_t curr_jump_frame = 0; if (game_state.sec_before_moves != 0) { if (subsecond == 0) --game_state.sec_before_moves; return; } display_obstacles(state); switch (game_state.jump_state) { case NOT_JUMPING: if (game_state.fuel_mode) { for (int i = 0; i < JUMP_FRAMES_FUEL_RECHARGE; i++) { if(game_state.fuel >= JUMP_FRAMES_FUEL || (state -> difficulty == DIFF_FUEL_1 && !game_state.fuel)) break; game_state.fuel++; } } break; case JUMPING_FINAL_FRAME: stop_jumping(state); break; default: if (game_state.fuel_mode) { if (!game_state.fuel) game_state.jump_state = JUMPING_FINAL_FRAME; else game_state.fuel--; if (!HAL_GPIO_BTN_ALARM_read() && !HAL_GPIO_BTN_LIGHT_read()) stop_jumping(state); } else { curr_jump_frame = game_state.jump_state - NOT_JUMPING; if (curr_jump_frame >= JUMP_FRAMES_EASY || (state -> difficulty >= DIFF_NORM && curr_jump_frame >= JUMP_FRAMES)) game_state.jump_state = JUMPING_FINAL_FRAME; else game_state.jump_state++; } break; } if (game_state.jump_state == NOT_JUMPING && (game_state.loc_2_on || game_state.loc_3_on)) { delay_ms(200); // To show the player jumping onto the obstacle before displaying the lose screen. display_lose_screen(state); } else if (game_state.fuel_mode) display_fuel(subsecond, state -> difficulty); } void endless_runner_face_setup(uint8_t watch_face_index, void ** context_ptr) { (void) watch_face_index; if (*context_ptr == NULL) { *context_ptr = malloc(sizeof(endless_runner_state_t)); memset(*context_ptr, 0, sizeof(endless_runner_state_t)); endless_runner_state_t *state = (endless_runner_state_t *)*context_ptr; state->difficulty = DIFF_NORM; state->tap_control_on = false; } } void endless_runner_face_activate(void *context) { (void) context; bool is_custom_lcd = watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM; ball_arr_com = is_custom_lcd ? custom_ball_arr_com : classic_ball_arr_com; ball_arr_seg = is_custom_lcd ? custom_ball_arr_seg : classic_ball_arr_seg; obstacle_arr_com = is_custom_lcd ? custom_obstacle_arr_com : classic_obstacle_arr_com; obstacle_arr_seg = is_custom_lcd ? custom_obstacle_arr_seg : classic_obstacle_arr_seg; movement_enable_tap_detection_if_available(); } bool endless_runner_face_loop(movement_event_t event, void *context) { endless_runner_state_t *state = (endless_runner_state_t *)context; switch (event.event_type) { case EVENT_ACTIVATE: disable_tap_control(state); check_and_reset_hi_score(state); display_title(state); break; case EVENT_TICK: switch (game_state.curr_screen) { case SCREEN_TITLE: case SCREEN_LOSE: break; default: update_game(state, event.subsecond); break; } break; case EVENT_LIGHT_BUTTON_UP: case EVENT_ALARM_BUTTON_UP: if (game_state.curr_screen == SCREEN_TITLE) { enable_tap_control(state); begin_playing(state); } else if (game_state.curr_screen == SCREEN_LOSE) { display_title(state); } break; case EVENT_LIGHT_LONG_PRESS: if (game_state.curr_screen == SCREEN_TITLE) change_difficulty(state); break; case EVENT_SINGLE_TAP: case EVENT_DOUBLE_TAP: if (state->difficulty > DIFF_HARD) break; // Don't do this on fuel modes // Allow starting a new game by tapping. if (game_state.curr_screen == SCREEN_TITLE) { begin_playing(state); break; } else if (game_state.curr_screen == SCREEN_LOSE) { display_title(state); break; } //fall through case EVENT_LIGHT_BUTTON_DOWN: case EVENT_ALARM_BUTTON_DOWN: if (game_state.curr_screen == SCREEN_PLAYING && game_state.jump_state == NOT_JUMPING){ if (game_state.fuel_mode && !game_state.fuel) break; game_state.jump_state = JUMPING_START; display_ball(game_state.jump_state != NOT_JUMPING); } break; case EVENT_ALARM_LONG_PRESS: if (game_state.curr_screen == SCREEN_TITLE) toggle_sound(state); break; case EVENT_TIMEOUT: disable_tap_control(state); if (game_state.curr_screen != SCREEN_TITLE) display_title(state); break; case EVENT_LOW_ENERGY_UPDATE: display_time(movement_get_local_date_time(), movement_clock_mode_24h()); break; default: return movement_default_loop_handler(event); } return true; } void endless_runner_face_resign(void *context) { endless_runner_state_t *state = (endless_runner_state_t *)context; disable_tap_control(state); }