]> git.earman.xyz Git - sensor-watch.git/commitdiff
Hi-lo: Initial game face commit
authorChris <chris.ellis.git.dev@gmail.com>
Sun, 25 Jun 2023 19:43:01 +0000 (20:43 +0100)
committerChris <chris.ellis.git.dev@gmail.com>
Sun, 23 Jul 2023 14:51:22 +0000 (15:51 +0100)
movement/make/Makefile
movement/movement_faces.h
movement/watch_faces/complication/higher_lower_game_face.c [new file with mode: 0755]
movement/watch_faces/complication/higher_lower_game_face.h [new file with mode: 0755]

index 8b056a02583bc3094526426e6c954583ea4e1657..c761c8fc9350bdda7a9db7306ea67d04f2941184 100644 (file)
@@ -116,6 +116,7 @@ SRCS += \
   ../watch_faces/complication/geomancy_face.c \
   ../watch_faces/clock/simple_clock_bin_led_face.c \
   ../watch_faces/complication/flashlight_face.c \
+  ../watch_faces/complication/higher_lower_game_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.
index bf63732eafda16ec111c37a18c63471216974d7c..4b510da4956e861d9bdd0e02a3405ac7d34cbe91 100644 (file)
@@ -93,6 +93,7 @@
 #include "dual_timer_face.h"
 #include "simple_clock_bin_led_face.h"
 #include "flashlight_face.h"
+#include "higher_lower_game_face.h"
 // New includes go above this line.
 
 #endif // MOVEMENT_FACES_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 (executable)
index 0000000..3c35bc2
--- /dev/null
@@ -0,0 +1,371 @@
+/*\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
+// TODO: Win animation?\r
+// TODO: Save highscore?\r
+// TODO: Add sounds?\r
+//       Add sound option\r
+// TODO: Flip board direction?\r
+\r
+// Future Ideas:\r
+// - Use lap indicator for larger score improvement?\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 3 //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
+\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
+\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 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
+                                     ? generate_random_number(MAX_CARD_VALUE - MIN_CARD_VALUE + 1) + MIN_CARD_VALUE\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 = generate_random_number(MAX_CARD_VALUE - MIN_CARD_VALUE + 1) + MIN_CARD_VALUE,\r
+                .value = i + MIN_CARD_VALUE,\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_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 = BOARD_DISPLAY_END - board_position;\r
+    const bool revealed = game_board[board_position].revealed;\r
+\r
+    //if (board_position == guess_position) {\r
+    //    // Current spot\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('H', 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
+            set_segment_at_position(G, display_position);\r
+            break;\r
+        case 12: // Q (=)\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 11: // J (-)\r
+            watch_display_character('-', display_position);\r
+            break;\r
+        case 10: // 10 (0)\r
+            watch_display_character('0', display_position);\r
+            break;\r
+        default: {\r
+            const char display_char = value + '0';\r
+            watch_display_character(display_char, display_position);\r
+        }\r
+    }\r
+}\r
+\r
+static void render_board() {\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() {\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(movement_settings_t *settings, uint8_t watch_face_index, void **context_ptr) {\r
+    (void) settings;\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(movement_settings_t *settings, void *context) {\r
+    (void) settings;\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, movement_settings_t *settings, 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, settings);\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(movement_settings_t *settings, void *context) {\r
+    (void) settings;\r
+    (void) context;\r
+\r
+    // handle any cleanup before your watch face goes off-screen.\r
+}\r
+\r
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 (executable)
index 0000000..1cef05d
--- /dev/null
@@ -0,0 +1,55 @@
+/*\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
+ * A DESCRIPTION OF YOUR WATCH FACE\r
+ *\r
+ * and a description of how use it\r
+ *\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(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr);\r
+void higher_lower_game_face_activate(movement_settings_t *settings, void *context);\r
+bool higher_lower_game_face_loop(movement_event_t event, movement_settings_t *settings, void *context);\r
+void higher_lower_game_face_resign(movement_settings_t *settings, 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
+\r