Merge pull request #172 from voloved/ping-face

Ping face
This commit is contained in:
voloved
2025-12-27 19:24:04 -05:00
committed by GitHub
4 changed files with 656 additions and 0 deletions

View File

@ -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.

View File

@ -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.

View File

@ -0,0 +1,583 @@
/*
* 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_COUNT
} PingCurrScreen;
typedef enum {
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_COUNT
} PingDifficulty;
typedef enum {
RESULT_LOSE = -1,
RESULT_NONE = 0,
RESULT_HIT = 1,
RESULT_FIRST_HIT = 2,
} PingResult;
#define FREQ_BABY 2
#define FREQ_EASY 4
#define FREQ_NORM 8
#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
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;
bool moving_from_tap;
} 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;
}
}
return false;
}
static uint8_t get_next_ball_pos(bool ball_hit, uint8_t difficulty) {
int8_t offset_next;
if (ball_hit) {
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;
} 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;
}
if (difficulty == DIFF_HARD) {
if (next_pos == 4) {
next_pos = 8;
} else if (next_pos == 7) {
next_pos = 3;
}
}
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(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;
}
}
game_state.ball_pos = get_next_ball_pos(ball_hit, difficulty);
if (game_state.ball_pos == BALL_OFF_SCREEN) {
return RESULT_LOSE;
}
display_ball();
if (ball_hit) {
return first_hit ? RESULT_FIRST_HIT : 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.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;
}
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);
game_state.moving_from_tap = false;
break;
default:
break;
}
game_state.paddle_hit = 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, "PI ", "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 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_NORM:
case DIFF_HARD:
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.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);
}
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) {
if (game_state.ball_is_moving) {
watch_display_character(' ', ball_pos_to_char_pos(game_state.ball_pos)); // remove the old ball.
}
update_paddle();
int game_result = update_ball(state -> difficulty);
if (game_result == RESULT_LOSE) {
display_lose_screen(state);
} else if (game_result == RESULT_HIT) {
add_to_score(state);
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);
}
}
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);
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:
break;
case SCREEN_PLAYING:
default:
update_game(state);
break;
}
break;
case EVENT_ALARM_BUTTON_UP:
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_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;
}
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;
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_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);
}

View File

@ -0,0 +1,71 @@
/*
* MIT License
*
* Copyright (c) 2025 <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.
*/
#ifndef PING_FACE_H_
#define PING_FACE_H_
#include "movement.h"
/*
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. 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.
*/
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_