#!/usr/bin/perl -w
# 1, 2, 3, 4, 5(, 6)
# Red, Blue, Green, Yellow
# Triangles, Squares, Circles, Diamonds, Stars
use strict;
my $g_n_items = 9;
my @g_n_characteristics = (undef, undef, undef); # Actual values are assigned randomly on each iteration.
#my @g_n_min_characteristic_count = (1, 2, 2);
my @g_n_min_characteristic_count = (1, 1, 1);
#my @g_n_max_characteristic_count = map { int($g_n_items/3) } @g_n_characteristics;
my @g_n_max_characteristic_count = map { int($g_n_items-1) } @g_n_characteristics;
my $g_t_failure = 10;
my ($g_max_min_cost, $g_max_avg_cost, $g_max_max_cost);
my $g_n_solves = 100;
my ($g_c_quilts, $g_c_well_formed, $g_c_solvable) = (0, 0, 0);
while (1)
{
warn "\nNew quilt.\n";
++$g_c_quilts;
# Randomize the number of characteristics.
# @g_n_characteristics = sort { $b <=> $a } map { int(3 + rand(3)) } @g_n_characteristics;
@g_n_characteristics = sort { $b <=> $a } (2, 2, 3);
my $n_permutations = 1;
map { $n_permutations *= $_ } @g_n_characteristics;
redo unless $n_permutations >= $g_n_items;
warn "\tGenerating...\n";
my $q = &random_quilt;
warn "\tChecking for well-formedness...\n";
next unless &well_formed_quilt($q);
++$g_c_well_formed;
warn "\tChecking for solvability...\n";
if ( &solution_cost($q) )
{
#--$g_t_failure if $g_t_failure > 1;
warn "\tEvaluating...\n";
++$g_c_solvable;
&evaluate_quilt($q);
}
else
{
#$g_t_failure++;
}
warn sprintf '%d quilts, %d well-formed (%.2f%%), %d solvable (%.2f%%)'."\n",
$g_c_quilts, $g_c_well_formed, 100*$g_c_well_formed/$g_c_quilts, $g_c_solvable, 100*$g_c_solvable/$g_c_well_formed;
}
exit;
###
sub random_quilt
{
my @q;
my %seen;
for (0..$g_n_items-1)
{
#warn "[$_]\n";
my @item;
for my $i_characteristic (0..$#g_n_characteristics)
{
#warn " [$i_characteristic]\n";
push @item, int rand $g_n_characteristics[$i_characteristic];
}
#warn "checking if seen\n";
if ( $seen{&string_from_item(\@item)}++ )
{
#warn "g_n_characteristics: " . join(', ', @g_n_characteristics) . "\n";
#warn "seen " . &string_from_item(\@item) . " in this quilt:\n" . &string_from_quilt(\@q);
redo;
}
push @q, \@item;
}
return \@q;
}
sub solution_cost
# Given a quilt and timeout, return the cost of the solution if one was found.
{
my ($q, $known_good) = @_;
#warn "q $q, known_good $known_good";
my @cost_of_characteristic = (3, 1, 2);
# Determine which items are in each others' sets.
my (%n_similarities, %same_set);
for my $i (0..$g_n_items-2)
{
for my $j ($i+1..$g_n_items-1)
{
for my $k (sort { $cost_of_characteristic[$a] <=> $cost_of_characteristic[$b] } 0..$#g_n_characteristics)
{
if ( ${${$q}[$i]}[$k] eq ${${$q}[$j]}[$k] )
{
#warn "$i,$j\n";
++$n_similarities{"$i,$j"};
#$same_set{"$i,$j"} = $same_set{"$j,$i"} = $cost_of_characteristic[$k];
#last;
}
}
}
}
map { $same_set{$_} = 1 if $n_similarities{$_} == 2 } keys %n_similarities;
#map { print "$_\t$n_similarities{$_}\t$same_set{$_}\n" } sort keys %n_similarities;
#die;
my $t_start = $known_good? undef : time;
#warn "[$known_good] [$t_start]";
my $solution_cost = &recurse_solution_cost($q, $t_start, [0], [1..$g_n_items-1], \%same_set);
return $solution_cost if $solution_cost > 0;
return 0;
}
sub recurse_solution_cost
# >0: cost to solve.
# <0: not solvable (negative of cost to check)
# undef: timed out.
{
# q: the quilt
# t_start
# sequence: the indexes of the items so far.
# available_items: the indexes of the remaining items.
my ($q, $t_start, $sequence, $available_items, $same_set) = @_;
my $this_cost = 1; # Minimum cost.
#warn $t_start;
#die unless $t_start;
if ( defined($t_start) and time - $t_start > $g_t_failure )
{
return undef;
}
if ( scalar(@{$available_items}) == 0 )
{
# We have a full sequence; just need to check if we can loop back to the beginning.
if (${$same_set}{"${$sequence}[0],${$sequence}[$#{$sequence}]"})
{
#warn scalar(@{$sequence}) . ': ' . join(', ', @{$sequence}) . "\n";
return $this_cost + ${$same_set}{"${$sequence}[0],${$sequence}[$#{$sequence}]"};
}
else
{
return -$this_cost;
}
}
# Originally this code searched the remaining available items in
# random order. I changed it to look at the "least expensive"
# choices first. For example, look for items of the same color
# first. This is like how I actually solve the problems; the idea
# is to find problems where the user is forced to skip from color
# to color because the "easy" strategy doesn't work. So far, this
# method doesn't appear to create harder problems.
#warn scalar(@{$sequence}) . ': ' . join(', ', @{$sequence});
#&fisher_yates_shuffle($available_items);
my $last = ${$sequence}[$#{$sequence}];
my @reachable_from_here_by_cost =
sort { ${$same_set}{"$last,$a"} <=> ${$same_set}{"$last,$b"} }
grep { defined ${$same_set}{"$last,$_"} }
@{$available_items};
#warn "available items: " . join(' ', @{$available_items});
#warn "reachable_from_here_by_cost: " . join(' ', @reachable_from_here_by_cost);
#die;
#my $last_item = ${$sequence}[$#{$sequence}];
#foreach my $item (@{$available_items})
foreach my $item (@reachable_from_here_by_cost)
{
#next unless ${$same_set}{"$last_item,$item"};
++$this_cost;
my @remaining_items = grep { $_ ne $item } @{$available_items};
my $recurse_cost = &recurse_solution_cost( $q, $t_start, [@{$sequence}, $item], [@remaining_items], $same_set );
if ( $recurse_cost > 0 )
{
return $this_cost + $recurse_cost;
}
elsif ( $recurse_cost < 0 )
{
# Negative of the cost to check.
$this_cost += -$recurse_cost;
}
else
{
# Timed out.
die "should be undef [$recurse_cost]" if defined($recurse_cost);
return undef;
}
}
return -$this_cost;
}
sub well_formed_quilt
{
my ($q) = @_;
for my $i_characteristic (0..$#g_n_characteristics)
{
#warn "[$i_characteristic]\n";
my %count;
foreach my $item (@{$q})
{
#warn " [$item]\n";
$count{${$item}[$i_characteristic]}++;
}
foreach my $characteristic (keys %count)
{
if ( $count{$characteristic} < $g_n_min_characteristic_count[$i_characteristic] )
{
return 0;
}
if ( $count{$characteristic} > $g_n_max_characteristic_count[$i_characteristic] )
{
#warn "rejected ill-formed quilt due to a count of $count{$characteristic} on $i_characteristic\n" . &string_from_quilt($q);
return 0;
}
}
}
return 1;
}
sub string_from_quilt
{
my ($q) = @_;
my @q = @{$q};
# Map the indexes to actual values (e.g. 0 -> Blue).
# The most common indexes get the lower numbers.
my @characteristics = (
[qw(1 2 3 4 5 6)],
[qw(yellow red blue green purple)],
[qw(circles triangles squares diamonds stars)],
);
map { &fisher_yates_shuffle($characteristics[$_]) } (1); # shuffle the colors, but leave others ordered by frequency.
my @characteristic_mapping;
foreach my $i_characteristic ( 0..$#g_n_characteristics )
{
my %count;
foreach my $item (@q)
{
$count{${$item}[$i_characteristic]}++;
}
my @member_indexes = sort { $count{$b} <=> $count{$a} } keys %count;
for my $j_member (0..$#member_indexes)
{
${$characteristic_mapping[$i_characteristic]}{$member_indexes[$j_member]} = ${$characteristics[$i_characteristic]}[$j_member];
}
}
my $s;
for my $i (0..$#q)
{
$s .= join(' ', "[$i]", map { ${$characteristic_mapping[$_]}{${$q[$i]}[$_]} } (0..$#g_n_characteristics)) . "\n";
}
return $s;
}
sub html_from_string
# HTML version of the quilt string.
{
my ($s) = @_;
my @s = split "\n", $s;
my @items;
for (@s)
{
chomp;
next unless m/^\[(\d+)\] (.+)$/;
push @items, $2;
}
die unless scalar(@items) == $g_n_items;
# Handle squares or one-off.
my $width = $g_n_items/(int ($g_n_items**0.5));
my $height = $g_n_items/$width;
die "can't handle $g_n_items items" unless $width == int($width);
# Reorder the items by color scheme.
@items = &order_string_items_by_characterstic(1, @items);
my @html_items = map { &html_from_item($_) } @items;
my $script_setConnected;
for my $i (0..$#items-1)
{
for my $j ($i+1..$#items)
{
$script_setConnected .= "setConnected($i, $j);" if &same_set_from_strings($items[$i], $items[$j]);
}
}
my $h = <
Puzzle Quilt
HTML
$h .= "
\n";
my $i = 0;
for (1..$height)
{
$h .= "
\n";
for (1..$width)
{
$h .= "
" .
$html_items[$i++] . "
\n";
}
$h .= "
\n";
}
$h .= "
\n";
$h .= "\n\n";
return $h;
}
sub same_set_from_strings
{
my ($a, $b) = @_;
my @a = split ' ', $a;
my @b = split ' ', $b;
my $c_matched = 0;
map { $c_matched++ if $a[$_] eq $b[$_] } (0..$#a);
return $c_matched == 2;
}
sub order_string_items_by_characterstic
# I'm sure this code is far more complicated than it needs to be.
{
my ($i_characteristic, @items) = @_;
my @characteristics;
for ( @items )
{
my @this_item = split ' ', $_;
push @characteristics, $this_item[$i_characteristic];
}
my %ordering_by_counts = (
'3,2,2,2' => [
0, 1, 2,
3, 0, 3,
2, 1, 0,
],
'3,3,3' => [
0, 1, 2,
2, 0, 1,
1, 2, 0,
],
'3,3,2,2,2' => [
0,2,1,3,
1,3,4,0,
4,0,2,1,
],
'3,3,3,3' => [
0,3,1,2,
1,2,3,0,
3,0,2,1,
],
'4,3,3,2' => [
0,1,0,2,
3,2,1,3,
1,0,2,0,
],
'4,4,2,2' => [
0,3,1,0,
2,1,0,3,
1,0,2,1,
],
'5,5,3,3' => [
0,3,1,3,
1,2,0,1,
0,1,3,0,
2,0,2,1,
],
'5,4,3,2,2' => [
2,0,4,1,
0,2,1,0,
3,1,0,3,
1,0,4,2,
],
'4,3,3,3,3' => [
1,0,3,2,
2,4,1,0,
0,3,2,4,
4,1,0,3,
],
'4,4,4,4' => [
0,1,2,3,
3,0,1,0,
2,3,2,1,
1,0,3,2,
],
'4,4,4,2,2' => [
3,0,1,2,
0,4,2,0,
2,1,3,1,
1,0,2,4,
],
'5,4,4,3' => [
2,0,3,1,
0,2,1,0,
3,1,0,2,
1,0,2,3,
],
'4,4,3,3,2' => [
3,0,2,1,
0,1,3,4,
4,2,0,1,
0,3,1,2,
],
'5,3,3,3,2' => [
2,0,3,1,
0,4,1,0,
1,3,0,2,
3,0,2,4,
],
'5,5,4,2' => [
3,0,1,2,
0,1,2,0,
1,2,0,1,
2,0,1,3,
],
);
my %c;
map { $c{$_}++ } @characteristics;
my @characteristics_ordered_by_frequency = sort { $c{$b} <=> $c{$a} } keys %c;
my $counts = join ',', map { $c{$_} } @characteristics_ordered_by_frequency;
my %position_of_characteristic_by_frequency;
map { $position_of_characteristic_by_frequency{$characteristics_ordered_by_frequency[$_]} = $_ }
(0..$#characteristics_ordered_by_frequency);
warn $counts;
unless ( defined $ordering_by_counts{$counts} )
{
warn "no predefined layout for $counts\n";
return @items;
}
my @ordering = @{$ordering_by_counts{$counts}};
# Separate indexes into the @ordering array for each group.
my @i = (0);
for my $i_i (1..$#characteristics_ordered_by_frequency)
{
$i[$i_i] = $i[$i_i-1] + $c{$characteristics_ordered_by_frequency[$i_i-1]};
}
# Order the items by characteristic.
my @items_ordered;
for my $c (@characteristics_ordered_by_frequency)
{
push @items_ordered, map { $items[$_] } grep { $characteristics[$_] eq $c } (0..$#items);
}
#warn join ' ', @items_ordered;
# Do the ordering.
my @return_items;
my $i_item = 0;
for my $i_ordering (0..$#ordering)
{
# Place an item of characteristic $characteristics_ordered_by_frequency[$ordering[$i_ordering]] in position $i_ordering.
$return_items[$i_ordering] = $items_ordered[$i[$position_of_characteristic_by_frequency{$characteristics_ordered_by_frequency[$ordering[$i_ordering]]}]++];
}
return @return_items;
}
sub html_from_item
{
my ($item) = @_;
die "can't match item [$item]" unless $item =~ m/(\d+) (\w+) (\w+)/;
my ($n, $color, $shape) = ($1, $2, $3);
$shape =~ s/s$//;
my %pattern_by_number = (
1 => '#IMG#',
2 => '
#IMG#
#IMG#
',
3 => '
#IMG#
#IMG#
#IMG#
',
4 => '
#IMG#
#IMG#
#IMG#
#IMG#
',
5 => '
#IMG#
#IMG#
#IMG#
#IMG#
#IMG#
',
);
my $image_tag = "";
my $h = $pattern_by_number{$n};
$h =~ s/#IMG#/$image_tag/g;
return $h;
}
sub string_from_item
{
my ($item) = @_;
my $s = join(':', @{$item}) . "\n";
return $s;
}
# fisher_yates_shuffle( \@array ) : generate a random permutation
# of @array in place
sub fisher_yates_shuffle {
my $array = shift;
my $i;
for ($i = @$array; --$i; ) {
my $j = int rand ($i+1);
next if $i == $j;
@$array[$i,$j] = @$array[$j,$i];
}
}
sub evaluate_quilt
# $q is a known-solvable quilt.
{
my ($q) = @_;
#print "evaluating this quilt:\n", &string_from_quilt($q);
#warn "evaluating a quilt\n";
my $n_tests = $g_n_solves;;
my ($this_min_cost, $this_max_cost, $this_total_cost);
my $test = 1;
$| = 1 if $test;
for my $c_test (1..$n_tests)
{
print '.' if $test;
&fisher_yates_shuffle($q);
my $cost = &solution_cost($q, 'known good');
unless ( $cost )
{
warn "no solution found (cost $cost) on test $c_test for this quilt:\n" . &string_from_quilt($q);
}
#warn "[$c_test] cost $cost\n";
#if ( $cost > $g_
$this_total_cost += $cost;
$this_min_cost = $cost if not defined $this_min_cost or $cost < $this_min_cost;
$this_max_cost = $cost if not defined $this_max_cost or $cost > $this_max_cost;
}
print "\n" if $test;
my $this_avg_cost = int(0.5 + $this_total_cost / $n_tests);
# my $found_better_solution = 0;
# if ( not defined $g_max_min_cost or $this_min_cost > $g_max_min_cost )
# {
# $g_max_min_cost = $this_min_cost;
# $found_better_solution = 1;
# }
# if ( not defined $g_max_avg_cost or $this_avg_cost > $g_max_avg_cost )
# {
# $g_max_avg_cost = $this_avg_cost;
# $found_better_solution = 1;
# }
# if ( not defined $g_max_max_cost or $this_max_cost > $g_max_max_cost )
# {
# $g_max_max_cost = $this_max_cost;
# $found_better_solution = 1;
# }
#warn "min cost $this_min_cost; average cost $this_avg_cost; max cost $this_max_cost\n";
if (
&exceeds_max($this_min_cost, $g_max_min_cost) +
&exceeds_max($this_avg_cost, $g_max_avg_cost) +
&exceeds_max($this_max_cost, $g_max_max_cost)
)
{
my $s = &string_from_quilt($q);
print $s;
print "min cost $this_min_cost; average cost $this_avg_cost; max cost $this_max_cost\n";
print "g_n_characteristics: " . join(', ', @g_n_characteristics) . "\n";
print "\n";
open Q, ">html/quilt.html" or die $!;
print Q &html_from_string($s);
close Q;
exit;
}
else
{
#--$g_max_avg_cost;
return;
}
}
sub exceeds_max
{
# $_[0]: this
# $_[1]: global
if ( not defined($_[1]) or $_[0] > $_[1] )
{
#warn "increasing from $_[1] to $_[0]\n";
$_[1] = $_[0];
return 1;
}
return 0;
}