<?php

error_reporting(-1);

const PLACEHOLDER = " .";
const CAT = " K";

/** Класс Игра */
class Game
{	
	/** @var Field игровое поле */
	private $field;
	/** @var array массив животных */
	private $animals;
	/** @var int кол-во ходов */
	private $numOfMoves;
	/**
	 * Конструктор класса Игра
	 * @param Animal $animals,... Объекты-животные через запятую
	 * @param Field $field Игровое поле
	 * @param int $numOfMoves сколько ходов нужно отобразить
	 */
	public function __construct(Field $field, int $numOfMoves, Animal ...$animals){
		$this->animals = $animals;
		$this->field = $field;
		$this->numOfMoves = $numOfMoves;
	}
	/** запустить игру */
	public function play(){
		$this->getStats();
		$this->field->displayField();
		echo "\n";
		for($i = 0; $i < $this->numOfMoves; $i++){
			foreach ($this->animals as $activeAnimal) {
				$result = $activeAnimal->go($this->field);
				if ($result > 0) {
					foreach ($this->animals as $key => $passiveAnimal) {
						if ($result == $passiveAnimal->getPosition() and $activeAnimal !== $passiveAnimal) {
							unset($this->animals[$key]);
						}
					}
				}
			}
			$this->getStats();
			$this->field->displayField();
			echo "\n";
		}
	}
	/** сколько зверей в игре */
	private function getStats(){
		$uniqNames = [];
		$stats = [];
		foreach ($this->animals as $animal) {
			$uniqNames[] = get_class($animal);
		}
		$uniqNames = array_unique($uniqNames);
		$stats = array_fill_keys($uniqNames, 0);
		foreach ($stats as $key => &$value) {
			foreach ($this->animals as $animal) {
				if ($key == get_class($animal)) {
					$value++;
				}
			}
		}
		unset($value);
		foreach ($stats as $key => $value) {
			echo "$key: $value ";
		}
		echo "\n";
	}

}

/** Класс Игровое поле */
class Field
{	
	/** @var int кол-во столбцов(ширина) */
	private $cols;
	/** @var array игровое поле */
	private $field;
	/**
	 * @param int $rows кол-во строк
	 * @param int $cols кол-во столбцов
	 * @param string $placeholder вид клеточки
	 */
	public function __construct(int $rows, int $cols, string $placeholder)
	{
		$this->field = array_fill(1, $rows * $cols, $placeholder);
		$this->rows = $rows;
		$this->cols = $cols;
	}
	/** отобразить поле */
	public function displayField(){
		$i = 1;
		foreach ($this->field as $value) {
			if ($i == $this->cols) {
				echo $value . "\n";
				$i = 1;
			} else {
				echo $value;
				$i++;
			}
		}		
	}

	/**
	 * вернуть значение позиции поля
	 * @param int $position координата
	 * @return string
	 */
	public function read(int $position):string {
		if ($position < 1 or $position > $this->getSize()) {
			throw new OutOfRangeException("Выход за пределы игрового поля!\n");
		} else {
			return $this->field[$position];
		}
	}

	/**
	 * записать значение в позицию
	 * @param int $position координата
	 * @param string $value значение
	 * @return string
	 */
	public function write(int $position, string $value) {
		if ($position < 1 or $position > $this->getSize()) {
			throw new OutOfRangeException("Выход за пределы игрового поля!\n");
		} else {
			$this->field[$position] = $value;
		}
	}

	/**
	 * получить ширину поля
	 * @return int
	 */
	public function getWidth():int {
		return $this->cols;
	}

	/**
	 * получить размер поля (кол-во клеток)
	 * @return int
	 */
	public function getSize():int {
		return count($this->field);
	}
}

/** Класс Животное */
abstract class Animal
{	
	/** варианты сторон для шага */
	const DOWN = 1;
	const UP = 2;
	const RIGHT = 3;
	const LEFT = 4;
	const NORTHWEST = 5;
	const NORTHEAST = 6;
	const SOUTHWEST = 7;
	const SOUTHEAST = 8;
	const STAND = 9;
	const ALL = 10;
	const AROUND = 11;

	/** @var int позиция на поле */
	protected $position;
	/** @var string символ животного */
	protected $symbol;

	public function __construct(string $symbol, Field $field)
	{
		$this->symbol = $symbol;
		$this->generatePosition($field);
	}

	/** Возвращает позицию */
	public function getPosition():int {
		return $this->position;
	}

	/**
	 * Генерирует начальную позицию
	 * @param Field $field Объект-Поле
	 */
	private function generatePosition(Field $field):void {
		$position = rand(1, $field->getSize());
		if (!($field->read($position) == PLACEHOLDER)) {
			$this->generatePosition($field);	
		} else {
			$field->write($position, $this->symbol);
			$this->position = $position;
		}
	}

	/**
	 * Проверяет cвободен ли путь, не занятали сама клеточка и клеточки в ее направлении
	 * @param callable $isMe калбек ф-я
	 * @param Field $field игровое поле
	 * @param int $move ход
	 * @param int $step длина шага
	 * @param int $way направление хода(если шаг больше 1)
	 * @return bool
	 */
	protected function isOccupied(callable $isMe, Field $field, int $move, int $step = 1, int $way = 0):bool {
			if ($isMe($field, $move)) {
				return true;
			} elseif ($step == 1 and $way == 0) {
				return false;
			} else {
				$position = $this->position;
				$stepsToGoal = --$step;
				for ($i =  1; $i <= $stepsToGoal; $i++) { 
					$result = $this->generateMoves($isMe, $field, $position, 1, $way);
					if ($result == false) {
						return true;
					} else {
						$position = $result[0];
					}		
				}
				return false;
			}
	}

	/** 
	 * Универсальная ф-я для ген-ии возможных ходов
	 * @param Field $field поле
	 * @param callable $isMe калбек ф-я определяет есть ли на клеточке сородич, принимает объект-поле и ход, возвращает true/false
	 * @param int $position позиция животного
	 * @param int $step величина шага
	 * @param int $ways,... направления для шага через запятую, принимает константы
	 * @return array
	 */
	protected function generateMoves(callable $isMe, Field $field, int $position, int $step, int ...$ways){
		if (count($ways) == 1) {
			if ($ways[0] == self::ALL) {
				$ways = [1, 2, 3, 4, 5, 6, 7, 8, 9];
			}
			if ($ways[0] == self::AROUND) {
				$ways = [1, 2, 3, 4, 5, 6, 7, 8];
			}
		}

		$moves = [];

		//если шаг = 1
		if ($step == 1) {
			foreach ($ways as $way) {
				if ($way == self::DOWN) {
					if (!($position + $field->getWidth() > $field->getSize())) { //Ход вниз
						$move = $position + $field->getWidth();
						if (!$this->isOccupied($isMe, $field, $move)) {
							$moves[] = $move;
						}	
					}		
				}
				if ($way == self::UP){
					if (!($position - $field->getWidth() < 1)) { //Ход вверх
						$move = $position - $field->getWidth();
						if (!$this->isOccupied($isMe, $field, $move)) {
							$moves[] = $move;
						}	
					}
				}
				if ($way == self::RIGHT){
					if (!(($position % $field->getWidth()) == 0)) { //Ход вправо
						$move = $position + $step;
						if (!$this->isOccupied($isMe, $field, $move)) {
							$moves[] = $move;
						}
					}
				}
				if ($way == self::LEFT){
					if (!((($position - 1) % $field->getWidth()) == 0)) { //Ход влево
						$move = $position - $step;
						if (!$this->isOccupied($isMe, $field, $move)) {
							$moves[] = $move;
						}
					}
				}
				if ($way == self::NORTHWEST){
					if (!(((($position - 1) % $field->getWidth()) == 0) or ($position - $field->getWidth() < 1))) { // диагональ влево вверх
						$move = $position - ($field->getWidth() + $step);
						if (!$this->isOccupied($isMe, $field, $move)) {
							$moves[] = $move;
						}
					}
				}
				if ($way == self::NORTHEAST){
					if (!((($position % $field->getWidth()) == 0) or ($position - $field->getWidth() < 1))) { //диагональ вправо вверх
						$move = $position - ($field->getWidth() - $step);
						if (!$this->isOccupied($isMe, $field, $move)) {
							$moves[] = $move;
						}
					}
				}
				if ($way == self::SOUTHWEST){
					if (!(((($position - 1) % $field->getWidth()) == 0) or ($position + $field->getWidth() > $field->getSize()))) { //диаг влево вниз
						$move = $position + ($field->getWidth() - $step);
						if (!$this->isOccupied($isMe, $field, $move)) {
							$moves[] = $move;
						}
					}
				}
				if ($way == self::SOUTHEAST){
					if (!((($position % $field->getWidth() == 0) or ($position + $field->getWidth() > $field->getSize())))) { //диаг вправо вниз
						$move = $position + ($field->getWidth() + $step);
						if (!$this->isOccupied($isMe, $field, $move)) {
							$moves[] = $move;
						}
					}
				}
				if ($way == self::STAND) {
					$moves[] = $position;
				}
			}
		} else { //если шаг больше 1
			foreach ($ways as $way) {
				if ($way == self::DOWN) {
					if (!($position + ($field->getWidth() * $step) > $field->getSize())) { //Ход вниз
						$move = $position + ($field->getWidth() * $step);
						if (!$this->isOccupied($isMe, $field,$move,$step, $way)) {
							$moves[] = $move;
						}	
					}
				}
				if ($way == self::UP){
					if (!($position - ($field->getWidth() * $step) < 1)) { //Ход вверх
						$move = $position - ($field->getWidth() * $step);
						if (!$this->isOccupied($isMe, $field, $move,$step, $way)) {
							$moves[] = $move;
						}	
					}
				}
				if ($way == self::RIGHT){
					if (!(($position % $field->getWidth()) == 0)) { //Ход вправо
						$partOfStep = $step;
						$partOfStep--;
						while ($partOfStep > 0) {
							if ((($position + $step) - $partOfStep) % $field->getWidth() == 0) {
								continue 2;
							} else {
								$partOfStep--;
							}
						}
						$move = $position + $step;
						if (!$this->isOccupied($isMe, $field, $move,$step, $way)) {
							$moves[] = $move;
						}
					}
				}
				if ($way == self::LEFT){
					if (!((($position - 1) % $field->getWidth()) == 0)) { //Ход влево
						$partOfStep = $step - 2;
						while ($partOfStep >= 0) {
							if ((($position - $step) + $partOfStep) % $field->getWidth() == 0) {
								continue 2;
							} else {
								$partOfStep--;
							}
						}
						$move = $position - $step;
						if (!$this->isOccupied($isMe, $field, $move,$step, $way)) {
							$moves[] = $move;
						}
					}
				}
				if ($way == self::NORTHWEST){
					if (!(((($position - 1) % $field->getWidth()) == 0) or ($position - ($field->getWidth() * $step) < 1))) { // диагональ влево вверх
						$partOfStep = $step - 2;
						while ($partOfStep >= 0) {
							if ((($position - $step) + $partOfStep) % $field->getWidth() == 0) {
								continue 2;
							} else {
								$partOfStep--;
							}
						}
						$move = $position - (($field->getWidth() * $step) + $step);
						if (!$this->isOccupied($isMe, $field, $move,$step, $way)) {
							$moves[] = $move;
						}
					}
				}
				if ($way == self::NORTHEAST){
					if (!((($position % $field->getWidth()) == 0) or ($position - ($field->getWidth() * $step) < 1))) { //диагональ вправо вверх
						$partOfStep = $step;
						$partOfStep--;
						while ($partOfStep > 0) {
							if ((($position + $step) - $partOfStep) % $field->getWidth() == 0) {
								continue 2;
							} else {
								$partOfStep--;
							}
						}
						$move = $position - (($field->getWidth() * $step) - $step);
						if (!$this->isOccupied($isMe, $field, $move,$step, $way)) {
							$moves[] = $move;
						}
					}
				}
				if ($way == self::SOUTHWEST){
					if (!(((($position - 1) % $field->getWidth()) == 0) or ($position + $field->getWidth() * $step > $field->getSize()))) { //диаг влево вниз
						$partOfStep = $step - 2;
						while ($partOfStep >= 0) {
							if ((($position - $step) + $partOfStep) % $field->getWidth() == 0) {
								continue 2;
							} else {
								$partOfStep--;
							}
						}
						$move = $position + (($field->getWidth() * $step) - $step);
						if (!$this->isOccupied($isMe, $field,$move,$step, $way)) {
							$moves[] = $move;
						}
					}
				}
				if ($way == self::SOUTHEAST){
					if (!((($position % $field->getWidth()) == 0) or ($position + $field->getWidth() * $step > $field->getSize()))) { //диаг вправо вниз
						$partOfStep = $step;
						$partOfStep--;
						while ($partOfStep > 0) {
							if ((($position + $step) - $partOfStep) % $field->getWidth() == 0) {
								continue 2;
							} else {
								$partOfStep--;
							}
						}
						$move = $position + (($field->getWidth() * $step) + $step);
						if (!$this->isOccupied($isMe, $field, $move,$step, $way)) {
							$moves[] = $move;
						}	
					}
				}
				if ($way == self::STAND) {
					$moves[] = $position;
				}
			}
		}
		if (count($moves) == 0) {
			return $moves;
		} else {
			return $moves;
		}
	}

	/**
	 * Метод для оценки ходов
	 * @param array $moves - массив ходов полученный функцией generateMoves()
	 * @return int ход с наибольшим весом
	 */
	abstract protected function rateMoves(Field $field, array $moves):int;

	/**
	 * Сделать ход
	 * Тут происходит вызов generateMoves() и rateMoves()
	 * @return int возвращаем 0 если ход обычный либо позицию съеденного животного
	 */
	abstract public function go(Field $field):int;
}

class Cat extends Animal
{	
	/** @var int кол-во ходов без отдыха */
	private $movesWithoutRest;
	/** @var int съеденные мышки */
	private $eatenMice;

	protected function rateMoves(Field $field, array $moves):int {
		$movesWithRate = [];
		foreach ($moves as $move) {
		$rate = 0;
			if (is_numeric(trim($field->read($move)))) { //клетка с мышкой
				$neighboringMice = 0;
				$aroundArea = $this->generateMoves(function($f,$m){return false;}, $field, $move, 1, self::AROUND); //берем клетки вокруг мыша
				foreach ($aroundArea as $cell) {
					if (is_numeric(trim($field->read($cell)))) {
						$neighboringMice++;
					}	
				}
				if ($neighboringMice >= 2) {//если рядом с мышкой еще две мышки 0 баллов
					$movesWithRate[] = [$rate,$move];		
				} else {
					$rate = 12; //иначе высокий балл
					$movesWithRate[] = [$rate,$move];
				}
			} elseif($field->read($move) == PLACEHOLDER) { //пустая клетка
				$rate++; //просто пустая клеточка 1 балл
				$mice = 0;
				$aroundArea = $this->generateMoves(function($f,$m){return false;}, $field, $move, 1, self::AROUND);//берем клетки вокруг клетки
				foreach ($aroundArea as $cell) {
					if (is_numeric(trim($field->read($cell)))) {
						$mice++; // по +1 за мышку рядом с пустой клеточкой
					}	
				}
				$rate += $mice;
				$movesWithRate[] = [$rate, $move];
			} else { //своя позиция
				if (empty($movesWithRate)) { //других ходов нет
					return $move;	
				} else { 
					$sumRate = 0;
					foreach ($movesWithRate as $moveWithRate) {//посчитаем сумму предыдущих ходов
						$sumRate += $moveWithRate[0];
					}
					if ($sumRate == 0) {
						return $move;
					} else { //если сумма больше 1 стоять не нужно
						$movesWithRate[] = [$rate, $move];
					}
				}
			}
		}
		$rates = array_column($movesWithRate, 0);
		$moves = array_column($movesWithRate, 1);
		array_multisort($rates, SORT_DESC, $moves);
		//Если есть несколько одинаковых крупнейших оценок выберем рандомно одну из них
		$largestRateKeys = array_keys($rates, $rates[0]);
		if ($largestRateKeys > 1) {
			$key = $largestRateKeys[rand(0, count($largestRateKeys) - 1)];
			return $moves[$key];
		} else {//Иначе самую крупную
			return $moves[0];
		}
	}

	public function go(Field $field):int {
		if ($this->eatenMice > 0) {
			$this->eatenMice = 0;
			$this->movesWithoutRest = 0;
			$field->write($this->position, " @");
			return 0;
		}
		if ($this->movesWithoutRest == 8) {
			$this->movesWithoutRest = 0;
			$field->write($this->position, " @");
			return 0;
		}
		$isMe = function(Field $field, int $move){
			if ($field->read($move) == $this->symbol or $field->read($move) == " @") {
				return true;
			} else {
				return false;
			}
		};
		$moves = $this->generateMoves($isMe, $field, $this->position, 1, self::ALL);
		$move = $this->rateMoves($field, $moves);
		if (is_numeric(trim($field->read($move)))) {
			$this->eatenMice++;
		}
		$field->write($this->position, PLACEHOLDER);
		$this->position = $move;
		$field->write($move, $this->symbol);
		$this->movesWithoutRest++;
		if ($this->eatenMice > 0) {
			return $move;
		} else {
			return 0;
		}
	}
}

class Mouse extends Animal
{
	protected function rateMoves(Field $field, array $moves):int {
		$cellsWithCats = [];
		$emptyCellsWithCats = [];
		$emptyCells = [];
		$myCell = [];
		foreach($moves as $move){
			$rate = 0;
			if (!is_numeric(trim($field->read($move))) and $field->read($move) != PLACEHOLDER) { // на клеточке враг 0 баллов
				$cellsWithCats[] = [$rate, $move];
			} elseif ($field->read($move) == PLACEHOLDER) { // клеточка пуста
				/* Посчитаем есть ли рядом с клеткой враги */
				if ($move == current($this->generateMoves(function($f,$m){return false;}, $field, $this->position, 1, self::UP))) {
					$neighboringCells = $this->generateMoves(function($f,$m){return false;}, $field, $move, 1, self::LEFT, self::RIGHT, self::SOUTHEAST, self::SOUTHWEST);
				} elseif($move == current($this->generateMoves(function($f,$m){return false;}, $field, $this->position, 1, self::DOWN))) {
					$neighboringCells = $this->generateMoves(function($f,$m){return false;}, $field, $move, 1, self::LEFT, self::RIGHT, self::NORTHEAST, self::NORTHWEST);	
				} elseif ($move == current($this->generateMoves(function($f,$m){return false;}, $field, $this->position, 1, self::LEFT))) {
					$neighboringCells = $this->generateMoves(function($f,$m){return false;}, $field, $move, 1, self::UP, self::DOWN, self::NORTHEAST, self::SOUTHEAST);
				} else {
					$neighboringCells = $this->generateMoves(function($f,$m){return false;}, $field, $move, 1, self::UP, self::DOWN, self::NORTHWEST, self::SOUTHWEST);
				}
				$cats = 0;
				foreach ($neighboringCells as $cell) {
					if (!is_numeric(trim($field->read($cell))) and $field->read($cell) != PLACEHOLDER) {
						$cats++;
					}
				}
				if ($cats > 0) { // враги рядом с клеткочкой есть - 1 балл
					$rate++;
					$emptyCellsWithCats[] = [$rate, $move];
				} else {
					$rate += 10; // путь свободен - 10 баллов
					$emptyCells[] = [$rate, $move];
				}
			} else {
				if (empty($emptyCells)) { // ходов на 10 баллов нет
					if (array_sum(array_column(array_merge($cellsWithCats, $emptyCellsWithCats), 0)) == 0) { // все пути закрыты, стоим
						return $move;
					} else { // иначе выбор между ходом в 1 балл и стоять
						$friends = 0;
						$aroundArea = $this->generateMoves(function($f,$m){return false;}, $field, $move, 1, self::AROUND);
						foreach ($aroundArea as $cell) {
							if (is_numeric(trim($field->read($cell)))) {
								$friends++;
							}
						}
						if ($friends >= 2) { // если рядом друзья - стоим
							return $move;
						}
						$myCell[] = [$rate, $move]; // иначе стоять оценим в 0 баллов
					}		
				} else {
					$myCell[] = [$rate, $move];
				}
			}
		}
		$movesWithRate = array_merge($cellsWithCats, $emptyCellsWithCats, $emptyCells, $myCell);
		$rates = array_column($movesWithRate, 0);
		$moves = array_column($movesWithRate, 1);
		array_multisort($rates, SORT_DESC, $moves);
		//Если есть несколько одинаковых крупнейших оценок выберем рандомно одну из них
		$largestRateKeys = array_keys($rates, $rates[0]);
		if ($largestRateKeys > 1) {
			$key = $largestRateKeys[rand(0, count($largestRateKeys) - 1)];
			return $moves[$key];
		} else {//Иначе самую крупную
			return $moves[0];
		}
	}

	public function go(Field $field):int {
		$isMe = function(Field $field, int $move){
			if (is_numeric(trim($field->read($move)))) {
				return true;
			} else {
				return false;
			}
		};
		$moves = $this->generateMoves($isMe, $field, $this->position, 1, self::UP, self::DOWN, self::LEFT, self::RIGHT, self::STAND);
		$move = $this->rateMoves($field, $moves);
		$field->write($this->position, PLACEHOLDER);
		$this->position = $move;
		$field->write($move, $this->symbol);
		return 0;
	}	
}


$field = new Field(10, 10, PLACEHOLDER);
$m = new Mouse(" 1", $field);
$m2 = new Mouse(" 2", $field);
$m3 = new Mouse(" 3", $field);
$c = new Cat(CAT, $field);
$c2 = new Cat(CAT, $field);
$c3 = new Cat(CAT, $field);
$game = new Game($field, 20, $m, $m2, $m3, $c, $c2, $c3);
$game->play();
