Monday, January 11, 2016

2011 Sprouts Clash of the Titans - Game 1 Analysis

Clash of the Titans, Game 1: Analysis by Josh Jordan

Clash of the Titans, Game 1

Analysis by Josh Jordan

Game 1 of the World Game of Sprouts Association-sponsored “2011 Clash of the Titans” match between WGOSA World Champion Roman Khorkov and me took place over email in early February, 2011. I was assisted by my computer program Aunt Beast, while Roman used only his brain. After I won the game, Roman wrote:

“This was an amazing game. Very intriguing... It added my white hair. It will be good if you make a detailed analysis. We simply have to bring it to the public!”

As requested, here is my analysis. First, though, let's have a look at these two titans.

Roman Khorkov posing on a boat Josh Jordan, leaning against a bridge in Paris
Roman Khorkov Josh Jordan

27- (Khorkov*)

Sprouts is a two-player pencil-and-paper game invented in the 1960s by mathematicians John Conway and Michael Patterson. The game starts with a field of dots on an otherwise-empty piece of paper. The move is to add a new dot along with two lines joining it to existing dots subject to the conditions that (a) lines may not cross and (b) no dot may have more than 3 lines coming out of it. (Mathematically speaking, each move extends a plane multigraph of maximum degree 3 by adding a new vertex of degree 2.) The players alternate turns until it is no longer possible to move, at which point the game is over. Like any impartial combinatorial game, sprouts can be played with two different win conditions: (a) last player wins or (b) last player loses. The former is known as normal play, the latter as misère (pronounced something like "MEE-zair") play.

The top human players seem to be able to play normal sprouts quite accurately. For this reason, the emphasis in modern tournament sprouts has shifted to misère play. (According to the Wikipedia entry on misère games, "Perhaps the only misère combinatorial game played competitively in an organized forum is Sprouts.") However, skill in normal play does not go to waste: the main principle of human misère sprouts strategy is to to create a large component with firm nim-values (see below), in the presence of which the game's misère outcome is usually the same as its normal outcome. (A component is a minimal contiguous region of the game with the property that a move in that region does not affect the moves available in the rest of the game.)

In sprouts, one player serves, that is, determines the number of initial spots and the win condition (normal or misère), and the other player then decides who plays first.. The player who plays first is known as Left; the other is known as Right. Here Roman has served 27 spots, misère. Had he intended normal play, the serve would have been written as "27+".


27- (Jordan+Aunt Beast - Khorkov*) 1(28)2

The shape that results from connects two initial spots like this is known as an anago.

Even though 27- is thought to be a win for Right, I opt to move first, in part to show how a computer can help win a theoretically losing position. We're a long way from knowing for sure whether 27- is a win for Left or a win for Right, so in the opening phase I merely try to make the position more complex, in accordance with the Enough Rope Principle.

The diagrams here follow Winning Ways by coloring Left's move bLue and Right's move Red.


1(29)1[2,3]

Roman responds with the analogue of the correct reply in 15-. According to theory, adding a multiple of 6 spots to a region with a large number of spots doesn't change the outcome of the game, so this move should also work for 27-.


4(30)29

Continuing the standard opening for 15-, I make a *3 component in S(2). (The notation S(x) means the entire component containing spot x.) I will try to make more of these *2 and *3 components as the game continues. An identical pair of nim-heaps of size at least 2 make a component with firm nim-values 00. In misère play, when the position contains a component with firm nim-values, further identical pairs of these nim-heaps can can usually be ignored as in normal play, but not always…

It may be useful to recall some definitions and notation. A nim-heap of size x is denoted by *a. The nim-value of a position x is the size of the nim-heap that must be adjoined to x to result in a P-position, that is, a loss for the player to move. A position that is a win for the player to move is known as an N-position.

The nim-values of a position are its normal nim-value a and its misère nim-value b, written ab. A component with firm nim-values is a component with identical normal and misère nim-values. This is not the same as a firm component, which is a tame component with firm nim-values. A component x is tame whenever there exist nim-heaps a, b, …, c such that, for any impartial combinatorial game y, a+b+…+c+y has the same misère outcome as x+y. (Note that y doesn't necessarily have to follow the rules of sprouts or nim.) Most components with firm nim-values are not tame. A partially-correct table of nim-values for various common sprouts components can be found at Playing Sprouts with Misère Grundy Numbers (Peltier, 2008).


4(31)4[5-20]

Roman makes a group of 16 spots and a group of 7. If spots 30 and 31 were removed, both of these groups would be components with firm nim-values of 11.


5(32)5[6-9]

With Aunt Beast, I try to disrupt the human strategy by steering the game into positions in which the normal outcome differs from the misère outcome, even in the presence of components with firm nim-values. Playing those types of positions correctly seems to be very difficult for today's human sprouts players.

I have noticed that many of these tricky positions contain groups of four spots, so I try to create such a group here. This was almost certainly a mistake, since it allows Roman to effectively remove those four spots from the game on his next move.


5(33)32[6]

Roman simplifies. S(6) is now effectively dead (equivalent to *0).

Dead spots and components which I know to be effectively dead (equivalent to *0, either by themselves or in combination with other components) are colored gray in the diagrams. Further play in these dead components is pointless.


10(34)11

Trying again to add complexity, I fall back on the usual approach of playing an anago.


30(35)31[21,22]

I expect Roman to close off the anago with 10(35)10[11] or 10(35)10[11,12], but he surprises me by playing this move instead, splitting the rest of the position into two components with nim-values 30 and 00.

I think it's bad practice for a human player in a theoretically winning position to leave an anago exposed like this, since the opponent can use it to create complex positions that could otherwise be prevented from arising.

In the diagram, the nim-values ab of various components have been noted in boxes. When a box contains *n, the component is equivalent to a nim-heap of size n.


23(36)24

Trying to make things even more complex, I play a third anago.


21(37)35

Roman is now facing two exposed anagos, a situation in which he does not usually find himself.


10(38)10[11]

This move looks to have been a mistake on my part. Even if the 01 component were equivalent to *0, the remaining components do not make a misère P-position. If instead the 30 component is replaced by *3, the resulting position is also not a misère P-position.

My move does have a possible virtue: there are only three replies that make the normal-play nim-values sum to *0 by changing the 30 component into a 01, namely 23(39)23, 25(39)36, and 23(39)27. Each of these are subtly different from a misère perspective, and each yields a different unusual configuration in S(23). These unusual configurations are unlikely to have been well-studied by human sprouts theorists.

Instead, though, I should have played 12(38)34 or 12(38)13, both of which better accord with the Enough Rope Principle.


12(39)12[13]

Roman returns to a position in which the normal-play nim-values sum to *0. Interestingly, though, he does so by raising the nim-values of S(11) from 01 to 34.


23(40)23[25]

I have set a trap. Aunt Beast still can't figure out who wins here, but she can see that Roman loses if he moves in the 51 component. It's likely that he will, since human players can't abide positions with normal-play nim-values greater than 3.

Update (March 2011): In subsequent analysis, Aunt Beast determined that this move is not just a trap — it actually wins! How strange that *2 + *2 + 51 + 34 would yield a misere P-position. If God doesn't play dice with the universe, he must not play sprouts either, because both seem equally random.


26(41)26 N

Roman moves in the 51 component, reducing it to *3. Now Aunt Beast can see that Roman has lost. Adopting a notation from combinatorial game theory, I have appended "N" to losing moves, which stands for "next player wins".


14(42)15 P

There are six winning options here: 34(42)38, 12(42)13, 14(42)14[15-16,38], 14(42)14[15-18], 14(42)14[15-17], and 14(42)15. I like to play an anago whenever possible. Aunt Beast didn't compute the exact normal-play nim-value of the anago component during the game, but in the course of writing this article, I used Glop to determine that it is at least 5.

Adopting another notation from combinatorial game theory, I have appended "P" to winning moves, which stands for "previous player wins". In this case, the previous player is the person who just played the move, that is, me.


14(43)14[16-20] N

Looping on a tail of the anago, Roman encloses a group of 5 spots.

According to the "component with firm nim-values" heuristic, this position's nim-values 33 + *3 + *3 + *3 would seem to yield a clear 00, that is, a P-position in normal and misère play. Actually, though, it's a misère N-position — a perfect example of a case in which the heuristic goes horribly wrong.

Even though it loses, this is still an excellent move: it leaves me with only one winning reply, a reply which would be impossible to find today (early 2011) without the aid of a computer.


16(44)16[43] P

This was the only winning move. The fact that this position is P cannot be predicted by a theory that looks only at the nim-values of its components.


17(45)17[18,19] N

Roman adds an additional level of nesting to the loops and pivots in S(11). This is another excellent move that leaves me only one winning reply.

In 2006, Dan Hoey pointed out that a related class of positions containing nested pivot spots “tend to have reasonably large game complexity, since Dawson's Kayles produces nim-values up to *9.” For more on this, see these messages in the “Dawson's Sprouts” thread on sprouts-theory.


18(46)19 P

Again, this was the only winning move. Now the game's outcome and its nim-values begin to correspond: the 13 component has restive nim-values, so we can guess that the position is P if and only if *3 is equal to *3 + *2 + *2, which indeed it is.


26(47)41 N

Ordinarily, when you are winning, the answer to an anago in an otherwise-empty region is to convert it to a big loop, but doing that here via 18(47)19 allows four different winning replies, while Roman's move allows only one.


21(48)21[22] P

For the third time in a row, this was the only winning move. Game trees in which two moves in a row have unique winning replies tend to be difficult for humans to analyze, so I can only imagine what a tree with three in a row is like.

This position is clearly P on the basis of its nim-values alone.


2(49)2[3] N

This is another good move; it allows me only two winning replies.


12(50@13)39 P

This was one of only two winning moves; the other was 17(50)46, which leaves S(11) with nim-values 61.

Roman usually resigns in losing positions with such few remaining live spots. Either the normal-play nim-value of 5 is clouding his vision, or he is doing an excellent job of defending a losing position, by forcing his opponent to play very accurately.


18(51)19 N

Roman plays yet another excellent move which allows me only one winning reply.


17(52)18 P

This was the only winning move.


20(53)20[45] N

Another strong move that leaves me only two winning replies.


16(54@20)44 I P

After this move, S(17) is effectively dead and Roman concedes. This was the simpler of only two winning moves; the other was 19(54@45)46, with nim-values 50.

Josh Jordan, February, 2011

Tuesday, December 8, 2015

Advent of Code 2015, Day 9

The Day 9 Puzzle of the 2015 Advent of Code asks for the length of the shortest and longest hamiltonian path on a graph. The vertices are cities and the edges between adjacent cities are labelled with the distance between the cities.

Here's a solution ts.lp in AnsProlog using the Potassco ASP toolkit:

% The distance relation is symmetric.
dist(B, A, N) :- dist(A, B, N).

% Two cities are "adjacent" if there is a defined distance between them.
adjacent(A, B) :- dist(A, B, _).

% A city exists if it has a distance defined to it from somewhere.
city(A) :- adjacent(A, _).

% Assign a city to each stop on the path from 1..n.
1 { path(N, C): city(C) } 1 :- N = 1..n.

% No city can occur twice in the path.
:- path(N, C), path(M, C), N != M.

% There must be a defined distance between successive cities on the path.
:- path(N, C), path(N+1, D), not adjacent(C, D).

% Minimize the sum of distances between successive cities in the path.
#minimize { X, N : path(N, C), path(N+1, D), dist(C,D,X), N = 1..n }.
#show path/2.
It requires the following data file city.lp as input:
dist(faerun, norrath, 129).
dist(faerun, tristram, 58).
dist(faerun, alphacentauri, 13).
dist(faerun, arbre, 24).
dist(faerun, snowdin, 60).
dist(faerun, tambi, 71).
dist(faerun, straylight, 67).
dist(norrath, tristram, 142).
dist(norrath, alphacentauri, 15).
dist(norrath, arbre, 135).
dist(norrath, snowdin, 75).
dist(norrath, tambi, 82).
dist(norrath, straylight, 54).
dist(tristram, alphacentauri, 118).
dist(tristram, arbre, 122).
dist(tristram, snowdin, 103).
dist(tristram, tambi, 49).
dist(tristram, straylight, 97).
dist(alphacentauri, arbre, 116).
dist(alphacentauri, snowdin, 12).
dist(alphacentauri, tambi, 18).
dist(alphacentauri, straylight, 91).
dist(arbre, snowdin, 129).
dist(arbre, tambi, 53).
dist(arbre, straylight, 40).
dist(snowdin, tambi, 15).
dist(snowdin, straylight, 99).
dist(tambi, straylight, 70).
city(alphacentauri;arbre;faerun;norrath;snowdin;straylight;tambi;tristram).
#const n = 8.
Here's the output from running it:
$ clingo city.lp ts.lp clingo version 4.4.0 Reading from city.lp ... Solving... Answer: 1 path(1,faerun) path(3,norrath) path(8,tristram) path(2,alphacentauri) path(6,arbre) path(5,snowdin) path(4,tambi) path(7,straylight) Optimization: 391 Answer: 2 path(8,faerun) path(3,norrath) path(5,tristram) path(2,alphacentauri) path(6,arbre) path(1,snowdin) path(4,tambi) path(7,straylight) Optimization: 387 Answer: 3 path(5,faerun) path(3,norrath) path(8,tristram) path(2,alphacentauri) path(6,arbre) path(1,snowdin) path(4,tambi) path(7,straylight) Optimization: 341 Answer: 4 path(5,faerun) path(1,norrath) path(6,tristram) path(2,alphacentauri) path(8,arbre) path(3,snowdin) path(4,tambi) path(7,straylight) Optimization: 308 Answer: 5 path(5,faerun) path(1,norrath) path(8,tristram) path(2,alphacentauri) path(6,arbre) path(3,snowdin) path(4,tambi) path(7,straylight) Optimization: 274 Answer: 6 path(7,faerun) path(5,norrath) path(1,tristram) path(6,alphacentauri) path(8,arbre) path(3,snowdin) path(2,tambi) path(4,straylight) Optimization: 269 Answer: 7 path(7,faerun) path(5,norrath) path(8,tristram) path(6,alphacentauri) path(3,arbre) path(1,snowdin) path(2,tambi) path(4,straylight) Optimization: 248 Answer: 8 path(2,faerun) path(5,norrath) path(1,tristram) path(6,alphacentauri) path(3,arbre) path(7,snowdin) path(8,tambi) path(4,straylight) Optimization: 218 Answer: 9 path(5,faerun) path(8,norrath) path(1,tristram) path(4,alphacentauri) path(6,arbre) path(3,snowdin) path(2,tambi) path(7,straylight) Optimization: 207 OPTIMUM FOUND Models : 9 Optimum : yes Optimization : 207 Calls : 1 Time : 0.050s (Solving: 0.04s 1st Model: 0.00s Unsat: 0.03s) CPU Time : 0.040s
So the shortest path is from Faerun-Norrath-Tristram-AlphaCentauri-Arbre-Snowdin-Tambi-Straylight with a length of 207.

Changing #minimize to #maximize in ts.lp yields a longest path of Norrath-Tristram-AlphaCentauri-Arbre-Snowdin-Tambi-Straylight, with a length of 804.

Friday, November 20, 2015

Brute-force Sudoku solver in JavaScript

// Brute-force Sudoku solver by Josh Jordan (public domain):

// https://tentorcupu.wordpress.com/2013/11/08/just-another-extreme-sudoku-solution/
var coly013= [
  0, 0, 0, 0, 9, 0, 0, 5, 0,
  0, 1, 0, 0, 0, 0, 0, 3, 0,
  0, 0, 2, 3, 0, 0, 7, 0, 0,
  0, 0, 4, 5, 0, 0, 0, 7, 0,
  8, 0, 0, 0, 0, 0, 2, 0, 0,
  0, 0, 0, 0, 0, 6, 4, 0, 0,
  0, 9, 0, 0, 1, 0, 0, 0, 0,
  0, 8, 0, 0, 6, 0, 0, 0, 0,
  0, 0, 5, 4, 0, 0, 0, 0, 7,
];

// https://tentorcupu.wordpress.com/2013/11/08/just-another-extreme-sudoku-solution/
var tarx0134 = [
  0, 0, 0, 0, 0, 0, 0, 0, 8,
  0, 0, 3, 0, 0, 0, 4, 0, 0,
  0, 9, 0, 0, 2, 0, 0, 6, 0,
  0, 0, 0, 0, 7, 9, 0, 0, 0,
  0, 0, 0, 0, 6, 1, 2, 0, 0, 
  0, 6, 0, 5, 0, 2, 0, 7, 0,
  0, 0, 8, 0, 0, 0, 5, 0, 0,
  0, 1, 0, 0, 0, 0, 0, 2, 0,
  4, 0, 5, 0, 0, 0, 0, 0, 3,
];

var platinum_blonde = [
  0, 0, 0, 0, 0, 0, 0, 1, 2,
  0, 0, 0, 0, 0, 0, 0, 0, 3,
  0, 0, 2, 3, 0, 0, 4, 0, 0,
  0, 0, 1, 8, 0, 0, 0, 0, 5,
  0, 6, 0, 0, 7, 0, 8, 0, 0,
  0, 0, 0, 0, 0, 9, 0, 0, 0,
  0, 0, 8, 5, 0, 0, 0, 0, 0,
  9, 0, 0, 0, 4, 0, 5, 0, 0,
  4, 7, 0, 0, 0, 6, 0, 0, 0,
];

// "Golden Nugget... accepted by many sources as the world's hardest sudoku
// puzzle" - http://www.sudokusnake.com/goldennugget.php
var golden_nugget = [
  0, 0, 0, 0, 0, 0, 0, 3, 9,
  0, 0, 0, 0, 1, 0, 0, 0, 5,
  0, 0, 3, 0, 0, 5, 8, 0, 0,
  0, 0, 8, 0, 0, 9, 0, 0, 6,
  0, 7, 0, 0, 2, 0, 0, 0, 0,
  1, 0, 0, 4, 0, 0, 0, 0, 0,
  0, 0, 9, 0, 0, 8, 0, 5, 0,
  0, 2, 0, 0, 0, 0, 6, 0, 0,
  4, 0, 0, 7, 0, 0, 0, 0, 0,
];

// Arto Inkala's "Hardest Sudoko" from 2010
// http://www.mirror.co.uk/news/weird-news/worlds-hardest-sudoku-can-you-242294
var inkala2010 = [
  0, 0, 5, 3, 0, 0, 0, 0, 0,
  8, 0, 0, 0, 0, 0, 0, 2,  ,
  0, 7, 0, 0, 1, 0, 5, 0, 0,
  4, 0, 0, 0, 0, 5, 3, 0, 0,
  0, 1, 0, 0, 7, 0, 0, 0, 6,
  0, 0, 3, 2, 0, 0, 0, 8, 0,
  0, 6, 0, 5, 0, 0, 0, 0, 9,
  0, 0, 4, 0, 0, 0, 0, 3, 0,
  0, 0, 0, 0, 0, 9, 7, 0, 0,
];

// Peter Norvig's "hardest (for my program) of the million random puzzles"
// http://norvig.com/sudoku.html
var norvig_hard = "000006000059000008200008000045000000003000000006003054000325006000000000000000000".split('').map(function(x) { return +x})

// Peter Norvig's impossible sudoko (has no solutions) from
// http://norvig.com/sudoku.html
var norvig_impossible = [
0, 0, 0, 0, 0, 5, 0, 8, 0,
0, 0, 0, 6, 0, 1, 0, 4, 3,
0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 0, 5, 0, 0, 0, 0, 0,
0, 0, 0, 1, 0, 6, 0, 0, 0,
3, 0, 0, 0, 0, 0, 0, 0, 5,
5, 3, 0, 0, 0, 0, 0, 6, 1,
0, 0, 0, 0, 0, 0, 0, 0, 4,
0, 0, 0, 0, 0, 0, 0, 0, 0,
];

var box_grid;

function initBoxGrid() {
box_grid = [];
for (var i = 0; i < 9; i++) {
box_grid[i] = []
}
for (var i = 0; i < 81; i++) {
box_grid[cell_box(i)].push(i);
}
}

function solveable(board) {
return solveable_r(board.slice(), 80);
}

function solveable_r(board, i) {
  if (box_grid === undefined) {
initBoxGrid();
}
if (i < 0) {
console.log(board); // show the solution
return board;
}
if (board[i] > 0) {
return solveable_r(board, i-1);
}
for (var j = 1; j < 10; j++) {
board[i] = j;
if(valid(board, i) && solveable_r(board, i-1)) {
return true;
}
board[i] = 0;
}
return false;
}

function valid(board, cell) {
return valid_row(board, cell_row(cell))
  && valid_col(board, cell_col(cell))
  && valid_box(board, cell_box(cell));
}

// Given a cell (0-80), return the number of the row to which it belongs (0-8).
function cell_row(i) {
return Math.floor(i / 9);
}

// Given a cell (0-80), return the number of the column to which it belongs (0-8).
function cell_col(i) {
return i % 9;
}

// Given a cell (0-80), return the number of the box to which it belongs (0-8).
function cell_box(i) {
return 3 * Math.floor(cell_row(i) / 3) + Math.floor(cell_col(i) / 3)
}

function valid_row(board, row) {
  return valid_line(board, row, 9, 1);
}

function valid_col(board, col) {
return valid_line(board, col, 1, 9);
}

function valid_line(board, row, a, b) {
var seen = 0;
for (var i = 0; i < 9; i++) {
j = board[row * a + i * b];
if (j == 0) continue;
if (seen & (1 << j)) {
return false;
}
seen = seen | (1 << j);
}
return true;
}

function valid_box(board, b) {
var seen = 0;
for (var i = 0; i < 9; i++) {
j = board[box_grid[b][i]];
if (j == 0) continue;
if (seen & (1 << j)) {
return false;
}
seen = seen | (1 << j);
}
return true;
}

console.log(solveable(coly013));
console.log(solveable(platinum_blonde));
console.log(solveable(tarx0134));
console.log(solveable(golden_nugget));
console.log(solveable(inkala2010));
console.log(solveable(norvig_hard));
console.log(solveable(norvig_impossible));

Thursday, February 12, 2015

0hn0.com solver

Below is a solver for the puzzles at 0hn0.com. Using it I calculated that an empty 2x2 puzzle can be solved in 10 different ways, an empty 3x3 puzzle can be solved in 250 different ways, and an empty 4x4 puzzle can be solved in in 22946 different ways.

% uses clingo ASP solver from http://potassco.sourceforge.net/
% USAGE: clingo thisfile.txt
% The solution is in the squarecolor(Y,X,blue;red) terms in the output.

% problem specification; example 8x8 puzzle from 0hn0.com:
% - * 6 - 5 - - -
% - * - * - - 5 -
% - - - - - 4 8 -
% 4 - * - * - - -
% - - 5 - * 2 - -
% - - - - - 7 - 3
% 3 - - 7 6 - - -


% begin problem description in ASP
% num(Y,X,N) means the number in square (Y,X) is N
% (squares with numbers are all initially blue.)
% red(Y,X) means square (Y,X) is initially red.
#const ysize=8.
#const xsize=8.


num(1,1,1).
num(1,2,1).
num(1,5,2).
num(1,8,1).
red(2,2).
num(2,3,6).
num(2,5,5).
red(3,2).
red(3,4).
num(3,7,5).
num(4,6,4).
num(4,7,8).
num(5,1,4).
red(5,3).
red(5,5).
num(6,3,5).
red(6,5).
num(6,6,2).
num(7,6,7).
num(7,8,3).
num(8,1,3).
num(8,4,7).
num(8,5,6).

% end ASP problem description
% nothing below this line needs to be edited

% list the valid colors and directions
color(red;blue).
dir(up;down;left;right).

% squares with numbers are blue
squarecolor(Y,X,blue) :- num(Y,X).

% squares specified as red in the initial problem are red in the solution
squarecolor(Y,X,red) :- red(Y,X).

% num(Y,X) :- square(Y,X) was assigned a number in the initial problem
num(Y,X) :- num(Y,X,C).

% choose a color for each initially-empty square
1 { squarecolor(Y,X,C) : color(C) } 1 :- square(Y,X), not num(Y,X), not red(Y,X).

% dir(Y,X,D,YD,XD) = (YD,XD) is 1 square in direction D from (Y,X)
% square 0,0 is used to represent an invalid square, e.g. going up
% from a square in the top row.
row(1..ysize).
col(1..xsize).
square(Y,X) :- row(Y), col(X).
dirok(Y,X, up, Y-1,X) :- square(Y,X).
dirok(Y,X, down, Y+1,X) :- square(Y,X).
dirok(Y,X, left, Y,X-1) :- square(Y,X).
dirok(Y,X, right, Y,X+1) :- square(Y,X).
dir(Y,X,D, 0,0) :- square(Y,X), dir(D), dirok(Y,X,D,YD,XD), not square(YD,XD).
dir(Y,X,D, YD,XD) :- square(Y,X), dir(D), dirok(Y,X,D,YD,XD), square(YD,XD).

% consider any invalid square (0,0) to be colored red.
squarecolor(0,0,red).


% seendir(Y,X,D,N) = N blue squares can be seen in direction D from square (Y,X)
seendir(Y,X,D,1+SD) :- squarecolor(Y,X,blue), dir(D), dir(Y,X,D,YD,XD), seendir(YD,XD,D,SD).

% red squares block vision, so nothing can be seen in any direction from a red square.
seendir(Y,X,D,0) :- squarecolor(Y,X,red), dir(D).

% seen(Y,X,N) = Blue square (Y,X) can see N blue discs in all directions.
seen(Y,X,U+D+L+R-4) :- square(Y,X), squarecolor(Y,X,blue), seendir(Y,X,up,U),
                seendir(Y,X,down,D), seendir(Y,X,left,L), seendir(Y,X,right,R).


% each blue square must see at least 1 other blue square (can't see only 0)
:- squarecolor(Y,X,blue), seen(Y,X,0).

% every blue square must see a number of neighbors equal to the number written on it.
:- num(Y,X,C), seen(Y,X,D), C != D.

Followers

Contributors