From e3239ec47d81493deac2fa3d5296940d49cea717 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 21 Dec 2025 09:03:19 -0500 Subject: [PATCH 01/10] Began adding ping face --- movement_faces.h | 1 + watch-faces.mk | 1 + watch-faces/complication/ping_face.c | 619 +++++++++++++++++++++++++++ watch-faces/complication/ping_face.h | 58 +++ 4 files changed, 679 insertions(+) create mode 100644 watch-faces/complication/ping_face.c create mode 100644 watch-faces/complication/ping_face.h diff --git a/movement_faces.h b/movement_faces.h index eef0e3ac..b75d5d7e 100644 --- a/movement_faces.h +++ b/movement_faces.h @@ -78,4 +78,5 @@ #include "higher_lower_game_face.h" #include "lander_face.h" #include "simon_face.h" +#include "ping_face.h" // New includes go above this line. diff --git a/watch-faces.mk b/watch-faces.mk index 3c424923..5fd366a0 100644 --- a/watch-faces.mk +++ b/watch-faces.mk @@ -53,4 +53,5 @@ SRCS += \ ./watch-faces/complication/higher_lower_game_face.c \ ./watch-faces/complication/lander_face.c \ ./watch-faces/complication/simon_face.c \ + ./watch-faces/complication/ping_face.c \ # New watch faces go above this line. diff --git a/watch-faces/complication/ping_face.c b/watch-faces/complication/ping_face.c new file mode 100644 index 00000000..cb45600b --- /dev/null +++ b/watch-faces/complication/ping_face.c @@ -0,0 +1,619 @@ +/* + * 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 "ping_face.h" +#include "delay.h" +#include "watch_common_display.h" + +typedef enum { + PADDLE_RETRACTED = 0, + PADDLE_EXTENDING, + PADDLE_EXTENDED, + PADDLE_RETRACTING, +} PingPaddleState; + +typedef enum { + SCREEN_TITLE = 0, + SCREEN_SCORE, + SCREEN_PLAYING, + SCREEN_LOSE, + SCREEN_TIME, + SCREEN_COUNT +} PingCurrScreen; + +typedef enum { + DIFF_BABY = 0, // FREQ_SLOW FPS + DIFF_EASY, // FREQ FPS + DIFF_NORM, // FREQ FPS + DIFF_HARD, // FREQ FPS + DIFF_COUNT +} PingDifficulty; + +typedef enum { + RESULT_LOSE = -1, + RESULT_NONE = 0, + RESULT_HIT = 1 +} PingResult; + +#define BALL_POS_MAX 11 +#define BALL_OFF_SCREEN 100 +#define FREQ_BABY 2 +#define FREQ_EASY 4 +#define FREQ_NORM 8 +#define FREQ_HARD 16 +#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 + +typedef struct { + uint8_t ball_pos; // 0 to 11; 0 is the bottom-right and 11 is the top right. + // | 6 | 7 | 8 | 9 | 10 | 11 | + // | 5 | 4 | 3 | 2 | 1 | 0 | + PingPaddleState paddle_pos; + uint8_t ball_char_pos; // Derived from ball_pos + bool ball_is_clockwise; + bool ball_is_moving; + uint16_t curr_score; + PingCurrScreen curr_screen; + bool paddle_hit; + bool paddle_released; + uint8_t curr_freq; +} game_state_t; + +static game_state_t game_state; +static int8_t _ticks_show_title = 0; +static bool _is_custom_lcd; + +static int8_t start_tune[] = { + BUZZER_NOTE_C5, 15, + BUZZER_NOTE_E5, 15, + BUZZER_NOTE_G5, 15, + 0 +}; + +static int8_t lose_tune[] = { + BUZZER_NOTE_D3, 10, + BUZZER_NOTE_C3SHARP_D3FLAT, 10, + BUZZER_NOTE_C3, 10, + 0 +}; + +static uint8_t ball_pos_to_char_pos(uint8_t ball_pos) { + switch (ball_pos) + { + case 5: + case 6: + return 4; + case 4: + case 7: + return 5; + case 3: + case 8: + return 6; + case 2: + case 9: + return 7; + case 1: + case 10: + return 8; + case 0: + case 11: + return 9; + default: + return BALL_OFF_SCREEN; + } +} + +static bool paddle_and_ball_on_same_segment(void) { + if (game_state.paddle_pos == PADDLE_EXTENDED) { + if (game_state.ball_pos == 9 || game_state.ball_pos == 2) { + return true; + } + } + else if (game_state.paddle_pos == PADDLE_EXTENDING || game_state.paddle_pos == PADDLE_RETRACTING) { + if (game_state.ball_pos == 10 || game_state.ball_pos == 1) { + return true; + } + } + else if (game_state.paddle_pos == PADDLE_RETRACTED) { + if (game_state.ball_pos == 11 || game_state.ball_pos == 0) { + return true; + } + } + return false; +} + +static bool paddle_hit_ball(void) { + if (game_state.paddle_pos == PADDLE_EXTENDED) { + if (game_state.ball_pos >= 9 && game_state.ball_is_clockwise) { + return true; + } + if (game_state.ball_pos <= 2 && !game_state.ball_is_clockwise) { + return true; + } + } + else if (game_state.paddle_pos == PADDLE_EXTENDING) { + if (game_state.ball_pos >= 10 && game_state.ball_is_clockwise) { + return true; + } + if (game_state.ball_pos <= 1 && !game_state.ball_is_clockwise) { + return true; + } + } + else if (game_state.paddle_pos == PADDLE_EXTENDING) { + if (game_state.ball_pos >= 11 && game_state.ball_is_clockwise) { + return true; + } + if (game_state.ball_pos <= 0 && !game_state.ball_is_clockwise) { + return true; + } + } + return false; +} + +static uint8_t get_next_ball_pos(bool ball_hit) { + int8_t offset_next; + if (ball_hit) { + game_state.ball_is_clockwise = game_state.ball_pos < 6; + } + if (game_state.ball_is_clockwise) { + offset_next = 1; + } else { + offset_next = -1; + } + int8_t next_pos = game_state.ball_pos + offset_next; + if (next_pos > BALL_POS_MAX || next_pos < 0) { + return BALL_OFF_SCREEN; + } + return next_pos; +} + +static void display_ball(void) { + uint8_t char_pos = ball_pos_to_char_pos(game_state.ball_pos); + uint8_t char_display; + bool overlap = paddle_and_ball_on_same_segment(); + if (game_state.ball_pos > 5) { + if (overlap) { + char_display = 'q'; + } else { + char_display = '#'; + } + } else { + if (_is_custom_lcd || char_pos == 4 || char_pos == 6) { + char_display = 'n'; // No need to check for overlap on these segments + } else { + if (overlap) { + char_display = 'd'; + } else { + char_display = 'o'; + } + } + } + watch_display_character(char_display, char_pos); +} + +static PingResult update_ball(void) { + bool ball_hit = paddle_hit_ball(); + if (!game_state.ball_is_moving) { + if (ball_hit) { + game_state.ball_is_moving = true; + } else { + return RESULT_NONE; + } + } + watch_display_character(' ', ball_pos_to_char_pos(game_state.ball_pos)); // remove the old ball. + game_state.ball_pos = get_next_ball_pos(ball_hit); + if (game_state.ball_pos == BALL_OFF_SCREEN) { + return RESULT_LOSE; + } + display_ball(); + if (ball_hit) { + return RESULT_HIT; + } else { + return RESULT_NONE; + } +} + +static void display_paddle(void) { + switch (game_state.paddle_pos) + { + case PADDLE_EXTENDING: + case PADDLE_RETRACTING: + watch_display_character('-', 9); + watch_display_character('1', 8); + break; + case PADDLE_EXTENDED: + watch_display_character('-', 9); + watch_display_character('-', 8); + watch_display_character('1', 7); + break; + case PADDLE_RETRACTED: + default: + watch_display_character('1', 9); + break; + } +} + +static void update_paddle(void) { + switch (game_state.paddle_pos) + { + case PADDLE_RETRACTED: + if (game_state.paddle_hit) { + game_state.paddle_pos = PADDLE_EXTENDING; + } + break; + case PADDLE_EXTENDING: + if (game_state.paddle_released) { + game_state.paddle_pos = PADDLE_RETRACTED; + } else { + game_state.paddle_pos = PADDLE_EXTENDED; + } + break; + case PADDLE_EXTENDED: + game_state.paddle_pos = PADDLE_RETRACTING; + watch_display_character(' ', 7); + break; + case PADDLE_RETRACTING: + game_state.paddle_pos = PADDLE_RETRACTED; + watch_display_character(' ', 8); + break; + default: + break; + } + game_state.paddle_hit = false; + game_state.paddle_released = false; + display_paddle(); +} + +static inline bool paddle_is_extending(void) { + return game_state.paddle_pos == PADDLE_EXTENDING || game_state.paddle_pos == PADDLE_EXTENDED; +} + +static void display_score(uint8_t score) { + char buf[3]; + score %= (MAX_DISP_SCORE + 1); + sprintf(buf, "%2d", score); + watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); +} + +static void add_to_score(ping_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; + } + display_score(game_state.curr_score); +} + +static void check_and_reset_hi_score(ping_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_NORM] = " N", + [DIFF_HARD] = " H" + }; + watch_display_text(WATCH_POSITION_TOP_RIGHT, labels[difficulty]); +} + +static void change_difficulty(ping_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 display_sound_indicator(bool soundOn) { + if (soundOn){ + watch_set_indicator(WATCH_INDICATOR_BELL); + } else { + watch_clear_indicator(WATCH_INDICATOR_BELL); + } +} + +static void toggle_sound(ping_state_t *state) { + state -> soundOn = !state -> soundOn; + display_sound_indicator(state -> soundOn); + if (state -> soundOn){ + watch_buzzer_play_note(BUZZER_NOTE_C5, 30); + } +} + +static void enable_tap_control(ping_state_t *state) { + if (!state->tap_control_on) { + movement_enable_tap_detection_if_available(); + state->tap_control_on = true; + } +} + +static void disable_tap_control(ping_state_t *state) { + if (state->tap_control_on) { + movement_disable_tap_detection_if_available(); + state->tap_control_on = false; + } +} + +static void display_title(ping_state_t *state) { + movement_request_tick_frequency(1); + game_state.curr_screen = SCREEN_TITLE; + watch_clear_colon(); + watch_display_text_with_fallback(WATCH_POSITION_TOP, "PING", "PI "); + watch_display_text(WATCH_POSITION_BOTTOM, " Ping "); + display_sound_indicator(state -> soundOn); + _ticks_show_title = 1; +} + +static void display_score_screen(ping_state_t *state) { + uint16_t hi_score = state -> hi_score; + uint8_t difficulty = state -> difficulty; + movement_request_tick_frequency(1); + bool sound_on = state -> soundOn; + memset(&game_state, 0, sizeof(game_state)); + game_state.curr_screen = SCREEN_SCORE; + watch_set_colon(); + watch_display_text_with_fallback(WATCH_POSITION_TOP, "PING ", "PI "); + 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); + display_sound_indicator(sound_on); +} + +static void display_time(void) { + static watch_date_time_t previous_date_time; + watch_date_time_t date_time = movement_get_local_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 (!watch_sleep_animation_is_running()) { + watch_set_colon(); + watch_start_indicator_blink_if_possible(WATCH_INDICATOR_COLON, 500); + } + if (movement_clock_is_24h()) watch_set_indicator(WATCH_INDICATOR_24H); + else { + if (hour >= 12) watch_set_indicator(WATCH_INDICATOR_PM); + hour %= 12; + if (hour == 0) hour = 12; + } + sprintf( buf, movement_clock_has_leading_zeroes() ? "%02d%02d " : "%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; +} + +static void begin_playing(ping_state_t *state) { + game_state.curr_screen = SCREEN_PLAYING; + watch_clear_colon(); + display_sound_indicator(state -> soundOn); + switch (state -> difficulty) + { + case DIFF_BABY: + game_state.curr_freq = FREQ_BABY; + break; + case DIFF_EASY: + game_state.curr_freq = FREQ_EASY; + break; + case DIFF_HARD: + game_state.curr_freq = FREQ_HARD; + break; + case DIFF_NORM: + default: + game_state.curr_freq = FREQ_NORM; + break; + } + movement_request_tick_frequency(game_state.curr_freq); + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + watch_display_text(WATCH_POSITION_BOTTOM, " "); + game_state.paddle_pos = PADDLE_RETRACTED; + game_state.ball_pos = 1; + game_state.paddle_hit = false; + game_state.paddle_released = false; + game_state.ball_is_moving = false; + game_state.ball_is_clockwise = false; + game_state.curr_score = 0; + display_paddle(); + display_ball(); + display_score( game_state.curr_score); + if (state -> soundOn){ + watch_buzzer_play_sequence(start_tune, NULL); + } +} + +static void display_lose_screen(ping_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_sequence(lose_tune, NULL); + delay_ms(600); + } +} + +static void update_game(ping_state_t *state) { + update_paddle(); + bool can_earn_point = game_state.ball_is_moving; + int game_result = update_ball(); + if (game_result == RESULT_LOSE) { + display_lose_screen(state); + } else if (game_result == RESULT_HIT && can_earn_point) { + add_to_score(state); + if (game_state.curr_score % 10 == 0) { // Up the speed every 10 points + game_state.curr_freq *= 2; + movement_request_tick_frequency(game_state.curr_freq); + } + } + printf("freq %d\r\n", game_state.curr_freq); +} + +void ping_face_setup(uint8_t watch_face_index, void ** context_ptr) { + (void) watch_face_index; + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(ping_state_t)); + memset(*context_ptr, 0, sizeof(ping_state_t)); + ping_state_t *state = (ping_state_t *)*context_ptr; + state->difficulty = DIFF_NORM; + state->tap_control_on = false; + } +} + +void ping_face_activate(void *context) { + (void) context; + _is_custom_lcd = watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM; + if (watch_sleep_animation_is_running()) { + watch_stop_blink(); + } +} + +bool ping_face_loop(movement_event_t event, void *context) { + ping_state_t *state = (ping_state_t *)context; + switch (event.event_type) { + case EVENT_ACTIVATE: + disable_tap_control(state); + check_and_reset_hi_score(state); + if (game_state.curr_screen != SCREEN_TIME) { + display_title(state); + } + break; + case EVENT_TICK: + switch (game_state.curr_screen) + { + case SCREEN_TITLE: + if (_ticks_show_title > 0) {_ticks_show_title--;} + else { + watch_clear_display(); + display_score_screen(state); + } + case SCREEN_SCORE: + case SCREEN_LOSE: + case SCREEN_TIME: + break; + case SCREEN_PLAYING: + default: + update_game(state); + break; + } + break; + case EVENT_ALARM_BUTTON_UP: + if (game_state.curr_screen == SCREEN_PLAYING){ + game_state.paddle_released = true; + break; + } + // fall-through + case EVENT_LIGHT_BUTTON_UP: + switch (game_state.curr_screen) { + case SCREEN_SCORE: + enable_tap_control(state); + begin_playing(state); + break; + case SCREEN_TITLE: + enable_tap_control(state); + // fall through + case SCREEN_TIME: + case SCREEN_LOSE: + watch_clear_display(); + display_score_screen(state); + default: + break; + } + break; + case EVENT_LIGHT_LONG_PRESS: + if (game_state.curr_screen == SCREEN_SCORE) + change_difficulty(state); + break; + case EVENT_SINGLE_TAP: + case EVENT_DOUBLE_TAP: + // Allow starting a new game by tapping. + if (game_state.curr_screen == SCREEN_SCORE) { + begin_playing(state); + break; + } + else if (game_state.curr_screen == SCREEN_LOSE) { + display_score_screen(state); + break; + } + //fall through + case EVENT_ALARM_BUTTON_DOWN: + if (game_state.curr_screen == SCREEN_PLAYING){ + game_state.paddle_hit = true; + } + break; + case EVENT_ALARM_LONG_PRESS: + if (game_state.curr_screen == SCREEN_TITLE || game_state.curr_screen == SCREEN_SCORE) + toggle_sound(state); + break; + case EVENT_TIMEOUT: + disable_tap_control(state); + if (game_state.curr_screen != SCREEN_SCORE) + display_score_screen(state); + break; + case EVENT_LOW_ENERGY_UPDATE: + if (game_state.curr_screen != SCREEN_TIME) { + movement_request_tick_frequency(1); + watch_display_text_with_fallback(WATCH_POSITION_TOP, "PING ", "PI "); + display_sound_indicator(state -> soundOn); + display_difficulty(state->difficulty); + } + display_time(); + break; + case EVENT_LIGHT_BUTTON_DOWN: + break; + default: + return movement_default_loop_handler(event); + } + return true; +} + +void ping_face_resign(void *context) { + ping_state_t *state = (ping_state_t *)context; + disable_tap_control(state); +} diff --git a/watch-faces/complication/ping_face.h b/watch-faces/complication/ping_face.h new file mode 100644 index 00000000..96b6b636 --- /dev/null +++ b/watch-faces/complication/ping_face.h @@ -0,0 +1,58 @@ +/* + * 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. + */ + +#ifndef PING_FACE_H_ +#define PING_FACE_H_ + +#include "movement.h" + +/* + PING face +*/ + +typedef struct { + uint16_t hi_score : 10; + uint8_t difficulty : 3; + uint8_t month_last_hi_score : 4; + uint8_t year_last_hi_score : 6; + uint8_t soundOn : 1; + uint8_t tap_control_on : 1; + uint8_t unused : 7; +} ping_state_t; + +void ping_face_setup(uint8_t watch_face_index, void ** context_ptr); +void ping_face_activate(void *context); +bool ping_face_loop(movement_event_t event, void *context); +void ping_face_resign(void *context); + +#define ping_face ((const watch_face_t){ \ + ping_face_setup, \ + ping_face_activate, \ + ping_face_loop, \ + ping_face_resign, \ + NULL, \ +}) + +#endif // ping_FACE_H_ + From d785419912e618d75abef77a4f0bec0b995ff9e5 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 21 Dec 2025 10:41:16 -0500 Subject: [PATCH 02/10] bug fixes to ping face --- watch-faces/complication/ping_face.c | 79 ++++++++++++++-------------- 1 file changed, 39 insertions(+), 40 deletions(-) diff --git a/watch-faces/complication/ping_face.c b/watch-faces/complication/ping_face.c index cb45600b..98665fe0 100644 --- a/watch-faces/complication/ping_face.c +++ b/watch-faces/complication/ping_face.c @@ -45,10 +45,11 @@ typedef enum { } PingCurrScreen; typedef enum { - DIFF_BABY = 0, // FREQ_SLOW FPS - DIFF_EASY, // FREQ FPS - DIFF_NORM, // FREQ FPS - DIFF_HARD, // FREQ FPS + DIFF_BABY = 0, // FREQ_BABY FPS + DIFF_EASY, // FREQ_EASY FPS + DIFF_NORM, // FREQ_NORM FPS + DIFF_HARD, // FREQ_NORM FPS, smaller travel-distance for ball + DIFF_FAST, // FREQ_FAST FPS DIFF_COUNT } PingDifficulty; @@ -58,12 +59,13 @@ typedef enum { RESULT_HIT = 1 } PingResult; -#define BALL_POS_MAX 11 -#define BALL_OFF_SCREEN 100 #define FREQ_BABY 2 #define FREQ_EASY 4 #define FREQ_NORM 8 -#define FREQ_HARD 16 +#define FREQ_FAST 16 + +#define BALL_POS_MAX 11 +#define BALL_OFF_SCREEN 100 #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 @@ -162,21 +164,20 @@ static bool paddle_hit_ball(void) { return true; } } - else if (game_state.paddle_pos == PADDLE_EXTENDING) { - if (game_state.ball_pos >= 11 && game_state.ball_is_clockwise) { - return true; - } - if (game_state.ball_pos <= 0 && !game_state.ball_is_clockwise) { - return true; - } - } return false; } -static uint8_t get_next_ball_pos(bool ball_hit) { +static uint8_t get_next_ball_pos(bool ball_hit, uint8_t difficulty) { int8_t offset_next; if (ball_hit) { - game_state.ball_is_clockwise = game_state.ball_pos < 6; + bool ball_on_top = game_state.ball_pos > 5; + game_state.ball_is_clockwise = !ball_on_top; + // ball is at the same frame as the paddle + if (game_state.paddle_pos == PADDLE_EXTENDED) { + return ball_on_top ? 9 : 2; + } else if (game_state.paddle_pos == PADDLE_EXTENDING) { + return ball_on_top ? 10 : 1; + } } if (game_state.ball_is_clockwise) { offset_next = 1; @@ -187,6 +188,13 @@ static uint8_t get_next_ball_pos(bool ball_hit) { if (next_pos > BALL_POS_MAX || next_pos < 0) { return BALL_OFF_SCREEN; } + if (difficulty == DIFF_HARD) { + if (next_pos == 4) { + next_pos = 8; + } else if (next_pos == 7) { + next_pos = 3; + } + } return next_pos; } @@ -214,7 +222,7 @@ static void display_ball(void) { watch_display_character(char_display, char_pos); } -static PingResult update_ball(void) { +static PingResult update_ball(uint8_t difficulty) { bool ball_hit = paddle_hit_ball(); if (!game_state.ball_is_moving) { if (ball_hit) { @@ -223,8 +231,7 @@ static PingResult update_ball(void) { return RESULT_NONE; } } - watch_display_character(' ', ball_pos_to_char_pos(game_state.ball_pos)); // remove the old ball. - game_state.ball_pos = get_next_ball_pos(ball_hit); + game_state.ball_pos = get_next_ball_pos(ball_hit, difficulty); if (game_state.ball_pos == BALL_OFF_SCREEN) { return RESULT_LOSE; } @@ -265,7 +272,7 @@ static void update_paddle(void) { } break; case PADDLE_EXTENDING: - if (game_state.paddle_released) { + if (!HAL_GPIO_BTN_ALARM_read()) { game_state.paddle_pos = PADDLE_RETRACTED; } else { game_state.paddle_pos = PADDLE_EXTENDED; @@ -283,7 +290,6 @@ static void update_paddle(void) { break; } game_state.paddle_hit = false; - game_state.paddle_released = false; display_paddle(); } @@ -325,7 +331,8 @@ static void display_difficulty(uint16_t difficulty) { [DIFF_BABY] = " b", [DIFF_EASY] = " E", [DIFF_NORM] = " N", - [DIFF_HARD] = " H" + [DIFF_HARD] = " H", + [DIFF_FAST] = " F" }; watch_display_text(WATCH_POSITION_TOP_RIGHT, labels[difficulty]); } @@ -442,10 +449,11 @@ static void begin_playing(ping_state_t *state) { case DIFF_EASY: game_state.curr_freq = FREQ_EASY; break; - case DIFF_HARD: - game_state.curr_freq = FREQ_HARD; + case DIFF_FAST: + game_state.curr_freq = FREQ_FAST; break; case DIFF_NORM: + case DIFF_HARD: default: game_state.curr_freq = FREQ_NORM; break; @@ -456,7 +464,6 @@ static void begin_playing(ping_state_t *state) { game_state.paddle_pos = PADDLE_RETRACTED; game_state.ball_pos = 1; game_state.paddle_hit = false; - game_state.paddle_released = false; game_state.ball_is_moving = false; game_state.ball_is_clockwise = false; game_state.curr_score = 0; @@ -480,19 +487,16 @@ static void display_lose_screen(ping_state_t *state) { } static void update_game(ping_state_t *state) { + if (game_state.ball_is_moving) { + watch_display_character(' ', ball_pos_to_char_pos(game_state.ball_pos)); // remove the old ball. + } update_paddle(); - bool can_earn_point = game_state.ball_is_moving; - int game_result = update_ball(); + int game_result = update_ball(state -> difficulty); if (game_result == RESULT_LOSE) { display_lose_screen(state); - } else if (game_result == RESULT_HIT && can_earn_point) { + } else if (game_result == RESULT_HIT) { add_to_score(state); - if (game_state.curr_score % 10 == 0) { // Up the speed every 10 points - game_state.curr_freq *= 2; - movement_request_tick_frequency(game_state.curr_freq); - } } - printf("freq %d\r\n", game_state.curr_freq); } void ping_face_setup(uint8_t watch_face_index, void ** context_ptr) { @@ -501,7 +505,7 @@ void ping_face_setup(uint8_t watch_face_index, void ** context_ptr) { *context_ptr = malloc(sizeof(ping_state_t)); memset(*context_ptr, 0, sizeof(ping_state_t)); ping_state_t *state = (ping_state_t *)*context_ptr; - state->difficulty = DIFF_NORM; + state->difficulty = DIFF_BABY; state->tap_control_on = false; } } @@ -544,11 +548,6 @@ bool ping_face_loop(movement_event_t event, void *context) { } break; case EVENT_ALARM_BUTTON_UP: - if (game_state.curr_screen == SCREEN_PLAYING){ - game_state.paddle_released = true; - break; - } - // fall-through case EVENT_LIGHT_BUTTON_UP: switch (game_state.curr_screen) { case SCREEN_SCORE: From fe085f81fd08a51b591307e66e3b7dbea58d7f34 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 21 Dec 2025 10:50:11 -0500 Subject: [PATCH 03/10] Adding description --- watch-faces/complication/ping_face.h | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/watch-faces/complication/ping_face.h b/watch-faces/complication/ping_face.h index 96b6b636..bdd9e395 100644 --- a/watch-faces/complication/ping_face.h +++ b/watch-faces/complication/ping_face.h @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2024 + * Copyright (c) 2025 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -29,6 +29,12 @@ /* PING face + I saw the face made on the Ollee watch and thought it'd be fun to have on my Sensorwatch. + https://www.instagram.com/reel/DNlTb-ERE1F/ + On the title screen, you can select a difficulty by long-pressing LIGHT or toggle sound by long-pressing ALARM. + ALARM are used to paddle. Holding the ALARM button longer makes the paddle travel further. + If the accelerometer is installed, you can tap the screen to move the paddle. + High-score is displayed on the top-right on the title screen. During a game, the current score is displayed. */ typedef struct { From 6627ff1fbec75f676d7240d404d4fab2a166e8fe Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 21 Dec 2025 10:50:59 -0500 Subject: [PATCH 04/10] accelerometer moving paddle fully out --- watch-faces/complication/ping_face.c | 11 +++++++++-- watch-faces/complication/ping_face.h | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/watch-faces/complication/ping_face.c b/watch-faces/complication/ping_face.c index 98665fe0..e35762e9 100644 --- a/watch-faces/complication/ping_face.c +++ b/watch-faces/complication/ping_face.c @@ -82,6 +82,7 @@ typedef struct { bool paddle_hit; bool paddle_released; uint8_t curr_freq; + bool moving_from_tap; } game_state_t; static game_state_t game_state; @@ -272,7 +273,7 @@ static void update_paddle(void) { } break; case PADDLE_EXTENDING: - if (!HAL_GPIO_BTN_ALARM_read()) { + if (!game_state.moving_from_tap && !HAL_GPIO_BTN_ALARM_read()) { game_state.paddle_pos = PADDLE_RETRACTED; } else { game_state.paddle_pos = PADDLE_EXTENDED; @@ -285,6 +286,7 @@ static void update_paddle(void) { case PADDLE_RETRACTING: game_state.paddle_pos = PADDLE_RETRACTED; watch_display_character(' ', 8); + game_state.moving_from_tap = false; break; default: break; @@ -580,9 +582,14 @@ bool ping_face_loop(movement_event_t event, void *context) { display_score_screen(state); break; } - //fall through + else if (game_state.curr_screen == SCREEN_PLAYING){ + game_state.moving_from_tap = true; + game_state.paddle_hit = true; + } + break; case EVENT_ALARM_BUTTON_DOWN: if (game_state.curr_screen == SCREEN_PLAYING){ + game_state.moving_from_tap = false; game_state.paddle_hit = true; } break; diff --git a/watch-faces/complication/ping_face.h b/watch-faces/complication/ping_face.h index bdd9e395..5aa5a62f 100644 --- a/watch-faces/complication/ping_face.h +++ b/watch-faces/complication/ping_face.h @@ -33,7 +33,7 @@ https://www.instagram.com/reel/DNlTb-ERE1F/ On the title screen, you can select a difficulty by long-pressing LIGHT or toggle sound by long-pressing ALARM. ALARM are used to paddle. Holding the ALARM button longer makes the paddle travel further. - If the accelerometer is installed, you can tap the screen to move the paddle. + If the accelerometer is installed, you can tap the screen to move the paddle. Paddle will travel its full distance when tapping is used. High-score is displayed on the top-right on the title screen. During a game, the current score is displayed. */ From 66257c523274637f7593329f2a8db1421c9e2702 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 21 Dec 2025 11:04:31 -0500 Subject: [PATCH 05/10] Removed time display on ping face --- watch-faces/complication/ping_face.c | 47 ++-------------------------- 1 file changed, 2 insertions(+), 45 deletions(-) diff --git a/watch-faces/complication/ping_face.c b/watch-faces/complication/ping_face.c index e35762e9..96d3f4f8 100644 --- a/watch-faces/complication/ping_face.c +++ b/watch-faces/complication/ping_face.c @@ -40,7 +40,6 @@ typedef enum { SCREEN_SCORE, SCREEN_PLAYING, SCREEN_LOSE, - SCREEN_TIME, SCREEN_COUNT } PingCurrScreen; @@ -409,36 +408,6 @@ static void display_score_screen(ping_state_t *state) { display_sound_indicator(sound_on); } -static void display_time(void) { - static watch_date_time_t previous_date_time; - watch_date_time_t date_time = movement_get_local_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 (!watch_sleep_animation_is_running()) { - watch_set_colon(); - watch_start_indicator_blink_if_possible(WATCH_INDICATOR_COLON, 500); - } - if (movement_clock_is_24h()) watch_set_indicator(WATCH_INDICATOR_24H); - else { - if (hour >= 12) watch_set_indicator(WATCH_INDICATOR_PM); - hour %= 12; - if (hour == 0) hour = 12; - } - sprintf( buf, movement_clock_has_leading_zeroes() ? "%02d%02d " : "%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; -} - static void begin_playing(ping_state_t *state) { game_state.curr_screen = SCREEN_PLAYING; watch_clear_colon(); @@ -526,9 +495,7 @@ bool ping_face_loop(movement_event_t event, void *context) { case EVENT_ACTIVATE: disable_tap_control(state); check_and_reset_hi_score(state); - if (game_state.curr_screen != SCREEN_TIME) { - display_title(state); - } + display_title(state); break; case EVENT_TICK: switch (game_state.curr_screen) @@ -541,7 +508,6 @@ bool ping_face_loop(movement_event_t event, void *context) { } case SCREEN_SCORE: case SCREEN_LOSE: - case SCREEN_TIME: break; case SCREEN_PLAYING: default: @@ -559,7 +525,6 @@ bool ping_face_loop(movement_event_t event, void *context) { case SCREEN_TITLE: enable_tap_control(state); // fall through - case SCREEN_TIME: case SCREEN_LOSE: watch_clear_display(); display_score_screen(state); @@ -599,17 +564,9 @@ bool ping_face_loop(movement_event_t event, void *context) { break; case EVENT_TIMEOUT: disable_tap_control(state); - if (game_state.curr_screen != SCREEN_SCORE) + if (game_state.curr_screen != SCREEN_SCORE) { display_score_screen(state); - break; - case EVENT_LOW_ENERGY_UPDATE: - if (game_state.curr_screen != SCREEN_TIME) { - movement_request_tick_frequency(1); - watch_display_text_with_fallback(WATCH_POSITION_TOP, "PING ", "PI "); - display_sound_indicator(state -> soundOn); - display_difficulty(state->difficulty); } - display_time(); break; case EVENT_LIGHT_BUTTON_DOWN: break; From 2935f0b6042606f3cb4c2c2a042dad618df591db Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 21 Dec 2025 11:23:29 -0500 Subject: [PATCH 06/10] Changed default difficulty --- watch-faces/complication/ping_face.c | 2 +- watch-faces/complication/ping_face.h | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/watch-faces/complication/ping_face.c b/watch-faces/complication/ping_face.c index 96d3f4f8..ba650b55 100644 --- a/watch-faces/complication/ping_face.c +++ b/watch-faces/complication/ping_face.c @@ -476,7 +476,7 @@ void ping_face_setup(uint8_t watch_face_index, void ** context_ptr) { *context_ptr = malloc(sizeof(ping_state_t)); memset(*context_ptr, 0, sizeof(ping_state_t)); ping_state_t *state = (ping_state_t *)*context_ptr; - state->difficulty = DIFF_BABY; + state->difficulty = DIFF_NORM; state->tap_control_on = false; } } diff --git a/watch-faces/complication/ping_face.h b/watch-faces/complication/ping_face.h index 5aa5a62f..a816b5d3 100644 --- a/watch-faces/complication/ping_face.h +++ b/watch-faces/complication/ping_face.h @@ -35,6 +35,14 @@ ALARM are used to paddle. Holding the ALARM button longer makes the paddle travel further. If the accelerometer is installed, you can tap the screen to move the paddle. Paddle will travel its full distance when tapping is used. High-score is displayed on the top-right on the title screen. During a game, the current score is displayed. + + Difficulties: + Baby: 2 FPS + Easy: 4 FPS + Normal: 8 FPS + Hard: 8 FPS and the ball travels half the half the board. + Fast: 16 FPS + */ typedef struct { From 4ef85c6f2c54761d692fc377c217a2f439ed477d Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 21 Dec 2025 11:28:40 -0500 Subject: [PATCH 07/10] Watch's refresh can't handle Fast difficulty --- watch-faces/complication/ping_face.c | 8 +------- watch-faces/complication/ping_face.h | 1 - 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/watch-faces/complication/ping_face.c b/watch-faces/complication/ping_face.c index ba650b55..414ab8e8 100644 --- a/watch-faces/complication/ping_face.c +++ b/watch-faces/complication/ping_face.c @@ -48,7 +48,6 @@ typedef enum { DIFF_EASY, // FREQ_EASY FPS DIFF_NORM, // FREQ_NORM FPS DIFF_HARD, // FREQ_NORM FPS, smaller travel-distance for ball - DIFF_FAST, // FREQ_FAST FPS DIFF_COUNT } PingDifficulty; @@ -61,7 +60,6 @@ typedef enum { #define FREQ_BABY 2 #define FREQ_EASY 4 #define FREQ_NORM 8 -#define FREQ_FAST 16 #define BALL_POS_MAX 11 #define BALL_OFF_SCREEN 100 @@ -332,8 +330,7 @@ static void display_difficulty(uint16_t difficulty) { [DIFF_BABY] = " b", [DIFF_EASY] = " E", [DIFF_NORM] = " N", - [DIFF_HARD] = " H", - [DIFF_FAST] = " F" + [DIFF_HARD] = " H" }; watch_display_text(WATCH_POSITION_TOP_RIGHT, labels[difficulty]); } @@ -420,9 +417,6 @@ static void begin_playing(ping_state_t *state) { case DIFF_EASY: game_state.curr_freq = FREQ_EASY; break; - case DIFF_FAST: - game_state.curr_freq = FREQ_FAST; - break; case DIFF_NORM: case DIFF_HARD: default: diff --git a/watch-faces/complication/ping_face.h b/watch-faces/complication/ping_face.h index a816b5d3..2207bec3 100644 --- a/watch-faces/complication/ping_face.h +++ b/watch-faces/complication/ping_face.h @@ -41,7 +41,6 @@ Easy: 4 FPS Normal: 8 FPS Hard: 8 FPS and the ball travels half the half the board. - Fast: 16 FPS */ From 2c3259b2e372c7a360752aaa71e7870d80527f05 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 21 Dec 2025 13:22:43 -0500 Subject: [PATCH 08/10] sound effect on hit --- watch-faces/complication/ping_face.c | 1 + 1 file changed, 1 insertion(+) diff --git a/watch-faces/complication/ping_face.c b/watch-faces/complication/ping_face.c index 414ab8e8..4f80c8d7 100644 --- a/watch-faces/complication/ping_face.c +++ b/watch-faces/complication/ping_face.c @@ -461,6 +461,7 @@ static void update_game(ping_state_t *state) { display_lose_screen(state); } else if (game_result == RESULT_HIT) { add_to_score(state); + watch_buzzer_play_note(BUZZER_NOTE_C5, 60); } } From 90e99f89575aaacb419178367cdaf947c6ccd836 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Sun, 21 Dec 2025 13:39:16 -0500 Subject: [PATCH 09/10] First hit not counted; bugfixes on sound --- watch-faces/complication/ping_face.c | 30 ++++++++++++++++------------ 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/watch-faces/complication/ping_face.c b/watch-faces/complication/ping_face.c index 4f80c8d7..56165215 100644 --- a/watch-faces/complication/ping_face.c +++ b/watch-faces/complication/ping_face.c @@ -54,7 +54,8 @@ typedef enum { typedef enum { RESULT_LOSE = -1, RESULT_NONE = 0, - RESULT_HIT = 1 + RESULT_HIT = 1, + RESULT_FIRST_HIT = 2, } PingResult; #define FREQ_BABY 2 @@ -207,7 +208,7 @@ static void display_ball(void) { char_display = '#'; } } else { - if (_is_custom_lcd || char_pos == 4 || char_pos == 6) { + if (!_is_custom_lcd && (char_pos == 4 || char_pos == 6)) { char_display = 'n'; // No need to check for overlap on these segments } else { if (overlap) { @@ -222,9 +223,11 @@ static void display_ball(void) { static PingResult update_ball(uint8_t difficulty) { bool ball_hit = paddle_hit_ball(); + bool first_hit = false; if (!game_state.ball_is_moving) { if (ball_hit) { game_state.ball_is_moving = true; + first_hit = true; } else { return RESULT_NONE; } @@ -235,7 +238,7 @@ static PingResult update_ball(uint8_t difficulty) { } display_ball(); if (ball_hit) { - return RESULT_HIT; + return first_hit ? RESULT_FIRST_HIT : RESULT_HIT; } else { return RESULT_NONE; } @@ -345,7 +348,7 @@ static void change_difficulty(ping_state_t *state) { } static void display_sound_indicator(bool soundOn) { - if (soundOn){ + if (soundOn) { watch_set_indicator(WATCH_INDICATOR_BELL); } else { watch_clear_indicator(WATCH_INDICATOR_BELL); @@ -355,7 +358,7 @@ static void display_sound_indicator(bool soundOn) { static void toggle_sound(ping_state_t *state) { state -> soundOn = !state -> soundOn; display_sound_indicator(state -> soundOn); - if (state -> soundOn){ + if (state -> soundOn) { watch_buzzer_play_note(BUZZER_NOTE_C5, 30); } } @@ -378,7 +381,7 @@ static void display_title(ping_state_t *state) { movement_request_tick_frequency(1); game_state.curr_screen = SCREEN_TITLE; watch_clear_colon(); - watch_display_text_with_fallback(WATCH_POSITION_TOP, "PING", "PI "); + watch_display_text_with_fallback(WATCH_POSITION_TOP, "Ping", "PI "); watch_display_text(WATCH_POSITION_BOTTOM, " Ping "); display_sound_indicator(state -> soundOn); _ticks_show_title = 1; @@ -392,7 +395,7 @@ static void display_score_screen(ping_state_t *state) { memset(&game_state, 0, sizeof(game_state)); game_state.curr_screen = SCREEN_SCORE; watch_set_colon(); - watch_display_text_with_fallback(WATCH_POSITION_TOP, "PING ", "PI "); + watch_display_text_with_fallback(WATCH_POSITION_TOP, "PI ", "PI "); if (hi_score > MAX_HI_SCORE) { watch_display_text(WATCH_POSITION_BOTTOM, "HS --"); } @@ -435,9 +438,6 @@ static void begin_playing(ping_state_t *state) { display_paddle(); display_ball(); display_score( game_state.curr_score); - if (state -> soundOn){ - watch_buzzer_play_sequence(start_tune, NULL); - } } static void display_lose_screen(ping_state_t *state) { @@ -461,7 +461,11 @@ static void update_game(ping_state_t *state) { display_lose_screen(state); } else if (game_result == RESULT_HIT) { add_to_score(state); - watch_buzzer_play_note(BUZZER_NOTE_C5, 60); + if (state -> soundOn) { + watch_buzzer_play_note(BUZZER_NOTE_C5, 60); + } + } else if (game_result == RESULT_FIRST_HIT && state -> soundOn) { + watch_buzzer_play_sequence(start_tune, NULL); } } @@ -542,13 +546,13 @@ bool ping_face_loop(movement_event_t event, void *context) { display_score_screen(state); break; } - else if (game_state.curr_screen == SCREEN_PLAYING){ + else if (game_state.curr_screen == SCREEN_PLAYING) { game_state.moving_from_tap = true; game_state.paddle_hit = true; } break; case EVENT_ALARM_BUTTON_DOWN: - if (game_state.curr_screen == SCREEN_PLAYING){ + if (game_state.curr_screen == SCREEN_PLAYING) { game_state.moving_from_tap = false; game_state.paddle_hit = true; } From cac1f50e8d0f3e82dbf90a556ec5aa7cfb2df049 Mon Sep 17 00:00:00 2001 From: David Volovskiy Date: Thu, 25 Dec 2025 09:25:47 -0500 Subject: [PATCH 10/10] Fixed clearing paddle on fast press --- watch-faces/complication/ping_face.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/watch-faces/complication/ping_face.c b/watch-faces/complication/ping_face.c index 56165215..a0591ba3 100644 --- a/watch-faces/complication/ping_face.c +++ b/watch-faces/complication/ping_face.c @@ -275,6 +275,8 @@ static void update_paddle(void) { case PADDLE_EXTENDING: if (!game_state.moving_from_tap && !HAL_GPIO_BTN_ALARM_read()) { game_state.paddle_pos = PADDLE_RETRACTED; + watch_display_character(' ', 8); + game_state.moving_from_tap = false; } else { game_state.paddle_pos = PADDLE_EXTENDED; }