24 game/ABAP
Appearance
< 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.
Global Data
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.
RPN Code
" 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.
24 Game
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.