# Enigmatic Code

Programming Enigma Puzzles

From New Scientist #1353, 14th April 1983 [link]

Hook, Line and Sinker were the judges for the Cooker Book prize this year as usual. They assembled a short list of six and then, as usual, could not agree on the order of merit. In the end each did his own ranking, giving 6 points for first place, 5 for second and so on (no ties). Then they totalled the points, which produced a final order (also without ties). Hook gave 5 points to the book which in fact came out second and 1 point to the book which finished third. He ranked “Stuff” above “Nonsense” and gave “Umph” the number of points which Line gave to “Impenetrables”. Line ranked “Gawp” above “Elements” and placed “Impenetrables” below “Umph”. Sinker ranked “Nonsense” third and “Stuff” fifth. No book totalled 13 or 10 or received the same number of points from any two judges. One of the books totalled 8.

Can you spell out the final order?

[enigma207]

1. Jim Randell 17 July 2014 at 7:08 am

This Python program isn’t particularly elegant, or particularly quick, but it does find the answer. It runs in 535ms.

```from itertools import permutations
from enigma import irange, diff, printf

# indices for the books
books = tuple("IEGNSU")

# possible scores
scores = tuple(irange(1, 6))

# Hook's scores
for s in permutations(books):
hook = dict(zip(s, scores))
# he rated S > N
if not(hook['S'] > hook['N']): continue

# Line's scores
# he gave I the number of points Hook gave to U
hU = hook['U']
(lb, ls) = (diff(books, 'I'), diff(scores, (hU,)))
for s in permutations(lb):
line = { 'I': hU }
line.update(zip(s, ls))
# he rated G > E and I < U
if not(line['G'] > line['E'] and line['I'] < line['U']): continue

# no two scores are the same
if any(line[x] == hook[x] for x in books): continue

# Sinker's scores
# he ranked N 3rd (4 points) and S 5th (2 points)
(sb, ss) = (diff(books, 'NS'), diff(scores, (4, 2)))
for s in permutations(sb):
sinker = { 'N': 4, 'S': 2 }
sinker.update(zip(s, ss))

# no two scores are the same
if any(sinker[x] in (hook[x], line[x]) for x in books): continue

# compute the totals
total = dict((x, hook[x] + line[x] + sinker[x]) for x in books)

# all totals should be different
s = set(total.values())
if len(s) != 6: continue
# no book scored 10 or 13, but one did score 8
if not(8 in s and 10 not in s and 13 not in s): continue

# work out the order
order = sorted(books, key=lambda x: total[x], reverse=True)

# Hook gave 5 points to the book that finished 2nd
if not(hook[order[1]] == 5): continue
# and 1 point to the book that finished 3rd
if not(hook[order[2]] == 1): continue

printf("line={line} hook={hook} sinker={sinker}")
printf("total={total} order={order}")
```

Solution: The final order is: Gawp, Elements, Nonsense, Impenetrables, Umph, Stuff.

The actual scores given are shown below:

• Hugh Casement 19 November 2014 at 1:45 pm

Note that the initial letters of the book titles read GENIUS.
I’m still not inspired to read any of them, though.

2. Jim Randell 19 October 2016 at 10:05 pm

Here is the problem expressed as a set of MiniZinc constraints, which can be executed directly to give the solution.

```include "globals.mzn";

% identify the books
set of int: Books = 1..6;

int: Gawp = 1;
int: Elements = 2;
int: Nonsense = 3;
int: Impenetrables = 4;
int: Umph = 5;
int: Stuff = 6;

% identify the judges
set of int: Judges = 1..3;

int: Hook = 1;
int: Line = 2;
int: Sinker = 3;

% scores
array[Judges, Books] of var 1..6: scores;

% each judge gives each book a different score
constraint forall (j in Judges) (alldifferent([scores[j, b] | b in Books]));

% totals
array[Books] of var int: totals;

constraint forall (b in Books) (totals[b] = (sum (j in Judges) (scores[j, b])));

% totals are all different
constraint alldifferent(totals);

% positions, map position -> book
array[Books] of var 1..6: positions;

constraint alldifferent(positions);

% position 1 has the highest score, position 6 the lowest
constraint decreasing([totals[positions[i]] | i in 1..6]);

% "Hook gave 5 points to the book that came second"
constraint scores[Hook, positions[2]] = 5;

% "and 1 point to the book that finished third"
constraint scores[Hook, positions[3]] = 1;

% "He ranked 'Stuff' above 'Nonsense'"
constraint scores[Hook, Stuff] > scores[Hook, Nonsense];

% "gave 'Umph' the number of points which Line gave to 'Impenetrables'"
constraint scores[Hook, Umph] = scores[Line, Impenetrables];

% "Line ranked 'Gawp' above 'Elements'"
constraint scores[Line, Gawp] > scores[Line, Elements];

% "and placed 'Impenetrables' below 'Umph'"
constraint scores[Line, Impenetrables] < scores[Line, Umph];

% "Sinker ranked 'Nonsense' third" (i.e. 4 points)
constraint scores[Sinker, Nonsense] = 4;

% "and 'Stuff' fifth" (i.e. 2 points)
constraint scores[Sinker, Stuff] = 2;

% "No book totalled 13 or 10"
constraint forall (b in Books) (totals[b] != 13 /\ totals[b] != 10);

% "or received the same number of points from any two judges"
constraint forall (b in Books) (alldifferent([scores[j, b] | j in Judges]));

% "One of the books totalled 8."
constraint exists (b in Books) (totals[b] = 8);

solve satisfy;
```

I used the minizinc.py wrapper library that I wrote for Enigma 361 to let me bring the solution(s) back into Python for easier formatting.

This program runs in 144ms.

```from enigma import join, concat, printf
from minizinc import MiniZinc

p = MiniZinc("enigma207.mzn")

# labels (1-indexed)
books = [ None, "Gawp", "Elements", "Nonsense", "Impenetrables", "Umph", "Stuff" ]
judges = [ None, "Hook", "Line", "Sinker" ]

for (scores, totals, positions) in p.solve(result="scores totals positions", solver="mzn-gecode -a"):
# output the results by position
for (i, b) in enumerate(positions, start=1):
printf(
"{i}: {b}, {p} pts ({s})",
b=books[b],
p=totals[b],
s=join((concat(j, '=', scores[k][b]) for (k, j) in enumerate(judges[1:], start=1)), sep=', ')
)
printf()
```