language: raku prompt: https://adventofcode.com/2020/day/17 learn: raku


#!/usr/bin/env raku
use v6;

say 'After 6 generations (3d): ', transform_3d(load_coords, 6).elems, ' active cells.';
say 'After 6 generations (4d): ', transform_4d(load_coords, 6).elems, ' active cells.';

# Debug for checking (e.g.) the parsed coordinates
sub say_map(%m) {
    for %m.kv -> $key, $val {
        say "$key: $val";
    }
}

sub transform_4d(%coords, $n) {
    for 1..$n {
        my %new_coords;
        my %bounds = get_bounds %coords;
        iter_4d %bounds, -> $x, $y, $z, $w {
            my $xyzw = to_coord_key $x, $y, $z, $w;
            if next_cell_state $xyzw, %coords {
                %new_coords{$xyzw} = True;
            }
        }
        %coords = %new_coords;
    }
    return %coords;
}

sub transform_3d(%coords, $n) {
    for 1..$n {
        my %new_coords;
        my %bounds = get_bounds %coords;
        iter_3d %bounds, -> $x, $y, $z {
            my $xyz = to_coord_key $x, $y, $z;
            if next_cell_state $xyz, %coords {
                %new_coords{$xyz} = True;
            }
        }
        %coords = %new_coords;
    }
    return %coords;
}

sub next_cell_state($xyzw, %coords) {
    my $active = %coords{$xyzw} || False;
    my $neighbors = neighbor_count $xyzw, %coords;

    if $active && ($neighbors < 2 || 3 < $neighbors) {
        $active = False;
    }
    elsif !$active && $neighbors == 3 {
        $active = True;
    }

    return $active;
}

sub iter_4d(%bounds, &f) {
    for minmax_as_range %bounds{'x'} -> $x {
        for minmax_as_range %bounds{'y'} -> $y {
            for minmax_as_range %bounds{'z'} -> $z {
                for minmax_as_range %bounds{'w'} -> $w {
                    &f($x, $y, $z, $w);
                }
            }
        }
    }
}

sub iter_3d(%bounds, &f) {
    for minmax_as_range %bounds{'x'} -> $x {
        for minmax_as_range %bounds{'y'} -> $y {
            for minmax_as_range %bounds{'z'} -> $z {
                &f($x, $y, $z);
            }
        }
    }
}
sub neighbor_count($xyzw, %coords) {
    my $sum = 0;
    for neighbors $xyzw -> $n {
        if %coords{$n}:exists {
            $sum += 1;
        }
    }
    return $sum;
}

sub neighbors($xyzw) {
    my @neighbors;
    my %coords = from_coord_key $xyzw;
    my ($x, $y, $z, $w) = (
        %coords{'x'},
        %coords{'y'},
        %coords{'z'},
        %coords{'w'}
    );

    for ($x - 1)..($x + 1) -> $n_x {
        for ($y - 1)..($y + 1) -> $n_y {
            for ($z - 1)..($z + 1) -> $n_z {
                for ($w - 1)..($w + 1) -> $n_w {
                    my $n = to_coord_key $n_x, $n_y, $n_z, $n_w;
                    @neighbors.push($n) if $n ne $xyzw;
                }
            }
        }
    }
    return @neighbors;
}

sub get_bounds(%coords) {
    my %bounds;
    for %coords.keys -> $xyzw {
        for from_coord_key($xyzw).kv -> $d, $val {
            if %bounds{$d}{'min'}:!exists || $val < %bounds{$d}{'min'} {
                %bounds{$d}{'min'} = $val;
            }
            if %bounds{$d}{'max'}:!exists || $val > %bounds{$d}{'max'} {
                %bounds{$d}{'max'} = $val;
            }
        }
    }
    return %bounds;
}

sub minmax_as_range(%minmax) {
    return (%minmax{'min'} - 1)..(%minmax{'max'}+1);
}

constant COORD_DELIM = ',';

sub from_coord_key($xyzw) {
    my ($x, $y, $z, $w) = $xyzw.split(COORD_DELIM);
    return {x => $x, y => $y, z => $z, w => $w};
}

sub to_coord_key($x, $y, $z=0, $w=0) {
    return join(COORD_DELIM, $x, $y, $z, $w);
}

sub load_coords {
    my %coords;
    my $input = open 'input';
    my $x = 0;
    for $input.lines -> $line {
        next if $line.chars == 0;
        my $y = 0;
        for $line.split('') -> $cell {
            next if $cell.chars == 0;
            if $cell ~~ '#' {
                %coords{to_coord_key($x, $y)} = True;
            }
            $y += 1;
        }
        $x += 1;
    }
    return %coords;
}