Merge branch 'main' into simon-port

This commit is contained in:
voloved
2025-11-20 18:27:59 -05:00
committed by GitHub
16 changed files with 1546 additions and 178 deletions

View File

@ -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();

View File

@ -73,5 +73,9 @@
#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"
#include "lander_face.h"
#include "simon_face.h"
// New includes go above this line.

View File

@ -48,5 +48,9 @@ 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 \
./watch-faces/complication/lander_face.c \
./watch-faces/complication/simon_face.c \
# New watch faces go above this line.

View 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();
}
}

View 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_

View File

@ -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;
}
}

View File

@ -25,6 +25,7 @@
#include <stdlib.h>
#include <string.h>
#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);
}

View File

@ -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);

View File

@ -30,19 +30,22 @@
#include <stdlib.h>
#include <string.h>
#include "higher_lower_game_face.h"
#include "watch_private_display.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 STATUS_DISPLAY_START 0
#define BOARD_SCORE_DISPLAY_START 2
#define BOARD_DISPLAY_START 4
#define BOARD_DISPLAY_END 9
#define MIN_CARD_VALUE 2
#define MAX_CARD_VALUE 14
#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)
@ -111,7 +114,6 @@ static void shuffle_deck(void) {
static void reset_deck(void) {
current_card = 0;
stack_deck();
shuffle_deck();
}
@ -141,8 +143,8 @@ static void reset_board(bool first_round) {
static void init_game(void) {
watch_clear_display();
watch_display_string(TITLE_TEXT, BOARD_DISPLAY_START);
watch_display_string("GA", STATUS_DISPLAY_START);
watch_display_text(WATCH_POSITION_BOTTOM, TITLE_TEXT);
watch_display_text(WATCH_POSITION_TOP_LEFT, "HL");
reset_deck();
reset_board(true);
score = 0;
@ -151,16 +153,23 @@ static void init_game(void) {
}
static void set_segment_at_position(segment_t segment, uint8_t position) {
const uint64_t position_segment_data = (Segment_Map[position] >> (8 * (uint8_t) segment)) & 0xFF;
const uint8_t com_pin = position_segment_data >> 6;
const uint8_t seg = position_segment_data & 0x3F;
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 = FLIP_BOARD_DIRECTION
? BOARD_DISPLAY_START + board_position
: BOARD_DISPLAY_END - board_position;
const size_t display_position = get_display_position(board_position);
const bool revealed = game_board[board_position].revealed;
//// Current position indicator spot
@ -178,18 +187,18 @@ static void render_board_position(size_t board_position) {
const uint8_t value = game_board[board_position].value;
switch (value) {
case 14: // A (≡)
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 13: // K (=)
case QUEEN: // Q (=)
watch_display_character(' ', display_position);
set_segment_at_position(A, display_position);
set_segment_at_position(D, display_position);
break;
case 12: // Q (-)
case JACK: // J (-)
watch_display_character('-', display_position);
break;
default: {
@ -209,16 +218,16 @@ static void render_board_count(void) {
// Render completed boards (screens)
char buf[3] = {0};
snprintf(buf, sizeof(buf), "%2hhu", completed_board_count);
watch_display_string(buf, BOARD_SCORE_DISPLAY_START);
watch_display_text(WATCH_POSITION_TOP_RIGHT, buf);
}
static void render_final_score(void) {
watch_display_string("SC", STATUS_DISPLAY_START);
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_string(buf, BOARD_DISPLAY_START);
watch_display_text(WATCH_POSITION_BOTTOM, buf);
}
static guess_t get_answer(void) {
@ -251,13 +260,13 @@ static void do_game_loop(guess_t user_guess) {
// Render answer indicator
switch (answer) {
case HL_GUESS_EQUAL:
watch_display_string("==", STATUS_DISPLAY_START);
watch_display_text(WATCH_POSITION_TOP_LEFT, "==");
break;
case HL_GUESS_HIGHER:
watch_display_string("HI", STATUS_DISPLAY_START);
watch_display_text(WATCH_POSITION_TOP_LEFT, "HI");
break;
case HL_GUESS_LOWER:
watch_display_string("LO", STATUS_DISPLAY_START);
watch_display_text(WATCH_POSITION_TOP_LEFT, "LO");
break;
}
@ -268,18 +277,22 @@ static void do_game_loop(guess_t user_guess) {
// No score for two consecutive identical cards
} else {
// Incorrect guess, game over
watch_display_string("GO", STATUS_DISPLAY_START);
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_string("WI", STATUS_DISPLAY_START);
watch_display_string(" ", BOARD_SCORE_DISPLAY_START);
watch_display_string("------", BOARD_DISPLAY_START);
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;
}
@ -309,12 +322,12 @@ static void do_game_loop(guess_t user_guess) {
break;
case HL_GS_SHOW_SCORE:
watch_clear_display();
watch_display_string(TITLE_TEXT, BOARD_DISPLAY_START);
watch_display_string("GA", STATUS_DISPLAY_START);
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_string("ERROR", BOARD_DISPLAY_START);
watch_display_text(WATCH_POSITION_BOTTOM, "ERROR");
break;
}
}
@ -344,6 +357,7 @@ void higher_lower_game_face_activate(void *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) {
@ -353,8 +367,8 @@ bool higher_lower_game_face_loop(movement_event_t event, void *context) {
switch (event.event_type) {
case EVENT_ACTIVATE:
// Show your initial UI here.
watch_display_string(TITLE_TEXT, BOARD_DISPLAY_START);
watch_display_string("GA", STATUS_DISPLAY_START);
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.

View File

@ -0,0 +1,577 @@
/*
* MIT License
*
* Copyright (c) 2024 Klingon Jane
*
* 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 <math.h>
#include "lander_face.h"
#include "watch_common_display.h"
#ifndef max
#define max(x, y) ((y) > (x) ? (y) : (x))
#endif
#ifndef min
#define min(x, y) ((x) > (y) ? (y) : (x))
#endif
#define LANDER_TICK_FREQUENCY 8
#define MONSTER_DISPLAY_TICKS 9
#define ENGINE_THRUST 11
#define MODE_WAITING_TO_START 0
#define MODE_DISPLAY_SKILL_LEVEL 1
#define MODE_PLAYING 2
#define MODE_TOUCHDOWN_BLANK 3
#define MODE_DISPLAY_FINAL_STATUS 4
#define MODE_MONSTER 5
#define MODE_FIND_EARTH_MESSAGE 6
#define CREWS_COMPLIMENT 13
// Granularity is divisions per foot - height display
#define GRANUL 40
// Next lines for repeat heroes only.
#define PROMOTION_INTERVAL 3
#define LEVEL_ACE 8
#define LEVEL_STARBUCK 11
#define HARD_EARTH_INCREMENTS 11
#define MAX_HARD_EARTH_CHANCE 6
// The gory final result calculations:
#define SPEED_FATALITY_ALL 41
#define SPEED_FATALITY_NONE 26
#define SPEED_NO_DAMAGE 21
#define SPEED_LEVEL_INCREMENTS 2
#define SPEED_MAJOR_CRASH 73
#define MAJOR_CRASH_INCREMENTS 65
#define SPEED_INJURY_NONE 20
#define SPEED_INJURY_FULCRUM 32
#define INJURY_FULCRUM_PROB 65
#define FUEL_SCORE_GOOD 145
#define FUEL_SCORE_GREAT 131
#define FUEL_SCORE_FANTASTIC 125
// Joey Castillo to oversee storage allocation row
#define LANDER_STORAGE_ROW 2
#define STORAGE_KEY_NUMBER 110
#define DIFFICULTY_LEVELS 3
char lander_difficulty_names[DIFFICULTY_LEVELS][7] = {
"NOrMAL", "HArd ", "HArdEr"
};
#define MONSTER_TYPES 4
char lander_monster_names[MONSTER_TYPES][7] = {
"mOnStr", "6Erbil", "HAmStr", "Rabbit"
};
#define MONSTER_ACTIONS 8
char lander_monster_actions[MONSTER_ACTIONS][7] = {
"HUn6ry", " EAtS", "6Reedy", "annoYd", "nASty ", "SAVOry", "HO66SH", " pI66Y"
};
// --------------
// Custom methods
// --------------
static int gen_random_int (int16_t lower, int16_t upper) {
int range;
int retVal;
range = upper - lower + 1;
if ( range < 2 ) range = 2;
// Emulator: use rand. Hardware: use arc4random.
#if __EMSCRIPTEN__
retVal = rand() % range;
#else
retVal = arc4random_uniform(range);
#endif
retVal += lower;
return retVal;
}
static uint8_t assignProb ( uint8_t lowerProb, uint8_t upperProb, int16_t lowerSpeed, int16_t upperSpeed, int16_t actSpeed ) {
float probRange, speedRange;
float ratio, probFloat;
int probInt;
speedRange = upperSpeed - lowerSpeed;
if (speedRange<1.0) speedRange = 1.0;
probRange = upperProb - lowerProb;
ratio = ( (float) actSpeed - (float) lowerSpeed ) / speedRange;
probFloat = (float) lowerProb + ( ratio * probRange );
probInt = (int) ( probFloat + 0.5 );
probInt = min ( probInt, upperProb );
probInt = max ( probInt, lowerProb );
return (uint8_t) probInt;
}
static void write_to_lander_EEPROM(lander_state_t *state) {
uint8_t output_array [ 3 ];
output_array [ 0 ] = STORAGE_KEY_NUMBER;
output_array [ 1 ] = state->hero_counter;
output_array [ 2 ] = state->legend_counter;
watch_storage_erase ( LANDER_STORAGE_ROW );
watch_storage_sync ( );
watch_storage_write ( LANDER_STORAGE_ROW, 0, output_array, 3 );
}
// ---------------------------
// Standard watch face methods
// ---------------------------
void lander_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(lander_state_t));
memset(*context_ptr, 0, sizeof(lander_state_t));
lander_state_t *state = (lander_state_t *)*context_ptr;
state->led_enabled = false;
}
// Emulator only: Seed random number generator
#if __EMSCRIPTEN__
srand(time(NULL));
#endif
}
void lander_face_activate(void *context) {
lander_state_t *state = (lander_state_t *)context;
char buf [ 7 ];
state->mode = MODE_WAITING_TO_START;
state->led_active = false;
state->reset_counter = 0;
watch_clear_all_indicators ( );
uint32_t offset = 0;
uint32_t size = 3;
uint8_t stored_data [ size ];
// See if the hero_counter was ever written to EEPROM storage
watch_storage_read (LANDER_STORAGE_ROW, offset, stored_data, size);
if (stored_data[0] == STORAGE_KEY_NUMBER )
{
state->hero_counter = stored_data [1]; // There's real data in there.
state->legend_counter = stored_data [2];
}
else
{
state->hero_counter = 0; // Nope. Nothing there.
state->legend_counter = 0;
write_to_lander_EEPROM(state); // Initial EEPROM tracking data.
}
state->difficulty_level = state->hero_counter / PROMOTION_INTERVAL;
state->difficulty_level = min ( state->difficulty_level, DIFFICULTY_LEVELS - 1 ); // Upper limit
// Fancy intro
if ( state->legend_counter == 0 ) watch_display_text(WATCH_POSITION_TOP_LEFT, "LA");
else watch_display_text(WATCH_POSITION_TOP_LEFT, "LE");
if ( ( state->hero_counter == 0 ) || ( state->hero_counter >= 40 ) ) watch_display_text ( WATCH_POSITION_TOP_RIGHT, " ");
else
{
sprintf ( buf, "%2d", state->hero_counter );
watch_display_text ( WATCH_POSITION_TOP_RIGHT, buf);
}
if ( state->hero_counter >= 100 ) sprintf ( buf, "Str%3d", state->hero_counter );
else if ( state->hero_counter >= 40 ) sprintf ( buf, "Strb%2d", state->hero_counter );
else if ( state->hero_counter >= LEVEL_STARBUCK ) sprintf ( buf, "StrbUC" );
else if ( state->hero_counter >= LEVEL_ACE ) sprintf ( buf, " ACE " ); // This human is good
else if ( state->difficulty_level == 0 ) sprintf ( buf, " " );
else sprintf ( buf, "%s", lander_difficulty_names[state->difficulty_level] );
watch_display_text ( WATCH_POSITION_BOTTOM, buf);
if (state->led_enabled) watch_set_indicator(WATCH_INDICATOR_SIGNAL);
else watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
}
bool lander_face_loop(movement_event_t event, void *context) {
lander_state_t *state = (lander_state_t *)context;
char buf [ 20 ]; // [11] is more correct and works; compiler too helpful.
switch (event.event_type) {
case EVENT_TICK:
state->tick_counter++;
if ( state->mode == MODE_PLAYING ) {
int16_t accel = state->gravity;
bool gas_pedal_on = HAL_GPIO_BTN_ALARM_read() || HAL_GPIO_BTN_LIGHT_read();
if ( gas_pedal_on && ( state->fuel_remaining > 0 ) ) {
accel = ENGINE_THRUST + state->gravity; // Gravity is negative
state->fuel_remaining--; // Used 1 fuel unit
watch_set_indicator ( WATCH_INDICATOR_LAP );
// Low fuel warning indicators
if ( state->fuel_remaining == ( 3 * LANDER_TICK_FREQUENCY ) ) { // 3 seconds of fuel left
watch_set_indicator ( WATCH_INDICATOR_SIGNAL );
watch_set_indicator ( WATCH_INDICATOR_BELL );
watch_set_indicator ( WATCH_INDICATOR_PM );
watch_set_indicator ( WATCH_INDICATOR_24H );
}
else if ( state->fuel_remaining == 0 ) { // 0 seconds of fuel left, empty!
watch_clear_all_indicators ( );
}
}
else {
watch_clear_indicator ( WATCH_INDICATOR_LAP );
}
state->speed += accel;
state->height += state->speed;
if ( state->height > 971 * 80 ) { // Escape height
watch_clear_all_indicators ();
watch_display_text( WATCH_POSITION_BOTTOM, "ESCAPE" );
state->tick_counter = 0;
state->mode = MODE_WAITING_TO_START;
}
else if ( state->height <= 0 ) { // Touchdown
state->tick_counter = 0;
state->mode = MODE_TOUCHDOWN_BLANK;
}
else {
// Update height display
sprintf ( buf, "%4d", (int) ( state->height / GRANUL ) );
watch_display_text( WATCH_POSITION_BOTTOM, buf );
}
}
else if ( state->mode == MODE_TOUCHDOWN_BLANK ) {
// Blank display on touchdown
if ( state->tick_counter == 1 ) {
watch_clear_all_indicators ();
watch_display_text( WATCH_POSITION_BOTTOM, " " );
// Also calc fuel score now.
float fuel_score_float;
uint16_t fuel_used;
fuel_used = state->fuel_start - state->fuel_remaining;
fuel_score_float = (float) fuel_used / (float) state->fuel_tpl;
state->fuel_score = (int) (fuel_score_float * 100.0 + 0.5);
if ( state->legend_counter == 0 ) state->fuel_score -= 8; // First Earth is easier
// Monitor reset_counter
if ( fuel_used >= 1 ) state->reset_counter = 0;
else state->reset_counter++;
if ( state->reset_counter >= 3 ) {
state->hero_counter = 0;
state->difficulty_level = 0;
if ( state->reset_counter >= 6 ) state->legend_counter = 0;
watch_display_text(WATCH_POSITION_BOTTOM, "rESET ");
write_to_lander_EEPROM(state);
}
}
// Wait until time for next display
if ( state->tick_counter >= ( 1 * LANDER_TICK_FREQUENCY ) ) {
state->tick_counter = 0;
state->mode = MODE_DISPLAY_FINAL_STATUS;
}
}
else if ( state->mode == MODE_DISPLAY_FINAL_STATUS ) {
bool last_pass = false;
if ( state->tick_counter >= LANDER_TICK_FREQUENCY ) last_pass = true;
// Show final status
if ( state->tick_counter == 1 ) {
// Calculate many attributes
// 1) Major crash: bug, crater, vaporized (gone).
// 2) Rank ship's health 0 to 8
// 3) Crew fatalities and injuries
// 4) Special conditions: hero
// 5) Set fuel conservation indicators as appropriate
// 6) Set coffee maker OK indicator as appropriate
// 7) Green light if ship intact
// 8) Set standard display if not preempted.
bool allDone;
int16_t finalSpeed, boostedSpeed, levelsDamage;
int8_t shipsHealth, myRand;
uint8_t fatalities, probFatal, probInjury;
uint8_t i;
allDone = false;
// Easiest implementation for difficulty_level is to increase touchdown speed above actual.
finalSpeed = abs ( state->speed ) + state->difficulty_level * 4;
// First Earth is a bit easier than all the others
if ( state->legend_counter == 0 ) finalSpeed -= 2;
// 1) Major crash: bug, crater, vaporized (gone).
if ( finalSpeed >= SPEED_MAJOR_CRASH ) {
allDone = true;
shipsHealth = -1;
if ( finalSpeed >= ( SPEED_MAJOR_CRASH + 2 * MAJOR_CRASH_INCREMENTS ) ) sprintf ( buf, "GOnE " );
else if ( finalSpeed >= ( SPEED_MAJOR_CRASH + MAJOR_CRASH_INCREMENTS ) ) sprintf ( buf, " CrAtr" );
else sprintf ( buf, " bU6" );
}
// 2) Rank ship's health 0 to 8
if (!allDone) {
boostedSpeed = finalSpeed + SPEED_LEVEL_INCREMENTS - 1;
levelsDamage = (int) ( ( boostedSpeed - SPEED_NO_DAMAGE ) / SPEED_LEVEL_INCREMENTS );
shipsHealth = 8 - levelsDamage;
shipsHealth = min ( shipsHealth, 8 ); // Keep between 0 and 8
shipsHealth = max ( shipsHealth, 0 );
}
state->ships_health = shipsHealth; // Remember ships health
// 3) Crew fatalities and injuries
if (!allDone) {
// Fatalies
probFatal = assignProb ( 0, 92, SPEED_FATALITY_NONE, SPEED_FATALITY_ALL, finalSpeed );
// Injuries
if ( finalSpeed <= SPEED_INJURY_FULCRUM ) {
probInjury = assignProb ( 0, INJURY_FULCRUM_PROB, SPEED_INJURY_NONE, SPEED_INJURY_FULCRUM, finalSpeed );
} else {
probInjury = assignProb ( INJURY_FULCRUM_PROB, 96, SPEED_INJURY_FULCRUM, SPEED_FATALITY_ALL, finalSpeed );
}
fatalities = 0;
state->injured = 0;
for ( i = 0; i < CREWS_COMPLIMENT; i++ ) {
myRand = gen_random_int ( 1, 100 );
if ( myRand <= probFatal ) fatalities++;
else if ( myRand <= probInjury ) state->injured++;
}
state->uninjured = CREWS_COMPLIMENT - fatalities - state->injured;
}
// 4) Special conditions: hero
if (!allDone) {
if ( (shipsHealth>=8) && ( state->fuel_score <= FUEL_SCORE_FANTASTIC ) ) {
state->hero_counter++;
if ( state->hero_counter==1 ) sprintf ( buf, "HErO " );
else if ( state->hero_counter == LEVEL_ACE ) sprintf ( buf, " ACE " );
else if ( state->hero_counter == LEVEL_STARBUCK ) sprintf ( buf, "STrbUC" );
else if ( state->hero_counter>99 ) sprintf ( buf, "HEr%3d", state->hero_counter );
else sprintf ( buf, "HErO%2d", state->hero_counter ); // Typical case
allDone = true;
// Two rule sets for finding Earth. Alternate between easy and hard.
int8_t my_odds, temp;
if ( state->legend_counter %2 == 0 ) my_odds = (int8_t) state->hero_counter - LEVEL_STARBUCK; // Easy
else {
temp = ( state->hero_counter - LEVEL_STARBUCK ) + HARD_EARTH_INCREMENTS - 1;
my_odds = temp / HARD_EARTH_INCREMENTS;
my_odds = min ( my_odds, MAX_HARD_EARTH_CHANCE );
}
// Display odds in weekday region if positive value
if ( my_odds > 0 ) {
char buff3 [ 5 ];
sprintf ( buff3, "%2d", my_odds );
watch_display_text( WATCH_POSITION_TOP_RIGHT, buff3 );
} else watch_display_text( WATCH_POSITION_TOP_RIGHT, " " );
if ( my_odds >= gen_random_int ( 1, 200 ) ) { // EARTH!!!! The final objective.
sprintf ( buf, "EArTH " ); // 17% within 8, 50% by 16, 79% by 24, 94% by 32 <- easy mode
state->hero_counter = 0;
state->legend_counter++;
}
// Recalculate difficulty level base on new hero_counter.
state->difficulty_level = state->hero_counter / PROMOTION_INTERVAL;
state->difficulty_level = min ( state->difficulty_level, DIFFICULTY_LEVELS - 1 ); // Upper limit
// Write to EEPROM
write_to_lander_EEPROM(state);
}
}
// 5) Set fuel conservation indicators as appropriate
if ( shipsHealth >= 1 && ( state->fuel_score <= FUEL_SCORE_FANTASTIC ) ) watch_set_indicator ( WATCH_INDICATOR_LAP );
if ( shipsHealth >= 1 && ( state->fuel_score <= FUEL_SCORE_GREAT ) ) watch_set_indicator ( WATCH_INDICATOR_24H );
if ( shipsHealth >= 1 && ( state->fuel_score <= FUEL_SCORE_GOOD ) ) watch_set_indicator ( WATCH_INDICATOR_PM );
// 6) Set coffee maker OK indicator as appropriate
if ( shipsHealth >= 5 || ( shipsHealth >= 0 && ( gen_random_int ( 0, 3 ) != 1 ) ) ){
watch_set_indicator ( WATCH_INDICATOR_SIGNAL );
}
// 7) Green light if ship intact
if ( shipsHealth >= 8 && state->led_enabled) {
watch_set_led_green ( );
state->led_active = true;
}
// 8) Set standard display if not preempted.
if (!allDone) {
if ( ( state->injured > 0 ) || ( state->uninjured == 0 ) ) {
sprintf ( buf, "%d %2d%2d", shipsHealth, state->uninjured, state->injured );
}
else {
sprintf ( buf, "%d %2d ", shipsHealth, state->uninjured );
}
}
// Display final status.
watch_display_text(WATCH_POSITION_BOTTOM, buf );
} // End if tick_counter == 1
// Major crash - ship burning with red LED.
if ( state->ships_health < 0 && state->led_enabled) {
if ( ( gen_random_int ( 0, 1 ) != 1 ) && !last_pass ) { // Always off on last pass
// Turn on red LED.
watch_set_led_red ( );
state->led_active = true;
} else {
watch_set_led_off ( );
}
}
// Wait long enough, then allow waiting for next game.
if ( last_pass ) {
watch_set_led_off ( );
// No change to display text, allow new game to start.
state->mode = MODE_WAITING_TO_START;
// Unless it's time for monsters
uint8_t survivors = state->injured + state->uninjured;
if ( ( state->ships_health >= 0 ) && ( survivors > 0 ) &&
( gen_random_int ( -1, 3 ) >= state->ships_health ) ) {
state->mode = MODE_MONSTER;
state->tick_counter = 0;
state->monster_type = gen_random_int ( 0, MONSTER_TYPES - 1 );
}
}
} // End if MODE_DISPLAY_FINAL_STATUS
else if ( state->mode == MODE_DISPLAY_SKILL_LEVEL ) {
// Display skill level
if ( state->tick_counter == 1 ) {
sprintf ( buf, " %d", state->skill_level );
watch_display_text ( WATCH_POSITION_TOP_RIGHT, buf );
sprintf ( buf, " %d ", state->skill_level );
watch_display_text ( WATCH_POSITION_BOTTOM, buf );
}
// Wait long enough, then start game.
if ( state->tick_counter >= ( 2.0 * LANDER_TICK_FREQUENCY ) ) {
state->tick_counter = 0;
// Houston, WE ARE LAUNCHING NOW....
state->mode = MODE_PLAYING;
}
}
else if ( state->mode == MODE_FIND_EARTH_MESSAGE ) {
// Display "Find" then "Earth"
if ( state->tick_counter == 1 ) {
sprintf ( buf, " FInd " );
watch_display_text ( WATCH_POSITION_TOP_RIGHT, " " );
watch_display_text ( WATCH_POSITION_BOTTOM, buf );
}
if ( state->tick_counter == (int) ( 1.5 * LANDER_TICK_FREQUENCY + 1 ) ) {
sprintf ( buf, "EArTH " );
watch_display_text ( WATCH_POSITION_TOP_RIGHT, " " );
watch_display_text ( WATCH_POSITION_BOTTOM, buf );
}
// Wait long enough, then display skill level.
if ( state->tick_counter >= ( 3 * LANDER_TICK_FREQUENCY ) ) {
state->tick_counter = 0;
state->mode = MODE_DISPLAY_SKILL_LEVEL;
}
}
else if ( state->mode == MODE_MONSTER ) {
if ( state->tick_counter == 1 ) watch_display_text ( WATCH_POSITION_BOTTOM, lander_monster_names[state->monster_type] );
else if ( state->tick_counter == MONSTER_DISPLAY_TICKS + 1 ) {
uint8_t my_rand;
my_rand = gen_random_int ( 0 , MONSTER_ACTIONS - 1 );
watch_display_text ( WATCH_POSITION_BOTTOM, lander_monster_actions[my_rand] );
}
else if ( state->tick_counter == MONSTER_DISPLAY_TICKS * 2 ) { // Display 1st monster character
sprintf ( buf, "%s", lander_monster_names[state->monster_type] );
buf [1] = 0;
watch_display_text(WATCH_POSITION_BOTTOM, buf);
}
else if ( state->tick_counter == MONSTER_DISPLAY_TICKS * 2 + 1 ) { // Display current population, close mouth
sprintf ( buf, " c%2d%2d", state->uninjured, state->injured );
watch_display_text ( WATCH_POSITION_BOTTOM, buf );
}
else if ( state->tick_counter == MONSTER_DISPLAY_TICKS * 2 + 3 ) watch_display_character ( 'C', 5 ); // Open mouth
else if ( state->tick_counter == MONSTER_DISPLAY_TICKS * 2 + 5 ) {
// Decision to: continue loop, end loop or eat astronaut
uint8_t survivors = state->injured + state->uninjured;
uint8_t myRand = gen_random_int ( 0, 16 );
if ( survivors == 0 ) state->mode = MODE_WAITING_TO_START;
else if ( myRand <= 1 ) { // Leave loop with survivors
sprintf ( buf, "%d %2d%2d", state->ships_health, state->uninjured, state->injured );
watch_display_text ( WATCH_POSITION_BOTTOM, buf);
state->mode = MODE_WAITING_TO_START;
} else if ( myRand <= 11 ) state->tick_counter = MONSTER_DISPLAY_TICKS * 2; // Do nothing, loop continues
else { // Eat an astronaut - welcome to the space program!
if ( state->injured > 0 && state->uninjured > 0 ) {
if ( gen_random_int ( 0,1 ) == 0 ) state->injured--;
else state->uninjured--;
}
else if ( state->injured > 0 ) state->injured--;
else state->uninjured--;
state->tick_counter = MONSTER_DISPLAY_TICKS * 2; // Re-display
}
}
else if ( state->tick_counter >= MONSTER_DISPLAY_TICKS * 4 ) state->mode = MODE_WAITING_TO_START; // Safety
} // End if MODE_MONSTER
break; // End case EVENT_TICK
case EVENT_ALARM_BUTTON_DOWN:
if ( state->mode == MODE_WAITING_TO_START ) {
// That was the go signal - start a new game!!
float numerator, denominator, timeSquared;
int16_t gravity, thrust;
float myTime, distToTop, fuel_mult;
uint8_t skill_level;
int32_t tplTop; // Top lander height for TPL calculations
movement_request_tick_frequency(LANDER_TICK_FREQUENCY);
watch_set_led_off ( ); // Safety
watch_clear_all_indicators ( );
// Randomize starting parameters
state->height = gen_random_int ( 131, 181 ) * 80;
// Per line below; see Mars Orbiter September 23, 1999
if ( gen_random_int ( 0, 8 ) == 5 ) state->height = gen_random_int ( 240, 800 ) * 80;
state->speed = gen_random_int ( -120, 35 ); // Positive is up
state->gravity = gen_random_int ( -3, -2 ) * 2; // negative downwards value
skill_level = gen_random_int ( 1, 4 ); // Precursor to fuel allocation
// Theoretical Perfect Landing (TPL) calculations start here.
myTime = (float) state->speed / (float) state->gravity; // How long to reach this speed? Don't care which way sign is.
distToTop = fabs ( 0.5 * state->gravity * myTime * myTime );
tplTop = (int) ( state->height + distToTop + 0.5 ); // Theoretical highest point based on all of speed, height and gravity.
// Time squared = ( 2 * grav * height ) / ( t*t + g*t ), where t is net acceleration with thrust on.
gravity = abs ( state->gravity );
thrust = ENGINE_THRUST + state->gravity;
numerator = 2.0 * (float) gravity * (float) tplTop;
denominator = thrust * thrust + thrust * gravity;
timeSquared = numerator / denominator;
state->fuel_tpl = (int) ( sqrt ( timeSquared ) + 0.5 ); // Fuel required for theoretical perfect landing (TPL).
if ( skill_level == 1 ) fuel_mult = 4.0; // TPL + 300%
else if ( skill_level == 2 ) fuel_mult = 2.5; // TPL + 150%
else if ( skill_level == 3 ) fuel_mult = 1.6; // TPL + 60%
else fuel_mult = 1.3; // TPL + 30%
state->fuel_start = state->fuel_tpl * fuel_mult;
state->fuel_remaining = state->fuel_start;
state->skill_level = skill_level;
state->tick_counter = 0;
if ( gen_random_int ( 1, 109 ) != 37 ) {
// Houston, approaching launch....
state->mode = MODE_DISPLAY_SKILL_LEVEL;
}
else state->mode = MODE_FIND_EARTH_MESSAGE;
}
break;
case EVENT_LIGHT_BUTTON_DOWN:
if ( state->mode == MODE_WAITING_TO_START ) {
// Display difficulty level
watch_display_text(WATCH_POSITION_BOTTOM, lander_difficulty_names [state->difficulty_level]);
}
break;
case EVENT_LIGHT_LONG_PRESS:
if ( state->mode != MODE_WAITING_TO_START ) break;
state->led_enabled = !state->led_enabled;
if (state->led_enabled) watch_set_indicator(WATCH_INDICATOR_SIGNAL);
else watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
break;
case EVENT_LIGHT_LONG_UP:
if ( ( state->mode == MODE_WAITING_TO_START ) && ( state->legend_counter > 0 ) ) {
if ( state->legend_counter > 9 ) sprintf (buf,"EArt%2d", state->legend_counter );
else sprintf (buf,"EArth%d", state->legend_counter );
// Display legend counter
watch_display_text(WATCH_POSITION_BOTTOM, buf);
}
break;
default:
movement_default_loop_handler(event);
break;
}
if ( !state->led_active ) return true;
else return false;
}
void lander_face_resign(void *context) {
(void) context;
watch_set_led_off ( );
}

View File

@ -0,0 +1,152 @@
/*
* MIT License
*
* Copyright (c) 2024 Klingon Jane
*
* 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 LANDER_FACE_H_
#define LANDER_FACE_H_
#include "movement.h"
/*
My remake of a classic planet landing game.
Objective: Safely land the Cringeworthy.
Use your limited fuel supply to achieve a soft touch-down.
End scenarios and ship's health:
Hero They name this planet after you.
8 Life is very cozy.
7
6
5 Life is tolerable, plus some creature comforts
4
3 Marooned.
2
1
0 Ship destroyed. Life is harsh, no shelter. Giant hamsters are cute. **
Bug As in squished.
Crater They name this crater after you.
Gone As in vapourized.
Landing display format is:
Ship's health, intact crewmen, injured crewmen.
Additional data:
Crew's compliment: 13.
Low fuel warning icons: activates when 3 seconds of full thrust remains.
** Yes, hamsters are very cute. However; some eating of astronauts may occur.
Starting velocity, height and gravity are randomized each scenario.
Fuel levels randomly assigned from 1 to 4 (hardest) to match starting parameters.
A safe landing is always possible.
End of game icons:
LAP - Fantastic budgeting of fuel supply ( Required for heroic landing status. )
24H - Great budgeting of fuel supply
PM - Good budgeting of fuel supply
SIGNAL - The combination coffee and tea maker survived
Landings get progressively harder with the number of heroic landings made.
Number of heroic landings are remembered.
Heroic
Landings Status
0 Normal
3 Hard ( first difficulty increase )
6 Harder ( final difficulty increase )
8 Ace
11 ??????
Save yourself. Save the coffee maker.
END of standard training manual
*/
/*
What is really going on here?
The fleet is lost. You are a newbie pilot making a name for yourself.
Objective: Find Earth.
After reaching ?????? status, future heroic sorties will have 'some' chance in 200
of finding Earth.
Your chances improve by 1 chance in 200 for each subsequent Heroic Landing (HL).
Completing HL 12 will give you 1 chance in 200, for that landing.
HL 13 will give you 2 chances in 200, for that landing.
HL 14 will give you 3 chances in 200, for that landing.
HL 20 will give you 9 chances in 200, for that landing, and so on.
At these higher levels, your chances in 200 are displayed in the upper right corner on a heroic landing.
For wannabe pilots only: The HL counter can be reset by crashing three consecutive
missions without touching the thrust button. ( 6 to reset Earth-found counter )
Find Earth. Save Humanity.
*/
typedef struct {
int32_t height;
int16_t speed; // Positive is up
uint16_t tick_counter; // For minimum delays
uint16_t fuel_start;
uint16_t fuel_remaining;
uint16_t fuel_tpl; // Fuel required for theoretical perfect landing
uint16_t fuel_score; // 100 is perfect; higher is less perfect
int8_t gravity; // negative downwards value
bool led_enabled; // Can the led be turned on?
bool led_active; // Did we use it this scenario?
uint8_t mode; // 0 Pre-launch waiting, 1 show level, 2 playing, 3 touchdown blank, 4 final display, 5 monster
uint8_t skill_level; // 1 thru 4. Dictates fuel alloted
int8_t ships_health; // 0 thru 8. -1 = major crash
uint8_t hero_counter; // Total heroic landings ever
uint8_t legend_counter; // Historic events counter ( Earth )
uint8_t difficulty_level; // Based on hero_counter
uint8_t reset_counter; // Can reset hero_counter by crashing using zero fuel several consecutive scenarios
uint8_t monster_type; // Which monster is hungry?
uint8_t uninjured; // OK survivors
uint8_t injured; // Hurt survivors
} lander_state_t;
void lander_face_setup(uint8_t watch_face_index, void ** context_ptr);
void lander_face_activate(void *context);
bool lander_face_loop(movement_event_t event, void *context);
void lander_face_resign(void *context);
#define lander_face ((const watch_face_t){ \
lander_face_setup, \
lander_face_activate, \
lander_face_loop, \
lander_face_resign, \
NULL, \
})
#endif // LANDER_FACE_H_

View File

@ -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();

View File

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

View File

@ -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);
}

View File

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