/* * MIT License * * Copyright (c) 2024 <#author_name#> * * 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. */ #include #include #include "wordle_face.h" /* TODO: * Add quick iteration (8x freq to get to the letter we want) * Fix the word matching (if answer is AAAAA and we put in AACAA, the C blinks) * Verify pressing back always work when the board is G_G_G * Add daily streak and wait for next day * Add a way tpo recount previous attempts */ // From: https://github.com/charlesreid1/five-letter-words/blob/master/sgb-words.txt /* Letter | Usage in sgb-words.txt S | 2674 E | 2658 A | 2181 R | 1799 O | 1683 I | 1539 T | 1462 But looks bad across all positions L | 1434 N | 1219 D | 1100 lowercase d looks like a in certain positions U | 1068 C has more words with the other letters here (457 total vs 390) C | 920 P | 895 */ static const char _valid_letters[] = {'A', 'C', 'E', 'I', 'L', 'N', 'O', 'P', 'R', 'S'}; static const char _legal_words[][WORDLE_LENGTH + 1] = { "PLACE", "SINCE", "PAPER", "LINES", "LEARN", "SPACE", "CLOSE", "CLASS", "PIECE", "COLOR", "ALONE", "PLANE", "SPELL", "CLEAR", "AREAS", "SENSE", "OCEAN", "SCALE", "CELLS", "SLEEP", "LOCAL", "CLEAN", "PEACE", "CROSS", "CASES", "CROPS", "PLAIN", "PAIRS", "SCORE", "NOISE", "PIANO", "PLANS", "PRICE", "RAISE", "SCENE", "PRESS", "APPLE", "CALLS", "POLES", "LOOSE", "OPERA", "INNER", "SOLAR", "RISES", "SALES", "ACRES", "ERROR", "NAILS", "COINS", "SLOPE", "CANOE", "CANAL", "LIONS", "AROSE", "RACES", "SPARE", "PIPES", "RAINS", "ROLLS", "SAILS", "ARISE", "ROPES", "CRIES", "OPENS", "APRON", "SPEAR", "SLICE", "SPOON", "ROSES", "LINEN", "POLAR", "PEARL", "LOSES", "CORAL", "SPOIL", "PANEL", "SELLS", "COOLS", "SOILS", "POOLS", "RISEN", "PILES", "SLAIN", "PANIC", "CRISP", "RAILS", "SCOPE", "CONES", "COCOA", "REINS", "NOSES", "SCARE", "CEASE", "PRIOR", "POLIO", "SEALS", "COALS", "LOANS", "SPINS", "PAINS", "ONION", "SCRAP", "ROLES", "SNAIL", "LOOPS", "CREEP", "CARES", "ALIEN", "CRANE", "SLIPS", "SPINE", "LEAPS", "PROSE", "SNARE", "PINES", "SCALP", "LEASE", "COILS", "ARENA", "PEARS", "CLIPS", "LANES", "RESIN", "SONAR", "CORPS", "NIECE", "CELLO", "REPEL", "SPICE", "OASIS", "PACES", "COLON", "CACAO", "RINSE", "SPOOL", "SPILL", "SNAPS", "EERIE", "EARNS", "PERIL", "LINER", "SCARS", "SOLES", "PAILS", "CAROL", "CANON", "POLLS", "NASAL", "SCORN", "OASES", "ASSES", "PILLS", "SPORE", "SPIES", "ERASE", "AISLE", "LOINS", "LEANS", "LANCE", "PANES", "PORES", "POISE", "IRONS", "ROARS", "SCOOP", "LASSO", "PRONE", "NICER", "SNARL", "CORES", "RIPEN", "PEERS", "EASEL", "LACES", "SOAPS", "SNIPS", "PIERS", "LASER", "LILAC", "SIREN", "CLASP", "POSSE", "POSES", "SLOOP", "SLAPS", "SOLOS", "SISAL", "SEEPS", "SPANS", "CLAPS", "ACORN", "LOSER", "LAPSE", "ASPEN", "SNORE", "PROPS", "PESOS", "SONIC", "SPARS", "SORES", "RELIC", "NOOSE", "NEARS", "CAPES", "CANES", "SPIRE", "ISLES", "SNEER", "RARER", "NINES", "PECAN", "PENCE", "SILLS", "COPRA", "POPES", "SPREE", "SCANS", "REELS", "LIARS", "LEPER", "SOARS", "PLEAS", "PALER", "EPICS", "CAPER", "CONIC", "OPALS", "EASES", "ARSON", "CLANS", "PLIES", "CROON", "PREEN", "SEERS", "COCCI", "SILOS", "SNIPE", "SANER", "RILLS", "CORNS", "PRIES", "LOONS", "EARLS", "PEONS", "PALES", "LAIRS", "PEELS", "PEALS", "CRONE", "ENROL", "COOPS", "LAPEL", "RASPS", "ASPIC", "PENIS", "INANE", "SLOPS", "COCOS", "LEERS", "LOPES", "ALIAS", "RACER", "COPSE", "PALLS", "COPES", "ICONS", "REAPS", "SNOOP", "LORES", "REARS", "COLIC", "PLOPS", "SIRES", "CARPS", "LISPS", "PEEPS", "SORER", "LOLLS", "PARES", "ACNES", "NEONS", "NOONS", "PAPAS", "RIPER", "ELOPE", "CRESS", "NAPES", "ICIER", "CILIA", "SEARS", "SARIS", "PAPAL", "ROSIN", "CREPE", "RISER", "PACER", "SALON", "CRIER", "LOESS", "PIPER", "COONS", "SEINE", "IONIC", "SCRIP", "PENAL", "ALOES", "APACE", "SIREE", "ROPER", "ANION", "LONER", "CIRCA", "CARNE", "ANISE", "SPECS", "ANNAS", "PICAS", "REALS", "AERIE", "ORLON", "CRASS", "SPIEL", "LAPIS", "ARIAS", "PAEAN", "SINES", "SCION", "RAPES", "SCARP", "SEPIA", "POSER", "LIENS", "RILES", "APSES", "NONCE", "ANOLE", "RECAP", "CARON", "PORNO", "CREEL", "CAPOS", "OPINE", "NISEI", "RERAN", "NARCO", "CLOPS", "ESSES", "SCONE", "SALSA", "ROANS", "RAPER", "CORER", "COLAS", "CAIRN", "CRAPS", "CLONE", "NOELS", "ORCAS", "PARSE", "EPEES", "LANAI", "SEPAL", "CAPON", "PREPS", "NARCS", "REPRO", "ORALS", "ROILS", "ILIAC", "CILLS", "LOCOS", "RENAL", "CROCS", "PARAS", "SECCO", "PIONS", "PARER", "PLEIN", "AREAL", "SOLON", "PSOAS", "SCOPS", "SLOES", "NOIRE", "POLIS", "PASSE", "NONES", "SARAN", "POLOS", "APERS", "ARRAS", "PRISE", "SPIER", "AIRER", "APIAN", "CASAS", "CARER", "POOPS", "SPINA", "PENES", "RILLE", "CANER", "LIRAS", "PRIER", "LOPER", "CALLA", "PONES", "COCAS", "OILER", "ASSAI", "LAPIN", "ASANA", "OLEOS", "LIERS", "ANILE", "PLENA", "AIOLI", "SLIER", "CANNA", "PEASE", "LASES", "RASAE", "PAREN", "SOCLE", "RICER", "RICES", "ELANS", "CEILS", "LISLE", "OLIOS", "APSOS", "RIALS", "ICERS", "COPER", "PEENS", "POLER", "LACER", "ARSES", "SPOOR", "CIRRI", "APNEA", "NARES", "OSIER", }; static const uint32_t _num_words = (sizeof(_legal_words) / sizeof(_legal_words[0])); static const uint8_t _num_valid_letters = (sizeof(_valid_letters) / sizeof(_valid_letters[0])); static uint32_t get_random(uint32_t max) { #if __EMSCRIPTEN__ return rand() % max; #else return arc4random_uniform(max); #endif } static void display_letter(wordle_state_t *state, bool display_dash) { char buf[1 + 1]; if (state->word_elements[state->position] >= _num_valid_letters) { if (display_dash) watch_display_string("-", state->position + 5); else watch_display_string(" ", state->position + 5); return; } sprintf(buf, "%c", _valid_letters[state->word_elements[state->position]]); watch_display_string(buf, state->position + 5); } static void display_all_letters(wordle_state_t *state) { uint8_t prev_pos = state->position; for (size_t i = 0; i < WORDLE_LENGTH; i++) { state->position = i; display_letter(state, false); } state->position = prev_pos; } static bool check_word(wordle_state_t *state) { // Exact bool is_exact_match = true; for (size_t i = 0; i < WORDLE_LENGTH; i++) { if (_valid_letters[state->word_elements[i]] == _legal_words[state->curr_answer][i]) state->word_elements_result[i] = WORDLE_LETTER_CORRECT; else { state->word_elements_result[i] = WORDLE_LETTER_WRONG; is_exact_match = false; } } // Wrong Location for (size_t i = 0; i < WORDLE_LENGTH; i++) { for (size_t j = 0; j < WORDLE_LENGTH; j++) { if (state->word_elements_result[j] != WORDLE_LETTER_WRONG) continue; if (_valid_letters[state->word_elements[i]] == _legal_words[state->curr_answer][j]) { printf("me: %c them: %c\r\n", _valid_letters[state->word_elements[i]], _legal_words[state->curr_answer][j]); state->word_elements_result[j] = WORDLE_LETTER_WRONG_LOC; break; } } } for (size_t i = 0; i < WORDLE_LENGTH; i++) { printf("%d : %d\r\n", i, state->word_elements_result[i]); } return is_exact_match; } static void display_attempt(uint8_t attempt) { char buf[2]; sprintf(buf, "%d", attempt); watch_display_string(buf, 3); } static void reset_board(wordle_state_t *state) { for (size_t i = 0; i < WORDLE_LENGTH; i++) { state->word_elements[i] = _num_valid_letters; state->word_elements_result[i] = WORDLE_LETTER_WRONG; } state->curr_answer = get_random(_num_words); state->position = 0; state->attempt = 1; state->curr_screen = SCREEN_PLAYING; watch_clear_colon(); watch_display_string(" ", 4); display_attempt(state->attempt); display_all_letters(state); watch_display_string("-", 5); printf("rand: %s\r\n", _legal_words[state->curr_answer]); } static void display_title(wordle_state_t *state) { state->curr_screen = SCREEN_TITLE; watch_display_string("WO WORDLE", 0); } static void display_streak(wordle_state_t *state) { char buf[12]; state->curr_screen = SCREEN_STREAK; printf("streak %d \r\n", state->streak); sprintf(buf, "WO St%2ddy", state->streak); watch_display_string(buf, 0); watch_set_colon(); } static void display_lose(wordle_state_t *state, uint8_t subsecond) { char buf[WORDLE_LENGTH + 6]; sprintf(buf," L %s", subsecond % 2 ? _legal_words[state->curr_answer] : " "); watch_display_string(buf, 0); } static void display_win(wordle_state_t *state, uint8_t subsecond) { (void) state; char buf[13]; sprintf(buf," W %s ", subsecond % 2 ? "NICE" : "JOb "); watch_display_string(buf, 0); } static uint8_t get_first_pos(WordleLetterResult *word_elements_result) { for (size_t i = 0; i < WORDLE_LENGTH; i++) { if (word_elements_result[i] != WORDLE_LETTER_CORRECT) return i; } return 0; } static uint8_t get_next_pos(uint8_t curr_pos, WordleLetterResult *word_elements_result) { uint8_t pos = curr_pos; do { pos++; if (pos > WORDLE_LENGTH) return WORDLE_LENGTH + 1; } while (word_elements_result[pos] == WORDLE_LETTER_CORRECT); return pos; } static uint8_t get_prev_pos(uint8_t curr_pos, WordleLetterResult *word_elements_result) { int8_t pos = curr_pos; do { pos--; if (pos < 0) return curr_pos; } while (word_elements_result[pos] == WORDLE_LETTER_CORRECT); return pos; } static void display_result(wordle_state_t *state, uint8_t subsecond) { char buf[WORDLE_LENGTH + 1]; for (size_t i = 0; i < WORDLE_LENGTH; i++) { switch (state->word_elements_result[i]) { case WORDLE_LETTER_WRONG: buf[i] = '-'; break; case WORDLE_LETTER_CORRECT: buf[i] = _valid_letters[state->word_elements[i]]; break; case WORDLE_LETTER_WRONG_LOC: if (subsecond % 2) buf[i] = ' '; else buf[i] = _valid_letters[state->word_elements[i]]; default: break; } } watch_display_string(buf, 5); } static bool act_on_btn(wordle_state_t *state) { switch (state->curr_screen) { case SCREEN_RESULT: for (size_t i = 0; i < WORDLE_LENGTH; i++) { if (state->word_elements_result[i] != WORDLE_LETTER_CORRECT) state->word_elements[i] = _num_valid_letters; } display_attempt(state->attempt); display_all_letters(state); state->position = get_first_pos(state->word_elements_result); state->curr_screen = SCREEN_PLAYING; return true; case SCREEN_TITLE: display_streak(state); return true; case SCREEN_STREAK: reset_board(state); return true; case SCREEN_WIN: case SCREEN_LOSE: display_title(state); return true; default: return false; } return false; } void wordle_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(wordle_state_t)); memset(*context_ptr, 0, sizeof(wordle_state_t)); wordle_state_t *state = (wordle_state_t *)*context_ptr; state->curr_screen = SCREEN_TITLE; } // Do any pin or peripheral setup here; this will be called whenever the watch wakes from deep sleep. } void wordle_face_activate(movement_settings_t *settings, void *context) { (void) settings; wordle_state_t *state = (wordle_state_t *)context; movement_request_tick_frequency(2); if (state->curr_screen == SCREEN_TITLE) display_title(state); } bool wordle_face_loop(movement_event_t event, movement_settings_t *settings, void *context) { wordle_state_t *state = (wordle_state_t *)context; switch (event.event_type) { case EVENT_ACTIVATE: if (state->curr_screen == SCREEN_PLAYING) display_all_letters(state); break; case EVENT_TICK: switch (state->curr_screen) { case SCREEN_PLAYING: if (event.subsecond % 2) { display_letter(state, true); } else { watch_display_string(" ", state->position + 5); } break; case SCREEN_RESULT: display_result(state, event.subsecond); break; case SCREEN_LOSE: display_lose(state, event.subsecond); break; case SCREEN_WIN: display_win(state, event.subsecond); break; default: break; } break; case EVENT_LIGHT_BUTTON_UP: if (act_on_btn(state)) break; if (state->word_elements[state->position] >= _num_valid_letters) state->word_elements[state->position] = 0; else state->word_elements[state->position] = (state->word_elements[state->position] + 1) % _num_valid_letters; display_letter(state, true); break; case EVENT_LIGHT_LONG_PRESS: if (state->word_elements[state->position] >= _num_valid_letters) state->word_elements[state->position] = _num_valid_letters - 1; else state->word_elements[state->position] = (state->word_elements[state->position] + _num_valid_letters - 1) % _num_valid_letters; display_letter(state, true); break; case EVENT_ALARM_BUTTON_UP: if (act_on_btn(state)) break; display_letter(state, true); if (state->word_elements[state->position] == _num_valid_letters) break; state->position = get_next_pos(state->position, state->word_elements_result); if(WORDLE_LENGTH == (state->position)) { bool exact_match = check_word(state); if (exact_match) { state->curr_screen = SCREEN_WIN; state->streak++; break; } if (state->attempt++ >= WORDLE_MAX_ATTEMPTS) { state->curr_screen = SCREEN_LOSE; state->streak = 0; break; } state->curr_screen = SCREEN_RESULT; break; } break; case EVENT_ALARM_LONG_PRESS: display_letter(state, true); state->position = get_prev_pos(state->position, state->word_elements_result); break; case EVENT_LIGHT_BUTTON_DOWN: break; case EVENT_TIMEOUT: case EVENT_LOW_ENERGY_UPDATE: break; default: return movement_default_loop_handler(event, settings); } return true; } void wordle_face_resign(movement_settings_t *settings, void *context) { (void) settings; (void) context; }