Blue Prince Clock Tower (CLP(Z), Scryer)

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


The puzzle memos

There are eight clocks in a line. Each memo constrains one or more clocks:

  1. Each clock is a different time.
  2. Five clocks are set on the hour.
  3. Clock 3 does not have all different digits.
  4. Clock 4’s neighbours (clocks 3 and 5) each contain a 7.
  5. Clock 5 is set on the hour.
  6. No clock contains the digits 1, 2, 3, or 4.
  7. Clock 7 is the reverse-digit time of another clock.
  8. Clock times increase from left to right.

Note: “this clock” memos are mapped to positions 3, 4, 5, and 7 in the list.


CLP(Z) model

The model keeps everything relational and pure:


Example run

 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.


Full source (standalone)

:- 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.