diff --git a/include/game.h b/include/game.h index 6e428ba..4ba9a03 100644 --- a/include/game.h +++ b/include/game.h @@ -175,8 +175,6 @@ void display_mult(void); void display_money(void); void set_retrigger(bool new_retrigger); -u32 get_rand(void); - // joker specific functions bool is_shortcut_joker_active(void); int get_straight_and_flush_size(void); diff --git a/include/game_variables.h b/include/game_variables.h index f8ddd95..eb5ff76 100644 --- a/include/game_variables.h +++ b/include/game_variables.h @@ -7,6 +7,7 @@ #define GAME_VARIABLES_H #include "blind.h" +#include "random.h" #include @@ -37,8 +38,7 @@ typedef struct // Internal variables s32 timer; // This might already exist in libtonc but idk so i'm just making my own - u32 rng_seed; - u32 rng_step; // Position in the rng sequence. + RngInfo rng_info; // Variables visible by the player diff --git a/include/random.h b/include/random.h new file mode 100644 index 0000000..bf6682a --- /dev/null +++ b/include/random.h @@ -0,0 +1,76 @@ +/** + * @file random.h + * + * @brief Common functions to handle RNG manipulation. Using this interface has three goals: + * 1. Make RNG repeatable so that people can share seeds and saves, and end up with the same run + * 2. Prevent the players from engaging in save-scumming since the pseudo-RNG is deterministic + * 3. Accomodate for base-36 seeds, so that we can freely chose one with alpha-numeric characters in + * the seed screen input + */ +#ifndef RANDOM_H +#define RANDOM_H + +#include + +/** + * @brief Hex value of "ZZZZZZ" in base 36 + */ +#define MAX_SEED 0x81BF0FFF + +/** + * @brief Information to track and restore RNG state + */ +typedef struct +{ + /** Initial seed */ + u32 seed; + /** Position in the rng sequence. */ + u32 step; +} RngInfo; + +/** + * @brief Starts counting CPU cycles, this will be used by rng_shuffle_seed to + * generate a more random seed. To be called once on game start. + */ +void rng_init(void); + +/** + * @brief Update the CPU cycles counter and accumulate it into a bigger u32 `timer_acc` variable. + */ +void rng_update(void); + +/** + * @brief Set the rng seed to the chosen value, and reset the step counter to 0. + * The seed will be capped at `MAX_SEED` for compatibility with the Seed Input screen + * used to choose a seed for seeded runs. + * + * @param seed the new RNG seed + */ +void rng_set_seed(u32 seed); + +/** + * @brief Uses the CPU cycles counter to randomize the RNG seed as much as possible. + * This will be called by the main menu and the game over screens so that + * the next run's seed isn't the same as the last's. + * rng_start_sampling needs to have been called, and will stop the profiling. + */ +void rng_shuffle_seed(void); + +/** + * @brief Get the next "randomly" generated number in the sequence from the current seed. + * + * @return u32 + */ +u32 rng_get_u32(void); + +/** + * @brief Restore RNG info struct in the GameVariables. Sets the `seed` and seeks the + * position `step` in the rng sequence. + * + * @param info RngInfo struct applied + * + * @note Consistency of the RNG after reloading the RngInfo struct has yet to be tested properly + */ +void rng_restore(RngInfo info); + +#endif // RANDOM_H diff --git a/source/blind.c b/source/blind.c index 49d1b47..6451bcb 100644 --- a/source/blind.c +++ b/source/blind.c @@ -4,6 +4,7 @@ #include "game.h" #include "graphic_utils.h" #include "list.h" +#include "random.h" #include "stdbool.h" #include "util.h" @@ -139,7 +140,7 @@ enum BlindType roll_blind_type(bool showdown) } // roll a random blind among the unbeaten ones - int random_blind_idx = get_rand() % list_get_len(p_unbeaten_blinds); + int random_blind_idx = rng_get_u32() % list_get_len(p_unbeaten_blinds); Blind* random_blind = list_get_at_idx(p_unbeaten_blinds, random_blind_idx); return random_blind->type; diff --git a/source/game.c b/source/game.c index 6059fc1..5a65a68 100644 --- a/source/game.c +++ b/source/game.c @@ -21,6 +21,7 @@ #include "joker.h" #include "layout.h" #include "list.h" +#include "random.h" #include "save.h" #include "selection_grid.h" #include "soundbank.h" @@ -299,7 +300,8 @@ static ContainedHandTypes _contained_hands = {0}; // Initialization of the global vars // clang-format off GameVariables g_game_vars = { - .timer = 0, .rng_seed = 0, .rng_step = 0, + .timer = 0, + .rng_info = {0, 0}, .round = 0, .ante = 0, .money = 0, @@ -629,6 +631,8 @@ static inline void jokers_update_loop(void) void game_update() { + rng_update(); + g_game_vars.timer++; jokers_update_loop(); @@ -654,12 +658,6 @@ void game_change_state(enum GameState new_game_state) } } -u32 get_rand() -{ - g_game_vars.rng_step++; - return rand(); -} - CardObject** get_hand_array(void) { return hand; @@ -1429,7 +1427,7 @@ static inline void deck_shuffle(void) { for (int i = deck_top; i > 0; i--) { - int j = get_rand() % (i + 1); + int j = rng_get_u32() % (i + 1); Card* temp = deck[i]; deck[i] = deck[j]; deck[j] = temp; @@ -1505,14 +1503,6 @@ static void game_round_on_init(void) game_playing_selection_grid.selection = GAME_PLAYING_INIT_SEL; } -// General functions -static inline void set_seed(int seed) -{ - srand(seed); - g_game_vars.rng_seed = seed; - g_game_vars.rng_step = 0; -} - // Playing state functions static void game_playing_discard_on_pressed(void) { @@ -1670,7 +1660,7 @@ static bool game_playing_hand_row_on_selection_changed( */ play_sfx( SFX_CARD_FOCUS, - MM_BASE_PITCH_RATE + get_rand() % CARD_FOCUS_SFX_PITCH_OFFSET_RANGE, + MM_BASE_PITCH_RATE + rng_get_u32() % CARD_FOCUS_SFX_PITCH_OFFSET_RANGE, SFX_DEFAULT_VOLUME ); } @@ -1690,7 +1680,7 @@ static bool game_playing_hand_row_on_selection_changed( */ play_sfx( SFX_CARD_FOCUS, - MM_BASE_PITCH_RATE + get_rand() % CARD_FOCUS_SFX_PITCH_OFFSET_RANGE, + MM_BASE_PITCH_RATE + rng_get_u32() % CARD_FOCUS_SFX_PITCH_OFFSET_RANGE, SFX_DEFAULT_VOLUME ); } @@ -3020,8 +3010,7 @@ static void game_playing_on_update(void) void game_start(void) { - // set_seed(9); // 9 is a full house - set_seed(g_game_vars.rng_seed); + rng_shuffle_seed(); affine_background_change_background(AFFINE_BG_GAME); diff --git a/source/game/game_over.c b/source/game/game_over.c index 71679e5..6c12d01 100644 --- a/source/game/game_over.c +++ b/source/game/game_over.c @@ -7,6 +7,7 @@ #include "game/main_menu.h" #include "graphic_utils.h" #include "layout.h" +#include "random.h" #include "soundbank.h" #include "timer.h" #include "util.h" @@ -128,4 +129,5 @@ void game_over_on_exit(void) { condition = END_CONDITION_NONE; game_reset(); + rng_shuffle_seed(); } diff --git a/source/game/main_menu.c b/source/game/main_menu.c index 5b7303a..19381e0 100644 --- a/source/game/main_menu.c +++ b/source/game/main_menu.c @@ -127,14 +127,6 @@ void game_main_menu_on_update(void) main_menu_ace->sprite_object->trotation = lu_sin((g_game_vars.timer << 8) / 2) / 3; card_object_update(main_menu_ace); - // Seed randomization - g_game_vars.rng_seed++; - // If the keys have changed, make it more pseudo-random - if (key_curr_state() != key_prev_state()) - { - g_game_vars.rng_seed *= 2; - } - selection_grid_process_input(&main_menu_selection_grid); } diff --git a/source/game/shop.c b/source/game/shop.c index 8f1e06c..b68966c 100644 --- a/source/game/shop.c +++ b/source/game/shop.c @@ -17,6 +17,7 @@ #include "joker.h" #include "layout.h" #include "list.h" +#include "random.h" #include "save.h" #include "soundbank.h" #include "timer.h" @@ -196,7 +197,7 @@ static inline int game_shop_get_rand_available_joker_id(void) return UNDEFINED; int matching_joker_ids[jokers_avail_size]; - int fallback_random_idx = get_rand() % jokers_avail_size; + int fallback_random_idx = rng_get_u32() % jokers_avail_size; int fallback_random_joker_id = UNDEFINED; int match_count = 0; @@ -215,8 +216,8 @@ static inline int game_shop_get_rand_available_joker_id(void) } } - int selected_joker_id = - (match_count > 0) ? matching_joker_ids[get_rand() % match_count] : fallback_random_joker_id; + int selected_joker_id = (match_count > 0) ? matching_joker_ids[rng_get_u32() % match_count] + : fallback_random_joker_id; return selected_joker_id; } diff --git a/source/joker.c b/source/joker.c index 3e47929..bdb56ca 100644 --- a/source/joker.c +++ b/source/joker.c @@ -5,6 +5,7 @@ #include "graphic_utils.h" #include "joker_gfx.h" #include "pool.h" +#include "random.h" #include "soundbank.h" #include "util.h" @@ -368,7 +369,7 @@ Sprite* joker_object_get_sprite(JokerObject* joker_object) int joker_get_random_rarity() { int joker_rarity = 0; - int rarity_roll = get_rand() % 100; + int rarity_roll = rng_get_u32() % 100; if (rarity_roll < COMMON_JOKER_CHANCE) { joker_rarity = COMMON_JOKER; diff --git a/source/joker_effects.c b/source/joker_effects.c index 894662c..f172c7b 100644 --- a/source/joker_effects.c +++ b/source/joker_effects.c @@ -4,6 +4,7 @@ #include "joker.h" #include "list.h" #include "pool.h" +#include "random.h" #include "util.h" #include @@ -563,7 +564,7 @@ static u32 misprint_joker_effect( *joker_effect = &shared_joker_effect; - (*joker_effect)->mult = get_rand() % (MISPRINT_MAX_MULT + 1); + (*joker_effect)->mult = rng_get_u32() % (MISPRINT_MAX_MULT + 1); return JOKER_EFFECT_FLAG_MULT; } @@ -776,7 +777,7 @@ static u32 reserved_parking_joker_effect( u32 effect_flags_ret = JOKER_EFFECT_FLAG_NONE; - if ((get_rand() % 2 == 0) && card_is_face(scored_card)) + if ((rng_get_u32() % 2 == 0) && card_is_face(scored_card)) { *joker_effect = &shared_joker_effect; @@ -798,7 +799,7 @@ static u32 business_card_joker_effect( u32 effect_flags_ret = JOKER_EFFECT_FLAG_NONE; - if ((get_rand() % 2 == 0) && card_is_face(scored_card)) + if ((rng_get_u32() % 2 == 0) && card_is_face(scored_card)) { *joker_effect = &shared_joker_effect; diff --git a/source/main.c b/source/main.c index 1ab1b97..4e8b34c 100644 --- a/source/main.c +++ b/source/main.c @@ -6,6 +6,7 @@ #include "gbalatro_sys8.h" #include "graphic_utils.h" #include "joker.h" +#include "random.h" #include "save.h" #include "sprite.h" @@ -24,6 +25,8 @@ void init() { + rng_init(); + irq_init(NULL); irq_add(II_VBLANK, mmVBlank); irq_add(II_HBLANK, affine_background_hblank); diff --git a/source/random.c b/source/random.c new file mode 100644 index 0000000..8f0c9ff --- /dev/null +++ b/source/random.c @@ -0,0 +1,50 @@ +#include "random.h" + +#include "game_variables.h" + +#include +#include + +// Accumulate timer 1 into a bigger variable so we can generate more diverse seeds +static u32 timer_acc = 0; + +// Timers usage docs: https://gbadev.net/tonc/timers.html +void rng_init(void) +{ + REG_TM1D = 0; + REG_TM1CNT = TM_FREQ_1 | TM_ENABLE; // using timer with x1 prescale +} + +void rng_update(void) +{ + timer_acc += (u32)REG_TM1D; +} + +void rng_set_seed(u32 seed) +{ + g_game_vars.rng_info.seed = seed % (MAX_SEED + 1); + g_game_vars.rng_info.step = 0; + srand(g_game_vars.rng_info.seed); +} + +void rng_shuffle_seed(void) +{ + rng_set_seed(timer_acc); +} + +u32 rng_get_u32(void) +{ + g_game_vars.rng_info.step++; + return rand(); +} + +void rng_restore(RngInfo info) +{ + g_game_vars.rng_info = info; + + srand(g_game_vars.rng_info.seed); + for (u32 i = 0; i < g_game_vars.rng_info.step; i++) + { + (void)rng_get_u32(); + } +} diff --git a/source/save.c b/source/save.c index 3622e27..6fa9510 100644 --- a/source/save.c +++ b/source/save.c @@ -105,8 +105,8 @@ typedef struct JokerObjectSaveData * 2 | 'L' | ' ' | 'D' | 'A' | - | - * 3 | 'T' | 'A' | ' ' | '-' | - | - * 4 | T[0] | T[1] | T[2] | T[3] | GLOB TIMER | The global timer used for animations thoughout the game - * 5 | SED[0] | SED[1] | SED[2] | SED[3] | RNG SEED | The seed used for RNG, either randomly shuffled or chosen by the player at game start - * 6 | STP[0] | STP[1] | STP[2] | STP[3] | RNG STEP | The current position in the RNG sequence for the given seed, since the start of the run + * 5 | RNG[0] | RNG[1] | RNG[2] | RNG[3] | RNG INFO | RNG Info struct, containing the seed used for RNG, either randomly shuffled or chosen by the player + * 6 | RNG[4] | RNG[5] | RNG[6] | RNG[7] | - | at game start, and the current position in the RNG sequence for the given seed, since the start of the run * 7 | RND[0] | RND[1] | RND[2] | RND[3] | ROUND | What Round we are about to start * 8 | ANT[0] | ANT[1] | ANT[2] | ANT[3] | ANTE | What Ante we are on * 9 | MNY[0] | MNY[1] | MNY[2] | MNY[3] | MONEY | How much money we currently have left @@ -127,8 +127,7 @@ typedef struct SaveGame { char tag_internal[SAVE_LABEL_SIZE]; s32 timer; - u32 rng_seed; - u32 rng_step; + RngInfo rng_info; int round; int ante; int money; @@ -168,8 +167,7 @@ static const SaveOptions SaveOptions_default = { static const SaveGame SaveGame_default = { .tag_internal = "-INTERNAL DATA -", .timer = 0, - .rng_seed = 0, - .rng_step = 0, + .rng_info = {0, 0}, .round = 0, .ante = 0, .money = 0, @@ -328,8 +326,7 @@ void save_game(void) // Fixed data game.timer = g_game_vars.timer; - game.rng_seed = g_game_vars.rng_seed; - game.rng_step = g_game_vars.rng_step; + game.rng_info = g_game_vars.rng_info; game.round = g_game_vars.round; game.ante = g_game_vars.ante; game.money = g_game_vars.money; @@ -370,17 +367,10 @@ void load_game(void) read_sram(GAME_ADDRESS, (u8*)&game, sizeof(game)); g_game_vars.timer = game.timer; - g_game_vars.rng_seed = game.rng_seed; - g_game_vars.rng_step = game.rng_step; + rng_restore(game.rng_info); g_game_vars.round = game.round; g_game_vars.ante = game.ante; g_game_vars.money = game.money; // TODO: load Jokers from stored minimal data - - // return to where we were in the random sequence so that the run stays reproducible - for (u32 i = 0; i < g_game_vars.rng_step; i++) - { - (void)rand(); - } } diff --git a/source/sprite.c b/source/sprite.c index a2de41f..09a9508 100644 --- a/source/sprite.c +++ b/source/sprite.c @@ -5,6 +5,7 @@ #include "game_variables.h" #include "graphic_utils.h" #include "pool.h" +#include "random.h" #include "soundbank.h" #include "util.h" @@ -330,7 +331,7 @@ void sprite_object_set_focus(SpriteObject* sprite_object, bool focus) play_sfx( SFX_CARD_FOCUS, - MM_BASE_PITCH_RATE + get_rand() % CARD_FOCUS_SFX_PITCH_OFFSET_RANGE, + MM_BASE_PITCH_RATE + rng_get_u32() % CARD_FOCUS_SFX_PITCH_OFFSET_RANGE, SFX_DEFAULT_VOLUME ); sprite_object->ty = sprite_object->ty + int2fx((focus ? -1 : 1) * SPRITE_FOCUS_RAISE_PX);