diff --git a/movement/make/Makefile b/movement/make/Makefile index 9bafa6d..8d045bf 100644 --- a/movement/make/Makefile +++ b/movement/make/Makefile @@ -132,6 +132,7 @@ SRCS += \ ../watch_faces/complication/tuning_tones_face.c \ ../watch_faces/sensor/minmax_face.c \ ../watch_faces/complication/kitchen_conversions_face.c \ + ../watch_faces/complication/butterfly_game_face.c \ ../watch_faces/complication/wareki_face.c \ ../watch_faces/complication/wordle_face.c \ ../watch_faces/complication/endless_runner_face.c \ diff --git a/movement/movement_faces.h b/movement/movement_faces.h index c0bcc75..1364ca9 100644 --- a/movement/movement_faces.h +++ b/movement/movement_faces.h @@ -107,6 +107,7 @@ #include "tuning_tones_face.h" #include "minmax_face.h" #include "kitchen_conversions_face.h" +#include "butterfly_game_face.h" #include "wareki_face.h" #include "wordle_face.h" #include "endless_runner_face.h" diff --git a/movement/watch_faces/complication/butterfly_game_face.c b/movement/watch_faces/complication/butterfly_game_face.c new file mode 100644 index 0000000..3c498a9 --- /dev/null +++ b/movement/watch_faces/complication/butterfly_game_face.c @@ -0,0 +1,462 @@ +/* + * MIT License + * + * Copyright (c) 2023 Hugo Chargois + * + * 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 "butterfly_game_face.h" + +static char butterfly_shapes[][3] = { + "[]", "][", "25", "52", "9e", "e9", "6a", "a6", "3E", "E3", "00", "HH", "88" +}; + +static int8_t single_beep[] = {BUZZER_NOTE_A7, 4, 0}; +static int8_t round_win_melody[] = { + BUZZER_NOTE_C6, 4, + BUZZER_NOTE_E6, 4, + BUZZER_NOTE_G6, 4, + BUZZER_NOTE_C7, 12, + 0}; +static int8_t round_lose_melody[] = { + BUZZER_NOTE_E6, 4, + BUZZER_NOTE_F6, 4, + BUZZER_NOTE_D6SHARP_E6FLAT, 4, + BUZZER_NOTE_C6, 12, + 0}; +static int8_t game_win_melody[] = { + BUZZER_NOTE_G6, 4, + BUZZER_NOTE_A6, 4, + BUZZER_NOTE_B6, 4, + BUZZER_NOTE_C7, 12, + BUZZER_NOTE_D7, 4, + BUZZER_NOTE_E7, 4, + BUZZER_NOTE_D7, 4, + BUZZER_NOTE_C7, 12, + BUZZER_NOTE_B6, 4, + BUZZER_NOTE_C7, 4, + BUZZER_NOTE_D7, 4, + BUZZER_NOTE_G7, 24, + 0}; + +#define NUM_SHAPES (sizeof(butterfly_shapes) / sizeof(butterfly_shapes[0])) + +#define POS_LEFT 4 +#define POS_CENTER 6 +#define POS_RIGHT 8 + +#define TICK_FREQ 8 +#define TICKS_PER_SHAPE 8 + +#define PLAYER_1 0 +#define PLAYER_2 1 + + +// returns a random integer r with 0 <= r < max +static inline uint8_t _get_rand(uint8_t max) { +#if __EMSCRIPTEN__ + return rand() % max; +#else + return arc4random_uniform(max); +#endif +} + +/* + * The game is built with a simple state machine where each state is called a + * "screen". Each screen can draw on the display and handles events, including + * the "activate" event, which is repurposed and sent whenever we move from one + * screen to another via the _transition_to function. Basically it's a mini + * movement inside movement. + */ +typedef bool (*screen_fn_t)(movement_event_t, butterfly_game_state_t*); + +static screen_fn_t cur_screen_fn; + +static bool _transition_to(screen_fn_t sf, butterfly_game_state_t *state) { + movement_event_t ev = {EVENT_ACTIVATE, 0}; + cur_screen_fn = sf; + return sf(ev, state); +} + +static uint8_t _pick_wrong_shape(butterfly_game_state_t *state, bool skip_wrong_shape) { + if (!skip_wrong_shape) { + // easy case, we only need to skip over 1 shape: the correct shape + uint8_t r = _get_rand(NUM_SHAPES-1); + if (r >= state->correct_shape) { + r++; + } + return r; + } else { + // a bit more complex, we need to skip over 2 shapes: the correct one + // and the current wrong one + uint8_t r = _get_rand(NUM_SHAPES-2); + uint8_t i1, i2; // the 2 indices to skip over, with i1 < i2 + if (state->correct_shape < state->current_shape) { + i1 = state->correct_shape; + i2 = state->current_shape; + } else { + i1 = state->current_shape; + i2 = state->correct_shape; + } + + if (r >= i1) { + r++; + } + if (r >= i2) { + r++; + } + + return r; + } +} + +static void _display_shape(uint8_t shape, uint8_t pos) { + watch_display_string(butterfly_shapes[shape], pos); +} + +static void _display_scores(butterfly_game_state_t *state) { + char buf[] = " "; + buf[0] = '0' + state->score_p1; + watch_display_string(buf, 0); + buf[0] = '0' + state->score_p2; + watch_display_string(buf, 3); +} + +static void _play_sound(butterfly_game_state_t *state, int8_t *seq) { + if (state->sound) watch_buzzer_play_sequence(seq, NULL); +} + +static bool _round_start_screen(movement_event_t event, butterfly_game_state_t *state); +static bool _reset_screen(movement_event_t event, butterfly_game_state_t *state); + +static bool _game_win_screen(movement_event_t event, butterfly_game_state_t *state) { + switch (event.event_type) { + case EVENT_ACTIVATE: + state->ctr = 4 * TICK_FREQ; + watch_clear_display(); + + if (state->score_p1 >= state->goal_score) { + watch_display_string("pl1 wins", 0); + } else { + watch_display_string("pl2 wins", 0); + } + _play_sound(state, game_win_melody); + + break; + case EVENT_TICK: + state->ctr--; + if (state->ctr == 0) { + return _transition_to(_reset_screen, state); + } + break; + } + return true; +} + +static bool _round_win_screen(movement_event_t event, butterfly_game_state_t *state) { + switch (event.event_type) { + case EVENT_ACTIVATE: + state->ctr = TICK_FREQ; + + if (state->round_winner == PLAYER_1) { + state->score_p1++; + } else { + state->score_p2++; + } + + watch_clear_display(); + _display_scores(state); + _display_shape(state->correct_shape, state->round_winner == PLAYER_1 ? POS_LEFT : POS_RIGHT); + _play_sound(state, round_win_melody); + break; + case EVENT_TICK: + state->ctr--; + if (state->ctr == 0) { + if (state->score_p1 >= state->goal_score || state->score_p2 >= state->goal_score) { + return _transition_to(_game_win_screen, state); + } + return _transition_to(_round_start_screen, state); + } + break; + } + return true; +} + +static bool _round_lose_screen(movement_event_t event, butterfly_game_state_t *state) { + switch (event.event_type) { + case EVENT_ACTIVATE: + state->ctr = TICK_FREQ; + + if (state->round_winner == PLAYER_1) { + if (state->score_p2 > 0) state->score_p2--; + } else { + if (state->score_p1 > 0) state->score_p1--; + } + + _display_shape(state->correct_shape, POS_CENTER); + _play_sound(state, round_lose_melody); + break; + case EVENT_TICK: + if (--state->ctr == 0) { + return _transition_to(_round_start_screen, state); + } + _display_shape(state->ctr%2 ? state->correct_shape : state->current_shape, POS_CENTER); + break; + + } + return true; +} + +static bool _correct_shape_screen(movement_event_t event, butterfly_game_state_t *state) { + switch (event.event_type) { + case EVENT_ACTIVATE: + _display_shape(state->correct_shape, POS_CENTER); + _play_sound(state, single_beep); + break; + case EVENT_LIGHT_BUTTON_DOWN: + state->round_winner = PLAYER_1; + return _transition_to(_round_win_screen, state); + case EVENT_ALARM_BUTTON_DOWN: + state->round_winner = PLAYER_2; + return _transition_to(_round_win_screen, state); + } + return true; +} + +static bool _wrong_shape_screen(movement_event_t event, butterfly_game_state_t *state) { + switch (event.event_type) { + case EVENT_ACTIVATE: + state->ctr = TICKS_PER_SHAPE; + state->current_shape = _pick_wrong_shape(state, true); + _display_shape(state->current_shape, POS_CENTER); + _play_sound(state, single_beep); + break; + case EVENT_TICK: + if (--state->ctr == 0) { + if (--state->show_correct_shape_after == 0) { + return _transition_to(_correct_shape_screen, state); + } + return _transition_to(_wrong_shape_screen, state); + } + break; + case EVENT_LIGHT_BUTTON_DOWN: + state->round_winner = PLAYER_2; + return _transition_to(_round_lose_screen, state); + case EVENT_ALARM_BUTTON_DOWN: + state->round_winner = PLAYER_1; + return _transition_to(_round_lose_screen, state); + } + return true; +} + +static bool _first_wrong_shape_screen(movement_event_t event, butterfly_game_state_t *state) { + // the first of the wrong shape screens is a bit different than the next + // ones, for 2 reasons: + // * we can pick any shape except one (the correct shape); whereas in the + // subsequent wrong shape screens, we also must not pick the same wrong + // shape as the last + // * we don't act on the light/alarm button events; they would normally be + // a fail in a wrong shape screen, but in this case it may just be that + // the 2 players acknowledge the picked shape (in the previous screen) in + // quick succession, and we don't want the second player to immediately + // fail. + switch (event.event_type) { + case EVENT_ACTIVATE: + state->ctr = TICKS_PER_SHAPE; + state->current_shape = _pick_wrong_shape(state, false); + _display_shape(state->current_shape, POS_CENTER); + _play_sound(state, single_beep); + break; + case EVENT_TICK: + if (--state->ctr == 0) { + return _transition_to(_wrong_shape_screen, state); + } + break; + } + return true; +} + +static bool _round_start_screen(movement_event_t event, butterfly_game_state_t *state) { + switch (event.event_type) { + case EVENT_ACTIVATE: + state->correct_shape = _get_rand(NUM_SHAPES); + state->show_correct_shape_after = _get_rand(10) + 1; + watch_display_string(" - -", 0); + _display_scores(state); + _display_shape(state->correct_shape, POS_CENTER); + break; + case EVENT_LIGHT_BUTTON_DOWN: + case EVENT_ALARM_BUTTON_DOWN: + watch_display_string(" ", 4); + return _transition_to(_first_wrong_shape_screen, state); + } + return true; +} + +static bool _goal_select_screen(movement_event_t event, butterfly_game_state_t *state) { + switch (event.event_type) { + case EVENT_ACTIVATE: + watch_clear_display(); + state->goal_score = 6; + break; + case EVENT_LIGHT_BUTTON_DOWN: + return _transition_to(_round_start_screen, state); + case EVENT_ALARM_BUTTON_DOWN: + state->goal_score += 3; + if (state->goal_score > 9) state->goal_score = 3; + break; + } + + char buf[] = "GOaL "; + buf[5] = '0' + state->goal_score; + watch_display_string(buf, 4); + return true; +} + +static bool _reset_screen(movement_event_t event, butterfly_game_state_t *state) { + state->score_p1 = 0; + state->score_p2 = 0; + + return _transition_to(_goal_select_screen, state); +} + +static bool _continue_select_screen(movement_event_t event, butterfly_game_state_t *state) { + switch (event.event_type) { + case EVENT_ACTIVATE: + watch_clear_display(); + + // no game in progress, start a new game + if (state->score_p1 == 0 && state->score_p2 == 0) { + return _transition_to(_goal_select_screen, state); + } + + state->cont = false; + break; + case EVENT_LIGHT_BUTTON_DOWN: + if (state->cont) { + return _transition_to(_round_start_screen, state); + } + return _transition_to(_reset_screen, state); + case EVENT_ALARM_BUTTON_DOWN: + state->cont = !state->cont; + break; + } + + if (state->cont) { + watch_display_string("Cont y", 4); + } else { + watch_display_string("Cont n", 4); + } + return true; +} + +static bool _sound_select_screen(movement_event_t event, butterfly_game_state_t *state) { + switch (event.event_type) { + case EVENT_ACTIVATE: + watch_clear_display(); + break; + case EVENT_LIGHT_BUTTON_DOWN: + return _transition_to(_continue_select_screen, state); + case EVENT_ALARM_BUTTON_DOWN: + state->sound = !state->sound; + break; + } + + if (state->sound) { + watch_display_string("snd y", 5); + } else { + watch_display_string("snd n", 5); + } + return true; +} + +static bool _splash_screen(movement_event_t event, butterfly_game_state_t *state) { + switch (event.event_type) { + case EVENT_ACTIVATE: + state->ctr = TICK_FREQ; + + watch_clear_display(); + watch_display_string("Btrfly", 4); + break; + case EVENT_LIGHT_BUTTON_DOWN: + case EVENT_ALARM_BUTTON_DOWN: + return _transition_to(_sound_select_screen, state); + case EVENT_TICK: + if (--state->ctr == 0) { + return _transition_to(_sound_select_screen, state); + } + break; + } + return true; +} + +void butterfly_game_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr) { + (void) settings; + if (*context_ptr == NULL) { + *context_ptr = malloc(sizeof(butterfly_game_state_t)); + memset(*context_ptr, 0, sizeof(butterfly_game_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 random number generator + time_t t; + srand((unsigned) time(&t)); +#endif +} + +void butterfly_game_face_activate(movement_settings_t *settings, void *context) { + (void) settings; + + movement_request_tick_frequency(TICK_FREQ); +} + +bool butterfly_game_face_loop(movement_event_t event, movement_settings_t *settings, void *context) { + butterfly_game_state_t *state = (butterfly_game_state_t *)context; + + switch (event.event_type) { + case EVENT_ACTIVATE: + return _transition_to(_splash_screen, state); + case EVENT_TICK: + case EVENT_LIGHT_BUTTON_DOWN: + case EVENT_ALARM_BUTTON_DOWN: + return (*cur_screen_fn)(event, state); + case EVENT_TIMEOUT: + movement_move_to_face(0); + return true; + default: + return movement_default_loop_handler(event, settings); + } +} + +void butterfly_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/butterfly_game_face.h b/movement/watch_faces/complication/butterfly_game_face.h new file mode 100644 index 0000000..d02636e --- /dev/null +++ b/movement/watch_faces/complication/butterfly_game_face.h @@ -0,0 +1,125 @@ +/* + * MIT License + * + * Copyright (c) 2023 Hugo Chargois + * + * 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 BUTTERFLY_GAME_FACE_H_ +#define BUTTERFLY_GAME_FACE_H_ + +#include "movement.h" + +/* + * BUTTERFLY + * + * A GAME OF SHAPE RECOGNITION AND QUICK REFLEXES FOR 2 PLAYERS + * + * Setup + * ===== + * + * The game is played by 2 players, each using a distinct button: + * - player 1 plays with the LIGHT (upper left) button + * - player 2 plays with the ALARM (lower right) button + * + * To play, both players need a firm grip on the watch. A suggested method is to + * face each other, remove the watch from the wrist, and position it sideways + * between you. Hold one side of the strap in your preferred hand (right or + * left) and use your thumb to play. + * + * Start of the game + * ================= + * + * After the splash screen (BtrFly) is shown, the game proceeds through a couple + * configuration screens. Use ALARM to cycle through the possible values, and + * LIGHT to validate and move to the next screen. + * + * The configuration options are: + * + * - snd y/n Toggle sound effects on or off + * - goal 3/6/9 Choose to play a game of 3, 6 or 9 points + * - cont y/n Decide to continue an unfinished game or start a new one + * (this option appears only if a game is in progress) + * + * Rules + * ===== + * + * Prior to each round, a symmetrical shape composed of 2 characters is shown in + * the center of the screen. This shape, representing a butterfly's wings, is + * randomly chosen from a set of a dozen or so possible shapes. For example: + * + * ][ + * + * Memorize this shape! Your objective in the round will be to "catch" this + * "butterfly" by pressing your button before your opponent does. + * + * Once you believe you've memorized the shape, press your button. The round + * officially begins as soon as either player presses their button. + * + * Various "butterflies" will then appear on the screen, one after the other. + * The fastest player to press their button when the correct butterfly is shown + * wins the round. However, if a player presses their button when an incorrect + * butterfly is shown, they immediately lose the round. + * + * Scoring + * ======= + * + * The scores are displayed at the top of the screen at all times. + * + * When a round is won by a player, their score increases by one. When a round + * is lost by a player, their score decreases by one; unless they have a score + * of 0, in which case it remains unchanged. + * + * The game ends when a player reaches the set point goal (3, 6 or 9 points). + * + */ + +typedef struct { + bool cont : 1; // continue + bool sound : 1; + uint8_t goal_score : 4; + + // a generic ctr used by multiple states to display themselves for multiple frames + uint8_t ctr : 6; + + uint8_t correct_shape : 5; + uint8_t current_shape : 5; + uint8_t show_correct_shape_after : 5; + uint8_t round_winner : 1; + + uint8_t score_p1 : 5; + uint8_t score_p2 : 5; +} butterfly_game_state_t; + +void butterfly_game_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); +void butterfly_game_face_activate(movement_settings_t *settings, void *context); +bool butterfly_game_face_loop(movement_event_t event, movement_settings_t *settings, void *context); +void butterfly_game_face_resign(movement_settings_t *settings, void *context); + +#define butterfly_game_face ((const watch_face_t){ \ + butterfly_game_face_setup, \ + butterfly_game_face_activate, \ + butterfly_game_face_loop, \ + butterfly_game_face_resign, \ + NULL, \ +}) + +#endif // BUTTERFLY_GAME_FACE_H_ +