From a831ed33364edb9b94f0ce43365a3956fa45c903 Mon Sep 17 00:00:00 2001 From: Chris Date: Sun, 25 Jun 2023 20:43:01 +0100 Subject: [PATCH] Hi-lo: Initial game face commit --- movement/make/Makefile | 1 + movement/movement_faces.h | 1 + .../complication/higher_lower_game_face.c | 371 ++++++++++++++++++ .../complication/higher_lower_game_face.h | 55 +++ 4 files changed, 428 insertions(+) create mode 100755 movement/watch_faces/complication/higher_lower_game_face.c create mode 100755 movement/watch_faces/complication/higher_lower_game_face.h diff --git a/movement/make/Makefile b/movement/make/Makefile index 8b056a02..c761c8fc 100644 --- a/movement/make/Makefile +++ b/movement/make/Makefile @@ -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. diff --git a/movement/movement_faces.h b/movement/movement_faces.h index bf63732e..4b510da4 100644 --- a/movement/movement_faces.h +++ b/movement/movement_faces.h @@ -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 index 00000000..3c35bc25 --- /dev/null +++ b/movement/watch_faces/complication/higher_lower_game_face.c @@ -0,0 +1,371 @@ +/* + * 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. + */ + +// TODO: Win animation? +// TODO: Save highscore? +// TODO: Add sounds? +// Add sound option +// TODO: Flip board direction? + +// Future Ideas: +// - Use lap indicator for larger score improvement? + +// 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 3 //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 + +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 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 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 + ? generate_random_number(MAX_CARD_VALUE - MIN_CARD_VALUE + 1) + MIN_CARD_VALUE + : 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 = generate_random_number(MAX_CARD_VALUE - MIN_CARD_VALUE + 1) + MIN_CARD_VALUE, + .value = i + MIN_CARD_VALUE, + .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_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 = BOARD_DISPLAY_END - board_position; + const bool revealed = game_board[board_position].revealed; + + //if (board_position == guess_position) { + // // Current spot + // 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('H', 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); + set_segment_at_position(G, display_position); + break; + case 12: // Q (=) + watch_display_character(' ', display_position); + set_segment_at_position(A, display_position); + set_segment_at_position(D, display_position); + break; + case 11: // J (-) + watch_display_character('-', display_position); + break; + case 10: // 10 (0) + watch_display_character('0', display_position); + break; + default: { + const char display_char = value + '0'; + watch_display_character(display_char, display_position); + } + } +} + +static void render_board() { + 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() { + 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 00000000..1cef05dc --- /dev/null +++ b/movement/watch_faces/complication/higher_lower_game_face.h @@ -0,0 +1,55 @@ +/* + * 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" + +/* + * A DESCRIPTION OF YOUR WATCH FACE + * + * and a description of how use it + * + */ + +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_ +