<?php

class Battlefield
{
    public $height;
    public $width;
    public $totalTurns;
    const EMPTYCELL = " . ";    //пустая ячейка на поле, не занятая живностью
    const TAKENCELL = " _ ";    //использую константу для недопущения спавна на одно место.


    public function __construct($height, $width, $totalTurns)
    {
        $this->height = $height;
        $this->width = $width;
        $this->totalTurns = $totalTurns;
    }

    //добавление новых животных в массив животных с рандомным указанием их стартовой позиции
    public $animalList = [];
    public function addAnimal(Animal $animal)
    {
        $this->animalList[] = $animal;
        $animal->setMap($this);
        for ($i = 0; $i <= 100; $i++) {
            $y = rand(0, $this->height);
            $x = rand(0, $this->width);
            if ($this->arrayField[$y][$x] == Battlefield::EMPTYCELL) {
                $animal->y = $y;
                $animal->x = $x;
                $this->arrayField[$y][$x] = Battlefield::TAKENCELL;
                break;
            }
        }
    }

    //удаляем животное из базы игры.
    public function removeAnimal(Animal $deadAnimal)
    {
        foreach($this->animalList as $key => $animal){
            if($animal === $deadAnimal) {
                unset($this->animalList[$key]);
            }
        }
    }

    //создание массива массивов - визуальное поле игровое, короче
    public $arrayField = [];
    public function setField()
    {
        for ($i = 0; $i <= $this->height; $i++) {
            for ($j = 0; $j <= $this->width; $j++) {
                $this->arrayField[$i][] = Battlefield::EMPTYCELL;
            }
        }
    }
    //заполняю поле живностью
    public function fillFieldWithAnimals()
    {
        foreach ($this->animalList as $animal) {
            $this->arrayField[$animal->y][$animal->x] = $animal->sign;
        }
    }
    //печать текущего состояния поля
    public function printField()
    {
        for ($i = 0; $i <= $this->height; $i++) {
            foreach ($this->arrayField[$i] as $place) {
                print $place;
            }
            print PHP_EOL;
        }
    }
    //возможность походить куда-либо, первый ход - остаться на месте, всегда доступен
    public function getPossibilityOfMovement(array $arrayMove)
    {
        foreach ($arrayMove as $i => $move) {
            if (isset($this->arrayField[$move->y][$move->x])) {
                if ($this->arrayField[$move->y][$move->x] != Battlefield::EMPTYCELL AND $i != 0) {
                    unset($arrayMove[$i]);
                }
            } else {
                unset($arrayMove[$i]);
            }
        }
        return $arrayMove;
    }

    //высчитать расстояние между двумя объектами
    public static function getDistance($y, $x, $y2, $x2)
    {
        $distanceY = abs($y - $y2);
        $distanceX = abs($x - $x2);
        if($distanceY>=$distanceX){
            return $distanceY;
        } else {
            return $distanceX;
        }
    }
    //механизм поедания мыши
    public function eating(Cat $cat, Mouse $mouse)
    {
        $mouse->map->arrayField[$mouse->y][$mouse->x] = $cat->altSign;
        $cat->map->arrayField[$cat->y][$cat->x] = Battlefield::EMPTYCELL;
        $cat->y = $mouse->y;
        $cat->x = $mouse->x;
        self::removeAnimal($mouse);
        $cat->isAsleep = TRUE;
        $cat->killed++;

    }

    public function findNearestAnimal(Animal $centreAnimal, $id)
    {
        $nearestCat = NULL;
        $nearestMouse = NULL;
        $nearestDog = NULL;
        $minDistance1 = INF;
        $minDistance2 = INF;
        $minDistance3 = INF;
        foreach ($this->animalList as $animal) {
            if ($animal instanceof Cat AND $animal != $centreAnimal) {
                $distance = self::getDistance($animal->y, $animal->x, $centreAnimal->y, $centreAnimal->x);
                if ($distance <= $centreAnimal->los AND $distance < $minDistance1) {
                    $minDistance1 = $distance;
                    $nearestCat = $animal;
                }
            }
            if($animal instanceof Mouse AND $animal != $centreAnimal) {
                $distance = self::getDistance($animal->y, $animal->x, $centreAnimal->y, $centreAnimal->x);
                if ($distance <= $centreAnimal->los AND $distance < $minDistance2) {
                    $minDistance2 = $distance;
                    $nearestMouse = $animal;
                }
            }
            if($animal instanceof Dog AND $animal != $centreAnimal) {
                $distance = self::getDistance($animal->y, $animal->x, $centreAnimal->y, $centreAnimal->x);
                if ($distance <= $centreAnimal->los AND $distance < $minDistance3) {
                    $minDistance3 = $distance;
                    $nearestDog = $animal;
                }
            }
        }

        switch($id):
            case 1:
                return $nearestMouse;
                break;
            case 2:
                return $nearestCat;
                break;
            case 3:
                return $nearestDog;
                break;
            default:
                return NULL;
                break;
            endswitch;
    }


    //измеряю расстояние до границ поля
    public function measureDistanceToEdges($y, $x)
    {
        $distanceY = 0;
        $distanceX = 0;
        if($y >= $this->height/2) {
            $distanceY = $this->height - $y;
        } else {
            $distanceY = $y - 1;
        }

        if($x >= $this->width/2) {
            $distanceX = $this->width - $x;
        } else {
            $distanceX = $x - 1;
        }

        $edgeDisntances = [
            "y" => $distanceY,
            "x" => $distanceX
        ];
        return $edgeDisntances;

    }

    //выбираем лучший ход, когда мышь почуяла кошку и пытается спастись.
    public function pickTheBestChoice(Mouse $mouse, Cat $cat, Movement $move)
    {
        $movePoints = 0;
        //смотрю, чтоб новая позиция увеличила расстояние меж мышью и котофеем
        //т.к. это оче важно даю 30 поинтов
        $currentDistance = self::getDistance($mouse->y, $mouse->x, $cat->y, $cat->x);
        $newDistance = self::getDistance($move->y, $move->x, $cat->y, $cat->x);
            if($newDistance > $currentDistance)  { $movePoints += 50; }
            if($newDistance < $currentDistance)  { $movePoints += 0; }

        //смотрю, чтоб новая позиция была подальше от краёв и углов.
        //если дальше, даю 10 баллов
        $currentEdgeDistance = self::measureDistanceToEdges($mouse->y, $mouse->x);
        $newEdgeDistance = self::measureDistanceToEdges($move->y, $move->x);
            if(array_sum($newEdgeDistance) > array_sum($currentEdgeDistance)) { $movePoints += 30; }

        //даю поинты за то, чтоб мышь шла наискось (т.к. стояние наискось может иметь то же расстояние,
        //что и на одной линии, но поймать сложнее. Иными словами, мышь ПЕТЛЯЕТ
            if($move->y != $cat->y AND $move->x != $cat->x)  { $movePoints += 15; }
        //даю оче много поинтов за то, чтоб прижаться к псу, ЕСЛИ он рядом.
        // НЕ задаю целью мышек бегать за псом.
        $nearestDog = $mouse->map->findNearestAnimal($mouse, 3);
        if($nearestDog != NULL) {
            $newDistanceToTheDog = self::getDistance($move->y, $move->x, $nearestDog->y, $nearestDog->x);
            if ($newDistanceToTheDog == 1) { $movePoints += 70; }
        }

        return $movePoints;
    }

    //выбираем лучший ход для кошки. Тут незамысловато:
    //дистанция должна сокращаться, отдаём предпочтение тому ходу, что
    //выведет нас на одну линию с добычей.
    //также боимся собаки, не встаём на ближайшую клетку с ней
    //
    //если расстояние = 1, то включается код ДЗЕРО - уничтожить мышь одним прыжком
    //и заканчиваем функцию.
    public function huntDown(Cat $cat, Mouse $mouse, Movement $move)
    {
        $movePoints = 0;
        $codeZero = -1;
        $nearestDog = $cat->map->findNearestAnimal($cat, 3);
        $distanceBetweenDogAndMouse = self::getDistance($mouse->y, $mouse->x, $nearestDog->y, $nearestDog->x);
        if($nearestDog != NULL){
            $newDistanceToTheDog = self::getDistance($move->y, $move->x, $nearestDog->y, $nearestDog->x);
        } else {
            $newDistanceToTheDog = INF;
        }
        $nearestFriendMouse = $mouse->map->findNearestAnimal($mouse, 1);
        if($nearestFriendMouse != NULL) {
            $distanceToFriendMouse = self::getDistance($nearestFriendMouse->y, $nearestFriendMouse->x, $mouse->y, $mouse->x );
        } else {
            $distanceToFriendMouse = INF;
        }
        $currentDistance = self::getDistance($cat->y, $cat->x, $mouse->y, $mouse->x);
        if($currentDistance == 1 AND $distanceBetweenDogAndMouse > 1 AND $distanceToFriendMouse != 1){
            return $codeZero;
        }
        $newDistance = self::getDistance($move->y, $move->x, $mouse->y, $mouse->x);
            if($newDistance < $currentDistance) { $movePoints += 40; }

            if($move->y == $mouse->y OR $move->x == $mouse->x) { $movePoints += 10; }
            if($newDistanceToTheDog == 1){ $movePoints -= 100; }

        return $movePoints;
    }


    public function letTheGameBegin()
    {
        $this->fillFieldWithAnimals();
        for ($i = 0; $i <= $this->totalTurns; $i++) {
            $this->printField();
            echo PHP_EOL;
            echo "Dogs 'team': " . PHP_EOL;
            foreach ($this->animalList as $animal) {
                if ($animal instanceof Dog) {
                    echo $animal . PHP_EOL .
                    $animal->go();
                }
            }

            echo PHP_EOL;
            echo "Mice team: " . PHP_EOL;
            foreach ($this->animalList as $animal) {
                if($animal instanceof Mouse){
                    echo $animal . PHP_EOL;
                    $animal->go();
                    $animal->turnsAlive++;
                }
            }
            echo PHP_EOL;
            echo "Cats team: " . PHP_EOL;
            foreach ($this->animalList as $animal) {
                if($animal instanceof Cat){
                    echo $animal;
                    if($animal->isAsleep == TRUE){
                        echo " ". $animal->name . " is sleeping at the moment." . PHP_EOL;
                    } else {
                        echo PHP_EOL;
                    }
                    $animal->go();

                }

            }
            echo PHP_EOL;
        }
    }

}

class Movement {
    public $x;
    public $y;
    public function __construct($y, $x){
        $this->y = $y;
        $this->x = $x;
    }
}

abstract class Animal {
    public $name;   //имя
    public $x;      //координата по ширине
    public $y;      //по высоте
    public $map;    //аналог объекта $field, только для животных
    public $sign;   //значок на карте
    public $los;    //обзор

    abstract public function makeBestMove(array $arrayMove);
    abstract public function go();
    abstract public function moves();

    public static function create(){

    }

    //для вывода статистики под полем игры
    public function __toString()
    {
        return substr($this->sign, 1, 1) .". ". $this->name
        . "'s position: " . "Y: ". $this->y . " X: ". $this->x . ".";
    }

    public function __construct($name)
    {
        $this->name = $name;
    }

    //добыть карту животного, то же, что и field, только относительно животного буду называть её map.
    //то есть $animal->map это та же хуйня, что и $field и можно обращаться к переменным класса Battlefield
    public function setMap(Battlefield $field)
    {
        $this->map = $field;
    }

    //совершить ход. Тут идёт работа с визуальным массивом игрового поля + обновляем свои координаты
    public function makeMove(Movement $move)
    {
        $this->map->arrayField[$this->y][$this->x] = Battlefield::EMPTYCELL;
        $this->map->arrayField[$move->y][$move->x] = $this->sign;
        $this->y = $move->y;
        $this->x = $move->x;

    }
}

class Mouse extends Animal {

    public $sign = " m ";
    //решил не задавать жёстко поле зрения, чтоб можно было каждой прописывать своё,
    ////делая "слеповатых" мышей

    public $turnsAlive = 0;

    public function __toString()
    {
        return parent::__toString() . " Alive for " . $this->turnsAlive . " turns.";
    }

    //конструктор, добавляю поле зрения
    public function __construct($name, $los)
    {
        parent::__construct($name);
        $this->los = $los;
    }

    //возможные базовые ходы
    public function moves()
    {
        $move1 = new Movement($this->y, $this->x);
        $move2 = new Movement($this->y + 1, $this->x);
        $move3 = new Movement($this->y - 1, $this->x);
        $move4 = new Movement($this->y, $this->x + 1);
        $move5 = new Movement($this->y, $this->x - 1);
        $arrayMove = array( $move1, $move2, $move3, $move4, $move5 );

        return $arrayMove; //на выходе массив с возможными ходами, не выходящими за рамки игрового поля
    }

    //обобщающая функция для передвижения, сделал для краткости
    //суть: мышь ходит так - через свою функцию makeBestMove, куда берёт аргументом
    //массив из всех ходов из функции moves()
    public function go()
    {
        $this->makeBestMove($this->moves());
    }

    //функция, которая решает, какой ход из всех доступных совершить
    public function makeBestMove(array $arrayMove)
    {
        //проверяю, вообще можно ли ходить туда-то, т.е. свободна ли ячейка.
        $arrayMove = $this->map->getPossibilityOfMovement($arrayMove);

        //ищу ближайшую кошку в обзоре, если её нет, то мышь спокойно идёт, куда хочет.
        //если есть в поле зрения котофей, то запускаю функцию по оценке следующего хода.
        $nearestCat = $this->map->findNearestAnimal($this, 2);
        switch($nearestCat):
            case NULL:
                $finalMove = $arrayMove[array_rand($arrayMove)];    //рандомный ход спокойной мыши
                break;
            case !NULL:
                $maxPoints = -INF;
                foreach($arrayMove as $move) {
                    $points = $this->map->pickTheBestChoice($this, $nearestCat, $move);
                    if($points > $maxPoints) {
                        $maxPoints = $points;
                        $finalMove = $move;
                        }
                    }
                break;
        endswitch;

        $this->makeMove($finalMove);
    }

}

class Cat extends Animal
{
    public $sign = " K ";
    public $altSign = " @ "; //альтернативный значок
    public $los = INF;
    public $isAsleep = FALSE;
    public $tireLevel = 0; //счётчики для сна
    public $daysSlept = 0;
    public $killed = 0;


    public function __toString()
    {
        return parent::__toString()  . "Has killed " . $this->killed . " mice.";
    }

    //обобщающая функция ходьбы для кота. Принцип тот же, что и у мыхана, только проверяем усталость
    public function go()
    {
        //пробуждаемся
        if($this->daysSlept == 1){
            $this->isAsleep = FALSE;
            $this->tireLevel = 0;
            $this->daysSlept = 0;
            $this->map->arrayField[$this->y][$this->x] = $this->sign;
        }
        //сам ход, если спим - считаем дни, если нет - ходим.
        if($this->isAsleep == TRUE){
            $this->tireLevel = 0;
            $this->daysSlept++;
            $this->map->arrayField[$this->y][$this->x] = $this->altSign;
        } else {
            $this->makeBestMove($this->moves());
            $this->tireLevel++;
        }
        //проверяем усталость, если 8, то ложимся спать
        if($this->tireLevel == 8){
            $this->isAsleep = TRUE;
            $this->tireLevel = 0;
            $this->map->arrayField[$this->y][$this->x] = $this->altSign;
        }
    }

    public function moves()
    {
        $move1 = new Movement($this->y, $this->x);
        $move2 = new Movement($this->y + 1, $this->x);
        $move3 = new Movement($this->y - 1, $this->x);
        $move4 = new Movement($this->y, $this->x + 1);
        $move5 = new Movement($this->y, $this->x - 1);
        $move6 = new Movement($this->y + 1, $this->x + 1);
        $move7 = new Movement($this->y - 1, $this->x - 1);
        $move8 = new Movement($this->y + 1, $this->x - 1);
        $move9 = new Movement($this->y - 1, $this->x + 1);
        $arrayMove = array( $move1, $move2, $move3, $move4, $move5, $move6, $move7, $move8, $move9 );

        return $arrayMove;
    }
    //выбираем лучший ход кота. Т.к. он видит всё, то ему проще. Проверяем, свободна ли клетка для ходьбы,
    //ищем ближайщего мыхана и бежим к нему.
    //если расстояние меж ними = 1, то жрём.
    public function makeBestMove(array $arrayMove)
    {
        $arrayMove = $this->map->getPossibilityOfMovement($arrayMove);

        $nearestMouse = $this->map->findNearestAnimal($this, 1);
        if($nearestMouse == NULL){
            $finalMove = $arrayMove[array_rand($arrayMove)];
        } else {
            $maxPoints = -INF;
            foreach ($arrayMove as $move) {
                $points = $this->map->huntDown($this, $nearestMouse, $move);
                if ($points == -1) {                                       //активируем код ДЗЕРО
                    $this->map->eating($this, $nearestMouse);
                    return;
                } elseif ($points > $maxPoints) {
                    $maxPoints = $points;
                    $finalMove = $move;
                }
            }

        }
        $this->makeMove($finalMove);
    }
}

class Dog extends Animal
{
    public $los = INF; //пусть будет
    public $sign = " D ";
    public $saying = [  "Just wandering around.",
                        "Being a dog.", "Has no idea what he's doing.",
                        "Being blind to mice problems.", "Just being a dog.",
                        "Chasing a bird in the sky", " ", " ", " ", " ",
                        "Barking at a tree.", "Has done nothing so far." ];

    public function __toString()
    {
        return parent::__toString() . " ".  $this->saying[array_rand($this->saying)];
    }

    public function go()
    {
        $this->makeBestMove($this->moves());
    }

    public function moves()
    {
        $move1 = new Movement($this->y, $this->x);
        $move2 = new Movement($this->y + 2, $this->x);
        $move3 = new Movement($this->y - 2, $this->x);
        $move4 = new Movement($this->y, $this->x + 2);
        $move5 = new Movement($this->y, $this->x - 2);
        $move6 = new Movement($this->y + 2, $this->x + 2);
        $move7 = new Movement($this->y - 2, $this->x - 2);
        $move8 = new Movement($this->y + 2, $this->x - 2);
        $move9 = new Movement($this->y - 2, $this->x + 2);
        $arrayMove = array( $move1, $move2, $move3, $move4, $move5, $move6, $move7, $move8, $move9 );

        return $arrayMove;
    }

    //лучший ход для пса - рандомный в свободную клетку. Если таковых нет - стоять на месте.
    public function makeBestMove(array $arrayMove)
    {
        $arrayMove = $this->map->getPossibilityOfMovement($arrayMove);
        if(count($arrayMove) != 1) {
            unset($arrayMove[0]);   //первый ход - стоять, тут его ансетаем, т.к. есть куда шагать
        }
        $finalMove = $arrayMove[array_rand($arrayMove)];

        $this->makeMove($finalMove);
    }
}

$field = new Battlefield(11,11, 30);
$field->setField();
$mouse1 = new Mouse("Dorothy", 4);
$mouse2 = new Mouse("Benedicta", 4);
$mouse3 = new Mouse("Roberta", 4);
$mouse5 = new Mouse("Stevie Wonder", 1);
$mouse5->sign = " s ";
$mouse6 = new Mouse("Gerthrude", 4);
$mouse7 = new Mouse("Gwyneth", 4);

$dog1 = new Dog("Stephan");

$cat1 = new Cat("Mew");
$cat2 = new Cat("Mr. Whiskers ");
$cat3 = new Cat("Alexander Anderson");

$field->addAnimal($mouse1);
$field->addAnimal($mouse2);
$field->addAnimal($mouse3);

$field->addAnimal($mouse5);
$field->addAnimal($mouse6);
$field->addAnimal($mouse7);
$field->addAnimal($dog1);
$field->addAnimal($cat1);
$field->addAnimal($cat2);
$field->addAnimal($cat3);

$field->letTheGameBegin();
