Minesweeper game

enum Tile-Type <Empty Mine>;

class Tile {
    has Tile-Type $.type;
    has $.face is rw;
    method Str { with $!face { ~$!face } else { '.' } }
}

class Field {
    has @.grid;
    has Int $.width;
    has Int $.height;
    has Int $.mine-spots;
    has Int $.empty-spots;

    method new (Int $height, Int $width, Rat $mines-ratio=0.1) {

        my $mine-spots = round $width*$height*$mines-ratio;
        my $empty-spots = $width*$height - $mine-spots;

        my @grid;
        for ^$height X ^$width -> ($y, $x) {
            @grid[$y][$x] = Tile.new(type => Empty);
        }
        for (^$height).pick($mine-spots) Z (^$width).pick($mine-spots) -> ($y, $x) {
            @grid[$y][$x] = Tile.new( type => Mine);
        }
        self.bless(:$height, :$width, :@grid, :$mine-spots, :$empty-spots);
    }

    method open( $y, $x) {
        return if @!grid[$y][$x].face.defined;

        self.end-game("KaBoom") if @!grid[$y][$x].type ~~ Mine;

        my @neighbors = gather do
        take [$y+.[0],$x+.[1]]
        if 0 <= $y + .[0] < $!height && 0 <= $x + .[1] < $!width
         for [-1,-1],[+0,-1],[+1,-1],
             [-1,+0],        [+1,+0],
             [-1,+1],[+0,+1],[+1,+1];

        my $mines = +@neighbors.grep: { @!grid[.[0]][.[1]].type ~~ Mine };

        $!empty-spots--;
        @!grid[$y][$x].face = $mines || ' ';

        if $mines == 0 {
            self.open(.[0], .[1]) for @neighbors;
        }
        self.end-game("You won") if $!empty-spots == 0;
    }

    method end-game(Str $msg ) {
        for ^$!height X ^$!width -> ($y, $x) {
            @!grid[$y][$x].face = '*' if @!grid[$y][$x].type ~~ Mine
        }
        die $msg;
    }

    method mark ( $y, $x) {
        if !@!grid[$y][$x].face.defined {
            @!grid[$y][$x].face = "⚐";
            $!mine-spots-- if @!grid[$y][$x].type ~~ Mine;
        }
    elsif !@!grid[$y][$x].face eq "⚐" {
            undefine @!grid[$y][$x].face;
            $!mine-spots++ if @!grid[$y][$x].type ~~ Mine;
        }
        self.end-game("You won") if $!mine-spots == 0;
    }

    constant @digs = |('a'..'z') xx *;

    method Str {
        [~] flat '  ', @digs[^$!width], "\n",
             ' ┌', '─' xx $!width, "┐\n",
        join '', do for ^$!height -> $y {
          $y, '│', @!grid[$y][*],   "│\n"; },
             ' └', '─' xx $!width, '┘';
    }

    method valid ($y, $x) {
        0 <= $y < $!height && 0 <= $x < $!width
    }
}

sub a2n($a) { $a.ord > 64 ?? $a.ord % 32 - 1 !! +$a }

my $f = Field.new(6,10);

loop {
    say ~$f;
    my @w = prompt("[open loc|mark loc|loc]: ").words;
    last unless @w;
    unshift @w, 'open' if @w < 2;
    my ($x,$y) = $0, $1 if @w[1] ~~ /(<alpha>)(<digit>)|$1=<digit>$0=<alpha>/;
    $x = a2n($x);
    given @w[0] {
    when !$f.valid($y,$x) { say "invalid coordinates" }
    when /^o/ { $f.open($y,$x)   }
    when /^m/ { $f.mark($y,$x)   }
    default     { say "invalid cmd" }
    }
    CATCH {
    say "$_: end of game.";
    last;
    }
}

say ~$f;