Merge pull request #550 from trwyant/60_Mastermind_perl_trw

Port 60_Mastermind to Perl.
This commit is contained in:
Jeff Atwood
2022-02-03 15:33:42 -08:00
committed by GitHub
2 changed files with 423 additions and 0 deletions

View File

@@ -1,3 +1,7 @@
Original source downloaded [from Vintage Basic](http://www.vintage-basic.net/games.html)
Conversion to [Perl](https://www.perl.org/)
This is pretty much a re-implementation of the BASIC, taking advantage
of Perl's array functionality and working directly with the alphabetic
color codes.

419
60_Mastermind/perl/mastermind.pl Executable file
View File

@@ -0,0 +1,419 @@
#!/usr/bin/env perl
use 5.010; # To get 'state' and 'say'
use strict; # Require explicit declaration of variables
use warnings; # Enable optional compiler warnings
use English; # Use more friendly names for Perl's magic variables
use List::Util qw{ min sum }; # Convenient list utilities
use Term::ReadLine; # Prompt and return user input
our $VERSION = '0.000_01';
use constant MAX_GUESSES => 10;
print <<'EOD';
MASTERMIND
Creative Computing Morristown, New Jersey
EOD
=begin comment
MASTERMIND II
STEVE NORTH
CREATIVE COMPUTING
PO BOX 789-M MORRISTOWN NEW JERSEY 07960
=end comment
=cut
# NOTE that mixed-case 'my' variables are 'global' in the sense that
# they are used in subroutines, but not passed to them.
say '';
my $number_of_colors = get_input(
'Number of colors [1-8]: ',
sub { m/ \A [1-8] \z /smx },
"No more than 8, please!\n",
);
say '';
my $Number_of_Positions = get_input(
'Number of positions: ',
sub { m/ \A [0-9]+ \z /smx && $ARG },
"A positive number, please\n",
);
say '';
my $number_of_rounds = get_input(
'Number of rounds: ',
sub { m/ \A [0-9]+ \z /smx && $ARG },
"A positive number, please\n",
);
my $P = $number_of_colors ** $Number_of_Positions;
say 'Total possibilities = ', $P;
my @colors = ( qw{
Black White Red Green Orange Yellow Purple Tan
})[ 0 .. $number_of_colors - 1 ];
my @Color_Codes = map { uc substr $ARG, 0, 1 } @colors;
print <<'EOD';
Color Letter
===== ======
EOD
foreach my $inx ( 0 .. $#colors ) {
printf "%-13s%s\n", $colors[$inx], $Color_Codes[$inx];
}
say '';
my $computer_score = 0; # Computer score
my $human_score = 0; # Human score
foreach my $round_number ( 1 .. $number_of_rounds ) {
print <<"EOD";
Round number $round_number ----
Guess my combination.
EOD
$human_score += human_guesses( $Number_of_Positions );
print_score( $computer_score, $human_score );
$computer_score += computer_guesses();
print_score( $computer_score, $human_score );
}
# Make a $pattern into a hash with one key for each possible color. The
# value for each color is the number of times it appears in the pattern.
sub hashify_pattern {
my $pattern = uc $ARG[0];
my %p = map { $ARG => 0 } @Color_Codes;
$p{$ARG}++ for split qr//, $pattern;
return \%p;
}
# Given a $pattern, a $guess at that pattern, and $black and $white
# scores, return a true value if the $black and $white scores of the
# $guess are those supplied as arguments; otherwise return a false
# value. This is used by computer_guesses() to eliminate possibilities.
sub analyze_black_white {
my ( $pattern, $guess, $black, $white ) = @ARG;
my $info = analyze_guess( $pattern, $guess );
return $info->{black} == $black && $info->{white} == $white;
}
# Given a $pattern and a $guess at that pattern, return a reference to a
# hash with the following keys:
# {guess} is the guess;
# {black} is the black score of the guess
# {white} is the white score of the guess
sub analyze_guess {
my ( $pattern, $guess ) = @ARG;
my $pattern_hash = hashify_pattern( $pattern );
my $guess_hash = hashify_pattern( $guess );
my $white = sum(
map { min( $pattern_hash->{$ARG}, $guess_hash->{$ARG} ) } @Color_Codes,
);
my $black = 0;
foreach my $inx ( 0 .. length( $pattern ) - 1 ) {
if ( substr( $pattern, $inx, 1 ) eq substr( $guess, $inx, 1 ) )
{
$black++;
--$white;
}
}
return +{
guess => $guess,
black => $black,
white => $white,
}
}
# Used by the computer to guess the human's choice. The return is the
# number of guesses the computer took. The return is the maximum plus
# one if the computer failed to guess.
sub computer_guesses {
print <<'EOD';
Now I guess. Think of a combination.
EOD
get_input(
'Hit <return> when ready:',
);
# Generate all possible permutations.
my @possible;
foreach my $permutation ( 0 .. @Color_Codes ** $Number_of_Positions - 1 ) {
my $guess;
for ( 1 .. $Number_of_Positions ) {
my $inx = $permutation % @Color_Codes;
$guess .= $Color_Codes[ $inx ];
$permutation = int( $permutation / @Color_Codes );
}
push @possible, $guess;
}
# Guess ...
foreach my $guess_num ( 1 .. MAX_GUESSES ) {
# Guess a possible permutation at random, removing it from the
# list.
my $guess = splice @possible, int rand @possible, 1;
say 'My guess is: ', $guess;
# Find out its black/white score.
my ( $black, $white ) = split qr< , >smx, get_input(
'Blacks, Whites: ',
sub { m/ \A [0-9]+ , [0-9]+ \z /smx },
"Please enter two unsigned integers\n",
);
# If it's all black, the computer wins.
if ( $black == $Number_of_Positions ) {
say "I got it in $guess_num moves!";
return $guess_num;
}
# Eliminate all possible permutations that give the black/white
# score that our guess got. If there are any left, take another
# guess.
next if @possible = grep { analyze_black_white( $ARG, $guess, $black,
$white ) } @possible;
# There were no permutations left. Complain.
print <<'EOD';
You have given me inconsistent information.
Try again, and this time please be more careful.
EOD
goto &computer_guesses; # Tail-call ourselves to try again.
}
print <<'EOD';
I used up all my moves!
I guess my CPU is just having an off day.
EOD
return MAX_GUESSES + 1;
}
# Used to generate a pattern and process the human's guesses. The return
# is the number of guesses the human took. The return is the maximum
# plus one if the human failed to guess.
sub human_guesses {
my @saved_moves; # Saved moves
my $pattern = uc join '',
map { $Color_Codes[ rand @Color_Codes ] } 1 .. $Number_of_Positions;
foreach my $guess_num ( 1 .. MAX_GUESSES ) {
my $guess = uc get_input(
"Move # $guess_num guess: ",
sub {
# If the user entered 'quit', bail out.
if ( m/ \A quit \z /smxi ) {
die "Quitter! My combination was $pattern\n\nGood bye\n";
}
# If the user entered 'board', display the board so far.
# We return success to prevent the warning message, but
# we also clear $ARG. The caller's caller sees this and
# re-queries.
if ( m/ \A board \z /smxi ) {
print <<'EOD';
Board
Move Guess Black White
EOD
my $number = 1;
foreach my $item ( @saved_moves ) {
printf "%4d %-13s %3d %3d\n", $number++,
@{ $item }{ qw{ guess black white } };
}
return undef; # Validation failure, but suppress warning.
}
# End of special-case code. Below here we are dealing
# with guess input.
# The length of the input must equal the number of
# positions.
if ( $Number_of_Positions != length ) {
warn "Bad number of positions\n";
return 0;
}
# The input may contain only valid color codes.
state $invalid_color = do { # Evaluated only once
local $LIST_SEPARATOR = '';
qr< [^@Color_Codes] >smxi;
};
if ( m/ ( $invalid_color ) /smxi ) {
warn "'$1' is unrecognized.\n";
return 0;
}
# We're good.
return 1;
},
"Please enter 'board', 'quit', or any $Number_of_Positions of @{[
join ', ', map { qq<'$ARG'> } @Color_Codes ]}.\n",
);
my $rslt = analyze_guess( $pattern, $guess );
push @saved_moves, $rslt;
if ( $rslt->{black} == $Number_of_Positions ) {
say "You guessed it in $guess_num moves.";
return $guess_num;
}
say "You have $rslt->{black} blacks and $rslt->{white} whites.";
}
print <<"EOD";
You ran out of moves. That's all you get.
The actual combination was: $pattern
EOD
return MAX_GUESSES + 1;
}
# Print the $computer and $human score
sub print_score {
my ( $computer, $human ) = @ARG;
print <<"EOD";
Score:
Computer: $computer
Human: $human
EOD
return;
}
# Get input from the user. The arguments are:
# * The prompt
# * A reference to validation code. This code receives the response in
# $ARG and returns true for a valid response.
# * A warning to print if the response is not valid. This must end in a
# return. It is suppressed if the validation code returned undef.
# The first valid response is returned. An end-of-file terminates the
# script.
sub get_input {
my ( $prompt, $validate, $warning ) = @ARG;
# If no validator is passed, default to one that always returns
# true.
$validate ||= sub { 1 };
# Create the readline object. The 'state' causes the variable to be
# initialized only once, no matter how many times this subroutine is
# called. The do { ... } is a compound statement used because we
# need to tweak the created object before we store it.
state $term = do {
my $obj = Term::ReadLine->new( 'reverse' );
$obj->ornaments( 0 );
$obj;
};
while ( 1 ) { # Iterate indefinitely
# Read the input into the topic variable, localized to prevent
# Spooky Action at a Distance. We exit on undef, which signals
# end-of-file.
exit unless defined( local $ARG = $term->readline( $prompt ) );
# Return the input if it is valid.
return $ARG if my $rslt = $validate->();
# Issue the warning, and go around the merry-go-round again.
warn $warning if defined $rslt;
}
}
# NOTE the following is unused, but left in place in case someone wants
# to add a 'Do you want instructions?'
#
# Get a yes-or-no answer. The argument is the prompt, which will have
# '? [y/n]: ' appended. The donkey work is done by get_input(), which is
# requested to validate the response as beginning with 'y' or 'n',
# case-insensitive. The return is a true value for 'y' and a false value
# for 'n'.
sub get_yes_no {
my ( $prompt ) = @ARG;
state $map_answer = {
n => 0,
y => 1,
};
my $resp = lc get_input(
"$prompt? [y/n]: ",
sub { m/ \A [yn] /smxi },
"Please respond 'y' or 'n'\n",
);
return $map_answer->{ substr $resp, 0, 1 };
}
__END__
=head1 TITLE
mastermind - Play the game 'Mastermind' from Basic Computer Games
=head1 SYNOPSIS
mastermind.pl
=head1 DETAILS
This Perl script is a port of mastermind, which is the 60th
entry in Basic Computer Games.
This is pretty much a re-implementation of the BASIC, taking advantage
of Perl's array functionality and working directly with the alphabetic
color codes.
=head1 PORTED BY
Thomas R. Wyant, III F<wyant at cpan dot org>
=head1 COPYRIGHT AND LICENSE
Copyright (C) 2022 by Thomas R. Wyant, III
This program is free software; you can redistribute it and/or modify it
under the same terms as Perl 5.10.0. For more details, see the Artistic
License 1.0 at
L<https://www.perlfoundation.org/artistic-license-10.html>, and/or the
Gnu GPL at L<http://www.gnu.org/licenses/old-licenses/gpl-1.0.txt>.
This program is distributed in the hope that it will be useful, but
without any warranty; without even the implied warranty of
merchantability or fitness for a particular purpose.
=cut
# ex: set expandtab tabstop=4 textwidth=72 :