Stepping Winner with Raku

by Arne Sommer

Stepping Winner with Raku

[63] Published 6. March 2020.

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

Challenge #52.1: Stepping Numbers

Write a script to accept two numbers between 100 and 999. It should then print all Stepping Numbers between them.

A number is called a stepping number if the adjacent digits have a difference of 1. For example, 456 is a stepping number but 129 is not.

Brute force should work, as the number of numbers (so to speak) isn't that large:

File: stepping-loop
subset SteppingLimit of Int where 100 <= * <= 999;                    # [1]

unit sub MAIN (SteppingLimit $from,                                   # [2]
               SteppingLimit $to where $to > $from,                   # [3]
	       :$verbose);                                            # [4]

say ": Candidates: { ($from.Int .. $to.Int).list }" if $verbose;      # [5]

for $from .. $to -> $number                                           # [6]
{
  say $number if is_stepping($number);                                # [7]
}

sub is_stepping (SteppingLimit $number)                               # [8]
{
  my @digits = $number.comb;                                          # [9]

  my $current = @digits.shift;                                        # [10]

  while (@digits)                                                     # [11]
  {
    return False unless @digits[0] == any($current +1, $current -1);  # [12]
    $current = @digits.shift;                                         # [13]
  }

  return True;                                                        # [14]
}

[1] The legal input values are between 100 and 999, both included. I have used a custom type (with subset) to enforce that. Note the way we can check both boundaries at once, as done here. The * represents the current value in the where check.

[2] Enforcing legal values on the from variable,

[3] and the to variable. Note the extra check to enforce that to is higher than from.

[4] Verbose mode, as I am fond of that. Note my private rule about a leading colon on the verbose output to make them stand out.

[5] Just to show that we actually get a full list.

[6] For each number,

[7] • print it if it as a stepping number.

[8] Check the given number.

[9] Split the number into a list of the three individual digits.

[10] Get the first digit.

[11] As long as there are more digits (initially there are 2).

[12] Return «False» if the next digit is not one higher or one lower than the current one. Note the any junction that makes it possible to do multiple comparisons at once.

[13] The digit is legal (as [12] didn't return). Get the next digit, ready for the next iteration (in [11]).

[14] If we get here, we have checked all the digits, and the number is a stepping number.

Running it:

$ raku stepping-loop 100 199
101
121
123

$ raku stepping-loop 100 299
101
121
123
210
212
232
234

$ raku stepping-loop 100 999
101
121
123
210
212
232
234
321
323
343
345
432
434
454
456
543
545
565
567
654
656
676
678
765
767
787
789
876
878
898
987
989

That looks ok. It would probably look nicer if we printed the values on a single line. So I'll do that. In the next program.

Brute force works out quite nicely as the numbers (the difference between the lower and upper limits) are small. But it doesn't scale well. We can fix that by only generating the actual stepping numbers. And gather/take is a good choice here.

File: stepping-gather
subset SteppingLimit of Int where 100 <= * <= 999;

unit sub MAIN (SteppingLimit $from,
               SteppingLimit $to where $to > $from,
	       :$verbose);

say ": Candidates: { ($from.Int .. $to.Int).list }" if $verbose;

my $a = $from.substr(0,1).Int;                                       # [1]
my $b = $to.substr(0,1).Int;                                         # [2]

my $stepping := gather                                               # [3]
{
  for $a .. $b -> $first-digit                                       # [4]
  {
    for $first-digit -1, $first-digit + 1 -> $second-digit           # [5]
    {
      next unless -1 < $second-digit < 10;                           # [6]
      
      for $second-digit -1, $second-digit + 1 -> $third-digit        # [7]
      {
        next unless -1 < $third-digit < 10;                          # [8]
	my $current = "$first-digit$second-digit$third-digit".Int;   # [9]
	take $current if $from <= $current <= $to;                   # [10]
      }
    }
  }
}

say $stepping.join(", ");                                            # [11]

[1] Get the first digit of the from number,

[2] and the same for the to number.

[3] Set it all up with gather/take.

[4] A loop for the first digit, iterating over the legal first digits.

[5] A loop for the second digit, where we use the values below (-1) and above (+1) the current first digit.

[6] Skip illegal digits (-1 and 10), as they don't exist.

[7] A loop for the third digit, where we use the values below (-1) and above (+1) the current second digit.

[8] As [6], for the third digit.

[9] We have a stepping number, glue the digits together.

[10] Return the number if it is within the from and to limits. This check is necessary, as e.g. a start value of «199» would give the very first result at «123» (as we only take the first digit into account in the loops).

[11] Nicer output this time, on a single line.

Running it:

$ raku stepping-gather 100 199
101, 121, 123

$ raku stepping-gather 100 299
101, 121, 123, 210, 212, 232, 234

$ raku stepping-gather 100 999
101, 121, 123, 210, 212, 232, 234, 321, 323, 343, 345, 432, 434, 454, 456, \
543, 545, 565, 567, 654, 656, 676, 678, 765, 767, 787, 789, 876, 878, 898, \
987, 989

Note that this algorithm requires the same length (number of digits) in the from and to values. That is enforced in this program by the subset, so we are good.

Challenge #52.2: Lucky Winner

Suppose there are following coins arranged on a table in a line in random order.

    £1, 50p, 1p, 10p, 5p, 20p, £2, 2p
Suppose you are playing against the computer. Player can only pick one coin at a time from either ends. Find out the lucky winner, who has the larger amounts in total?

I have chosen to write a «coin» class, where we get the coin label (e.g. «£2») when we stringify the object, and the decimal value (e.g. «200») when we use it in a numeric context.

A hash, where we map the strings and the values, would work just as well. But a class is cooler.

File: lucky (partial)
unit sub MAIN (:$verbose);           # [1]

my $player's-turn = Bool.pick;       # [2]

say ": The { $player's-turn ?? 'Player' !! 'Machine' } starts." if $verbose

class coin                           # [3]
{
  has $.value;                       # [3a]
  has $.label;                       # [3b]

  method Str     { self.label }      # [3c]
  method Numeric { self.value }      # [3d]
  method Real    { self.value }      # [3e]
}

my @coins;                           # [4]

@coins.push: coin.new(label => "1p",  value =>   1);  # [4a]
@coins.push: coin.new(label => "2p",  value =>   2);
@coins.push: coin.new(label => "5p",  value =>   5);
@coins.push: coin.new(label => "10p", value =>  10);
@coins.push: coin.new(label => "20p", value =>  20);
@coins.push: coin.new(label => "50p", value =>  50);
@coins.push: coin.new(label => "£1",  value => 100);
@coins.push: coin.new(label => "£2",  value => 200);

@coins.=pick(*);                     # [5]

say ": Coins (val): { @coins>>.Numeric.join(", ") }" if $verbose;      # [6]
say ": Coins (txt): { @coins>>.Str.join(", ") }"     if $verbose;      # [6]

my $player   = 0;                    # [7]
my $computer = 0;                    # [7]

[1] Verbose mode, yet again.

[2] The challenge doesn't say if the player or the computer should start first, so we let the program decide. (True, False).pick would also work, but is more typing. Note the ' in the variable name. That is legal in Raku. This variable is used throughout the program to keep track of whose turn it is.

[3] The coin has a value (in pence) [3a] and a label (a string) [3b]. The Str method is used to stringify the object, and Numeric is used when we add the coins (in [7]). Real is used when we numerically compare two coins (in [14]).

[4] An array of coins, from left to right. Add the coins objects [4a].

[5] Reorder the coins (pick(*) picks them all, in random order), and assign them back to the original array (with .=).

[6] Note the hyper operator call >>. that applies the method (Numeric and Str) on each element in the array. The next method call (.join) is done on the whole array, as usual.

[7] The initial amount for the player and computer.

Note the missing check for a matching value and label in the «coin» class. I have played nice here, and specified values that are equal. But don't write code on the assumption that a programmer will not screw up. They do. (I know, as I am one.)

File: lucky (the rest)
while @coins                                           # [8]
{
  say "Available Coins: { @coins>>.Str.join(", ") }."; # [9]

  if $player's-turn                                    # [10]
  {
    my $choice = @coins.elems > 1                      # [11]
      ?? prompt "Do you want the Left (L) or the Right (R): "
      !! "L";

    my $coin = $choice.substr(0,1) eq "L"              # [12]
      ?? @coins.shift
      !! @coins.pop;

    say "You choose $coin";                            # [12a]
    $player += $coin;                                  # [12b]
  }
  
  else # The computer has a go                         # [13]
  {
    my $coin = @coins[0] > @coins[*-1]                 # [14]
      ?? @coins.shift
      !! @coins.pop;
      
    say "The Computer choose $coin";                   # [14a]
    $computer += $coin;                                # [14b]
 }
  
  say "Player:   $player p";                           # [15]
  say "Computer: $computer p";
  say "";
  $player's-turn = ! $player's-turn                    # [16]
}

if    $player > $computer { say "Player won"; }        # [17]
elsif $player < $computer { say "Computer won"; }
else                      { say "Nobody won"; }

[8] As long as there are more coins.

[9] Show the list of available coins.

[10] If it is the Player's turn,

[11] • Ask about the left or right coin, if more than 1. If only one, take the left without asking.

[12] • If the user has chosen the left, take the left (with shift). If not, take the right (with pop). Add the value [12b].

[13] The computer's turn.

[14] • Choose the coin with the highest value. (This is not the best strategy, as it doesn't look ahead, but it is the easiest one to implement that has a chance of success.) Show which coin was chosen [14a], as the user wouldn't know otherwise.

[15] Show the score.

[16] The other player's turn, ready for the next iteration.

[17] When there are no more coins (see [8]), show who won, if any. (The else part is actually redundant with the chosen coins, as it is impossible to get a tie. The player that gets hold of the «£2» coin will be the winner, regardless of the other coins. (As both players get 4 coins each, and the sum of the four most valuable coins, excluding the «£2» coin, is «£1.75».)

Running it:

$ raku lucky
Available Coins: 10p, 1p, 5p, £1, 50p, 2p, £2, 20p.
The Computer choose 20p
Player:   0 p
Computer: 20 p

Available Coins: 10p, 1p, 5p, £1, 50p, 2p, £2.
Do you want the Left (L) or the Right (R): R
You choose £2
Player:   200 p
Computer: 20 p

Available Coins: 10p, 1p, 5p, £1, 50p, 2p.
The Computer choose 10p
Player:   200 p
Computer: 30 p

Available Coins: 1p, 5p, £1, 50p, 2p.
Do you want the Left (L) or the Right (R): R
You choose 2p
Player:   202 p
Computer: 30 p

Available Coins: 1p, 5p, £1, 50p.
The Computer choose 50p
Player:   202 p
Computer: 80 p

Available Coins: 1p, 5p, £1.
Do you want the Left (L) or the Right (R): R
You choose £1
Player:   302 p
Computer: 80 p

Available Coins: 1p, 5p.
The Computer choose 5p
Player:   302 p
Computer: 85 p

Available Coins: 1p.
You choose 1p
Player:   303 p
Computer: 85 p

Player won

And another one:

$ raku lucky
Available Coins: £1, 10p, 1p, 5p, 50p, £2, 2p, 20p.
The Computer choose £1
Player:   0 p
Computer: 100 p

Available Coins: 10p, 1p, 5p, 50p, £2, 2p, 20p.
Do you want the Left (L) or the Right (R): R
You choose 20p
Player:   20 p
Computer: 100 p

Available Coins: 10p, 1p, 5p, 50p, £2, 2p.
The Computer choose 10p
Player:   20 p
Computer: 110 p

Available Coins: 1p, 5p, 50p, £2, 2p.
Do you want the Left (L) or the Right (R): L
You choose 1p
Player:   21 p
Computer: 110 p

Available Coins: 5p, 50p, £2, 2p.
The Computer choose 5p
Player:   21 p
Computer: 115 p

Available Coins: 50p, £2, 2p.
Do you want the Left (L) or the Right (R): L
You choose 50p
Player:   71 p
Computer: 115 p

Available Coins: £2, 2p.
The Computer choose £2
Player:   71 p
Computer: 315 p

Available Coins: 2p.
You choose 2p
Player:   73 p
Computer: 315 p

Computer won

And that's it.