diff --git a/lander_face.c b/lander_face.c new file mode 100644 index 000000000..2c3d24461 --- /dev/null +++ b/lander_face.c @@ -0,0 +1,768 @@ +/* + * MIT License + * + * Copyright (c) 2024 Klingon Jane + * + * 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. + */ + +// Submitted github version: +// Granularity at 40, improved fuel feedback, no optional difficulty_level reduction. +// First-earth difficulty reductions. Signficant difficulty reduction on 1st six landings. +// Best ship's health at 8, minus 1 level for each 2 points of speed. + +// Emulator only: need time() to seed the random number generator. +#if __EMSCRIPTEN__ +#include +#endif + +#include +#include +#include +#include "lander_face.h" + +#define LANDER_TICK_FREQUENCY 8 +#define MONSTER_DISPLAY_TICKS 9 +#define ENGINE_THRUST 11 +#define MODE_WAITING_TO_START 0 +#define MODE_DISPLAY_SKILL_LEVEL 1 +#define MODE_PLAYING 2 +#define MODE_TOUCHDOWN_BLANK 3 +#define MODE_DISPLAY_FINAL_STATUS 4 +#define MODE_MONSTER 5 +#define MODE_FIND_EARTH_MESSAGE 6 +#define CREWS_COMPLIMENT 13 +// Granularity is divisions per foot - height display +#define GRANUL 40 +// Next lines for repeat heroes only. +#define PROMOTION_INTERVAL 3 +#define LEVEL_ACE 8 +#define LEVEL_SPENCER 11 +#define HARD_EARTH_INCREMENTS 11 +#define MAX_HARD_EARTH_CHANCE 6 + +// The gory final result calculations: +#define SPEED_FATALITY_ALL 41 +#define SPEED_FATALITY_NONE 26 +#define SPEED_NO_DAMAGE 21 +#define SPEED_LEVEL_INCREMENTS 2 +#define SPEED_MAJOR_CRASH 73 +#define MAJOR_CRASH_INCREMENTS 65 +#define SPEED_INJURY_NONE 20 +#define SPEED_INJURY_FULCRUM 32 +#define INJURY_FULCRUM_PROB 65 +#define FUEL_SCORE_GOOD 145 +#define FUEL_SCORE_GREAT 131 +#define FUEL_SCORE_FANTASTIC 125 + +#define VERSION_NUMBER 19 +// Joey Castillo to oversee storage allocation row +#define LANDER_STORAGE_ROW 2 +#define STORAGE_KEY_NUMBER 126 +// Base date changed to 1999 from 2000 starting in version 125 +#define STORAGE_KEY_RATE_2000 124 +// Versions at or above LEGENDARY have valid hero_counter and legend_counter data. +#define STORAGE_KEY_LEGENDARY 110 + +#define DIFFICULTY_LEVELS 3 +char lander_difficulty_names[DIFFICULTY_LEVELS][7] = { + "NOrMAL", "HArd ", "HArdEr" +}; +char lander_months[13][3]= { + "Un", "JA", "FE", "Mr", "AP", "MY", "Jn", "JL", "AU", "SE", "OC", "NO", "dE" +}; +#define MONSTER_TYPES 4 +char lander_monster_names[MONSTER_TYPES][7] = { + "mOnStr", "6Erbil", "HAmStr", "Rabbit" +}; +#define MONSTER_ACTIONS 8 +char lander_monster_actions[MONSTER_ACTIONS][7] = { + "HUn6ry", " EAtS", "6Reedy", "annoYd", "nASty ", "SAVOry", "HO66SH", " pI66Y" +}; + + +// -------------- +// Custom methods +// -------------- + + +static int gen_random_int (int16_t lower, int16_t upper) { + int range; + int retVal; + range = upper - lower + 1; + if ( range < 2 ) range = 2; + // Emulator: use rand. Hardware: use arc4random. + #if __EMSCRIPTEN__ + retVal = rand() % range; + #else + retVal = arc4random_uniform(range); + #endif + retVal += lower; + return retVal; +} + +static uint8_t assignProb ( uint8_t lowerProb, uint8_t upperProb, int16_t lowerSpeed, int16_t upperSpeed, int16_t actSpeed ) { + float probRange, speedRange; + float ratio, probFloat; + int probInt; + speedRange = upperSpeed - lowerSpeed; + if (speedRange<1.0) speedRange = 1.0; + probRange = upperProb - lowerProb; + ratio = ( (float) actSpeed - (float) lowerSpeed ) / speedRange; + probFloat = (float) lowerProb + ( ratio * probRange ); + probInt = (int) ( probFloat + 0.5 ); + probInt = min ( probInt, upperProb ); + probInt = max ( probInt, lowerProb ); + return (uint8_t) probInt; +} + +static void write_to_lander_EEPROM(lander_state_t *state) { + uint8_t size = 10; // Length of data + uint8_t output_array [ size ]; + uint8_t offset = 0; + + output_array [ 0 ] = STORAGE_KEY_NUMBER; + output_array [ 1 ] = state->hero_counter; + output_array [ 2 ] = state->legend_counter; + output_array [ 3 ] = state->pilot_rating; + output_array [ 4 ] = state->pilot_rating_highest; + output_array [ 5 ] = state->rating_overflow_count; + // Storage 6 and 7 are game counter, storage 6 is high byte, storage 7 is low byte + output_array [ 6 ] = state->game_counter / 256; + output_array [ 7 ] = state->game_counter % 256; + output_array [ 8 ] = state->start_year - 1999; // Hopefully solved by the year 2255 with Star Trek. + output_array [ 9 ] = state->start_month; + watch_storage_erase ( LANDER_STORAGE_ROW ); + watch_storage_sync ( ); + watch_storage_write ( LANDER_STORAGE_ROW, offset, output_array, size ); +} + +// Reset all stats except hero variables +static void lander_reset(lander_state_t *state) { + state->pilot_rating = 15; + state->pilot_rating_highest = state->pilot_rating; + state->rating_overflow_count = 0; + state->game_counter = 0; + state->start_year = 1999; // Overwritten on 1st mission + state->start_month = 12; +} + + +// --------------------------- +// Standard watch face methods +// --------------------------- +void lander_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(lander_state_t)); + memset(*context_ptr, 0, sizeof(lander_state_t)); + } + lander_state_t *state = (lander_state_t *)*context_ptr; + // Emulator only: Seed random number generator + #if __EMSCRIPTEN__ + srand(time(NULL)); + #endif + // Read in stored hero counter, pilot history etc. from EEPROM. + uint32_t offset = 0; + uint32_t size = 10; // How many bytes we want + uint8_t stored_data [ size ]; + // See if the hero_counter was ever written to EEPROM storage + watch_storage_read (LANDER_STORAGE_ROW, offset, stored_data, size); + if ( ( stored_data[0] <= STORAGE_KEY_NUMBER ) && + ( stored_data[0] >= STORAGE_KEY_RATE_2000 ) ) + { + state->hero_counter = stored_data [1]; // There's real data in there. + state->legend_counter = stored_data [2]; + state->pilot_rating = stored_data [3]; + state->pilot_rating_highest = stored_data [4]; + state->rating_overflow_count = stored_data [5]; + state->game_counter = stored_data [6] * 256; + state->game_counter += stored_data [7]; + state->start_year = stored_data [8] + 1999; // Ex. 2024 + state->start_month = stored_data [9]; + state->difficulty_level = state->hero_counter / PROMOTION_INTERVAL; + state->difficulty_level = min ( state->difficulty_level, DIFFICULTY_LEVELS - 1 ); // Upper limit + state->write_pending=false; + if ( stored_data[0] == STORAGE_KEY_RATE_2000 ) { + state->start_year = stored_data [8] + 2000; // Ex. 2024 + } + if ( stored_data[0] < STORAGE_KEY_NUMBER ) { // New system starting storage version 126 + state->rating_overflow_count = 0; + state->pilot_rating = (int) ( state->pilot_rating * 0.77 ); + state->pilot_rating_highest = (int) ( state->pilot_rating_highest * 0.77 ); + } + } + else + { + lander_reset(state); + state->hero_counter = 0; + state->legend_counter = 0; + state->difficulty_level = 0; + state->write_pending=false; + if ( (stored_data[0] >= STORAGE_KEY_LEGENDARY ) && ( stored_data[0] < STORAGE_KEY_NUMBER ) ) // Only invoked on software upgrade + { + state->hero_counter = stored_data [1]; // Your existing values go here + state->legend_counter = stored_data [2]; // and here + state->difficulty_level = state->hero_counter / PROMOTION_INTERVAL; + state->difficulty_level = min ( state->difficulty_level, DIFFICULTY_LEVELS - 1 ); // Upper limit + } + } + state->granularity = GRANUL; +} + +void lander_face_activate(movement_settings_t *settings, void *context) { + (void) settings; + lander_state_t *state = (lander_state_t *)context; + char buf [ 7 ]; + watch_date_time date_time; + state->mode = MODE_WAITING_TO_START; + state->led_active = false; + state->reset_counter = 0; + state->aux_display_mode = 0; + watch_clear_all_indicators ( ); + // Fancy intro + if ( state->legend_counter == 0 ) watch_display_string("LA", 0); + else watch_display_string("LE", 0); + if ( ( state->hero_counter == 0 ) || ( state->hero_counter >= 40 ) ) watch_display_string ( " ", 2); + else + { + sprintf ( buf, "%2d", state->hero_counter ); + watch_display_string(buf, 2); + } + if ( state->hero_counter >= 100 ) sprintf ( buf, "Str%3d", state->hero_counter ); + else if ( state->hero_counter >= 40 ) sprintf ( buf, "Strb%2d", state->hero_counter ); + else if ( state->hero_counter >= LEVEL_SPENCER ) sprintf ( buf, "StrbUC" ); + else if ( state->hero_counter >= LEVEL_ACE ) sprintf ( buf, " ACE " ); // This human is good + else if ( state->difficulty_level == 0 ) sprintf ( buf, " " ); + else sprintf ( buf, "%s", lander_difficulty_names[state->difficulty_level] ); + watch_display_string ( buf, 4); + if ( state->start_month == 0 ) { // Only executed once after new software version loaded + date_time = watch_rtc_get_date_time(); + state->start_year = date_time.unit.year + 2020; // 2020 is baseline movement uses. + state->start_month = date_time.unit.month; + state->write_pending = 1; + } +} + +bool lander_face_loop(movement_event_t event, movement_settings_t *settings, void *context) { + (void) settings; + lander_state_t *state = (lander_state_t *)context; + char buf [ 20 ]; // [11] is more correct and works; compiler too helpful. + uint8_t i; + + switch (event.event_type) { + case EVENT_TICK: + state->tick_counter++; + if ( state->mode == MODE_PLAYING ) { + int16_t accel = state->gravity; + bool gas_pedal_on = watch_get_pin_level(BTN_ALARM); + if ( gas_pedal_on && ( state->fuel_remaining > 0 ) ) { + accel = ENGINE_THRUST + state->gravity; // Gravity is negative + state->fuel_remaining--; // Used 1 fuel unit + watch_set_indicator ( WATCH_INDICATOR_LAP ); + // Low fuel warning indicators + if ( state->fuel_remaining == ( 3 * LANDER_TICK_FREQUENCY ) ) { // 3 seconds of fuel left + watch_set_indicator ( WATCH_INDICATOR_SIGNAL ); + watch_set_indicator ( WATCH_INDICATOR_BELL ); + watch_set_indicator ( WATCH_INDICATOR_PM ); + watch_set_indicator ( WATCH_INDICATOR_24H ); + } + else if ( state->fuel_remaining == 0 ) { // 0 seconds of fuel left, empty! + watch_clear_all_indicators ( ); + } + } + else { + watch_clear_indicator ( WATCH_INDICATOR_LAP ); + } + state->speed += accel; + state->height += state->speed; + if ( ( state->height > 971 * 80 ) && ( state->speed > 70 ) ) { // Escape height + watch_clear_all_indicators (); + watch_display_string ( "ESCAPE", 4 ); + state->tick_counter = 0; + state->mode = MODE_WAITING_TO_START; + } + else if ( state->height <= 0 ) { // Touchdown + state->tick_counter = 0; + state->mode = MODE_TOUCHDOWN_BLANK; + } + else { + // Update height display + sprintf ( buf, "%4d", (int) ( state->height / state->granularity ) ); + watch_display_string ( buf, 4 ); + } + } + else if ( state->mode == MODE_TOUCHDOWN_BLANK ) { + // Blank display on touchdown + if ( state->tick_counter == 1 ) { + watch_clear_all_indicators (); + watch_display_string ( " ", 4 ); + state->game_counter++; + if ( state->game_counter == 1 ) { // Grab year/month of very first mission + watch_date_time date_time; + date_time = watch_rtc_get_date_time(); + state->start_year = date_time.unit.year + 2020; // 2020 is baseline movement uses. + state->start_month = date_time.unit.month; + } + // Also calc fuel score now. + float fuel_score_float; + uint16_t fuel_used; + fuel_used = state->fuel_start - state->fuel_remaining; + fuel_score_float = (float) fuel_used / (float) state->fuel_tpl; + state->fuel_score = (int) (fuel_score_float * 100.0 + 0.5); + if ( state->legend_counter == 0 ) state->fuel_score -= 8; // First Earth is easier + // Monitor reset_counter + if ( fuel_used >= 1 ) state->reset_counter = 0; + else state->reset_counter++; + if ( state->reset_counter == 3 ) { + lander_reset(state); + watch_display_string ( " rESET ", 2 ); + delay_ms(2000); + sprintf ( buf, " %4d ", state->pilot_rating); + watch_display_string ( buf, 2 ); + state->tick_counter = 0; + state->mode = MODE_WAITING_TO_START; + } + else if ( state->reset_counter == 7 ) { + lander_reset(state); + state->hero_counter = 0; + state->legend_counter = 0; + state->difficulty_level = 0; + watch_display_string ( " CLEAr ", 2 ); + delay_ms(2000); + watch_display_string ( " ", 4 ); + state->tick_counter = 0; + state->mode = MODE_WAITING_TO_START; + } + } + // Wait until time for next display + if ( state->tick_counter >= ( 1 * LANDER_TICK_FREQUENCY ) ) { + state->tick_counter = 0; + state->mode = MODE_DISPLAY_FINAL_STATUS; + } + } + else if ( state->mode == MODE_DISPLAY_FINAL_STATUS ) { + bool last_pass = false; + if ( state->tick_counter >= LANDER_TICK_FREQUENCY ) last_pass = true; + // Show final status + if ( state->tick_counter == 1 ) { + // Calculate many attributes + // 1) Display touchdown speed - upper right, may get overwritten + // 2) Check for major crash: bug, crater, vaporized (gone). + // 3) Rank ship's health 0 to 8 + // 4) Calculate pilot ranking + // 5) Special conditions: hero, stellar and Earth + // 6) Crew fatalities and injuries + // 7) Set fuel conservation indicators as appropriate + // 8) Set coffee maker OK indicator as appropriate + // 9) Green light if ship intact + // 10) Set standard display if not preempted. + + bool shipDestroyed, stellarPilot, foundEarth, disableText; + int16_t actSpeed, adjSpeed, dispSpeed, tempSpeed, levelsDamage; + int8_t shipsHealth, shipsHealthDisp, myRand; + uint8_t fatalities, probFatal, probInjury, prevPilotRating; + char buf2[3]; + int8_t landing_points; + int16_t delta_fuel, fuel_value, fuel_score_adj; + + shipDestroyed = false; + stellarPilot = false; + foundEarth = false; + disableText = false; + // Landings more difficult with difficulty_level. + actSpeed = abs ( state->speed ); + adjSpeed = actSpeed + state->difficulty_level * 4; + fuel_score_adj = state->fuel_score; + // First Earth is a bit easier than all the others + if ( state->legend_counter == 0 ) { + adjSpeed -= 3; + adjSpeed = max ( 1, adjSpeed ); // Unlikely, but technically possible + fuel_score_adj -= 8; + } + // 1) Display touchdown speed - upper right, may get overwritten + dispSpeed = actSpeed; + if ( dispSpeed < 0 ) dispSpeed = 0; + if ( dispSpeed > 999 ) dispSpeed = 999; + if ( dispSpeed > 399 ) dispSpeed = - ( dispSpeed / 100 ); + else if ( dispSpeed > 39 ) dispSpeed = dispSpeed / 10; + sprintf ( buf2, "%2d", dispSpeed ); + watch_display_string ( buf2, 2 ); + // 2) Check for major crash: bug, crater, vaporized (gone). + if ( (!shipDestroyed) && adjSpeed >= SPEED_MAJOR_CRASH ) { + shipDestroyed = true; + disableText = true; + if ( adjSpeed >= ( SPEED_MAJOR_CRASH + 2 * MAJOR_CRASH_INCREMENTS ) ) sprintf ( buf, "GOnE " ); + else if ( adjSpeed >= ( SPEED_MAJOR_CRASH + MAJOR_CRASH_INCREMENTS ) ) sprintf ( buf, " CrAtr" ); + else sprintf ( buf, " bU6" ); + } + // 3) Rank ship's health 0 to 8 + shipsHealth = -1; + if ( !shipDestroyed ) { + tempSpeed = adjSpeed + SPEED_LEVEL_INCREMENTS - 1; + levelsDamage = (int) ( ( tempSpeed - SPEED_NO_DAMAGE ) / SPEED_LEVEL_INCREMENTS ); + shipsHealth = 8 - levelsDamage; + shipsHealth = min ( shipsHealth, 8 ); // Keep between 0 and 8 + shipsHealth = max ( shipsHealth, 0 ); + } + state->ships_health = shipsHealth; // Remember ships health + shipsHealthDisp = shipsHealth; + state->ships_health_disp = shipsHealthDisp; // Remember ships displayed health + // 4) Calculate pilot ranking + tempSpeed = actSpeed + SPEED_LEVEL_INCREMENTS - 1; + levelsDamage = (int) ( ( tempSpeed - ( SPEED_NO_DAMAGE - 8 ) ) / SPEED_LEVEL_INCREMENTS ); + landing_points = 8 - levelsDamage; + landing_points = min ( landing_points, 8 ); // Keep between 0 and 8 + landing_points = max ( landing_points, 0 ); + delta_fuel=floor((FUEL_SCORE_FANTASTIC - state->fuel_score)/6.0); + fuel_value = delta_fuel + 5; + fuel_value = min ( 5, fuel_value ); // Keep between 0 and 5 + fuel_value = max ( 0, fuel_value ); + if ( fuel_value > landing_points ) fuel_value = landing_points; + landing_points += fuel_value; + prevPilotRating = state->pilot_rating; + state->pilot_rating += landing_points; + // Emulate overflow bug above rating of 255 + if ( state->pilot_rating < prevPilotRating ) // Overflow/wrap - very good pilot! + { + state->rating_overflow_count += 1; + state->pilot_rating = 120; + state->pilot_rating_highest = state->pilot_rating; + sprintf ( buf, "StELAr" ); + stellarPilot = true; + disableText = true; + } + else if ( state->pilot_rating >= state->pilot_rating_highest ) { + watch_set_indicator ( WATCH_INDICATOR_BELL ); + if ( state->pilot_rating > state->pilot_rating_highest ) { + state->pilot_rating_highest = state->pilot_rating; + watch_display_string("HI", 0); + } + } + // 5) Special conditions: hero, stellar and Earth + if ( (shipsHealth >= 8) && ( fuel_score_adj <= FUEL_SCORE_FANTASTIC ) ) { // A hero's landing!! + state->hero_counter++; + disableText = true; // Suppress usual ship and crew count status message + + // Did we happen to find Earth? + // Two rule sets for finding Earth. Alternate between easy and hard. + int16_t my_odds, temp; + if ( state->legend_counter %2 == 0 ) my_odds = (int8_t) state->hero_counter - LEVEL_SPENCER; // Easy + else { + temp = ( state->hero_counter - LEVEL_SPENCER ) + HARD_EARTH_INCREMENTS - 1; + my_odds = temp / HARD_EARTH_INCREMENTS; + my_odds = min ( my_odds, MAX_HARD_EARTH_CHANCE ); + } + // Display odds in weekday region if positive value - overwrites touch-down speed + if ( my_odds > 0 ) { + char buff3 [ 3 ]; + sprintf ( buff3, "%2d", my_odds ); + watch_display_string ( buff3, 2 ); + } + // Check if we found Earth + if ( my_odds >= gen_random_int ( 1, 200 ) ) { // EARTH!!!! The final objective. + // last chance to remind user which heroic landing this was prior + // to wiping it out forever + char buff4 [ 3 ]; + sprintf ( buff4, "%2d", state->hero_counter % 40 ); + watch_display_string ( buff4, 2 ); + state->hero_counter = 0; + state->legend_counter++; + foundEarth = true; + } + // Message priorities are Earth, then Stellar, than Hero + if ( foundEarth ) sprintf ( buf, "EArTH " ); // 17% within 8, 50% by 16, 79% by 24, 94% by 32 <- easy mode + else if ( stellarPilot ) sprintf ( buf, "StELAr" ); + else { + if ( state->hero_counter==1 ) sprintf ( buf, "HErO " ); + else if ( state->hero_counter == LEVEL_ACE ) sprintf ( buf, " ACE " ); + else if ( state->hero_counter == LEVEL_SPENCER ) sprintf ( buf, "STrbUC" ); + else if ( state->hero_counter>99 ) sprintf ( buf, "HEr%3d", state->hero_counter ); + else sprintf ( buf, "HErO%2d", state->hero_counter ); // Typical case + } + // Recalculate difficulty level base on new hero_counter. + state->difficulty_level = state->hero_counter / PROMOTION_INTERVAL; + state->difficulty_level = min ( state->difficulty_level, DIFFICULTY_LEVELS - 1 ); // Upper limit + // Write to EEPROM on resigning + state->write_pending=true; + } + // 6) Crew fatalities and injuries + if (!disableText) { + // Fatalies + probFatal = assignProb ( 0, 92, SPEED_FATALITY_NONE, SPEED_FATALITY_ALL, adjSpeed ); + // Injuries + if ( adjSpeed <= SPEED_INJURY_FULCRUM ) { + probInjury = assignProb ( 0, INJURY_FULCRUM_PROB, SPEED_INJURY_NONE, SPEED_INJURY_FULCRUM, adjSpeed ); + } else { + probInjury = assignProb ( INJURY_FULCRUM_PROB, 96, SPEED_INJURY_FULCRUM, SPEED_FATALITY_ALL, adjSpeed ); + } + fatalities = 0; + state->injured = 0; + for ( i = 0; i < CREWS_COMPLIMENT; i++ ) { + myRand = gen_random_int ( 1, 100 ); + if ( myRand <= probFatal ) fatalities++; + else if ( myRand <= probInjury ) state->injured++; + } + state->uninjured = CREWS_COMPLIMENT - fatalities - state->injured; + } + // 7) Set fuel conservation indicators as appropriate + if ( shipsHealth >= 1 && ( fuel_score_adj <= FUEL_SCORE_FANTASTIC ) ) watch_set_indicator ( WATCH_INDICATOR_LAP ); + if ( shipsHealth >= 1 && ( fuel_score_adj <= FUEL_SCORE_GREAT ) ) watch_set_indicator ( WATCH_INDICATOR_24H ); + if ( shipsHealth >= 1 && ( fuel_score_adj <= FUEL_SCORE_GOOD ) ) watch_set_indicator ( WATCH_INDICATOR_PM ); + // 8) Set coffee maker OK indicator as appropriate + if ( shipsHealth >= 5 || ( shipsHealth >= 0 && ( gen_random_int ( 0, 3 ) != 1 ) ) ){ + watch_set_indicator ( WATCH_INDICATOR_SIGNAL ); + } + // 9) Green light if ship intact + if ( shipsHealth >= 8 ) { + watch_set_led_green ( ); + state->led_active = true; + } + // 10) Set standard display if not preempted. + if (!disableText) { + if ( ( state->injured > 0 ) || ( state->uninjured == 0 ) ) { + sprintf ( buf, "%d %2d%2d", shipsHealthDisp, state->uninjured, state->injured ); + } + else { + sprintf ( buf, "%d %2d ", shipsHealthDisp, state->uninjured ); + } + } + // Display final status. + watch_display_string ( buf, 4 ); + } // End if tick_counter == 1 + + // Major crash - ship burning with red LED. + if ( state->ships_health < 0 ) { + if ( ( gen_random_int ( 0, 1 ) != 1 ) && !last_pass ) { // Always off on last pass + // Turn on red LED. + watch_set_led_red ( ); + state->led_active = true; + } else { + watch_set_led_off ( ); + } + } + // Wait long enough, then allow waiting for next game. + if ( last_pass ) { + watch_set_led_off ( ); + // No change to display text, allow new game to start. + state->mode = MODE_WAITING_TO_START; + // Unless it's time for monsters + uint8_t survivors = state->injured + state->uninjured; + if ( ( state->ships_health >= 0 ) && ( survivors > 0 ) && + ( gen_random_int ( -3, 3 ) >= state->ships_health ) ) { + state->mode = MODE_MONSTER; + state->tick_counter = 0; + state->monster_type = gen_random_int ( 0, MONSTER_TYPES - 1 ); + } + } + } // End if MODE_DISPLAY_FINAL_STATUS + else if ( state->mode == MODE_DISPLAY_SKILL_LEVEL ) { + // Display skill level + if ( state->tick_counter == 1 ) { + sprintf ( buf, " %d %d ", state->skill_level, state->skill_level ); + watch_display_string ( buf, 2 ); + } + // Wait long enough, then start game. + if ( state->tick_counter >= ( 2.0 * LANDER_TICK_FREQUENCY ) ) { + state->tick_counter = 0; + // Houston, WE ARE LAUNCHING NOW.... + state->mode = MODE_PLAYING; + } + } + else if ( state->mode == MODE_FIND_EARTH_MESSAGE ) { + // Display "Find" then "Earth" + if ( state->tick_counter == 1 ) { + sprintf ( buf, " FInd " ); + watch_display_string ( buf, 2 ); + } + if ( state->tick_counter == (int) ( 1.5 * LANDER_TICK_FREQUENCY + 1 ) ) { + sprintf ( buf, " EArTH " ); + watch_display_string ( buf, 2 ); + } + // Wait long enough, then display skill level. + if ( state->tick_counter >= ( 3 * LANDER_TICK_FREQUENCY ) ) { + state->tick_counter = 0; + state->mode = MODE_DISPLAY_SKILL_LEVEL; + } + } + else if ( state->mode == MODE_MONSTER ) { + if ( state->tick_counter == 1 ) watch_display_string ( lander_monster_names[state->monster_type], 4 ); + else if ( state->tick_counter == MONSTER_DISPLAY_TICKS + 1 ) { + uint8_t my_rand; + my_rand = gen_random_int ( 0 , MONSTER_ACTIONS - 1 ); + watch_display_string ( lander_monster_actions[my_rand], 4 ); + } + else if ( state->tick_counter == MONSTER_DISPLAY_TICKS * 2 ) { // Display 1st monster character + sprintf ( buf, "%s", lander_monster_names[state->monster_type] ); + buf [1] = 0; + watch_display_string(buf,4); + } + else if ( state->tick_counter == MONSTER_DISPLAY_TICKS * 2 + 1 ) { // Display current population, close mouth + sprintf ( buf, "c%2d%2d", state->uninjured, state->injured ); + watch_display_string ( buf, 5 ); + } + else if ( state->tick_counter == MONSTER_DISPLAY_TICKS * 2 + 3 ) watch_display_string ( "C", 5 ); // Open mouth + else if ( state->tick_counter == MONSTER_DISPLAY_TICKS * 2 + 5 ) { + // Decision to: continue loop, end loop or eat astronaut + uint8_t survivors = state->injured + state->uninjured; + uint8_t myRand = gen_random_int ( 0, 14 ); + if ( survivors == 0 ) state->mode = MODE_WAITING_TO_START; + else if ( myRand <= 1 ) { // Leave loop with survivors + sprintf ( buf, "%d %2d%2d", state->ships_health_disp, state->uninjured, state->injured ); + watch_display_string ( buf, 4 ); + state->mode = MODE_WAITING_TO_START; + } else if ( myRand <= 11 ) state->tick_counter = MONSTER_DISPLAY_TICKS * 2; // Do nothing, loop continues + else { // Eat an astronaut - welcome to the space program! + if ( state->injured > 0 && state->uninjured > 0 ) { + if ( gen_random_int ( 0,1 ) == 0 ) state->injured--; + else state->uninjured--; + } + else if ( state->injured > 0 ) state->injured--; + else state->uninjured--; + state->tick_counter = MONSTER_DISPLAY_TICKS * 2; // Re-display + } + } + else if ( state->tick_counter >= MONSTER_DISPLAY_TICKS * 4 ) state->mode = MODE_WAITING_TO_START; // Safety + } // End if MODE_MONSTER + break; // End case EVENT_TICK + case EVENT_ALARM_BUTTON_UP: + if ( state->mode == MODE_WAITING_TO_START ) { + // That was the go signal - start a new game!! + float numerator, denominator, timeSquared; + int16_t absGravity, thrust; + float myTime, distToTop, fuel_mult; + uint8_t skill_level; + int32_t tplTop; // Top lander height for TPL calculations + movement_request_tick_frequency(LANDER_TICK_FREQUENCY); + watch_set_led_off ( ); // Safety + watch_clear_all_indicators ( ); + // Randomize starting parameters + state->height = gen_random_int ( 131, 181 ) * 80; + // Per line below; see Mars Orbiter September 23, 1999 + if ( gen_random_int ( 0, 8 ) == 5 ) state->height = gen_random_int ( 240, 800 ) * 80; + state->speed = gen_random_int ( -120, 35 ); // Positive is up + state->gravity = gen_random_int ( -3, -2 ) * 2; // negative downwards value + skill_level = gen_random_int ( 1, 4 ); // Precursor to fuel allocation + // Theoretical Perfect Landing (TPL) calculations start here. + myTime = (float) state->speed / (float) state->gravity; // How long to reach this speed? Don't care which way sign is. + distToTop = fabs ( 0.5 * state->gravity * myTime * myTime ); + tplTop = (int) ( state->height + distToTop + 0.5 ); // Theoretical highest point based on all of speed, height and gravity. + // Time squared = ( 2 * grav * height ) / ( t*t + g*t ), where t is net acceleration with thrust on. + absGravity = abs ( state->gravity ); + thrust = ENGINE_THRUST + state->gravity; + numerator = 2.0 * (float) absGravity * (float) tplTop; + denominator = thrust * thrust + thrust * absGravity; + timeSquared = numerator / denominator; + state->fuel_tpl = (int) ( sqrt ( timeSquared ) + 0.5 ); // Fuel required for theoretical perfect landing (TPL). + if ( skill_level == 1 ) fuel_mult = 4.0; // TPL + 300% + else if ( skill_level == 2 ) fuel_mult = 2.5; // TPL + 150% + else if ( skill_level == 3 ) fuel_mult = 1.6; // TPL + 60% + else fuel_mult = 1.3; // TPL + 30% + state->fuel_start = state->fuel_tpl * fuel_mult; + state->fuel_remaining = state->fuel_start; + state->skill_level = skill_level; + state->tick_counter = 0; + // Early update to pilot ranking here to prevent cheating + if ( state->pilot_rating > 0 ) state->pilot_rating--; + state->pilot_rating = (int) ( state->pilot_rating * 0.9627 + 0.5 ); // Dec 2024 - even tougher! +// state->pilot_rating = (int) ( state->pilot_rating * 0.9698 + 0.5 ); // Need 9 point avg to wrap + state->write_pending = true; + state->aux_display_mode = 0; + if ( state->legend_counter == 0 ) watch_display_string("LA", 0); + else watch_display_string("LE", 0); + if ( gen_random_int ( 1, 109 ) != 37 ) { + // Houston, approaching launch.... + state->mode = MODE_DISPLAY_SKILL_LEVEL; + } + else state->mode = MODE_FIND_EARTH_MESSAGE; + } + break; + case EVENT_LIGHT_BUTTON_DOWN: + if ( state->mode == MODE_WAITING_TO_START ) { + // Display difficulty level + watch_display_string ( lander_difficulty_names [state->difficulty_level], 4 ); + } + break; + case EVENT_LIGHT_LONG_UP: + if ( ( state->mode == MODE_WAITING_TO_START ) && ( state->legend_counter > 0 ) ) { + if ( state->legend_counter > 9 ) sprintf (buf,"EArt%2d", state->legend_counter ); + else sprintf (buf,"EArth%d", state->legend_counter ); + // Display legend counter + watch_display_string ( buf, 4 ); + } + break; + case EVENT_LOW_ENERGY_UPDATE: + if (state->write_pending) { + write_to_lander_EEPROM(state); + watch_storage_sync ( ); + state->write_pending=false; + } + watch_display_string("SLEEP ", 4); + break; + case EVENT_ALARM_LONG_PRESS: + if ( state->mode == MODE_WAITING_TO_START ) { + watch_clear_all_indicators ( ); + if ( state->rating_overflow_count >= 1 ) watch_set_indicator ( WATCH_INDICATOR_SIGNAL ); + if ( state->rating_overflow_count >= 2 ) watch_set_indicator ( WATCH_INDICATOR_BELL ); + if ( state->rating_overflow_count >= 3 ) watch_set_indicator ( WATCH_INDICATOR_PM ); + if ( state->rating_overflow_count >= 4 ) watch_set_indicator ( WATCH_INDICATOR_24H ); + if ( state->rating_overflow_count >= 5 ) watch_set_indicator ( WATCH_INDICATOR_LAP ); + if ( state->aux_display_mode == 0 ) { + // Display pilot rating score here + sprintf ( buf, " %3d ", state->pilot_rating ); + } + else if ( state->aux_display_mode == 1 ) { + // Display pilot rating score here + sprintf ( buf, " %3dHI", state->pilot_rating_highest ); + } + else if ( state->aux_display_mode == 2 ) { + // Display game counter + sprintf ( buf, " %5d", state->game_counter ); + } + else if ( state->aux_display_mode == 3 ) { + // Display date + sprintf ( buf, "%4d%s", state->start_year, lander_months [state->start_month] ); + } + else { + // Display version number + sprintf ( buf, " vEr%2d", VERSION_NUMBER ); + } + watch_display_string ( buf, 4 ); + state->aux_display_mode++; + state->aux_display_mode = state->aux_display_mode % 5; + } + break; + default: + movement_default_loop_handler(event, settings); + break; + } + if ( !state->led_active ) return true; + else return false; +} + +void lander_face_resign(movement_settings_t *settings, void *context) { + (void) settings; + lander_state_t *state = (lander_state_t *)context; + watch_set_led_off ( ); + if (state->write_pending) { + write_to_lander_EEPROM(state); + state->write_pending=false; + } +} diff --git a/lander_face.h b/lander_face.h new file mode 100644 index 000000000..9778f8ad0 --- /dev/null +++ b/lander_face.h @@ -0,0 +1,194 @@ +/* + * MIT License + * + * Copyright (c) 2024 Klingon Jane + * + * 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 LANDER_FACE_H_ +#define LANDER_FACE_H_ + +#include "movement.h" + +/* + +A single button game to land your spacecraft on the planet Cringeworthy. + +Fuel supply is a moderate concern. + +Starting parameters are randomized each round: +- fuel from 1 to 4. Shown at start of round. 4 is tight but doable. +- height +- initial speed +- gravity. Only 2 choices, light (4 units/sec sq.) or heavy (6 units/sec sq.) + +Crew of 13, maximum ship's health is 8. +Engine thrust acceleration is 11 units/sec sq., before gravity. + +End of game displays +"8 13 " - a safe/perfect landing +"5 7 4" - ship damaged, 7 astronauts OK, 4 injured. ( Looks like you lost 2 ) + +That's all most people need to know to play. + +Don't be a hero. + +END OF BASICS + +*/ + + + +typedef struct { + int32_t height; + int16_t speed; // Positive is up + uint16_t tick_counter; // For minimum delays + uint16_t fuel_start; + uint16_t fuel_remaining; + uint16_t fuel_tpl; // Fuel required for theoretical perfect landing + uint16_t fuel_score; // 100 is perfect; higher is less perfect + int8_t gravity; // negative downwards value + bool led_active; // Did we use it this scenario? + uint8_t mode; // 0 Pre-launch waiting, 1 show level, 2 playing, 3 touchdown blank, 4 final display, 5 monster + uint8_t skill_level; // 1 thru 4. Dictates fuel alloted + int8_t ships_health; // 0 thru 8. -1 = major crash + int8_t ships_health_disp; // Same as above, except Galactica 0 thru 6 + uint8_t hero_counter; // Total heroic landings + uint8_t legend_counter; // Historic events counter ( Earth ) + uint8_t difficulty_level; // Based on hero_counter + uint8_t reset_counter; // Can reset hero_counter by crashing using zero fuel several consecutive scenarios + uint8_t monster_type; // Which monster is hungry? + uint8_t uninjured; // OK survivors + uint8_t injured; // Hurt survivors + uint8_t granularity; // It's a constant in the sane versions of the code + bool write_pending; // Need to update EEPROM when resigning + uint8_t pilot_rating; // Overall goodness based on history + uint8_t pilot_rating_highest; // Overall goodness based on history + uint8_t rating_overflow_count; // Number of times the pilot_rating exceeded 255. + uint16_t game_counter; // Games played since reset + uint16_t start_year; // Date of first game ( since reset ), Base is 2000. 24 + 2000 = 2024 + uint8_t start_month; // Jan = 1, Feb = 2... + uint8_t aux_display_mode; // 0 - pilot rating, 1 - highest rating, 2 - game counter, 3 - start year/month +} lander_state_t; + +void lander_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); +void lander_face_activate(movement_settings_t *settings, void *context); +bool lander_face_loop(movement_event_t event, movement_settings_t *settings, void *context); +void lander_face_resign(movement_settings_t *settings, void *context); + +#define lander_face ((const watch_face_t){ \ + lander_face_setup, \ + lander_face_activate, \ + lander_face_loop, \ + lander_face_resign, \ + NULL, \ +}) + + + +/* + +ADVANCED + +There is more for the hardcore players. + +You'll need a safe landing plus fuel. Indicators: +Good - PM +Great - 24H +Fantastic - LAP ( required for Hero status ) +Coffee maker survived - SIGNAL + +Touchdown speed shown in upper right corner. In harsh landings where the +speed exceeds 39, the displayed speed is reduced by a factor of 10. +Ex. 187 = 18 + +Scenarios: + +Hero They name this planet after you. +8 Life is very cozy. +7 +6 +5 Life is tolerable, plus some creature comforts. +4 Six months to repair ship. +3 Marooned. +2 +1 +0 Ship destroyed. Life is harsh, no shelter. Giant hamsters are cute. ** +Bug As in squished. +Crater They name this crater after you. +Gone As in vapourized. + +Low fuel warning icons: activates when 3 seconds of thrust remains. +** Yes, hamsters are very cute. However; some eating of astronauts may occur. + +Landings get progressively harder with the number of heroic landings made. + +Heroic +Landings Status + 0 Normal + 3 Hard ( first difficulty increase ) + 6 Harder ( final difficulty increase ) + 8 Ace + 11 ?????? + +Successive long presses of the alarm button before the start of a mission will +cycle through the pilot ranking screens + +Save yourself. Save the coffee maker. + +Be a hero. + +END OF ADVANCED TIPS + +*/ + + +/* + +*** START of legendary/dubious/veiled training manual *** + +What is really going on here? +The fleet is lost. You are a newbie pilot making a name for yourself. + +Objective: Find Earth. + +After reaching ?????? status, future heroic sorties will have 'some' chance in 200 +of finding Earth. + +Your chances improve by 1 chance in 200 for each subsequent Heroic Landing (HL). + +Completing HL 12 will give you 1 chance in 200, for that landing. +HL 13 will give you 2 chances in 200, for that landing. +HL 14 will give you 3 chances in 200, for that landing. +HL 20 will give you 9 chances in 200, for that landing, and so on. + +At these higher levels, your chances in 200 are displayed in the upper right corner on a heroic landing. + +For wannabe pilots only: The HL counter can be reset by crashing three consecutive +missions without touching the thrust button. + +Find Earth. Save Humanity. + +*** END of legendary/dubious/veiled training manual *** + +*/ + +#endif // LANDER_FACE_H_ + diff --git a/xyzzy_face.c b/xyzzy_face.c new file mode 100644 index 000000000..e7805b09f --- /dev/null +++ b/xyzzy_face.c @@ -0,0 +1,1204 @@ +/* + * MIT License + * + * Copyright (c) 2025 Klingon Jane + * + * 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. + */ + +// XYZZY Micro-Adventure +// SEE xyzzy_face.h FOR GAME PLAY DOCUMENTATION + +// Emulator only: need time() to seed the random number generator. +#if __EMSCRIPTEN__ +#include +#endif + +#include +#include +#include +#include "xyzzy_face.h" +#include "watch.h" +#include "watch_utility.h" + +#define XY_TICK_FREQUENCY 32 +#define XY_LONG_PRESS 19 +#define XY_SHORT_PRESS 2 + +#define XY_MODE_WAITING_TO_START 0 +#define XY_MODE_LEVEL_SELECT 1 +#define XY_MODE_PLAYING 2 +#define XY_MODE_CODE_ENTRY 3 +#define XY_MODE_TIME_DISPLAY 4 + +#define XY_VERSION_NUMBER 17 +// Joey Castillo to oversee storage allocation row +#define XY_STORAGE_ROW 5 +#define XY_STORAGE_KEY_NUMBER 11 + +char xyzzy_select_names[XY_NUM_LEVELS+1][7] = + { " tIny ", "Stndrd", "COLOSL", "SELECT" }; +char xyzzy_threat_names[XY_MAX_THREATS][7] = + { " bEAr ", "CAnYON", " trOLL", " rivEr", " drA6n" }; +char xyzzy_item_names[XY_MAX_THREATS][7] = + { "HOnEY ", " rOPE ", " 6EMS ", " bOArd", " PhASr" }; +char xyzzy_action_names[XY_MAX_THREATS][7] = + { " fEEd ", "CLImb ", " bribE", "SUrF ", "Stun " }; + +#define XY_NUM_KEYWORDS 9 +char xyzzy_keywords[XY_NUM_KEYWORDS][7] = + { " --- ", "SUn ", " MOOn ", " tree ", "EArtH ", " HILL ", "CLOUd ", " POnd ", "Star " }; +#define XY_NUM_TELEPORT 9 +char teleportStrings[XY_NUM_TELEPORT][7] = + { "- - - ", " - -", " - ", " - ", "- - -", " -- - ", "- -- -", " -- --", "--- -" }; + +// Final items/countries rewards not relavent to game play. +#define XY_NUM_COUNTRIES 6 +char xyzzy_countries[XY_NUM_COUNTRIES][7] = + { " EntEr", "IdaHO ", "VULCAN", " MarS ", " ArUbA", " FIJI " }; +#define XY_NUM_REWARDS 7 +char xyzzy_rewards[XY_NUM_REWARDS][7] = + { "HUmAn ", "StICk ", " PEbbL", "rAnCH ", "CASTLE", " YaCHT", "CHatEU" }; + +// Some arbitrary branch value for main path +#define XY_MAIN_PATH 37 + +// -------------- +// Custom methods +// -------------- + + +static int gen_random_int (int32_t lower, int32_t upper) { + int32_t range; + int32_t retVal; + range = upper - lower + 1; + if ( range < 2 ) range = 2; + // Emulator: use rand. Hardware: use arc4random. + #if __EMSCRIPTEN__ + retVal = rand() % range; + #else + retVal = arc4random_uniform(range); + #endif + retVal += lower; + return retVal; +} + +static void xyzzy_display_location (xyzzy_state_t *state ) { + char buf[11]; + uint8_t i; + if ( state->mode == XY_MODE_PLAYING ) { + sprintf ( buf, " %2d ", state->loc ); + watch_display_string ( buf, 4 ); + if ( state->loc == 0 ) watch_display_string ( "EN", 8 ); + if ( state->onMainPath ) { + if ( state->loc == state->locXyzzy ) watch_display_string ( "2Y", 8 ); + for ( i = 0; i < XY_MAX_BRANCHES; i++ ) { + if ( state->loc == state->locBranchPoint[i] ) watch_display_string ( "br", 8 ); + } + } + } +} + + +static void xyzzy_display_code_entry (xyzzy_state_t *state ) { + char buf[11]; + sprintf ( buf, "%2d%s", state->userCodeIndex+1, xyzzy_keywords[state->userCodes[state->userCodeIndex]] ); + watch_display_string ( buf, 2 ); +} + +static void write_to_xyzzy_EEPROM(xyzzy_state_t *state) { + uint8_t base = 13; // Array writes being here + // Do not exceed 64 bytes per write + uint8_t size; + uint8_t output_array [ 64 ]; // More than large enough for any one page + uint8_t offset = 0; + uint8_t i, j; + uint8_t index; + uint32_t bigNum; // Working copy of big number we are parsing into pieces + uint8_t piece; // The current 8-bit piece + + // 1st page + size = base + XY_MAX_PATH_LENGTH + ( XY_MAX_CODES * 3 ) + ( XY_MAX_BRANCHES * 2 ); // (55 bytes) + offset = 0; + output_array [ 0 ] = XY_STORAGE_KEY_NUMBER; + output_array [ 1 ] = state->mode; + output_array [ 2 ] = state->levelSelected; + output_array [ 3 ] = state->loc; + output_array [ 4 ] = state->locMainPathEnd; + output_array [ 5 ] = state->numCodes; + output_array [ 6 ] = state->totalCodesSeen; + output_array [ 7 ] = state->locXyzzy; + output_array [ 8 ] = state->xyzzyKnown; + output_array [ 9 ] = state->brIndex; + output_array [ 10 ] = state->onMainPath; + output_array [ 11 ] = state->secretDestination; + output_array [ 12 ] = state->secretItem; + index = base; // First array output start location + for ( i = 0; i < XY_MAX_PATH_LENGTH; i++ ) output_array [ index + i ] = state->mainPath[i]; + index += XY_MAX_PATH_LENGTH; // Advance to next array output start location + for ( i = 0; i < XY_MAX_CODES; i++ ) output_array [ index + i ] = state->locCodes[i]; + index += XY_MAX_CODES; // Advance to next array output start location + for ( i = 0; i < XY_MAX_CODES; i++ ) output_array [ index + i ] = state->codeValues[i]; + index += XY_MAX_CODES; // Advance to next array output start location + for ( i = 0; i < XY_MAX_CODES; i++ ) output_array [ index + i ] = state->codesSeen[i]; + index += XY_MAX_CODES; // Advance to next array output start location + for ( i = 0; i < XY_MAX_BRANCHES; i++ ) output_array [ index + i ] = state->locBranchPoint[i]; + index += XY_MAX_BRANCHES; // Advance to next array output start location + for ( i = 0; i < XY_MAX_BRANCHES; i++ ) output_array [ index + i ] = state->locBranchEnd[i]; + index += XY_MAX_BRANCHES; // Advance to next array output start location + + // Erase full row before first write only + watch_storage_erase ( XY_STORAGE_ROW ); + watch_storage_sync ( ); + watch_storage_write ( XY_STORAGE_ROW, offset, output_array, size ); + watch_storage_sync ( ); + + + // 2nd page + size = ( XY_MAX_THREATS * 6 ) + ( XY_MAX_BRANCHES * XY_MAX_BRANCH_LENGTH ); // (54 bytes) + offset = 64; + index = 0; + for ( i = 0; i < XY_MAX_THREATS; i++ ) output_array [ index + i ] = state->locThreat[i]; + index += XY_MAX_THREATS; // Advance to next array output start location + for ( i = 0; i < XY_MAX_THREATS; i++ ) output_array [ index + i ] = state->brThreat[i]; + index += XY_MAX_THREATS; // Advance to next array output start location + for ( i = 0; i < XY_MAX_THREATS; i++ ) output_array [ index + i ] = state->inventory[i]; + index += XY_MAX_THREATS; // Advance to next array output start location + for ( i = 0; i < XY_MAX_THREATS; i++ ) output_array [ index + i ] = state->locItem[i]; + index += XY_MAX_THREATS; // Advance to next array output start location + for ( i = 0; i < XY_MAX_THREATS; i++ ) output_array [ index + i ] = state->brItem[i]; + index += XY_MAX_THREATS; // Advance to next array output start location + for ( i = 0; i < XY_MAX_THREATS; i++ ) output_array [ index + i ] = state->ri[i]; + index += XY_MAX_THREATS; // Advance to next array output start location + for ( j = 0; j < XY_MAX_BRANCHES; j++ ) { + for ( i = 0; i < XY_MAX_BRANCH_LENGTH; i++ ) output_array [ index + i ] = state->branch[j][i]; + index += XY_MAX_BRANCH_LENGTH; // Advance to next array output start location + } + watch_storage_write ( XY_STORAGE_ROW, offset, output_array, size ); + watch_storage_sync ( ); + + // 3rd page + size = ( 4 * 2 ) + ( 4 * XY_NUM_LEVELS ); // (20 bytes) + offset = 128; + index = 0; + // Parse the start time into four 8-bit integers + bigNum = state->startTime; + for ( i = 0; i < 4; i++ ) { + piece = bigNum % 256; + output_array [ index + 3 - i ] = piece; + bigNum = bigNum / 256; + } + index += 4; + // Parse the lastSolveTime into four 8-bit integers + bigNum = state->lastSolveTime; + for ( i = 0; i < 4; i++ ) { + piece = bigNum % 256; + output_array [ index + 3 - i ] = piece; + bigNum = bigNum / 256; + } + index += 4; + // Also all the bestTime array + for ( j = 0; j < XY_NUM_LEVELS; j++ ) { + bigNum = state->bestTime[j]; + for ( i = 0; i < 4; i++ ) { + piece = bigNum % 256; + output_array [ index + 3 - i ] = piece; + bigNum = bigNum / 256; + } + index += 4; + } + watch_storage_write ( XY_STORAGE_ROW, offset, output_array, size ); + watch_storage_sync ( ); +} + + +static void read_from_xyzzy_EEPROM(xyzzy_state_t *state) { + uint8_t base = 13; // Array reads being here + uint8_t size; + uint8_t stored_data [ 64 ]; // More than enough for largest read + uint8_t offset = 0; + uint8_t i, j; + uint8_t index; + uint32_t bigNum; + + // Read 1st page + size = base + XY_MAX_PATH_LENGTH + ( XY_MAX_CODES * 3 ) + ( XY_MAX_BRANCHES * 2 ); // (55 bytes) + offset = 0; + watch_storage_read (XY_STORAGE_ROW, offset, stored_data, size); + // See if data was ever written to EEPROM storage + if ( stored_data[0] == XY_STORAGE_KEY_NUMBER ) + { + state->mode = stored_data [ 1 ]; + state->loc = stored_data [ 2 ]; + state->levelSelected = stored_data [ 3 ]; + state->locMainPathEnd = stored_data [ 4 ]; + state->numCodes = stored_data [ 5 ]; + state->totalCodesSeen = stored_data [ 6 ]; + state->locXyzzy = stored_data [ 7 ]; + state->xyzzyKnown = stored_data [ 8 ]; + state->brIndex = stored_data [ 9 ]; + state->onMainPath = stored_data [ 10 ]; + state->secretDestination = stored_data [ 11 ]; + state->secretItem = stored_data [ 12 ]; + index = base; // First array output start location + for ( i = 0; i < XY_MAX_PATH_LENGTH; i++ ) state->mainPath[i] = stored_data [ index + i ]; + index += XY_MAX_PATH_LENGTH; // Advance to next array output start location + for ( i = 0; i < XY_MAX_CODES; i++ ) state->locCodes[i] = stored_data [ index + i ]; + index += XY_MAX_CODES; // Advance to next array output start location + for ( i = 0; i < XY_MAX_CODES; i++ ) state->codeValues[i] = stored_data [ index + i ]; + index += XY_MAX_CODES; // Advance to next array output start location + for ( i = 0; i < XY_MAX_CODES; i++ ) state->codesSeen[i] = stored_data [ index + i ]; + index += XY_MAX_CODES; // Advance to next array output start location + for ( i = 0; i < XY_MAX_BRANCHES; i++ ) state->locBranchPoint[i] = stored_data [ index + i ]; + index += XY_MAX_BRANCHES; // Advance to next array output start location + for ( i = 0; i < XY_MAX_BRANCHES; i++ ) state->locBranchEnd[i] = stored_data [ index + i ]; + index += XY_MAX_BRANCHES; // Advance to next array output start location + + // Read 2nd page + size = ( XY_MAX_THREATS * 6 ) + ( XY_MAX_BRANCHES * XY_MAX_BRANCH_LENGTH ); // (54 bytes) + offset = 64; + index = 0; + watch_storage_read (XY_STORAGE_ROW, offset, stored_data, size); + for ( i = 0; i < XY_MAX_THREATS; i++ ) state->locThreat[i] = stored_data [ index + i ]; + index += XY_MAX_THREATS; // Advance to next array output start location + for ( i = 0; i < XY_MAX_THREATS; i++ ) state->brThreat[i] = stored_data [ index + i ]; + index += XY_MAX_THREATS; // Advance to next array output start location + for ( i = 0; i < XY_MAX_THREATS; i++ ) state->inventory[i] = stored_data [ index + i ]; + index += XY_MAX_THREATS; // Advance to next array output start location + for ( i = 0; i < XY_MAX_THREATS; i++ ) state->locItem[i] = stored_data [ index + i ]; + index += XY_MAX_THREATS; // Advance to next array output start location + for ( i = 0; i < XY_MAX_THREATS; i++ ) state->brItem[i] = stored_data [ index + i ]; + index += XY_MAX_THREATS; // Advance to next array output start location + for ( i = 0; i < XY_MAX_THREATS; i++ ) state->ri[i] = stored_data [ index + i ]; + index += XY_MAX_THREATS; // Advance to next array output start location + for ( j = 0; j < XY_MAX_BRANCHES; j++ ) { + for ( i = 0; i < XY_MAX_BRANCH_LENGTH; i++ ) state->branch[j][i] = stored_data [ index + i ]; + index += XY_MAX_BRANCH_LENGTH; // Advance to next array output start location + } + + // Read 3rd page + size = ( 4 * 2 ) + ( 4 * XY_NUM_LEVELS ); // (20 bytes) + offset = 128; + index = 0; + watch_storage_read (XY_STORAGE_ROW, offset, stored_data, size); + // Read 32-bit startTime + bigNum = 0; + for ( i = 0; i < 4; i++ ) { + bigNum *= 256; + bigNum += stored_data [ index + i ]; + } + state->startTime = bigNum; + index += 4; // Advance to next array output start location + // Read 32-bit lastSolveTime + bigNum = 0; + for ( i = 0; i < 4; i++ ) { + bigNum *= 256; + bigNum += stored_data [ index + i ]; + } + state->lastSolveTime = bigNum; + index += 4; // Advance to next array output start location + // Also the bestTime array + for ( j = 0; j < XY_NUM_LEVELS; j++ ) { + bigNum = 0; + for ( i = 0; i < 4; i++ ) { + bigNum *= 256; + bigNum += stored_data [ index + i ]; + } + state->bestTime[j] = bigNum; + index += 4; // Advance to next array output start location + } + } + else + { + state->mode = XY_MODE_WAITING_TO_START; + state->secretDestination = 0; + state->secretItem = 0; + for ( i = 0; i < XY_NUM_LEVELS; i++ ) { + state->bestTime[i] = 0; + } + } + state->writePending = false; +} + + +// Generate a new cave +static void xyzzy_gen_new_cave(xyzzy_state_t *state) { + uint8_t i; + bool done; + bool valOK; + uint8_t newLoc; + uint8_t newCode; + uint8_t j; + uint8_t temp; + uint8_t numBranches; + uint8_t numThreats; // Based on user selected difficulty + uint8_t maxBranchPoint; + + state->mode = XY_MODE_PLAYING; + state->loc = 0; // Entrance + state->totalCodesSeen = 0; + state->xyzzyKnown = 0; + state->onMainPath = true; + state->brIndex = 0; // Not meaningful yet + state->shortcutCounter = 0; + state->tickCounter = 0; + state->writePending = true; + state->secretDestination = gen_random_int ( 1, XY_NUM_COUNTRIES - 1 ); + state->secretItem = gen_random_int ( 1, XY_NUM_REWARDS - 1 ); + state->resetCounter = 0; + +// INITIALIZE CAVE + + // Remember when this cave was generated - I.E. right now. + state->startTime = watch_utility_date_time_to_unix_time ( watch_rtc_get_date_time(), 0 ); + // Generate all paths at full length + for ( i = 0; i < XY_MAX_PATH_LENGTH; i++ ) state->mainPath[i] = gen_random_int(0,1); // Main path + for ( j = 0; j < XY_MAX_BRANCHES; j++ ) { + for ( i = 0; i < XY_MAX_BRANCH_LENGTH; i++ ) state->branch[j][i] = gen_random_int(0,1); // Branches + } + // Initialize other arrays + for ( i = 0; i < XY_MAX_CODES; i++ ) { + state->locCodes[i] = 199; // Init as not applicable + state->codeValues[i] = 199; + state->codesSeen[i] = 0; + } + for ( i = 0; i < XY_MAX_CODES; i++ ) { // Unique code values for final room + done = false; + do { + valOK = true; + newCode = gen_random_int(1,XY_NUM_KEYWORDS-1); + for ( j = 0; j < XY_MAX_CODES; j++ ) if ( newCode == state->codeValues[j] ) valOK = false; + if ( valOK ) done = true; + } while ( !done ); + state->codeValues[i] = newCode; + } + for ( i = 0; i < XY_MAX_THREATS; i++ ) { + state->inventory[i] = 0; // Start out with nothing + state->locItem[i] = 199; // Init as not applicable + state->brItem[i] = 0; // Init only, will reassign later if used. + state->locThreat[i] = 199; // Init as not applicable + state->brThreat[i] = 0; // Init only, will reassign later if used. + } + for ( i = 0; i < XY_MAX_BRANCHES; i++ ) { + state->locBranchPoint[i] = 199; // Init as not applicable + state->locBranchEnd[i] = 201; // Init as anot applicable + } + for ( i = 0; i < XY_MAX_THREATS; i++ ) state->ri[i] = 199; // Randomized index 0 to n-1, but scrambled + for ( i = 0; i < XY_MAX_THREATS; i++ ) { + done = false; + do { + valOK = true; + newCode = gen_random_int ( 0, XY_MAX_THREATS - 1 ); + for ( j = 0; j < XY_MAX_THREATS; j++ ) if ( newCode == state->ri[j] ) valOK = false; + if ( valOK ) done = true; + } while ( !done ); + state->ri[i] = newCode; + } + + if ( state->levelSelected == 0 ) { // Easy/demo level + state->locMainPathEnd = 8; + state->numCodes = 2; + numBranches = 0; + numThreats = 0; + } else if ( state->levelSelected == 1 ) { // Moderate game + state->locMainPathEnd = 21; + state->numCodes = 4; + numBranches = 1; + numThreats = 1; + } else { // The real game + state->locMainPathEnd = gen_random_int ( XY_MAX_PATH_LENGTH-2, XY_MAX_PATH_LENGTH ); + state->numCodes = 4; + numBranches = 3; + numThreats = 3; + } + if ( numThreats > numBranches ) numThreats = numBranches; // A limitation, for now.... + // ... could allow numThreats = numBranches + 1 with a code change to place last item + // on main path + + // Assign locXyzzy + // Place primary threat near final room + // Randomly assign all branch point locations and branch ends + // LOOP - assign all items and threats + // Assign all locCodes to be at unique locations, and not at any branch path start. + + // Assign locXyzzy + state->locXyzzy = 199; // Not used if very short cave + if ( state->locMainPathEnd > 15 ) state->locXyzzy = ( state->locMainPathEnd / 2 ) + 1; + // Place primary threat near final room + state->locThreat[0] = 199; // No primary threat + state->brThreat[0] = XY_MAIN_PATH; + if ( numThreats > 0 ) state->locThreat[0] = gen_random_int ( state->locMainPathEnd-4, state->locMainPathEnd-1 ); + + // Randomly assign all branch point locations and branch ends + // Add in any branches next, all unique and not at xyzzy + maxBranchPoint = min ( state->locThreat[0], state->locMainPathEnd ) - 1; + for ( i = 0; i < numBranches; i++ ) { + done = false; + while ( ! done ) { + valOK = true; + newLoc = gen_random_int ( 2, maxBranchPoint ); + if ( newLoc == state->locXyzzy ) valOK = false; + for ( j = 0; j < XY_MAX_BRANCHES; j++ ) { + if ( newLoc == state->locBranchPoint[j] ) valOK = false; + } + if ( valOK ) done = true; + } + state->locBranchPoint[i] = newLoc; + state->locBranchEnd[i] = state->locBranchPoint[i] + gen_random_int ( XY_MAX_BRANCH_LENGTH -3, XY_MAX_BRANCH_LENGTH ); + } + + // LOOP - assign all items and threats + for ( i = 0; i < numThreats; i++ ) { + state->brItem[i] = i; // Simply assign one item per branch + // Usually place item right at end of branch+1 ( discover item AND dead end ) + if ( gen_random_int ( 0, 2 ) != 1 ) { + state->locItem[i] = state->locBranchEnd[i] + 1; // Friendly + } else { + state->locItem[i] = state->locBranchEnd[i] - gen_random_int ( 0, 2 ); // Half friendly + state->locItem[i] = max ( state->locBranchPoint[i] + 2, state->locItem[i] ); // Safety + } + // Place a threat at the front of this branch, if applicable + if ( ( i + 1 ) < numThreats ) { + state->brThreat[i+1] = i; + state->locThreat[i+1] = gen_random_int ( state->locBranchPoint[i]+1, state->locItem[i]-1 ); + state->locThreat[i+1] = max ( state->locBranchPoint[i]+1, state->locThreat[i+1] ); // Safety + } + } + // Assign all locCodes to be at unique locations, and not at any branch path start. + for ( i = 0; i < state->numCodes; i++ ) { + done = false; + while ( ! done ) { + valOK = true; + newLoc = gen_random_int ( 2, state->locMainPathEnd - 2 ); + for ( j = 0; j < XY_MAX_BRANCHES; j++ ) { + if ( newLoc == state->locBranchPoint[j] ) valOK = false; + } + for ( j = 0; j < state->numCodes; j++ ) { + if ( newLoc == state->locCodes[j] ) valOK = false; + } + if ( valOK ) done = true; + } + state->locCodes[i] = newLoc; + } + // And locCodes must be in ascending order when done + for ( i = 0; i < state->numCodes - 1; i++ ) + { + for ( j = 0; j < state->numCodes - i - 1; j++ ) { + if ( state->locCodes[j] > state->locCodes[j+1] ) { + temp = state->locCodes[j]; + state->locCodes[j] = state->locCodes[j+1]; + state->locCodes[j+1] = temp; + } + } + } + // Small easement for tiny cave + if ( state->levelSelected == 0 ) { + state->locCodes[0] = 3; + state->locCodes[1] = 6; + } +} + + +// --------------------------- +// Standard watch face methods +// --------------------------- +void xyzzy_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(xyzzy_state_t)); + memset(*context_ptr, 0, sizeof(xyzzy_state_t)); + } + xyzzy_state_t *state = (xyzzy_state_t *)*context_ptr; + // Emulator only: Seed random number generator + #if __EMSCRIPTEN__ + srand(time(NULL)); + #endif + // Read in cave layout, secretDestination etc. from EEPROM. + read_from_xyzzy_EEPROM(state); + state->shortcutCounter = 0; +} + +void xyzzy_face_activate(movement_settings_t *settings, void *context) { + (void) settings; + xyzzy_state_t *state = (xyzzy_state_t *)context; + char buf[16]; + uint8_t i; + movement_request_tick_frequency(XY_TICK_FREQUENCY); + // No fancy intro + watch_clear_all_indicators ( ); + watch_display_string("CA ", 0); // Cave + // What to display depends on what mode we re-enter the game in + if ( state->mode == XY_MODE_WAITING_TO_START ) { + sprintf ( buf, "%s", xyzzy_countries[state->secretDestination] ); + watch_display_string ( buf, 4 ); + } else if ( state->mode == XY_MODE_LEVEL_SELECT ) { + state->levelSelected = XY_NUM_LEVELS; // Shows 'Select' + sprintf ( buf, "%s", xyzzy_select_names[state->levelSelected] ); + watch_display_string ( buf, 4 ); + } else { + xyzzy_display_location ( state ); + for ( i = 0; i < XY_MAX_THREATS; i++ ) if (state->inventory[i]) watch_set_indicator (state->ri[i]); + } + state->tickCounter = 0; + state->buttonTimer = 0; + state->bothPressedTimer = 0; + state->ignoreNextRelease = false; +} + +bool xyzzy_face_loop(movement_event_t event, movement_settings_t *settings, void *context) { + (void) settings; + xyzzy_state_t *state = (xyzzy_state_t *)context; + char buf [ 20 ]; // [11] is more correct and works; compiler too helpful. + bool userPickedLeft = false; // During gameplay + bool userPickedRight = false; // During gameplay + bool displayLocation = false; + bool correctPick; // Did user make 'correct' choice - advance further into cave + bool branchMatch = false; // There is a path branch at this location + bool codeMatch = false; // There is a keyword at this location + bool valOK; + uint8_t codeMatchIndex; // Which keyword is at this location? + uint16_t i; + uint8_t j; + uint8_t branchInd; // Index when on a branch - starts at 0 + uint8_t oldVal; + uint32_t bigNum; + bool jumpToXyzzy; // Jump to xyzzy instead of entrance + int8_t itemIndex; // Index if item was found, or -1 + int8_t threatIndex; // Index if threat encountered, or -1 + // Debouncing of buttons + bool alarmPress = false; // Pressed right now + bool lightPress = false; // Pressed right now + bool alarmUpShort = false; // Short release + bool lightUpShort = false; // Short release + bool alarmDownLong = false; // Long press + bool lightDownLong = false; // Long press + uint32_t nowSeconds; // Unix time is in seconds + uint32_t deltaSeconds; // Total play time + uint8_t hours, minutes, seconds; + uint16_t days; + uint8_t codePending[XY_MAX_CODES]; // Check user code entries + bool codeEntryCompleted = false; // Did user just finish entering a code? + + switch (event.event_type) { + case EVENT_TICK: + // ticks are used for input debounce or the alternating the display updates + // if the game is over, or the best times display + state->tickCounter++; + if ( state->mode == XY_MODE_WAITING_TO_START ) { + // While waiting, we display the high score list and other items until + // the user starts the next game. + if ( state->tickCounter == 1 ) { + watch_display_string ( " tHE ", 4 ); + } else if ( state->tickCounter == (int) (XY_TICK_FREQUENCY * 1.6) ) { // Blank space + watch_display_string ( " ", 4 ); + } else if ( state->tickCounter == XY_TICK_FREQUENCY * 2 ) { + watch_display_string ( "CAVE ", 4 ); + } else if ( state->tickCounter == (int) (XY_TICK_FREQUENCY * 5.9) ) { // Blank space + watch_display_string ( " ", 4 ); + } else if ( state->tickCounter == XY_TICK_FREQUENCY * 7 ) { // Show 'Enter ' or reward destination + sprintf ( buf, "%s", xyzzy_countries[state->secretDestination] ); + watch_display_string ( buf, 4 ); + } else if ( state->tickCounter == (int) (XY_TICK_FREQUENCY * 9.6) ) { // Blank space + watch_display_string ( " ", 4 ); + } else if ( state->tickCounter == XY_TICK_FREQUENCY * 10 ) { // Show 'Human ' or reward item + sprintf ( buf, "%s", xyzzy_rewards[state->secretItem] ); + watch_display_string ( buf, 4 ); + } else if ( state->tickCounter == (int) (XY_TICK_FREQUENCY * 12.6) ) { // Blank space + watch_display_string ( " ", 4 ); + } else if ( state->tickCounter >= XY_TICK_FREQUENCY * 14 ) state->tickCounter = 0; + } else if ( state->mode == XY_MODE_TIME_DISPLAY ) { + if ( state->tickCounter == 1 ) { // Display the currently selected request + if ( state->tdSubLevel == 0 ) { // Beginning of a new level, ex: tiny level + // Will need to do the math + if ( state->tdLevel < XY_NUM_LEVELS ) { + bigNum = state->bestTime[state->tdLevel]; + sprintf ( buf, "%6s", xyzzy_select_names[state->tdLevel] ); + } else { + bigNum = state->lastSolveTime; + sprintf ( buf, " LaSt " ); + } + if ( bigNum == 0 ) { // Avoid normal handling if this cave never completed + watch_display_string ( buf, 4 ); + delay_ms ( 1000 ); + sprintf ( buf, "---- " ); + state->tdSubLevel = 4; + } + state->seconds = bigNum % 60; + bigNum = bigNum / 60; + state->minutes = bigNum % 60; + bigNum = bigNum / 60; + state->hours = bigNum % 24; + bigNum = bigNum / 24; + state->days = bigNum; + } else if ( state->tdSubLevel == 1 ) { // Days + sprintf ( buf, "%4ddY", state->days ); + } else if ( state->tdSubLevel == 2 ) { // Hours + sprintf ( buf, "%4dHr", state->hours ); + } else if ( state->tdSubLevel == 3 ) { // Minutes + sprintf ( buf, "%4dMI", state->minutes ); + } else if ( state->tdSubLevel == 4 ) { // Seconds + sprintf ( buf, "%4dSE", state->seconds ); + } else { // Blank + sprintf ( buf, " " ); + } + watch_display_string ( buf, 4 ); + } else if ( ( state->tickCounter >= XY_TICK_FREQUENCY * 1.0 ) || + ( ( state->tdSubLevel >= 5 ) && ( state->tickCounter >= XY_TICK_FREQUENCY * 0.3 ) ) ) { + // Time to advance to next item + state->tdSubLevel++; + // But skip days if they are zero + if ( ( state->tdSubLevel == 1 ) && ( state->days == 0 ) ) state->tdSubLevel = 2; + // Also skip hours if both days and hours are zero + if ( ( state->tdSubLevel == 2 ) && ( state->days == 0 ) && ( state->hours == 0 ) ) { + state->tdSubLevel = 3; + } + // Are we done displaying this level? + if ( state->tdSubLevel > 5 ) { + state->tdSubLevel = 0; // Advance to next level display + state->tdLevel++; + if ( state->tdLevel > XY_NUM_LEVELS ) { + state->mode = XY_MODE_WAITING_TO_START; // All done time display + sprintf ( buf, " vEr%2d", XY_VERSION_NUMBER ); + watch_display_string ( buf, 4 ); + delay_ms (2500); + } + } + state->tickCounter = 0; // Reset counter + } + } + + // We'll monitor both the light and alarm buttons + alarmPress = watch_get_pin_level(BTN_ALARM); + lightPress = watch_get_pin_level(BTN_LIGHT); + // If neither button pressed, we check for a button just being release + if ( !alarmPress && !lightPress ) { + // Possibly a button was just released + if ( state->buttonTimer > 0 ) { + if ( !state->ignoreNextRelease ) { + // Which button and for how long? + if ( state->alarmWasPressed ) { + if ( state->buttonTimer >= XY_SHORT_PRESS ) alarmUpShort = true; + // else nothing - it was noise + } + if ( state->lightWasPressed ) { + if ( state->buttonTimer >= XY_SHORT_PRESS ) lightUpShort = true; + // else nothing - it was noise + } + } else { // We just ignored a release after a long press + state->ignoreNextRelease = false; + } + state->buttonTimer = 0; + } + state->alarmWasPressed = false; + state->lightWasPressed = false; + state->bothPressedTimer = 0; + } + else if ( alarmPress && !lightPress ) { + state->alarmWasPressed = true; + state->buttonTimer++; + state->bothPressedTimer = 0; + if ( state->buttonTimer == XY_LONG_PRESS ) { + alarmDownLong = true; + state->ignoreNextRelease = true; + } + } + else if ( lightPress && !alarmPress) { + state->lightWasPressed = true; + state->buttonTimer++; + state->bothPressedTimer = 0; + if ( state->buttonTimer == XY_LONG_PRESS ) { + lightDownLong = true; + state->ignoreNextRelease = true; + } + } else { // Both pressed + state->ignoreNextRelease = true; + state->bothPressedTimer++; + if ( state->bothPressedTimer == XY_LONG_PRESS ) { + state->tickCounter = 0; + watch_clear_all_indicators(); + if ( state->mode != XY_MODE_WAITING_TO_START ) { + state->resetCounter = 0; + watch_display_string ( " rESEt ", 2 ); + delay_ms ( 700 ); + } else { + state->resetCounter++; + state->bothPressedTimer = XY_LONG_PRESS / 2; + if ( state->resetCounter < 7 ) { + sprintf ( buf, " rESET%d", state->resetCounter ); + watch_display_string ( buf, 2 ); + delay_ms ( 500 ); + state->tickCounter=2; // Avoid flashing intro while full reset + } else { + for ( i = 0; i < 5; i++ ) { + watch_display_string ( " ", 4 ); + delay_ms ( 75 ); + watch_display_string ( "rESET ", 4 ); + delay_ms ( 150 ); + } + watch_display_string ( "CLEAr ", 4 ); + delay_ms ( 1000 ); + state->resetCounter = 0; + for ( i = 0; i < XY_NUM_LEVELS; i++ ) state->bestTime[i] = 0; + state->lastSolveTime = 0; + } + } + state->mode = XY_MODE_WAITING_TO_START; + state->secretDestination = 0; + state->secretItem = 0; + } + } + break; // End case EVENT_TICK + case EVENT_LIGHT_LONG_UP: + case EVENT_LIGHT_BUTTON_UP: + case EVENT_ALARM_LONG_UP: + case EVENT_ALARM_BUTTON_UP: + case EVENT_LIGHT_BUTTON_DOWN: // Disable the backlight + case EVENT_ALARM_BUTTON_DOWN: +// state->tickCounter = 0; // Why? + break; + case EVENT_LOW_ENERGY_UPDATE: + if (state->writePending) { + write_to_xyzzy_EEPROM(state); + watch_storage_sync ( ); + state->writePending=false; + } + watch_display_string("SLEEP ", 4); + break; + default: + movement_default_loop_handler(event, settings); + break; + } // END event switch statement + + if ( lightDownLong ) { + if ( ( state->mode == XY_MODE_WAITING_TO_START ) || + ( state->mode == XY_MODE_TIME_DISPLAY ) ) { + // Advance to level select menu + state->mode = XY_MODE_LEVEL_SELECT; + state->levelSelected = XY_NUM_LEVELS; // Shows "SELECT" + sprintf ( buf, "%s", xyzzy_select_names[state->levelSelected] ); + watch_display_string ( buf, 4 ); + } else if ( state->mode == XY_MODE_PLAYING ) { + state->onMainPath = 1; + state->xyzzyCounter = 0; + displayLocation = true; + if ( ( state->loc != state->locXyzzy ) && state->xyzzyKnown ) { // Jump to xyzzy location + state->loc = state->locXyzzy; + } else { // Jump to entrance + state->loc = 0; + } + } else if ( state->mode == XY_MODE_CODE_ENTRY ) { // User is done entering code. + codeEntryCompleted = true; + } + } + + if ( lightUpShort ) { + if ( state->mode == XY_MODE_LEVEL_SELECT ) { + // User made a level selection + if ( state->levelSelected != XY_NUM_LEVELS ) { // Ignore when menu says 'Select' + // NEW CAVE - NEW GAME + xyzzy_gen_new_cave ( state ); + watch_display_string ( " NEW ", 4 ); + delay_ms ( 1000 ); + watch_display_string ( "CAvE ", 4 ); + delay_ms ( 1000 ); + watch_display_string ( " --- ", 4 ); + delay_ms (400); + // The game is starting + state->mode = XY_MODE_PLAYING; + displayLocation = true; + } + } else if ( state->mode == XY_MODE_PLAYING ) { + userPickedLeft = true; + } else if ( state->mode == XY_MODE_CODE_ENTRY ) { // Switch to next code value + state->userCodeIndex++; + if ( state->userCodeIndex >= state->totalCodesSeen ) state->userCodeIndex = 0; + xyzzy_display_code_entry ( state ); + } + } + + if ( alarmDownLong ) { + if ( state->mode == XY_MODE_PLAYING ) { + state->onMainPath = 1; + state->xyzzyCounter = 0; + displayLocation = true; + if ( state->loc != 0 ) { + // Jump to main entrance + state->loc = 0; + } else if ( state->xyzzyKnown ) { + // Already at entrance, jump to XYZZY + state->loc = state->locXyzzy; + } + } else if ( state->mode == XY_MODE_WAITING_TO_START ) { + // Start time display mode + state->mode = XY_MODE_TIME_DISPLAY; + state->tdLevel = 0; + state->tdSubLevel = 0; + state->tickCounter = 0; + } + } + + if ( alarmUpShort ) { + if ( state->mode == XY_MODE_PLAYING ) { + userPickedRight = true; + } else if ( state->mode == XY_MODE_LEVEL_SELECT ) { + state->levelSelected++; + if ( state->levelSelected >= XY_NUM_LEVELS ) state->levelSelected = 0; + sprintf ( buf, "%s", xyzzy_select_names[state->levelSelected] ); + watch_display_string ( buf, 4 ); + } else if ( state->mode == XY_MODE_CODE_ENTRY ) { // Increase current code value + state->userCodes[state->userCodeIndex]++; + if ( state->userCodes[state->userCodeIndex] >= XY_NUM_KEYWORDS ) state->userCodes[state->userCodeIndex] = 1; + xyzzy_display_code_entry ( state ); + } + } + + if ( userPickedLeft || userPickedRight ) { + state->writePending = true; + displayLocation = true; + // Did user pick 'correctly'? + correctPick = false; + if ( state->onMainPath ) { + if ( ( userPickedLeft && ( state->mainPath[state->loc] == 0 ) ) || + ( userPickedRight && ( state->mainPath[state->loc] == 1 ) ) ) { + correctPick = true; + } + } else { // On branches use relative index + branchInd = state->loc - state->locBranchPoint[state->brIndex] - 1; + if ( branchInd >= XY_MAX_BRANCH_LENGTH ) branchInd = 0; // Unnecessary safety ( I hope ) + if ( ( userPickedLeft && ( state->branch[state->brIndex][branchInd] == 0 ) ) || + ( userPickedRight && ( state->branch[state->brIndex][branchInd] == 1 ) ) ) { + correctPick = true; + } + } + + // Advance if correct pick + if ( correctPick ) { + state->loc++; + if ( ( state->loc >= state->locXyzzy ) && ( state->onMainPath ) ) state->xyzzyKnown = 1; + + // Check for: + // final room + // end of current branch ( dead end ) + // any threat + // any item + + bool bFinalRoom = false; + bool bDeadEnd = false; + bool bThreat = false; + bool bItem = false; + + if ( state->onMainPath ) { + if ( state->loc >= state->locMainPathEnd ) bFinalRoom = true; + // No need to check deadEnd + threatIndex = -1; + for ( i = 0; i < XY_MAX_THREATS; i++ ) { + if ( state->loc == state->locThreat[i] ) { + if ( state->brThreat[i] == XY_MAIN_PATH ) { + bThreat = true; + threatIndex = i; + } + } + } + // No need to check item on main branch ( yet ) + } else { + // No need to check finalRoom + // Check for dead end + if ( state->loc > ( state->locBranchEnd[state->brIndex] ) ) bDeadEnd = true; + threatIndex = -1; + for ( i = 0; i < XY_MAX_THREATS; i++ ) { + if ( ( state->loc == state->locThreat[i] ) && ( state->brIndex == state->brThreat[i] ) ) { + bThreat = true; + threatIndex = i; + } + } + itemIndex = -1; + for ( i = 0; i < XY_MAX_THREATS; i++ ) { // Item match? + if ( ( state->brIndex == state->brItem[i] ) && ( state->loc == state->locItem[i] ) ) { + // Found an item!! + itemIndex = i; + bItem = true; + } + } + } + if ( bFinalRoom ) { // Entering final room + watch_display_string ( " FINAL", 4 ); + delay_ms ( 1500 ); + watch_display_string ( " rooM ", 4 ); + delay_ms ( 1500 ); + if ( state->totalCodesSeen > 0 ) + { + state->mode = XY_MODE_CODE_ENTRY; + for ( i = 0; i < XY_MAX_CODES; i++ ) state->userCodes[i] = 0; + state->userCodeIndex = 0; + displayLocation = false; + xyzzy_display_code_entry ( state ); + } else { + codeEntryCompleted = true; + } + } + if ( bItem ) { + // Player discovered item! + state->inventory[itemIndex] = 1; + state->locItem[itemIndex] = 199; // Won't find a second time. + sprintf ( buf, "%s", xyzzy_item_names[state->ri[itemIndex]] ); + watch_display_string ( buf, 4 ); + delay_ms ( 800 ); + for ( i = 0; i < 6; i++ ) { + watch_clear_indicator ( state->ri[itemIndex] ); + delay_ms ( 100 ); + watch_set_indicator ( state->ri[itemIndex] ); + delay_ms ( 100 ); + } + delay_ms ( 800 ); + } + if ( bDeadEnd ) { + watch_display_string ( " dEAd ", 4 ); + delay_ms ( 1000 ); + watch_display_string ( " End ", 4 ); + delay_ms ( 1000 ); + state->onMainPath = 1; + state->xyzzyCounter = 0; + if ( ( state->xyzzyKnown ) && ( state->locBranchPoint[state->brIndex] > state->locXyzzy ) ) { + // Return to xyzzy + state->loc = state->locXyzzy; + } else { // Return to entrance + state->loc = 0; + } + } + if ( bThreat ) { + sprintf ( buf, "%s", xyzzy_threat_names[state->ri[threatIndex]] ); + watch_display_string ( buf, 4 ); + delay_ms ( 2000 ); + watch_display_string ( " ", 4 ); + delay_ms ( 500 ); + if ( state->inventory[threatIndex] ) { // OK - required item in inventory + sprintf ( buf, "%s", xyzzy_item_names[state->ri[threatIndex]] ); + watch_display_string ( buf, 4 ); + delay_ms ( 1000 ); + sprintf ( buf, "%s", xyzzy_action_names[state->ri[threatIndex]] ); + for ( i = 0; i < 4; i++ ) { + watch_display_string ( buf, 4 ); + delay_ms ( 500 ); + watch_display_string ( " ", 4 ); + delay_ms ( 200 ); + } + watch_display_string ( " dONE ", 4); + delay_ms ( 800 ); + state->inventory[threatIndex] = 0; + state->locThreat[threatIndex] = 199; // Removed from cave + } else { // Kicked back to entrance + state->loc = 0; + state->onMainPath = 1; + } + } + } else { // User picked 'incorrectly' + branchMatch = false; + codeMatch = false; + if ( state->onMainPath ) { + for ( i = 0; i < XY_MAX_BRANCHES; i++ ) { + if ( state->loc == state->locBranchPoint[i] ) { + branchMatch = true; + state->brIndex = i; + } + } + for ( i = 0; i < state->numCodes; i++ ) { + if ( ( state->loc == state->locCodes[i] ) && state->onMainPath ) { + codeMatch = true; + codeMatchIndex = i; + } + } + } + if ( branchMatch ) { + // Begin proceeding down other path + state->loc++; + state->onMainPath = false; + } else if ( codeMatch ) { + // Stay at this position, display code segment + if ( state->codesSeen[codeMatchIndex] == 0 ) { + state->totalCodesSeen++; + state->codesSeen[codeMatchIndex] = 1; + } + sprintf ( buf, "%s", xyzzy_keywords[state->codeValues[codeMatchIndex]] ); + watch_display_string ( buf, 4 ); + delay_ms ( 1000 ); + } else { // Incorrect move + // A quick blink if already at the entrance + if ( state->loc == 0 ) { + watch_display_string ( " ", 4 ); + delay_ms ( 200 ); + } + // Jump back to entrance, or xyzzy location if applicable + jumpToXyzzy = false; + if ( ( state->xyzzyKnown ) && ( state->xyzzyCounter < 6 ) ) { + if ( ( state->onMainPath ) && + ( state->loc > state->locXyzzy ) ) { + jumpToXyzzy = true; + } + if ( !state->onMainPath ) { + if ( state->locBranchPoint[state->brIndex] > state->locXyzzy ) { + jumpToXyzzy = true; + } + } + } + state->onMainPath = 1; + if ( jumpToXyzzy ) { + state->loc = state->locXyzzy; + state->xyzzyCounter++; + } else { + state->loc = 0; + state->xyzzyCounter = 0; + } + } + } + } // END If User picked left or right + + if ( codeEntryCompleted ) + { + // User is done entering code. Is it right? + // Only need to enter codes seen, don't care about order + for ( i = 0; i < XY_MAX_CODES; i++ ) codePending[i] = state->codesSeen[i]; + for ( i = 0; i < state->numCodes; i++ ) { + for ( j = 0; j < state->numCodes; j++ ) { + if ( state->userCodes[i] == state->codeValues[j] ) codePending[j] = 0; + } + } + // There should be no codes left pending + valOK = true; + for ( i = 0; i < state->numCodes; i++ ) if ( codePending[i] ) valOK = false; + if ( !valOK ) { // Nope, back to entrance. + state->mode = XY_MODE_PLAYING; + state->loc = 0; + watch_display_string ( " NO ", 2 ); + delay_ms ( 1000 ); + displayLocation = true; + } else { // They did it - winner! + nowSeconds = watch_utility_date_time_to_unix_time ( watch_rtc_get_date_time(), 0 ); + if ( state->totalCodesSeen > 0 ) { + watch_display_string ( " YES ", 2 ); + delay_ms ( 1100 ); + watch_display_string ( " ", 4 ); + delay_ms ( 200 ); + } + watch_display_string ( " 6AME ", 2 ); + delay_ms ( 1000 ); + watch_display_string ( " ", 4 ); + delay_ms ( 200 ); + watch_display_string ( "OvEr ", 4 ); + delay_ms ( 1000 ); + watch_display_string ( " ", 4 ); + delay_ms ( 200 ); + watch_clear_all_indicators ( ); + watch_display_string ( " TELE ", 4 ); + delay_ms ( 700 ); + watch_display_string ( " POrt ", 4 ); + delay_ms ( 700 ); + watch_display_string ( " ", 4 ); + delay_ms ( 200 ); + oldVal = 100; + for ( i = 0; i < 15; i++ ) { + do { + j = gen_random_int ( 0, XY_NUM_TELEPORT - 1 ); + } while ( j == oldVal ); + oldVal = j; + watch_display_string ( teleportStrings[j], 4 ); + delay_ms ( 100 ); + } + watch_display_string ( " ", 4 ); + delay_ms ( 350 ); + watch_display_string ( " dESt ", 4 ); + delay_ms ( 800 ); + watch_display_string ( " ", 4 ); + delay_ms ( 1500 ); + sprintf ( buf, "%s", xyzzy_countries[state->secretDestination] ); + watch_display_string ( buf, 4 ); + delay_ms ( 4000 ); + watch_display_string ( " ", 4 ); + delay_ms ( 300 ); + watch_display_string ( " YOU ", 4 ); + delay_ms ( 800 ); + watch_display_string ( " 6Et ", 4 ); + delay_ms ( 800 ); + watch_display_string ( " ", 4 ); + delay_ms ( 600 ); + sprintf ( buf, "%s", xyzzy_rewards[state->secretItem] ); + watch_display_string ( buf, 4 ); + delay_ms ( 2500 ); + watch_display_string ( " ", 4 ); + delay_ms ( 400 ); + deltaSeconds = nowSeconds - state->startTime; + state->lastSolveTime = deltaSeconds; + // Check for fastest run at this level + if ( ( deltaSeconds < state->bestTime[state->levelSelected] ) || ( state->bestTime[state->levelSelected]==0 ) ) { + // New fastest time for at this cave size + state->bestTime[state->levelSelected] = deltaSeconds; + watch_display_string ( " NEw ", 4 ); + delay_ms ( 700 ); + for ( i = 0; i < 5; i++ ) { + watch_display_string ( " ", 4 ); + delay_ms ( 200 ); + watch_display_string ( " bESt ", 4 ); + delay_ms ( 200 ); + } + watch_display_string ( " tIME ", 4 ); + delay_ms ( 700 ); + } + // Break this down in to days, hours, minutes and seconds + seconds = deltaSeconds % 60; + deltaSeconds = deltaSeconds / 60; + minutes = deltaSeconds % 60; + deltaSeconds = deltaSeconds / 60; + hours = deltaSeconds % 24; + days = deltaSeconds / 24; + if ( days > 0 ) { + sprintf ( buf, "%4ddy", days ); + watch_display_string ( buf, 4 ); + delay_ms ( 2500 ); + } + if ( ( hours > 0 ) || ( days > 0 ) ) { + sprintf ( buf, "%4dHr", hours ); + watch_display_string ( buf, 4 ); + delay_ms ( 2500 ); + } + sprintf ( buf, "%4dMI", minutes ); + watch_display_string ( buf, 4 ); + delay_ms ( 2500 ); + sprintf ( buf, "%4dSE", seconds ); + watch_display_string ( buf, 4 ); + delay_ms ( 2500 ); + state->mode = XY_MODE_WAITING_TO_START; + state->tickCounter = 0; + displayLocation = false; + } + } // ENDIF Code entry completed + + if ( ( displayLocation ) && ( state->mode == XY_MODE_PLAYING ) ) xyzzy_display_location ( state ); + + return true; +} + +void xyzzy_face_resign(movement_settings_t *settings, void *context) { + (void) settings; + xyzzy_state_t *state = (xyzzy_state_t *)context; + if (state->writePending) { + write_to_xyzzy_EEPROM(state); + state->writePending=false; + } +} + diff --git a/xyzzy_face.h b/xyzzy_face.h new file mode 100644 index 000000000..9a964bdfa --- /dev/null +++ b/xyzzy_face.h @@ -0,0 +1,115 @@ +/* + * MIT License + * + * Copyright (c) 2025 Klingon Jane + * + * 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 thNe 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 XYZZY_FACE_H_ +#define XYZZY_FACE_H_ + +#include "movement.h" +#define XY_MAX_PATH_LENGTH 24 +#define XY_MAX_BRANCH_LENGTH 8 +#define XY_MAX_CODES 4 +#define XY_NUM_LEVELS 3 +#define XY_MAX_BRANCHES 3 +#define XY_MAX_THREATS 5 + +/* + +XYZZY Micro-adventure. + +A lightning-quick tribute to the original game. + +Full acknowledgements to Will Crowther 1976 and Don Woods 1977 for +"Colossal Cave Adventure", which started this genre of games. + +Tiny cave hint: The keywords always appear at locations 3 and 6. + +Thankfully; keywords only appear on the main path in all caves. + +*/ + + +typedef struct { + // An asterisk '*' means saved to EEPROM + uint8_t mode; // * 0 game over show country, 1 level select, 2 playing + uint8_t levelSelected; // * 0 = training, 1 = normal, 2 = select + uint8_t loc; // * Current progress into the cave + uint8_t mainPath[XY_MAX_PATH_LENGTH]; // * Main path defined by 0s and 1s + uint8_t locMainPathEnd; // * Final room - end of main path - end of game + uint8_t numCodes; // * Actual codes in use, up to XY_MAX_CODES + uint8_t locCodes[XY_MAX_CODES]; // * Code numbers on main path + uint8_t codeValues[XY_MAX_CODES]; // * Values from 0 thru N + uint8_t codesSeen[XY_MAX_CODES]; // Booleans + uint8_t totalCodesSeen; // How many codes has player seen? + uint8_t userCodes[XY_MAX_CODES]; // Codes enter by player + uint8_t userCodeIndex; // Code currently being entered + uint8_t locXyzzy; // * Return point short-cut when deeper in cave + uint8_t xyzzyKnown; // * Has user ever reached xyzzy location? + uint8_t xyzzyCounter; // Usually jump back to xyzzy + uint8_t locThreat[XY_MAX_THREATS]; // * Where is the threat? + uint8_t brThreat[XY_MAX_THREATS]; // * Which branch is threat on? + uint8_t inventory[XY_MAX_THREATS]; // * Do we have this item? + uint8_t locItem[XY_MAX_THREATS]; // * How deep is this item + uint8_t brItem[XY_MAX_THREATS]; // * Which branch is this item? + uint8_t ri[XY_MAX_THREATS]; // * Randomized index from 0 to n-1 for threat concealment + uint8_t locBranchPoint[XY_MAX_BRANCHES]; // * Where does branch occur? + uint8_t locBranchEnd[XY_MAX_BRANCHES]; // * Last valid place in this branch + uint8_t branch[XY_MAX_BRANCHES][XY_MAX_BRANCH_LENGTH]; // * Branch defined by 0s and 1s + uint8_t brIndex; // * Which branch you are on + bool onMainPath; // * You are on the main path ( not any branch ) + uint8_t shortcutCounter; // How many consec times we've jumped back to locXyzzy + uint16_t tickCounter; // For minimum delays and debounce + bool writePending; // Need to update EEPROM when resigning + uint8_t secretDestination; // * Your fixed secret country/planet destination + uint8_t secretItem; // * Your fixed item reward + uint32_t startTime; // * Seconds in UNIX time of cave generation + uint32_t lastSolveTime; // * Time of last solution + uint32_t bestTime[XY_NUM_LEVELS]; // * Best times ever + uint16_t days; // For display of solve times + uint8_t hours; // For display of solve times + uint8_t minutes; // For display of solve times + uint8_t seconds; // For display of solve times + uint8_t tdLevel; // For time display + uint8_t tdSubLevel; // For time display + bool alarmWasPressed; // Was the alarm button down previously? + bool lightWasPressed; // Was the light button down previously? + bool ignoreNextRelease; // Ignore release after long press + uint16_t buttonTimer; // How many ticks was button held down + uint8_t bothPressedTimer; // How many ticks both buttons held down + uint8_t resetCounter; // To clear all saved times +} xyzzy_state_t; + +void xyzzy_face_setup(movement_settings_t *settings, uint8_t watch_face_index, void ** context_ptr); +void xyzzy_face_activate(movement_settings_t *settings, void *context); +bool xyzzy_face_loop(movement_event_t event, movement_settings_t *settings, void *context); +void xyzzy_face_resign(movement_settings_t *settings, void *context); + +#define xyzzy_face ((const watch_face_t){ \ + xyzzy_face_setup, \ + xyzzy_face_activate, \ + xyzzy_face_loop, \ + xyzzy_face_resign, \ + NULL, \ +}) + +#endif // XYZZY_FACE_H_