[Enhancement] Game Over screen rework (#548)

* WIP

* getting somewhere

* clean up everything real good

* doc

* clang format

* Fix round.c after merge

* Remove extra deck_pop declaration

* Remove unnecessary comment

* address MeirGavish's review

---------

Co-authored-by: MathisMartin31 <mathis.martin31@gmail.com>
This commit is contained in:
Geralt
2026-07-08 02:48:31 +02:00
committed by GitHub
parent da16e6a486
commit 369c680cb7
10 changed files with 359 additions and 81 deletions
+1
View File
@@ -0,0 +1 @@
-gB8 -mR8 -mLs -mRtf -pn 64
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

+1 -1
View File
@@ -7,7 +7,7 @@
#include <maxmod.h>
#include <tonc.h>
#define MAX_CARDS (NUM_SUITS * NUM_RANKS)
#define MAX_CARDS 128 // Arbitrary high value so we don't have issues
#define MAX_CARDS_ON_SCREEN 16
#define CARD_TID 0
+4
View File
@@ -7,6 +7,7 @@
#define GAME_VARIABLES_H
#include "blind.h"
#include "hand.h"
#include "random.h"
#include <tonc.h>
@@ -49,6 +50,9 @@ typedef struct
s32 hand_size;
s32 deck;
u32 best_hand_score;
u32 nb_played_hands[HAND_TYPE_MAX];
// Blind variables
enum BlindType current_blind;
+6 -1
View File
@@ -40,7 +40,8 @@ enum HandType
ROYAL_FLUSH,
FIVE_OF_A_KIND,
FLUSH_HOUSE,
FLUSH_FIVE
FLUSH_FIVE,
HAND_TYPE_MAX = FLUSH_FIVE
};
// clang-format off
@@ -71,6 +72,10 @@ typedef struct ContainedHandTypes
} ContainedHandTypes;
// clang-format on
// Misc Hand Functions
const char* get_hand_type_name(enum HandType hand_type);
// Hand Structure Manipulation
/**
+11 -1
View File
@@ -36,6 +36,7 @@
#include <maxmod.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#define STRAIGHT_AND_FLUSH_SIZE_FOUR_FINGERS 4
#define STRAIGHT_AND_FLUSH_SIZE_DEFAULT 5
@@ -95,11 +96,15 @@ static StateMachine game_sm = STATE_MACHINE_DEFINE(state_info, GAME_STATE_MAX);
// Initialization of the global vars
// clang-format off
GameVariables g_game_vars = {
.timer = 0, .rng_info = {0, 0},
.timer = 0,
// Setting the seed to an invalid value so that the Run Setup screen knows we're not reusing a previous Run's seed
.rng_info = {UNDEFINED, 0},
.round = 0, .ante = 0, .money = 0, .hand_size = DEFAULT_HAND_SIZE,
.deck = DECK_TYPE_RED,
.best_hand_score = 0, .nb_played_hands = {0},
.current_blind = BLIND_TYPE_SMALL,
.next_boss_blind = BLIND_TYPE_BIG,
.blinds_states =
@@ -186,10 +191,15 @@ void game_init()
g_game_vars.ante = STARTING_ANTE;
g_game_vars.money = STARTING_MONEY;
g_game_vars.score = STARTING_SCORE;
g_game_vars.round = 0;
g_game_vars.chips = 0;
g_game_vars.mult = 0;
g_game_vars.round = STARTING_ROUND;
g_game_vars.best_hand_score = 0;
for (int i = 0; i < HAND_TYPE_MAX; i++)
g_game_vars.nb_played_hands[i] = 0;
// Initialize/reset unbeaten Boss/Showdown Blinds so they are all available
init_unbeaten_blinds_list(false);
init_unbeaten_blinds_list(true);
+290 -68
View File
@@ -2,17 +2,30 @@
#include "affine_background.h"
#include "audio_utils.h"
#include "background_game_over_gfx.h"
#include "button.h"
#include "card.h"
#include "game.h"
#include "graphic_utils.h"
#include "hand.h"
#include "joker.h"
#include "layout.h"
#include "list.h"
#include "random.h"
#include "selection_grid.h"
#include "soundbank.h"
#include "timer.h"
#include "util.h"
#include <tonc.h>
#define RED_BTN_MAIN_COLOR_PAL_IDX 6
#define BLUE_BTN_MAIN_COLOR_PAL_IDX 7
#define REUSE_SEED_BTN_OUTLINE_PAL_IDX 8
#define NEW_RUN_BTN_OUTLINE_PAL_IDX 9
#define MAIN_MENU_BTN_OUTLINE_PAL_IDX 10
#define ENDLESS_BTN_OUTLINE_PAL_IDX 11
enum EndCondition
{
END_CONDITION_NONE,
@@ -21,52 +34,158 @@ enum EndCondition
};
// clang-format off
static const BG_POINT GAME_OVER_SRC_RECT_3X3_POS = { 25, 29};
static const Rect GAME_OVER_DIALOG_DEST_RECT = { 11, 21, 23, 28};
static const Rect GAME_OVER_ANIM_RECT = { 11, 8, 23, 28};
static const Rect GAME_LOSE_MSG_TEXT_RECT = {104, 72, UNDEFINED, UNDEFINED};
// 1 character to the right of GAME_LOSE
static const Rect GAME_WIN_MSG_TEXT_RECT = {112, 72, UNDEFINED, UNDEFINED};
static const BG_POINT NEW_RUN_BTN_DEST_POS = { 15, 26};
static const Rect NEW_RUN_BTN_SRC_RECT = { 0, 30, 4, 31};
static const Rect GAME_OVER_ANIM_RECT = { 3, 5, 26, 31};
static const Rect GAME_OVER_TEXT_TILES_SRC_RECT_1 = { 27, 24, 31, 25};
static const Rect GAME_OVER_TEXT_TILES_SRC_RECT_2 = { 27, 26, 31, 27};
static const BG_POINT GAME_OVER_TEXT_TILES_DEST_POS_1 = { 10, 21};
static const BG_POINT GAME_OVER_TEXT_TILES_DEST_POS_2 = { 15, 21};
static const Rect DEFEATED_BY_TEXT_SRC_RECT = { 28, 30, 31, 31};
static const BG_POINT DEFEATED_BY_TEXT_DEST_POS = { 22, 24};
static const BG_POINT DEFEATED_BY_TOKEN_INIT_POS = {180, 207};
static const Rect REUSE_SEED_BTN_OFF_SRC_RECT = { 28, 28, 29, 29};
static const Rect REUSE_SEED_BTN_ON_SRC_RECT = { 30, 28, 31, 29};
static const BG_POINT REUSE_SEED_BTN_DEST_POS = { 4, 11};
static const Rect BEST_HAND_SCORE_RECT = {126, 64, 166, 72};
static const Rect MOST_PLAYED_HAND_RECT = {112, 80, 168, 88};
static const BG_POINT SEED_VALUE_POS = {112, 96};
static const BG_POINT NEW_RUN_BTN_TEXT_POS = { 48, 112};
static const BG_POINT MAIN_MENU_BTN_TEXT_POS = {120, 112};
// clang-format on
static const u32 GAME_OVER_ANIM_FRAMES = 15;
static const u32 GAME_OVER_ANIM_FRAMES = 17;
static const mm_byte GAME_OVER_SFX_VOL = 178; // 70% of MM_SFX_FULL_VOLUME
static const mm_word GAME_OVER_WHOOSH_RATE = 922; // 90% of MM_BASE_PITCH_RATE
// Selection Grid
enum GameOverRows
{
GAME_OVER_SEED_ROW,
GAME_OVER_RUN_MENU_ROW,
GAME_OVER_ROW_MAX
};
#define GAME_OVER_SEED_BTN_COL 0
#define GAME_OVER_NEW_RUN_BTN_COL 0
#define GAME_OVER_MAIN_MENU_BTN_COL 1
static int game_over_get_row_size1(void)
{
return 1;
}
static int game_over_get_row_size2(void)
{
return 2;
}
static void game_over_on_key_transit(SelectionGrid* selection_grid, Selection* selection);
static bool game_over_on_selection_changed(
SelectionGrid* selection_grid,
int row_idx,
const Selection* prev_selection,
const Selection* new_selection
);
static const SelectionGridRow game_over_selection_rows[] = {
{GAME_OVER_SEED_ROW, game_over_get_row_size1, game_over_on_selection_changed, game_over_on_key_transit, {.wrap = false}},
{GAME_OVER_RUN_MENU_ROW, game_over_get_row_size2, game_over_on_selection_changed, game_over_on_key_transit, {.wrap = false}}
};
static const Selection GAME_OVER_INIT_SEL = {0, GAME_OVER_RUN_MENU_ROW};
static SelectionGrid game_over_selection_grid = {
game_over_selection_rows,
GAME_OVER_ROW_MAX,
GAME_OVER_INIT_SEL
};
// Buttons
static void reuse_seed_on_pressed(void);
static void new_run_on_pressed(void);
static void main_menu_on_pressed(void);
static Button* game_over_get_button_from_sel(const Selection* selection);
// clang-format off
static Button game_over_buttons[GAME_OVER_ROW_MAX][2] = {
// GAME_OVER_SEED_ROW
{
{ REUSE_SEED_BTN_OUTLINE_PAL_IDX, RED_BTN_MAIN_COLOR_PAL_IDX, reuse_seed_on_pressed, NULL }
},
// GAME_OVER_RUN_MENU_ROW
{
{ NEW_RUN_BTN_OUTLINE_PAL_IDX, RED_BTN_MAIN_COLOR_PAL_IDX, new_run_on_pressed, NULL },
{ MAIN_MENU_BTN_OUTLINE_PAL_IDX, RED_BTN_MAIN_COLOR_PAL_IDX, main_menu_on_pressed, NULL }
}
};
// clang-format on
// Internal Variables
static enum EndCondition condition = END_CONDITION_NONE;
static u32 timer = TM_ZERO;
static bool reuse_seed = false;
static bool continue_run = false;
static void game_over_init(enum EndCondition init_condition)
static void game_over_common_init(enum EndCondition init_condition)
{
condition = init_condition;
// Clears the round end menu
main_bg_se_clear_rect(POP_MENU_ANIM_RECT);
main_bg_se_copy_expand_3x3_rect(GAME_OVER_DIALOG_DEST_RECT, GAME_OVER_SRC_RECT_3X3_POS);
main_bg_se_copy_rect(NEW_RUN_BTN_SRC_RECT, NEW_RUN_BTN_DEST_POS);
timer = TM_ZERO;
reuse_seed = false;
continue_run = false;
// Using the text color to match the Win/Loss condition
switch (condition)
// Hide all Joker sprites
JokerObject* joker_object = NULL;
ListItr itr = list_itr_create(get_jokers_list());
while ((joker_object = list_itr_next(&itr)))
{
case END_CONDITION_WIN:
affine_background_set_color(TEXT_CLR_BLUE);
break;
case END_CONDITION_LOSS:
affine_background_set_color(TEXT_CLR_RED);
break;
default:
break;
obj_hide(joker_object->sprite_object->sprite->obj);
}
timer = TM_ZERO;
// Destroy any preexisting cards in the deck, which will be present if we restart a run
Card* card = NULL;
while (get_deck_top() >= 0)
{
card = deck_pop();
card_destroy(&card);
}
// Clears the round end menu
toggle_windows(false, false);
GRIT_CPY(pal_bg_mem, background_game_over_gfxPal);
GRIT_CPY(&tile_mem[MAIN_BG_CBB], background_game_over_gfxTiles);
GRIT_CPY(&se_mem[MAIN_BG_SBB], background_game_over_gfxMap);
// Move blind token offscreen at initial position before moving it up
sprite_position(
g_game_vars.playing_blind_token,
DEFEATED_BY_TOKEN_INIT_POS.x,
DEFEATED_BY_TOKEN_INIT_POS.y
);
// Highlight New Run button
game_over_selection_grid.selection = GAME_OVER_INIT_SEL;
button_set_highlight(&game_over_buttons[GAME_OVER_SEED_ROW][GAME_OVER_SEED_BTN_COL], false);
button_set_highlight(
&game_over_buttons[GAME_OVER_RUN_MENU_ROW][GAME_OVER_NEW_RUN_BTN_COL],
true
);
button_set_highlight(
&game_over_buttons[GAME_OVER_RUN_MENU_ROW][GAME_OVER_MAIN_MENU_BTN_COL],
false
);
}
void game_win_on_init(void)
{
play_sfx(SFX_GAME_WIN, MM_BASE_PITCH_RATE, SFX_DEFAULT_VOLUME);
game_over_init(END_CONDITION_WIN);
game_over_common_init(END_CONDITION_WIN);
affine_background_set_color(TEXT_CLR_BLUE);
}
void game_lose_on_init(void)
@@ -76,67 +195,170 @@ void game_lose_on_init(void)
play_lose_music();
game_over_init(END_CONDITION_LOSS);
}
game_over_common_init(END_CONDITION_LOSS);
affine_background_set_color(TEXT_CLR_RED);
/**
* @brief Makes the message background frame move 1 tile up, called until it is in its place.
*/
static inline void game_over_anim_frame(void)
{
main_bg_se_move_rect_1_tile_vert(GAME_OVER_ANIM_RECT, SCREEN_UP);
}
// Change top text to "GAME OVER"
main_bg_se_copy_rect(GAME_OVER_TEXT_TILES_SRC_RECT_1, GAME_OVER_TEXT_TILES_DEST_POS_1);
main_bg_se_copy_rect(GAME_OVER_TEXT_TILES_SRC_RECT_2, GAME_OVER_TEXT_TILES_DEST_POS_2);
/**
* @brief Polls user input to detect "Retry" button press.
*/
static inline void game_over_process_user_input()
{
if (key_hit(SELECT_CARD))
{
play_sfx(SFX_BUTTON, MM_BASE_PITCH_RATE, BUTTON_SFX_VOLUME);
game_change_state(GAME_STATE_BLIND_SELECT);
}
// Change blind frame text
main_bg_se_copy_rect(DEFEATED_BY_TEXT_SRC_RECT, DEFEATED_BY_TEXT_DEST_POS);
}
void game_over_on_update(void)
{
timer++;
// Need to clear text here or the number of cards remaining in deck stays for some reason
if (timer == 1)
tte_erase_screen();
// Move whole panel and Blind Token sprite up by a tile each frame
if (timer < GAME_OVER_ANIM_FRAMES)
{
game_over_anim_frame();
main_bg_se_move_rect_1_tile_vert(GAME_OVER_ANIM_RECT, SCREEN_UP);
sprite_position(
g_game_vars.playing_blind_token,
g_game_vars.playing_blind_token->pos.x,
g_game_vars.playing_blind_token->pos.y - TILE_SIZE
);
}
// Print values and buttons text
else if (timer == GAME_OVER_ANIM_FRAMES)
{
switch (condition)
char best_hand_str[UINT_MAX_DIGITS + 1];
truncate_uint_to_suffixed_str(
g_game_vars.best_hand_score,
rect_width(&BEST_HAND_SCORE_RECT) / TTE_CHAR_SIZE,
best_hand_str
);
Rect best_hand_rect = BEST_HAND_SCORE_RECT;
update_text_rect_to_center_str(&best_hand_rect, best_hand_str, SCREEN_LEFT);
tte_printf(
"#{P:%d,%d; cx:0x%X000}%s",
best_hand_rect.left,
best_hand_rect.top,
TTE_RED_PB,
best_hand_str
);
// Most played hand
enum HandType most_played_hand = HIGH_CARD;
for (enum HandType hand_type = PAIR; hand_type <= HAND_TYPE_MAX; hand_type++)
{
case END_CONDITION_WIN:
tte_printf(
"#{P:%d,%d; cx:0x%X000}YOU WIN",
GAME_WIN_MSG_TEXT_RECT.left,
GAME_WIN_MSG_TEXT_RECT.top,
TTE_BLUE_PB
);
break;
case END_CONDITION_LOSS:
tte_printf(
"#{P:%d,%d; cx:0x%X000}GAME OVER",
GAME_LOSE_MSG_TEXT_RECT.left,
GAME_LOSE_MSG_TEXT_RECT.top,
TTE_RED_PB
);
break;
default:
break;
if (g_game_vars.nb_played_hands[hand_type - 1] >
g_game_vars.nb_played_hands[most_played_hand - 1])
most_played_hand = hand_type;
}
const char* hand_name_str = get_hand_type_name(most_played_hand);
Rect hand_type_rect = MOST_PLAYED_HAND_RECT;
update_text_rect_to_center_str(&hand_type_rect, hand_name_str, SCREEN_LEFT);
tte_printf(
"#{P:%d,%d; cx:0x%X000}%s",
hand_type_rect.left,
hand_type_rect.top,
TTE_WHITE_PB,
hand_name_str
);
// Run's seed
char seed_str[BASE36_MAX_DIGITS + 1] = {'\0'};
u32_to_base36(g_game_vars.rng_info.seed, seed_str);
tte_printf(
"#{P:%d,%d; cx:0x%X000}%s",
SEED_VALUE_POS.x,
SEED_VALUE_POS.y,
TTE_WHITE_PB,
seed_str
);
// Buttons' text
tte_printf(
"#{P:%d,%d; cx:0x%X000}New Run",
NEW_RUN_BTN_TEXT_POS.x,
NEW_RUN_BTN_TEXT_POS.y,
TTE_WHITE_PB
);
tte_printf(
"#{P:%d,%d; cx:0x%X000}Main Menu",
MAIN_MENU_BTN_TEXT_POS.x,
MAIN_MENU_BTN_TEXT_POS.y,
TTE_WHITE_PB
);
}
game_over_process_user_input();
if (timer >= GAME_OVER_ANIM_FRAMES)
selection_grid_process_input(&game_over_selection_grid);
}
void game_over_on_exit(void)
{
toggle_windows(false, false);
play_regular_music();
condition = END_CONDITION_NONE;
game_reset();
rng_shuffle_seed();
u32 previous_seed = g_game_vars.rng_info.seed;
if (!continue_run)
game_reset();
if (reuse_seed)
rng_set_seed(previous_seed);
else
g_game_vars.rng_info.seed = UNDEFINED;
}
// SelectionGrid Implementation
static Button* game_over_get_button_from_sel(const Selection* selection)
{
return &game_over_buttons[selection->y][selection->x];
}
static void game_over_on_key_transit(SelectionGrid* selection_grid, Selection* selection)
{
if (key_hit(SELECT_CARD))
button_press(&game_over_buttons[selection->y][selection->x]);
}
static bool game_over_on_selection_changed(
SelectionGrid* selection_grid,
int row_idx,
const Selection* prev_selection,
const Selection* new_selection
)
{
if (row_idx == prev_selection->y)
button_set_highlight(game_over_get_button_from_sel(prev_selection), false);
if (row_idx == new_selection->y)
button_set_highlight(game_over_get_button_from_sel(new_selection), true);
return true;
}
// Buttons
static void reuse_seed_on_pressed(void)
{
reuse_seed = !reuse_seed;
main_bg_se_copy_rect(
reuse_seed ? REUSE_SEED_BTN_ON_SRC_RECT : REUSE_SEED_BTN_OFF_SRC_RECT,
REUSE_SEED_BTN_DEST_POS
);
}
static void new_run_on_pressed(void)
{
game_change_state(GAME_STATE_RUN_SETUP);
}
static void main_menu_on_pressed(void)
{
game_change_state(GAME_STATE_MAIN_MENU);
}
+7 -1
View File
@@ -497,8 +497,11 @@ static inline void game_round_execute_play_hand(void)
return;
set_hand_state(HAND_PLAY);
--g_game_vars.hands;
display_hands();
g_game_vars.nb_played_hands[get_hand_type() - 1]++;
}
/**
@@ -1069,6 +1072,9 @@ static inline void game_round_process_input_and_state(void)
lerped_temp_score = int2fx(temp_score);
lerped_score = int2fx(g_game_vars.score);
if (temp_score > g_game_vars.best_hand_score)
g_game_vars.best_hand_score = temp_score;
display_temp_score(temp_score);
g_game_vars.chips = 0;
@@ -2071,4 +2077,4 @@ void game_round_on_update(void)
// animate score flames if we exceed the score requirement
game_round_process_flaming_score();
}
}
+29 -9
View File
@@ -18,6 +18,7 @@
#include "state_machine.h"
#include "util.h"
#include <string.h>
#include <tonc.h>
// Palette Indices
@@ -29,13 +30,13 @@
#define BLUE_DISABLED_BTN_MAIN_COLOR_PAL_IDX 13
#define BACK_BTN_MAIN_COLOR_PAL_IDX 14
#define NEW_RUN_BTN_OUTLINE_COLOR_PAL_IDX 30
#define RESUME_BTN_OUTLINE_COLOR_PAL_IDX 31
#define CHANGE_DECK_BTN_OUTINE_COLOR_PAL_IDX 32
#define SEED_CHECK_BTN_OUTLINE_COLOR_PAL_IDX 33
#define SEED_DECK_BTN_OUTLINE_COLOR_PAL_IDX 34
#define PLAY_BTN_OUTLINE_COLOR_PAL_IDX 35
#define BACK_BTN_OUTLINE_COLOR_PAL_IDX 36
#define NEW_RUN_BTN_OUTLINE_COLOR_PAL_IDX 30
#define RESUME_BTN_OUTLINE_COLOR_PAL_IDX 31
#define CHANGE_DECK_BTN_OUTLINE_COLOR_PAL_IDX 32
#define SEED_CHECK_BTN_OUTLINE_COLOR_PAL_IDX 33
#define SEED_DECK_BTN_OUTLINE_COLOR_PAL_IDX 34
#define PLAY_BTN_OUTLINE_COLOR_PAL_IDX 35
#define BACK_BTN_OUTLINE_COLOR_PAL_IDX 36
#define KEYBOARD_1_BTN_OUTLINE_COLOR_PAL_IDX 40
#define KEYBOARD_2_BTN_OUTLINE_COLOR_PAL_IDX 41
@@ -292,7 +293,7 @@ static SelectionGrid choose_deck_selection_grid = {
static void change_deck_on_pressed(void);
static Button change_deck_button = {
CHANGE_DECK_BTN_OUTINE_COLOR_PAL_IDX,
CHANGE_DECK_BTN_OUTLINE_COLOR_PAL_IDX,
CHANGE_DECK_BTN_MAIN_COLOR_PAL_IDX,
change_deck_on_pressed,
NULL
@@ -566,6 +567,7 @@ enum RunSetupResumeRows
void game_run_setup_change_background(void)
{
tte_erase_screen();
GRIT_CPY(pal_bg_mem, background_run_setup_gfxPal);
GRIT_CPY(&tile_mem[MAIN_BG_CBB], background_run_setup_gfxTiles);
GRIT_CPY(&se_mem[MAIN_BG_SBB], background_run_setup_gfxMap);
@@ -576,6 +578,22 @@ void game_run_setup_on_init(void)
state_machine_register(&run_setup_sm);
game_run_setup_change_background();
// Apply the current use_seed value if seed is UNDEFINED
if (g_game_vars.rng_info.seed == UNDEFINED)
{
// Make the string empty instead of showing all zeroes
memset(seed_str, '\0', BASE36_MAX_DIGITS + 1);
seed_cursor_pos = 0;
}
// Or use previous Run's seed if it hasn't been reset
else
{
u32_to_base36(g_game_vars.rng_info.seed, seed_str);
seed_cursor_pos = BASE36_MAX_DIGITS;
use_seed = true;
}
toggle_seed_enabled(use_seed);
// Rank doesn't matter, won't see it
run_setup_deck = card_object_new(card_new(SPADES, ACE));
@@ -660,9 +678,9 @@ static void choose_deck_substate_init(void)
// Set button highlights
button_set_highlight(&change_deck_button, true);
button_set_highlight(&choose_deck_bottom_buttons[RUN_SETUP_DECK_BB_USE_SEED], false);
toggle_seed_enabled(use_seed); // This just re-applies the current value
button_set_highlight(&choose_deck_bottom_buttons[RUN_SETUP_DECK_BB_PLAY], false);
button_set_highlight(&back_button, false);
toggle_seed_enabled(use_seed);
// Print button text
tte_printf(
@@ -1161,6 +1179,8 @@ static void play_on_pressed(void)
else
rng_shuffle_seed();
use_seed = false;
game_change_state(GAME_STATE_GAME_START);
}
+10
View File
@@ -75,6 +75,16 @@ static Hand hand = {
static ContainedHandTypes compute_contained_hand_types(void);
static enum HandType compute_hand_type(struct ContainedHandTypes contained_types);
// Misc Hand Functions
const char* get_hand_type_name(enum HandType hand_type)
{
if (hand_type <= NONE || hand_type > FLUSH_FIVE)
return NULL;
return hand_base_values[hand_type].display_name;
}
// Hand Struct Manipulation
enum HandState get_hand_state(void)