The Perl Toolchain Summit 2025 Needs You: You can help 🙏 Learn more

use strict;
#use feature qw( say );
# basic Sudoku structures
# don't panic - all basic Sudoku structures are constant
package main;
our @cells; # cell objects (1 .. 81)
our @rows; # row objects (1 .. 9)
use version; our $VERSION = qv('0.0.2'); # PBP
use Tk;
# globals of module Games::Sudoku::Trainer::GUIpause_restrict
my $mw; # main window
my $display_mode_restrict; # for display of mode restriction
sub _config_disp_mode_restrict {
my $restrict_lb = shift;
$mw = $restrict_lb->toplevel;
$restrict_lb->configure( -textvariable => \$display_mode_restrict );
return;
}
#-----------------------------------------------------------------------
# pause mode restriction
#-----------------------------------------------------------------------
{
# Dialog to ask for a unit name
# Used when restricting pause mode 'Value found' to a unit
#
sub enter_unitname {
$mw or _config_disp_mode_restrict(shift);
my $pause_restrict; # temp. pause mode restriction
_kill_tracewindow();
require Tk::DialogBox;
my $db = $mw->DialogBox(
-title => 'Restrict pause mode',
-buttons => [qw/Ok Cancel/],
-default_button => 'Ok',
-cancel_button => 'Cancel'
);
$db->Label( -text => "Restrict pause mode 'Value found'"
. " to a unit\n(e. g. b5 for the center block)" )->pack();
my $en = $db->add(
'Entry',
-width => 2,
-textvariable => \$pause_restrict,
-validate => 'key'
)->pack();
$en->configure( -validatecommand => [ \&_validate_unit, $en ] );
$en->selectionRange( 0, 'end' );
$db->configure( -focus => $en );
my $answer = $db->Show();
if ( $answer eq 'Cancel' or $pause_restrict !~ /^(r|c|b)[1-9]$/ ) {
Games::Sudoku::Trainer::GUI::set_pause_mode('default');
return;
}
my $unit = Games::Sudoku::Trainer::Unit->by_name($pause_restrict);
my $count = $unit->active_Members;
if ( $unit->active_Members <= 1 ) {
Games::Sudoku::Trainer::Run::user_err(
"All values of $pause_restrict already found");
Games::Sudoku::Trainer::GUI::set_pause_mode('default');
}
$display_mode_restrict = $pause_restrict;
Games::Sudoku::Trainer::Pause->setMode_restriction($pause_restrict);
return;
} ## end sub enter_unitname
sub _validate_unit {
my $en = shift;
my ( $unit_name, undef, undef, undef, $action ) = @_;
if ( $action > 1 ) { $action -= 7 }
; # repair a bug in ActivePerl
if ( $action != 1 ) { return 1 }
; # wait for action 'insert'
if ( $unit_name =~ /^(r|c|b)$/ ) {
return 1; # wait for 2nd char
}
if ( $unit_name !~ /^(r|c|b)[1-9]$/ ) {
Games::Sudoku::Trainer::Run::user_err(
"Invalid unit name $unit_name,"
. " format is 'unit type - unit number'",
"\n(unit type: 'r' for row, 'c' for column, 'b' for block)"
);
$en->focusForce();
return 0;
}
return 1;
}
}
{
# Dialog to ask for a cell name
# Used for pause mode 'Trace a cell'
# and when restricting pause mode 'Value found' to a cell
#
sub enter_cellname {
$mw or _config_disp_mode_restrict(shift);
my $pause_restrict; # temp. pause mode restriction
require Tk::DialogBox;
my $db = $mw->DialogBox(
-title => 'Enter cell name',
-buttons => [qw/Ok Cancel/],
-default_button => 'Ok',
-cancel_button => 'Cancel'
);
$db->Label( -text =>
"Enter the cell name\n(e. g. r1c9 for the upper right cell)" )
->pack();
my $en = $db->add(
'Entry',
-width => 4,
-textvariable => \$pause_restrict,
-validate => 'key'
)->pack();
$en->configure( -validatecommand => [ \&_validate_cell, $en ] );
$en->selectionRange( 0, 'end' );
$db->configure( -focus => $en );
my $answer = $db->Show();
$pause_restrict = lc($pause_restrict);
if ( $answer eq 'Cancel' or $pause_restrict !~ /^r[1-9]c[1-9]$/ ) {
Games::Sudoku::Trainer::GUI::set_pause_mode('default');
return;
}
my $cell = Games::Sudoku::Trainer::Cell->by_name($pause_restrict);
my $candcount = $cell->cands_count;
# check if we are just inserting this value
my $pause_info_ref = Games::Sudoku::Trainer::Pause->Info_ref;
if ( $candcount > 0
and defined $pause_info_ref
and $pause_info_ref->[1] eq 'insert'
and $pause_info_ref->[3]->[1] == $cell )
{
$candcount = 0;
}
if ( $candcount == 0 ) {
Games::Sudoku::Trainer::Run::user_err(
"Value of $pause_restrict already found");
Games::Sudoku::Trainer::GUI::set_pause_mode('default');
return;
}
$display_mode_restrict = $pause_restrict;
Games::Sudoku::Trainer::Pause->setMode_restriction($pause_restrict);
return;
} ## end sub enter_cellname
# validate cell name
#
sub _validate_cell {
my $en = shift;
my ( $cell_name, undef, undef, undef, $action ) = @_;
if ( $action > 1 ) { $action -= 7 }; # repair a bug in ActivePerl
if ( $action != 1 ) { return 1 }; # wait for action 'insert'
if ( $cell_name =~ /^r[1-9]?c?[1-9]?$/i ) {
return 1; # (still) ok
} else {
Games::Sudoku::Trainer::Run::user_err(
"Invalid cell name $cell_name, format is\n",
'"r" - row number - "c" - column number'
);
$en->focusForce();
return 0;
}
}
}
# Clear current pause mode restriction
#
sub norestrict_pause {
my $menubar = shift;
my $pause_restriction =
$display_mode_restrict; # current pause mode restriction
$pause_restriction or return;
length($pause_restriction) == 4 and _kill_tracewindow();
$pause_restriction or return;
# now must be a 'Value found' restriction
my $pausemode_menu = $menubar->entrycget( 'Pause Mode', -menu );
my $valfound = $pausemode_menu->entrycget( 'Value found', -menu );
my $anywhere = $valfound->entrycget( 'anywhere', -variable );
$$anywhere = 'anywhere';
$display_mode_restrict = '';
Games::Sudoku::Trainer::Pause->setMode_restriction('');
return;
}
#-----------------------------------------------------------------------
# view trace window
#-----------------------------------------------------------------------
{
my $pause_restriction; # current pause mode restriction
my $tracewindow; # Toplevel widget
my $canv; # Canvas widget of trace window
my $tracestatus; # status text
sub build_tracewindow {
my ( $candsize, $cellsize ) = @_;
$pause_restriction =
$display_mode_restrict; # current pause mode restriction
$pause_restriction or return;
# if the user selected 'trace a cell' while 'trace a cell' is
# already active, the old trace window must be destroyed
if ( Exists($tracewindow) ) {
$tracewindow->destroy;
}
$tracewindow = $mw->Toplevel();
$tracewindow->overrideredirect(1); # no window frame
# separator
$tracewindow->Frame( -height => 1, -bg => 'black' )
->pack( -pady => 4, -expand => 1, -fill => 'x' );
$tracewindow->Label( -text => "Trace of cell $pause_restriction" )
->pack( -pady => 4 );
$canv =
$tracewindow->Canvas( -borderwidth => 2, -relief => 'groove' )
->pack( -anchor => 'w' );
$tracewindow->Label( -textvariable => \$tracestatus )->pack();
Games::Sudoku::Trainer::GUI::build_candsquares( $canv,
Games::Sudoku::Trainer::Cell->by_name($pause_restriction) );
$canv->move( 'new', 2, 2 );
$canv->itemconfigure(
'black',
-fill => 'black',
-outline => 'black',
-state => 'normal'
);
$canv->itemconfigure(
'red',
-fill => 'red',
-outline => 'red',
-state => 'normal'
);
# frame the cand.s squares collection
$canv->createRectangle(
0, 0,
$cellsize + 1,
$cellsize + 1,
-tags => 'new'
);
$canv->scale( 'new', 0, 0, 1.5, 1.5 );
$canv->dtag('new');
# legend
my $pointfac = 1 / $canv->fpixels('1p');
my $legendx = $cellsize + 30;
my $legendy = 3;
my %legend_values =
( qw/black Active red Excluded/, 'orange' => 'Just excluded' );
foreach my $key ( keys %legend_values ) {
$canv->createRectangle(
$legendx, $legendy, $legendx + $candsize - 1,
$legendy + $candsize - 1,
-fill => $key,
-outline => $key,
);
$canv->createText(
$legendx + 10, $legendy + $candsize / 2 - 1,
-anchor => 'w',
-text => $legend_values{$key}
);
$legendy += 20 * $pointfac; # linespace 20 points
}
$canv->configure( -scrollregion => [ $canv->bbox('all') ] );
my ( $x_ul, $y_ul, $x_lr, $y_lr ) = $canv->bbox('all');
$canv->configure(
-height => $y_lr - $y_ul - 1,
-width => $x_lr - $x_ul - 1
);
$tracewindow->resizable( 0, 1 ); # freeze window width
# follow the main window when it moves
$mw->bind(
'<Configure>' => sub {
$mw->after(
200 => sub {
$tracewindow or return;
$tracewindow->state ne 'normal' and return;
$tracewindow->withdraw;
$tracewindow->Popup(
-overanchor => 'sw',
-popanchor => 'nw'
);
}
);
}
);
# nothing to update directly after presetting
# (the pause directly after presetting doesn't set
# Games::Sudoku::Trainer::Pause::Info_ref)
my $pause_info_ref = Games::Sudoku::Trainer::Pause->Info_ref;
$pause_info_ref and update_tracewindow($pause_info_ref);
$tracewindow->Popup(
-popover => $mw,
-overanchor => 'sw',
-popanchor => 'nw'
);
return;
} ## end sub build_tracewindow
# update the colors of the candidate squares in the trace window,
# then pause until the user hits the Run button.
# called from Games::Sudoku::Trainer::Check_pause::check_pause
# also called from show_tracewindow to initialize the colors (doesn't pause)
#
sub update_tracewindow {
my $found_info_ref = shift;
$tracestatus = '';
if ( $found_info_ref->[1] eq 'insert' ) {
my $cell = $found_info_ref->[3]->[1];
if ( $cell->Name eq $pause_restriction ) {
$tracestatus =
"Value found for the trace cell\nby strategy "
. Games::Sudoku::Trainer::Pause->Strat
. ".\nEnd of trace mode";
Games::Sudoku::Trainer::GUI::set_pause_mode('default');
Games::Sudoku::Trainer::Check_pause::pause();
# don't kill the tracewindow if the user created a new one
# during the pause
_kill_tracewindow()
if ( Games::Sudoku::Trainer::Pause->Mode ne 'Trace a cell' );
return;
}
else {
my @trace_units =
Games::Sudoku::Trainer::Cell->by_name($pause_restriction)
->get_Containers();
my @cell_units = $cell->get_Containers();
# the trace cell may be in the same block _and_ in a same line
# as the cell with the just found value. In this case a
# duplicate pause is avoided in sub _change_color
foreach ( 0 .. 2 ) {
next if ( $cell_units[$_] != $trace_units[$_] );
_change_color( $found_info_ref->[3]->[0] );
}
}
}
else { # exclude cand.
my $exclude_info_ref = $found_info_ref->[3];
# get freshly excluded cand.s
my @trace_this = map {
$_ =~ /(\d+)-(\d)/;
my $cellname = $cells[$1]->Name;
my $cand = $2;
$cellname =~ /$pause_restriction/ ? ($cand) : ()
} @$exclude_info_ref;
_change_color(@trace_this);
}
return;
} ## end sub update_tracewindow
# change color of freshly excluded cand.s
# color goes from black to orange, then after the pause to red
# _change_color(freshly-excluded_list);
#
sub _change_color {
# digits freshly excluded as cand.s for the trace cell
my @trace_this = @_;
my $fresh; # count of fresh squares
foreach my $digit (@trace_this) {
my ($square) =
$canv->find( withtag => $pause_restriction . "&&d$digit" );
next unless $square;
# avoid to confuse the user by pausing twice on a square
grep ( $_ =~ 'black', $canv->gettags($square) ) or next;
$canv->dtag( $square, 'black' );
$canv->addtag( 'fresh',
withtag => $pause_restriction . "&&d$digit" );
$fresh++;
}
$fresh or return;
$canv->itemconfigure(
'fresh',
-fill => 'orange',
-outline => 'orange'
);
$tracestatus =
"$fresh candidates excluded by strategy "
. Games::Sudoku::Trainer::Pause->Strat . ".\n";
# if the trace window isn't popped up yet, we are currently in a pause
$canv->ismapped or return;
Games::Sudoku::Trainer::Check_pause::pause();
if ( !Tk::Exists($canv) ) {
return; # user terminated trace mode
}
$canv->itemconfigure( 'fresh', -fill => 'red', -outline => 'red' );
my @fresh = $canv->find( withtag => 'fresh' );
$canv->addtag( 'red', withtag => 'fresh' );
$canv->dtag('fresh');
$tracewindow->update();
return;
} ## end sub _change_color
# kill the trace window
#
sub _kill_tracewindow {
$tracewindow or return;
$tracewindow->destroy;
$tracewindow = undef;
$display_mode_restrict = '';
Games::Sudoku::Trainer::Pause->setMode_restriction('');
return;
}
}
1;