]> git.earman.xyz Git - sensor-watch.git/commitdiff
Invaders Face (#210)
authorTheOnePerson <a.nebinger@web.de>
Sat, 11 Mar 2023 21:31:17 +0000 (22:31 +0100)
committerGitHub <noreply@github.com>
Sat, 11 Mar 2023 21:31:17 +0000 (16:31 -0500)
* invaders face: Initial commit, fully functional so far

* invaders face: silence compiler warning

* invaders face: prevent involuntary restarts when the game is over and save some bytes on flags

---------

Co-authored-by: joeycastillo <joeycastillo@utexas.edu>
movement/make/Makefile
movement/movement_faces.h
movement/watch_faces/complication/invaders_face.c [new file with mode: 0644]
movement/watch_faces/complication/invaders_face.h [new file with mode: 0644]

index a18c70641d50c06ac6e422957b494cbd048efca0..dec3ffabdbc64521631982a7657fd6db3b77767f 100644 (file)
@@ -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.
index 568c817cd1588c49537f098d94690a432e780811..50b084d3d8e15c44d4c00226688b50896a3ed9aa 100644 (file)
@@ -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 (file)
index 0000000..c3b13c6
--- /dev/null
@@ -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 (file)
index 0000000..59126dd
--- /dev/null
@@ -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_
+