]> git.earman.xyz Git - sensor-watch.git/commitdiff
Moved face
authorDavid Volovskiy <devolov@gmail.com>
Tue, 12 Aug 2025 02:03:15 +0000 (22:03 -0400)
committerDavid Volovskiy <devolov@gmail.com>
Tue, 12 Aug 2025 02:03:15 +0000 (22:03 -0400)
legacy/watch_faces/complication/higher_lower_game_face.c [deleted file]
legacy/watch_faces/complication/higher_lower_game_face.h [deleted file]
watch-faces/complication/higher_lower_game_face.c [new file with mode: 0755]
watch-faces/complication/higher_lower_game_face.h [new file with mode: 0755]

diff --git a/legacy/watch_faces/complication/higher_lower_game_face.c b/legacy/watch_faces/complication/higher_lower_game_face.c
deleted file mode 100755 (executable)
index 3491b5b..0000000
+++ /dev/null
@@ -1,393 +0,0 @@
-/*\r
- * MIT License\r
- *\r
- * Copyright (c) 2023 Chris Ellis\r
- *\r
- * Permission is hereby granted, free of charge, to any person obtaining a copy\r
- * of this software and associated documentation files (the "Software"), to deal\r
- * in the Software without restriction, including without limitation the rights\r
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\r
- * copies of the Software, and to permit persons to whom the Software is\r
- * furnished to do so, subject to the following conditions:\r
- *\r
- * The above copyright notice and this permission notice shall be included in all\r
- * copies or substantial portions of the Software.\r
- *\r
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\r
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\r
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\r
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\r
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\r
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\r
- * SOFTWARE.\r
- */\r
-\r
-// Emulator only: need time() to seed the random number generator.\r
-#if __EMSCRIPTEN__\r
-#include <time.h>\r
-#endif\r
-\r
-#include <stdlib.h>\r
-#include <string.h>\r
-#include "higher_lower_game_face.h"\r
-#include "watch_private_display.h"\r
-\r
-#define TITLE_TEXT "Hi-Lo"\r
-#define GAME_BOARD_SIZE 6\r
-#define MAX_BOARDS 40\r
-#define GUESSES_PER_SCREEN 5\r
-#define WIN_SCORE (MAX_BOARDS * GUESSES_PER_SCREEN)\r
-#define STATUS_DISPLAY_START 0\r
-#define BOARD_SCORE_DISPLAY_START 2\r
-#define BOARD_DISPLAY_START 4\r
-#define BOARD_DISPLAY_END 9\r
-#define MIN_CARD_VALUE 2\r
-#define MAX_CARD_VALUE 14\r
-#define CARD_RANK_COUNT (MAX_CARD_VALUE - MIN_CARD_VALUE + 1)\r
-#define CARD_SUIT_COUNT 4\r
-#define DECK_SIZE (CARD_SUIT_COUNT * CARD_RANK_COUNT)\r
-#define FLIP_BOARD_DIRECTION false\r
-\r
-typedef struct card_t {\r
-    uint8_t value;\r
-    bool revealed;\r
-} card_t;\r
-\r
-typedef enum {\r
-    A, B, C, D, E, F, G\r
-} segment_t;\r
-\r
-typedef enum {\r
-    HL_GUESS_EQUAL,\r
-    HL_GUESS_HIGHER,\r
-    HL_GUESS_LOWER\r
-} guess_t;\r
-\r
-typedef enum {\r
-    HL_GS_TITLE_SCREEN,\r
-    HL_GS_GUESSING,\r
-    HL_GS_WIN,\r
-    HL_GS_LOSE,\r
-    HL_GS_SHOW_SCORE,\r
-} game_state_t;\r
-\r
-static game_state_t game_state = HL_GS_TITLE_SCREEN;\r
-static card_t game_board[GAME_BOARD_SIZE] = {0};\r
-static uint8_t guess_position = 0;\r
-static uint8_t score = 0;\r
-static uint8_t completed_board_count = 0;\r
-static uint8_t deck[DECK_SIZE] = {0};\r
-static uint8_t current_card = 0;\r
-\r
-static uint8_t generate_random_number(uint8_t num_values) {\r
-    // Emulator: use rand. Hardware: use arc4random.\r
-#if __EMSCRIPTEN__\r
-    return rand() % num_values;\r
-#else\r
-    return arc4random_uniform(num_values);\r
-#endif\r
-}\r
-\r
-static void stack_deck(void) {\r
-    for (size_t i = 0; i < CARD_RANK_COUNT; i++) {\r
-        for (size_t j = 0; j < CARD_SUIT_COUNT; j++)\r
-            deck[(i * CARD_SUIT_COUNT) + j] = MIN_CARD_VALUE + i;\r
-    }\r
-}\r
-\r
-static void shuffle_deck(void) {\r
-    // Randomize shuffle with Fisher Yates\r
-    size_t i;\r
-    size_t j;\r
-    uint8_t tmp;\r
-\r
-    for (i = DECK_SIZE - 1; i > 0; i--) {\r
-        j = generate_random_number(0xFF) % (i + 1);\r
-        tmp = deck[j];\r
-        deck[j] = deck[i];\r
-        deck[i] = tmp;\r
-    }\r
-}\r
-\r
-static void reset_deck(void) {\r
-    current_card = 0;\r
-    stack_deck();\r
-    shuffle_deck();\r
-}\r
-\r
-static uint8_t get_next_card(void) {\r
-    if (current_card >= DECK_SIZE)\r
-        reset_deck();\r
-    return deck[current_card++];\r
-}\r
-\r
-static void reset_board(bool first_round) {\r
-    // First card is random on the first board, and carried over from the last position on subsequent boards\r
-    const uint8_t first_card_value = first_round\r
-                                     ? get_next_card()\r
-                                     : game_board[GAME_BOARD_SIZE - 1].value;\r
-\r
-    game_board[0].value = first_card_value;\r
-    game_board[0].revealed = true; // Always reveal first card\r
-\r
-    // Fill remainder of board\r
-    for (size_t i = 1; i < GAME_BOARD_SIZE; ++i) {\r
-        game_board[i] = (card_t) {\r
-                .value = get_next_card(),\r
-                .revealed = false\r
-        };\r
-    }\r
-}\r
-\r
-static void init_game(void) {\r
-    watch_clear_display();\r
-    watch_display_string(TITLE_TEXT, BOARD_DISPLAY_START);\r
-    watch_display_string("GA", STATUS_DISPLAY_START);\r
-    reset_deck();\r
-    reset_board(true);\r
-    score = 0;\r
-    completed_board_count = 0;\r
-    guess_position = 1;\r
-}\r
-\r
-static void set_segment_at_position(segment_t segment, uint8_t position) {\r
-    const uint64_t position_segment_data = (Segment_Map[position] >> (8 * (uint8_t) segment)) & 0xFF;\r
-    const uint8_t com_pin = position_segment_data >> 6;\r
-    const uint8_t seg = position_segment_data & 0x3F;\r
-    watch_set_pixel(com_pin, seg);\r
-}\r
-\r
-static void render_board_position(size_t board_position) {\r
-    const size_t display_position = FLIP_BOARD_DIRECTION\r
-                                    ? BOARD_DISPLAY_START + board_position\r
-                                    : BOARD_DISPLAY_END - board_position;\r
-    const bool revealed = game_board[board_position].revealed;\r
-\r
-    //// Current position indicator spot\r
-    //if (board_position == guess_position) {\r
-    //    watch_display_character('-', display_position);\r
-    //    return;\r
-    //}\r
-\r
-    if (!revealed) {\r
-        // Higher or lower indicator (currently just an empty space)\r
-        watch_display_character(' ', display_position);\r
-        //set_segment_at_position(F, display_position);\r
-        return;\r
-    }\r
-\r
-    const uint8_t value = game_board[board_position].value;\r
-    switch (value) {\r
-        case 14: // A (≡)\r
-            watch_display_character(' ', display_position);\r
-            set_segment_at_position(A, display_position);\r
-            set_segment_at_position(D, display_position);\r
-            set_segment_at_position(G, display_position);\r
-            break;\r
-        case 13: // K (=)\r
-            watch_display_character(' ', display_position);\r
-            set_segment_at_position(A, display_position);\r
-            set_segment_at_position(D, display_position);\r
-            break;\r
-        case 12: // Q (-)\r
-            watch_display_character('-', display_position);\r
-            break;\r
-        default: {\r
-            const char display_char = (value - MIN_CARD_VALUE) + '0';\r
-            watch_display_character(display_char, display_position);\r
-        }\r
-    }\r
-}\r
-\r
-static void render_board(void) {\r
-    for (size_t i = 0; i < GAME_BOARD_SIZE; ++i) {\r
-        render_board_position(i);\r
-    }\r
-}\r
-\r
-static void render_board_count(void) {\r
-    // Render completed boards (screens)\r
-    char buf[3] = {0};\r
-    snprintf(buf, sizeof(buf), "%2hhu", completed_board_count);\r
-    watch_display_string(buf, BOARD_SCORE_DISPLAY_START);\r
-}\r
-\r
-static void render_final_score(void) {\r
-    watch_display_string("SC", STATUS_DISPLAY_START);\r
-    char buf[7] = {0};\r
-    const uint8_t complete_boards = score / GUESSES_PER_SCREEN;\r
-    snprintf(buf, sizeof(buf), "%2hu %03hu", complete_boards, score);\r
-    watch_set_colon();\r
-    watch_display_string(buf, BOARD_DISPLAY_START);\r
-}\r
-\r
-static guess_t get_answer(void) {\r
-    if (guess_position < 1 || guess_position > GAME_BOARD_SIZE)\r
-        return HL_GUESS_EQUAL; // Maybe add an error state, shouldn't ever hit this.\r
-\r
-    game_board[guess_position].revealed = true;\r
-    const uint8_t previous_value = game_board[guess_position - 1].value;\r
-    const uint8_t current_value = game_board[guess_position].value;\r
-\r
-    if (current_value > previous_value)\r
-        return HL_GUESS_HIGHER;\r
-    else if (current_value < previous_value)\r
-        return HL_GUESS_LOWER;\r
-    else\r
-        return HL_GUESS_EQUAL;\r
-}\r
-\r
-static void do_game_loop(guess_t user_guess) {\r
-    switch (game_state) {\r
-        case HL_GS_TITLE_SCREEN:\r
-            init_game();\r
-            render_board();\r
-            render_board_count();\r
-            game_state = HL_GS_GUESSING;\r
-            break;\r
-        case HL_GS_GUESSING: {\r
-            const guess_t answer = get_answer();\r
-\r
-            // Render answer indicator\r
-            switch (answer) {\r
-                case HL_GUESS_EQUAL:\r
-                    watch_display_string("==", STATUS_DISPLAY_START);\r
-                    break;\r
-                case HL_GUESS_HIGHER:\r
-                    watch_display_string("HI", STATUS_DISPLAY_START);\r
-                    break;\r
-                case HL_GUESS_LOWER:\r
-                    watch_display_string("LO", STATUS_DISPLAY_START);\r
-                    break;\r
-            }\r
-\r
-            // Scoring\r
-            if (answer == user_guess) {\r
-                score++;\r
-            } else if (answer == HL_GUESS_EQUAL) {\r
-                // No score for two consecutive identical cards\r
-            } else {\r
-                // Incorrect guess, game over\r
-                watch_display_string("GO", STATUS_DISPLAY_START);\r
-                game_board[guess_position].revealed = true;\r
-                render_board_position(guess_position);\r
-                game_state = HL_GS_LOSE;\r
-                return;\r
-            }\r
-\r
-            if (score >= WIN_SCORE) {\r
-                // Win, perhaps some kind of animation sequence?\r
-                watch_display_string("WI", STATUS_DISPLAY_START);\r
-                watch_display_string("  ", BOARD_SCORE_DISPLAY_START);\r
-                watch_display_string("------", BOARD_DISPLAY_START);\r
-                game_state = HL_GS_WIN;\r
-                return;\r
-            }\r
-\r
-            // Next guess position\r
-            const bool final_board_guess = guess_position == GAME_BOARD_SIZE - 1;\r
-            if (final_board_guess) {\r
-                // Seed new board\r
-                completed_board_count++;\r
-                render_board_count();\r
-                guess_position = 1;\r
-                reset_board(false);\r
-                render_board();\r
-            } else {\r
-                guess_position++;\r
-                render_board_position(guess_position - 1);\r
-                render_board_position(guess_position);\r
-            }\r
-            break;\r
-        }\r
-        case HL_GS_WIN:\r
-        case HL_GS_LOSE:\r
-            // Show score screen on button press from either state\r
-            watch_clear_display();\r
-            render_final_score();\r
-            game_state = HL_GS_SHOW_SCORE;\r
-            break;\r
-        case HL_GS_SHOW_SCORE:\r
-            watch_clear_display();\r
-            watch_display_string(TITLE_TEXT, BOARD_DISPLAY_START);\r
-            watch_display_string("GA", STATUS_DISPLAY_START);\r
-            game_state = HL_GS_TITLE_SCREEN;\r
-            break;\r
-        default:\r
-            watch_display_string("ERROR", BOARD_DISPLAY_START);\r
-            break;\r
-    }\r
-}\r
-\r
-static void light_button_handler(void) {\r
-    do_game_loop(HL_GUESS_HIGHER);\r
-}\r
-\r
-static void alarm_button_handler(void) {\r
-    do_game_loop(HL_GUESS_LOWER);\r
-}\r
-\r
-void higher_lower_game_face_setup(uint8_t watch_face_index, void **context_ptr) {\r
-    (void) watch_face_index;\r
-\r
-    if (*context_ptr == NULL) {\r
-        *context_ptr = malloc(sizeof(higher_lower_game_face_state_t));\r
-        memset(*context_ptr, 0, sizeof(higher_lower_game_face_state_t));\r
-        // Do any one-time tasks in here; the inside of this conditional happens only at boot.\r
-        memset(game_board, 0, sizeof(game_board));\r
-    }\r
-    // Do any pin or peripheral setup here; this will be called whenever the watch wakes from deep sleep.\r
-}\r
-\r
-void higher_lower_game_face_activate(void *context) {\r
-    higher_lower_game_face_state_t *state = (higher_lower_game_face_state_t *) context;\r
-    (void) state;\r
-    // Handle any tasks related to your watch face coming on screen.\r
-    game_state = HL_GS_TITLE_SCREEN;\r
-}\r
-\r
-bool higher_lower_game_face_loop(movement_event_t event, void *context) {\r
-    higher_lower_game_face_state_t *state = (higher_lower_game_face_state_t *) context;\r
-    (void) state;\r
-\r
-    switch (event.event_type) {\r
-        case EVENT_ACTIVATE:\r
-            // Show your initial UI here.\r
-            watch_display_string(TITLE_TEXT, BOARD_DISPLAY_START);\r
-            watch_display_string("GA", STATUS_DISPLAY_START);\r
-            break;\r
-        case EVENT_TICK:\r
-            // If needed, update your display here.\r
-            break;\r
-        case EVENT_LIGHT_BUTTON_UP:\r
-            light_button_handler();\r
-            break;\r
-        case EVENT_LIGHT_BUTTON_DOWN:\r
-            // Don't trigger light\r
-            break;\r
-        case EVENT_ALARM_BUTTON_UP:\r
-            alarm_button_handler();\r
-            break;\r
-        case EVENT_TIMEOUT:\r
-            // Your watch face will receive this event after a period of inactivity. If it makes sense to resign,\r
-            // you may uncomment this line to move back to the first watch face in the list:\r
-            // movement_move_to_face(0);\r
-            break;\r
-        default:\r
-            return movement_default_loop_handler(event);\r
-    }\r
-\r
-    // return true if the watch can enter standby mode. Generally speaking, you should always return true.\r
-    // Exceptions:\r
-    //  * If you are displaying a color using the low-level watch_set_led_color function, you should return false.\r
-    //  * If you are sounding the buzzer using the low-level watch_set_buzzer_on function, you should return false.\r
-    // Note that if you are driving the LED or buzzer using Movement functions like movement_illuminate_led or\r
-    // movement_play_alarm, you can still return true. This guidance only applies to the low-level watch_ functions.\r
-    return true;\r
-}\r
-\r
-void higher_lower_game_face_resign(void *context) {\r
-    (void) context;\r
-\r
-    // handle any cleanup before your watch face goes off-screen.\r
-}\r
diff --git a/legacy/watch_faces/complication/higher_lower_game_face.h b/legacy/watch_faces/complication/higher_lower_game_face.h
deleted file mode 100755 (executable)
index 51689b5..0000000
+++ /dev/null
@@ -1,106 +0,0 @@
-/*\r
- * MIT License\r
- *\r
- * Copyright (c) 2023 Chris Ellis\r
- *\r
- * Permission is hereby granted, free of charge, to any person obtaining a copy\r
- * of this software and associated documentation files (the "Software"), to deal\r
- * in the Software without restriction, including without limitation the rights\r
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\r
- * copies of the Software, and to permit persons to whom the Software is\r
- * furnished to do so, subject to the following conditions:\r
- *\r
- * The above copyright notice and this permission notice shall be included in all\r
- * copies or substantial portions of the Software.\r
- *\r
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\r
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\r
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\r
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\r
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\r
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\r
- * SOFTWARE.\r
- */\r
-\r
-#ifndef HIGHER_LOWER_GAME_FACE_H_\r
-#define HIGHER_LOWER_GAME_FACE_H_\r
-\r
-#include "movement.h"\r
-\r
-/*\r
- * Higher-Lower game face\r
- * ======================\r
- *\r
- * A game face based on the "higher-lower" card game where the objective is to correctly guess if the next card will\r
- * be higher or lower than the last revealed cards.\r
- *\r
- * Game Flow:\r
- * - When the face is selected, the "Hi-Lo" "Title" screen will be displayed, and the status indicator will display "GA" for game\r
- * - Pressing `ALARM` or `LIGHT` will start the game and proceed to the "Guessing" screen\r
- *   - The first card will be revealed and the player must now make a guess\r
- *   - A player can guess `Higher` by pressing the `LIGHT` button, and `Lower` by pressing the `ALARM` button\r
- *   - The status indicator will show the result of the guess: HI (Higher), LO (Lower), or == (Equal)\r
- *   - There are five guesses to make on each game screen, once the end of the screen is reached, a new screen\r
- *     will be started, with the last revealed card carried over\r
- *   - The number of completed screens is displayed in the top right (see Scoring)\r
- * - If the player has guessed correctly, the score is updated and play continues (see Scoring)\r
- * - If the player has guessed incorrectly, the status will change to GO (Game Over)\r
- *   - The current card will be revealed\r
- *   - Pressing `ALARM` or `LIGHT` will transition to the "Score" screen\r
- * - If the game is won, the status indicator will display "WI" and the "Win" screen will be displayed\r
- *   - Pressing `ALARM` or `LIGHT` will transition to the "Score" screen\r
- * - The status indicator will change to "SC" when the final score is displayed\r
- *   - The number of completed game screens will be displayed on using the first two digits\r
- *   - The number of correct guesses will be displayed using the final three digits\r
- *   - E.g. "13: 063" represents 13 completed screens, with 63 correct guesses\r
- * - Pressing `ALARM` or `LIGHT` while on the "Score" screen will transition to back to the "Title" screen\r
- *\r
- * Scoring:\r
- * - If the player guesses correctly (HI/LO) a point is gained\r
- * - If the player guesses incorrectly the game ends\r
- *   - Unless the revealed card is equal (==) to the last card, in which case play continues, but no point is gained\r
- * - If the player completes 40 screens full of cards, the game ends and a win screen is displayed\r
- *\r
- * Misc:\r
- * The face tries to remain true to the spirit of using "cards"; to cope with the display limitations I've arrived at\r
- * the following mapping of card values to screen display, but am open to better suggestions:\r
- *\r
- * Thanks to voloved for adding deck shuffling and drawing!\r
- *\r
- * | Cards   |                          |\r
- * |---------|--------------------------|\r
- * | Value   |2|3|4|5|6|7|8|9|10|J|Q|K|A|\r
- * | Display |0|1|2|3|4|5|6|7|8 |9|-|=|≡|\r
- *\r
- * A previous alternative can be found in the git history:\r
- * | Cards   |                          |\r
- * |---------|--------------------------|\r
- * | Value   |2|3|4|5|6|7|8|9|10|J|Q|K|A|\r
- * | Display |2|3|4|5|6|7|8|9| 0|-|=|≡|H|\r
- *\r
- *\r
- * Future Ideas:\r
- * - Add sounds\r
- * - Save/Display high score\r
- * - Add a "Win" animation\r
- * - Consider using lap indicator for larger score limit\r
- */\r
-\r
-typedef struct {\r
-    // Anything you need to keep track of, put it here!\r
-} higher_lower_game_face_state_t;\r
-\r
-void higher_lower_game_face_setup(uint8_t watch_face_index, void ** context_ptr);\r
-void higher_lower_game_face_activate(void *context);\r
-bool higher_lower_game_face_loop(movement_event_t event, void *context);\r
-void higher_lower_game_face_resign(void *context);\r
-\r
-#define higher_lower_game_face ((const watch_face_t){ \\r
-    higher_lower_game_face_setup, \\r
-    higher_lower_game_face_activate, \\r
-    higher_lower_game_face_loop, \\r
-    higher_lower_game_face_resign, \\r
-    NULL, \\r
-})\r
-\r
-#endif // HIGHER_LOWER_GAME_FACE_H_\r
diff --git a/watch-faces/complication/higher_lower_game_face.c b/watch-faces/complication/higher_lower_game_face.c
new file mode 100755 (executable)
index 0000000..3491b5b
--- /dev/null
@@ -0,0 +1,393 @@
+/*\r
+ * MIT License\r
+ *\r
+ * Copyright (c) 2023 Chris Ellis\r
+ *\r
+ * Permission is hereby granted, free of charge, to any person obtaining a copy\r
+ * of this software and associated documentation files (the "Software"), to deal\r
+ * in the Software without restriction, including without limitation the rights\r
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\r
+ * copies of the Software, and to permit persons to whom the Software is\r
+ * furnished to do so, subject to the following conditions:\r
+ *\r
+ * The above copyright notice and this permission notice shall be included in all\r
+ * copies or substantial portions of the Software.\r
+ *\r
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\r
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\r
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\r
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\r
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\r
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\r
+ * SOFTWARE.\r
+ */\r
+\r
+// Emulator only: need time() to seed the random number generator.\r
+#if __EMSCRIPTEN__\r
+#include <time.h>\r
+#endif\r
+\r
+#include <stdlib.h>\r
+#include <string.h>\r
+#include "higher_lower_game_face.h"\r
+#include "watch_private_display.h"\r
+\r
+#define TITLE_TEXT "Hi-Lo"\r
+#define GAME_BOARD_SIZE 6\r
+#define MAX_BOARDS 40\r
+#define GUESSES_PER_SCREEN 5\r
+#define WIN_SCORE (MAX_BOARDS * GUESSES_PER_SCREEN)\r
+#define STATUS_DISPLAY_START 0\r
+#define BOARD_SCORE_DISPLAY_START 2\r
+#define BOARD_DISPLAY_START 4\r
+#define BOARD_DISPLAY_END 9\r
+#define MIN_CARD_VALUE 2\r
+#define MAX_CARD_VALUE 14\r
+#define CARD_RANK_COUNT (MAX_CARD_VALUE - MIN_CARD_VALUE + 1)\r
+#define CARD_SUIT_COUNT 4\r
+#define DECK_SIZE (CARD_SUIT_COUNT * CARD_RANK_COUNT)\r
+#define FLIP_BOARD_DIRECTION false\r
+\r
+typedef struct card_t {\r
+    uint8_t value;\r
+    bool revealed;\r
+} card_t;\r
+\r
+typedef enum {\r
+    A, B, C, D, E, F, G\r
+} segment_t;\r
+\r
+typedef enum {\r
+    HL_GUESS_EQUAL,\r
+    HL_GUESS_HIGHER,\r
+    HL_GUESS_LOWER\r
+} guess_t;\r
+\r
+typedef enum {\r
+    HL_GS_TITLE_SCREEN,\r
+    HL_GS_GUESSING,\r
+    HL_GS_WIN,\r
+    HL_GS_LOSE,\r
+    HL_GS_SHOW_SCORE,\r
+} game_state_t;\r
+\r
+static game_state_t game_state = HL_GS_TITLE_SCREEN;\r
+static card_t game_board[GAME_BOARD_SIZE] = {0};\r
+static uint8_t guess_position = 0;\r
+static uint8_t score = 0;\r
+static uint8_t completed_board_count = 0;\r
+static uint8_t deck[DECK_SIZE] = {0};\r
+static uint8_t current_card = 0;\r
+\r
+static uint8_t generate_random_number(uint8_t num_values) {\r
+    // Emulator: use rand. Hardware: use arc4random.\r
+#if __EMSCRIPTEN__\r
+    return rand() % num_values;\r
+#else\r
+    return arc4random_uniform(num_values);\r
+#endif\r
+}\r
+\r
+static void stack_deck(void) {\r
+    for (size_t i = 0; i < CARD_RANK_COUNT; i++) {\r
+        for (size_t j = 0; j < CARD_SUIT_COUNT; j++)\r
+            deck[(i * CARD_SUIT_COUNT) + j] = MIN_CARD_VALUE + i;\r
+    }\r
+}\r
+\r
+static void shuffle_deck(void) {\r
+    // Randomize shuffle with Fisher Yates\r
+    size_t i;\r
+    size_t j;\r
+    uint8_t tmp;\r
+\r
+    for (i = DECK_SIZE - 1; i > 0; i--) {\r
+        j = generate_random_number(0xFF) % (i + 1);\r
+        tmp = deck[j];\r
+        deck[j] = deck[i];\r
+        deck[i] = tmp;\r
+    }\r
+}\r
+\r
+static void reset_deck(void) {\r
+    current_card = 0;\r
+    stack_deck();\r
+    shuffle_deck();\r
+}\r
+\r
+static uint8_t get_next_card(void) {\r
+    if (current_card >= DECK_SIZE)\r
+        reset_deck();\r
+    return deck[current_card++];\r
+}\r
+\r
+static void reset_board(bool first_round) {\r
+    // First card is random on the first board, and carried over from the last position on subsequent boards\r
+    const uint8_t first_card_value = first_round\r
+                                     ? get_next_card()\r
+                                     : game_board[GAME_BOARD_SIZE - 1].value;\r
+\r
+    game_board[0].value = first_card_value;\r
+    game_board[0].revealed = true; // Always reveal first card\r
+\r
+    // Fill remainder of board\r
+    for (size_t i = 1; i < GAME_BOARD_SIZE; ++i) {\r
+        game_board[i] = (card_t) {\r
+                .value = get_next_card(),\r
+                .revealed = false\r
+        };\r
+    }\r
+}\r
+\r
+static void init_game(void) {\r
+    watch_clear_display();\r
+    watch_display_string(TITLE_TEXT, BOARD_DISPLAY_START);\r
+    watch_display_string("GA", STATUS_DISPLAY_START);\r
+    reset_deck();\r
+    reset_board(true);\r
+    score = 0;\r
+    completed_board_count = 0;\r
+    guess_position = 1;\r
+}\r
+\r
+static void set_segment_at_position(segment_t segment, uint8_t position) {\r
+    const uint64_t position_segment_data = (Segment_Map[position] >> (8 * (uint8_t) segment)) & 0xFF;\r
+    const uint8_t com_pin = position_segment_data >> 6;\r
+    const uint8_t seg = position_segment_data & 0x3F;\r
+    watch_set_pixel(com_pin, seg);\r
+}\r
+\r
+static void render_board_position(size_t board_position) {\r
+    const size_t display_position = FLIP_BOARD_DIRECTION\r
+                                    ? BOARD_DISPLAY_START + board_position\r
+                                    : BOARD_DISPLAY_END - board_position;\r
+    const bool revealed = game_board[board_position].revealed;\r
+\r
+    //// Current position indicator spot\r
+    //if (board_position == guess_position) {\r
+    //    watch_display_character('-', display_position);\r
+    //    return;\r
+    //}\r
+\r
+    if (!revealed) {\r
+        // Higher or lower indicator (currently just an empty space)\r
+        watch_display_character(' ', display_position);\r
+        //set_segment_at_position(F, display_position);\r
+        return;\r
+    }\r
+\r
+    const uint8_t value = game_board[board_position].value;\r
+    switch (value) {\r
+        case 14: // A (≡)\r
+            watch_display_character(' ', display_position);\r
+            set_segment_at_position(A, display_position);\r
+            set_segment_at_position(D, display_position);\r
+            set_segment_at_position(G, display_position);\r
+            break;\r
+        case 13: // K (=)\r
+            watch_display_character(' ', display_position);\r
+            set_segment_at_position(A, display_position);\r
+            set_segment_at_position(D, display_position);\r
+            break;\r
+        case 12: // Q (-)\r
+            watch_display_character('-', display_position);\r
+            break;\r
+        default: {\r
+            const char display_char = (value - MIN_CARD_VALUE) + '0';\r
+            watch_display_character(display_char, display_position);\r
+        }\r
+    }\r
+}\r
+\r
+static void render_board(void) {\r
+    for (size_t i = 0; i < GAME_BOARD_SIZE; ++i) {\r
+        render_board_position(i);\r
+    }\r
+}\r
+\r
+static void render_board_count(void) {\r
+    // Render completed boards (screens)\r
+    char buf[3] = {0};\r
+    snprintf(buf, sizeof(buf), "%2hhu", completed_board_count);\r
+    watch_display_string(buf, BOARD_SCORE_DISPLAY_START);\r
+}\r
+\r
+static void render_final_score(void) {\r
+    watch_display_string("SC", STATUS_DISPLAY_START);\r
+    char buf[7] = {0};\r
+    const uint8_t complete_boards = score / GUESSES_PER_SCREEN;\r
+    snprintf(buf, sizeof(buf), "%2hu %03hu", complete_boards, score);\r
+    watch_set_colon();\r
+    watch_display_string(buf, BOARD_DISPLAY_START);\r
+}\r
+\r
+static guess_t get_answer(void) {\r
+    if (guess_position < 1 || guess_position > GAME_BOARD_SIZE)\r
+        return HL_GUESS_EQUAL; // Maybe add an error state, shouldn't ever hit this.\r
+\r
+    game_board[guess_position].revealed = true;\r
+    const uint8_t previous_value = game_board[guess_position - 1].value;\r
+    const uint8_t current_value = game_board[guess_position].value;\r
+\r
+    if (current_value > previous_value)\r
+        return HL_GUESS_HIGHER;\r
+    else if (current_value < previous_value)\r
+        return HL_GUESS_LOWER;\r
+    else\r
+        return HL_GUESS_EQUAL;\r
+}\r
+\r
+static void do_game_loop(guess_t user_guess) {\r
+    switch (game_state) {\r
+        case HL_GS_TITLE_SCREEN:\r
+            init_game();\r
+            render_board();\r
+            render_board_count();\r
+            game_state = HL_GS_GUESSING;\r
+            break;\r
+        case HL_GS_GUESSING: {\r
+            const guess_t answer = get_answer();\r
+\r
+            // Render answer indicator\r
+            switch (answer) {\r
+                case HL_GUESS_EQUAL:\r
+                    watch_display_string("==", STATUS_DISPLAY_START);\r
+                    break;\r
+                case HL_GUESS_HIGHER:\r
+                    watch_display_string("HI", STATUS_DISPLAY_START);\r
+                    break;\r
+                case HL_GUESS_LOWER:\r
+                    watch_display_string("LO", STATUS_DISPLAY_START);\r
+                    break;\r
+            }\r
+\r
+            // Scoring\r
+            if (answer == user_guess) {\r
+                score++;\r
+            } else if (answer == HL_GUESS_EQUAL) {\r
+                // No score for two consecutive identical cards\r
+            } else {\r
+                // Incorrect guess, game over\r
+                watch_display_string("GO", STATUS_DISPLAY_START);\r
+                game_board[guess_position].revealed = true;\r
+                render_board_position(guess_position);\r
+                game_state = HL_GS_LOSE;\r
+                return;\r
+            }\r
+\r
+            if (score >= WIN_SCORE) {\r
+                // Win, perhaps some kind of animation sequence?\r
+                watch_display_string("WI", STATUS_DISPLAY_START);\r
+                watch_display_string("  ", BOARD_SCORE_DISPLAY_START);\r
+                watch_display_string("------", BOARD_DISPLAY_START);\r
+                game_state = HL_GS_WIN;\r
+                return;\r
+            }\r
+\r
+            // Next guess position\r
+            const bool final_board_guess = guess_position == GAME_BOARD_SIZE - 1;\r
+            if (final_board_guess) {\r
+                // Seed new board\r
+                completed_board_count++;\r
+                render_board_count();\r
+                guess_position = 1;\r
+                reset_board(false);\r
+                render_board();\r
+            } else {\r
+                guess_position++;\r
+                render_board_position(guess_position - 1);\r
+                render_board_position(guess_position);\r
+            }\r
+            break;\r
+        }\r
+        case HL_GS_WIN:\r
+        case HL_GS_LOSE:\r
+            // Show score screen on button press from either state\r
+            watch_clear_display();\r
+            render_final_score();\r
+            game_state = HL_GS_SHOW_SCORE;\r
+            break;\r
+        case HL_GS_SHOW_SCORE:\r
+            watch_clear_display();\r
+            watch_display_string(TITLE_TEXT, BOARD_DISPLAY_START);\r
+            watch_display_string("GA", STATUS_DISPLAY_START);\r
+            game_state = HL_GS_TITLE_SCREEN;\r
+            break;\r
+        default:\r
+            watch_display_string("ERROR", BOARD_DISPLAY_START);\r
+            break;\r
+    }\r
+}\r
+\r
+static void light_button_handler(void) {\r
+    do_game_loop(HL_GUESS_HIGHER);\r
+}\r
+\r
+static void alarm_button_handler(void) {\r
+    do_game_loop(HL_GUESS_LOWER);\r
+}\r
+\r
+void higher_lower_game_face_setup(uint8_t watch_face_index, void **context_ptr) {\r
+    (void) watch_face_index;\r
+\r
+    if (*context_ptr == NULL) {\r
+        *context_ptr = malloc(sizeof(higher_lower_game_face_state_t));\r
+        memset(*context_ptr, 0, sizeof(higher_lower_game_face_state_t));\r
+        // Do any one-time tasks in here; the inside of this conditional happens only at boot.\r
+        memset(game_board, 0, sizeof(game_board));\r
+    }\r
+    // Do any pin or peripheral setup here; this will be called whenever the watch wakes from deep sleep.\r
+}\r
+\r
+void higher_lower_game_face_activate(void *context) {\r
+    higher_lower_game_face_state_t *state = (higher_lower_game_face_state_t *) context;\r
+    (void) state;\r
+    // Handle any tasks related to your watch face coming on screen.\r
+    game_state = HL_GS_TITLE_SCREEN;\r
+}\r
+\r
+bool higher_lower_game_face_loop(movement_event_t event, void *context) {\r
+    higher_lower_game_face_state_t *state = (higher_lower_game_face_state_t *) context;\r
+    (void) state;\r
+\r
+    switch (event.event_type) {\r
+        case EVENT_ACTIVATE:\r
+            // Show your initial UI here.\r
+            watch_display_string(TITLE_TEXT, BOARD_DISPLAY_START);\r
+            watch_display_string("GA", STATUS_DISPLAY_START);\r
+            break;\r
+        case EVENT_TICK:\r
+            // If needed, update your display here.\r
+            break;\r
+        case EVENT_LIGHT_BUTTON_UP:\r
+            light_button_handler();\r
+            break;\r
+        case EVENT_LIGHT_BUTTON_DOWN:\r
+            // Don't trigger light\r
+            break;\r
+        case EVENT_ALARM_BUTTON_UP:\r
+            alarm_button_handler();\r
+            break;\r
+        case EVENT_TIMEOUT:\r
+            // Your watch face will receive this event after a period of inactivity. If it makes sense to resign,\r
+            // you may uncomment this line to move back to the first watch face in the list:\r
+            // movement_move_to_face(0);\r
+            break;\r
+        default:\r
+            return movement_default_loop_handler(event);\r
+    }\r
+\r
+    // return true if the watch can enter standby mode. Generally speaking, you should always return true.\r
+    // Exceptions:\r
+    //  * If you are displaying a color using the low-level watch_set_led_color function, you should return false.\r
+    //  * If you are sounding the buzzer using the low-level watch_set_buzzer_on function, you should return false.\r
+    // Note that if you are driving the LED or buzzer using Movement functions like movement_illuminate_led or\r
+    // movement_play_alarm, you can still return true. This guidance only applies to the low-level watch_ functions.\r
+    return true;\r
+}\r
+\r
+void higher_lower_game_face_resign(void *context) {\r
+    (void) context;\r
+\r
+    // handle any cleanup before your watch face goes off-screen.\r
+}\r
diff --git a/watch-faces/complication/higher_lower_game_face.h b/watch-faces/complication/higher_lower_game_face.h
new file mode 100755 (executable)
index 0000000..51689b5
--- /dev/null
@@ -0,0 +1,106 @@
+/*\r
+ * MIT License\r
+ *\r
+ * Copyright (c) 2023 Chris Ellis\r
+ *\r
+ * Permission is hereby granted, free of charge, to any person obtaining a copy\r
+ * of this software and associated documentation files (the "Software"), to deal\r
+ * in the Software without restriction, including without limitation the rights\r
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\r
+ * copies of the Software, and to permit persons to whom the Software is\r
+ * furnished to do so, subject to the following conditions:\r
+ *\r
+ * The above copyright notice and this permission notice shall be included in all\r
+ * copies or substantial portions of the Software.\r
+ *\r
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\r
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\r
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\r
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\r
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\r
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\r
+ * SOFTWARE.\r
+ */\r
+\r
+#ifndef HIGHER_LOWER_GAME_FACE_H_\r
+#define HIGHER_LOWER_GAME_FACE_H_\r
+\r
+#include "movement.h"\r
+\r
+/*\r
+ * Higher-Lower game face\r
+ * ======================\r
+ *\r
+ * A game face based on the "higher-lower" card game where the objective is to correctly guess if the next card will\r
+ * be higher or lower than the last revealed cards.\r
+ *\r
+ * Game Flow:\r
+ * - When the face is selected, the "Hi-Lo" "Title" screen will be displayed, and the status indicator will display "GA" for game\r
+ * - Pressing `ALARM` or `LIGHT` will start the game and proceed to the "Guessing" screen\r
+ *   - The first card will be revealed and the player must now make a guess\r
+ *   - A player can guess `Higher` by pressing the `LIGHT` button, and `Lower` by pressing the `ALARM` button\r
+ *   - The status indicator will show the result of the guess: HI (Higher), LO (Lower), or == (Equal)\r
+ *   - There are five guesses to make on each game screen, once the end of the screen is reached, a new screen\r
+ *     will be started, with the last revealed card carried over\r
+ *   - The number of completed screens is displayed in the top right (see Scoring)\r
+ * - If the player has guessed correctly, the score is updated and play continues (see Scoring)\r
+ * - If the player has guessed incorrectly, the status will change to GO (Game Over)\r
+ *   - The current card will be revealed\r
+ *   - Pressing `ALARM` or `LIGHT` will transition to the "Score" screen\r
+ * - If the game is won, the status indicator will display "WI" and the "Win" screen will be displayed\r
+ *   - Pressing `ALARM` or `LIGHT` will transition to the "Score" screen\r
+ * - The status indicator will change to "SC" when the final score is displayed\r
+ *   - The number of completed game screens will be displayed on using the first two digits\r
+ *   - The number of correct guesses will be displayed using the final three digits\r
+ *   - E.g. "13: 063" represents 13 completed screens, with 63 correct guesses\r
+ * - Pressing `ALARM` or `LIGHT` while on the "Score" screen will transition to back to the "Title" screen\r
+ *\r
+ * Scoring:\r
+ * - If the player guesses correctly (HI/LO) a point is gained\r
+ * - If the player guesses incorrectly the game ends\r
+ *   - Unless the revealed card is equal (==) to the last card, in which case play continues, but no point is gained\r
+ * - If the player completes 40 screens full of cards, the game ends and a win screen is displayed\r
+ *\r
+ * Misc:\r
+ * The face tries to remain true to the spirit of using "cards"; to cope with the display limitations I've arrived at\r
+ * the following mapping of card values to screen display, but am open to better suggestions:\r
+ *\r
+ * Thanks to voloved for adding deck shuffling and drawing!\r
+ *\r
+ * | Cards   |                          |\r
+ * |---------|--------------------------|\r
+ * | Value   |2|3|4|5|6|7|8|9|10|J|Q|K|A|\r
+ * | Display |0|1|2|3|4|5|6|7|8 |9|-|=|≡|\r
+ *\r
+ * A previous alternative can be found in the git history:\r
+ * | Cards   |                          |\r
+ * |---------|--------------------------|\r
+ * | Value   |2|3|4|5|6|7|8|9|10|J|Q|K|A|\r
+ * | Display |2|3|4|5|6|7|8|9| 0|-|=|≡|H|\r
+ *\r
+ *\r
+ * Future Ideas:\r
+ * - Add sounds\r
+ * - Save/Display high score\r
+ * - Add a "Win" animation\r
+ * - Consider using lap indicator for larger score limit\r
+ */\r
+\r
+typedef struct {\r
+    // Anything you need to keep track of, put it here!\r
+} higher_lower_game_face_state_t;\r
+\r
+void higher_lower_game_face_setup(uint8_t watch_face_index, void ** context_ptr);\r
+void higher_lower_game_face_activate(void *context);\r
+bool higher_lower_game_face_loop(movement_event_t event, void *context);\r
+void higher_lower_game_face_resign(void *context);\r
+\r
+#define higher_lower_game_face ((const watch_face_t){ \\r
+    higher_lower_game_face_setup, \\r
+    higher_lower_game_face_activate, \\r
+    higher_lower_game_face_loop, \\r
+    higher_lower_game_face_resign, \\r
+    NULL, \\r
+})\r
+\r
+#endif // HIGHER_LOWER_GAME_FACE_H_\r