Count Minesweeper
with Raku and Perl

by Arne Sommer

Count Minesweeper with Raku and Perl

[142] Published 22. August 2021.

This is my response to the Perl Weekly Challenge #126.

Challenge #126.1: Count Numbers

You are given a positive integer $N.

Write a script to print count of numbers from 1 to $N that don’t contain digit 1.

Example:
Input: $N = 15
Output: 8

    There are 8 numbers between 1 and 15 that don't contain digit 1.
    2, 3, 4, 5, 6, 7, 8, 9.

Input: $N = 25
Output: 13

    There are 13 numbers between 1 and 25 that don't contain digit 1.
    2, 3, 4, 5, 6, 7, 8, 9, 20, 22, 23, 24, 25.

Let us start with the basic sequence:

File: non-one-seq
#! /usr/bin/env raku

unit sub MAIN (Int $N where $N > 0);           # [1]

my $non-one := gather                          # [2]
{
  for 2 .. Inf -> $candidate                   # [3]
  {
    take $candidate unless $candidate ~~ /1/;  # [4]
  }
}

say "First $N non-one numbers: { $non-one[^$N].join(', ') }.";  # [5]

[1] Ensure a positive integer.

[2] Setting up the sequence with gather/take is ideal here.

[3] Start at 2 (as 1 cannot be part of the solution, and go of ad infinitum.

[4] Use (take) the value if it does not contain any 1 digit.

[5] Print the specified number of values. The sequnce is lazy, so the values are only computed on demand.

See my Raku Gather, I Take article or docs.raku.org/syntax/gather take for more information about gather/take.

Running it:

$ ./non-one-seq 10
First 10 non-one numbers: 2, 3, 4, 5, 6, 7, 8, 9, 20, 22.

$ ./non-one-seq 15
First 15 non-one numbers: 2, 3, 4, 5, 6, 7, 8, 9, 20, 22, 23, 24, 25, 26, 27.

$ ./non-one-seq 20
First 20 non-one numbers: 2, 3, 4, 5, 6, 7, 8, 9, 20, 22, 23, 24, 25, 26, 27,\
  28, 29, 30, 32, 33.

We can make it more compact, and definitely less readable:

File: non-one-seq-shorter
#! /usr/bin/env raku

unit sub MAIN (Int $N where $N > 0);

my $non-one := gather take $_ unless /1/ for 2 .. Inf;  # [1]

say "First $N non-one numbers: { $non-one[^$N].join(', ') }.";

[1] Yes, this works.

$ ./non-one-seq-shorter 10
First 10 non-one numbers: 2, 3, 4, 5, 6, 7, 8, 9, 20, 22.

Then we can use this sequence to answer the challenge:

File: count-numbers
#! /usr/bin/env raku

unit sub MAIN (Int $N where $N > 0, :v(:$verbose));

my $non-one := gather take $_ unless /1/ for 2 .. Inf;

my @numbers = $non-one.map({ $_ <= $N ?? $_ !! last });  # [1]

say ": Numbers: { @numbers.join(", ") }" if $verbose;    # [3]

say @numbers.elems;                                      # [2]

[1] This gives us a list (non lazy) with values lower than the limit. The last statement terminates the implicit loop (the map).

[2] Print the number of elements.

[3] use verbose mode if you want the sequence as well.

See docs.raku.org/routine/last for more information about last.

Running it:

$ ./count-numbers -v 15
: Numbers: 2, 3, 4, 5, 6, 7, 8, 9
8

$ ./count-numbers -v 25
: Numbers: 2, 3, 4, 5, 6, 7, 8, 9, 20, 22, 23, 24, 25
13

Or we could turn it upside down, and generate a sequence that stops where we want it to - instead of pulling the plug later on:

File: count-numbers-last
#! /usr/bin/env raku

unit sub MAIN (Int $N where $N > 0, :v(:$verbose));

my $non-one := gather
{
  for 2 .. Inf -> $candidate
  {
    last if $candidate > $N;                    # [1]
    take $candidate unless $candidate ~~ /1/;
  }
}

say ": Numbers: { @$non-one.join(', ') }." if $verbose;

say @$non-one.elems;                            # [2]

[1] The sequence stops when we have passed the target.

[2] print the whole sequence. This works, as it isn<'t infinite anymore.

Running it:

$ ./count-numbers-last -v 15
: Numbers: 2, 3, 4, 5, 6, 7, 8, 9.
8

$ ./count-numbers-last -v 25
: Numbers: 2, 3, 4, 5, 6, 7, 8, 9, 20, 22, 23, 24, 25.
13

We can do a one linerish trick here as well, but it is not as elegant:

File: count-numbers-last-shorter
#! /usr/bin/env raku

unit sub MAIN (Int $N where $N > 0, :v(:$verbose));

my $non-one :=
  gather ( $_ > $N ?? (last) !! take $_ unless $_ ~~ /1/ ) for 2 .. Inf;

say ": Numbers: { @$non-one.join(', ') }." if $verbose;

say @$non-one.elems;

Note the parens around last. The keyword will gobble up the following !! if we did not supply them.

Running it:

$ ./count-numbers-last-shorter -v 15
: Numbers: 2, 3, 4, 5, 6, 7, 8, 9.
8

$ ./count-numbers-last-shorter -v 25
: Numbers: 2, 3, 4, 5, 6, 7, 8, 9, 20, 22, 23, 24, 25.
13

A Non-Gathering Sequence

It is entirely possible to do the sequence without gather/take:

> say (2 .. Inf).grep({ $_ !~~ /1/})[^10];
(2 3 4 5 6 7 8 9 20 22)

The challenge itself, as a true one liner:

> say (2 .. Inf).grep({ $_ > 15 ?? (last) !! $_ !~~ /1/}).eager.elems;
8

The eager keyword is required, as elems does not work on lazy data structures (as we get here).

See docs.raku.org/routine/eager for more information about eager.

Infinity or Not?

Using Infinity really is silly, as we have an upper limit:

> say (2 .. 15).grep({ $_ !~~ /1/}).elems;
8

> say (2 .. 25).grep({ !! $_ !~~ /1/}).elems;
13

As a program, with the «crash bang» line:

File: count-numbers-oneliner
#! /usr/bin/env raku

say (2 .. @*ARGS[0]).grep( * !~~ /1/ ).elems;

We can fix the gather/take version, keeping the MAIN line as it takes care of error handling:

File: count-numbers-gather
#! /usr/bin/env raku

unit sub MAIN (Int $N where $N > 0, :v(:$verbose));

my @non-one = gather ( take $_ unless $_ ~~ /1/ ) for 2 .. $N;

say ": Numbers: { @non-one.join(', ') }." if $verbose;

say @non-one.elems;

Note the use of assignment to an array, instead of binding to a scalar, as the data structure clearly isn't infinite anymore.

A Perl Version

This is straight forward version with an explicit loop:

File: count-numbers-perl
#! /usr/bin/env perl

use strict;
use warnings;
use feature 'say';

my $N = $ARGV[0] // die "Please specify a positive integer";
die "Please specify a positive integer" unless $N =~ /^[1-9]\d*$/;

my $count = 0;

/1/ ? () : $count++ for (2 .. $N);

say $count;

We can use map instead of the loop:

File: count-numbers-map-perl
#! /usr/bin/env perl

use strict;
use warnings;
use feature 'say';

my $N = $ARGV[0] // die "Please specify a positive integer";
die "Please specify a positive integer" unless $N =~ /^[1-9]\d*$/;

say scalar (grep { $_ !~ /1/ } (2 .. $N));  # [1]

[1] Evaluating an array in scalar context (as we do her, quite explicitly) gives us the size.

Running them gives the correct result:

$ ./count-numbers-perl 15
8

$ ./count-numbers-map-perl 15
8

$ ./count-numbers-perl 25
13

$ ./count-numbers-map-perl 25
13

Challenge #126.2: Minesweeper Game

You are given a rectangle with points marked with either x or *. Please consider the x as a land mine.

Write a script to print a rectangle with numbers and x as in the Minesweeper game.

A number in a square of the minesweeper game indicates the number of mines within the neighbouring squares (usually 8), also implies that there are no bombs on that square.

Example:
Input:
    x * * * x * x x x x
    * * * * * * * * * x
    * * * * x * x * x *
    * * * x x * * * * *
    x * * * x * * * * x

Output:
    x 1 0 1 x 2 x x x x
    1 1 0 2 2 4 3 5 5 x
    0 0 1 3 x 3 x 2 x 2
    1 1 1 x x 4 1 2 2 2
    x 1 1 3 x 2 0 0 1 x

We need a way of specifying the rectangle, and I have chosen to recycle the command line idea last used a week ago (Challenge #125.2: Binary Tree Diameter). Specify the rows as given above, and use a vertical bar to separate the rows. E.g. "x * * * | x * x x | * * * * | * * * x".

File: minesweeper-game
#! /usr/bin/env raku

unit sub MAIN (Str $game = "x * * * x * x x x x | * * * * * * * * * x | \
  * * * * x * x * x * | * * * x x * * * * * | x * * * x * * * * x");      # [1]

my @board = $game.split("|")>>.words;          # [2]

die "Uneven row length" unless [==] @board>>.elems;  # [3]

die "Illegal character(s)"
  unless all( $game.split("|")>>.words.flat) eq any("x", "*");
                                               # [4]

for ^@board.elems -> $row                      # [5]
{
  for ^@(@board[$row]).elems -> $col           # [6]
  {
    print get-cell(@board, $row, $col), " ";   # [7]
  }
  say "";                                      # [8]
}

sub get-cell (@board, Int $row, Int $col)      # [9]
{
  return 'x' if @board[$row][$col] eq 'x';     # [10]

  my $count = 0;                               # [11]

  for -1, 0, 1 -> $row-offset                  # [12]
  {
    for -1, 0, 1 -> $col-offset                # [13]
    {
      next if $row-offset == $col-offset == 0; # [14]
      next unless @board[$row + $row-offset][$col + $col-offset];         # [15]
      $count++ if @board[$row + $row-offset][$col + $col-offset] eq "x";  # [16]
    }
  }
  return $count;                               # [17]
}

[1] The rectangle given in the challenge is used, unless another one is specified.

[2] Turn the string into a two dimentional array.

[3] Check that all the rows have the same number of elements. (Note that zero is ok.)

[4] Check for illegal characters (i.e. anything except * and x).

[5] For each row (the index).

[6] For each cell on that row (also as the index).

[7] Get the current cell, convert it as needed (see [7]) and print it.

[8] Add a newline after printing each row.

[9] Procedure giving the value to print.

[10] The simplest case, a mine returns a mine sign.

[11] The number of neighboring mines will go here.

[12] A 3x3 square around the current cell can be computed like this,

[13] and this (as offset to the indices).

[14] The cell itself, ignore.

[15] Off the edge? If so, ignore.

[16] Add to the count if we find a mine at the given position.

[17] Return the mine count.

Running it:

$ ./minesweeper-game
x 1 0 1 x 2 x x x x 
1 1 0 2 2 4 3 5 5 x 
0 0 1 3 x 3 x 2 x 2 
1 1 1 x x 4 1 2 2 2 
x 1 1 3 x 2 0 0 1 x 

Looking good.

Error checking:

$ ./minesweeper-game "x x x | * * * | x x x"
x x x 
4 6 4 
x x x 

$ ./minesweeper-game "x x x | * * * | x x @"
Illegal character(s)
  in sub MAIN at ./minesweeper-game line 9

$ ./minesweeper-game "x x x | * * * | x x"
Uneven row length
  in sub MAIN at ./minesweeper-game line 7

It does not catch empty rows, though:

$ ./minesweeper-game ""

$ ./minesweeper-game "|"


Note the number of blank lines, one for each empty row.

Perl

This is a straight forward translation of the Raku version.

File: minesweeper-game-perl
#! /usr/bin/env perl

use strict;
use warnings;
use feature 'say';
use feature 'signatures';
use Perl6::Junction qw/all any/;

no warnings qw(experimental::signatures);

my $string = $ARGV[0] // "x * * * x * x x x x | * * * * * * * * * x | \
  * * * * x * x * x * | * * * x x * * * * * | x * * * x * * * * x";

my @board;
my @size;

for my $row (split(/\s*\|\s*/, $string))  # [1]
{
  my @row = split(/\s+/, $row);
  die "Illegal character(s)" unless all(@row) eq any("x", "*");  # [2]
  push(@board, \@row);
  push(@size, scalar @row);
}

die "Uneven row length" unless all(@size) == $size[0];           # [2]

for my $row (0 .. scalar @board -1)
{
  for my $col (0 .. scalar @{$board[$row]} -1)
  {
    print get_cell($row, $col, @board), " ";
  }
  say "";
}

sub get_cell ($row, $col, @board)         # [3]
{
  return 'x' if $board[$row][$col] eq 'x';

  my $count = 0;

  for my $row_offset (-1, 0, 1)
  {
    for my $col_offset (-1, 0, 1)
    {
      next if $row_offset == $col_offset && $col_offset == 0;
      next unless $board[$row + $row_offset][$col + $col_offset];
      next if $row + $row_offset < 0;     # [4]
      next if $col + $col_offset < 0;     # [4]

      $count++ if $board[$row + $row_offset][$col + $col_offset] eq "x";
    }
  }
  return $count;
}

[1] Compare this loop with the elegant single line of Raku code.

[2] The checks on row length (they should be the same) and legal characters are done with Junctions, supplied by the Raku inspired module «Perl6::Junction».

[3] I have moved the array argument to the end, as it will gobble up all the other arguments if placed up front (as in the Raku version).

[4] Negative indices (as we may have in the line above) are perfectly ok in Perl, and are from the end of the array. So we need an explicit check on negative indices.

Running it gives the same result as the Raku version:

$ ./minesweeper-game-perl
x 1 0 1 x 2 x x x x 
1 1 0 2 2 4 3 5 5 x 
0 0 1 3 x 3 x 2 x 2 
1 1 1 x x 4 1 2 2 2 
x 1 1 3 x 2 0 0 1 x 

And that's it.