This page walks through a small constraint-logic programme in Prolog
that solves the Blue
Prince clock-tower puzzle using library(clpz). It is
written as pure CLP(Z) constraints: no cuts, no
negation-as-failure for logic, no ad-hoc search in the rules, and no
procedural pruning hidden inside helper predicates. The only search step
is the final labeling/2, which is kept outside the model
(and is still a pure relation). A small run/0 predicate is
provided as an executable entry-point when you want to print solutions.
Constraints are posted in a single clockdigits_/2, which
applies every memo discovered from memo/3 so it is obvious
what is enforced.
The goal is to show the CLP(Z) style of thinking: you describe the shape of the solution (domains, relationships, reified counts), and then let the solver enumerate concrete values. That separation between constraints and search makes the code readable, testable, and easy to adapt.
Written for mietek in #scryer with help and
assistance (as always) from triska! c:
Download the source: blue-prince-clock-tower.pl
There are eight clocks in a line. Each memo constrains one or more clocks:
Note: “this clock” memos are mapped to positions 3, 4, 5, and 7 in the list.
The model keeps everything relational and pure:
[Hour, MinTens, MinOnes] with a linked numeric time
HMM.#<==> for counting and matching.all_distinct/1 and
sum/3 express “all different” and “exactly five”.clockdigits_/2 posts
the full memo list, making omissions harder to miss.findall/3 collects
memo indices from memo/3 using fixed-length lists, so every
memo is applied without a separate hard-coded list.labeling([ffc], ...) performs
search and is kept separate, while remaining a pure relation.待 scryer-prolog -g run -t halt blue-prince-clock-tower.pl
["5:00","5:08","5:57","6:00","7:00","8:00","8:05","9:00"].Each entry is a clock time formatted as H:MM, emitted as
a Prolog list of strings.
If you are running from the command line, -t halt tells
Scryer to exit cleanly even if the goal fails or throws.
:- use_module(library(clpz)).
:- use_module(library(lists)).
:- use_module(library(dcgs)).
:- use_module(library(format)).
:- use_module(library(lambda)).
% ~byakuren
% ^ ^
% =(o.o)=
% m m
% Representation:
% - A clock time is a 3-digit integer HMM with H in 1..9 and MM in 00..59.
% - Clock positions are 1..8, left to right, and each has a digit triple [Hour, MinTens, MinOnes].
% Representation helpers
clock_time_digits(Time, [Hour, MinTens, MinOnes]) :-
Hour in 1..9,
MinTens in 0..5,
MinOnes in 0..9,
Time #= Hour * 100 + MinTens * 10 + MinOnes.
clock_layout(ClockTimes, ClockDigits) :-
length(ClockTimes, 8),
length(ClockDigits, 8),
maplist(clock_time_digits, ClockTimes, ClockDigits).
% Memo helpers
is_on_the_hour([_, MinTens, MinOnes], B) :-
B #<==> (MinTens #= 0 #/\ MinOnes #= 0).
has_repeat_digit([Hour, MinTens, MinOnes]) :-
Hour #= MinTens #\/ Hour #= MinOnes #\/ MinTens #= MinOnes.
has_digit(D, [Hour, MinTens, MinOnes]) :-
Hour #= D #\/ MinTens #= D #\/ MinOnes #= D.
digits_exclude_1_4([Hour, MinTens, MinOnes]) :-
Hour in 5..9,
MinTens in 0 \/ 5,
MinOnes in 0 \/ 5..9.
is_reverse_digits_of([Hour1, MinTens1, MinOnes1], [Hour2, MinTens2, MinOnes2], B) :-
B #<==> (Hour1 #= MinOnes2 #/\ MinTens1 #= MinTens2 #/\ MinOnes1 #= Hour2).
% Memo 1: each of these clocks should be set to a different time.
memo(1, ClockTimes, _ClockDigits) :-
all_distinct(ClockTimes).
% Memo 2: five of these clocks should be set to a time ending on the hour.
memo(2, _ClockTimes, ClockDigits) :-
maplist(is_on_the_hour, ClockDigits, Flags),
sum(Flags, #=, 5).
% Memo 3: this clock should not have all different digits.
memo(3, _ClockTimes, ClockDigits) :-
nth1(3, ClockDigits, Third),
has_repeat_digit(Third).
% Memo 4: this clock's neighbours should both be set to a time containing a 7.
memo(4, _ClockTimes, ClockDigits) :-
nth1(3, ClockDigits, Third),
nth1(5, ClockDigits, Fifth),
has_digit(7, Third),
has_digit(7, Fifth).
% Memo 5: this clock should be set on the hour (ending in :00).
memo(5, _ClockTimes, ClockDigits) :-
nth1(5, ClockDigits, Fifth),
is_on_the_hour(Fifth, 1).
% Memo 6: none of these clocks should be set to times that contain 1, 2, 3, or 4.
memo(6, _ClockTimes, ClockDigits) :-
maplist(digits_exclude_1_4, ClockDigits).
% Memo 7: this clock should be set to a time that contains the same three numbers as another clock but in reverse order.
memo(7, _ClockTimes, ClockDigits) :-
nth1(7, ClockDigits, Seventh),
maplist(is_reverse_digits_of(Seventh), ClockDigits, Flags),
nth1(7, Flags, 0),
sum(Flags, #>=, 1).
% Memo 8: the clocks are positioned so that their times go up in ascending order.
memo(8, ClockTimes, _ClockDigits) :-
chain(#<, ClockTimes).
% Output
print_solution(ClockDigits) :-
findall(Chars,
( member([Hour, MinTens, MinOnes], ClockDigits),
phrase(format_("~d:~d~d", [Hour, MinTens, MinOnes]), Chars)
),
Pretty),
portray_clause(Pretty).
% Solver and entrypoint
memos(ClockTimes, ClockDigits, Ns) :-
findall(N, memo(N, ClockTimes, ClockDigits), Ns).
clockdigits_(ClockDigits, ClockTimes) :-
clock_layout(ClockTimes, ClockDigits),
memos(ClockTimes, ClockDigits, Ns),
maplist([ClockTimes, ClockDigits]+\N^memo(N, ClockTimes, ClockDigits), Ns).
clockdigits(ClockDigits) :-
clockdigits_(ClockDigits, ClockTimes),
labeling([ffc], ClockTimes).
run :-
clockdigits(ClockDigits),
print_solution(ClockDigits),
false.