--- /dev/null
+/*
+ * MIT License
+ *
+ * Copyright (c) 2024 <David Volovskiy>
+ *
+ * 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 <stdlib.h>
+#include <string.h>
+#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);
+}