#!/usr/bin/ruby

#`(if running under some shell) {
    eval 'exec /usr/bin/sidef $0 ${1+"$@"}'
}

# Author: Daniel "Trizen" Șuteu
# License: GPLv3
# Created on: 21 August 2012
# Latest edit on: 10 November 2013
# Translated to Sidef in 21 September 2014
# Latest edit on: 15 November 2022
# Website: https://github.com/trizen/asciiplanes

# Find the planes' positions on a grid. (text-based game)

var asciitable = (
                   try   { require('Text::ASCIITable') }
                   catch { STDERR.print("Can't load the 'Text::ASCIITable' Perl module...\n"); Sys.exit(2) }
                 )

var ANSI = (
                try   { frequire('Term::ANSIColor') }
                catch { nil }
           )

## Package variables
var pkgname = 'asciiplanes';
var version = 0.05;

## Game variables
var BOARD_SIZE = 8;
var PLANES_NUM = 3;

var parts = (['head'] + ['hit']*7);
var plane_chars = ['$', '#', '@'];

var use_colors = defined(ANSI)
var wrap_plane = false;
var hit_char   = %q{*};
var miss_char  = %q{`};

func usage {
    print <<"EOT";
usage: #{__MAIN__} [options]

main:
        --size=i    : length side of the board (default: #{BOARD_SIZE})
        --planes=i  : the total number of planes (default: #{PLANES_NUM})
        --wrap!     : wrap the plane around the play board (default: #{wrap_plane})
        --hit=s     : character used when a plane is hit (default: "#{hit_char}")
        --miss=s    : character used when a plane is missed (default: "#{miss_char}")
        --colors!   : use ANSI colors (requires Term::ANSIColor) (default: #{use_colors})

help:
        --help      : print this message and exit
        --version   : print the version number and exit

example:
        #{__MAIN__} --size=12 --planes=6 --hit='*'

EOT

    Sys.exit;
}

func ver {
    print "#{pkgname} #{version}\n";
    Sys.exit;
}

if (!ARGV.is_empty) {
    var getopt = (
                    try   { frequire('Getopt::Long') }
                    catch { STDERR.print("Can't load the 'Getopt::Long' Perl module...\n"); Sys.exit(2) }
                 );

    getopt.GetOptions(
         'board-size|size=i' => func(_,v) { BOARD_SIZE = Num(v)  },
         'planes-num=i'      => func(_,v) { PLANES_NUM = Num(v)  },
         'hit-char=s'        => func(_,v) { hit_char   = Str(v)  },
         'miss-char=s'       => func(_,v) { miss_char  = Str(v)  },
         'wrap!'             => func(_,v) { wrap_plane = Bool(v) },
         'colors!'           => func(_,v) { use_colors = Bool(v) },
         'help|h|?'          => func(*_)  { usage() },
         'version|v|V'       => func(*_)  { ver() },
    );
}

## The play-board of the game, and some other arrays
#---------------------------------------------------------------
var play_board = BOARD_SIZE.of { [nil] * BOARD_SIZE };
var info_board = BOARD_SIZE.of { [' '] * BOARD_SIZE };

var letters = Hash.new;
play_board.range.each { |i|
    static char = 'a';
    letters{char++} = i;
}

#---------------------------------------------------------------

func pointers(board, x, y, indices) {

    var output = [];
    [[0,0]] + indices -> each { |pair|

        pair.is_an(Array) || next;
        var (row, col) = (x + pair[0], y + pair[1]);

        if (row > board.end) {
            wrap_plane || return [];
            row %= board.len;
        }

        if (col > board[row].end) {
            wrap_plane || return [];
            col %= board[row].len;
        }

        if (!wrap_plane && (row<0 || col<0)) {
            return [];
        }
        output.append(\board[row][col]);
    }

    return output;
}

func up(board, x, y) {
    pointers(board, x, y, [
                 '[+0, +0]',
        [+1, -1], [+1, +0], [+1, +1],
                  [+2, +0],
        [+3, -1], [+3, +0], [+3, +1],
    ])
}

func down(board, x, y) {
    pointers(board, x, y, [
        [-3, -1], [-3, +0], [-3, +1],
                  [-2, +0],
        [-1, -1], [-1, +0], [-1, +1],
                 '[+0, +0]',
    ])
}

func left(board, x, y) {
    pointers(board, x, y, [
                    [-1, +1],           [-1, +3],
        '[+0, +0]', [+0, +1], [+0, +2], [+0, +3],
                    [+1, +1],           [+1, +3],
    ])
}

func right(board, x, y) {
    pointers(board, x, y, [
        [-1, -3],           [-1, -1],
        [+0, -3], [+0, -2], [+0, -1], '[+0, +0]',
        [+1, -3],           [+1, -1],
    ])
}

func assign(change=false, plane=[], data=[]) {

    plane.len == 0 ->
        && return false;

    change || (
        plane.each { |c|
            *c == nil || return false;
        }
    );

    plane.range.each { |i|
        *plane[i] = data[i];
    }

    return true;
}

func print_ascii_table {
    var table = asciitable.new(Hash(headingText => "#{pkgname} #{version}"))

    table.setCols(' ', (1..BOARD_SIZE)...)

    var char = 'a'
    info_board.each { |row|
        table.addRow([char++, row...])
        table.addRowLine()
    }

    var t = table.drawit

    if (defined(ANSI) && use_colors) {
        t.gsub!(hit_char, ANSI.colored(hit_char, 'bold red'))
        t.gsub!(miss_char, ANSI.colored(miss_char, 'yellow'))
        plane_chars.each {|c|
            t.gsub!(c, ANSI.colored(c, "bold green"))
        }
    }

    say t
}

var count = 0;
var max_tries = 1_000;
var directions = [up, down, left, right];

while (count != PLANES_NUM) {
    var x = play_board.end.irand;
    var y = play_board[0].end.irand;

    var rand = directions.end.irand;
    var code = directions[rand];

    if (--max_tries <= 0) {
        die "FATAL ERROR: try to increase the size of the grid (--size=x).\n"
    }

    assign(
           change: false,
           plane:  code.call(play_board, x, y),
           data:   parts.map {|c| "#{c}_#{rand}" },
    ) || next;

    count++;
}

## MAIN

var tries      = 0;
var start_time = Time.new.sec;

print_ascii_table();

while (count > 0) {

    var letter = letters.keys.rand;
    var number = irand(1, BOARD_SIZE);
    var text = "=>> Your guess (e.g.: #{letter}#{number})\n> ";

    var input = lc(Sys.scanln(text) \\ break);
    input ~~ ['q', 'quit'] && break;

    var m = input.match(/^\h*([a-z]+)\D*([0-9]+)/) || next;

    letters.has_key(m[0]) || next;

    var x = letters{m[0]};
    var y = m[1].to_num.dec;

    (y >= 0) && (y < BOARD_SIZE) || next;

    var point = play_board[x][y];

    if (point == nil) {
        info_board[x][y] = miss_char;
    }
    elsif (var m = point.match(/^head_(\d)$/i)) {
        var dir  = m[0].to_num;
        var item = plane_chars[(PLANES_NUM - count) % plane_chars.len];
        var code = directions[dir];

        [play_board, info_board].each { |board|
            assign(
                   change: true,
                   data:   [item]*8,
                   plane:  code.call($board, $x, $y),
            ) || die "#{__MAIN__}: unexpected error!"
        }

        count--;
    }
    elsif (point ~~ /^hit_\d$/i) {
        info_board[x][y] = hit_char;
    }

    tries++;
    print_ascii_table();
}

printf("** Info: %d tries in %d seconds\n", tries, Time.new.sec - start_time);

if (count == 0) {
    say "** Congratulations! All the planes are destroyed!";
}
