From 43d837571ec1d4494459321acfbb8f1e1aa49bfe Mon Sep 17 00:00:00 2001 From: MeirGavish Date: Tue, 2 Dec 2025 08:28:36 +0200 Subject: [PATCH] Truncate Scores (#269) * Fixed score truncation erasure, and centering for chips, mult, score and "temp score * Added centering for money * Added tests for truncate_uint_to_suffixed_str() * Fixed u32 bug with get_digits * Fixed score flames for large numbers * Small change to update documentation * Added 1 pixel on to score rect on the left to make room for full character tile + reset_top_left_panel_bottom_row() refactor * Replaced directional defines with enums * Updated ante_lut comment --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .gitignore | 6 +- graphics/background_blind_select_gfx.png | Bin 3848 -> 3996 bytes graphics/background_gfx.png | Bin 19072 -> 5393 bytes graphics/background_shop_gfx.png | Bin 3623 -> 3860 bytes include/blind.h | 2 +- include/graphic_utils.h | 73 ++++++---- include/util.h | 51 +++++-- source/blind.c | 9 +- source/game.c | 172 +++++++++++------------ source/graphic_utils.c | 56 +++++--- source/util.c | 40 ++++++ tests/run_tests.sh | 1 + tests/util/Makefile | 16 +++ tests/util/util_test.c | 133 ++++++++++++++++++ 14 files changed, 411 insertions(+), 148 deletions(-) create mode 100644 tests/util/Makefile create mode 100644 tests/util/util_test.c diff --git a/.gitignore b/.gitignore index fc00239..e30fdf5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,10 @@ /build /.vs *.sln -*.vcxproj* -/balatro-gba/x64/** +*.vcxproj* +/balatro-gba/x64/** /tests/bitset/build /tests/list/build /tests/pool/build +tests/util/.vscode +tests/util/build diff --git a/graphics/background_blind_select_gfx.png b/graphics/background_blind_select_gfx.png index d7335e565486ac8bf67a80ce52450d10917a6e47..b842183e2fd1dfad006b295e5a2885db9a9ee724 100644 GIT binary patch literal 3996 zcma)9c{r4P+a3m^(4@lH8YxAR9$VJ2G!#AdBHKiEjZC(fEQ5-QgqUQqBqY1+>rmOZ zj3r~6LY6U6V+;oK-p}{F$M+rYalG4K_kAC~`?#*_cb?aFKDIEsEO1clAP58!FusDc z0-oz25C@2t2Y5>;XX%1KP*-E5{|$z;{9_O15OwJVvmgUX6p)`D3+)#Ufn z_gY6D`3xt%Dl+ws*r`CH(bszJ@IC+J>`#n0RhNt>Ta+joi7q)+ zu!!Io#@FRjbkOQkfpzER<65uXj^n~BI5we)b7OR7gK;hRxsw}?#2*9Up;oqxbyX?g z(9&Rn2dwX~nCx*8f6x;{9`!mnH7jwl(;W`$x;Y<8Nvb$tcUwieV2|Ern^?3MtpVYu z#dDx;tGJb;%)`axw$6T!uox2)@N9L%$##GepRJ`7@!_AMuX-DGGE=9yq*A(0sKC)g zUx|nR%Tdy$CGm@Qj*ULEqLHhMR!;+Wy-$faPqe6nfW#H#|HnF{~yy;I^2a zwkIdQRP)zx%9~YG-D&FV)|0q6e#hM_KP9Iu{hxxEU%=oe8b>LBK{v|x%f32);JgO7 zV0zdAt`NX{$sTH;pR*eY*~)4iY~>aVB5ZF(*4y4hO6sAc6AjjOC>q_YbnZq($iUVN zN&Md9d5Q9Ziv-aJph8jb?gZJi{vPJC8}W$+l8A6wRf>@aTCR#q-i$^ z_WHs&jP<^YGU_-@HW0j+mFCS#s(Act`5f&4i<3j8>lnrPWzW)=H!a87$%{~}A$cWk zb=adiOrZFBIW4yZ*-rKpy9IuAeOGzZr}}#1T1271=s~y*5A;;3Wn@6LT)(+)T0%mC z$*!>8_L1qM@@2VI#+U4a_i#nGVuwOAZy8$!PcXpe=L`=0vuds27@#|PCs+2C!ZGQ; z44zdRUE-kz33jt%TE@Hr2`LJV5mJTeSLr2XkwH^F>F`}o{X=GEpHPBcO)spF2OH({ zmVbmAjt}O&xbnunq^4<^u8jW_9HL`vu)5(S2Uz9(9m`z$l1uxh6DEtD3m_GaUH$!; zsDDl>=47G{xrGQfQGB-_%C4k@VKVA)ti#U4n(_-Qd?-ou5>-Uw!B2c{l>{+bXfgle zM^OSaW77{W-PZwPeVi=`yVM1#vHIm}kcy=! z4u|NEv(9>^mkV=y;`E{XvNkJ{8a-pZk>VzYgZDl}!pGV2r%3yLJ$o)^es)=^{p{k9 zk7iZc9lAB4c~D2QrL0`*){6vj2vL)yUtGtu^51zTfjgToU;L*I2Jo(aP~dCwMw;gI z3qhi#my}N0~D^i*gbsR zF4q{5ecRalWKdh!arM*MSD(_Al+-Fz6{IEUGXU@w$@i^f3Qul{)kNIb!wI#yR7Tdb zRnc}G8Dw`c1IQ(5>71Mck7P_k+FSIJ!$d!otYtAbb{?LUy@HWNi;10aQjF?j5nc4- zZsji0nLD@-?WM&}8dA~gd&iU@>b}p@gcq%25yfVaqHVvw_pH@K5M@{VyUI~&E4EkU z+Rp?vi)6nJ1|)r!u%`Wovxl>&gUU%tT5N=zdmdvpZI2}ajA1FHnA3; z;jP$Ha|P0uRv$9I-Aw!78Mr;~<`&nyj=-!p8eTL-A`w}3wEDxgE%ZJ33njvr?`x4e zu7+=wn;43NmLhpyyyD`zruO9uTSDKus>AsG>}Xo75#PT&4sC9Kbg*H0c+RBwj^<>F zn$1posknuNsN>u5c)THU1P{MfHeQQ3EG_*(aY5NFoKk>~4>HOL�_1Pig#!*qwy% zAd&N9JJ`)w$nc}};q--R@TZS!qn#>U3F4{V8`Uxj;H&9~~# z5;S%eoPQwm0(Oux5t4&=>Ojjz;AHFtKJvH|%h7Yn&g6&`+n>a;Oh%{VPrXx%HHEaw zHf%f85Z*m7?<_eQ*3Uc3hd=95SBJ3e`jY zlrS{)7c1=Bl+j4I=E1`LR2c@cYN0GLM z1}oJ@_V}c$m2Hv%BLj$_N&8UgTMVDF<`jrq@DH8a3y-gO+uj#S06AQ!jWlul79AH? zHKTh%S~}Gao;t=lLq;_-=klACd>?MDTGuXYY?yTZ_MELdD=n?4`$vN+`}fgYhu*!l zptO3DeVivivz$haYGf0mAujIE>k-3hYYOyMxa0Uu58VeZvyT2C!jXzYGgTs9wp?K9 z%{vGE1b4q}{D@i0n4zBG`daC%x91`(SkNx-UofuPY@0S$GM><;!f4-%1Bsl2Emfvt zDV2=-7gD5&X?Zl4m`kf~(%T(^-^F}m3})oB1R)jZ8+B4j`=&_``FwSXFG@1S2%9~X zK-5pP{k=Rpqlzg*6B&t<%x3GcJMy%QWLJMj)P&}9q3Tv~0#w|C)w>e3K@?uK+s`1m z?|=WUGuG3S+jozdS!-jH>D~|ra=_5qvZ`MjDG5AnUM>$JKN{o{QR}Um|4!Gc!OjA> zkkzygWgK$o)Q!Yq4bLN5Kiad>O2g_#U61<#o0;PwhXPt$wM0suJly_vRxdB+P%`1{ zK`ncy5dl6hXc=Fwo*8fXB~|@%l4H zk_v9#C!GTfM1M0q@bY6&R3;sb-v^xbf9M>ewtphzrLx&iGJKqTxj#4 z{2o$_`j8O*ueNnGRlA&Rl3PUQQ>r8VOx3zOW@F+KU(d z40{bztU_dqz{);JTi{qK#_yWDeR~5V@Z-b@WX}|4yKl`Q~+`tbG zdrkv`9Z31MY%Hj<+a10&w$qRr1VVl|PH~`8eRZfnM;R&~W^qpU?#TvT7}{UhXxF>v zKLO#f;HTcp+hD$*sRG*Ty}IXMi#`)vWAot{J`EhYSRjX`@SuOxBZo0cWn80@T7tbN z3%Jcax+uTf-Ru2e@xloa#f>UXmLL`I$FUH6l0YfEHX4Q(dGrVp_N)bi5b;I_z%V(swB4(O7X;i4g zy@-w3)XG0gXHtjRYt|baFmvB`X-nHg_Zr1+dQ>m^W||=tzs&sjbF)<2s1sN z*;q8P2#)k$SDj#bs$TF7Y3>^e0p8Mv(ZHN7q-Z%NIY3u#xv$~ jXD2`SYWDvp>MFZCeQ`?Uj0gvyx;M2T6GNSA)}d3R8B>pQOMIO_PHCveylCxl^_PSxuXfew zRAX0-;U2i1VT;^h;|sEPv|vJUY~PGchc)Tsqp6}y4W9?y>0$_eG-Bm>{TZic35`U$ zywP7#wl&Oj*eLJvU?vI)1`1#1E%DmH0phycwyNCriDR%E!j0c>d*n3C&CT8DivbBQdi7bN+8I;5Y6cCm`J?CZ;Ih-Y9AlPfKvwX44qh9SR zaF(e``AN`VT$0~e2!!)-WM1{_@j8?fItY;jF}F3UTHonQBd#0b`jh&0=JS4nOM*>cBNHVSU5vMphv9(bwC8H!QFp(nWn<= zunNZpDWrr@DJ98P$*nI=NEV6^PJg;=U81$`JWT$Xb+9^O4^2rY;D^YwXuZE%-QTr_ z6#PJwuzPS}I_Mf!v55AY3<31_3z-s<87oh@=Rg$Q*Vk&L>|<(;d>rz8vWlWv7NGhQ z9J&k5?6{E?1Kgxh3F~NTSr8F=86J7kUfa+#y}PWyC%;kBIJpCF8{hi0bTT{Qi^8-A zJ|DNua`nDdCUM4j+wTw^4mRDJ3id)zAQb+*NCgwoi#LWes4MpxJ78y&37*{T?Xn&O zyt=@S`Fk8X9PTaHYqXy2-#Cq(4QaF1FT;jNjPec_IJKls}P<$p^&3Ud#8?m{tl@W&bB6tQB``i#`UJ zjO`pnzWUCUse(9NEgo}iC0~O-F*A?JXI(~JEQv1;wlQjZv{Zr*H!IOOv*_RW)dcYj z!7{Uu#C{7`Y1C~a>l_g3?(W{)f$|nT=-^tqg_rKppb5T3fmZstEHY#MR(pkNRBa>N zEYD7Zo%0?gms%HWE5)#Ygur18>r^(i+5q6{H&%_>y}`LH7i z_FPlA9_3`GL2SXuWDHHA(1a7mg!xNt3`G&#qfJm>IpHAyacds(6vsCW`DHG+#Lp?G zSSalA5@|^96BFne&wq?r4-p&t&}gqx{feMz1@}-W3`b$1%UahXBN^rH>%vCf3Pah5 zS~wTD`>YyC;b+oS{)R7!=Sux_VICV=`_1ge<4((9iecfb9r9A=FgopPT%QWZZ}Hsf zppg%Lj=%T{LH(rmaX2Mo;h(ffwso#9c=Ie5)-ii002I}H> z`cww7+hyY8#fder-H*f*CnB`&Nzh5thiikoVV2FUO+%grp9+{xV06qKi}f;Qdw9aACJo%k7Oj>9bTSQ3t@^JG#o1T~ zg*o>M1fFQV9-^_CEc6xnKwG=f-#@%=TjqjfCoX+=qy0L~yK+#JY;f1yhW}_GnS^i5 zM!XG7;ISGZLXRDlPGjSjnf*R}egw;=Pi|hFYzZfre|5X=i7NA7IOg?VXvtQ*CnY}= zS6kV3p;G{@!*eC=0??Ck>>WM1|2xkc|Q8^)lbQd?l#-Q2nhSc@@2*s2P=Lew{PLGcmDOmaLoV zbk!j7drV{Ml)2lR4FW1&ZZnm98_{>8!3?PNcbhUG!_=!;u;_v}SCZ;{?ZS%F-mtLG ztGA;(R?<^*pCCY?v*+O09aZJreU+#vj)uM}S6nZ%SM7M+w)(Ez0I{g3sOHQTgX4qn8{gX6 zrt`D$BiUEst9j~h`iSJO11PS7h%zscH7Mz3it09>q%K_wIjn8`q0XU6?jlY7q}PoT zec7(T7e1&7GT6gg-36jT%2)wwku!B=;z}t6d?ryyP_G){?x9v8y)UKltwNO2ni2!S= z^!^LItB-Hjpe3@I|dm9^*@|Oz2cb~<_w`?993>-5gCvT{3gqMe8qv~Tq z{eIrPDkK*2-8{&Nl{J;Uq#&?gWv~%pH%D}GW7X;#r%Rito~M7o?h#@zGnidUGj`H~ z2pyS+y|mMu*|T zUbZ*wWpHnHj){g<+!NGjSlf#5EwN14Zm)3K+!-uly{u{=aKgphwY63fKG1)+t3wWj z3cihnySuAfl}M$ zkgU2l(h06?^SMVmk%&M(y)7cA%81j(%BuRfw6FB_G>O6WNo=cm+x>DJYE6N(%fElz z*vQnlwZ?y!bhG(r*6f4oXxLwM&1<{)6>4MtR$~>d`k0Re;&F*Bd$unXXr9!3?IDKV zyYcmlxJp$EOpe1()da&{n4j*PQ>d-4NUI!8Md__et=K)Lt*={IN;UC<{+zsTlP7K= ziS4^QE_g`jd|88)Wc*F}aD>5An(kCh7PgIDy2eXremaT z(B|ayTaVk}Mm-RTB#AZdx3spa5;gzFd7UZ~3A>TKM$6B{WleU%4n_uDsMGpQWgMq@&CF&U`#=y#SXn=9U1aQQSr5t z7^&7JFT0EYTbeZ(Fzr9)=W}T3Pb77A-vP_5L?EUpc6g&A#(`dzBx2FHtOcb`BSRo( zaRjGGTkYIkl{2Mi*AH()II8gIsya%p=QYpXN_yr7P0gy?J4S-HiHT<`^!p-+T{61 z0nbh2Yd+>M#*#=73Pm1R2z5Z&GsIuTXhFhR`Z{tLZ&@yhBtnq6{7*4@2-9I8+L;B$ zg9r)2!Cgq{0=Nv~Z|^3sHN0V_nn+|BQ6Gu{k432S7hHhf*c}vEldCC3NYvsN1^*Sd cGAXB!^yiZ|@mFTy;8hba)HTzo&_YK23vMzTkpKVy diff --git a/graphics/background_gfx.png b/graphics/background_gfx.png index 432ac0467b530e1f113e4d25019af100199c520a..a7b37228a2d2481dc3c2a27a57485560b735863f 100644 GIT binary patch literal 5393 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 z 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 X=e=NER3Hl7SON+%%F;EG#v%U;8dK5` diff --git a/graphics/background_shop_gfx.png b/graphics/background_shop_gfx.png index c06ebdbc0812495ae7479a27186dcbdd1a6bb398..0de4cd3aa5848225413524feea9b1c0a2fbf6771 100644 GIT binary patch literal 3860 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 literal 3623 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 - ((rect)->left) + 1); + return max(0, rect->right - rect->left + 1); } INLINE int rect_height(const Rect* rect) { - return (((rect)->bottom) - ((rect)->top) + 1); + return max(0, rect->bottom - rect->top + 1); } /* Copies an SE rect vertically in direction by a single tile. * bg_sbb is the SBB of the background in which to move the rect - * direction must be either SE_UP or SE_DOWN. * se_rect dimensions are in number of tiles. * * NOTE: This does not work with TTE_SBB, probably because it's 4BPP... */ -void bg_se_copy_rect_1_tile_vert(u16 bg_sbb, Rect se_rect, int direction); +void bg_se_copy_rect_1_tile_vert(u16 bg_sbb, Rect se_rect, enum ScreenVertDir direction); /* Clears a rect in the main background. * The se_rect dimensions need to be in number of tiles. @@ -105,10 +108,9 @@ void bg_se_copy_rect_1_tile_vert(u16 bg_sbb, Rect se_rect, int direction); void main_bg_se_clear_rect(Rect se_rect); /* Copies a rect in the main background vertically in direction by a single tile. - * direction must be either SE_UP or SE_DOWN. * se_rect dimensions are in number of tiles. */ -void main_bg_se_copy_rect_1_tile_vert(Rect se_rect, int direction); +void main_bg_se_copy_rect_1_tile_vert(Rect se_rect, enum ScreenVertDir direction); /* Copies a rect in the main background from se_rect to the position (x, y). * se_rect dimensions are in number of tiles. @@ -134,27 +136,46 @@ void main_bg_se_copy_expand_3x3_rect(Rect se_rect_dest, BG_POINT se_rect_src_3x3 /* Moves a rect in the main background vertically in direction by a single tile. * Note that tiles in the previous location will be transparent (0x000) * so maybe copy would be a better choice if you don't want to delete things - * direction must be either SE_UP or SE_DOWN. * se_rect dimensions are in number of tiles. */ -void main_bg_se_move_rect_1_tile_vert(Rect se_rect, int direction); +void main_bg_se_move_rect_1_tile_vert(Rect se_rect, enum ScreenVertDir direction); // A wrapper for tte_erase_rect that would use the rect struct void tte_erase_rect_wrapper(Rect rect); -/* Changes rect->left so it fits the digits of num exactly when right aligned to rect->right. - * Assumes num is not negative. +/* Changes rect->left so it fits a string exactly when right aligned to rect->right. * - * overflow_direction determines the direction the number will overflow - * if it's too large to fit inside the rect. - * Should be either OVERFLOW_LEFT or OVERFLOW_RIGHT. + * overflow_direction determines the direction the string will overflow + * if it's too large to fit inside the rect. * - * The rect is in number of pixels but should be a multiple of TILE_SIZE + * The rect is in number of pixels but should be a multiple of TTE_CHAR_SIZE * so it's a whole number of tiles to fit TTE characters * * Note that both rect->left and rect-right need to be defined, top and bottom don't matter */ -void update_text_rect_to_right_align_num(Rect* rect, int num, int overflow_direction); +void update_text_rect_to_right_align_str(Rect* rect, const char* str, enum OverflowDir overflow_direction); + + +/** + * @brief Updates a rect so a string is centered within it. + * + * @param rect The rect provided, the provided values are used to determine the center + * and it is then updated so the string starting in rect->left is centered + * The rect is in number of pixels but should be a multiple of TTE_CHAR_SIZE + * so it's a whole number of tiles to fit TTE characters. + * + * @param str The string, the center of the string will be at the center of the updated rect. + * + * @param bias_direction Which direction to bias when the string can't be evenly centered + * with respect to char tiles. + * Examples: + * | |S|T|R| | - Can be evenly centered, bias has no effect + * | | |S|T|R| | - Bias right + * | |S|T|R| | | - Bias left + * |A|B|C|D| | - Bias left + * | |A|B|C|D| - Bias right + */ +void update_text_rect_to_center_str(Rect* rect, const char* str, enum ScreenHorzDir bias_direction); /*Copies 16 bit data from src to dst, applying a palette offset to the data. * This is intended solely for use with tile8/8bpp data for dst and src. diff --git a/include/util.h b/include/util.h index 4c2fd7e..413f0d9 100644 --- a/include/util.h +++ b/include/util.h @@ -3,9 +3,10 @@ #include + #define GBLA_UNUSED __attribute__((unused)) -static inline int get_digits(int n) // https://stackoverflow.com/questions/1068849/how-do-i-determine-the-number-of-digits-of-an-integer-in-c +static inline int u32_get_digits(uint32_t n) // https://stackoverflow.com/questions/1068849/how-do-i-determine-the-number-of-digits-of-an-integer-in-c { if (n < 10) return 1; if (n < 100) return 2; @@ -19,7 +20,7 @@ static inline int get_digits(int n) // https://stackoverflow.com/questions/10688 return 10; } -static inline int get_digits_odd(int n) +static inline int get_digits_even(int n) { if (n < 100) return 1; if (n < 10000) return 2; @@ -28,15 +29,6 @@ static inline int get_digits_odd(int n) return 5; } -static inline int get_digits_even(int n) -{ - if (n < 10) return 1; - if (n < 1000) return 2; - if (n < 100000) return 3; - if (n < 10000000) return 4; - return 5; -} - #define UNDEFINED -1 #define NUM_ELEM_IN_ARR(arr) (sizeof(arr) / sizeof((arr)[0])) @@ -45,8 +37,45 @@ static inline int get_digits_even(int n) #define UINT_MAX_DIGITS 10 // strlen(str(UINT32_MAX)) = strlen("4294967295") #define UINT8_MAX_DIGITS 3 // strlen(str(UINT8_MAX)) = strlen("255") +#define ONE_K 1000 +#define ONE_M 1000000 +#define ONE_B 1000000000 + +#define ONE_K_ZEROS 3 +#define ONE_M_ZEROS 6 +#define ONE_B_ZEROS 9 + +// The suffix replaces everything past the third digit, e.g. "999K" -> "1M" +// so it needs at least this number of chars to be able to display any suffixed number +#define SUFFIXED_NUM_MIN_REQ_CHARS 4 + int int_arr_max(int int_arr[], int size); +/** + * @brief Truncate an unsigned number into a suffixed string representation e.g. 12000 -> "12K" + * The least significant digits are rounded down e.g. 12345 -> "12K", 12987 -> "12K" + * + * @param num The number to truncate, can be anything from 0 to UINT32_MAX. + * + * @param num_req_chars The number of characters to constrain the string to. + * The function will use up as much characters as it can + * in order to maintain as much accuracy as possible. + * So numbers are not fully truncated if not necessary, + * e.g. 123123000 -> "123123K" for example value 7, + * and if num_req_chars > u32_get_digits(num) the number will not + * be truncated at all. + * Passing less than SUFFIXED_NUM_MIN_REQ_CHARS may result in an + * output string longer than num_req_chars but + * can be done to truncate 1000s -> "1K", 2000 -> "2K" etc. + * which wouldn't be otherwise. + * + * @param out_str An output buffer to write the resulting string to. + * Must be of size UINT_MAX_DIGITS + 1. + 1 for null-terminator. + * At that size the suffix character will always be accounted for since + * a number with more digits than UINT_MAX_DIGITS will not be handled nor truncated. + */ +void truncate_uint_to_suffixed_str(uint32_t num, int num_req_chars, char out_str[UINT_MAX_DIGITS + 1]); + uint32_t u32_protected_add (uint32_t a, uint32_t b); uint16_t u16_protected_add (uint16_t a, uint16_t b); uint32_t u32_protected_mult(uint32_t a, uint32_t b); diff --git a/source/blind.c b/source/blind.c index d1f1078..55326c1 100644 --- a/source/blind.c +++ b/source/blind.c @@ -8,9 +8,10 @@ #include -// +1 is added because we'll actually be indexing at 1, but if something causes you to go to ante 0, there will still be -// a value there. -static const int ante_lut[MAX_ANTE + 1] = {100, 300, 800, 2000, 5000, 11000, 20000, 35000, 50000}; +// Maps the ante number to the base blind requirement for that ante. +// The game starts at ante 1 which is at index 1 for base requirement 300. +// Ante 0 is also there in case it is ever reached. +static const u32 ante_lut[] = {100, 300, 800, 2000, 5000, 11000, 20000, 35000, 50000}; // Palettes for the blinds (Transparency, Text Color, Shadow, Highlight, Main Color) Use this: // http://www.budmelvin.com/dev/15bitconverter.html @@ -69,7 +70,7 @@ void blind_init() return; } -int blind_get_requirement(enum BlindType type, int ante) +u32 blind_get_requirement(enum BlindType type, int ante) { // Ensure ante is within valid range if (ante < 0 || ante > MAX_ANTE) diff --git a/source/game.c b/source/game.c index b84fe3b..5da5bca 100644 --- a/source/game.c +++ b/source/game.c @@ -342,20 +342,6 @@ static inline Card *discard_pop() return discard_pile[discard_top--]; } -// Resets bottom row bg tiles of the top left panel (shop/blind) after -// it is dismissed to match the rest of the game panel background. -static inline void reset_top_left_panel_bottom_row() -{ - int y = 6; - - memset16(&se_mem[MAIN_BG_SBB][32 * (y - 1)], 0x0006, 1); - memset16(&se_mem[MAIN_BG_SBB][1 + 32 * (y - 1)], 0x0007, 2); - memset16(&se_mem[MAIN_BG_SBB][3 + 32 * (y - 1)], 0x0008, 1); - memset16(&se_mem[MAIN_BG_SBB][4 + 32 * (y - 1)], 0x0009, 3); - memset16(&se_mem[MAIN_BG_SBB][7 + 32 * (y - 1)], 0x000A, 1); - memset16(&se_mem[MAIN_BG_SBB][8 + 32 * (y - 1)], 0x0406, 1); -} - // get-functions, for other files to view game state (mainly for jokers) CardObject **get_hand_array(void) { @@ -552,6 +538,7 @@ static const Rect TOP_LEFT_PANEL_ANIM_RECT = {0, 0, 8, 4 }; * TOP_LEFT_PANEL_ANIM_RECT should be used for animations, * TOP_LEFT_PANEL_RECT for copies etc. but mind the overlap */ +static const Rect TOP_LEFT_PANEL_BOTTOM_ROW_RESET_RECT = {0, 28, 8, 28 }; static const BG_POINT TOP_LEFT_BLIND_TITLE_POINT = {0, 21, }; static const Rect BIG_BLIND_TITLE_SRC_RECT = {0, 26, 8, 26 }; static const Rect BOSS_BLIND_TITLE_SRC_RECT = {0, 27, 8, 27 }; @@ -580,7 +567,7 @@ static const Rect HAND_SIZE_RECT_PLAYING = {128, 152, 152, 160 }; static const Rect HAND_TYPE_RECT = {8, 64, 64, 72 }; // Score displayed in the same place as the hand type static const Rect TEMP_SCORE_RECT = {8, 64, 64, 72 }; -static const Rect SCORE_RECT = {32, 48, 64, 56 }; +static const Rect SCORE_RECT = {24, 48, 64, 56 }; static const Rect PLAYED_CARDS_SCORES_RECT = {72, 48, 240, 56 }; static const Rect HELD_CARDS_SCORES_RECT = {72, 108, 240, 116 }; @@ -647,9 +634,6 @@ static const BG_POINT MAIN_MENU_ACE_T = {88, 26}; #define STARTING_MONEY 4 #define STARTING_SCORE 0 -#define TEN_K 10000 -#define ONE_K 1000 - #define CARD_FOCUSED_UNSEL_Y 10 #define CARD_UNFOCUSED_SEL_Y 15 #define CARD_FOCUSED_SEL_Y 20 @@ -886,6 +870,16 @@ void bg_copy_current_item_to_top_left_panel() main_bg_se_copy_rect(TOP_LEFT_ITEM_SRC_RECT, TOP_LEFT_PANEL_POINT); } +// Resets bottom row bg tiles of the top left panel (shop/blind) after +// it is dismissed to match the rest of the game panel background. +static inline void reset_top_left_panel_bottom_row() +{ + BG_POINT top_left_panel_bottom_row_pos = TOP_LEFT_PANEL_POINT; + // Use the source rect height to offset to the bottom row point + top_left_panel_bottom_row_pos.y += rect_height(&TOP_LEFT_ITEM_SRC_RECT) - 1; + main_bg_se_copy_rect(TOP_LEFT_PANEL_BOTTOM_ROW_RESET_RECT, top_left_panel_bottom_row_pos); +} + void change_background(enum BackgroundId id) { if (background == id) @@ -948,7 +942,7 @@ void change_background(enum BackgroundId id) for (int i = 0; i <= 2; i++) { - main_bg_se_move_rect_1_tile_vert(HAND_BG_RECT_SELECTING, SE_DOWN); + main_bg_se_move_rect_1_tile_vert(HAND_BG_RECT_SELECTING, SCREEN_DOWN); } tte_erase_rect_wrapper(HAND_SIZE_RECT_SELECT); @@ -1046,7 +1040,7 @@ void change_background(enum BackgroundId id) int x_from = 0; int y_from = 27; - main_bg_se_copy_rect_1_tile_vert(curr_blind_rect, SE_UP); + main_bg_se_copy_rect_1_tile_vert(curr_blind_rect, SCREEN_UP); int x_to = curr_blind_rect.left; int y_to = 31; @@ -1129,53 +1123,49 @@ void change_background(enum BackgroundId id) void display_temp_score(u32 value) { - int x_offset = 40 - get_digits_even(value) * TILE_SIZE; + char temp_score_str_buff[UINT_MAX_DIGITS + 1]; + Rect temp_score_rect = TEMP_SCORE_RECT; + truncate_uint_to_suffixed_str(value, rect_width(&temp_score_rect)/TTE_CHAR_SIZE, temp_score_str_buff); + update_text_rect_to_center_str(&temp_score_rect, temp_score_str_buff, SCREEN_RIGHT); + tte_erase_rect_wrapper(TEMP_SCORE_RECT); - tte_printf("#{P:%d,%d; cx:0x%X000}%lu", x_offset, TEMP_SCORE_RECT.top, TTE_WHITE_PB, value); + tte_printf("#{P:%d,%d; cx:0x%X000}%s", temp_score_rect.left, temp_score_rect.top, TTE_WHITE_PB, temp_score_str_buff); } void display_score(u32 value) { + Rect score_rect = SCORE_RECT; // Clear the existing text before redrawing tte_erase_rect_wrapper(SCORE_RECT); - char score_suffix = ' '; - u32 display_value = value; + char score_str_buff[UINT_MAX_DIGITS + 1]; + + truncate_uint_to_suffixed_str(value, rect_width(&score_rect)/TTE_CHAR_SIZE, score_str_buff); + update_text_rect_to_center_str(&score_rect, score_str_buff, SCREEN_RIGHT); - if(value >= TEN_K) - { - score_suffix = 'k'; - display_value = value / ONE_K; // 12,986 = 12k - } - - // Calculate text width: digits + suffix character (if 'k') - int num_digits = get_digits(display_value); - int text_width = num_digits * TILE_SIZE; - if(score_suffix == 'k') - { - text_width += TILE_SIZE; // Add width for 'k' suffix - } - - // Calculate center position within SCORE_RECT - int rect_width = SCORE_RECT.right - SCORE_RECT.left; - int x_offset = SCORE_RECT.left + (rect_width - text_width) / 2; - - tte_printf("#{P:%d,48; cx:0x%X000}%lu%c", x_offset, TTE_WHITE_PB, display_value, score_suffix); + tte_printf("#{P:%d,%d; cx:0x%X000}%s", score_rect.left, score_rect.top, TTE_WHITE_PB, score_str_buff); } void display_money() { - int x_offset = 32 - get_digits_odd(money) * TILE_SIZE; + Rect money_text_rect = MONEY_TEXT_RECT; tte_erase_rect_wrapper(MONEY_TEXT_RECT); - tte_printf("#{P:%d,%d; cx:0x%X000}$%d", x_offset, MONEY_TEXT_RECT.top, TTE_YELLOW_PB, money); + + char money_str_buff[INT_MAX_DIGITS + 2]; // + 2 for null terminator and "$" sign + snprintf(money_str_buff, sizeof(money_str_buff), "$%d", money); + + // Bias left so the number is centered and the "$" sign is on the left + update_text_rect_to_center_str(&money_text_rect, money_str_buff, SCREEN_LEFT); + + tte_printf("#{P:%d,%d; cx:0x%X000}%s", money_text_rect.left, money_text_rect.top, TTE_YELLOW_PB, money_str_buff); } // Show/Hide flaming score effect if we will score // more than the required amount or not void check_flaming_score() { - int curr_score = chips * mult; - int required_score = blind_get_requirement(current_blind, ante); + u32 curr_score = u32_protected_mult(chips, mult); + u32 required_score = blind_get_requirement(current_blind, ante); if (curr_score >= required_score && !score_flames_active) { // start flaming score @@ -1198,16 +1188,32 @@ void check_flaming_score() void display_chips() { Rect chips_text_rect = CHIPS_TEXT_RECT; - tte_erase_rect_wrapper(CHIPS_TEXT_RECT); - update_text_rect_to_right_align_num(&chips_text_rect, chips, OVERFLOW_LEFT); - tte_printf("#{P:%d,%d; cx:0x%X000;}%lu", chips_text_rect.left, chips_text_rect.top, TTE_WHITE_PB, chips); + + // In case of overflow, the rect overflow left by 1 char + Rect chips_text_overflow_rect = chips_text_rect; + chips_text_overflow_rect.left -= TTE_CHAR_SIZE; + tte_erase_rect_wrapper(chips_text_overflow_rect); + + char chips_str_buff[UINT_MAX_DIGITS + 1]; + truncate_uint_to_suffixed_str(chips, rect_width(&chips_text_rect)/TTE_CHAR_SIZE, chips_str_buff); + + update_text_rect_to_right_align_str(&chips_text_rect, chips_str_buff, OVERFLOW_LEFT); + + tte_printf("#{P:%d,%d; cx:0x%X000;}%s", chips_text_rect.left, chips_text_rect.top, TTE_WHITE_PB, chips_str_buff); check_flaming_score(); } void display_mult() { - tte_erase_rect_wrapper(MULT_TEXT_RECT); - tte_printf("#{P:%d,%d; cx:0x%X000;}%lu", MULT_TEXT_RECT.left, MULT_TEXT_RECT.top, TTE_WHITE_PB, mult); + Rect mult_text_overflow_rect = MULT_TEXT_RECT; + // In case of overflow the rect will overflow right by 1 char + mult_text_overflow_rect.right += TTE_CHAR_SIZE; + tte_erase_rect_wrapper(mult_text_overflow_rect); + + char mult_str_buff[UINT_MAX_DIGITS + 1]; + truncate_uint_to_suffixed_str(mult, rect_width(&MULT_TEXT_RECT)/TTE_CHAR_SIZE, mult_str_buff); + + tte_printf("#{P:%d,%d; cx:0x%X000;}%s", MULT_TEXT_RECT.left, MULT_TEXT_RECT.top, TTE_WHITE_PB, mult_str_buff); check_flaming_score(); } @@ -1415,30 +1421,16 @@ static void game_round_on_init() } Rect blind_req_text_rect = BLIND_REQ_TEXT_RECT; - int blind_requirement = blind_get_requirement(current_blind, ante); + u32 blind_requirement = blind_get_requirement(current_blind, ante); - // TODO: Address Copilot review at - // https://github.com/cellos51/balatro-gba/pull/46#pullrequestreview-3045772903 - char score_suffix = ' '; - if(blind_requirement >= TEN_K) - { - // clear existing text - tte_erase_rect_wrapper(blind_req_text_rect); - - score_suffix = 'k'; - blind_requirement /= ONE_K; // 11,000 = 11k - } + char blind_req_str_buff[UINT_MAX_DIGITS + 1]; + + truncate_uint_to_suffixed_str(blind_requirement, rect_width(&BLIND_REQ_TEXT_RECT)/TTE_CHAR_SIZE, blind_req_str_buff); // Update text rect for right alignment AFTER shortening the number - update_text_rect_to_right_align_num(&blind_req_text_rect, blind_requirement, OVERFLOW_RIGHT); - - // If we added a suffix, adjust position to account for the extra character - if(score_suffix == 'k') - { - blind_req_text_rect.left -= TILE_SIZE; // Move left by one character width to make room for 'k' - } + update_text_rect_to_right_align_str(&blind_req_text_rect, blind_req_str_buff, OVERFLOW_RIGHT); - tte_printf("#{P:%d,%d; cx:0x%X000}%d%c", blind_req_text_rect.left, blind_req_text_rect.top, TTE_RED_PB, blind_requirement, score_suffix); // Blind requirement + tte_printf("#{P:%d,%d; cx:0x%X000}%s", blind_req_text_rect.left, blind_req_text_rect.top, TTE_RED_PB, blind_req_str_buff); tte_printf("#{P:%d,%d; cx:0x%X000}$%d", BLIND_REWARD_RECT.left, BLIND_REWARD_RECT.top, TTE_YELLOW_PB, blind_get_reward(current_blind)); // Blind reward deck_shuffle(); // Shuffle the deck at the start of the round @@ -2748,7 +2740,7 @@ static void game_round_end_start() static void game_round_end_start_expand_popup() { - main_bg_se_copy_rect_1_tile_vert(POP_MENU_ANIM_RECT, SE_UP); + main_bg_se_copy_rect_1_tile_vert(POP_MENU_ANIM_RECT, SCREEN_UP); if (timer == TM_END_POP_MENU_ANIM) { @@ -2762,7 +2754,7 @@ static void game_round_end_extend_black_panel_down(int black_panel_bottom) Rect single_line_rect = ROUND_END_MENU_RECT; single_line_rect.bottom = black_panel_bottom; single_line_rect.top = single_line_rect.bottom - 1; - main_bg_se_copy_rect_1_tile_vert(single_line_rect, SE_DOWN); + main_bg_se_copy_rect_1_tile_vert(single_line_rect, SCREEN_DOWN); } static void game_round_end_display_finished_blind() @@ -2773,10 +2765,18 @@ static void game_round_end_display_finished_blind() if (current_blind == BLIND_TYPE_BOSS) current_ante--; // Beating the boss blind increases the ante, so we need to display the previous ante value Rect blind_req_rect = ROUND_END_BLIND_REQ_RECT; - int blind_req = blind_get_requirement(current_blind, current_ante); - update_text_rect_to_right_align_num(&blind_req_rect, blind_req, OVERFLOW_RIGHT); + u32 blind_req = blind_get_requirement(current_blind, current_ante); + + /* Not bothering to truncate here because there are 8 tiles + * and the blind requirement will not increase past ante 8 + * so there's enough room for sure. + */ + char blind_req_str_buff[UINT_MAX_DIGITS + 1]; + snprintf(blind_req_str_buff, sizeof(blind_req_str_buff), "%lu", blind_req); + + update_text_rect_to_right_align_str(&blind_req_rect, blind_req_str_buff, OVERFLOW_RIGHT); - tte_printf("#{P:%d,%d; cx:0x%X000}%d", blind_req_rect.left, blind_req_rect.top, TTE_RED_PB, blind_req); + tte_printf("#{P:%d,%d; cx:0x%X000}%s", blind_req_rect.left, blind_req_rect.top, TTE_RED_PB, blind_req_str_buff); if (timer == TM_START_ROUND_END_REWARDS_ANIM) { @@ -2836,7 +2836,7 @@ static void game_round_end_panel_exit() // magic numbers. if (timer < 8) { - main_bg_se_copy_rect_1_tile_vert(TOP_LEFT_PANEL_ANIM_RECT, SE_UP); + main_bg_se_copy_rect_1_tile_vert(TOP_LEFT_PANEL_ANIM_RECT, SCREEN_UP); if (timer == 1) // Copied from shop. Feels slightly too niche of a function for me personally to make one. { @@ -2989,7 +2989,7 @@ static void game_round_end_dismiss_round_end_panel() { Rect round_end_down = ROUND_END_MENU_RECT; round_end_down.top--; - main_bg_se_copy_rect_1_tile_vert(round_end_down, SE_DOWN); + main_bg_se_copy_rect_1_tile_vert(round_end_down, SCREEN_DOWN); if (timer >= TM_DISMISS_ROUND_END_TM) { @@ -3115,7 +3115,7 @@ static void game_shop_create_items() // Intro sequence (menu and shop icon coming into frame) static void game_shop_intro() { - main_bg_se_copy_rect_1_tile_vert(POP_MENU_ANIM_RECT, SE_UP); + main_bg_se_copy_rect_1_tile_vert(POP_MENU_ANIM_RECT, SCREEN_UP); if (timer == TM_CREATE_SHOP_ITEMS_WAIT) { @@ -3426,9 +3426,9 @@ static void game_shop_lights_anim_frame() static void game_shop_outro() { // Shift the shop panel - main_bg_se_move_rect_1_tile_vert(POP_MENU_ANIM_RECT, SE_DOWN); + main_bg_se_move_rect_1_tile_vert(POP_MENU_ANIM_RECT, SCREEN_DOWN); - main_bg_se_copy_rect_1_tile_vert(TOP_LEFT_PANEL_ANIM_RECT, SE_UP); + main_bg_se_copy_rect_1_tile_vert(TOP_LEFT_PANEL_ANIM_RECT, SCREEN_UP); // TODO: make heads or tails of what's going on here and replace // magic numbers. @@ -3531,7 +3531,7 @@ static void game_blind_select_on_update() static void game_blind_select_start_anim_seq() { change_background(BG_BLIND_SELECT); - main_bg_se_copy_rect_1_tile_vert(POP_MENU_ANIM_RECT, SE_UP); + main_bg_se_copy_rect_1_tile_vert(POP_MENU_ANIM_RECT, SCREEN_UP); for (int i = 0; i < BLIND_TYPE_MAX; i++) { @@ -3579,7 +3579,7 @@ static void game_blind_select_handle_input() // TODO: Create a generic vertical move by any number of tiles to avoid for loops? for (int i = 0; i < 12; i++) { - main_bg_se_copy_rect_1_tile_vert(POP_MENU_ANIM_RECT, SE_UP); + main_bg_se_copy_rect_1_tile_vert(POP_MENU_ANIM_RECT, SCREEN_UP); } for (int i = 0; i < BLIND_TYPE_MAX; i++) @@ -3611,7 +3611,7 @@ static void game_blind_select_selected_anim_seq() { Rect blinds_rect = POP_MENU_ANIM_RECT; blinds_rect.top -= 1; // Because of the raised blind - main_bg_se_move_rect_1_tile_vert(blinds_rect, SE_DOWN); + main_bg_se_move_rect_1_tile_vert(blinds_rect, SCREEN_DOWN); for (int i = 0; i < BLIND_TYPE_MAX; i++) { @@ -3817,7 +3817,7 @@ static void jokers_update_loop() static void game_over_anim_frame() { - main_bg_se_move_rect_1_tile_vert(GAME_OVER_ANIM_RECT, SE_UP); + main_bg_se_move_rect_1_tile_vert(GAME_OVER_ANIM_RECT, SCREEN_UP); } static void game_lose_on_update() diff --git a/source/graphic_utils.c b/source/graphic_utils.c index d85f55b..9e2cbf1 100644 --- a/source/graphic_utils.c +++ b/source/graphic_utils.c @@ -4,6 +4,7 @@ #include "util.h" #include "graphic_utils.h" +#include const Rect FULL_SCREENBLOCK_RECT = { 0, 0, SE_ROW_LEN - 1, SE_COL_LEN - 1}; @@ -31,14 +32,14 @@ SE main_bg_se_get_se(BG_POINT pos) // Clips a rect of screenblock entries to be within one step of // screenblock boundaries vertically depending on direction. -static void clip_se_rect_within_step_of_full_screen_vert(Rect* se_rect, int direction) +static void clip_se_rect_within_step_of_full_screen_vert(Rect* se_rect, enum ScreenVertDir direction) { Rect bounding_rect = FULL_SCREENBLOCK_RECT; - if (direction == SE_UP) + if (direction == SCREEN_UP) { bounding_rect.top += 1; } - else if (direction == SE_DOWN) + else if (direction == SCREEN_DOWN) { bounding_rect.bottom -= 1; } @@ -60,10 +61,10 @@ void main_bg_se_clear_rect(Rect se_rect) } // Internal static function to merge implementation of move/copy functions. -static void bg_se_copy_or_move_rect_1_tile_vert(u16 bg_sbb, Rect se_rect, int direction, bool move) +static void bg_se_copy_or_move_rect_1_tile_vert(u16 bg_sbb, Rect se_rect, enum ScreenVertDir direction, bool move) { if (se_rect.left > se_rect.right - || (direction != SE_UP && direction != SE_DOWN)) + || (direction != SCREEN_UP && direction != SCREEN_DOWN)) { return; } @@ -71,8 +72,8 @@ static void bg_se_copy_or_move_rect_1_tile_vert(u16 bg_sbb, Rect se_rect, int di // Clip to avoid read/write overflow of the screenblock clip_se_rect_within_step_of_full_screen_vert(&se_rect, direction); - int start = (direction == SE_UP) ? se_rect.top : se_rect.bottom; - int end = (direction == SE_UP) ? se_rect.bottom : se_rect.top; + int start = (direction == SCREEN_UP) ? se_rect.top : se_rect.bottom; + int end = (direction == SCREEN_UP) ? se_rect.bottom : se_rect.top; for (int y = start; y != end - direction; y -= direction) { @@ -87,27 +88,27 @@ static void bg_se_copy_or_move_rect_1_tile_vert(u16 bg_sbb, Rect se_rect, int di } } -static void main_bg_se_copy_or_move_rect_1_tile_vert(Rect se_rect, int direction, bool move) +static void main_bg_se_copy_or_move_rect_1_tile_vert(Rect se_rect, enum ScreenVertDir direction, bool move) { bg_se_copy_or_move_rect_1_tile_vert(MAIN_BG_SBB, se_rect, direction, move); } -void bg_se_copy_rect_1_tile_vert(u16 bg_sbb, Rect se_rect, int direction) +void bg_se_copy_rect_1_tile_vert(u16 bg_sbb, Rect se_rect, enum ScreenVertDir direction) { bg_se_copy_or_move_rect_1_tile_vert(MAIN_BG_SBB, se_rect, direction, false); } -void bg_se_move_rect_1_tile_vert(u16 bg_sbb, Rect se_rect, int direction) +void bg_se_move_rect_1_tile_vert(u16 bg_sbb, Rect se_rect, enum ScreenVertDir direction) { bg_se_copy_or_move_rect_1_tile_vert(MAIN_BG_SBB, se_rect, direction, true); } -void main_bg_se_copy_rect_1_tile_vert(Rect se_rect, int direction) +void main_bg_se_copy_rect_1_tile_vert(Rect se_rect, enum ScreenVertDir direction) { main_bg_se_copy_or_move_rect_1_tile_vert(se_rect, direction, false); } -void main_bg_se_move_rect_1_tile_vert(Rect se_rect, int direction) +void main_bg_se_move_rect_1_tile_vert(Rect se_rect, enum ScreenVertDir direction) { main_bg_se_copy_or_move_rect_1_tile_vert(se_rect, direction, true); } @@ -235,22 +236,41 @@ void tte_erase_rect_wrapper(Rect rect) tte_erase_rect(rect.left, rect.top, rect.right, rect.bottom); } -void update_text_rect_to_right_align_num(Rect* rect, int num, int overflow_direction) +void update_text_rect_to_right_align_str(Rect* rect, const char* str, enum OverflowDir overflow_direction) { - int num_digits = get_digits(num); + // TODO: Allow passing string length to avoid calling strlen()? + int str_len = strlen(str); if (overflow_direction == OVERFLOW_LEFT) { - rect->left = max(0, rect->right - num_digits * TILE_SIZE); + rect->left = max(0, rect->right - str_len * TTE_CHAR_SIZE); } else if (overflow_direction == OVERFLOW_RIGHT) { - int num_fitting_digits = rect_width(rect) / TILE_SIZE; - if (num_digits < num_fitting_digits) - rect->left += (num_fitting_digits - num_digits) * TILE_SIZE; + int num_fitting_chars = rect_width(rect) / TTE_CHAR_SIZE; + if (str_len < num_fitting_chars) + rect->left += (num_fitting_chars - str_len) * TTE_CHAR_SIZE; //else nothing is to be updated, entire rect is filled and may overflow } } +void update_text_rect_to_center_str(Rect* rect, const char* str, enum ScreenHorzDir bias_direction) +{ + if (rect == NULL || str == NULL) + { + return; + } + + int text_width_chars = strlen(str); + int rect_width_chars = rect_width(rect) / TTE_CHAR_SIZE; + + bool bias_right = (bias_direction == SCREEN_RIGHT); + + /* Adding bias_right makes sure that we round up when biased right + * but round down when biased left. + */ + rect->left += max(0, (rect_width_chars - text_width_chars + bias_right) / 2) * TTE_CHAR_SIZE; +} + void memcpy16_tile8_with_palette_offset(u16* dst, const u16* src, uint hwcount, u8 palette_offset) { const u16 offset = (((palette_offset) << 8) | (palette_offset)); diff --git a/source/util.c b/source/util.c index 6e8afff..2f623ad 100644 --- a/source/util.c +++ b/source/util.c @@ -1,5 +1,7 @@ #include "util.h" #include +#include +#include int int_arr_max(int int_arr[], int size) { @@ -15,6 +17,44 @@ int int_arr_max(int int_arr[], int size) return max; } +void truncate_uint_to_suffixed_str(uint32_t num, int num_req_chars, char out_str_buff[UINT_MAX_DIGITS + 1]) +{ + bool inevitable_overflow = num_req_chars < SUFFIXED_NUM_MIN_REQ_CHARS; + if (inevitable_overflow) + { + num_req_chars = SUFFIXED_NUM_MIN_REQ_CHARS; + } + + int num_digits = u32_get_digits(num); + int overflow_size = num_digits - num_req_chars; + char* suffix = ""; + + /* If there is overflow, divide by the next suffixed power of 10 + * to truncate the number back within num_req_chars. + * UINT32_MAX is in the billions so no need to check larger numbers + * or perform complex mathematical operations. + */ + if (overflow_size >= ONE_M_ZEROS) + { + num /= ONE_B; + suffix = "B"; + } + else if (overflow_size >= ONE_K_ZEROS) + { + num /= ONE_M; + suffix = "M"; + } + else if (overflow_size > 0 + || (inevitable_overflow && num_digits == SUFFIXED_NUM_MIN_REQ_CHARS)) + // Special case - alleviate inevitable overflow for 1000s and truncate them to "1K"s + { + num /= ONE_K; + suffix = "K"; + } + + snprintf(out_str_buff, UINT_MAX_DIGITS + 1, "%lu%s", num, suffix); +} + // Avoid uint overflow when add/multiplying score uint32_t u32_protected_add(uint32_t a, uint32_t b) diff --git a/tests/run_tests.sh b/tests/run_tests.sh index 14cd473..93ba3ec 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -19,3 +19,4 @@ run_test() { run_test bitset run_test pool run_test list +run_test util diff --git a/tests/util/Makefile b/tests/util/Makefile new file mode 100644 index 0000000..56bbfc2 --- /dev/null +++ b/tests/util/Makefile @@ -0,0 +1,16 @@ + +CC := gcc +CFLAGS := -I../../include -I. \ + -g -O3 -Wall -Werror -Wno-format + +SRC := util_test.c ../../source/util.c +OUT := build/util_test + +$(OUT): $(SRC) | build + $(CC) $(CFLAGS) -o $@ $^ + +build: + mkdir -p build + +clean: + rm -f $(OUT) diff --git a/tests/util/util_test.c b/tests/util/util_test.c new file mode 100644 index 0000000..d32a249 --- /dev/null +++ b/tests/util/util_test.c @@ -0,0 +1,133 @@ +#include +#include +#include +#include + + +void test_truncate_uint_to_suffixed_str() +{ + /* + * I want to avoid testing the rounding so it can be easily changed + * so all tests are numbers that are rounded down regardless of rounding method. + * That way the function can be modified to round to nearest integer easily. + */ + + char suffixed_str_buff[UINT_MAX_DIGITS + 1] = {'\0'}; + + truncate_uint_to_suffixed_str(100, 3, suffixed_str_buff); + assert(strcmp(suffixed_str_buff, "100") == 0); + + suffixed_str_buff[0] = '\0'; + truncate_uint_to_suffixed_str(1000, 3, suffixed_str_buff); + assert(strcmp(suffixed_str_buff, "1K") == 0); + + suffixed_str_buff[0] = '\0'; + truncate_uint_to_suffixed_str(1000, 2, suffixed_str_buff); + assert(strcmp(suffixed_str_buff, "1K") == 0); + + suffixed_str_buff[0] = '\0'; + truncate_uint_to_suffixed_str(1000, 4, suffixed_str_buff); + assert(strcmp(suffixed_str_buff, "1000") == 0); + + suffixed_str_buff[0] = '\0'; + truncate_uint_to_suffixed_str(1000, 5, suffixed_str_buff); + assert(strcmp(suffixed_str_buff, "1000") == 0); + + suffixed_str_buff[0] = '\0'; + truncate_uint_to_suffixed_str(12123, 4, suffixed_str_buff); + assert(strcmp(suffixed_str_buff, "12K") == 0); + + suffixed_str_buff[0] = '\0'; + truncate_uint_to_suffixed_str(123123, 4, suffixed_str_buff); + assert(strcmp(suffixed_str_buff, "123K") == 0); + + suffixed_str_buff[0] = '\0'; + truncate_uint_to_suffixed_str(123123, 5, suffixed_str_buff); + assert(strcmp(suffixed_str_buff, "123K") == 0); + + suffixed_str_buff[0] = '\0'; + truncate_uint_to_suffixed_str(123123, 6, suffixed_str_buff); + assert(strcmp(suffixed_str_buff, "123123") == 0); + + suffixed_str_buff[0] = '\0'; + truncate_uint_to_suffixed_str(123123, 7, suffixed_str_buff); + assert(strcmp(suffixed_str_buff, "123123") == 0); + + suffixed_str_buff[0] = '\0'; + truncate_uint_to_suffixed_str(12345123, 6, suffixed_str_buff); + assert(strcmp(suffixed_str_buff, "12345K") == 0); + + suffixed_str_buff[0] = '\0'; + truncate_uint_to_suffixed_str(12123123, 5, suffixed_str_buff); + assert(strcmp(suffixed_str_buff, "12M") == 0); + + suffixed_str_buff[0] = '\0'; + truncate_uint_to_suffixed_str(12123123, 4, suffixed_str_buff); + assert(strcmp(suffixed_str_buff, "12M") == 0); + + suffixed_str_buff[0] = '\0'; + truncate_uint_to_suffixed_str(54123123, 4, suffixed_str_buff); + assert(strcmp(suffixed_str_buff, "54M") == 0); + + suffixed_str_buff[0] = '\0'; + truncate_uint_to_suffixed_str(123123123, 4, suffixed_str_buff); + assert(strcmp(suffixed_str_buff, "123M") == 0); + + suffixed_str_buff[0] = '\0'; + truncate_uint_to_suffixed_str(123123123, 6, suffixed_str_buff); + assert(strcmp(suffixed_str_buff, "123M") == 0); + + suffixed_str_buff[0] = '\0'; + truncate_uint_to_suffixed_str(987123123, 6, suffixed_str_buff); + assert(strcmp(suffixed_str_buff, "987M") == 0); + + suffixed_str_buff[0] = '\0'; + truncate_uint_to_suffixed_str(123123123, 7, suffixed_str_buff); + assert(strcmp(suffixed_str_buff, "123123K") == 0); + + suffixed_str_buff[0] = '\0'; + truncate_uint_to_suffixed_str(1123123123, 4, suffixed_str_buff); + assert(strcmp(suffixed_str_buff, "1B") == 0); + + suffixed_str_buff[0] = '\0'; + truncate_uint_to_suffixed_str((uint32_t)3123123123, 4, suffixed_str_buff); + assert(strcmp(suffixed_str_buff, "3B") == 0); + + suffixed_str_buff[0] = '\0'; + truncate_uint_to_suffixed_str(1234123123, 5, suffixed_str_buff); + assert(strcmp(suffixed_str_buff, "1234M") == 0); + + suffixed_str_buff[0] = '\0'; + truncate_uint_to_suffixed_str(1123123123, 10, suffixed_str_buff); + assert(strcmp(suffixed_str_buff, "1123123123") == 0); + + suffixed_str_buff[0] = '\0'; + truncate_uint_to_suffixed_str(1123123123, UINT_MAX_DIGITS, suffixed_str_buff); + assert(strcmp(suffixed_str_buff, "1123123123") == 0); + + suffixed_str_buff[0] = '\0'; + truncate_uint_to_suffixed_str(1123123123, 100, suffixed_str_buff); + assert(strcmp(suffixed_str_buff, "1123123123") == 0); + + suffixed_str_buff[0] = '\0'; + truncate_uint_to_suffixed_str(UINT32_MAX, 4, suffixed_str_buff); + assert(strcmp(suffixed_str_buff, "4B") == 0); + + // This is the only test that checks rounding down, don't add any more + suffixed_str_buff[0] = '\0'; + truncate_uint_to_suffixed_str(UINT32_MAX, 5, suffixed_str_buff); + assert(strcmp(suffixed_str_buff, "4294M") == 0); + + char max_uint_str_buff[UINT_MAX_DIGITS + 1] = {'\0'}; + snprintf(max_uint_str_buff, sizeof(max_uint_str_buff), "%lu", UINT32_MAX); + + suffixed_str_buff[0] = '\0'; + truncate_uint_to_suffixed_str(UINT32_MAX, UINT_MAX_DIGITS, suffixed_str_buff); + assert(strcmp(suffixed_str_buff, max_uint_str_buff) == 0); +} + +int main() +{ + test_truncate_uint_to_suffixed_str(); + return 0; +} \ No newline at end of file