Merge branch 'main' into lander_face_port
This commit is contained in:
467
watch-faces/complication/blackjack_face.c
Executable file
467
watch-faces/complication/blackjack_face.c
Executable file
@@ -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 <time.h>
|
||||
#endif
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#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; i<MAX_PLAYER_CARDS_DISPLAY; i++) {
|
||||
card = player.hand[player.idx_hand - MAX_PLAYER_CARDS_DISPLAY + i];
|
||||
display_card_at_position(card, BOARD_DISPLAY_START + i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void display_dealer_hand(void) {
|
||||
uint8_t card = dealer.hand[dealer.idx_hand - 1];
|
||||
display_card_at_position(card, 0);
|
||||
}
|
||||
|
||||
static void display_score(uint8_t score, watch_position_t pos) {
|
||||
char buf[4];
|
||||
sprintf(buf, "%2d", score);
|
||||
watch_display_text(pos, buf);
|
||||
}
|
||||
|
||||
static void add_to_game_scores(bool add_to_wins) {
|
||||
g_state->games_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();
|
||||
}
|
||||
}
|
||||
91
watch-faces/complication/blackjack_face.h
Executable file
91
watch-faces/complication/blackjack_face.h
Executable file
@@ -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_
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
667
watch-faces/complication/endless_runner_face.c
Normal file
667
watch-faces/complication/endless_runner_face.c
Normal file
@@ -0,0 +1,667 @@
|
||||
/*
|
||||
* 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 "endless_runner_face.h"
|
||||
#include "delay.h"
|
||||
|
||||
typedef enum {
|
||||
JUMPING_FINAL_FRAME = 0,
|
||||
NOT_JUMPING,
|
||||
JUMPING_START,
|
||||
} RunnerJumpState;
|
||||
|
||||
typedef enum {
|
||||
SCREEN_TITLE = 0,
|
||||
SCREEN_SCORE,
|
||||
SCREEN_PLAYING,
|
||||
SCREEN_LOSE,
|
||||
SCREEN_TIME,
|
||||
SCREEN_COUNT
|
||||
} RunnerCurrScreen;
|
||||
|
||||
typedef enum {
|
||||
DIFF_BABY = 0, // FREQ_SLOW FPS; MIN_ZEROES 0's min; Jump is JUMP_FRAMES_EASY frames
|
||||
DIFF_EASY, // FREQ FPS; MIN_ZEROES 0's min; Jump is JUMP_FRAMES_EASY frames
|
||||
DIFF_NORM, // FREQ FPS; MIN_ZEROES 0's min; Jump is JUMP_FRAMES frames
|
||||
DIFF_HARD, // FREQ FPS; MIN_ZEROES_HARD 0's min; jump is JUMP_FRAMES frames
|
||||
DIFF_FUEL, // Mode where the top-right displays the amoount of fuel that you can be above the ground for, dodging obstacles. When on the ground, your fuel recharges.
|
||||
DIFF_FUEL_1, // Same as DIFF_FUEL, but if your fuel is 0, then you won't recharge
|
||||
DIFF_COUNT
|
||||
} RunnerDifficulty;
|
||||
|
||||
#define NUM_GRID 12 // This the length that the obstacle track can be on
|
||||
#define FREQ 8 // Frequency request for the game
|
||||
#define FREQ_SLOW 4 // Frequency request for baby mode
|
||||
#define JUMP_FRAMES 2 // Wait this many frames on difficulties above EASY before coming down from the jump button pressed
|
||||
#define JUMP_FRAMES_EASY 3 // Wait this many frames on difficulties at or below EASY before coming down from the jump button pressed
|
||||
#define MIN_ZEROES 4 // At minimum, we'll have this many spaces between obstacles
|
||||
#define MIN_ZEROES_HARD 3 // At minimum, we'll have this many spaces between obstacles on hard mode
|
||||
#define MAX_HI_SCORE 9999 // Max hi score to store and display on the title screen.
|
||||
#define MAX_DISP_SCORE 39 // The top-right digits can't properly display above 39
|
||||
#define JUMP_FRAMES_FUEL 30 // The max fuel that fuel that the fuel mode game will hold
|
||||
#define JUMP_FRAMES_FUEL_RECHARGE 3 // How much fuel each frame on the ground adds
|
||||
#define MAX_DISP_SCORE_FUEL 9 // Since the fuel mode displays the score in the weekday slot, two digits will display wonky data
|
||||
|
||||
typedef struct {
|
||||
uint32_t obst_pattern;
|
||||
uint16_t obst_indx : 8;
|
||||
uint16_t jump_state : 5;
|
||||
uint16_t sec_before_moves : 3;
|
||||
uint16_t curr_score : 10;
|
||||
uint16_t curr_screen : 4;
|
||||
bool loc_2_on;
|
||||
bool loc_3_on;
|
||||
bool success_jump;
|
||||
bool fuel_mode;
|
||||
uint8_t fuel;
|
||||
} game_state_t;
|
||||
|
||||
// always-on, left, right, bottom, jump-top, jump-left, jump-right
|
||||
int8_t classic_ball_arr_com[] = {1, 0, 1, 0, 2, 1, 2};
|
||||
int8_t classic_ball_arr_seg[] = {20, 20, 21, 21, 20, 17, 21};
|
||||
int8_t custom_ball_arr_com[] = {2, 1, 1, 0, 3, 3, 2};
|
||||
int8_t custom_ball_arr_seg[] = {15, 15, 14, 15, 14, 15, 14};
|
||||
|
||||
// obstacle 0-11
|
||||
int8_t classic_obstacle_arr_com[] = {0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1};
|
||||
int8_t classic_obstacle_arr_seg[] = {18, 19, 20, 21, 22, 23, 0, 1, 2, 4, 5, 6};
|
||||
int8_t custom_obstacle_arr_com[] = {1, 1, 1, 1, 1, 0, 1, 0, 3, 0, 0, 2};
|
||||
int8_t custom_obstacle_arr_seg[] = {22, 16, 15, 14, 1, 2, 3, 4, 4, 5, 6, 7};
|
||||
|
||||
int8_t *ball_arr_com;
|
||||
int8_t *ball_arr_seg;
|
||||
int8_t *obstacle_arr_com;
|
||||
int8_t *obstacle_arr_seg;
|
||||
|
||||
static game_state_t game_state;
|
||||
static const uint8_t _num_bits_obst_pattern = sizeof(game_state.obst_pattern) * 8;
|
||||
|
||||
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("%u", (value >> i) & 1);
|
||||
// Optional: add a space every 4 bits for readability
|
||||
if (i % 4 == 0 && i != 0) {
|
||||
printf(" ");
|
||||
}
|
||||
}
|
||||
printf("\r\n");
|
||||
#else
|
||||
(void) value;
|
||||
(void) bits;
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
static uint32_t get_random(uint32_t max) {
|
||||
#if __EMSCRIPTEN__
|
||||
return rand() % max;
|
||||
#else
|
||||
return arc4random_uniform(max);
|
||||
#endif
|
||||
}
|
||||
|
||||
static uint32_t get_random_nonzero(uint32_t max) {
|
||||
uint32_t random;
|
||||
do
|
||||
{
|
||||
random = get_random(max);
|
||||
} while (random == 0);
|
||||
return random;
|
||||
}
|
||||
|
||||
static uint32_t get_random_kinda_nonzero(uint32_t max) {
|
||||
// Returns a number that's between 1 and max, unless max is 0 or 1, then it returns 0 to max.
|
||||
if (max == 0) return 0;
|
||||
else if (max == 1) return get_random(max);
|
||||
return get_random_nonzero(max);
|
||||
}
|
||||
|
||||
static uint32_t get_random_fuel(uint32_t prev_val) {
|
||||
static uint8_t prev_rand_subset = 0;
|
||||
uint32_t rand;
|
||||
uint8_t max_ones, subset;
|
||||
uint32_t rand_legal = 0;
|
||||
prev_val = prev_val & ~0xFFFF;
|
||||
|
||||
for (int i = 0; i < 2; i++) {
|
||||
subset = 0;
|
||||
max_ones = 8;
|
||||
if (prev_rand_subset > 4)
|
||||
max_ones -= prev_rand_subset;
|
||||
rand = get_random_kinda_nonzero(max_ones);
|
||||
if (rand > 5 && prev_rand_subset) rand = 5; // The gap of one or two is awkward
|
||||
for (uint32_t j = 0; j < rand; j++) {
|
||||
subset |= (1 << j);
|
||||
}
|
||||
if (prev_rand_subset >= 7)
|
||||
subset = subset << 1;
|
||||
subset &= 0xFF;
|
||||
rand_legal |= subset << (8 * i);
|
||||
prev_rand_subset = rand;
|
||||
}
|
||||
|
||||
rand_legal = prev_val | rand_legal;
|
||||
print_binary(rand_legal, 32);
|
||||
return rand_legal;
|
||||
}
|
||||
|
||||
static uint32_t get_random_legal(uint32_t prev_val, uint16_t difficulty) {
|
||||
/** @brief A legal random number starts with the previous number (which should be the 12 bits on the screen).
|
||||
* @param prev_val The previous value to tack onto. The return will have its first NUM_GRID MSBs be the same as prev_val, and the rest be new
|
||||
* @param difficulty To dictate how spread apart the obsticles must be
|
||||
* @return the new random value, where it's first NUM_GRID MSBs are the same as prev_val
|
||||
*/
|
||||
uint8_t min_zeros = (difficulty == DIFF_HARD) ? MIN_ZEROES_HARD : MIN_ZEROES;
|
||||
uint32_t max = (1 << (_num_bits_obst_pattern - NUM_GRID)) - 1;
|
||||
uint32_t rand = get_random_nonzero(max);
|
||||
uint32_t rand_legal = 0;
|
||||
prev_val = prev_val & ~max;
|
||||
|
||||
for (int i = (NUM_GRID + 1); i <= _num_bits_obst_pattern; i++) {
|
||||
uint32_t mask = 1 << (_num_bits_obst_pattern - i);
|
||||
bool msb = (rand & mask) >> (_num_bits_obst_pattern - i);
|
||||
if (msb) {
|
||||
rand_legal = rand_legal << min_zeros;
|
||||
i+=min_zeros;
|
||||
}
|
||||
rand_legal |= msb;
|
||||
rand_legal = rand_legal << 1;
|
||||
}
|
||||
|
||||
rand_legal = rand_legal & max;
|
||||
for (int i = 0; i <= min_zeros; i++) {
|
||||
if (prev_val & (1 << (i + _num_bits_obst_pattern - NUM_GRID))){
|
||||
rand_legal = rand_legal >> (min_zeros - i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
rand_legal = prev_val | rand_legal;
|
||||
print_binary(rand_legal, 32);
|
||||
return rand_legal;
|
||||
}
|
||||
|
||||
static void display_ball(bool jumping) {
|
||||
if (!jumping) {
|
||||
watch_set_pixel(ball_arr_com[3], ball_arr_seg[3]);
|
||||
watch_set_pixel(ball_arr_com[2], ball_arr_seg[2]);
|
||||
watch_set_pixel(ball_arr_com[1], ball_arr_seg[1]);
|
||||
watch_set_pixel(ball_arr_com[0], ball_arr_seg[0]);
|
||||
watch_clear_pixel(ball_arr_com[6], ball_arr_seg[6]);
|
||||
watch_clear_pixel(ball_arr_com[5], ball_arr_seg[5]);
|
||||
watch_clear_pixel(ball_arr_com[4], ball_arr_seg[4]);
|
||||
}
|
||||
else {
|
||||
watch_clear_pixel(ball_arr_com[3], ball_arr_seg[3]);
|
||||
watch_clear_pixel(ball_arr_com[2], ball_arr_seg[2]);
|
||||
watch_clear_pixel(ball_arr_com[1], ball_arr_seg[1]);
|
||||
watch_set_pixel(ball_arr_com[0], ball_arr_seg[0]);
|
||||
watch_set_pixel(ball_arr_com[6], ball_arr_seg[6]);
|
||||
watch_set_pixel(ball_arr_com[5], ball_arr_seg[5]);
|
||||
watch_set_pixel(ball_arr_com[4], ball_arr_seg[4]);
|
||||
}
|
||||
}
|
||||
|
||||
static void display_score(uint8_t score) {
|
||||
char buf[3];
|
||||
if (game_state.fuel_mode) {
|
||||
score %= (MAX_DISP_SCORE_FUEL + 1);
|
||||
sprintf(buf, "%1d", score);
|
||||
watch_display_text(WATCH_POSITION_TOP_LEFT, buf);
|
||||
}
|
||||
else {
|
||||
score %= (MAX_DISP_SCORE + 1);
|
||||
sprintf(buf, "%2d", score);
|
||||
watch_display_text(WATCH_POSITION_TOP_RIGHT, buf);
|
||||
}
|
||||
}
|
||||
|
||||
static void add_to_score(endless_runner_state_t *state) {
|
||||
if (game_state.curr_score <= MAX_HI_SCORE) {
|
||||
game_state.curr_score++;
|
||||
if (game_state.curr_score > state -> hi_score)
|
||||
state -> hi_score = game_state.curr_score;
|
||||
}
|
||||
game_state.success_jump = true;
|
||||
display_score(game_state.curr_score);
|
||||
}
|
||||
|
||||
static void display_fuel(uint8_t subsecond, uint8_t difficulty) {
|
||||
char buf[4];
|
||||
if (difficulty == DIFF_FUEL_1 && game_state.fuel == 0 && subsecond % (FREQ/2) == 0) {
|
||||
watch_display_text(WATCH_POSITION_TOP_RIGHT, " "); // Blink the 0 fuel to show it cannot be refilled.
|
||||
return;
|
||||
}
|
||||
sprintf(buf, "%2d", game_state.fuel);
|
||||
watch_display_text(WATCH_POSITION_TOP_RIGHT, buf);
|
||||
}
|
||||
|
||||
static void check_and_reset_hi_score(endless_runner_state_t *state) {
|
||||
// Resets the hi score at the beginning of each month.
|
||||
watch_date_time_t date_time = movement_get_local_date_time();
|
||||
if ((state -> year_last_hi_score != date_time.unit.year) ||
|
||||
(state -> month_last_hi_score != date_time.unit.month))
|
||||
{
|
||||
// The high score resets itself every new month.
|
||||
state -> hi_score = 0;
|
||||
state -> year_last_hi_score = date_time.unit.year;
|
||||
state -> month_last_hi_score = date_time.unit.month;
|
||||
}
|
||||
}
|
||||
|
||||
static void display_difficulty(uint16_t difficulty) {
|
||||
static const char *labels[] = {
|
||||
[DIFF_BABY] = " b",
|
||||
[DIFF_EASY] = " E",
|
||||
[DIFF_HARD] = " H",
|
||||
[DIFF_FUEL] = " F",
|
||||
[DIFF_FUEL_1] = "1F",
|
||||
[DIFF_NORM] = " N"
|
||||
};
|
||||
watch_display_text(WATCH_POSITION_TOP_RIGHT, labels[difficulty]);
|
||||
game_state.fuel_mode = difficulty >= DIFF_FUEL && difficulty <= DIFF_FUEL_1;
|
||||
}
|
||||
|
||||
static void change_difficulty(endless_runner_state_t *state) {
|
||||
state -> difficulty = (state -> difficulty + 1) % DIFF_COUNT;
|
||||
display_difficulty(state -> difficulty);
|
||||
if (state -> soundOn) {
|
||||
if (state -> difficulty == 0) watch_buzzer_play_note(BUZZER_NOTE_B4, 30);
|
||||
else watch_buzzer_play_note(BUZZER_NOTE_C5, 30);
|
||||
}
|
||||
}
|
||||
|
||||
static void display_sound_indicator(bool soundOn) {
|
||||
if (soundOn){
|
||||
watch_set_indicator(WATCH_INDICATOR_BELL);
|
||||
} 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;
|
||||
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_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();
|
||||
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 (!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;
|
||||
}
|
||||
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 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(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_clear_display();
|
||||
game_state.obst_pattern = get_random_fuel(0);
|
||||
if ((16 * JUMP_FRAMES_FUEL_RECHARGE) < JUMP_FRAMES_FUEL) // 16 frames of zeros at the start of a level
|
||||
game_state.fuel = JUMP_FRAMES_FUEL - (16 * JUMP_FRAMES_FUEL_RECHARGE); // Have it below its max to show it counting up when starting.
|
||||
if (game_state.fuel < JUMP_FRAMES_FUEL_RECHARGE) game_state.fuel = JUMP_FRAMES_FUEL_RECHARGE;
|
||||
}
|
||||
else {
|
||||
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
|
||||
watch_display_text(WATCH_POSITION_BOTTOM, " ");
|
||||
game_state.obst_pattern = get_random_legal(0, difficulty);
|
||||
}
|
||||
game_state.jump_state = NOT_JUMPING;
|
||||
display_ball(game_state.jump_state != NOT_JUMPING);
|
||||
display_score( game_state.curr_score);
|
||||
if (state -> soundOn){
|
||||
watch_buzzer_play_sequence(start_tune, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
static void display_lose_screen(endless_runner_state_t *state) {
|
||||
game_state.curr_screen = SCREEN_LOSE;
|
||||
game_state.curr_score = 0;
|
||||
watch_clear_display();
|
||||
watch_display_text(WATCH_POSITION_BOTTOM, " LOSE ");
|
||||
if (state -> soundOn) {
|
||||
watch_buzzer_play_sequence(lose_tune, NULL);
|
||||
delay_ms(600);
|
||||
}
|
||||
}
|
||||
|
||||
static void display_obstacle(bool obstacle, int grid_loc, endless_runner_state_t *state) {
|
||||
static bool prev_obst_pos_two = 0;
|
||||
switch (grid_loc)
|
||||
{
|
||||
case 2:
|
||||
game_state.loc_2_on = obstacle;
|
||||
if (obstacle)
|
||||
watch_set_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]);
|
||||
else if (game_state.jump_state != NOT_JUMPING) {
|
||||
watch_clear_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]);
|
||||
if (game_state.fuel_mode && prev_obst_pos_two)
|
||||
add_to_score(state);
|
||||
}
|
||||
prev_obst_pos_two = obstacle;
|
||||
break;
|
||||
case 3:
|
||||
game_state.loc_3_on = obstacle;
|
||||
if (obstacle)
|
||||
watch_set_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]);
|
||||
else if (game_state.jump_state != NOT_JUMPING)
|
||||
watch_clear_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]);
|
||||
break;
|
||||
|
||||
case 1:
|
||||
if (!game_state.fuel_mode && obstacle) // If an obstacle is here, it means the ball cleared it
|
||||
add_to_score(state);
|
||||
//fall through
|
||||
default:
|
||||
if (obstacle)
|
||||
watch_set_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]);
|
||||
else
|
||||
watch_clear_pixel(obstacle_arr_com[grid_loc], obstacle_arr_seg[grid_loc]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void stop_jumping(endless_runner_state_t *state) {
|
||||
game_state.jump_state = NOT_JUMPING;
|
||||
display_ball(game_state.jump_state != NOT_JUMPING);
|
||||
if (state -> soundOn){
|
||||
if (game_state.success_jump)
|
||||
watch_buzzer_play_note(BUZZER_NOTE_C5, 60);
|
||||
else
|
||||
watch_buzzer_play_note(BUZZER_NOTE_C3, 60);
|
||||
}
|
||||
game_state.success_jump = false;
|
||||
}
|
||||
|
||||
static void display_obstacles(endless_runner_state_t *state) {
|
||||
for (int i = 0; i < NUM_GRID; i++) {
|
||||
// Use a bitmask to isolate each bit and shift it to the least significant position
|
||||
uint32_t mask = 1 << ((_num_bits_obst_pattern - 1) - i);
|
||||
bool obstacle = (game_state.obst_pattern & mask) >> ((_num_bits_obst_pattern - 1) - i);
|
||||
display_obstacle(obstacle, i, state);
|
||||
}
|
||||
game_state.obst_pattern = game_state.obst_pattern << 1;
|
||||
game_state.obst_indx++;
|
||||
if (game_state.fuel_mode) {
|
||||
if (game_state.obst_indx >= (_num_bits_obst_pattern / 2)) {
|
||||
game_state.obst_indx = 0;
|
||||
game_state.obst_pattern = get_random_fuel(game_state.obst_pattern);
|
||||
}
|
||||
}
|
||||
else if (game_state.obst_indx >= _num_bits_obst_pattern - NUM_GRID) {
|
||||
game_state.obst_indx = 0;
|
||||
game_state.obst_pattern = get_random_legal(game_state.obst_pattern, state -> difficulty);
|
||||
}
|
||||
}
|
||||
|
||||
static void update_game(endless_runner_state_t *state, uint8_t subsecond) {
|
||||
uint8_t curr_jump_frame = 0;
|
||||
if (game_state.sec_before_moves != 0) {
|
||||
if (subsecond == 0) --game_state.sec_before_moves;
|
||||
return;
|
||||
}
|
||||
display_obstacles(state);
|
||||
switch (game_state.jump_state)
|
||||
{
|
||||
case NOT_JUMPING:
|
||||
if (game_state.fuel_mode) {
|
||||
for (int i = 0; i < JUMP_FRAMES_FUEL_RECHARGE; i++)
|
||||
{
|
||||
if(game_state.fuel >= JUMP_FRAMES_FUEL || (state -> difficulty == DIFF_FUEL_1 && !game_state.fuel))
|
||||
break;
|
||||
game_state.fuel++;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case JUMPING_FINAL_FRAME:
|
||||
stop_jumping(state);
|
||||
break;
|
||||
default:
|
||||
if (game_state.fuel_mode) {
|
||||
if (!game_state.fuel)
|
||||
game_state.jump_state = JUMPING_FINAL_FRAME;
|
||||
else
|
||||
game_state.fuel--;
|
||||
if (!HAL_GPIO_BTN_ALARM_read() && !HAL_GPIO_BTN_LIGHT_read()) stop_jumping(state);
|
||||
}
|
||||
else {
|
||||
curr_jump_frame = game_state.jump_state - NOT_JUMPING;
|
||||
if (curr_jump_frame >= JUMP_FRAMES_EASY || (state -> difficulty >= DIFF_NORM && curr_jump_frame >= JUMP_FRAMES))
|
||||
game_state.jump_state = JUMPING_FINAL_FRAME;
|
||||
else
|
||||
game_state.jump_state++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (game_state.jump_state == NOT_JUMPING && (game_state.loc_2_on || game_state.loc_3_on)) {
|
||||
delay_ms(200); // To show the player jumping onto the obstacle before displaying the lose screen.
|
||||
display_lose_screen(state);
|
||||
}
|
||||
else if (game_state.fuel_mode)
|
||||
display_fuel(subsecond, state -> difficulty);
|
||||
}
|
||||
|
||||
void endless_runner_face_setup(uint8_t watch_face_index, void ** context_ptr) {
|
||||
(void) watch_face_index;
|
||||
if (*context_ptr == NULL) {
|
||||
*context_ptr = malloc(sizeof(endless_runner_state_t));
|
||||
memset(*context_ptr, 0, sizeof(endless_runner_state_t));
|
||||
endless_runner_state_t *state = (endless_runner_state_t *)*context_ptr;
|
||||
state->difficulty = DIFF_NORM;
|
||||
state->tap_control_on = false;
|
||||
}
|
||||
}
|
||||
|
||||
void endless_runner_face_activate(void *context) {
|
||||
(void) context;
|
||||
bool is_custom_lcd = watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM;
|
||||
ball_arr_com = is_custom_lcd ? custom_ball_arr_com : classic_ball_arr_com;
|
||||
ball_arr_seg = is_custom_lcd ? custom_ball_arr_seg : classic_ball_arr_seg;
|
||||
obstacle_arr_com = is_custom_lcd ? custom_obstacle_arr_com : classic_obstacle_arr_com;
|
||||
obstacle_arr_seg = is_custom_lcd ? custom_obstacle_arr_seg : classic_obstacle_arr_seg;
|
||||
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);
|
||||
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);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_UP:
|
||||
case EVENT_ALARM_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);
|
||||
}
|
||||
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:
|
||||
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){
|
||||
if (game_state.fuel_mode && !game_state.fuel) break;
|
||||
game_state.jump_state = JUMPING_START;
|
||||
display_ball(game_state.jump_state != NOT_JUMPING);
|
||||
}
|
||||
break;
|
||||
case EVENT_ALARM_LONG_PRESS:
|
||||
if (game_state.curr_screen == SCREEN_TITLE || 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) {
|
||||
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);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void endless_runner_face_resign(void *context) {
|
||||
endless_runner_state_t *state = (endless_runner_state_t *)context;
|
||||
disable_tap_control(state);
|
||||
}
|
||||
65
watch-faces/complication/endless_runner_face.h
Normal file
65
watch-faces/complication/endless_runner_face.h
Normal file
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#ifndef ENDLESS_RUNNER_FACE_H_
|
||||
#define ENDLESS_RUNNER_FACE_H_
|
||||
|
||||
#include "movement.h"
|
||||
|
||||
/*
|
||||
ENDLESS_RUNNER face
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
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;
|
||||
} endless_runner_state_t;
|
||||
|
||||
void endless_runner_face_setup(uint8_t watch_face_index, void ** context_ptr);
|
||||
void endless_runner_face_activate(void *context);
|
||||
bool endless_runner_face_loop(movement_event_t event, void *context);
|
||||
void endless_runner_face_resign(void *context);
|
||||
|
||||
#define endless_runner_face ((const watch_face_t){ \
|
||||
endless_runner_face_setup, \
|
||||
endless_runner_face_activate, \
|
||||
endless_runner_face_loop, \
|
||||
endless_runner_face_resign, \
|
||||
NULL, \
|
||||
})
|
||||
|
||||
#endif // ENDLESS_RUNNER_FACE_H_
|
||||
|
||||
407
watch-faces/complication/higher_lower_game_face.c
Executable file
407
watch-faces/complication/higher_lower_game_face.c
Executable file
@@ -0,0 +1,407 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// Emulator only: need time() to seed the random number generator.
|
||||
#if __EMSCRIPTEN__
|
||||
#include <time.h>
|
||||
#endif
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "higher_lower_game_face.h"
|
||||
#include "watch_common_display.h"
|
||||
|
||||
|
||||
#define KING 12
|
||||
#define QUEEN 11
|
||||
#define JACK 10
|
||||
|
||||
#define TITLE_TEXT "Hi-Lo"
|
||||
#define GAME_BOARD_SIZE 6
|
||||
#define MAX_BOARDS 40
|
||||
#define GUESSES_PER_SCREEN 5
|
||||
#define WIN_SCORE (MAX_BOARDS * GUESSES_PER_SCREEN)
|
||||
#define BOARD_DISPLAY_START 4
|
||||
#define BOARD_DISPLAY_END 9
|
||||
#define MIN_CARD_VALUE 2
|
||||
#define MAX_CARD_VALUE KING
|
||||
#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 FLIP_BOARD_DIRECTION false
|
||||
|
||||
typedef struct card_t {
|
||||
uint8_t value;
|
||||
bool revealed;
|
||||
} card_t;
|
||||
|
||||
typedef enum {
|
||||
A, B, C, D, E, F, G
|
||||
} segment_t;
|
||||
|
||||
typedef enum {
|
||||
HL_GUESS_EQUAL,
|
||||
HL_GUESS_HIGHER,
|
||||
HL_GUESS_LOWER
|
||||
} guess_t;
|
||||
|
||||
typedef enum {
|
||||
HL_GS_TITLE_SCREEN,
|
||||
HL_GS_GUESSING,
|
||||
HL_GS_WIN,
|
||||
HL_GS_LOSE,
|
||||
HL_GS_SHOW_SCORE,
|
||||
} game_state_t;
|
||||
|
||||
static game_state_t game_state = HL_GS_TITLE_SCREEN;
|
||||
static card_t game_board[GAME_BOARD_SIZE] = {0};
|
||||
static uint8_t guess_position = 0;
|
||||
static uint8_t score = 0;
|
||||
static uint8_t completed_board_count = 0;
|
||||
static uint8_t deck[DECK_SIZE] = {0};
|
||||
static uint8_t current_card = 0;
|
||||
|
||||
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 void reset_board(bool first_round) {
|
||||
// First card is random on the first board, and carried over from the last position on subsequent boards
|
||||
const uint8_t first_card_value = first_round
|
||||
? get_next_card()
|
||||
: game_board[GAME_BOARD_SIZE - 1].value;
|
||||
|
||||
game_board[0].value = first_card_value;
|
||||
game_board[0].revealed = true; // Always reveal first card
|
||||
|
||||
// Fill remainder of board
|
||||
for (size_t i = 1; i < GAME_BOARD_SIZE; ++i) {
|
||||
game_board[i] = (card_t) {
|
||||
.value = get_next_card(),
|
||||
.revealed = false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
static void init_game(void) {
|
||||
watch_clear_display();
|
||||
watch_display_text(WATCH_POSITION_BOTTOM, TITLE_TEXT);
|
||||
watch_display_text(WATCH_POSITION_TOP_LEFT, "HL");
|
||||
reset_deck();
|
||||
reset_board(true);
|
||||
score = 0;
|
||||
completed_board_count = 0;
|
||||
guess_position = 1;
|
||||
}
|
||||
|
||||
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 inline size_t get_display_position(size_t board_position) {
|
||||
return FLIP_BOARD_DIRECTION ? BOARD_DISPLAY_START + board_position : BOARD_DISPLAY_END - board_position;
|
||||
}
|
||||
|
||||
static void render_board_position(size_t board_position) {
|
||||
const size_t display_position = get_display_position(board_position);
|
||||
const bool revealed = game_board[board_position].revealed;
|
||||
|
||||
//// Current position indicator spot
|
||||
//if (board_position == guess_position) {
|
||||
// watch_display_character('-', display_position);
|
||||
// return;
|
||||
//}
|
||||
|
||||
if (!revealed) {
|
||||
// Higher or lower indicator (currently just an empty space)
|
||||
watch_display_character(' ', display_position);
|
||||
//set_segment_at_position(F, display_position);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t value = game_board[board_position].value;
|
||||
switch (value) {
|
||||
case KING: // K (≡)
|
||||
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: // Q (=)
|
||||
watch_display_character(' ', display_position);
|
||||
set_segment_at_position(A, display_position);
|
||||
set_segment_at_position(D, display_position);
|
||||
break;
|
||||
case JACK: // J (-)
|
||||
watch_display_character('-', display_position);
|
||||
break;
|
||||
default: {
|
||||
const char display_char = (value - MIN_CARD_VALUE) + '0';
|
||||
watch_display_character(display_char, display_position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void render_board(void) {
|
||||
for (size_t i = 0; i < GAME_BOARD_SIZE; ++i) {
|
||||
render_board_position(i);
|
||||
}
|
||||
}
|
||||
|
||||
static void render_board_count(void) {
|
||||
// Render completed boards (screens)
|
||||
char buf[3] = {0};
|
||||
snprintf(buf, sizeof(buf), "%2hhu", completed_board_count);
|
||||
watch_display_text(WATCH_POSITION_TOP_RIGHT, buf);
|
||||
}
|
||||
|
||||
static void render_final_score(void) {
|
||||
watch_display_text_with_fallback(WATCH_POSITION_TOP, "SCORE", "SC ");
|
||||
char buf[7] = {0};
|
||||
const uint8_t complete_boards = score / GUESSES_PER_SCREEN;
|
||||
snprintf(buf, sizeof(buf), "%2hu %03hu", complete_boards, score);
|
||||
watch_set_colon();
|
||||
watch_display_text(WATCH_POSITION_BOTTOM, buf);
|
||||
}
|
||||
|
||||
static guess_t get_answer(void) {
|
||||
if (guess_position < 1 || guess_position > GAME_BOARD_SIZE)
|
||||
return HL_GUESS_EQUAL; // Maybe add an error state, shouldn't ever hit this.
|
||||
|
||||
game_board[guess_position].revealed = true;
|
||||
const uint8_t previous_value = game_board[guess_position - 1].value;
|
||||
const uint8_t current_value = game_board[guess_position].value;
|
||||
|
||||
if (current_value > previous_value)
|
||||
return HL_GUESS_HIGHER;
|
||||
else if (current_value < previous_value)
|
||||
return HL_GUESS_LOWER;
|
||||
else
|
||||
return HL_GUESS_EQUAL;
|
||||
}
|
||||
|
||||
static void do_game_loop(guess_t user_guess) {
|
||||
switch (game_state) {
|
||||
case HL_GS_TITLE_SCREEN:
|
||||
init_game();
|
||||
render_board();
|
||||
render_board_count();
|
||||
game_state = HL_GS_GUESSING;
|
||||
break;
|
||||
case HL_GS_GUESSING: {
|
||||
const guess_t answer = get_answer();
|
||||
|
||||
// Render answer indicator
|
||||
switch (answer) {
|
||||
case HL_GUESS_EQUAL:
|
||||
watch_display_text(WATCH_POSITION_TOP_LEFT, "==");
|
||||
break;
|
||||
case HL_GUESS_HIGHER:
|
||||
watch_display_text(WATCH_POSITION_TOP_LEFT, "HI");
|
||||
break;
|
||||
case HL_GUESS_LOWER:
|
||||
watch_display_text(WATCH_POSITION_TOP_LEFT, "LO");
|
||||
break;
|
||||
}
|
||||
|
||||
// Scoring
|
||||
if (answer == user_guess) {
|
||||
score++;
|
||||
} else if (answer == HL_GUESS_EQUAL) {
|
||||
// No score for two consecutive identical cards
|
||||
} else {
|
||||
// Incorrect guess, game over
|
||||
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "End", "GO");
|
||||
game_board[guess_position].revealed = true;
|
||||
watch_display_text(WATCH_POSITION_BOTTOM, "------");
|
||||
render_board_position(guess_position - 1);
|
||||
render_board_position(guess_position);
|
||||
if (game_board[guess_position].value == JACK && guess_position < GAME_BOARD_SIZE) // Adds a space in case the revealed option is '-'
|
||||
watch_display_character(' ', get_display_position(guess_position + 1));
|
||||
game_state = HL_GS_LOSE;
|
||||
return;
|
||||
}
|
||||
|
||||
if (score >= WIN_SCORE) {
|
||||
// Win, perhaps some kind of animation sequence?
|
||||
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "WIN", "WI");
|
||||
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
|
||||
watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, "WINNER", "winnEr");
|
||||
game_state = HL_GS_WIN;
|
||||
return;
|
||||
}
|
||||
|
||||
// Next guess position
|
||||
const bool final_board_guess = guess_position == GAME_BOARD_SIZE - 1;
|
||||
if (final_board_guess) {
|
||||
// Seed new board
|
||||
completed_board_count++;
|
||||
render_board_count();
|
||||
guess_position = 1;
|
||||
reset_board(false);
|
||||
render_board();
|
||||
} else {
|
||||
guess_position++;
|
||||
render_board_position(guess_position - 1);
|
||||
render_board_position(guess_position);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case HL_GS_WIN:
|
||||
case HL_GS_LOSE:
|
||||
// Show score screen on button press from either state
|
||||
watch_clear_display();
|
||||
render_final_score();
|
||||
game_state = HL_GS_SHOW_SCORE;
|
||||
break;
|
||||
case HL_GS_SHOW_SCORE:
|
||||
watch_clear_display();
|
||||
watch_display_text(WATCH_POSITION_BOTTOM, TITLE_TEXT);
|
||||
watch_display_text(WATCH_POSITION_TOP_LEFT, "HL");
|
||||
game_state = HL_GS_TITLE_SCREEN;
|
||||
break;
|
||||
default:
|
||||
watch_display_text(WATCH_POSITION_BOTTOM, "ERROR");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void light_button_handler(void) {
|
||||
do_game_loop(HL_GUESS_HIGHER);
|
||||
}
|
||||
|
||||
static void alarm_button_handler(void) {
|
||||
do_game_loop(HL_GUESS_LOWER);
|
||||
}
|
||||
|
||||
void higher_lower_game_face_setup(uint8_t watch_face_index, void **context_ptr) {
|
||||
(void) watch_face_index;
|
||||
|
||||
if (*context_ptr == NULL) {
|
||||
*context_ptr = malloc(sizeof(higher_lower_game_face_state_t));
|
||||
memset(*context_ptr, 0, sizeof(higher_lower_game_face_state_t));
|
||||
// Do any one-time tasks in here; the inside of this conditional happens only at boot.
|
||||
memset(game_board, 0, sizeof(game_board));
|
||||
}
|
||||
// Do any pin or peripheral setup here; this will be called whenever the watch wakes from deep sleep.
|
||||
}
|
||||
|
||||
void higher_lower_game_face_activate(void *context) {
|
||||
higher_lower_game_face_state_t *state = (higher_lower_game_face_state_t *) context;
|
||||
(void) state;
|
||||
// Handle any tasks related to your watch face coming on screen.
|
||||
game_state = HL_GS_TITLE_SCREEN;
|
||||
stack_deck();
|
||||
}
|
||||
|
||||
bool higher_lower_game_face_loop(movement_event_t event, void *context) {
|
||||
higher_lower_game_face_state_t *state = (higher_lower_game_face_state_t *) context;
|
||||
(void) state;
|
||||
|
||||
switch (event.event_type) {
|
||||
case EVENT_ACTIVATE:
|
||||
// Show your initial UI here.
|
||||
watch_display_text(WATCH_POSITION_BOTTOM, TITLE_TEXT);
|
||||
watch_display_text(WATCH_POSITION_TOP_LEFT, "HL");
|
||||
break;
|
||||
case EVENT_TICK:
|
||||
// If needed, update your display here.
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_UP:
|
||||
light_button_handler();
|
||||
break;
|
||||
case EVENT_LIGHT_BUTTON_DOWN:
|
||||
// Don't trigger light
|
||||
break;
|
||||
case EVENT_ALARM_BUTTON_UP:
|
||||
alarm_button_handler();
|
||||
break;
|
||||
case EVENT_TIMEOUT:
|
||||
// Your watch face will receive this event after a period of inactivity. If it makes sense to resign,
|
||||
// you may uncomment this line to move back to the first watch face in the list:
|
||||
// movement_move_to_face(0);
|
||||
break;
|
||||
default:
|
||||
return movement_default_loop_handler(event);
|
||||
}
|
||||
|
||||
// return true if the watch can enter standby mode. Generally speaking, you should always return true.
|
||||
// Exceptions:
|
||||
// * If you are displaying a color using the low-level watch_set_led_color function, you should return false.
|
||||
// * If you are sounding the buzzer using the low-level watch_set_buzzer_on function, you should return false.
|
||||
// Note that if you are driving the LED or buzzer using Movement functions like movement_illuminate_led or
|
||||
// movement_play_alarm, you can still return true. This guidance only applies to the low-level watch_ functions.
|
||||
return true;
|
||||
}
|
||||
|
||||
void higher_lower_game_face_resign(void *context) {
|
||||
(void) context;
|
||||
|
||||
// handle any cleanup before your watch face goes off-screen.
|
||||
}
|
||||
106
watch-faces/complication/higher_lower_game_face.h
Executable file
106
watch-faces/complication/higher_lower_game_face.h
Executable file
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* 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 HIGHER_LOWER_GAME_FACE_H_
|
||||
#define HIGHER_LOWER_GAME_FACE_H_
|
||||
|
||||
#include "movement.h"
|
||||
|
||||
/*
|
||||
* Higher-Lower game face
|
||||
* ======================
|
||||
*
|
||||
* A game face based on the "higher-lower" card game where the objective is to correctly guess if the next card will
|
||||
* be higher or lower than the last revealed cards.
|
||||
*
|
||||
* Game Flow:
|
||||
* - When the face is selected, the "Hi-Lo" "Title" screen will be displayed, and the status indicator will display "GA" for game
|
||||
* - Pressing `ALARM` or `LIGHT` will start the game and proceed to the "Guessing" screen
|
||||
* - The first card will be revealed and the player must now make a guess
|
||||
* - A player can guess `Higher` by pressing the `LIGHT` button, and `Lower` by pressing the `ALARM` button
|
||||
* - The status indicator will show the result of the guess: HI (Higher), LO (Lower), or == (Equal)
|
||||
* - There are five guesses to make on each game screen, once the end of the screen is reached, a new screen
|
||||
* will be started, with the last revealed card carried over
|
||||
* - The number of completed screens is displayed in the top right (see Scoring)
|
||||
* - If the player has guessed correctly, the score is updated and play continues (see Scoring)
|
||||
* - If the player has guessed incorrectly, the status will change to GO (Game Over)
|
||||
* - The current card will be revealed
|
||||
* - Pressing `ALARM` or `LIGHT` will transition to the "Score" screen
|
||||
* - If the game is won, the status indicator will display "WI" and the "Win" screen will be displayed
|
||||
* - Pressing `ALARM` or `LIGHT` will transition to the "Score" screen
|
||||
* - The status indicator will change to "SC" when the final score is displayed
|
||||
* - The number of completed game screens will be displayed on using the first two digits
|
||||
* - The number of correct guesses will be displayed using the final three digits
|
||||
* - E.g. "13: 063" represents 13 completed screens, with 63 correct guesses
|
||||
* - Pressing `ALARM` or `LIGHT` while on the "Score" screen will transition to back to the "Title" screen
|
||||
*
|
||||
* Scoring:
|
||||
* - If the player guesses correctly (HI/LO) a point is gained
|
||||
* - If the player guesses incorrectly the game ends
|
||||
* - Unless the revealed card is equal (==) to the last card, in which case play continues, but no point is gained
|
||||
* - If the player completes 40 screens full of cards, the game ends and a win screen is displayed
|
||||
*
|
||||
* Misc:
|
||||
* The face tries to remain true to the spirit of using "cards"; to cope with the display limitations I've arrived at
|
||||
* the following mapping of card values to screen display, but am open to better suggestions:
|
||||
*
|
||||
* Thanks to voloved for adding deck shuffling and drawing!
|
||||
*
|
||||
* | Cards | |
|
||||
* |---------|--------------------------|
|
||||
* | Value |2|3|4|5|6|7|8|9|10|J|Q|K|A|
|
||||
* | Display |0|1|2|3|4|5|6|7|8 |9|-|=|≡|
|
||||
*
|
||||
* A previous alternative can be found in the git history:
|
||||
* | 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|-|=|≡|H|
|
||||
*
|
||||
*
|
||||
* Future Ideas:
|
||||
* - Add sounds
|
||||
* - Save/Display high score
|
||||
* - Add a "Win" animation
|
||||
* - Consider using lap indicator for larger score limit
|
||||
*/
|
||||
|
||||
typedef struct {
|
||||
// Anything you need to keep track of, put it here!
|
||||
} higher_lower_game_face_state_t;
|
||||
|
||||
void higher_lower_game_face_setup(uint8_t watch_face_index, void ** context_ptr);
|
||||
void higher_lower_game_face_activate(void *context);
|
||||
bool higher_lower_game_face_loop(movement_event_t event, void *context);
|
||||
void higher_lower_game_face_resign(void *context);
|
||||
|
||||
#define higher_lower_game_face ((const watch_face_t){ \
|
||||
higher_lower_game_face_setup, \
|
||||
higher_lower_game_face_activate, \
|
||||
higher_lower_game_face_loop, \
|
||||
higher_lower_game_face_resign, \
|
||||
NULL, \
|
||||
})
|
||||
|
||||
#endif // HIGHER_LOWER_GAME_FACE_H_
|
||||
@@ -26,6 +26,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#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();
|
||||
|
||||
Reference in New Issue
Block a user