diff --git a/make.mk b/make.mk index 055a4e9..3091598 100644 --- a/make.mk +++ b/make.mk @@ -215,6 +215,20 @@ SRCS += \ endif +ifeq ($(LED), BLUE) +CFLAGS += -DWATCH_IS_BLUE_BOARD +endif + +ifndef COLOR +$(error Set the COLOR variable to RED, BLUE, or GREEN depending on what board you have.) +endif + +COLOR_VALID := $(filter $(COLOR),RED BLUE GREEN) + +ifeq ($(COLOR_VALID),) +$(error COLOR must be RED, BLUE, or GREEN) +endif + ifeq ($(COLOR), BLUE) CFLAGS += -DWATCH_IS_BLUE_BOARD endif diff --git a/movement/make/Makefile b/movement/make/Makefile index da5486b..e347d59 100644 --- a/movement/make/Makefile +++ b/movement/make/Makefile @@ -52,6 +52,7 @@ SRCS += \ ../shell.c \ ../shell_cmd_list.c \ ../watch_faces/clock/simple_clock_face.c \ + ../watch_faces/clock/close_enough_clock_face.c \ ../watch_faces/clock/clock_face.c \ ../watch_faces/clock/world_clock_face.c \ ../watch_faces/clock/beats_face.c \ @@ -129,6 +130,17 @@ SRCS += \ ../watch_faces/clock/minute_repeater_decimal_face.c \ ../watch_faces/complication/tuning_tones_face.c \ ../watch_faces/complication/kitchen_conversions_face.c \ + ../watch_faces/complication/wordle_face.c \ + ../watch_faces/complication/endless_runner_face.c \ + ../watch_faces/complication/periodic_face.c \ + ../watch_faces/complication/deadline_face.c \ + ../watch_faces/complication/higher_lower_game_face.c \ + ../watch_faces/clock/french_revolutionary_face.c \ + ../watch_faces/clock/minimal_clock_face.c \ + ../watch_faces/complication/simon_face.c \ + ../watch_faces/complication/simple_calculator_face.c \ + ../watch_faces/sensor/alarm_thermometer_face.c \ + ../watch_faces/demo/beeps_face.c \ # New watch faces go above this line. # Leave this line at the bottom of the file; it has all the targets for making your project. diff --git a/movement/movement.c b/movement/movement.c index bed1199..fe6f9d1 100644 --- a/movement/movement.c +++ b/movement/movement.c @@ -95,31 +95,6 @@ #define MOVEMENT_DEFAULT_LED_DURATION 1 #endif -// Default to no set location latitude -#ifndef MOVEMENT_DEFAULT_LATITUDE -#define MOVEMENT_DEFAULT_LATITUDE 0 -#endif - -// Default to no set location longitude -#ifndef MOVEMENT_DEFAULT_LONGITUDE -#define MOVEMENT_DEFAULT_LONGITUDE 0 -#endif - -// Default to no set birthdate year -#ifndef MOVEMENT_DEFAULT_BIRTHDATE_YEAR -#define MOVEMENT_DEFAULT_BIRTHDATE_YEAR 0 -#endif - -// Default to no set birthdate month -#ifndef MOVEMENT_DEFAULT_BIRTHDATE_MONTH -#define MOVEMENT_DEFAULT_BIRTHDATE_MONTH 0 -#endif - -// Default to no set birthdate day -#ifndef MOVEMENT_DEFAULT_BIRTHDATE_DAY -#define MOVEMENT_DEFAULT_BIRTHDATE_DAY 0 -#endif - #if __EMSCRIPTEN__ #include #endif @@ -264,14 +239,24 @@ void movement_request_tick_frequency(uint8_t freq) { } void movement_illuminate_led(void) { - if (movement_state.settings.bit.led_duration) { + if (movement_state.settings.bit.led_duration != 0b111) { watch_set_led_color(movement_state.settings.bit.led_red_color ? (0xF | movement_state.settings.bit.led_red_color << 4) : 0, movement_state.settings.bit.led_green_color ? (0xF | movement_state.settings.bit.led_green_color << 4) : 0); - movement_state.light_ticks = (movement_state.settings.bit.led_duration * 2 - 1) * 128; + if (movement_state.settings.bit.led_duration == 0) { + movement_state.light_ticks = 1; + } else { + movement_state.light_ticks = (movement_state.settings.bit.led_duration * 2 - 1) * 128; + } _movement_enable_fast_tick_if_needed(); } } +static void _movement_led_off(void) { + watch_set_led_off(); + movement_state.light_ticks = -1; + _movement_disable_fast_tick_if_possible(); +} + bool movement_default_loop_handler(movement_event_t event, movement_settings_t *settings) { (void)settings; @@ -282,6 +267,11 @@ bool movement_default_loop_handler(movement_event_t event, movement_settings_t * case EVENT_LIGHT_BUTTON_DOWN: movement_illuminate_led(); break; + case EVENT_LIGHT_BUTTON_UP: + if (movement_state.settings.bit.led_duration == 0) { + _movement_led_off(); + } + break; case EVENT_MODE_LONG_PRESS: if (MOVEMENT_SECONDARY_FACE_INDEX && movement_state.current_face_idx == 0) { movement_move_to_face(MOVEMENT_SECONDARY_FACE_INDEX); @@ -416,11 +406,7 @@ void app_init(void) { movement_state.settings.bit.to_interval = MOVEMENT_DEFAULT_TIMEOUT_INTERVAL; movement_state.settings.bit.le_interval = MOVEMENT_DEFAULT_LOW_ENERGY_INTERVAL; movement_state.settings.bit.led_duration = MOVEMENT_DEFAULT_LED_DURATION; - movement_state.location.bit.latitude = MOVEMENT_DEFAULT_LATITUDE; - movement_state.location.bit.longitude = MOVEMENT_DEFAULT_LONGITUDE; - movement_state.birthdate.bit.year = MOVEMENT_DEFAULT_BIRTHDATE_YEAR; - movement_state.birthdate.bit.month = MOVEMENT_DEFAULT_BIRTHDATE_MONTH; - movement_state.birthdate.bit.day = MOVEMENT_DEFAULT_BIRTHDATE_DAY; + movement_state.light_ticks = -1; movement_state.alarm_ticks = -1; movement_state.next_available_backup_register = 4; @@ -443,14 +429,10 @@ void app_init(void) { void app_wake_from_backup(void) { movement_state.settings.reg = watch_get_backup_data(0); - movement_state.location.reg = watch_get_backup_data(1); - movement_state.birthdate.reg = watch_get_backup_data(2); } void app_setup(void) { watch_store_backup_data(movement_state.settings.reg, 0); - watch_store_backup_data(movement_state.location.reg, 1); - watch_store_backup_data(movement_state.birthdate.reg, 2); static bool is_first_launch = true; @@ -544,9 +526,7 @@ bool app_loop(void) { if (watch_get_pin_level(BTN_LIGHT)) { movement_state.light_ticks = 1; } else { - watch_set_led_off(); - movement_state.light_ticks = -1; - _movement_disable_fast_tick_if_possible(); + _movement_led_off(); } } @@ -584,6 +564,17 @@ bool app_loop(void) { event.subsecond = movement_state.subsecond; // the first trip through the loop overrides the can_sleep state can_sleep = wf->loop(event, &movement_state.settings, watch_face_contexts[movement_state.current_face_idx]); + + // Keep light on if user is still interacting with the watch. + if (movement_state.light_ticks > 0) { + switch (event.event_type) { + case EVENT_LIGHT_BUTTON_DOWN: + case EVENT_MODE_BUTTON_DOWN: + case EVENT_ALARM_BUTTON_DOWN: + movement_illuminate_led(); + } + } + event.event_type = EVENT_NONE; } diff --git a/movement/movement.h b/movement/movement.h index 3560009..6747ff6 100644 --- a/movement/movement.h +++ b/movement/movement.h @@ -50,7 +50,7 @@ typedef union { uint8_t to_interval : 2; // an inactivity interval for asking the active face to resign. bool to_always : 1; // if true, always time out from the active face to face 0. otherwise only faces that time out will resign (the default). uint8_t le_interval : 3; // 0 to disable low energy mode, or an inactivity interval for going into low energy mode. - uint8_t led_duration : 2; // how many seconds to shine the LED for (x2), or 0 to disable it. + uint8_t led_duration : 3; // how many seconds to shine the LED for (x2), 0 to shine only while the button is depressed, or all bits set to disable the LED altogether. uint8_t led_red_color : 4; // for general purpose illumination, the red LED value (0-15) uint8_t led_green_color : 4; // for general purpose illumination, the green LED value (0-15) uint8_t time_zone : 6; // an integer representing an index in the time zone table. @@ -60,9 +60,10 @@ typedef union { // time-oriented complication like a sunrise/sunset timer, and a simple locale preference could tell an // altimeter to display feet or meters as easily as it tells a thermometer to display degrees in F or C. bool clock_mode_24h : 1; // indicates whether clock should use 12 or 24 hour mode. + bool clock_24h_leading_zero : 1; // indicates whether clock should leading zero to indicate 24 hour mode. bool use_imperial_units : 1; // indicates whether to use metric units (the default) or imperial. bool alarm_enabled : 1; // indicates whether there is at least one alarm enabled. - uint8_t reserved : 6; // room for more preferences if needed. + uint8_t reserved : 5; // room for more preferences if needed. } bit; uint32_t reg; } movement_settings_t; @@ -242,8 +243,6 @@ typedef struct { typedef struct { // properties stored in BACKUP register movement_settings_t settings; - movement_location_t location; - movement_birthdate_t birthdate; // transient properties int16_t current_face_idx; diff --git a/movement/movement_config.h b/movement/movement_config.h index abceacf..10a30af 100644 --- a/movement/movement_config.h +++ b/movement/movement_config.h @@ -76,15 +76,15 @@ const watch_face_t watch_faces[] = { /* Set the timeout before switching to low energy mode * Valid values are: * 0: Never - * 1: 10 mins - * 2: 1 hour - * 3: 2 hours - * 4: 6 hours - * 5: 12 hours - * 6: 1 day + * 1: 1 hour + * 2: 2 hours + * 3: 6 hours + * 4: 12 hours + * 5: 1 day + * 6: 2 days * 7: 7 days */ -#define MOVEMENT_DEFAULT_LOW_ENERGY_INTERVAL 2 +#define MOVEMENT_DEFAULT_LOW_ENERGY_INTERVAL 1 /* Set the led duration * Valid values are: @@ -95,20 +95,4 @@ const watch_face_t watch_faces[] = { */ #define MOVEMENT_DEFAULT_LED_DURATION 1 -/* The latitude and longitude used for the wearers location - * Set signed values in 1/100ths of a degree - */ -#define MOVEMENT_DEFAULT_LATITUDE 0 -#define MOVEMENT_DEFAULT_LONGITUDE 0 - -/* The wearers birthdate - * Valid values: - * Year: 1 - 4095 - * Month: 1 - 12 - * Day: 1 - 31 - */ -#define MOVEMENT_DEFAULT_BIRTHDATE_YEAR 0 -#define MOVEMENT_DEFAULT_BIRTHDATE_MONTH 0 -#define MOVEMENT_DEFAULT_BIRTHDATE_DAY 0 - #endif // MOVEMENT_CONFIG_H_ diff --git a/movement/movement_custom_signal_tunes.h b/movement/movement_custom_signal_tunes.h index 0ac527a..78f8d12 100644 --- a/movement/movement_custom_signal_tunes.h +++ b/movement/movement_custom_signal_tunes.h @@ -67,6 +67,33 @@ int8_t signal_tune[] = { }; #endif // SIGNAL_TUNE_MARIO_THEME +#ifdef SIGNAL_TUNE_MGS_CODEC +int8_t signal_tune[] = { + BUZZER_NOTE_G5SHARP_A5FLAT, 1, + BUZZER_NOTE_C6, 1, + BUZZER_NOTE_G5SHARP_A5FLAT, 1, + BUZZER_NOTE_C6, 1, + BUZZER_NOTE_G5SHARP_A5FLAT, 1, + BUZZER_NOTE_C6, 1, + BUZZER_NOTE_G5SHARP_A5FLAT, 1, + BUZZER_NOTE_C6, 1, + BUZZER_NOTE_G5SHARP_A5FLAT, 1, + BUZZER_NOTE_C6, 1, + BUZZER_NOTE_REST, 6, + BUZZER_NOTE_G5SHARP_A5FLAT, 1, + BUZZER_NOTE_C6, 1, + BUZZER_NOTE_G5SHARP_A5FLAT, 1, + BUZZER_NOTE_C6, 1, + BUZZER_NOTE_G5SHARP_A5FLAT, 1, + BUZZER_NOTE_C6, 1, + BUZZER_NOTE_G5SHARP_A5FLAT, 1, + BUZZER_NOTE_C6, 1, + BUZZER_NOTE_G5SHARP_A5FLAT, 1, + BUZZER_NOTE_C6, 1, + 0 +}; +#endif // SIGNAL_TUNE_MGS_CODEC + #ifdef SIGNAL_TUNE_KIM_POSSIBLE int8_t signal_tune[] = { BUZZER_NOTE_G7, 6, @@ -119,4 +146,60 @@ int8_t signal_tune[] = { }; #endif // SIGNAL_TUNE_LAYLA +#ifdef SIGNAL_TUNE_HARRY_POTTER_SHORT +int8_t signal_tune[] = { + BUZZER_NOTE_B5, 12, + BUZZER_NOTE_REST, 1, + BUZZER_NOTE_E6, 12, + BUZZER_NOTE_REST, 1, + BUZZER_NOTE_G6, 6, + BUZZER_NOTE_REST, 1, + BUZZER_NOTE_F6SHARP_G6FLAT, 6, + BUZZER_NOTE_REST, 1, + BUZZER_NOTE_E6, 16, + BUZZER_NOTE_REST, 1, + BUZZER_NOTE_B6, 8, + BUZZER_NOTE_REST, 1, + BUZZER_NOTE_A6, 24, + BUZZER_NOTE_REST, 1, + BUZZER_NOTE_F6SHARP_G6FLAT, 24, + 0 +}; +#endif // SIGNAL_TUNE_HARRY_POTTER_SHORT + +#ifdef SIGNAL_TUNE_HARRY_POTTER_LONG +int8_t signal_tune[] = { + BUZZER_NOTE_B5, 12, + BUZZER_NOTE_REST, 1, + BUZZER_NOTE_E6, 12, + BUZZER_NOTE_REST, 1, + BUZZER_NOTE_G6, 6, + BUZZER_NOTE_REST, 1, + BUZZER_NOTE_F6SHARP_G6FLAT, 6, + BUZZER_NOTE_REST, 1, + BUZZER_NOTE_E6, 16, + BUZZER_NOTE_REST, 1, + BUZZER_NOTE_B6, 8, + BUZZER_NOTE_REST, 1, + BUZZER_NOTE_A6, 24, + BUZZER_NOTE_REST, 1, + BUZZER_NOTE_F6SHARP_G6FLAT, 24, + BUZZER_NOTE_REST, 1, + + BUZZER_NOTE_E6, 12, + BUZZER_NOTE_REST, 1, + BUZZER_NOTE_G6, 6, + BUZZER_NOTE_REST, 1, + BUZZER_NOTE_F6SHARP_G6FLAT, 6, + BUZZER_NOTE_REST, 1, + BUZZER_NOTE_D6SHARP_E6FLAT, 16, + BUZZER_NOTE_REST, 1, + BUZZER_NOTE_F6, 8, + BUZZER_NOTE_REST, 1, + BUZZER_NOTE_B5, 24, + + 0 +}; +#endif // SIGNAL_TUNE_HARRY_POTTER_LONG + #endif // MOVEMENT_CUSTOM_SIGNAL_TUNES_H_ diff --git a/movement/movement_faces.h b/movement/movement_faces.h index 3557110..d1dbe44 100644 --- a/movement/movement_faces.h +++ b/movement/movement_faces.h @@ -26,6 +26,7 @@ #define MOVEMENT_FACES_H_ #include "simple_clock_face.h" +#include "close_enough_clock_face.h" #include "clock_face.h" #include "world_clock_face.h" #include "preferences_face.h" @@ -104,6 +105,17 @@ #include "minute_repeater_decimal_face.h" #include "tuning_tones_face.h" #include "kitchen_conversions_face.h" +#include "wordle_face.h" +#include "endless_runner_face.h" +#include "periodic_face.h" +#include "deadline_face.h" +#include "higher_lower_game_face.h" +#include "french_revolutionary_face.h" +#include "minimal_clock_face.h" +#include "simon_face.h" +#include "simple_calculator_face.h" +#include "alarm_thermometer_face.h" +#include "beeps_face.h" // New includes go above this line. #endif // MOVEMENT_FACES_H_ diff --git a/movement/watch_faces/clock/clock_face.c b/movement/watch_faces/clock/clock_face.c index 20a02e7..d517897 100644 --- a/movement/watch_faces/clock/clock_face.c +++ b/movement/watch_faces/clock/clock_face.c @@ -60,6 +60,10 @@ static bool clock_is_in_24h_mode(movement_settings_t *settings) { #endif } +static bool clock_should_set_leading_zero(movement_settings_t *settings) { + return clock_is_in_24h_mode(settings) && settings->bit.clock_24h_leading_zero; +} + static void clock_indicate(WatchIndicatorSegment indicator, bool on) { if (on) { watch_set_indicator(indicator); @@ -124,13 +128,13 @@ static void clock_toggle_time_signal(clock_state_t *clock) { clock_indicate_time_signal(clock); } -static void clock_display_all(watch_date_time date_time) { +static void clock_display_all(watch_date_time date_time, bool leading_zero) { char buf[10 + 1]; snprintf( buf, sizeof(buf), - "%s%2d%2d%02d%02d", + leading_zero? "%s%02d%02d%02d%02d" : "%s%2d%2d%02d%02d", watch_utility_get_weekday(date_time), date_time.unit.day, date_time.unit.hour, @@ -180,7 +184,7 @@ static void clock_display_clock(movement_settings_t *settings, clock_state_t *cl clock_indicate_pm(settings, current); current = clock_24h_to_12h(current); } - clock_display_all(current); + clock_display_all(current, clock_should_set_leading_zero(settings)); } } diff --git a/movement/watch_faces/clock/close_enough_clock_face.c b/movement/watch_faces/clock/close_enough_clock_face.c new file mode 100644 index 0000000..cbd62e2 --- /dev/null +++ b/movement/watch_faces/clock/close_enough_clock_face.c @@ -0,0 +1,233 @@ +/* + * MIT License + * + * Copyright (c) 2024 Ruben Nic + * + * 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 +#include +#include +#include "close_enough_clock_face.h" +#include "watch.h" +#include "watch_utility.h" + +const char *words[12] = { + " ", + " 5", + "10", + "15", + "20", + "25", + "30", + "35", + "40", + "45", + "50", + "55", +}; + +static const char *past_word = " P"; +static const char *to_word = " 2"; +static const char *oclock_word = "OC"; + +// sets when in the five minute period we switch +// from "X past HH" to "X to HH+1" +static const int hour_switch_index = 8; + +static void _update_alarm_indicator(bool settings_alarm_enabled, close_enough_clock_state_t *state) { + state->alarm_enabled = settings_alarm_enabled; + if (state->alarm_enabled) { + watch_set_indicator(WATCH_INDICATOR_BELL); + } else { + watch_clear_indicator(WATCH_INDICATOR_BELL); + }; +} + +void close_enough_clock_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) { + (void) settings; + (void) watch_face_index; + + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(close_enough_clock_state_t)); + } +} + +void close_enough_clock_face_activate(movement_settings_t *settings, void *context) { + close_enough_clock_state_t *state = (close_enough_clock_state_t *)context; + + if (watch_tick_animation_is_running()) { + watch_stop_tick_animation(); + } + + if (settings->bit.clock_mode_24h) { + watch_set_indicator(WATCH_INDICATOR_24H); + } + + // show alarm indicator if there is an active alarm + _update_alarm_indicator(settings->bit.alarm_enabled, state); + + // this ensures that none of the five_minute_periods will match, so we always rerender when the face activates + state->prev_five_minute_period = -1; + state->prev_min_checked = -1; +} + +bool close_enough_clock_face_loop(movement_event_t event, movement_settings_t *settings, void *context) { + close_enough_clock_state_t *state = (close_enough_clock_state_t *)context; + + char buf[11]; + watch_date_time date_time; + bool show_next_hour = false; + int prev_five_minute_period; + int prev_min_checked; + int close_enough_hour; + + switch (event.event_type) { + case EVENT_ACTIVATE: + case EVENT_TICK: + case EVENT_LOW_ENERGY_UPDATE: + date_time = watch_rtc_get_date_time(); + prev_five_minute_period = state->prev_five_minute_period; + prev_min_checked = state->prev_min_checked; + + // check the battery voltage once a day... + if (date_time.unit.day != state->last_battery_check) { + state->last_battery_check = date_time.unit.day; + watch_enable_adc(); + uint16_t voltage = watch_get_vcc_voltage(); + watch_disable_adc(); + // 2.2 volts will happen when the battery has maybe 5-10% remaining? + // we can refine this later. + state->battery_low = (voltage < 2200); + } + + // ...and set the LAP indicator if low. + if (state->battery_low) { + watch_set_indicator(WATCH_INDICATOR_LAP); + } + + // same minute, skip update + if (date_time.unit.minute == prev_min_checked) { + break; + } else { + state->prev_min_checked = date_time.unit.minute; + } + + int five_minute_period = (date_time.unit.minute / 5) % 12; + + // If we are 60% to the next 5 interval, move up to the next period + if (fmodf(date_time.unit.minute / 5.0f, 1.0f) > 0.5f) { + // If we are on the last 5 interval and moving to the next period we need to display the next hour because we are wrapping around + if (five_minute_period == 11) { + show_next_hour = true; + } + + five_minute_period = (five_minute_period + 1) % 12; + } + + // same five_minute_period, skip update + if (five_minute_period == prev_five_minute_period) { + break; + } + + // we don't want to modify date_time.unit.hour just in case other watch faces use it + close_enough_hour = date_time.unit.hour; + + // move from "MM(mins) P HH" to "MM(mins) 2 HH+1" + if (five_minute_period >= hour_switch_index || show_next_hour) { + close_enough_hour = (close_enough_hour + 1) % 24; + } + + if (!settings->bit.clock_mode_24h) { + // if we are in 12 hour mode, do some cleanup. + if (close_enough_hour < 12) { + watch_clear_indicator(WATCH_INDICATOR_PM); + } else { + watch_set_indicator(WATCH_INDICATOR_PM); + } + + close_enough_hour %= 12; + if (close_enough_hour == 0) { + close_enough_hour = 12; + } + + date_time.unit.hour %= 12; + if (date_time.unit.hour == 0) { + date_time.unit.hour = 12; + } + } + + char first_word[3]; + char second_word[3]; + char third_word[3]; + if (five_minute_period == 0) { // "HH OC", + sprintf(first_word, "%2d", close_enough_hour); + strncpy(second_word, words[five_minute_period], 3); + strncpy(third_word, oclock_word, 3); + } else { + int words_length = sizeof(words) / sizeof(words[0]); + + strncpy( + first_word, + five_minute_period >= hour_switch_index ? + words[words_length - five_minute_period] : + words[five_minute_period], + 3 + ); + strncpy( + second_word, + five_minute_period >= hour_switch_index ? + to_word : past_word, + 3 + ); + sprintf(third_word, "%2d", close_enough_hour); + } + + sprintf( + buf, + "%s%2d%s%s%s", + watch_utility_get_weekday(date_time), + date_time.unit.day, + first_word, + second_word, + third_word + ); + + watch_display_string(buf, 0); + state->prev_five_minute_period = five_minute_period; + + // handle alarm indicator + if (state->alarm_enabled != settings->bit.alarm_enabled) { + _update_alarm_indicator(settings->bit.alarm_enabled, state); + } + + break; + + default: + return movement_default_loop_handler(event, settings); + } + + return true; +} + +void close_enough_clock_face_resign(movement_settings_t *settings, void *context) { + (void) settings; + (void) context; +} diff --git a/movement/watch_faces/clock/close_enough_clock_face.h b/movement/watch_faces/clock/close_enough_clock_face.h new file mode 100644 index 0000000..736a6c6 --- /dev/null +++ b/movement/watch_faces/clock/close_enough_clock_face.h @@ -0,0 +1,62 @@ +/* + * MIT License + * + * Copyright (c) 2024 Ruben Nic + * + * 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 CLOSE_ENOUGH_CLOCK_FACE_H_ +#define CLOSE_ENOUGH_CLOCK_FACE_H_ + +/* + * CLOSE ENOUGH CLOCK FACE + * + * Displays the current time; but only in periods of 5. + * Just in the in the formats of: + * - "10 past 5" + * - "15 to 7" + * - "6 o'clock" + * + */ + +#include "movement.h" + +typedef struct { + int prev_five_minute_period; + int prev_min_checked; + uint8_t last_battery_check; + bool battery_low; + bool alarm_enabled; +} close_enough_clock_state_t; + +void close_enough_clock_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); +void close_enough_clock_face_activate(movement_settings_t *settings, void *context); +bool close_enough_clock_face_loop(movement_event_t event, movement_settings_t *settings, void *context); +void close_enough_clock_face_resign(movement_settings_t *settings, void *context); + +#define close_enough_clock_face ((const watch_face_t){ \ + close_enough_clock_face_setup, \ + close_enough_clock_face_activate, \ + close_enough_clock_face_loop, \ + close_enough_clock_face_resign, \ + NULL, \ +}) + +#endif // CLOSE_ENOUGH_CLOCK_FACE_H_ diff --git a/movement/watch_faces/clock/french_revolutionary_face.c b/movement/watch_faces/clock/french_revolutionary_face.c new file mode 100644 index 0000000..da94fc9 --- /dev/null +++ b/movement/watch_faces/clock/french_revolutionary_face.c @@ -0,0 +1,245 @@ +/* + * MIT License + * + * Copyright (c) 2023 CarpeNoctem + * + * 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 +#include +#include "french_revolutionary_face.h" + +void french_revolutionary_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) { + (void) settings; + (void) watch_face_index; + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(french_revolutionary_state_t)); + memset(*context_ptr, 0, sizeof(french_revolutionary_state_t)); + // Do any one-time tasks in here; the inside of this conditional happens only at boot. + french_revolutionary_state_t *state = (french_revolutionary_state_t *)*context_ptr; + state->use_am_pm = false; + state->show_seconds = true; + state->display_type = 0; + state->colon_set_after_splash = false; + } + // Do any pin or peripheral setup here; this will be called whenever the watch wakes from deep sleep. +} + +void french_revolutionary_face_activate(movement_settings_t *settings, void *context) { + (void) settings; + french_revolutionary_state_t *state = (french_revolutionary_state_t *)context; + + // Handle any tasks related to your watch face coming on screen. + state->colon_set_after_splash = false; +} + +bool french_revolutionary_face_loop(movement_event_t event, movement_settings_t *settings, void *context) { + french_revolutionary_state_t *state = (french_revolutionary_state_t *)context; + + char buf[11]; + watch_date_time date_time; + fr_decimal_time decimal_time; + + switch (event.event_type) { + case EVENT_ACTIVATE: + // Initial UI - Show a quick "splash screen" + watch_clear_display(); + watch_display_string("FR dECimL", 0); + break; + case EVENT_TICK: + case EVENT_LOW_ENERGY_UPDATE: + + date_time = watch_rtc_get_date_time(); + + decimal_time = get_decimal_time(&date_time); + + set_display_buffer(buf, state, &decimal_time, &date_time); + + // If we're in low-energy mode, don't write out the seconds. Also start the LE tick animation if it's not already going. + if (event.event_type == EVENT_LOW_ENERGY_UPDATE) { + buf[8] = ' '; + buf[9] = ' '; + if (!watch_tick_animation_is_running()) { watch_start_tick_animation(500); } + } + + // Update the display with our decimal time + watch_display_string(buf, 0); + + // Oh, and a one-off to set the colon after the "splash screen" + if (!state->colon_set_after_splash) { + watch_set_colon(); + state->colon_set_after_splash = true; + } + break; + case EVENT_ALARM_BUTTON_UP: + state->display_type += 1 ; // cycle through the display types + if (state->display_type > 2) { state->display_type = 0; } // but return to 0 after 2 + break; + case EVENT_ALARM_LONG_PRESS: + // I originally had chiming on the decimal-hour enabled, and this would enable/disable that chime, just like on + // the simple clock and decimal time faces. But because decimal seconds don't always line up with normal seconds, + // I assume the (decimal-)hourly chime could sometimes be missed. Additionally, I need this button for other purposes, + // now that I added seconds on/off toggle and upper normal-time with the ability to toggle that between 12/24hr format. + state->show_seconds = !state->show_seconds; + if (!state->show_seconds) { watch_display_string(" ", 8); } + else { watch_display_string("--", 8); } + break; + case EVENT_LIGHT_LONG_PRESS: + // In case anyone really wants that upper time in 12-hour format. I thought about using the global setting (settings->bit.clock_mode_24h) + // for this preference, but thought someone who prefers 12-hour format normally, might prefer 24hr when compared to a 10hr decimal day, + // so this is separate for now. + state->use_am_pm = !state->use_am_pm; + if (state->use_am_pm) { + watch_clear_indicator(WATCH_INDICATOR_24H); + date_time = watch_rtc_get_date_time(); + if (date_time.unit.hour < 12) { watch_clear_indicator(WATCH_INDICATOR_PM); } + else { watch_set_indicator(WATCH_INDICATOR_PM); } + } else { + watch_clear_indicator(WATCH_INDICATOR_PM); + watch_set_indicator(WATCH_INDICATOR_24H); + } + 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 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 french_revolutionary_face_resign(movement_settings_t *settings, void *context) { + (void) settings; + (void) context; + + // handle any cleanup before your watch face goes off-screen. +} + +// Calculate decimal time from normal (24hr) time +fr_decimal_time get_decimal_time(watch_date_time *date_time) { + uint32_t current_24hr_secs, current_decimal_seconds; + fr_decimal_time decimal_time; + // Current 24-hr time in seconds (There are 86400 of these in a day.) + current_24hr_secs = date_time->unit.hour * 3600 + date_time->unit.minute * 60 + date_time->unit.second; + + // Current Decimal Time in seconds. There are 100000 seconds in a 10-hr decimal-time day. + // current_decimal_seconds = current_24hr_seconds * 100000 / 86400, or = current_24_seconds * 1000 / 864; + // By chopping the extra zeros off the end, we can use uint32 instead of uint64. + current_decimal_seconds = current_24hr_secs * 1000 / 864; + + decimal_time.hour = current_decimal_seconds / 10000; + // Remove the hours from total seconds and keep the remainder for below. + current_decimal_seconds = current_decimal_seconds - decimal_time.hour * 10000; + + decimal_time.minute = current_decimal_seconds / 100; + // Remove the minutes from total seconds and keep the remaining seconds + // Note: I think I used an extra seconds variable here because sprintf or movement weren't liking a uint32... + decimal_time.second = current_decimal_seconds - decimal_time.minute * 100; + return decimal_time; +} + +// Fills in the display buffer, depending on the currently-selected display option (and sub-options): +// - Decimal-time only +// - Decimal-time with date in top-right +// - Decimal-time with normal time in the top (minutes first, then hours, due to display limitations) +// TODO: There is some power-saving stuff that simple clock does here around not redrawing characters that haven't changed, but we're not doing that here. +// I'll try to add that optimization could be added in a future commit. +void set_display_buffer(char *buf, french_revolutionary_state_t *state, fr_decimal_time *decimal_time, watch_date_time *date_time) { + switch (state->display_type) { + // Decimal time only + case 0: + // Originally I had the day slot set to "FR" (French Revolutionary time), but my brain kept thinking "Friday" whenever I saw it, + // so I changed it to dT (Decimal Time) to avoid that confusion. Apologies to anyone who has the other decimal_time face and this one + // installed concurrently. Maybe the splash screen will help a little. + sprintf( buf, "dT %2d%02d%02d", decimal_time->hour, decimal_time->minute, decimal_time->second ); + watch_clear_indicator(WATCH_INDICATOR_PM); + watch_clear_indicator(WATCH_INDICATOR_24H); + break; + // Decimal time and date + case 1: + sprintf( buf, "dT%2d%2d%02d%02d", date_time->unit.day, decimal_time->hour, decimal_time->minute, decimal_time->second ); + watch_clear_indicator(WATCH_INDICATOR_PM); + watch_clear_indicator(WATCH_INDICATOR_24H); + break; + // Decimal time on bottom, normal time above + case 2: + if (state->use_am_pm) { + // if we are in 12 hour mode, do some cleanup. + watch_clear_indicator(WATCH_INDICATOR_24H); + if (date_time->unit.hour < 12) { + watch_clear_indicator(WATCH_INDICATOR_PM); + } else { + watch_set_indicator(WATCH_INDICATOR_PM); + } + date_time->unit.hour %= 12; + if (date_time->unit.hour == 0) date_time->unit.hour = 12; + } else { + watch_clear_indicator(WATCH_INDICATOR_PM); + watch_set_indicator(WATCH_INDICATOR_24H); + } + // Note, the date digits don't display a leading zero well, so we don't use it. + sprintf( buf, "%02d%2d%2d%02d%02d", date_time->unit.minute, date_time->unit.hour, decimal_time->hour, decimal_time->minute, decimal_time->second ); + + // Make the second character of the Day area more readable + buf[1] = fix_character_one(buf[1]); + break; + } + // Finally, if show_seconds is disabled, trim those off. + if (!state->show_seconds) { + buf[8] = ' '; + buf[9] = ' '; + } +} + +// Sadly, the second character of the Day field cannot show all numbers, so we make some replacements. +// See https://www.sensorwatch.net/docs/wig/display/#limitations-of-the-weekday-digits +char fix_character_one(char digit) { + char return_char = digit; // We don't need to update this for 0, 1, 3, 7 and 8. + switch(digit) { + case '2': + // Roman numeral / tally representation of 2 + return_char = '|'; // Thanks, Joey, for already having this in the character set. + break; + case '4': + // Looks almost like a 4 - just missing the top-left segment. + // 0b01000110 + return_char = '&'; // Slight hack - I want 0b01000110, but 0b01000100 is already in the character set and will do, since B and C segments are linked in this position. + break; + case '5': + return_char = 'F'; // F for Five + break; + case '6': + return_char = 'E'; // Looks almost like a 6 - just missing the bottom-right segment. Not super happy with it, but liked it best of the options I tried. + break; + case '9': + return_char = 'N'; // N for Nine + break; + } + return return_char; +} diff --git a/movement/watch_faces/clock/french_revolutionary_face.h b/movement/watch_faces/clock/french_revolutionary_face.h new file mode 100644 index 0000000..294f422 --- /dev/null +++ b/movement/watch_faces/clock/french_revolutionary_face.h @@ -0,0 +1,84 @@ +/* + * MIT License + * + * Copyright (c) 2023 CarpeNoctem + * + * 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 FRENCH_REVOLUTIONARY_FACE_H_ +#define FRENCH_REVOLUTIONARY_FACE_H_ + +#include "movement.h" + +/* + * French Revolutionary Decimal Time + * + * Similar to the Decimal Time face, but with the day divided into ten hours instead of twenty four. + * Each hour is divided into one hundred minutes, and those minutes are divided into 100 seconds. + * I came across this one the Svalbard watch site here: https://svalbard.watch/pages/about_decimal_time.html + * More info here as well: https://en.wikipedia.org/wiki/Decimal_time + * + * By default, the face just displays the current decimal time. Pressing the alarm button will toggle through other display options: + * 1) Just decimal time (with dT indicator at top) + * 2) Decimal time, with dT indicator and date above. + * 3) Decimal time, with 24-hr time above (where Day and Date would normally be displayed), BUT minutes first then hours. + * Sadly, the first character of the date area only goes up to 3 (see https://www.sensorwatch.net/docs/wig/display/#the-day-digits) + * I was going to begrudgindly leave this display option out when I realized that, but thought it would be better to have this backwards + * representation of the "normal" time than not at all. + * + * A long-press of the light button will toggle the upper time between 12-hr AM/PM and 24-hr mode. I thought of reading the main setting for this, + * but thought that a person could normally prefer 12hr time, but next to a 10hr day want to see the normal time in the 24hr format. + * + * A long-press of the alarm button will toggle the seconds off and on. + * + */ + +typedef struct { + bool use_am_pm; // Use 12-hr AM/PM for upper display instead of 24-hr? (Default is 24-hr) + bool show_seconds; + bool colon_set_after_splash; + uint8_t display_type : 2; +} french_revolutionary_state_t; + +typedef struct { + uint8_t second : 8; // 0-99 + uint8_t minute : 8; // 0-99 + uint8_t hour : 5; // 0-10 +} fr_decimal_time; + +void french_revolutionary_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); +void french_revolutionary_face_activate(movement_settings_t *settings, void *context); +bool french_revolutionary_face_loop(movement_event_t event, movement_settings_t *settings, void *context); +void french_revolutionary_face_resign(movement_settings_t *settings, void *context); +char fix_character_one(char digit); +fr_decimal_time get_decimal_time(watch_date_time *date_time); +void set_display_buffer(char *buf, french_revolutionary_state_t *state, fr_decimal_time *decimal_time, watch_date_time *date_time); + + +#define french_revolutionary_face ((const watch_face_t){ \ + french_revolutionary_face_setup, \ + french_revolutionary_face_activate, \ + french_revolutionary_face_loop, \ + french_revolutionary_face_resign, \ + NULL, \ +}) + +#endif // FRENCH_REVOLUTIONARY_FACE_H_ + diff --git a/movement/watch_faces/clock/minimal_clock_face.c b/movement/watch_faces/clock/minimal_clock_face.c new file mode 100644 index 0000000..fa4880e --- /dev/null +++ b/movement/watch_faces/clock/minimal_clock_face.c @@ -0,0 +1,117 @@ +/* + * MIT License + * + * Copyright (c) 2023 Dennisman219 + * + * 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 +#include +#include "minimal_clock_face.h" + +static void _minimal_clock_face_update_display(movement_settings_t *settings) { + watch_date_time date_time = watch_rtc_get_date_time(); + char buffer[11]; + + if (!settings->bit.clock_mode_24h) { + date_time.unit.hour %= 12; + sprintf(buffer, "%2d%02d ", date_time.unit.hour, date_time.unit.minute); + } else { + sprintf(buffer, "%02d%02d ", date_time.unit.hour, date_time.unit.minute); + } + + watch_display_string(buffer, 4); +} + +void minimal_clock_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) { + (void) settings; + (void) watch_face_index; + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(minimal_clock_state_t)); + memset(*context_ptr, 0, sizeof(minimal_clock_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. +} + +void minimal_clock_face_activate(movement_settings_t *settings, void *context) { + (void) settings; + (void) context; + // Handle any tasks related to your watch face coming on screen. + watch_set_colon(); +} + +bool minimal_clock_face_loop(movement_event_t event, movement_settings_t *settings, void *context) { + (void) context; + + switch (event.event_type) { + case EVENT_ACTIVATE: + // Show your initial UI here. + _minimal_clock_face_update_display(settings); + break; + case EVENT_TICK: + // If needed, update your display here. + _minimal_clock_face_update_display(settings); + break; + case EVENT_LIGHT_BUTTON_UP: + // You can use the Light button for your own purposes. Note that by default, Movement will also + // illuminate the LED in response to EVENT_LIGHT_BUTTON_DOWN; to suppress that behavior, add an + // empty case for EVENT_LIGHT_BUTTON_DOWN. + break; + case EVENT_ALARM_BUTTON_UP: + // Just in case you have need for another button. + 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; + 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); + _minimal_clock_face_update_display(settings); + 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 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 minimal_clock_face_resign(movement_settings_t *settings, void *context) { + (void) settings; + (void) context; + + // handle any cleanup before your watch face goes off-screen. +} + diff --git a/movement/watch_faces/clock/minimal_clock_face.h b/movement/watch_faces/clock/minimal_clock_face.h new file mode 100644 index 0000000..d1f6ddf --- /dev/null +++ b/movement/watch_faces/clock/minimal_clock_face.h @@ -0,0 +1,57 @@ +/* + * MIT License + * + * Copyright (c) 2023 Dennisman219 + * + * 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 MINIMAL_CLOCK_FACE_H_ +#define MINIMAL_CLOCK_FACE_H_ + +#include "movement.h" + +/* + * MINIMAL CLOCK FACE + * + * A minimal clock face that just shows hours and minutes. + * There is nothing to configure. The face follows the 12h/24h setting + * + */ + +typedef struct { + // Anything you need to keep track of, put it here! + uint8_t unused; +} minimal_clock_state_t; + +void minimal_clock_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); +void minimal_clock_face_activate(movement_settings_t *settings, void *context); +bool minimal_clock_face_loop(movement_event_t event, movement_settings_t *settings, void *context); +void minimal_clock_face_resign(movement_settings_t *settings, void *context); + +#define minimal_clock_face ((const watch_face_t){ \ + minimal_clock_face_setup, \ + minimal_clock_face_activate, \ + minimal_clock_face_loop, \ + minimal_clock_face_resign, \ + NULL, \ +}) + +#endif // MINIMAL_CLOCK_FACE_H_ + diff --git a/movement/watch_faces/clock/repetition_minute_face.c b/movement/watch_faces/clock/repetition_minute_face.c index e9e5e31..71a1f78 100644 --- a/movement/watch_faces/clock/repetition_minute_face.c +++ b/movement/watch_faces/clock/repetition_minute_face.c @@ -68,7 +68,7 @@ void repetition_minute_face_activate(movement_settings_t *settings, void *contex if (watch_tick_animation_is_running()) watch_stop_tick_animation(); - if (settings->bit.clock_mode_24h) watch_set_indicator(WATCH_INDICATOR_24H); + if (settings->bit.clock_mode_24h && !settings->bit.clock_24h_leading_zero) watch_set_indicator(WATCH_INDICATOR_24H); // handle chime indicator if (state->signal_enabled) watch_set_indicator(WATCH_INDICATOR_BELL); @@ -112,6 +112,7 @@ bool repetition_minute_face_loop(movement_event_t event, movement_settings_t *se // ...and set the LAP indicator if low. if (state->battery_low) watch_set_indicator(WATCH_INDICATOR_LAP); + bool set_leading_zero = false; if ((date_time.reg >> 6) == (previous_date_time >> 6) && event.event_type != EVENT_LOW_ENERGY_UPDATE) { // everything before seconds is the same, don't waste cycles setting those segments. watch_display_character_lp_seconds('0' + date_time.unit.second / 10, 8); @@ -132,6 +133,8 @@ bool repetition_minute_face_loop(movement_event_t event, movement_settings_t *se } date_time.unit.hour %= 12; if (date_time.unit.hour == 0) date_time.unit.hour = 12; + } else if (settings->bit.clock_24h_leading_zero && date_time.unit.hour < 10) { + set_leading_zero = true; } pos = 0; if (event.event_type == EVENT_LOW_ENERGY_UPDATE) { @@ -142,6 +145,8 @@ bool repetition_minute_face_loop(movement_event_t event, movement_settings_t *se } } watch_display_string(buf, pos); + if (set_leading_zero) + watch_display_string("0", 4); // handle alarm indicator if (state->alarm_enabled != settings->bit.alarm_enabled) _update_alarm_indicator(settings->bit.alarm_enabled, state); break; diff --git a/movement/watch_faces/clock/simple_clock_bin_led_face.c b/movement/watch_faces/clock/simple_clock_bin_led_face.c index cf39c18..371f1e1 100644 --- a/movement/watch_faces/clock/simple_clock_bin_led_face.c +++ b/movement/watch_faces/clock/simple_clock_bin_led_face.c @@ -60,7 +60,7 @@ void simple_clock_bin_led_face_activate(movement_settings_t *settings, void *con if (watch_tick_animation_is_running()) watch_stop_tick_animation(); - if (settings->bit.clock_mode_24h) watch_set_indicator(WATCH_INDICATOR_24H); + if (settings->bit.clock_mode_24h && !settings->bit.clock_24h_leading_zero) watch_set_indicator(WATCH_INDICATOR_24H); // handle chime indicator if (state->signal_enabled) watch_set_indicator(WATCH_INDICATOR_BELL); @@ -138,6 +138,7 @@ bool simple_clock_bin_led_face_loop(movement_event_t event, movement_settings_t // ...and set the LAP indicator if low. if (state->battery_low) watch_set_indicator(WATCH_INDICATOR_LAP); + bool set_leading_zero = false; if ((date_time.reg >> 6) == (previous_date_time >> 6) && event.event_type != EVENT_LOW_ENERGY_UPDATE) { // everything before seconds is the same, don't waste cycles setting those segments. watch_display_character_lp_seconds('0' + date_time.unit.second / 10, 8); @@ -158,6 +159,8 @@ bool simple_clock_bin_led_face_loop(movement_event_t event, movement_settings_t } date_time.unit.hour %= 12; if (date_time.unit.hour == 0) date_time.unit.hour = 12; + } else if (settings->bit.clock_24h_leading_zero && date_time.unit.hour < 10) { + set_leading_zero = true; } pos = 0; if (event.event_type == EVENT_LOW_ENERGY_UPDATE) { @@ -168,6 +171,8 @@ bool simple_clock_bin_led_face_loop(movement_event_t event, movement_settings_t } } watch_display_string(buf, pos); + if (set_leading_zero) + watch_display_string("0", 4); // handle alarm indicator if (state->alarm_enabled != settings->bit.alarm_enabled) _update_alarm_indicator(settings->bit.alarm_enabled, state); } diff --git a/movement/watch_faces/clock/simple_clock_face.c b/movement/watch_faces/clock/simple_clock_face.c index ac4ec2e..68e1ccd 100644 --- a/movement/watch_faces/clock/simple_clock_face.c +++ b/movement/watch_faces/clock/simple_clock_face.c @@ -99,6 +99,7 @@ bool simple_clock_face_loop(movement_event_t event, movement_settings_t *setting // ...and set the LAP indicator if low. if (state->battery_low) watch_set_indicator(WATCH_INDICATOR_LAP); + bool set_leading_zero = false; if ((date_time.reg >> 6) == (previous_date_time >> 6) && event.event_type != EVENT_LOW_ENERGY_UPDATE) { // everything before seconds is the same, don't waste cycles setting those segments. watch_display_character_lp_seconds('0' + date_time.unit.second / 10, 8); @@ -122,6 +123,11 @@ bool simple_clock_face_loop(movement_event_t event, movement_settings_t *setting if (date_time.unit.hour == 0) date_time.unit.hour = 12; } #endif + + if (settings->bit.clock_mode_24h && settings->bit.clock_24h_leading_zero && date_time.unit.hour < 10) { + set_leading_zero = true; + } + pos = 0; if (event.event_type == EVENT_LOW_ENERGY_UPDATE) { if (!watch_tick_animation_is_running()) watch_start_tick_animation(500); @@ -131,6 +137,10 @@ bool simple_clock_face_loop(movement_event_t event, movement_settings_t *setting } } watch_display_string(buf, pos); + + if (set_leading_zero) + watch_display_string("0", 4); + // handle alarm indicator if (state->alarm_enabled != settings->bit.alarm_enabled) _update_alarm_indicator(settings->bit.alarm_enabled, state); break; diff --git a/movement/watch_faces/clock/weeknumber_clock_face.c b/movement/watch_faces/clock/weeknumber_clock_face.c index 81df584..ed694e6 100644 --- a/movement/watch_faces/clock/weeknumber_clock_face.c +++ b/movement/watch_faces/clock/weeknumber_clock_face.c @@ -50,7 +50,7 @@ void weeknumber_clock_face_activate(movement_settings_t *settings, void *context if (watch_tick_animation_is_running()) watch_stop_tick_animation(); - if (settings->bit.clock_mode_24h) watch_set_indicator(WATCH_INDICATOR_24H); + if (settings->bit.clock_mode_24h && !settings->bit.clock_24h_leading_zero) watch_set_indicator(WATCH_INDICATOR_24H); // handle chime indicator if (state->signal_enabled) watch_set_indicator(WATCH_INDICATOR_BELL); @@ -94,6 +94,7 @@ bool weeknumber_clock_face_loop(movement_event_t event, movement_settings_t *set // ...and set the LAP indicator if low. if (state->battery_low) watch_set_indicator(WATCH_INDICATOR_LAP); + bool set_leading_zero = false; if ((date_time.reg >> 12) == (previous_date_time >> 12) && event.event_type != EVENT_LOW_ENERGY_UPDATE) { // everything before minutes is the same. pos = 6; @@ -109,6 +110,8 @@ bool weeknumber_clock_face_loop(movement_event_t event, movement_settings_t *set } date_time.unit.hour %= 12; if (date_time.unit.hour == 0) date_time.unit.hour = 12; + } else if (settings->bit.clock_24h_leading_zero && date_time.unit.hour < 10) { + set_leading_zero = true; } pos = 0; if (event.event_type == EVENT_LOW_ENERGY_UPDATE) { @@ -119,6 +122,8 @@ bool weeknumber_clock_face_loop(movement_event_t event, movement_settings_t *set } } watch_display_string(buf, pos); + if (set_leading_zero) + watch_display_string("0", 4); // handle alarm indicator if (state->alarm_enabled != settings->bit.alarm_enabled) _update_alarm_indicator(settings->bit.alarm_enabled, state); break; diff --git a/movement/watch_faces/clock/world_clock2_face.c b/movement/watch_faces/clock/world_clock2_face.c index 2e1e969..5b3f8a9 100644 --- a/movement/watch_faces/clock/world_clock2_face.c +++ b/movement/watch_faces/clock/world_clock2_face.c @@ -174,7 +174,7 @@ static bool mode_display(movement_event_t event, movement_settings_t *settings, if (refresh_face) { watch_clear_indicator(WATCH_INDICATOR_SIGNAL); watch_set_colon(); - if (settings->bit.clock_mode_24h) + if (settings->bit.clock_mode_24h && !settings->bit.clock_24h_leading_zero) watch_set_indicator(WATCH_INDICATOR_24H); state->previous_date_time = REFRESH_TIME; @@ -188,6 +188,7 @@ static bool mode_display(movement_event_t event, movement_settings_t *settings, previous_date_time = state->previous_date_time; state->previous_date_time = date_time.reg; + bool set_leading_zero = false; if ((date_time.reg >> 6) == (previous_date_time >> 6) && event.event_type != EVENT_LOW_ENERGY_UPDATE) { /* Everything before seconds is the same, don't waste cycles setting those segments. */ pos = 8; @@ -208,7 +209,9 @@ static bool mode_display(movement_event_t event, movement_settings_t *settings, date_time.unit.hour %= 12; if (date_time.unit.hour == 0) date_time.unit.hour = 12; - } + } else if (settings->bit.clock_24h_leading_zero && date_time.unit.hour < 10) { + set_leading_zero = true; + } pos = 0; if (event.event_type == EVENT_LOW_ENERGY_UPDATE) { @@ -230,6 +233,8 @@ static bool mode_display(movement_event_t event, movement_settings_t *settings, } } watch_display_string(buf, pos); + if (set_leading_zero) + watch_display_string("0", 4); break; case EVENT_ALARM_BUTTON_UP: state->current_zone = find_selected_zone(state, FORWARD); diff --git a/movement/watch_faces/clock/world_clock_face.c b/movement/watch_faces/clock/world_clock_face.c index b12d9cd..6b731e8 100644 --- a/movement/watch_faces/clock/world_clock_face.c +++ b/movement/watch_faces/clock/world_clock_face.c @@ -60,7 +60,7 @@ static bool world_clock_face_do_display_mode(movement_event_t event, movement_se watch_date_time date_time; switch (event.event_type) { case EVENT_ACTIVATE: - if (settings->bit.clock_mode_24h) watch_set_indicator(WATCH_INDICATOR_24H); + if (settings->bit.clock_mode_24h && !settings->bit.clock_24h_leading_zero) watch_set_indicator(WATCH_INDICATOR_24H); watch_set_colon(); state->previous_date_time = 0xFFFFFFFF; // fall through @@ -72,6 +72,7 @@ static bool world_clock_face_do_display_mode(movement_event_t event, movement_se previous_date_time = state->previous_date_time; state->previous_date_time = date_time.reg; + bool set_leading_zero = false; if ((date_time.reg >> 6) == (previous_date_time >> 6) && event.event_type != EVENT_LOW_ENERGY_UPDATE) { // everything before seconds is the same, don't waste cycles setting those segments. pos = 8; @@ -91,6 +92,8 @@ static bool world_clock_face_do_display_mode(movement_event_t event, movement_se } date_time.unit.hour %= 12; if (date_time.unit.hour == 0) date_time.unit.hour = 12; + } else if (settings->bit.clock_24h_leading_zero && date_time.unit.hour < 10) { + set_leading_zero = true; } pos = 0; if (event.event_type == EVENT_LOW_ENERGY_UPDATE) { @@ -112,6 +115,8 @@ static bool world_clock_face_do_display_mode(movement_event_t event, movement_se } } watch_display_string(buf, pos); + if (set_leading_zero) + watch_display_string("0", 4); break; case EVENT_ALARM_LONG_PRESS: movement_request_tick_frequency(4); diff --git a/movement/watch_faces/complication/activity_face.c b/movement/watch_faces/complication/activity_face.c index f92984c..b26fbeb 100644 --- a/movement/watch_faces/complication/activity_face.c +++ b/movement/watch_faces/complication/activity_face.c @@ -293,6 +293,7 @@ static void _activity_update_logging_screen(movement_settings_t *settings, activ } // Briefly, show time without seconds else { + bool set_leading_zero = false; watch_clear_indicator(WATCH_INDICATOR_LAP); watch_date_time now = watch_rtc_get_date_time(); uint8_t hour = now.unit.hour; @@ -304,14 +305,18 @@ static void _activity_update_logging_screen(movement_settings_t *settings, activ watch_set_indicator(WATCH_INDICATOR_PM); hour %= 12; if (hour == 0) hour = 12; - } - else { - watch_set_indicator(WATCH_INDICATOR_24H); + } else { watch_clear_indicator(WATCH_INDICATOR_PM); + if (!settings->bit.clock_24h_leading_zero) + watch_set_indicator(WATCH_INDICATOR_24H); + else if (hour < 10) + set_leading_zero = true; } sprintf(activity_buf, "%2d%02d ", hour, now.unit.minute); watch_set_colon(); watch_display_string(activity_buf, 4); + if (set_leading_zero) + watch_display_string("0", 4); } } diff --git a/movement/watch_faces/complication/alarm_face.c b/movement/watch_faces/complication/alarm_face.c index 3cacc98..d71ea20 100644 --- a/movement/watch_faces/complication/alarm_face.c +++ b/movement/watch_faces/complication/alarm_face.c @@ -72,6 +72,7 @@ static void _alarm_face_draw(movement_settings_t *settings, alarm_state_t *state i = state->alarm[state->alarm_idx].day + 1; } //handle am/pm for hour display + bool set_leading_zero = false; uint8_t h = state->alarm[state->alarm_idx].hour; if (!settings->bit.clock_mode_24h) { if (h >= 12) { @@ -81,8 +82,17 @@ static void _alarm_face_draw(movement_settings_t *settings, alarm_state_t *state watch_clear_indicator(WATCH_INDICATOR_PM); } if (h == 0) h = 12; + } else { + watch_set_indicator(WATCH_INDICATOR_24H); + + if (settings->bit.clock_24h_leading_zero) { + if (h < 10) { + set_leading_zero = true; + } + } } - sprintf(buf, "%c%c%2d%2d%02d ", + + 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, @@ -92,7 +102,7 @@ static void _alarm_face_draw(movement_settings_t *settings, alarm_state_t *state buf[_blink_idx[state->setting_state]] = buf[_blink_idx2[state->setting_state]] = ' '; } watch_display_string(buf, 0); - + if (state->is_setting) { // draw pitch level indicator if ((subsecond % 2) == 0 || (state->setting_state != alarm_setting_idx_pitch)) { diff --git a/movement/watch_faces/complication/countdown_face.c b/movement/watch_faces/complication/countdown_face.c index b48ef59..f585ebc 100644 --- a/movement/watch_faces/complication/countdown_face.c +++ b/movement/watch_faces/complication/countdown_face.c @@ -1,6 +1,7 @@ /* * MIT License * + * Copyright (c) 2024 Joseph Bryant * Copyright (c) 2023 Konrad Rieck * Copyright (c) 2022 Wesley Ellis * @@ -68,17 +69,30 @@ static inline void button_beep(movement_settings_t *settings) { watch_buzzer_play_note(BUZZER_NOTE_C7, 50); } -static void start(countdown_state_t *state, movement_settings_t *settings) { - watch_date_time now = watch_rtc_get_date_time(); +static void schedule_countdown(countdown_state_t *state, movement_settings_t *settings) { - state->mode = cd_running; - state->now_ts = watch_utility_date_time_to_unix_time(now, get_tz_offset(settings)); - state->target_ts = watch_utility_offset_timestamp(state->now_ts, state->hours, state->minutes, state->seconds); + // Calculate the new state->now_ts but don't update it until we've updated the target - + // avoid possible race where the old target is compared to the new time and immediately triggers + uint32_t new_now = watch_utility_date_time_to_unix_time(watch_rtc_get_date_time(), get_tz_offset(settings)); + state->target_ts = watch_utility_offset_timestamp(new_now, state->hours, state->minutes, state->seconds); + state->now_ts = new_now; watch_date_time target_dt = watch_utility_date_time_from_unix_time(state->target_ts, get_tz_offset(settings)); - movement_schedule_background_task(target_dt); - watch_set_indicator(WATCH_INDICATOR_BELL); + movement_schedule_background_task_for_face(state->watch_face_index, target_dt); } +static void auto_repeat(countdown_state_t *state, movement_settings_t *settings) { + movement_play_alarm(); + load_countdown(state); + schedule_countdown(state, settings); +} + +static void start(countdown_state_t *state, movement_settings_t *settings) { + state->mode = cd_running; + schedule_countdown(state, settings); +} + + + static void draw(countdown_state_t *state, uint8_t subsecond) { char buf[16]; @@ -100,7 +114,7 @@ static void draw(countdown_state_t *state, uint8_t subsecond) { break; case cd_reset: case cd_paused: - watch_clear_indicator(WATCH_INDICATOR_BELL); + watch_clear_indicator(WATCH_INDICATOR_SIGNAL); sprintf(buf, "CD %2d%02d%02d", state->hours, state->minutes, state->seconds); break; case cd_setting: @@ -127,13 +141,13 @@ static void draw(countdown_state_t *state, uint8_t subsecond) { static void pause(countdown_state_t *state) { state->mode = cd_paused; - movement_cancel_background_task(); - watch_clear_indicator(WATCH_INDICATOR_BELL); + movement_cancel_background_task_for_face(state->watch_face_index); + watch_clear_indicator(WATCH_INDICATOR_SIGNAL); } static void reset(countdown_state_t *state) { state->mode = cd_reset; - movement_cancel_background_task(); + movement_cancel_background_task_for_face(state->watch_face_index); load_countdown(state); } @@ -142,6 +156,15 @@ static void ring(countdown_state_t *state) { reset(state); } +static void times_up(movement_settings_t *settings, countdown_state_t *state) { + if(state->repeat) { + auto_repeat(state, settings); + } + else { + ring(state); + } +} + static void settings_increment(countdown_state_t *state) { switch(state->selection) { case 0: @@ -170,6 +193,7 @@ void countdown_face_setup(movement_settings_t *settings, uint8_t watch_face_inde memset(*context_ptr, 0, sizeof(countdown_state_t)); state->minutes = DEFAULT_MINUTES; state->mode = cd_reset; + state->watch_face_index = watch_face_index; store_countdown(state); } } @@ -180,9 +204,11 @@ void countdown_face_activate(movement_settings_t *settings, void *context) { if(state->mode == cd_running) { watch_date_time now = watch_rtc_get_date_time(); state->now_ts = watch_utility_date_time_to_unix_time(now, get_tz_offset(settings)); - watch_set_indicator(WATCH_INDICATOR_BELL); + watch_set_indicator(WATCH_INDICATOR_SIGNAL); } watch_set_colon(); + if(state->repeat) + watch_set_indicator(WATCH_INDICATOR_BELL); movement_request_tick_frequency(1); quick_ticks_running = false; @@ -252,6 +278,7 @@ bool countdown_face_loop(movement_event_t event, movement_settings_t *settings, // Only start the timer if we have a valid time. start(state, settings); button_beep(settings); + watch_set_indicator(WATCH_INDICATOR_SIGNAL); } break; case cd_setting: @@ -261,9 +288,19 @@ bool countdown_face_loop(movement_event_t event, movement_settings_t *settings, draw(state, event.subsecond); break; case EVENT_ALARM_LONG_PRESS: - if (state->mode == cd_setting) { - quick_ticks_running = true; - movement_request_tick_frequency(8); + switch(state->mode) { + case cd_setting: + quick_ticks_running = true; + movement_request_tick_frequency(8); + break; + default: + // Toggle auto-repeat + button_beep(settings); + state->repeat = !state->repeat; + if(state->repeat) + watch_set_indicator(WATCH_INDICATOR_BELL); + else + watch_clear_indicator(WATCH_INDICATOR_BELL); } break; case EVENT_LIGHT_LONG_PRESS: @@ -285,7 +322,7 @@ bool countdown_face_loop(movement_event_t event, movement_settings_t *settings, abort_quick_ticks(state); break; case EVENT_BACKGROUND_TASK: - ring(state); + times_up(settings, state); break; case EVENT_TIMEOUT: abort_quick_ticks(state); diff --git a/movement/watch_faces/complication/countdown_face.h b/movement/watch_faces/complication/countdown_face.h index 1fe7c37..0f9cd8d 100644 --- a/movement/watch_faces/complication/countdown_face.h +++ b/movement/watch_faces/complication/countdown_face.h @@ -62,6 +62,8 @@ typedef struct { uint8_t set_seconds; uint8_t selection; countdown_mode_t mode; + bool repeat; + uint8_t watch_face_index; } countdown_state_t; diff --git a/movement/watch_faces/complication/deadline_face.c b/movement/watch_faces/complication/deadline_face.c new file mode 100644 index 0000000..ae9cab8 --- /dev/null +++ b/movement/watch_faces/complication/deadline_face.c @@ -0,0 +1,649 @@ +/* + * MIT License + * + * Copyright (c) 2023-2024 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 +#include +#include "deadline_face.h" +#include "watch.h" +#include "watch_utility.h" + +#define SETTINGS_NUM (5) +const char settings_titles[SETTINGS_NUM][3] = { "YR", "MO", "DA", "HR", "M1" }; + +/* Local functions */ +static void _running_init(movement_settings_t *settings, deadline_state_t *state); +static bool _running_loop(movement_event_t event, movement_settings_t *settings, void *context); +static void _running_display(movement_event_t event, movement_settings_t *settings, deadline_state_t *state); +static void _setting_init(movement_settings_t *settings, deadline_state_t *state); +static bool _setting_loop(movement_event_t event, movement_settings_t *settings, void *context); +static void _setting_display(movement_event_t event, movement_settings_t *settings, deadline_state_t *state, watch_date_time date); + +/* Utility functions */ +static void _background_alarm_play(movement_settings_t *settings, deadline_state_t *state); +static void _background_alarm_schedule(movement_settings_t *settings, deadline_state_t *state); +static void _background_alarm_cancel(movement_settings_t *settings, deadline_state_t *state); +static void _increment_date(movement_settings_t *settings, deadline_state_t *state, watch_date_time date_time); +static inline int32_t _get_tz_offset(movement_settings_t *settings); +static inline void _change_tick_freq(uint8_t freq, deadline_state_t *state); +static inline bool _is_leap(int16_t y); +static inline int _days_in_month(int16_t mpnth, int16_t y); +static inline unsigned int _mod(int a, int b); +static inline void _beep_button(movement_settings_t *settings); +static inline void _beep_enable(movement_settings_t *settings); +static inline void _beep_disable(movement_settings_t *settings); +static inline void _reset_deadline(movement_settings_t *settings, deadline_state_t *state); + +/* 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]; + } +} + +/* Return time zone offset */ +static inline int32_t _get_tz_offset(movement_settings_t *settings) +{ + return movement_timezone_offsets[settings->bit.time_zone] * 60; +} + +/* Beep for a button press*/ +static inline void _beep_button(movement_settings_t *settings) +{ + // Play a beep as confirmation for a button press (if applicable) + if (!settings->bit.button_should_sound) + return; + + watch_buzzer_play_note(BUZZER_NOTE_C7, 50); +} + +/* Beep for entering settings */ +static inline void _beep_enable(movement_settings_t *settings) +{ + if (!settings->bit.button_should_sound) + return; + + watch_buzzer_play_note(BUZZER_NOTE_G7, 50); + watch_buzzer_play_note(BUZZER_NOTE_REST, 75); + watch_buzzer_play_note(BUZZER_NOTE_C8, 75); +} + +/* Beep for leaving settings */ +static inline void _beep_disable(movement_settings_t *settings) +{ + if (!settings->bit.button_should_sound) + return; + + watch_buzzer_play_note(BUZZER_NOTE_C8, 50); + watch_buzzer_play_note(BUZZER_NOTE_REST, 75); + watch_buzzer_play_note(BUZZER_NOTE_G7, 75); +} + +/* 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(movement_settings_t *settings, deadline_state_t *state) +{ + watch_date_time now = watch_rtc_get_date_time(); + uint32_t now_ts = watch_utility_date_time_to_unix_time(now, _get_tz_offset(settings)); + uint32_t min_ts = UINT32_MAX; + uint8_t min_index = 0; + + for (uint8_t i = 0; i < DEADLINE_FACE_DATES; i++) { + 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(movement_settings_t *settings, deadline_state_t *state) +{ + (void) settings; + + /* Use the default alarm from movement and move to foreground */ + if (state->alarm_enabled) { + movement_play_alarm(); + movement_move_to_face(state->face_idx); + } +} + +/* Schedule background alarm */ +static void _background_alarm_schedule(movement_settings_t *settings, deadline_state_t *state) +{ + /* We simply re-use the scheduling in the background task */ + deadline_face_wants_background_task(settings, state); +} + +/* Cancel background alarm */ +static void _background_alarm_cancel(movement_settings_t *settings, deadline_state_t *state) +{ + (void) settings; + + movement_cancel_background_task_for_face(state->face_idx); +} + +/* Reset deadline to tomorrow */ +static inline void _reset_deadline(movement_settings_t *settings, deadline_state_t *state) +{ + /* Get current time and reset hours/minutes/seconds */ + watch_date_time date_time = watch_rtc_get_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, _get_tz_offset(settings)); + ts += 24 * 60 * 60; + + state->deadlines[state->current_index] = ts; +} + +/* Increment date in settings mode. Function taken from `set_time_face.c` */ +static void _increment_date(movement_settings_t *settings, deadline_state_t *state, watch_date_time 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, _get_tz_offset(settings)); + state->deadlines[state->current_index] = ts; +} + +/* Update display in running mode */ +static void _running_display(movement_event_t event, movement_settings_t *settings, deadline_state_t *state) +{ + (void) event; + (void) settings; + + /* Seconds, minutes, hours, days, months, years */ + int16_t unit[] = { 0, 0, 0, 0, 0, 0 }; + uint8_t i, range[] = { 60, 60, 24, 30, 12, 0 }; + char buf[16]; + + /* Display indicators */ + if (state->alarm_enabled) + watch_set_indicator(WATCH_INDICATOR_BELL); + else + watch_clear_indicator(WATCH_INDICATOR_BELL); + + watch_date_time now = watch_rtc_get_date_time(); + uint32_t now_ts = watch_utility_date_time_to_unix_time(now, _get_tz_offset(settings)); + + /* Deadline expired */ + if (state->deadlines[state->current_index] < now_ts) { + if (state->deadlines[state->current_index] + 24 * 60 * 60 > now_ts) + sprintf(buf, "DL%2dOVER ", state->current_index + 1); + else + sprintf(buf, "DL%2d---- ", state->current_index + 1); + + //watch_clear_indicator(WATCH_INDICATOR_BELL); + watch_display_string(buf, 0); + return; + } + + /* Get date time structs */ + watch_date_time deadline = watch_utility_date_time_from_unix_time(state->deadlines[state->current_index], _get_tz_offset(settings) + ); + + /* Calculate naive difference of dates */ + unit[0] = deadline.unit.second - now.unit.second; + unit[1] = deadline.unit.minute - now.unit.minute; + unit[2] = deadline.unit.hour - now.unit.hour; + unit[3] = deadline.unit.day - now.unit.day; + unit[4] = deadline.unit.month - now.unit.month; + unit[5] = deadline.unit.year - now.unit.year; + + /* Correct errors of naive difference */ + for (i = 0; i < 6; i++) { + if (unit[i] < 0) { + /* Correct remaining units */ + if (i == 3) + unit[i] += _days_in_month(deadline.unit.month - 1, deadline.unit.year); + else + unit[i] += range[i]; + + /* Carry over change to next unit if non-zero */ + if (i < 5 && unit[i + 1] != 0) + unit[i + 1] -= 1; + } + } + + /* Set range */ + i = state->current_index + 1; + if (unit[5] > 0) { + /* years:months */ + sprintf(buf, "DL%2d%02d%02dYR", i, unit[5] % 100, unit[4] % 12); + } else if (unit[4] > 0) { + /* months:days */ + sprintf(buf, "DL%2d%02d%02dMO", i, (unit[5] * 12 + unit[4]) % 100, unit[3] % 32); + } else if (unit[3] > 0) { + /* days:hours */ + sprintf(buf, "DL%2d%02d%02ddY", i, unit[3] % 32, unit[2] % 24); + } else { + /* hours:minutes:seconds */ + sprintf(buf, "DL%2d%02d%02d%02d", i, unit[2] % 24, unit[1] % 60, unit[0] % 60); + } + watch_display_string(buf, 0); +} + +/* Init running mode */ +static void _running_init(movement_settings_t *settings, deadline_state_t *state) +{ + (void) settings; + (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 _running_loop(movement_event_t event, movement_settings_t *settings, void *context) +{ + deadline_state_t *state = (deadline_state_t *) context; + + if (event.event_type != EVENT_BACKGROUND_TASK) + _running_display(event, settings, state); + + switch (event.event_type) { + case EVENT_ALARM_BUTTON_UP: + _beep_button(settings); + state->current_index = (state->current_index + 1) % DEADLINE_FACE_DATES; + _running_display(event, settings, state); + break; + case EVENT_ALARM_LONG_PRESS: + _beep_enable(settings); + _setting_init(settings, state); + state->mode = DEADLINE_FACE_SETTING; + 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_button(settings); + state->alarm_enabled = !state->alarm_enabled; + if (state->alarm_enabled) { + _background_alarm_schedule(settings, context); + } else { + _background_alarm_cancel(settings, context); + } + _running_display(event, settings, state); + break; + case EVENT_TIMEOUT: + movement_move_to_face(0); + break; + case EVENT_BACKGROUND_TASK: + _background_alarm_play(settings, state); + break; + case EVENT_LOW_ENERGY_UPDATE: + break; + default: + return movement_default_loop_handler(event, settings); + } + + return true; +} + +/* Update display in settings mode */ +static void _setting_display(movement_event_t event, movement_settings_t *settings, deadline_state_t *state, watch_date_time date_time) +{ + char buf[11]; + + int i = state->current_index + 1; + if (state->current_page > 2) { + watch_set_colon(); + if (settings->bit.clock_mode_24h) { + watch_set_indicator(WATCH_INDICATOR_24H); + sprintf(buf, "%s%2d%2d%02d ", settings_titles[state->current_page], i, date_time.unit.hour, date_time.unit.minute); + } else { + sprintf(buf, "%s%2d%2d%02d ", settings_titles[state->current_page], i, (date_time.unit.hour % 12) ? (date_time.unit.hour % 12) : 12, + date_time.unit.minute); + if (date_time.unit.hour < 12) + watch_clear_indicator(WATCH_INDICATOR_PM); + else + watch_set_indicator(WATCH_INDICATOR_PM); + } + } else { + watch_clear_colon(); + watch_clear_indicator(WATCH_INDICATOR_24H); + watch_clear_indicator(WATCH_INDICATOR_PM); + sprintf(buf, "%s%2d%2d%02d%02d", settings_titles[state->current_page], i, 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[4] = buf[5] = ' '; + break; + case 1: + case 4: + buf[6] = buf[7] = ' '; + break; + case 2: + buf[8] = buf[9] = ' '; + break; + } + } + + watch_display_string(buf, 0); +} + +/* Init setting mode */ +static void _setting_init(movement_settings_t *settings, deadline_state_t *state) +{ + state->current_page = 0; + + /* Init fresh deadline to next day */ + if (state->deadlines[state->current_index] == 0) { + _reset_deadline(settings, state); + } + + /* Ensure 1Hz updates only */ + _change_tick_freq(1, state); +} + +/* Loop of setting mode */ +static bool _setting_loop(movement_event_t event, movement_settings_t *settings, void *context) +{ + deadline_state_t *state = (deadline_state_t *) context; + watch_date_time date_time; + date_time = watch_utility_date_time_from_unix_time(state->deadlines[state->current_index], _get_tz_offset(settings)); + + if (event.event_type != EVENT_BACKGROUND_TASK) + _setting_display(event, settings, state, date_time); + + switch (event.event_type) { + case EVENT_TICK: + if (state->tick_freq == 8) { + if (watch_get_pin_level(BTN_ALARM)) { + _increment_date(settings, state, date_time); + _setting_display(event, settings, 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_button(settings); + _reset_deadline(settings, state); + break; + case EVENT_LIGHT_BUTTON_DOWN: + break; + case EVENT_LIGHT_BUTTON_UP: + state->current_page = (state->current_page + 1) % SETTINGS_NUM; + _setting_display(event, settings, state, date_time); + break; + case EVENT_ALARM_BUTTON_UP: + _change_tick_freq(4, state); + _increment_date(settings, state, date_time); + _setting_display(event, settings, state, date_time); + break; + case EVENT_TIMEOUT: + _beep_button(settings); + _background_alarm_schedule(settings, context); + _change_tick_freq(1, state); + state->mode = DEADLINE_FACE_RUNNING; + movement_move_to_face(0); + break; + case EVENT_MODE_BUTTON_UP: + _beep_disable(settings); + _background_alarm_schedule(settings, context); + _running_init(settings, state); + _running_display(event, settings, state); + state->mode = DEADLINE_FACE_RUNNING; + break; + case EVENT_BACKGROUND_TASK: + _background_alarm_play(settings, state); + break; + default: + return movement_default_loop_handler(event, settings); + } + + return true; +} + +/* Setup face */ +void deadline_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void **context_ptr) +{ + (void) settings; + (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(movement_settings_t *settings, void *context) +{ + (void) settings; + deadline_state_t *state = (deadline_state_t *) context; + + /* Set display options */ + _running_init(settings, state); + state->mode = DEADLINE_FACE_RUNNING; + state->current_index = _closest_deadline(settings, state); +} + +/* Loop face */ +bool deadline_face_loop(movement_event_t event, movement_settings_t *settings, void *context) +{ + (void) settings; + deadline_state_t *state = (deadline_state_t *) context; + switch (state->mode) { + case DEADLINE_FACE_SETTING: + _setting_loop(event, settings, context); + break; + default: + case DEADLINE_FACE_RUNNING: + _running_loop(event, settings, context); + break; + } + + return true; +} + +/* Resign face */ +void deadline_face_resign(movement_settings_t *settings, void *context) +{ + (void) settings; + (void) context; +} + +/* Want background task */ +bool deadline_face_wants_background_task(movement_settings_t *settings, void *context) +{ + deadline_state_t *state = (deadline_state_t *) context; + + if (!state->alarm_enabled) + return false; + + /* Determine closest deadline */ + watch_date_time now = watch_rtc_get_date_time(); + uint32_t now_ts = watch_utility_date_time_to_unix_time(now, _get_tz_offset(settings)); + uint32_t next_ts = state->deadlines[_closest_deadline(settings, state)]; + + /* No active deadline */ + if (next_ts < now_ts) + return false; + + /* No deadline within next 60 seconds */ + if (next_ts >= now_ts + 60) + return false; + + /* Deadline within next minute. Let's set up an alarm */ + watch_date_time next = watch_utility_date_time_from_unix_time(next_ts, _get_tz_offset(settings)); + movement_request_wake(); + movement_schedule_background_task_for_face(state->face_idx, next); + return false; +} diff --git a/movement/watch_faces/complication/deadline_face.h b/movement/watch_faces/complication/deadline_face.h new file mode 100644 index 0000000..4e4efe0 --- /dev/null +++ b/movement/watch_faces/complication/deadline_face.h @@ -0,0 +1,65 @@ +/* + * MIT License + * + * Copyright (c) 2023-2024 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_FACE_RUNNING = 0, + DEADLINE_FACE_SETTING +} 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(movement_settings_t *settings, uint8_t watch_face_index, void **context_ptr); +void deadline_face_activate(movement_settings_t *settings, void *context); +bool deadline_face_loop(movement_event_t event, movement_settings_t *settings, void *context); +void deadline_face_resign(movement_settings_t *settings, void *context); +bool deadline_face_wants_background_task(movement_settings_t *settings, void *context); + +#define deadline_face ((const watch_face_t){ \ + deadline_face_setup, \ + deadline_face_activate, \ + deadline_face_loop, \ + deadline_face_resign, \ + deadline_face_wants_background_task \ +}) + +#endif // DEADLINE_FACE_H_ diff --git a/movement/watch_faces/complication/endless_runner_face.c b/movement/watch_faces/complication/endless_runner_face.c new file mode 100644 index 0000000..3509fc2 --- /dev/null +++ b/movement/watch_faces/complication/endless_runner_face.c @@ -0,0 +1,617 @@ +/* + * MIT License + * + * Copyright (c) 2024 + * + * 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 +#include +#include "endless_runner_face.h" + +typedef enum { + JUMPING_FINAL_FRAME = 0, + NOT_JUMPING, + JUMPING_START, +} RunnerJumpState; + +typedef enum { + SCREEN_TITLE = 0, + 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; + +static game_state_t game_state; +static const uint8_t _num_bits_obst_pattern = sizeof(game_state.obst_pattern) * 8; + +static void print_binary(uint32_t value, int bits) { +#if __EMSCRIPTEN__ + for (int i = bits - 1; i >= 0; i--) { + // Print each bit + printf("%lu", (value >> i) & 1); + // 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(0, 21); + watch_set_pixel(1, 21); + watch_set_pixel(0, 20); + watch_set_pixel(1, 20); + watch_clear_pixel(1, 17); + watch_clear_pixel(2, 20); + watch_clear_pixel(2, 21); + } + else { + watch_clear_pixel(0, 21); + watch_clear_pixel(1, 21); + watch_clear_pixel(0, 20); + watch_set_pixel(1, 20); + watch_set_pixel(1, 17); + watch_set_pixel(2, 20); + watch_set_pixel(2, 21); + } +} + +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_string(buf, 0); + } + else { + score %= (MAX_DISP_SCORE + 1); + sprintf(buf, "%2d", score); + watch_display_string(buf, 2); + } +} + +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_string(" ", 2); // Blink the 0 fuel to show it cannot be refilled. + return; + } + sprintf(buf, "%2d", game_state.fuel); + watch_display_string(buf, 2); +} + +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 date_time = watch_rtc_get_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) { + switch (difficulty) + { + case DIFF_BABY: + watch_display_string(" b", 2); + break; + case DIFF_EASY: + watch_display_string(" E", 2); + break; + case DIFF_HARD: + watch_display_string(" H", 2); + break; + case DIFF_FUEL: + watch_display_string(" F", 2); + break; + case DIFF_FUEL_1: + watch_display_string("1F", 2); + break; + case DIFF_NORM: + default: + watch_display_string(" N", 2); + break; + } + 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 toggle_sound(endless_runner_state_t *state) { + state -> soundOn = !state -> soundOn; + if (state -> soundOn){ + watch_buzzer_play_note(BUZZER_NOTE_C5, 30); + watch_set_indicator(WATCH_INDICATOR_BELL); + } + else { + watch_clear_indicator(WATCH_INDICATOR_BELL); + } +} + +static void display_title(endless_runner_state_t *state) { + uint16_t hi_score = state -> hi_score; + uint8_t difficulty = state -> difficulty; + bool sound_on = state -> soundOn; + game_state.curr_screen = SCREEN_TITLE; + memset(&game_state, 0, sizeof(game_state)); + game_state.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(); + if (hi_score > MAX_HI_SCORE) { + watch_display_string("ER HS --", 0); + } + else { + char buf[14]; + sprintf(buf, "ER HS%4d", hi_score); + watch_display_string(buf, 0); + } + display_difficulty(difficulty); +} + +static void display_time(watch_date_time date_time, bool clock_mode_24h) { + static watch_date_time previous_date_time; + char buf[6 + 1]; + + // If the hour needs updating or it's the first time displaying the time + if ((game_state.curr_screen != SCREEN_TIME) || (date_time.unit.hour != previous_date_time.unit.hour)) { + uint8_t hour = date_time.unit.hour; + game_state.curr_screen = SCREEN_TIME; + + if (clock_mode_24h) watch_set_indicator(WATCH_INDICATOR_24H); + else { + if (hour >= 12) watch_set_indicator(WATCH_INDICATOR_PM); + hour %= 12; + if (hour == 0) hour = 12; + } + watch_set_colon(); + sprintf( buf, "%2d%02d ", hour, date_time.unit.minute); + watch_display_string(buf, 4); + } + // If both digits of the minute need updating + else if ((date_time.unit.minute / 10) != (previous_date_time.unit.minute / 10)) { + sprintf( buf, "%02d ", date_time.unit.minute); + watch_display_string(buf, 6); + } + // If only the ones-place of the minute needs updating. + else if (date_time.unit.minute != previous_date_time.unit.minute) { + sprintf( buf, "%d ", date_time.unit.minute % 10); + watch_display_string(buf, 7); + } + 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(); + movement_request_tick_frequency((state -> difficulty == DIFF_BABY) ? FREQ_SLOW : FREQ); + if (game_state.fuel_mode) { + watch_display_string(" ", 0); + game_state.obst_pattern = get_random_fuel(0); + if ((16 * JUMP_FRAMES_FUEL_RECHARGE) < JUMP_FRAMES_FUEL) // 16 frames of zeros at the start of a level + game_state.fuel = JUMP_FRAMES_FUEL - (16 * JUMP_FRAMES_FUEL_RECHARGE); // Have it below its max to show it counting up when starting. + if (game_state.fuel < JUMP_FRAMES_FUEL_RECHARGE) game_state.fuel = JUMP_FRAMES_FUEL_RECHARGE; + } + else { + watch_display_string(" ", 2); + game_state.obst_pattern = get_random_legal(0, difficulty); + } + game_state.jump_state = NOT_JUMPING; + display_ball(game_state.jump_state != NOT_JUMPING); + display_score( game_state.curr_score); + if (state -> soundOn){ + watch_buzzer_play_note(BUZZER_NOTE_C5, 200); + watch_buzzer_play_note(BUZZER_NOTE_E5, 200); + watch_buzzer_play_note(BUZZER_NOTE_G5, 200); + } +} + +static void display_lose_screen(endless_runner_state_t *state) { + game_state.curr_screen = SCREEN_LOSE; + game_state.curr_score = 0; + watch_display_string(" LOSE ", 0); + if (state -> soundOn) + watch_buzzer_play_note(BUZZER_NOTE_A1, 600); + else + 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(0, 20); + else if (game_state.jump_state != NOT_JUMPING) { + watch_clear_pixel(0, 20); + 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(1, 21); + else if (game_state.jump_state != NOT_JUMPING) + watch_clear_pixel(1, 21); + break; + + case 1: + if (!game_state.fuel_mode && obstacle) // If an obstacle is here, it means the ball cleared it + add_to_score(state); + //fall through + case 0: + case 5: + if (obstacle) + watch_set_pixel(0, 18 + grid_loc); + else + watch_clear_pixel(0, 18 + grid_loc); + break; + case 4: + if (obstacle) + watch_set_pixel(1, 22); + else + watch_clear_pixel(1, 22); + break; + case 6: + if (obstacle) + watch_set_pixel(1, 0); + else + watch_clear_pixel(1, 0); + break; + case 7: + case 8: + if (obstacle) + watch_set_pixel(0, grid_loc - 6); + else + watch_clear_pixel(0, grid_loc - 6); + break; + case 9: + case 10: + if (obstacle) + watch_set_pixel(0, grid_loc - 5); + else + watch_clear_pixel(0, grid_loc - 5); + break; + case 11: + if (obstacle) + watch_set_pixel(1, 6); + else + watch_clear_pixel(1, 6); + break; + default: + 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 (!watch_get_pin_level(BTN_ALARM) && !watch_get_pin_level(BTN_LIGHT)) 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(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) { + (void) settings; + (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; + } +} + +void endless_runner_face_activate(movement_settings_t *settings, void *context) { + (void) settings; + (void) context; +} + +bool endless_runner_face_loop(movement_event_t event, movement_settings_t *settings, void *context) { + endless_runner_state_t *state = (endless_runner_state_t *)context; + switch (event.event_type) { + case EVENT_ACTIVATE: + check_and_reset_hi_score(state); + if (state -> soundOn) watch_set_indicator(WATCH_INDICATOR_BELL); + display_title(state); + break; + case EVENT_TICK: + switch (game_state.curr_screen) + { + case SCREEN_TITLE: + case SCREEN_LOSE: + break; + default: + update_game(state, event.subsecond); + break; + } + break; + case EVENT_LIGHT_BUTTON_UP: + case EVENT_ALARM_BUTTON_UP: + if (game_state.curr_screen == SCREEN_TITLE) + begin_playing(state); + else if (game_state.curr_screen == SCREEN_LOSE) + display_title(state); + break; + case EVENT_LIGHT_LONG_PRESS: + if (game_state.curr_screen == SCREEN_TITLE) + change_difficulty(state); + break; + 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_PLAYING) + toggle_sound(state); + break; + case EVENT_TIMEOUT: + if (game_state.curr_screen != SCREEN_TITLE) + display_title(state); + break; + case EVENT_LOW_ENERGY_UPDATE: + display_time(watch_rtc_get_date_time(), settings->bit.clock_mode_24h); + break; + default: + return movement_default_loop_handler(event, settings); + } + return true; +} + +void endless_runner_face_resign(movement_settings_t *settings, void *context) { + (void) settings; + (void) context; +} + diff --git a/movement/watch_faces/complication/endless_runner_face.h b/movement/watch_faces/complication/endless_runner_face.h new file mode 100644 index 0000000..8c8fa21 --- /dev/null +++ b/movement/watch_faces/complication/endless_runner_face.h @@ -0,0 +1,62 @@ +/* + * MIT License + * + * Copyright (c) 2024 + * + * 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. + 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; + /* 24 bits, likely aligned to 32 bits = 4 bytes */ +} endless_runner_state_t; + +void endless_runner_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); +void endless_runner_face_activate(movement_settings_t *settings, void *context); +bool endless_runner_face_loop(movement_event_t event, movement_settings_t *settings, void *context); +void endless_runner_face_resign(movement_settings_t *settings, 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_ + diff --git a/movement/watch_faces/complication/higher_lower_game_face.c b/movement/watch_faces/complication/higher_lower_game_face.c new file mode 100755 index 0000000..f9dbcd0 --- /dev/null +++ b/movement/watch_faces/complication/higher_lower_game_face.c @@ -0,0 +1,396 @@ +/* + * 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 +#endif + +#include +#include +#include "higher_lower_game_face.h" +#include "watch_private_display.h" + +#define TITLE_TEXT "Hi-Lo" +#define GAME_BOARD_SIZE 6 +#define MAX_BOARDS 40 +#define GUESSES_PER_SCREEN 5 +#define WIN_SCORE (MAX_BOARDS * GUESSES_PER_SCREEN) +#define STATUS_DISPLAY_START 0 +#define BOARD_SCORE_DISPLAY_START 2 +#define BOARD_DISPLAY_START 4 +#define BOARD_DISPLAY_END 9 +#define MIN_CARD_VALUE 2 +#define MAX_CARD_VALUE 14 +#define 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; + stack_deck(); + 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_string(TITLE_TEXT, BOARD_DISPLAY_START); + watch_display_string("GA", STATUS_DISPLAY_START); + 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) { + const uint64_t position_segment_data = (Segment_Map[position] >> (8 * (uint8_t) segment)) & 0xFF; + const uint8_t com_pin = position_segment_data >> 6; + const uint8_t seg = position_segment_data & 0x3F; + watch_set_pixel(com_pin, seg); +} + +static void render_board_position(size_t board_position) { + const size_t display_position = FLIP_BOARD_DIRECTION + ? BOARD_DISPLAY_START + board_position + : BOARD_DISPLAY_END - board_position; + const 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 14: // A (≡) + watch_display_character(' ', display_position); + set_segment_at_position(A, display_position); + set_segment_at_position(D, display_position); + set_segment_at_position(G, display_position); + break; + case 13: // K (=) + watch_display_character(' ', display_position); + set_segment_at_position(A, display_position); + set_segment_at_position(D, display_position); + break; + case 12: // Q (-) + 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_string(buf, BOARD_SCORE_DISPLAY_START); +} + +static void render_final_score(void) { + watch_display_string("SC", STATUS_DISPLAY_START); + char buf[7] = {0}; + const uint8_t complete_boards = score / GUESSES_PER_SCREEN; + snprintf(buf, sizeof(buf), "%2hu %03hu", complete_boards, score); + watch_set_colon(); + watch_display_string(buf, BOARD_DISPLAY_START); +} + +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_string("==", STATUS_DISPLAY_START); + break; + case HL_GUESS_HIGHER: + watch_display_string("HI", STATUS_DISPLAY_START); + break; + case HL_GUESS_LOWER: + watch_display_string("LO", STATUS_DISPLAY_START); + 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_string("GO", STATUS_DISPLAY_START); + game_board[guess_position].revealed = true; + render_board_position(guess_position); + game_state = HL_GS_LOSE; + return; + } + + if (score >= WIN_SCORE) { + // Win, perhaps some kind of animation sequence? + watch_display_string("WI", STATUS_DISPLAY_START); + watch_display_string(" ", BOARD_SCORE_DISPLAY_START); + watch_display_string("------", BOARD_DISPLAY_START); + 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_string(TITLE_TEXT, BOARD_DISPLAY_START); + watch_display_string("GA", STATUS_DISPLAY_START); + game_state = HL_GS_TITLE_SCREEN; + break; + default: + watch_display_string("ERROR", BOARD_DISPLAY_START); + 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(movement_settings_t *settings, uint8_t watch_face_index, void **context_ptr) { + (void) settings; + (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(movement_settings_t *settings, void *context) { + (void) settings; + 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; +} + +bool higher_lower_game_face_loop(movement_event_t event, movement_settings_t *settings, 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_string(TITLE_TEXT, BOARD_DISPLAY_START); + watch_display_string("GA", STATUS_DISPLAY_START); + 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, settings); + } + + // 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(movement_settings_t *settings, void *context) { + (void) settings; + (void) context; + + // handle any cleanup before your watch face goes off-screen. +} diff --git a/movement/watch_faces/complication/higher_lower_game_face.h b/movement/watch_faces/complication/higher_lower_game_face.h new file mode 100755 index 0000000..13da586 --- /dev/null +++ b/movement/watch_faces/complication/higher_lower_game_face.h @@ -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(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); +void higher_lower_game_face_activate(movement_settings_t *settings, void *context); +bool higher_lower_game_face_loop(movement_event_t event, movement_settings_t *settings, void *context); +void higher_lower_game_face_resign(movement_settings_t *settings, 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_ diff --git a/movement/watch_faces/complication/periodic_face.c b/movement/watch_faces/complication/periodic_face.c new file mode 100644 index 0000000..69ae4ad --- /dev/null +++ b/movement/watch_faces/complication/periodic_face.c @@ -0,0 +1,503 @@ +/* + * MIT License + * + * Copyright (c) 2023 PrimmR + * 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 +#include +#include "periodic_face.h" + +#define FREQ_FAST 8 +#define FREQ 2 + +static bool _quick_ticks_running; +static uint8_t _ts_ticks = 0; +static int16_t _text_pos; +static const char* _text_looping; +static const char title_text[] = "Periodic Table"; + +void periodic_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void **context_ptr) +{ + (void)settings; + (void)watch_face_index; + if (*context_ptr == NULL) + { + *context_ptr = malloc(sizeof(periodic_state_t)); + memset(*context_ptr, 0, sizeof(periodic_state_t)); + } +} + +void periodic_face_activate(movement_settings_t *settings, void *context) +{ + (void)settings; + periodic_state_t *state = (periodic_state_t *)context; + + state->atomic_num = 0; + state->mode = 0; + state->selection_index = 0; + _quick_ticks_running = false; + movement_request_tick_frequency(FREQ); +} + +typedef struct +{ + char symbol[3]; + char name[14]; // Longest is Rutherfordium + int16_t year_discovered; // Negative is BC + uint16_t atomic_mass; // In units of 0.01 AMU + uint16_t electronegativity; // In units of 0.01 + char group[3]; +} element; + +typedef enum { + SCREEN_TITLE = 0, + SCREEN_ELEMENT, + SCREEN_ATOMIC_MASS, + SCREEN_DISCOVER_YEAR, + SCREEN_ELECTRONEGATIVITY, + SCREEN_FULL_NAME, + SCREENS_COUNT +} PeriodicScreens; + +const char screen_name[SCREENS_COUNT][3] = { + [SCREEN_ATOMIC_MASS] = "am", + [SCREEN_DISCOVER_YEAR] = " y", + [SCREEN_ELECTRONEGATIVITY] = "EL", + [SCREEN_FULL_NAME] = " n", +}; + +// Comments on the table denote symbols that cannot be displayed +#define MAX_ELEMENT 118 +const element table[MAX_ELEMENT] = { + { .symbol = "H", .name = "Hydrogen", .year_discovered = 1671, .atomic_mass = 101, .electronegativity = 220, .group = " " }, + { .symbol = "HE", .name = "Helium", .year_discovered = 1868, .atomic_mass = 400, .electronegativity = 0, .group = "0" }, + { .symbol = "LI", .name = "Lithium", .year_discovered = 1817, .atomic_mass = 694, .electronegativity = 98, .group = "1" }, + { .symbol = "BE", .name = "Beryllium", .year_discovered = 1798, .atomic_mass = 901, .electronegativity = 157, .group = "2" }, + { .symbol = "B", .name = "Boron", .year_discovered = 1787, .atomic_mass = 1081, .electronegativity = 204, .group = "3" }, + { .symbol = "C", .name = "Carbon", .year_discovered = -26000, .atomic_mass = 1201, .electronegativity = 255, .group = "4" }, + { .symbol = "N", .name = "Nitrogen", .year_discovered = 1772, .atomic_mass = 1401, .electronegativity = 304, .group = "5" }, + { .symbol = "O", .name = "Oxygen", .year_discovered = 1771, .atomic_mass = 1600, .electronegativity = 344, .group = "6" }, + { .symbol = "F", .name = "Fluorine", .year_discovered = 1771, .atomic_mass = 1900, .electronegativity = 398, .group = "7" }, + { .symbol = "NE", .name = "Neon", .year_discovered = 1898, .atomic_mass = 2018, .electronegativity = 0, .group = "0" }, + { .symbol = "NA", .name = "Sodium", .year_discovered = 1702, .atomic_mass = 2299, .electronegativity = 93, .group = "1" }, + { .symbol = "MG", .name = "Magnesium", .year_discovered = 1755, .atomic_mass = 2431, .electronegativity = 131, .group = "2" }, + { .symbol = "AL", .name = "Aluminium", .year_discovered = 1746, .atomic_mass = 2698, .electronegativity = 161, .group = "3" }, + { .symbol = "SI", .name = "Silicon", .year_discovered = 1739, .atomic_mass = 2809, .electronegativity = 190, .group = "4" }, + { .symbol = "P", .name = "Phosphorus", .year_discovered = 1669, .atomic_mass = 3097, .electronegativity = 219, .group = "5" }, + { .symbol = "S", .name = "Sulfur", .year_discovered = -2000, .atomic_mass = 3206, .electronegativity = 258, .group = "6" }, + { .symbol = "CL", .name = "Chlorine", .year_discovered = 1774, .atomic_mass = 3545., .electronegativity = 316, .group = "7" }, + { .symbol = "AR", .name = "Argon", .year_discovered = 1894, .atomic_mass = 3995., .electronegativity = 0, .group = "0" }, + { .symbol = "K", .name = "Potassium", .year_discovered = 1702, .atomic_mass = 3910, .electronegativity = 82, .group = "1" }, + { .symbol = "CA", .name = "Calcium", .year_discovered = 1739, .atomic_mass = 4008, .electronegativity = 100, .group = "2" }, + { .symbol = "SC", .name = "Scandium", .year_discovered = 1879, .atomic_mass = 4496, .electronegativity = 136, .group = " T" }, + { .symbol = "TI", .name = "Titanium", .year_discovered = 1791, .atomic_mass = 4787, .electronegativity = 154, .group = " T" }, + { .symbol = "W", .name = "Vanadium", .year_discovered = 1801, .atomic_mass = 5094, .electronegativity = 163, .group = " T" }, + { .symbol = "CR", .name = "Chromium", .year_discovered = 1797, .atomic_mass = 5200, .electronegativity = 166, .group = " T" }, + { .symbol = "MN", .name = "Manganese", .year_discovered = 1774, .atomic_mass = 5494, .electronegativity = 155, .group = " T" }, + { .symbol = "FE", .name = "Iron", .year_discovered = -5000, .atomic_mass = 5585, .electronegativity = 183, .group = " T" }, + { .symbol = "CO", .name = "Cobalt", .year_discovered = 1735, .atomic_mass = 5893, .electronegativity = 188, .group = " T" }, + { .symbol = "NI", .name = "Nickel", .year_discovered = 1751, .atomic_mass = 5869, .electronegativity = 191, .group = " T" }, + { .symbol = "CU", .name = "Copper", .year_discovered = -9000, .atomic_mass = 6355, .electronegativity = 190, .group = " T" }, + { .symbol = "ZN", .name = "Zinc", .year_discovered = -1000, .atomic_mass = 6538, .electronegativity = 165, .group = " T" }, + { .symbol = "GA", .name = "Gallium", .year_discovered = 1875, .atomic_mass = 6972, .electronegativity = 181, .group = "3" }, + { .symbol = "GE", .name = "Germanium", .year_discovered = 1886, .atomic_mass = 7263, .electronegativity = 201, .group = "4" }, + { .symbol = "AS", .name = "Arsenic", .year_discovered = 300, .atomic_mass = 7492, .electronegativity = 218, .group = "5" }, + { .symbol = "SE", .name = "Selenium", .year_discovered = 1817, .atomic_mass = 7897, .electronegativity = 255, .group = "6" }, + { .symbol = "BR", .name = "Bromine", .year_discovered = 1825, .atomic_mass = 7990., .electronegativity = 296, .group = "7" }, + { .symbol = "KR", .name = "Krypton", .year_discovered = 1898, .atomic_mass = 8380, .electronegativity = 300, .group = "0" }, + { .symbol = "RB", .name = "Rubidium", .year_discovered = 1861, .atomic_mass = 8547, .electronegativity = 82, .group = "1" }, + { .symbol = "SR", .name = "Strontium", .year_discovered = 1787, .atomic_mass = 8762, .electronegativity = 95, .group = "2" }, + { .symbol = "Y", .name = "Yttrium", .year_discovered = 1794, .atomic_mass = 8891, .electronegativity = 122, .group = " T" }, + { .symbol = "ZR", .name = "Zirconium", .year_discovered = 1789, .atomic_mass = 9122, .electronegativity = 133, .group = " T" }, + { .symbol = "NB", .name = "Niobium", .year_discovered = 1801, .atomic_mass = 9291, .electronegativity = 160, .group = " T" }, + { .symbol = "MO", .name = "Molybdenum", .year_discovered = 1778, .atomic_mass = 9595, .electronegativity = 216, .group = " T" }, + { .symbol = "TC", .name = "Technetium", .year_discovered = 1937, .atomic_mass = 9700, .electronegativity = 190, .group = " T" }, + { .symbol = "RU", .name = "Ruthenium", .year_discovered = 1844, .atomic_mass = 10107, .electronegativity = 220, .group = " T" }, + { .symbol = "RH", .name = "Rhodium", .year_discovered = 1804, .atomic_mass = 10291, .electronegativity = 228, .group = " T" }, + { .symbol = "PD", .name = "Palladium", .year_discovered = 1802, .atomic_mass = 10642, .electronegativity = 220, .group = " T" }, + { .symbol = "AG", .name = "Silver", .year_discovered = -5000, .atomic_mass = 10787, .electronegativity = 193, .group = " T" }, + { .symbol = "CD", .name = "Cadmium", .year_discovered = 1817, .atomic_mass = 11241, .electronegativity = 169, .group = " T" }, + { .symbol = "IN", .name = "Indium", .year_discovered = 1863, .atomic_mass = 11482, .electronegativity = 178, .group = "3" }, + { .symbol = "SN", .name = "Tin", .year_discovered = -3500, .atomic_mass = 11871, .electronegativity = 196, .group = "4" }, + { .symbol = "SB", .name = "Antimony", .year_discovered = -3000, .atomic_mass = 12176, .electronegativity = 205, .group = "5" }, + { .symbol = "TE", .name = "Tellurium", .year_discovered = 1782, .atomic_mass = 12760, .electronegativity = 210, .group = "6" }, + { .symbol = "I", .name = "Iodine", .year_discovered = 1811, .atomic_mass = 12690, .electronegativity = 266, .group = "7" }, + { .symbol = "XE", .name = "Xenon", .year_discovered = 1898, .atomic_mass = 13129, .electronegativity = 260, .group = "0" }, + { .symbol = "CS", .name = "Caesium", .year_discovered = 1860, .atomic_mass = 13291, .electronegativity = 79, .group = "1" }, + { .symbol = "BA", .name = "Barium", .year_discovered = 1772, .atomic_mass = 13733., .electronegativity = 89, .group = "2" }, + { .symbol = "LA", .name = "Lanthanum", .year_discovered = 1838, .atomic_mass = 13891, .electronegativity = 110, .group = "1a" }, + { .symbol = "CE", .name = "Cerium", .year_discovered = 1803, .atomic_mass = 14012, .electronegativity = 112, .group = "1a" }, + { .symbol = "PR", .name = "Praseodymium", .year_discovered = 1885, .atomic_mass = 14091, .electronegativity = 113, .group = "1a" }, + { .symbol = "ND", .name = "Neodymium", .year_discovered = 1841, .atomic_mass = 14424, .electronegativity = 114, .group = "1a" }, + { .symbol = "PM", .name = "Promethium", .year_discovered = 1945, .atomic_mass = 14500, .electronegativity = 113, .group = "1a" }, + { .symbol = "SM", .name = "Samarium", .year_discovered = 1879, .atomic_mass = 15036., .electronegativity = 117, .group = "1a" }, + { .symbol = "EU", .name = "Europium", .year_discovered = 1896, .atomic_mass = 15196, .electronegativity = 120, .group = "1a" }, + { .symbol = "GD", .name = "Gadolinium", .year_discovered = 1880, .atomic_mass = 15725, .electronegativity = 120, .group = "1a" }, + { .symbol = "TB", .name = "Terbium", .year_discovered = 1843, .atomic_mass = 15893, .electronegativity = 120, .group = "1a" }, + { .symbol = "DY", .name = "Dysprosium", .year_discovered = 1886, .atomic_mass = 16250, .electronegativity = 122, .group = "1a" }, + { .symbol = "HO", .name = "Holmium", .year_discovered = 1878, .atomic_mass = 16493, .electronegativity = 123, .group = "1a" }, + { .symbol = "ER", .name = "Erbium", .year_discovered = 1843, .atomic_mass = 16726, .electronegativity = 124, .group = "1a" }, + { .symbol = "TM", .name = "Thulium", .year_discovered = 1879, .atomic_mass = 16893, .electronegativity = 125, .group = "1a" }, + { .symbol = "YB", .name = "Ytterbium", .year_discovered = 1878, .atomic_mass = 17305, .electronegativity = 110, .group = "1a" }, + { .symbol = "LU", .name = "Lutetium", .year_discovered = 1906, .atomic_mass = 17497, .electronegativity = 127, .group = "1a" }, + { .symbol = "HF", .name = "Hafnium", .year_discovered = 1922, .atomic_mass = 17849, .electronegativity = 130, .group = " T" }, + { .symbol = "TA", .name = "Tantalum", .year_discovered = 1802, .atomic_mass = 18095, .electronegativity = 150, .group = " T" }, + { .symbol = "W", .name = "Tungsten", .year_discovered = 1781, .atomic_mass = 18384, .electronegativity = 236, .group = " T" }, + { .symbol = "RE", .name = "Rhenium", .year_discovered = 1908, .atomic_mass = 18621, .electronegativity = 190, .group = " T" }, + { .symbol = "OS", .name = "Osmium", .year_discovered = 1803, .atomic_mass = 19023, .electronegativity = 220, .group = " T" }, + { .symbol = "IR", .name = "Iridium", .year_discovered = 1803, .atomic_mass = 19222, .electronegativity = 220, .group = " T" }, + { .symbol = "PT", .name = "Platinum", .year_discovered = -600, .atomic_mass = 19508, .electronegativity = 228, .group = " T" }, + { .symbol = "AU", .name = "Gold", .year_discovered = -6000, .atomic_mass = 19697, .electronegativity = 254, .group = " T" }, + { .symbol = "HG", .name = "Mercury", .year_discovered = -1500, .atomic_mass = 20059, .electronegativity = 200, .group = " T" }, + { .symbol = "TL", .name = "Thallium", .year_discovered = 1861, .atomic_mass = 20438, .electronegativity = 162, .group = "3" }, + { .symbol = "PB", .name = "Lead", .year_discovered = -7000, .atomic_mass = 20720, .electronegativity = 187, .group = "4" }, + { .symbol = "BI", .name = "Bismuth", .year_discovered = 1500, .atomic_mass = 20898, .electronegativity = 202, .group = "5" }, + { .symbol = "PO", .name = "Polonium", .year_discovered = 1898, .atomic_mass = 20900, .electronegativity = 200, .group = "6" }, + { .symbol = "AT", .name = "Astatine", .year_discovered = 1940, .atomic_mass = 21000, .electronegativity = 220, .group = "7" }, + { .symbol = "RN", .name = "Radon", .year_discovered = 1899, .atomic_mass = 22200, .electronegativity = 220, .group = "0" }, + { .symbol = "FR", .name = "Francium", .year_discovered = 1939, .atomic_mass = 22300, .electronegativity = 79, .group = "1" }, + { .symbol = "RA", .name = "Radium", .year_discovered = 1898, .atomic_mass = 22600, .electronegativity = 90, .group = "2" }, + { .symbol = "AC", .name = "Actinium", .year_discovered = 1902, .atomic_mass = 22700, .electronegativity = 110, .group = "Ac" }, + { .symbol = "TH", .name = "Thorium", .year_discovered = 1829, .atomic_mass = 23204, .electronegativity = 130, .group = "Ac" }, + { .symbol = "PA", .name = "Protactinium", .year_discovered = 1913, .atomic_mass = 23104, .electronegativity = 150, .group = "Ac" }, + { .symbol = "U", .name = "Uranium", .year_discovered = 1789, .atomic_mass = 23803, .electronegativity = 138, .group = "Ac" }, + { .symbol = "NP", .name = "Neptunium", .year_discovered = 1940, .atomic_mass = 23700, .electronegativity = 136, .group = "Ac" }, + { .symbol = "PU", .name = "Plutonium", .year_discovered = 1941, .atomic_mass = 24400, .electronegativity = 128, .group = "Ac" }, + { .symbol = "AM", .name = "Americium", .year_discovered = 1944, .atomic_mass = 24300, .electronegativity = 113, .group = "Ac" }, + { .symbol = "CM", .name = "Curium", .year_discovered = 1944, .atomic_mass = 24700, .electronegativity = 128, .group = "Ac" }, + { .symbol = "BK", .name = "Berkelium", .year_discovered = 1949, .atomic_mass = 24700, .electronegativity = 130, .group = "Ac" }, + { .symbol = "CF", .name = "Californium", .year_discovered = 1950, .atomic_mass = 25100, .electronegativity = 130, .group = "Ac" }, + { .symbol = "ES", .name = "Einsteinium", .year_discovered = 1952, .atomic_mass = 25200, .electronegativity = 130, .group = "Ac" }, + { .symbol = "FM", .name = "Fermium", .year_discovered = 1953, .atomic_mass = 25700, .electronegativity = 130, .group = "Ac" }, + { .symbol = "MD", .name = "Mendelevium", .year_discovered = 1955, .atomic_mass = 25800, .electronegativity = 130, .group = "Ac" }, + { .symbol = "NO", .name = "Nobelium", .year_discovered = 1965, .atomic_mass = 25900, .electronegativity = 130, .group = "Ac" }, + { .symbol = "LR", .name = "Lawrencium", .year_discovered = 1961, .atomic_mass = 26600, .electronegativity = 130, .group = "Ac" }, + { .symbol = "RF", .name = "Rutherfordium", .year_discovered = 1969, .atomic_mass = 26700, .electronegativity = 0, .group = " T" }, + { .symbol = "DB", .name = "Dubnium", .year_discovered = 1970, .atomic_mass = 26800, .electronegativity = 0, .group = " T" }, + { .symbol = "SG", .name = "Seaborgium", .year_discovered = 1974, .atomic_mass = 26700, .electronegativity = 0, .group = " T" }, + { .symbol = "BH", .name = "Bohrium", .year_discovered = 1981, .atomic_mass = 27000, .electronegativity = 0, .group = " T" }, + { .symbol = "HS", .name = "Hassium", .year_discovered = 1984, .atomic_mass = 27100, .electronegativity = 0, .group = " T" }, + { .symbol = "MT", .name = "Meitnerium", .year_discovered = 1982, .atomic_mass = 27800, .electronegativity = 0, .group = " T" }, + { .symbol = "DS", .name = "Darmstadtium", .year_discovered = 1994, .atomic_mass = 28100, .electronegativity = 0, .group = " T" }, + { .symbol = "RG", .name = "Roentgenium", .year_discovered = 1994, .atomic_mass = 28200, .electronegativity = 0, .group = " T" }, + { .symbol = "CN", .name = "Copernicium", .year_discovered = 1996, .atomic_mass = 28500, .electronegativity = 0, .group = " T" }, + { .symbol = "NH", .name = "Nihonium", .year_discovered = 2004, .atomic_mass = 28600, .electronegativity = 0, .group = "3" }, + { .symbol = "FL", .name = "Flerovium", .year_discovered = 1999, .atomic_mass = 28900, .electronegativity = 0, .group = "4" }, + { .symbol = "MC", .name = "Moscovium", .year_discovered = 2003, .atomic_mass = 29000, .electronegativity = 0, .group = "5" }, + { .symbol = "LW", .name = "Livermorium", .year_discovered = 2000, .atomic_mass = 29300, .electronegativity = 0, .group = "6" }, + { .symbol = "TS", .name = "Tennessine", .year_discovered = 2009, .atomic_mass = 29400, .electronegativity = 0, .group = "7" }, + { .symbol = "OG", .name = "Oganesson", .year_discovered = 2002, .atomic_mass = 29400, .electronegativity = 0, .group = "0" }, +}; + +static void _make_upper(char *string) { + size_t i = 0; + while(string[i] != 0) { + if (string[i] >= 'a' && string[i] <= 'z') + string[i]-=32; // 32 = 'a'-'A' + i++; + } +} + +static void _display_element(periodic_state_t *state) +{ + char buf[9]; + char ele[3]; + uint8_t atomic_num = state->atomic_num; + strcpy(ele, table[atomic_num - 1].symbol); + _make_upper(ele); + sprintf(buf, "%2s%3d %-2s", table[atomic_num - 1].group, atomic_num, ele); + watch_display_string(buf, 2); +} + +static void _display_atomic_mass(periodic_state_t *state) +{ + char buf[11]; + uint16_t mass = table[state->atomic_num - 1].atomic_mass; + uint16_t integer = mass / 100; + uint16_t decimal = mass % 100; + if (decimal == 0) + sprintf(buf, "%-2s%2s%4d", table[state->atomic_num - 1].symbol, screen_name[state->mode], integer); + else + sprintf(buf, "%-2s%2s%3d_%.2d", table[state->atomic_num - 1].symbol, screen_name[state->mode], integer, decimal); + watch_display_string(buf, 0); +} + +static void _display_year_discovered(periodic_state_t *state) +{ + char buf[11]; + char year_buf[7]; + int16_t year = table[state->atomic_num - 1].year_discovered; + if (abs(year) > 9999) + sprintf(year_buf, "---- "); + else + sprintf(year_buf, "%4d ", abs(year)); + if (year < 0) { + year_buf[4] = 'b'; + year_buf[5] = 'c'; + } + sprintf(buf, "%-2s%-2s%s", table[state->atomic_num - 1].symbol, screen_name[state->mode], year_buf); + watch_display_string(buf, 0); +} + +static void _display_name(periodic_state_t *state) +{ + char buf[11]; + _text_looping = table[state->atomic_num - 1].name; + _text_pos = 0; + sprintf(buf, "%-2s%-2s%s", table[state->atomic_num - 1].symbol, screen_name[state->mode], table[state->atomic_num - 1].name); + watch_display_string(buf, 0); +} + +static void _display_electronegativity(periodic_state_t *state) +{ + char buf[11]; + uint16_t electronegativity = table[state->atomic_num - 1].electronegativity; + uint16_t integer = electronegativity / 100; + uint16_t decimal = electronegativity % 100; + if (decimal == 0) + sprintf(buf, "%-2s%2s%4d", table[state->atomic_num - 1].symbol, screen_name[state->mode], integer); + else + sprintf(buf, "%-2s%2s%3d_%.2d", table[state->atomic_num - 1].symbol, screen_name[state->mode], integer, decimal); + watch_display_string(buf, 0); +} + +static void start_quick_cyc(void){ + _quick_ticks_running = true; + movement_request_tick_frequency(FREQ_FAST); +} + +static void stop_quick_cyc(void){ + _quick_ticks_running = false; + movement_request_tick_frequency(FREQ); +} + +static int16_t _loop_text(const char* text, int8_t curr_loc, uint8_t char_len){ + // if curr_loc, then use that many ticks as a delay before looping + char buf[15]; + uint8_t next_pos; + uint8_t text_len = strlen(text); + uint8_t pos = 10 - char_len; + if (curr_loc == -1) curr_loc = 0; // To avoid double-showing the 0 + if (char_len >= text_len || curr_loc < 0) { + sprintf(buf, "%s", text); + watch_display_string(buf, pos); + if (curr_loc < 0) return ++curr_loc; + return 0; + } + else if (curr_loc == (text_len + 1)) + curr_loc = 0; + next_pos = curr_loc + 1; + sprintf(buf, "%.6s %.6s", text + curr_loc, text); + watch_display_string(buf, pos); + return next_pos; +} + +static void _display_title(periodic_state_t *state){ + state->atomic_num = 0; + watch_clear_colon(); + watch_clear_all_indicators(); + _text_looping = title_text; + _text_pos = FREQ * -1; + _text_pos = _loop_text(_text_looping, _text_pos, 5); +} + +static void _display_screen(periodic_state_t *state, bool should_sound){ + watch_clear_display(); + watch_clear_all_indicators(); + switch (state->mode) + { + case SCREEN_TITLE: + _display_title(state); + break; + case SCREEN_ELEMENT: + case SCREENS_COUNT: + _display_element(state); + break; + case SCREEN_ATOMIC_MASS: + _display_atomic_mass(state); + break; + case SCREEN_DISCOVER_YEAR: + _display_year_discovered(state); + break; + case SCREEN_ELECTRONEGATIVITY: + _display_electronegativity(state); + break; + case SCREEN_FULL_NAME: + _display_name(state); + break; + } + if (should_sound) watch_buzzer_play_note(BUZZER_NOTE_C7, 50); +} + +static void _handle_forward(periodic_state_t *state, bool should_sound){ + state->atomic_num = (state->atomic_num % MAX_ELEMENT) + 1; // Wraps back to 1 + state->mode = SCREEN_ELEMENT; + _display_screen(state, false); + if (should_sound) watch_buzzer_play_note(BUZZER_NOTE_C7, 50); +} + +static void _handle_backward(periodic_state_t *state, bool should_sound){ + if (state->atomic_num <= 1) state->atomic_num = MAX_ELEMENT; + else state->atomic_num = state->atomic_num - 1; + state->mode = SCREEN_ELEMENT; + _display_screen(state, false); + if (should_sound) watch_buzzer_play_note(BUZZER_NOTE_A6, 50); +} + +static void _handle_mode_still_pressed(periodic_state_t *state, bool should_sound) { + if (_ts_ticks != 0){ + if (!watch_get_pin_level(BTN_MODE)) { + _ts_ticks = 0; + return; + } + else if (--_ts_ticks == 0){ + switch (state->mode) + { + case SCREEN_TITLE: + movement_move_to_face(0); + return; + case SCREEN_ELEMENT: + state->mode = SCREEN_TITLE; + _display_screen(state, should_sound); + break; + default: + state->mode = SCREEN_ELEMENT; + _display_screen(state, should_sound); + break; + } + _ts_ticks = 2; + } + } +} + +bool periodic_face_loop(movement_event_t event, movement_settings_t *settings, void *context) +{ + periodic_state_t *state = (periodic_state_t *)context; + switch (event.event_type) + { + case EVENT_ACTIVATE: + state->mode = SCREEN_TITLE; + _display_screen(state, false); + break; + case EVENT_TICK: + if (state->mode == SCREEN_TITLE) _text_pos = _loop_text(_text_looping, _text_pos, 5); + else if (state->mode == SCREEN_FULL_NAME) _text_pos = _loop_text(_text_looping, _text_pos, 6); + if (_quick_ticks_running) { + if (watch_get_pin_level(BTN_LIGHT)) _handle_backward(state, false); + else if (watch_get_pin_level(BTN_ALARM)) _handle_forward(state, false); + else stop_quick_cyc(); + } + + _handle_mode_still_pressed(state, settings->bit.button_should_sound); + break; + case EVENT_LIGHT_BUTTON_UP: + if (state->mode <= SCREEN_ELEMENT) { + _handle_backward(state, settings->bit.button_should_sound); + } + else { + state->mode = SCREEN_ELEMENT; + _display_screen(state, settings->bit.button_should_sound); + } + break; + case EVENT_LIGHT_BUTTON_DOWN: + break; + case EVENT_ALARM_BUTTON_UP: + if (state->mode <= SCREEN_ELEMENT) { + _handle_forward(state, settings->bit.button_should_sound); + } + else { + state->mode = SCREEN_ELEMENT; + _display_screen(state, settings->bit.button_should_sound); + } + break; + case EVENT_ALARM_LONG_PRESS: + if (state->mode <= SCREEN_ELEMENT) { + start_quick_cyc(); + _handle_forward(state, settings->bit.button_should_sound); + } + break; + case EVENT_LIGHT_LONG_PRESS: + if (state->mode <= SCREEN_ELEMENT) { + start_quick_cyc(); + _handle_backward(state, settings->bit.button_should_sound); + } + else { + movement_illuminate_led(); + } + break; + case EVENT_MODE_BUTTON_UP: + if (state->mode == SCREEN_TITLE) movement_move_to_next_face(); + else { + state->mode = (state->mode + 1) % SCREENS_COUNT; + if (state->mode == SCREEN_TITLE) + state->mode = (state->mode + 1) % SCREENS_COUNT; + if (state->mode == SCREEN_ELEMENT){ + _display_screen(state, false); + if (settings->bit.button_should_sound) watch_buzzer_play_note(BUZZER_NOTE_A6, 50); + } + else + _display_screen(state, settings->bit.button_should_sound); + } + break; + case EVENT_MODE_LONG_PRESS: + switch (state->mode) + { + case SCREEN_TITLE: + movement_move_to_face(0); + return true; + case SCREEN_ELEMENT: + state->mode = SCREEN_TITLE; + _display_screen(state, settings->bit.button_should_sound); + break; + default: + state->mode = SCREEN_ELEMENT; + _display_screen(state, settings->bit.button_should_sound); + break; + } + _ts_ticks = 2; + break; + case EVENT_TIMEOUT: + // Display title after timeout + if (state->mode == SCREEN_TITLE) break; + state->mode = SCREEN_TITLE; + _display_screen(state, false); + break; + case EVENT_LOW_ENERGY_UPDATE: + // Display static title and tick animation during LE + watch_display_string("Pd Table", 0); + watch_start_tick_animation(500); + break; + default: + return movement_default_loop_handler(event, settings); + } + + return true; +} + +void periodic_face_resign(movement_settings_t *settings, void *context) +{ + (void)settings; + (void)context; + + // handle any cleanup before your watch face goes off-screen. +} diff --git a/movement/watch_faces/complication/periodic_face.h b/movement/watch_faces/complication/periodic_face.h new file mode 100644 index 0000000..730b0fe --- /dev/null +++ b/movement/watch_faces/complication/periodic_face.h @@ -0,0 +1,89 @@ +/* + * MIT License + * + * Copyright (c) 2023 PrimmR + * 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 PERIODIC_FACE_H_ +#define PERIODIC_FACE_H_ + +#include "movement.h" + +/* + * Periodic Table Face + * Allows for viewing data of the Periodic Table on your wrist. + * When looking at an element, it'll show you the atomic number on the center of the screen, + * symbol on the right, and it's group on the top-right. + * Pressing the mode button will cycle through the pages. + * Page 1: Atomic Mass + * Page 2: Year Discovered + * Page 3: Electronegativity + * Page 4: Full Name of the Element + * + * Controls: + * Mode Press + * On Title: Next Screen + * Else: Cycle through info of an element + * Mode Hold + * On Title: First Screen + * On Element Symbol Screen: Go to Title Screen + * Else: Go to Symbol Screen of current element + * If you are in a subscreen and just keep holding MODE, you will go through all of these menus without needing to depress. + * + * Light Press + * On Title or Element Symbol Screen: Previous Element + * Else: Display currenlt-selected element symbol page + * Light Hold + * On Title Screen or Element Symbol: Fast Cycle through Previous Elements + * Else: Activate LED backlight + * + * Alarm Press + * On Title or Element Symbol Screen: Next Element + * Else: Display currenlt-selected element symbol page + * Alarm Hold + * On Title Screen or Element Symbol: Fast Cycle through Next Elements + */ + +#define MODE_VIEW 0 +#define MODE_SELECT 1 + +typedef struct { + uint8_t atomic_num; + uint8_t mode; + uint8_t selection_index; +} periodic_state_t; + +void periodic_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); +void periodic_face_activate(movement_settings_t *settings, void *context); +bool periodic_face_loop(movement_event_t event, movement_settings_t *settings, void *context); +void periodic_face_resign(movement_settings_t *settings, void *context); + +#define periodic_face ((const watch_face_t){ \ + periodic_face_setup, \ + periodic_face_activate, \ + periodic_face_loop, \ + periodic_face_resign, \ + NULL, \ +}) + +#endif // PERIODIC_FACE_H_ + diff --git a/movement/watch_faces/complication/planetary_hours_face.c b/movement/watch_faces/complication/planetary_hours_face.c index acded91..e13466b 100644 --- a/movement/watch_faces/complication/planetary_hours_face.c +++ b/movement/watch_faces/complication/planetary_hours_face.c @@ -228,6 +228,7 @@ static void _planetary_hours(movement_settings_t *settings, planetary_hours_stat uint8_t weekday, planet, planetary_hour; uint32_t current_hour_epoch; watch_date_time scratch_time; + bool set_leading_zero = false; // check if we have a location. If not, display error if ( state->no_location ) { @@ -253,7 +254,7 @@ static void _planetary_hours(movement_settings_t *settings, planetary_hours_stat return; } - if (settings->bit.clock_mode_24h) watch_set_indicator(WATCH_INDICATOR_24H); + if (settings->bit.clock_mode_24h && !settings->bit.clock_24h_leading_zero) watch_set_indicator(WATCH_INDICATOR_24H); // roll over hour iterator if ( state->hour < 0 ) state->hour = 23; @@ -313,6 +314,8 @@ static void _planetary_hours(movement_settings_t *settings, planetary_hours_stat } scratch_time.unit.hour %= 12; if (scratch_time.unit.hour == 0) scratch_time.unit.hour = 12; + } else if (settings->bit.clock_24h_leading_zero && scratch_time.unit.hour < 10) { + set_leading_zero = true; } // planetary ruler of the hour @@ -328,6 +331,8 @@ static void _planetary_hours(movement_settings_t *settings, planetary_hours_stat watch_set_colon(); watch_display_string(buf, 0); + if (set_leading_zero) + watch_display_string("0", 4); if ( state->ruler == 2 ) _planetary_icon(planet); } diff --git a/movement/watch_faces/complication/planetary_time_face.c b/movement/watch_faces/complication/planetary_time_face.c index 56a18cf..7e7ac6c 100644 --- a/movement/watch_faces/complication/planetary_time_face.c +++ b/movement/watch_faces/complication/planetary_time_face.c @@ -206,6 +206,7 @@ static void _planetary_time(movement_event_t event, movement_settings_t *setting double night_hour_count = 0.0; uint8_t weekday, planet, planetary_hour; double hour_duration, current_hour, current_minute, current_second; + bool set_leading_zero = false; watch_set_colon(); @@ -218,7 +219,7 @@ static void _planetary_time(movement_event_t event, movement_settings_t *setting return; } - if (settings->bit.clock_mode_24h) watch_set_indicator(WATCH_INDICATOR_24H); + if (settings->bit.clock_mode_24h && !settings->bit.clock_24h_leading_zero) watch_set_indicator(WATCH_INDICATOR_24H); // PM for night hours, otherwise the night hours are counted from 13 if ( state->night ) { @@ -246,6 +247,9 @@ static void _planetary_time(movement_event_t event, movement_settings_t *setting state->scratch.unit.minute = floor(current_minute); state->scratch.unit.second = (uint8_t)floor(current_second) % 60; + if (settings->bit.clock_mode_24h && settings->bit.clock_24h_leading_zero && state->scratch.unit.hour < 10) + set_leading_zero = true; + // what weekday is it (0 - 6) weekday = watch_utility_get_iso8601_weekday_number(state->scratch.unit.year, state->scratch.unit.month, state->scratch.unit.day) - 1; @@ -263,6 +267,8 @@ static void _planetary_time(movement_event_t event, movement_settings_t *setting else sprintf(buf, "%s h%2d%02d%02d", ruler, state->scratch.unit.hour, state->scratch.unit.minute, state->scratch.unit.second); watch_display_string(buf, 0); + if (set_leading_zero) + watch_display_string("0", 4); if ( state->ruler == 2 ) _planetary_icon(planet); diff --git a/movement/watch_faces/complication/simon_face.c b/movement/watch_faces/complication/simon_face.c new file mode 100644 index 0000000..eb08f17 --- /dev/null +++ b/movement/watch_faces/complication/simon_face.c @@ -0,0 +1,335 @@ +/* + * 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 +#include +#include + +// Emulator only: need time() to seed the random number generator +#if __EMSCRIPTEN__ +#include +#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) { + if (state->playing_state == SIMON_NOT_PLAYING) { + watch_display_string(" ", 0); + } else { + sprintf(_simon_display_buf, " %2d ", state->sequence_length); + watch_display_string(_simon_display_buf, 0); + } +} + +static void _simon_not_playing_display(simon_state_t *state) { + _simon_clear_display(state); + + sprintf(_simon_display_buf, "SI %d", state->best_score); + 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); + watch_display_string(_simon_display_buf, 0); + switch (state->mode) + { + case SIMON_MODE_EASY: + watch_display_string("E", 9); + break; + case SIMON_MODE_HARD: + watch_display_string("H", 9); + 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) { + char *ndtemplate = NULL; + + switch (note) { + case SIMON_LED_NOTE: + ndtemplate = "LI%2d "; + break; + case SIMON_ALARM_NOTE: + ndtemplate = " %2d AL"; + break; + case SIMON_MODE_NOTE: + ndtemplate = " %2dDE "; + break; + case SIMON_WRONG_NOTE: + ndtemplate = "OH NOOOOO"; + } + + sprintf(_simon_display_buf, ndtemplate, state->sequence_length); + watch_display_string(_simon_display_buf, 0); +} + +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(movement_settings_t *settings, uint8_t watch_face_index, + void **context_ptr) { + (void)settings; + (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(movement_settings_t *settings, void *context) { + (void) settings; + (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, movement_settings_t *settings, + 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, settings); + } + + return true; +} + +void simon_face_resign(movement_settings_t *settings, void *context) { + (void)settings; + (void)context; + watch_set_led_off(); + watch_set_buzzer_off(); +} diff --git a/movement/watch_faces/complication/simon_face.h b/movement/watch_faces/complication/simon_face.h new file mode 100644 index 0000000..44bc9f3 --- /dev/null +++ b/movement/watch_faces/complication/simon_face.h @@ -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(movement_settings_t *settings, uint8_t watch_face_index, void **context_ptr); +void simon_face_activate(movement_settings_t *settings, void *context); +bool simon_face_loop(movement_event_t event, movement_settings_t *settings, void *context); +void simon_face_resign(movement_settings_t *settings, 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_ diff --git a/movement/watch_faces/complication/simple_calculator_face.c b/movement/watch_faces/complication/simple_calculator_face.c new file mode 100644 index 0000000..53fdd87 --- /dev/null +++ b/movement/watch_faces/complication/simple_calculator_face.c @@ -0,0 +1,465 @@ +/* + * MIT License + * + * Copyright (c) 2024 Patrick McGuire + * + * 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 +#include +#include +#include "simple_calculator_face.h" + +void simple_calculator_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) { + (void) settings; + (void) watch_face_index; + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(simple_calculator_state_t)); + memset(*context_ptr, 0, sizeof(simple_calculator_state_t)); + } +} + +static void reset_to_zero(calculator_number_t *number) { + number->negative = false; + number->hundredths = 0; + number->tenths = 0; + number->ones = 0; + number->tens = 0; + number->hundreds = 0; + number->thousands = 0; +} + +void simple_calculator_face_activate(movement_settings_t *settings, void *context) { + (void) settings; + simple_calculator_state_t *state = (simple_calculator_state_t *)context; + state->placeholder = PLACEHOLDER_ONES; + state->mode = MODE_ENTERING_FIRST_NUM; + reset_to_zero(&state->second_num); + reset_to_zero(&state->result); + movement_request_tick_frequency(4); +} + +static void increment_placeholder(calculator_number_t *number, calculator_placeholder_t placeholder) { + uint8_t *digits[] = { + &number->hundredths, + &number->tenths, + &number->ones, + &number->tens, + &number->hundreds, + &number->thousands + }; + *digits[placeholder] = (*digits[placeholder] + 1) % 10; +} + +static float convert_to_float(calculator_number_t number) { + float result = 0.0; + + // Add the whole number portion + result += number.thousands * 1000.0f; + result += number.hundreds * 100.0f; + result += number.tens * 10.0f; + result += number.ones * 1.0f; + + // Add the fractional portion + result += number.tenths * 0.1f; + result += number.hundredths * 0.01f; + + // Round to nearest hundredth + result = roundf(result * 100) / 100; + + // Handle negative numbers + if (number.negative) result = -result; + //printf("convert_to_float results = %f\n", result); // For debugging + + return result; +} + +static char* update_display_number(calculator_number_t *number, char *display_string, uint8_t which_num) { + char sign = ' '; + if (number->negative) sign = '-'; + + sprintf(display_string, "CA%d%c%d%d%d%d%d%d", + which_num, + sign, + number->thousands, + number->hundreds, + number->tens, + number->ones, + number->tenths, + number->hundredths); + + return display_string; +} + +static void set_operation(simple_calculator_state_t *state) { + switch (state->operation) { + case OP_ADD: + watch_display_string(" Add", 0); + break; + case OP_SUB: + watch_display_string(" sub", 0); + break; + case OP_MULT: + watch_display_string(" n&ul", 0); + break; + case OP_DIV: + watch_display_string(" div", 0); + break; + case OP_ROOT: + watch_display_string(" root", 0); + break; + case OP_POWER: + watch_display_string(" pow", 0); + break; + } +} + +static void cycle_operation(simple_calculator_state_t *state) { + state->operation = (state->operation + 1) % OPERATIONS_COUNT; // Assuming there are 6 operations +} + + +static calculator_number_t convert_to_string(float number) { + calculator_number_t result; + + // Handle negative numbers + if (number < 0) { + number = -number; + result.negative = true; + } else result.negative = false; + + // Get each digit from each placeholder + int int_part = (int)number; + + float decimal_part_float = ((number - int_part) * 100); // two decimal places + //printf("decimal_part_float = %f\n", decimal_part_float); //For debugging + + int decimal_part = round(decimal_part_float); + //printf("decimal_part = %d\n", decimal_part); //For debugging + + result.thousands = int_part / 1000 % 10; + result.hundreds = int_part / 100 % 10; + result.tens = int_part / 10 % 10; + result.ones = int_part % 10; + + result.tenths = decimal_part / 10 % 10; + result.hundredths = decimal_part % 10; + + return result; +} + +// This is the main function for setting the first_num and second_num +// WISH: there must be a way to pass less to this function? +static void set_number(calculator_number_t *number, calculator_placeholder_t placeholder, char *display_string, char *temp_display_string, movement_event_t event, uint8_t which_num) { + + // Create the display index + uint8_t display_index; + + // Update display string with current number and copy into temp string + update_display_number(number, display_string, which_num); + strcpy(temp_display_string, display_string); + + // Determine the display index based on the placeholder + display_index = 9 - placeholder; + + // Blink selected placeholder + // Check if `event.subsecond` is even + if (event.subsecond % 2 == 0) { + // Replace the character at the index corresponding to the current placeholder with a space + temp_display_string[display_index] = ' '; + } + + // Display the (possibly modified) string + watch_display_string(temp_display_string, 0); +} + +static void view_results(simple_calculator_state_t *state, char *display_string) { + + // Initialize float variables to do the math + float first_num_float, second_num_float, result_float = 0.0f; + + // Convert the passed numbers to floats + first_num_float = convert_to_float(state->first_num); + second_num_float = convert_to_float(state->second_num); + + // Perform the calculation based on the selected operation + switch (state->operation) { + case OP_ADD: + result_float = first_num_float + second_num_float; + break; + case OP_SUB: + result_float = first_num_float - second_num_float; + break; + case OP_MULT: + result_float = first_num_float * second_num_float; + break; + case OP_DIV: + if (second_num_float != 0) { + result_float = first_num_float / second_num_float; + } else { + state->mode = MODE_ERROR; + return; + } + break; + case OP_ROOT: + if (first_num_float >= 0) { + result_float = sqrtf(first_num_float); + } else { + state->mode = MODE_ERROR; + return; + } + break; + case OP_POWER: + result_float = powf(first_num_float, second_num_float); + break; + default: + result_float = 0.0f; + break; + } + + // Be sure the result can fit on the watch display, else error + if (result_float > 9999.99 || result_float < -9999.99) { + state->mode = MODE_ERROR; + return; + } + + result_float = roundf(result_float * 100.0f) / 100.0f; // Might not be needed + + //printf("result as float = %f\n", result_float); // For debugging + + // Convert the float result to a string + // This isn't strictly necessary, but allows easily reusing the result as + // the next calculation's first_num + state->result = convert_to_string(result_float); + + // Update the display with the result + update_display_number(&state->result, display_string, 3); + + //printf("display_string = %s\n", display_string); // For debugging + + watch_display_string(display_string, 0); +} + +// Used both when returning from errors and when long pressing MODE +static void reset_all(simple_calculator_state_t *state) { + reset_to_zero(&state->first_num); + reset_to_zero(&state->second_num); + state->mode = MODE_ENTERING_FIRST_NUM; + state->operation = OP_ADD; + state->placeholder = PLACEHOLDER_ONES; +} + +bool simple_calculator_face_loop(movement_event_t event, movement_settings_t *settings, void *context) { + simple_calculator_state_t *state = (simple_calculator_state_t *)context; + char display_string[10]; + char temp_display_string[10]; // Temporary buffer for blinking effect + + switch (event.event_type) { + case EVENT_ACTIVATE: + case EVENT_TICK: + switch (state->mode) { + case MODE_ENTERING_FIRST_NUM: + // See the WISH for this function above + set_number(&state->first_num, + state->placeholder, + display_string, + temp_display_string, + event, + 1); + break; + + case MODE_CHOOSING: + set_operation(state); + break; + + case MODE_ENTERING_SECOND_NUM: + // If doing a square root calculation, skip to results + if (state->operation == OP_ROOT) { + state->mode = MODE_VIEW_RESULTS; + } else { + // See the WISH for this function above + set_number(&state->second_num, + state->placeholder, + display_string, + temp_display_string, + event, + 2); + } + break; + + case MODE_VIEW_RESULTS: + view_results(state, display_string); + break; + + case MODE_ERROR: + watch_display_string("CA Error ", 0); + break; + } + break; + + case EVENT_LIGHT_BUTTON_DOWN: + break; + + case EVENT_LIGHT_BUTTON_UP: + switch (state->mode) { + case MODE_ENTERING_FIRST_NUM: + case MODE_ENTERING_SECOND_NUM: + // Move to the next placeholder when the light button is pressed + state->placeholder = (state->placeholder + 1) % MAX_PLACEHOLDERS; // Loop back to the start after PLACEHOLDER_THOUSANDS + break; + case MODE_CHOOSING: + cycle_operation(state); + break; + case MODE_ERROR: + reset_all(state); + break; + case MODE_VIEW_RESULTS: + break; + } + break; + + case EVENT_LIGHT_LONG_PRESS: + switch (state->mode) { + case MODE_ENTERING_FIRST_NUM: + // toggle negative on state->first_num + state->first_num.negative = !state->first_num.negative; + break; + case MODE_ENTERING_SECOND_NUM: + // toggle negative on state->second_num + state->second_num.negative = !state->second_num.negative; + break; + case MODE_ERROR: + reset_all(state); + break; + case MODE_CHOOSING: + case MODE_VIEW_RESULTS: + break; + } + break; + + case EVENT_ALARM_BUTTON_UP: + switch (state->mode) { + case MODE_ENTERING_FIRST_NUM: + // Increment the digit in the current placeholder + increment_placeholder(&state->first_num, state->placeholder); + update_display_number(&state->first_num, display_string, 1); + + //printf("display_string = %s\n", display_string); // For debugging + + break; + case MODE_CHOOSING: + // Confirm and select the current operation + state->mode = MODE_ENTERING_SECOND_NUM; + break; + case MODE_ENTERING_SECOND_NUM: + // Increment the digit in the current placeholder + increment_placeholder(&state->second_num, state->placeholder); + update_display_number(&state->second_num, display_string, 2); + + //printf("display_string = %s\n", display_string); // For debugging + + break; + case MODE_ERROR: + reset_all(state); + break; + case MODE_VIEW_RESULTS: + break; + } + break; + + case EVENT_ALARM_LONG_PRESS: + switch (state->mode) { + case MODE_ENTERING_FIRST_NUM: + reset_to_zero(&state->first_num); + break; + case MODE_ENTERING_SECOND_NUM: + reset_to_zero(&state->second_num); + break; + case MODE_ERROR: + reset_all(state); + break; + case MODE_CHOOSING: + case MODE_VIEW_RESULTS: + break; + } + break; + + case EVENT_MODE_BUTTON_DOWN: + break; + + case EVENT_MODE_BUTTON_UP: + if (state->mode == MODE_ERROR) { + reset_all(state); + } else if (state->mode == MODE_ENTERING_FIRST_NUM && + state->first_num.hundredths == 0 && + state->first_num.tenths == 0 && + state->first_num.ones== 0 && + state->first_num.tens == 0 && + state->first_num.hundreds == 0 && + state->first_num.thousands == 0) { + movement_move_to_next_face(); + } else { + // Reset the placeholder and proceed to the next MODE + state->placeholder = PLACEHOLDER_ONES; + state->mode = (state->mode + 1) % 4; + // When looping back to MODE_ENTERING_FIRST_NUM, reuse the + // previous calculation's results as the next calculation's + // first_num; also reset other numbers + if (state->mode == MODE_ENTERING_FIRST_NUM) { + state->first_num = state->result; + reset_to_zero(&state->second_num); + reset_to_zero(&state->result); + } + } + break; + + case EVENT_MODE_LONG_PRESS: + // Move to next face if first number is 0 + if (state->first_num.hundredths == 0 && + state->first_num.tenths == 0 && + state->first_num.ones== 0 && + state->first_num.tens == 0 && + state->first_num.hundreds == 0 && + state->first_num.thousands == 0) { + movement_move_to_face(0); + // otherwise, start over + } else { + reset_all(state); + } + break; + + case EVENT_TIMEOUT: + movement_request_tick_frequency(1); + movement_move_to_face(0); + break; + + default: + return movement_default_loop_handler(event, settings); + } + + return true; +} + +void simple_calculator_face_resign(movement_settings_t *settings, void *context) { + (void) settings; + (void) context; + movement_request_tick_frequency(1); +} + diff --git a/movement/watch_faces/complication/simple_calculator_face.h b/movement/watch_faces/complication/simple_calculator_face.h new file mode 100644 index 0000000..1f77dc0 --- /dev/null +++ b/movement/watch_faces/complication/simple_calculator_face.h @@ -0,0 +1,145 @@ +/* + * MIT License + * + * Copyright (c) 2024 Patrick McGuire + * + * 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 SIMPLE_CALCULATOR_FACE_H_ +#define SIMPLE_CALCULATOR_FACE_H_ + +#include "movement.h" + +/* + * Simple Calculator + * + * How to use: + * + * Flow: + * Enter first number -> Select operator -> Enter second number -> View Results + * + * How to read the display: + * - "CA" is displayed at the top to tell you that you're in the CAlculator + * - The top-right digit (1, 2, or 3) lets you know whether you're entering the + * first number (1), entering the second number (2), or viewing the results (3). + * - To the right of the top-right digit will show the number's sign. If the + * number is negative, a "-" will be displayed, otherwise it is empty. + * - The 4 large digits to the left are whole numbers and the 2 smaller digits + * on the right are the tenths and hundredths decimal places. + * + * Entering the first number: + * - Press ALARM to increment the selected (blinking) digit + * - Press LIGHT to move to the next placeholder + * - LONG PRESS the LIGHT button to toggle the number's sign to make it + * negative + * - LONG PRESS the ALARM button to reset the number to 0 + * - Press MODE to proceed to selecting the operator + * + * Selecting the operator: + * - Press the LIGHT button to cycle through available operators. They are: + * + Add + * - Subtract + * * Multiply + * / Divide + * sqrtf() Square root + * powf() Power (exponent calculation) + * - Press MODE or ALARM to proceed to entering the second number + * + * Entering the second number: + * - Everything is the same as setting the first number except that pressing + * MODE here will proceed to viewing the results + * + * Viewing the results: + * - Pressing MODE will start a new calculation with the result as the first + * number. (LONG PRESS ALARM to reset the value to 0) + * + * Errors: + * - An error will be triggered if the result is not able to be displayed, that + * is, if the value is greater than 9,999.99 or less than -9,999.99. + * - An error will also be triggered if an impossible operation is selected, + * for instance trying to divide by 0 or get the square root of a negative + * number. + * - Exit error mode and start over with any button press. + * + */ + +#define OPERATIONS_COUNT 6 +#define MAX_PLACEHOLDERS 6 + +typedef struct { + bool negative; + uint8_t hundredths; + uint8_t tenths; + uint8_t ones; + uint8_t tens; + uint8_t hundreds; + uint8_t thousands; +} calculator_number_t; + +typedef enum { + PLACEHOLDER_HUNDREDTHS, + PLACEHOLDER_TENTHS, + PLACEHOLDER_ONES, + PLACEHOLDER_TENS, + PLACEHOLDER_HUNDREDS, + PLACEHOLDER_THOUSANDS +} calculator_placeholder_t; + +typedef enum { + OP_ADD, + OP_SUB, + OP_MULT, + OP_DIV, + OP_ROOT, + OP_POWER, +} calculator_operation_t; + +typedef enum { + MODE_ENTERING_FIRST_NUM, + MODE_CHOOSING, + MODE_ENTERING_SECOND_NUM, + MODE_VIEW_RESULTS, + MODE_ERROR +} calculator_mode_t; + +typedef struct { + calculator_number_t first_num; + calculator_number_t second_num; + calculator_number_t result; + calculator_operation_t operation; + calculator_mode_t mode; + calculator_placeholder_t placeholder; +} simple_calculator_state_t; + +void simple_calculator_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); +void simple_calculator_face_activate(movement_settings_t *settings, void *context); +bool simple_calculator_face_loop(movement_event_t event, movement_settings_t *settings, void *context); +void simple_calculator_face_resign(movement_settings_t *settings, void *context); + +#define simple_calculator_face ((const watch_face_t){ \ + simple_calculator_face_setup, \ + simple_calculator_face_activate, \ + simple_calculator_face_loop, \ + simple_calculator_face_resign, \ + NULL, \ +}) + +#endif // SIMPLE_CALCULATOR_FACE_H_ + diff --git a/movement/watch_faces/complication/sunrise_sunset_face.c b/movement/watch_faces/complication/sunrise_sunset_face.c index fbf60cf..8747bd8 100644 --- a/movement/watch_faces/complication/sunrise_sunset_face.c +++ b/movement/watch_faces/complication/sunrise_sunset_face.c @@ -93,7 +93,7 @@ static void _sunrise_sunset_face_update(movement_settings_t *settings, sunrise_s } watch_set_colon(); - if (settings->bit.clock_mode_24h) watch_set_indicator(WATCH_INDICATOR_24H); + if (settings->bit.clock_mode_24h && !settings->bit.clock_24h_leading_zero) watch_set_indicator(WATCH_INDICATOR_24H); rise += hours_from_utc; set += hours_from_utc; @@ -113,12 +113,17 @@ static void _sunrise_sunset_face_update(movement_settings_t *settings, sunrise_s if (date_time.reg < scratch_time.reg || show_next_match) { if (state->rise_index == 0 || show_next_match) { + bool set_leading_zero = false; if (!settings->bit.clock_mode_24h) { if (watch_utility_convert_to_12_hour(&scratch_time)) watch_set_indicator(WATCH_INDICATOR_PM); else watch_clear_indicator(WATCH_INDICATOR_PM); + } else if (settings->bit.clock_24h_leading_zero && scratch_time.unit.hour < 10) { + set_leading_zero = true; } sprintf(buf, "rI%2d%2d%02d%s", scratch_time.unit.day, scratch_time.unit.hour, scratch_time.unit.minute,longLatPresets[state->longLatToUse].name); watch_display_string(buf, 0); + if (set_leading_zero) + watch_display_string("0", 4); return; } else { show_next_match = true; @@ -140,12 +145,17 @@ static void _sunrise_sunset_face_update(movement_settings_t *settings, sunrise_s if (date_time.reg < scratch_time.reg || show_next_match) { if (state->rise_index == 0 || show_next_match) { + bool set_leading_zero = false; if (!settings->bit.clock_mode_24h) { if (watch_utility_convert_to_12_hour(&scratch_time)) watch_set_indicator(WATCH_INDICATOR_PM); else watch_clear_indicator(WATCH_INDICATOR_PM); + } else if (settings->bit.clock_24h_leading_zero && scratch_time.unit.hour < 10) { + set_leading_zero = true; } sprintf(buf, "SE%2d%2d%02d%s", scratch_time.unit.day, scratch_time.unit.hour, scratch_time.unit.minute, longLatPresets[state->longLatToUse].name); watch_display_string(buf, 0); + if (set_leading_zero) + watch_display_string("0", 4); return; } else { show_next_match = true; diff --git a/movement/watch_faces/complication/tally_face.c b/movement/watch_faces/complication/tally_face.c index 896a54f..3ea5503 100644 --- a/movement/watch_faces/complication/tally_face.c +++ b/movement/watch_faces/complication/tally_face.c @@ -27,47 +27,162 @@ #include "tally_face.h" #include "watch.h" +#define TALLY_FACE_MAX 9999 +#define TALLY_FACE_MIN -99 + +static bool _init_val; +static bool _quick_ticks_running; + +static const int16_t _tally_default[] = { + 0, + +#ifdef TALLY_FACE_PRESETS_MTG + 20, + 40, +#endif /* TALLY_FACE_PRESETS_MTG */ + +#ifdef TALLY_FACE_PRESETS_YUGIOH + 4000, + 8000, +#endif /* TALLY_FACE_PRESETS_YUGIOH */ + +}; + +#define TALLY_FACE_PRESETS_SIZE() (sizeof(_tally_default) / sizeof(int16_t)) + void tally_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) { (void) settings; (void) watch_face_index; if (*context_ptr == NULL) { *context_ptr = malloc(sizeof(tally_state_t)); memset(*context_ptr, 0, sizeof(tally_state_t)); + tally_state_t *state = (tally_state_t *)*context_ptr; + state->tally_default_idx = 0; + state->tally_idx = _tally_default[state->tally_default_idx]; + _init_val = true; } } void tally_face_activate(movement_settings_t *settings, void *context) { (void) settings; (void) context; + _quick_ticks_running = false; +} + +static void start_quick_cyc(void){ + _quick_ticks_running = true; + movement_request_tick_frequency(8); +} + +static void stop_quick_cyc(void){ + _quick_ticks_running = false; + movement_request_tick_frequency(1); +} + +static void tally_face_increment(tally_state_t *state, bool sound_on) { + bool soundOn = !_quick_ticks_running && sound_on; + _init_val = false; + if (state->tally_idx >= TALLY_FACE_MAX){ + if (soundOn) watch_buzzer_play_note(BUZZER_NOTE_E7, 30); + } + else { + state->tally_idx++; + print_tally(state, sound_on); + if (soundOn) watch_buzzer_play_note(BUZZER_NOTE_E6, 30); + } +} + +static void tally_face_decrement(tally_state_t *state, bool sound_on) { + bool soundOn = !_quick_ticks_running && sound_on; + _init_val = false; + if (state->tally_idx <= TALLY_FACE_MIN){ + if (soundOn) watch_buzzer_play_note(BUZZER_NOTE_C5SHARP_D5FLAT, 30); + } + else { + state->tally_idx--; + print_tally(state, sound_on); + if (soundOn) watch_buzzer_play_note(BUZZER_NOTE_C6SHARP_D6FLAT, 30); + } +} + +static bool tally_face_should_move_back(tally_state_t *state) { + if (TALLY_FACE_PRESETS_SIZE() <= 1) { return false; } + return state->tally_idx == _tally_default[state->tally_default_idx]; } bool tally_face_loop(movement_event_t event, movement_settings_t *settings, void *context) { - (void) settings; tally_state_t *state = (tally_state_t *)context; - + static bool using_led = false; + + if (using_led) { + if(!watch_get_pin_level(BTN_MODE) && !watch_get_pin_level(BTN_LIGHT) && !watch_get_pin_level(BTN_ALARM)) + using_led = false; + else { + if (event.event_type == EVENT_LIGHT_BUTTON_DOWN || event.event_type == EVENT_ALARM_BUTTON_DOWN) + movement_illuminate_led(); + return true; + } + } + switch (event.event_type) { - case EVENT_ALARM_BUTTON_UP: - // increment tally index - state->tally_idx++; - if (state->tally_idx > 999999) { //0-999,999 - //reset tally index and play a reset tune - state->tally_idx = 0; - watch_buzzer_play_note(BUZZER_NOTE_G6, 30); - watch_buzzer_play_note(BUZZER_NOTE_REST, 30); + case EVENT_TICK: + if (_quick_ticks_running) { + bool light_pressed = watch_get_pin_level(BTN_LIGHT); + bool alarm_pressed = watch_get_pin_level(BTN_ALARM); + if (light_pressed && alarm_pressed) stop_quick_cyc(); + else if (light_pressed) tally_face_increment(state, settings->bit.button_should_sound); + else if (alarm_pressed) tally_face_decrement(state, settings->bit.button_should_sound); + else stop_quick_cyc(); } - print_tally(state); - watch_buzzer_play_note(BUZZER_NOTE_E6, 30); + break; + case EVENT_ALARM_BUTTON_UP: + tally_face_decrement(state, settings->bit.button_should_sound); break; case EVENT_ALARM_LONG_PRESS: - state->tally_idx = 0; // reset tally index - //play a reset tune - watch_buzzer_play_note(BUZZER_NOTE_G6, 30); - watch_buzzer_play_note(BUZZER_NOTE_REST, 30); - watch_buzzer_play_note(BUZZER_NOTE_E6, 30); - print_tally(state); + tally_face_decrement(state, settings->bit.button_should_sound); + start_quick_cyc(); + break; + case EVENT_MODE_LONG_PRESS: + if (tally_face_should_move_back(state)) { + _init_val = true; + movement_move_to_face(0); + } + else { + state->tally_idx = _tally_default[state->tally_default_idx]; // reset tally index + _init_val = true; + //play a reset tune + if (settings->bit.button_should_sound) watch_buzzer_play_note(BUZZER_NOTE_G6, 30); + if (settings->bit.button_should_sound) watch_buzzer_play_note(BUZZER_NOTE_REST, 30); + if (settings->bit.button_should_sound) watch_buzzer_play_note(BUZZER_NOTE_E6, 30); + print_tally(state, settings->bit.button_should_sound); + } + break; + case EVENT_LIGHT_BUTTON_UP: + tally_face_increment(state, settings->bit.button_should_sound); + break; + case EVENT_LIGHT_BUTTON_DOWN: + case EVENT_ALARM_BUTTON_DOWN: + if (watch_get_pin_level(BTN_MODE)) { + movement_illuminate_led(); + using_led = true; + } + break; + case EVENT_LIGHT_LONG_PRESS: + if (TALLY_FACE_PRESETS_SIZE() > 1 && _init_val){ + state->tally_default_idx = (state->tally_default_idx + 1) % TALLY_FACE_PRESETS_SIZE(); + state->tally_idx = _tally_default[state->tally_default_idx]; + if (settings->bit.button_should_sound) watch_buzzer_play_note(BUZZER_NOTE_E6, 30); + if (settings->bit.button_should_sound) watch_buzzer_play_note(BUZZER_NOTE_REST, 30); + if (settings->bit.button_should_sound) watch_buzzer_play_note(BUZZER_NOTE_G6, 30); + print_tally(state, settings->bit.button_should_sound); + } + else{ + tally_face_increment(state, settings->bit.button_should_sound); + start_quick_cyc(); + } break; case EVENT_ACTIVATE: - print_tally(state); + print_tally(state, settings->bit.button_should_sound); break; case EVENT_TIMEOUT: // ignore timeout @@ -81,9 +196,16 @@ bool tally_face_loop(movement_event_t event, movement_settings_t *settings, void } // print tally index at the center of display. -void print_tally(tally_state_t *state) { +void print_tally(tally_state_t *state, bool sound_on) { char buf[14]; - sprintf(buf, "TA %06d", (int)(state->tally_idx)); // center of LCD display + if (sound_on) + watch_set_indicator(WATCH_INDICATOR_BELL); + else + watch_clear_indicator(WATCH_INDICATOR_BELL); + if (state->tally_idx >= 0) + sprintf(buf, "TA %4d ", (int)(state->tally_idx)); // center of LCD display + else + sprintf(buf, "TA %-3d", (int)(state->tally_idx)); // center of LCD display watch_display_string(buf, 0); } diff --git a/movement/watch_faces/complication/tally_face.h b/movement/watch_faces/complication/tally_face.h index 8096592..80623f4 100644 --- a/movement/watch_faces/complication/tally_face.h +++ b/movement/watch_faces/complication/tally_face.h @@ -29,25 +29,41 @@ * TALLY face * * Tally face is designed to act as a tally counter. - * Based on the counter_face watch face by Shogo Okamoto. * - * To advance the counter, press the ALARM button. - * To reset, long press the ALARM button. + * Alarm + * Press: Decrement + * Hold : Fast Decrement + * + * Light + * Press: Increment + * Hold : On initial value: Cycles through other initial values. + * Else: Fast Increment + * + * Mode + * Press: Next face + * Hold : On initial value: Go to first face. + * Else: Resets counter + * + * Incrementing or Decrementing the tally will beep if Beeping is set in the global Preferences */ #include "movement.h" typedef struct { - uint32_t tally_idx; + int16_t tally_idx; + uint8_t tally_default_idx; } tally_state_t; +//#define TALLY_FACE_PRESETS_MTG +//#define TALLY_FACE_PRESETS_YUGIOH + void tally_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); void tally_face_activate(movement_settings_t *settings, void *context); bool tally_face_loop(movement_event_t event, movement_settings_t *settings, void *context); void tally_face_resign(movement_settings_t *settings, void *context); -void print_tally(tally_state_t *state); +void print_tally(tally_state_t *state, bool sound_on); #define tally_face ((const watch_face_t){ \ tally_face_setup, \ diff --git a/movement/watch_faces/complication/wake_face.c b/movement/watch_faces/complication/wake_face.c index 6fa801f..e645b25 100644 --- a/movement/watch_faces/complication/wake_face.c +++ b/movement/watch_faces/complication/wake_face.c @@ -38,12 +38,15 @@ void _wake_face_update_display(movement_settings_t *settings, wake_face_state_t uint8_t hour = state->hour; watch_clear_display(); - if ( settings->bit.clock_mode_24h ) - watch_set_indicator(WATCH_INDICATOR_24H); - else { + bool set_leading_zero = false; + if ( !settings->bit.clock_mode_24h ) { if ( hour >= 12 ) watch_set_indicator(WATCH_INDICATOR_PM); hour = hour % 12 ? hour % 12 : 12; + } else if ( !settings->bit.clock_24h_leading_zero ) { + watch_set_indicator(WATCH_INDICATOR_24H); + } else if ( hour < 10 ) { + set_leading_zero = true; } if ( state->mode ) @@ -54,6 +57,8 @@ void _wake_face_update_display(movement_settings_t *settings, wake_face_state_t watch_set_colon(); watch_display_string(lcdbuf, 0); + if ( set_leading_zero ) + watch_display_string("0", 4); } // diff --git a/movement/watch_faces/complication/wordle_face.c b/movement/watch_faces/complication/wordle_face.c new file mode 100644 index 0000000..8381cec --- /dev/null +++ b/movement/watch_faces/complication/wordle_face.c @@ -0,0 +1,602 @@ +/* + * MIT License + * + * Copyright (c) 2024 + * + * 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 +#include +#include "wordle_face.h" +#include "watch_utility.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(WordleLetterResult *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, WordleLetterResult *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, WordleLetterResult *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[1 + 1]; + if (state->word_elements[state->position] >= WORDLE_NUM_VALID_LETTERS) { + if (display_dash) + watch_display_string("-", state->position + 5); + else + watch_display_string(" ", state->position + 5); + return; + } + sprintf(buf, "%c", _valid_letters[state->word_elements[state->position]]); + watch_display_string(buf, state->position + 5); +} + +static void display_all_letters(wordle_state_t *state) { + uint8_t prev_pos = state->position; + watch_display_string(" ", 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 = SCREEN_NO_DICT; + watch_display_string("nodict", 4); +} + +static void display_already_guessed(wordle_state_t *state) { + state->curr_screen = SCREEN_ALREADY_GUESSED; + watch_display_string("GUESSD", 4); +} + +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, WordleScreen curr_screen) { + if (curr_screen >= SCREEN_PLAYING) return; + if (skipping) + watch_display_string("H", 3); + else + watch_display_string(" ", 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_string(buf, 3); +} + +static void display_playing(wordle_state_t *state) { + state->curr_screen = 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 void reset_board(wordle_state_t *state) { + reset_all_elements(state); + state->curr_answer = get_random(WORDLE_NUM_WORDS); + watch_clear_colon(); + state->position = get_first_pos(state->word_elements_result); + display_playing(state); + watch_display_string(" -", 4); +#if __EMSCRIPTEN__ + printf("ANSWER: %s\r\n", _valid_words[state->curr_answer]); +#endif +} + +static void display_title(wordle_state_t *state) { + state->curr_screen = SCREEN_TITLE; + watch_display_string("WO WordLE", 0); + 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_string(continuing ? "y" : "n", 9); +} + +static void display_continue(wordle_state_t *state) { + state->curr_screen = SCREEN_CONTINUE; + watch_display_string("Cont ", 4); + 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[12]; + state->curr_screen = SCREEN_STREAK; +#if WORDLE_USE_DAILY_STREAK == 2 + if (state->streak > 99) + sprintf(buf, "WO St--dy"); + else + sprintf(buf, "WO St%2ddy", state->streak); +#else + sprintf(buf, "WO St%4d", state->streak); +#endif + watch_display_string(buf, 0); + 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 = SCREEN_WAIT; + if (state->streak < 40) { + char buf[13]; + sprintf(buf,"WO%2d WaIt ", state->streak); + watch_display_string(buf, 0); + } + else { // Streak too long to display in top-right + watch_display_string("WO WaIt ", 0); + } + show_skip_wrong_letter_indicator(state->skip_wrong_letter, state->curr_screen); +} +#endif + +static uint32_t get_day_unix_time(void) { + watch_date_time 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[WORDLE_LENGTH + 6]; + sprintf(buf," L %s", subsecond % 2 ? _valid_words[state->curr_answer] : " "); + watch_display_string(buf, 0); +} + +static void display_win(wordle_state_t *state, uint8_t subsecond) { + (void) state; + char buf[13]; + sprintf(buf," W %s ", subsecond % 2 ? "NICE" : "JOb "); + watch_display_string(buf, 0); +} + +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[WORDLE_LENGTH + 1]; + for (size_t i = 0; i < WORDLE_LENGTH; i++) + { + switch (state->word_elements_result[i]) + { + case WORDLE_LETTER_WRONG: + buf[i] = '-'; + break; + case WORDLE_LETTER_CORRECT: + buf[i] = _valid_letters[state->word_elements[i]]; + break; + case WORDLE_LETTER_WRONG_LOC: + if (subsecond % 2) + buf[i] = ' '; + else + buf[i] = _valid_letters[state->word_elements[i]]; + default: + break; + } + } + watch_display_string(buf, 5); +} + +static bool act_on_btn(wordle_state_t *state, const uint8_t pin) { + switch (state->curr_screen) + { + case SCREEN_RESULT: + reset_incorrect_elements(state); + state->position = get_first_pos(state->word_elements_result); + display_playing(state); + return true; + case 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 SCREEN_STREAK: + state->day_last_game_started = get_day_unix_time(); + reset_board(state); + return true; + case SCREEN_WIN: + case SCREEN_LOSE: + display_title(state); + return true; + case SCREEN_NO_DICT: + case SCREEN_ALREADY_GUESSED: + state->position = get_first_pos(state->word_elements_result); + display_playing(state); + return true; +#if WORDLE_USE_DAILY_STREAK == 2 + case SCREEN_WAIT: + (void) pin; + display_title(state); + return true; +#else + case 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; + } + return true; +#endif + default: + return false; + } + return false; +} + +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) { + reset_all_elements(state); + state->curr_screen = SCREEN_WIN; + 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) { + reset_all_elements(state); + state->curr_screen = SCREEN_LOSE; + state->streak = 0; + return; + } + update_known_wrong_letters(state); + state->curr_screen = SCREEN_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 + +void wordle_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) { + (void) settings; + (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 = SCREEN_TITLE; + state->skip_wrong_letter = false; + reset_all_elements(state); + } + // Do any pin or peripheral setup here; this will be called whenever the watch wakes from deep sleep. +} + +void wordle_face_activate(movement_settings_t *settings, void *context) { + (void) settings; + wordle_state_t *state = (wordle_state_t *)context; +#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 >= SCREEN_RESULT) { + reset_incorrect_elements(state); + state->position = get_first_pos(state->word_elements_result); + } + movement_request_tick_frequency(2); + display_title(state); +} + +bool wordle_face_loop(movement_event_t event, movement_settings_t *settings, void *context) { + wordle_state_t *state = (wordle_state_t *)context; + + switch (event.event_type) { + case EVENT_TICK: + switch (state->curr_screen) + { + case SCREEN_PLAYING: + if (event.subsecond % 2) { + display_letter(state, true); + } else { + watch_display_string(" ", state->position + 5); + } + break; + case SCREEN_RESULT: + display_result(state, event.subsecond); + break; + case SCREEN_LOSE: + display_lose(state, event.subsecond); + break; + case 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 < 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 != 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 (watch_get_pin_level(BTN_LIGHT) && + (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 != 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 >= 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 != SCREEN_TITLE) + display_title(state); + break; + default: + return movement_default_loop_handler(event, settings); + } + return true; +} + +void wordle_face_resign(movement_settings_t *settings, void *context) { + (void) settings; + (void) context; +} + diff --git a/movement/watch_faces/complication/wordle_face.h b/movement/watch_faces/complication/wordle_face.h new file mode 100644 index 0000000..f390e73 --- /dev/null +++ b/movement/watch_faces/complication/wordle_face.h @@ -0,0 +1,149 @@ +/* + * MIT License + * + * Copyright (c) 2024 + * + * 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 2 +#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 +} WordleLetterResult; + +typedef enum { + SCREEN_TITLE = 0, + SCREEN_STREAK, + SCREEN_CONTINUE, +#if WORDLE_USE_DAILY_STREAK + SCREEN_WAIT, +#endif + SCREEN_PLAYING, + SCREEN_RESULT, + SCREEN_WIN, + SCREEN_LOSE, + SCREEN_NO_DICT, + SCREEN_ALREADY_GUESSED, + SCREEN_COUNT +} WordleScreen; + +typedef struct { + // Anything you need to keep track of, put it here! + uint8_t word_elements[WORDLE_LENGTH]; + WordleLetterResult 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; + WordleScreen curr_screen; + bool known_wrong_letters[WORDLE_NUM_VALID_LETTERS]; + uint32_t day_last_game_started; +} wordle_state_t; + +void wordle_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); +void wordle_face_activate(movement_settings_t *settings, void *context); +bool wordle_face_loop(movement_event_t event, movement_settings_t *settings, void *context); +void wordle_face_resign(movement_settings_t *settings, 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_ + diff --git a/movement/watch_faces/complication/wordle_face_dict.h b/movement/watch_faces/complication/wordle_face_dict.h new file mode 100644 index 0000000..6b82976 --- /dev/null +++ b/movement/watch_faces/complication/wordle_face_dict.h @@ -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_ \ No newline at end of file diff --git a/movement/watch_faces/demo/beeps_face.c b/movement/watch_faces/demo/beeps_face.c new file mode 100644 index 0000000..07df627 --- /dev/null +++ b/movement/watch_faces/demo/beeps_face.c @@ -0,0 +1,249 @@ +/* + * MIT License + * + * Copyright (c) 2024 Wesley + * + * 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 +#include +#include "beeps_face.h" + +void beeps_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) { + (void) settings; + (void) watch_face_index; + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(beeps_state_t)); + memset(*context_ptr, 0, sizeof(beeps_state_t)); + // Do any one-time tasks in here; the inside of this conditional happens only at boot. + } +} + +void beeps_face_activate(movement_settings_t *settings, void *context) { + (void) settings; + beeps_state_t *state = (beeps_state_t *)context; +} + +static void _beep_face_update_lcd(beeps_state_t *state) { + char buf[11]; + const char buzzernote[][7] = {" 5500", " 5827", " 6174"," 6541"," 6930"," 7342"," 7778"," 8241"," 8731"," 9250"," 9800"," 10383"," 11000"," 11654"," 12347"," 13081"," 13859"," 14683"," 15556"," 16481"," 17461"," 18500"," 19600"," 20765"," 22000"," 23308"," 24694"," 26163"," 27718"," 29366"," 31113"," 32963"," 34923"," 36999"," 39200"," 41530"," 44000"," 46616"," 49388"," 52325"," 55437"," 58733"," 62225"," 65925"," 69846"," 73999"," 78399"," 83061"," 88000"," 93233"," 98777"," 104650"," 110873"," 117466"," 124451"," 131851"," 139691"," 147998"," 156798"," 166122"," 176000"," 186466"," 197553"," 209300"," 221746"," 234932"," 248902"," 263702"," 279383"," 295996"," 313596"," 332244"," 352000"," 372931"," 395107"," 418601"," 443492"," 469863"," 497803"," 527404"," 558765"," 591991"," 627193"," 664488"," 704000"," 745862"," 790213"}; + sprintf(buf, "HZ %s", buzzernote[state->frequency]); + watch_display_string(buf, 0); +} + +bool beeps_face_loop(movement_event_t event, movement_settings_t *settings, void *context) { + beeps_state_t *state = (beeps_state_t *)context; + + switch (event.event_type) { + case EVENT_ACTIVATE: + _beep_face_update_lcd(state); + break; + case EVENT_LIGHT_BUTTON_DOWN: + state->frequency = (state->frequency + 1) % 87; + _beep_face_update_lcd(state); + break; + case EVENT_ALARM_BUTTON_DOWN: + if (state->frequency == 0) { + watch_buzzer_play_note(BUZZER_NOTE_A1, 500); + } else if (state->frequency == 1) { + watch_buzzer_play_note(BUZZER_NOTE_A1SHARP_B1FLAT, 500); + } else if (state->frequency == 2) { + watch_buzzer_play_note(BUZZER_NOTE_B1, 500); + } else if (state->frequency == 3) { + watch_buzzer_play_note(BUZZER_NOTE_C2, 500); + } else if (state->frequency == 4) { + watch_buzzer_play_note(BUZZER_NOTE_C2SHARP_D2FLAT, 500); + } else if (state->frequency == 5) { + watch_buzzer_play_note(BUZZER_NOTE_D2, 500); + } else if (state->frequency == 6) { + watch_buzzer_play_note(BUZZER_NOTE_D2SHARP_E2FLAT, 500); + } else if (state->frequency == 7) { + watch_buzzer_play_note(BUZZER_NOTE_E2, 500); + } else if (state->frequency == 8) { + watch_buzzer_play_note(BUZZER_NOTE_F2, 500); + } else if (state->frequency == 9) { + watch_buzzer_play_note(BUZZER_NOTE_F2SHARP_G2FLAT, 500); + } else if (state->frequency == 10) { + watch_buzzer_play_note(BUZZER_NOTE_G2, 500); + } else if (state->frequency == 11) { + watch_buzzer_play_note(BUZZER_NOTE_G2SHARP_A2FLAT, 500); + } else if (state->frequency == 12) { + watch_buzzer_play_note(BUZZER_NOTE_A2, 500); + } else if (state->frequency == 13) { + watch_buzzer_play_note(BUZZER_NOTE_A2SHARP_B2FLAT, 500); + } else if (state->frequency == 14) { + watch_buzzer_play_note(BUZZER_NOTE_B2, 500); + } else if (state->frequency == 15) { + watch_buzzer_play_note(BUZZER_NOTE_C3, 500); + } else if (state->frequency == 16) { + watch_buzzer_play_note(BUZZER_NOTE_C3SHARP_D3FLAT, 500); + } else if (state->frequency == 17) { + watch_buzzer_play_note(BUZZER_NOTE_D3, 500); + } else if (state->frequency == 18) { + watch_buzzer_play_note(BUZZER_NOTE_D3SHARP_E3FLAT, 500); + } else if (state->frequency == 19) { + watch_buzzer_play_note(BUZZER_NOTE_E3, 500); + } else if (state->frequency == 20) { + watch_buzzer_play_note(BUZZER_NOTE_F3, 500); + } else if (state->frequency == 21) { + watch_buzzer_play_note(BUZZER_NOTE_F3SHARP_G3FLAT, 500); + } else if (state->frequency == 22) { + watch_buzzer_play_note(BUZZER_NOTE_G3, 500); + } else if (state->frequency == 23) { + watch_buzzer_play_note(BUZZER_NOTE_G3SHARP_A3FLAT, 500); + } else if (state->frequency == 24) { + watch_buzzer_play_note(BUZZER_NOTE_A3, 500); + } else if (state->frequency == 25) { + watch_buzzer_play_note(BUZZER_NOTE_A3SHARP_B3FLAT, 500); + } else if (state->frequency == 26) { + watch_buzzer_play_note(BUZZER_NOTE_B3, 500); + } else if (state->frequency == 27) { + watch_buzzer_play_note(BUZZER_NOTE_C4, 500); + } else if (state->frequency == 28) { + watch_buzzer_play_note(BUZZER_NOTE_C4SHARP_D4FLAT, 500); + } else if (state->frequency == 29) { + watch_buzzer_play_note(BUZZER_NOTE_D4, 500); + } else if (state->frequency == 30) { + watch_buzzer_play_note(BUZZER_NOTE_D4SHARP_E4FLAT, 500); + } else if (state->frequency == 31) { + watch_buzzer_play_note(BUZZER_NOTE_E4, 500); + } else if (state->frequency == 32) { + watch_buzzer_play_note(BUZZER_NOTE_F4, 500); + } else if (state->frequency == 33) { + watch_buzzer_play_note(BUZZER_NOTE_F4SHARP_G4FLAT, 500); + } else if (state->frequency == 34) { + watch_buzzer_play_note(BUZZER_NOTE_G4, 500); + } else if (state->frequency == 35) { + watch_buzzer_play_note(BUZZER_NOTE_G4SHARP_A4FLAT, 500); + } else if (state->frequency == 36) { + watch_buzzer_play_note(BUZZER_NOTE_A4, 500); + } else if (state->frequency == 37) { + watch_buzzer_play_note(BUZZER_NOTE_A4SHARP_B4FLAT, 500); + } else if (state->frequency == 38) { + watch_buzzer_play_note(BUZZER_NOTE_B4, 500); + } else if (state->frequency == 39) { + watch_buzzer_play_note(BUZZER_NOTE_C5, 500); + } else if (state->frequency == 40) { + watch_buzzer_play_note(BUZZER_NOTE_C5SHARP_D5FLAT, 500); + } else if (state->frequency == 41) { + watch_buzzer_play_note(BUZZER_NOTE_D5, 500); + } else if (state->frequency == 42) { + watch_buzzer_play_note(BUZZER_NOTE_D5SHARP_E5FLAT, 500); + } else if (state->frequency == 43) { + watch_buzzer_play_note(BUZZER_NOTE_E5, 500); + } else if (state->frequency == 44) { + watch_buzzer_play_note(BUZZER_NOTE_F5, 500); + } else if (state->frequency == 45) { + watch_buzzer_play_note(BUZZER_NOTE_F5SHARP_G5FLAT, 500); + } else if (state->frequency == 46) { + watch_buzzer_play_note(BUZZER_NOTE_G5, 500); + } else if (state->frequency == 47) { + watch_buzzer_play_note(BUZZER_NOTE_G5SHARP_A5FLAT, 500); + } else if (state->frequency == 48) { + watch_buzzer_play_note(BUZZER_NOTE_A5, 500); + } else if (state->frequency == 49) { + watch_buzzer_play_note(BUZZER_NOTE_A5SHARP_B5FLAT, 500); + } else if (state->frequency == 50) { + watch_buzzer_play_note(BUZZER_NOTE_B5, 500); + } else if (state->frequency == 51) { + watch_buzzer_play_note(BUZZER_NOTE_C6, 500); + } else if (state->frequency == 52) { + watch_buzzer_play_note(BUZZER_NOTE_C6SHARP_D6FLAT, 500); + } else if (state->frequency == 53) { + watch_buzzer_play_note(BUZZER_NOTE_D6, 500); + } else if (state->frequency == 54) { + watch_buzzer_play_note(BUZZER_NOTE_D6SHARP_E6FLAT, 500); + } else if (state->frequency == 55) { + watch_buzzer_play_note(BUZZER_NOTE_E6, 500); + } else if (state->frequency == 56) { + watch_buzzer_play_note(BUZZER_NOTE_F6, 500); + } else if (state->frequency == 57) { + watch_buzzer_play_note(BUZZER_NOTE_F6SHARP_G6FLAT, 500); + } else if (state->frequency == 58) { + watch_buzzer_play_note(BUZZER_NOTE_G6, 500); + } else if (state->frequency == 59) { + watch_buzzer_play_note(BUZZER_NOTE_G6SHARP_A6FLAT, 500); + } else if (state->frequency == 60) { + watch_buzzer_play_note(BUZZER_NOTE_A6, 500); + } else if (state->frequency == 61) { + watch_buzzer_play_note(BUZZER_NOTE_A6SHARP_B6FLAT, 500); + } else if (state->frequency == 62) { + watch_buzzer_play_note(BUZZER_NOTE_B6, 500); + } else if (state->frequency == 63) { + watch_buzzer_play_note(BUZZER_NOTE_C7, 500); + } else if (state->frequency == 64) { + watch_buzzer_play_note(BUZZER_NOTE_C7SHARP_D7FLAT, 500); + } else if (state->frequency == 65) { + watch_buzzer_play_note(BUZZER_NOTE_D7, 500); + } else if (state->frequency == 66) { + watch_buzzer_play_note(BUZZER_NOTE_D7SHARP_E7FLAT, 500); + } else if (state->frequency == 67) { + watch_buzzer_play_note(BUZZER_NOTE_E7, 500); + } else if (state->frequency == 68) { + watch_buzzer_play_note(BUZZER_NOTE_F7, 500); + } else if (state->frequency == 69) { + watch_buzzer_play_note(BUZZER_NOTE_F7SHARP_G7FLAT, 500); + } else if (state->frequency == 70) { + watch_buzzer_play_note(BUZZER_NOTE_G7, 500); + } else if (state->frequency == 71) { + watch_buzzer_play_note(BUZZER_NOTE_G7SHARP_A7FLAT, 500); + } else if (state->frequency == 72) { + watch_buzzer_play_note(BUZZER_NOTE_A7, 500); + } else if (state->frequency == 73) { + watch_buzzer_play_note(BUZZER_NOTE_A7SHARP_B7FLAT, 500); + } else if (state->frequency == 74) { + watch_buzzer_play_note(BUZZER_NOTE_B7, 500); + } else if (state->frequency == 75) { + watch_buzzer_play_note(BUZZER_NOTE_C8, 500); + } else if (state->frequency == 76) { + watch_buzzer_play_note(BUZZER_NOTE_C8SHARP_D8FLAT, 500); + } else if (state->frequency == 77) { + watch_buzzer_play_note(BUZZER_NOTE_D8, 500); + } else if (state->frequency == 78) { + watch_buzzer_play_note(BUZZER_NOTE_D8SHARP_E8FLAT, 500); + } else if (state->frequency == 79) { + watch_buzzer_play_note(BUZZER_NOTE_E8, 500); + } else if (state->frequency == 80) { + watch_buzzer_play_note(BUZZER_NOTE_F8, 500); + } else if (state->frequency == 81) { + watch_buzzer_play_note(BUZZER_NOTE_F8SHARP_G8FLAT, 500); + } else if (state->frequency == 82) { + watch_buzzer_play_note(BUZZER_NOTE_G8, 500); + } else if (state->frequency == 83) { + watch_buzzer_play_note(BUZZER_NOTE_G8SHARP_A8FLAT, 500); + } else if (state->frequency == 84) { + watch_buzzer_play_note(BUZZER_NOTE_A8, 500); + } else if (state->frequency == 85) { + watch_buzzer_play_note(BUZZER_NOTE_A8SHARP_B8FLAT, 500); + } else if (state->frequency == 86) { + watch_buzzer_play_note(BUZZER_NOTE_B8, 500); + } + break; + default: + return movement_default_loop_handler(event, settings); + } + return true; +} + +void beeps_face_resign(movement_settings_t *settings, void *context) { + (void) settings; + (void) context; +} + diff --git a/movement/watch_faces/demo/beeps_face.h b/movement/watch_faces/demo/beeps_face.h new file mode 100644 index 0000000..73606fd --- /dev/null +++ b/movement/watch_faces/demo/beeps_face.h @@ -0,0 +1,61 @@ +/* + * MIT License + * + * Copyright (c) 2024 Wesley + * + * 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 BEEPS_FACE_H_ +#define BEEPS_FACE_H_ + +#include "movement.h" + +/* + * A simple watch face to test the different Buzzer Notes. + * + * Press the Light button to play a sound. + * Press the Alarm button to change the frequency. + * + * The watch face displays the frequency of the buzzer it will play + * this allows you to reference the watch_buzzer.h file to find the + * corresponding note. + * + * The watch_buzzer.h file is found at watch-library/shared/watch/watch_buzzer.h + */ + +typedef struct { + uint8_t frequency; +} beeps_state_t; + +void beeps_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); +void beeps_face_activate(movement_settings_t *settings, void *context); +bool beeps_face_loop(movement_event_t event, movement_settings_t *settings, void *context); +void beeps_face_resign(movement_settings_t *settings, void *context); + +#define beeps_face ((const watch_face_t){ \ + beeps_face_setup, \ + beeps_face_activate, \ + beeps_face_loop, \ + beeps_face_resign, \ + NULL, \ +}) + +#endif // BEEPS_FACE_H_ + diff --git a/movement/watch_faces/demo/lis2dw_logging_face.c b/movement/watch_faces/demo/lis2dw_logging_face.c index 0a957bb..cbae54b 100644 --- a/movement/watch_faces/demo/lis2dw_logging_face.c +++ b/movement/watch_faces/demo/lis2dw_logging_face.c @@ -41,6 +41,7 @@ static void _lis2dw_logging_face_update_display(movement_settings_t *settings, l char time_indication_character; int8_t pos; watch_date_time date_time; + bool set_leading_zero = false; if (logger_state->log_ticks) { pos = (logger_state->data_points - 1 - logger_state->display_index) % LIS2DW_LOGGING_NUM_DATA_POINTS; @@ -50,12 +51,14 @@ static void _lis2dw_logging_face_update_display(movement_settings_t *settings, l } else { date_time = logger_state->data[pos].timestamp; watch_set_colon(); - if (settings->bit.clock_mode_24h) { - watch_set_indicator(WATCH_INDICATOR_24H); - } else { + if (!settings->bit.clock_mode_24h) { if (date_time.unit.hour > 11) watch_set_indicator(WATCH_INDICATOR_PM); date_time.unit.hour %= 12; if (date_time.unit.hour == 0) date_time.unit.hour = 12; + } else if (!settings->bit.clock_24h_leading_zero) { + watch_set_indicator(WATCH_INDICATOR_24H); + } else if (date_time.unit.hour < 10) { + set_leading_zero = true; } switch (logger_state->axis_index) { case 0: @@ -89,6 +92,8 @@ static void _lis2dw_logging_face_update_display(movement_settings_t *settings, l logger_state->interrupts[2]); } watch_display_string(buf, 0); + if (set_leading_zero) + watch_display_string("0", 4); } static void _lis2dw_logging_face_log_data(lis2dw_logger_state_t *logger_state) { diff --git a/movement/watch_faces/sensor/alarm_thermometer_face.c b/movement/watch_faces/sensor/alarm_thermometer_face.c new file mode 100644 index 0000000..3b7821e --- /dev/null +++ b/movement/watch_faces/sensor/alarm_thermometer_face.c @@ -0,0 +1,154 @@ +/* + * MIT License + * + * Copyright (c) 2024 Christian Buschau + * + * 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 +#include +#include +#include "alarm_thermometer_face.h" +#include "thermistor_driver.h" + +static float _alarm_thermometer_face_update(bool in_fahrenheit) { + thermistor_driver_enable(); + float temperature_c = thermistor_driver_get_temperature(); + char buf[14]; + if (in_fahrenheit) { + sprintf(buf, "%4.1f#F", temperature_c * 1.8 + 32.0); + } else { + sprintf(buf, "%4.1f#C", temperature_c); + } + watch_display_string(buf, 4); + thermistor_driver_disable(); + return temperature_c; +} + +static void _alarm_thermometer_face_clear(int last[]) { + for (size_t i = 0; i < LAST_SIZE; i++) { + last[i] = INT_MIN; + } +} + +void alarm_thermometer_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) { + (void) settings; + (void) watch_face_index; + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(alarm_thermometer_state_t)); + memset(*context_ptr, 0, sizeof(alarm_thermometer_state_t)); + } +} + +void alarm_thermometer_face_activate(movement_settings_t *settings, void *context) { + (void) settings; + alarm_thermometer_state_t *state = (alarm_thermometer_state_t *)context; + state->mode = MODE_NORMAL; + _alarm_thermometer_face_clear(state->last); + watch_display_string("AT", 0); +} + +bool alarm_thermometer_face_loop(movement_event_t event, movement_settings_t *settings, void *context) { + alarm_thermometer_state_t *state = (alarm_thermometer_state_t *)context; + + switch (event.event_type) { + case EVENT_ACTIVATE: + _alarm_thermometer_face_update(settings->bit.use_imperial_units); + break; + case EVENT_TICK: + if (watch_rtc_get_date_time().unit.second % 5 == 0) { + switch (state->mode) { + case MODE_NORMAL: + _alarm_thermometer_face_update(settings->bit.use_imperial_units); + break; + case MODE_ALARM: + for (size_t i = LAST_SIZE - 1; i > 0; i--) { + state->last[i] = state->last[i - 1]; + } + state->last[0] = roundf(_alarm_thermometer_face_update(settings->bit.use_imperial_units) * 10.0f); + bool constant = true; + for (size_t i = 1; i < LAST_SIZE; i++) { + if (state->last[i - 1] != state->last[i]) { + constant = false; + break; + } + } + if (constant) { + state->mode = MODE_FREEZE; + watch_set_indicator(WATCH_INDICATOR_SIGNAL); + movement_play_alarm(); + } + break; + case MODE_FREEZE: + break; + } + } + break; + case EVENT_ALARM_BUTTON_UP: + switch (state->mode) { + case MODE_NORMAL: + state->mode = MODE_ALARM; + watch_set_indicator(WATCH_INDICATOR_BELL); + _alarm_thermometer_face_clear(state->last); + break; + case MODE_FREEZE: + state->mode = MODE_NORMAL; + watch_clear_indicator(WATCH_INDICATOR_BELL); + watch_clear_indicator(WATCH_INDICATOR_SIGNAL); + break; + case MODE_ALARM: + state->mode = MODE_NORMAL; + watch_clear_indicator(WATCH_INDICATOR_BELL); + _alarm_thermometer_face_update(settings->bit.use_imperial_units); + break; + } + if (settings->bit.button_should_sound) { + watch_buzzer_play_note(BUZZER_NOTE_C7, 50); + } + break; + case EVENT_ALARM_LONG_PRESS: + if (state->mode != MODE_FREEZE) { + settings->bit.use_imperial_units = !settings->bit.use_imperial_units; + _alarm_thermometer_face_update(settings->bit.use_imperial_units); + } + break; + case EVENT_LOW_ENERGY_UPDATE: + if (!watch_tick_animation_is_running()) { + state->mode = MODE_NORMAL; + watch_clear_indicator(WATCH_INDICATOR_BELL); + watch_clear_indicator(WATCH_INDICATOR_SIGNAL); + watch_start_tick_animation(1000); + } + if (watch_rtc_get_date_time().unit.minute % 5 == 0) { + _alarm_thermometer_face_update(settings->bit.use_imperial_units); + watch_display_string(" ", 8); + } + break; + default: + return movement_default_loop_handler(event, settings); + } + + return true; +} + +void alarm_thermometer_face_resign(movement_settings_t *settings, void *context) { + (void) settings; + (void) context; +} diff --git a/movement/watch_faces/sensor/alarm_thermometer_face.h b/movement/watch_faces/sensor/alarm_thermometer_face.h new file mode 100644 index 0000000..1b3aabf --- /dev/null +++ b/movement/watch_faces/sensor/alarm_thermometer_face.h @@ -0,0 +1,74 @@ +/* + * MIT License + * + * Copyright (c) 2024 Christian Buschau + * + * 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 ALARM_THERMOMETER_FACE_H_ +#define ALARM_THERMOMETER_FACE_H_ + +#include +#include "movement.h" + +/* + * ALARM THERMOMETER + * + * This watch face shows the current temperature in degrees Celsius. Press and + * hold the alarm button to toggle between Celsius and Fahrenheit. Press and + * release the alarm button to start a "timer". The watch will sound an alarm + * when the temperature remains constant for at least 30 seconds and the + * temperature will stop updating until you press the alarm button. You can + * cancel the alarm by pressing the button again. If the temperature doesn't + * remain constant until the low energy timeout is reached, the alarm will stop. + * This is useful to measure e.g. the room temperature. If you lay off your + * watch from your wrist, it will take some time until it cools down, and will + * notify you when the measurement is constant enough. + * THIS WATCH FACE IS NOT INTENDED TO DIAGNOSE, TREAT, CURE OR PREVENT ANY + * DISEASE. + */ + +#define LAST_SIZE 6 + +typedef enum { + MODE_NORMAL, + MODE_ALARM, + MODE_FREEZE +} alarm_thermometer_mode_t; + +typedef struct { + int last[LAST_SIZE]; + alarm_thermometer_mode_t mode; +} alarm_thermometer_state_t; + +void alarm_thermometer_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); +void alarm_thermometer_face_activate(movement_settings_t *settings, void *context); +bool alarm_thermometer_face_loop(movement_event_t event, movement_settings_t *settings, void *context); +void alarm_thermometer_face_resign(movement_settings_t *settings, void *context); + +#define alarm_thermometer_face ((const watch_face_t){ \ + alarm_thermometer_face_setup, \ + alarm_thermometer_face_activate, \ + alarm_thermometer_face_loop, \ + alarm_thermometer_face_resign, \ + NULL, \ +}) + +#endif // ALARM_THERMOMETER_FACE_H_ diff --git a/movement/watch_faces/sensor/thermistor_logging_face.c b/movement/watch_faces/sensor/thermistor_logging_face.c index 64f605e..18625cc 100644 --- a/movement/watch_faces/sensor/thermistor_logging_face.c +++ b/movement/watch_faces/sensor/thermistor_logging_face.c @@ -40,9 +40,10 @@ static void _thermistor_logging_face_log_data(thermistor_logger_state_t *logger_ thermistor_driver_disable(); } -static void _thermistor_logging_face_update_display(thermistor_logger_state_t *logger_state, bool in_fahrenheit, bool clock_mode_24h) { +static void _thermistor_logging_face_update_display(thermistor_logger_state_t *logger_state, bool in_fahrenheit, bool clock_mode_24h, bool clock_24h_leading_zero) { int8_t pos = (logger_state->data_points - 1 - logger_state->display_index) % THERMISTOR_LOGGING_NUM_DATA_POINTS; char buf[14]; + bool set_leading_zero = false; watch_clear_indicator(WATCH_INDICATOR_24H); watch_clear_indicator(WATCH_INDICATOR_PM); @@ -53,12 +54,14 @@ static void _thermistor_logging_face_update_display(thermistor_logger_state_t *l } else if (logger_state->ts_ticks) { watch_date_time date_time = logger_state->data[pos].timestamp; watch_set_colon(); - if (clock_mode_24h) { - watch_set_indicator(WATCH_INDICATOR_24H); - } else { + if (!clock_mode_24h) { if (date_time.unit.hour > 11) watch_set_indicator(WATCH_INDICATOR_PM); date_time.unit.hour %= 12; if (date_time.unit.hour == 0) date_time.unit.hour = 12; + } else if (!clock_24h_leading_zero) { + watch_set_indicator(WATCH_INDICATOR_24H); + } else if (date_time.unit.hour < 10) { + set_leading_zero = true; } sprintf(buf, "AT%2d%2d%02d%02d", date_time.unit.day, date_time.unit.hour, date_time.unit.minute, date_time.unit.second); } else { @@ -70,6 +73,8 @@ static void _thermistor_logging_face_update_display(thermistor_logger_state_t *l } watch_display_string(buf, 0); + if (set_leading_zero) + watch_display_string("0", 4); } void thermistor_logging_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) { @@ -100,18 +105,18 @@ bool thermistor_logging_face_loop(movement_event_t event, movement_settings_t *s break; case EVENT_LIGHT_BUTTON_DOWN: logger_state->ts_ticks = 2; - _thermistor_logging_face_update_display(logger_state, settings->bit.use_imperial_units, settings->bit.clock_mode_24h); + _thermistor_logging_face_update_display(logger_state, settings->bit.use_imperial_units, settings->bit.clock_mode_24h, settings->bit.clock_24h_leading_zero); break; case EVENT_ALARM_BUTTON_DOWN: logger_state->display_index = (logger_state->display_index + 1) % THERMISTOR_LOGGING_NUM_DATA_POINTS; logger_state->ts_ticks = 0; // fall through case EVENT_ACTIVATE: - _thermistor_logging_face_update_display(logger_state, settings->bit.use_imperial_units, settings->bit.clock_mode_24h); + _thermistor_logging_face_update_display(logger_state, settings->bit.use_imperial_units, settings->bit.clock_mode_24h, settings->bit.clock_24h_leading_zero); break; case EVENT_TICK: if (logger_state->ts_ticks && --logger_state->ts_ticks == 0) { - _thermistor_logging_face_update_display(logger_state, settings->bit.use_imperial_units, settings->bit.clock_mode_24h); + _thermistor_logging_face_update_display(logger_state, settings->bit.use_imperial_units, settings->bit.clock_mode_24h, settings->bit.clock_24h_leading_zero); } break; case EVENT_BACKGROUND_TASK: diff --git a/movement/watch_faces/settings/preferences_face.c b/movement/watch_faces/settings/preferences_face.c index 2297977..24836dc 100644 --- a/movement/watch_faces/settings/preferences_face.c +++ b/movement/watch_faces/settings/preferences_face.c @@ -84,6 +84,9 @@ bool preferences_face_loop(movement_event_t event, movement_settings_t *settings break; case 4: settings->bit.led_duration = settings->bit.led_duration + 1; + if (settings->bit.led_duration > 3) { + settings->bit.led_duration = 0b111; + } break; case 5: settings->bit.led_green_color = settings->bit.led_green_color + 1; @@ -93,6 +96,14 @@ bool preferences_face_loop(movement_event_t event, movement_settings_t *settings break; } break; + case EVENT_ALARM_LONG_PRESS: + switch (current_page) { + case 0: + if (settings->bit.clock_mode_24h) + settings->bit.clock_24h_leading_zero = !(settings->bit.clock_24h_leading_zero); + break; + } + break; case EVENT_TIMEOUT: movement_move_to_face(0); break; @@ -109,8 +120,10 @@ bool preferences_face_loop(movement_event_t event, movement_settings_t *settings char buf[8]; switch (current_page) { case 0: - if (settings->bit.clock_mode_24h) watch_display_string("24h", 4); - else watch_display_string("12h", 4); + if (settings->bit.clock_mode_24h) { + if (settings->bit.clock_24h_leading_zero) watch_display_string("024h", 4); + else watch_display_string("24h", 4); + } else watch_display_string("12h", 4); break; case 1: if (settings->bit.button_should_sound) watch_display_string("y", 9); @@ -161,11 +174,13 @@ bool preferences_face_loop(movement_event_t event, movement_settings_t *settings } break; case 4: - if (settings->bit.led_duration) { + if (settings->bit.led_duration == 0) { + watch_display_string("instnt", 4); + } else if (settings->bit.led_duration == 0b111) { + watch_display_string("no LEd", 4); + } else { sprintf(buf, " %1d SeC", settings->bit.led_duration * 2 - 1); watch_display_string(buf, 4); - } else { - watch_display_string("no LEd", 4); } break; case 5: diff --git a/movement/watch_faces/settings/set_time_face.c b/movement/watch_faces/settings/set_time_face.c index 503dffc..605849f 100644 --- a/movement/watch_faces/settings/set_time_face.c +++ b/movement/watch_faces/settings/set_time_face.c @@ -126,10 +126,14 @@ bool set_time_face_loop(movement_event_t event, movement_settings_t *settings, v } char buf[11]; + bool set_leading_zero = false; if (current_page < 3) { watch_set_colon(); if (settings->bit.clock_mode_24h) { - watch_set_indicator(WATCH_INDICATOR_24H); + if (!settings->bit.clock_24h_leading_zero) + watch_set_indicator(WATCH_INDICATOR_24H); + else if (date_time.unit.hour < 10) + set_leading_zero = true; sprintf(buf, "%s %2d%02d%02d", set_time_face_titles[current_page], date_time.unit.hour, date_time.unit.minute, date_time.unit.second); } else { sprintf(buf, "%s %2d%02d%02d", set_time_face_titles[current_page], (date_time.unit.hour % 12) ? (date_time.unit.hour % 12) : 12, date_time.unit.minute, date_time.unit.second); @@ -170,6 +174,8 @@ bool set_time_face_loop(movement_event_t event, movement_settings_t *settings, v } watch_display_string(buf, 0); + if (set_leading_zero) + watch_display_string("0", 4); return true; } diff --git a/movement/watch_faces/settings/set_time_hackwatch_face.c b/movement/watch_faces/settings/set_time_hackwatch_face.c index 2f4c034..75bc7bb 100644 --- a/movement/watch_faces/settings/set_time_hackwatch_face.c +++ b/movement/watch_faces/settings/set_time_hackwatch_face.c @@ -189,10 +189,14 @@ bool set_time_hackwatch_face_loop(movement_event_t event, movement_settings_t *s } char buf[11]; + bool set_leading_zero = false; if (current_page < 3) { watch_set_colon(); if (settings->bit.clock_mode_24h) { - watch_set_indicator(WATCH_INDICATOR_24H); + if (!settings->bit.clock_24h_leading_zero) + watch_set_indicator(WATCH_INDICATOR_24H); + else if (date_time_settings.unit.hour < 10) + set_leading_zero = true; sprintf(buf, "%s %2d%02d%02d", set_time_hackwatch_face_titles[current_page], @@ -258,6 +262,8 @@ bool set_time_hackwatch_face_loop(movement_event_t event, movement_settings_t *s } watch_display_string(buf, 0); + if (set_leading_zero) + watch_display_string("0", 4); return true; } diff --git a/utils/wordle_face/wordle_list.py b/utils/wordle_face/wordle_list.py new file mode 100644 index 0000000..dac5ad6 --- /dev/null +++ b/utils/wordle_face/wordle_list.py @@ -0,0 +1,1385 @@ +import random, itertools, time, numpy + +source_link = "https://matthewminer.name/projects/calculators/wordle-words-left/" +valid_list = [ + "CIGAR", "REBUT", "SISSY", "HUMPH", "AWAKE", "BLUSH", "FOCAL", "EVADE", "NAVAL", "SERVE", "HEATH", "DWARF", + "MODEL", "KARMA", "STINK", "GRADE", "QUIET", "BENCH", "ABATE", "FEIGN", "MAJOR", "DEATH", "FRESH", "CRUST", + "STOOL", "COLON", "ABASE", "MARRY", "REACT", "BATTY", "PRIDE", "FLOSS", "HELIX", "CROAK", "STAFF", "PAPER", + "UNFED", "WHELP", "TRAWL", "OUTDO", "ADOBE", "CRAZY", "SOWER", "REPAY", "DIGIT", "CRATE", "CLUCK", "SPIKE", + "MIMIC", "POUND", "MAXIM", "LINEN", "UNMET", "FLESH", "BOOBY", "FORTH", "FIRST", "STAND", "BELLY", "IVORY", + "SEEDY", "PRINT", "YEARN", "DRAIN", "BRIBE", "STOUT", "PANEL", "CRASS", "FLUME", "OFFAL", "AGREE", "ERROR", + "SWIRL", "ARGUE", "BLEED", "DELTA", "FLICK", "TOTEM", "WOOER", "FRONT", "SHRUB", "PARRY", "BIOME", "LAPEL", + "START", "GREET", "GONER", "GOLEM", "LUSTY", "LOOPY", "ROUND", "AUDIT", "LYING", "GAMMA", "LABOR", "ISLET", + "CIVIC", "FORGE", "CORNY", "MOULT", "BASIC", "SALAD", "AGATE", "SPICY", "SPRAY", "ESSAY", "FJORD", "SPEND", + "KEBAB", "GUILD", "ABACK", "MOTOR", "ALONE", "HATCH", "HYPER", "THUMB", "DOWRY", "OUGHT", "BELCH", "DUTCH", + "PILOT", "TWEED", "COMET", "JAUNT", "ENEMA", "STEED", "ABYSS", "GROWL", "FLING", "DOZEN", "BOOZY", "ERODE", + "WORLD", "GOUGE", "CLICK", "BRIAR", "GREAT", "ALTAR", "PULPY", "BLURT", "COAST", "DUCHY", "GROIN", "FIXER", + "GROUP", "ROGUE", "BADLY", "SMART", "PITHY", "GAUDY", "CHILL", "HERON", "VODKA", "FINER", "SURER", "RADIO", + "ROUGE", "PERCH", "RETCH", "WROTE", "CLOCK", "TILDE", "STORE", "PROVE", "BRING", "SOLVE", "CHEAT", "GRIME", + "EXULT", "USHER", "EPOCH", "TRIAD", "BREAK", "RHINO", "VIRAL", "CONIC", "MASSE", "SONIC", "VITAL", "TRACE", + "USING", "PEACH", "CHAMP", "BATON", "BRAKE", "PLUCK", "CRAZE", "GRIPE", "WEARY", "PICKY", "ACUTE", "FERRY", + "ASIDE", "TAPIR", "TROLL", "UNIFY", "REBUS", "BOOST", "TRUSS", "SIEGE", "TIGER", "BANAL", "SLUMP", "CRANK", + "GORGE", "QUERY", "DRINK", "FAVOR", "ABBEY", "TANGY", "PANIC", "SOLAR", "SHIRE", "PROXY", "POINT", "ROBOT", + "PRICK", "WINCE", "CRIMP", "KNOLL", "SUGAR", "WHACK", "MOUNT", "PERKY", "COULD", "WRUNG", "LIGHT", "THOSE", + "MOIST", "SHARD", "PLEAT", "ALOFT", "SKILL", "ELDER", "FRAME", "HUMOR", "PAUSE", "ULCER", "ULTRA", "ROBIN", + "CYNIC", "AROMA", "CAULK", "SHAKE", "DODGE", "SWILL", "TACIT", "OTHER", "THORN", "TROVE", "BLOKE", "VIVID", + "SPILL", "CHANT", "CHOKE", "RUPEE", "NASTY", "MOURN", "AHEAD", "BRINE", "CLOTH", "HOARD", "SWEET", "MONTH", + "LAPSE", "WATCH", "TODAY", "FOCUS", "SMELT", "TEASE", "CATER", "MOVIE", "SAUTE", "ALLOW", "RENEW", "THEIR", + "SLOSH", "PURGE", "CHEST", "DEPOT", "EPOXY", "NYMPH", "FOUND", "SHALL", "STOVE", "LOWLY", "SNOUT", "TROPE", + "FEWER", "SHAWL", "NATAL", "COMMA", "FORAY", "SCARE", "STAIR", "BLACK", "SQUAD", "ROYAL", "CHUNK", "MINCE", + "SHAME", "CHEEK", "AMPLE", "FLAIR", "FOYER", "CARGO", "OXIDE", "PLANT", "OLIVE", "INERT", "ASKEW", "HEIST", + "SHOWN", "ZESTY", "TRASH", "LARVA", "FORGO", "STORY", "HAIRY", "TRAIN", "HOMER", "BADGE", "MIDST", "CANNY", + "SHINE", "GECKO", "FARCE", "SLUNG", "TIPSY", "METAL", "YIELD", "DELVE", "BEING", "SCOUR", "GLASS", "GAMER", + "SCRAP", "MONEY", "HINGE", "ALBUM", "VOUCH", "ASSET", "TIARA", "CREPT", "BAYOU", "ATOLL", "MANOR", "CREAK", + "SHOWY", "PHASE", "FROTH", "DEPTH", "GLOOM", "FLOOD", "TRAIT", "GIRTH", "PIETY", "GOOSE", "FLOAT", "DONOR", + "ATONE", "PRIMO", "APRON", "BLOWN", "CACAO", "LOSER", "INPUT", "GLOAT", "AWFUL", "BRINK", "SMITE", "BEADY", + "RUSTY", "RETRO", "DROLL", "GAWKY", "HUTCH", "PINTO", "EGRET", "LILAC", "SEVER", "FIELD", "FLUFF", "AGAPE", + "VOICE", "STEAD", "BERTH", "MADAM", "NIGHT", "BLAND", "LIVER", "WEDGE", "ROOMY", "WACKY", "FLOCK", "ANGRY", + "TRITE", "APHID", "TRYST", "MIDGE", "POWER", "ELOPE", "CINCH", "MOTTO", "STOMP", "UPSET", "BLUFF", "CRAMP", + "QUART", "COYLY", "YOUTH", "RHYME", "BUGGY", "ALIEN", "SMEAR", "UNFIT", "PATTY", "CLING", "GLEAN", "LABEL", + "HUNKY", "KHAKI", "POKER", "GRUEL", "TWICE", "TWANG", "SHRUG", "TREAT", "WASTE", "MERIT", "WOVEN", "NEEDY", + "CLOWN", "IRONY", "RUDER", "GAUZE", "CHIEF", "ONSET", "PRIZE", "FUNGI", "CHARM", "GULLY", "INTER", "WHOOP", + "TAUNT", "LEERY", "CLASS", "THEME", "LOFTY", "TIBIA", "BOOZE", "ALPHA", "THYME", "DOUBT", "PARER", "CHUTE", + "STICK", "TRICE", "ALIKE", "RECAP", "SAINT", "GLORY", "GRATE", "ADMIT", "BRISK", "SOGGY", "USURP", "SCALD", + "SCORN", "LEAVE", "TWINE", "STING", "BOUGH", "MARSH", "SLOTH", "DANDY", "VIGOR", "HOWDY", "ENJOY", "VALID", + "IONIC", "EQUAL", "FLOOR", "CATCH", "SPADE", "STEIN", "EXIST", "QUIRK", "DENIM", "GROVE", "SPIEL", "MUMMY", + "FAULT", "FOGGY", "FLOUT", "CARRY", "SNEAK", "LIBEL", "WALTZ", "APTLY", "PINEY", "INEPT", "ALOUD", "PHOTO", + "DREAM", "STALE", "UNITE", "SNARL", "BAKER", "THERE", "GLYPH", "POOCH", "HIPPY", "SPELL", "FOLLY", "LOUSE", + "GULCH", "VAULT", "GODLY", "THREW", "FLEET", "GRAVE", "INANE", "SHOCK", "CRAVE", "SPITE", "VALVE", "SKIMP", + "CLAIM", "RAINY", "MUSTY", "PIQUE", "DADDY", "QUASI", "ARISE", "AGING", "VALET", "OPIUM", "AVERT", "STUCK", + "RECUT", "MULCH", "GENRE", "PLUME", "RIFLE", "COUNT", "INCUR", "TOTAL", "WREST", "MOCHA", "DETER", "STUDY", + "LOVER", "SAFER", "RIVET", "FUNNY", "SMOKE", "MOUND", "UNDUE", "SEDAN", "PAGAN", "SWINE", "GUILE", "GUSTY", + "EQUIP", "TOUGH", "CANOE", "CHAOS", "COVET", "HUMAN", "UDDER", "LUNCH", "BLAST", "STRAY", "MANGA", "MELEE", + "LEFTY", "QUICK", "PASTE", "GIVEN", "OCTET", "RISEN", "GROAN", "LEAKY", "GRIND", "CARVE", "LOOSE", "SADLY", + "SPILT", "APPLE", "SLACK", "HONEY", "FINAL", "SHEEN", "EERIE", "MINTY", "SLICK", "DERBY", "WHARF", "SPELT", + "COACH", "ERUPT", "SINGE", "PRICE", "SPAWN", "FAIRY", "JIFFY", "FILMY", "STACK", "CHOSE", "SLEEP", "ARDOR", + "NANNY", "NIECE", "WOOZY", "HANDY", "GRACE", "DITTO", "STANK", "CREAM", "USUAL", "DIODE", "VALOR", "ANGLE", + "NINJA", "MUDDY", "CHASE", "REPLY", "PRONE", "SPOIL", "HEART", "SHADE", "DINER", "ARSON", "ONION", "SLEET", + "DOWEL", "COUCH", "PALSY", "BOWEL", "SMILE", "EVOKE", "CREEK", "LANCE", "EAGLE", "IDIOT", "SIREN", "BUILT", + "EMBED", "AWARD", "DROSS", "ANNUL", "GOODY", "FROWN", "PATIO", "LADEN", "HUMID", "ELITE", "LYMPH", "EDIFY", + "MIGHT", "RESET", "VISIT", "GUSTO", "PURSE", "VAPOR", "CROCK", "WRITE", "SUNNY", "LOATH", "CHAFF", "SLIDE", + "QUEER", "VENOM", "STAMP", "SORRY", "STILL", "ACORN", "APING", "PUSHY", "TAMER", "HATER", "MANIA", "AWOKE", + "BRAWN", "SWIFT", "EXILE", "BIRCH", "LUCKY", "FREER", "RISKY", "GHOST", "PLIER", "LUNAR", "WINCH", "SNARE", + "NURSE", "HOUSE", "BORAX", "NICER", "LURCH", "EXALT", "ABOUT", "SAVVY", "TOXIN", "TUNIC", "PRIED", "INLAY", + "CHUMP", "LANKY", "CRESS", "EATER", "ELUDE", "CYCLE", "KITTY", "BOULE", "MORON", "TENET", "PLACE", "LOBBY", + "PLUSH", "VIGIL", "INDEX", "BLINK", "CLUNG", "QUALM", "CROUP", "CLINK", "JUICY", "STAGE", "DECAY", "NERVE", + "FLIER", "SHAFT", "CROOK", "CLEAN", "CHINA", "RIDGE", "VOWEL", "GNOME", "SNUCK", "ICING", "SPINY", "RIGOR", + "SNAIL", "FLOWN", "RABID", "PROSE", "THANK", "POPPY", "BUDGE", "FIBER", "MOLDY", "DOWDY", "KNEEL", "TRACK", + "CADDY", "QUELL", "DUMPY", "PALER", "SWORE", "REBAR", "SCUBA", "SPLAT", "FLYER", "HORNY", "MASON", "DOING", + "OZONE", "AMPLY", "MOLAR", "OVARY", "BESET", "QUEUE", "CLIFF", "MAGIC", "TRUCE", "SPORT", "FRITZ", "EDICT", + "TWIRL", "VERSE", "LLAMA", "EATEN", "RANGE", "WHISK", "HOVEL", "REHAB", "MACAW", "SIGMA", "SPOUT", "VERVE", + "SUSHI", "DYING", "FETID", "BRAIN", "BUDDY", "THUMP", "SCION", "CANDY", "CHORD", "BASIN", "MARCH", "CROWD", + "ARBOR", "GAYLY", "MUSKY", "STAIN", "DALLY", "BLESS", "BRAVO", "STUNG", "TITLE", "RULER", "KIOSK", "BLOND", + "ENNUI", "LAYER", "FLUID", "TATTY", "SCORE", "CUTIE", "ZEBRA", "BARGE", "MATEY", "BLUER", "AIDER", "SHOOK", + "RIVER", "PRIVY", "BETEL", "FRISK", "BONGO", "BEGUN", "AZURE", "WEAVE", "GENIE", "SOUND", "GLOVE", "BRAID", + "SCOPE", "WRYLY", "ROVER", "ASSAY", "OCEAN", "BLOOM", "IRATE", "LATER", "WOKEN", "SILKY", "WRECK", "DWELT", + "SLATE", "SMACK", "SOLID", "AMAZE", "HAZEL", "WRIST", "JOLLY", "GLOBE", "FLINT", "ROUSE", "CIVIL", "VISTA", + "RELAX", "COVER", "ALIVE", "BEECH", "JETTY", "BLISS", "VOCAL", "OFTEN", "DOLLY", "EIGHT", "JOKER", "SINCE", + "EVENT", "ENSUE", "SHUNT", "DIVER", "POSER", "WORST", "SWEEP", "ALLEY", "CREED", "ANIME", "LEAFY", "BOSOM", + "DUNCE", "STARE", "PUDGY", "WAIVE", "CHOIR", "STOOD", "SPOKE", "OUTGO", "DELAY", "BILGE", "IDEAL", "CLASP", + "SEIZE", "HOTLY", "LAUGH", "SIEVE", "BLOCK", "MEANT", "GRAPE", "NOOSE", "HARDY", "SHIED", "DRAWL", "DAISY", + "PUTTY", "STRUT", "BURNT", "TULIP", "CRICK", "IDYLL", "VIXEN", "FUROR", "GEEKY", "COUGH", "NAIVE", "SHOAL", + "STORK", "BATHE", "AUNTY", "CHECK", "PRIME", "BRASS", "OUTER", "FURRY", "RAZOR", "ELECT", "EVICT", "IMPLY", + "DEMUR", "QUOTA", "HAVEN", "CAVIL", "SWEAR", "CRUMP", "DOUGH", "GAVEL", "WAGON", "SALON", "NUDGE", "HAREM", + "PITCH", "SWORN", "PUPIL", "EXCEL", "STONY", "CABIN", "UNZIP", "QUEEN", "TROUT", "POLYP", "EARTH", "STORM", + "UNTIL", "TAPER", "ENTER", "CHILD", "ADOPT", "MINOR", "FATTY", "HUSKY", "BRAVE", "FILET", "SLIME", "GLINT", + "TREAD", "STEAL", "REGAL", "GUEST", "EVERY", "MURKY", "SHARE", "SPORE", "HOIST", "BUXOM", "INNER", "OTTER", + "DIMLY", "LEVEL", "SUMAC", "DONUT", "STILT", "ARENA", "SHEET", "SCRUB", "FANCY", "SLIMY", "PEARL", "SILLY", + "PORCH", "DINGO", "SEPIA", "AMBLE", "SHADY", "BREAD", "FRIAR", "REIGN", "DAIRY", "QUILL", "CROSS", "BROOD", + "TUBER", "SHEAR", "POSIT", "BLANK", "VILLA", "SHANK", "PIGGY", "FREAK", "WHICH", "AMONG", "FECAL", "SHELL", + "WOULD", "ALGAE", "LARGE", "RABBI", "AGONY", "AMUSE", "BUSHY", "COPSE", "SWOON", "KNIFE", "POUCH", "ASCOT", + "PLANE", "CROWN", "URBAN", "SNIDE", "RELAY", "ABIDE", "VIOLA", "RAJAH", "STRAW", "DILLY", "CRASH", "AMASS", + "THIRD", "TRICK", "TUTOR", "WOODY", "BLURB", "GRIEF", "DISCO", "WHERE", "SASSY", "BEACH", "SAUNA", "COMIC", + "CLUED", "CREEP", "CASTE", "GRAZE", "SNUFF", "FROCK", "GONAD", "DRUNK", "PRONG", "LURID", "STEEL", "HALVE", + "BUYER", "VINYL", "UTILE", "SMELL", "ADAGE", "WORRY", "TASTY", "LOCAL", "TRADE", "FINCH", "ASHEN", "MODAL", + "GAUNT", "CLOVE", "ENACT", "ADORN", "ROAST", "SPECK", "SHEIK", "MISSY", "GRUNT", "SNOOP", "PARTY", "TOUCH", + "MAFIA", "EMCEE", "ARRAY", "SOUTH", "VAPID", "JELLY", "SKULK", "ANGST", "TUBAL", "LOWER", "CREST", "SWEAT", + "CYBER", "ADORE", "TARDY", "SWAMI", "NOTCH", "GROOM", "ROACH", "HITCH", "YOUNG", "ALIGN", "READY", "FROND", + "STRAP", "PUREE", "REALM", "VENUE", "SWARM", "OFFER", "SEVEN", "DRYER", "DIARY", "DRYLY", "DRANK", "ACRID", + "HEADY", "THETA", "JUNTO", "PIXIE", "QUOTH", "BONUS", "SHALT", "PENNE", "AMEND", "DATUM", "BUILD", "PIANO", + "SHELF", "LODGE", "SUING", "REARM", "CORAL", "RAMEN", "WORTH", "PSALM", "INFER", "OVERT", "MAYOR", "OVOID", + "GLIDE", "USAGE", "POISE", "RANDY", "CHUCK", "PRANK", "FISHY", "TOOTH", "ETHER", "DROVE", "IDLER", "SWATH", + "STINT", "WHILE", "BEGAT", "APPLY", "SLANG", "TAROT", "RADAR", "CREDO", "AWARE", "CANON", "SHIFT", "TIMER", + "BYLAW", "SERUM", "THREE", "STEAK", "ILIAC", "SHIRK", "BLUNT", "PUPPY", "PENAL", "JOIST", "BUNNY", "SHAPE", + "BEGET", "WHEEL", "ADEPT", "STUNT", "STOLE", "TOPAZ", "CHORE", "FLUKE", "AFOOT", "BLOAT", "BULLY", "DENSE", + "CAPER", "SNEER", "BOXER", "JUMBO", "LUNGE", "SPACE", "AVAIL", "SHORT", "SLURP", "LOYAL", "FLIRT", "PIZZA", + "CONCH", "TEMPO", "DROOP", "PLATE", "BIBLE", "PLUNK", "AFOUL", "SAVOY", "STEEP", "AGILE", "STAKE", "DWELL", + "KNAVE", "BEARD", "AROSE", "MOTIF", "SMASH", "BROIL", "GLARE", "SHOVE", "BAGGY", "MAMMY", "SWAMP", "ALONG", + "RUGBY", "WAGER", "QUACK", "SQUAT", "SNAKY", "DEBIT", "MANGE", "SKATE", "NINTH", "JOUST", "TRAMP", "SPURN", + "MEDAL", "MICRO", "REBEL", "FLANK", "LEARN", "NADIR", "MAPLE", "COMFY", "REMIT", "GRUFF", "ESTER", "LEAST", + "MOGUL", "FETCH", "CAUSE", "OAKEN", "AGLOW", "MEATY", "GAFFE", "SHYLY", "RACER", "PROWL", "THIEF", "STERN", + "POESY", "ROCKY", "TWEET", "WAIST", "SPIRE", "GROPE", "HAVOC", "PATSY", "TRULY", "FORTY", "DEITY", "UNCLE", + "SWISH", "GIVER", "PREEN", "BEVEL", "LEMUR", "DRAFT", "SLOPE", "ANNOY", "LINGO", "BLEAK", "DITTY", "CURLY", + "CEDAR", "DIRGE", "GROWN", "HORDE", "DROOL", "SHUCK", "CRYPT", "CUMIN", "STOCK", "GRAVY", "LOCUS", "WIDER", + "BREED", "QUITE", "CHAFE", "CACHE", "BLIMP", "DEIGN", "FIEND", "LOGIC", "CHEAP", "ELIDE", "RIGID", "FALSE", + "RENAL", "PENCE", "ROWDY", "SHOOT", "BLAZE", "ENVOY", "POSSE", "BRIEF", "NEVER", "ABORT", "MOUSE", "MUCKY", + "SULKY", "FIERY", "MEDIA", "TRUNK", "YEAST", "CLEAR", "SKUNK", "SCALP", "BITTY", "CIDER", "KOALA", "DUVET", + "SEGUE", "CREME", "SUPER", "GRILL", "AFTER", "OWNER", "EMBER", "REACH", "NOBLY", "EMPTY", "SPEED", "GIPSY", + "RECUR", "SMOCK", "DREAD", "MERGE", "BURST", "KAPPA", "AMITY", "SHAKY", "HOVER", "CAROL", "SNORT", "SYNOD", + "FAINT", "HAUNT", "FLOUR", "CHAIR", "DETOX", "SHREW", "TENSE", "PLIED", "QUARK", "BURLY", "NOVEL", "WAXEN", + "STOIC", "JERKY", "BLITZ", "BEEFY", "LYRIC", "HUSSY", "TOWEL", "QUILT", "BELOW", "BINGO", "WISPY", "BRASH", + "SCONE", "TOAST", "EASEL", "SAUCY", "VALUE", "SPICE", "HONOR", "ROUTE", "SHARP", "BAWDY", "RADII", "SKULL", + "PHONY", "ISSUE", "LAGER", "SWELL", "URINE", "GASSY", "TRIAL", "FLORA", "UPPER", "LATCH", "WIGHT", "BRICK", + "RETRY", "HOLLY", "DECAL", "GRASS", "SHACK", "DOGMA", "MOVER", "DEFER", "SOBER", "OPTIC", "CRIER", "VYING", + "NOMAD", "FLUTE", "HIPPO", "SHARK", "DRIER", "OBESE", "BUGLE", "TAWNY", "CHALK", "FEAST", "RUDDY", "PEDAL", + "SCARF", "CRUEL", "BLEAT", "TIDAL", "SLUSH", "SEMEN", "WINDY", "DUSTY", "SALLY", "IGLOO", "NERDY", "JEWEL", + "SHONE", "WHALE", "HYMEN", "ABUSE", "FUGUE", "ELBOW", "CRUMB", "PANSY", "WELSH", "SYRUP", "TERSE", "SUAVE", + "GAMUT", "SWUNG", "DRAKE", "FREED", "AFIRE", "SHIRT", "GROUT", "ODDLY", "TITHE", "PLAID", "DUMMY", "BROOM", + "BLIND", "TORCH", "ENEMY", "AGAIN", "TYING", "PESKY", "ALTER", "GAZER", "NOBLE", "ETHOS", "BRIDE", "EXTOL", + "DECOR", "HOBBY", "BEAST", "IDIOM", "UTTER", "THESE", "SIXTH", "ALARM", "ERASE", "ELEGY", "SPUNK", "PIPER", + "SCALY", "SCOLD", "HEFTY", "CHICK", "SOOTY", "CANAL", "WHINY", "SLASH", "QUAKE", "JOINT", "SWEPT", "PRUDE", + "HEAVY", "WIELD", "FEMME", "LASSO", "MAIZE", "SHALE", "SCREW", "SPREE", "SMOKY", "WHIFF", "SCENT", "GLADE", + "SPENT", "PRISM", "STOKE", "RIPER", "ORBIT", "COCOA", "GUILT", "HUMUS", "SHUSH", "TABLE", "SMIRK", "WRONG", + "NOISY", "ALERT", "SHINY", "ELATE", "RESIN", "WHOLE", "HUNCH", "PIXEL", "POLAR", "HOTEL", "SWORD", "CLEAT", + "MANGO", "RUMBA", "PUFFY", "FILLY", "BILLY", "LEASH", "CLOUT", "DANCE", "OVATE", "FACET", "CHILI", "PAINT", + "LINER", "CURIO", "SALTY", "AUDIO", "SNAKE", "FABLE", "CLOAK", "NAVEL", "SPURT", "PESTO", "BALMY", "FLASH", + "UNWED", "EARLY", "CHURN", "WEEDY", "STUMP", "LEASE", "WITTY", "WIMPY", "SPOOF", "SANER", "BLEND", "SALSA", + "THICK", "WARTY", "MANIC", "BLARE", "SQUIB", "SPOON", "PROBE", "CREPE", "KNACK", "FORCE", "DEBUT", "ORDER", + "HASTE", "TEETH", "AGENT", "WIDEN", "ICILY", "SLICE", "INGOT", "CLASH", "JUROR", "BLOOD", "ABODE", "THROW", + "UNITY", "PIVOT", "SLEPT", "TROOP", "SPARE", "SEWER", "PARSE", "MORPH", "CACTI", "TACKY", "SPOOL", "DEMON", + "MOODY", "ANNEX", "BEGIN", "FUZZY", "PATCH", "WATER", "LUMPY", "ADMIN", "OMEGA", "LIMIT", "TABBY", "MACHO", + "AISLE", "SKIFF", "BASIS", "PLANK", "VERGE", "BOTCH", "CRAWL", "LOUSY", "SLAIN", "CUBIC", "RAISE", "WRACK", + "GUIDE", "FOIST", "CAMEO", "UNDER", "ACTOR", "REVUE", "FRAUD", "HARPY", "SCOOP", "CLIMB", "REFER", "OLDEN", + "CLERK", "DEBAR", "TALLY", "ETHIC", "CAIRN", "TULLE", "GHOUL", "HILLY", "CRUDE", "APART", "SCALE", "OLDER", + "PLAIN", "SPERM", "BRINY", "ABBOT", "RERUN", "QUEST", "CRISP", "BOUND", "BEFIT", "DRAWN", "SUITE", "ITCHY", + "CHEER", "BAGEL", "GUESS", "BROAD", "AXIOM", "CHARD", "CAPUT", "LEANT", "HARSH", "CURSE", "PROUD", "SWING", + "OPINE", "TASTE", "LUPUS", "GUMBO", "MINER", "GREEN", "CHASM", "LIPID", "TOPIC", "ARMOR", "BRUSH", "CRANE", + "MURAL", "ABLED", "HABIT", "BOSSY", "MAKER", "DUSKY", "DIZZY", "LITHE", "BROOK", "JAZZY", "FIFTY", "SENSE", + "GIANT", "SURLY", "LEGAL", "FATAL", "FLUNK", "BEGAN", "PRUNE", "SMALL", "SLANT", "SCOFF", "TORUS", "NINNY", + "COVEY", "VIPER", "TAKEN", "MORAL", "VOGUE", "OWING", "TOKEN", "ENTRY", "BOOTH", "VOTER", "CHIDE", "ELFIN", + "EBONY", "NEIGH", "MINIM", "MELON", "KNEED", "DECOY", "VOILA", "ANKLE", "ARROW", "MUSHY", "TRIBE", "CEASE", + "EAGER", "BIRTH", "GRAPH", "ODDER", "TERRA", "WEIRD", "TRIED", "CLACK", "COLOR", "ROUGH", "WEIGH", "UNCUT", + "LADLE", "STRIP", "CRAFT", "MINUS", "DICEY", "TITAN", "LUCID", "VICAR", "DRESS", "DITCH", "GYPSY", "PASTA", + "TAFFY", "FLAME", "SWOOP", "ALOOF", "SIGHT", "BROKE", "TEARY", "CHART", "SIXTY", "WORDY", "SHEER", "LEPER", + "NOSEY", "BULGE", "SAVOR", "CLAMP", "FUNKY", "FOAMY", "TOXIC", "BRAND", "PLUMB", "DINGY", "BUTTE", "DRILL", + "TRIPE", "BICEP", "TENOR", "KRILL", "WORSE", "DRAMA", "HYENA", "THINK", "RATIO", "COBRA", "BASIL", "SCRUM", + "BUSED", "PHONE", "COURT", "CAMEL", "PROOF", "HEARD", "ANGEL", "PETAL", "POUTY", "THROB", "MAYBE", "FETAL", + "SPRIG", "SPINE", "SHOUT", "CADET", "MACRO", "DODGY", "SATYR", "RARER", "BINGE", "TREND", "NUTTY", "LEAPT", + "AMISS", "SPLIT", "MYRRH", "WIDTH", "SONAR", "TOWER", "BARON", "FEVER", "WAVER", "SPARK", "BELIE", "SLOOP", + "EXPEL", "SMOTE", "BALER", "ABOVE", "NORTH", "WAFER", "SCANT", "FRILL", "AWASH", "SNACK", "SCOWL", "FRAIL", + "DRIFT", "LIMBO", "FENCE", "MOTEL", "OUNCE", "WREAK", "REVEL", "TALON", "PRIOR", "KNELT", "CELLO", "FLAKE", + "DEBUG", "ANODE", "CRIME", "SALVE", "SCOUT", "IMBUE", "PINKY", "STAVE", "VAGUE", "CHOCK", "FIGHT", "VIDEO", + "STONE", "TEACH", "CLEFT", "FROST", "PRAWN", "BOOTY", "TWIST", "APNEA", "STIFF", "PLAZA", "LEDGE", "TWEAK", + "BOARD", "GRANT", "MEDIC", "BACON", "CABLE", "BRAWL", "SLUNK", "RASPY", "FORUM", "DRONE", "WOMEN", "MUCUS", + "BOAST", "TODDY", "COVEN", "TUMOR", "TRUER", "WRATH", "STALL", "STEAM", "AXIAL", "PURER", "DAILY", "TRAIL", + "NICHE", "MEALY", "JUICE", "NYLON", "PLUMP", "MERRY", "FLAIL", "PAPAL", "WHEAT", "BERRY", "COWER", "ERECT", + "BRUTE", "LEGGY", "SNIPE", "SINEW", "SKIER", "PENNY", "JUMPY", "RALLY", "UMBRA", "SCARY", "MODEM", "GROSS", + "AVIAN", "GREED", "SATIN", "TONIC", "PARKA", "SNIFF", "LIVID", "STARK", "TRUMP", "GIDDY", "REUSE", "TABOO", + "AVOID", "QUOTE", "DEVIL", "LIKEN", "GLOSS", "GAYER", "BERET", "NOISE", "GLAND", "DEALT", "SLING", "RUMOR", + "OPERA", "THIGH", "TONGA", "FLARE", "WOUND", "WHITE", "BULKY", "ETUDE", "HORSE", "CIRCA", "PADDY", "INBOX", + "FIZZY", "GRAIN", "EXERT", "SURGE", "GLEAM", "BELLE", "SALVO", "CRUSH", "FRUIT", "SAPPY", "TAKER", "TRACT", + "OVINE", "SPIKY", "FRANK", "REEDY", "FILTH", "SPASM", "HEAVE", "MAMBO", "RIGHT", "CLANK", "TRUST", "LUMEN", + "BORNE", "SPOOK", "SAUCE", "AMBER", "LATHE", "CARAT", "CORER", "DIRTY", "SLYLY", "AFFIX", "ALLOY", "TAINT", + "SHEEP", "KINKY", "WOOLY", "MAUVE", "FLUNG", "YACHT", "FRIED", "QUAIL", "BRUNT", "GRIMY", "CURVY", "CAGEY", + "RINSE", "DEUCE", "STATE", "GRASP", "MILKY", "BISON", "GRAFT", "SANDY", "BASTE", "FLASK", "HEDGE", "GIRLY", + "SWASH", "BONEY", "COUPE", "ENDOW", "ABHOR", "WELCH", "BLADE", "TIGHT", "GEESE", "MISER", "MIRTH", "CLOUD", + "CABAL", "LEECH", "CLOSE", "TENTH", "PECAN", "DROIT", "GRAIL", "CLONE", "GUISE", "RALPH", "TANGO", "BIDDY", + "SMITH", "MOWER", "PAYEE", "SERIF", "DRAPE", "FIFTH", "SPANK", "GLAZE", "ALLOT", "TRUCK", "KAYAK", "VIRUS", + "TESTY", "TEPEE", "FULLY", "ZONAL", "METRO", "CURRY", "GRAND", "BANJO", "AXION", "BEZEL", "OCCUR", "CHAIN", + "NASAL", "GOOEY", "FILER", "BRACE", "ALLAY", "PUBIC", "RAVEN", "PLEAD", "GNASH", "FLAKY", "MUNCH", "DULLY", + "EKING", "THING", "SLINK", "HURRY", "THEFT", "SHORN", "PYGMY", "RANCH", "WRING", "LEMON", "SHORE", "MAMMA", + "FROZE", "NEWER", "STYLE", "MOOSE", "ANTIC", "DROWN", "VEGAN", "CHESS", "GUPPY", "UNION", "LEVER", "LORRY", + "IMAGE", "CABBY", "DRUID", "EXACT", "TRUTH", "DOPEY", "SPEAR", "CRIED", "CHIME", "CRONY", "STUNK", "TIMID", + "BATCH", "GAUGE", "ROTOR", "CRACK", "CURVE", "LATTE", "WITCH", "BUNCH", "REPEL", "ANVIL", "SOAPY", "METER", + "BROTH", "MADLY", "DRIED", "SCENE", "KNOWN", "MAGMA", "ROOST", "WOMAN", "THONG", "PUNCH", "PASTY", "DOWNY", + "KNEAD", "WHIRL", "RAPID", "CLANG", "ANGER", "DRIVE", "GOOFY", "EMAIL", "MUSIC", "STUFF", "BLEEP", "RIDER", + "MECCA", "FOLIO", "SETUP", "VERSO", "QUASH", "FAUNA", "GUMMY", "HAPPY", "NEWLY", "FUSSY", "RELIC", "GUAVA", + "RATTY", "FUDGE", "FEMUR", "CHIRP", "FORTE", "ALIBI", "WHINE", "PETTY", "GOLLY", "PLAIT", "FLECK", "FELON", + "GOURD", "BROWN", "THRUM", "FICUS", "STASH", "DECRY", "WISER", "JUNTA", "VISOR", "DAUNT", "SCREE", "IMPEL", + "AWAIT", "PRESS", "WHOSE", "TURBO", "STOOP", "SPEAK", "MANGY", "EYING", "INLET", "CRONE", "PULSE", "MOSSY", + "STAID", "HENCE", "PINCH", "TEDDY", "SULLY", "SNORE", "RIPEN", "SNOWY", "ATTIC", "GOING", "LEACH", "MOUTH", + "HOUND", "CLUMP", "TONAL", "BIGOT", "PERIL", "PIECE", "BLAME", "HAUTE", "SPIED", "UNDID", "INTRO", "BASAL", + "RODEO", "GUARD", "STEER", "LOAMY", "SCAMP", "SCRAM", "MANLY", "HELLO", "VAUNT", "ORGAN", "FERAL", "KNOCK", + "EXTRA", "CONDO", "ADAPT", "WILLY", "POLKA", "RAYON", "SKIRT", "FAITH", "TORSO", "MATCH", "MERCY", "TEPID", + "SLEEK", "RISER", "TWIXT", "PEACE", "FLUSH", "CATTY", "LOGIN", "EJECT", "ROGER", "RIVAL", "UNTIE", "REFIT", + "AORTA", "ADULT", "JUDGE", "ROWER", "ARTSY", "RURAL", "SHAVE", "BOBBY", "ECLAT", "FELLA", "GAILY", "HARRY", + "HASTY", "HYDRO", "LIEGE", "OCTAL", "OMBRE", "PAYER", "SOOTH", "UNSET", "UNLIT", "VOMIT", "FANNY", "FETUS", + "BUTCH", "STALK", "FLACK", "WIDOW", "AUGUR", "LITER", +] + +possible_list = [ + "AAHED", "AALII", "AARGH", "AARTI", "ABACA", "ABACI", "ABACS", "ABAFT", "ABAKA", "ABAMP", "ABAND", "ABASH", + "ABASK", "ABAYA", "ABBAS", "ABBED", "ABBES", "ABCEE", "ABEAM", "ABEAR", "ABELE", "ABERS", "ABETS", "ABIES", + "ABLER", "ABLES", "ABLET", "ABLOW", "ABMHO", "ABOHM", "ABOIL", "ABOMA", "ABOON", "ABORD", "ABORE", "ABRAM", + "ABRAY", "ABRIM", "ABRIN", "ABRIS", "ABSEY", "ABSIT", "ABUNA", "ABUNE", "ABUTS", "ABUZZ", "ABYES", "ABYSM", + "ACAIS", "ACARI", "ACCAS", "ACCOY", "ACERB", "ACERS", "ACETA", "ACHAR", "ACHED", "ACHES", "ACHOO", "ACIDS", + "ACIDY", "ACING", "ACINI", "ACKEE", "ACKER", "ACMES", "ACMIC", "ACNED", "ACNES", "ACOCK", "ACOLD", "ACRED", + "ACRES", "ACROS", "ACTED", "ACTIN", "ACTON", "ACYLS", "ADAWS", "ADAYS", "ADBOT", "ADDAX", "ADDED", "ADDER", + "ADDIO", "ADDLE", "ADEEM", "ADHAN", "ADIEU", "ADIOS", "ADITS", "ADMAN", "ADMEN", "ADMIX", "ADOBO", "ADOWN", + "ADOZE", "ADRAD", "ADRED", "ADSUM", "ADUKI", "ADUNC", "ADUST", "ADVEW", "ADYTA", "ADZED", "ADZES", "AECIA", + "AEDES", "AEGIS", "AEONS", "AERIE", "AEROS", "AESIR", "AFALD", "AFARA", "AFARS", "AFEAR", "AFLAJ", "AFORE", + "AFRIT", "AFROS", "AGAMA", "AGAMI", "AGARS", "AGAST", "AGAVE", "AGAZE", "AGENE", "AGERS", "AGGER", "AGGIE", + "AGGRI", "AGGRO", "AGGRY", "AGHAS", "AGILA", "AGIOS", "AGISM", "AGIST", "AGITA", "AGLEE", "AGLET", "AGLEY", + "AGLOO", "AGLUS", "AGMAS", "AGOGE", "AGONE", "AGONS", "AGOOD", "AGORA", "AGRIA", "AGRIN", "AGROS", "AGUED", + "AGUES", "AGUNA", "AGUTI", "AHEAP", "AHENT", "AHIGH", "AHIND", "AHING", "AHINT", "AHOLD", "AHULL", "AHURU", + "AIDAS", "AIDED", "AIDES", "AIDOI", "AIDOS", "AIERY", "AIGAS", "AIGHT", "AILED", "AIMED", "AIMER", "AINEE", + "AINGA", "AIOLI", "AIRED", "AIRER", "AIRNS", "AIRTH", "AIRTS", "AITCH", "AITUS", "AIVER", "AIYEE", "AIZLE", + "AJIES", "AJIVA", "AJUGA", "AJWAN", "AKEES", "AKELA", "AKENE", "AKING", "AKITA", "AKKAS", "ALAAP", "ALACK", + "ALAMO", "ALAND", "ALANE", "ALANG", "ALANS", "ALANT", "ALAPA", "ALAPS", "ALARY", "ALATE", "ALAYS", "ALBAS", + "ALBEE", "ALCID", "ALCOS", "ALDEA", "ALDER", "ALDOL", "ALECK", "ALECS", "ALEFS", "ALEFT", "ALEPH", "ALEWS", + "ALEYE", "ALFAS", "ALGAL", "ALGAS", "ALGID", "ALGIN", "ALGOR", "ALGUM", "ALIAS", "ALIFS", "ALINE", "ALIST", + "ALIYA", "ALKIE", "ALKOS", "ALKYD", "ALKYL", "ALLEE", "ALLEL", "ALLIS", "ALLOD", "ALLYL", "ALMAH", "ALMAS", + "ALMEH", "ALMES", "ALMUD", "ALMUG", "ALODS", "ALOED", "ALOES", "ALOHA", "ALOIN", "ALOOS", "ALOWE", "ALTHO", + "ALTOS", "ALULA", "ALUMS", "ALURE", "ALVAR", "ALWAY", "AMAHS", "AMAIN", "AMATE", "AMAUT", "AMBAN", "AMBIT", + "AMBOS", "AMBRY", "AMEBA", "AMEER", "AMENE", "AMENS", "AMENT", "AMIAS", "AMICE", "AMICI", "AMIDE", "AMIDO", + "AMIDS", "AMIES", "AMIGA", "AMIGO", "AMINE", "AMINO", "AMINS", "AMIRS", "AMLAS", "AMMAN", "AMMON", "AMMOS", + "AMNIA", "AMNIC", "AMNIO", "AMOKS", "AMOLE", "AMORT", "AMOUR", "AMOVE", "AMOWT", "AMPED", "AMPUL", "AMRIT", + "AMUCK", "AMYLS", "ANANA", "ANATA", "ANCHO", "ANCLE", "ANCON", "ANDRO", "ANEAR", "ANELE", "ANENT", "ANGAS", + "ANGLO", "ANIGH", "ANILE", "ANILS", "ANIMA", "ANIMI", "ANION", "ANISE", "ANKER", "ANKHS", "ANKUS", "ANLAS", + "ANNAL", "ANNAS", "ANNAT", "ANOAS", "ANOLE", "ANOMY", "ANSAE", "ANTAE", "ANTAR", "ANTAS", "ANTED", "ANTES", + "ANTIS", "ANTRA", "ANTRE", "ANTSY", "ANURA", "ANYON", "APACE", "APAGE", "APAID", "APAYD", "APAYS", "APEAK", + "APEEK", "APERS", "APERT", "APERY", "APGAR", "APHIS", "APIAN", "APIOL", "APISH", "APISM", "APODE", "APODS", + "APOOP", "APORT", "APPAL", "APPAY", "APPEL", "APPRO", "APPUI", "APPUY", "APRES", "APSES", "APSIS", "APSOS", + "APTED", "APTER", "AQUAE", "AQUAS", "ARABA", "ARAKS", "ARAME", "ARARS", "ARBAS", "ARCED", "ARCHI", "ARCOS", + "ARCUS", "ARDEB", "ARDRI", "AREAD", "AREAE", "AREAL", "AREAR", "AREAS", "ARECA", "AREDD", "AREDE", "AREFY", + "AREIC", "ARENE", "AREPA", "ARERE", "ARETE", "ARETS", "ARETT", "ARGAL", "ARGAN", "ARGIL", "ARGLE", "ARGOL", + "ARGON", "ARGOT", "ARGUS", "ARHAT", "ARIAS", "ARIEL", "ARIKI", "ARILS", "ARIOT", "ARISH", "ARKED", "ARLED", + "ARLES", "ARMED", "ARMER", "ARMET", "ARMIL", "ARNAS", "ARNUT", "AROBA", "AROHA", "AROID", "ARPAS", "ARPEN", + "ARRAH", "ARRAS", "ARRET", "ARRIS", "ARROZ", "ARSED", "ARSES", "ARSEY", "ARSIS", "ARTAL", "ARTEL", "ARTIC", + "ARTIS", "ARUHE", "ARUMS", "ARVAL", "ARVEE", "ARVOS", "ARYLS", "ASANA", "ASCON", "ASCUS", "ASDIC", "ASHED", + "ASHES", "ASHET", "ASKED", "ASKER", "ASKOI", "ASKOS", "ASPEN", "ASPER", "ASPIC", "ASPIE", "ASPIS", "ASPRO", + "ASSAI", "ASSAM", "ASSES", "ASSEZ", "ASSOT", "ASTER", "ASTIR", "ASTUN", "ASURA", "ASWAY", "ASWIM", "ASYLA", + "ATAPS", "ATAXY", "ATIGI", "ATILT", "ATIMY", "ATLAS", "ATMAN", "ATMAS", "ATMOS", "ATOCS", "ATOKE", "ATOKS", + "ATOMS", "ATOMY", "ATONY", "ATOPY", "ATRIA", "ATRIP", "ATTAP", "ATTAR", "ATUAS", "AUDAD", "AUGER", "AUGHT", + "AULAS", "AULIC", "AULOI", "AULOS", "AUMIL", "AUNES", "AUNTS", "AURAE", "AURAL", "AURAR", "AURAS", "AUREI", + "AURES", "AURIC", "AURIS", "AURUM", "AUTOS", "AUXIN", "AVALE", "AVANT", "AVAST", "AVELS", "AVENS", "AVERS", + "AVGAS", "AVINE", "AVION", "AVISE", "AVISO", "AVIZE", "AVOWS", "AVYZE", "AWARN", "AWATO", "AWAVE", "AWAYS", + "AWDLS", "AWEEL", "AWETO", "AWING", "AWMRY", "AWNED", "AWNER", "AWOLS", "AWORK", "AXELS", "AXILE", "AXILS", + "AXING", "AXITE", "AXLED", "AXLES", "AXMAN", "AXMEN", "AXOID", "AXONE", "AXONS", "AYAHS", "AYAYA", "AYELP", + "AYGRE", "AYINS", "AYONT", "AYRES", "AYRIE", "AZANS", "AZIDE", "AZIDO", "AZINE", "AZLON", "AZOIC", "AZOLE", + "AZONS", "AZOTE", "AZOTH", "AZUKI", "AZURN", "AZURY", "AZYGY", "AZYME", "AZYMS", "BAAED", "BAALS", "BABAS", + "BABEL", "BABES", "BABKA", "BABOO", "BABUL", "BABUS", "BACCA", "BACCO", "BACCY", "BACHA", "BACHS", "BACKS", + "BADDY", "BAELS", "BAFFS", "BAFFY", "BAFTS", "BAGHS", "BAGIE", "BAHTS", "BAHUS", "BAHUT", "BAILS", "BAIRN", + "BAISA", "BAITH", "BAITS", "BAIZA", "BAIZE", "BAJAN", "BAJRA", "BAJRI", "BAJUS", "BAKED", "BAKEN", "BAKES", + "BAKRA", "BALAS", "BALDS", "BALDY", "BALED", "BALES", "BALKS", "BALKY", "BALLS", "BALLY", "BALMS", "BALOO", + "BALSA", "BALTI", "BALUN", "BALUS", "BAMBI", "BANAK", "BANCO", "BANCS", "BANDA", "BANDH", "BANDS", "BANDY", + "BANED", "BANES", "BANGS", "BANIA", "BANKS", "BANNS", "BANTS", "BANTU", "BANTY", "BANYA", "BAPUS", "BARBE", + "BARBS", "BARBY", "BARCA", "BARDE", "BARDO", "BARDS", "BARDY", "BARED", "BARER", "BARES", "BARFI", "BARFS", + "BARIC", "BARKS", "BARKY", "BARMS", "BARMY", "BARNS", "BARNY", "BARPS", "BARRA", "BARRE", "BARRO", "BARRY", + "BARYE", "BASAN", "BASED", "BASEN", "BASER", "BASES", "BASHO", "BASIJ", "BASKS", "BASON", "BASSE", "BASSI", + "BASSO", "BASSY", "BASTA", "BASTI", "BASTO", "BASTS", "BATED", "BATES", "BATHS", "BATIK", "BATTA", "BATTS", + "BATTU", "BAUDS", "BAUKS", "BAULK", "BAURS", "BAVIN", "BAWDS", "BAWKS", "BAWLS", "BAWNS", "BAWRS", "BAWTY", + "BAYED", "BAYER", "BAYES", "BAYLE", "BAYTS", "BAZAR", "BAZOO", "BEADS", "BEAKS", "BEAKY", "BEALS", "BEAMS", + "BEAMY", "BEANO", "BEANS", "BEANY", "BEARE", "BEARS", "BEATH", "BEATS", "BEATY", "BEAUS", "BEAUT", "BEAUX", + "BEBOP", "BECAP", "BECKE", "BECKS", "BEDAD", "BEDEL", "BEDES", "BEDEW", "BEDIM", "BEDYE", "BEEDI", "BEEFS", + "BEEPS", "BEERS", "BEERY", "BEETS", "BEFOG", "BEGAD", "BEGAR", "BEGEM", "BEGOT", "BEGUM", "BEIGE", "BEIGY", + "BEINS", "BEKAH", "BELAH", "BELAR", "BELAY", "BELEE", "BELGA", "BELLS", "BELON", "BELTS", "BEMAD", "BEMAS", + "BEMIX", "BEMUD", "BENDS", "BENDY", "BENES", "BENET", "BENGA", "BENIS", "BENNE", "BENNI", "BENNY", "BENTO", + "BENTS", "BENTY", "BEPAT", "BERAY", "BERES", "BERGS", "BERKO", "BERKS", "BERME", "BERMS", "BEROB", "BERYL", + "BESAT", "BESAW", "BESEE", "BESES", "BESIT", "BESOM", "BESOT", "BESTI", "BESTS", "BETAS", "BETED", "BETES", + "BETHS", "BETID", "BETON", "BETTA", "BETTY", "BEVER", "BEVOR", "BEVUE", "BEVVY", "BEWET", "BEWIG", "BEZES", + "BEZIL", "BEZZY", "BHAIS", "BHAJI", "BHANG", "BHATS", "BHELS", "BHOOT", "BHUNA", "BHUTS", "BIACH", "BIALI", + "BIALY", "BIBBS", "BIBES", "BICCY", "BICES", "BIDED", "BIDER", "BIDES", "BIDET", "BIDIS", "BIDON", "BIELD", + "BIERS", "BIFFO", "BIFFS", "BIFFY", "BIFID", "BIGAE", "BIGGS", "BIGGY", "BIGHA", "BIGHT", "BIGLY", "BIGOS", + "BIJOU", "BIKED", "BIKER", "BIKES", "BIKIE", "BILBO", "BILBY", "BILED", "BILES", "BILGY", "BILKS", "BILLS", + "BIMAH", "BIMAS", "BIMBO", "BINAL", "BINDI", "BINDS", "BINER", "BINES", "BINGS", "BINGY", "BINIT", "BINKS", + "BINTS", "BIOGS", "BIONT", "BIOTA", "BIPED", "BIPOD", "BIRDS", "BIRKS", "BIRLE", "BIRLS", "BIROS", "BIRRS", + "BIRSE", "BIRSY", "BISES", "BISKS", "BISOM", "BITCH", "BITER", "BITES", "BITOS", "BITOU", "BITSY", "BITTE", + "BITTS", "BIVIA", "BIVVY", "BIZES", "BIZZO", "BIZZY", "BLABS", "BLADS", "BLADY", "BLAER", "BLAES", "BLAFF", + "BLAGS", "BLAHS", "BLAIN", "BLAMS", "BLART", "BLASE", "BLASH", "BLATE", "BLATS", "BLATT", "BLAUD", "BLAWN", + "BLAWS", "BLAYS", "BLEAR", "BLEBS", "BLECH", "BLEES", "BLENT", "BLERT", "BLEST", "BLETS", "BLEYS", "BLIMY", + "BLING", "BLINI", "BLINS", "BLINY", "BLIPS", "BLIST", "BLITE", "BLITS", "BLIVE", "BLOBS", "BLOCS", "BLOGS", + "BLOOK", "BLOOP", "BLORE", "BLOTS", "BLOWS", "BLOWY", "BLUBS", "BLUDE", "BLUDS", "BLUDY", "BLUED", "BLUES", + "BLUET", "BLUEY", "BLUID", "BLUME", "BLUNK", "BLURS", "BLYPE", "BOABS", "BOAKS", "BOARS", "BOART", "BOATS", + "BOBAC", "BOBAK", "BOBAS", "BOBOL", "BOBOS", "BOCCA", "BOCCE", "BOCCI", "BOCHE", "BOCKS", "BODED", "BODES", + "BODGE", "BODHI", "BODLE", "BOEPS", "BOETS", "BOEUF", "BOFFO", "BOFFS", "BOGAN", "BOGEY", "BOGGY", "BOGIE", + "BOGLE", "BOGUE", "BOGUS", "BOHEA", "BOHOS", "BOILS", "BOING", "BOINK", "BOITE", "BOKED", "BOKEH", "BOKES", + "BOKOS", "BOLAR", "BOLAS", "BOLDS", "BOLES", "BOLIX", "BOLLS", "BOLOS", "BOLTS", "BOLUS", "BOMAS", "BOMBE", + "BOMBO", "BOMBS", "BONCE", "BONDS", "BONED", "BONER", "BONES", "BONGS", "BONIE", "BONKS", "BONNE", "BONNY", + "BONZA", "BONZE", "BOOAI", "BOOAY", "BOOBS", "BOODY", "BOOED", "BOOFY", "BOOGY", "BOOHS", "BOOKS", "BOOKY", + "BOOLS", "BOOMS", "BOOMY", "BOONG", "BOONS", "BOORD", "BOORS", "BOOSE", "BOOTS", "BOPPY", "BORAK", "BORAL", + "BORAS", "BORDE", "BORDS", "BORED", "BOREE", "BOREL", "BORER", "BORES", "BORGO", "BORIC", "BORKS", "BORMS", + "BORNA", "BORON", "BORTS", "BORTY", "BORTZ", "BOSIE", "BOSKS", "BOSKY", "BOSON", "BOSUN", "BOTAS", "BOTEL", + "BOTES", "BOTHY", "BOTTE", "BOTTS", "BOTTY", "BOUGE", "BOUKS", "BOULT", "BOUNS", "BOURD", "BOURG", "BOURN", + "BOUSE", "BOUSY", "BOUTS", "BOVID", "BOWAT", "BOWED", "BOWER", "BOWES", "BOWET", "BOWIE", "BOWLS", "BOWNE", + "BOWRS", "BOWSE", "BOXED", "BOXEN", "BOXES", "BOXLA", "BOXTY", "BOYAR", "BOYAU", "BOYED", "BOYFS", "BOYGS", + "BOYLA", "BOYOS", "BOYSY", "BOZOS", "BRAAI", "BRACH", "BRACK", "BRACT", "BRADS", "BRAES", "BRAGS", "BRAIL", + "BRAKS", "BRAKY", "BRAME", "BRANE", "BRANK", "BRANS", "BRANT", "BRAST", "BRATS", "BRAVA", "BRAVI", "BRAWS", + "BRAXY", "BRAYS", "BRAZA", "BRAZE", "BREAM", "BREDE", "BREDS", "BREEM", "BREER", "BREES", "BREID", "BREIS", + "BREME", "BRENS", "BRENT", "BRERE", "BRERS", "BREVE", "BREWS", "BREYS", "BRIER", "BRIES", "BRIGS", "BRIKI", + "BRIKS", "BRILL", "BRIMS", "BRINS", "BRIOS", "BRISE", "BRISS", "BRITH", "BRITS", "BRITT", "BRIZE", "BROCH", + "BROCK", "BRODS", "BROGH", "BROGS", "BROME", "BROMO", "BRONC", "BROND", "BROOL", "BROOS", "BROSE", "BROSY", + "BROWS", "BRUGH", "BRUIN", "BRUIT", "BRULE", "BRUME", "BRUNG", "BRUSK", "BRUST", "BRUTS", "BUATS", "BUAZE", + "BUBAL", "BUBAS", "BUBBA", "BUBBE", "BUBBY", "BUBUS", "BUCHU", "BUCKO", "BUCKS", "BUCKU", "BUDAS", "BUDIS", + "BUDOS", "BUFFA", "BUFFE", "BUFFI", "BUFFO", "BUFFS", "BUFFY", "BUFOS", "BUFTY", "BUHLS", "BUHRS", "BUIKS", + "BUIST", "BUKES", "BULBS", "BULGY", "BULKS", "BULLA", "BULLS", "BULSE", "BUMBO", "BUMFS", "BUMPH", "BUMPS", + "BUMPY", "BUNAS", "BUNCE", "BUNCO", "BUNDE", "BUNDH", "BUNDS", "BUNDT", "BUNDU", "BUNDY", "BUNGS", "BUNGY", + "BUNIA", "BUNJE", "BUNJY", "BUNKO", "BUNKS", "BUNNS", "BUNTS", "BUNTY", "BUNYA", "BUOYS", "BUPPY", "BURAN", + "BURAS", "BURBS", "BURDS", "BURET", "BURFI", "BURGH", "BURGS", "BURIN", "BURKA", "BURKE", "BURKS", "BURLS", + "BURNS", "BUROO", "BURPS", "BURQA", "BURRO", "BURRS", "BURRY", "BURSA", "BURSE", "BUSBY", "BUSES", "BUSKS", + "BUSKY", "BUSSU", "BUSTI", "BUSTS", "BUSTY", "BUTEO", "BUTES", "BUTLE", "BUTOH", "BUTTS", "BUTTY", "BUTUT", + "BUTYL", "BUZZY", "BWANA", "BWAZI", "BYDED", "BYDES", "BYKED", "BYKES", "BYRES", "BYRLS", "BYSSI", "BYTES", + "BYWAY", "CAAED", "CABAS", "CABER", "CABOB", "CABOC", "CABRE", "CACAS", "CACKS", "CACKY", "CADEE", "CADES", + "CADGE", "CADGY", "CADIE", "CADIS", "CADRE", "CAECA", "CAESE", "CAFES", "CAFFS", "CAGED", "CAGER", "CAGES", + "CAGOT", "CAHOW", "CAIDS", "CAINS", "CAIRD", "CAJON", "CAJUN", "CAKED", "CAKES", "CAKEY", "CALFS", "CALID", + "CALIF", "CALIX", "CALKS", "CALLA", "CALLS", "CALMS", "CALMY", "CALOS", "CALPA", "CALPS", "CALVE", "CALYX", + "CAMAN", "CAMAS", "CAMES", "CAMIS", "CAMOS", "CAMPI", "CAMPO", "CAMPS", "CAMPY", "CAMUS", "CANED", "CANEH", + "CANER", "CANES", "CANGS", "CANID", "CANNA", "CANNS", "CANSO", "CANST", "CANTO", "CANTS", "CANTY", "CAPAS", + "CAPED", "CAPES", "CAPEX", "CAPHS", "CAPIZ", "CAPLE", "CAPON", "CAPOS", "CAPOT", "CAPRI", "CAPUL", "CARAP", + "CARBO", "CARBS", "CARBY", "CARDI", "CARDS", "CARDY", "CARED", "CARER", "CARES", "CARET", "CAREX", "CARKS", + "CARLE", "CARLS", "CARNS", "CARNY", "CAROB", "CAROM", "CARON", "CARPI", "CARPS", "CARRS", "CARSE", "CARTA", + "CARTE", "CARTS", "CARVY", "CASAS", "CASCO", "CASED", "CASES", "CASKS", "CASKY", "CASTS", "CASUS", "CATES", + "CAUDA", "CAUKS", "CAULD", "CAULS", "CAUMS", "CAUPS", "CAURI", "CAUSA", "CAVAS", "CAVED", "CAVEL", "CAVER", + "CAVES", "CAVIE", "CAWED", "CAWKS", "CAXON", "CEAZE", "CEBID", "CECAL", "CECUM", "CEDED", "CEDER", "CEDES", + "CEDIS", "CEIBA", "CEILI", "CEILS", "CELEB", "CELLA", "CELLI", "CELLS", "CELOM", "CELTS", "CENSE", "CENTO", + "CENTS", "CENTU", "CEORL", "CEPES", "CERCI", "CERED", "CERES", "CERGE", "CERIA", "CERIC", "CERNE", "CEROC", + "CEROS", "CERTS", "CERTY", "CESSE", "CESTA", "CESTI", "CETES", "CETYL", "CEZVE", "CHACE", "CHACK", "CHACO", + "CHADO", "CHADS", "CHAFT", "CHAIS", "CHALS", "CHAMS", "CHANA", "CHANG", "CHANK", "CHAPE", "CHAPS", "CHAPT", + "CHARA", "CHARE", "CHARK", "CHARR", "CHARS", "CHARY", "CHATS", "CHAVE", "CHAVS", "CHAWK", "CHAWS", "CHAYA", + "CHAYS", "CHEEP", "CHEFS", "CHEKA", "CHELA", "CHELP", "CHEMO", "CHEMS", "CHERE", "CHERT", "CHETH", "CHEVY", + "CHEWS", "CHEWY", "CHIAO", "CHIAS", "CHIBS", "CHICA", "CHICH", "CHICO", "CHICS", "CHIEL", "CHIKS", "CHILE", + "CHIMB", "CHIMO", "CHIMP", "CHINE", "CHING", "CHINK", "CHINO", "CHINS", "CHIPS", "CHIRK", "CHIRL", "CHIRM", + "CHIRO", "CHIRR", "CHIRT", "CHIRU", "CHITS", "CHIVE", "CHIVS", "CHIVY", "CHIZZ", "CHOCO", "CHOCS", "CHODE", + "CHOGS", "CHOIL", "CHOKO", "CHOKY", "CHOLA", "CHOLI", "CHOLO", "CHOMP", "CHONS", "CHOOF", "CHOOK", "CHOOM", + "CHOON", "CHOPS", "CHOTA", "CHOTT", "CHOUT", "CHOUX", "CHOWK", "CHOWS", "CHUBS", "CHUFA", "CHUFF", "CHUGS", + "CHUMS", "CHURL", "CHURR", "CHUSE", "CHUTS", "CHYLE", "CHYME", "CHYND", "CIBOL", "CIDED", "CIDES", "CIELS", + "CIGGY", "CILIA", "CILLS", "CIMAR", "CIMEX", "CINCT", "CINES", "CINQS", "CIONS", "CIPPI", "CIRCS", "CIRES", + "CIRLS", "CIRRI", "CISCO", "CISSY", "CISTS", "CITAL", "CITED", "CITER", "CITES", "CIVES", "CIVET", "CIVIE", + "CIVVY", "CLACH", "CLADE", "CLADS", "CLAES", "CLAGS", "CLAME", "CLAMS", "CLANS", "CLAPS", "CLAPT", "CLARO", + "CLART", "CLARY", "CLAST", "CLATS", "CLAUT", "CLAVE", "CLAVI", "CLAWS", "CLAYS", "CLECK", "CLEEK", "CLEEP", + "CLEFS", "CLEGS", "CLEIK", "CLEMS", "CLEPE", "CLEPT", "CLEVE", "CLEWS", "CLIED", "CLIES", "CLIFT", "CLIME", + "CLINE", "CLINT", "CLIPE", "CLIPS", "CLIPT", "CLITS", "CLOAM", "CLODS", "CLOFF", "CLOGS", "CLOKE", "CLOMB", + "CLOMP", "CLONK", "CLONS", "CLOOP", "CLOOT", "CLOPS", "CLOTE", "CLOTS", "CLOUR", "CLOUS", "CLOWS", "CLOYE", + "CLOYS", "CLOZE", "CLUBS", "CLUES", "CLUEY", "CLUNK", "CLYPE", "CNIDA", "COACT", "COADY", "COALA", "COALS", + "COALY", "COAPT", "COARB", "COATE", "COATI", "COATS", "COBBS", "COBBY", "COBIA", "COBLE", "COBZA", "COCAS", + "COCCI", "COCCO", "COCKS", "COCKY", "COCOS", "CODAS", "CODEC", "CODED", "CODEN", "CODER", "CODES", "CODEX", + "CODON", "COEDS", "COFFS", "COGIE", "COGON", "COGUE", "COHAB", "COHEN", "COHOE", "COHOG", "COHOS", "COIFS", + "COIGN", "COILS", "COINS", "COIRS", "COITS", "COKED", "COKES", "COLAS", "COLBY", "COLDS", "COLED", "COLES", + "COLEY", "COLIC", "COLIN", "COLLS", "COLLY", "COLOG", "COLTS", "COLZA", "COMAE", "COMAL", "COMAS", "COMBE", + "COMBI", "COMBO", "COMBS", "COMBY", "COMER", "COMES", "COMIX", "COMMO", "COMMS", "COMMY", "COMPO", "COMPS", + "COMPT", "COMTE", "COMUS", "CONED", "CONES", "CONEY", "CONFS", "CONGA", "CONGE", "CONGO", "CONIA", "CONIN", + "CONKS", "CONKY", "CONNE", "CONNS", "CONTE", "CONTO", "CONUS", "CONVO", "COOCH", "COOED", "COOEE", "COOER", + "COOEY", "COOFS", "COOKS", "COOKY", "COOLS", "COOLY", "COOMB", "COOMS", "COOMY", "COONS", "COOPS", "COOPT", + "COOST", "COOTS", "COOZE", "COPAL", "COPAY", "COPED", "COPEN", "COPER", "COPES", "COPPY", "COPRA", "COPSY", + "COQUI", "CORAM", "CORBE", "CORBY", "CORDS", "CORED", "CORES", "COREY", "CORGI", "CORIA", "CORKS", "CORKY", + "CORMS", "CORNI", "CORNO", "CORNS", "CORNU", "CORPS", "CORSE", "CORSO", "COSEC", "COSED", "COSES", "COSET", + "COSEY", "COSIE", "COSTA", "COSTE", "COSTS", "COTAN", "COTED", "COTES", "COTHS", "COTTA", "COTTS", "COUDE", + "COUPS", "COURB", "COURD", "COURE", "COURS", "COUTA", "COUTH", "COVED", "COVES", "COVIN", "COWAL", "COWAN", + "COWED", "COWKS", "COWLS", "COWPS", "COWRY", "COXAE", "COXAL", "COXED", "COXES", "COXIB", "COYAU", "COYED", + "COYER", "COYPU", "COZED", "COZEN", "COZES", "COZEY", "COZIE", "CRAAL", "CRABS", "CRAGS", "CRAIC", "CRAIG", + "CRAKE", "CRAME", "CRAMS", "CRANS", "CRAPE", "CRAPS", "CRAPY", "CRARE", "CRAWS", "CRAYS", "CREDS", "CREEL", + "CREES", "CREMS", "CRENA", "CREPS", "CREPY", "CREWE", "CREWS", "CRIAS", "CRIBS", "CRIES", "CRIMS", "CRINE", + "CRIOS", "CRIPE", "CRIPS", "CRISE", "CRITH", "CRITS", "CROCI", "CROCS", "CROFT", "CROGS", "CROMB", "CROME", + "CRONK", "CRONS", "CROOL", "CROON", "CROPS", "CRORE", "CROST", "CROUT", "CROWS", "CROZE", "CRUCK", "CRUDO", + "CRUDS", "CRUDY", "CRUES", "CRUET", "CRUFT", "CRUNK", "CRUOR", "CRURA", "CRUSE", "CRUSY", "CRUVE", "CRWTH", + "CRYER", "CTENE", "CUBBY", "CUBEB", "CUBED", "CUBER", "CUBES", "CUBIT", "CUDDY", "CUFFO", "CUFFS", "CUIFS", + "CUING", "CUISH", "CUITS", "CUKES", "CULCH", "CULET", "CULEX", "CULLS", "CULLY", "CULMS", "CULPA", "CULTI", + "CULTS", "CULTY", "CUMEC", "CUNDY", "CUNEI", "CUNIT", "CUNTS", "CUPEL", "CUPID", "CUPPA", "CUPPY", "CURAT", + "CURBS", "CURCH", "CURDS", "CURDY", "CURED", "CURER", "CURES", "CURET", "CURFS", "CURIA", "CURIE", "CURLI", + "CURLS", "CURNS", "CURNY", "CURRS", "CURSI", "CURST", "CUSEC", "CUSHY", "CUSKS", "CUSPS", "CUSPY", "CUSSO", + "CUSUM", "CUTCH", "CUTER", "CUTES", "CUTEY", "CUTIN", "CUTIS", "CUTTO", "CUTTY", "CUTUP", "CUVEE", "CUZES", + "CWTCH", "CYANO", "CYANS", "CYCAD", "CYCAS", "CYCLO", "CYDER", "CYLIX", "CYMAE", "CYMAR", "CYMAS", "CYMES", + "CYMOL", "CYSTS", "CYTES", "CYTON", "CZARS", "DAALS", "DABBA", "DACES", "DACHA", "DACKS", "DADAH", "DADAS", + "DADOS", "DAFFS", "DAFFY", "DAGGA", "DAGGY", "DAGOS", "DAHLS", "DAIKO", "DAINE", "DAINT", "DAKER", "DALED", + "DALES", "DALIS", "DALLE", "DALTS", "DAMAN", "DAMAR", "DAMES", "DAMME", "DAMNS", "DAMPS", "DAMPY", "DANCY", + "DANGS", "DANIO", "DANKS", "DANNY", "DANTS", "DARAF", "DARBS", "DARCY", "DARED", "DARER", "DARES", "DARGA", + "DARGS", "DARIC", "DARIS", "DARKS", "DARKY", "DARNS", "DARRE", "DARTS", "DARZI", "DASHI", "DASHY", "DATAL", + "DATED", "DATER", "DATES", "DATOS", "DATTO", "DAUBE", "DAUBS", "DAUBY", "DAUDS", "DAULT", "DAURS", "DAUTS", + "DAVEN", "DAVIT", "DAWAH", "DAWDS", "DAWED", "DAWEN", "DAWKS", "DAWNS", "DAWTS", "DAYAN", "DAYCH", "DAYNT", + "DAZED", "DAZER", "DAZES", "DEADS", "DEAIR", "DEALS", "DEANS", "DEARE", "DEARN", "DEARS", "DEARY", "DEASH", + "DEAVE", "DEAWS", "DEAWY", "DEBAG", "DEBBY", "DEBEL", "DEBES", "DEBTS", "DEBUD", "DEBUR", "DEBUS", "DEBYE", + "DECAD", "DECAF", "DECAN", "DECKO", "DECKS", "DECOS", "DEDAL", "DEEDS", "DEEDY", "DEELY", "DEEMS", "DEENS", + "DEEPS", "DEERE", "DEERS", "DEETS", "DEEVE", "DEEVS", "DEFAT", "DEFFO", "DEFIS", "DEFOG", "DEGAS", "DEGUM", + "DEGUS", "DEICE", "DEIDS", "DEIFY", "DEILS", "DEISM", "DEIST", "DEKED", "DEKES", "DEKKO", "DELED", "DELES", + "DELFS", "DELFT", "DELIS", "DELLS", "DELLY", "DELOS", "DELPH", "DELTS", "DEMAN", "DEMES", "DEMIC", "DEMIT", + "DEMOB", "DEMOI", "DEMOS", "DEMPT", "DENAR", "DENAY", "DENCH", "DENES", "DENET", "DENIS", "DENTS", "DEOXY", + "DERAT", "DERAY", "DERED", "DERES", "DERIG", "DERMA", "DERMS", "DERNS", "DERNY", "DEROS", "DERRO", "DERRY", + "DERTH", "DERVS", "DESEX", "DESHI", "DESIS", "DESKS", "DESSE", "DEVAS", "DEVEL", "DEVIS", "DEVON", "DEVOS", + "DEVOT", "DEWAN", "DEWAR", "DEWAX", "DEWED", "DEXES", "DEXIE", "DHABA", "DHAKS", "DHALS", "DHIKR", "DHOBI", + "DHOLE", "DHOLL", "DHOLS", "DHOTI", "DHOWS", "DHUTI", "DIACT", "DIALS", "DIANE", "DIAZO", "DIBBS", "DICED", + "DICER", "DICES", "DICHT", "DICKS", "DICKY", "DICOT", "DICTA", "DICTS", "DICTY", "DIDDY", "DIDIE", "DIDOS", + "DIDST", "DIEBS", "DIELS", "DIENE", "DIETS", "DIFFS", "DIGHT", "DIKAS", "DIKED", "DIKER", "DIKES", "DIKEY", + "DILDO", "DILLI", "DILLS", "DIMBO", "DIMER", "DIMES", "DIMPS", "DINAR", "DINED", "DINES", "DINGE", "DINGS", + "DINIC", "DINKS", "DINKY", "DINNA", "DINOS", "DINTS", "DIOLS", "DIOTA", "DIPPY", "DIPSO", "DIRAM", "DIRER", + "DIRKE", "DIRKS", "DIRLS", "DIRTS", "DISAS", "DISCI", "DISCS", "DISHY", "DISKS", "DISME", "DITAL", "DITAS", + "DITED", "DITES", "DITSY", "DITTS", "DITZY", "DIVAN", "DIVAS", "DIVED", "DIVES", "DIVIS", "DIVNA", "DIVOS", + "DIVOT", "DIVVY", "DIWAN", "DIXIE", "DIXIT", "DIYAS", "DIZEN", "DJINN", "DJINS", "DOABS", "DOATS", "DOBBY", + "DOBES", "DOBIE", "DOBLA", "DOBRA", "DOBRO", "DOCHT", "DOCKS", "DOCOS", "DOCUS", "DODDY", "DODOS", "DOEKS", + "DOERS", "DOEST", "DOETH", "DOFFS", "DOGAN", "DOGES", "DOGEY", "DOGGO", "DOGGY", "DOGIE", "DOHYO", "DOILT", + "DOILY", "DOITS", "DOJOS", "DOLCE", "DOLCI", "DOLED", "DOLES", "DOLIA", "DOLLS", "DOLMA", "DOLOR", "DOLOS", + "DOLTS", "DOMAL", "DOMED", "DOMES", "DOMIC", "DONAH", "DONAS", "DONEE", "DONER", "DONGA", "DONGS", "DONKO", + "DONNA", "DONNE", "DONNY", "DONSY", "DOOBS", "DOOCE", "DOODY", "DOOKS", "DOOLE", "DOOLS", "DOOLY", "DOOMS", + "DOOMY", "DOONA", "DOORN", "DOORS", "DOOZY", "DOPAS", "DOPED", "DOPER", "DOPES", "DORAD", "DORBA", "DORBS", + "DOREE", "DORES", "DORIC", "DORIS", "DORKS", "DORKY", "DORMS", "DORMY", "DORPS", "DORRS", "DORSA", "DORSE", + "DORTS", "DORTY", "DOSAI", "DOSAS", "DOSED", "DOSEH", "DOSER", "DOSES", "DOSHA", "DOTAL", "DOTED", "DOTER", + "DOTES", "DOTTY", "DOUAR", "DOUCE", "DOUCS", "DOUKS", "DOULA", "DOUMA", "DOUMS", "DOUPS", "DOURA", "DOUSE", + "DOUTS", "DOVED", "DOVEN", "DOVER", "DOVES", "DOVIE", "DOWAR", "DOWDS", "DOWED", "DOWER", "DOWIE", "DOWLE", + "DOWLS", "DOWLY", "DOWNA", "DOWNS", "DOWPS", "DOWSE", "DOWTS", "DOXED", "DOXES", "DOXIE", "DOYEN", "DOYLY", + "DOZED", "DOZER", "DOZES", "DRABS", "DRACK", "DRACO", "DRAFF", "DRAGS", "DRAIL", "DRAMS", "DRANT", "DRAPS", + "DRATS", "DRAVE", "DRAWS", "DRAYS", "DREAR", "DRECK", "DREED", "DREER", "DREES", "DREGS", "DREKS", "DRENT", + "DRERE", "DREST", "DREYS", "DRIBS", "DRICE", "DRIES", "DRILY", "DRIPS", "DRIPT", "DROID", "DROIL", "DROKE", + "DROLE", "DROME", "DRONY", "DROOB", "DROOG", "DROOK", "DROPS", "DROPT", "DROUK", "DROWS", "DRUBS", "DRUGS", + "DRUMS", "DRUPE", "DRUSE", "DRUSY", "DRUXY", "DRYAD", "DRYAS", "DSOBO", "DSOMO", "DUADS", "DUALS", "DUANS", + "DUARS", "DUBBO", "DUCAL", "DUCAT", "DUCES", "DUCKS", "DUCKY", "DUCTS", "DUDDY", "DUDED", "DUDES", "DUELS", + "DUETS", "DUETT", "DUFFS", "DUFUS", "DUING", "DUITS", "DUKAS", "DUKED", "DUKES", "DUKKA", "DULCE", "DULES", + "DULIA", "DULLS", "DULSE", "DUMAS", "DUMBO", "DUMBS", "DUMKA", "DUMKY", "DUMPS", "DUNAM", "DUNCH", "DUNES", + "DUNGS", "DUNGY", "DUNKS", "DUNNO", "DUNNY", "DUNSH", "DUNTS", "DUOMI", "DUOMO", "DUPED", "DUPER", "DUPES", + "DUPLE", "DUPLY", "DUPPY", "DURAL", "DURAS", "DURED", "DURES", "DURGY", "DURNS", "DUROC", "DUROS", "DUROY", + "DURRA", "DURRS", "DURRY", "DURST", "DURUM", "DURZI", "DUSKS", "DUSTS", "DUXES", "DWAAL", "DWALE", "DWALM", + "DWAMS", "DWANG", "DWAUM", "DWEEB", "DWILE", "DWINE", "DYADS", "DYERS", "DYKED", "DYKES", "DYKEY", "DYKON", + "DYNEL", "DYNES", "DZHOS", "EAGRE", "EALED", "EALES", "EANED", "EARDS", "EARED", "EARLS", "EARNS", "EARNT", + "EARST", "EASED", "EASER", "EASES", "EASLE", "EASTS", "EATHE", "EAVED", "EAVES", "EBBED", "EBBET", "EBONS", + "EBOOK", "ECADS", "ECHED", "ECHES", "ECHOS", "ECRUS", "EDEMA", "EDGED", "EDGER", "EDGES", "EDILE", "EDITS", + "EDUCE", "EDUCT", "EEJIT", "EENSY", "EEVEN", "EEVNS", "EFFED", "EGADS", "EGERS", "EGEST", "EGGAR", "EGGED", + "EGGER", "EGMAS", "EHING", "EIDER", "EIDOS", "EIGNE", "EIKED", "EIKON", "EILDS", "EISEL", "EJIDO", "EKKAS", + "ELAIN", "ELAND", "ELANS", "ELCHI", "ELDIN", "ELEMI", "ELFED", "ELIAD", "ELINT", "ELMEN", "ELOGE", "ELOGY", + "ELOIN", "ELOPS", "ELPEE", "ELSIN", "ELUTE", "ELVAN", "ELVEN", "ELVER", "ELVES", "EMACS", "EMBAR", "EMBAY", + "EMBOG", "EMBOW", "EMBOX", "EMBUS", "EMEER", "EMEND", "EMERG", "EMERY", "EMEUS", "EMICS", "EMIRS", "EMITS", + "EMMAS", "EMMER", "EMMET", "EMMEW", "EMMYS", "EMOJI", "EMONG", "EMOTE", "EMOVE", "EMPTS", "EMULE", "EMURE", + "EMYDE", "EMYDS", "ENARM", "ENATE", "ENDED", "ENDER", "ENDEW", "ENDUE", "ENEWS", "ENFIX", "ENIAC", "ENLIT", + "ENMEW", "ENNOG", "ENOKI", "ENOLS", "ENORM", "ENOWS", "ENROL", "ENSEW", "ENSKY", "ENTIA", "ENURE", "ENURN", + "ENVOI", "ENZYM", "EORLS", "EOSIN", "EPACT", "EPEES", "EPHAH", "EPHAS", "EPHOD", "EPHOR", "EPICS", "EPODE", + "EPOPT", "EPRIS", "EQUES", "EQUID", "ERBIA", "EREVS", "ERGON", "ERGOS", "ERGOT", "ERHUS", "ERICA", "ERICK", + "ERICS", "ERING", "ERNED", "ERNES", "EROSE", "ERRED", "ERSES", "ERUCT", "ERUGO", "ERUVS", "ERVEN", "ERVIL", + "ESCAR", "ESCOT", "ESILE", "ESKAR", "ESKER", "ESNES", "ESSES", "ESTOC", "ESTOP", "ESTRO", "ETAGE", "ETAPE", + "ETATS", "ETENS", "ETHAL", "ETHNE", "ETHYL", "ETICS", "ETNAS", "ETTIN", "ETTLE", "ETUIS", "ETWEE", "ETYMA", + "EUGHS", "EUKED", "EUPAD", "EUROS", "EUSOL", "EVENS", "EVERT", "EVETS", "EVHOE", "EVILS", "EVITE", "EVOHE", + "EWERS", "EWEST", "EWHOW", "EWKED", "EXAMS", "EXEAT", "EXECS", "EXEEM", "EXEME", "EXFIL", "EXIES", "EXINE", + "EXING", "EXITS", "EXODE", "EXOME", "EXONS", "EXPAT", "EXPOS", "EXUDE", "EXULS", "EXURB", "EYASS", "EYERS", + "EYOTS", "EYRAS", "EYRES", "EYRIE", "EYRIR", "EZINE", "FABBY", "FACED", "FACER", "FACES", "FACIA", "FACTA", + "FACTS", "FADDY", "FADED", "FADER", "FADES", "FADGE", "FADOS", "FAENA", "FAERY", "FAFFS", "FAFFY", "FAGGY", + "FAGIN", "FAGOT", "FAIKS", "FAILS", "FAINE", "FAINS", "FAIRS", "FAKED", "FAKER", "FAKES", "FAKEY", "FAKIE", + "FAKIR", "FALAJ", "FALLS", "FAMED", "FAMES", "FANAL", "FANDS", "FANES", "FANGA", "FANGO", "FANGS", "FANKS", + "FANON", "FANOS", "FANUM", "FAQIR", "FARAD", "FARCI", "FARCY", "FARDS", "FARED", "FARER", "FARES", "FARLE", + "FARLS", "FARMS", "FAROS", "FARRO", "FARSE", "FARTS", "FASCI", "FASTI", "FASTS", "FATED", "FATES", "FATLY", + "FATSO", "FATWA", "FAUGH", "FAULD", "FAUNS", "FAURD", "FAUTS", "FAUVE", "FAVAS", "FAVEL", "FAVER", "FAVES", + "FAVUS", "FAWNS", "FAWNY", "FAXED", "FAXES", "FAYED", "FAYER", "FAYNE", "FAYRE", "FAZED", "FAZES", "FEALS", + "FEARE", "FEARS", "FEART", "FEASE", "FEATS", "FEAZE", "FECES", "FECHT", "FECIT", "FECKS", "FEDEX", "FEEBS", + "FEEDS", "FEELS", "FEENS", "FEERS", "FEESE", "FEEZE", "FEHME", "FEINT", "FEIST", "FELCH", "FELID", "FELLS", + "FELLY", "FELTS", "FELTY", "FEMAL", "FEMES", "FEMMY", "FENDS", "FENDY", "FENIS", "FENKS", "FENNY", "FENTS", + "FEODS", "FEOFF", "FERER", "FERES", "FERIA", "FERLY", "FERMI", "FERMS", "FERNS", "FERNY", "FESSE", "FESTA", + "FESTS", "FESTY", "FETAS", "FETED", "FETES", "FETOR", "FETTA", "FETTS", "FETWA", "FEUAR", "FEUDS", "FEUED", + "FEYED", "FEYER", "FEYLY", "FEZES", "FEZZY", "FIARS", "FIATS", "FIBRE", "FIBRO", "FICES", "FICHE", "FICHU", + "FICIN", "FICOS", "FIDES", "FIDGE", "FIDOS", "FIEFS", "FIENT", "FIERE", "FIERS", "FIEST", "FIFED", "FIFER", + "FIFES", "FIFIS", "FIGGY", "FIGOS", "FIKED", "FIKES", "FILAR", "FILCH", "FILED", "FILES", "FILII", "FILKS", + "FILLE", "FILLO", "FILLS", "FILMI", "FILMS", "FILOS", "FILUM", "FINCA", "FINDS", "FINED", "FINES", "FINIS", + "FINKS", "FINNY", "FINOS", "FIORD", "FIQHS", "FIQUE", "FIRED", "FIRER", "FIRES", "FIRIE", "FIRKS", "FIRMS", + "FIRNS", "FIRRY", "FIRTH", "FISCS", "FISKS", "FISTS", "FISTY", "FITCH", "FITLY", "FITNA", "FITTE", "FITTS", + "FIVER", "FIVES", "FIXED", "FIXES", "FIXIT", "FJELD", "FLABS", "FLAFF", "FLAGS", "FLAKS", "FLAMM", "FLAMS", + "FLAMY", "FLANE", "FLANS", "FLAPS", "FLARY", "FLATS", "FLAVA", "FLAWN", "FLAWS", "FLAWY", "FLAXY", "FLAYS", + "FLEAM", "FLEAS", "FLEEK", "FLEER", "FLEES", "FLEGS", "FLEME", "FLEUR", "FLEWS", "FLEXI", "FLEXO", "FLEYS", + "FLICS", "FLIED", "FLIES", "FLIMP", "FLIMS", "FLIPS", "FLIRS", "FLISK", "FLITE", "FLITS", "FLITT", "FLOBS", + "FLOCS", "FLOES", "FLOGS", "FLONG", "FLOPS", "FLORS", "FLORY", "FLOSH", "FLOTA", "FLOTE", "FLOWS", "FLUBS", + "FLUED", "FLUES", "FLUEY", "FLUKY", "FLUMP", "FLUOR", "FLURR", "FLUTY", "FLUYT", "FLYBY", "FLYPE", "FLYTE", + "FOALS", "FOAMS", "FOEHN", "FOGEY", "FOGIE", "FOGLE", "FOGOU", "FOHNS", "FOIDS", "FOILS", "FOINS", "FOLDS", + "FOLEY", "FOLIA", "FOLIC", "FOLIE", "FOLKS", "FOLKY", "FOMES", "FONDA", "FONDS", "FONDU", "FONES", "FONLY", + "FONTS", "FOODS", "FOODY", "FOOLS", "FOOTS", "FOOTY", "FORAM", "FORBS", "FORBY", "FORDO", "FORDS", "FOREL", + "FORES", "FOREX", "FORKS", "FORKY", "FORME", "FORMS", "FORTS", "FORZA", "FORZE", "FOSSA", "FOSSE", "FOUAT", + "FOUDS", "FOUER", "FOUET", "FOULE", "FOULS", "FOUNT", "FOURS", "FOUTH", "FOVEA", "FOWLS", "FOWTH", "FOXED", + "FOXES", "FOXIE", "FOYLE", "FOYNE", "FRABS", "FRACK", "FRACT", "FRAGS", "FRAIM", "FRANC", "FRAPE", "FRAPS", + "FRASS", "FRATE", "FRATI", "FRATS", "FRAUS", "FRAYS", "FREES", "FREET", "FREIT", "FREMD", "FRENA", "FREON", + "FRERE", "FRETS", "FRIBS", "FRIER", "FRIES", "FRIGS", "FRISE", "FRIST", "FRITH", "FRITS", "FRITT", "FRIZE", + "FRIZZ", "FROES", "FROGS", "FRONS", "FRORE", "FRORN", "FRORY", "FROSH", "FROWS", "FROWY", "FRUGS", "FRUMP", + "FRUSH", "FRUST", "FRYER", "FUBAR", "FUBBY", "FUBSY", "FUCKS", "FUCUS", "FUDDY", "FUDGY", "FUELS", "FUERO", + "FUFFS", "FUFFY", "FUGAL", "FUGGY", "FUGIE", "FUGIO", "FUGLE", "FUGLY", "FUGUS", "FUJIS", "FULLS", "FUMED", + "FUMER", "FUMES", "FUMET", "FUNDI", "FUNDS", "FUNDY", "FUNGO", "FUNGS", "FUNKS", "FURAL", "FURAN", "FURCA", + "FURLS", "FUROL", "FURRS", "FURTH", "FURZE", "FURZY", "FUSED", "FUSEE", "FUSEL", "FUSES", "FUSIL", "FUSKS", + "FUSTS", "FUSTY", "FUTON", "FUZED", "FUZEE", "FUZES", "FUZIL", "FYCES", "FYKED", "FYKES", "FYLES", "FYRDS", + "FYTTE", "GABBA", "GABBY", "GABLE", "GADDI", "GADES", "GADGE", "GADID", "GADIS", "GADJE", "GADJO", "GADSO", + "GAFFS", "GAGED", "GAGER", "GAGES", "GAIDS", "GAINS", "GAIRS", "GAITA", "GAITS", "GAITT", "GAJOS", "GALAH", + "GALAS", "GALAX", "GALEA", "GALED", "GALES", "GALLS", "GALLY", "GALOP", "GALUT", "GALVO", "GAMAS", "GAMAY", + "GAMBA", "GAMBE", "GAMBO", "GAMBS", "GAMED", "GAMES", "GAMEY", "GAMIC", "GAMIN", "GAMME", "GAMMY", "GAMPS", + "GANCH", "GANDY", "GANEF", "GANEV", "GANGS", "GANJA", "GANOF", "GANTS", "GAOLS", "GAPED", "GAPER", "GAPES", + "GAPOS", "GAPPY", "GARBE", "GARBO", "GARBS", "GARDA", "GARES", "GARIS", "GARMS", "GARNI", "GARRE", "GARTH", + "GARUM", "GASES", "GASPS", "GASPY", "GASTS", "GATCH", "GATED", "GATER", "GATES", "GATHS", "GATOR", "GAUCH", + "GAUCY", "GAUDS", "GAUJE", "GAULT", "GAUMS", "GAUMY", "GAUPS", "GAURS", "GAUSS", "GAUZY", "GAVOT", "GAWCY", + "GAWDS", "GAWKS", "GAWPS", "GAWSY", "GAYAL", "GAZAL", "GAZAR", "GAZED", "GAZES", "GAZON", "GAZOO", "GEALS", + "GEANS", "GEARE", "GEARS", "GEATS", "GEBUR", "GECKS", "GEEKS", "GEEPS", "GEEST", "GEIST", "GEITS", "GELDS", + "GELEE", "GELID", "GELLY", "GELTS", "GEMEL", "GEMMA", "GEMMY", "GEMOT", "GENAL", "GENAS", "GENES", "GENET", + "GENIC", "GENII", "GENIP", "GENNY", "GENOA", "GENOM", "GENRO", "GENTS", "GENTY", "GENUA", "GENUS", "GEODE", + "GEOID", "GERAH", "GERBE", "GERES", "GERLE", "GERMS", "GERMY", "GERNE", "GESSE", "GESSO", "GESTE", "GESTS", + "GETAS", "GETUP", "GEUMS", "GEYAN", "GEYER", "GHAST", "GHATS", "GHAUT", "GHAZI", "GHEES", "GHEST", "GHYLL", + "GIBED", "GIBEL", "GIBER", "GIBES", "GIBLI", "GIBUS", "GIFTS", "GIGAS", "GIGHE", "GIGOT", "GIGUE", "GILAS", + "GILDS", "GILET", "GILLS", "GILLY", "GILPY", "GILTS", "GIMEL", "GIMME", "GIMPS", "GIMPY", "GINCH", "GINGE", + "GINGS", "GINKS", "GINNY", "GINZO", "GIPON", "GIPPO", "GIPPY", "GIRDS", "GIRLS", "GIRNS", "GIRON", "GIROS", + "GIRRS", "GIRSH", "GIRTS", "GISMO", "GISMS", "GISTS", "GITCH", "GITES", "GIUST", "GIVED", "GIVES", "GIZMO", + "GLACE", "GLADS", "GLADY", "GLAIK", "GLAIR", "GLAMS", "GLANS", "GLARY", "GLAUM", "GLAUR", "GLAZY", "GLEBA", + "GLEBE", "GLEBY", "GLEDE", "GLEDS", "GLEED", "GLEEK", "GLEES", "GLEET", "GLEIS", "GLENS", "GLENT", "GLEYS", + "GLIAL", "GLIAS", "GLIBS", "GLIFF", "GLIFT", "GLIKE", "GLIME", "GLIMS", "GLISK", "GLITS", "GLITZ", "GLOAM", + "GLOBI", "GLOBS", "GLOBY", "GLODE", "GLOGG", "GLOMS", "GLOOP", "GLOPS", "GLOST", "GLOUT", "GLOWS", "GLOZE", + "GLUED", "GLUER", "GLUES", "GLUEY", "GLUGS", "GLUME", "GLUMS", "GLUON", "GLUTE", "GLUTS", "GNARL", "GNARR", + "GNARS", "GNATS", "GNAWN", "GNAWS", "GNOWS", "GOADS", "GOAFS", "GOALS", "GOARY", "GOATS", "GOATY", "GOBAN", + "GOBAR", "GOBBI", "GOBBO", "GOBBY", "GOBIS", "GOBOS", "GODET", "GODSO", "GOELS", "GOERS", "GOEST", "GOETH", + "GOETY", "GOFER", "GOFFS", "GOGGA", "GOGOS", "GOIER", "GOJIS", "GOLDS", "GOLDY", "GOLES", "GOLFS", "GOLPE", + "GOLPS", "GOMBO", "GOMER", "GOMPA", "GONCH", "GONEF", "GONGS", "GONIA", "GONIF", "GONKS", "GONNA", "GONOF", + "GONYS", "GONZO", "GOOBY", "GOODS", "GOOFS", "GOOGS", "GOOKS", "GOOKY", "GOOLD", "GOOLS", "GOOLY", "GOONS", + "GOONY", "GOOPS", "GOOPY", "GOORS", "GOORY", "GOOSY", "GOPAK", "GOPIK", "GORAL", "GORAS", "GORED", "GORES", + "GORIS", "GORMS", "GORMY", "GORPS", "GORSE", "GORSY", "GOSHT", "GOSSE", "GOTCH", "GOTHS", "GOTHY", "GOTTA", + "GOUCH", "GOUKS", "GOURA", "GOUTS", "GOUTY", "GOWAN", "GOWDS", "GOWFS", "GOWKS", "GOWLS", "GOWNS", "GOXES", + "GOYIM", "GOYLE", "GRAAL", "GRABS", "GRADS", "GRAFF", "GRAIP", "GRAMA", "GRAME", "GRAMP", "GRAMS", "GRANA", + "GRANS", "GRAPY", "GRAVS", "GRAYS", "GREBE", "GREBO", "GRECE", "GREEK", "GREES", "GREGE", "GREGO", "GREIN", + "GRENS", "GRESE", "GREVE", "GREWS", "GREYS", "GRICE", "GRIDE", "GRIDS", "GRIFF", "GRIFT", "GRIGS", "GRIKE", + "GRINS", "GRIOT", "GRIPS", "GRIPT", "GRIPY", "GRISE", "GRIST", "GRISY", "GRITH", "GRITS", "GRIZE", "GROAT", + "GRODY", "GROGS", "GROKS", "GROMA", "GRONE", "GROOF", "GROSZ", "GROTS", "GROUF", "GROVY", "GROWS", "GRRLS", + "GRRRL", "GRUBS", "GRUED", "GRUES", "GRUFE", "GRUME", "GRUMP", "GRUND", "GRYCE", "GRYDE", "GRYKE", "GRYPE", + "GRYPT", "GUACO", "GUANA", "GUANO", "GUANS", "GUARS", "GUCKS", "GUCKY", "GUDES", "GUFFS", "GUGAS", "GUIDS", + "GUIMP", "GUIRO", "GULAG", "GULAR", "GULAS", "GULES", "GULET", "GULFS", "GULFY", "GULLS", "GULPH", "GULPS", + "GULPY", "GUMMA", "GUMMI", "GUMPS", "GUNDY", "GUNGE", "GUNGY", "GUNKS", "GUNKY", "GUNNY", "GUQIN", "GURDY", + "GURGE", "GURLS", "GURLY", "GURNS", "GURRY", "GURSH", "GURUS", "GUSHY", "GUSLA", "GUSLE", "GUSLI", "GUSSY", + "GUSTS", "GUTSY", "GUTTA", "GUTTY", "GUYED", "GUYLE", "GUYOT", "GUYSE", "GWINE", "GYALS", "GYANS", "GYBED", + "GYBES", "GYELD", "GYMPS", "GYNAE", "GYNIE", "GYNNY", "GYNOS", "GYOZA", "GYPOS", "GYPPO", "GYPPY", "GYRAL", + "GYRED", "GYRES", "GYRON", "GYROS", "GYRUS", "GYTES", "GYVED", "GYVES", "HAAFS", "HAARS", "HABLE", "HABUS", + "HACEK", "HACKS", "HADAL", "HADED", "HADES", "HADJI", "HADST", "HAEMS", "HAETS", "HAFFS", "HAFIZ", "HAFTS", + "HAGGS", "HAHAS", "HAICK", "HAIKA", "HAIKS", "HAIKU", "HAILS", "HAILY", "HAINS", "HAINT", "HAIRS", "HAITH", + "HAJES", "HAJIS", "HAJJI", "HAKAM", "HAKAS", "HAKEA", "HAKES", "HAKIM", "HAKUS", "HALAL", "HALED", "HALER", + "HALES", "HALFA", "HALFS", "HALID", "HALLO", "HALLS", "HALMA", "HALMS", "HALON", "HALOS", "HALSE", "HALTS", + "HALVA", "HALWA", "HAMAL", "HAMBA", "HAMED", "HAMES", "HAMMY", "HAMZA", "HANAP", "HANCE", "HANCH", "HANDS", + "HANGI", "HANGS", "HANKS", "HANKY", "HANSA", "HANSE", "HANTS", "HAOLE", "HAOMA", "HAPAX", "HAPLY", "HAPPI", + "HAPUS", "HARAM", "HARDS", "HARED", "HARES", "HARIM", "HARKS", "HARLS", "HARMS", "HARNS", "HAROS", "HARPS", + "HARTS", "HASHY", "HASKS", "HASPS", "HASTA", "HATED", "HATES", "HATHA", "HAUDS", "HAUFS", "HAUGH", "HAULD", + "HAULM", "HAULS", "HAULT", "HAUNS", "HAUSE", "HAVER", "HAVES", "HAWED", "HAWKS", "HAWMS", "HAWSE", "HAYED", + "HAYER", "HAYEY", "HAYLE", "HAZAN", "HAZED", "HAZER", "HAZES", "HEADS", "HEALD", "HEALS", "HEAME", "HEAPS", + "HEAPY", "HEARE", "HEARS", "HEAST", "HEATS", "HEBEN", "HEBES", "HECHT", "HECKS", "HEDER", "HEDGY", "HEEDS", + "HEEDY", "HEELS", "HEEZE", "HEFTE", "HEFTS", "HEIDS", "HEIGH", "HEILS", "HEIRS", "HEJAB", "HEJRA", "HELED", + "HELES", "HELIO", "HELLS", "HELMS", "HELOS", "HELOT", "HELPS", "HELVE", "HEMAL", "HEMES", "HEMIC", "HEMIN", + "HEMPS", "HEMPY", "HENCH", "HENDS", "HENGE", "HENNA", "HENNY", "HENRY", "HENTS", "HEPAR", "HERBS", "HERBY", + "HERDS", "HERES", "HERLS", "HERMA", "HERMS", "HERNS", "HEROS", "HERRY", "HERSE", "HERTZ", "HERYE", "HESPS", + "HESTS", "HETES", "HETHS", "HEUCH", "HEUGH", "HEVEA", "HEWED", "HEWER", "HEWGH", "HEXAD", "HEXED", "HEXER", + "HEXES", "HEXYL", "HEYED", "HIANT", "HICKS", "HIDED", "HIDER", "HIDES", "HIEMS", "HIGHS", "HIGHT", "HIJAB", + "HIJRA", "HIKED", "HIKER", "HIKES", "HIKOI", "HILAR", "HILCH", "HILLO", "HILLS", "HILTS", "HILUM", "HILUS", + "HIMBO", "HINAU", "HINDS", "HINGS", "HINKY", "HINNY", "HINTS", "HIOIS", "HIPLY", "HIRED", "HIREE", "HIRER", + "HIRES", "HISSY", "HISTS", "HITHE", "HIVED", "HIVER", "HIVES", "HIZEN", "HOAED", "HOAGY", "HOARS", "HOARY", + "HOAST", "HOBOS", "HOCKS", "HOCUS", "HODAD", "HODJA", "HOERS", "HOGAN", "HOGEN", "HOGGS", "HOGHS", "HOHED", + "HOICK", "HOIED", "HOIKS", "HOING", "HOISE", "HOKAS", "HOKED", "HOKES", "HOKEY", "HOKIS", "HOKKU", "HOKUM", + "HOLDS", "HOLED", "HOLES", "HOLEY", "HOLKS", "HOLLA", "HOLLO", "HOLME", "HOLMS", "HOLON", "HOLOS", "HOLTS", + "HOMAS", "HOMED", "HOMES", "HOMEY", "HOMIE", "HOMME", "HOMOS", "HONAN", "HONDA", "HONDS", "HONED", "HONER", + "HONES", "HONGI", "HONGS", "HONKS", "HONKY", "HOOCH", "HOODS", "HOODY", "HOOEY", "HOOFS", "HOOKA", "HOOKS", + "HOOKY", "HOOLY", "HOONS", "HOOPS", "HOORD", "HOORS", "HOOSH", "HOOTS", "HOOTY", "HOOVE", "HOPAK", "HOPED", + "HOPER", "HOPES", "HOPPY", "HORAH", "HORAL", "HORAS", "HORIS", "HORKS", "HORME", "HORNS", "HORST", "HORSY", + "HOSED", "HOSEL", "HOSEN", "HOSER", "HOSES", "HOSEY", "HOSTA", "HOSTS", "HOTCH", "HOTEN", "HOTTY", "HOUFF", + "HOUFS", "HOUGH", "HOURI", "HOURS", "HOUTS", "HOVEA", "HOVED", "HOVEN", "HOVES", "HOWBE", "HOWES", "HOWFF", + "HOWFS", "HOWKS", "HOWLS", "HOWRE", "HOWSO", "HOXED", "HOXES", "HOYAS", "HOYED", "HOYLE", "HUBBY", "HUCKS", + "HUDNA", "HUDUD", "HUERS", "HUFFS", "HUFFY", "HUGER", "HUGGY", "HUHUS", "HUIAS", "HULAS", "HULES", "HULKS", + "HULKY", "HULLO", "HULLS", "HULLY", "HUMAS", "HUMFS", "HUMIC", "HUMPS", "HUMPY", "HUNKS", "HUNTS", "HURDS", + "HURLS", "HURLY", "HURRA", "HURST", "HURTS", "HUSHY", "HUSKS", "HUSOS", "HUTIA", "HUZZA", "HUZZY", "HWYLS", + "HYDRA", "HYENS", "HYGGE", "HYING", "HYKES", "HYLAS", "HYLEG", "HYLES", "HYLIC", "HYMNS", "HYNDE", "HYOID", + "HYPED", "HYPES", "HYPHA", "HYPHY", "HYPOS", "HYRAX", "HYSON", "HYTHE", "IAMBI", "IAMBS", "IBRIK", "ICERS", + "ICHED", "ICHES", "ICHOR", "ICIER", "ICKER", "ICKLE", "ICONS", "ICTAL", "ICTIC", "ICTUS", "IDANT", "IDEAS", + "IDEES", "IDENT", "IDLED", "IDLES", "IDOLA", "IDOLS", "IDYLS", "IFTAR", "IGAPO", "IGGED", "IGLUS", "IHRAM", + "IKANS", "IKATS", "IKONS", "ILEAC", "ILEAL", "ILEUM", "ILEUS", "ILIAD", "ILIAL", "ILIUM", "ILLER", "ILLTH", + "IMAGO", "IMAMS", "IMARI", "IMAUM", "IMBAR", "IMBED", "IMIDE", "IMIDO", "IMIDS", "IMINE", "IMINO", "IMMEW", + "IMMIT", "IMMIX", "IMPED", "IMPIS", "IMPOT", "IMPRO", "IMSHI", "IMSHY", "INAPT", "INARM", "INBYE", "INCEL", + "INCLE", "INCOG", "INCUS", "INCUT", "INDEW", "INDIA", "INDIE", "INDOL", "INDOW", "INDRI", "INDUE", "INERM", + "INFIX", "INFOS", "INFRA", "INGAN", "INGLE", "INION", "INKED", "INKER", "INKLE", "INNED", "INNIT", "INORB", + "INRUN", "INSET", "INSPO", "INTEL", "INTIL", "INTIS", "INTRA", "INULA", "INURE", "INURN", "INUST", "INVAR", + "INWIT", "IODIC", "IODID", "IODIN", "IOTAS", "IPPON", "IRADE", "IRIDS", "IRING", "IRKED", "IROKO", "IRONE", + "IRONS", "ISBAS", "ISHES", "ISLED", "ISLES", "ISNAE", "ISSEI", "ISTLE", "ITEMS", "ITHER", "IVIED", "IVIES", + "IXIAS", "IXNAY", "IXORA", "IXTLE", "IZARD", "IZARS", "IZZAT", "JAAPS", "JABOT", "JACAL", "JACKS", "JACKY", + "JADED", "JADES", "JAFAS", "JAFFA", "JAGAS", "JAGER", "JAGGS", "JAGGY", "JAGIR", "JAGRA", "JAILS", "JAKER", + "JAKES", "JAKEY", "JALAP", "JALOP", "JAMBE", "JAMBO", "JAMBS", "JAMBU", "JAMES", "JAMMY", "JAMON", "JANES", + "JANNS", "JANNY", "JANTY", "JAPAN", "JAPED", "JAPER", "JAPES", "JARKS", "JARLS", "JARPS", "JARTA", "JARUL", + "JASEY", "JASPE", "JASPS", "JATOS", "JAUKS", "JAUPS", "JAVAS", "JAVEL", "JAWAN", "JAWED", "JAXIE", "JEANS", + "JEATS", "JEBEL", "JEDIS", "JEELS", "JEELY", "JEEPS", "JEERS", "JEEZE", "JEFES", "JEFFS", "JEHAD", "JEHUS", + "JELAB", "JELLO", "JELLS", "JEMBE", "JEMMY", "JENNY", "JEONS", "JERID", "JERKS", "JERRY", "JESSE", "JESTS", + "JESUS", "JETES", "JETON", "JEUNE", "JEWED", "JEWIE", "JHALA", "JIAOS", "JIBBA", "JIBBS", "JIBED", "JIBER", + "JIBES", "JIFFS", "JIGGY", "JIGOT", "JIHAD", "JILLS", "JILTS", "JIMMY", "JIMPY", "JINGO", "JINKS", "JINNE", + "JINNI", "JINNS", "JIRDS", "JIRGA", "JIRRE", "JISMS", "JIVED", "JIVER", "JIVES", "JIVEY", "JNANA", "JOBED", + "JOBES", "JOCKO", "JOCKS", "JOCKY", "JOCOS", "JODEL", "JOEYS", "JOHNS", "JOINS", "JOKED", "JOKES", "JOKEY", + "JOKOL", "JOLED", "JOLES", "JOLLS", "JOLTS", "JOLTY", "JOMON", "JOMOS", "JONES", "JONGS", "JONTY", "JOOKS", + "JORAM", "JORUM", "JOTAS", "JOTTY", "JOTUN", "JOUAL", "JOUGS", "JOUKS", "JOULE", "JOURS", "JOWAR", "JOWED", + "JOWLS", "JOWLY", "JOYED", "JUBAS", "JUBES", "JUCOS", "JUDAS", "JUDGY", "JUDOS", "JUGAL", "JUGUM", "JUJUS", + "JUKED", "JUKES", "JUKUS", "JULEP", "JUMAR", "JUMBY", "JUMPS", "JUNCO", "JUNKS", "JUNKY", "JUPES", "JUPON", + "JURAL", "JURAT", "JUREL", "JURES", "JUSTS", "JUTES", "JUTTY", "JUVES", "JUVIE", "KAAMA", "KABAB", "KABAR", + "KABOB", "KACHA", "KACKS", "KADAI", "KADES", "KADIS", "KAFIR", "KAGOS", "KAGUS", "KAHAL", "KAIAK", "KAIDS", + "KAIES", "KAIFS", "KAIKA", "KAIKS", "KAILS", "KAIMS", "KAING", "KAINS", "KAKAS", "KAKIS", "KALAM", "KALES", + "KALIF", "KALIS", "KALPA", "KAMAS", "KAMES", "KAMIK", "KAMIS", "KAMME", "KANAE", "KANAS", "KANDY", "KANEH", + "KANES", "KANGA", "KANGS", "KANJI", "KANTS", "KANZU", "KAONS", "KAPAS", "KAPHS", "KAPOK", "KAPOW", "KAPUS", + "KAPUT", "KARAS", "KARAT", "KARKS", "KARNS", "KAROO", "KAROS", "KARRI", "KARST", "KARSY", "KARTS", "KARZY", + "KASHA", "KASME", "KATAL", "KATAS", "KATIS", "KATTI", "KAUGH", "KAURI", "KAURU", "KAURY", "KAVAL", "KAVAS", + "KAWAS", "KAWAU", "KAWED", "KAYLE", "KAYOS", "KAZIS", "KAZOO", "KBARS", "KEBAR", "KEBOB", "KECKS", "KEDGE", + "KEDGY", "KEECH", "KEEFS", "KEEKS", "KEELS", "KEEMA", "KEENO", "KEENS", "KEEPS", "KEETS", "KEEVE", "KEFIR", + "KEHUA", "KEIRS", "KELEP", "KELIM", "KELLS", "KELLY", "KELPS", "KELPY", "KELTS", "KELTY", "KEMBO", "KEMBS", + "KEMPS", "KEMPT", "KEMPY", "KENAF", "KENCH", "KENDO", "KENOS", "KENTE", "KENTS", "KEPIS", "KERBS", "KEREL", + "KERFS", "KERKY", "KERMA", "KERNE", "KERNS", "KEROS", "KERRY", "KERVE", "KESAR", "KESTS", "KETAS", "KETCH", + "KETES", "KETOL", "KEVEL", "KEVIL", "KEXES", "KEYED", "KEYER", "KHADI", "KHAFS", "KHANS", "KHAPH", "KHATS", + "KHAYA", "KHAZI", "KHEDA", "KHETH", "KHETS", "KHOJA", "KHORS", "KHOUM", "KHUDS", "KIAAT", "KIACK", "KIANG", + "KIBBE", "KIBBI", "KIBEI", "KIBES", "KIBLA", "KICKS", "KICKY", "KIDDO", "KIDDY", "KIDEL", "KIDGE", "KIEFS", + "KIERS", "KIEVE", "KIEVS", "KIGHT", "KIKES", "KIKOI", "KILEY", "KILIM", "KILLS", "KILNS", "KILOS", "KILPS", + "KILTS", "KILTY", "KIMBO", "KINAS", "KINDA", "KINDS", "KINDY", "KINES", "KINGS", "KININ", "KINKS", "KINOS", + "KIORE", "KIPES", "KIPPA", "KIPPS", "KIRBY", "KIRKS", "KIRNS", "KIRRI", "KISAN", "KISSY", "KISTS", "KITED", + "KITER", "KITES", "KITHE", "KITHS", "KITUL", "KIVAS", "KIWIS", "KLANG", "KLAPS", "KLETT", "KLICK", "KLIEG", + "KLIKS", "KLONG", "KLOOF", "KLUGE", "KLUTZ", "KNAGS", "KNAPS", "KNARL", "KNARS", "KNAUR", "KNAWE", "KNEES", + "KNELL", "KNISH", "KNITS", "KNIVE", "KNOBS", "KNOPS", "KNOSP", "KNOTS", "KNOUT", "KNOWE", "KNOWS", "KNUBS", + "KNURL", "KNURR", "KNURS", "KNUTS", "KOANS", "KOAPS", "KOBAN", "KOBOS", "KOELS", "KOFFS", "KOFTA", "KOGAL", + "KOHAS", "KOHEN", "KOHLS", "KOINE", "KOJIS", "KOKAM", "KOKAS", "KOKER", "KOKRA", "KOKUM", "KOLAS", "KOLOS", + "KOMBU", "KONBU", "KONDO", "KONKS", "KOOKS", "KOOKY", "KOORI", "KOPEK", "KOPHS", "KOPJE", "KOPPA", "KORAI", + "KORAN", "KORAS", "KORAT", "KORES", "KORMA", "KOROS", "KORUN", "KORUS", "KOSES", "KOTCH", "KOTOS", "KOTOW", + "KOURA", "KRAAL", "KRABS", "KRAFT", "KRAIS", "KRAIT", "KRANG", "KRANS", "KRANZ", "KRAUT", "KRAYS", "KREEP", + "KRENG", "KREWE", "KRONA", "KRONE", "KROON", "KRUBI", "KRUNK", "KSARS", "KUBIE", "KUDOS", "KUDUS", "KUDZU", + "KUFIS", "KUGEL", "KUIAS", "KUKRI", "KUKUS", "KULAK", "KULAN", "KULAS", "KULFI", "KUMIS", "KUMYS", "KURIS", + "KURRE", "KURTA", "KURUS", "KUSSO", "KUTAS", "KUTCH", "KUTIS", "KUTUS", "KUZUS", "KVASS", "KVELL", "KWELA", + "KYACK", "KYAKS", "KYANG", "KYARS", "KYATS", "KYBOS", "KYDST", "KYLES", "KYLIE", "KYLIN", "KYLIX", "KYLOE", + "KYNDE", "KYNDS", "KYPES", "KYRIE", "KYTES", "KYTHE", "LAARI", "LABDA", "LABIA", "LABIS", "LABRA", "LACED", + "LACER", "LACES", "LACET", "LACEY", "LACKS", "LADDY", "LADED", "LADER", "LADES", "LAERS", "LAEVO", "LAGAN", + "LAHAL", "LAHAR", "LAICH", "LAICS", "LAIDS", "LAIGH", "LAIKA", "LAIKS", "LAIRD", "LAIRS", "LAIRY", "LAITH", + "LAITY", "LAKED", "LAKER", "LAKES", "LAKHS", "LAKIN", "LAKSA", "LALDY", "LALLS", "LAMAS", "LAMBS", "LAMBY", + "LAMED", "LAMER", "LAMES", "LAMIA", "LAMMY", "LAMPS", "LANAI", "LANAS", "LANCH", "LANDE", "LANDS", "LANES", + "LANKS", "LANTS", "LAPIN", "LAPIS", "LAPJE", "LARCH", "LARDS", "LARDY", "LAREE", "LARES", "LARGO", "LARIS", + "LARKS", "LARKY", "LARNS", "LARNT", "LARUM", "LASED", "LASER", "LASES", "LASSI", "LASSU", "LASSY", "LASTS", + "LATAH", "LATED", "LATEN", "LATEX", "LATHI", "LATHS", "LATHY", "LATKE", "LATUS", "LAUAN", "LAUCH", "LAUDS", + "LAUFS", "LAUND", "LAURA", "LAVAL", "LAVAS", "LAVED", "LAVER", "LAVES", "LAVRA", "LAVVY", "LAWED", "LAWER", + "LAWIN", "LAWKS", "LAWNS", "LAWNY", "LAXED", "LAXER", "LAXES", "LAXLY", "LAYED", "LAYIN", "LAYUP", "LAZAR", + "LAZED", "LAZES", "LAZOS", "LAZZI", "LAZZO", "LEADS", "LEADY", "LEAFS", "LEAKS", "LEAMS", "LEANS", "LEANY", + "LEAPS", "LEARE", "LEARS", "LEARY", "LEATS", "LEAVY", "LEAZE", "LEBEN", "LECCY", "LEDES", "LEDGY", "LEDUM", + "LEEAR", "LEEKS", "LEEPS", "LEERS", "LEESE", "LEETS", "LEEZE", "LEFTE", "LEFTS", "LEGER", "LEGES", "LEGGE", + "LEGGO", "LEGIT", "LEHRS", "LEHUA", "LEIRS", "LEISH", "LEMAN", "LEMED", "LEMEL", "LEMES", "LEMMA", "LEMME", + "LENDS", "LENES", "LENGS", "LENIS", "LENOS", "LENSE", "LENTI", "LENTO", "LEONE", "LEPID", "LEPRA", "LEPTA", + "LERED", "LERES", "LERPS", "LESBO", "LESES", "LESTS", "LETCH", "LETHE", "LETUP", "LEUCH", "LEUCO", "LEUDS", + "LEUGH", "LEVAS", "LEVEE", "LEVES", "LEVIN", "LEVIS", "LEWIS", "LEXES", "LEXIS", "LEZES", "LEZZA", "LEZZY", + "LIANA", "LIANE", "LIANG", "LIARD", "LIARS", "LIART", "LIBER", "LIBRA", "LIBRI", "LICHI", "LICHT", "LICIT", + "LICKS", "LIDAR", "LIDOS", "LIEFS", "LIENS", "LIERS", "LIEUS", "LIEVE", "LIFER", "LIFES", "LIFTS", "LIGAN", + "LIGER", "LIGGE", "LIGNE", "LIKED", "LIKER", "LIKES", "LIKIN", "LILLS", "LILOS", "LILTS", "LIMAN", "LIMAS", + "LIMAX", "LIMBA", "LIMBI", "LIMBS", "LIMBY", "LIMED", "LIMEN", "LIMES", "LIMEY", "LIMMA", "LIMNS", "LIMOS", + "LIMPA", "LIMPS", "LINAC", "LINCH", "LINDS", "LINDY", "LINED", "LINES", "LINEY", "LINGA", "LINGS", "LINGY", + "LININ", "LINKS", "LINKY", "LINNS", "LINNY", "LINOS", "LINTS", "LINTY", "LINUM", "LINUX", "LIONS", "LIPAS", + "LIPES", "LIPIN", "LIPOS", "LIPPY", "LIRAS", "LIRKS", "LIROT", "LISKS", "LISLE", "LISPS", "LISTS", "LITAI", + "LITAS", "LITED", "LITER", "LITES", "LITHO", "LITHS", "LITRE", "LIVED", "LIVEN", "LIVES", "LIVOR", "LIVRE", + "LLANO", "LOACH", "LOADS", "LOAFS", "LOAMS", "LOANS", "LOAST", "LOAVE", "LOBAR", "LOBED", "LOBES", "LOBOS", + "LOBUS", "LOCHE", "LOCHS", "LOCIE", "LOCIS", "LOCKS", "LOCOS", "LOCUM", "LODEN", "LODES", "LOESS", "LOFTS", + "LOGAN", "LOGES", "LOGGY", "LOGIA", "LOGIE", "LOGOI", "LOGON", "LOGOS", "LOHAN", "LOIDS", "LOINS", "LOIPE", + "LOIRS", "LOKES", "LOLLS", "LOLLY", "LOLOG", "LOMAS", "LOMED", "LOMES", "LONER", "LONGA", "LONGE", "LONGS", + "LOOBY", "LOOED", "LOOEY", "LOOFA", "LOOFS", "LOOIE", "LOOKS", "LOOKY", "LOOMS", "LOONS", "LOONY", "LOOPS", + "LOORD", "LOOTS", "LOPED", "LOPER", "LOPES", "LOPPY", "LORAL", "LORAN", "LORDS", "LORDY", "LOREL", "LORES", + "LORIC", "LORIS", "LOSED", "LOSEL", "LOSEN", "LOSES", "LOSSY", "LOTAH", "LOTAS", "LOTES", "LOTIC", "LOTOS", + "LOTSA", "LOTTA", "LOTTE", "LOTTO", "LOTUS", "LOUED", "LOUGH", "LOUIE", "LOUIS", "LOUMA", "LOUND", "LOUNS", + "LOUPE", "LOUPS", "LOURE", "LOURS", "LOURY", "LOUTS", "LOVAT", "LOVED", "LOVES", "LOVEY", "LOVIE", "LOWAN", + "LOWED", "LOWES", "LOWND", "LOWNE", "LOWNS", "LOWPS", "LOWRY", "LOWSE", "LOWTS", "LOXED", "LOXES", "LOZEN", + "LUACH", "LUAUS", "LUBED", "LUBES", "LUBRA", "LUCES", "LUCKS", "LUCRE", "LUDES", "LUDIC", "LUDOS", "LUFFA", + "LUFFS", "LUGED", "LUGER", "LUGES", "LULLS", "LULUS", "LUMAS", "LUMBI", "LUMME", "LUMMY", "LUMPS", "LUNAS", + "LUNES", "LUNET", "LUNGI", "LUNGS", "LUNKS", "LUNTS", "LUPIN", "LURED", "LURER", "LURES", "LUREX", "LURGI", + "LURGY", "LURKS", "LURRY", "LURVE", "LUSER", "LUSHY", "LUSKS", "LUSTS", "LUSUS", "LUTEA", "LUTED", "LUTER", + "LUTES", "LUVVY", "LUXED", "LUXER", "LUXES", "LWEIS", "LYAMS", "LYARD", "LYART", "LYASE", "LYCEA", "LYCEE", + "LYCRA", "LYMES", "LYNCH", "LYNES", "LYRES", "LYSED", "LYSES", "LYSIN", "LYSIS", "LYSOL", "LYSSA", "LYTED", + "LYTES", "LYTHE", "LYTIC", "LYTTA", "MAAED", "MAARE", "MAARS", "MABES", "MACAS", "MACED", "MACER", "MACES", + "MACHE", "MACHI", "MACHS", "MACKS", "MACLE", "MACON", "MADGE", "MADID", "MADRE", "MAERL", "MAFIC", "MAGES", + "MAGGS", "MAGOT", "MAGUS", "MAHOE", "MAHUA", "MAHWA", "MAIDS", "MAIKO", "MAIKS", "MAILE", "MAILL", "MAILS", + "MAIMS", "MAINS", "MAIRE", "MAIRS", "MAISE", "MAIST", "MAKAR", "MAKES", "MAKIS", "MAKOS", "MALAM", "MALAR", + "MALAS", "MALAX", "MALES", "MALIC", "MALIK", "MALIS", "MALLS", "MALMS", "MALMY", "MALTS", "MALTY", "MALUS", + "MALVA", "MALWA", "MAMAS", "MAMBA", "MAMEE", "MAMEY", "MAMIE", "MANAS", "MANAT", "MANDI", "MANEB", "MANED", + "MANEH", "MANES", "MANET", "MANGS", "MANIS", "MANKY", "MANNA", "MANOS", "MANSE", "MANTA", "MANTO", "MANTY", + "MANUL", "MANUS", "MAPAU", "MAQUI", "MARAE", "MARAH", "MARAS", "MARCS", "MARDY", "MARES", "MARGE", "MARGS", + "MARIA", "MARID", "MARKA", "MARKS", "MARLE", "MARLS", "MARLY", "MARMS", "MARON", "MAROR", "MARRA", "MARRI", + "MARSE", "MARTS", "MARVY", "MASAS", "MASED", "MASER", "MASES", "MASHY", "MASKS", "MASSA", "MASSY", "MASTS", + "MASTY", "MASUS", "MATAI", "MATED", "MATER", "MATES", "MATHS", "MATIN", "MATLO", "MATTE", "MATTS", "MATZA", + "MATZO", "MAUBY", "MAUDS", "MAULS", "MAUND", "MAURI", "MAUSY", "MAUTS", "MAUZY", "MAVEN", "MAVIE", "MAVIN", + "MAVIS", "MAWED", "MAWKS", "MAWKY", "MAWNS", "MAWRS", "MAXED", "MAXES", "MAXIS", "MAYAN", "MAYAS", "MAYED", + "MAYOS", "MAYST", "MAZED", "MAZER", "MAZES", "MAZEY", "MAZUT", "MBIRA", "MEADS", "MEALS", "MEANE", "MEANS", + "MEANY", "MEARE", "MEASE", "MEATH", "MEATS", "MEBOS", "MECHS", "MECKS", "MEDII", "MEDLE", "MEEDS", "MEERS", + "MEETS", "MEFFS", "MEINS", "MEINT", "MEINY", "MEITH", "MEKKA", "MELAS", "MELBA", "MELDS", "MELIC", "MELIK", + "MELLS", "MELTS", "MELTY", "MEMES", "MEMOS", "MENAD", "MENDS", "MENED", "MENES", "MENGE", "MENGS", "MENSA", + "MENSE", "MENSH", "MENTA", "MENTO", "MENUS", "MEOUS", "MEOWS", "MERCH", "MERCS", "MERDE", "MERED", "MEREL", + "MERER", "MERES", "MERIL", "MERIS", "MERKS", "MERLE", "MERLS", "MERSE", "MESAL", "MESAS", "MESEL", "MESES", + "MESHY", "MESIC", "MESNE", "MESON", "MESSY", "MESTO", "METED", "METES", "METHO", "METHS", "METIC", "METIF", + "METIS", "METOL", "METRE", "MEUSE", "MEVED", "MEVES", "MEWED", "MEWLS", "MEYNT", "MEZES", "MEZZE", "MEZZO", + "MHORR", "MIAOU", "MIAOW", "MIASM", "MIAUL", "MICAS", "MICHE", "MICHT", "MICKS", "MICKY", "MICOS", "MICRA", + "MIDDY", "MIDGY", "MIDIS", "MIENS", "MIEVE", "MIFFS", "MIFFY", "MIFTY", "MIGGS", "MIHAS", "MIHIS", "MIKED", + "MIKES", "MIKRA", "MIKVA", "MILCH", "MILDS", "MILER", "MILES", "MILFS", "MILIA", "MILKO", "MILKS", "MILLE", + "MILLS", "MILOR", "MILOS", "MILPA", "MILTS", "MILTY", "MILTZ", "MIMED", "MIMEO", "MIMER", "MIMES", "MIMSY", + "MINAE", "MINAR", "MINAS", "MINCY", "MINDS", "MINED", "MINES", "MINGE", "MINGS", "MINGY", "MINIS", "MINKE", + "MINKS", "MINNY", "MINOS", "MINTS", "MIRED", "MIRES", "MIREX", "MIRID", "MIRIN", "MIRKS", "MIRKY", "MIRLY", + "MIROS", "MIRVS", "MIRZA", "MISCH", "MISDO", "MISES", "MISGO", "MISOS", "MISSA", "MISTS", "MISTY", "MITCH", + "MITER", "MITES", "MITIS", "MITRE", "MITTS", "MIXED", "MIXEN", "MIXER", "MIXES", "MIXTE", "MIXUP", "MIZEN", + "MIZZY", "MNEME", "MOANS", "MOATS", "MOBBY", "MOBES", "MOBEY", "MOBIE", "MOBLE", "MOCHI", "MOCHS", "MOCHY", + "MOCKS", "MODER", "MODES", "MODGE", "MODII", "MODUS", "MOERS", "MOFOS", "MOGGY", "MOHEL", "MOHOS", "MOHRS", + "MOHUA", "MOHUR", "MOILE", "MOILS", "MOIRA", "MOIRE", "MOITS", "MOJOS", "MOKES", "MOKIS", "MOKOS", "MOLAL", + "MOLAS", "MOLDS", "MOLED", "MOLES", "MOLLA", "MOLLS", "MOLLY", "MOLTO", "MOLTS", "MOLYS", "MOMES", "MOMMA", + "MOMMY", "MOMUS", "MONAD", "MONAL", "MONAS", "MONDE", "MONDO", "MONER", "MONGO", "MONGS", "MONIC", "MONIE", + "MONKS", "MONOS", "MONTE", "MONTY", "MOOBS", "MOOCH", "MOODS", "MOOED", "MOOKS", "MOOLA", "MOOLI", "MOOLS", + "MOOLY", "MOONG", "MOONS", "MOONY", "MOOPS", "MOORS", "MOORY", "MOOTS", "MOOVE", "MOPED", "MOPER", "MOPES", + "MOPEY", "MOPPY", "MOPSY", "MOPUS", "MORAE", "MORAS", "MORAT", "MORAY", "MOREL", "MORES", "MORIA", "MORNE", + "MORNS", "MORRA", "MORRO", "MORSE", "MORTS", "MOSED", "MOSES", "MOSEY", "MOSKS", "MOSSO", "MOSTE", "MOSTS", + "MOTED", "MOTEN", "MOTES", "MOTET", "MOTEY", "MOTHS", "MOTHY", "MOTIS", "MOTTE", "MOTTS", "MOTTY", "MOTUS", + "MOTZA", "MOUCH", "MOUES", "MOULD", "MOULS", "MOUPS", "MOUST", "MOUSY", "MOVED", "MOVES", "MOWAS", "MOWED", + "MOWRA", "MOXAS", "MOXIE", "MOYAS", "MOYLE", "MOYLS", "MOZED", "MOZES", "MOZOS", "MPRET", "MUCHO", "MUCIC", + "MUCID", "MUCIN", "MUCKS", "MUCOR", "MUCRO", "MUDGE", "MUDIR", "MUDRA", "MUFFS", "MUFTI", "MUGGA", "MUGGS", + "MUGGY", "MUHLY", "MUIDS", "MUILS", "MUIRS", "MUIST", "MUJIK", "MULCT", "MULED", "MULES", "MULEY", "MULGA", + "MULIE", "MULLA", "MULLS", "MULSE", "MULSH", "MUMMS", "MUMPS", "MUMSY", "MUMUS", "MUNGA", "MUNGE", "MUNGO", + "MUNGS", "MUNIS", "MUNTS", "MUNTU", "MUONS", "MURAS", "MURED", "MURES", "MUREX", "MURID", "MURKS", "MURLS", + "MURLY", "MURRA", "MURRE", "MURRI", "MURRS", "MURRY", "MURTI", "MURVA", "MUSAR", "MUSCA", "MUSED", "MUSER", + "MUSES", "MUSET", "MUSHA", "MUSIT", "MUSKS", "MUSOS", "MUSSE", "MUSSY", "MUSTH", "MUSTS", "MUTCH", "MUTED", + "MUTER", "MUTES", "MUTHA", "MUTIS", "MUTON", "MUTTS", "MUXED", "MUXES", "MUZAK", "MUZZY", "MVULE", "MYALL", + "MYLAR", "MYNAH", "MYNAS", "MYOID", "MYOMA", "MYOPE", "MYOPS", "MYOPY", "MYSID", "MYTHI", "MYTHS", "MYTHY", + "MYXOS", "MZEES", "NAAMS", "NAANS", "NABES", "NABIS", "NABKS", "NABLA", "NABOB", "NACHE", "NACHO", "NACRE", + "NADAS", "NAEVE", "NAEVI", "NAFFS", "NAGAS", "NAGGY", "NAGOR", "NAHAL", "NAIAD", "NAIFS", "NAIKS", "NAILS", + "NAIRA", "NAIRU", "NAKED", "NAKER", "NAKFA", "NALAS", "NALED", "NALLA", "NAMED", "NAMER", "NAMES", "NAMMA", + "NAMUS", "NANAS", "NANCE", "NANCY", "NANDU", "NANNA", "NANOS", "NANUA", "NAPAS", "NAPED", "NAPES", "NAPOO", + "NAPPA", "NAPPE", "NAPPY", "NARAS", "NARCO", "NARCS", "NARDS", "NARES", "NARIC", "NARIS", "NARKS", "NARKY", + "NARRE", "NASHI", "NATCH", "NATES", "NATIS", "NATTY", "NAUCH", "NAUNT", "NAVAR", "NAVES", "NAVEW", "NAVVY", + "NAWAB", "NAZES", "NAZIR", "NAZIS", "NDUJA", "NEAFE", "NEALS", "NEAPS", "NEARS", "NEATH", "NEATS", "NEBEK", + "NEBEL", "NECKS", "NEDDY", "NEEDS", "NEELD", "NEELE", "NEEMB", "NEEMS", "NEEPS", "NEESE", "NEEZE", "NEGRO", + "NEGUS", "NEIFS", "NEIST", "NEIVE", "NELIS", "NELLY", "NEMAS", "NEMNS", "NEMPT", "NENES", "NEONS", "NEPER", + "NEPIT", "NERAL", "NERDS", "NERKA", "NERKS", "NEROL", "NERTS", "NERTZ", "NERVY", "NESTS", "NETES", "NETOP", + "NETTS", "NETTY", "NEUKS", "NEUME", "NEUMS", "NEVEL", "NEVES", "NEVUS", "NEWBS", "NEWED", "NEWEL", "NEWIE", + "NEWSY", "NEWTS", "NEXTS", "NEXUS", "NGAIO", "NGANA", "NGATI", "NGOMA", "NGWEE", "NICAD", "NICHT", "NICKS", + "NICOL", "NIDAL", "NIDED", "NIDES", "NIDOR", "NIDUS", "NIEFS", "NIEVE", "NIFES", "NIFFS", "NIFFY", "NIFTY", + "NIGER", "NIGHS", "NIHIL", "NIKAB", "NIKAH", "NIKAU", "NILLS", "NIMBI", "NIMBS", "NIMPS", "NINER", "NINES", + "NINON", "NIPAS", "NIPPY", "NIQAB", "NIRLS", "NIRLY", "NISEI", "NISSE", "NISUS", "NITER", "NITES", "NITID", + "NITON", "NITRE", "NITRO", "NITRY", "NITTY", "NIVAL", "NIXED", "NIXER", "NIXES", "NIXIE", "NIZAM", "NKOSI", + "NOAHS", "NOBBY", "NOCKS", "NODAL", "NODDY", "NODES", "NODUS", "NOELS", "NOGGS", "NOHOW", "NOILS", "NOILY", + "NOINT", "NOIRS", "NOLES", "NOLLS", "NOLOS", "NOMAS", "NOMEN", "NOMES", "NOMIC", "NOMOI", "NOMOS", "NONAS", + "NONCE", "NONES", "NONET", "NONGS", "NONIS", "NONNY", "NONYL", "NOOBS", "NOOIT", "NOOKS", "NOOKY", "NOONS", + "NOOPS", "NOPAL", "NORIA", "NORIS", "NORKS", "NORMA", "NORMS", "NOSED", "NOSER", "NOSES", "NOTAL", "NOTED", + "NOTER", "NOTES", "NOTUM", "NOULD", "NOULE", "NOULS", "NOUNS", "NOUNY", "NOUPS", "NOVAE", "NOVAS", "NOVUM", + "NOWAY", "NOWED", "NOWLS", "NOWTS", "NOWTY", "NOXAL", "NOXES", "NOYAU", "NOYED", "NOYES", "NUBBY", "NUBIA", + "NUCHA", "NUDDY", "NUDER", "NUDES", "NUDIE", "NUDZH", "NUFFS", "NUGAE", "NUKED", "NUKES", "NULLA", "NULLS", + "NUMBS", "NUMEN", "NUMMY", "NUNNY", "NURDS", "NURDY", "NURLS", "NURRS", "NUTSO", "NUTSY", "NYAFF", "NYALA", + "NYING", "NYSSA", "OAKED", "OAKER", "OAKUM", "OARED", "OASES", "OASIS", "OASTS", "OATEN", "OATER", "OATHS", + "OAVES", "OBANG", "OBEAH", "OBELI", "OBEYS", "OBIAS", "OBIED", "OBIIT", "OBITS", "OBJET", "OBOES", "OBOLE", + "OBOLI", "OBOLS", "OCCAM", "OCHER", "OCHES", "OCHRE", "OCHRY", "OCKER", "OCREA", "OCTAD", "OCTAN", "OCTAS", + "OCTYL", "OCULI", "ODAHS", "ODALS", "ODEON", "ODEUM", "ODISM", "ODIST", "ODIUM", "ODORS", "ODOUR", "ODYLE", + "ODYLS", "OFAYS", "OFFED", "OFFIE", "OFLAG", "OFTER", "OGAMS", "OGEED", "OGEES", "OGGIN", "OGHAM", "OGIVE", + "OGLED", "OGLER", "OGLES", "OGMIC", "OGRES", "OHIAS", "OHING", "OHMIC", "OHONE", "OIDIA", "OILED", "OILER", + "OINKS", "OINTS", "OJIME", "OKAPI", "OKAYS", "OKEHS", "OKRAS", "OKTAS", "OLDIE", "OLEIC", "OLEIN", "OLENT", + "OLEOS", "OLEUM", "OLIOS", "OLLAS", "OLLAV", "OLLER", "OLLIE", "OLOGY", "OLPAE", "OLPES", "OMASA", "OMBER", + "OMBUS", "OMENS", "OMERS", "OMITS", "OMLAH", "OMOVS", "OMRAH", "ONCER", "ONCES", "ONCET", "ONCUS", "ONELY", + "ONERS", "ONERY", "ONIUM", "ONKUS", "ONLAY", "ONNED", "ONTIC", "OOBIT", "OOHED", "OOMPH", "OONTS", "OOPED", + "OORIE", "OOSES", "OOTID", "OOZED", "OOZES", "OPAHS", "OPALS", "OPENS", "OPEPE", "OPING", "OPPOS", "OPSIN", + "OPTED", "OPTER", "ORACH", "ORACY", "ORALS", "ORANG", "ORANT", "ORATE", "ORBED", "ORCAS", "ORCIN", "ORDOS", + "OREAD", "ORFES", "ORGIA", "ORGIC", "ORGUE", "ORIBI", "ORIEL", "ORIXA", "ORLES", "ORLON", "ORLOP", "ORMER", + "ORNIS", "ORPIN", "ORRIS", "ORTHO", "ORVAL", "ORZOS", "OSCAR", "OSHAC", "OSIER", "OSMIC", "OSMOL", "OSSIA", + "OSTIA", "OTAKU", "OTARY", "OTTAR", "OTTOS", "OUBIT", "OUCHT", "OUENS", "OUIJA", "OULKS", "OUMAS", "OUNDY", + "OUPAS", "OUPED", "OUPHE", "OUPHS", "OURIE", "OUSEL", "OUSTS", "OUTBY", "OUTED", "OUTRE", "OUTRO", "OUTTA", + "OUZEL", "OUZOS", "OVALS", "OVELS", "OVENS", "OVERS", "OVIST", "OVOLI", "OVOLO", "OVULE", "OWCHE", "OWIES", + "OWLED", "OWLER", "OWLET", "OWNED", "OWRES", "OWRIE", "OWSEN", "OXBOW", "OXERS", "OXEYE", "OXIDS", "OXIES", + "OXIME", "OXIMS", "OXLIP", "OXTER", "OYERS", "OZEKI", "OZZIE", "PAALS", "PAANS", "PACAS", "PACED", "PACER", + "PACES", "PACEY", "PACHA", "PACKS", "PACOS", "PACTA", "PACTS", "PADIS", "PADLE", "PADMA", "PADRE", "PADRI", + "PAEAN", "PAEDO", "PAEON", "PAGED", "PAGER", "PAGES", "PAGLE", "PAGOD", "PAGRI", "PAIKS", "PAILS", "PAINS", + "PAIRE", "PAIRS", "PAISA", "PAISE", "PAKKA", "PALAS", "PALAY", "PALEA", "PALED", "PALES", "PALET", "PALIS", + "PALKI", "PALLA", "PALLS", "PALLY", "PALMS", "PALMY", "PALPI", "PALPS", "PALSA", "PAMPA", "PANAX", "PANCE", + "PANDA", "PANDS", "PANDY", "PANED", "PANES", "PANGA", "PANGS", "PANIM", "PANKO", "PANNE", "PANNI", "PANTO", + "PANTS", "PANTY", "PAOLI", "PAOLO", "PAPAS", "PAPAW", "PAPES", "PAPPI", "PAPPY", "PARAE", "PARAS", "PARCH", + "PARDI", "PARDS", "PARDY", "PARED", "PAREN", "PAREO", "PARES", "PAREU", "PAREV", "PARGE", "PARGO", "PARIS", + "PARKI", "PARKS", "PARKY", "PARLE", "PARLY", "PARMA", "PAROL", "PARPS", "PARRA", "PARRS", "PARTI", "PARTS", + "PARVE", "PARVO", "PASEO", "PASES", "PASHA", "PASHM", "PASKA", "PASPY", "PASSE", "PASTS", "PATED", "PATEN", + "PATER", "PATES", "PATHS", "PATIN", "PATKA", "PATLY", "PATTE", "PATUS", "PAUAS", "PAULS", "PAVAN", "PAVED", + "PAVEN", "PAVER", "PAVES", "PAVID", "PAVIN", "PAVIS", "PAWAS", "PAWAW", "PAWED", "PAWER", "PAWKS", "PAWKY", + "PAWLS", "PAWNS", "PAXES", "PAYED", "PAYOR", "PAYSD", "PEAGE", "PEAGS", "PEAKS", "PEAKY", "PEALS", "PEANS", + "PEARE", "PEARS", "PEART", "PEASE", "PEATS", "PEATY", "PEAVY", "PEAZE", "PEBAS", "PECHS", "PECKE", "PECKS", + "PECKY", "PEDES", "PEDIS", "PEDRO", "PEECE", "PEEKS", "PEELS", "PEENS", "PEEOY", "PEEPE", "PEEPS", "PEERS", + "PEERY", "PEEVE", "PEGGY", "PEGHS", "PEINS", "PEISE", "PEIZE", "PEKAN", "PEKES", "PEKIN", "PEKOE", "PELAS", + "PELAU", "PELES", "PELFS", "PELLS", "PELMA", "PELON", "PELTA", "PELTS", "PENDS", "PENDU", "PENED", "PENES", + "PENGO", "PENIE", "PENIS", "PENKS", "PENNA", "PENNI", "PENTS", "PEONS", "PEONY", "PEPLA", "PEPOS", "PEPPY", + "PEPSI", "PERAI", "PERCE", "PERCS", "PERDU", "PERDY", "PEREA", "PERES", "PERIS", "PERKS", "PERMS", "PERNS", + "PEROG", "PERPS", "PERRY", "PERSE", "PERST", "PERTS", "PERVE", "PERVO", "PERVS", "PERVY", "PESOS", "PESTS", + "PESTY", "PETAR", "PETER", "PETIT", "PETRE", "PETRI", "PETTI", "PETTO", "PEWEE", "PEWIT", "PEYSE", "PHAGE", + "PHANG", "PHARE", "PHARM", "PHEER", "PHENE", "PHEON", "PHESE", "PHIAL", "PHISH", "PHIZZ", "PHLOX", "PHOCA", + "PHONO", "PHONS", "PHOTS", "PHPHT", "PHUTS", "PHYLA", "PHYLE", "PIANI", "PIANS", "PIBAL", "PICAL", "PICAS", + "PICCY", "PICKS", "PICOT", "PICRA", "PICUL", "PIEND", "PIERS", "PIERT", "PIETA", "PIETS", "PIEZO", "PIGHT", + "PIGMY", "PIING", "PIKAS", "PIKAU", "PIKED", "PIKER", "PIKES", "PIKEY", "PIKIS", "PIKUL", "PILAE", "PILAF", + "PILAO", "PILAR", "PILAU", "PILAW", "PILCH", "PILEA", "PILED", "PILEI", "PILER", "PILES", "PILIS", "PILLS", + "PILOW", "PILUM", "PILUS", "PIMAS", "PIMPS", "PINAS", "PINED", "PINES", "PINGO", "PINGS", "PINKO", "PINKS", + "PINNA", "PINNY", "PINON", "PINOT", "PINTA", "PINTS", "PINUP", "PIONS", "PIONY", "PIOUS", "PIOYE", "PIOYS", + "PIPAL", "PIPAS", "PIPED", "PIPES", "PIPET", "PIPIS", "PIPIT", "PIPPY", "PIPUL", "PIRAI", "PIRLS", "PIRNS", + "PIROG", "PISCO", "PISES", "PISKY", "PISOS", "PISSY", "PISTE", "PITAS", "PITHS", "PITON", "PITOT", "PITTA", + "PIUMS", "PIXES", "PIZED", "PIZES", "PLAAS", "PLACK", "PLAGE", "PLANS", "PLAPS", "PLASH", "PLASM", "PLAST", + "PLATS", "PLATT", "PLATY", "PLAYA", "PLAYS", "PLEAS", "PLEBE", "PLEBS", "PLENA", "PLEON", "PLESH", "PLEWS", + "PLICA", "PLIES", "PLIMS", "PLING", "PLINK", "PLOAT", "PLODS", "PLONG", "PLONK", "PLOOK", "PLOPS", "PLOTS", + "PLOTZ", "PLOUK", "PLOWS", "PLOYE", "PLOYS", "PLUES", "PLUFF", "PLUGS", "PLUMS", "PLUMY", "PLUOT", "PLUTO", + "PLYER", "POACH", "POAKA", "POAKE", "POBOY", "POCKS", "POCKY", "PODAL", "PODDY", "PODEX", "PODGE", "PODGY", + "PODIA", "POEMS", "POEPS", "POETS", "POGEY", "POGGE", "POGOS", "POHED", "POILU", "POIND", "POKAL", "POKED", + "POKES", "POKEY", "POKIE", "POLED", "POLER", "POLES", "POLEY", "POLIO", "POLIS", "POLJE", "POLKS", "POLLS", + "POLLY", "POLOS", "POLTS", "POLYS", "POMBE", "POMES", "POMMY", "POMOS", "POMPS", "PONCE", "PONCY", "PONDS", + "PONES", "PONEY", "PONGA", "PONGO", "PONGS", "PONGY", "PONKS", "PONTS", "PONTY", "PONZU", "POODS", "POOED", + "POOFS", "POOFY", "POOHS", "POOJA", "POOKA", "POOKS", "POOLS", "POONS", "POOPS", "POOPY", "POORI", "POORT", + "POOTS", "POOVE", "POOVY", "POPES", "POPPA", "POPSY", "PORAE", "PORAL", "PORED", "PORER", "PORES", "PORGE", + "PORGY", "PORIN", "PORKS", "PORKY", "PORNO", "PORNS", "PORNY", "PORTA", "PORTS", "PORTY", "POSED", "POSES", + "POSEY", "POSHO", "POSTS", "POTAE", "POTCH", "POTED", "POTES", "POTIN", "POTOO", "POTSY", "POTTO", "POTTS", + "POTTY", "POUFF", "POUFS", "POUKE", "POUKS", "POULE", "POULP", "POULT", "POUPE", "POUPT", "POURS", "POUTS", + "POWAN", "POWIN", "POWND", "POWNS", "POWNY", "POWRE", "POXED", "POXES", "POYNT", "POYOU", "POYSE", "POZZY", + "PRAAM", "PRADS", "PRAHU", "PRAMS", "PRANA", "PRANG", "PRAOS", "PRASE", "PRATE", "PRATS", "PRATT", "PRATY", + "PRAUS", "PRAYS", "PREDY", "PREED", "PREES", "PREIF", "PREMS", "PREMY", "PRENT", "PREON", "PREOP", "PREPS", + "PRESA", "PRESE", "PREST", "PREVE", "PREXY", "PREYS", "PRIAL", "PRICY", "PRIEF", "PRIER", "PRIES", "PRIGS", + "PRILL", "PRIMA", "PRIMI", "PRIMP", "PRIMS", "PRIMY", "PRINK", "PRION", "PRISE", "PRISS", "PROAS", "PROBS", + "PRODS", "PROEM", "PROFS", "PROGS", "PROIN", "PROKE", "PROLE", "PROLL", "PROMO", "PROMS", "PRONK", "PROPS", + "PRORE", "PROSO", "PROSS", "PROST", "PROSY", "PROTO", "PROUL", "PROWS", "PROYN", "PRUNT", "PRUTA", "PRYER", + "PRYSE", "PSEUD", "PSHAW", "PSION", "PSOAE", "PSOAI", "PSOAS", "PSORA", "PSYCH", "PSYOP", "PUBCO", "PUBES", + "PUBIS", "PUCAN", "PUCER", "PUCES", "PUCKA", "PUCKS", "PUDDY", "PUDGE", "PUDIC", "PUDOR", "PUDSY", "PUDUS", + "PUERS", "PUFFA", "PUFFS", "PUGGY", "PUGIL", "PUHAS", "PUJAH", "PUJAS", "PUKAS", "PUKED", "PUKER", "PUKES", + "PUKEY", "PUKKA", "PUKUS", "PULAO", "PULAS", "PULED", "PULER", "PULES", "PULIK", "PULIS", "PULKA", "PULKS", + "PULLI", "PULLS", "PULLY", "PULMO", "PULPS", "PULUS", "PUMAS", "PUMIE", "PUMPS", "PUNAS", "PUNCE", "PUNGA", + "PUNGS", "PUNJI", "PUNKA", "PUNKS", "PUNKY", "PUNNY", "PUNTO", "PUNTS", "PUNTY", "PUPAE", "PUPAL", "PUPAS", + "PUPUS", "PURDA", "PURED", "PURES", "PURIN", "PURIS", "PURLS", "PURPY", "PURRS", "PURSY", "PURTY", "PUSES", + "PUSLE", "PUSSY", "PUTID", "PUTON", "PUTTI", "PUTTO", "PUTTS", "PUZEL", "PWNED", "PYATS", "PYETS", "PYGAL", + "PYINS", "PYLON", "PYNED", "PYNES", "PYOID", "PYOTS", "PYRAL", "PYRAN", "PYRES", "PYREX", "PYRIC", "PYROS", + "PYXED", "PYXES", "PYXIE", "PYXIS", "PZAZZ", "QADIS", "QAIDS", "QAJAQ", "QANAT", "QAPIK", "QIBLA", "QOPHS", + "QORMA", "QUADS", "QUAFF", "QUAGS", "QUAIR", "QUAIS", "QUAKY", "QUALE", "QUANT", "QUARE", "QUASS", "QUATE", + "QUATS", "QUAYD", "QUAYS", "QUBIT", "QUEAN", "QUEME", "QUENA", "QUERN", "QUEYN", "QUEYS", "QUICH", "QUIDS", + "QUIFF", "QUIMS", "QUINA", "QUINE", "QUINO", "QUINS", "QUINT", "QUIPO", "QUIPS", "QUIPU", "QUIRE", "QUIRT", + "QUIST", "QUITS", "QUOAD", "QUODS", "QUOIF", "QUOIN", "QUOIT", "QUOLL", "QUONK", "QUOPS", "QURAN", "QURSH", + "QUYTE", "RABAT", "RABIC", "RABIS", "RACED", "RACES", "RACHE", "RACKS", "RACON", "RADGE", "RADIX", "RADON", + "RAFFS", "RAFTS", "RAGAS", "RAGDE", "RAGED", "RAGEE", "RAGER", "RAGES", "RAGGA", "RAGGS", "RAGGY", "RAGIS", + "RAGUS", "RAHED", "RAHUI", "RAIAS", "RAIDS", "RAIKS", "RAILE", "RAILS", "RAINE", "RAINS", "RAIRD", "RAITA", + "RAITS", "RAJAS", "RAJES", "RAKED", "RAKEE", "RAKER", "RAKES", "RAKIA", "RAKIS", "RAKUS", "RALES", "RAMAL", + "RAMEE", "RAMET", "RAMIE", "RAMIN", "RAMIS", "RAMMY", "RAMPS", "RAMUS", "RANAS", "RANCE", "RANDS", "RANEE", + "RANGA", "RANGI", "RANGS", "RANGY", "RANID", "RANIS", "RANKE", "RANKS", "RANTS", "RAPED", "RAPER", "RAPES", + "RAPHE", "RAPPE", "RARED", "RAREE", "RARES", "RARKS", "RASED", "RASER", "RASES", "RASPS", "RASSE", "RASTA", + "RATAL", "RATAN", "RATAS", "RATCH", "RATED", "RATEL", "RATER", "RATES", "RATHA", "RATHE", "RATHS", "RATOO", + "RATOS", "RATUS", "RAUNS", "RAUPO", "RAVED", "RAVEL", "RAVER", "RAVES", "RAVEY", "RAVIN", "RAWER", "RAWIN", + "RAWLY", "RAWNS", "RAXED", "RAXES", "RAYAH", "RAYAS", "RAYED", "RAYLE", "RAYNE", "RAZED", "RAZEE", "RAZER", + "RAZES", "RAZOO", "READD", "READS", "REAIS", "REAKS", "REALO", "REALS", "REAME", "REAMS", "REAMY", "REANS", + "REAPS", "REARS", "REAST", "REATA", "REATE", "REAVE", "REBBE", "REBEC", "REBID", "REBIT", "REBOP", "REBUY", + "RECAL", "RECCE", "RECCO", "RECCY", "RECIT", "RECKS", "RECON", "RECTA", "RECTI", "RECTO", "REDAN", "REDDS", + "REDDY", "REDED", "REDES", "REDIA", "REDID", "REDIP", "REDLY", "REDON", "REDOS", "REDOX", "REDRY", "REDUB", + "REDUX", "REDYE", "REECH", "REEDE", "REEDS", "REEFS", "REEFY", "REEKS", "REEKY", "REELS", "REENS", "REEST", + "REEVE", "REFED", "REFEL", "REFFO", "REFIS", "REFIX", "REFLY", "REFRY", "REGAR", "REGES", "REGGO", "REGIE", + "REGMA", "REGNA", "REGOS", "REGUR", "REHEM", "REIFS", "REIFY", "REIKI", "REIKS", "REINK", "REINS", "REIRD", + "REIST", "REIVE", "REJIG", "REJON", "REKED", "REKES", "REKEY", "RELET", "RELIE", "RELIT", "RELLO", "REMAN", + "REMAP", "REMEN", "REMET", "REMEX", "REMIX", "RENAY", "RENDS", "RENEY", "RENGA", "RENIG", "RENIN", "RENNE", + "RENOS", "RENTE", "RENTS", "REOIL", "REORG", "REPEG", "REPIN", "REPLA", "REPOS", "REPOT", "REPPS", "REPRO", + "RERAN", "RERIG", "RESAT", "RESAW", "RESAY", "RESEE", "RESES", "RESEW", "RESID", "RESIT", "RESOD", "RESOW", + "RESTO", "RESTS", "RESTY", "RESUS", "RETAG", "RETAX", "RETEM", "RETIA", "RETIE", "RETOX", "REVET", "REVIE", + "REWAN", "REWAX", "REWED", "REWET", "REWIN", "REWON", "REWTH", "REXES", "REZES", "RHEAS", "RHEME", "RHEUM", + "RHIES", "RHIME", "RHINE", "RHODY", "RHOMB", "RHONE", "RHUMB", "RHYNE", "RHYTA", "RIADS", "RIALS", "RIANT", + "RIATA", "RIBAS", "RIBBY", "RIBES", "RICED", "RICER", "RICES", "RICEY", "RICHT", "RICIN", "RICKS", "RIDES", + "RIDGY", "RIDIC", "RIELS", "RIEMS", "RIEVE", "RIFER", "RIFFS", "RIFTE", "RIFTS", "RIFTY", "RIGGS", "RIGOL", + "RILED", "RILES", "RILEY", "RILLE", "RILLS", "RIMAE", "RIMED", "RIMER", "RIMES", "RIMUS", "RINDS", "RINDY", + "RINES", "RINGS", "RINKS", "RIOJA", "RIOTS", "RIPED", "RIPES", "RIPPS", "RISES", "RISHI", "RISKS", "RISPS", + "RISUS", "RITES", "RITTS", "RITZY", "RIVAS", "RIVED", "RIVEL", "RIVEN", "RIVES", "RIYAL", "RIZAS", "ROADS", + "ROAMS", "ROANS", "ROARS", "ROARY", "ROATE", "ROBED", "ROBES", "ROBLE", "ROCKS", "RODED", "RODES", "ROGUY", + "ROHES", "ROIDS", "ROILS", "ROILY", "ROINS", "ROIST", "ROJAK", "ROJIS", "ROKED", "ROKER", "ROKES", "ROLAG", + "ROLES", "ROLFS", "ROLLS", "ROMAL", "ROMAN", "ROMEO", "ROMPS", "RONDE", "RONDO", "RONEO", "RONES", "RONIN", + "RONNE", "RONTE", "RONTS", "ROODS", "ROOFS", "ROOFY", "ROOKS", "ROOKY", "ROOMS", "ROONS", "ROOPS", "ROOPY", + "ROOSA", "ROOSE", "ROOTS", "ROOTY", "ROPED", "ROPER", "ROPES", "ROPEY", "ROQUE", "RORAL", "RORES", "RORIC", + "RORID", "RORIE", "RORTS", "RORTY", "ROSED", "ROSES", "ROSET", "ROSHI", "ROSIN", "ROSIT", "ROSTI", "ROSTS", + "ROTAL", "ROTAN", "ROTAS", "ROTCH", "ROTED", "ROTES", "ROTIS", "ROTLS", "ROTON", "ROTOS", "ROTTE", "ROUEN", + "ROUES", "ROULE", "ROULS", "ROUMS", "ROUPS", "ROUPY", "ROUST", "ROUTH", "ROUTS", "ROVED", "ROVEN", "ROVES", + "ROWAN", "ROWED", "ROWEL", "ROWEN", "ROWIE", "ROWME", "ROWND", "ROWTH", "ROWTS", "ROYNE", "ROYST", "ROZET", + "ROZIT", "RUANA", "RUBAI", "RUBBY", "RUBEL", "RUBES", "RUBIN", "RUBLE", "RUBLI", "RUBUS", "RUCHE", "RUCKS", + "RUDAS", "RUDDS", "RUDES", "RUDIE", "RUDIS", "RUEDA", "RUERS", "RUFFE", "RUFFS", "RUGAE", "RUGAL", "RUGGY", + "RUING", "RUINS", "RUKHS", "RULED", "RULES", "RUMAL", "RUMBO", "RUMEN", "RUMES", "RUMLY", "RUMMY", "RUMPO", + "RUMPS", "RUMPY", "RUNCH", "RUNDS", "RUNED", "RUNES", "RUNGS", "RUNIC", "RUNNY", "RUNTS", "RUNTY", "RUPIA", + "RURPS", "RURUS", "RUSAS", "RUSES", "RUSHY", "RUSKS", "RUSMA", "RUSSE", "RUSTS", "RUTHS", "RUTIN", "RUTTY", + "RYALS", "RYBAT", "RYKED", "RYKES", "RYMME", "RYNDS", "RYOTS", "RYPER", "SAAGS", "SABAL", "SABED", "SABER", + "SABES", "SABHA", "SABIN", "SABIR", "SABLE", "SABOT", "SABRA", "SABRE", "SACKS", "SACRA", "SADDO", "SADES", + "SADHE", "SADHU", "SADIS", "SADOS", "SADZA", "SAFED", "SAFES", "SAGAS", "SAGER", "SAGES", "SAGGY", "SAGOS", + "SAGUM", "SAHEB", "SAHIB", "SAICE", "SAICK", "SAICS", "SAIDS", "SAIGA", "SAILS", "SAIMS", "SAINE", "SAINS", + "SAIRS", "SAIST", "SAITH", "SAJOU", "SAKAI", "SAKER", "SAKES", "SAKIA", "SAKIS", "SAKTI", "SALAL", "SALAT", + "SALEP", "SALES", "SALET", "SALIC", "SALIX", "SALLE", "SALMI", "SALOL", "SALOP", "SALPA", "SALPS", "SALSE", + "SALTO", "SALTS", "SALUE", "SALUT", "SAMAN", "SAMAS", "SAMBA", "SAMBO", "SAMEK", "SAMEL", "SAMEN", "SAMES", + "SAMEY", "SAMFU", "SAMMY", "SAMPI", "SAMPS", "SANDS", "SANED", "SANES", "SANGA", "SANGH", "SANGO", "SANGS", + "SANKO", "SANSA", "SANTO", "SANTS", "SAOLA", "SAPAN", "SAPID", "SAPOR", "SARAN", "SARDS", "SARED", "SAREE", + "SARGE", "SARGO", "SARIN", "SARIS", "SARKS", "SARKY", "SAROD", "SAROS", "SARUS", "SASER", "SASIN", "SASSE", + "SATAI", "SATAY", "SATED", "SATEM", "SATES", "SATIS", "SAUBA", "SAUCH", "SAUGH", "SAULS", "SAULT", "SAUNT", + "SAURY", "SAUTS", "SAVED", "SAVER", "SAVES", "SAVEY", "SAVIN", "SAWAH", "SAWED", "SAWER", "SAXES", "SAYED", + "SAYER", "SAYID", "SAYNE", "SAYON", "SAYST", "SAZES", "SCABS", "SCADS", "SCAFF", "SCAGS", "SCAIL", "SCALA", + "SCALL", "SCAMS", "SCAND", "SCANS", "SCAPA", "SCAPE", "SCAPI", "SCARP", "SCARS", "SCART", "SCATH", "SCATS", + "SCATT", "SCAUD", "SCAUP", "SCAUR", "SCAWS", "SCEAT", "SCENA", "SCEND", "SCHAV", "SCHMO", "SCHUL", "SCHWA", + "SCLIM", "SCODY", "SCOGS", "SCOOG", "SCOOT", "SCOPA", "SCOPS", "SCOTS", "SCOUG", "SCOUP", "SCOWP", "SCOWS", + "SCRAB", "SCRAE", "SCRAG", "SCRAN", "SCRAT", "SCRAW", "SCRAY", "SCRIM", "SCRIP", "SCROB", "SCROD", "SCROG", + "SCROW", "SCUDI", "SCUDO", "SCUDS", "SCUFF", "SCUFT", "SCUGS", "SCULK", "SCULL", "SCULP", "SCULS", "SCUMS", + "SCUPS", "SCURF", "SCURS", "SCUSE", "SCUTA", "SCUTE", "SCUTS", "SCUZZ", "SCYES", "SDAYN", "SDEIN", "SEALS", + "SEAME", "SEAMS", "SEAMY", "SEANS", "SEARE", "SEARS", "SEASE", "SEATS", "SEAZE", "SEBUM", "SECCO", "SECHS", + "SECTS", "SEDER", "SEDES", "SEDGE", "SEDGY", "SEDUM", "SEEDS", "SEEKS", "SEELD", "SEELS", "SEELY", "SEEMS", + "SEEPS", "SEEPY", "SEERS", "SEFER", "SEGAR", "SEGNI", "SEGNO", "SEGOL", "SEGOS", "SEHRI", "SEIFS", "SEILS", + "SEINE", "SEIRS", "SEISE", "SEISM", "SEITY", "SEIZA", "SEKOS", "SEKTS", "SELAH", "SELES", "SELFS", "SELLA", + "SELLE", "SELLS", "SELVA", "SEMEE", "SEMES", "SEMIE", "SEMIS", "SENAS", "SENDS", "SENES", "SENGI", "SENNA", + "SENOR", "SENSA", "SENSI", "SENTE", "SENTI", "SENTS", "SENVY", "SENZA", "SEPAD", "SEPAL", "SEPIC", "SEPOY", + "SEPTA", "SEPTS", "SERAC", "SERAI", "SERAL", "SERED", "SERER", "SERES", "SERFS", "SERGE", "SERIC", "SERIN", + "SERKS", "SERON", "SEROW", "SERRA", "SERRE", "SERRS", "SERRY", "SERVO", "SESEY", "SESSA", "SETAE", "SETAL", + "SETON", "SETTS", "SEWAN", "SEWAR", "SEWED", "SEWEL", "SEWEN", "SEWIN", "SEXED", "SEXER", "SEXES", "SEXTO", + "SEXTS", "SEYEN", "SHADS", "SHAGS", "SHAHS", "SHAKO", "SHAKT", "SHALM", "SHALY", "SHAMA", "SHAMS", "SHAND", + "SHANS", "SHAPS", "SHARN", "SHASH", "SHAUL", "SHAWM", "SHAWN", "SHAWS", "SHAYA", "SHAYS", "SHCHI", "SHEAF", + "SHEAL", "SHEAS", "SHEDS", "SHEEL", "SHEND", "SHENT", "SHEOL", "SHERD", "SHERE", "SHERO", "SHETS", "SHEVA", + "SHEWN", "SHEWS", "SHIAI", "SHIEL", "SHIER", "SHIES", "SHILL", "SHILY", "SHIMS", "SHINS", "SHIPS", "SHIRR", + "SHIRS", "SHISH", "SHISO", "SHIST", "SHITE", "SHITS", "SHIUR", "SHIVA", "SHIVE", "SHIVS", "SHLEP", "SHLUB", + "SHMEK", "SHMOE", "SHOAT", "SHOED", "SHOER", "SHOES", "SHOGI", "SHOGS", "SHOJI", "SHOJO", "SHOLA", "SHOOL", + "SHOON", "SHOOS", "SHOPE", "SHOPS", "SHORL", "SHOTE", "SHOTS", "SHOTT", "SHOWD", "SHOWS", "SHOYU", "SHRED", + "SHRIS", "SHROW", "SHTIK", "SHTUM", "SHTUP", "SHULE", "SHULN", "SHULS", "SHUNS", "SHURA", "SHUTE", "SHUTS", + "SHWAS", "SHYER", "SIALS", "SIBBS", "SIBYL", "SICES", "SICHT", "SICKO", "SICKS", "SICKY", "SIDAS", "SIDED", + "SIDER", "SIDES", "SIDHA", "SIDHE", "SIDLE", "SIELD", "SIENS", "SIENT", "SIETH", "SIEUR", "SIFTS", "SIGHS", + "SIGIL", "SIGLA", "SIGNA", "SIGNS", "SIJOS", "SIKAS", "SIKER", "SIKES", "SILDS", "SILED", "SILEN", "SILER", + "SILES", "SILEX", "SILKS", "SILLS", "SILOS", "SILTS", "SILTY", "SILVA", "SIMAR", "SIMAS", "SIMBA", "SIMIS", + "SIMPS", "SIMUL", "SINDS", "SINED", "SINES", "SINGS", "SINHS", "SINKS", "SINKY", "SINUS", "SIPED", "SIPES", + "SIPPY", "SIRED", "SIREE", "SIRES", "SIRIH", "SIRIS", "SIROC", "SIRRA", "SIRUP", "SISAL", "SISES", "SISTA", + "SISTS", "SITAR", "SITED", "SITES", "SITHE", "SITKA", "SITUP", "SITUS", "SIVER", "SIXER", "SIXES", "SIXMO", + "SIXTE", "SIZAR", "SIZED", "SIZEL", "SIZER", "SIZES", "SKAGS", "SKAIL", "SKALD", "SKANK", "SKART", "SKATS", + "SKATT", "SKAWS", "SKEAN", "SKEAR", "SKEDS", "SKEED", "SKEEF", "SKEEN", "SKEER", "SKEES", "SKEET", "SKEGG", + "SKEGS", "SKEIN", "SKELF", "SKELL", "SKELM", "SKELP", "SKENE", "SKENS", "SKEOS", "SKEPS", "SKERS", "SKETS", + "SKEWS", "SKIDS", "SKIED", "SKIES", "SKIEY", "SKIMO", "SKIMS", "SKINK", "SKINS", "SKINT", "SKIOS", "SKIPS", + "SKIRL", "SKIRR", "SKITE", "SKITS", "SKIVE", "SKIVY", "SKLIM", "SKOAL", "SKODY", "SKOFF", "SKOGS", "SKOLS", + "SKOOL", "SKORT", "SKOSH", "SKRAN", "SKRIK", "SKUAS", "SKUGS", "SKYED", "SKYER", "SKYEY", "SKYFS", "SKYRE", + "SKYRS", "SKYTE", "SLABS", "SLADE", "SLAES", "SLAGS", "SLAID", "SLAKE", "SLAMS", "SLANE", "SLANK", "SLAPS", + "SLART", "SLATS", "SLATY", "SLAVE", "SLAWS", "SLAYS", "SLEBS", "SLEDS", "SLEER", "SLEWS", "SLEYS", "SLIER", + "SLILY", "SLIMS", "SLIPE", "SLIPS", "SLIPT", "SLISH", "SLITS", "SLIVE", "SLOAN", "SLOBS", "SLOES", "SLOGS", + "SLOID", "SLOJD", "SLOMO", "SLOOM", "SLOOT", "SLOPS", "SLOPY", "SLORM", "SLOTS", "SLOVE", "SLOWS", "SLOYD", + "SLUBB", "SLUBS", "SLUED", "SLUES", "SLUFF", "SLUGS", "SLUIT", "SLUMS", "SLURB", "SLURS", "SLUSE", "SLUTS", + "SLYER", "SLYPE", "SMAAK", "SMAIK", "SMALM", "SMALT", "SMARM", "SMAZE", "SMEEK", "SMEES", "SMEIK", "SMEKE", + "SMERK", "SMEWS", "SMIRR", "SMIRS", "SMITS", "SMOGS", "SMOKO", "SMOLT", "SMOOR", "SMOOT", "SMORE", "SMORG", + "SMOUT", "SMOWT", "SMUGS", "SMURS", "SMUSH", "SMUTS", "SNABS", "SNAFU", "SNAGS", "SNAPS", "SNARF", "SNARK", + "SNARS", "SNARY", "SNASH", "SNATH", "SNAWS", "SNEAD", "SNEAP", "SNEBS", "SNECK", "SNEDS", "SNEED", "SNEES", + "SNELL", "SNIBS", "SNICK", "SNIES", "SNIFT", "SNIGS", "SNIPS", "SNIPY", "SNIRT", "SNITS", "SNOBS", "SNODS", + "SNOEK", "SNOEP", "SNOGS", "SNOKE", "SNOOD", "SNOOK", "SNOOL", "SNOOT", "SNOTS", "SNOWK", "SNOWS", "SNUBS", + "SNUGS", "SNUSH", "SNYES", "SOAKS", "SOAPS", "SOARE", "SOARS", "SOAVE", "SOBAS", "SOCAS", "SOCES", "SOCKO", + "SOCKS", "SOCLE", "SODAS", "SODDY", "SODIC", "SODOM", "SOFAR", "SOFAS", "SOFTA", "SOFTS", "SOFTY", "SOGER", + "SOHUR", "SOILS", "SOILY", "SOJAS", "SOJUS", "SOKAH", "SOKEN", "SOKES", "SOKOL", "SOLAH", "SOLAN", "SOLAS", + "SOLDE", "SOLDI", "SOLDO", "SOLDS", "SOLED", "SOLEI", "SOLER", "SOLES", "SOLON", "SOLOS", "SOLUM", "SOLUS", + "SOMAN", "SOMAS", "SONCE", "SONDE", "SONES", "SONGS", "SONLY", "SONNE", "SONNY", "SONSE", "SONSY", "SOOEY", + "SOOKS", "SOOKY", "SOOLE", "SOOLS", "SOOMS", "SOOPS", "SOOTE", "SOOTS", "SOPHS", "SOPHY", "SOPOR", "SOPPY", + "SOPRA", "SORAL", "SORAS", "SORBO", "SORBS", "SORDA", "SORDO", "SORDS", "SORED", "SOREE", "SOREL", "SORER", + "SORES", "SOREX", "SORGO", "SORNS", "SORRA", "SORTA", "SORTS", "SORUS", "SOTHS", "SOTOL", "SOUCE", "SOUCT", + "SOUGH", "SOUKS", "SOULS", "SOUMS", "SOUPS", "SOUPY", "SOURS", "SOUSE", "SOUTS", "SOWAR", "SOWCE", "SOWED", + "SOWFF", "SOWFS", "SOWLE", "SOWLS", "SOWMS", "SOWND", "SOWNE", "SOWPS", "SOWSE", "SOWTH", "SOYAS", "SOYLE", + "SOYUZ", "SOZIN", "SPACY", "SPADO", "SPAED", "SPAER", "SPAES", "SPAGS", "SPAHI", "SPAIL", "SPAIN", "SPAIT", + "SPAKE", "SPALD", "SPALE", "SPALL", "SPALT", "SPAMS", "SPANE", "SPANG", "SPANS", "SPARD", "SPARS", "SPART", + "SPATE", "SPATS", "SPAUL", "SPAWL", "SPAWS", "SPAYD", "SPAYS", "SPAZA", "SPAZZ", "SPEAL", "SPEAN", "SPEAT", + "SPECS", "SPECT", "SPEEL", "SPEER", "SPEIL", "SPEIR", "SPEKS", "SPELD", "SPELK", "SPEOS", "SPETS", "SPEUG", + "SPEWS", "SPEWY", "SPIAL", "SPICA", "SPICK", "SPICS", "SPIDE", "SPIER", "SPIES", "SPIFF", "SPIFS", "SPIKS", + "SPILE", "SPIMS", "SPINA", "SPINK", "SPINS", "SPIRT", "SPIRY", "SPITS", "SPITZ", "SPIVS", "SPLAY", "SPLOG", + "SPODE", "SPODS", "SPOOM", "SPOOR", "SPOOT", "SPORK", "SPOSH", "SPOTS", "SPRAD", "SPRAG", "SPRAT", "SPRED", + "SPREW", "SPRIT", "SPROD", "SPROG", "SPRUE", "SPRUG", "SPUDS", "SPUED", "SPUER", "SPUES", "SPUGS", "SPULE", + "SPUME", "SPUMY", "SPURS", "SPUTA", "SPYAL", "SPYRE", "SQUAB", "SQUAW", "SQUEG", "SQUID", "SQUIT", "SQUIZ", + "STABS", "STADE", "STAGS", "STAGY", "STAIG", "STANE", "STANG", "STAPH", "STAPS", "STARN", "STARR", "STARS", + "STATS", "STAUN", "STAWS", "STAYS", "STEAN", "STEAR", "STEDD", "STEDE", "STEDS", "STEEK", "STEEM", "STEEN", + "STEIL", "STELA", "STELE", "STELL", "STEME", "STEMS", "STEND", "STENO", "STENS", "STENT", "STEPS", "STEPT", + "STERE", "STETS", "STEWS", "STEWY", "STEYS", "STICH", "STIED", "STIES", "STILB", "STILE", "STIME", "STIMS", + "STIMY", "STIPA", "STIPE", "STIRE", "STIRK", "STIRP", "STIRS", "STIVE", "STIVY", "STOAE", "STOAI", "STOAS", + "STOAT", "STOBS", "STOEP", "STOGY", "STOIT", "STOLN", "STOMA", "STOND", "STONG", "STONK", "STONN", "STOOK", + "STOOR", "STOPE", "STOPS", "STOPT", "STOSS", "STOTS", "STOTT", "STOUN", "STOUP", "STOUR", "STOWN", "STOWP", + "STOWS", "STRAD", "STRAE", "STRAG", "STRAK", "STREP", "STREW", "STRIA", "STRIG", "STRIM", "STROP", "STROW", + "STROY", "STRUM", "STUBS", "STUDE", "STUDS", "STULL", "STULM", "STUMM", "STUMS", "STUNS", "STUPA", "STUPE", + "STURE", "STURT", "STYED", "STYES", "STYLI", "STYLO", "STYME", "STYMY", "STYRE", "STYTE", "SUBAH", "SUBAS", + "SUBBY", "SUBER", "SUBHA", "SUCCI", "SUCKS", "SUCKY", "SUCRE", "SUDDS", "SUDOR", "SUDSY", "SUEDE", "SUENT", + "SUERS", "SUETE", "SUETS", "SUETY", "SUGAN", "SUGHS", "SUGOS", "SUHUR", "SUIDS", "SUINT", "SUITS", "SUJEE", + "SUKHS", "SUKUK", "SULCI", "SULFA", "SULFO", "SULKS", "SULPH", "SULUS", "SUMIS", "SUMMA", "SUMOS", "SUMPH", + "SUMPS", "SUNIS", "SUNKS", "SUNNA", "SUNNS", "SUNUP", "SUPES", "SUPRA", "SURAH", "SURAL", "SURAS", "SURAT", + "SURDS", "SURED", "SURES", "SURFS", "SURFY", "SURGY", "SURRA", "SUSED", "SUSES", "SUSUS", "SUTOR", "SUTRA", + "SUTTA", "SWABS", "SWACK", "SWADS", "SWAGE", "SWAGS", "SWAIL", "SWAIN", "SWALE", "SWALY", "SWAMY", "SWANG", + "SWANK", "SWANS", "SWAPS", "SWAPT", "SWARD", "SWARE", "SWARF", "SWART", "SWATS", "SWAYL", "SWAYS", "SWEAL", + "SWEDE", "SWEED", "SWEEL", "SWEER", "SWEES", "SWEIR", "SWELT", "SWERF", "SWEYS", "SWIES", "SWIGS", "SWILE", + "SWIMS", "SWINK", "SWIPE", "SWIRE", "SWISS", "SWITH", "SWITS", "SWIVE", "SWIZZ", "SWOBS", "SWOLE", "SWOLN", + "SWOPS", "SWOPT", "SWOTS", "SWOUN", "SYBBE", "SYBIL", "SYBOE", "SYBOW", "SYCEE", "SYCES", "SYCON", "SYENS", + "SYKER", "SYKES", "SYLIS", "SYLPH", "SYLVA", "SYMAR", "SYNCH", "SYNCS", "SYNDS", "SYNED", "SYNES", "SYNTH", + "SYPED", "SYPES", "SYPHS", "SYRAH", "SYREN", "SYSOP", "SYTHE", "SYVER", "TAALS", "TAATA", "TABER", "TABES", + "TABID", "TABIS", "TABLA", "TABOR", "TABUN", "TABUS", "TACAN", "TACES", "TACET", "TACHE", "TACHO", "TACHS", + "TACKS", "TACOS", "TACTS", "TAELS", "TAFIA", "TAGGY", "TAGMA", "TAHAS", "TAHRS", "TAIGA", "TAIGS", "TAIKO", + "TAILS", "TAINS", "TAIRA", "TAISH", "TAITS", "TAJES", "TAKAS", "TAKES", "TAKHI", "TAKIN", "TAKIS", "TAKKY", + "TALAK", "TALAQ", "TALAR", "TALAS", "TALCS", "TALCY", "TALEA", "TALER", "TALES", "TALKS", "TALKY", "TALLS", + "TALMA", "TALPA", "TALUK", "TALUS", "TAMAL", "TAMED", "TAMES", "TAMIN", "TAMIS", "TAMMY", "TAMPS", "TANAS", + "TANGA", "TANGI", "TANGS", "TANHS", "TANKA", "TANKS", "TANKY", "TANNA", "TANSY", "TANTI", "TANTO", "TANTY", + "TAPAS", "TAPED", "TAPEN", "TAPES", "TAPET", "TAPIS", "TAPPA", "TAPUS", "TARAS", "TARDO", "TARED", "TARES", + "TARGA", "TARGE", "TARNS", "TAROC", "TAROK", "TAROS", "TARPS", "TARRE", "TARRY", "TARSI", "TARTS", "TARTY", + "TASAR", "TASED", "TASER", "TASES", "TASKS", "TASSA", "TASSE", "TASSO", "TATAR", "TATER", "TATES", "TATHS", + "TATIE", "TATOU", "TATTS", "TATUS", "TAUBE", "TAULD", "TAUON", "TAUPE", "TAUTS", "TAVAH", "TAVAS", "TAVER", + "TAWAI", "TAWAS", "TAWED", "TAWER", "TAWIE", "TAWSE", "TAWTS", "TAXED", "TAXER", "TAXES", "TAXIS", "TAXOL", + "TAXON", "TAXOR", "TAXUS", "TAYRA", "TAZZA", "TAZZE", "TEADE", "TEADS", "TEAED", "TEAKS", "TEALS", "TEAMS", + "TEARS", "TEATS", "TEAZE", "TECHS", "TECHY", "TECTA", "TEELS", "TEEMS", "TEEND", "TEENE", "TEENS", "TEENY", + "TEERS", "TEFFS", "TEGGS", "TEGUA", "TEGUS", "TEHRS", "TEIID", "TEILS", "TEIND", "TEINS", "TELAE", "TELCO", + "TELES", "TELEX", "TELIA", "TELIC", "TELLS", "TELLY", "TELOI", "TELOS", "TEMED", "TEMES", "TEMPI", "TEMPS", + "TEMPT", "TEMSE", "TENCH", "TENDS", "TENDU", "TENES", "TENGE", "TENIA", "TENNE", "TENNO", "TENNY", "TENON", + "TENTS", "TENTY", "TENUE", "TEPAL", "TEPAS", "TEPOY", "TERAI", "TERAS", "TERCE", "TEREK", "TERES", "TERFE", + "TERFS", "TERGA", "TERMS", "TERNE", "TERNS", "TERRY", "TERTS", "TESLA", "TESTA", "TESTE", "TESTS", "TETES", + "TETHS", "TETRA", "TETRI", "TEUCH", "TEUGH", "TEWED", "TEWEL", "TEWIT", "TEXAS", "TEXES", "TEXTS", "THACK", + "THAGI", "THAIM", "THALE", "THALI", "THANA", "THANE", "THANG", "THANS", "THANX", "THARM", "THARS", "THAWS", + "THAWY", "THEBE", "THECA", "THEED", "THEEK", "THEES", "THEGN", "THEIC", "THEIN", "THELF", "THEMA", "THENS", + "THEOW", "THERM", "THESP", "THETE", "THEWS", "THEWY", "THIGS", "THILK", "THILL", "THINE", "THINS", "THIOL", + "THIRL", "THOFT", "THOLE", "THOLI", "THORO", "THORP", "THOUS", "THOWL", "THRAE", "THRAW", "THRID", "THRIP", + "THROE", "THUDS", "THUGS", "THUJA", "THUNK", "THURL", "THUYA", "THYMI", "THYMY", "TIANS", "TIARS", "TICAL", + "TICCA", "TICED", "TICES", "TICHY", "TICKS", "TICKY", "TIDDY", "TIDED", "TIDES", "TIERS", "TIFFS", "TIFOS", + "TIFTS", "TIGES", "TIGON", "TIKAS", "TIKES", "TIKIS", "TIKKA", "TILAK", "TILED", "TILER", "TILES", "TILLS", + "TILLY", "TILTH", "TILTS", "TIMBO", "TIMED", "TIMES", "TIMON", "TIMPS", "TINAS", "TINCT", "TINDS", "TINEA", + "TINED", "TINES", "TINGE", "TINGS", "TINKS", "TINNY", "TINTS", "TINTY", "TIPIS", "TIPPY", "TIRED", "TIRES", + "TIRLS", "TIROS", "TIRRS", "TITCH", "TITER", "TITIS", "TITRE", "TITTY", "TITUP", "TIYIN", "TIYNS", "TIZES", + "TIZZY", "TOADS", "TOADY", "TOAZE", "TOCKS", "TOCKY", "TOCOS", "TODDE", "TOEAS", "TOFFS", "TOFFY", "TOFTS", + "TOFUS", "TOGAE", "TOGAS", "TOGED", "TOGES", "TOGUE", "TOHOS", "TOILE", "TOILS", "TOING", "TOISE", "TOITS", + "TOKAY", "TOKED", "TOKER", "TOKES", "TOKOS", "TOLAN", "TOLAR", "TOLAS", "TOLED", "TOLES", "TOLLS", "TOLLY", + "TOLTS", "TOLUS", "TOLYL", "TOMAN", "TOMBS", "TOMES", "TOMIA", "TOMMY", "TOMOS", "TONDI", "TONDO", "TONED", + "TONER", "TONES", "TONEY", "TONGS", "TONKA", "TONKS", "TONNE", "TONUS", "TOOLS", "TOOMS", "TOONS", "TOOTS", + "TOPED", "TOPEE", "TOPEK", "TOPER", "TOPES", "TOPHE", "TOPHI", "TOPHS", "TOPIS", "TOPOI", "TOPOS", "TOPPY", + "TOQUE", "TORAH", "TORAN", "TORAS", "TORCS", "TORES", "TORIC", "TORII", "TOROS", "TOROT", "TORRS", "TORSE", + "TORSI", "TORSK", "TORTA", "TORTE", "TORTS", "TOSAS", "TOSED", "TOSES", "TOSHY", "TOSSY", "TOTED", "TOTER", + "TOTES", "TOTTY", "TOUKS", "TOUNS", "TOURS", "TOUSE", "TOUSY", "TOUTS", "TOUZE", "TOUZY", "TOWED", "TOWIE", + "TOWNS", "TOWNY", "TOWSE", "TOWSY", "TOWTS", "TOWZE", "TOWZY", "TOYED", "TOYER", "TOYON", "TOYOS", "TOZED", + "TOZES", "TOZIE", "TRABS", "TRADS", "TRAGI", "TRAIK", "TRAMS", "TRANK", "TRANQ", "TRANS", "TRANT", "TRAPE", + "TRAPS", "TRAPT", "TRASS", "TRATS", "TRATT", "TRAVE", "TRAYF", "TRAYS", "TRECK", "TREED", "TREEN", "TREES", + "TREFA", "TREIF", "TREKS", "TREMA", "TREMS", "TRESS", "TREST", "TRETS", "TREWS", "TREYF", "TREYS", "TRIAC", + "TRIDE", "TRIER", "TRIES", "TRIFF", "TRIGO", "TRIGS", "TRIKE", "TRILD", "TRILL", "TRIMS", "TRINE", "TRINS", + "TRIOL", "TRIOR", "TRIOS", "TRIPS", "TRIPY", "TRIST", "TROAD", "TROAK", "TROAT", "TROCK", "TRODE", "TRODS", + "TROGS", "TROIS", "TROKE", "TROMP", "TRONA", "TRONC", "TRONE", "TRONK", "TRONS", "TROOZ", "TROTH", "TROTS", + "TROWS", "TROYS", "TRUED", "TRUES", "TRUGO", "TRUGS", "TRULL", "TRYER", "TRYKE", "TRYMA", "TRYPS", "TSADE", + "TSADI", "TSARS", "TSKED", "TSUBA", "TSUBO", "TUANS", "TUART", "TUATH", "TUBAE", "TUBAR", "TUBAS", "TUBBY", + "TUBED", "TUBES", "TUCKS", "TUFAS", "TUFFE", "TUFFS", "TUFTS", "TUFTY", "TUGRA", "TUILE", "TUINA", "TUISM", + "TUKTU", "TULES", "TULPA", "TULSI", "TUMID", "TUMMY", "TUMPS", "TUMPY", "TUNAS", "TUNDS", "TUNED", "TUNER", + "TUNES", "TUNGS", "TUNNY", "TUPEK", "TUPIK", "TUPLE", "TUQUE", "TURDS", "TURFS", "TURFY", "TURKS", "TURME", + "TURMS", "TURNS", "TURNT", "TURPS", "TURRS", "TUSHY", "TUSKS", "TUSKY", "TUTEE", "TUTTI", "TUTTY", "TUTUS", + "TUXES", "TUYER", "TWAES", "TWAIN", "TWALS", "TWANK", "TWATS", "TWAYS", "TWEEL", "TWEEN", "TWEEP", "TWEER", + "TWERK", "TWERP", "TWIER", "TWIGS", "TWILL", "TWILT", "TWINK", "TWINS", "TWINY", "TWIRE", "TWIRP", "TWITE", + "TWITS", "TWOER", "TWYER", "TYEES", "TYERS", "TYIYN", "TYKES", "TYLER", "TYMPS", "TYNDE", "TYNED", "TYNES", + "TYPAL", "TYPED", "TYPES", "TYPEY", "TYPIC", "TYPOS", "TYPPS", "TYPTO", "TYRAN", "TYRED", "TYRES", "TYROS", + "TYTHE", "TZARS", "UDALS", "UDONS", "UGALI", "UGGED", "UHLAN", "UHURU", "UKASE", "ULAMA", "ULANS", "ULEMA", + "ULMIN", "ULNAD", "ULNAE", "ULNAR", "ULNAS", "ULPAN", "ULVAS", "ULYIE", "ULZIE", "UMAMI", "UMBEL", "UMBER", + "UMBLE", "UMBOS", "UMBRE", "UMIAC", "UMIAK", "UMIAQ", "UMMAH", "UMMAS", "UMMED", "UMPED", "UMPHS", "UMPIE", + "UMPTY", "UMRAH", "UMRAS", "UNAIS", "UNAPT", "UNARM", "UNARY", "UNAUS", "UNBAG", "UNBAN", "UNBAR", "UNBED", + "UNBID", "UNBOX", "UNCAP", "UNCES", "UNCIA", "UNCOS", "UNCOY", "UNCUS", "UNDAM", "UNDEE", "UNDOS", "UNDUG", + "UNETH", "UNFIX", "UNGAG", "UNGET", "UNGOD", "UNGOT", "UNGUM", "UNHAT", "UNHIP", "UNICA", "UNITS", "UNJAM", + "UNKED", "UNKET", "UNKID", "UNLAW", "UNLAY", "UNLED", "UNLET", "UNLID", "UNMAN", "UNMEW", "UNMIX", "UNPAY", + "UNPEG", "UNPEN", "UNPIN", "UNRED", "UNRID", "UNRIG", "UNRIP", "UNSAW", "UNSAY", "UNSEE", "UNSEW", "UNSEX", + "UNSOD", "UNTAX", "UNTIN", "UNWET", "UNWIT", "UNWON", "UPBOW", "UPBYE", "UPDOS", "UPDRY", "UPEND", "UPJET", + "UPLAY", "UPLED", "UPLIT", "UPPED", "UPRAN", "UPRUN", "UPSEE", "UPSEY", "UPTAK", "UPTER", "UPTIE", "URAEI", + "URALI", "URAOS", "URARE", "URARI", "URASE", "URATE", "URBEX", "URBIA", "URDEE", "UREAL", "UREAS", "UREDO", + "UREIC", "URENA", "URENT", "URGED", "URGER", "URGES", "URIAL", "URITE", "URMAN", "URNAL", "URNED", "URPED", + "URSAE", "URSID", "URSON", "URUBU", "URVAS", "USERS", "USNEA", "USQUE", "USURE", "USURY", "UTERI", "UVEAL", + "UVEAS", "UVULA", "VACUA", "VADED", "VADES", "VAGAL", "VAGUS", "VAILS", "VAIRE", "VAIRS", "VAIRY", "VAKAS", + "VAKIL", "VALES", "VALIS", "VALSE", "VAMPS", "VAMPY", "VANDA", "VANED", "VANES", "VANGS", "VANTS", "VAPED", + "VAPER", "VAPES", "VARAN", "VARAS", "VARDY", "VAREC", "VARES", "VARIA", "VARIX", "VARNA", "VARUS", "VARVE", + "VASAL", "VASES", "VASTS", "VASTY", "VATIC", "VATUS", "VAUCH", "VAUTE", "VAUTS", "VAWTE", "VAXES", "VEALE", + "VEALS", "VEALY", "VEENA", "VEEPS", "VEERS", "VEERY", "VEGAS", "VEGES", "VEGIE", "VEGOS", "VEHME", "VEILS", + "VEILY", "VEINS", "VEINY", "VELAR", "VELDS", "VELDT", "VELES", "VELLS", "VELUM", "VENAE", "VENAL", "VENDS", + "VENDU", "VENEY", "VENGE", "VENIN", "VENTS", "VENUS", "VERBS", "VERRA", "VERRY", "VERST", "VERTS", "VERTU", + "VESPA", "VESTA", "VESTS", "VETCH", "VEXED", "VEXER", "VEXES", "VEXIL", "VEZIR", "VIALS", "VIAND", "VIBES", + "VIBEX", "VIBEY", "VICED", "VICES", "VICHY", "VIERS", "VIEWS", "VIEWY", "VIFDA", "VIFFS", "VIGAS", "VIGIA", + "VILDE", "VILER", "VILLI", "VILLS", "VIMEN", "VINAL", "VINAS", "VINCA", "VINED", "VINER", "VINES", "VINEW", + "VINIC", "VINOS", "VINTS", "VIOLD", "VIOLS", "VIRED", "VIREO", "VIRES", "VIRGA", "VIRGE", "VIRID", "VIRLS", + "VIRTU", "VISAS", "VISED", "VISES", "VISIE", "VISNE", "VISON", "VISTO", "VITAE", "VITAS", "VITEX", "VITRO", + "VITTA", "VIVAS", "VIVAT", "VIVDA", "VIVER", "VIVES", "VIZIR", "VIZOR", "VLEIS", "VLIES", "VLOGS", "VOARS", + "VOCAB", "VOCES", "VODDY", "VODOU", "VODUN", "VOEMA", "VOGIE", "VOIDS", "VOILE", "VOIPS", "VOLAE", "VOLAR", + "VOLED", "VOLES", "VOLET", "VOLKS", "VOLTA", "VOLTE", "VOLTI", "VOLTS", "VOLVA", "VOLVE", "VOMER", "VOTED", + "VOTES", "VOUGE", "VOULU", "VOWED", "VOWER", "VOXEL", "VOZHD", "VRAIC", "VRILS", "VROOM", "VROUS", "VROUW", + "VROWS", "VUGGS", "VUGGY", "VUGHS", "VUGHY", "VULGO", "VULNS", "VULVA", "VUTTY", "WAACS", "WACKE", "WACKO", + "WACKS", "WADDS", "WADDY", "WADED", "WADER", "WADES", "WADGE", "WADIS", "WADTS", "WAFFS", "WAFTS", "WAGED", + "WAGES", "WAGGA", "WAGYU", "WAHOO", "WAIDE", "WAIFS", "WAIFT", "WAILS", "WAINS", "WAIRS", "WAITE", "WAITS", + "WAKAS", "WAKED", "WAKEN", "WAKER", "WAKES", "WAKFS", "WALDO", "WALDS", "WALED", "WALER", "WALES", "WALIE", + "WALIS", "WALKS", "WALLA", "WALLS", "WALLY", "WALTY", "WAMED", "WAMES", "WAMUS", "WANDS", "WANED", "WANES", + "WANEY", "WANGS", "WANKS", "WANKY", "WANLE", "WANLY", "WANNA", "WANTS", "WANTY", "WANZE", "WAQFS", "WARBS", + "WARBY", "WARDS", "WARED", "WARES", "WAREZ", "WARKS", "WARMS", "WARNS", "WARPS", "WARRE", "WARST", "WARTS", + "WASES", "WASHY", "WASMS", "WASPS", "WASPY", "WASTS", "WATAP", "WATTS", "WAUFF", "WAUGH", "WAUKS", "WAULK", + "WAULS", "WAURS", "WAVED", "WAVES", "WAVEY", "WAWAS", "WAWES", "WAWLS", "WAXED", "WAXER", "WAXES", "WAYED", + "WAZIR", "WAZOO", "WEALD", "WEALS", "WEAMB", "WEANS", "WEARS", "WEBBY", "WEBER", "WECHT", "WEDEL", "WEDGY", + "WEEDS", "WEEKE", "WEEKS", "WEELS", "WEEMS", "WEENS", "WEENY", "WEEPS", "WEEPY", "WEEST", "WEETE", "WEETS", + "WEFTE", "WEFTS", "WEIDS", "WEILS", "WEIRS", "WEISE", "WEIZE", "WEKAS", "WELDS", "WELKE", "WELKS", "WELKT", + "WELLS", "WELLY", "WELTS", "WEMBS", "WENCH", "WENDS", "WENGE", "WENNY", "WENTS", "WEROS", "WERSH", "WESTS", + "WETAS", "WETLY", "WEXED", "WEXES", "WHAMO", "WHAMS", "WHANG", "WHAPS", "WHARE", "WHATA", "WHATS", "WHAUP", + "WHAUR", "WHEAL", "WHEAR", "WHEEN", "WHEEP", "WHEFT", "WHELK", "WHELM", "WHENS", "WHETS", "WHEWS", "WHEYS", + "WHIDS", "WHIFT", "WHIGS", "WHILK", "WHIMS", "WHINS", "WHIOS", "WHIPS", "WHIPT", "WHIRR", "WHIRS", "WHISH", + "WHISS", "WHIST", "WHITS", "WHITY", "WHIZZ", "WHOMP", "WHOOF", "WHOOT", "WHOPS", "WHORE", "WHORL", "WHORT", + "WHOSO", "WHOWS", "WHUMP", "WHUPS", "WHYDA", "WICCA", "WICKS", "WICKY", "WIDDY", "WIDES", "WIELS", "WIFED", + "WIFES", "WIFEY", "WIFIE", "WIFTY", "WIGAN", "WIGGA", "WIGGY", "WIKIS", "WILCO", "WILDS", "WILED", "WILES", + "WILGA", "WILIS", "WILJA", "WILLS", "WILTS", "WIMPS", "WINDS", "WINED", "WINES", "WINEY", "WINGE", "WINGS", + "WINGY", "WINKS", "WINNA", "WINNS", "WINOS", "WINZE", "WIPED", "WIPER", "WIPES", "WIRED", "WIRER", "WIRES", + "WIRRA", "WISED", "WISES", "WISHA", "WISHT", "WISPS", "WISTS", "WITAN", "WITED", "WITES", "WITHE", "WITHS", + "WITHY", "WIVED", "WIVER", "WIVES", "WIZEN", "WIZES", "WOADS", "WOALD", "WOCKS", "WODGE", "WOFUL", "WOJUS", + "WOKER", "WOKKA", "WOLDS", "WOLFS", "WOLLY", "WOLVE", "WOMBS", "WOMBY", "WOMYN", "WONGA", "WONGI", "WONKS", + "WONKY", "WONTS", "WOODS", "WOOED", "WOOFS", "WOOFY", "WOOLD", "WOOLS", "WOONS", "WOOPS", "WOOPY", "WOOSE", + "WOOSH", "WOOTZ", "WORDS", "WORKS", "WORMS", "WORMY", "WORTS", "WOWED", "WOWEE", "WOXEN", "WRANG", "WRAPS", + "WRAPT", "WRAST", "WRATE", "WRAWL", "WRENS", "WRICK", "WRIED", "WRIER", "WRIES", "WRITS", "WROKE", "WROOT", + "WROTH", "WRYER", "WUDDY", "WUDUS", "WULLS", "WURST", "WUSES", "WUSHU", "WUSSY", "WUXIA", "WYLED", "WYLES", + "WYNDS", "WYNNS", "WYTED", "WYTES", "XEBEC", "XENIA", "XENIC", "XENON", "XERIC", "XEROX", "XERUS", "XOANA", + "XRAYS", "XYLAN", "XYLEM", "XYLIC", "XYLOL", "XYLYL", "XYSTI", "XYSTS", "YAARS", "YABAS", "YABBA", "YABBY", + "YACCA", "YACKA", "YACKS", "YAFFS", "YAGER", "YAGES", "YAGIS", "YAHOO", "YAIRD", "YAKKA", "YAKOW", "YALES", + "YAMEN", "YAMPY", "YAMUN", "YANGS", "YANKS", "YAPOK", "YAPON", "YAPPS", "YAPPY", "YARAK", "YARCO", "YARDS", + "YARER", "YARFA", "YARKS", "YARNS", "YARRS", "YARTA", "YARTO", "YATES", "YAUDS", "YAULD", "YAUPS", "YAWED", + "YAWEY", "YAWLS", "YAWNS", "YAWNY", "YAWPS", "YBORE", "YCLAD", "YCLED", "YCOND", "YDRAD", "YDRED", "YEADS", + "YEAHS", "YEALM", "YEANS", "YEARD", "YEARS", "YECCH", "YECHS", "YECHY", "YEDES", "YEEDS", "YEESH", "YEGGS", + "YELKS", "YELLS", "YELMS", "YELPS", "YELTS", "YENTA", "YENTE", "YERBA", "YERDS", "YERKS", "YESES", "YESKS", + "YESTS", "YESTY", "YETIS", "YETTS", "YEUKS", "YEUKY", "YEVEN", "YEVES", "YEWEN", "YEXED", "YEXES", "YFERE", + "YIKED", "YIKES", "YILLS", "YINCE", "YIPES", "YIPPY", "YIRDS", "YIRKS", "YIRRS", "YIRTH", "YITES", "YITIE", + "YLEMS", "YLIKE", "YLKES", "YMOLT", "YMPES", "YOBBO", "YOBBY", "YOCKS", "YODEL", "YODHS", "YODLE", "YOGAS", + "YOGEE", "YOGHS", "YOGIC", "YOGIN", "YOGIS", "YOICK", "YOJAN", "YOKED", "YOKEL", "YOKER", "YOKES", "YOKUL", + "YOLKS", "YOLKY", "YOMIM", "YOMPS", "YONIC", "YONIS", "YONKS", "YOOFS", "YOOPS", "YORES", "YORKS", "YORPS", + "YOUKS", "YOURN", "YOURS", "YOURT", "YOUSE", "YOWED", "YOWES", "YOWIE", "YOWLS", "YOWZA", "YRAPT", "YRENT", + "YRIVD", "YRNEH", "YSAME", "YTOST", "YUANS", "YUCAS", "YUCCA", "YUCCH", "YUCKO", "YUCKS", "YUCKY", "YUFTS", + "YUGAS", "YUKED", "YUKES", "YUKKY", "YUKOS", "YULAN", "YULES", "YUMMO", "YUMMY", "YUMPS", "YUPON", "YUPPY", + "YURTA", "YURTS", "YUZUS", "ZABRA", "ZACKS", "ZAIDA", "ZAIDY", "ZAIRE", "ZAKAT", "ZAMAN", "ZAMBO", "ZAMIA", + "ZANJA", "ZANTE", "ZANZA", "ZANZE", "ZAPPY", "ZARFS", "ZARIS", "ZATIS", "ZAXES", "ZAYIN", "ZAZEN", "ZEALS", + "ZEBEC", "ZEBUB", "ZEBUS", "ZEDAS", "ZEINS", "ZENDO", "ZERDA", "ZERKS", "ZEROS", "ZESTS", "ZETAS", "ZEXES", + "ZEZES", "ZHOMO", "ZIBET", "ZIFFS", "ZIGAN", "ZILAS", "ZILCH", "ZILLA", "ZILLS", "ZIMBI", "ZIMBS", "ZINCO", + "ZINCS", "ZINCY", "ZINEB", "ZINES", "ZINGS", "ZINGY", "ZINKE", "ZINKY", "ZIPPO", "ZIPPY", "ZIRAM", "ZITIS", + "ZIZEL", "ZIZIT", "ZLOTE", "ZLOTY", "ZOAEA", "ZOBOS", "ZOBUS", "ZOCCO", "ZOEAE", "ZOEAL", "ZOEAS", "ZOISM", + "ZOIST", "ZOMBI", "ZONAE", "ZONDA", "ZONED", "ZONER", "ZONES", "ZONKS", "ZOOEA", "ZOOEY", "ZOOID", "ZOOKS", + "ZOOMS", "ZOONS", "ZOOTY", "ZOPPA", "ZOPPO", "ZORIL", "ZORIS", "ZORRO", "ZOUKS", "ZOWEE", "ZOWIE", "ZULUS", + "ZUPAN", "ZUPAS", "ZUPPA", "ZURFS", "ZUZIM", "ZYGAL", "ZYGON", "ZYMES", "ZYMIC", +] + +alphabet = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'] + +def most_used_letters(words=valid_list, letters=alphabet, print_result=True): + ''' + Outputs how many times each letter is used in the words array. + ''' + use_each_letter = {} + for i in letters: + count = 0 + for word in words: + for letter in word: + if i.upper() == letter.upper(): + count+=1 + break + use_each_letter[i] = count + use_each_letter = dict(sorted(use_each_letter.items(), key=lambda item: item[1], reverse=True)) + if print_result: + print("Letter | Usage | Percent") + print("----------------------------") + for k in use_each_letter: + print(f"{k.upper()} | {use_each_letter[k]:5} | {round((100 * use_each_letter[k]) / len(words)):2}%") + return use_each_letter + + +def list_of_valid_words(letters, words=valid_list): + ''' + Outputs the array of valid words that can be made with the combination of letters. + ''' + letters = sorted(letters) + for i, letter in enumerate(letters): # Force all letters to be capitalized + letters[i] = letter.upper() + valid_words = [] + for word in words: + valid_word = True + for letter in word: + if letter.upper() not in letters: + valid_word = False + break + if valid_word and word not in valid_words: + valid_words.append(word) + return valid_words + + +def rearrange_words_by_uniqueness(words): + unique = [word for word in words if len(word) == len(set(word))] + duplicates = [word for word in words if len(word) != len(set(word))] + return unique + duplicates, len(unique) + +def capitalize_all_and_remove_duplicates(arr): + for i,word in enumerate(arr): + arr[i] = word.upper() + result = [] + for word in arr: + if word not in result: + result.append(word) + return arr + + +def clean_chars(string): + # D looks bad on the watch when d does not. + string = string.replace('D', 'd') + return string + + +def print_valid_words(letters=alphabet): + ''' + Prints the array of valid words that the wordle_face.c can use + ''' + print("#ifndef WORDLE_FACE_DICT_H_") + print("#define WORDLE_FACE_DICT_H_") + + print("\n#ifndef WORDLE_LENGTH") + print("#define WORDLE_LENGTH 5") + print("#endif") + + print("\n#ifndef WORDLE_USE_RANDOM_GUESS") + print("#define WORDLE_USE_RANDOM_GUESS 2") + print("#endif\n") + + items_per_row = 9 + top_words_percent = 3 + valid_words = list_of_valid_words(letters, valid_list) + valid_words = capitalize_all_and_remove_duplicates(valid_words) + random.shuffle(valid_words) + # Just in case the watch's random function is too pseudo, better to shuffle th elist so it's less likely to always have the same starting letter + valid_words, num_uniq = rearrange_words_by_uniqueness(valid_words) + best_words = list(best_first_word(letters=letters, print_result=False).keys()) + num_best_words = round(len(valid_words) * top_words_percent / 100) + for i in range(num_best_words, 0, -1): + valid_words.insert(0, valid_words.pop(valid_words.index(best_words[i-1]))) + + print("static const char _valid_letters[] = {", end='') + letters = sorted(letters) + for letter in letters[:-1]: + print(f"'{clean_chars(letter)}', ", end='') + print(f"'{letters[-1]}'" + "};") + print("") + print(f"// From: {source_link}") + print(f"// Number of words found: {len(valid_words)}") + print("static const char _valid_words[][WORDLE_LENGTH + 1] = {") + i = 0 + while i < len(valid_words): + print(" ", end='') + for _ in range(min(items_per_row, len(valid_words)-i)): + print(f'"{clean_chars(valid_words[i])}", ', end='') + i+=1 + print('') + possible_words = list_of_valid_words(letters, possible_list) + possible_words = [word for word in possible_words if word not in valid_list] + possible_words = capitalize_all_and_remove_duplicates(possible_words) + print("};") + print("\n// These are words that'll never be used, but still need to be in the dictionary for guesses.") + print(f"// Number of words found: {len(possible_words)}") + print("static const char _possible_words[][WORDLE_LENGTH + 1] = {") + print("#if !WORDLE_ALLOW_NON_WORD_AND_REPEAT_GUESSES") + i = 0 + while i < len(possible_words): + print(" ", end='') + for j in range(min(items_per_row, len(possible_words)-i)): + print(f'"{clean_chars(possible_words[i])}", ', end='') + i+=1 + print('') + print("#endif") + print("};\n") + + print("#if (WORDLE_USE_RANDOM_GUESS == 3)") + print(f"static const uint16_t _num_random_guess_words = {num_best_words}; // The valid_words array begins with this many words that are considered the top {top_words_percent}% best options.") + print("#elif (WORDLE_USE_RANDOM_GUESS == 2)") + print(f"static const uint16_t _num_random_guess_words = {num_uniq}; // The valid_words array begins with this many words where each letter is different.") + print("#elif (WORDLE_USE_RANDOM_GUESS == 1)") + print("static const uint16_t _num_random_guess_words = _num_words;") + print("#endif") + print("\n#endif // WORDLE_FACE_DICT_H_") + + +def get_sec_val_and_units(seconds): + if seconds < 1: + return f"{round(seconds * 1000)} ms" + hours = int(seconds // 3600) + minutes = int((seconds % 3600) // 60) + secs = int(seconds % 60) + if hours > 0: + return f"{hours} hr {minutes} min {secs} sec" + elif minutes > 0: + return f"{minutes} min {secs} sec" + else: + return f"{secs} sec" + + +def txt_of_all_letter_combos(num_letters_in_set, words=valid_list, min_letter_occ_percent_to_consider=10, txt_out=True): + ''' + Creates a txt file that shows every combination of letters and how many words + their combo can make. + num_letters_in_set - How many letters should be in each combination + ''' + num_status_prints = 100 + dict_combos_counts = {} + print_iter = 0 + prev = time.time() + start = prev + letters_to_ignore = ['D','M'] # Don't diplay well on the watch + letter_usage = most_used_letters(words=words, print_result=False) + for letter in letter_usage: + if (100 * letter_usage[letter])/len(words) < min_letter_occ_percent_to_consider: + letters_to_ignore.append(letter) + valid_letters = [item for item in alphabet if item not in letters_to_ignore] + num_letters_in_set = min(num_letters_in_set, len(valid_letters)) + print(f"Finding all {num_letters_in_set} letter combinations with the following letters: {valid_letters}") + all_combos = list(itertools.combinations(valid_letters, num_letters_in_set)) + len_all_combos = len(all_combos) + to_print = max(1, int(len_all_combos/ num_status_prints)) + print(f"Amount of Combos: {len_all_combos}") + estimated_prints = round(len_all_combos / to_print) + for i, letters in enumerate(all_combos): + letters = sorted(letters) + dict_combos_counts[repr(letters)] = len(list_of_valid_words(letters, words=words)) + print_iter+=1 + if print_iter >= to_print: + curr = time.time() + delta = curr - prev + time_passed = curr - start + total_time_estimate = delta * estimated_prints + time_left_estimate = (delta * estimated_prints) - time_passed + output = f"Time Passed: {get_sec_val_and_units(time_passed)} | " + output+= f"Amount of time for {to_print} items: {get_sec_val_and_units(delta)} | " + output+= f"Estimate for total: {get_sec_val_and_units(total_time_estimate)} | " + output+= f"items Left {len_all_combos - i} | " + output+= f"Percent Complete {round((100 * i) / len_all_combos)}% | " + output+= f"Estimated Time Left : {get_sec_val_and_units(time_left_estimate)}" + print(output) + prev = curr + print_iter = 0 + dict_combos_counts = dict(sorted(dict_combos_counts.items(), key=lambda item: item[1], reverse=True)) + + most_common_key = next(iter(dict_combos_counts)) + print(f"The Most Common Combo for {num_letters_in_set} letters is: {most_common_key} with {dict_combos_counts[most_common_key]} words.") + # print_valid_words(ast.literal_eval(most_common_key)) # Uncomment to display the text it creates + + if txt_out: + with open('output.txt', 'w') as file: + for key, value in dict_combos_counts.items(): + file.write(f'{key}: {value}\n') + return dict_combos_counts + + +def txt_of_all_letter_combos_differing_sizes(min=9, max=15, num_combos_print=20, words=valid_list): + max_each_count = {} + with open('output.txt', 'w') as file: + for x in range(max, min-1, -1): + dict_combos_counts = txt_of_all_letter_combos(x, words=words, min_letter_occ_percent_to_consider=10, txt_out=False) + file.write(f'{x} Letter Options\n') + for i, key in enumerate(dict_combos_counts): + file.write(f'{key}: {dict_combos_counts[key]}\n') + if i == 0: + max_each_count[x] = dict_combos_counts[key] + if i >= num_combos_print-1: + break + file.write('\n') + print("num_letters, count of words") + for key, item in max_each_count.items(): + print(f"{key}, {item}") + + +def location_of_letters(letters=alphabet, list=valid_list, print_result=True): + letter_location_percentages = {} + if print_result: + print(" 1 2 3 4 5 ") + print("-----------------------------------------") + letters = sorted(letters) + for letter in letters: + location = [0, 0, 0, 0, 0] + for word in list: + for i, char in enumerate(word): + if char.upper() == letter.upper(): + location[i]+=1 + location = [((100 * x) / sum(location)) for x in location] + letter_location_percentages[letter] = location + location_txt = [f"{round(x):2}%" for x in location] + if print_result: + print(f"{letter} : {location_txt}") + return letter_location_percentages + + +def best_first_word(letters=alphabet, list=valid_list, print_result=True, words_to_print=None): + ''' + Word_good has every word with only unique letters as keys and that values are: + 1. Take the usage of every letter, normalize the max to 100 and the min to 0. + 2. Go through each letter in the word and see how often that letter appears in that exact location. + 3. Multiply that occurrance in location with the normalized total usage. + 4. Do this for each letter and add it all together. + + Ex: SLATE + Normalized usage: S=38, L=42, A=79, T=46, E=100 + S in position 1: 55% + L in position 2: 27% + A in position 3: 31% + T in position 4: 19% + E in position 5: 34% + Total = (38*55) + (42*27) + (79*31) + (46*19) + (100*35) = 10047 + ''' + valid_words = list_of_valid_words(letters, list) + letter_usage = most_used_letters(words=list, letters=letters, print_result=False) + a=[[max(letter_usage.values()),1],[min(letter_usage.values()),1]] + b=[100,0] + m,b = numpy.linalg.solve(a,b).tolist() + letter_usage_normalized = {key: ((m * value) + b) for key, value in letter_usage.items()} + loc_letters = location_of_letters(letters=letters, list=list, print_result=False) + word_good = {} + valid_words_unique = [word for word in valid_words if len(word) == len(set(word))] + for word in valid_words_unique: + usage = 0 + for i, char in enumerate(word): + usage += loc_letters[char][i] * letter_usage_normalized[char] + word_good[word] = round(usage) + word_good = {k: v for k, v in sorted(word_good.items(), key=lambda item: item[1], reverse=True)} + if print_result: + print("Word, Usage Value") + print("------------------") + for i,[k,v] in enumerate(word_good.items()): + if words_to_print is not None and i+1 > words_to_print: + break + print(f"{k}, {v}") + return word_good + + +if __name__ == "__main__": + my_letters = ['A', 'C', 'E', 'H', 'I', 'L', 'N', 'O', 'P', 'R', 'S', 'T'] + #print(f"{len(list_of_valid_words(my_letters, valid_list))} Words can be made with {my_letters}") + #most_used_letters(letters=my_letters) + #location_of_letters(letters=my_letters) + print_valid_words(my_letters) + #txt_of_all_letter_combos_differing_sizes(max = 16, min=10) + #txt_of_all_letter_combos(14) + #best_first_word(letters=my_letters, print_result=True, words_to_print=10) \ No newline at end of file diff --git a/watch-library/simulator/main.c b/watch-library/simulator/main.c index 6898fd0..5e5070f 100644 --- a/watch-library/simulator/main.c +++ b/watch-library/simulator/main.c @@ -89,6 +89,7 @@ void main_loop_sleep(uint32_t ms) { main_loop_set_sleeping(true); emscripten_sleep(ms); main_loop_set_sleeping(false); + animation_frame_id = ANIMATION_FRAME_ID_INVALID; } bool main_loop_is_sleeping(void) {