diff options
-rw-r--r-- | movement/make/Makefile | 1 | ||||
-rw-r--r-- | movement/movement_faces.h | 1 | ||||
-rw-r--r-- | movement/watch_faces/complication/invaders_face.c | 434 | ||||
-rw-r--r-- | movement/watch_faces/complication/invaders_face.h | 82 |
4 files changed, 518 insertions, 0 deletions
diff --git a/movement/make/Makefile b/movement/make/Makefile index a18c7064..dec3ffab 100644 --- a/movement/make/Makefile +++ b/movement/make/Makefile @@ -104,6 +104,7 @@ SRCS += \ ../watch_faces/complication/habit_face.c \ ../watch_faces/clock/repetition_minute_face.c \ ../watch_faces/complication/timer_face.c \ + ../watch_faces/complication/invaders_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 568c817c..50b084d3 100644 --- a/movement/movement_faces.h +++ b/movement/movement_faces.h @@ -80,6 +80,7 @@ #include "habit_face.h" #include "repetition_minute_face.h" #include "timer_face.h" +#include "invaders_face.h" // New includes go above this line. #endif // MOVEMENT_FACES_H_ diff --git a/movement/watch_faces/complication/invaders_face.c b/movement/watch_faces/complication/invaders_face.c new file mode 100644 index 00000000..c3b13c68 --- /dev/null +++ b/movement/watch_faces/complication/invaders_face.c @@ -0,0 +1,434 @@ +/* + * MIT License + * + * Copyright (c) 2023 Andreas Nebinger + * + * 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 <time.h> +#endif + +#include <stdlib.h> +#include <string.h> +#include "watch_private_display.h" +#include "invaders_face.h" + +#define INVADERS_FACE_WAVES_PER_STAGE 9 // number of waves per stage (there are two stages) +#define INVADERS_FACE_WAVE_INVADERS 16 // number of invaders attacking per wave + +static const uint8_t _defense_lines_segdata[3][2] = {{2, 12}, {2, 11}, {0, 11}}; +static const uint8_t _bonus_points_segdata[4][2] = {{2, 7}, {2, 8}, {2, 9}, {0, 10}}; +static const uint8_t _bonus_points_helper[] = {1, 5, 9, 11, 15, 19, 21, 25, 29}; + +static const int8_t _sound_seq_game_start[] = {BUZZER_NOTE_A6, 1, BUZZER_NOTE_A7, 3, -2, 1, BUZZER_NOTE_REST, 10, BUZZER_NOTE_A6, 1, BUZZER_NOTE_A7, 3, -2, 1, 0}; +static const int8_t _sound_seq_shot_hit[] = {BUZZER_NOTE_A6, 1, BUZZER_NOTE_A7, 2, 0}; +static const int8_t _sound_seq_shot_miss[] = {BUZZER_NOTE_A7, 1, 0}; +static const int8_t _sound_seq_ufo_hit[] = {BUZZER_NOTE_A6, 1, BUZZER_NOTE_A7, 2, -2, 1, 0}; +static const int8_t _sound_seq_def_gone[] = {BUZZER_NOTE_A6, 1, BUZZER_NOTE_A7, 3, -2, 3, BUZZER_NOTE_REST, 40, BUZZER_NOTE_A6, 1, BUZZER_NOTE_A7, 3, -2, 4, 0}; +static const int8_t _sound_seq_next_wave[] = {BUZZER_NOTE_A6, 2, BUZZER_NOTE_A7, 2, BUZZER_NOTE_REST, 8, BUZZER_NOTE_A6, 2, BUZZER_NOTE_A7, 2, -2, 1, + BUZZER_NOTE_REST, 32, + BUZZER_NOTE_A6, 2, BUZZER_NOTE_A7, 2, BUZZER_NOTE_REST, 8, BUZZER_NOTE_A6, 2, BUZZER_NOTE_A7, 2, -2, 1, 0}; +static const int8_t _sound_seq_game_over[] = {BUZZER_NOTE_A6, 1, BUZZER_NOTE_A7, 3, -2, 11, 0}; + +typedef enum { + invaders_state_activated, + invaders_state_pre_game, + invaders_state_playing, + invaders_state_in_wave_break, + invaders_state_pre_next_wave, + invaders_state_next_wave, + invaders_state_game_over +} invaders_current_state_t; + +typedef struct { + bool ufo_next : 1; // indicates whether next invader is a ufo + bool inv_checking : 1; // flag to indicate whether we are currently moving invaders (to prevent race conditions) + bool suspend_buttons : 1; // used while playing the game over sequence to prevent involuntary immediate restarts +} invaders_signals_t; + +static int8_t _invaders[6]; // array of current invaders values (-1 = empty, 10 = ufo) +static uint8_t _wave_invaders[INVADERS_FACE_WAVE_INVADERS]; // all invaders for the current wave. (Predefined to save cpu cycles when playing.) +static invaders_current_state_t _current_state; +static uint8_t _defense_lines; // number of defense lines which have been broken in the current wave +static uint8_t _aim; // current "aim" digit +static uint8_t _invader_idx; // index of next invader attacking in current wave (0 to 15) +static uint8_t _wave_position; // current position of first invader. When > 6 the defense is broken +static uint8_t _wave_tick_freq; // number of ticks passing until the next invader is inserted +static uint8_t _ticks; // counts the ticks +static uint8_t _bonus_countdown; // ticks countdown until the bonus point indicator is cleared +static uint8_t _waves; // counts the waves (_wave_tick_freq decreases slowly depending on _wave value) +static uint8_t _shots_in_wave; // number of shots in current wave. If 30 is reached, the game is over +static uint8_t _invaders_shot; // number of sucessfully shot invaders in current wave +static uint8_t _invaders_shot_sum; // current sum of invader digits shot (needed to determine if a ufo is coming) +static invaders_signals_t _signals; // holds severals flags +static uint16_t _score; // score of the current game + +/// @brief return a random number. 0 <= return_value < num_values +static inline uint8_t _get_rand_num(uint8_t num_values) { +#if __EMSCRIPTEN__ + return rand() % num_values; +#else + return arc4random_uniform(num_values); +#endif +} + +/// @brief callback function to re-enable light and alarm buttons after playing a sound sequence +static inline void _resume_buttons() { + _signals.suspend_buttons = false; +} + +/// @brief play a sound sequence if the game is in beepy mode +static inline void _play_sequence(invaders_state_t *state, int8_t *sequence) { + if (state->sound_on) watch_buzzer_play_sequence((int8_t *)sequence, NULL); +} + +/// @brief draw the remaining defense lines +static void _display_defense_lines() { + watch_display_character(' ', 1); + for (uint8_t i = 0; i < 3 - _defense_lines; i++) watch_set_pixel(_defense_lines_segdata[i][0], _defense_lines_segdata[i][1]); +} + +/** @brief draw label followed by the given score value + * @param label string displayed in the upper left corner + * @param score score to display + */ +static void _display_score(char *label, uint16_t score) { + watch_display_character(label[0], 0); + watch_display_character(label[1], 1); + char buf[10]; + sprintf(buf, " %06d", (score * 10)); + watch_display_string(buf, 2); +} + +/// @brief draw an invader at the given position +static inline void _display_invader(int8_t invader, uint8_t position) { + switch (invader) { + case 10: + watch_display_character('n', position); + break; + case -1: + watch_display_character(' ', position); + break; + default: + watch_display_character(invader + 48, position); + break; + } +} + +/// @brief game over: show score and set state +static void _game_over(invaders_state_t *state) { + _display_score("GO", _score); + _current_state = invaders_state_game_over; + movement_request_tick_frequency(1); + _signals.suspend_buttons = true; + if (state->sound_on) watch_buzzer_play_sequence((int8_t *)_sound_seq_game_over, _resume_buttons); + // save current score to highscore, if applicable + if (_score > state->highscore) state->highscore = _score; +} + +/// @brief initialize the current wave +static void _init_wave() { + uint8_t i; + if (_current_state == invaders_state_in_wave_break) { + _invader_idx = _invaders_shot; + } else { + _invader_idx = _invaders_shot = _invaders_shot_sum = _defense_lines = _shots_in_wave = 0; + } + // pre-fill invaders + for (i = _invader_idx; i < INVADERS_FACE_WAVE_INVADERS; i++) _wave_invaders[i] = _get_rand_num(10); + // init invaders field + for (i = 1; i < 6; i++) _invaders[i] = -1; + _invaders[0] = _wave_invaders[_invader_idx]; + _wave_position = _aim = _bonus_countdown = 0; + _signals.ufo_next = _signals.inv_checking = _signals.suspend_buttons = false; + _current_state = invaders_state_playing; + // determine wave speed + _wave_tick_freq = 6 - ((_waves % INVADERS_FACE_WAVES_PER_STAGE) + 1) / 2; + if (_waves >= INVADERS_FACE_WAVES_PER_STAGE) _wave_tick_freq--; + // clear display + watch_display_string(" ", 2); + watch_display_character('0', 0); + _display_defense_lines(); + // draw first invader + watch_display_character(_wave_invaders[_invader_idx] + 48, 9); +} + +/** @brief move invaders and add a new one, if necessary + * @returns true, if invaders have reached position 6, false otherwise + */ +static bool _move_invaders() { + if (_wave_position == 5) return true; + _signals.inv_checking = true; + if (_invaders[_wave_position] >= 0) _wave_position++; + int8_t i; + // move invaders + for (i = _wave_position; i > 0; i--) _invaders[i] = _invaders[i - 1]; + if (_invader_idx < INVADERS_FACE_WAVE_INVADERS - 1) { + // add invader + _invader_idx++; + if (_signals.ufo_next) { + _invaders[0] = 10; + _signals.ufo_next = false; + } else { + _invaders[0] = _wave_invaders[_invader_idx]; + } + } else { + // just add an empty invader slot + _invaders[0] = -1; + } + // update display + for (i = 0; i <= _wave_position; i++) { + _display_invader(_invaders[i], 9 - i); + } + _signals.inv_checking = false; + return false; +} + +void invaders_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(invaders_state_t)); + memset(*context_ptr, 0, sizeof(invaders_state_t)); + invaders_state_t *state = (invaders_state_t *)*context_ptr; + // default: sound on + state->sound_on = true; + } +#if __EMSCRIPTEN__ + // simulator only: seed the randon number generator + time_t t; + srand((unsigned) time(&t)); +#endif +} + +void invaders_face_activate(movement_settings_t *settings, void *context) { + (void) settings; + (void) context; + _current_state = invaders_state_activated; + _signals.suspend_buttons = false; +} + +bool invaders_face_loop(movement_event_t event, movement_settings_t *settings, void *context) { + invaders_state_t *state = (invaders_state_t *)context; + + switch (event.event_type) { + case EVENT_ACTIVATE: + // show highscore + _display_score("GA", state->highscore); + break; + case EVENT_TICK: + _ticks++; + switch (_current_state) { + case invaders_state_in_wave_break: + case invaders_state_pre_game: + case invaders_state_next_wave: + // wait 2 secs to start the first round + if (_ticks >= 2) { + _ticks = 0; + _init_wave(); + _current_state = invaders_state_playing; + movement_request_tick_frequency(4); + } + break; + case invaders_state_playing: + // game is playing + if (_ticks >= _wave_tick_freq) { + _ticks = 0; + if (_move_invaders()) { + // invaders broke through + if (_defense_lines < 2) { + // start current wave over + _defense_lines++; + _display_defense_lines(); + _display_score("GA", _score); + _current_state = invaders_state_in_wave_break; + movement_request_tick_frequency(1); + _play_sequence(state, (int8_t *)_sound_seq_def_gone); + } else { + // game over + _game_over(state); + } + } + } + // handle bonus points indicators + if (_bonus_countdown) { + _bonus_countdown--; + if (!_bonus_countdown) { + watch_display_character(' ', 2); + watch_display_character(' ', 3); + } + } + break; + case invaders_state_pre_next_wave: + if (_ticks >= 3) { + // switch to next wave + _ticks = 0; + movement_request_tick_frequency(1); + _display_score("GA", _score); + watch_set_pixel(1, 9); + watch_display_character((_waves % INVADERS_FACE_WAVES_PER_STAGE) + 49, 3); + _current_state = invaders_state_next_wave; + _waves++; + if (_waves == INVADERS_FACE_WAVES_PER_STAGE * 2) _waves = 0; + _play_sequence(state, (int8_t *)_sound_seq_next_wave); + } + default: + break; + } + break; + case EVENT_LIGHT_BUTTON_DOWN: + if (!_signals.suspend_buttons) { + if (_current_state == invaders_state_playing) { + // cycle the aim + _aim = (_aim + 1) % 11; + _display_invader(_aim, 0); + } else if (_current_state == invaders_state_activated || _current_state == invaders_state_game_over) { + // just illuminate the LED + movement_illuminate_led(); + } + } + break; + case EVENT_LIGHT_LONG_PRESS: + if ((_current_state == invaders_state_activated || _current_state == invaders_state_game_over) && !_signals.suspend_buttons) { + // switch between beepy and silent mode + state->sound_on = !state->sound_on; + watch_buzzer_play_note(BUZZER_NOTE_A7, state->sound_on ? 65 : 25); + } + break; + case EVENT_ALARM_BUTTON_DOWN: + if (!_signals.suspend_buttons) { + switch (_current_state) { + case invaders_state_game_over: + case invaders_state_activated: + // initialize the game + _waves = 0; + _score = 0; + movement_request_tick_frequency(1); + _ticks = 0; + _current_state = invaders_state_pre_game; + _play_sequence(state, (int8_t *)_sound_seq_game_start); + break; + case invaders_state_playing: { + // "shoot" + _shots_in_wave++; + if (_shots_in_wave == 30) { + // max number of shots reached: game over + _game_over(state); + } else { + // wait if we are currently deleting an invader + while (_signals.inv_checking); + // proceed + _signals.inv_checking = true; + bool skip = false; + for (int8_t i = _wave_position; i >= 0 && !skip; i--) { + // if (_invaders[i] == -1) break; + if (_invaders[i] == _aim) { + // invader is shot + skip = true; + _invaders_shot++; + _play_sequence(state, _aim == 10 ? (int8_t *)_sound_seq_ufo_hit : (int8_t *)_sound_seq_shot_hit); + if (_invaders_shot == INVADERS_FACE_WAVE_INVADERS) { + // last invader shot: wave sucessfully completed + watch_display_character(' ', 9 - _wave_position); + _ticks = 0; + _current_state = invaders_state_pre_next_wave; + _signals.inv_checking = false; + } else { + // check for ufo appearance + if (_aim && _aim < 10) { + _invaders_shot_sum = (_invaders_shot_sum + _aim) % 10; + if (_invaders_shot_sum == 0) _signals.ufo_next = true; + } + // remove invader + if (_wave_position == 0 || i == 5) { + _invaders[i] = -1; + } else { + for (uint8_t j = i; j < _wave_position; j++) { + _invaders[j] = _invaders[j + 1]; + _display_invader(_invaders[j], 9 - j); + } + } + watch_display_character(' ', 9 - _wave_position); + if (_wave_position) _wave_position--; + // update score + if (_aim == 10) { + // ufo shot. The original game uses a ridiculously complicated scoring system here... + uint8_t bonus_points = 0; + uint8_t j; + for (j = 0; j < sizeof(_bonus_points_helper) && !bonus_points; j++) { + if (_shots_in_wave == _bonus_points_helper[j]) { + bonus_points = 30; + } else if (_shots_in_wave - 1 == _bonus_points_helper[j]) { + bonus_points = 20; + } + } + if (!bonus_points) bonus_points = 10; + bonus_points += (6 - i); + if ((_waves >= INVADERS_FACE_WAVES_PER_STAGE) && i) bonus_points += (6 - i); + _score += bonus_points; + // represent bonus points by bars + for (j = 0; j < (bonus_points / 10); j++) watch_set_pixel(_bonus_points_segdata[j][0], _bonus_points_segdata[j][1]); + _bonus_countdown = 9; + } else { + // regular invader + _score += (6 - _wave_position) * (_waves >= INVADERS_FACE_WAVES_PER_STAGE ? 2 : 1); + } + } + } + } + if (!skip) _play_sequence(state, (int8_t *)_sound_seq_shot_miss); + _signals.inv_checking = false; + } + break; + } + default: + break; + } + } + break; + case EVENT_TIMEOUT: + movement_move_to_face(0); + break; + default: + // Movement's default loop handler will step in for any cases you don't handle above: + // * EVENT_LIGHT_BUTTON_DOWN lights the LED + // * EVENT_MODE_BUTTON_UP moves to the next watch face in the list + // * EVENT_MODE_LONG_PRESS returns to the first watch face (or skips to the secondary watch face, if configured) + // You can override any of these behaviors by adding a case for these events to this switch statement. + return movement_default_loop_handler(event, settings); + } + + // return true if the watch can enter standby mode. Generally speaking, you should always return true. + // Exceptions: + // * If you are displaying a color using the low-level watch_set_led_color function, you should return false. + // * If you are sounding the buzzer using the low-level watch_set_buzzer_on function, you should return false. + // Note that if you are driving the LED or buzzer using Movement functions like movement_illuminate_led or + // movement_play_alarm, you can still return true. This guidance only applies to the low-level watch_ functions. + return true; +} + +void invaders_face_resign(movement_settings_t *settings, void *context) { + (void) settings; + (void) context; + _current_state = invaders_state_game_over; +} + diff --git a/movement/watch_faces/complication/invaders_face.h b/movement/watch_faces/complication/invaders_face.h new file mode 100644 index 00000000..59126dd5 --- /dev/null +++ b/movement/watch_faces/complication/invaders_face.h @@ -0,0 +1,82 @@ +/* + * MIT License + * + * Copyright (c) 2023 Andreas Nebinger + * + * 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 INVADERS_FACE_H_ +#define INVADERS_FACE_H_ + +#include "movement.h" + +/* + * Remake of the "famous" Casio Number Invaders Game + * + * This is an authentic remake of the invaders game, found on the Casio + * calculator wristwatch CA-85 or CA-851. There were also some calculators + * sold with this game, like MG-880. + * + * How to play: + * + * Press the alarm button to start the game. + * "Invaders" (just digits, tbh) will start coming in from the right hand side. + * Press the light button to "aim". The digit on the top of the display cycles + * from 0 to 9. If your aiming digit is identical to one of the invaders, + * press the alarm button to "shoot". The corresponding invader will disappear. + * If the invaders reach beneath the very first position, you loose one defense + * line. When all three defense lines are gone, the game is over. + * Also: If you shoot more than 29 times per round, you loose the game. + * Good to know: There are 16 invaders per wave. There is a short break between + * waves. + * + * What are the "n" invaders? Ufos! + * + * Whenever the sum of all invaders shot is divisible by 10 the next invader + * will be an ufo, represented by the n-symbol. Shooting a ufo gets you extra + * points. Example: shoot 2, 5, 3 --> ufo next + * + * As for points: the earlier you shoot an invader, the more points you get. + * + * Anything else? Long pressing the light button toggles sound on or off. (Not + * while playing.) + * + */ + +typedef struct { + uint16_t highscore; + bool sound_on; +} invaders_state_t; + +void invaders_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); +void invaders_face_activate(movement_settings_t *settings, void *context); +bool invaders_face_loop(movement_event_t event, movement_settings_t *settings, void *context); +void invaders_face_resign(movement_settings_t *settings, void *context); + +#define invaders_face ((const watch_face_t){ \ + invaders_face_setup, \ + invaders_face_activate, \ + invaders_face_loop, \ + invaders_face_resign, \ + NULL, \ +}) + +#endif // INVADERS_FACE_H_ + |