
#include "snake.h"

#include <QGraphicsPixmapItem>
#include <QGraphicsScene>

BodyPart::BodyPart( const unsigned x, const unsigned y, const Direction d, const Direction pd, QGraphicsPixmapItem*const img ) noexcept
    : x{ x }
    , y{ y }
    , direction{ d }
    , prev_direction{ pd }
    , image{ img }
{

}
BodyPart::BodyPart( BodyPart&& other ) noexcept
    : x{ other.x }
    , y{ other.y }
    , direction{ other.direction }
    , prev_direction{ other.prev_direction }
    , image{ other.image }
{
    other.image = nullptr;
}
BodyPart& BodyPart::operator=( BodyPart&& other ) noexcept
{
    if ( this == &other ) return *this;
    this->x = other.x;
    this->y = other.y;
    this->direction = other.direction;
    this->prev_direction = other.prev_direction;
    this->image = other.image;
    other.image = nullptr;
    return *this;
}
void BodyPart::update( const unsigned new_x, const unsigned new_y, const Direction& new_direction ) noexcept {
    this->x = new_x;
    this->y = new_y;
    this->image->setOffset( 16+(new_x*32), 16+(new_y*32) );
    this->prev_direction = this->direction;
    this->direction = new_direction;
}


Snake::Snake( const bool is_adversary ) noexcept
    : adversary( is_adversary )
{
    if ( is_adversary ) {
        // only AI needs to keep track of the field state
        this->field_map.fill( std::array<Tile, 16ul>{} );
    }
}


const QPixmap& Snake::getHeadImage() const noexcept
{
    if ( this->adversary ) {
        return this->img_snakeHead_;
    } else {
        return this->img_snakeHead;
    }
}


bool Snake::inTile( const unsigned x, const unsigned y, const bool avoid_tail ) const noexcept
{
    if ( this->size() > 0 ) {
        size_t i{ 0 };
        size_t max{ this->size()-1ul };
        if ( !avoid_tail ) {
            ++ max;
        }
        for ( auto bp{ this->cbegin() }; bp != this->cend(); ++bp ) {
            if ( bp->x == x && bp->y == y ) {
                return true;
            }
            ++i;
            if ( i >= max ) {
                break;
            }
        }
    }
    return false;
}


void Snake::setDirection( const Direction new_direction ) noexcept
{
    this->head_direction = new_direction;
}
const Direction& Snake::direction() const noexcept
{
    return this->head_direction;
}

void Snake::willGrow() noexcept
{
    this->will_grow |= true;
}
void Snake::grow( const bool is_borning )
{
    // build from the tail
    const BodyPart& tail{ this->back() };
    unsigned x{ tail.x };
    unsigned y{ tail.y };
    const Direction d{  tail.direction };
    const Direction ld{ tail.prev_direction };
    if ( is_borning ) {
        // one tile behind
        switch ( d ) {
            case Direction::UP:
                ++ y;
                break;
            case Direction::DOWN:
                -- y;
                break;
            case Direction::LEFT:
                ++ x;
                break;
            case Direction::RIGHT:
                -- x;
                break;
            default:
                // should be unreachable
                throw("Unexpected direction: "+std::to_string(d));
        }
    }
    this->emplace_back(
        x, y,
        d, ld,
        new QGraphicsPixmapItem( (this->adversary) ? this->img_snakeTail_ : this->img_snakeTail )
    );
    this->update( nullptr, true );
    this->back().update( x, y, d );
}


void Snake::update( QGraphicsScene* field_scene, const bool dry , const bool is_borning )
{
    // grow if planned
    if ( this->will_grow ) {
        this->will_grow &= false;
        this->grow( is_borning );
        field_scene->addItem( this->back().image );
    }
    // anyway, update the whole body
    size_t i{ 0 };
    const size_t max_i{ this->size()-1ul };
    unsigned new_x, prev_x, new_y, prev_y;
    Direction new_direction, prev_direction, prev_body_d;
    const QPixmap& head_img{  (this->adversary) ? this->img_snakeHead_  : this->img_snakeHead  };
    const QPixmap& body_img{  (this->adversary) ? this->img_snakeBody_  : this->img_snakeBody  };
    const QPixmap& curve_img{ (this->adversary) ? this->img_snakeCurve_ : this->img_snakeCurve };
    const QPixmap& tail_img{  (this->adversary) ? this->img_snakeTail_  : this->img_snakeTail  };
    for ( auto bp{ this->begin() }; bp != this->end(); ++bp ) {
        if ( ! dry ) {
            // future position
            if ( i == 0ul ) {
                // head doesn't follow any other part of the body
                switch ( this->head_direction ) {
                    case Direction::UP:
                        new_y = bp->y - 1u;
                        new_x = bp->x;
                        break;
                    case Direction::DOWN:
                        new_y = bp->y + 1u;
                        new_x = bp->x;
                        break;
                    case Direction::LEFT:
                        new_x = bp->x - 1u;
                        new_y = bp->y;
                        break;
                    case Direction::RIGHT:
                        new_x = bp->x + 1u;
                        new_y = bp->y;
                        break;
                    default:
                        // should be unreachable
                        throw("Unexpected direction: "+std::to_string(this->head_direction));
                }
                new_direction = this->head_direction;
            } else {
                // follow the previous part of the body
                new_x = prev_x;
                new_y = prev_y;
                new_direction = prev_direction;
            }
            // store for the next part
            prev_x = bp->x;
            prev_y = bp->y;
            prev_direction = bp->direction;

            // update the body-part position
            bp->update( new_x, new_y, new_direction );
        }

        // finally set the image to be shown
        switch ( bp->direction ) {

            case Direction::UP:
                if ( i == 0ul ) {
                    bp->image->setPixmap(
                        head_img );
                } else if ( i == max_i ) {
                    switch ( prev_body_d ) {
                        case Direction::UP:
                            bp->image->setPixmap(
                                tail_img );
                            break;
                        case Direction::LEFT:
                            bp->image->setPixmap(
                                tail_img.transformed(
                                    QTransform().rotate( -90.0 ) ) );
                            break;
                        case Direction::RIGHT:
                            bp->image->setPixmap(
                                tail_img.transformed(
                                    QTransform().rotate( 90.0 ) ) );
                            break;
                        default:
                            // should be unreachable
                            throw("Unexpected direction: "+std::to_string(prev_body_d));
                    }
                } else {
                    switch ( prev_body_d ) {
                        case Direction::UP:
                            bp->image->setPixmap(
                                body_img );
                            break;
                        case Direction::LEFT:
                            bp->image->setPixmap(
                                curve_img.transformed(
                                    QTransform().rotate( 90.0 ) ) );
                            break;
                        case Direction::RIGHT:
                            bp->image->setPixmap(
                                curve_img );
                            break;
                        default:
                            // should be unreachable
                            throw("Unexpected direction: "+std::to_string(prev_body_d));
                    }
                }
                break;

            case Direction::DOWN:
                if ( i == 0ul ) {
                    bp->image->setPixmap(
                        head_img.transformed(
                            QTransform().rotate( 180.0 ) ) );
                } else if ( i == max_i ) {
                    switch ( prev_body_d ) {
                        case Direction::DOWN:
                            bp->image->setPixmap(
                                tail_img.transformed(
                                    QTransform().rotate( 180.0 ) ) );
                            break;
                        case Direction::LEFT:
                            bp->image->setPixmap(
                                tail_img.transformed(
                                    QTransform().rotate( -90.0 ) ) );
                            break;
                        case Direction::RIGHT:
                            bp->image->setPixmap(
                                tail_img.transformed(
                                    QTransform().rotate( 90.0 ) ) );
                            break;
                        default:
                            // should be unreachable
                            throw("Unexpected direction: "+std::to_string(prev_body_d));
                    }
                } else {
                    switch ( prev_body_d ) {
                        case Direction::DOWN:
                            bp->image->setPixmap(
                                body_img );
                            break;
                        case Direction::LEFT:
                            bp->image->setPixmap(
                                curve_img.transformed(
                                    QTransform().rotate( 180.0 ) ) );
                            break;
                        case Direction::RIGHT:
                            bp->image->setPixmap(
                                curve_img.transformed(
                                    QTransform().rotate( -90.0 ) ) );
                            break;
                        default:
                            // should be unreachable
                            throw("Unexpected direction: "+std::to_string(prev_body_d));
                    }
                }
                break;

            case Direction::LEFT:
                if ( i == 0ul ) {
                    bp->image->setPixmap(
                        head_img.transformed(
                            QTransform().rotate( -90.0 ) ) );
                } else if ( i == max_i ) {
                    switch ( prev_body_d ) {
                        case Direction::LEFT:
                            bp->image->setPixmap(
                                tail_img.transformed(
                                    QTransform().rotate( -90.0 ) ) );
                            break;
                        case Direction::UP:
                            bp->image->setPixmap(
                                tail_img );
                            break;
                        case Direction::DOWN:
                            bp->image->setPixmap(
                                tail_img.transformed(
                                    QTransform().rotate( 180.0 ) ) );
                            break;
                        default:
                            // should be unreachable
                            throw("Unexpected direction: "+std::to_string(prev_body_d));
                    }
                } else {
                    switch ( prev_body_d ) {
                        case Direction::LEFT:
                            bp->image->setPixmap(
                                body_img.transformed(
                                    QTransform().rotate( -90.0 ) ) );
                            break;
                        case Direction::UP:
                            bp->image->setPixmap(
                                curve_img.transformed(
                                    QTransform().rotate( -90.0 ) ) );
                            break;
                        case Direction::DOWN:
                            bp->image->setPixmap(
                                curve_img );
                            break;
                        default:
                            // should be unreachable
                            throw("Unexpected direction: "+std::to_string(prev_body_d));
                    }
                }
                break;

            case Direction::RIGHT:
                if ( i == 0ul ) {
                    bp->image->setPixmap(
                        head_img.transformed(
                            QTransform().rotate( 90.0 ) ) );
                } else if ( i == max_i ) {
                    switch ( prev_body_d ) {
                        case Direction::RIGHT:
                            bp->image->setPixmap(
                                tail_img.transformed(
                                    QTransform().rotate( 90.0 ) ) );
                            break;
                        case Direction::UP:
                            bp->image->setPixmap(
                                tail_img );
                            break;
                        case Direction::DOWN:
                            bp->image->setPixmap(
                                tail_img.transformed(
                                    QTransform().rotate( 180.0 ) ) );
                            break;
                        default:
                            // should be unreachable
                            throw("Unexpected direction: "+std::to_string(prev_body_d));
                    }
                } else {
                    switch ( prev_body_d ) {
                        case Direction::RIGHT:
                            bp->image->setPixmap(
                                body_img.transformed(
                                    QTransform().rotate( 90.0 ) ) );
                            break;
                        case Direction::UP:
                            bp->image->setPixmap(
                                curve_img.transformed(
                                    QTransform().rotate( 180.0 ) ) );
                            break;
                        case Direction::DOWN:
                            bp->image->setPixmap(
                                curve_img.transformed(
                                    QTransform().rotate( 90.0 ) ) );
                            break;
                        default:
                            // should be unreachable
                            throw("Unexpected direction: "+std::to_string(prev_body_d));
                    }
                }
                break;

            default:
                // should be unreachable
                throw("Unexpected direction: "+std::to_string(bp->direction));
        }
        prev_body_d = bp->direction;
        ++i;
    }
}


void Snake::move( const Snake& adv_snake, const unsigned food_x, const unsigned food_y )
{
    const std::array<Direction,4> classes{
        Direction::UP,
        Direction::DOWN,
        Direction::LEFT,
        Direction::RIGHT,
    };

    std::array<std::array<float,7>,4> dataset{{
        {0.f,0.f,0.f,0.f,0.f,0.f,0.f},
        {0.f,0.f,0.f,0.f,0.f,0.f,0.f},
        {0.f,0.f,0.f,0.f,0.f,0.f,0.f},
        {0.f,0.f,0.f,0.f,0.f,0.f,0.f}
    }};

    const std::array<float,7> weights{
        -1.0f,   // blocked way
        -0.4f,   // dead way
         0.003f, // dead way steps
         0.1f,   // food way
         0.4f,   // aggressive way
         0.01f,  // same direction
        -1.0f    // opposite direction
    };

    this->updateFieldMap( adv_snake, food_x, food_y );

    for ( size_t i{0}; i<4ul; ++i ) {
        this->collectData( dataset.at(i), classes.at(i), adv_snake, food_x, food_y );
    }

    // decide
    this->head_direction = this->predictDirection( dataset, weights, classes );
}


Direction Snake::predictDirection( const std::array<std::array<float,7>,4>& data, const std::array<float,7>& weights, const std::array<Direction,4>& classes ) const noexcept
{
    float results[]{ 1.f, 1.f, 1.f, 1.f };
    bool keep_current{ false };
    Direction class_label{ this->head_direction };

    // process data
    for ( size_t i{0}; i<4ul; ++i ) {
        const std::array<float, 7>& d = data.at(i);
        float& r = results[i];
        for ( size_t j{0}; j<7ul; ++j ) {
            r += d.at(j) * weights.at(j);
        }
    }

    // normalize results (not really a need here...)
    float min{10.f}, max{-10.f};
    for ( const float& r : results ) {
        if ( r < min ) {
            min = r;
        }
        if ( r > max ) {
            max = r;
        }
    }
    if ( max != min ) {
        for ( size_t i{0}; i<4ul; ++i ) {
            results[i] = (results[i]-min) / (max-min);
        }
    } else {
        keep_current |= true;
        for ( size_t i{0}; i<4ul; ++i ) {
            results[i] = 0.f;
        }
    }

    // choose the best result
    if ( ! keep_current ) {
        max = 0.f;
        for ( size_t i{0}; i<4ul; ++i ) {
            if ( results[i] > max ) {
                class_label = classes.at(i);
                max = results[i];
            } else if ( results[i] == max ) {
                if ( rand()%2 ) {
                    class_label = classes.at(i);
                }
            }
        }
    }

    return class_label;
}


void Snake::collectData( std::array<float,7>& data, const Direction& direction, const Snake& adv_snake, const unsigned food_x, const unsigned food_y ) const
{
    unsigned
        blocked_way        { 0 },
        dead_way           { 0 },
        dead_way_steps     { 0 },
        food_way           { 0 },
        aggressive_way     { 0 },
        same_direction     { 0 },
        opposite_direction { 0 };

    const unsigned head_x{ this->front().x };
    const unsigned head_y{ this->front().y };

    unsigned x, y;

    // avoid choosing the opposite direction
    switch ( direction ) {

        case Direction::UP:
            x = head_x;
            y = head_y-1u;
            if ( this->head_direction == Direction::UP ) {
                same_direction = 1u;
            } else if ( this->head_direction == Direction::DOWN ) {
                opposite_direction = 1u;
            }
            if ( head_y == 0u ) {
                blocked_way = 1u;
            }
            break;

        case Direction::DOWN:
            x = head_x;
            y = head_y+1u;
            if ( this->head_direction == Direction::DOWN ) {
                same_direction = 1u;
            } else if ( this->head_direction == Direction::UP ) {
                opposite_direction = 1u;
            }
            if ( head_y == 15u ) {
                blocked_way = 1u;
            }
            break;

        case Direction::LEFT:
            x = head_x-1u;
            y = head_y;
            if ( this->head_direction == Direction::LEFT ) {
                same_direction = 1u;
            } else if ( this->head_direction == Direction::RIGHT ) {
                opposite_direction = 1u;
            }
            if ( head_x == 0u ) {
                blocked_way = 1u;
            }
            break;

        case Direction::RIGHT:
            x = head_x+1u;
            y = head_y;
            if ( this->head_direction == Direction::RIGHT ) {
                same_direction = 1u;
            } else if ( this->head_direction == Direction::LEFT ) {
                opposite_direction = 1u;
            }
            if ( head_x == 15u ) {
                blocked_way = 1u;
            }
            break;

        default:
            // should be unreachable
            throw("Unexpected direction: "+std::to_string(this->head_direction));
    }


    if ( !(blocked_way || opposite_direction) ) {

        // check snakes
        switch ( this->field_map.at( x ).at( y ).entity ) {
            case Entity::S:
            case Entity::A:
                blocked_way = 1u;
                break;
            default:
                break;
        }

        if ( ! blocked_way ) {

            // check for deadhole
            dead_way_steps = this->isDeadHole( x, y, direction );
            if ( dead_way_steps > 0u ) {
                dead_way = 1u;
            }

            // check for aggressivity purposes
            switch ( direction ) {

                case Direction::UP:
                    if ( adv_snake.direction() == Direction::LEFT && this->head_direction == Direction::RIGHT ) {
                        if ( this->inTileAdv( x+2u, y+1u ) ) {
                            aggressive_way = 1u;
                        }
                    } else if ( adv_snake.direction() == Direction::RIGHT && this->head_direction == Direction::LEFT ) {
                        if ( this->inTileAdv( x-2u, y+1u ) ) {
                            aggressive_way = 1u;
                        }
                    } else if ( adv_snake.direction() == this->head_direction ) {
                        if ( this->inTileAdv( x+2u, y )
                          || this->inTileAdv( x-2u, y ) ) {
                            if ( rand()%this->aggressiveness == 0 ) {
                                aggressive_way = 1u;
                            }
                        }
                    }
                    break;

                case Direction::DOWN:
                    if ( adv_snake.direction() == Direction::LEFT && this->head_direction == Direction::RIGHT ) {
                        if ( this->inTileAdv( x+2u, y-1u ) ) {
                            aggressive_way = 1u;
                        }
                    } else if ( adv_snake.direction() == Direction::RIGHT && this->head_direction == Direction::LEFT ) {
                        if ( this->inTileAdv( x-2u, y-1u ) ) {
                            aggressive_way = 1u;
                        }
                    } else if ( adv_snake.direction() == this->head_direction ) {
                        if ( this->inTileAdv( x+2u, y )
                          || this->inTileAdv( x-2u, y ) ) {
                            if ( rand()%this->aggressiveness == 0 ) {
                                aggressive_way = 1u;
                            }
                        }
                    }
                    break;

                case Direction::LEFT:
                    if ( adv_snake.direction() == Direction::UP && this->head_direction == Direction::DOWN ) {
                        if ( this->inTileAdv( x+1u, y+2u ) ) {
                            aggressive_way = 1u;
                        }
                    } else if ( adv_snake.direction() == Direction::DOWN && this->head_direction == Direction::UP ) {
                        if ( this->inTileAdv( x+1u, y-2u ) ) {
                            aggressive_way = 1u;
                        }
                    } else if ( adv_snake.direction() == this->head_direction ) {
                        if ( this->inTileAdv( x, y+2u )
                          || this->inTileAdv( x, y-2u ) ) {
                            if ( rand()%this->aggressiveness == 0 ) {
                                aggressive_way = 1u;
                            }
                        }
                    }
                    break;

                case Direction::RIGHT:
                    if ( adv_snake.direction() == Direction::UP && this->head_direction == Direction::DOWN ) {
                        if ( this->inTileAdv( x-1u, y+2u ) ) {
                            aggressive_way = 1u;
                        }
                    } else if ( adv_snake.direction() == Direction::DOWN && this->head_direction == Direction::UP ) {
                        if ( this->inTileAdv( x-1u, y-2u ) ) {
                            aggressive_way = 1u;
                        }
                    } else if ( adv_snake.direction() == this->head_direction ) {
                        if ( this->inTileAdv( x, y+2u )
                          || this->inTileAdv( x, y-2u ) ) {
                            if ( rand()%this->aggressiveness == 0 ) {
                                aggressive_way = 1u;
                            }
                        }
                    }
                    break;

                default:
                    // should be unreachable
                    throw("Unexpected direction: "+std::to_string(this->head_direction));
            }
        }
    }


    if ( !(blocked_way || dead_way) ) {

        // check for food
        switch ( direction ) {

            case Direction::UP:
                if ( food_y < head_y ) {
                    if ( ! opposite_direction ) {
                        food_way = 1u;
                    }
                } else if ( food_y == head_y ) {
                    if ( food_x > head_x && this->head_direction == Direction::LEFT ) {
                        food_way = 1u;
                    } else if ( food_x < head_x && this->head_direction == Direction::RIGHT ) {
                        food_way = 1u;
                    }
                }
                break;

            case Direction::DOWN:
                if ( food_y > head_y ) {
                    if ( ! opposite_direction ) {
                        food_way = 1u;
                    }
                } else if ( food_y == head_y ) {
                    if ( food_x > head_x && this->head_direction == Direction::LEFT ) {
                        food_way = 1u;
                    } else if ( food_x < head_x && this->head_direction == Direction::RIGHT ) {
                        food_way = 1u;
                    }
                }
                break;

            case Direction::LEFT:
                if ( food_x < head_x ) {
                    if ( ! opposite_direction ) {
                        food_way = 1u;
                    }
                } else if ( food_x == head_x ) {
                    if ( food_y > head_y && this->head_direction == Direction::UP ) {
                        food_way = 1u;
                    } else if ( food_y < head_y && this->head_direction == Direction::DOWN ) {
                        food_way = 1u;
                    }
                }
                break;

            case Direction::RIGHT:
                if ( food_x > head_x ) {
                    if ( ! opposite_direction ) {
                        food_way = 1u;
                    }
                } else if ( food_x == head_x ) {
                    if ( food_y > head_y && this->head_direction == Direction::UP ) {
                        food_way = 1u;
                    } else if ( food_y < head_y && this->head_direction == Direction::DOWN ) {
                        food_way = 1u;
                    }
                }
                break;

            default:
                // should be unreachable
                throw("Unexpected direction: "+std::to_string(this->head_direction));
        }
    }


    // update data
    data.at(0) = blocked_way;
    data.at(1) = dead_way;
    data.at(2) = dead_way_steps;
    data.at(3) = food_way;
    data.at(4) = aggressive_way;
    data.at(5) = same_direction;
    data.at(6) = opposite_direction;
}


void Snake::updateFieldMap( const Snake& adv_snake, const unsigned& food_x, const unsigned& food_y ) noexcept
{
    // reset to default state
    for ( size_t x{0}; x<16ul; ++x ) {
        for ( size_t y{0}; y<16ul; ++y ) {
            Tile& t = this->field_map.at(x).at(y);
            t.entity = Entity::N;
            t.s_index = 0u;
        }
    }
    // update food position
    this->field_map.at(food_x).at(food_y).entity = Entity::F;
    // update self
    unsigned i{ static_cast<unsigned>( this->size() ) };
    for ( auto bp{ this->cbegin() }; bp != this->cend(); ++bp ) {
        Tile& t = this->field_map.at(bp->x).at(bp->y);
        t.entity = Entity::S;
        t.s_index = i;
        --i;
    }
    // update adversary
    i = static_cast<unsigned>( adv_snake.size() );
    for ( const auto& bp : adv_snake ) {
        Tile& t = this->field_map.at(bp.x).at(bp.y);
        t.entity = Entity::A;
        t.s_index = i;
        --i;
    }
}


bool Snake::inTileAdv(const unsigned x, const unsigned y ) const noexcept
{
    if ( x < 16 && y < 16 ) {
        switch ( this->field_map.at(x).at(y).entity ) {
            case Entity::A:
                return true;
            default:
                return false;
        }
    }
    return false;
}

bool Snake::inTileMinusSteps(const unsigned x, const unsigned y, const unsigned steps ) const noexcept
{
    switch ( this->field_map.at(x).at(y).entity ) {
        case Entity::S:
        case Entity::A:
            if ( this->field_map.at(x).at(y).s_index > steps ) {
                return true;
            }
            return false;
        default:
            return false;
    }
}


std::array<unsigned,8> Snake::checkAround( const Direction& direction, const unsigned x, const unsigned y ) const
{
    std::array<unsigned,8> around{
        0u, 0u, 0u,
        0u,     0u,
        0u, 0u, 0u,
    };

    std::array<int,8> x_pattern, y_pattern;

    switch ( direction ) {
        case Direction::UP:
            x_pattern = { -1,  0,  1,
                          -1,      1,
                          -1,  0,  1 };
            y_pattern = { -1, -1, -1,
                           0,      0,
                           1,  1,  1 };
            break;
        case Direction::DOWN:
            x_pattern = {  1,  0, -1,
                           1,     -1,
                           1,  0, -1 };
            y_pattern = {  1,  1,  1,
                           0,      0,
                          -1, -1, -1 };
            break;
        case Direction::LEFT:
            x_pattern = { -1, -1, -1,
                           0,      0,
                           1,  1,  1 };
            y_pattern = {  1,  0, -1,
                           1,     -1,
                           1,  0, -1 };
            break;
        case Direction::RIGHT:
            x_pattern = {  1,  1,  1,
                           0,      0,
                          -1, -1, -1 };
            y_pattern = { -1,  0,  1,
                          -1,      1,
                          -1,  0,  1 };
            break;
        default:
            // should be unreachable
            throw("Unexpected direction: "+std::to_string(direction));
    }

    unsigned x_, y_;
    for ( size_t i{0}; i<8ul; ++i ) {
        x_  = x;
        x_ += x_pattern.at(i);
        y_  = y;
        y_ += y_pattern.at(i);
        if ( x_ > 15 || y_ > 15 ) {
            around.at(i) = 1u;
        } else {
            switch ( this->field_map.at( x_ ).at( y_ ).entity ) {
                case Entity::S:
                case Entity::A:
                    around.at(i) = 2u;
                    break;
                default:
                    break;
            }
        }
    }

    return around;
}


unsigned Snake::isDeadHole( const unsigned start_x, const unsigned start_y, const Direction& start_direction ) const
{
    bool result{false}, check{false}, check_clockwise{false};
    Direction direction{ start_direction };
    unsigned steps{ 1 };

    const auto blocked_around{ this->checkAround( direction, start_x, start_y ) };
    if ( (blocked_around.at(3)>0u && blocked_around.at(4)>0u)
      && (blocked_around.at(3)>1u || blocked_around.at(4)>1u) ) {
        check |= true;
    } else if ( (blocked_around.at(3)>0u && blocked_around.at(7)>0u)
             && (blocked_around.at(3)>1u || blocked_around.at(7)>1u) ) {
        check |= true;
    } else if ( (blocked_around.at(4)>0u && blocked_around.at(5)>0u)
             && (blocked_around.at(4)>1u || blocked_around.at(5)>1u) ) {
        check |= true;
        check_clockwise |= true;
    } else if ( (blocked_around.at(5)>0u && blocked_around.at(7)>0u)
             && (blocked_around.at(5)>1u || blocked_around.at(7)>1u) ) {
        check |= true;
    } else if ( blocked_around.at(0)>0u && blocked_around.at(1)>0u && blocked_around.at(2)>0u ) {
        check |= true;
    }


    if ( check ) {

        int front_step, front_check, side_check;
        unsigned side, front, aux_side, aux_front;
        const unsigned max_steps{ static_cast<unsigned>(this->size()-1ul) };
        std::vector<std::tuple<unsigned, unsigned>> tried_positions{
            std::make_tuple( start_x, start_y )
        };


        const auto change_direction = [&] (const bool clockwise)
        {
            switch ( direction ) {
                case Direction::UP:
                    if ( clockwise ) {
                        direction   = Direction::RIGHT;
                        front_check = +1;
                        side_check  = (check_clockwise) ? +1 : -1;
                    } else {
                        direction   = Direction::LEFT;
                        front_check = -1;
                        side_check  = (check_clockwise) ? -1 : +1;
                    }
                    break;
                case Direction::DOWN:
                    if ( clockwise ) {
                        direction   = Direction::LEFT;
                        front_check = -1;
                        side_check  = (check_clockwise) ? -1 : +1;
                    } else {
                        direction   = Direction::RIGHT;
                        front_check = +1;
                        side_check  = (check_clockwise) ? +1 : -1;
                    }
                    break;
                case Direction::LEFT:
                    if ( clockwise ) {
                        direction   = Direction::UP;
                        front_check = -1;
                        side_check  = (check_clockwise) ? +1 : -1;
                    } else {
                        direction   = Direction::DOWN;
                        front_check = +1;
                        side_check  = (check_clockwise) ? -1 : +1;
                    }
                    break;
                case Direction::RIGHT:
                    if ( clockwise ) {
                        direction   = Direction::DOWN;
                        front_check = +1;
                        side_check  = (check_clockwise) ? -1 : +1;
                    } else {
                        direction   = Direction::UP;
                        front_check = -1;
                        side_check  = (check_clockwise) ? +1 : -1;
                    }
                    break;
                default:
                    // should be unreachable
                    throw("Unexpected direction: "+std::to_string(direction));
            }
            // swap front/side
            std::swap( front, side );
            front_step = front_check;
        };


        const auto tried_already = [&] (const unsigned side_, const unsigned front_, const bool update) -> bool
        {
            bool tried{ false };
            unsigned x, y;
            switch ( direction ) {
                case Direction::UP:
                case Direction::DOWN:
                    x = side_;
                    y = front_;
                    break;
                case Direction::LEFT:
                case Direction::RIGHT:
                    y = side_;
                    x = front_;
                    break;
                default:
                    // should be unreachable
                    throw("Unexpected direction: "+std::to_string(direction));
            }
            for ( const auto& position : tried_positions ) {
                if ( std::get<0>(position) == x ) {
                    if ( std::get<1>(position) == y ) {
                        tried |= true;
                        break;
                    }
                }
            }
            if ( update ) {
                tried_positions.push_back( std::make_tuple( x, y ) );
            }
            return tried;
        };


        const auto tile_blocked = [&] (const unsigned side_, const unsigned front_) -> bool
        {
            bool blocked{ false };
            unsigned x, y, x_, y_;
            switch ( direction ) {
                case Direction::UP:
                case Direction::DOWN:
                    x = side_;
                    y = front_;
                    x_ = side;
                    y_ = front;
                    break;
                case Direction::LEFT:
                case Direction::RIGHT:
                    y = side_;
                    x = front_;
                    y_ = side;
                    x_ = front;
                    break;
                default:
                    // should be unreachable
                    throw("Unexpected direction: "+std::to_string(direction));
            }
            if ( x > 15 || y > 15 ) {
                blocked |= true;
            } else if ( this->inTileMinusSteps( x, y, steps ) ) {
                blocked |= true;
            } else if ( tried_already( side_, front_, false ) ) {
                if ( this->field_map.at( x_ ).at( y_ ).entity == Entity::S ) {
                    blocked |= true;
                }
            }
            return blocked;
        };


        const auto check_deadhole = [&] ()
        {
            switch ( start_direction ) {
                case Direction::UP:
                    front = start_y;
                    side  = start_x;
                    front_check = -1;
                    side_check  = (check_clockwise) ? +1 : -1;
                    break;
                case Direction::DOWN:
                    front = start_y;
                    side  = start_x;
                    front_check = +1;
                    side_check  = (check_clockwise) ? -1 : +1;
                    break;
                case Direction::LEFT:
                    front = start_x;
                    side  = start_y;
                    front_check = -1;
                    side_check  = (check_clockwise) ? -1 : +1;
                    break;
                case Direction::RIGHT:
                    front = start_x;
                    side  = start_y;
                    front_check = +1;
                    side_check  = (check_clockwise) ? +1 : -1;
                    break;
                default:
                    // should be unreachable
                    throw("Unexpected direction: "+std::to_string(start_direction));
            }
            front_step = front_check;

            while ( true ) {
                // check the side
                aux_side  = side;
                aux_side += side_check;
                if ( !tile_blocked( aux_side, front ) ) {
                    // side is free, check for another deadhole
                    aux_front  = front;
                    aux_front += front_check;
                    unsigned aux_front_  = front;
                    aux_front_ += front_check*(-2);
                    if ( tile_blocked( side, aux_front ) && tile_blocked( aux_side, aux_front_ ) ) {
                        // may-be deadhole at side, check the opposite
                        aux_side  = side;
                        aux_side += (side_check*(-1));
                        if ( !tile_blocked( aux_side, front ) ) {
                            // opposite side free
                            change_direction( !check_clockwise );
                        } else {
                            // opposite side blocked too
                            change_direction( check_clockwise );
                        }
                    } else {
                        // turn at side
                        change_direction( check_clockwise );
                    }
                } else {
                    // side blocked, check front
                    aux_front  = front;
                    aux_front += front_check;
                    if ( tile_blocked( side, aux_front ) ) {
                        // front blocked, check opposite side
                        aux_side  = side;
                        aux_side += (side_check*(-1));
                        if ( !tile_blocked( aux_side, front ) ) {
                            // opposite side free
                            change_direction( !check_clockwise );
                        } else {
                            // opposite side blocked too
                            result |= true;
                            break;
                        }
                    }
                }

                front += front_step;

                if ( tried_already( side, front, true ) ) {
                    result = true;
                    break;
                } else {
                    ++ steps;
                    if ( steps >= max_steps ) {
                        break;
                    }
                }
            }
        };


        // start checking
        bool aux_result{ false };
        unsigned aux_steps{0}, i{0};
        while ( true ) {
            check_deadhole();
            if ( !result ) {
                steps = 0;
            }
            ++ i;
            if ( i == 2u ) {
                break;
            }
            aux_result = result;
            result &= false;
            aux_steps = steps;
            steps = 1u;
            check_clockwise = !check_clockwise;
            direction = start_direction;
            tried_positions.clear();
        }

        // mean result
        result |= aux_result;
        steps = (steps>aux_steps) ? steps : aux_steps;
    }

    if ( !result ) {
        steps = 0u;
    }
    return steps;
}
