summaryrefslogtreecommitdiffstats
path: root/movement/watch_faces/complication/discgolf_face.c
blob: 7b5142ab4297bc9688cad2f11132c14f676a38e4 (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
#include <stdlib.h>
#include <string.h>
#include "discgolf_face.h"
#include "watch.h"          // Remember to change number of courses in this file
#include "watch_utility.h"

/* 
 * Keep track of scores in discgolf or golf!
 * The watch face operates in three different modes:
 *
 *  - dg_setting: Select a course
 *      Enter this mode by holding down the light button. The screen will display
 *      the label for the hole and the lowest score since last boot. 
 *      Press alarm to loop through the holes. Press the light button to make a 
 *      selection. This will reset all scores and start a new game in dg_idle mode.
 *
 *  -dg_idle: We're playing a hole
 *      This either shows your current score relative to par, or the score for a
 *      particular hole. 
 *      At the start of a game, press alarm to loop through the holes and leave it
 *      your starting hole. For optimal experience, play the course linearly after that
 *      If you're viewing the hole you're supposed to be playing, the watch face will
 *      display your score relative to par.
 *      Use the alarm button to view other holes than the one you're playing, in which
 *      case the input score for that hole will be displayed, in case it needs changing.
 *      Long press the alarm button to snap back to currently playing hole.
 *      To input scores for a hole in this mode, press the light button.
 *
 *  -dg_scoring: Input score for a hole
 *      In this mode, if the score is 0 (hasn't been entered during this round),
 *      it will blink, indicating we're in scoring mode. Press the alarm button
 *      to increment the score up until 15, in which case it loops back to 0. 
 *      Press the light button to save the score for that hole, advance one hole
 *      if you're not editing an already input score, and returning to idle mode.
 *
 *  When all scores have been entered, the LAP indicator turns on. At that point, if we enter 
 *  dg_setting to select a course, the score for that round is evaluated against the current 
 *  lowest score for that course, and saved if it is better.
*/
///////////////////////////////////////////////////////////////////////////////////////////////
// Enter course data
/* Initialize lowest scores with a high number */
int8_t best[courses];

static const uint8_t pars[][18] = {
   { 3, 3, 4, 3, 3, 3, 5, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3 },    // Grafarholt
   { 3, 4, 3, 3, 4, 3, 3, 3, 3, 4, 3, 3, 3, 3, 3, 3, 3, 3 },    // Gufunes
   { 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0 },    // Vífilsstaðir
   { 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0 },    // Dalvegur
   { 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0 },    // Laugardalur
   { 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0 },    // Guðmundarlundur
   { 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0 },    // Víðistaðatún
   { 3, 3, 3, 4, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0 },    // Fossvogur
   { 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0 },    // Klambratún
   { 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0 },    // Seljahverfi
   { 3, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0 }     // Fella- og Hóla
};
static const uint8_t holes[] = {                                // Number of holes on each course
    18,
    18,
    10,
    10,
    10,
    10,
    9,
    9,
    9,
    9,
    9
};
/* Two-letter descriptive labels, second field can only be A, B, C, D, E, F, H, I, J, L, N, O, R, T, U and X. */
static const char labels[][2] = {
    { 'G', 'H' },
    { 'G', 'N' },
    { 'V', 'I' },
    { 'D', 'V' },
    { 'L', 'A' },
    { 'G', 'L' },
    { 'V', 'T' },
    { 'F', 'V' },
    { 'K', 'T' },
    { 'S', 'E' },
    { 'F', 'H' }
};

// End of course data
///////////////////////////////////////////////////////////////////////////////////////////////

/* Beep function */
static inline void beep(movement_settings_t *settings) {
    if (settings->bit.button_should_sound) watch_buzzer_play_note(BUZZER_NOTE_C7, 50);
}

/* Prep for a new round */
static inline void reset(discgolf_state_t *state) {
    for (int i = 0; i < holes[state->course]; i++) {
        state->scores[i] = 0;
    }
    state->hole = 1;
    watch_clear_indicator(WATCH_INDICATOR_LAP);
    return;
}

/* Total number of throws so far */
static inline uint8_t score_sum(discgolf_state_t *state) {
    uint8_t sum = 0;
    for (int i = 0; i < holes[state->course]; i++) {
        sum = sum + state->scores[i];
    }
    return sum;
}

/* Count how many holes have been played */
static inline uint8_t count_played(discgolf_state_t *state) {
    uint8_t holes_played = 0;
    for (int i = 0; i < holes[state->course]; i++) {
        if (state->scores[i] > 0) holes_played++;
    }
    return holes_played;
}


/* Calculate the current score relative to par */
static inline int8_t calculate_score(discgolf_state_t *state) {
    uint8_t par_sum = 0;
    uint8_t score_sum = 0;
    
    for (int i = 0; i < holes[state->course]; i++) {
        if (state->scores[i] > 0) {
            par_sum = par_sum + pars[state->course][i];
            score_sum = score_sum + state->scores[i];
        }
    }
    return score_sum - par_sum;
}

/* Store score if it's the best so far */
static inline void store_best(discgolf_state_t *state) {
    uint8_t played = count_played(state);
    if ( played == holes[state->course] ) {
        int8_t high_score = calculate_score(state);
        if (high_score < best[state->course] ) {
            best[state->course] = high_score;
        }
    }
}

/* Configuration at boot, the high score array can be initialized with your high scores if they're known */
void discgolf_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(discgolf_state_t));
       discgolf_state_t *state = (discgolf_state_t *)*context_ptr;
       memset(*context_ptr, 0, sizeof(discgolf_state_t));
       state->hole = 1;
       state->course = 0;
       state->playing = holes[state->course] + 1;
       for (int i = 0; i < courses; i++) best[i] = 99;
       state->mode = dg_setting;
    }
}

void discgolf_face_activate(movement_settings_t *settings, void *context) {
    (void) settings;
    watch_clear_colon();
    discgolf_state_t *state = (discgolf_state_t *)context;

    /* If we were playing, go to current hole */
    if (state->playing <= holes[0]) {
        state->hole = state->playing;
    }
    /* Set LAP if round finished */
    if (count_played(state) == holes[state->course] ) {
        watch_set_indicator(WATCH_INDICATOR_LAP);
    }
    movement_request_tick_frequency(4);
}

bool discgolf_face_loop(movement_event_t event, movement_settings_t *settings, void *context) {
    (void) settings;
    
    discgolf_state_t *state = (discgolf_state_t *)context;

    switch (event.event_type) {
        case EVENT_TIMEOUT:
            /* Snap to first screen if we're not playing*/
            if ( count_played(state) == holes[state->course] || state->mode == dg_setting) {
                movement_move_to_face(0);
            }
            /* Cancel scoring if timed out */
            if (state->mode == dg_scoring) {
                state->scores[state->hole] = 0;
                state->mode = dg_idle;
            }
            break;
        /* Advance if not scoring */
        case EVENT_MODE_BUTTON_UP:
            if ( state->mode != dg_scoring ) {
				movement_move_to_next_face();
            }
            break;
        /* Go to default face if not scoring */
        case EVENT_MODE_LONG_PRESS:
            if ( state->mode != dg_scoring ) {
                movement_move_to_face(0);
            }
            break;
        case EVENT_LIGHT_BUTTON_UP:
            switch ( state->mode ) {
                case dg_idle:
                    /* Check if selected hole is the first one */
                    if ( score_sum(state) == 0 ) {
                        state->playing = state->hole;
                    }
                    /* Enter scoring mode */
                    state->mode = dg_scoring;
                    break;
                case dg_scoring:
                    /* Set the LAP indicator if all scores are entered */
                    if (count_played(state) == holes[state->course] ) {
                        watch_set_indicator(WATCH_INDICATOR_LAP);
                    }
                    /* Advance to next hole if not editing previously set score */
                    if ( state->hole == state->playing ) {
                        if (state->hole < holes[state->course]) state->hole++;
                        else state->hole = 1;
                        if (state->playing < holes[state->course]) state->playing++;
                        else state->playing = 1;
                    }
                    /* Return to idle */
                    state->mode = dg_idle;
                    break;
                case dg_setting:
                    /* Return to idle */
                    state->playing = holes[state->course] + 1;
                    state->mode = dg_idle;
                    break;
            }
            beep(settings);
            break;
        case EVENT_ALARM_BUTTON_UP:
            switch (state->mode) {
                /* Setting, loop through courses */
                case dg_setting:
                    state->course = (state->course + 1) % courses;
                    break;
                /* Scoring, increment score for current hole */
                case dg_scoring:
                    state->scores[state->hole - 1] = (state->scores[state->hole - 1] + 1) % 16;   // Loop around at 15
                    break;
                /* Idle, loop through holes */
                case dg_idle:
                    if (state->hole < holes[state->course]) {
                        state->hole++;
                    } else { state->hole = 1; }
                    break;
            }
            break;
        case EVENT_LIGHT_LONG_PRESS:
            /* Enter setting mode, reset state */
            if ( state->mode == dg_idle ) {
                state->mode = dg_setting;
                store_best(state);
                reset(state);
                beep(settings);
            }
            break;
        case EVENT_ALARM_LONG_PRESS:
            /* Snap back to currently playing hole if we've established one*/
            if ( (state->mode == dg_idle) && (state->hole != state->playing) && (state->playing <= holes[state->course]) ) {
                state->hole = state->playing;
                beep(settings);
            }
            break;
        default:
            break;
    }

    char buf[21];
    char prefix;
    int8_t diff;

    switch (state->mode) {
        /* Setting mode, display course label and high score */
        case dg_setting:
            if ( best[state->course] < 0 ) { 
                prefix = '-';
            } else { prefix = ' '; }
            sprintf(buf, "%c%c   %c%2d  ", labels[state->course][0], labels[state->course][1], prefix, abs(best[state->course]));
            break;
        /* Idle, show relative or input score */
        case dg_idle:
            if (state->hole == state->playing) {
                diff = calculate_score(state);
                if ( diff < 0 ) {
                    prefix = '-';
                } else { prefix = ' '; }
                sprintf(buf, "%c%c%2d %c%2d  ", labels[state->course][0], labels[state->course][1], state->hole, prefix, abs(diff));
            } else {
                sprintf(buf, "%c%c%2d  %2d  ", labels[state->course][0], labels[state->course][1], state->hole, state->scores[state->hole - 1]);
            }
            break;
        /* Scoring, show set score */
        case dg_scoring:
            sprintf(buf, "%c%c%2d  %2d  ", labels[state->course][0], labels[state->course][1], state->hole, state->scores[state->hole - 1]);
            break;
    }

    /* Blink during scoring */
    if (event.subsecond % 2 && state->mode == dg_scoring) {
        buf[6] = buf[7] = ' ';
    }

    /* Draw screen */
    watch_display_string(buf, 0);

    return true;
}

void discgolf_face_resign(movement_settings_t *settings, void *context) {
    (void) settings;
    (void) context;
    watch_clear_indicator(WATCH_INDICATOR_LAP);
}