Merge pull request #487 from trwyant/75_Roulette_perl_trw

Port 75_Roulette to Perl.
This commit is contained in:
Jeff Atwood
2022-01-11 14:05:47 -08:00
committed by GitHub
4 changed files with 2561 additions and 0 deletions

View File

@@ -1,3 +1,15 @@
Original source downloaded [from Vintage Basic](http://www.vintage-basic.net/games.html)
Conversion to [Perl](https://www.perl.org/)
This conversion consists of three files in `75_Roulette/perl/`:
- `roulette.pl` is the port of the BASIC to Perl;
- `roulette-test.t` is a Perl test for correctness of display and payout;
- `make-roulette-test.pl` generates roulette-test.t from roulette.bas.
The ported version of the game numbers the slots from 0 rather than 1, and uses a dispatch table to figure out the payout.
The Perl test loads `roulette.pl` and verifies the Perl slot display and payout logic against the BASIC for all combinations of slots and bets. If any tests fail that fact will be noted at the end of the output.
The test code is generated by reading the BASIC, retaining only the slot display and payout logic (based on line numbers), and wrapping this in code that generates all combinations of bet and spin result. The result is run, and the result is captured and parsed to produce `roulette-test.t`. `make-roulette-test.pl` has some command-line options that may be of interest. `--help` will display the documentation.

View File

@@ -0,0 +1,263 @@
#!/usr/bin/env perl
use 5.014; # For s///r
use strict;
use warnings;
use File::Temp;
use Getopt::Long 2.33 qw{ :config auto_version };
use IPC::Cmd qw{ can_run }; # Core as of Perl 5.9.5.
use Pod::Usage;
our $VERSION = '0.000_01';
my %opt = (
program => find_basic(),
output => make_default_output(),
);
GetOptions( \%opt,
qw{ output=s program=s },
help => sub { pod2usage( { -verbose => 2 } ) },
) or pod2usage( { -verbose => 0 } );
die "No default BASIC found; you must specify --program\n"
unless defined $opt{program};
my $game_dir = ( File::Spec->splitdir( $0 ) )[0];
my $basic_file = File::Spec->catfile( $game_dir, 'roulette.bas' );
open my $basic_handle, '<', $basic_file
or die "Unable to open $basic_file: $!\n";
my $munged = File::Temp->new();
print { $munged } <<'EOD';
1000 Y=50
1010 DIM B(100),C(100),T(100)
1090 FOR S=1 TO 38
1095 PRINT "SPIN ";S
1100 FOR C=1 TO Y
1110 B(C)=1
1120 T(C)=C
1130 NEXT C
EOD
transcribe( $basic_file, $basic_handle, $munged, 1860, 2810 );
transcribe( $basic_file, $basic_handle, $munged, 2950 );
say { $munged } '4000 NEXT S';
$munged->flush();
if ( $opt{output} ne '-' ) {
my $dir = ( File::Spec->splitpath( $0 ) )[1];
my $fn = File::Spec->rel2abs( $opt{output}, $dir );
$fn = File::Spec->abs2rel( $fn );
open my $fh, '>', $fn
or die "Unable to open $fn: $!\n";
warn "Writing $fn\n";
select $fh;
}
print <<'EOD';
package main;
use 5.010;
use strict;
use warnings;
use File::Spec;
use Test::More 0.88; # Because of done_testing();
EOD
print <<"EOD";
# NOTE: This file is generated by $0.
# Any edits made to it will be lost the next time it is regenerated.
# Caveat coder.
EOD
print <<'EOD';
my $dir = ( File::Spec->splitpath( $0 ) )[1];
my $script = File::Spec->catfile( $dir, 'roulette.pl' );
{
# Modern Perls do not have . in @INC, but we need it there to load a
# relative path.
local @INC = ( File::Spec->curdir(), @INC );
require $script; # Load game as module
}
EOD
my $spin;
my $name;
foreach ( `$opt{program} @{[ $munged->filename() ]}` ) {
s/\N{U+1D}/ /smxg; # Artifact of the BASIC I'm using.
s/ \s+ \z //smx;
s/ \A \s+ //smx;
if ( $_ eq '' ) {
# Ignore empty lines.
} elsif ( m/ \A SPIN \s* ( [0-9]+ ) /smx ) {
$spin = $1 - 1; # BASIC is 1-based, but Perl is 0-based
} elsif ( m/ \A YOU \s+ WIN \s* ( [0-9]+ ) \s*
DOLLARS \s+ ON \s+ BET \s* ( [0-9]+ ) /smx ) {
say "is payout( $spin, $2 ), $1, 'Spin $spin ($name), bet $2 pays $1';";
} elsif ( m/ \A YOU \s+ LOSE \s* ( [0-9]+ ) \s*
DOLLARS \s+ ON \s+ BET \s* ( [0-9]+ ) /smx ) {
say "is payout( $spin, $2 ), -$1, 'Spin $spin ($name), bet $2 pays -$1';";
} elsif ( m/ \A \s* ( [0-9]+ ) (?: \s* ( [[:alpha:]]+ ) )? \z /smx ) {
$name = $2 ? sprintf( '%d %s', $1, ucfirst lc $2 ) : $1;
say "is format_spin( $spin ), '$name', 'Spin $spin is $name';";
} else {
die "Unexpected input $_";
}
}
print <<'EOD';
done_testing;
1;
# ex: set textwidth=72 :
EOD
sub find_basic {
# yabasic seems not to work
foreach my $prog ( qw{ basic cbmbasic } ) {
return $prog if can_run( $prog )
}
return undef;
}
sub make_default_output {
( my $rslt = $0 ) =~ s/ [.] pl \z /.t/smx;
$rslt =~ s/ .* \b make- //smx;
return $rslt;
}
sub transcribe {
my ( $in_file, $in_handle, $out_handle, $first_line, $last_line ) = @_;
$last_line //= $first_line;
while ( <$in_handle> ) {
m/ \A \s* ( [0-9]+ )+ \s /smx
or next;
$1 < $first_line
and next;
say { $out_handle } sprintf '%04d REM BEGIN VERBATIM FROM %s',
$first_line - 10, $in_file;
print { $out_handle } $_;
last;
}
while ( <$in_handle> ) {
m/ \A \s* ( [0-9]+ )+ \s /smx
and $1 > $last_line
and last;
print { $out_handle } $_;
}
say { $out_handle } sprintf '%04d REM END VERBATIM FROM %s',
$last_line + 10, $in_file;
return;
}
__END__
=head1 TITLE
make-roulette-test.pl - Generate the tests for 75_Roulette/perl/roulette.pl
=head1 SYNOPSIS
perl 75_Roulette/perl/make-roulette-test.pl
perl 75_Roulette/perl/make-roulette-test.pl --program mybasic
perl 75_Roulette/perl/make-roulette-test.pl --help
perl 75_Roulette/perl/make-roulette-test.pl --version
=head1 OPTIONS
<<< replace boiler plate >>>
=head2 --help
This option displays the documentation for this script. The script then
exits.
=head2 --output
--output fubar.t
This option specifies the output file. This needs to be in the same
directory as F<roulette.pl>, and defaults to that directory. A single
dash (C<'-'>) is special-cased to send the output to standard out.
The default is C<--output=test-roulette.t>.
=head2 --program
--program my_basic
This option specifies the name of your BASIC interpreter. This must be
the name of an executable file in your PATH (aliases do not work).
The default is the first-found in the list C<qw{ basic cbmbasic }>.
=head2 --version
This option displays the version of this script. The script then exits.
=head1 DETAILS
This Perl script generates F<roulette-test.t>, which tests
F<roulette.pl>. The latter is expected to be written as a modulino.
This script assumes that:
=over
=item * it is in the same directory as F<roulette.pl>;
=item * F<roulette.bas> is in the first-level subdirectory under the current directory;
=back
The generated test assumes that it is in the same directory as
F<roulette.pl>.
This script works by abstracting the internals of F<roulette.bas> and
wrapping them in a loop that generates all possible spins, and places
all possible bets on each spin. The generated BASIC is written to a
temporary file, and executed by a BASIC interpreter. The output is
parsed and used to generate the output.
Obviously there is some ad-hocery going on, and this script has only
been tested under C<cbmbasic>, which was what I had on hand.
B<Caveat:> the abstraction process is driven by BASIC line numbers. Any
change of these puts the ad-hocery at risk.
=head1 AUTHOR
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 textwidth=72 :

File diff suppressed because it is too large Load Diff

319
75_Roulette/perl/roulette.pl Executable file
View File

@@ -0,0 +1,319 @@
#!/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 POSIX qw{ strftime }; # Time formatting
use Term::ReadLine; # Prompt and return user input
our $VERSION = '0.000_01';
# A main() function is not usual in Perl scripts. I have installed one
# here to make the script into a "modulino." The next line executes
# main() if and only if caller() returns false. It will do this if we
# were loaded by another Perl script but not otherwise. This was done so
# I could test the payout and spin formatting logic.
main() unless caller;
sub main {
print <<'EOD';
ROULETTE
Creative Computing Morristown, New Jersey
Welcome to the roulette table.
EOD
if ( get_yes_no( 'Do you want instructions' ) ) {
print <<'EOD';
This is the betting layout
(*=red)
1* 2 3*
4 5* 6
7* 8 9*
10 11 12*
---------------
13 14* 15
16* 17 18*
19* 20 21*
22 23* 24
---------------
25* 26 27*
28 29 30*
31 32* 33
34* 35 36*
---------------
00 0
Types of bets:
The numbers 1 to 36 signify a straight bet
on that number.
These pay off 35:1
The 2:1 bets are:
37) 1-12 40) first column
38) 13-24 41) second column
39) 25-36 42) third column
The even money bets are:
43) 1-18 46) odd
44) 19-36 47) red
45) even 48) black
49) 0 and 50) 00 pay off 35:1
Note: 0 and 00 do not count under any
bets except their own.
When I ask for each bet, type the number
and the amount, separated by a comma.
For example: to bet $500 on black, type 48,500
when I ask for a bet.
The minimum bet is $5, the maximum is $500.
EOD
}
my $P = 1000;
my $D = 100000.;
while ( 1 ) { # Iterate indefinitely
my $Y = get_input( 'How many bets? ',
sub { m/ \A [0-9]+ \z /smx && $ARG > 0 && $ARG <= 50 },
"Please enter a positive integer no greater than 50\n",
);
my @B;
my @T;
foreach my $C ( 1 .. $Y ) {
my ( $X, $Z ) = split qr< , >smx, get_input(
"Number $C: ",
sub { m/ \A ( [0-9]+ ) , ( [0-9]+ ) \z /smx
&& $1 > 0 && $1 <= 50 && $2 >= 5 && $2 <= 500 },
"Please enter two comma-separated positive numbers\n",
);
if ( $B[$X] ) {
say 'You made that bet once already, dum-dum.';
redo;
}
$B[$X] = $Z; # BASIC does $B[$C] = $Z
$T[$C] = $X;
}
print <<'EOD';
Spinning ...
EOD
my $S = int rand 38; # Zero-based, versus 1-based in BASIC
say format_spin( $S );
say '';
foreach my $C ( 1 .. $Y ) {
my $X = $T[$C];
my $payout = payout( $S, $X ) * $B[$X];
$D -= $payout;
$P += $payout;
if ( $payout > 0 ) {
say "You win $payout dollars on bet $C";
} else {
$payout = -$payout;
say "You lose $payout dollars on bet $C";
}
}
say "Totals\tMe\tYou";
say "\t$D\t$P";
say '';
last unless get_yes_no( 'Again' );
}
say '';
if ( $P > 0 ) {
my $B = get_input(
'To whom shall I make out the check? ',
);
my $check_number = 1000 + int rand 9000;
my $todays_date = strftime( '%B %d, %Y', localtime );
print <<"EOD";
------------------------------------------------------------ Check number $check_number
$todays_date
Pay to the order of ------ $B ----- \$$P
The Memory Bank of New York
The Computer
---------X-----
-------------------------------------------------------------------------------
Come back soon!
EOD
} else {
print <<'EOD';
Thanks for your money.
I'll use it to buy a solid gold roulette wheel
EOD
}
}
{
# Define the kind of each possible spin. 0 is '0' or '00', 1 is
# black, and 2 is red. We assign the values in a BEGIN block because
# execution never actually reaches this point in the script.
my @kind;
BEGIN {
@kind = ( 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 2, 1, 2, 1, 2, 1,
2, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 1, 2, 1, 2, 1, 2, 1, 2, 0,
0 );
}
# Convert the spin (0-37) to its name on the wheel.
sub format_spin {
my ( $number ) = @_;
state $format = [
sub { '0' x ( $_[0] - 35 ) },
sub { sprintf '%s Black', $_[0] + 1 },
sub { sprintf '%s Red', $_[0] + 1 },
];
return $format->[$kind[$number]]( $number );
}
# Compute the payout given the spin (0-37) and the bet (1-50).
sub payout {
my ( $number, $bet ) = @_;
# We compute the payout on '0' and '00' directly, since under
# our rules they are only eligible for the 35-to-1 bet.
$kind[$number]
or return $number == $bet - 49 + 36 ? 35 : -1;
--$bet; # #bet is 1-based coming in
# Dispatch table for computing the payout for spins 0-36.
state $payout = [
( sub { $_[0] == $_[1] ? 35 : -1 } ) x 36,
( sub { int( $_[0] / 12 ) == $_[1] - 36 ? 2 : -1 } ) x 3,
( sub { $_[0] % 3 == $_[1] - 39 ? 2 : -1 } ) x 3,
( sub { int( $_[0] / 18 ) == $_[1] - 42 ? 1 : -1 } ) x 2,
( sub { $_[0] % 2 == 45 - $_[1] ? 1 : -1 } ) x 2,
( sub { $kind[$_[0]] == 48 - $_[1] ? 1 : -1 } ) x 2,
( sub { -1 } ) x 2, # Bet on '0' or '00' loses
];
return $payout->[$bet]->( $number, $bet );
}
}
# 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.
# 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 $validate->();
# Issue the warning, and go around the merry-go-round again.
warn $warning;
}
}
# 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
roulette - Play the game 'Roulette' from Basic Computer Games
=head1 SYNOPSIS
roulette.pl
=head1 DETAILS
This Perl script is a port of roulette, which is the 75th
entry in Basic Computer Games.
The main internal changes are converting the roulette slot numbering to
0-based and replacing most of the payout logic with a dispatch table.
These changes were tested for correctness against the original BASIC.
=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 :