< 24 game
This is essentially a modified port of the C version.
Firstly we need to make a Reverse Polish Notation parser. To make it easier, I simply put this in the report used in the game itself. The following data declarations should be common to both for ease of use.
report z24_with_rpn constants: c_eval_to type i value 24, c_tolerance type f value '0.0001'. data: gt_val type table of f, gv_val type f, gv_pac type p, gv_chk type c.
" Log a message from the RPN Calculator. form rpn_log using lv_msg type string. write : / 'RPN Message: ', lv_msg. endform. " Performs add in Reverse Polish Notation. form rpn_add. data: lv_val1 type f, lv_val2 type f. " Get the last two values from the stack to add together. perform rpn_pop changing: lv_val1, lv_val2. add lv_val2 to lv_val1. " Add them and then add them back to the "top". perform rpn_push using lv_val1. endform. " Perform subtraction in RPN. form rpn_sub. data: lv_val1 type f, lv_val2 type f. " Get the last two values, subtract them, and push them back on. perform rpn_pop changing: lv_val1, lv_val2. subtract lv_val1 from lv_val2. perform rpn_push using lv_val2. endform. " Perform multiplication in RPN. form rpn_mul. data: lv_val1 type f, lv_val2 type f. " Get the last two values, multiply, and push them back. perform rpn_pop changing: lv_val1, lv_val2. multiply lv_val1 by lv_val2. perform rpn_push using lv_val1. endform. " Perform division in RPN. form rpn_div. data: lv_val1 type f, lv_val2 type f. " Get the last two values, divide the first by the second " and then add it back to the stack. perform rpn_pop changing: lv_val1, lv_val2. divide lv_val1 by lv_val2. perform rpn_push using lv_val1. endform. " Negate a number in RPN. form rpn_neg. data: lv_val type f. " Simply get the last number and negate it before pushing it back. perform rpn_pop changing lv_val. multiply lv_val by -1. perform rpn_push using lv_val. endform. " Swap the top two values on the RPN Stack. form rpn_swap. data: lv_val1 type f, lv_val2 type f. " Get the top two values and then add them back in reverse order. perform rpn_pop changing: lv_val1, lv_val2. perform rpn_push using: lv_val2, lv_val1. endform. " Call the relevant RPN operation. form rpn_call_op using iv_op type string. case iv_op. when '+'. perform rpn_add. when '-'. perform rpn_sub. when '*'. perform rpn_mul. when '/'. perform rpn_div. when 'n'. perform rpn_neg. when 's'. perform rpn_swap. when others. " Bad op-code found! perform rpn_log using 'Operation not found!'. endcase. endform. " Reverse_Polish_Notation Parser. form rpn_pop changing ev_out type f. " Attempt to get the entry from the 'top' of the table. " If it's empty --> log an error and bail. data: lv_lines type i. describe table gt_val lines lv_lines. if lv_lines > 0. " After we have retrieved the value, we must remove it from the table. read table gt_val index lv_lines into ev_out. delete gt_val index lv_lines. else. perform rpn_log using 'RPN Stack is empty! Underflow!'. ev_out = 0. endif. endform. " Pushes the supplied value onto the RPN table / stack. form rpn_push using iv_val type f. " Simple append - other languages this involves a stack of a certain size. append iv_val to gt_val. endform. " Refreshes the RPN stack / table. form rpn_reset. " Clear the stack to start anew. refresh gt_val. endform. " Checks if the supplied string is numeric. " Lazy evaluation - only checkcs for numbers without formatting. form rpn_numeric using iv_in type string changing ev_out type c. data: lv_moff type i, lv_len type i. " Match digits with optional decimal places. find regex '\d+(\.\d+)*' in iv_in match offset lv_moff match length lv_len. " Get the offset and length of the first occurence, and work " out the length of the match. subtract lv_moff from lv_len. " If the length is different to the length of the whole string, " then it's NOT a match, else it is. if lv_len ne strlen( iv_in ). ev_out = ' '. else. ev_out = 'X'. endif. endform. " Convert input to a number. Added safety net of is_numeric. form rpn_get_num using iv_in type string changing ev_num type f. data: lv_check type c. " Check if it's numeric - built in redundancy. perform rpn_numeric using iv_in changing lv_check. if lv_check = 'X'. ev_num = iv_in. else. perform rpn_log using 'Wrong call!'. endif. endform. " Evaluate the RPN expression and return true if success in eval. form rpn_eval using in_expr type string changing ev_out type c. data: lv_len type i, lv_off type i value 0, lv_num type c, lv_val type f, lv_tok type string. lv_len = strlen( in_expr ). do lv_len times. lv_tok = in_expr+lv_off(1). perform rpn_numeric using lv_tok changing lv_num. if lv_num = 'X'. perform: rpn_get_num using lv_tok changing lv_val, rpn_push using lv_val. else. perform rpn_call_op using lv_tok. endif. add 1 to lv_off. enddo. ev_out = 'X'. endform.
We can now play the game since we have a parser. The interface is a hacked up screen, and is a bit more clunky than even a CLI (No such option for ABAP, at least not which I'm aware of).
The supplied Random Number Generator seems to highly favour a five as the first digit as well (It does occasionally take on other values). It doesn't appear to be a seeding issue, as the other numbers appear sufficiently random.
selection-screen begin of block main with frame title lv_title. parameters: p_first type i, p_second type i, p_third type i, p_fourth type i, p_expr type string. selection-screen end of block main. initialization. perform ranged_rand using 1 9 changing p_first. perform ranged_rand using 1 9 changing p_second. perform ranged_rand using 1 9 changing p_third. perform ranged_rand using 1 9 changing p_fourth. at selection-screen output. " Set-up paramter texts. lv_title = 'Reverse Polish Notation Tester - Enter expression that evaluates to 24.'. %_p_first_%_app_%-text = 'First Number: '. %_p_second_%_app_%-text = 'Second Number: '. %_p_third_%_app_%-text = 'Third Number: '. %_p_fourth_%_app_%-text = 'Fourth Number: '. %_p_expr_%_app_%-text = 'Expression: '. " Disallow modification of supplied numbers. loop at screen. if screen-name = 'P_FIRST' or screen-name = 'P_SECOND' or screen-name = 'P_THIRD' or screen-name = 'P_FOURTH'. screen-input = '0'. modify screen. endif. endloop. start-of-selection. " Check the expression is valid. perform check_expr using p_expr changing gv_chk. if gv_chk <> 'X'. write : / 'Invalid input!'. stop. endif. " Check if the expression actually evalutes. perform rpn_eval using p_expr changing gv_chk. " If it doesn't, warning!. if gv_chk <> 'X'. write : / 'Invalid expression!'. stop. endif. " Get the evaluated value. Transform it to something that displays a bit better. " Then check if it's a valid answer, with a certain tolerance. " If they're wrong, give them instructions to on how to go back. perform rpn_pop changing gv_val. gv_pac = gv_val. gv_val = abs( gv_val - c_eval_to ). if gv_val < c_tolerance. write : / 'Answer correct'. else. write : / 'Your expression evalutes to ', gv_pac. write : / 'Press "F3" to go back and try again!'. endif. write : / 'Re-run the program to generate a new set.'. " Check that the input expression is valid - i.e. all supplied numbers " appears exactly once. This does not validate the expression itself. form check_expr using iv_exp type string changing ev_ok type c. data: lv_chk type c, lv_tok type string, lv_val type i value 0, lv_len type i, lv_off type i, lv_num type i, lt_nums type standard table of i. ev_ok = 'X'. " Update the number count table - indexes 1-9 correspond to numbers. " The value stored corresponds to the number of occurences. do 9 times. if p_first = sy-index. add 1 to lv_val. endif. if p_second = sy-index. add 1 to lv_val. endif. if p_third = sy-index. add 1 to lv_val. endif. if p_fourth = sy-index. add 1 to lv_val. endif. append lv_val to lt_nums. lv_val = 0. enddo. " Loop through the expression parsing the numbers. lv_len = strlen( p_expr ). do lv_len times. lv_tok = p_expr+lv_off(1). " Check if the current token is a number. perform rpn_numeric using lv_tok changing lv_chk. if lv_chk = 'X'. lv_num = lv_tok. " If it's a number, it must be from 1 - 9. if lv_num < 1 or lv_num > 9. ev_ok = ' '. write : / 'Numbers must be between 1 and 9!'. return. else. " Check how many times the number was supplied. If it wasn't supplied " or if we have used it up, we should give an error. read table lt_nums index lv_num into lv_val. if lv_val <= 0. ev_ok = ' '. write : / 'You can not use numbers more than once'. return. endif. " If we have values left for this number, we decrement the remaining amount. subtract 1 from lv_val. modify lt_nums index lv_num from lv_val. endif. endif. add 1 to lv_off. enddo. " Loop through the table and check we have no numbers left for use. do 9 times. read table lt_nums index sy-index into lv_val. if lv_val > 0. write : / 'You must use all numbers'. ev_ok = ' '. return. endif. enddo. endform. " Generate a random number within the given range. form ranged_rand using iv_min type i iv_max type i changing ev_val type i. call function 'QF05_RANDOM_INTEGER' exporting ran_int_max = iv_max ran_int_min = iv_min importing ran_int = ev_val. endform.