24 game/ABAP

From Rosetta Code
Revision as of 05:27, 5 January 2011 by rosettacode>Rjf89 (→‎RPN Code: Left off closing tag.)

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

<lang ABAP>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.</lang>

RPN Code

<lang ABAP>" 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.</lang>

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. <lang ABAP> 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.</lang>