summaryrefslogtreecommitdiffstats
path: root/movement/watch_faces/complication/invaders_face.c
blob: c3b13c680d94dbd4c4e33bc65c3f2ba065218717 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
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;
}