#!/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: 16 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.06

## 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{O}
var miss_char  = %q{`}

func print_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 print_version {
    print "#{pkgname} #{version}\n"
    Sys.exit
}

if (ARGV) {
    ARGV.getopt!(
         'board-size|size=i' => \BOARD_SIZE,
         'planes-num=i'      => \PLANES_NUM,
         'hit-char=s'        => \hit_char,
         'miss-char=s'       => \miss_char,
         'wrap!'             => \wrap_plane,
         'colors!'           => \use_colors,
         'help|h|?'          => print_usage,
         'version|v|V'       => print_version,
    )
}

## 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()
play_board.range.each { |i|
    static char = 'a'
    letters{char++} = i
}

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

func pointers(board, x, y, indices) {
    gather {
        [[0,0]] + indices -> each { |pair|

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

            if (wrap_plane) {
                row %= BOARD_SIZE   #=
                col %= BOARD_SIZE   #=
            }

            row.is_between(0, BOARD_SIZE-1) || return []
            col.is_between(0, BOARD_SIZE-1) || return []

            take(\board[row][col])
        }
    }
}

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 || 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)

    say "=>> Your guess (e.g.: #{letter}#{number})"

    var input = (Sys.scanln("> ") \\ break -> lc)
    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 = Num(m[1]).dec

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

    var point = play_board[x][y]

    if (point == nil) {
        info_board[x][y] = miss_char
    }
    elsif (point.match(/^head_(\d)$/i)) { |m|
        var dir  = Num(m[0])
        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!"
}
