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.


with Ada.Float_Text_IO;
with Ada.Text_IO;
with Ada.Numerics.Discrete_Random;
procedure Game_24 is
   subtype Digit is Character range '1' .. '9';
   package Random_Digit is new Ada.Numerics.Discrete_Random (Digit);
   Exp_Error : exception;
   Digit_Generator : Random_Digit.Generator;
   Given_Digits : array (1 .. 4) of Digit;
   Float_Value : constant array (Digit) of Float :=
      (1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0);
   function Apply_Op (L, R : Float; Op : Character) return Float is
   begin
      case Op is
         when '+' =>
            return L + R;
         when '-' =>
            return L - R;
         when '*' =>
            return L * R;
         when '/' =>
            return L / R;
         when others =>
            Ada.Text_IO.Put_Line ("Unexpected operator: " & Op);
            raise Exp_Error;
      end case;
   end Apply_Op;
   function Eval_Exp (E : String) return Float is
      Flt : Float;
      First : Positive := E'First;
      Last : Positive;
      function Match_Paren (Start : Positive) return Positive is
         Pos : Positive := Start + 1;
         Level : Natural := 1;
      begin
         loop
            if Pos > E'Last then
               Ada.Text_IO.Put_Line ("Unclosed parentheses.");
               raise Exp_Error;
            elsif E (Pos) = '(' then
               Level := Level + 1;
            elsif E (Pos) = ')' then
               Level := Level - 1;
               exit when Level = 0;
            end if;
            Pos := Pos + 1;
         end loop;
         return Pos;
      end Match_Paren;
   begin
      if E (First) = '(' then
         Last := Match_Paren (First);
         Flt := Eval_Exp (E (First + 1 .. Last - 1));
      elsif E (First) in Digit then
         Last := First;
         Flt := Float_Value (E (First));
      else
         Ada.Text_IO.Put_Line ("Unexpected character: " & E (First));
         raise Exp_Error;
      end if;
      loop
         if Last = E'Last then
            return Flt;
         elsif Last = E'Last - 1 then
            Ada.Text_IO.Put_Line ("Unexpected end of expression.");
            raise Exp_Error;
         end if;
         First := Last + 2;
         if E (First) = '(' then
            Last := Match_Paren (First);
            Flt := Apply_Op (Flt, Eval_Exp (E (First + 1 .. Last - 1)),
                             Op => E (First - 1));
         elsif E (First) in Digit then
            Last := First;
            Flt := Apply_Op (Flt, Float_Value (E (First)),
                             Op => E (First - 1));
         else
            Ada.Text_IO.Put_Line ("Unexpected character: " & E (First));
            raise Exp_Error;
         end if;
      end loop;
   end Eval_Exp;
begin
   Ada.Text_IO.Put_Line ("24 Game");
   Ada.Text_IO.Put_Line ("- Enter Q to Quit");
   Ada.Text_IO.Put_Line ("- Enter N for New digits");
   Ada.Text_IO.Put_Line ("Note: Operators are evaluated left-to-right");
   Ada.Text_IO.Put_Line ("      (use parentheses to override)");
   Random_Digit.Reset (Digit_Generator);
   <<GEN_DIGITS>>
   Ada.Text_IO.Put_Line ("Generating 4 digits...");
   for I in Given_Digits'Range loop
      Given_Digits (I) := Random_Digit.Random (Digit_Generator);
   end loop;
   <<GET_EXP>>
   Ada.Text_IO.Put ("Your Digits:");
   for I in Given_Digits'Range loop
      Ada.Text_IO.Put (" " & Given_Digits (I));
   end loop;
   Ada.Text_IO.New_Line;
   Ada.Text_IO.Put ("Enter your Expression: ");
   declare
      Value : Float;
      Response : constant String := Ada.Text_IO.Get_Line;
      Prev_Ch : Character := ' ';
      Unused_Digits : array (Given_Digits'Range) of Boolean :=
        (others => True);
   begin
      if Response = "n" or Response = "N" then
         goto GEN_DIGITS;
      end if;
      if Response = "q" or Response = "Q" then
         return;
      end if;
      -- check input
      for I in Response'Range loop
         declare
            Ch : constant Character := Response (I);
            Found : Boolean;
         begin
            if Ch in Digit then
               if Prev_Ch in Digit then
                  Ada.Text_IO.Put_Line ("Illegal multi-digit number used.");
                  goto GET_EXP;
               end if;
               Found := False;
               for J in Given_Digits'Range loop
                  if Unused_Digits (J) and then
                        Given_Digits (J) = Ch then
                     Unused_Digits (J) := False;
                     Found := True;
                     exit;
                  end if;
               end loop;
               if not Found then
                  Ada.Text_IO.Put_Line ("Illegal number used: " & Ch);
                  goto GET_EXP;
               end if;
            elsif Ch /= '(' and Ch /= ')' and Ch /= '+' and
                  Ch /= '-' and Ch /= '*' and Ch /= '/' then
               Ada.Text_IO.Put_Line ("Illegal character used: " & Ch);
               goto GET_EXP;
            end if;
            Prev_Ch := Ch;
         end;
      end loop;
      -- check all digits used
      for I in Given_Digits'Range loop
         if Unused_Digits (I) then
            Ada.Text_IO.Put_Line ("Digit not used: " & Given_Digits (I));
            goto GET_EXP;
         end if;
      end loop;
      -- check value
      begin
         Value := Eval_Exp (Response);
      exception
         when Exp_Error =>
            goto GET_EXP; -- Message displayed by Eval_Exp;
      end;
      if abs (Value - 24.0) > 0.001 then
         Ada.Text_IO.Put ("Value ");
         Ada.Float_Text_IO.Put (Value, Fore => 0, Aft => 3, Exp => 0);
         Ada.Text_IO.Put_Line (" is not 24!");
         goto GET_EXP;
      else
         Ada.Text_IO.Put_Line ("You won!");
         Ada.Text_IO.Put_Line ("Enter N for a new game, or try another solution.");
         goto GET_EXP;
      end if;
   end;
end Game_24;