**Run:** [ @repl.it ]

from itertools import product from enigma import nconcat, is_prime, printf # digits that can be successfully mirrored mirror = { 0: 0, 1: 1, 2: 5, 3: 3, 5: 2, 8: 8 } # consider the initial 4 digits keyed in for ds in product(mirror.keys(), repeat=4): # the initial 4-digit number n = nconcat(ds) if n < 1000: continue # the mirrored number (is less than n) m = nconcat(mirror[d] for d in ds) if not(m < n): continue # their sum is a 5-digit number s = n + m if s < 10000: continue # their difference d = n - m # the quotient s/d is a 4-digit prime (q, r) = divmod(s, d) if r != 0 or q < 1000 or not is_prime(q): continue printf("n={n} m={m}, s={s} d={d}, q={q}")

**Solution:** The original number was 8805.

The reflected number is 8802. Giving a sum of 17607, and difference of 3. The quotient is 17607 / 3 = 5869.

]]>**Run:** [ @repl.it ]

from itertools import product from enigma import tuples, printf # T = always tells the truth def T(ss): return all(ss) # F = always tells lies def F(ss): return not any(ss) # X = alternates between true and false def X(ss): return all(a ^ b for (a, b) in tuples(ss, 2)) # choose an assignment for Left, Right handers for (L, R) in [(T, F), (F, T)]: # assign traits to each for (A, B, C, D, E) in product((L, R, X), repeat=5): # check the statements for each person if not all([ A([B is L, E is L]), B([A is R, D is R]), C([A is X, C is X]), D([C is L, D is R]), E([A is L, B is X]), ]): continue # output solution (nL, nR, nA, nB, nC, nD, nE) = (x.__name__ for x in (L, R, A, B, C, D, E)) printf("L={nL} R={nR}: A={nA} B={nB} C={nC} D={nD} E={nE}")

**Solution:** A is left-handed; B is ambidextrous; C is left-handed; D is right-handed; E is right-handed.

Left-handers always lie, and right-handers always tell the truth.

]]>**Run:** [ @repl.it ]

from enigma import Football, digit_map, irange, concat # scoring system football = Football(games='wdlx', points=dict(w=2, d=1)) # the columns of the table (digits in the table stand for themselves) table = dict(played='???2', l='?1??', d='0??1', points='?14?') # find possible match outcomes for (ms, d) in football.substituted_table(table, d=digit_map()): # consider goals for B = x, then goals against D = x + 3 for x in irange(0, 6): gf = concat(5, x, 6, 3) ga = concat(2, 2, 7, x + 3) # find possible scorelines for ss in football.substituted_table_goals(gf, ga, ms, d=d): # output solution football.output_matches(ms, ss, teams="ABCD")

**Solution:** The scores in the played games are: A vs C = 5-2; B vs C = 0-1; B vs D = 1-1; C vs D = 3-2.

The A vs B and A vs D matches are not yet played.

]]>Factor 11 is not possible, because there can be no double digits.

I think I’m right in saying that in 72 games one could expect to win (on average) with factor 13 seven times, 17 and 19 five times each, 23 four times, 29 and 31 three times each, 37, 41, 43, and 47 twice each, and each higher prime up to 97 once. That is an overall probability 5/8 = 0.625 of winning.

**Run:** [ @repl.it ]

from itertools import permutations from collections import defaultdict from enigma import irange, prime_factor, printf # find 2-digit numbers that do not have a 2-digit prime factor # store them as a map of: <tens digit> -> <possible unit digits> d = defaultdict(list) for (a, b) in permutations(irange(1, 9), 2): if any(p > 10 for (p, e) in prime_factor(10 * a + b)): continue d[a].append(b) # add k more digits to the sequence s def solve(s, k): # are we done? if k == 0: # check the the last and first digit if s[0] in d[s[-1]]: yield s else: # consider the next digit for x in d[s[-1]]: # we can't have already used it if x in s: continue # solve for the rest yield from solve(s + [x], k - 1) # start with digit 1, and add 8 more digits for s in solve([1], 8): # output the sequence printf("{s}")

**Solution:** The digits are: 1, 6, 3, 2, 7, 5, 4, 9, 8.

Giving the 2-digit numbers: 16, 63, 32, 27, 75, 54, 49, 98, 81.

]]>% A Solution in MiniZinc include "globals.mzn"; % I J K L % ------------ % GH ) A B C D E F % M N P % ---- % Q R D % S T U % ----- % V E % W X % --- % Y F % Y F % === var 2..9:A; var 2..9:B; var 2..9:C; var 2..9:D; var 2..9:E; var 2..9:F; var 2..9:G; var 2..9:H; var 2..9:I; var 2..9:J; var 2..9:K; var 2..9:L; var 2..9:M; var 2..9:N; var 2..9:P; var 2..9:Q; var 2..9:R; var 2..9:S; var 2..9:T; var 2..9:U; var 2..9:V; var 2..9:W; var 2..9:X; var 2..9:Y; % all four digits of the answer were different constraint all_different ( [I,J,K,L] ); var 10..99: GH = 10*G + H; var 100000..999999: ABCDEF = 100000*A + 10000*B + 1000*C + 100*D + 10*E + F; var 1000..9999:IJKL = 1000*I + 100*J + 10*K + L; var 100..999: ABC = 100*A + 10*B + C; var 100..999: MNP = 100*M + 10*N + P; var 10..99: QR = 10*Q + R; var 100..999: QRD = 100*Q + 10*R + D; var 100..999: STU = 100*S + 10*T + U; var 10..99: VE = 10*V + E; var 10..99: WX = 10*W + X; var 10..99: YF = 10*Y + F; % intermediate multiplication and subtraction sums constraint I * GH == MNP /\ ABC - MNP == QR; constraint J * GH == STU /\ QRD - STU == V; constraint K * GH == WX /\ VE - WX == Y; constraint L * GH == YF; % all other digits (apart from 0 and 1) occurred at least twice constraint forall (d in 2..9) (at_least(2, [A,B,C,D,E,F,G,H,I,J,K,L,M,N,P,Q,R,S,T,U,V,W,X,Y], d)); solve satisfy; % A = 2; B = 5; C = 3; D = 5; E = 3; F = 6; % G = 3; H = 2; I = 7; J = 9; K = 2; L = 3; % M = 2; N = 2; P = 4; Q = 2; R = 9; S = 2; % T = 8; U = 8; V = 7; W = 6; X = 4; Y = 9; % ---------- % ========== % Finished in 247msec % 7 9 2 3 % ------------ % 3 2 )2 5 3 5 3 6 % 2 2 4 % ----- % 2 9 5 % 2 8 8 % ----- % 7 3 % 6 4 % --- % 9 6 % 9 6 % ===]]>

**Run:** [ @repl.it ]

from itertools import permutations from enigma import printf # check a statement: X says S def check(X, S): # only X == 1 makes true statements return (X == 1) == S # order the weights for (A, B, C, D) in permutations((1, 2, 3, 4)): # check the statements if not all([ check(A, B < D), check(B, A > C), check(C, C > D), check(D, C > B), ]): continue # output the solution printf("A={A} B={B} C={C} D={D}")

**Solution:** A is the lightest; then C; then B; and D is the heaviest.

But as you construct the original sum to output the solution you could move the check into the Python code, and have a simpler *MiniZinc* model (although this requires MiniZinc to be returning all solutions (which is the default behaviour)).

from minizinc import MiniZinc, var, alphametic fmt = """ A B C D ------------- E F ) G H I J K L M N O ----- P Q J R S T ----- U K V W --- X L X L === """ p = MiniZinc(f""" % declare the decision variables {var("2..9", "ABCDEFGHIJKLMNOPQRSTUVWX")}; % overall division sum constraint {alphametic("{EF} * {ABCD} = {GHIJKL}")}; % partial products constraint {alphametic("{A} * {EF} = {MNO}")}; constraint {alphametic("{B} * {EF} = {RST}")}; constraint {alphametic("{C} * {EF} = {VW}")}; constraint {alphametic("{D} * {EF} = {XL}")}; % partial differences constraint {alphametic("{GHI} - {MNO} = {PQ}")}; constraint {alphametic("{PQJ} - {RST} = {U}")}; constraint {alphametic("{UK} - {VW} = {X}")}; solve satisfy; """) for s in p.solve(): r = p.substitute(s, fmt) # check each digit 2..9 occurs (at least) twice in the sum if all(r.count(x) > 1 for x in '23456789'): print(r)]]>

from minizinc import MiniZinc, var, alphametic, substitute fmt = """ A B C D ------------- E F ) G H I J K L M N O ----- P Q J R S T ----- U K V W --- X L X L === """ p = MiniZinc(f""" include "globals.mzn"; % declare the decision variables {var("2..9", "ABCDEFGHIJKLMNOPQRSTUVWX")}; {var("array[1..24] of", "2..9", "Y")} = [{', '.join("ABCDEFGHIJKLMNOPQRSTUVWX")}]; % overall division sum constraint {alphametic("{EF} * {ABCD} = {GHIJKL}")}; % partial products constraint {alphametic("{A} * {EF} = {MNO}")}; constraint {alphametic("{B} * {EF} = {RST}")}; constraint {alphametic("{C} * {EF} = {VW}")}; constraint {alphametic("{D} * {EF} = {XL}")}; % partial differences constraint {alphametic("{GHI} - {MNO} = {PQ}")}; constraint {alphametic("{PQJ} - {RST} = {U}")}; constraint {alphametic("{UK} - {VW} = {X}")}; % at least two digits from each of 2..9 constraint forall (x in 2..9) (at_least(2, Y, x)); solve satisfy; """) for s in p.solve(): print(p.substitute(s, fmt))]]>

We can use the [[ `--extra` ]] parameter to include the additional check that all digits between 2 and 9 occur at least twice in the completed sum.

This run file executes in 134ms.

**Run:** [ @repl.it ]

#!/usr/bin/env python -m enigma -r # IJKL # -------- # GH ) ABCDEF # MNP # --- # QRD # STU # --- # VE # WX # -- # YF # YF # == SubstitutedDivision # there are no 0's and no 1's --digits="2-9" # the main division sum "ABCDEF / GH = IJKL" # the intermediate subtraction sums "ABC - MNP = QR" "QRD - STU = V" "VE - WX = Y" "YF - YF = 0" # extra condition: each valid digit occurs (at least) twice in the full sum --code="check = lambda *s: all(s.count(d) > 1 for d in irange(2, 9))" --extra="check(I, J, K, L, G, H, A, B, C, D, E, F, M, N, P, Q, R, D, S, T, U, V, E, W, X, Y, F, Y, F)" # the answer is the dividend --answer="ABCDEF" # the digits in the result are distinct [optional] --distinct="IJKL" # tidy up output [optional] --solution=""

**Solution:** The dividend is 253536.

The full sum is: 253536 ÷ 32 = 7923.

]]>**Run:** [ @repl.it ]

from collections import defaultdict from enigma import irange, factor, nreverse, printf # find 3-digit semi-primes semis = list(n for n in irange(100, 999) if len(factor(n)) == 2) # record the sum semi-primes d = defaultdict(list) # find n where the reverse is a (higher) semi-prime # and the sum of n and its reverse is also a semi-prime for n in semis: r = nreverse(n) if r < n: continue s = n + r if not(r in semis and s in semis): continue printf("[{n} + {r} = {s}]") d[s].append((n, r)) # look for consecutive sums for s in sorted(d.keys()): if s + 1 in d: printf("{s} -> {ds}, {s1} -> {ds1}", ds=d[s], s1=s + 1, ds1=d[s + 1])

**Solution:** The consecutive numbers are 706 (= 155 + 551) and 707 (=205 + 502).

The prime factorisations of these numbers are:

155 = 5 × 31

205 = 5 × 41

502 = 2 × 251

551 = 19 × 29

706 = 2 × 353

707 = 7 × 101

The full list of semi-prime sums are:

]]>505 = 203 + 302

626 = 115 + 511

706 = 155 + 551

707 = 205 + 502

766 = 185 + 581

949 = 326 + 623

It is based on the observation that the sum of all permutations of *ABCD* is *6666 × (A + B + C + D)*.

It runs in 160ms.

#!/usr/bin/env python -m enigma -r SubstitutedExpression --digits="1-9" "match(6666 * (A + B + C + D) - ABCD, '122??0')"]]>

If we colour the nodes of the graph as follows:

We see that we can only proceed to a green node from a red node, and vice versa. So any path will alternate between visiting red and green nodes.

We can therefore partition the given route into those nodes in an odd position and those in an even position.

For a valid route each node will appear either in only odd positions, or only even positions.

So we can find the transposed nodes by looking for those that appear in both partitions.

This Python program runs in 95ms.

**Run:** [ @repl.it ]

from enigma import printf # given route route = "PADMOICTFKGBJRHNLQESPALQJRHNDMOICTSFEKGB" # partition the nodes even = route[0::2] odd = route[1::2] # find transposed nodes (that appear in both partitions) t = set(even).intersection(odd) if len(t) == 2: printf("transposed = {t}", t=sorted(t))

**Solution:** F and S have been transposed.

F and S only appear in adjacent positions towards the end, so the correct route should end:

]]>… M O I C T

FSE K G B

**Run:** [ @repl.it ]

from itertools import product, permutations from enigma import irange, icount, seq_all_same, tuples, printf # houses houses = irange(1, 5) # m records who sends cards to who # m[(x, y)] = True; if x sends a card to y # = False; if x does not send a card to y # = None; if currently unknown m0 = dict(((x, y), None) for (x, y) in product(houses, repeat=2) if x != y) # update map m, for keys not involving x, according to vs # return a new map, or None def update(m, x, vs): m1 = m.copy() for (k, v) in m.items(): if x in k: continue u = (k in vs) if v is None: m1[k] = u else: if v != u: return None return m1 # house number 3 gives the following information # for cards not involving house 3 vs1 = set([(1, 5), (5, 1), (2, 1)]) for vs in [[(2, 4)], [(4, 2)]]: m1 = update(m0, 3, vs1.union(vs)) if m1 is None: continue # assign females to the houses for (fA, fB, fC, fD, fE) in permutations(houses): # fB gives information about all cards not invloving fB vs2 = set([(fA, fE), (fE, fA), (fD, fC), (fC, fD), (fD, fE)]) m2 = update(m1, fB, vs2) if m2 is None: continue # assign surnames to the houses for (S, T, U, V, W) in permutations(houses): # S gives information about all cards not involving S vs3 = set([(T, U), (U, T), (W, V), (V, W), (T, W)]) for vs in [[(U, V)], [(V, U)]]: m3 = update(m2, S, vs3.union(vs)) if m3 is None: continue # assign males to the houses for (mA, mB, mC, mD, mE) in permutations(houses): # make sure there is someone called "Charles Thomas" if not(mC == T): continue # mA gives information about all cards not involving mA vs4 = set([(mB, mC), (mC, mB), (mB, mD), (mD, mB)]) for vs in product([(mC, mD), (mD, mC)], [(mB, mE), (mE, mB)]): m4 = update(m3, mA, vs4.union(vs)) if m4 is None: continue # make sure Mr Thomas sends and receives the same number of cards if not seq_all_same(icount(k for (k, v) in m4.items() if k[i] == T and v) for i in (0, 1)): continue # choose an order for Mr Thomas's walk for w in permutations(houses): # he starts at his own house if not(w[0] == T): continue # and there is a corresponding chain of cards if not all(m4[k] for k in tuples(w + w[:1], 2)): continue printf("f={f}, m={m}, s={s}, w={w}", f=(fA, fB, fC, fD, fE), m=(mA, mB, mC, mD, mE), s=(S, T, U, V, W))

**Solution:** The residents of the close are:

1: Alan and Christine, Williams

2: Charles and Brenda, Thomas

3: Brian and Emma, Unwin

4: Derek and Alice, Smith

5: Eric and Dawn, Vincent

The card exchanges are (3 bidirectional, 3 unidirectional):

1 ↔ 5

2 ↔ 3

3 ↔ 4

2 → 1

4 → 2

5 → 3

Charles’s walk is: 2 → 1 → 5 → 3 → 4 → 2.

]]>Here is a version of this puzzle that uses matplotlib and sympy with Python to produce the graph and the solution.

from sympy import symbols, cancel, fraction, solveset, N import matplotlib.pyplot as plt from matplotlib.ticker import MultipleLocator # set up the function in sympy (line is red when f(x) > 0) x = symbols('x') e = 1 / (x - 1) + 2 / (x - 2) + 3 / (x - 3) + 4 / (x - 4) - 5 # find its algebraic numerator and denominator n, d = fraction(cancel(e)) print(f"numerator: {n}") print(f"denominator: {d}") # evaluate the sums of its roots and poles as the # negatives of the ratios of the coefficients of # the x^3 and x^4 terms in the expansions of the # numerator and denominator sum_roots = - n.coeff(x, 3) // n.coeff(x, 4) sum_poles = - d.coeff(x, 3) print(f"The length of the red parts is {sum_roots - sum_poles} metres.") # compute the numeric positions of its poles and roots roots = [complex(N(x)).real for x in solveset(n)] poles = [float(N(x)) for x in solveset(d)] # data for the function plot xx, yy = [], [] for i in range(0, 601): xv = i / 100 if xv != int(xv): yv = 1 / (xv - 1) + 2 / (xv - 2) + 3 / (xv - 3) + 4 / (xv - 4) - 5 xx.append(xv) yy.append(yv) # set up the graph paper ax = plt.subplot() ax.set(xlabel='x', title="1 / (x - 1) + 2 / (x - 2) + 3 / (x - 3) + 4 / (x - 4) - 5") ax.axis([0, 6, -20, 20]) ax.xaxis.set_major_locator(MultipleLocator(1)) ax.xaxis.set_minor_locator(MultipleLocator(0.1)) ax.yaxis.set_major_locator(MultipleLocator(10)) ax.yaxis.set_minor_locator(MultipleLocator(1)) ax.grid(axis='both', which='both', color='0.8', linewidth=1) # plot and show the graph ax.plot(xx, yy, color='k') for x0, x1 in zip(poles, roots): ax.plot([x0, x1], [0, 0], color='r') plt.show()]]>

from fractions import Fraction from enigma import Polynomial, irange, printf # the sum of the roots of a polynomial sum_roots = lambda p: Fraction(-p[-2], p[-1]) # form the polynomials, p(x) / q(x) = f(x) - 5 (x1, x2, x3, x4) = (Polynomial([-k, 1]) for k in irange(1, 4)) q = x1 * x2 * x3 * x4 p = ( (Polynomial([1]) * x2 * x3 * x4) + (Polynomial([2]) * x1 * x3 * x4) + (Polynomial([3]) * x1 * x2 * x4) + (Polynomial([4]) * x1 * x2 * x3) + (Polynomial([-5]) * q) ) printf("[p = {p}, q = {q}]") # the answer is... r = sum_roots(p) - sum_roots(q) printf("red = {r} m")]]>

I hadn’t come across the result for the sum and product of polynomial roots before. Namely:

]]>For a polynomial of the form:

f(x) = a.x^n + b.x^(n – 1) + c.x^(n – 2) + … + zThe sum of the roots is:

–b/aThe product of the roots is:

z/a(whennis even);–z/a(whennis odd).

f(x) = 1/(x – 1) + 2/(x – 2) + 3/(x – 3) + 4/(x – 4)

One way to solve this problem is to divide the line into segments, and then colour each segment according to the value of *f(x)* at some point in the segment.

In this Python program we divide the line into 1 million segments, and give an answer to the nearest millimetre. It runs in 413ms.

**Run:** [ @repl.it ]

from enigma import irange, number, arg, printf # the function (for float x) f = lambda x: sum(i / (x - i) for i in (1.0, 2.0, 3.0, 4.0)) # divide the line into 1/N segments N = number(arg("1_000_000", 0)) n = 1.0 / float(N) # count the number of segments painted red red = 0 for i in irange(0, N - 1): x = float(6 * i + 3) * n try: v = f(x) if not(v < 5.0): red += 1 except ArithmeticError: continue printf("{d:.3f}m red [{red} of {N} segments]", d=red * n * 6.0)

**Solution:** The total length of the red parts is 2 metres.

The program finds that 333,333 of the 1,000,000 segments are coloured red, which is almost exactly a third.

We can divide the line into segments of length 1 micrometre, and consider 6 million segments, and we would expect exactly 2 million of them to be coloured red. This takes a little longer (1.2s), but gives us the answer we expect:

% time pypy enigma1017.py 6_000_000 2.000m red [2000000 of 6000000 segments]

To get a faster numerical solution we can consider the graph of *f(x)*.

The function is undefined at x = 1, 2, 3, 4. So we consider the segments (0, 1), (1, 2), (2, 3), (3, 4), (4, 6), where the function is defined.

In the (0, 1) segment all the fractions are negative, and as *x* approaches 1, then *f(x)* approaches –∞, so in this segment the value of the function is always less than 5. So this segment contributes no red to the finished work.

In the (1, 2) segment, when *x* is only a tiny bit larger than 1, then the first fraction give a large positive value, and the remaining fractions contribute much smaller negative values. So the function starts at +∞ at *x = 1* and as *x* approaches 2 the function approaches –∞. Somewhere in the middle of the segment the function will have the value 5. We call this value *(1 + x1)*, and in the interval *(1, 1 + x1]* the function will have a value greater than or equal to 5. So this segment will contribute a length or *x1* of red to the finished work.

Similarly in the (2, 3) segment, the function goes from +∞ to –∞, and so takes on the value 5, at *(2 + x2)*. So this contributes a value of *x2* to the finished work.

Likewise, in the segment (3, 4) will contribute *x3* to the finished work.

In the segment (4, 6), the function starts at +∞, and as *x* increases the fractions will get smaller and smaller, so the value of the function would eventually approach 0. If it reaches the value of 5 in the segment (4, 6) then *x4* will be less than 2, if not the entire segment will contribute red to the finished work and *x4* will be 2.

So if we find when the function takes on the value 5 in the appropriate segments we can find the total length of red in the finished work.

The graph actually looks like this: (*f(x)* in blue, the areas where *f(x) ≥ 5* indicated in red)

We can use the following program to calculate the total length of the red segments. It uses the [[ `find_value()` ]] function from the **enigma.py** library and runs in 74ms.

from enigma import tuples, find_value, printf # the function (for float x) f = lambda x: sum(i / (x - i) for i in (1.0, 2.0, 3.0, 4.0)) # amount of red red = 0.0 # consider the segments for (a, b) in tuples((1.0, 2.0, 3.0, 4.0, 6.0), 2): try: # find when f = 5 in the segment (a, b) r = find_value(f, 5.0, a, b) red += r.v - a printf("segment ({a}, {b}): f({r.v:.6f}) = {r.fv:.6f}") except ValueError: # otherwise: f > 5 for the whole segment red += b - a # output total red printf("red = {red:.6f} m")

The four places where the function have has a value of 5 are: (to 6 dp)

x = 1.098289

x = 2.197573

x = 3.331426

x = 5.372712

These are the roots of the equation:

f(x) = 5

which are also the roots of:

x⁴ – 12x³ + 49x² – 80x + 216/5 = 0

*Wolfram Alpha* can work out the exact values of these: [ link ].

Also if they were born a couple of days apart, on the day in between they would have different numerical ages, but *Z* could still reasonably say “*X* and *Y* are the same age”.

So here is my variation (taking the description of the tribes as read):

On the Island of Imperfection they always count their eggs before they are hatched. Indeed the number of unhatched eggs is so important everybody knows exactly how many everybody else has.

Between them A, B, C, and D represent all three tribes.

A:(1) My number of eggs is a multiple of 6;

(2) B has a different number of eggs to me;

(3) D belongs to a more truthful tribe than I do.

B:(1) I have more eggs than A;

(2) A and C have exactly the same number of eggs;

(3) D’s number of eggs is a multiple of twelve.

C:(1) I have more eggs than A;

(2) D has an even number of eggs;

(3) B’s second remark is true.

D:(1) B has exactly three times as many eggs as C;

(2) I have exactly 1 dozen more eggs than A;

(3) C’s number of eggs is a multiple of thirteen.Everyone has at least 12 eggs, bit no-one has more than 105.

Find the tribes to which each belongs, and how many eggs they have.

We can then proceed to solve this variation.

This Python program generates potential sets of numbers depending on who is telling the truth. It then examines the possible assignments of tribes, and chooses an appropriate set of numbers to consider. But it’s not quick, it runs in 16.7s (but there is a way to speed this up – see below).

**Run:** [ @repl.it ]

from itertools import product from enigma import tuples, irange, divc, Delay, printf # Pukka - always tells the truth def P(ss): return all(ss) # Wotta-Woppa - never tells the truth def W(ss): return not any(ss) # Shilli-Shalla - alternates between true and false def S(ss): return all(a ^ b for (a, b) in tuples(ss, 2)) # truthfulness (how many true statements out of every 2) T = { P: 2, W: 0, S: 1 } # min, max number of eggs (m, M) = (14, 105) # generate numbers according to a truth-teller # if A is Pukka: A % 6 = 0, B != A def eggsA(): for A in irange(6 * divc(m, 6), M, step=6): for B in irange(m, M): if B == A: continue for (C, D) in product(irange(m, M), repeat=2): yield (A, B, C, D) # if B is Pukka: B > A, C = A, D % 12 = 0 def eggsB(): for D in irange(12 * divc(m, 12), M, step=12): for A in irange(m, M): for B in irange(A + 1, M): yield (A, B, A, D) # if C is Pukka: C > A, D % 2 = 0, A = C # this is not possible eggsC = lambda: [] # if D is Pukka: B = 3C, D = A + 12, C % 13 = 0 def eggsD(): for C in irange(13 * divc(m, 13), M, step=13): B = 3 * C if B > M: break for A in irange(m, M): D = A + 12 if D > M: break yield (A, B, C, D) # collect the potential sets of eggs eggs = ( Delay(lambda: list(eggsA())), Delay(lambda: list(eggsB())), Delay(lambda: list(eggsC())), Delay(lambda: list(eggsD())), ) # consider the permutations of the tribes for ts in product((P, W, S), repeat=4): # there is 1 representative from each tribe if not(all(t in ts for t in (P, W, S))): continue (tA, tB, tC, tD) = ts # neither A nor C can be Pukka [this can be determined by analysis] if tA == P or tC == P: continue # choose the smallest set of eggs, defined by a Pukka eggsP = min((x.value for (x, t) in zip(eggs, ts) if t == P), key=len) # consider eggs for (nA, nB, nC, nD) in eggsP: # A's statements if not tA([ # "A is a multiple of 6" nA % 6 == 0, # "A is different to B" nA != nB, # "D belongs to a more truthful tribe than I do" T[tD] > T[tA], ]): continue # B's statements if not tB([ # "B > A" nB > nA, # "A = C" nA == nC, # "D is a multiple of 12" nD % 12 == 0, ]): continue # C's statements if not tC([ # "C > A" nC > nA, # "D is even" nD % 2 == 0, # "B's second remark [A = C] is true" nA == nC, ]): continue # D's statements if not tD([ # "B = 3C" nB == 3 * nC, # "D = A + 12" nD == nA + 12, # "C is a multiple of 13" nC % 13 == 0, ]): continue printf("A={tA.__name__}:{nA}, B={tB.__name__}:{nB}, C={tC.__name__}:{nC} D={tD.__name__}:{nD}")

**Solution:** A is a Shilla-Shalla, with 78 eggs; B is a Wotta-Woppa, with 78 eggs; C is a Shilla-Shalla, with 26 eggs; D is a Pukka, with 90 eggs.

(Using ages, with the issue described above we can get multiple solutions. For example:

A is Shilli-Shalla, age 105; B is Shilli-Shalla, age 104; C is Pukka, age 105; D is Wotta-Woppa, age 104

This would provide a viable solution if C born on the same day as A, but slightly earlier in the day).

We can do a bit of analysis to speed the program up:

We can see that *C* cannot be a Pukka (he says: (1) “C > A” and (3) “A = C”). Also *A* cannot be a Pukka (he says: (3) “D is more truthful than me”).

If we remove these cases from consideration (see line 69) the run time is reduced to a more acceptable 772ms.

The program is written to use delayed evaluation provided by the [[ `Delay()` ]] class in the **enigma.py** library (see my comment on **Enigma 371**), and it turns out that the [[ `eggsA()` ]] function is never called (it generates over 11 million potential sets of numbers, so we save a lot of time by not generating and considering them).