Merge pull request #1 from joeycastillo/main

Updating.
This commit is contained in:
Michael Ciuffo
2025-11-23 13:35:02 -08:00
committed by GitHub
62 changed files with 5036 additions and 520 deletions

View File

@@ -40,9 +40,9 @@ typedef enum {
alarm_setting_idx_beeps
} alarm_setting_idx_t;
static const char _dow_strings[ALARM_DAY_STATES + 1][2] ={"AL", "MO", "TU", "WE", "TH", "FR", "SA", "SO", "ED", "1t", "MF", "WN"};
static const uint8_t _blink_idx[ALARM_SETTING_STATES] = {2, 0, 4, 6, 8, 9};
static const uint8_t _blink_idx2[ALARM_SETTING_STATES] = {3, 1, 5, 7, 8, 9};
static const char _dow_strings_classic[ALARM_DAY_STATES + 1][2] ={"AL", "MO", "TU", "WE", "TH", "FR", "SA", "SU", "ED", "1t", "MF", "WN"};
static const char _dow_strings_custom[ALARM_DAY_STATES + 1][3] ={ "AL ", "MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN", "DAY", "1t ", "M-F", "WKD"};
static const uint8_t _beeps_blink_idx = 9;
static const watch_buzzer_note_t _buzzer_notes[3] = {BUZZER_NOTE_B6, BUZZER_NOTE_C8, BUZZER_NOTE_A8};
// Volume is indicated by the three segments 5D, 5G and 5A
@@ -67,6 +67,10 @@ static void _alarm_set_signal(alarm_state_t *state) {
watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
}
static void _alarm_show_alarm_on_text(alarm_state_t *state) {
watch_display_text(WATCH_POSITION_SECONDS, state->alarm[state->alarm_idx].enabled ? "on" : "--");
}
static void _advanced_alarm_face_draw(alarm_state_t *state, uint8_t subsecond) {
char buf[12];
bool set_leading_zero = movement_clock_mode_24h() == MOVEMENT_CLOCK_MODE_024H;
@@ -90,18 +94,34 @@ static void _advanced_alarm_face_draw(alarm_state_t *state, uint8_t subsecond) {
watch_set_indicator(WATCH_INDICATOR_24H);
}
sprintf(buf, set_leading_zero? "%c%c%2d%02d%02d " : "%c%c%2d%2d%02d ",
_dow_strings[i][0], _dow_strings[i][1],
(state->alarm_idx + 1),
h,
state->alarm[state->alarm_idx].minute);
// blink items if in settings mode
if (state->is_setting && subsecond % 2 && state->setting_state < alarm_setting_idx_pitch && !state->alarm_quick_ticks) {
buf[_blink_idx[state->setting_state]] = buf[_blink_idx2[state->setting_state]] = ' ';
bool blinking = state->is_setting && subsecond % 2 && state->setting_state < alarm_setting_idx_pitch && !state->alarm_quick_ticks;
if (state->setting_state == alarm_setting_idx_alarm && blinking) {
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
} else {
sprintf(buf, "%2d", (state->alarm_idx + 1));
watch_display_text(WATCH_POSITION_TOP_RIGHT, buf);
}
if (state->setting_state == alarm_setting_idx_day && blinking) {
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, " ", " ");
} else {
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, _dow_strings_custom[i], _dow_strings_classic[i]);
}
if (state->setting_state == alarm_setting_idx_hour && blinking) {
watch_display_text(WATCH_POSITION_HOURS, " ");
} else {
sprintf(buf, set_leading_zero? "%02d" : "%2d", h);
watch_display_text(WATCH_POSITION_HOURS, buf);
}
if (state->setting_state == alarm_setting_idx_minute && blinking) {
watch_display_text(WATCH_POSITION_MINUTES, " ");
} else {
sprintf(buf, "%02d", state->alarm[state->alarm_idx].minute);
watch_display_text(WATCH_POSITION_MINUTES, buf);
}
watch_display_text(WATCH_POSITION_FULL, buf);
if (state->is_setting) {
watch_display_text(WATCH_POSITION_SECONDS, " ");
// draw pitch level indicator
if ((subsecond % 2) == 0 || (state->setting_state != alarm_setting_idx_pitch)) {
for (i = 0; i <= state->alarm[state->alarm_idx].pitch && i < 3; i++)
@@ -110,15 +130,18 @@ static void _advanced_alarm_face_draw(alarm_state_t *state, uint8_t subsecond) {
// draw beep rounds indicator
if ((subsecond % 2) == 0 || (state->setting_state != alarm_setting_idx_beeps)) {
if (state->alarm[state->alarm_idx].beeps == ALARM_MAX_BEEP_ROUNDS - 1)
watch_display_character('L', _blink_idx[alarm_setting_idx_beeps]);
watch_display_character('L', _beeps_blink_idx);
else {
if (state->alarm[state->alarm_idx].beeps == 0)
watch_display_character('o', _blink_idx[alarm_setting_idx_beeps]);
watch_display_character('o', _beeps_blink_idx);
else
watch_display_character(state->alarm[state->alarm_idx].beeps + 48, _blink_idx[alarm_setting_idx_beeps]);
watch_display_character(state->alarm[state->alarm_idx].beeps + 48, _beeps_blink_idx);
}
}
}
else {
_alarm_show_alarm_on_text(state);
}
// set alarm indicator
_alarm_set_signal(state);
@@ -303,6 +326,7 @@ bool advanced_alarm_face_loop(movement_event_t event, void *context) {
// revert change of enabled flag and show it briefly
state->alarm[state->alarm_idx].enabled ^= 1;
_alarm_set_signal(state);
_alarm_show_alarm_on_text(state);
delay_ms(275);
state->alarm_idx = 0;
}

View File

@@ -0,0 +1,439 @@
/*
* MIT License
*
* Copyright (c) 2025 Gábor Nyéki
*
* 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 "baby_kicks_face.h"
#include "watch.h"
#include "watch_utility.h"
static inline void _play_failure_sound_if_beep_is_on() {
if (movement_button_should_sound()) {
watch_buzzer_play_note(BUZZER_NOTE_E7, 10);
}
}
static inline void _play_successful_increment_sound_if_beep_is_on() {
if (movement_button_should_sound()) {
watch_buzzer_play_note(BUZZER_NOTE_E6, 10);
}
}
static inline void _play_successful_decrement_sound_if_beep_is_on() {
if (movement_button_should_sound()) {
watch_buzzer_play_note(BUZZER_NOTE_D6, 10);
}
}
static inline void _play_button_sound_if_beep_is_on() {
if (movement_button_should_sound()) {
watch_buzzer_play_note(BUZZER_NOTE_C7, 10);
}
}
/** @brief Predicate for whether the counter has been started.
*/
static inline bool _is_running(baby_kicks_state_t *state) {
return state->start > 0;
}
/** @brief Gets the current time, and caches it for re-use.
*/
static inline watch_date_time_t *_get_now(baby_kicks_state_t *state) {
if (state->now.unit.year == 0) {
state->now = movement_get_local_date_time();
}
return &state->now;
}
/** @brief Clears the current time. Should only be called at the end of
* `baby_kicks_face_loop`.
*/
static inline void _clear_now(baby_kicks_state_t *state) {
if (state->now.unit.year > 0) {
memset(&state->now, 0, sizeof(state->now));
}
}
/** @brief Calculates the number of minutes since the timer was started.
* @return If the counter has been started, then the number of full
* minutes that have elapsed. If it has not been started, then
* 255.
*/
static inline uint32_t _elapsed_minutes(baby_kicks_state_t *state) {
if (!_is_running(state)) {
return 0xff;
}
watch_date_time_t *now = _get_now(state);
return (
watch_utility_date_time_to_unix_time(*now, 0) - state->start
) / 60;
}
/** @brief Predicate for whether the counter has started but run for too
* long.
*/
static inline bool _has_timed_out(baby_kicks_state_t *state) {
return _elapsed_minutes(state) > BABY_KICKS_TIMEOUT;
}
/** @brief Determines what we should display based on `state`. Should
* only be called from `baby_kicks_face_loop`.
*/
static void _update_display_mode(baby_kicks_state_t *state) {
if (watch_sleep_animation_is_running()) {
state->mode = BABY_KICKS_MODE_LE_MODE;
} else if (!_is_running(state)) {
state->mode = BABY_KICKS_MODE_SPLASH;
} else if (_has_timed_out(state)) {
state->mode = BABY_KICKS_MODE_TIMED_OUT;
} else {
state->mode = BABY_KICKS_MODE_ACTIVE;
}
}
/** @brief Starts the counter.
* @details Sets the start time which will be used to calculate the
* elapsed minutes.
*/
static inline void _start(baby_kicks_state_t *state) {
watch_date_time_t *now = _get_now(state);
uint32_t now_unix = watch_utility_date_time_to_unix_time(*now, 0);
state->start = now_unix;
}
/** @brief Resets the counter.
* @details Zeros out the watch face state and clears the undo ring
* buffer. Effectively sets `state->mode` to
* `BABY_KICKS_MODE_SPLASH`.
*/
static void _reset(baby_kicks_state_t *state) {
memset(state, 0, sizeof(baby_kicks_state_t));
memset(
state->undo_buffer.stretches,
0xff,
sizeof(state->undo_buffer.stretches)
);
}
/** @brief Records a movement.
* @details Increments the movement counter, and along with it, if
* necessary, the counter of one-minute stretches. Also adds
* the movement to the undo buffer.
*/
static inline void _increment_counts(baby_kicks_state_t *state) {
watch_date_time_t *now = _get_now(state);
uint32_t now_unix = watch_utility_date_time_to_unix_time(*now, 0);
/* Add movement to the undo ring buffer. */
state->undo_buffer.stretches[state->undo_buffer.head] =
state->stretch_count;
state->undo_buffer.head++;
state->undo_buffer.head %= sizeof(state->undo_buffer.stretches);
state->movement_count++;
if (state->stretch_count == 0
|| state->latest_stretch_start + 60 < now_unix) {
/* Start new stretch. */
state->latest_stretch_start = now_unix;
state->stretch_count++;
}
}
/** @brief Undoes the last movement.
* @details Decrements the movement counter and, if necessary, the
* counter of one-minute stretches.
* @return True if and only if there was a movement to undo.
*/
static inline bool _successfully_undo(baby_kicks_state_t *state) {
uint8_t latest_mvmt, pre_undo_stretch_count;
/* The latest movement is stored one position before `.head`. */
if (state->undo_buffer.head == 0) {
latest_mvmt = sizeof(state->undo_buffer.stretches) - 1;
} else {
latest_mvmt = state->undo_buffer.head - 1;
}
pre_undo_stretch_count =
state->undo_buffer.stretches[latest_mvmt];
if (pre_undo_stretch_count == 0xff) {
/* Nothing to undo. */
return false;
} else if (pre_undo_stretch_count < state->stretch_count) {
state->latest_stretch_start = 0;
state->stretch_count--;
}
state->movement_count--;
state->undo_buffer.stretches[latest_mvmt] = 0xff;
state->undo_buffer.head = latest_mvmt;
return true;
}
/** @brief Updates the display with the movement counts if the counter
* has been started.
*/
static inline void _display_counts(baby_kicks_state_t *state) {
if (!_is_running(state)) {
watch_display_text(WATCH_POSITION_BOTTOM, "baby ");
watch_clear_colon();
} else {
char buf[9];
snprintf(
buf,
sizeof(buf),
"%2d%4d",
state->stretch_count,
state->movement_count
);
watch_display_text(WATCH_POSITION_BOTTOM, buf);
watch_set_colon();
}
}
/** @brief Updates the display with the number of minutes since the
* timer was started.
* @details If more than `BABY_KICKS_TIMEOUT` minutes have elapsed,
* then it displays "TO".
*/
static void _display_elapsed_minutes(baby_kicks_state_t *state) {
if (!_is_running(state)) {
watch_display_text(WATCH_POSITION_TOP_LEFT, " ");
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
} else if (_has_timed_out(state)) {
watch_display_text(WATCH_POSITION_TOP_LEFT, "TO");
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
} else {
/* NOTE We display the elapsed minutes in two parts. This is
* because on the classic LCD, neither the "weekday digits" nor
* the "day digits" position is suitable to display the elapsed
* minutes:
*
* - The classic LCD cannot display 2, 4, 5, 6, or 9 as the last
* digit in the "weekday digits" position.
* - It cannot display any number greater than 3 as the first
* digit in the "day digits" position.
*
* As a workaround, we split the elapsed minutes into 30-minute
* "laps." The elapsed minutes in the current "lap" are shown
* in the "day digits" position. This is any number between 0
* and 29. The elapsed minutes in past "laps" are shown in the
* "weekday digits" position. This is either nothing, 30, 60,
* or 90.
*
* The sum of the numbers shown in the two positions is equal to
* the total elapsed minutes.
*/
char buf[5];
uint32_t elapsed_minutes = _elapsed_minutes(state);
uint8_t multiple = elapsed_minutes / 30;
uint8_t remainder = elapsed_minutes % 30;
if (multiple == 0) {
watch_display_text(WATCH_POSITION_TOP_LEFT, " ");
} else {
snprintf(buf, sizeof(buf), "%2d", multiple * 30);
watch_display_text(WATCH_POSITION_TOP_LEFT, buf);
}
snprintf(buf, sizeof(buf), "%2d", remainder);
watch_display_text(WATCH_POSITION_TOP_RIGHT, buf);
}
}
static void _update_display(baby_kicks_state_t *state) {
_display_counts(state);
_display_elapsed_minutes(state);
}
static inline void _start_sleep_face() {
if (!watch_sleep_animation_is_running()) {
watch_display_text(WATCH_POSITION_TOP_LEFT, " ");
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
watch_display_text(WATCH_POSITION_BOTTOM, "baby ");
watch_clear_colon();
watch_start_sleep_animation(500);
}
}
static inline void _stop_sleep_face() {
if (watch_sleep_animation_is_running()) {
watch_stop_sleep_animation();
}
}
void baby_kicks_face_setup(uint8_t watch_face_index,
void **context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(baby_kicks_state_t));
_reset(*context_ptr);
}
}
void baby_kicks_face_activate(void *context) {
(void) context;
_stop_sleep_face();
}
void baby_kicks_face_resign(void *context) {
baby_kicks_state_t *state = (baby_kicks_state_t *)context;
state->currently_displayed = false;
}
bool baby_kicks_face_loop(movement_event_t event, void *context) {
baby_kicks_state_t *state = (baby_kicks_state_t *)context;
switch (event.event_type) {
case EVENT_ACTIVATE:
state->currently_displayed = true;
_update_display_mode(state);
_update_display(state);
break;
case EVENT_ALARM_BUTTON_UP: /* Start or increment. */
/* Update `state->mode` in case we have a running counter
* that has just timed out. */
_update_display_mode(state);
switch (state->mode) {
case BABY_KICKS_MODE_SPLASH:
_start(state);
_update_display_mode(state);
_update_display(state);
_play_button_sound_if_beep_is_on();
break;
case BABY_KICKS_MODE_ACTIVE:
_increment_counts(state);
_update_display(state);
_play_successful_increment_sound_if_beep_is_on();
break;
case BABY_KICKS_MODE_TIMED_OUT:
_play_failure_sound_if_beep_is_on();
break;
case BABY_KICKS_MODE_LE_MODE: /* fallthrough */
default:
break;
}
break;
case EVENT_ALARM_LONG_PRESS: /* Undo. */
_update_display_mode(state);
switch (state->mode) {
case BABY_KICKS_MODE_ACTIVE:
if (!_successfully_undo(state)) {
_play_failure_sound_if_beep_is_on();
} else {
_update_display(state);
_play_successful_decrement_sound_if_beep_is_on();
}
break;
case BABY_KICKS_MODE_SPLASH: /* fallthrough */
case BABY_KICKS_MODE_TIMED_OUT:
_play_failure_sound_if_beep_is_on();
break;
case BABY_KICKS_MODE_LE_MODE: /* fallthrough */
default:
break;
}
break;
case EVENT_MODE_LONG_PRESS: /* Reset. */
_update_display_mode(state);
switch (state->mode) {
case BABY_KICKS_MODE_ACTIVE: /* fallthrough */
case BABY_KICKS_MODE_TIMED_OUT:
_reset(state);
/* This shows the splash screen because `_reset`
* sets `state->mode` to `BABY_KICKS_MODE_SPLASH`.
*/
_update_display(state);
_play_button_sound_if_beep_is_on();
break;
case BABY_KICKS_MODE_SPLASH:
_play_failure_sound_if_beep_is_on();
break;
case BABY_KICKS_MODE_LE_MODE: /* fallthrough */
default:
break;
}
break;
case EVENT_BACKGROUND_TASK: /* Update minute display. */
_update_display_mode(state);
switch (state->mode) {
case BABY_KICKS_MODE_ACTIVE: /* fallthrough */
case BABY_KICKS_MODE_TIMED_OUT:
if (state->currently_displayed) {
_display_elapsed_minutes(state);
}
break;
case BABY_KICKS_MODE_LE_MODE: /* fallthrough */
case BABY_KICKS_MODE_SPLASH: /* fallthrough */
default:
break;
}
break;
case EVENT_LOW_ENERGY_UPDATE:
_start_sleep_face();
break;
default:
movement_default_loop_handler(event);
break;
}
_clear_now(state);
return true;
}
movement_watch_face_advisory_t baby_kicks_face_advise(void *context) {
movement_watch_face_advisory_t retval = { 0 };
baby_kicks_state_t *state = (baby_kicks_state_t *)context;
retval.wants_background_task =
state->mode == BABY_KICKS_MODE_ACTIVE;
return retval;
}

View File

@@ -0,0 +1,132 @@
/*
* MIT License
*
* Copyright (c) 2025 Gábor Nyéki
*
* 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.
*/
#pragma once
/*
* Baby kicks face
*
* Count the movements of your in-utero baby.
*
* Background:
*
* This practice is recommended particularly in the third trimester
* (from week 28 onwards). The exact recommendations vary as to how to
* count the baby's movements. Some recommend drawing a chart with the
* number of "kicks" within a 12-hour period:
*
* - https://en.wikipedia.org/wiki/Kick_chart
*
* Others recommend measuring the time that it takes for the baby to
* "kick" 10 times:
*
* - https://my.clevelandclinic.org/health/articles/23497-kick-counts
* - https://healthy.kaiserpermanente.org/health-wellness/health-encyclopedia/he.pregnancy-kick-counts.aa107042
*
* (Of course, not every movement that the baby makes is a kick, and we
* are interested in all movements, not only kicks.)
*
* This watch face follows the latter set of recommendations. When you
* start the counter, it measures the number of elapsed minutes, and it
* tracks the number of movements as you increment the counter. Since
* some consecutive movements made by the baby are actually part of a
* longer maneuver, the watch face also displays the number of
* one-minute stretches in which the baby moved at least once.
*
* Usage:
*
* - ALARM button, short press:
* * start the counter if it isn't running
* * increment the count otherwise
* - ALARM button, long press: undo the last count
* - MODE button, long press: reset the count to zero
*
* The watch face displays two numbers in the "clock digits" positions:
*
* 1. Count of movements (in the "second" and "minute" positions).
* 2. Count of one-minute stretches in which at least one movement
* occurred (in the "hour" position).
*
* The number of elapsed minutes, up to and including 29, is shown in
* the "day digits" position. Due to the limitations of the classic LCD
* display, completed 30-minute intervals are shown in the "weekday
* digits" position. The total number of elapsed minutes is the sum of
* these two numbers.
*
* The watch face times out after 99 minutes, since it cannot display
* more than 99 one-minute stretches in the "hour" position. When this
* happens, the "weekday digits" position shows "TO".
*/
#include "movement.h"
typedef enum {
BABY_KICKS_MODE_SPLASH = 0,
BABY_KICKS_MODE_ACTIVE,
BABY_KICKS_MODE_TIMED_OUT,
BABY_KICKS_MODE_LE_MODE,
} baby_kicks_mode_t;
/* Stop counting after 99 minutes. The classic LCD cannot display any
* larger number in the "weekday digits" position. */
#define BABY_KICKS_TIMEOUT 99
/* Ring buffer to store and allow undoing up to 10 movements. */
typedef struct {
/* For each movement in the undo buffer, this array stores the value
* of `state->stretch_count` right before the movement was
* recorded. This is used for decrementing `state->stretch_count`
* as part of the undo operation if necessary. */
uint8_t stretches[10];
/* Index of the next available slot in `.stretches`. */
uint8_t head;
} baby_kicks_undo_buffer_t;
typedef struct {
bool currently_displayed;
baby_kicks_mode_t mode;
watch_date_time_t now;
uint32_t start;
uint32_t latest_stretch_start;
uint8_t stretch_count; /* Between 0 and `BABY_KICKS_TIMEOUT`. */
uint16_t movement_count; /* Between 0 and 9999. */
baby_kicks_undo_buffer_t undo_buffer;
} baby_kicks_state_t;
void baby_kicks_face_setup(uint8_t watch_face_index, void **context_ptr);
void baby_kicks_face_activate(void *context);
bool baby_kicks_face_loop(movement_event_t event, void *context);
void baby_kicks_face_resign(void *context);
movement_watch_face_advisory_t baby_kicks_face_advise(void *context);
#define baby_kicks_face ((const watch_face_t) { \
baby_kicks_face_setup, \
baby_kicks_face_activate, \
baby_kicks_face_loop, \
baby_kicks_face_resign, \
baby_kicks_face_advise, \
})

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

@@ -0,0 +1,153 @@
/*
* MIT License
*
* Copyright (c) 2022 Shogo Okamoto
*
* 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 "counter_face.h"
#include "watch.h"
#include "watch_utility.h"
#include "watch_common_display.h"
static inline bool lcd_is_custom(void) {
return watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM;
}
void counter_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(counter_state_t));
memset(*context_ptr, 0, sizeof(counter_state_t));
counter_state_t *state = (counter_state_t *)*context_ptr;
state->beep_on = true;
}
}
void counter_face_activate(void *context) {
counter_state_t *state = (counter_state_t *)context;
if (state->beep_on) {
watch_set_indicator(WATCH_INDICATOR_SIGNAL);
}
}
bool counter_face_loop(movement_event_t event, void *context) {
counter_state_t *state = (counter_state_t *)context;
switch (event.event_type) {
case EVENT_ALARM_BUTTON_UP:
watch_buzzer_abort_sequence(); //abort running buzzer sequence when counting fast
state->counter_idx++; // increment counter index
if (state->counter_idx>99) { //0-99
state->counter_idx=0;//reset counter index
}
print_counter(state);
if (state->beep_on) {
beep_counter(state);
}
break;
case EVENT_LIGHT_LONG_PRESS:
watch_buzzer_abort_sequence();
state->beep_on = !state->beep_on;
if (state->beep_on) {
watch_set_indicator(WATCH_INDICATOR_SIGNAL);
} else {
watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
}
break;
case EVENT_ALARM_LONG_PRESS:
state->counter_idx=0; // reset counter index
print_counter(state);
break;
case EVENT_ACTIVATE:
print_counter(state);
break;
case EVENT_TIMEOUT:
// ignore timeout
break;
default:
movement_default_loop_handler(event);
break;
}
return true;
}
// beep counter index times
void beep_counter(counter_state_t *state) {
int low_count = state->counter_idx/5;
int high_count = state->counter_idx - low_count * 5;
static int8_t sound_seq[15];
memset(sound_seq, 0, 15);
int i = 0;
if (low_count > 0) {
sound_seq[i] = BUZZER_NOTE_A6;
i++;
sound_seq[i] = 3;
i++;
sound_seq[i] = BUZZER_NOTE_REST;
i++;
sound_seq[i] = 6;
i++;
if (low_count > 1) {
sound_seq[i] = -2;
i++;
sound_seq[i] = low_count-1;
i++;
}
sound_seq[i] = BUZZER_NOTE_REST;
i++;
sound_seq[i] = 6;
i++;
}
if (high_count > 0) {
sound_seq[i] = BUZZER_NOTE_B6;
i++;
sound_seq[i] = 3;
i++;
sound_seq[i] = BUZZER_NOTE_REST;
i++;
sound_seq[i] = 6;
i++;
}
if (high_count > 1) {
sound_seq[i] = -2;
i++;
sound_seq[i] = high_count-1;
}
watch_buzzer_play_sequence((int8_t *)sound_seq, NULL);
}
// print counter index at the center of display.
void print_counter(counter_state_t *state) {
char buf[14];
watch_display_text_with_fallback(WATCH_POSITION_TOP, "COUNT", "CO");
sprintf(buf, " %02d", state->counter_idx); // center of LCD display
watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf);
}
void counter_face_resign(void *context) {
(void) context;
}

View File

@@ -0,0 +1,63 @@
/*
* MIT License
*
* Copyright (c) 2022 Shogo Okamoto
*
* 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 COUNTER_FACE_H_
#define COUNTER_FACE_H_
/*
* COUNTER face
*
* Counter face is designed to count the number of running laps during exercises.
*
* Usage:
* Short-press ALARM to increment the counter (loops at 99)
* Long-press ALARM to reset the counter.
* Long-press LIGHT to toggle sound.
*/
#include "movement.h"
typedef struct {
uint8_t counter_idx;
bool beep_on;
} counter_state_t;
void counter_face_setup(uint8_t watch_face_index, void ** context_ptr);
void counter_face_activate(void *context);
bool counter_face_loop(movement_event_t event, void *context);
void counter_face_resign(void *context);
void print_counter(counter_state_t *state);
void beep_counter(counter_state_t *state);
#define counter_face ((const watch_face_t){ \
counter_face_setup, \
counter_face_activate, \
counter_face_loop, \
counter_face_resign, \
NULL, \
})
#endif // COUNTER_FACE_H_

View File

@@ -0,0 +1,637 @@
/*
* MIT License
*
* Copyright (c) 2023-2025 Konrad Rieck
*
* 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.
*/
/*
* # Deadline Face
*
* This is a watch face for tracking deadlines. It draws inspiration from
* other watch faces of the project but focuses on keeping track of
* deadlines. You can enter and monitor up to four different deadlines by
* providing their respective date and time. The face has two modes:
* *running mode* and *settings mode*.
*
* ## Running Mode
*
* When the watch face is activated, it defaults to running mode. The top
* right corner shows the current deadline number, and the main display
* presents the time left until the deadline. The format of the display
* varies depending on the remaining time.
*
* - When less than a day is left, the display shows the remaining hours,
* minutes, and seconds in the form `HH:MM:SS`.
*
* - When less than a month is left, the display shows the remaining days
* and hours in the form `DD:HH` with the unit `dy` for days.
*
* - When less than a year is left, the display shows the remaining months
* and days in the form `MM:DD` with the unit `mo` for months.
*
* - When more than a year is left, the years and months are displayed in
* the form `YY:MM` with the unit `yr` for years.
*
* - When a deadline has passed in the last 24 hours, the display shows
* `over` to indicate that the deadline has just recently been reached.
*
* - When no deadline is set for a particular slot, or if a deadline has
* already passed by more than 24 hours, `--:--` is displayed.
*
* The user can navigate in running mode using the following buttons:
*
* - The *alarm button* moves the next deadline. There are currently four
* slots available for deadlines. When the last slot has been reached,
* pressing the button moves to the first slot.
*
* - A *long press* on the *alarm button* activates settings mode and
* enables configuring the currently selected deadline.
*
* - A *long press* on the *light button* activates a deadline alarm. The
* bell icon is displayed, and the alarm will ring upon reaching any of
* the deadlines set. It is important to note that the watch will not
* enter low-energy sleep mode while the alarm is enabled.
*
*
* ## Settings Mode
*
* In settings mode, the currently selected slot for a deadline can be
* configured by providing the date and the time. Like running mode, the
* top right corner of the display indicates the current deadline number.
* The main display shows the date and, on the next page, the time to be
* configured.
*
* The user can use the following buttons in settings mode.
*
* - The *light button* navigates through the different date and time
* settings, going from year, month, day, hour, to minute. The selected
* position is blinking.
*
* - A *long press* on the light button resets the date and time to the next
* day at midnight. This is the default deadline.
*
* - The *alarm button* increments the currently selected position. A *long
* press* on the *alarm button* changes the value faster.
*
* - The *mode button* exists setting mode and returns to *running mode*.
* Here the selected deadline slot can be changed.
*
*/
#include <stdlib.h>
#include <string.h>
#include "deadline_face.h"
#include "watch.h"
#include "watch_utility.h"
/* Beep types */
typedef enum {
BEEP_BUTTON,
BEEP_ENABLE,
BEEP_DISABLE
} beep_type_t;
#define SETTINGS_NUM (5)
const char settings_titles[SETTINGS_NUM][6] = { "Year ", "Month", "Day ", "Hour ", "Minut" };
const char settings_fallback_titles[SETTINGS_NUM][3] = { "YR", "MO", "DA", "HR", "M1" };
const char *running_title = "DUE";
const char *running_fallback_title = "DL";
/* Local functions */
static void _deadline_running_init(deadline_state_t * state);
static bool _deadline_running_loop(movement_event_t event, void *context);
static void _deadline_running_display(movement_event_t event, deadline_state_t * state);
static void _deadline_settings_init(deadline_state_t * state);
static bool _deadline_settings_loop(movement_event_t event, void *context);
static void _deadline_settings_display(movement_event_t event, deadline_state_t * state,
watch_date_time_t date);
/* Check for leap year */
static inline bool _is_leap(int16_t y)
{
y += 1900;
return !(y % 4) && ((y % 100) || !(y % 400));
}
/* Modulo function */
static inline unsigned int _mod(int a, int b)
{
int r = a % b;
return r < 0 ? r + b : r;
}
/* Return days in month */
static inline int _days_in_month(int16_t month, int16_t year)
{
uint8_t days[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
month = _mod(month - 1, 12);
if (month == 1 && _is_leap(year)) {
return days[month] + 1;
} else {
return days[month];
}
}
/* Play beep sound based on type */
static inline void _beep(beep_type_t beep_type)
{
if (!movement_button_should_sound())
return;
switch (beep_type) {
case BEEP_BUTTON:
watch_buzzer_play_note_with_volume(BUZZER_NOTE_C7, 50, movement_button_volume());
break;
case BEEP_ENABLE:
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_with_volume(BUZZER_NOTE_C8, 50, movement_button_volume());
break;
case BEEP_DISABLE:
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_with_volume(BUZZER_NOTE_G7, 50, movement_button_volume());
break;
}
}
/* Change tick frequency */
static inline void _change_tick_freq(uint8_t freq, deadline_state_t *state)
{
if (state->tick_freq != freq) {
movement_request_tick_frequency(freq);
state->tick_freq = freq;
}
}
/* Determine index of closest deadline */
static uint8_t _closest_deadline(deadline_state_t *state)
{
watch_date_time_t now = movement_get_local_date_time();
uint32_t now_ts = watch_utility_date_time_to_unix_time(now, 0);
uint32_t min_ts = UINT32_MAX;
uint8_t min_index = 0;
for (uint8_t i = 0; i < DEADLINE_FACE_DATES; i++) {
/* Skip expired deadlines and those further in the future than current minimum */
if (state->deadlines[i] < now_ts || state->deadlines[i] > min_ts) {
continue;
}
min_ts = state->deadlines[i];
min_index = i;
}
return min_index;
}
/* Play background alarm */
static void _background_alarm_play(deadline_state_t *state)
{
movement_play_alarm();
movement_move_to_face(state->face_idx);
}
/* Reset deadline to tomorrow */
static inline void _reset_deadline(deadline_state_t *state)
{
/* Get current time and reset hours/minutes/seconds */
watch_date_time_t date_time = movement_get_local_date_time();
date_time.unit.second = 0;
date_time.unit.minute = 0;
date_time.unit.hour = 0;
/* Add 24 hours to obtain first second of tomorrow */
uint32_t ts = watch_utility_date_time_to_unix_time(date_time, 0);
ts += 24 * 60 * 60;
state->deadlines[state->current_index] = ts;
}
/* Calculate the naive difference between deadline and current time */
static void _calculate_time_remaining(watch_date_time_t dl, watch_date_time_t now, int16_t *units)
{
units[0] = dl.unit.second - now.unit.second;
units[1] = dl.unit.minute - now.unit.minute;
units[2] = dl.unit.hour - now.unit.hour;
units[3] = dl.unit.day - now.unit.day;
units[4] = dl.unit.month - now.unit.month;
units[5] = dl.unit.year - now.unit.year;
}
/* Format the remaining time for display */
static void _format_time_remaining(int16_t *units, char *buffer, size_t buffer_size)
{
const int16_t years = units[5];
const int16_t months = units[4];
const int16_t days = units[3];
const int16_t hours = units[2];
const int16_t minutes = units[1];
const int16_t seconds = units[0];
if (years > 0) {
/* years:months */
snprintf(buffer, buffer_size, "%02d%02dYR", years % 100, months % 12);
} else if (months > 0) {
/* months:days */
snprintf(buffer, buffer_size, "%02d%02dMO", (years * 12 + months) % 100, days % 32);
} else if (days > 0) {
/* days:hours */
snprintf(buffer, buffer_size, "%02d%02ddY", days % 32, hours % 24);
} else {
/* hours:minutes:seconds */
snprintf(buffer, buffer_size, "%02d%02d%02d", hours % 24, minutes % 60, seconds % 60);
}
}
/* Correct the naive time difference calculation */
static void _correct_time_difference(int16_t *units, watch_date_time_t deadline)
{
const uint8_t range[] = { 60, 60, 24, 30, 12, 0 };
for (uint8_t i = 0; i < 6; i++) {
if (units[i] < 0) {
/* Correct remaining units */
if (i == 3) {
units[i] += _days_in_month(deadline.unit.month - 1, deadline.unit.year);
} else {
units[i] += range[i];
}
/* Carry over change to next unit if non-zero */
if (i < 5 && units[i + 1] != 0) {
units[i + 1] -= 1;
}
}
}
}
/* Increment date in settings mode. Function taken from `set_time_face.c` */
static void _increment_date(deadline_state_t *state, watch_date_time_t date_time)
{
const uint8_t days_in_month[12] = { 31, 28, 31, 30, 31, 30, 30, 31, 30, 31, 30, 31 };
switch (state->current_page) {
case 0:
/* Only 10 years covered. Fix this sometime next decade */
date_time.unit.year = ((date_time.unit.year % 10) + 1);
break;
case 1:
date_time.unit.month = (date_time.unit.month % 12) + 1;
break;
case 2:
date_time.unit.day = date_time.unit.day + 1;
/* Check for leap years */
int8_t days = days_in_month[date_time.unit.month - 1];
if (date_time.unit.month == 2 && _is_leap(date_time.unit.year))
days++;
if (date_time.unit.day > days)
date_time.unit.day = 1;
break;
case 3:
date_time.unit.hour = (date_time.unit.hour + 1) % 24;
break;
case 4:
date_time.unit.minute = (date_time.unit.minute + 1) % 60;
break;
}
uint32_t ts = watch_utility_date_time_to_unix_time(date_time, 0);
state->deadlines[state->current_index] = ts;
}
/* Update display in running mode */
static void _deadline_running_display(movement_event_t event, deadline_state_t *state)
{
(void) event;
/* Seconds, minutes, hours, days, months, years */
int16_t units[] = { 0, 0, 0, 0, 0, 0 };
char buf[16];
/* Top row with face name and deadline index */
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, running_title, running_fallback_title);
sprintf(buf, "%2d", state->current_index + 1);
watch_display_text_with_fallback(WATCH_POSITION_TOP_RIGHT, buf, buf);
/* Display indicators */
if (state->alarm_enabled)
watch_set_indicator(WATCH_INDICATOR_BELL);
else
watch_clear_indicator(WATCH_INDICATOR_BELL);
watch_date_time_t now = movement_get_local_date_time();
uint32_t now_ts = watch_utility_date_time_to_unix_time(now, 0);
/* Deadline expired */
if (state->deadlines[state->current_index] < now_ts) {
if (state->deadlines[state->current_index] + 24 * 60 * 60 > now_ts)
sprintf(buf, "OVER ");
else
sprintf(buf, "---- ");
watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf);
return;
}
/* Get date time structs */
uint32_t dl_ts = state->deadlines[state->current_index];
watch_date_time_t deadline = watch_utility_date_time_from_unix_time(dl_ts, 0);
/* Calculate and format time remaining */
_calculate_time_remaining(deadline, now, units);
_correct_time_difference(units, deadline);
_format_time_remaining(units, buf, sizeof(buf));
watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf);
}
/* Init running mode */
static void _deadline_running_init(deadline_state_t *state)
{
(void) state;
watch_clear_indicator(WATCH_INDICATOR_24H);
watch_clear_indicator(WATCH_INDICATOR_PM);
watch_set_colon();
/* Ensure 1Hz updates only */
_change_tick_freq(1, state);
}
/* Loop of running mode */
static bool _deadline_running_loop(movement_event_t event, void *context)
{
deadline_state_t *state = (deadline_state_t *) context;
if (event.event_type != EVENT_BACKGROUND_TASK)
_deadline_running_display(event, state);
switch (event.event_type) {
case EVENT_ALARM_BUTTON_UP:
_beep(BEEP_BUTTON);
state->current_index = (state->current_index + 1) % DEADLINE_FACE_DATES;
_deadline_running_display(event, state);
break;
case EVENT_ALARM_LONG_PRESS:
_beep(BEEP_ENABLE);
_deadline_settings_init(state);
state->mode = DEADLINE_SETTINGS;
break;
case EVENT_MODE_BUTTON_UP:
movement_move_to_next_face();
return false;
case EVENT_LIGHT_BUTTON_DOWN:
break;
case EVENT_LIGHT_LONG_PRESS:
_beep(BEEP_BUTTON);
state->alarm_enabled = !state->alarm_enabled;
_deadline_running_display(event, state);
break;
case EVENT_TIMEOUT:
movement_move_to_face(0);
break;
case EVENT_BACKGROUND_TASK:
_background_alarm_play(state);
break;
case EVENT_LOW_ENERGY_UPDATE:
break;
default:
return movement_default_loop_handler(event);
}
return true;
}
/* Update display in settings mode */
static void _deadline_settings_display(movement_event_t event,
deadline_state_t *state, watch_date_time_t date_time)
{
char buf[7];
watch_display_text_with_fallback(WATCH_POSITION_TOP, settings_titles[state->current_page],
settings_fallback_titles[state->current_page]);
if (state->current_page > 2) {
/* Time settings */
watch_set_colon();
if (movement_clock_mode_24h() == MOVEMENT_CLOCK_MODE_24H) {
/* 24h format */
watch_set_indicator(WATCH_INDICATOR_24H);
sprintf(buf, "%2d%02d ", date_time.unit.hour, date_time.unit.minute);
} else {
/* 12h format */
if (date_time.unit.hour < 12)
watch_clear_indicator(WATCH_INDICATOR_PM);
else
watch_set_indicator(WATCH_INDICATOR_PM);
uint8_t hour = date_time.unit.hour % 12;
sprintf(buf, "%2d%02d ", hour ? hour : 12, date_time.unit.minute);
}
} else {
/* Date settings */
watch_clear_colon();
watch_clear_indicator(WATCH_INDICATOR_24H);
watch_clear_indicator(WATCH_INDICATOR_PM);
sprintf(buf, "%2d%02d%02d",
date_time.unit.year + 20, date_time.unit.month, date_time.unit.day);
}
/* Blink up the parameter we are setting */
if (event.subsecond % 2) {
switch (state->current_page) {
case 0:
case 3:
buf[0] = buf[1] = ' ';
break;
case 1:
case 4:
buf[2] = buf[3] = ' ';
break;
case 2:
buf[4] = buf[5] = ' ';
break;
}
}
watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf);
}
/* Init setting mode */
static void _deadline_settings_init(deadline_state_t *state)
{
state->current_page = 0;
/* Init fresh deadline to next day */
if (state->deadlines[state->current_index] == 0) {
_reset_deadline(state);
}
/* Ensure 1Hz updates only */
_change_tick_freq(1, state);
}
/* Loop of setting mode */
static bool _deadline_settings_loop(movement_event_t event, void *context)
{
deadline_state_t *state = (deadline_state_t *) context;
watch_date_time_t date_time;
date_time = watch_utility_date_time_from_unix_time(state->deadlines[state->current_index], 0);
if (event.event_type != EVENT_BACKGROUND_TASK)
_deadline_settings_display(event, state, date_time);
switch (event.event_type) {
case EVENT_TICK:
if (state->tick_freq == 8) {
if (HAL_GPIO_BTN_ALARM_read()) {
_increment_date(state, date_time);
_deadline_settings_display(event, state, date_time);
} else {
_change_tick_freq(4, state);
}
}
break;
case EVENT_ALARM_LONG_PRESS:
_change_tick_freq(8, state);
break;
case EVENT_ALARM_LONG_UP:
_change_tick_freq(4, state);
break;
case EVENT_LIGHT_LONG_PRESS:
_beep(BEEP_BUTTON);
_reset_deadline(state);
break;
case EVENT_LIGHT_BUTTON_DOWN:
break;
case EVENT_LIGHT_BUTTON_UP:
state->current_page = (state->current_page + 1) % SETTINGS_NUM;
_deadline_settings_display(event, state, date_time);
break;
case EVENT_ALARM_BUTTON_UP:
_change_tick_freq(4, state);
_increment_date(state, date_time);
_deadline_settings_display(event, state, date_time);
break;
case EVENT_TIMEOUT:
_beep(BEEP_BUTTON);
_change_tick_freq(1, state);
state->mode = DEADLINE_RUNNING;
movement_move_to_face(0);
break;
case EVENT_MODE_BUTTON_UP:
_beep(BEEP_DISABLE);
_deadline_running_init(state);
_deadline_running_display(event, state);
state->mode = DEADLINE_RUNNING;
break;
case EVENT_BACKGROUND_TASK:
_background_alarm_play(state);
break;
default:
return movement_default_loop_handler(event);
}
return true;
}
/* Setup face */
void deadline_face_setup(uint8_t watch_face_index, void **context_ptr)
{
(void) watch_face_index;
if (*context_ptr != NULL)
return; /* Skip setup if context available */
/* Allocate state */
*context_ptr = malloc(sizeof(deadline_state_t));
memset(*context_ptr, 0, sizeof(deadline_state_t));
/* Store face index for background tasks */
deadline_state_t *state = (deadline_state_t *) * context_ptr;
state->face_idx = watch_face_index;
}
/* Activate face */
void deadline_face_activate(void *context)
{
deadline_state_t *state = (deadline_state_t *) context;
/* Set display options */
_deadline_running_init(state);
state->mode = DEADLINE_RUNNING;
state->current_index = _closest_deadline(state);
}
/* Loop face */
bool deadline_face_loop(movement_event_t event, void *context)
{
deadline_state_t *state = (deadline_state_t *) context;
switch (state->mode) {
case DEADLINE_SETTINGS:
_deadline_settings_loop(event, context);
break;
default:
case DEADLINE_RUNNING:
_deadline_running_loop(event, context);
break;
}
return true;
}
/* Resign face */
void deadline_face_resign(void *context)
{
(void) context;
}
/* Background task */
movement_watch_face_advisory_t deadline_face_advise(void *context)
{
deadline_state_t *state = (deadline_state_t *) context;
movement_watch_face_advisory_t retval = { 0 };
if (!state->alarm_enabled)
return retval;
/* Determine closest deadline */
watch_date_time_t now = movement_get_local_date_time();
uint32_t now_ts = watch_utility_date_time_to_unix_time(now, 0);
uint32_t next_ts = state->deadlines[_closest_deadline(state)];
/* No active deadline */
if (next_ts < now_ts)
return retval;
/* No deadline within next 60 seconds */
if (next_ts >= now_ts + 60)
return retval;
/* Deadline within next minute. Let's set up an alarm */
retval.wants_background_task = true;
return retval;
}

View File

@@ -0,0 +1,65 @@
/*
* MIT License
*
* Copyright (c) 2023-2025 Konrad Rieck
*
* 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 DEADLINE_FACE_H_
#define DEADLINE_FACE_H_
#include "movement.h"
/* Modes of face */
typedef enum {
DEADLINE_RUNNING = 0,
DEADLINE_SETTINGS
} deadline_mode_t;
/* Number of deadline dates */
#define DEADLINE_FACE_DATES (4)
/* Deadline configuration */
typedef struct {
deadline_mode_t mode:1;
uint8_t current_page:3;
uint8_t current_index:2;
uint8_t alarm_enabled:1;
uint8_t tick_freq;
uint8_t face_idx;
uint32_t deadlines[DEADLINE_FACE_DATES];
} deadline_state_t;
void deadline_face_setup(uint8_t watch_face_index, void **context_ptr);
void deadline_face_activate(void *context);
bool deadline_face_loop(movement_event_t event, void *context);
void deadline_face_resign(void *context);
movement_watch_face_advisory_t deadline_face_advise(void *context);
#define deadline_face ((const watch_face_t){ \
deadline_face_setup, \
deadline_face_activate, \
deadline_face_loop, \
deadline_face_resign, \
deadline_face_advise, \
})
#endif // DEADLINE_FACE_H_

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

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

View 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.
}

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

View File

@@ -0,0 +1,681 @@
/*
* MIT License
*
* Copyright (c) 2022 Andreas Nebinger
*
* 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 "interval_face.h"
#include "watch.h"
#include "watch_utility.h"
typedef enum {
interval_setting_0_timer_idx,
interval_setting_1_clear_yn,
interval_setting_2_warmup_minutes,
interval_setting_3_warmup_seconds,
interval_setting_4_work_minutes,
interval_setting_5_work_seconds,
interval_setting_6_work_rounds,
interval_setting_7_break_minutes,
interval_setting_8_break_seconds,
interval_setting_9_full_rounds,
interval_setting_10_cooldown_minutes,
interval_setting_11_cooldown_seconds,
interval_setting_max
} interval_setting_idx_t;
#define INTERVAL_FACE_STATE_DEFAULT "IT" // Interval Timer
#define INTERVAL_FACE_STATE_DEFAULT_CD "INT"
#define INTERVAL_FACE_STATE_WARMUP "PR" // PRepare / warm up
#define INTERVAL_FACE_STATE_WARMUP_CD "PRE"
#define INTERVAL_FACE_STATE_WORK "WO" // WOrk
#define INTERVAL_FACE_STATE_WORK_CD "WOR"
#define INTERVAL_FACE_STATE_BREAK "BR" // BReak
#define INTERVAL_FACE_STATE_BREAK_CD "BRK"
#define INTERVAL_FACE_STATE_COOLDOWN "CD" // CoolDown
#define INTERVAL_FACE_STATE_COOLDOWN_CD "CLD"
// Define some default timer settings. Each timer is described in an array like this:
// 1. warm-up seconds,
// 2. work time (seconds/minutes)
// 3. break time (seconds/minutes)
// 4. full rounds (0 = no limit)
// 5. cooldown seconds
// Work time and break time: positive number = seconds, negative number = minutes
static const int8_t _default_timers[6][5] = {{0, 40, 20, 0, 0},
{0, 45, 15, 0, 0},
{10, 20, 10, 8, 10},
{0, 35, 0, 0, 0},
{0, -25, -5, 0, 0},
{0, -20, -5, 0, 0}};
static const uint8_t _intro_segdata[4][2] = {{1, 8}, {0, 8}, {0, 7}, {1, 7}};
static const uint8_t _intro_segdata_cd[4][2] = {{1, 8}, {1, 9}, {0, 9}, {0, 8}};
static const uint8_t _setting_page_idx[] = {1, 0, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4};
static const int8_t _sound_seq_warmup[] = {BUZZER_NOTE_F6, 8, BUZZER_NOTE_REST, 1, -2, 3, 0};
static const int8_t _sound_seq_work[] = {BUZZER_NOTE_F6, 8, BUZZER_NOTE_REST, 1, -2, 2, BUZZER_NOTE_C7, 24, 0};
static const int8_t _sound_seq_break[] = {BUZZER_NOTE_B6, 15, BUZZER_NOTE_REST, 1, -2, 1, BUZZER_NOTE_B6, 16, 0};
static const int8_t _sound_seq_cooldown[] = {BUZZER_NOTE_C7, 15, BUZZER_NOTE_REST, 1, -2, 1, BUZZER_NOTE_C7, 24, 0};
static const int8_t _sound_seq_finish[] = {BUZZER_NOTE_C7, 6, BUZZER_NOTE_E7, 6, BUZZER_NOTE_G7, 6, BUZZER_NOTE_C8, 18, 0};
static interval_setting_idx_t _setting_idx;
static int8_t _ticks;
static bool _erase_timer_flag;
static uint32_t _target_ts;
static uint32_t _now_ts;
static uint32_t _paused_ts;
static uint8_t _timer_work_round;
static uint8_t _timer_full_round;
static uint8_t _timer_run_state;
static inline void _inc_uint8(uint8_t *value, uint8_t step, uint8_t max) {
*value += step;
if (*value >= max) *value = 0;
}
static uint32_t _get_now_ts() {
// returns the current date time as unix timestamp
watch_date_time_t now = watch_rtc_get_date_time();
return watch_utility_date_time_to_unix_time(now, 0);
}
static inline void _button_beep() {
// play a beep as confirmation for a button press (if applicable)
if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_C7, 50);
}
static void _timer_write_info(interval_face_state_t *state, char* bottom_row, char state_str[2][4], char* index_str, uint8_t timer_page) {
// fill display string with requested timer information
switch (timer_page) {
case 0:
// clear timer?
sprintf(bottom_row, "CLEARn");
sprintf(state_str[0], "%2s", INTERVAL_FACE_STATE_DEFAULT);
sprintf(state_str[1], "%3s", INTERVAL_FACE_STATE_DEFAULT_CD);
sprintf(index_str, " %1d", state->timer_idx + 1);
if (_erase_timer_flag) bottom_row[5] = 'y';
watch_clear_colon();
break;
case 1:
// warmup time info
sprintf(bottom_row, "%02d%02d ",
state->timer[state->timer_idx].warmup_minutes,
state->timer[state->timer_idx].warmup_seconds);
sprintf(state_str[0], "%2s", INTERVAL_FACE_STATE_WARMUP);
sprintf(state_str[1], "%3s", INTERVAL_FACE_STATE_WARMUP_CD);
sprintf(index_str, " %1d", state->timer_idx + 1);
break;
case 2:
// work interval info
sprintf(bottom_row, "%02d%02d%2d",
state->timer[state->timer_idx].work_minutes,
state->timer[state->timer_idx].work_seconds,
state->timer[state->timer_idx].work_rounds);
sprintf(state_str[0], "%2s", INTERVAL_FACE_STATE_WORK);
sprintf(state_str[1], "%3s", INTERVAL_FACE_STATE_WORK_CD);
sprintf(index_str, " %1d", state->timer_idx + 1);
break;
case 3:
// break interval info
sprintf(bottom_row, "%02d%02d%2d",
state->timer[state->timer_idx].break_minutes,
state->timer[state->timer_idx].break_seconds,
state->timer[state->timer_idx].full_rounds);
if (!state->timer[state->timer_idx].full_rounds) bottom_row[5] = '-';
sprintf(state_str[0], "%2s", INTERVAL_FACE_STATE_BREAK);
sprintf(state_str[1], "%3s", INTERVAL_FACE_STATE_BREAK_CD);
sprintf(index_str, " %1d", state->timer_idx + 1);
break;
case 4:
// cooldown time info
sprintf(bottom_row, "%02d%02d ",
state->timer[state->timer_idx].cooldown_minutes,
state->timer[state->timer_idx].cooldown_seconds);
sprintf(state_str[0], "%2s", INTERVAL_FACE_STATE_COOLDOWN);
sprintf(state_str[1], "%3s", INTERVAL_FACE_STATE_COOLDOWN_CD);
sprintf(index_str, " %1d", state->timer_idx + 1);
break;
default:
break;
}
}
static void _face_draw(interval_face_state_t *state, uint8_t subsecond) {
// draws current face state
if (!state->is_active) return;
char bottom_row[10];
char int_state_str[2][4];
char int_index_str[5];
bottom_row[0] = 0;
uint8_t tmp;
if (state->face_state == interval_state_waiting && _ticks >= 0) {
// play info slideshow for current timer
int8_t ticks = _ticks % 12;
if (ticks == 0) {
if ((state->timer[state->timer_idx].warmup_minutes + state->timer[state->timer_idx].warmup_seconds) == 0) {
// skip warmup info if there is none for this timer
ticks = 3;
_ticks += 3;
}
}
tmp = ticks / 3 + 1;
_timer_write_info(state, bottom_row, int_state_str, int_index_str, tmp);
// don't show '1 round' when displaying workout time to avoid detail overload
if (tmp == 2 && state->timer[state->timer_idx].work_rounds == 1) bottom_row[5] = ' ';
// blink colon
if (subsecond % 2 == 0 && _ticks < 24) watch_clear_colon();
else watch_set_colon();
} else if (state->face_state == interval_state_setting) {
if (_setting_idx == interval_setting_0_timer_idx) {
if ((state->timer[state->timer_idx].warmup_minutes + state->timer[state->timer_idx].warmup_seconds) == 0)
tmp = 1;
else
tmp = 2;
} else {
tmp = _setting_page_idx[_setting_idx];
}
_timer_write_info(state, bottom_row, int_state_str, int_index_str, tmp);
// blink at cursor position
if (subsecond % 2 && _ticks != -2) {
switch (_setting_idx) {
case interval_setting_0_timer_idx:
int_index_str[0] = int_index_str[1] = ' ';
break;
case interval_setting_1_clear_yn:
bottom_row[5] = ' ';
break;
case interval_setting_2_warmup_minutes:
case interval_setting_4_work_minutes:
case interval_setting_7_break_minutes:
case interval_setting_10_cooldown_minutes:
bottom_row[0] = bottom_row[1] = ' ';
break;
case interval_setting_3_warmup_seconds:
case interval_setting_5_work_seconds:
case interval_setting_8_break_seconds:
case interval_setting_11_cooldown_seconds:
bottom_row[2] = bottom_row[3] = ' ';
break;
case interval_setting_6_work_rounds:
case interval_setting_9_full_rounds:
bottom_row[4] = bottom_row[5] = ' ';
break;
default:
break;
}
}
// show lap indicator only when rounds are set
if (_setting_idx == interval_setting_6_work_rounds || _setting_idx == interval_setting_9_full_rounds)
watch_set_indicator(WATCH_INDICATOR_LAP);
else
watch_clear_indicator(WATCH_INDICATOR_LAP);
} else if (state->face_state == interval_state_running || state->face_state == interval_state_pausing) {
tmp = _timer_full_round;
switch (_timer_run_state) {
case 0:
sprintf(int_state_str[0], "%2s", INTERVAL_FACE_STATE_WARMUP);
sprintf(int_state_str[1], "%3s", INTERVAL_FACE_STATE_WARMUP_CD);
break;
case 1:
sprintf(int_state_str[0], "%2s", INTERVAL_FACE_STATE_WORK);
sprintf(int_state_str[1], "%3s", INTERVAL_FACE_STATE_WORK_CD);
if (state->timer[state->timer_idx].work_rounds > 1) tmp = _timer_work_round;
break;
case 2:
sprintf(int_state_str[0], "%2s", INTERVAL_FACE_STATE_BREAK);
sprintf(int_state_str[1], "%3s", INTERVAL_FACE_STATE_BREAK_CD);
break;
case 3:
sprintf(int_state_str[0], "%2s", INTERVAL_FACE_STATE_COOLDOWN);
sprintf(int_state_str[1], "%3s", INTERVAL_FACE_STATE_COOLDOWN_CD);
break;
default:
break;
}
div_t delta;
if (state->face_state == interval_state_pausing) {
// pausing
delta = div(_target_ts - _paused_ts, 60);
// blink the bell icon
if (_now_ts % 2) watch_set_indicator(WATCH_INDICATOR_BELL);
else watch_clear_indicator(WATCH_INDICATOR_BELL);
} else
// running
delta = div(_target_ts - _now_ts, 60);
sprintf(bottom_row, "%02d%02d%2d", delta.quot, delta.rem, tmp + 1);
sprintf(int_index_str, " %1d", state->timer_idx + 1);
}
// write out to lcd
if (int_state_str[0][0]) {
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, int_state_str[1], int_state_str[0]);
watch_display_text_with_fallback(WATCH_POSITION_TOP_RIGHT, int_index_str, int_index_str);
// set the bar for the i-like symbol on position 2
if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) {
watch_set_pixel(2, 9);
} else {
watch_set_pixel(2, 10);
}
// display the rest of the string
watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, bottom_row, bottom_row);
}
}
static void _initiate_setting(interval_face_state_t *state, uint8_t subsecond) {
state->face_state = interval_state_setting;
_setting_idx = interval_setting_0_timer_idx;
_ticks = 0;
_erase_timer_flag = false;
watch_set_colon();
movement_request_tick_frequency(4);
_face_draw(state, subsecond);
}
static void _resume_setting(interval_face_state_t *state, uint8_t subsecond) {
state->face_state = interval_state_waiting;
_ticks = 0;
_face_draw(state, subsecond);
movement_request_tick_frequency(2);
watch_clear_indicator(WATCH_INDICATOR_LAP);
}
static void _abort_quick_ticks() {
if (_ticks == -2) {
_ticks = -1;
movement_request_tick_frequency(4);
}
}
static void _handle_alarm_button(interval_face_state_t *state) {
// handles the alarm button press and alters the corresponding timer settings
switch (_setting_idx) {
case interval_setting_0_timer_idx:
_inc_uint8(&state->timer_idx, 1, INTERVAL_TIMERS);
_erase_timer_flag = false;
break;
case interval_setting_1_clear_yn:
_erase_timer_flag ^= 1;
break;
case interval_setting_2_warmup_minutes:
_inc_uint8(&state->timer[state->timer_idx].warmup_minutes, 1, 60);
break;
case interval_setting_3_warmup_seconds:
_inc_uint8(&state->timer[state->timer_idx].warmup_seconds, 5, 60);
break;
case interval_setting_4_work_minutes:
_inc_uint8(&state->timer[state->timer_idx].work_minutes, 1, 60);
if (state->timer[state->timer_idx].work_rounds == 0) state->timer[state->timer_idx].work_rounds = 1;
break;
case interval_setting_5_work_seconds:
_inc_uint8(&state->timer[state->timer_idx].work_seconds, 5, 60);
if (state->timer[state->timer_idx].work_rounds == 0) state->timer[state->timer_idx].work_rounds = 1;
break;
case interval_setting_6_work_rounds:
_inc_uint8(&state->timer[state->timer_idx].work_rounds, 1, 100);
break;
case interval_setting_7_break_minutes:
_inc_uint8(&state->timer[state->timer_idx].break_minutes, 1, 60);
break;
case interval_setting_8_break_seconds:
_inc_uint8(&state->timer[state->timer_idx].break_seconds, 5, 60);
break;
case interval_setting_9_full_rounds:
_inc_uint8(&state->timer[state->timer_idx].full_rounds, 1, 100);
break;
case interval_setting_10_cooldown_minutes:
_inc_uint8(&state->timer[state->timer_idx].cooldown_minutes, 1, 60);
break;
case interval_setting_11_cooldown_seconds:
_inc_uint8(&state->timer[state->timer_idx].cooldown_seconds, 5, 60);
break;
default:
break;
}
}
static void _set_next_timestamp(interval_face_state_t *state) {
// set next timestamp for the running timer, set background task and pay sound sequence
uint16_t delta = 0;
int8_t *sound_seq;
interval_timer_setting_t timer = state->timer[state->timer_idx];
switch (_timer_run_state) {
case 0:
delta = timer.warmup_minutes * 60 + timer.warmup_seconds;
sound_seq = (int8_t *)_sound_seq_warmup;
break;
case 1:
delta = timer.work_minutes * 60 + timer.work_seconds;
sound_seq = (int8_t *)_sound_seq_work;
break;
case 2:
delta = timer.break_minutes * 60 + timer.break_seconds;
sound_seq = (int8_t *)_sound_seq_break;
break;
case 3:
delta = timer.cooldown_minutes * 60 + timer.cooldown_seconds;
sound_seq = (int8_t *)_sound_seq_cooldown;
break;
default:
sound_seq = NULL;
break;
}
// failsafe
if (delta <= 0) delta = 1;
_target_ts += delta;
// schedule next background task
watch_date_time_t target_dt = watch_utility_date_time_from_unix_time(_target_ts, 0);
movement_schedule_background_task_for_face(state->face_idx, target_dt);
// play sound
watch_buzzer_play_sequence(sound_seq, NULL);
}
static inline bool _is_timer_empty(interval_timer_setting_t *timer) {
// checks if a timer is empty
return (timer->warmup_minutes + timer->warmup_seconds
+ timer->work_minutes + timer->work_seconds
+ timer->break_minutes + timer->break_seconds
+ timer->cooldown_minutes + timer->cooldown_seconds == 0);
}
static void _init_timer_info(interval_face_state_t *state) {
state->face_state = interval_state_waiting;
_ticks = 0;
if (state->is_active) movement_request_tick_frequency(2);
}
static void _abort_running_timer() {
_timer_work_round = _timer_full_round = 0;
_timer_run_state = 0;
movement_cancel_background_task();
watch_clear_indicator(WATCH_INDICATOR_BELL);
watch_buzzer_play_note(BUZZER_NOTE_C8, 100);
}
static void _resume_paused_timer(interval_face_state_t *state) {
// resume paused timer
_now_ts = _get_now_ts();
_target_ts += _now_ts - _paused_ts;
watch_date_time_t target_dt = watch_utility_date_time_from_unix_time(_target_ts, 0);
movement_schedule_background_task_for_face(state->face_idx, target_dt);
state->face_state = interval_state_running;
watch_set_indicator(WATCH_INDICATOR_BELL);
}
void interval_face_setup(uint8_t watch_face_index, void **context_ptr) {
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(interval_face_state_t));
interval_face_state_t *state = (interval_face_state_t *)*context_ptr;
memset(*context_ptr, 0, sizeof(interval_face_state_t));
state->face_idx = watch_face_index;
// somehow the memset above doesn't to the trick. So set the state explicitly
state->face_state = interval_state_waiting;
for (uint8_t i = 0; i < INTERVAL_TIMERS; i++) state->timer[i].work_rounds = 1;
// set up default timers
for (uint8_t i = 0; i < 6; i++) {
state->timer[i].warmup_seconds = _default_timers[i][0];
if (_default_timers[i][1] < 0) state->timer[i].work_minutes = -_default_timers[i][1];
else state->timer[i].work_seconds = _default_timers[i][1];
state->timer[i].work_rounds = 1;
if (_default_timers[i][2] < 0) state->timer[i].break_minutes = -_default_timers[i][2];
else state->timer[i].break_seconds = _default_timers[i][2];
state->timer[i].full_rounds = _default_timers[i][3];
state->timer[i].cooldown_seconds = _default_timers[i][4];
}
}
}
void interval_face_activate(void *context) {
interval_face_state_t *state = (interval_face_state_t *)context;
_erase_timer_flag = false;
state->is_active = true;
if (state->face_state <= interval_state_waiting) {
// initiate the intro loop
state->face_state = interval_state_intro;
_ticks = 0;
movement_request_tick_frequency(8);
} else watch_set_colon();
}
void interval_face_resign(void *context) {
interval_face_state_t *state = (interval_face_state_t *)context;
if (state->face_state <= interval_state_setting) state->face_state = interval_state_waiting;
watch_set_led_off();
movement_request_tick_frequency(1);
state->is_active = false;
}
bool interval_face_loop(movement_event_t event, void *context) {
interval_face_state_t *state = (interval_face_state_t *)context;
interval_timer_setting_t *timer = &state->timer[state->timer_idx];
switch (event.event_type) {
case EVENT_TICK:
if (state->face_state == interval_state_intro) {
// play intro animation so the wearer knows the face
if (_ticks == 4) {
// transition to default view of current interval slot
watch_set_colon();
_init_timer_info(state);
_face_draw(state, event.subsecond);
break;
}
if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) {
watch_set_pixel(_intro_segdata[_ticks][0], _intro_segdata[_ticks][1]);
} else {
watch_set_pixel(_intro_segdata_cd[_ticks][0], _intro_segdata_cd[_ticks][1]);
}
_ticks++;
} else if (state->face_state == interval_state_waiting && _ticks >= 0) {
// play information slideshow for current interval timer
_ticks++;
if ((_ticks % 12 == 9) && (timer->cooldown_minutes + timer->cooldown_seconds == 0)) _ticks += 3;
if (_ticks > 24) _ticks = -1;
else _face_draw(state, event.subsecond);
} else if (state->face_state == interval_state_setting) {
if (_ticks == -2) {
// fast counting
_handle_alarm_button(state);
}
_face_draw(state, event.subsecond);
} else if (state->face_state == interval_state_running || state->face_state == interval_state_pausing) {
_now_ts = _get_now_ts();
_face_draw(state, event.subsecond);
}
break;
case EVENT_ACTIVATE:
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, INTERVAL_FACE_STATE_DEFAULT_CD, INTERVAL_FACE_STATE_DEFAULT);
if (state->face_state) _face_draw(state, event.subsecond);
break;
case EVENT_LIGHT_BUTTON_UP:
if (state->face_state == interval_state_setting) {
if (_setting_idx == interval_setting_0_timer_idx) {
// skip clear page if timer is empty
if (_is_timer_empty(timer)) _setting_idx = interval_setting_1_clear_yn;
} else if (_setting_idx == interval_setting_1_clear_yn) {
watch_set_colon();
if (_erase_timer_flag) {
// clear the current timer
memset((void *)timer, 0, sizeof(interval_timer_setting_t));
// play a short beep as confirmation
watch_buzzer_play_note(BUZZER_NOTE_C8, 70);
}
} else if (_setting_idx == interval_setting_9_full_rounds && !timer->full_rounds) {
// skip cooldown if full rounds are not limited
_setting_idx = interval_setting_11_cooldown_seconds;
}
_setting_idx += 1;
if (_setting_idx == interval_setting_max) {
// we have done a full settings circle: resume setting
_resume_setting(state, event.subsecond);
} else
_face_draw(state, event.subsecond);
} else {
movement_illuminate_led();
}
break;
case EVENT_LIGHT_LONG_PRESS:
_button_beep();
if (state->face_state == interval_state_setting) {
_resume_setting(state, event.subsecond);
} else {
if (state->face_state >= interval_state_running ) _abort_running_timer();
_initiate_setting(state, event.subsecond);
}
break;
case EVENT_ALARM_BUTTON_UP:
switch (state->face_state) {
case interval_state_waiting:
// cycle through timers, skipping empty ones
do {
_inc_uint8(&state->timer_idx, 1, INTERVAL_TIMERS);
} while (_is_timer_empty(&state->timer[state->timer_idx]) && state->timer_idx != 0);
_ticks = 0;
_face_draw(state, event.subsecond);
break;
case interval_state_setting:
// alter timer settings
_abort_quick_ticks();
_handle_alarm_button(state);
break;
case interval_state_running:
// pause timer
_button_beep();
_paused_ts = _get_now_ts();
state->face_state = interval_state_pausing;
movement_cancel_background_task();
_face_draw(state, event.subsecond);
break;
case interval_state_pausing:
// resume paused timer
_button_beep();
_resume_paused_timer(state);
_face_draw(state, event.subsecond);
break;
default:
break;
}
break;
case EVENT_ALARM_LONG_PRESS:
if (state->face_state == interval_state_setting && _setting_idx != interval_setting_1_clear_yn) {
// initiate quick counting
_ticks = -2;
movement_request_tick_frequency(8);
break;
} else if (state->face_state <= interval_state_waiting) {
if (_is_timer_empty(timer)) {
// jump back to timer #1
_button_beep();
state->timer_idx = 0;
_init_timer_info(state);
} else {
// set initial state and start timer
_timer_work_round = _timer_full_round = 0;
if (timer->warmup_minutes + timer->warmup_seconds) _timer_run_state = 0;
else if (timer->work_minutes + timer->work_seconds) _timer_run_state = 1;
else if (timer->break_minutes + timer->break_seconds) _timer_run_state = 2;
else if (timer->cooldown_minutes + timer->cooldown_seconds) _timer_run_state = 3;
movement_request_tick_frequency(1);
_now_ts = _get_now_ts();
_target_ts = _now_ts;
_set_next_timestamp(state);
state->face_state = interval_state_running;
watch_set_indicator(WATCH_INDICATOR_BELL);
watch_set_colon();
}
} else if (state->face_state == interval_state_running) {
// stop the timer
_abort_running_timer();
_init_timer_info(state);
} else if (state->face_state == interval_state_pausing) {
// resume paused timer
_button_beep();
_resume_paused_timer(state);
}
_face_draw(state, event.subsecond);
break;
case EVENT_ALARM_LONG_UP:
_abort_quick_ticks();
break;
case EVENT_BACKGROUND_TASK:
// find the next timestamp or end the timer
if (_timer_run_state == 0) {
// warmup finished
if (timer->work_minutes + timer->work_seconds) _timer_run_state = 1;
else if (timer->break_minutes + timer->break_seconds) _timer_run_state = 2;
else if (timer->cooldown_minutes + timer->cooldown_seconds) _timer_run_state = 3;
else _timer_run_state = 4;
} else if (_timer_run_state == 1) {
// work finished
_timer_work_round++;
if (_timer_work_round == timer->work_rounds) {
_timer_work_round = 0;
if (timer->break_minutes + timer->break_seconds && (timer->full_rounds == 0
|| (timer->full_rounds && _timer_full_round + 1 < timer->full_rounds))) _timer_run_state = 2;
else {
_timer_full_round++;
if (timer->full_rounds && _timer_full_round == timer->full_rounds) {
if (timer->cooldown_minutes + timer->cooldown_seconds) _timer_run_state = 3;
else _timer_run_state = 4;
} else _timer_run_state = 1;
}
}
} else if (_timer_run_state == 2) {
// break finished
_timer_full_round++;
_timer_work_round = 0;
if (timer->full_rounds && _timer_full_round == timer->full_rounds) {
if (timer->cooldown_minutes + timer->cooldown_seconds) _timer_run_state = 3;
else _timer_run_state = 4;
_timer_full_round--;
} else {
if (timer->work_minutes + timer->work_seconds) _timer_run_state = 1;
}
} else if (_timer_run_state == 3)
// cooldown finished
_timer_run_state = 4;
// set next timestamp or play final sound sequence
if (_timer_run_state < 4) {
// transition to next timer phase
_set_next_timestamp(state);
} else {
// timer has finished
state->face_state = interval_state_waiting;
_init_timer_info(state);
_face_draw(state, event.subsecond);
watch_buzzer_play_sequence((int8_t *)_sound_seq_finish, NULL);
}
break;
case EVENT_TIMEOUT:
if (state->face_state != interval_state_running) movement_move_to_face(0);
break;
case EVENT_LIGHT_BUTTON_DOWN:
// don't light up every time light is hit
break;
default:
movement_default_loop_handler(event);
break;
}
return true;
}

View File

@@ -0,0 +1,127 @@
/*
* MIT License
*
* Copyright (c) 2022 Andreas Nebinger
*
* 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 INTERVAL_FACE_H_
#define INTERVAL_FACE_H_
/*
* INTERVAL TIMER face
*
* This face brings 9 customizable interval timers to the sensor watch,
* to be used as hiit training device and/or for time management techniques.
*
* - There are 9 interval timer slots, you can cycle through these with the
* alarm button (short press). For each timer slot, a short "slideshow"
* displaying the relevant details (like length of each phase - see below)
* is shown.
*
* - To start an interval timer, press and hold the alarm button.
*
* - To pause a running timer, press the alarm button (short press).
*
* - To completely abort a running timer, press and hold the alarm button.
*
* - Press and hold the light button to enter settings mode for each interval
* timer slot.
*
* - Each interval timer has 1 to 4 phases of customizable length like so:
* (1) prepare/warum up --> (2) work --> (3) break --> (4) cool down.
* When setting up or running a timer, each of these phases is displayed by
* the letters "PR" (prepare), "WO" (work), "BR" (break), "CD" (cool down).
*
* - Each of these phases is optional, you can set the corresponding
* minutes and seconds to zero. But at least one phase needs to be set, if
* you want to use the timer.
*
* - You can define the number of rounds either only for the work
* phase and/or for the combination of work + break phase. Let's say you
* want an interval timer that counts 3 rounds of 30 seconds work,
* followed by 20 seconds rest:
* work 30s --> work 30s --> work 30s --> break 20s
* You can do this by setting 30s for the "WO"rk phase and setting a 3
* in the lower right hand corner of the work page. The "LAP" indicator
* lights up at this position, to explain that we are setting laps here.
* After that, set the "BR"eak phase to 20s and leave the rest as it is.
*
* - If you want to set up a certain number of "full rounds", consisting
* of work phase(s) plus breaks, you can do so at the "BR"eak page. The
* number in the lower right hand corner determines the number of full
* rounds to be counted. A "-" means, that there is no limit and the
* timer keeps alternating between work and break phases.
*
* - This watch face comes with several pre-defined interval timers,
* suitable for hiit training (timer slots 1 to 4) as well as doing
* work according to the pomodoro principle (timer slots 5 to 6).
* Feel free to adjust the timer slots to your own needs (or completely
* wipe them ;-)
*/
#include "movement.h"
#define INTERVAL_TIMERS 9 // no of available customizable timers (be aware: only 4 bits reserved for this value in struct below)
typedef struct {
uint8_t warmup_minutes;
uint8_t warmup_seconds;
uint8_t work_minutes;
uint8_t work_seconds;
uint8_t break_minutes;
uint8_t break_seconds;
uint8_t cooldown_minutes;
uint8_t cooldown_seconds;
uint8_t work_rounds;
uint8_t full_rounds;
} interval_timer_setting_t;
typedef enum {
interval_state_intro,
interval_state_waiting,
interval_state_setting,
interval_state_running,
interval_state_pausing
} interval_timer_state_t;
typedef struct {
bool is_active;
uint8_t face_idx;
uint8_t timer_idx;
uint8_t timer_running_idx;
interval_timer_state_t face_state;
interval_timer_setting_t timer[INTERVAL_TIMERS];
} interval_face_state_t;
void interval_face_setup(uint8_t watch_face_index, void ** context_ptr);
void interval_face_activate(void *context);
bool interval_face_loop(movement_event_t event, void *context);
void interval_face_resign(void *context);
#define interval_face ((const watch_face_t) { \
interval_face_setup, \
interval_face_activate, \
interval_face_loop, \
interval_face_resign, \
NULL \
})
#endif // INTERVAL_FACE_H_

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

@@ -184,10 +184,19 @@ bool moon_phase_face_loop(movement_event_t event, void *context) {
state->offset += 86400;
_update(state, state->offset);
break;
case EVENT_ALARM_LONG_PRESS:
state->offset = 0;
case EVENT_ALARM_LONG_PRESS:
state->offset = 0;
_update(state, state->offset);
break;
break;
case EVENT_LIGHT_BUTTON_DOWN:
break;
case EVENT_LIGHT_BUTTON_UP:
state->offset -= 86400;
_update(state, state->offset);
break;
case EVENT_LIGHT_LONG_PRESS:
movement_illuminate_led();
break;
case EVENT_TIMEOUT:
// QUESTION: Should timeout reset offset to 0?
break;

View File

@@ -47,6 +47,9 @@
* each button press, and both the text and the graphical representation will
* display the moon phase for that day. Try pressing the Alarm button 27 times
* now, just to visualize what the moon will look like over the next month.
* Pressing the Light button will move back in time.
*
* Holding the Light button will illuminate the display.
*/
#include "movement.h"

View File

@@ -0,0 +1,216 @@
/* SPDX-License-Identifier: MIT */
/*
* MIT License
*
* Copyright © 2021-2022 Joey Castillo <joeycastillo@utexas.edu> <jose.castillo@gmail.com>
* Copyright © 2023 Jeremy O'Brien <neutral@fastmail.com>
* Copyright © 2024 Matheus Afonso Martins Moreira <matheus.a.m.moreira@gmail.com> (https://www.matheusmoreira.com/)
*
* 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 "pulsometer_face.h"
#include "watch.h"
#include "watch_common_display.h"
#ifndef PULSOMETER_FACE_CALIBRATION_DEFAULT
#define PULSOMETER_FACE_CALIBRATION_DEFAULT (30)
#endif
#ifndef PULSOMETER_FACE_CALIBRATION_INCREMENT
#define PULSOMETER_FACE_CALIBRATION_INCREMENT (10)
#endif
// tick frequency will be 2 to this power Hz (0 for 1 Hz, 2 for 4 Hz, etc.)
#ifndef PULSOMETER_FACE_FREQUENCY_FACTOR
#define PULSOMETER_FACE_FREQUENCY_FACTOR (4ul)
#endif
#define PULSOMETER_FACE_FREQUENCY (1 << PULSOMETER_FACE_FREQUENCY_FACTOR)
typedef struct {
bool measuring;
int16_t pulses;
int16_t ticks;
int8_t calibration;
} pulsometer_state_t;
static inline bool lcd_is_custom(void) {
return watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM;
}
static void pulsometer_display_title(pulsometer_state_t *pulsometer) {
(void) pulsometer;
watch_display_text_with_fallback(WATCH_POSITION_TOP, "PULSE", "PL");
}
static void pulsometer_display_calibration(pulsometer_state_t *pulsometer) {
char buf[3];
if (lcd_is_custom()) {
snprintf(buf, sizeof(buf), "%2hhd", pulsometer->calibration);
watch_display_text(WATCH_POSITION_SECONDS, buf);
} else {
snprintf(buf, sizeof(buf), "%2hhd", pulsometer->calibration);
watch_display_text(WATCH_POSITION_TOP_RIGHT, buf);
}
}
static void pulsometer_display_measurement(pulsometer_state_t *pulsometer) {
if (lcd_is_custom()) {
char buf[5];
int16_t value = pulsometer->pulses;
if (value < 0) value = 0;
if (value > 9999) value = 9999;
snprintf(buf, sizeof(buf), "%-4hd", value);
watch_display_text(WATCH_POSITION_BOTTOM, buf);
} else {
char buf[7];
snprintf(buf, sizeof(buf), "%-6hd", pulsometer->pulses);
watch_display_text(WATCH_POSITION_BOTTOM, buf);
}
}
static void pulsometer_indicate(pulsometer_state_t *pulsometer) {
if (pulsometer->measuring) {
watch_set_indicator(WATCH_INDICATOR_LAP);
} else {
watch_clear_indicator(WATCH_INDICATOR_LAP);
}
}
static void pulsometer_start_measurement(pulsometer_state_t *pulsometer) {
pulsometer->measuring = true;
pulsometer->pulses = INT16_MAX;
pulsometer->ticks = 0;
pulsometer_indicate(pulsometer);
movement_request_tick_frequency(PULSOMETER_FACE_FREQUENCY);
}
static void pulsometer_measure(pulsometer_state_t *pulsometer) {
if (!pulsometer->measuring) { return; }
pulsometer->ticks++;
float ticks_per_minute = 60 << PULSOMETER_FACE_FREQUENCY_FACTOR;
float pulses_while_button_held = ticks_per_minute / pulsometer->ticks;
float calibrated_pulses = pulses_while_button_held * pulsometer->calibration;
calibrated_pulses += 0.5f;
pulsometer->pulses = (int16_t) calibrated_pulses;
pulsometer_display_measurement(pulsometer);
}
static void pulsometer_stop_measurement(pulsometer_state_t *pulsometer) {
movement_request_tick_frequency(1);
pulsometer->measuring = false;
pulsometer_display_measurement(pulsometer);
pulsometer_indicate(pulsometer);
}
static void pulsometer_cycle_calibration(pulsometer_state_t *pulsometer, int8_t increment) {
if (pulsometer->measuring) { return; }
if (pulsometer->calibration <= 0) {
pulsometer->calibration = 1;
}
int8_t last = pulsometer->calibration;
pulsometer->calibration += increment;
if (pulsometer->calibration > 39) {
pulsometer->calibration = last == 39? 1 : 39;
}
pulsometer_display_calibration(pulsometer);
}
void pulsometer_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
pulsometer_state_t *pulsometer = malloc(sizeof(pulsometer_state_t));
pulsometer->calibration = PULSOMETER_FACE_CALIBRATION_DEFAULT;
pulsometer->pulses = 0;
pulsometer->ticks = 0;
*context_ptr = pulsometer;
}
}
void pulsometer_face_activate(void *context) {
pulsometer_state_t *pulsometer = context;
pulsometer->measuring = false;
pulsometer_display_title(pulsometer);
pulsometer_display_calibration(pulsometer);
pulsometer_display_measurement(pulsometer);
}
bool pulsometer_face_loop(movement_event_t event,void *context) {
pulsometer_state_t *pulsometer = (pulsometer_state_t *) context;
switch (event.event_type) {
case EVENT_ALARM_BUTTON_DOWN:
pulsometer_start_measurement(pulsometer);
break;
case EVENT_ALARM_BUTTON_UP:
case EVENT_ALARM_LONG_UP:
pulsometer_stop_measurement(pulsometer);
break;
case EVENT_TICK:
pulsometer_measure(pulsometer);
break;
case EVENT_LIGHT_BUTTON_UP:
pulsometer_cycle_calibration(pulsometer, 1);
break;
case EVENT_LIGHT_LONG_UP:
pulsometer_cycle_calibration(pulsometer, PULSOMETER_FACE_CALIBRATION_INCREMENT);
break;
case EVENT_LIGHT_BUTTON_DOWN:
// Inhibit the LED
break;
case EVENT_TIMEOUT:
movement_move_to_face(0);
break;
default:
movement_default_loop_handler(event);
break;
}
return true;
}
void pulsometer_face_resign(void *context) {
(void) context;
}

View File

@@ -0,0 +1,87 @@
/* SPDX-License-Identifier: MIT */
/*
* MIT License
*
* Copyright © 2021-2022 Joey Castillo <joeycastillo@utexas.edu> <jose.castillo@gmail.com>
* Copyright © 2022 Alexsander Akers <me@a2.io>
* Copyright © 2023 Alex Utter <ooterness@gmail.com>
* Copyright © 2024 Matheus Afonso Martins Moreira <matheus.a.m.moreira@gmail.com> (https://www.matheusmoreira.com/)
*
* 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 PULSOMETER_FACE_H_
#define PULSOMETER_FACE_H_
/*
* PULSOMETER face
*
* The pulsometer implements a classic mechanical watch complication.
* A mechanical pulsometer involves a chronograph with a scale that
* allows the user to compute the number of heart beats per minute
* in less time. The scale is calibrated, or graduated, for a fixed
* number of heart beats, most often 30. The user starts the chronograph
* and simultaneously begins counting the heart beats. The movement of
* the chronograph's seconds hand over time automatically performs the
* computations required. When the calibrated number of heart beats
* is reached, the chronograph is stopped and the seconds hand shows
* the heart rate.
*
* The Sensor Watch pulsometer improves this design with user calibration:
* it can be graduated to any value between 1 and 39 pulsations per minute.
* The default is still 30, mirroring the classic pulsometer calibration.
* This feature allows the user to reconfigure the pulsometer to count
* many other types of periodic minutely events, making it more versatile.
* For example, it can be set to 5 respirations per minute to turn it into
* an asthmometer, a nearly identical mechanical watch complication
* that doctors might use to quickly measure respiratory rate.
*
* To use the pulsometer, hold the ALARM button and count the pulses.
* When the calibrated number of pulses is reached, release the button.
* The display will show the number of pulses per minute.
*
* In order to measure heart rate, feel for a pulse using the hand with
* the watch while holding the button down with the other.
* The pulse can be easily felt on the carotid artery of the neck.
*
* In order to measure breathing rate, simply hold the ALARM button
* and count the number of breaths.
*
* To calibrate the pulsometer, press LIGHT
* to cycle to the next integer calibration.
* Long press LIGHT to cycle it by 10.
*/
#include "movement.h"
void pulsometer_face_setup(uint8_t watch_face_index, void ** context_ptr);
void pulsometer_face_activate(void *context);
bool pulsometer_face_loop(movement_event_t event,void *context);
void pulsometer_face_resign(void *context);
#define pulsometer_face ((const watch_face_t){ \
pulsometer_face_setup, \
pulsometer_face_activate, \
pulsometer_face_loop, \
pulsometer_face_resign, \
NULL, \
})
#endif // PULSOMETER_FACE_H_

View File

@@ -0,0 +1,336 @@
/*
* MIT License
*
* Copyright (c) 2024 <#author_name#>
*
* 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 "simon_face.h"
#include "delay.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Emulator only: need time() to seed the random number generator
#if __EMSCRIPTEN__
#include <time.h>
#endif
static char _simon_display_buf[12];
static uint8_t _timer;
static uint16_t _delay_beep;
static uint16_t _timeout;
static uint8_t _secSub;
static inline uint8_t _simon_get_rand_num(uint8_t num_values) {
#if __EMSCRIPTEN__
return rand() % num_values;
#else
return arc4random_uniform(num_values);
#endif
}
static void _simon_clear_display(simon_state_t *state) {
watch_clear_display();
if (state->playing_state != SIMON_NOT_PLAYING) {
sprintf(_simon_display_buf, "%2d", state->sequence_length);
watch_display_text(WATCH_POSITION_TOP_RIGHT, _simon_display_buf);
}
}
static void _simon_not_playing_display(simon_state_t *state) {
_simon_clear_display(state);
watch_display_text_with_fallback(WATCH_POSITION_TOP, "SIMON", "SI");
sprintf(_simon_display_buf, "%d", state->best_score);
watch_display_text(WATCH_POSITION_BOTTOM, _simon_display_buf);
if (!state->soundOff)
watch_set_indicator(WATCH_INDICATOR_BELL);
else
watch_clear_indicator(WATCH_INDICATOR_BELL);
if (!state->lightOff)
watch_set_indicator(WATCH_INDICATOR_SIGNAL);
else
watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
switch (state->mode)
{
case SIMON_MODE_EASY:
watch_display_text(WATCH_POSITION_SECONDS, " E");
break;
case SIMON_MODE_HARD:
watch_display_text(WATCH_POSITION_SECONDS, " H");
break;
default:
break;
}
}
static void _simon_reset(simon_state_t *state) {
state->playing_state = SIMON_NOT_PLAYING;
state->listen_index = 0;
state->sequence_length = 0;
_simon_not_playing_display(state);
}
static void _simon_display_note(SimonNote note, simon_state_t *state) {
watch_clear_display();
if (note == SIMON_WRONG_NOTE) {
watch_display_text(WATCH_POSITION_TOP_LEFT, "OH");
watch_display_text(WATCH_POSITION_BOTTOM, "NOOOOO");
return;
}
sprintf(_simon_display_buf, "%2d", state->sequence_length);
watch_display_text(WATCH_POSITION_TOP_RIGHT, _simon_display_buf);
switch (note) {
case SIMON_LED_NOTE:
watch_display_text(WATCH_POSITION_TOP_LEFT, "LI");
break;
case SIMON_ALARM_NOTE:
watch_display_text(WATCH_POSITION_SECONDS, "AL");
break;
case SIMON_MODE_NOTE:
watch_display_text_with_fallback(WATCH_POSITION_HOURS, "Md", "DE");
break;
default:
break;
}
}
static void _simon_play_note(SimonNote note, simon_state_t *state, bool skip_rest) {
_simon_display_note(note, state);
switch (note) {
case SIMON_LED_NOTE:
if (!state->lightOff) watch_set_led_yellow();
if (state->soundOff)
delay_ms(_delay_beep);
else
watch_buzzer_play_note(BUZZER_NOTE_D3, _delay_beep);
break;
case SIMON_MODE_NOTE:
if (!state->lightOff) watch_set_led_red();
if (state->soundOff)
delay_ms(_delay_beep);
else
watch_buzzer_play_note(BUZZER_NOTE_E4, _delay_beep);
break;
case SIMON_ALARM_NOTE:
if (!state->lightOff) watch_set_led_green();
if (state->soundOff)
delay_ms(_delay_beep);
else
watch_buzzer_play_note(BUZZER_NOTE_C3, _delay_beep);
break;
case SIMON_WRONG_NOTE:
if (state->soundOff)
delay_ms(800);
else
watch_buzzer_play_note(BUZZER_NOTE_A1, 800);
break;
}
watch_set_led_off();
if (note != SIMON_WRONG_NOTE) {
_simon_clear_display(state);
if (!skip_rest) {
watch_buzzer_play_note(BUZZER_NOTE_REST, (_delay_beep * 2)/3);
}
}
}
static void _simon_setup_next_note(simon_state_t *state) {
if (state->sequence_length > state->best_score) {
state->best_score = state->sequence_length;
}
_simon_clear_display(state);
state->playing_state = SIMON_TEACHING;
state->sequence[state->sequence_length] = _simon_get_rand_num(3) + 1;
state->sequence_length = state->sequence_length + 1;
state->teaching_index = 0;
state->listen_index = 0;
}
static void _simon_listen(SimonNote note, simon_state_t *state) {
if (state->sequence[state->listen_index] == note) {
_simon_play_note(note, state, true);
state->listen_index++;
_timer = 0;
if (state->listen_index == state->sequence_length) {
state->playing_state = SIMON_READY_FOR_NEXT_NOTE;
}
} else {
_simon_play_note(SIMON_WRONG_NOTE, state, true);
_simon_reset(state);
}
}
static void _simon_begin_listening(simon_state_t *state) {
state->playing_state = SIMON_LISTENING_BACK;
state->listen_index = 0;
}
static void _simon_change_speed(simon_state_t *state){
switch (state->mode)
{
case SIMON_MODE_HARD:
_delay_beep = DELAY_FOR_TONE_MS / 2;
_secSub = SIMON_FACE_FREQUENCY / 2;
_timeout = (TIMER_MAX * SIMON_FACE_FREQUENCY) / 2;
break;
default:
_delay_beep = DELAY_FOR_TONE_MS;
_secSub = SIMON_FACE_FREQUENCY;
_timeout = TIMER_MAX * SIMON_FACE_FREQUENCY;
break;
}
}
void simon_face_setup(uint8_t watch_face_index,
void **context_ptr) {
(void)watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(simon_state_t));
memset(*context_ptr, 0, sizeof(simon_state_t));
// Do any one-time tasks in here; the inside of this conditional happens
// only at boot.
}
// Do any pin or peripheral setup here; this will be called whenever the watch
// wakes from deep sleep.
#if __EMSCRIPTEN__
// simulator only: seed the randon number generator
time_t t;
srand((unsigned)time(&t));
#endif
}
void simon_face_activate(void *context) {
(void) context;
simon_state_t *state = (simon_state_t *)context;
_simon_change_speed(state);
movement_request_tick_frequency(SIMON_FACE_FREQUENCY);
_timer = 0;
}
bool simon_face_loop(movement_event_t event,
void *context) {
simon_state_t *state = (simon_state_t *)context;
switch (event.event_type) {
case EVENT_ACTIVATE:
// Show your initial UI here.
_simon_reset(state);
break;
case EVENT_TICK:
if (state->playing_state == SIMON_LISTENING_BACK && state->mode != SIMON_MODE_EASY)
{
_timer++;
if(_timer >= (_timeout)){
_timer = 0;
_simon_play_note(SIMON_WRONG_NOTE, state, true);
_simon_reset(state);
}
}
else if (state->playing_state == SIMON_TEACHING && event.subsecond == 0) {
SimonNote note = state->sequence[state->teaching_index];
// if this is the final note in the sequence, don't play the rest to let
// the player jump in faster
_simon_play_note(note, state, state->teaching_index == (state->sequence_length - 1));
state->teaching_index++;
if (state->teaching_index == state->sequence_length) {
_simon_begin_listening(state);
}
}
else if (state->playing_state == SIMON_READY_FOR_NEXT_NOTE && (event.subsecond % _secSub) == 0) {
_timer = 0;
_simon_setup_next_note(state);
}
break;
case EVENT_LIGHT_BUTTON_DOWN:
break;
case EVENT_LIGHT_LONG_PRESS:
if (state->playing_state == SIMON_NOT_PLAYING) {
state->lightOff = !state->lightOff;
_simon_not_playing_display(state);
}
break;
case EVENT_ALARM_LONG_PRESS:
if (state->playing_state == SIMON_NOT_PLAYING) {
state->soundOff = !state->soundOff;
_simon_not_playing_display(state);
if (!state->soundOff)
watch_buzzer_play_note(BUZZER_NOTE_D3, _delay_beep);
}
break;
case EVENT_LIGHT_BUTTON_UP:
if (state->playing_state == SIMON_NOT_PLAYING) {
state->sequence_length = 0;
watch_clear_indicator(WATCH_INDICATOR_BELL);
watch_clear_indicator(WATCH_INDICATOR_SIGNAL);
_simon_setup_next_note(state);
} else if (state->playing_state == SIMON_LISTENING_BACK) {
_simon_listen(SIMON_LED_NOTE, state);
}
break;
case EVENT_MODE_LONG_PRESS:
if (state->playing_state == SIMON_NOT_PLAYING) {
movement_move_to_face(0);
} else {
state->playing_state = SIMON_NOT_PLAYING;
_simon_reset(state);
}
break;
case EVENT_MODE_BUTTON_UP:
if (state->playing_state == SIMON_NOT_PLAYING) {
movement_move_to_next_face();
} else if (state->playing_state == SIMON_LISTENING_BACK) {
_simon_listen(SIMON_MODE_NOTE, state);
}
break;
case EVENT_ALARM_BUTTON_UP:
if (state->playing_state == SIMON_LISTENING_BACK) {
_simon_listen(SIMON_ALARM_NOTE, state);
}
else if (state->playing_state == SIMON_NOT_PLAYING){
state->mode = (state->mode + 1) % SIMON_MODE_TOTAL;
_simon_change_speed(state);
_simon_not_playing_display(state);
}
break;
case EVENT_TIMEOUT:
movement_move_to_face(0);
break;
case EVENT_LOW_ENERGY_UPDATE:
break;
default:
return movement_default_loop_handler(event);
}
return true;
}
void simon_face_resign(void *context) {
(void)context;
watch_set_led_off();
watch_set_buzzer_off();
}

View File

@@ -0,0 +1,111 @@
/*
* MIT License
*
* Copyright (c) 2024 <#author_name#>
*
* 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 SIMON_FACE_H_
#define SIMON_FACE_H_
#include "movement.h"
/*
* simon_face
* -----------
* The classic electronic game, Simon, reduced to be played on a Sensor-Watch
*
* How to play:
*
* When first arriving at the face, it will show your best score.
*
* Press the light button to start the game.
*
* A sequence will be played, starting with length 1. The sequence can be
* made up of tones corresponding to any of the three buttons.
*
* light button: "LI" will display at the top of the screen, the LED will be yellow, and a high D will play
* mode button: "DE" will display at the left of the screen, the LED will be red, and a high E will play
* alarm button: "AL" will display on the right of the screen, the LED will be green, and a high C will play
*
* Once the sequence has finished, press the same buttons to recreate the sequence.
*
* If correct, the sequence will get one tone longer and play again. See how long of a sequence you can get.
*
* If you recreate the sequence incorrectly, a low note will play with "OH NOOOOO" displayed and the game is over.
* Press light to play again.
*
* Once playing, long press the mode button when it is your turn to exit the game early.
*/
#define MAX_SEQUENCE 99
typedef enum SimonNote {
SIMON_LED_NOTE = 1,
SIMON_MODE_NOTE,
SIMON_ALARM_NOTE,
SIMON_WRONG_NOTE
} SimonNote;
typedef enum SimonPlayingState {
SIMON_NOT_PLAYING = 0,
SIMON_TEACHING,
SIMON_LISTENING_BACK,
SIMON_READY_FOR_NEXT_NOTE
} SimonPlayingState;
typedef enum SimonMode {
SIMON_MODE_NORMAL = 0, // 5 Second timeout if nothing is input
SIMON_MODE_EASY, // There is no timeout in this mode
SIMON_MODE_HARD, // The speed of the teaching is doubled and th etimeout is halved
SIMON_MODE_TOTAL
} SimonMode;
typedef struct {
uint8_t best_score;
SimonNote sequence[MAX_SEQUENCE];
uint8_t sequence_length;
uint8_t teaching_index;
uint8_t listen_index;
bool soundOff;
bool lightOff;
uint8_t mode:6;
SimonPlayingState playing_state;
} simon_state_t;
void simon_face_setup(uint8_t watch_face_index, void **context_ptr);
void simon_face_activate(void *context);
bool simon_face_loop(movement_event_t event, void *context);
void simon_face_resign(void *context);
#define simon_face \
((const watch_face_t){ \
simon_face_setup, \
simon_face_activate, \
simon_face_loop, \
simon_face_resign, \
NULL, \
})
#define TIMER_MAX 5
#define SIMON_FACE_FREQUENCY 8
#define DELAY_FOR_TONE_MS 300
#endif // SIMON_FACE_H_

View File

@@ -0,0 +1,180 @@
/*
* MIT License
*
* Copyright (c) 2023 Wesley Aptekar-Cassels
* Copyright (c) 2025 Vaipex
*
* 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 "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;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(simple_coin_flip_face_state_t));
memset(*context_ptr, 0, sizeof(simple_coin_flip_face_state_t));
}
}
void simple_coin_flip_face_activate(void *context) {
(void) context;
}
static uint32_t get_random(uint32_t max) {
#if __EMSCRIPTEN__
return rand() % max;
#else
return arc4random_uniform(max);
#endif
}
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");
} else {
watch_display_text(WATCH_POSITION_BOTTOM, "Flip");
}
}
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();
for(int j = 0; j<j_len; j++){
watch_set_pixel(pixels[i][j][0], pixels[i][j][1]);
}
delay_ms(150);
}
}
}
static void load_animation(void) {
if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) {
int j_len = 2;
int pixels[3][4][2] = {
{
{0, 3},
{0, 6}
},
{
{1, 3},
{1, 5}
},
{
{2, 2},
{2, 4}
}
};
set_pixels(pixels, j_len);
} else{
int j_len = 4;
int pixels[3][4][2] = {
{
{2, 22},
{2, 15},
{1, 2},
{1, 4}
},
{
{0, 16},
{0, 15},
{0, 1},
{0, 3}
},
{
{3, 16},
{3, 14},
{3, 1},
{3, 3}
}
};
set_pixels(pixels, j_len);
}
}
static void _blink_face_update_lcd(simple_coin_flip_face_state_t *state) {
(void) state;
watch_clear_display();
load_animation();
watch_clear_display();
int r = get_random(2);
if(r){
watch_display_text(WATCH_POSITION_BOTTOM, "Heads");
}else{
if (watch_get_lcd_type() == WATCH_LCD_TYPE_CLASSIC) {
watch_display_text(WATCH_POSITION_BOTTOM, " Tails");
}else {
watch_display_text(WATCH_POSITION_BOTTOM, "Tails");
}
}
}
bool simple_coin_flip_face_loop(movement_event_t event, void *context) {
simple_coin_flip_face_state_t *state = (simple_coin_flip_face_state_t *)context;
switch (event.event_type) {
case EVENT_ACTIVATE:
draw_start_face();
break;
case EVENT_TICK:
if(!state->is_start_face && !state->active){
if(state->inactivity_ticks >= 15){
state->is_start_face = false;
state->inactivity_ticks = 0;
draw_start_face();
}else{
state->inactivity_ticks++;
}
} else {
state->inactivity_ticks = 0;
}
break;
//execute same action for light and alarm button
case EVENT_LIGHT_BUTTON_UP:
case EVENT_ALARM_BUTTON_UP:
if (!state->active) {
state->active = true;
_blink_face_update_lcd(state);
state->active = false;
state->is_start_face = false;
state->inactivity_ticks = 0;
}
break;
case EVENT_TIMEOUT:
movement_move_to_face(0);
break;
case EVENT_LOW_ENERGY_UPDATE:
break;
default:
return movement_default_loop_handler(event);
}
return true;
}
void simple_coin_flip_face_resign(void *context) {
(void) context;
}

View File

@@ -0,0 +1,60 @@
/*
* MIT License
*
* Copyright (c) 2023 Wesley Aptekar-Cassels
* Copyright (c) 2025 Vaipex
*
* 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.
*/
#pragma once
#include "movement.h"
/*
* A extremely simple coin flip face updated from Wesley Aptekar-Cassels version.
*
* Press ALARM or LIGHT to flip a coin, after a short animation it will display
* "Heads" or "Tails". Press ALARM or LIGHT to flip again.
*
* This is for people who want a simpler UI than probability_face or
* randonaut_face. While those have more features, this one is more immediately
* obvious - useful, for instance, if you are using a coin flip to agree on
* something with someone, and want the operation to be clear to someone who
* has not had anything explained to them.
*/
typedef struct {
bool active;
bool is_start_face;
uint8_t inactivity_ticks;
} simple_coin_flip_face_state_t;
void simple_coin_flip_face_setup(uint8_t watch_face_index, void ** context_ptr);
void simple_coin_flip_face_activate(void *context);
bool simple_coin_flip_face_loop(movement_event_t event, void *context);
void simple_coin_flip_face_resign(void *context);
#define simple_coin_flip_face ((const watch_face_t){ \
simple_coin_flip_face_setup, \
simple_coin_flip_face_activate, \
simple_coin_flip_face_loop, \
simple_coin_flip_face_resign, \
NULL, \
})

View File

@@ -114,6 +114,7 @@ bool stopwatch_face_loop(movement_event_t event, void *context) {
stopwatch_state->start_time.reg = 0;
stopwatch_state->seconds_counted = 0;
watch_display_text(WATCH_POSITION_BOTTOM, "000000");
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
}
break;
case EVENT_ALARM_BUTTON_DOWN:

View File

@@ -280,6 +280,9 @@ void tarot_face_setup(uint8_t watch_face_index, void ** context_ptr) {
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(tarot_state_t));
memset(*context_ptr, 0, sizeof(tarot_state_t));
tarot_state_t *state = (tarot_state_t *)*context_ptr;
state->major_arcana_only = true;
state->num_cards_to_draw = 3;
}
// Emulator only: Seed random number generator
#if __EMSCRIPTEN__
@@ -292,8 +295,6 @@ void tarot_face_activate(void *context) {
watch_display_text_with_fallback(WATCH_POSITION_TOP, "Tarot", "TA");
init_deck(state);
state->num_cards_to_draw = 3;
state->major_arcana_only = true;
}
bool tarot_face_loop(movement_event_t event, void *context) {

View File

@@ -0,0 +1,361 @@
/*
* MIT License
*
* Copyright (c) 2022 Andreas Nebinger, building on Wesley Ellis countdown_face.c
*
* 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 "timer_face.h"
#include "watch.h"
#include "watch_utility.h"
static const uint32_t _default_timer_values[] = {0x000200, 0x000500, 0x000A00, 0x001400, 0x002D02}; // default timers: 2 min, 5 min, 10 min, 20 min, 2 h 45 min
// sound sequence for a single beeping sequence
static const int8_t _sound_seq_beep[] = {BUZZER_NOTE_C8, 3, BUZZER_NOTE_REST, 3, -2, 2, BUZZER_NOTE_C8, 5, BUZZER_NOTE_REST, 25, 0};
static const int8_t _sound_seq_start[] = {BUZZER_NOTE_C8, 2, 0};
static uint8_t _beeps_to_play; // temporary counter for ring signals playing
static void _signal_callback() {
if (_beeps_to_play) {
_beeps_to_play--;
watch_buzzer_play_sequence((int8_t *)_sound_seq_beep, _signal_callback);
}
}
static void _start(timer_state_t *state, bool with_beep) {
if (state->timers[state->current_timer].value == 0) return;
watch_date_time_t now = watch_rtc_get_date_time();
state->now_ts = watch_utility_date_time_to_unix_time(now, movement_get_current_timezone_offset());
if (state->mode == pausing)
state->target_ts = state->now_ts + state->paused_left;
else
state->target_ts = watch_utility_offset_timestamp(state->now_ts,
state->timers[state->current_timer].unit.hours,
state->timers[state->current_timer].unit.minutes,
state->timers[state->current_timer].unit.seconds);
watch_date_time_t target_dt = watch_utility_date_time_from_unix_time(state->target_ts, movement_get_current_timezone_offset());
state->mode = running;
movement_schedule_background_task_for_face(state->watch_face_index, target_dt);
watch_set_indicator(WATCH_INDICATOR_BELL);
if (with_beep) watch_buzzer_play_sequence((int8_t *)_sound_seq_start, NULL);
}
static void _draw(timer_state_t *state, uint8_t subsecond) {
char bottom_time[10];
char timer_id[3];
uint32_t delta;
div_t result;
uint8_t h, min, sec;
switch (state->mode) {
case pausing:
if (state->pausing_seconds % 2)
watch_clear_indicator(WATCH_INDICATOR_BELL);
else
watch_set_indicator(WATCH_INDICATOR_BELL);
if (state->pausing_seconds != 1)
// not 1st iteration (or 256th): do not write anything
return;
// fall through
case running:
delta = state->target_ts - state->now_ts;
result = div(delta, 60);
sec = result.rem;
result = div(result.quot, 60);
min = result.rem;
h = result.quot;
sprintf(bottom_time, "%02u%02u%02u", h, min, sec);
break;
case setting:
if (state->settings_state == 1) {
// ask it the current timer shall be erased
sprintf(bottom_time, "CLEAR%c", state->erase_timer_flag ? 'y' : 'n');
watch_clear_colon();
} else if (state->settings_state == 5) {
sprintf(bottom_time, " LOOP%c", state->timers[state->current_timer].unit.repeat ? 'y' : 'n');
watch_clear_colon();
} else {
sprintf(bottom_time, "%02u%02u%02u", state->timers[state->current_timer].unit.hours,
state->timers[state->current_timer].unit.minutes,
state->timers[state->current_timer].unit.seconds);
watch_set_colon();
}
break;
case waiting:
sprintf(bottom_time, "%02u%02u%02u", state->timers[state->current_timer].unit.hours,
state->timers[state->current_timer].unit.minutes,
state->timers[state->current_timer].unit.seconds);
watch_set_colon();
break;
}
sprintf(timer_id, "%2u", state->current_timer + 1);
if (state->mode == setting && subsecond % 2) {
// blink the current settings value
if (state->settings_state == 0) timer_id[0] = timer_id[1] = ' ';
else if (state->settings_state == 1 || state->settings_state == 5) bottom_time[5] = ' ';
else bottom_time[(state->settings_state - 1) * 2 - 2] = bottom_time[(state->settings_state - 1) * 2 - 1] = ' ';
}
watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, bottom_time, bottom_time);
watch_display_text_with_fallback(WATCH_POSITION_TOP_RIGHT, timer_id, timer_id);
// set lap indicator when we have a looping timer
if (state->timers[state->current_timer].unit.repeat) watch_set_indicator(WATCH_INDICATOR_LAP);
else watch_clear_indicator(WATCH_INDICATOR_LAP);
}
static void _reset(timer_state_t *state) {
state->mode = waiting;
movement_cancel_background_task_for_face(state->watch_face_index);
watch_clear_indicator(WATCH_INDICATOR_BELL);
}
static void _set_next_valid_timer(timer_state_t *state) {
if ((state->timers[state->current_timer].value & 0xFFFFFF) == 0) {
uint8_t i = state->current_timer;
do {
i = (i + 1) % TIMER_SLOTS;
} while ((state->timers[i].value & 0xFFFFFF) == 0 && i != state->current_timer);
state->current_timer = i;
}
}
static void _resume_setting(timer_state_t *state) {
state->settings_state = 0;
state->mode = waiting;
movement_request_tick_frequency(1);
_set_next_valid_timer(state);
}
static void _settings_increment(timer_state_t *state) {
switch(state->settings_state) {
case 0:
state->current_timer = (state->current_timer + 1) % TIMER_SLOTS;
break;
case 1:
state->erase_timer_flag ^= 1;
break;
case 2:
state->timers[state->current_timer].unit.hours = (state->timers[state->current_timer].unit.hours + 1) % 24;
break;
case 3:
state->timers[state->current_timer].unit.minutes = (state->timers[state->current_timer].unit.minutes + 1) % 60;
break;
case 4:
state->timers[state->current_timer].unit.seconds = (state->timers[state->current_timer].unit.seconds + 1) % 60;
break;
case 5:
state->timers[state->current_timer].unit.repeat ^= 1;
break;
default:
// should never happen
break;
}
return;
}
static void _abort_quick_cycle(timer_state_t *state) {
if (state->quick_cycle) {
state->quick_cycle = false;
movement_request_tick_frequency(4);
}
}
static inline bool _check_for_signal() {
if (_beeps_to_play) {
_beeps_to_play = 0;
return true;
}
return false;
}
void timer_face_setup(uint8_t watch_face_index, void ** context_ptr) {
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(timer_state_t));
timer_state_t *state = (timer_state_t *)*context_ptr;
memset(*context_ptr, 0, sizeof(timer_state_t));
state->watch_face_index = watch_face_index;
for (uint8_t i = 0; i < sizeof(_default_timer_values) / sizeof(uint32_t); i++) {
state->timers[i].value = _default_timer_values[i];
}
}
}
void timer_face_activate(void *context) {
timer_state_t *state = (timer_state_t *)context;
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "TMR", "TR");
watch_set_colon();
if(state->mode == running) {
watch_date_time_t now = watch_rtc_get_date_time();
state->now_ts = watch_utility_date_time_to_unix_time(now, movement_get_current_timezone_offset());
watch_set_indicator(WATCH_INDICATOR_BELL);
} else {
state->pausing_seconds = 1;
_beeps_to_play = 0;
}
}
bool timer_face_loop(movement_event_t event, void *context) {
timer_state_t *state = (timer_state_t *)context;
uint8_t subsecond = event.subsecond;
switch (event.event_type) {
case EVENT_ACTIVATE:
_draw(state, event.subsecond);
break;
case EVENT_TICK:
if (state->mode == running) state->now_ts++;
else if (state->mode == pausing) state->pausing_seconds++;
else if (state->quick_cycle) {
if (HAL_GPIO_BTN_ALARM_read()) {
_settings_increment(state);
subsecond = 0;
} else _abort_quick_cycle(state);
}
_draw(state, subsecond);
break;
case EVENT_LIGHT_BUTTON_DOWN:
switch (state->mode) {
case pausing:
case running:
movement_illuminate_led();
break;
case setting:
if (state->erase_timer_flag) {
state->timers[state->current_timer].value = 0;
state->erase_timer_flag = false;
}
state->settings_state = (state->settings_state + 1) % 6;
if (state->settings_state == 1 && state->timers[state->current_timer].value == 0) state->settings_state = 2;
else if (state->settings_state == 5 && (state->timers[state->current_timer].value & 0xFFFFFF) == 0) state->settings_state = 0;
break;
default:
break;
}
_draw(state, event.subsecond);
break;
case EVENT_LIGHT_BUTTON_UP:
if (state->mode == waiting) movement_illuminate_led();
break;
case EVENT_ALARM_BUTTON_UP:
_abort_quick_cycle(state);
if (_check_for_signal()) break;;
switch (state->mode) {
case running:
state->mode = pausing;
state->pausing_seconds = 0;
state->paused_left = state->target_ts - state->now_ts;
movement_cancel_background_task();
break;
case pausing:
_start(state, false);
break;
case waiting: {
uint8_t last_timer = state->current_timer;
state->current_timer = (state->current_timer + 1) % TIMER_SLOTS;
_set_next_valid_timer(state);
// start the time immediately if there is only one valid timer slot
if (last_timer == state->current_timer) _start(state, true);
break;
}
case setting:
_settings_increment(state);
subsecond = 0;
break;
}
_draw(state, subsecond);
break;
case EVENT_LIGHT_LONG_PRESS:
if (state->mode == waiting) {
// initiate settings
state->mode = setting;
state->settings_state = 0;
state->erase_timer_flag = false;
movement_request_tick_frequency(4);
} else if (state->mode == setting) {
_resume_setting(state);
}
_draw(state, event.subsecond);
break;
case EVENT_BACKGROUND_TASK:
// play the alarm
_beeps_to_play = 4;
watch_buzzer_play_sequence((int8_t *)_sound_seq_beep, _signal_callback);
_reset(state);
if (state->timers[state->current_timer].unit.repeat) _start(state, false);
break;
case EVENT_ALARM_LONG_PRESS:
switch(state->mode) {
case setting:
switch (state->settings_state) {
case 0:
state->current_timer = 0;
break;
case 2:
case 3:
case 4:
state->quick_cycle = true;
movement_request_tick_frequency(8);
break;
default:
break;
}
break;
case waiting:
_start(state, true);
break;
case pausing:
case running:
_reset(state);
if (movement_button_should_sound()) watch_buzzer_play_note(BUZZER_NOTE_C7, 50);
break;
default:
break;
}
_draw(state, event.subsecond);
break;
case EVENT_ALARM_LONG_UP:
_abort_quick_cycle(state);
break;
case EVENT_MODE_LONG_PRESS:
case EVENT_TIMEOUT:
_abort_quick_cycle(state);
movement_move_to_face(0);
break;
default:
movement_default_loop_handler(event);
break;
}
return true;
}
void timer_face_resign(void *context) {
timer_state_t *state = (timer_state_t *)context;
if (state->mode == setting) {
state->settings_state = 0;
state->mode = waiting;
}
}

View File

@@ -0,0 +1,103 @@
/*
* MIT License
*
* Copyright (c) 2022 Andreas Nebinger, based on Wesley Ellis countdown face.
*
* 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 TIMER_FACE_H_
#define TIMER_FACE_H_
/*
* TIMER face
* Advanced timer/countdown face with pre-set timer lengths
*
* This watch face provides the functionality of starting a countdown by choosing
* one out of nine programmable timer presets. A timer/countdown can be 23 hours,
* 59 minutes, and 59 seconds max. A timer can also be set to auto-repeat, which
* is indicated by the lap indicator.
*
* How to use in NORMAL mode:
* - Short-pressing the alarm button cycles through all pre-set timer lengths.
* Find the current timer slot number in the upper right-hand corner.
* - Long-pressing the alarm button starts the timer.
* - Long-pressing the light button initiates settings mode.
*
* How to use in SETTINGS mode:
* - There are up to nine slots for storing a timer setting. The current slot is
* indicated by the number in the upper right-hand corner.
* - Short-pressing the light button cycles through the settings values of each
* timer slot in the following order: hours - minutes - seconds - timer repeat
* - Short-pressing the alarm button alters the current settings value.
* - Long-pressing the light button resumes to normal mode.
*
*/
#include "movement.h"
#define TIMER_SLOTS 9 // offer 9 timer slots
typedef enum {
waiting,
running,
setting,
pausing
} timer_mode_t;
typedef union {
struct {
uint8_t hours;
uint8_t minutes;
uint8_t seconds;
bool repeat;
} unit;
uint32_t value;
} timer_setting_t;
typedef struct {
uint32_t target_ts;
uint32_t now_ts;
uint16_t paused_left;
uint8_t pausing_seconds;
uint8_t watch_face_index;
timer_setting_t timers[TIMER_SLOTS];
uint8_t settings_state : 4;
uint8_t current_timer : 4;
uint8_t set_timers : 4;
bool erase_timer_flag : 1;
timer_mode_t mode : 3;
bool quick_cycle : 1;
} timer_state_t;
void timer_face_setup(uint8_t watch_face_index, void ** context_ptr);
void timer_face_activate(void *context);
bool timer_face_loop(movement_event_t event, void *context);
void timer_face_resign(void *context);
#define timer_face ((const watch_face_t){ \
timer_face_setup, \
timer_face_activate, \
timer_face_loop, \
timer_face_resign, \
NULL, \
})
#endif // TIMER_FACE_H_

View File

@@ -0,0 +1,323 @@
/*
* MIT License
*
* Copyright (c) 2022 Wesley Ellis (https://github.com/tahnok)
*
* 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 <math.h>
#include "TOTP.h"
#include "base32.h"
#include "watch.h"
#include "watch_utility.h"
#include "filesystem.h"
#include "totp_lfs_face.h"
#define MAX_TOTP_RECORDS 30
#define MAX_TOTP_SECRET_SIZE 128
#define TOTP_FILE "totp_uris.txt"
const char* TOTP_URI_START = "otpauth://totp/";
struct totp_record {
char label[4];
hmac_alg algorithm;
uint8_t period;
uint8_t secret_size;
union {
uint8_t *secret;
struct {
uint16_t file_secret_offset;
uint16_t file_secret_length;
};
};
};
/* This is used if we're not storing all the secrets but instead
* calculating them on demand. Avoids malloc in normal operation.
*/
static uint8_t current_secret[MAX_TOTP_SECRET_SIZE];
static struct totp_record totp_records[MAX_TOTP_RECORDS];
static uint8_t num_totp_records = 0;
static void init_totp_record(struct totp_record *totp_record) {
totp_record->label[0] = 'A';
totp_record->label[1] = 'A';
totp_record->label[2] = 'A';
totp_record->label[3] = 0;
totp_record->algorithm = SHA1;
totp_record->period = 30;
totp_record->secret_size = 0;
}
static bool totp_lfs_face_read_param(struct totp_record *totp_record, char *param, char *value) {
if (!strcmp(param, "issuer")) {
if (value[0] == '\0') {
printf("TOTP issuer must be a non-empty string\n");
return false;
}
snprintf(totp_record->label, sizeof(totp_record->label), "%-3s", value);
} else if (!strcmp(param, "secret")) {
totp_record->file_secret_length = strlen(value);
if (UNBASE32_LEN(totp_record->file_secret_length) > MAX_TOTP_SECRET_SIZE) {
printf("TOTP secret too long: %s\n", value);
return false;
}
totp_record->secret_size = base32_decode((unsigned char *)value, current_secret);
if (totp_record->secret_size == 0) {
printf("TOTP can't decode secret: %s\n", value);
return false;
}
} else if (!strcmp(param, "digits")) {
if (!strcmp(param, "6")) {
printf("TOTP got %s, not 6 digits\n", value);
return false;
}
} else if (!strcmp(param, "period")) {
totp_record->period = atoi(value);
if (totp_record->period == 0) {
printf("TOTP invalid period %s\n", value);
return false;
}
} else if (!strcmp(param, "algorithm")) {
if (!strcmp(value, "SHA1")) {
totp_record->algorithm = SHA1;
}
else if (!strcmp(value, "SHA224")) {
totp_record->algorithm = SHA224;
}
else if (!strcmp(value, "SHA256")) {
totp_record->algorithm = SHA256;
}
else if (!strcmp(value, "SHA384")) {
totp_record->algorithm = SHA384;
}
else if (!strcmp(value, "SHA512")) {
totp_record->algorithm = SHA512;
}
else {
printf("TOTP ignored due to algorithm %s\n", value);
return false;
}
}
return true;
}
static void totp_lfs_face_read_file(char *filename) {
// For 'format' of file, see comment at top.
const size_t uri_start_len = strlen(TOTP_URI_START);
if (!filesystem_file_exists(filename)) {
printf("TOTP file error: %s\n", filename);
return;
}
char line[256];
int32_t offset = 0, old_offset = 0;
while (old_offset = offset, filesystem_read_line(filename, line, &offset, 255) && strlen(line)) {
if (num_totp_records == MAX_TOTP_RECORDS) {
printf("TOTP max records: %d\n", MAX_TOTP_RECORDS);
break;
}
// Check that it looks like a URI
if (strncmp(TOTP_URI_START, line, uri_start_len)) {
printf("TOTP invalid uri start: %s\n", line);
continue;
}
// Check that we can find a '?' (to start our parameters)
char *param;
char *param_saveptr = NULL;
char *params = strchr(line + uri_start_len, '?');
if (params == NULL) {
printf("TOTP no params: %s\n", line);
continue;
}
// Process the parameters and put them in the record
init_totp_record(&totp_records[num_totp_records]);
bool error = false;
param = strtok_r(params + 1, "&", &param_saveptr);
do {
char *param_middle = strchr(param, '=');
*param_middle = '\0';
if (totp_lfs_face_read_param(&totp_records[num_totp_records], param, param_middle + 1)) {
if (!strcmp(param, "secret")) {
totp_records[num_totp_records].file_secret_offset = old_offset + (param_middle + 1 - line);
}
} else {
error = true;
}
} while ((param = strtok_r(NULL, "&", &param_saveptr)));
if (error) {
totp_records[num_totp_records].secret_size = 0;
continue;
}
// If we found a probably valid TOTP record, keep it.
if (totp_records[num_totp_records].secret_size) {
num_totp_records += 1;
} else {
printf("TOTP missing secret: %s\n", line);
}
}
}
void totp_lfs_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(totp_lfs_state_t));
}
#if !(__EMSCRIPTEN__)
if (num_totp_records == 0) {
totp_lfs_face_read_file(TOTP_FILE);
}
#endif
}
static uint8_t *totp_lfs_face_get_file_secret(struct totp_record *record) {
char buffer[BASE32_LEN(MAX_TOTP_SECRET_SIZE) + 1];
int32_t file_secret_offset = record->file_secret_offset;
/* TODO filesystem_read_line is quite inefficient. Consider writing a new function,
* and keeping the file open?
*/
if (!filesystem_read_line(TOTP_FILE, buffer, &file_secret_offset, record->file_secret_length + 1)) {
/* Shouldn't happen at this point. Return current_secret, which is misleading but will not cause a crash. */
printf("TOTP can't read expected secret from totp_uris.txt (failed readline)\n");
return current_secret;
}
if (base32_decode((unsigned char *)buffer, current_secret) != record->secret_size) {
printf("TOTP can't properly decode secret '%s' from totp_uris.txt; failed at offset %d; read to %ld\n", buffer, record->file_secret_offset, file_secret_offset);
}
return current_secret;
}
static void totp_face_set_record(totp_lfs_state_t *totp_state, int i) {
struct totp_record *record;
if (num_totp_records == 0 && i >= num_totp_records) {
return;
}
totp_state->current_index = i;
record = &totp_records[i];
TOTP(
totp_lfs_face_get_file_secret(record),
record->secret_size,
record->period,
record->algorithm
);
totp_state->current_code = getCodeFromTimestamp(totp_state->timestamp);
totp_state->steps = totp_state->timestamp / record->period;
}
void totp_lfs_face_activate(void *context) {
memset(context, 0, sizeof(totp_lfs_state_t));
totp_lfs_state_t *totp_state = (totp_lfs_state_t *)context;
#if __EMSCRIPTEN__
if (num_totp_records == 0) {
// Doing this here rather than in setup makes things a bit more pleasant in the simulator, since there's no easy way to trigger
// setup again after uploading the data.
totp_lfs_face_read_file(TOTP_FILE);
}
#endif
totp_state->timestamp = watch_utility_date_time_to_unix_time(movement_get_utc_date_time(), 0);
totp_face_set_record(totp_state, 0);
}
static void totp_face_display(totp_lfs_state_t *totp_state) {
uint8_t index = totp_state->current_index;
char buf[7];
if (num_totp_records == 0) {
watch_display_text(WATCH_POSITION_FULL, "No2F Codes");
return;
}
div_t result = div(totp_state->timestamp, totp_records[index].period);
if (result.quot != totp_state->steps) {
totp_state->current_code = getCodeFromTimestamp(totp_state->timestamp);
totp_state->steps = result.quot;
}
uint8_t valid_for = totp_records[index].period - result.rem;
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, totp_records[index].label, totp_records[index].label);
sprintf(buf, "%2d", valid_for);
watch_display_text_with_fallback(WATCH_POSITION_TOP_RIGHT, buf, buf);
sprintf(buf, "%06lu", totp_state->current_code);
watch_display_text_with_fallback(WATCH_POSITION_BOTTOM, buf, buf);
}
bool totp_lfs_face_loop(movement_event_t event, void *context) {
totp_lfs_state_t *totp_state = (totp_lfs_state_t *)context;
switch (event.event_type) {
case EVENT_TICK:
totp_state->timestamp++;
totp_face_display(totp_state);
break;
case EVENT_ACTIVATE:
totp_face_display(totp_state);
break;
case EVENT_TIMEOUT:
movement_move_to_face(0);
break;
case EVENT_ALARM_BUTTON_UP:
totp_face_set_record(totp_state, (totp_state->current_index + 1) % num_totp_records);
totp_face_display(totp_state);
break;
case EVENT_LIGHT_BUTTON_UP:
totp_face_set_record(totp_state, (totp_state->current_index + num_totp_records - 1) % num_totp_records);
totp_face_display(totp_state);
break;
case EVENT_ALARM_BUTTON_DOWN:
case EVENT_ALARM_LONG_PRESS:
case EVENT_LIGHT_BUTTON_DOWN:
break;
case EVENT_LIGHT_LONG_PRESS:
movement_illuminate_led();
break;
default:
movement_default_loop_handler(event);
break;
}
return true;
}
void totp_lfs_face_resign(void *context) {
(void) context;
}

View File

@@ -0,0 +1,77 @@
/*
* MIT License
*
* Copyright (c) 2022 Wesley Ellis (https://github.com/tahnok)
*
* 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 TOTP_FACE_LFS_H_
#define TOTP_FACE_LFS_H_
/*
* TOTP-LFS face
* Time-based one-time password (TOTP) generator using LFS
*
* Reads from a file "totp_uris.txt", containing a single secret key in a
* series of URLs. Each line is what's in a QR code, e.g.:
* otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example
* otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30
*
* This is also the same as what Aegis exports in plain-text format.
* This face performs minimal sanitisation of input, however, and you
* will only see errors if you use the simulator or view the serial console.
*
* At the moment, to get the records onto the filesystem, start a serial connection and do:
* echo otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example > totp_uris.txt
* echo otpauth://totp/ACME%20Co:john.doe@email.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30 >> totp_uris.txt
* (note the double >> in the second one)
*
* You may want to customise the characters that appear to identify the 2FA
* code. These are just the first two characters of the issuer, and it's fine
* to modify the URI.
*
* If you have more than one secret key, press ALARM to cycle through them.
* Press LIGHT to cycle in the other direction or keep it pressed longer to
* activate the light.
*/
#include "movement.h"
typedef struct {
uint32_t timestamp;
uint8_t steps;
uint32_t current_code;
uint8_t current_index;
} totp_lfs_state_t;
void totp_lfs_face_setup(uint8_t watch_face_index, void ** context_ptr);
void totp_lfs_face_activate(void *context);
bool totp_lfs_face_loop(movement_event_t event, void *context);
void totp_lfs_face_resign(void *context);
#define totp_lfs_face ((const watch_face_t){ \
totp_lfs_face_setup, \
totp_lfs_face_activate, \
totp_lfs_face_loop, \
totp_lfs_face_resign, \
NULL, \
})
#endif // TOTP_FACE_LFS_H_

View File

@@ -0,0 +1,293 @@
/*
The displayed Japanese Era can be changed by the buttons on the watch, making it also usable as a converter between the Gregorian calendar and the Japanese Era.
Light button: Subtract one year from the Japanese Era.
Start/Stop button: Add one year to the Japanese Era.
Button operations support long-press functionality.
Japanese Era Notations:
r : REIWA (令和)
h : HEISEI (平成)
s : SHOWA(昭和)
*/
#include <stdlib.h>
#include <string.h>
#include "wareki_face.h"
#include "filesystem.h"
#include "watch_utility.h"
#include "watch.h"
//Long press status flag
static bool _alarm_button_press;
static bool _light_button_press;
void wareki_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
//printf("wareki_setup() \n");
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(wareki_state_t));
memset(*context_ptr, 0, sizeof(wareki_state_t));
//debug code
// watch_date_time datetime = watch_rtc_get_date_time();
// datetime.unit.year = 2022 - WATCH_RTC_REFERENCE_YEAR;
// datetime.unit.month = 12;
// datetime.unit.day = 31;
// datetime.unit.hour = 23;
// datetime.unit.minute= 59;
// datetime.unit.second= 30;
// watch_rtc_set_date_time(datetime);
// settings->bit.clock_mode_24h = true; //24時間表記
// settings->bit.to_interval = 1;//0=60sec 1=2m 2=5m 3=30m
// watch_store_backup_data(settings->reg, 0);
}
}
// splash view
static void draw_wareki_splash(wareki_state_t *state) {
(void) state;
watch_clear_colon();
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "WA ", "wa");
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
watch_display_text(WATCH_POSITION_BOTTOM, " ");
}
//draw year and Japanese wareki
static void draw_year_and_wareki(wareki_state_t *state) {
char buf[16];
bool is_custom_lcd = watch_get_lcd_type() == WATCH_LCD_TYPE_CUSTOM;
if(state->disp_year == REIWA_GANNEN){
//The first year of Reiwa (2019) began on May 1. The period before May 1 is Heisei 31.
//In other words, 2019 is Heisei Year 31 and it is possible that it is Reiwa Year 1.
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "H31", " r");
if (is_custom_lcd)
{
//For custom LCDs, display both Heisei and Reiwa.
watch_display_text(WATCH_POSITION_TOP_RIGHT, "r1");
}
else{
watch_display_text(WATCH_POSITION_TOP_RIGHT, " 1");
}
}
else if(state->disp_year == HEISEI_GANNEN){
//The year 1989 could be Showa 64 or it could be Heisei 1.
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "S64", " h");
if (is_custom_lcd)
{
//For custom LCDs, display both Showa and Heisei.
watch_display_text(WATCH_POSITION_TOP_RIGHT, "h1");
}
else{
watch_display_text(WATCH_POSITION_TOP_RIGHT, " 1");
}
}
else if(state->disp_year < HEISEI_GANNEN){
//Showa
//sprintf(buf, " h%2d%4d ", (int)state->disp_year - HEISEI_GANNEN + 1, (int)state->disp_year);
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "Sho", "s ");
sprintf(buf, "%2d", (int)state->disp_year - SHOWA_GANNEN + 1);
watch_display_text(WATCH_POSITION_TOP_RIGHT, buf);
}
else if(state->disp_year < REIWA_GANNEN){
//Heisei
//sprintf(buf, " h%2d%4d ", (int)state->disp_year - HEISEI_GANNEN + 1, (int)state->disp_year);
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "HEI", "h ");
sprintf(buf, "%2d", (int)state->disp_year - HEISEI_GANNEN + 1);
watch_display_text(WATCH_POSITION_TOP_RIGHT, buf);
}
else{
//Reiwa
//sprintf(buf, " r%2d%4d ", (int)state->disp_year - REIWA_GANNEN + 1 , (int)state->disp_year);
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "REI", "r ");
sprintf(buf, "%2d", (int)state->disp_year - REIWA_GANNEN + 1);
watch_display_text(WATCH_POSITION_TOP_RIGHT, buf);
}
sprintf(buf, "%4d ",(int)state->disp_year);
watch_display_text(WATCH_POSITION_BOTTOM, buf);
}
void wareki_activate(void *context) {
//printf("wareki_activate() \n");
wareki_state_t *state = (wareki_state_t *)context;
if (watch_sleep_animation_is_running()) watch_stop_sleep_animation();
state->active = false;
_alarm_button_press = false;
_light_button_press = false;
state->real_year = watch_rtc_get_date_time().unit.year + WATCH_RTC_REFERENCE_YEAR;
state->start_year = state->real_year;
state->disp_year = state->real_year;
movement_request_tick_frequency(1);
}
void addYear(wareki_state_t* state,int count){
state->disp_year = state->disp_year + count;
if(state->disp_year > REIWA_LIMIT ){
state->disp_year = REIWA_LIMIT;
}
else{
}
}
void subYear(wareki_state_t* state,int count){
state->disp_year = state->disp_year - count;
if(state->disp_year < SHOWA_GANNEN ){
state->disp_year = SHOWA_GANNEN;
}
else{
}
}
bool wareki_loop(movement_event_t event, void *context) {
wareki_state_t *state = (wareki_state_t *)context;
state->real_year = watch_rtc_get_date_time().unit.year + WATCH_RTC_REFERENCE_YEAR;
if( state->real_year != state->start_year ){
state->start_year = state->real_year;
state->disp_year = state->real_year;
}
switch (event.event_type) {
case EVENT_ACTIVATE:
draw_wareki_splash(state);
break;
case EVENT_MODE_BUTTON_UP:
movement_move_to_next_face();
break;
case EVENT_LOW_ENERGY_UPDATE:
case EVENT_TICK:
//printf("tick %d\n",state->disp_year );
if (_alarm_button_press && HAL_GPIO_BTN_ALARM_pin() ){
//printf("ALARM ON\n");
}
else{
//printf("ALARM OFF\n");
_alarm_button_press = false;
}
if (_light_button_press && HAL_GPIO_BTN_LIGHT_pin()){
//printf("LIGHT ON\n");
}
else{
//printf("LIGHT OFF\n");
_light_button_press = false;
}
if (_alarm_button_press) {
addYear(state,1);
}
if (_light_button_press) {
subYear(state,1);
}
draw_year_and_wareki(state);
break;
case EVENT_LIGHT_BUTTON_DOWN:
//printf("LIGHT DOWN\n");
subYear(state,1);
break;
case EVENT_LIGHT_LONG_PRESS:
//printf("LIGHTPRESS \n");
_light_button_press = true;
movement_request_tick_frequency(8);
break;
case EVENT_LIGHT_LONG_UP:
//printf("LIGHTPRESS UP\n");
_light_button_press = false;
movement_request_tick_frequency(4);
break;
case EVENT_LIGHT_BUTTON_UP:
//printf("LIGHT UP\n");
_light_button_press = false;
movement_request_tick_frequency(4);
break;
case EVENT_ALARM_BUTTON_DOWN:
//printf("ALARM DOWN\n");
addYear(state,1);
break;
case EVENT_ALARM_LONG_PRESS:
//printf("LONGPRESS \n");
_alarm_button_press = true;
movement_request_tick_frequency(8);
break;
case EVENT_ALARM_LONG_UP:
//printf("LONGPRESS UP\n");
_alarm_button_press = false;
movement_request_tick_frequency(4);
break;
case EVENT_ALARM_BUTTON_UP:
//printf("ALARM UP\n");
movement_request_tick_frequency(4);
break;
case EVENT_TIMEOUT:
//printf("time out ! \n");
movement_move_to_face(0);
break;
//case EVENT_LOW_ENERGY_UPDATE:
// If you did not resign in EVENT_TIMEOUT, you can use this event to update the display once a minute.
// Avoid displaying fast-updating values like seconds, since the display won't update again for 60 seconds.
// You should also consider starting the tick animation, to show the wearer that this is sleep mode:
// watch_start_tick_animation(500);
//break;
default:
// Movement's default loop handler will step in for any cases you don't handle above:
// * EVENT_LIGHT_BUTTON_DOWN lights the LED
// * EVENT_MODE_BUTTON_UP moves to the next watch face in the list
// * EVENT_MODE_LONG_PRESS returns to the first watch face (or skips to the secondary watch face, if configured)
// You can override any of these behaviors by adding a case for these events to this switch statement.
//return movement_default_loop_handler(event, settings);
return movement_default_loop_handler(event);
}
return true;
}
void wareki_resign(void *context) {
(void) context;
}

View File

@@ -0,0 +1,35 @@
#ifndef WAREKI_FACE_H_
#define WAREKI_FACE_H_
#include "movement.h"
#define REIWA_LIMIT 2018 + 99
#define REIWA_GANNEN 2019
#define HEISEI_GANNEN 1989
#define SHOWA_GANNEN 1926
typedef struct {
bool active;
uint32_t disp_year; //Current displayed year
uint32_t start_year; //Year when this screen was launched
uint32_t real_year; //The actual current year
} wareki_state_t;
void wareki_setup(uint8_t watch_face_index, void ** context_ptr);
void wareki_activate(void *context);
bool wareki_loop(movement_event_t event, void *context);
void wareki_resign(void *context);
void addYear(wareki_state_t* state,int count);
void subYear(wareki_state_t* state,int count);
#define wareki_face ((const watch_face_t){ \
wareki_setup, \
wareki_activate, \
wareki_loop, \
wareki_resign, \
NULL, \
})
#endif // WAREKI_FACE_H_

View File

@@ -0,0 +1,648 @@
/*
* 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 "wordle_face.h"
#include "watch_utility.h"
#include "watch_common_display.h"
static uint32_t get_random(uint32_t max) {
#if __EMSCRIPTEN__
return rand() % max;
#else
return arc4random_uniform(max);
#endif
}
static uint8_t get_first_pos(wordle_letter_result *word_elements_result) {
for (size_t i = 0; i < WORDLE_LENGTH; i++) {
if (word_elements_result[i] != WORDLE_LETTER_CORRECT)
return i;
}
return 0;
}
static uint8_t get_next_pos(uint8_t curr_pos, wordle_letter_result *word_elements_result) {
for (size_t pos = curr_pos; pos < WORDLE_LENGTH;) {
if (word_elements_result[++pos] != WORDLE_LETTER_CORRECT)
return pos;
}
return WORDLE_LENGTH;
}
static uint8_t get_prev_pos(uint8_t curr_pos, wordle_letter_result *word_elements_result) {
if (curr_pos == 0) return 0;
for (int8_t pos = curr_pos; pos >= 0;) {
if (word_elements_result[--pos] != WORDLE_LETTER_CORRECT)
return pos;
}
return curr_pos;
}
static void get_next_letter(const uint8_t curr_pos, uint8_t *word_elements, const bool *known_wrong_letters, const bool skip_wrong_letter) {
do {
if (word_elements[curr_pos] >= WORDLE_NUM_VALID_LETTERS) word_elements[curr_pos] = 0;
else word_elements[curr_pos] = (word_elements[curr_pos] + 1) % WORDLE_NUM_VALID_LETTERS;
} while (skip_wrong_letter && known_wrong_letters[word_elements[curr_pos]]);
}
static void get_prev_letter(const uint8_t curr_pos, uint8_t *word_elements, const bool *known_wrong_letters, const bool skip_wrong_letter) {
do {
if (word_elements[curr_pos] >= WORDLE_NUM_VALID_LETTERS) word_elements[curr_pos] = WORDLE_NUM_VALID_LETTERS - 1;
else word_elements[curr_pos] = (word_elements[curr_pos] + WORDLE_NUM_VALID_LETTERS - 1) % WORDLE_NUM_VALID_LETTERS;
} while (skip_wrong_letter && known_wrong_letters[word_elements[curr_pos]]);
}
static void display_letter(wordle_state_t *state, bool display_dash) {
char buf[3];
if (state->word_elements[state->position] >= WORDLE_NUM_VALID_LETTERS) {
if (display_dash)
watch_display_character('-', state->position + 5);
else
watch_display_character(' ', state->position + 5);
return;
}
sprintf(buf, "%c", _valid_letters[state->word_elements[state->position]]);
watch_display_character(buf[0], state->position + 5);
}
static void display_all_letters(wordle_state_t *state) {
uint8_t prev_pos = state->position;
watch_display_character(' ', 4);
for (size_t i = 0; i < WORDLE_LENGTH; i++) {
state->position = i;
display_letter(state, false);
}
state->position = prev_pos;
}
#if !WORDLE_ALLOW_NON_WORD_AND_REPEAT_GUESSES
static void display_not_in_dict(wordle_state_t *state) {
state->curr_screen = WORDLE_SCREEN_NO_DICT;
watch_display_text(WATCH_POSITION_BOTTOM, "nodict");
state->ignore_btn_ticks = WORDLE_TICK_BAD_GUESS;
}
static void display_already_guessed(wordle_state_t *state) {
state->curr_screen = WORDLE_SCREEN_ALREADY_GUESSED;
watch_display_text(WATCH_POSITION_BOTTOM, "GUESSD");
state->ignore_btn_ticks = WORDLE_TICK_BAD_GUESS;
}
static uint32_t check_word_in_dict(uint8_t *word_elements) {
bool is_exact_match;
for (uint16_t i = 0; i < WORDLE_NUM_WORDS; i++) {
is_exact_match = true;
for (size_t j = 0; j < WORDLE_LENGTH; j++) {
if (_valid_letters[word_elements[j]] != _valid_words[i][j]) {
is_exact_match = false;
break;
}
}
if (is_exact_match) return i;
}
for (uint16_t i = 0; i < WORDLE_NUM_POSSIBLE_WORDS; i++) {
is_exact_match = true;
for (size_t j = 0; j < WORDLE_LENGTH; j++) {
if (_valid_letters[word_elements[j]] != _possible_words[i][j]) {
is_exact_match = false;
break;
}
}
if (is_exact_match) return WORDLE_NUM_WORDS + i;
}
return WORDLE_NUM_WORDS + WORDLE_NUM_POSSIBLE_WORDS;
}
#endif
static bool check_word(wordle_state_t *state) {
// Exact
bool is_exact_match = true;
bool answer_already_accounted[WORDLE_LENGTH] = { false };
for (size_t i = 0; i < WORDLE_LENGTH; i++) {
if (_valid_letters[state->word_elements[i]] == _valid_words[state->curr_answer][i]) {
state->word_elements_result[i] = WORDLE_LETTER_CORRECT;
answer_already_accounted[i] = true;
}
else {
state->word_elements_result[i] = WORDLE_LETTER_WRONG;
is_exact_match = false;
}
}
if (is_exact_match) return true;
// Wrong Location
for (size_t i = 0; i < WORDLE_LENGTH; i++) {
if (state->word_elements_result[i] != WORDLE_LETTER_WRONG) continue;
for (size_t j = 0; j < WORDLE_LENGTH; j++) {
if (answer_already_accounted[j]) continue;
if (_valid_letters[state->word_elements[i]] == _valid_words[state->curr_answer][j]) {
state->word_elements_result[i] = WORDLE_LETTER_WRONG_LOC;
answer_already_accounted[j] = true;
break;
}
}
}
return false;
}
static void show_skip_wrong_letter_indicator(bool skipping, wordle_screen curr_screen) {
if (curr_screen >= WORDLE_SCREEN_PLAYING) return;
if (skipping)
watch_display_character('H', 3);
else
watch_display_character(' ', 3);
}
static void update_known_wrong_letters(wordle_state_t *state) {
bool wrong_loc[WORDLE_NUM_VALID_LETTERS] = {false};
// To ignore letters that appear, but are in the wrong location, as letters that are guessed
// more often than they appear in the word will display as WORDLE_LETTER_WRONG
for (size_t i = 0; i < WORDLE_LENGTH; i++) {
if (state->word_elements_result[i] == WORDLE_LETTER_WRONG_LOC) {
for (size_t j = 0; j < WORDLE_NUM_VALID_LETTERS; j++) {
if (state->word_elements[i] == j)
wrong_loc[j] = true;
}
}
}
for (size_t i = 0; i < WORDLE_LENGTH; i++) {
if (state->word_elements_result[i] == WORDLE_LETTER_WRONG) {
for (size_t j = 0; j < WORDLE_NUM_VALID_LETTERS; j++) {
if (state->word_elements[i] == j && !wrong_loc[j])
state->known_wrong_letters[j] = true;
}
}
}
}
static void display_attempt(uint8_t attempt) {
char buf[3];
sprintf(buf, "%d", attempt+1);
watch_display_character(buf[0], 3);
}
static void display_playing(wordle_state_t *state) {
state->curr_screen = WORDLE_SCREEN_PLAYING;
display_attempt(state->attempt);
display_all_letters(state);
}
static void reset_all_elements(wordle_state_t *state) {
for (size_t i = 0; i < WORDLE_LENGTH; i++) {
state->word_elements[i] = WORDLE_NUM_VALID_LETTERS;
state->word_elements_result[i] = WORDLE_LETTER_WRONG;
}
for (size_t i = 0; i < WORDLE_NUM_VALID_LETTERS; i++){
state->known_wrong_letters[i] = false;
}
#if !WORDLE_ALLOW_NON_WORD_AND_REPEAT_GUESSES
for (size_t i = 0; i < WORDLE_MAX_ATTEMPTS; i++) {
state->guessed_words[i] = WORDLE_NUM_WORDS + WORDLE_NUM_POSSIBLE_WORDS;
}
#endif
state->using_random_guess = false;
state->attempt = 0;
}
static void reset_incorrect_elements(wordle_state_t *state) {
for (size_t i = 0; i < WORDLE_LENGTH; i++) {
if (state->word_elements_result[i] != WORDLE_LETTER_CORRECT)
state->word_elements[i] = WORDLE_NUM_VALID_LETTERS;
}
}
static bool is_in_do_not_use_list(uint16_t guess, const uint16_t *not_to_use, uint8_t max_repeats) {
for (size_t i = 0; i < max_repeats; i++) {
if (guess == not_to_use[i]) return true;
}
return false;
}
static void reset_board(wordle_state_t *state) {
reset_all_elements(state);
do {
state->curr_answer = get_random(WORDLE_NUM_WORDS);
} while (is_in_do_not_use_list(state->curr_answer, state->not_to_use, WORDLE_MAX_BETWEEN_REPEATS));
watch_clear_colon();
state->position = get_first_pos(state->word_elements_result);
display_playing(state);
watch_display_character('-', 5);
#if __EMSCRIPTEN__
printf("ANSWER: %s\r\n", _valid_words[state->curr_answer]);
#endif
}
static void display_title(wordle_state_t *state) {
state->curr_screen = WORDLE_SCREEN_TITLE;
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "Wdl", "WO");
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
watch_display_text(WATCH_POSITION_BOTTOM, "WordLE");
show_skip_wrong_letter_indicator(state->skip_wrong_letter, state->curr_screen);
}
#if WORDLE_USE_DAILY_STREAK != 2
static void display_continue_result(bool continuing) {
watch_display_character(continuing ? 'y' : 'n', 9);
}
static void display_continue(wordle_state_t *state) {
state->curr_screen = WORDLE_SCREEN_CONTINUE;
watch_display_text(WATCH_POSITION_BOTTOM, "Cont ");
show_skip_wrong_letter_indicator(state->skip_wrong_letter, state->curr_screen);
display_continue_result(state->continuing);
}
#endif
static void display_streak(wordle_state_t *state) {
char buf[10];
state->curr_screen = WORDLE_SCREEN_STREAK;
#if WORDLE_USE_DAILY_STREAK == 2
if (state->streak > 99)
sprintf(buf, "St--dy");
else
sprintf(buf, "St%2ddy", state->streak);
#else
sprintf(buf, "St%4d", state->streak);
#endif
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "Wdl", "WO");
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
watch_display_text(WATCH_POSITION_BOTTOM, buf);
watch_set_colon();
show_skip_wrong_letter_indicator(state->skip_wrong_letter, state->curr_screen);
}
#if WORDLE_USE_DAILY_STREAK == 2
static void display_wait(wordle_state_t *state) {
state->curr_screen = WORDLE_SCREEN_WAIT;
if (state->streak < 40) {
char buf[5];
sprintf(buf,"%2d", state->streak);
watch_display_text(WATCH_POSITION_TOP_RIGHT, buf);
}
else { // Streak too long to display in top-right
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
}
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "Wdl", "WO");
watch_display_text(WATCH_POSITION_BOTTOM, " WaIt ");
show_skip_wrong_letter_indicator(state->skip_wrong_letter, state->curr_screen);
}
#endif
static uint32_t get_day_unix_time(void) {
watch_date_time_t now = watch_rtc_get_date_time();
#if WORDLE_USE_DAILY_STREAK == 2
now.unit.hour = now.unit.minute = now.unit.second = 0;
#endif
return watch_utility_date_time_to_unix_time(now, 0);
}
static void display_lose(wordle_state_t *state, uint8_t subsecond) {
char buf[10];
sprintf(buf," %s", subsecond % 2 ? _valid_words[state->curr_answer] : " ");
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
watch_display_text_with_fallback(WATCH_POSITION_TOP, "LOSE", "L ");
watch_display_text(WATCH_POSITION_BOTTOM, buf);
}
static void display_win(wordle_state_t *state, uint8_t subsecond) {
(void) state;
char buf[10];
sprintf(buf," %s ", subsecond % 2 ? "NICE" : "JOb ");
watch_display_text(WATCH_POSITION_TOP_RIGHT, " ");
watch_display_text_with_fallback(WATCH_POSITION_TOP_LEFT, "WIN", "W ");
watch_display_text(WATCH_POSITION_BOTTOM, buf);
}
static bool is_playing(const wordle_state_t *state) {
if (state->attempt > 0) return true;
for (size_t i = 0; i < WORDLE_LENGTH; i++) {
if (state->word_elements[i] != WORDLE_NUM_VALID_LETTERS)
return true;
}
return false;
}
static void display_result(wordle_state_t *state, uint8_t subsecond) {
char buf[10];
buf[0] = ' ';
for (size_t i = 0; i < WORDLE_LENGTH; i++)
{
switch (state->word_elements_result[i])
{
case WORDLE_LETTER_WRONG:
buf[i+1] = '-';
break;
case WORDLE_LETTER_CORRECT:
buf[i+1] = _valid_letters[state->word_elements[i]];
break;
case WORDLE_LETTER_WRONG_LOC:
if (subsecond % 2)
buf[i+1] = ' ';
else
buf[i+1] = _valid_letters[state->word_elements[i]];
default:
break;
}
}
watch_display_text(WATCH_POSITION_BOTTOM, buf);
}
static bool act_on_btn(wordle_state_t *state, const wordle_pin_enum pin) {
if (state->ignore_btn_ticks > 0) return true;
switch (state->curr_screen)
{
case WORDLE_SCREEN_RESULT:
reset_incorrect_elements(state);
state->position = get_first_pos(state->word_elements_result);
display_playing(state);
return true;
case WORDLE_SCREEN_TITLE:
#if WORDLE_USE_DAILY_STREAK == 2
if (state->day_last_game_started == get_day_unix_time()) {
display_wait(state);
}
else if (is_playing(state))
display_playing(state);
else
display_streak(state);
#else
if (is_playing(state)) {
state->continuing = true;
display_continue(state);
}
else
display_streak(state);
#endif
return true;
case WORDLE_SCREEN_STREAK:
state->day_last_game_started = get_day_unix_time();
reset_board(state);
return true;
case WORDLE_SCREEN_WIN:
case WORDLE_SCREEN_LOSE:
display_title(state);
return true;
case WORDLE_SCREEN_NO_DICT:
case WORDLE_SCREEN_ALREADY_GUESSED:
state->position = get_first_pos(state->word_elements_result);
display_playing(state);
return true;
#if WORDLE_USE_DAILY_STREAK == 2
case WORDLE_SCREEN_WAIT:
(void) pin;
display_title(state);
return true;
#else
case WORDLE_SCREEN_CONTINUE:
switch (pin)
{
case BTN_ALARM:
if (state->continuing)
display_playing(state);
else {
reset_board(state);
state->streak = 0;
display_streak(state);
}
break;
case BTN_LIGHT:
state->continuing = !state->continuing;
display_continue_result(state->continuing);
break;
default:
break;
}
return true;
#endif
default:
return false;
}
return false;
}
static void win_lose_shared(wordle_state_t *state) {
reset_all_elements(state);
state->ignore_btn_ticks = WORDLE_TICK_WIN_LOSE;
state->not_to_use[state->not_to_use_position] = state->curr_answer;
state->not_to_use_position = (state->not_to_use_position + 1) % WORDLE_MAX_BETWEEN_REPEATS;
}
static void get_result(wordle_state_t *state) {
#if !WORDLE_ALLOW_NON_WORD_AND_REPEAT_GUESSES
// Check if it's in the dict
uint16_t in_dict = check_word_in_dict(state->word_elements);
if (in_dict == WORDLE_NUM_WORDS + WORDLE_NUM_POSSIBLE_WORDS) {
display_not_in_dict(state);
return;
}
// Check if already guessed
for (size_t i = 0; i < WORDLE_MAX_ATTEMPTS; i++) {
if(in_dict == state->guessed_words[i]) {
display_already_guessed(state);
return;
}
}
state->guessed_words[state->attempt] = in_dict;
#endif
bool exact_match = check_word(state);
if (exact_match) {
state->curr_screen = WORDLE_SCREEN_WIN;
win_lose_shared(state);
if (state->streak < 0x7F)
state->streak++;
#if WORDLE_USE_DAILY_STREAK == 2
state->day_last_game_started = get_day_unix_time(); // On the edge-case where we solve the puzzle at midnight
#endif
return;
}
if (++state->attempt >= WORDLE_MAX_ATTEMPTS) {
state->curr_screen = WORDLE_SCREEN_LOSE;
win_lose_shared(state);
state->streak = 0;
return;
}
update_known_wrong_letters(state);
state->curr_screen = WORDLE_SCREEN_RESULT;
state->ignore_btn_ticks = WORDLE_TICKS_RESULT;
return;
}
#if (WORDLE_USE_RANDOM_GUESS != 0)
static void insert_random_guess(wordle_state_t *state) {
uint16_t random_guess;
do { // Don't allow the guess to be the same as the answer
random_guess = get_random(_num_random_guess_words);
} while (random_guess == state->curr_answer);
for (size_t i = 0; i < WORDLE_LENGTH; i++) {
for (size_t j = 0; j < WORDLE_NUM_VALID_LETTERS; j++)
{
if (_valid_words[random_guess][i] == _valid_letters[j])
state->word_elements[i] = j;
}
}
state->position = WORDLE_LENGTH - 1;
display_all_letters(state);
state->using_random_guess = true;
}
#endif
static void _activate(wordle_state_t *state) {
#if WORDLE_USE_DAILY_STREAK != 0
uint32_t now = get_day_unix_time();
uint32_t one_day = 60 *60 * 24;
if ((WORDLE_USE_DAILY_STREAK == 2 && now >= (state->day_last_game_started + (2*one_day)))
|| (now >= (state->day_last_game_started + one_day) && is_playing(state))) {
state->streak = 0;
reset_board(state);
}
#endif
state->using_random_guess = false;
if (is_playing(state) && state->curr_screen >= WORDLE_SCREEN_RESULT) {
reset_incorrect_elements(state);
state->position = get_first_pos(state->word_elements_result);
}
movement_request_tick_frequency(WORDLE_FREQ);
watch_clear_all_indicators();
watch_clear_colon();
display_title(state);
}
void wordle_face_setup(uint8_t watch_face_index, void ** context_ptr) {
(void) watch_face_index;
if (*context_ptr == NULL) {
*context_ptr = malloc(sizeof(wordle_state_t));
memset(*context_ptr, 0, sizeof(wordle_state_t));
wordle_state_t *state = (wordle_state_t *)*context_ptr;
state->curr_screen = WORDLE_SCREEN_TITLE;
state->skip_wrong_letter = true;
reset_all_elements(state);
memset(state->not_to_use, 0xff, sizeof(state->not_to_use));
}
// Do any pin or peripheral setup here; this will be called whenever the watch wakes from deep sleep.
}
void wordle_face_activate(void *context) {
wordle_state_t *state = (wordle_state_t *)context;
_activate(state);
}
bool wordle_face_loop(movement_event_t event, void *context) {
wordle_state_t *state = (wordle_state_t *)context;
switch (event.event_type) {
case EVENT_TICK:
if (state->ignore_btn_ticks > 0) state->ignore_btn_ticks--;
switch (state->curr_screen)
{
case WORDLE_SCREEN_PLAYING:
if (event.subsecond % 2) {
display_letter(state, true);
} else {
watch_display_character(' ', state->position + 5);
}
break;
case WORDLE_SCREEN_RESULT:
display_result(state, event.subsecond);
break;
case WORDLE_SCREEN_LOSE:
display_lose(state, event.subsecond);
break;
case WORDLE_SCREEN_WIN:
display_win(state, event.subsecond);
break;
default:
break;
}
break;
case EVENT_LIGHT_BUTTON_UP:
if (act_on_btn(state, BTN_LIGHT)) break;
get_next_letter(state->position, state->word_elements, state->known_wrong_letters, state->skip_wrong_letter);
display_letter(state, true);
break;
case EVENT_LIGHT_LONG_PRESS:
if (state->curr_screen < WORDLE_SCREEN_PLAYING) {
state->skip_wrong_letter = !state->skip_wrong_letter;
show_skip_wrong_letter_indicator(state->skip_wrong_letter, state->curr_screen);
break;
}
if (state->curr_screen != WORDLE_SCREEN_PLAYING) break;
get_prev_letter(state->position, state->word_elements, state->known_wrong_letters, state->skip_wrong_letter);
display_letter(state, true);
break;
case EVENT_ALARM_BUTTON_UP:
if (act_on_btn(state, BTN_ALARM)) break;
display_letter(state, true);
if (state->word_elements[state->position] == WORDLE_NUM_VALID_LETTERS) break;
#if (WORDLE_USE_RANDOM_GUESS != 0)
if (HAL_GPIO_BTN_LIGHT_read() &&
(state->using_random_guess || (state->attempt == 0 && state->position == 0))) {
insert_random_guess(state);
break;
}
#endif
state->position = get_next_pos(state->position, state->word_elements_result);
if (state->position >= WORDLE_LENGTH) {
get_result(state);
state->using_random_guess = false;
}
break;
case EVENT_ALARM_LONG_PRESS:
if (state->curr_screen != WORDLE_SCREEN_PLAYING) break;
display_letter(state, true);
state->position = get_prev_pos(state->position, state->word_elements_result);
break;
case EVENT_LIGHT_BUTTON_DOWN:
case EVENT_ACTIVATE:
break;
case EVENT_TIMEOUT:
if (state->curr_screen >= WORDLE_SCREEN_RESULT) {
reset_incorrect_elements(state);
state->position = get_first_pos(state->word_elements_result);
display_title(state);
}
break;
case EVENT_LOW_ENERGY_UPDATE:
if (state->curr_screen != WORDLE_SCREEN_TITLE)
display_title(state);
break;
case EVENT_MODE_LONG_PRESS:
if (state->curr_screen >= WORDLE_SCREEN_PLAYING) {
_activate(state);
} else {
movement_move_to_face(0);
}
break;
default:
return movement_default_loop_handler(event);
}
return true;
}
void wordle_face_resign(void *context) {
(void) context;
}

View File

@@ -0,0 +1,165 @@
/*
* 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 WORDLE_FACE_H_
#define WORDLE_FACE_H_
#include "movement.h"
/*
* Wordle Face
* A port of NY Times' Wordle game (https://www.nytimes.com/games/wordle/index.html)
* A random 5 letter word is chosen and you have WORDLE_MAX_ATTEMPTS attempts to guess it.
* Each guess must be a valid 5-letter word found in _legal_words in the C file.
* The only letters used are _valid_letters, also found in the C file.
* After a guess, the letters in the correct spot will remain,
* and the letters found in the word, but in the incorrect spot will blink.
* The screen after the title screen if a new game is started shows the streak of games won in a row.
*
* If WORDLE_USE_DAILY_STREAK is set to True, then the game can only be played once per day,
* and the streak resets to 0 if a day goes by without playing the game.
*
* Controls:
* Light Press
* If Playing: Next letter
* Else: Next screen
* Light Hold
* If Playing: Previous letter
* Else: Toggle Hard-Mode. This is skipping over letters that have been confirmed
* to not be in the word (indicated via 'H' in the top-right)
*
* Alarm Press
* If Playing: If WORDLE_USE_RANDOM_GUESS is set and Light btn held and
* (on first letter or already used a random guess)
* and first attempt: Use a random 5 letter word with all letters that are different.
* Else: Next position
* Else: Next screen
* Alarm Hold
* If Playing: Previous position
* Else: None
*
* Note: Actual Hard Mode in Wordle game is "Any revealed hints must be used in subsequent guesses"
* But that came off as clunky UX on the Casio. So instead it only removes unused letters from the keyboard
* as that also simplifies the keyboard.
*/
#define WORDLE_LENGTH 5
#define WORDLE_MAX_ATTEMPTS 6
/* WORDLE_USE_DAILY_STREAK
* 0 = Don't ever reset the streak or the puzzle.
* 1 = Reset the streak and puzzle 24hrs after starting a puzzle and not finishing it.
* If the last puzzle was started at 8AM, it'll be considered failed at 8AM the next day.
* 2 = Reset the streak and puzzle if a puzzle goes unsolved or not started a day after the previous one.
* If the last puzzle was started at 8AM, it'll be considered failed at midnight the next day.
* This will not be the case if the puzzle is started at 8AM, continued at 11:59PM and solved at 12:01AM, the game will let that slide.
* Starting a new game instead of continuing is not allowed in this state.
*/
#define WORDLE_USE_DAILY_STREAK 1
#define WORDLE_ALLOW_NON_WORD_AND_REPEAT_GUESSES false // This allows non-words to be entered and repeat guesses to be made. It saves ~11.5KB of ROM.
/* WORDLE_USE_RANDOM_GUESS
* 0 = Don't allow quickly choosing a random quess
* 1 = Allow using a random guess of any value that can be an answer
* 2 = Allow using a random guess of any value that can be an answer where all of its letters are unique
* 3 = Allow using a random guess of any value that can be an answer, and it's considered one of the best initial choices.
*/
#define WORDLE_USE_RANDOM_GUESS 3
#define WORDLE_FREQ 2
// To avoid a button press immedietly skipping a screen, we wait this many ticks
#define WORDLE_TICKS_RESULT 4
#define WORDLE_TICK_WIN_LOSE 2
#define WORDLE_TICK_BAD_GUESS 0
// Store this many words in our list of words that were already used to avoid too much repetition of guesses
#define WORDLE_MAX_BETWEEN_REPEATS 50
#include "wordle_face_dict.h"
#define WORDLE_NUM_WORDS (sizeof(_valid_words) / sizeof(_valid_words[0]))
#define WORDLE_NUM_POSSIBLE_WORDS (sizeof(_possible_words) / sizeof(_possible_words[0]))
#define WORDLE_NUM_VALID_LETTERS (sizeof(_valid_letters) / sizeof(_valid_letters[0]))
typedef enum {
WORDLE_LETTER_WRONG = 0,
WORDLE_LETTER_WRONG_LOC,
WORDLE_LETTER_CORRECT,
WORDLE_LETTER_COUNT
} wordle_letter_result;
typedef enum {
WORDLE_SCREEN_TITLE = 0,
WORDLE_SCREEN_STREAK,
WORDLE_SCREEN_CONTINUE,
#if WORDLE_USE_DAILY_STREAK
WORDLE_SCREEN_WAIT,
#endif
WORDLE_SCREEN_PLAYING,
WORDLE_SCREEN_RESULT,
WORDLE_SCREEN_WIN,
WORDLE_SCREEN_LOSE,
WORDLE_SCREEN_NO_DICT,
WORDLE_SCREEN_ALREADY_GUESSED,
WORDLE_SCREEN_COUNT
} wordle_screen;
typedef enum {
BTN_MODE = 0,
BTN_ALARM,
BTN_LIGHT,
} wordle_pin_enum;
typedef struct {
// Anything you need to keep track of, put it here!
uint8_t word_elements[WORDLE_LENGTH];
wordle_letter_result word_elements_result[WORDLE_LENGTH];
#if !WORDLE_ALLOW_NON_WORD_AND_REPEAT_GUESSES
uint16_t guessed_words[WORDLE_MAX_ATTEMPTS];
#endif
uint8_t attempt : 4;
uint8_t position : 3;
bool using_random_guess : 1;
uint16_t curr_answer : 14;
bool continuing : 1;
bool skip_wrong_letter : 1;
uint8_t streak;
wordle_screen curr_screen;
bool known_wrong_letters[WORDLE_NUM_VALID_LETTERS];
uint32_t day_last_game_started;
uint8_t ignore_btn_ticks;
uint16_t not_to_use[WORDLE_MAX_BETWEEN_REPEATS];
uint8_t not_to_use_position;
} wordle_state_t;
void wordle_face_setup(uint8_t watch_face_index, void ** context_ptr);
void wordle_face_activate(void *context);
bool wordle_face_loop(movement_event_t event, void *context);
void wordle_face_resign(void *context);
#define wordle_face ((const watch_face_t){ \
wordle_face_setup, \
wordle_face_activate, \
wordle_face_loop, \
wordle_face_resign, \
NULL, \
})
#endif // WORDLE_FACE_H_

View File

@@ -0,0 +1,293 @@
#ifndef WORDLE_FACE_DICT_H_
#define WORDLE_FACE_DICT_H_
#ifndef WORDLE_LENGTH
#define WORDLE_LENGTH 5
#endif
#ifndef WORDLE_USE_RANDOM_GUESS
#define WORDLE_USE_RANDOM_GUESS 2
#endif
static const char _valid_letters[] = {'A', 'C', 'E', 'H', 'I', 'L', 'N', 'O', 'P', 'R', 'S', 'T'};
// From: https://matthewminer.name/projects/calculators/wordle-words-left/
// Number of words found: 432
static const char _valid_words[][WORDLE_LENGTH + 1] = {
"SLATE", "STARE", "SNARE", "SANER", "CRANE", "STALE", "CRATE", "RAISE", "TRACE",
"SHARE", "ARISE", "SCARE", "SPARE", "CHAOS", "TAPIR", "CAIRN", "TENOR", "CLEAN",
"HEART", "SCOPE", "SNARL", "SLEPT", "SINCE", "EPOCH", "SPACE", "RELIC", "SPOIL",
"LITER", "LEAPT", "LANCE", "RANCH", "HORSE", "LEACH", "LATER", "STEAL", "CHEAP",
"SHORT", "ETHIC", "CHANT", "ACTOR", "REACH", "SEPIA", "ONSET", "SPLAT", "LEANT",
"REACT", "OCTAL", "SPORE", "IRATE", "CORAL", "NICER", "SPILT", "SCENT", "PANIC",
"SHIRT", "PECAN", "SLAIN", "SPLIT", "ROACH", "ASCOT", "PHONE", "LITHE", "STOIC",
"STRIP", "RENAL", "POISE", "ENACT", "CHEAT", "PITCH", "NOISE", "INLET", "PEARL",
"POLAR", "PEACH", "STOLE", "CASTE", "CREST", "CRONE", "ETHOS", "THEIR", "STONE",
"SHIRE", "LATCH", "HASTE", "CLOSE", "SPINE", "SLANT", "SPEAR", "SCALE", "CAPER",
"RETCH", "PESTO", "CHIRP", "SPORT", "OPTIC", "SNAIL", "PRICE", "PLANE", "TORCH",
"PASTE", "RECAP", "SOLAR", "CRASH", "LINER", "OPINE", "ASHEN", "PALER", "ECLAT",
"SPELT", "TRIAL", "PERIL", "SLICE", "SCANT", "SAINT", "POSIT", "ATONE", "SPIRE",
"COAST", "INEPT", "SHOAL", "CLASH", "THORN", "PHASE", "SCORE", "TRICE", "PERCH",
"PORCH", "SHEAR", "CHOIR", "RHINO", "PLANT", "SHONE", "CHORE", "LEARN", "ALTER",
"CHAIN", "PANEL", "PLIER", "STEIN", "COPSE", "SONIC", "ALIEN", "CHOSE", "ACORN",
"ANTIC", "CHEST", "OTHER", "CHINA", "TALON", "SCORN", "PLAIN", "PILOT", "RIPEN",
"PATCH", "SPICE", "CLONE", "SCION", "SCONE", "STRAP", "PARSE", "SHALE", "RISEN",
"CANOE", "INTER", "LEASH", "ISLET", "PRINT", "SHINE", "NORTH", "CLEAT", "PLAIT",
"SCRAP", "CLEAR", "SLOTH", "LAPSE", "CHAIR", "SNORT", "SHARP", "OPERA", "STAIN",
"TEACH", "TRAIL", "TRAIN", "LATHE", "PIANO", "PINCH", "PETAL", "STERN", "PRONE",
"PROSE", "PLEAT", "TROPE", "PLACE", "POSER", "INERT", "CHASE", "CAROL", "STAIR",
"SATIN", "SPITE", "LOATH", "ROAST", "ARSON", "SHAPE", "CLASP", "LOSER", "SALON",
"CATER", "SHALT", "INTRO", "ALERT", "PENAL", "SHORE", "RINSE", "CREPT", "APRON",
"SONAR", "AISLE", "AROSE", "HATER", "NICHE", "POINT", "EARTH", "PINTO", "THOSE",
"CLOTH", "NOTCH", "TOPIC", "RESIN", "SCALP", "HEIST", "HERON", "TRIPE", "TONAL",
"TAPER", "SHORN", "TONIC", "HOIST", "SNORE", "STORE", "SLOPE", "OCEAN", "CHART",
"PAINT", "SPENT", "SNIPE", "CRISP", "TRASH", "PATIO", "PLATE", "HOTEL", "LEAST",
"ALONE", "RALPH", "SPIEL", "SIREN", "RATIO", "STOOP", "TROLL", "ATOLL", "SLASH",
"RETRO", "CREEP", "STILT", "SPREE", "TASTE", "CACHE", "CANON", "EATEN", "TEPEE",
"SHEET", "SNEER", "ERROR", "NATAL", "SLEEP", "STINT", "TROOP", "SHALL", "STALL",
"PIPER", "TOAST", "NASAL", "CORER", "THERE", "POOCH", "SCREE", "ELITE", "ALTAR",
"PENCE", "EATER", "ALPHA", "TENTH", "LINEN", "SHEER", "TAINT", "HEATH", "CRIER",
"TENSE", "CARAT", "CANAL", "APNEA", "THESE", "HATCH", "SHELL", "CIRCA", "APART",
"SPILL", "STEEL", "LOCAL", "STOOL", "SHEEN", "RESET", "STEEP", "ELATE", "PRESS",
"SLEET", "CROSS", "TOTAL", "TREAT", "ONION", "STATE", "CINCH", "ASSET", "THREE",
"TORSO", "SNOOP", "PENNE", "SPOON", "SHEEP", "PAPAL", "STILL", "CHILL", "THETA",
"LEECH", "INNER", "HONOR", "LOOSE", "CONIC", "SCENE", "COACH", "CONCH", "LATTE",
"ERASE", "ESTER", "PEACE", "PASTA", "INANE", "SPOOL", "TEASE", "HARSH", "PIECE",
"STEER", "SCOOP", "NINTH", "OTTER", "OCTET", "EERIE", "RISER", "LAPEL", "HIPPO",
"PREEN", "ETHER", "AORTA", "SENSE", "TRACT", "SHOOT", "SLOOP", "REPEL", "TITHE",
"IONIC", "CELLO", "CHESS", "SOOTH", "COCOA", "TITAN", "TOOTH", "TIARA", "CRESS",
"SLOSH", "RARER", "TERSE", "ERECT", "HELLO", "PARER", "RIPER", "NOOSE", "CREPE",
"CACAO", "ILIAC", "POSSE", "CACTI", "EASEL", "LASSO", "ROOST", "ALLOT", "COLON",
"LEPER", "TEETH", "TITLE", "HENCE", "NIECE", "PAPER", "TRITE", "SPELL", "RACER",
"ATTIC", "CRASS", "HITCH", "LEASE", "CEASE", "ROTOR", "ELOPE", "APPLE", "CHILI",
"START", "PHOTO", "SALSA", "STASH", "PRIOR", "TAROT", "COLOR", "CHEER", "CLASS",
"ARENA", "ELECT", "ENTER", "CATCH", "TENET", "TACIT", "TRAIT", "TERRA", "LILAC",
};
// These are words that'll never be used, but still need to be in the dictionary for guesses.
// Number of words found: 1898
static const char _possible_words[][WORDLE_LENGTH + 1] = {
#if !WORDLE_ALLOW_NON_WORD_AND_REPEAT_GUESSES
"AALII", "AARTI", "ACAIS", "ACARI", "ACCAS", "ACERS", "ACETA", "ACHAR", "ACHES",
"ACHOO", "ACINI", "ACNES", "ACRES", "ACROS", "ACTIN", "ACTON", "AECIA", "AEONS",
"AERIE", "AEROS", "AESIR", "AHEAP", "AHENT", "AHINT", "AINEE", "AIOLI", "AIRER",
"AIRNS", "AIRTH", "AIRTS", "AITCH", "ALAAP", "ALANE", "ALANS", "ALANT", "ALAPA",
"ALAPS", "ALATE", "ALCOS", "ALECS", "ALEPH", "ALIAS", "ALINE", "ALIST", "ALLEE",
"ALLEL", "ALLIS", "ALOES", "ALOHA", "ALOIN", "ALOOS", "ALTHO", "ALTOS", "ANANA",
"ANATA", "ANCHO", "ANCLE", "ANCON", "ANEAR", "ANELE", "ANENT", "ANILE", "ANILS",
"ANION", "ANISE", "ANLAS", "ANNAL", "ANNAS", "ANNAT", "ANOAS", "ANOLE", "ANSAE",
"ANTAE", "ANTAR", "ANTAS", "ANTES", "ANTIS", "ANTRA", "ANTRE", "APACE", "APERS",
"APERT", "APHIS", "APIAN", "APIOL", "APISH", "APOOP", "APORT", "APPAL", "APPEL",
"APPRO", "APRES", "APSES", "APSIS", "APSOS", "APTER", "ARARS", "ARCHI", "ARCOS",
"AREAE", "AREAL", "AREAR", "AREAS", "ARECA", "AREIC", "ARENE", "AREPA", "ARERE",
"ARETE", "ARETS", "ARETT", "ARHAT", "ARIAS", "ARIEL", "ARILS", "ARIOT", "ARISH",
"ARLES", "ARNAS", "AROHA", "ARPAS", "ARPEN", "ARRAH", "ARRAS", "ARRET", "ARRIS",
"ARSES", "ARSIS", "ARTAL", "ARTEL", "ARTIC", "ARTIS", "ASANA", "ASCON", "ASHES",
"ASHET", "ASPEN", "ASPER", "ASPIC", "ASPIE", "ASPIS", "ASPRO", "ASSAI", "ASSES",
"ASSOT", "ASTER", "ASTIR", "ATAPS", "ATILT", "ATLAS", "ATOCS", "ATRIA", "ATRIP",
"ATTAP", "ATTAR", "CACAS", "CAECA", "CAESE", "CAINS", "CALLA", "CALLS", "CALOS",
"CALPA", "CALPS", "CANEH", "CANER", "CANES", "CANNA", "CANNS", "CANSO", "CANST",
"CANTO", "CANTS", "CAPAS", "CAPES", "CAPHS", "CAPLE", "CAPON", "CAPOS", "CAPOT",
"CAPRI", "CARAP", "CARER", "CARES", "CARET", "CARLE", "CARLS", "CARNS", "CARON",
"CARPI", "CARPS", "CARRS", "CARSE", "CARTA", "CARTE", "CARTS", "CASAS", "CASCO",
"CASES", "CASTS", "CATES", "CECAL", "CEILI", "CEILS", "CELLA", "CELLI", "CELLS",
"CELTS", "CENSE", "CENTO", "CENTS", "CEORL", "CEPES", "CERCI", "CERES", "CERIA",
"CERIC", "CERNE", "CEROC", "CEROS", "CERTS", "CESSE", "CESTA", "CESTI", "CETES",
"CHACE", "CHACO", "CHAIS", "CHALS", "CHANA", "CHAPE", "CHAPS", "CHAPT", "CHARA",
"CHARE", "CHARR", "CHARS", "CHATS", "CHEEP", "CHELA", "CHELP", "CHERE", "CHERT",
"CHETH", "CHIAO", "CHIAS", "CHICA", "CHICH", "CHICO", "CHICS", "CHIEL", "CHILE",
"CHINE", "CHINO", "CHINS", "CHIPS", "CHIRL", "CHIRO", "CHIRR", "CHIRT", "CHITS",
"CHOCO", "CHOCS", "CHOIL", "CHOLA", "CHOLI", "CHOLO", "CHONS", "CHOON", "CHOPS",
"CHOTA", "CHOTT", "CIELS", "CILIA", "CILLS", "CINCT", "CINES", "CIONS", "CIPPI",
"CIRCS", "CIRES", "CIRLS", "CIRRI", "CISCO", "CISTS", "CITAL", "CITER", "CITES",
"CLACH", "CLAES", "CLANS", "CLAPS", "CLAPT", "CLARO", "CLART", "CLAST", "CLATS",
"CLEEP", "CLEPE", "CLEPT", "CLIES", "CLINE", "CLINT", "CLIPE", "CLIPS", "CLIPT",
"CLITS", "CLONS", "CLOOP", "CLOOT", "CLOPS", "CLOTE", "CLOTS", "COACT", "COALA",
"COALS", "COAPT", "COATE", "COATI", "COATS", "COCAS", "COCCI", "COCCO", "COCOS",
"COHEN", "COHOE", "COHOS", "COILS", "COINS", "COIRS", "COITS", "COLAS", "COLES",
"COLIC", "COLIN", "COLLS", "COLTS", "CONES", "CONIA", "CONIN", "CONNE", "CONNS",
"CONTE", "CONTO", "COOCH", "COOEE", "COOER", "COOLS", "COONS", "COOPS", "COOPT",
"COOST", "COOTS", "COPAL", "COPEN", "COPER", "COPES", "COPRA", "CORES", "CORIA",
"CORNI", "CORNO", "CORNS", "CORPS", "CORSE", "CORSO", "COSEC", "COSES", "COSET",
"COSIE", "COSTA", "COSTE", "COSTS", "COTAN", "COTES", "COTHS", "COTTA", "COTTS",
"CRAAL", "CRAIC", "CRANS", "CRAPE", "CRAPS", "CRARE", "CREEL", "CREES", "CRENA",
"CREPS", "CRIAS", "CRIES", "CRINE", "CRIOS", "CRIPE", "CRIPS", "CRISE", "CRITH",
"CRITS", "CROCI", "CROCS", "CRONS", "CROOL", "CROON", "CROPS", "CRORE", "CROST",
"CTENE", "EALES", "EARLS", "EARNS", "EARNT", "EARST", "EASER", "EASES", "EASLE",
"EASTS", "EATHE", "ECHES", "ECHOS", "EISEL", "ELAIN", "ELANS", "ELCHI", "ELINT",
"ELOIN", "ELOPS", "ELPEE", "ELSIN", "ENATE", "ENIAC", "ENLIT", "ENOLS", "ENROL",
"ENTIA", "EORLS", "EOSIN", "EPACT", "EPEES", "EPHAH", "EPHAS", "EPHOR", "EPICS",
"EPOPT", "EPRIS", "ERICA", "ERICS", "ERNES", "EROSE", "ERSES", "ESCAR", "ESCOT",
"ESILE", "ESNES", "ESSES", "ESTOC", "ESTOP", "ESTRO", "ETAPE", "ETATS", "ETENS",
"ETHAL", "ETHNE", "ETICS", "ETNAS", "ETTIN", "ETTLE", "HAARS", "HAETS", "HAHAS",
"HAILS", "HAINS", "HAINT", "HAIRS", "HAITH", "HALAL", "HALER", "HALES", "HALLO",
"HALLS", "HALON", "HALOS", "HALSE", "HALTS", "HANAP", "HANCE", "HANCH", "HANSA",
"HANSE", "HANTS", "HAOLE", "HAPPI", "HARES", "HARLS", "HARNS", "HAROS", "HARPS",
"HARTS", "HASPS", "HASTA", "HATES", "HATHA", "HEALS", "HEAPS", "HEARE", "HEARS",
"HEAST", "HEATS", "HECHT", "HEELS", "HEILS", "HEIRS", "HELES", "HELIO", "HELLS",
"HELOS", "HELOT", "HELPS", "HENCH", "HENNA", "HENTS", "HEPAR", "HERES", "HERLS",
"HERNS", "HEROS", "HERSE", "HESPS", "HESTS", "HETES", "HETHS", "HIANT", "HILAR",
"HILCH", "HILLO", "HILLS", "HILTS", "HINTS", "HIOIS", "HIREE", "HIRER", "HIRES",
"HISTS", "HITHE", "HOARS", "HOAST", "HOERS", "HOISE", "HOLES", "HOLLA", "HOLLO",
"HOLON", "HOLOS", "HOLTS", "HONAN", "HONER", "HONES", "HOOCH", "HOONS", "HOOPS",
"HOORS", "HOOSH", "HOOTS", "HOPER", "HOPES", "HORAH", "HORAL", "HORAS", "HORIS",
"HORNS", "HORST", "HOSEL", "HOSEN", "HOSER", "HOSES", "HOSTA", "HOSTS", "HOTCH",
"HOTEN", "ICERS", "ICHES", "ICHOR", "ICIER", "ICONS", "ICTAL", "ICTIC", "ILEAC",
"ILEAL", "ILIAL", "ILLER", "ILLTH", "INAPT", "INCEL", "INCLE", "INION", "INNIT",
"INSET", "INSPO", "INTEL", "INTIL", "INTIS", "INTRA", "IOTAS", "IPPON", "IRONE",
"IRONS", "ISHES", "ISLES", "ISNAE", "ISSEI", "ISTLE", "ITHER", "LAARI", "LACER",
"LACES", "LACET", "LAERS", "LAHAL", "LAHAR", "LAICH", "LAICS", "LAIRS", "LAITH",
"LALLS", "LANAI", "LANAS", "LANCH", "LANES", "LANTS", "LAPIN", "LAPIS", "LARCH",
"LAREE", "LARES", "LARIS", "LARNS", "LARNT", "LASER", "LASES", "LASSI", "LASTS",
"LATAH", "LATEN", "LATHI", "LATHS", "LEANS", "LEAPS", "LEARE", "LEARS", "LEATS",
"LEEAR", "LEEPS", "LEERS", "LEESE", "LEETS", "LEHRS", "LEIRS", "LEISH", "LENES",
"LENIS", "LENOS", "LENSE", "LENTI", "LENTO", "LEONE", "LEPRA", "LEPTA", "LERES",
"LERPS", "LESES", "LESTS", "LETCH", "LETHE", "LIANA", "LIANE", "LIARS", "LIART",
"LICHI", "LICHT", "LICIT", "LIENS", "LIERS", "LILLS", "LILOS", "LILTS", "LINAC",
"LINCH", "LINES", "LININ", "LINNS", "LINOS", "LINTS", "LIONS", "LIPAS", "LIPES",
"LIPIN", "LIPOS", "LIRAS", "LIROT", "LISLE", "LISPS", "LISTS", "LITAI", "LITAS",
"LITES", "LITHO", "LITHS", "LITRE", "LLANO", "LOACH", "LOANS", "LOAST", "LOCHE",
"LOCHS", "LOCIE", "LOCIS", "LOCOS", "LOESS", "LOHAN", "LOINS", "LOIPE", "LOIRS",
"LOLLS", "LONER", "LOOIE", "LOONS", "LOOPS", "LOOTS", "LOPER", "LOPES", "LORAL",
"LORAN", "LOREL", "LORES", "LORIC", "LORIS", "LOSEL", "LOSEN", "LOSES", "LOTAH",
"LOTAS", "LOTES", "LOTIC", "LOTOS", "LOTSA", "LOTTA", "LOTTE", "LOTTO", "NAANS",
"NACHE", "NACHO", "NACRE", "NAHAL", "NAILS", "NAIRA", "NALAS", "NALLA", "NANAS",
"NANCE", "NANNA", "NANOS", "NAPAS", "NAPES", "NAPOO", "NAPPA", "NAPPE", "NARAS",
"NARCO", "NARCS", "NARES", "NARIC", "NARIS", "NARRE", "NASHI", "NATCH", "NATES",
"NATIS", "NEALS", "NEAPS", "NEARS", "NEATH", "NEATS", "NEELE", "NEEPS", "NEESE",
"NEIST", "NELIS", "NENES", "NEONS", "NEPER", "NEPIT", "NERAL", "NEROL", "NERTS",
"NESTS", "NETES", "NETOP", "NETTS", "NICHT", "NICOL", "NIHIL", "NILLS", "NINER",
"NINES", "NINON", "NIPAS", "NIRLS", "NISEI", "NISSE", "NITER", "NITES", "NITON",
"NITRE", "NITRO", "NOAHS", "NOELS", "NOILS", "NOINT", "NOIRS", "NOLES", "NOLLS",
"NOLOS", "NONAS", "NONCE", "NONES", "NONET", "NONIS", "NOOIT", "NOONS", "NOOPS",
"NOPAL", "NORIA", "NORIS", "NOSER", "NOSES", "NOTAL", "NOTER", "NOTES", "OASES",
"OASIS", "OASTS", "OATEN", "OATER", "OATHS", "OCHER", "OCHES", "OCHRE", "OCREA",
"OCTAN", "OCTAS", "OHIAS", "OHONE", "OILER", "OINTS", "OLEIC", "OLEIN", "OLENT",
"OLEOS", "OLIOS", "OLLAS", "OLLER", "OLLIE", "OLPAE", "OLPES", "ONCER", "ONCES",
"ONCET", "ONERS", "ONTIC", "OONTS", "OORIE", "OOSES", "OPAHS", "OPALS", "OPENS",
"OPEPE", "OPPOS", "OPSIN", "OPTER", "ORACH", "ORALS", "ORANT", "ORATE", "ORCAS",
"ORCIN", "ORIEL", "ORLES", "ORLON", "ORLOP", "ORNIS", "ORPIN", "ORRIS", "ORTHO",
"OSCAR", "OSHAC", "OSIER", "OSSIA", "OSTIA", "OTTAR", "OTTOS", "PAALS", "PAANS",
"PACAS", "PACER", "PACES", "PACHA", "PACOS", "PACTA", "PACTS", "PAEAN", "PAEON",
"PAILS", "PAINS", "PAIRE", "PAIRS", "PAISA", "PAISE", "PALAS", "PALEA", "PALES",
"PALET", "PALIS", "PALLA", "PALLS", "PALPI", "PALPS", "PALSA", "PANCE", "PANES",
"PANNE", "PANNI", "PANTO", "PANTS", "PAOLI", "PAOLO", "PAPAS", "PAPES", "PAPPI",
"PARAE", "PARAS", "PARCH", "PAREN", "PAREO", "PARES", "PARIS", "PARLE", "PAROL",
"PARPS", "PARRA", "PARRS", "PARTI", "PARTS", "PASEO", "PASES", "PASHA", "PASSE",
"PASTS", "PATEN", "PATER", "PATES", "PATHS", "PATIN", "PATTE", "PEALS", "PEANS",
"PEARE", "PEARS", "PEART", "PEASE", "PEATS", "PECHS", "PEECE", "PEELS", "PEENS",
"PEEPE", "PEEPS", "PEERS", "PEINS", "PEISE", "PELAS", "PELES", "PELLS", "PELON",
"PELTA", "PELTS", "PENES", "PENIE", "PENIS", "PENNA", "PENNI", "PENTS", "PEONS",
"PEPLA", "PEPOS", "PEPSI", "PERAI", "PERCE", "PERCS", "PEREA", "PERES", "PERIS",
"PERNS", "PERPS", "PERSE", "PERST", "PERTS", "PESOS", "PESTS", "PETAR", "PETER",
"PETIT", "PETRE", "PETRI", "PETTI", "PETTO", "PHARE", "PHEER", "PHENE", "PHEON",
"PHESE", "PHIAL", "PHISH", "PHOCA", "PHONO", "PHONS", "PHOTS", "PHPHT", "PIANI",
"PIANS", "PICAL", "PICAS", "PICOT", "PICRA", "PIERS", "PIERT", "PIETA", "PIETS",
"PILAE", "PILAO", "PILAR", "PILCH", "PILEA", "PILEI", "PILER", "PILES", "PILIS",
"PILLS", "PINAS", "PINES", "PINNA", "PINON", "PINOT", "PINTA", "PINTS", "PIONS",
"PIPAL", "PIPAS", "PIPES", "PIPET", "PIPIS", "PIPIT", "PIRAI", "PIRLS", "PIRNS",
"PISCO", "PISES", "PISOS", "PISTE", "PITAS", "PITHS", "PITON", "PITOT", "PITTA",
"PLAAS", "PLANS", "PLAPS", "PLASH", "PLAST", "PLATS", "PLATT", "PLEAS", "PLENA",
"PLEON", "PLESH", "PLICA", "PLIES", "PLOAT", "PLOPS", "PLOTS", "POACH", "POEPS",
"POETS", "POLER", "POLES", "POLIO", "POLIS", "POLLS", "POLOS", "POLTS", "PONCE",
"PONES", "PONTS", "POOHS", "POOLS", "POONS", "POOPS", "POORI", "POORT", "POOTS",
"POPES", "POPPA", "PORAE", "PORAL", "PORER", "PORES", "PORIN", "PORNO", "PORNS",
"PORTA", "PORTS", "POSES", "POSHO", "POSTS", "POTAE", "POTCH", "POTES", "POTIN",
"POTOO", "POTTO", "POTTS", "PRANA", "PRAOS", "PRASE", "PRATE", "PRATS", "PRATT",
"PREES", "PRENT", "PREON", "PREOP", "PREPS", "PRESA", "PRESE", "PREST", "PRIAL",
"PRIER", "PRIES", "PRILL", "PRION", "PRISE", "PRISS", "PROAS", "PROIN", "PROLE",
"PROLL", "PROPS", "PRORE", "PROSO", "PROSS", "PROST", "PROTO", "PSION", "PSOAE",
"PSOAI", "PSOAS", "PSORA", "RACES", "RACHE", "RACON", "RAIAS", "RAILE", "RAILS",
"RAINE", "RAINS", "RAITA", "RAITS", "RALES", "RANAS", "RANCE", "RANEE", "RANIS",
"RANTS", "RAPER", "RAPES", "RAPHE", "RAPPE", "RAREE", "RARES", "RASER", "RASES",
"RASPS", "RASSE", "RASTA", "RATAL", "RATAN", "RATAS", "RATCH", "RATEL", "RATER",
"RATES", "RATHA", "RATHE", "RATHS", "RATOO", "RATOS", "REAIS", "REALO", "REALS",
"REANS", "REAPS", "REARS", "REAST", "REATA", "REATE", "RECAL", "RECCE", "RECCO",
"RECIT", "RECON", "RECTA", "RECTI", "RECTO", "REECH", "REELS", "REENS", "REEST",
"REINS", "REIST", "RELET", "RELIE", "RELIT", "RELLO", "RENIN", "RENNE", "RENOS",
"RENTE", "RENTS", "REOIL", "REPIN", "REPLA", "REPOS", "REPOT", "REPPS", "REPRO",
"RERAN", "RESAT", "RESEE", "RESES", "RESIT", "RESTO", "RESTS", "RETIA", "RETIE",
"RHEAS", "RHIES", "RHINE", "RHONE", "RIALS", "RIANT", "RIATA", "RICER", "RICES",
"RICHT", "RICIN", "RIELS", "RILES", "RILLE", "RILLS", "RINES", "RIOTS", "RIPES",
"RIPPS", "RISES", "RISHI", "RISPS", "RITES", "RITTS", "ROANS", "ROARS", "ROATE",
"ROHES", "ROILS", "ROINS", "ROIST", "ROLES", "ROLLS", "RONEO", "RONES", "RONIN",
"RONNE", "RONTE", "RONTS", "ROONS", "ROOPS", "ROOSA", "ROOSE", "ROOTS", "ROPER",
"ROPES", "RORAL", "RORES", "RORIC", "RORIE", "RORTS", "ROSES", "ROSET", "ROSHI",
"ROSIN", "ROSIT", "ROSTI", "ROSTS", "ROTAL", "ROTAN", "ROTAS", "ROTCH", "ROTES",
"ROTIS", "ROTLS", "ROTON", "ROTOS", "ROTTE", "SACRA", "SAICE", "SAICS", "SAILS",
"SAINE", "SAINS", "SAIRS", "SAIST", "SAITH", "SALAL", "SALAT", "SALEP", "SALES",
"SALET", "SALIC", "SALLE", "SALOL", "SALOP", "SALPA", "SALPS", "SALSE", "SALTO",
"SALTS", "SANES", "SANSA", "SANTO", "SANTS", "SAOLA", "SAPAN", "SAPOR", "SARAN",
"SAREE", "SARIN", "SARIS", "SAROS", "SASER", "SASIN", "SASSE", "SATAI", "SATES",
"SATIS", "SCAIL", "SCALA", "SCALL", "SCANS", "SCAPA", "SCAPE", "SCAPI", "SCARP",
"SCARS", "SCART", "SCATH", "SCATS", "SCATT", "SCEAT", "SCENA", "SCOOT", "SCOPA",
"SCOPS", "SCOTS", "SCRAE", "SCRAN", "SCRAT", "SCRIP", "SEALS", "SEANS", "SEARE",
"SEARS", "SEASE", "SEATS", "SECCO", "SECHS", "SECTS", "SEELS", "SEEPS", "SEERS",
"SEHRI", "SEILS", "SEINE", "SEIRS", "SEISE", "SELAH", "SELES", "SELLA", "SELLE",
"SELLS", "SENAS", "SENES", "SENNA", "SENOR", "SENSA", "SENSI", "SENTE", "SENTI",
"SENTS", "SEPAL", "SEPIC", "SEPTA", "SEPTS", "SERAC", "SERAI", "SERAL", "SERER",
"SERES", "SERIC", "SERIN", "SERON", "SERRA", "SERRE", "SERRS", "SESSA", "SETAE",
"SETAL", "SETON", "SETTS", "SHAHS", "SHANS", "SHAPS", "SHARN", "SHASH", "SHCHI",
"SHEAL", "SHEAS", "SHEEL", "SHENT", "SHEOL", "SHERE", "SHERO", "SHETS", "SHIAI",
"SHIEL", "SHIER", "SHIES", "SHILL", "SHINS", "SHIPS", "SHIRR", "SHIRS", "SHISH",
"SHISO", "SHIST", "SHITE", "SHITS", "SHLEP", "SHOAT", "SHOER", "SHOES", "SHOLA",
"SHOOL", "SHOON", "SHOOS", "SHOPE", "SHOPS", "SHORL", "SHOTE", "SHOTS", "SHOTT",
"SHRIS", "SIALS", "SICES", "SICHT", "SIENS", "SIENT", "SIETH", "SILEN", "SILER",
"SILES", "SILLS", "SILOS", "SILTS", "SINES", "SINHS", "SIPES", "SIREE", "SIRES",
"SIRIH", "SIRIS", "SIROC", "SIRRA", "SISAL", "SISES", "SISTA", "SISTS", "SITAR",
"SITES", "SITHE", "SLAES", "SLANE", "SLAPS", "SLART", "SLATS", "SLEER", "SLIER",
"SLIPE", "SLIPS", "SLIPT", "SLISH", "SLITS", "SLOAN", "SLOES", "SLOOT", "SLOPS",
"SLOTS", "SNAPS", "SNARS", "SNASH", "SNATH", "SNEAP", "SNEES", "SNELL", "SNIES",
"SNIPS", "SNIRT", "SNITS", "SNOEP", "SNOOL", "SNOOT", "SNOTS", "SOAPS", "SOARE",
"SOARS", "SOCAS", "SOCES", "SOCLE", "SOILS", "SOLAH", "SOLAN", "SOLAS", "SOLEI",
"SOLER", "SOLES", "SOLON", "SOLOS", "SONCE", "SONES", "SONNE", "SONSE", "SOOLE",
"SOOLS", "SOOPS", "SOOTE", "SOOTS", "SOPHS", "SOPOR", "SOPRA", "SORAL", "SORAS",
"SOREE", "SOREL", "SORER", "SORES", "SORNS", "SORRA", "SORTA", "SORTS", "SOTHS",
"SOTOL", "SPAER", "SPAES", "SPAHI", "SPAIL", "SPAIN", "SPAIT", "SPALE", "SPALL",
"SPALT", "SPANE", "SPANS", "SPARS", "SPART", "SPATE", "SPATS", "SPEAL", "SPEAN",
"SPEAT", "SPECS", "SPECT", "SPEEL", "SPEER", "SPEIL", "SPEIR", "SPEOS", "SPETS",
"SPIAL", "SPICA", "SPICS", "SPIER", "SPIES", "SPILE", "SPINA", "SPINS", "SPIRT",
"SPITS", "SPOOR", "SPOOT", "SPOSH", "SPOTS", "SPRAT", "SPRIT", "STANE", "STAPH",
"STAPS", "STARN", "STARR", "STARS", "STATS", "STEAN", "STEAR", "STEEN", "STEIL",
"STELA", "STELE", "STELL", "STENO", "STENS", "STENT", "STEPS", "STEPT", "STERE",
"STETS", "STICH", "STIES", "STILE", "STIPA", "STIPE", "STIRE", "STIRP", "STIRS",
"STOAE", "STOAI", "STOAS", "STOAT", "STOEP", "STOIT", "STOLN", "STONN", "STOOR",
"STOPE", "STOPS", "STOPT", "STOSS", "STOTS", "STOTT", "STRAE", "STREP", "STRIA",
"STROP", "TAALS", "TAATA", "TACAN", "TACES", "TACET", "TACHE", "TACHO", "TACHS",
"TACOS", "TACTS", "TAELS", "TAHAS", "TAHRS", "TAILS", "TAINS", "TAIRA", "TAISH",
"TAITS", "TALAR", "TALAS", "TALCS", "TALEA", "TALER", "TALES", "TALLS", "TALPA",
"TANAS", "TANHS", "TANNA", "TANTI", "TANTO", "TAPAS", "TAPEN", "TAPES", "TAPET",
"TAPIS", "TAPPA", "TARAS", "TARES", "TARNS", "TAROC", "TAROS", "TARPS", "TARRE",
"TARSI", "TARTS", "TASAR", "TASER", "TASES", "TASSA", "TASSE", "TASSO", "TATAR",
"TATER", "TATES", "TATHS", "TATIE", "TATTS", "TEALS", "TEARS", "TEATS", "TECHS",
"TECTA", "TEELS", "TEENE", "TEENS", "TEERS", "TEHRS", "TEILS", "TEINS", "TELAE",
"TELCO", "TELES", "TELIA", "TELIC", "TELLS", "TELOI", "TELOS", "TENCH", "TENES",
"TENIA", "TENNE", "TENNO", "TENON", "TENTS", "TEPAL", "TEPAS", "TERAI", "TERAS",
"TERCE", "TERES", "TERNE", "TERNS", "TERTS", "TESLA", "TESTA", "TESTE", "TESTS",
"TETES", "TETHS", "TETRA", "TETRI", "THALE", "THALI", "THANA", "THANE", "THANS",
"THARS", "THECA", "THEES", "THEIC", "THEIN", "THENS", "THESP", "THETE", "THILL",
"THINE", "THINS", "THIOL", "THIRL", "THOLE", "THOLI", "THORO", "THORP", "THRAE",
"THRIP", "THROE", "TIANS", "TIARS", "TICAL", "TICCA", "TICES", "TIERS", "TILER",
"TILES", "TILLS", "TILTH", "TILTS", "TINAS", "TINCT", "TINEA", "TINES", "TINTS",
"TIPIS", "TIRES", "TIRLS", "TIROS", "TIRRS", "TITCH", "TITER", "TITIS", "TITRE",
"TOCOS", "TOEAS", "TOHOS", "TOILE", "TOILS", "TOISE", "TOITS", "TOLAN", "TOLAR",
"TOLAS", "TOLES", "TOLLS", "TOLTS", "TONER", "TONES", "TONNE", "TOOLS", "TOONS",
"TOOTS", "TOPEE", "TOPER", "TOPES", "TOPHE", "TOPHI", "TOPHS", "TOPIS", "TOPOI",
"TOPOS", "TORAH", "TORAN", "TORAS", "TORCS", "TORES", "TORIC", "TORII", "TOROS",
"TOROT", "TORRS", "TORSE", "TORSI", "TORTA", "TORTE", "TORTS", "TOSAS", "TOSES",
"TOTER", "TOTES", "TRANS", "TRANT", "TRAPE", "TRAPS", "TRAPT", "TRASS", "TRATS",
"TRATT", "TREEN", "TREES", "TRESS", "TREST", "TRETS", "TRIAC", "TRIER", "TRIES",
"TRILL", "TRINE", "TRINS", "TRIOL", "TRIOR", "TRIOS", "TRIPS", "TRIST", "TROAT",
"TROIS", "TRONA", "TRONC", "TRONE", "TRONS", "TROTH", "TROTS", "TSARS",
#endif
};
#if (WORDLE_USE_RANDOM_GUESS == 3)
static const uint16_t _num_random_guess_words = 13; // The valid_words array begins with this many words that are considered the top 3% best options.
#elif (WORDLE_USE_RANDOM_GUESS == 2)
static const uint16_t _num_random_guess_words = 257; // The valid_words array begins with this many words where each letter is different.
#elif (WORDLE_USE_RANDOM_GUESS == 1)
static const uint16_t _num_random_guess_words = _num_words;
#endif
#endif // WORDLE_FACE_DICT_H_