#!/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