The 24 Game tests one's mental arithmetic.



Task
Write a program that randomly chooses and displays four digits, each from 1 ──► 9 (inclusive) with repetitions allowed.

The program should prompt for the player to enter an arithmetic expression using just those, and all of those four digits, used exactly once each. The program should check then evaluate the expression.

The goal is for the player to enter an expression that (numerically) evaluates to 24.

 Only the following operators/functions are allowed: multiplication, division, addition, subtraction
 Division should use floating point or rational arithmetic, etc, to preserve remainders.
 Brackets are allowed, if using an infix expression evaluator.
 Forming multiple digit numbers from the supplied digits is disallowed. (So an answer of 12+12 when given 1, 2, 2, and 1 is wrong).
 The order of the digits when given does not have to be preserved.


Notes
 The type of expression evaluator used is not mandated. An RPN evaluator is equally acceptable for example.
 The task is not for the program to generate the expression, or test whether an expression is even possible.


Related tasks
 24 game/Solve


Reference
 The 24 Game on h2g2.


-- Note this uses simple/strict left association, so for example:
--  1+2*1*8 is ((1+2)*1)*8 not 1+((2*1)*8) [or 1+(2*(1*8))], and
--  7-(2*2)*8 is (7-(2*2))*8 not 7-((2*2)*8)
--  Does not allow unary minus on the first digit.
-- Uses solve24() from the next task, when it can.
--  (you may want to comment out the last 2 lines/uncomment the if 0, in that file)
--
--include 24_game_solve.exw
 
--with trace
forward function eval(string equation, sequence unused, integer idx=1)
-- (the above definition is entirely optional, but good coding style)
 
constant errorcodes = {"digit expected",                    -- 1
                       "')' expected",                      -- 2
                       "digit already used",                -- 3
                       "digit not offered",                 -- 4
                       "operand expected"}                  -- 5
 
function card(integer idx)  -- (for error handling)
    if idx=1 then return "1st" end if
    if idx=2 then return "2nd" end if
    -- (assumes expression is less than 21 characters)
    return sprintf("%dth",idx)
end function
 
function errorchar(sequence equation, integer idx)
    if idx>length(equation) then return "" end if
    return sprintf("(%s)",equation[idx])
end function
 
sequence rset = repeat(0,4)
 
procedure new_rset()
    for i=1 to length(rset) do
        rset[i] = rand(9)
    end for
end procedure
 
function get_operand(string equation, integer idx, sequence unused)
    integer error = 1 -- "digit expected"
    atom res
 
    if idx<=length(equation) then
        integer ch = equation[idx]
        if ch='(' then
            {error,res,unused,idx} = eval(equation,unused,idx+1)
            if error=0
            and idx<=length(equation) then
                ch = equation[idx]
                if ch=')' then
                    return {0,res,unused,idx+1}
                end if
            end if
            if error=0 then
                error = 2   -- "')' expected"
            end if
        elsif ch>='0' and ch<='9' then
            res = ch-'0'
            integer k = find(res,unused)
            if k!=0 then
                unused[k..k] = {}
                return {0,res,unused,idx+1}
            end if
            if find(res,rset) then
                error = 3  -- "digit already used"
            else
                error = 4  -- "digit not offered"
            end if
        end if
    end if
    return {error,0,unused,idx}
end function
 
function get_operator(string equation, integer idx)
    integer error = 5 -- "operand expected"
    if idx<=length(equation) then
        integer ch = equation[idx]
        if find(ch,"+-/*") then
            return {0,ch,idx+1}
        end if
    end if
    return {error,0,idx}
end function
 
function eval(string equation, sequence unused, integer idx=1)
    atom lhs, rhs
    integer ch, error
    {error,lhs,unused,idx} = get_operand(equation,idx,unused)
    if error=0 then
        while 1 do
            {error,ch,idx} = get_operator(equation,idx)
            if error!=0 then exit end if
            {error,rhs,unused,idx} = get_operand(equation,idx,unused)
            if error!=0 then exit end if
            if    ch='+' then       lhs += rhs
            elsif ch='-' then       lhs -= rhs
            elsif ch='/' then       lhs /= rhs
            elsif ch='*' then       lhs *= rhs
            else ?9/0 -- (should not happen)
            end if
            if idx>length(equation) then
                return {0,lhs,unused,idx}
            end if
            ch = equation[idx]
            if ch=')' then
                return {0,lhs,unused,idx}
            end if
        end while
    end if
    return {error,0,unused,idx}
end function
 
function strip(string equation)
    for i=length(equation) to 1 by -1 do
        if find(equation[i]," \t\r\n") then
            equation[i..i] = ""
        end if
    end for
    return equation
end function
 
function strip0(atom a) -- (for error handling)
    string res = sprintf("%f",a)
    for i=length(res) to 2 by -1 do
        integer ch = res[i]
        if ch='.' then return res[1..i-1] end if
        if ch!='0' then return res[1..i] end if
    end for
    return res
end function
 
procedure play()
    sequence unused
    string equation
    integer error,idx
    atom res
 
    new_rset()
    printf(1,"Enter an expression which evaluates to exactly 24\n"&
             "Use all of, and only, the digits %d, %d, %d, and %d\n"&
             "You may only use the operators + - * /\n"&
             "Parentheses and spaces are allowed\n",rset)
    while 1 do
        equation = strip(gets(0))
        if upper(equation)="Q" then exit end if
        if equation="?" then
            puts(1,"\n")
            integer r_solve24 = routine_id("solve24") -- see below
            if r_solve24=-1 then -- (someone copied just this code out?)
                puts(1,"no solve24 routine\n")
            else
                call_proc(r_solve24,{rset})
            end if
        else
            {error,res,unused,idx} = eval(equation, rset)
            if error!=0 then
                printf(1,"  %s on the %s character%s\n",{errorcodes[error],card(idx),errorchar(equation,idx)})
            elsif idx<=length(equation) then
                printf(1,"\neval() returned only having processed %d of %d characters\n",{idx,length(equation)})
            elsif length(unused) then
                printf(1," not all the digits were used\n",error)
            elsif res!=24 then
                printf(1,"\nresult is %s, not 24\n",{strip0(res)})
            else
                puts(1," correct!  Press any key to quit\n")
                {} = wait_key()
                exit
            end if
        end if
        puts(1,"enter Q to give up and quit\n")
    end while
end procedure
 
play()
