package Games::Nonogram::Clue; use strict; use warnings; use base qw( Games::Nonogram::Base ); use Games::Nonogram::Line; use Games::Nonogram::Block; sub new { my ($class, %options) = @_; my $size = $options{size}; my $line = Games::Nonogram::Line->new( size => $size ); my $self = bless { id => $options{id}, size => $size, line => $line, is_done => 0, }, $class; if ( $options{blocks} ) { $self->set( @{ $options{blocks} || [] } ); } $self; } sub id { shift->{id} || '' } sub set { my ($self, @clues) = @_; my $id = 0; my @blocks = map { Games::Nonogram::Block->new( id => ++$id, length => $_, line_size => $self->size, ) } @clues; $self->{blocks} = \@blocks; $self->reset_blocks; my $free; unless ( @blocks ) { $self->line->off( from => 1, length => $self->size ); $free = 0; } else { $free = $blocks[0]->right - $blocks[0]->length; } $self->{free} = $free; } sub reset_blocks { my $self = shift; my $left = 1; foreach my $block ( $self->blocks ) { $block->clear; $block->left( $left ); $left += ( $block->length + 1 ); } my $right = $self->size; foreach my $block ( reverse $self->blocks ) { $block->right( $right ); $right -= ( $block->length + 1 ); } $self->{is_done} = 0; } sub blocks { @{ shift->{blocks} || [] } } sub size { shift->{size} } sub line { shift->{line} } sub block { my ($self, $id) = @_; $self->{blocks}->[$id]; } sub as_string { my $self = shift; my $str = $self->line->as_string; if ( $self->debug ) { $str .= ' '. join ' ', map { $_->length } $self->blocks; } return $str; } sub dump_blocks { my $self = shift; my $str = $self->id . "\n"; foreach my $block ( $self->blocks ) { for my $ct ( 1 .. $self->size ) { if ( $block->must_have( $ct ) ) { $str .= "X"; } elsif ( $block->might_have( $ct ) ) { $str .= "_"; } else { $str .= "."; } } $str .= sprintf " (%s, %s: %s)\n", $block->left, $block->right, $block->length; } $str .= $self->as_string . "\n"; defined wantarray ? return $str : print $str; } sub is_done { my $self = shift; unless ( $self->{is_done} ) { return unless $self->line->is_done; $self->die_if_invalid; $self->{is_done} = 1; } $self->{is_done}; } sub die_if_invalid { my $self = shift; my $ct = 1; foreach my $block ( $self->blocks ) { while ( $ct < $block->left ) { my $value = $self->line->value( $ct ); if ( $value != 0 ) { $self->dump_blocks if $self->debug; die "failed at $ct: $value != 0"; } $ct++; } while ( $ct <= $block->right ) { my $value = $self->line->value( $ct ); if ( $value != 1 ) { $self->dump_blocks if $self->debug; die "failed at $ct: $value != 1"; } $ct++; } } while ( $ct <= $self->size ) { my $value = $self->line->value( $ct ); if ( $value != 0 ) { $self->dump_blocks if $self->debug; die "failed at $ct: $value != 0"; } $ct++; } return 1; } sub on { my ($self, $id) = @_; $self->line->on( $id ); my $block = $self->might_have( $id ); return unless ref $block; my $offset = $block->length - 1; my $left = $id - $offset; $block->left( $left ) if $block->left < $left; my $right = $id + $offset; $block->right( $right ) if $block->right > $right; if ( $block->left == $id ) { my $left = $block->left; $self->line->on( $_ ) for ( $left .. $left + $offset ); } if ( $block->right == $id ) { my $right = $block->right; $self->line->on( $_ ) for ( $right - $offset .. $right ); } } sub off { my ($self, $id) = @_; $self->line->off( $id ); foreach my $block ( $self->blocks ) { $block->cant_have( $id ); } my $block = $self->might_have( $id ); return unless ref $block; my $offset = $block->length - 1; my $left = $id + 1; $block->left( $left ) if $block->left + $offset > $left; my $right = $id - 1; $block->right( $right ) if $block->right - $offset < $right; } sub value { my ($self, $id, $value) = @_; if ( defined $value ) { if ( $value == 0 ) { $self->off( $id ) } elsif ( $value == 1 ) { $self->on( $id ) } } else { $self->line->value( $id ); } } sub might_have { my ($self, $id) = @_; my $hit; foreach my $block ( $self->blocks ) { if ( $block->might_have( $id ) ) { return -1 if $hit; # multiple candidates; cannot decide $hit = $block; } } return $hit ? $hit : 0; } sub update { my ($self, $mode) = @_; unless ( $mode ) { $self->_update_basic; } elsif ( $mode eq 'more' ) { $self->_update_more; } foreach my $ct ( 1 .. $self->size ) { my $block = $self->might_have( $ct ); unless ( $block ) { $self->off( $ct ); } elsif ( ref $block ) { my $value = $self->line->value( $ct ); if ( $value == -1 ) { $self->on( $ct ) if $block->must_have( $ct ); } elsif ( $value == 0 ) { $self->off( $ct ); } elsif ( $value == 1 ) { $self->on( $ct ) unless $block->must_have( $ct ); } } } } sub _update_basic { my $self = shift; my $left = 1; foreach my $block ( $self->blocks ) { $left = $block->left if $block->left > $left; while ( $self->line->value( $left ) == 0 or $left > 1 && $self->line->value( $left - 1 ) == 1 ) { $left++; if ( $left > $self->size ) { die "puzzle data may be broken, unless you're trying to solve by brute force"; } } $block->left( $left ); $left += ( $block->length + 1 ); } my $right = $self->size; foreach my $block ( reverse $self->blocks ) { $right = $block->right if $block->right < $right; while ( $self->line->value( $right ) == 0 or $right < $self->size && $self->line->value( $right + 1 ) == 1 ) { $right--; if ( $right < 1 ) { die "puzzle data may be broken, unless you're trying to solve by brute force"; } } $block->right( $right ); $right -= ( $block->length + 1 ); } foreach my $block ( $self->blocks ) { my ($from, $length) = (0, 0); foreach my $ct ( $block->left .. $block->right ) { my $value = $self->line->value( $ct ); if ( $value == 1 ) { $from ||= $ct; $length++; } else { $block->try( $from, $length ); ($from, $length) = (0, 0); } } $block->try( $from, $length ); } } sub _update_more { my $self = shift; my ($toggle, $from, @blocks); foreach my $ct ( 1 .. $self->size ) { $from ||= $ct; my $value = $self->line->value( $ct ); if ( $value == 1 ) { $toggle = 1; } elsif ( $value == 0 ) { if ( $toggle ) { push @blocks, { from => $from, to => $ct - 1 }; } $from = $toggle = 0; } } if ( $toggle ) { push @blocks, { from => $from, to => $self->size }; } if ( @blocks == $self->blocks ) { foreach my $block ( $self->blocks ) { my $href = shift @blocks; $block->left( $href->{from} ) if $block->left < $href->{from}; $block->right( $href->{to} ) if $block->right > $href->{to}; } } } sub candidates { my $self = shift; my @candidates = $self->_candidates( $self->line->clone, 1, $self->{free}, $self->blocks ); } sub _candidates { my ($self, $line, $pos, $free, @blocks) = @_; my $clone = $line->clone; unless ( $free ) { while( my $block = shift @blocks ) { foreach my $ct ( 0 .. $block->length - 1 ) { return if $clone->value( $pos + $ct ) == 0; # conflicted; $clone->on( $pos + $ct ); } $pos += $block->length; unless ( $pos > $self->size ) { return if $clone->value( $pos ) == 1; # conflicted; $clone->off( $pos ); } $pos++; } } unless ( @blocks ) { foreach my $ct ( $pos .. $self->size ) { return if $clone->value( $ct ) == 1; # conflicted $clone->off( $ct ); } return $clone->as_vec; } my @candidates; LOOP: foreach my $ct ( 0 .. $free ) { $clone = $line->clone; my @clone_blocks = @blocks; my $clone_pos = $pos; foreach my $space_ct ( 1 .. $ct ) { next LOOP if $clone->value( $clone_pos ) == 1; # conflicted $clone->off( $clone_pos ); $clone_pos++; } my $block = shift @clone_blocks; foreach my $block_ct ( 1 .. $block->length ) { next LOOP if $clone->value( $clone_pos ) == 0; # conflicted $clone->on( $clone_pos ); $clone_pos++; } unless ( $clone_pos > $self->size ) { next LOOP if $clone->value( $clone_pos ) == 1; # conflicted $clone->off( $clone_pos ); $clone_pos++; push @candidates, $self->_candidates( $clone, $clone_pos, $free - $ct, @clone_blocks ); } else { push @candidates, $clone->as_vec; } } return @candidates; } 1; __END__ =head1 NAME Games::Nonogram::Clue =head1 DESCRIPTION This is used internally to handle clues in a row or in a column. =head1 METHODS =head2 new creates an object. =head2 set sets an array of the length of clues in a row/column. =head2 reset_blocks initializes the positions of clues. Actually this may fix them when there're no free cells, as this tries to put blocks so as not to cross each other. =head2 as_string returns a stringified form of the line for the clues. =head2 dump_blocks similar to above but returns more detailed information of the blocks of the clues. =head2 is_done returns if each block in the line is fixed or not. =head2 die_if_invalid dies if any of the blocks in the line can not have enough spaces (while brute-forcing, or when the puzzle is broken). =head2 on tells each block in the line that the given id (cell) should belong to one of the blocks. =head2 off tells each block in the line that the given id (cell) should not belong to one of the blocks. =head2 value returns if the given id (cell) belongs to a block or not. =head2 might_have returns if the given id (cell) can belong to a block or not. =head2 update sees through the line and turns on/off the cells it can surely tell. =head2 candidates returns all the possible patterns of the line. =head1 ACCESSORS =head2 id returns a clue id. =head2 blocks returns all the blocks in the clues. =head2 size returns how many cells are there in a row/column for the clues. =head2 line returns a row/column for the clues. =head2 block returns a block of the given (block) id. =head1 AUTHOR Kenichi Ishigaki, Eishigaki at cpan.orgE =head1 COPYRIGHT AND LICENSE Copyright 2007 by Kenichi Ishigaki This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut