diff --git a/movement.c b/movement.c index 0046a489..0a41b306 100644 --- a/movement.c +++ b/movement.c @@ -521,7 +521,7 @@ bool movement_enable_tap_detection_if_available(void) { // ramp data rate up to 400 Hz and high performance mode lis2dw_set_low_noise_mode(true); lis2dw_set_data_rate(LIS2DW_DATA_RATE_HP_400_HZ); - lis2dw_set_mode(LIS2DW_MODE_HIGH_PERFORMANCE); + lis2dw_set_mode(LIS2DW_MODE_LOW_POWER); // Settling time (1 sample duration, i.e. 1/400Hz) delay_ms(3); @@ -610,7 +610,7 @@ void app_init(void) { // check if we are plugged into USB power. HAL_GPIO_VBUS_DET_in(); HAL_GPIO_VBUS_DET_pulldown(); - delay_ms(10); + delay_ms(100); if (HAL_GPIO_VBUS_DET_read()){ /// if so, enable USB functionality. _watch_enable_usb(); diff --git a/movement_faces.h b/movement_faces.h index 24509a66..2113dc6a 100644 --- a/movement_faces.h +++ b/movement_faces.h @@ -73,5 +73,7 @@ #include "wareki_face.h" #include "deadline_face.h" #include "wordle_face.h" +#include "blackjack_face.h" +#include "endless_runner_face.h" #include "higher_lower_game_face.h" // New includes go above this line. diff --git a/watch-faces.mk b/watch-faces.mk index 11dab1d8..a2537dda 100644 --- a/watch-faces.mk +++ b/watch-faces.mk @@ -48,5 +48,7 @@ SRCS += \ ./watch-faces/sensor/lis2dw_monitor_face.c \ ./watch-faces/complication/wareki_face.c \ ./watch-faces/complication/deadline_face.c \ + ./watch-faces/complication/blackjack_face.c \ + ./watch-faces/complication/endless_runner_face.c \ ./watch-faces/complication/higher_lower_game_face.c \ # New watch faces go above this line. diff --git a/watch-faces/complication/blackjack_face.c b/watch-faces/complication/blackjack_face.c new file mode 100755 index 00000000..375e0435 --- /dev/null +++ b/watch-faces/complication/blackjack_face.c @@ -0,0 +1,467 @@ +/* + * 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. + */ + +// Emulator only: need time() to seed the random number generator. +#if __EMSCRIPTEN__ +#include +#endif + +#include +#include +#include "blackjack_face.h" +#include "watch_common_display.h" + +#define ACE 14 +#define KING 13 +#define QUEEN 12 +#define JACK 11 + +#define MIN_CARD_VALUE 2 +#define MAX_CARD_VALUE ACE +#define CARD_RANK_COUNT (MAX_CARD_VALUE - MIN_CARD_VALUE + 1) +#define CARD_SUIT_COUNT 4 +#define DECK_SIZE (CARD_SUIT_COUNT * CARD_RANK_COUNT) + +#define BLACKJACK_MAX_HAND_SIZE 11 // 4*1 + 4*2 + 3*3 = 21; 11 cards total +#define MAX_PLAYER_CARDS_DISPLAY 4 +#define BOARD_DISPLAY_START 4 + +typedef struct { + uint8_t score; + uint8_t idx_hand; + int8_t high_aces_in_hand; + uint8_t hand[BLACKJACK_MAX_HAND_SIZE]; +} hand_info_t; + +typedef enum { + BJ_TITLE_SCREEN, + BJ_PLAYING, + BJ_DEALER_PLAYING, + BJ_BUST, + BJ_RESULT, + BJ_WIN_RATIO, +} game_state_t; + +typedef enum { + A, B, C, D, E, F, G +} segment_t; + +static bool tap_turned_on = false; +static game_state_t game_state; +static uint8_t deck[DECK_SIZE] = {0}; +static uint8_t current_card = 0; +static blackjack_face_state_t *g_state = NULL; +hand_info_t player; +hand_info_t dealer; + +static uint8_t generate_random_number(uint8_t num_values) { + // Emulator: use rand. Hardware: use arc4random. +#if __EMSCRIPTEN__ + return rand() % num_values; +#else + return arc4random_uniform(num_values); +#endif +} + +static void stack_deck(void) { + for (size_t i = 0; i < CARD_RANK_COUNT; i++) { + for (size_t j = 0; j < CARD_SUIT_COUNT; j++) + deck[(i * CARD_SUIT_COUNT) + j] = MIN_CARD_VALUE + i; + } +} + +static void shuffle_deck(void) { + // Randomize shuffle with Fisher Yates + size_t i; + size_t j; + uint8_t tmp; + + for (i = DECK_SIZE - 1; i > 0; i--) { + j = generate_random_number(0xFF) % (i + 1); + tmp = deck[j]; + deck[j] = deck[i]; + deck[i] = tmp; + } +} + +static void reset_deck(void) { + current_card = 0; + shuffle_deck(); +} + +static uint8_t get_next_card(void) { + if (current_card >= DECK_SIZE) + reset_deck(); + return deck[current_card++]; +} + +static uint8_t get_card_value(uint8_t card) { + switch (card) + { + case ACE: + return 11; + case KING: + case QUEEN: + case JACK: + return 10; + default: + return card; + } +} + +static void modify_score_from_aces(hand_info_t *hand_info) { + while (hand_info->score > 21 && hand_info->high_aces_in_hand > 0) { + hand_info->score -= 10; + hand_info->high_aces_in_hand--; + } +} + +static void reset_hands(void) { + memset(&player, 0, sizeof(player)); + memset(&dealer, 0, sizeof(dealer)); + reset_deck(); +} + +static void give_card(hand_info_t *hand_info) { + uint8_t card = get_next_card(); + if (card == ACE) hand_info->high_aces_in_hand++; + hand_info->hand[hand_info->idx_hand++] = card; + uint8_t card_value = get_card_value(card); + hand_info->score += card_value; + modify_score_from_aces(hand_info); +} + +static void set_segment_at_position(segment_t segment, uint8_t position) { + digit_mapping_t segmap; + if (watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM) { + segmap = Custom_LCD_Display_Mapping[position]; + } else { + segmap = Classic_LCD_Display_Mapping[position]; + } + const uint8_t com_pin = segmap.segment[segment].address.com; + const uint8_t seg = segmap.segment[segment].address.seg; + watch_set_pixel(com_pin, seg); +} + +static void display_card_at_position(uint8_t card, uint8_t display_position) { + switch (card) { + case KING: + watch_display_character(' ', display_position); + set_segment_at_position(A, display_position); + set_segment_at_position(D, display_position); + set_segment_at_position(G, display_position); + break; + case QUEEN: + watch_display_character(' ', display_position); + set_segment_at_position(A, display_position); + set_segment_at_position(D, display_position); + break; + case JACK: + watch_display_character('-', display_position); + break; + case ACE: + watch_display_character(watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM ? 'A' : 'a', display_position); + break; + case 10: + watch_display_character('0', display_position); + break; + default: { + const char display_char = card + '0'; + watch_display_character(display_char, display_position); + break; + } + } +} + +static void display_player_hand(void) { + uint8_t card; + if (player.idx_hand <= MAX_PLAYER_CARDS_DISPLAY) { + card = player.hand[player.idx_hand - 1]; + display_card_at_position(card, BOARD_DISPLAY_START + player.idx_hand - 1); + } else { + for (uint8_t i=0; igames_played++; + if (g_state->games_played == 0) { + // Overflow + g_state->games_played = 1; + g_state->games_won = add_to_wins ? 1 : 0; + return; + } + if (add_to_wins) { + g_state->games_won++; + if (g_state->games_won == 0) { + // Overflow + g_state->games_played = 1; + g_state->games_won = 1; + } + } +} + +static void display_win(void) { + game_state = BJ_RESULT; + add_to_game_scores(true); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "WlN ", " WIN"); + display_score(player.score, WATCH_POSITION_SECONDS); + display_score(dealer.score, WATCH_POSITION_TOP_RIGHT); +} + +static void display_lose(void) { + game_state = BJ_RESULT; + add_to_game_scores(false); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "LOSE", "lOSE"); + display_score(player.score, WATCH_POSITION_SECONDS); + display_score(dealer.score, WATCH_POSITION_TOP_RIGHT); +} + +static void display_tie(void) { + game_state = BJ_RESULT; + // Don't record ties to the win ratio + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "TlE ", " TIE"); + display_score(player.score, WATCH_POSITION_SECONDS); +} + +static void display_bust(void) { + game_state = BJ_RESULT; + add_to_game_scores(false); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "8UST", "BUST"); +} + +static void display_title(void) { + game_state = BJ_TITLE_SCREEN; + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + watch_display_text_with_fallback(WATCH_POSITION_TOP, "BLACK ", "21"); + watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, " JACK ", "BLaKJK"); +} + +static void display_win_ratio(blackjack_face_state_t *state) { + char buf[7]; + game_state = BJ_WIN_RATIO; + uint8_t win_ratio = 0; + if (state->games_played > 0) { // Avoid dividing by zero + win_ratio = (uint8_t)(100 * state->games_won) / state->games_played; + } + watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); + watch_display_text_with_fallback(WATCH_POSITION_TOP, "WINS ", "WR"); + sprintf(buf, "%3dPct", win_ratio); + watch_display_text(WATCH_POSITION_BOTTOM, buf); +} + +static void begin_playing(bool tap_control_on) { + watch_clear_display(); + if (tap_control_on) { + watch_set_indicator(WATCH_INDICATOR_SIGNAL); + } + game_state = BJ_PLAYING; + reset_hands(); + // Give player their first 2 cards + give_card(&player); + display_player_hand(); + give_card(&player); + display_player_hand(); + display_score(player.score, WATCH_POSITION_SECONDS); + give_card(&dealer); + display_dealer_hand(); + display_score(dealer.score, WATCH_POSITION_TOP_RIGHT); +} + +static void perform_stand(void) { + game_state = BJ_DEALER_PLAYING; + watch_display_text(WATCH_POSITION_BOTTOM, "Stnd"); + display_score(player.score, WATCH_POSITION_SECONDS); +} + +static void perform_hit(void) { + if (player.score == 21) { + perform_stand(); + return; // Assume hitting on 21 is a mistake and stand + } + give_card(&player); + if (player.score > 21) { + game_state = BJ_BUST; + } + display_player_hand(); + display_score(player.score, WATCH_POSITION_SECONDS); +} + +static void dealer_performs_hits(void) { + give_card(&dealer); + display_dealer_hand(); + if (dealer.score > 21) { + display_win(); + } else if (dealer.score > player.score) { + display_lose(); + } else { + display_dealer_hand(); + display_score(dealer.score, WATCH_POSITION_TOP_RIGHT); + } +} + +static void see_if_dealer_hits(void) { + if (dealer.score > 16) { + if (dealer.score > player.score) { + display_lose(); + } else if (dealer.score < player.score) { + display_win(); + } else { + display_tie(); + } + } else { + dealer_performs_hits(); + } +} + +static void handle_button_presses(bool tap_control_on, bool hit) { + switch (game_state) + { + case BJ_TITLE_SCREEN: + if (!tap_turned_on && tap_control_on) { + if (movement_enable_tap_detection_if_available()) tap_turned_on = true; + } + begin_playing(tap_control_on); + break; + case BJ_PLAYING: + if (hit) { + perform_hit(); + } else { + perform_stand(); + } + break; + case BJ_DEALER_PLAYING: + see_if_dealer_hits(); + break; + case BJ_BUST: + display_bust(); + break; + case BJ_RESULT: + case BJ_WIN_RATIO: + display_title(); + break; + } +} + +static void toggle_tap_control(blackjack_face_state_t *state) { + if (state->tap_control_on) { + movement_disable_tap_detection_if_available(); + state->tap_control_on = false; + watch_clear_indicator(WATCH_INDICATOR_SIGNAL); + } else { + bool tap_could_enable = movement_enable_tap_detection_if_available(); + if (tap_could_enable) { + state->tap_control_on = true; + watch_set_indicator(WATCH_INDICATOR_SIGNAL); + } + } +} + +void blackjack_face_setup(uint8_t watch_face_index, void **context_ptr) { + (void) watch_face_index; + + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(blackjack_face_state_t)); + memset(*context_ptr, 0, sizeof(blackjack_face_state_t)); + blackjack_face_state_t *state = (blackjack_face_state_t *)*context_ptr; + state->tap_control_on = false; + } + g_state = (blackjack_face_state_t *)*context_ptr; +} + +void blackjack_face_activate(void *context) { + blackjack_face_state_t *state = (blackjack_face_state_t *) context; + (void) state; + display_title(); + stack_deck(); +} + +bool blackjack_face_loop(movement_event_t event, void *context) { + blackjack_face_state_t *state = (blackjack_face_state_t *) context; + switch (event.event_type) { + case EVENT_ACTIVATE: + if (state->tap_control_on) watch_set_indicator(WATCH_INDICATOR_SIGNAL); + break; + case EVENT_TICK: + if (game_state == BJ_DEALER_PLAYING) { + see_if_dealer_hits(); + } + else if (game_state == BJ_BUST) { + display_bust(); + } + break; + case EVENT_LIGHT_BUTTON_UP: + case EVENT_DOUBLE_TAP: + handle_button_presses(state->tap_control_on, false); + case EVENT_LIGHT_BUTTON_DOWN: + break; + case EVENT_ALARM_BUTTON_UP: + case EVENT_SINGLE_TAP: + handle_button_presses(state->tap_control_on, true); + break; + case EVENT_LIGHT_LONG_PRESS: + if (game_state == BJ_TITLE_SCREEN) { + display_win_ratio(state); + } else { + movement_illuminate_led(); + } + break; + case EVENT_ALARM_LONG_PRESS: + if (game_state == BJ_TITLE_SCREEN) { + toggle_tap_control(state); + } + break; + case EVENT_TIMEOUT: + case EVENT_LOW_ENERGY_UPDATE: + if (tap_turned_on) { + movement_disable_tap_detection_if_available(); + } + break; + default: + return movement_default_loop_handler(event); + } + return true; +} + +void blackjack_face_resign(void *context) { + (void) context; + if (tap_turned_on) { + tap_turned_on = false; + movement_disable_tap_detection_if_available(); + } +} diff --git a/watch-faces/complication/blackjack_face.h b/watch-faces/complication/blackjack_face.h new file mode 100755 index 00000000..c93143b9 --- /dev/null +++ b/watch-faces/complication/blackjack_face.h @@ -0,0 +1,91 @@ +/* + * MIT License + * + * Copyright (c) 2023 Chris Ellis + * + * 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 BLACKJACK_FACE_H_ +#define BLACKJACK_FACE_H_ + +#include "movement.h" + +/* + * Blackjack face + * ====================== + * + * Simple blackjack game. + * + * Aces are 11 unless you'd but, and if so, they become 1. + * King, Queen, and jack are all 10 points. + * Dealer deals to themselves until they get at least 17. + * The game plays with one shuffled deck that gets reshuffled with every game. + * + * Press either ALARM or LIGHT to begin playing. + * Your score is in the Seconds position. + * The dealer's score is in the Top-Right position. + * The dealer's last-shown card is in the Top-Left position. + * Your cards are in the Bottom row. From left to right, they are oldest to newest. Up to four cards will be dislayed. + * + * To hit, press the ALARM button. + * To stand, press the LIGHT button. + * If you're at 21, you cannoy hit, since we just assume it's a mispress on the button. + * + * Once you stand, the dealer will deal out to themselves once per second (or immidietly when you press the LIGHT or ALARM buttons). + * The game results are: + * WIN: You have a higher score than the dealer, but no more than 21. Or the dealer's score is over 21. + * LOSE: Your score is lower than the dealer's. + * BUST: Your score is above 21. + * TIE: Your score matches the dealer's final score + * + * On a watch that has the accelerometer, long-pressing the ALARM button will turn on the ability to play by tapping. + * The SIGNAL indicator will display when tapping is enabled. + * Tapping once will behave like the ALARM button and hit. + * Tapping twice behave like the LIGHT button and stand. + * + * | Cards | | + * |---------|--------------------------| + * | Value |2|3|4|5|6|7|8|9|10|J|Q|K|A| + * | Display |2|3|4|5|6|7|8|9| 0|-|=|≡|a| + * If you're using a custom display, Ace will display as 'A', not 'a' + */ + + + +typedef struct { + bool tap_control_on; + uint16_t games_played; + uint16_t games_won; +} blackjack_face_state_t; + +void blackjack_face_setup(uint8_t watch_face_index, void ** context_ptr); +void blackjack_face_activate(void *context); +bool blackjack_face_loop(movement_event_t event, void *context); +void blackjack_face_resign(void *context); + +#define blackjack_face ((const watch_face_t){ \ + blackjack_face_setup, \ + blackjack_face_activate, \ + blackjack_face_loop, \ + blackjack_face_resign, \ + NULL, \ +}) + +#endif // blackjack_FACE_H_ diff --git a/watch-faces/complication/deadline_face.c b/watch-faces/complication/deadline_face.c index 41240e76..d3204f20 100644 --- a/watch-faces/complication/deadline_face.c +++ b/watch-faces/complication/deadline_face.c @@ -161,19 +161,19 @@ static inline void _beep(beep_type_t beep_type) switch (beep_type) { case BEEP_BUTTON: - watch_buzzer_play_note(BUZZER_NOTE_C7, 50); + watch_buzzer_play_note_with_volume(BUZZER_NOTE_C7, 50, movement_button_volume()); break; case BEEP_ENABLE: - watch_buzzer_play_note(BUZZER_NOTE_G7, 50); + watch_buzzer_play_note_with_volume(BUZZER_NOTE_G7, 50, movement_button_volume()); watch_buzzer_play_note(BUZZER_NOTE_REST, 75); - watch_buzzer_play_note(BUZZER_NOTE_C8, 75); + watch_buzzer_play_note_with_volume(BUZZER_NOTE_C8, 50, movement_button_volume()); break; case BEEP_DISABLE: - watch_buzzer_play_note(BUZZER_NOTE_C8, 50); + watch_buzzer_play_note_with_volume(BUZZER_NOTE_C8, 50, movement_button_volume()); watch_buzzer_play_note(BUZZER_NOTE_REST, 75); - watch_buzzer_play_note(BUZZER_NOTE_G7, 75); + watch_buzzer_play_note_with_volume(BUZZER_NOTE_G7, 50, movement_button_volume()); break; } } diff --git a/legacy/watch_faces/complication/endless_runner_face.c b/watch-faces/complication/endless_runner_face.c similarity index 69% rename from legacy/watch_faces/complication/endless_runner_face.c rename to watch-faces/complication/endless_runner_face.c index 1a173293..8a4b7a3b 100644 --- a/legacy/watch_faces/complication/endless_runner_face.c +++ b/watch-faces/complication/endless_runner_face.c @@ -25,6 +25,7 @@ #include #include #include "endless_runner_face.h" +#include "delay.h" typedef enum { JUMPING_FINAL_FRAME = 0, @@ -34,6 +35,7 @@ typedef enum { typedef enum { SCREEN_TITLE = 0, + SCREEN_SCORE, SCREEN_PLAYING, SCREEN_LOSE, SCREEN_TIME, @@ -77,14 +79,45 @@ typedef struct { 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; +int8_t start_tune[] = { + BUZZER_NOTE_C5, 15, + BUZZER_NOTE_E5, 15, + BUZZER_NOTE_G5, 15, + 0 +}; + +int8_t lose_tune[] = { + BUZZER_NOTE_D3, 10, + BUZZER_NOTE_C3SHARP_D3FLAT, 10, + BUZZER_NOTE_C3, 10, + 0 +}; + static void print_binary(uint32_t value, int bits) { #if __EMSCRIPTEN__ for (int i = bits - 1; i >= 0; i--) { // Print each bit - printf("%lu", (value >> i) & 1); + printf("%u", (value >> i) & 1); // Optional: add a space every 4 bits for readability if (i % 4 == 0 && i != 0) { printf(" "); @@ -188,22 +221,22 @@ static uint32_t get_random_legal(uint32_t prev_val, uint16_t difficulty) { static void display_ball(bool jumping) { if (!jumping) { - watch_set_pixel(0, 21); - watch_set_pixel(1, 21); - watch_set_pixel(0, 20); - watch_set_pixel(1, 20); - watch_clear_pixel(1, 17); - watch_clear_pixel(2, 20); - watch_clear_pixel(2, 21); + 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(0, 21); - watch_clear_pixel(1, 21); - watch_clear_pixel(0, 20); - watch_set_pixel(1, 20); - watch_set_pixel(1, 17); - watch_set_pixel(2, 20); - watch_set_pixel(2, 21); + 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]); } } @@ -212,12 +245,12 @@ static void display_score(uint8_t score) { if (game_state.fuel_mode) { score %= (MAX_DISP_SCORE_FUEL + 1); sprintf(buf, "%1d", score); - watch_display_string(buf, 0); + watch_display_text(WATCH_POSITION_TOP_LEFT, buf); } else { score %= (MAX_DISP_SCORE + 1); sprintf(buf, "%2d", score); - watch_display_string(buf, 2); + watch_display_text(WATCH_POSITION_TOP_RIGHT, buf); } } @@ -234,16 +267,16 @@ static void add_to_score(endless_runner_state_t *state) { 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_string(" ", 2); // Blink the 0 fuel to show it cannot be refilled. + 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_string(buf, 2); + 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 = watch_rtc_get_date_time(); + 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)) { @@ -255,28 +288,15 @@ static void check_and_reset_hi_score(endless_runner_state_t *state) { } static void display_difficulty(uint16_t difficulty) { - switch (difficulty) - { - case DIFF_BABY: - watch_display_string(" b", 2); - break; - case DIFF_EASY: - watch_display_string(" E", 2); - break; - case DIFF_HARD: - watch_display_string(" H", 2); - break; - case DIFF_FUEL: - watch_display_string(" F", 2); - break; - case DIFF_FUEL_1: - watch_display_string("1F", 2); - break; - case DIFF_NORM: - default: - watch_display_string(" N", 2); - break; - } + 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; } @@ -289,65 +309,93 @@ static void change_difficulty(endless_runner_state_t *state) { } } -static void toggle_sound(endless_runner_state_t *state) { - state -> soundOn = !state -> soundOn; - if (state -> soundOn){ - watch_buzzer_play_note(BUZZER_NOTE_C5, 30); +static void display_sound_indicator(bool soundOn) { + if (soundOn){ watch_set_indicator(WATCH_INDICATOR_BELL); - } - else { + } else { watch_clear_indicator(WATCH_INDICATOR_BELL); } } +static void toggle_sound(endless_runner_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(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) { + game_state.curr_screen = SCREEN_TITLE; + watch_clear_colon(); + watch_display_text_with_fallback(WATCH_POSITION_TOP, "ENdLS", "ER "); + watch_display_text(WATCH_POSITION_BOTTOM, "RUNNER"); + display_sound_indicator(state -> soundOn); +} + +static void display_score_screen(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.curr_screen = SCREEN_SCORE; 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 "); if (hi_score > MAX_HI_SCORE) { - watch_display_string("ER HS --", 0); + watch_display_text(WATCH_POSITION_BOTTOM, "HS --"); } else { - char buf[14]; - sprintf(buf, "ER HS%4d", hi_score); - watch_display_string(buf, 0); + 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(watch_date_time_t date_time, bool clock_mode_24h) { +static void display_time(void) { static watch_date_time_t previous_date_time; + watch_date_time_t date_time = movement_get_local_date_time(); + movement_clock_mode_t clock_mode_24h = movement_clock_mode_24h(); 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); + if (!watch_sleep_animation_is_running()) { + watch_set_colon(); + watch_start_indicator_blink_if_possible(WATCH_INDICATOR_COLON, 500); + } + if (clock_mode_24h != MOVEMENT_CLOCK_MODE_12H) 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_string(buf, 4); + sprintf( buf, clock_mode_24h == MOVEMENT_CLOCK_MODE_024H ? "%02d%02d " : "%2d%02d ", hour, date_time.unit.minute); + watch_display_text(WATCH_POSITION_BOTTOM, buf); } - // If both digits of the minute need updating - else if ((date_time.unit.minute / 10) != (previous_date_time.unit.minute / 10)) { - sprintf( buf, "%02d ", date_time.unit.minute); - watch_display_string(buf, 6); - } - // If only the ones-place of the minute needs updating. - else if (date_time.unit.minute != previous_date_time.unit.minute) { - sprintf( buf, "%d ", date_time.unit.minute % 10); - watch_display_string(buf, 7); + // 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; } @@ -356,36 +404,37 @@ static void begin_playing(endless_runner_state_t *state) { uint8_t difficulty = state -> difficulty; game_state.curr_screen = SCREEN_PLAYING; watch_clear_colon(); + display_sound_indicator(state -> soundOn); movement_request_tick_frequency((state -> difficulty == DIFF_BABY) ? FREQ_SLOW : FREQ); if (game_state.fuel_mode) { - watch_display_string(" ", 0); + 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_string(" ", 2); + 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_note(BUZZER_NOTE_C5, 200); - watch_buzzer_play_note(BUZZER_NOTE_E5, 200); - watch_buzzer_play_note(BUZZER_NOTE_G5, 200); + 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_display_string(" LOSE ", 0); - if (state -> soundOn) - watch_buzzer_play_note(BUZZER_NOTE_A1, 600); - else + 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 display_obstacle(bool obstacle, int grid_loc, endless_runner_state_t *state) { @@ -395,9 +444,9 @@ static void display_obstacle(bool obstacle, int grid_loc, endless_runner_state_t case 2: game_state.loc_2_on = obstacle; if (obstacle) - watch_set_pixel(0, 20); + watch_set_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]); else if (game_state.jump_state != NOT_JUMPING) { - watch_clear_pixel(0, 20); + 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); } @@ -406,55 +455,20 @@ static void display_obstacle(bool obstacle, int grid_loc, endless_runner_state_t case 3: game_state.loc_3_on = obstacle; if (obstacle) - watch_set_pixel(1, 21); + watch_set_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]); else if (game_state.jump_state != NOT_JUMPING) - watch_clear_pixel(1, 21); + 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 - case 0: - case 5: - if (obstacle) - watch_set_pixel(0, 18 + grid_loc); - else - watch_clear_pixel(0, 18 + grid_loc); - break; - case 4: - if (obstacle) - watch_set_pixel(1, 22); - else - watch_clear_pixel(1, 22); - break; - case 6: - if (obstacle) - watch_set_pixel(1, 0); - else - watch_clear_pixel(1, 0); - break; - case 7: - case 8: - if (obstacle) - watch_set_pixel(0, grid_loc - 6); - else - watch_clear_pixel(0, grid_loc - 6); - break; - case 9: - case 10: - if (obstacle) - watch_set_pixel(0, grid_loc - 5); - else - watch_clear_pixel(0, grid_loc - 5); - break; - case 11: - if (obstacle) - watch_set_pixel(1, 6); - else - watch_clear_pixel(1, 6); - break; 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; } } @@ -546,26 +560,37 @@ void endless_runner_face_setup(uint8_t watch_face_index, void ** context_ptr) { 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; + if (watch_sleep_animation_is_running()) { + watch_stop_blink(); + } } 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); - if (state -> soundOn) watch_set_indicator(WATCH_INDICATOR_BELL); display_title(state); break; case EVENT_TICK: switch (game_state.curr_screen) { case SCREEN_TITLE: + case SCREEN_SCORE: case SCREEN_LOSE: + case SCREEN_TIME: break; default: update_game(state, event.subsecond); @@ -574,15 +599,37 @@ bool endless_runner_face_loop(movement_event_t event, void *context) { break; case EVENT_LIGHT_BUTTON_UP: case EVENT_ALARM_BUTTON_UP: - if (game_state.curr_screen == SCREEN_TITLE) - begin_playing(state); - else if (game_state.curr_screen == SCREEN_LOSE) - display_title(state); + 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); + } break; case EVENT_LIGHT_LONG_PRESS: - if (game_state.curr_screen == SCREEN_TITLE) + if (game_state.curr_screen == SCREEN_SCORE) 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_SCORE) { + begin_playing(state); + break; + } + else if (game_state.curr_screen == SCREEN_LOSE) { + display_score_screen(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){ @@ -592,15 +639,21 @@ bool endless_runner_face_loop(movement_event_t event, void *context) { } break; case EVENT_ALARM_LONG_PRESS: - if (game_state.curr_screen != SCREEN_PLAYING) + if (game_state.curr_screen == SCREEN_TITLE || game_state.curr_screen == SCREEN_SCORE) toggle_sound(state); break; case EVENT_TIMEOUT: - if (game_state.curr_screen != SCREEN_TITLE) - display_title(state); + disable_tap_control(state); + if (game_state.curr_screen != SCREEN_SCORE) + display_score_screen(state); break; case EVENT_LOW_ENERGY_UPDATE: - display_time(watch_rtc_get_date_time(), movement_clock_mode_24h()); + if (game_state.curr_screen != SCREEN_TIME) { + watch_display_text_with_fallback(WATCH_POSITION_TOP, "RUN ", "ER "); + display_sound_indicator(state -> soundOn); + display_difficulty(state->difficulty); + } + display_time(); break; default: return movement_default_loop_handler(event); @@ -609,6 +662,6 @@ bool endless_runner_face_loop(movement_event_t event, void *context) { } void endless_runner_face_resign(void *context) { - (void) context; + endless_runner_state_t *state = (endless_runner_state_t *)context; + disable_tap_control(state); } - diff --git a/legacy/watch_faces/complication/endless_runner_face.h b/watch-faces/complication/endless_runner_face.h similarity index 91% rename from legacy/watch_faces/complication/endless_runner_face.h rename to watch-faces/complication/endless_runner_face.h index 3cfa6814..3a17a611 100644 --- a/legacy/watch_faces/complication/endless_runner_face.h +++ b/watch-faces/complication/endless_runner_face.h @@ -33,6 +33,8 @@ This is a basic endless-runner, like the [Chrome Dino game](https://en.wikipedia.org/wiki/Dinosaur_Game). On the title screen, you can select a difficulty by long-pressing LIGHT or toggle sound by long-pressing ALARM. LED or ALARM are used to jump. + If the accelerometer is installed, you can tap the screen to jump and move through the menus after using the + buttons to go into the first game. High-score is displayed on the top-right on the title screen. During a game, the current score is displayed. */ @@ -42,7 +44,8 @@ typedef struct { uint8_t month_last_hi_score : 4; uint8_t year_last_hi_score : 6; uint8_t soundOn : 1; - /* 24 bits, likely aligned to 32 bits = 4 bytes */ + uint8_t tap_control_on : 1; + uint8_t unused : 7; } endless_runner_state_t; void endless_runner_face_setup(uint8_t watch_face_index, void ** context_ptr); diff --git a/watch-faces/complication/simple_coin_flip_face.c b/watch-faces/complication/simple_coin_flip_face.c index 446832be..635c70ff 100644 --- a/watch-faces/complication/simple_coin_flip_face.c +++ b/watch-faces/complication/simple_coin_flip_face.c @@ -26,6 +26,7 @@ #include #include #include "simple_coin_flip_face.h" +#include "delay.h" void simple_coin_flip_face_setup(uint8_t watch_face_index, void ** context_ptr) { (void) watch_face_index; @@ -36,7 +37,7 @@ void simple_coin_flip_face_setup(uint8_t watch_face_index, void ** context_ptr) } void simple_coin_flip_face_activate(void *context) { - simple_coin_flip_face_state_t *state = (simple_coin_flip_face_state_t *)context; + (void) context; } static uint32_t get_random(uint32_t max) { @@ -48,7 +49,7 @@ static uint32_t get_random(uint32_t max) { } -void draw_start_face() { +static void draw_start_face(void) { watch_clear_display(); if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) { watch_display_text(WATCH_POSITION_BOTTOM, " Flip"); @@ -57,7 +58,7 @@ void draw_start_face() { } } -void set_pixels(int pixels[3][4][2], int j_len) { +static void set_pixels(int pixels[3][4][2], int j_len) { for(int loopruns = 0; loopruns<2; loopruns++) { for(int i = 0; i<3; i++) { watch_clear_display(); @@ -69,7 +70,7 @@ void set_pixels(int pixels[3][4][2], int j_len) { } } -void load_animation() { +static void load_animation(void) { if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) { int j_len = 2; int pixels[3][4][2] = { @@ -114,6 +115,7 @@ void load_animation() { } static void _blink_face_update_lcd(simple_coin_flip_face_state_t *state) { + (void) state; watch_clear_display(); load_animation(); watch_clear_display(); diff --git a/watch-faces/settings/set_time_face.c b/watch-faces/settings/set_time_face.c index 63fc34fd..f394347d 100644 --- a/watch-faces/settings/set_time_face.c +++ b/watch-faces/settings/set_time_face.c @@ -46,7 +46,7 @@ static void _handle_alarm_button(watch_date_time_t date_time, uint8_t current_pa current_offset = movement_get_current_timezone_offset_for_zone(movement_get_timezone_index()); return; case 0: // year - date_time.unit.year = ((date_time.unit.year % 60) + 1); + date_time.unit.year = (date_time.unit.year + 1) % 60; break; case 1: // month date_time.unit.month = (date_time.unit.month % 12) + 1; diff --git a/watch-library/hardware/watch/watch_slcd.c b/watch-library/hardware/watch/watch_slcd.c index db6b1942..4ba2421b 100644 --- a/watch-library/hardware/watch/watch_slcd.c +++ b/watch-library/hardware/watch/watch_slcd.c @@ -252,7 +252,7 @@ void watch_enable_display(void) { slcd_clear(); if (_installed_display == WATCH_LCD_TYPE_CUSTOM) { - slcd_set_contrast(4); + slcd_set_contrast(0); } else { slcd_set_contrast(9); } diff --git a/watch-library/shared/watch/watch_utility.c b/watch-library/shared/watch/watch_utility.c index 6d024032..7179aa72 100644 --- a/watch-library/shared/watch/watch_utility.c +++ b/watch-library/shared/watch/watch_utility.c @@ -205,7 +205,8 @@ uint32_t watch_utility_date_time_to_unix_time(watch_date_time_t date_time, int32 watch_date_time_t watch_utility_date_time_from_unix_time(uint32_t timestamp, int32_t utc_offset) { watch_date_time_t retval; retval.reg = 0; - int32_t days, secs; + uint32_t secs; + int32_t days; int32_t remdays, remsecs, remyears; int32_t qc_cycles, c_cycles, q_cycles; int32_t years, months;