Applescript - Code to solve the Daily Telegraph 'Safe Cracker' puzzle

I don't know if anyone is going to be interested in this (or if it the correct place to post it!). I spent a couple of hours trying to write a Script that would solve the 'Safe Cracker' puzzle from the newspaper. My code worked but as you will see, it is very clunky and not at all well written (plus some of my maths is suspect!). I'm not really looking for critique on the code because it was only a test to see if I could do it, but I wound be really interested to see if someone has done a better job!

The Puzzle


set ListNoZeros to {}
set ListNoRepeats1 to {}
set ListNoRepeats2 to {}
set ListNoRepeats3 to {}
set ListNoRepeats4 to {}
set ListNoRepeats5 to {}
set ListNoRepeats6 to {}
set ListNoRepeats7 to {}
set ListNoRepeats8 to {}
set ListNoRepeats9 to {}
set ListNoRepeats10 to {}
set ListNoRepeats11 to {}
set ListNoRepeats12 to {}
set ListNoRepeats13 to {}
set ListNoRepeats14 to {}
set ListNoRepeats15 to {}
set ListNoRepeats16 to {}
set ListAnswer to {}

repeat with a from 1234 to 9876
   
   --split the number into seperate digits
   set theNumber to a as string
   
   set String1 to characters 1 thru 1 of theNumber
   set String2 to characters 2 thru 2 of theNumber
   set String3 to characters 3 thru 3 of theNumber
   set String4 to characters 4 thru 4 of theNumber
   
   set Num1 to String1 as number
   set Num2 to String2 as number
   set Num3 to String3 as number
   set Num4 to String4 as number
   
   -- Remove all zeros
   if Num1 is not 0 and Num2 is not 0 and Num3 is not 0 and Num4 is not 0 then copy a to the end of ListNoZeros -- Number of posibilities = 6357
end repeat

-- Is each number different? (Testing 1 & 2)
repeat with b from 1 to length of ListNoZeros
   set theCurrentListItem to item b of ListNoZeros
   
   --split the number into seperate digits
   set theNumber to theCurrentListItem as string
   
   set String1 to characters 1 thru 1 of theNumber
   set String2 to characters 2 thru 2 of theNumber
   set String3 to characters 3 thru 3 of theNumber
   set String4 to characters 4 thru 4 of theNumber
   
   --Convert back to number
   set Num1 to String1 as number
   set Num2 to String2 as number
   set Num3 to String3 as number
   set Num4 to String4 as number
   
   if Num1 / Num2 is not equal to 1 then copy theCurrentListItem to the end of ListNoRepeats1 -- Number of posibilities = 5790
end repeat

-- Is each number different? (Testing 1 & 3)
repeat with c from 1 to length of ListNoRepeats1
   set theCurrentListItem to item c of ListNoRepeats1
   
   
   --split the number into seperate digits
   set theNumber to theCurrentListItem as string
   
   set String1 to characters 1 thru 1 of theNumber
   set String2 to characters 2 thru 2 of theNumber
   set String3 to characters 3 thru 3 of theNumber
   set String4 to characters 4 thru 4 of theNumber
   
   --Convert back to number
   set Num1 to String1 as number
   set Num2 to String2 as number
   set Num3 to String3 as number
   set Num4 to String4 as number
   
   if Num1 / Num3 is not equal to 1 then copy theCurrentListItem to the end of ListNoRepeats2 -- Number of posibilities = 5160
end repeat

-- Is each number different? (Testing 1 & 4)
repeat with d from 1 to length of ListNoRepeats2
   set theCurrentListItem to item d of ListNoRepeats2
   
   --split the number into seperate digits
   set theNumber to theCurrentListItem as string
   
   set String1 to characters 1 thru 1 of theNumber
   set String2 to characters 2 thru 2 of theNumber
   set String3 to characters 3 thru 3 of theNumber
   set String4 to characters 4 thru 4 of theNumber
   
   --Convert back to number
   set Num1 to String1 as number
   set Num2 to String2 as number
   set Num3 to String3 as number
   set Num4 to String4 as number
   
   if Num1 / Num4 is not equal to 1 then copy theCurrentListItem to the end of ListNoRepeats3 -- Number of posibilities = 4588
end repeat

-- Is each number different? (Testing 2 & 3)
repeat with e from 1 to length of ListNoRepeats3
   set theCurrentListItem to item e of ListNoRepeats3
   
   --split the number into seperate digits
   set theNumber to theCurrentListItem as string
   
   set String1 to characters 1 thru 1 of theNumber
   set String2 to characters 2 thru 2 of theNumber
   set String3 to characters 3 thru 3 of theNumber
   set String4 to characters 4 thru 4 of theNumber
   
   --Convert back to number
   set Num1 to String1 as number
   set Num2 to String2 as number
   set Num3 to String3 as number
   set Num4 to String4 as number
   
   if Num2 / Num3 is not equal to 1 then copy theCurrentListItem to the end of ListNoRepeats4 -- Number of posibilities = 4028
end repeat

-- Is each number different? (Testing 2 & 4)
repeat with f from 1 to length of ListNoRepeats4
   set theCurrentListItem to item f of ListNoRepeats4
   
   --split the number into seperate digits
   set theNumber to theCurrentListItem as string
   
   set String1 to characters 1 thru 1 of theNumber
   set String2 to characters 2 thru 2 of theNumber
   set String3 to characters 3 thru 3 of theNumber
   set String4 to characters 4 thru 4 of theNumber
   
   --Convert back to number
   set Num1 to String1 as number
   set Num2 to String2 as number
   set Num3 to String3 as number
   set Num4 to String4 as number
   
   if Num2 / Num4 is not equal to 1 then copy theCurrentListItem to the end of ListNoRepeats5 -- Number of posibilities = 3526
end repeat

-- Is each number different? (Testing 3 & 4)
repeat with g from 1 to length of ListNoRepeats5
   set theCurrentListItem to item g of ListNoRepeats5
   
   --split the number into seperate digits
   set theNumber to theCurrentListItem as string
   
   set String1 to characters 1 thru 1 of theNumber
   set String2 to characters 2 thru 2 of theNumber
   set String3 to characters 3 thru 3 of theNumber
   set String4 to characters 4 thru 4 of theNumber
   
   --Convert back to number
   set Num1 to String1 as number
   set Num2 to String2 as number
   set Num3 to String3 as number
   set Num4 to String4 as number
   
   if Num3 / Num4 is not equal to 1 then copy theCurrentListItem to the end of ListNoRepeats6 -- Number of posibilities = 3024
end repeat

-- The 4th is 3 more than the 3rd
repeat with h from 1 to length of ListNoRepeats6
   set theCurrentListItem to item h of ListNoRepeats6
   
   --split the number into seperate digits
   set theNumber to theCurrentListItem as string
   
   set String1 to characters 1 thru 1 of theNumber
   set String2 to characters 2 thru 2 of theNumber
   set String3 to characters 3 thru 3 of theNumber
   set String4 to characters 4 thru 4 of theNumber
   
   --Convert back to number
   set Num1 to String1 as number
   set Num2 to String2 as number
   set Num3 to String3 as number
   set Num4 to String4 as number
   
   if Num4 = Num3 + (3) then copy theCurrentListItem to the end of ListNoRepeats7 -- Number of posibilities = 252
end repeat

-- The 1st and the 3rd differ by 4. First (1st > 3rd)
repeat with i from 1 to length of ListNoRepeats7
   set theCurrentListItem to item i of ListNoRepeats7
   
   --split the number into seperate digits
   set theNumber to theCurrentListItem as string
   
   set String1 to characters 1 thru 1 of theNumber
   set String2 to characters 2 thru 2 of theNumber
   set String3 to characters 3 thru 3 of theNumber
   set String4 to characters 4 thru 4 of theNumber
   
   --Convert back to number
   set Num1 to String1 as number
   set Num2 to String2 as number
   set Num3 to String3 as number
   set Num4 to String4 as number
   
   if (Num3 - Num1) = 4 then copy theCurrentListItem to the end of ListNoRepeats8 -- Number of posibilities = 12
end repeat

-- The 1st and the 3rd differ by 4. Second (1st < 3rd)
repeat with j from 1 to length of ListNoRepeats7 -- Using this list twice.....
   set theCurrentListItem to item j of ListNoRepeats7
   
   --split the number into seperate digits
   set theNumber to theCurrentListItem as string
   
   set String1 to characters 1 thru 1 of theNumber
   set String2 to characters 2 thru 2 of theNumber
   set String3 to characters 3 thru 3 of theNumber
   set String4 to characters 4 thru 4 of theNumber
   
   --Convert back to number
   set Num1 to String1 as number
   set Num2 to String2 as number
   set Num3 to String3 as number
   set Num4 to String4 as number
   
   if (Num1 - Num3) = 4 then copy theCurrentListItem to the end of ListNoRepeats9 -- Number of posibilities = 30
end repeat

-- Join these 2 lists together
set ListNoRepeats10 to ListNoRepeats8 & ListNoRepeats9 -- Number of posibilities = 42

-- The sum of the middle two is divisible by 5
repeat with k from 1 to length of ListNoRepeats10
   set theCurrentListItem to item k of ListNoRepeats10
   
   --split the number into seperate digits
   set theNumber to theCurrentListItem as string
   
   set String1 to characters 1 thru 1 of theNumber
   set String2 to characters 2 thru 2 of theNumber
   set String3 to characters 3 thru 3 of theNumber
   set String4 to characters 4 thru 4 of theNumber
   
   --Convert back to number
   set Num1 to String1 as number
   set Num2 to String2 as number
   set Num3 to String3 as number
   set Num4 to String4 as number
   
   if (Num2 + Num3) = 5 or (Num2 + Num3) = 10 then copy theCurrentListItem to the end of ListNoRepeats11 -- Number of posibilities = 7
end repeat

-- Exactly one of the first 2 is odd (First & Second)
repeat with l from 1 to length of ListNoRepeats11
   set theCurrentListItem to item l of ListNoRepeats11
   
   --split the number into seperate digits
   set theNumber to theCurrentListItem as string
   
   set String1 to characters 1 thru 1 of theNumber
   set String2 to characters 2 thru 2 of theNumber
   set String3 to characters 3 thru 3 of theNumber
   set String4 to characters 4 thru 4 of theNumber
   
   --Convert back to number
   set Num1 to String1 as number
   set Num2 to String2 as number
   set Num3 to String3 as number
   set Num4 to String4 as number
   
   if Num1 mod 2 = 0 and Num2 mod 2 = 1 then copy theCurrentListItem to the end of ListNoRepeats12
   
   if Num1 mod 2 = 1 and Num2 mod 2 = 0 then copy theCurrentListItem to the end of ListNoRepeats13
   
   -- Join these 2 lists together
   set ListNoRepeats14 to ListNoRepeats12 & ListNoRepeats13 -- Number of posibilities = 3
end repeat

-- 2nd or 4th are square (but not both)
repeat with m from 1 to length of ListNoRepeats14
   set theCurrentListItem to item m of ListNoRepeats14
   
   set theNumber to theCurrentListItem as string
   
   set String1 to characters 1 thru 1 of theNumber
   set String2 to characters 2 thru 2 of theNumber
   set String3 to characters 3 thru 3 of theNumber
   set String4 to characters 4 thru 4 of theNumber
   
   --Convert back to number
   set Num1 to String1 as number
   set Num2 to String2 as number
   set Num3 to String3 as number
   set Num4 to String4 as number
   
   if Num2 = 1 or Num2 = 4 or Num2 = 9 then copy theCurrentListItem to the end of ListNoRepeats15
   
   if Num4 = 1 or Num4 = 4 or Num4 = 9 then copy theCurrentListItem to the end of ListNoRepeats16
   
   -- Join these 2 lists together
   set ListAnswer to ListNoRepeats15 & ListNoRepeats16
end repeat

display dialog "The Correct Code is - " & ListAnswer ```

Ignoring my own comment about this being borderline offtopc, here is a (rather brute-force) Swift version looping over all possible combinations once.

func is_square(_ i: Int) -> Bool {
    return (i == 1) || (i == 4) || (i == 9)
}

func is_odd(_ i: Int) -> Bool {
    return (i % 2) == 1
}

func xor(_ b1: Bool, _ b2: Bool) -> Bool {
     return ((b1 && !b2) || (!b1 && b2))
}

for first in 1...9 {
    for second in 1...9 {
        for third in 1...9 {
            for fourth in 1...9 {
                if xor(is_square(second), is_square(fourth)) // 1 Either the second or fourth is square, but not both
                   && (third + 3 == fourth)                  // 2 The fourth is three more than the third
                   && xor(is_odd(first), is_odd(second))     // 3 Exactly one of the first two is odd
                   && (abs(first - third) == 4)              // 4 The first and third differ by four
                   && ((second + third) % 5 == 0)            // 5 The sum of the middle two is divisible by five
                {
                    print("\(first)\(second)\(third)\(fourth)")
                }
            }
        }
    }
}

Or you start with the list of all combinations and then apply a filter to determine the valid one (without error checking in this case as we already know there will be exactly one solution).

func digits(_ i: Int) -> (Int, Int, Int, Int) {
     let d1 = (i / 1000)
     let d2 = (i / 100) % 10
     let d3 = (i / 10) % 10
     let d4 = i % 10
     return (d1, d2, d3, d4)
}

let result = Array(1111...9999).filter({ (code: Int) -> Bool in
    let (first, second, third, fourth) = digits(code)
    return xor(is_square(second), is_square(fourth))
           && (third + 3 == fourth)
           && xor(is_odd(first), is_odd(second))
           && (abs(first - third) == 4)
           && ((second + third) % 5 == 0)
})[0]
print("\(result)")

PS: I didn't add a check for digit difference because I was too lazy :-) (but from looking at the rules, a combination where digits are the same doesn't match anyway)


Doing one loop and exiting early, with improvements to condition checking. AppleScript is definitely not the language one would commonly choose for this sort of problem!

set squarenumbers to {1, 4, 9}
repeat with n from 1234 to 9876
try
        -- split digits
        set nstr to n as string
        set n1 to character 1 of nstr as number
        set n2 to character 2 of nstr as number
        set n3 to character 3 of nstr as number
        set n4 to character 4 of nstr as number
        -- check distinct digits and for conditions 1 through 5
        if ( ¬
                n1 = n2 or n1 = n3 or n1 = n4 or n2 = n3 or n2 = n3 or n3 = n4 or ¬
                not ((squarenumbers contains n2 and squarenumbers does not contain n4) or (squarenumbers does not contain n2 and squarenumbers contains n4)) or ¬
                not (n3 + 3 = n4) or ¬
                not ((n1 mod 2 = 0 and n2 mod 2 = 1) or (n1 mod 2 = 1 and n2 mod 2 = 0)) or ¬
                not ((n1 - n3) = 4 or (n3 - n1) = 4) or ¬
                not ((n2 + n3) mod 5 = 0) ¬
        ) then
                error 0
        end if
        -- found
        return n
end try
end repeat

Replacing display dialog with return in your solution and using time osascript file shows 12.23 seconds for the solution in the question and 1.25 seconds for my solution. More optimisations in the iteration are possible.


I feel that this is one of the examples where writing code will take much longer than solving it on the paper :-). I think that this kind of solution is expected for solving puzzles in newspapers.

Paper and pencil algorithm:

determine possible third and fourth numbers

fourth is three more than third

9 is largest number possible, so maximum third number is 9-3 = 6 so possible third and fourth numbers are six pairs: (1, 4), (2, 5), (3, 6), (4, 7), (5, 8), (6, 9)

determine possible second number

either second or fourth is square but not both

sum of middel two (second and third) is divisible by 5

four different digits

possible squares are 1, 4, 9. Go through 6 pairs and if fourth number is square find if there is a number which makes sum divisible by 5; if fourth not square second number must be on of squares and find whether one of them delivers divisibility by 5.

This leaves one triplet which meets the conditions (1, 4, 7)

determine first number

exactly on of the first two is odd

the first and third differ by four

As third number is 4 and 0 is not allowed then it must be 8. As second number is odd, then 8 satisfies the condition.

EDIT: added algorithm implementation in Python language:

squares = {1, 4, 9}
digits = set(range(1, 10))
for third, fourth in ((i, i+3) for i in range(1, 7)):   
    for second in digits - {third, fourth}:             
        if (second + third) % 5 == 0:
            if fourth in squares and second in squares:
                continue
            if squares.intersection({second, fourth}):
                for first in digits - {second, third, fourth}:
                    if abs(first - third) == 4 and (first + second) % 2 != 0:
                        print(f'{first}{second}{third}{fourth}')