From 141475577bd311fd0f4e279977f1ac8902ad4a6b Mon Sep 17 00:00:00 2001 From: Darius Kasperavicius Date: Fri, 6 Jun 2014 04:52:57 +0300 Subject: [PATCH 1/2] Add dardar player --- src/XO/Player/Dardar/ActionsInterface.php | 21 + src/XO/Player/Dardar/Move/Move.php | 60 +++ src/XO/Player/Dardar/Move/MoveInterface.php | 10 + .../Dardar/Move/MoveNotFoundException.php | 10 + src/XO/Player/Dardar/Move/NullMove.php | 12 + src/XO/Player/Dardar/Situation.php | 336 +++++++++++++ src/XO/Player/Dardar/SituationCounter.php | 388 +++++++++++++++ .../Dardar/Specials/AbstractSpecial.php | 42 ++ src/XO/Player/Dardar/Specials/CenterMove.php | 18 + .../Player/Dardar/Specials/CornerAttack.php | 22 + src/XO/Player/Dardar/Specials/CrossAttack.php | 42 ++ .../Player/Dardar/Specials/CrossDefence.php | 28 ++ src/XO/Player/Dardar/Specials/EdgeAttack.php | 49 ++ .../Player/Dardar/Specials/PandoraAttack.php | 44 ++ .../Dardar/Specials/SpecialsInterface.php | 24 + .../Dardar/Strategy/AbstractStrategy.php | 127 +++++ .../Player/Dardar/Strategy/AttackStrategy.php | 45 ++ .../Dardar/Strategy/DefenceStrategy.php | 49 ++ .../Dardar/Strategy/SpecialMoveFinder.php | 53 ++ .../Dardar/Strategy/StrategyInterface.php | 7 + .../Dardar/Strategy/StrategySelector.php | 43 ++ src/XO/Player/Dardar/Utils.php | 13 + src/XO/Player/DardarPlayer.php | 30 ++ src/XO/Service/PlayerRegistry.php | 1 + .../Dardar/Specials/CrossAttackTest.php | 99 ++++ .../Player/Dardar/Specials/EdgeAttackTest.php | 48 ++ .../Dardar/Specials/PandoraAttackTest.php | 55 +++ .../Player/Dardar/Strategy/SituationTest.php | 456 ++++++++++++++++++ tests/XO/Player/DardarPlayerTest.php | 30 ++ 29 files changed, 2162 insertions(+) create mode 100644 src/XO/Player/Dardar/ActionsInterface.php create mode 100644 src/XO/Player/Dardar/Move/Move.php create mode 100644 src/XO/Player/Dardar/Move/MoveInterface.php create mode 100644 src/XO/Player/Dardar/Move/MoveNotFoundException.php create mode 100644 src/XO/Player/Dardar/Move/NullMove.php create mode 100644 src/XO/Player/Dardar/Situation.php create mode 100644 src/XO/Player/Dardar/SituationCounter.php create mode 100644 src/XO/Player/Dardar/Specials/AbstractSpecial.php create mode 100644 src/XO/Player/Dardar/Specials/CenterMove.php create mode 100644 src/XO/Player/Dardar/Specials/CornerAttack.php create mode 100644 src/XO/Player/Dardar/Specials/CrossAttack.php create mode 100644 src/XO/Player/Dardar/Specials/CrossDefence.php create mode 100644 src/XO/Player/Dardar/Specials/EdgeAttack.php create mode 100644 src/XO/Player/Dardar/Specials/PandoraAttack.php create mode 100644 src/XO/Player/Dardar/Specials/SpecialsInterface.php create mode 100644 src/XO/Player/Dardar/Strategy/AbstractStrategy.php create mode 100644 src/XO/Player/Dardar/Strategy/AttackStrategy.php create mode 100644 src/XO/Player/Dardar/Strategy/DefenceStrategy.php create mode 100644 src/XO/Player/Dardar/Strategy/SpecialMoveFinder.php create mode 100644 src/XO/Player/Dardar/Strategy/StrategyInterface.php create mode 100644 src/XO/Player/Dardar/Strategy/StrategySelector.php create mode 100644 src/XO/Player/Dardar/Utils.php create mode 100644 src/XO/Player/DardarPlayer.php create mode 100644 tests/XO/Player/Dardar/Specials/CrossAttackTest.php create mode 100644 tests/XO/Player/Dardar/Specials/EdgeAttackTest.php create mode 100644 tests/XO/Player/Dardar/Specials/PandoraAttackTest.php create mode 100644 tests/XO/Player/Dardar/Strategy/SituationTest.php create mode 100644 tests/XO/Player/DardarPlayerTest.php diff --git a/src/XO/Player/Dardar/ActionsInterface.php b/src/XO/Player/Dardar/ActionsInterface.php new file mode 100644 index 0000000..6f0ab04 --- /dev/null +++ b/src/XO/Player/Dardar/ActionsInterface.php @@ -0,0 +1,21 @@ +setX($x); + $this->setY($y); + } + + /** + * @return array + */ + public function getMove() + { + return array($this->x, $this->y); + } + + /** + * Returns a natural array with not swapped values + * @return array + */ + public function getNaturalMove() + { + return array($this->y, $this->x); + } + + /** + * @param mixed $x + * + * @throws \InvalidArgumentException + */ + public function setX($x) + { + if (!is_int($x)) { + throw new \InvalidArgumentException('X cannot be set! ' . $x); + } + $this->x = $x; + } + + + /** + * @param $y + * + * @throws \InvalidArgumentException + */ + public function setY($y) + { + if (!is_int($y)) { + throw new \InvalidArgumentException('Y cannot be set! ' . $y); + } + $this->y = $y; + } +} diff --git a/src/XO/Player/Dardar/Move/MoveInterface.php b/src/XO/Player/Dardar/Move/MoveInterface.php new file mode 100644 index 0000000..cbcc0ff --- /dev/null +++ b/src/XO/Player/Dardar/Move/MoveInterface.php @@ -0,0 +1,10 @@ +getRowLine(1); + $x = $this->findInLine($this->enemySymbol, $line); + return new Move(1, $x); + } + + public function isCrossAttack() + { + return is_bool($this->findInCrosses('isCrossAttackPattern')); + } + + /** + * @param $attack + * + * @return array + */ + public function addCorners(array $attack) + { + $corners = $this->addShuffledCorners(); + return array_merge($attack, $corners); + } + + public function addShuffledCorners() + { + $corners = $this->getCornerMoves(); + //find weak spots + shuffle($corners); + return $corners; + } + + protected function getCornerMoves() + { + return $this->createMovesFromCoords($this->getCorners()); + } + + public function getAllNeigbourgCoords(Move $move) + { + + list($x, $y) = $move->getMove(); + + $moves[] = [abs($x - 1), $y]; + $moves[] = [$x, abs($y - 1)]; + + if (!in_array($move, $this->getCorners())) { + $moves[] = [abs($x + 1), $y]; + $moves[] = [$x, abs($y + 1)]; + + } + + return $moves; + } + + public function findEmptySpaceNear(Move $targetMove) + { + + $coordanates = $this->getAllNeigbourgCoords($targetMove); + + $moves = $this->createMovesFromCoords($coordanates); + + foreach ($moves as $move) { + if ($this->isPossibleTurn($move)) { + Utils::log('Found empty space at' . var_export($move, true)); + return $move; + } + } + + return new NullMove(); + } + + public function moveIsSecond() + { + return $this->countMoves() == 2; + } + + + public function findCrossWithPattern($array) + { + return $this->findInCrosses('hasPattern', $array); + } + + /** + * @return bool + */ + public function isCornerAttackNeeded() + { + return $this->countMoves() == 1 || $this->countMoves() == 3; + } + + /** + * @param $attacker + * @param $defencer + * + * @return bool + */ + public function isPandoraMove($attacker, $defencer) + { + $moves = $this->countMoves() == 3; + return $moves && $this->isPandoraMovePattern($attacker, $defencer); + } + + /** + * @param $attacker + * @param $defender + * + * @return bool + */ + public function isPandoraMovePattern($attacker, $defender) + { + $linePattern = [null, $defender, $attacker]; + Utils::log($linePattern); + $row = $this->getRowLine(1); + $column = $this->getColumnLine(1); + + return $this->hasPattern($row, $linePattern) + && $this->hasPattern($column, $linePattern); + } + + /** + * @return bool + */ + public function isPandoraAttackable() + { + $linePattern = [null, $this->he(), $this->me()]; + Utils::log($linePattern); + $row = $this->getRowLine(1); + $column = $this->getColumnLine(1); + + return $this->hasPattern($row, $linePattern) || $this->hasPattern($column, $linePattern); + } + + public function isPandoraMistake() + { + $pattern1 = [$this->me(), $this->he(), $this->he()]; + $pattern2 = [null, $this->he(), $this->me()]; + $row = $this->getRowLine(1); + $column = $this->getColumnLine(1); + + return ($this->hasPattern($row, $pattern1) && $this->hasPattern($column, $pattern2)) || + ($this->hasPattern($column, $pattern1) && $this->hasPattern($row, $pattern2)); + } + + public function attackEdge() + { + $pattern = [null, $this->he(), null]; + $row = $this->getRowLine(1); + $column = $this->getColumnLine(1); + + if ($this->hasPattern($row, $pattern)) { + $x = $this->findInLine(null, $row); + return new Move(0, 1); + } + + if ($this->hasPattern($column, $pattern)) { + $y = $this->findInLine(null, $column); + return new Move(1, 0); + } + + } + + public function getDefendOrKillMove() + { + try { + $move = $this->findDefendRowMove(); + return $move; + + } catch (MoveNotFoundException $e) { + + } + + try { + $move = $this->findDefendColumnMove(); + return $move; + + } catch (MoveNotFoundException $e) { + + } + + try { + $move = $this->findDefendCrossMove(); + return $move; + + } catch (MoveNotFoundException $e) { + + } + + return new NullMove(); + } + + public function isMove($move) + { + if ($move instanceof NullMove) { + throw new MoveNotFoundException('This move is not possible'); + } + return true; + } + + public function invertSymbols($inverted = null) + { + $this->setEnemySymbol($this->invertSymbol($this->enemySymbol)); + $this->setMySymbol($this->invertSymbol($this->mySymbol)); + if (isset($inverted)) { + $this->inverted = $inverted; + } + } + + /** + * @throws MoveNotFoundException + * @return Move + */ + protected function findDefendColumnMove() + { + foreach ([0, 1, 2] as $index) { + $line = $this->getColumnLine($index); + if ($this->isDefendable($line)) { + $y = $this->findInLine(null, $line); + return new Move($index, $y); + } + } + + throw new MoveNotFoundException(); + } + + /** + * @throws MoveNotFoundException + * @return Move + */ + protected function findDefendRowMove() + { + foreach ([0, 1, 2] as $index) { + $line = $this->getRowLine($index); + if ($this->isDefendable($line)) { + $x = $this->findInLine(null, $line); + return new Move($x, $index); + } + } + + throw new MoveNotFoundException(); + } + + /** + * @return Move + * @throws MoveNotFoundException + */ + public function findDefendCrossMove() + { + $crossType = $this->findDefendedCross(); + + if (isset($crossType)) { + $cross = $this->getCrossLine($crossType); + + return $this->createMove($this->getSymbolCoordsInCross($cross, $crossType, null)); + } + + throw new MoveNotFoundException(); + } + + /** + * Create move from array + * + * @param array $array + * @return Move + */ + public function createMove(array $array) + { + list($x, $y) = $array; + return new Move($x, $y); + } + + /** + * @param $moves + * @param $action + * + * @return mixed + */ + public function getPosssibleMove($moves, $action = false) + { + foreach ($moves as $move) { + if ($this->isPossibleTurn($move)) { + if ($action) { + Utils::log($action); + } + return $move; + } + } + return new NullMove(); + } + + /** + * @param $move + * + * @return bool + */ + public function isPossibleTurn(MoveInterface $move) + { + try { + return $this->isCoordinatedEmpty($move->getMove()); + + } catch (MoveNotFoundException $e) { + return false; + } + } + + + public function createMovesFromCoords(array $array) + { + $movesArray = []; + foreach ($array as $coordinates) { + $movesArray[] = $this->createMove($coordinates); + } + return $movesArray; + } + + + +} + diff --git a/src/XO/Player/Dardar/SituationCounter.php b/src/XO/Player/Dardar/SituationCounter.php new file mode 100644 index 0000000..1605ba6 --- /dev/null +++ b/src/XO/Player/Dardar/SituationCounter.php @@ -0,0 +1,388 @@ +setSymbols($symbol); + $this->table = $table; + } + + + /** + * Sets symbols by my symbol + * @param $symbol - My symbol + */ + public function setSymbols($symbol) + { + $this->setMySymbol($symbol); + $this->setEnemySymbol($this->invertSymbol($symbol)); + } + + protected function invertSymbol($symbol) + { + return $symbol == PlayerInterface::SYMBOL_X + ? PlayerInterface::SYMBOL_O : PlayerInterface::SYMBOL_X; + } + + protected function setEnemySymbol($symbol) + { + $this->enemySymbol = $symbol; + } + + protected function setMySymbol($symbol) + { + $this->mySymbol = $symbol; + } + + protected function getTableHelper() + { + return new TableHelper($this->getTable()); + } + + /** + * @return mixed + */ + public function getTable() + { + return $this->table; + } + + protected function getPossibleMoves() + { + return $this->getTableHelper()->getPossibleMoves(); + } + + /** + * @param $index + * + * @return array + */ + public function getRowLine($index) + { + $items = $this->getTableHelper()->getRow($index); + return $items; + } + + public function getCrossLine($rtl) + { + $items = $this->getTableHelper()->getCross($rtl); + + if ($rtl) { + $items = array_reverse($items); + } + + return $items; + } + + /** + * Return cross bool $rtl on success, null on fail + * Callbacks will return booleans! + * @param $callback + * @return boolean|null Returns cross type on success, null on fail + */ + protected function findInCrosses($callback) + { + $args = func_get_args(); + + foreach ([false, true] as $crossRtl) { + $cross = $this->getCrossLine($crossRtl); + if (method_exists($this, $callback)) { + if (func_num_args() > 1) { + + $args[0] = $cross; + if (call_user_func_array(array($this, $callback), $args)) { + return $crossRtl; + } + } + + if ($this->$callback($cross)) { + return $crossRtl; + } + } + } + } + + protected function findDefendedCross() + { + return $this->findInCrosses('isDefendable'); + } + + public function findInLine($symbol, $line) + { + return array_search($symbol, $line, true); + } + + public function hasPattern($line, $pattern) + { + return $line == $pattern || $line == array_reverse($pattern); + } + + protected function isDefendable($line) + { + return $this->countBy($line, self::HE) == 2 && $this->countBy($line, self::EMPTIES) == 1; + } + + public function getCorners() + { + return array( + [0, 0], + [0, 2], + [2, 0], + [2, 2] + ); + } + + /** + * @param $index + * + * @return array + */ + public function getColumnLine($index) + { + $items = $this->getTableHelper()->getColumn($index); + return $items; + } + + public function countMoves() + { + return 9 - count($this->getPossibleMoves()); + } + + /** + * Shortcut public method for count with filter + * + * @param $array + * @param $type + * + * @return int + * @throws \InvalidArgumentException + */ + public function countBy($array, $type) + { + switch ($type) { + case (self::ME): + $filtered = $this->countWithFilter($array, 'filterMyField'); + break; + case (self::HE): + $filtered = $this->countWithFilter($array, 'filterEnemyField'); + break; + case (self::EMPTIES): + $filtered = $this->countWithFilter($array, 'filterEmpty'); + break; + case (self::NONEMPTY): + $filtered = $this->countWithFilter($array, 'filterNonEmpty'); + break; + default: + throw new \InvalidArgumentException('No valid type set'); + } + + return $filtered; + } + + public function isCrossAttackPattern($line) + { + $pattern = [$this->he(), $this->me(), $this->he()]; + return $this->hasPattern($line, $pattern); + } + + public function isCrossAttackable($line) + { + $pattern = [$this->me(), $this->he(), null]; + return $this->hasPattern($line, $pattern); + } + + protected function isCornerAttackable($line) + { + $pattern = [null, $this->me(), $this->he()]; + return $this->hasPattern($line, $pattern); + } + + public function findCornerAttackableCross() + { + return $this->findInCrosses('isCornerAttackable'); + } + + public function findCrosAttackableCross() + { + return $this->findInCrosses('isCrossAttackable'); + } + + + public function findSymbolCoordsInCross($line, $type, $symbol) + { + $x = array_search($symbol, $line); + $y = $this->getCrossY($line, $type, $symbol); + + if (is_int($x) && is_int($y)) { + return [$x, $y]; + } + } + + protected function filterEmpty($field) + { + return $field === null; + } + + protected function filterNonEmpty($field) + { + return $field !== null; + } + + protected function filterEnemyField($field) + { + return $field == $this->enemySymbol; + } + + protected function filterMyField($field) + { + return $field == $this->mySymbol; + } + + protected function countWithFilter($array, $filter) + { + if (!is_array($array)) { + throw new \InvalidArgumentException('Count with filter needs array'); + } + + return count( + array_filter( + $array, + array($this, $filter) + ) + ); + } + + public function countTableMovesBy($symbol) + { + $out = $this->findAllSymbolCoords($symbol); + return count($out); + } + + public function getSymbolCoordsInCross($line, $type, $symbol) + { + $x = array_search($symbol, $line); + $y = $this->getCrossY($line, $type, $symbol); + + if (is_int($x) && is_int($y)) { + return [$x, $y]; + } + } + + protected function getCrossY($cross, $crossRtl, $symbol = null) + { + if ($crossRtl === true) { + $cross = array_reverse($cross); + } + + return array_search($symbol, $cross); + } + + /** + * @param $symbol + * + * @return array + */ + private function findAllSymbolCoords($symbol) + { + $out = []; + + foreach ($this->table as $row => $data) { + foreach ($data as $col => $value) { + if ($value === $symbol) { + $out[] = [$row, $col]; + } + } + } + return $out; + } + + public function isCoordsNeighbours($move1, $move2) + { + if (!is_array($move1) || !is_array($move2)) { + throw new \InvalidArgumentException( + 'Not valid moves received ' + . "\n" . var_export($move1, true) + . "\n" . var_export($move2, true) + ); + } + + list($x1, $y1) = $move1; + list($x2, $y2) = $move2; + + if ($x1 == $x2) { + return $this->areValuesNear($y1, $y2); + } + + if ($y1 == $y2) { + return $this->areValuesNear($x1, $x2); + } + return false; + } + + + public function areValuesNear($x, $y) + { + return ($x + $y) % 2 == 1; + } + + public function randomMove() + { + $moves = $this->getPossibleMoves(); + shuffle($moves); + return $moves[0]; + } + + public function me() + { + return $this->mySymbol; + } + + public function he() + { + return $this->enemySymbol; + } + + public function getInfo() + { + return "\nhero . $this->mySymbol, enemy $this->enemySymbol .\n" + . json_encode($this->getTable()); + } + + + public function isCoordinatedEmpty($coordinates) + { + list($x, $y) = $coordinates; + + $table = $this->getTable(); + if (isset($table[$x][$y])) { + + return false; + } else { + return true; + } + } +} diff --git a/src/XO/Player/Dardar/Specials/AbstractSpecial.php b/src/XO/Player/Dardar/Specials/AbstractSpecial.php new file mode 100644 index 0000000..4ac9db8 --- /dev/null +++ b/src/XO/Player/Dardar/Specials/AbstractSpecial.php @@ -0,0 +1,42 @@ +situation = $situation; + return $this; + } + + public function setSkipped($skipped) + { + $this->skipped = $skipped; + return $this; + } + + public function isSkipped() + { + return $this->skipped >= rand(1, 100); + } + + protected function isFirstNotSkipped() + { + return $this->situation->countMoves() == 0 && !$this->isSkipped(); + } +} \ No newline at end of file diff --git a/src/XO/Player/Dardar/Specials/CenterMove.php b/src/XO/Player/Dardar/Specials/CenterMove.php new file mode 100644 index 0000000..64e122c --- /dev/null +++ b/src/XO/Player/Dardar/Specials/CenterMove.php @@ -0,0 +1,18 @@ +situation->countMoves() == 0 || $this->situation->countMoves() == 1; + } + + public function findMove() + { + return new Move(1, 1); + } +} diff --git a/src/XO/Player/Dardar/Specials/CornerAttack.php b/src/XO/Player/Dardar/Specials/CornerAttack.php new file mode 100644 index 0000000..b579ae2 --- /dev/null +++ b/src/XO/Player/Dardar/Specials/CornerAttack.php @@ -0,0 +1,22 @@ +situation->countMoves() == 2; + } + + public function findMove() + { + $type = $this->situation->findCornerAttackableCross(); + $line = $this->situation->getCrossLine($type); + return $this->situation->createMove( + $this->situation->findSymbolCoordsInCross($line, $type, null) + ); + } +} diff --git a/src/XO/Player/Dardar/Specials/CrossAttack.php b/src/XO/Player/Dardar/Specials/CrossAttack.php new file mode 100644 index 0000000..18826e8 --- /dev/null +++ b/src/XO/Player/Dardar/Specials/CrossAttack.php @@ -0,0 +1,42 @@ +isFirstNotSkipped() || $this->situation->countMoves() == 2; + } + + /** + * Get Move + * @return Move + */ + public function findMove() + { + if ($this->situation->countMoves() == 0) { + return new Move(0, 0); + } + + return $this->moveSecondCrossAttack(); + } + + public function moveSecondCrossAttack() + { + $rtl = $this->situation->findCrosAttackableCross(); + $line = $this->situation->getCrossLine($rtl); + return $this->situation->createMove( + $this->situation->findSymbolCoordsInCross($line, $rtl, null) + ); + } + +} diff --git a/src/XO/Player/Dardar/Specials/CrossDefence.php b/src/XO/Player/Dardar/Specials/CrossDefence.php new file mode 100644 index 0000000..1e8702b --- /dev/null +++ b/src/XO/Player/Dardar/Specials/CrossDefence.php @@ -0,0 +1,28 @@ +situation->countMoves() == 3 && $this->situation->isCrossAttack(); + } + + /** + * Get Move + * @return Move + */ + public function findMove() + { + return new Move(0, 1); + } +} diff --git a/src/XO/Player/Dardar/Specials/EdgeAttack.php b/src/XO/Player/Dardar/Specials/EdgeAttack.php new file mode 100644 index 0000000..30a749a --- /dev/null +++ b/src/XO/Player/Dardar/Specials/EdgeAttack.php @@ -0,0 +1,49 @@ +hasOpponnetMovedEdge() && $this->situation->moveIsSecond(); + } + + public function findMove() + { + list($x, $y) = $this->edgeMove; + if ($x == 0 || $y == 0) { + return new Move(2, 2); + } + if ($x == 2 || $y == 2) { + return new Move(0, 0); + } + + throw new MoveNotFoundException(); + } + + /** + * Sorry for dirty OOP + * @return bool + */ + public function hasOpponnetMovedEdge() + { + foreach ($this->situation->getTable() as $row => $data) { + foreach ($data as $col => $value) { + if ($value === $this->situation->he() && ($row == 1 || $col == 1)) { + $this->edgeMove = [$col, $row]; + return true; + } + } + } + return false; + } +} + diff --git a/src/XO/Player/Dardar/Specials/PandoraAttack.php b/src/XO/Player/Dardar/Specials/PandoraAttack.php new file mode 100644 index 0000000..d247c5b --- /dev/null +++ b/src/XO/Player/Dardar/Specials/PandoraAttack.php @@ -0,0 +1,44 @@ +isFirstNotSkipped() + || ($this->situation->countMoves() == 2 && $this->situation->isPandoraAttackable() + || ($this->situation->countMoves() == 4 && $this->situation->isPandoraMistake())); + } + + /** + * Get Move + * @return Move + */ + public function findMove() + { + switch ($this->situation->countMoves()) { + case (0): + return new Move(0, 1); + break; + case (2): + return $this->situation->attackEdge(); + break; + } + + return new Move(0, 0); + + } + + +} diff --git a/src/XO/Player/Dardar/Specials/SpecialsInterface.php b/src/XO/Player/Dardar/Specials/SpecialsInterface.php new file mode 100644 index 0000000..b2b7cad --- /dev/null +++ b/src/XO/Player/Dardar/Specials/SpecialsInterface.php @@ -0,0 +1,24 @@ +situation = $situation; + } + + /** + * Create map for action order + * @return array + */ + public function strategyActions() + { + return array( + ActionsInterface::KILL, + ActionsInterface::DEFEND, + ActionsInterface::ATTACK, + ActionsInterface::RANDOM + ); + } + + public function getTurn() + { + foreach ($this->strategyActions() as $action) { + try { + return $this->$action()->getMove(); + } catch (MoveNotFoundException $e) { + + } + } + throw new \Exception('Turn is not generated'); + } + + /** + * @return Move + */ + public function defend() + { + return $this->situation->getDefendOrKillMove(); + } + + /** + * Attacking is allways different + * @return Move + */ + abstract public function attack(); + + public function kill() + { + //killing is opposite to defend, so just invert symbol + $this->situation->invertSymbols(true); + $move = $this->situation->getDefendOrKillMove(); + + //revert + $this->situation->invertSymbols(false); + return $move; + } + + /** + * @return Move + */ + public function random() + { + //possible moves returns inverted! + list($y, $x) = $this->situation->randomMove(); + return new Move($x, $y); + } + + public function isTurn($move) + { + return $this->situation->isTurn($move); + } + + public function me() + { + return $this->situation->me(); + } + + public function he() + { + return $this->situation->he(); + } + + + protected function addAttacks(array $attacks) + { + $this->setAttack(array_merge($this->getAttack(), $attacks)); + } + + protected function addAttack(MoveInterface $move) + { + $this->attack[] = $move; + } + + /** + * @param array $attack + */ + public function setAttack($attack) + { + $this->attack = $attack; + } + + /** + * @return mixed + */ + public function getAttack() + { + return $this->attack; + } +} diff --git a/src/XO/Player/Dardar/Strategy/AttackStrategy.php b/src/XO/Player/Dardar/Strategy/AttackStrategy.php new file mode 100644 index 0000000..28579d2 --- /dev/null +++ b/src/XO/Player/Dardar/Strategy/AttackStrategy.php @@ -0,0 +1,45 @@ +situation); + + $specialMoves->add(new PandoraAttack($this->situation), 80); + $specialMoves->add(new CrossAttack($this->situation), 80); + $specialMoves->add(new CenterMove($this->situation)); + $specialMoves->add(new EdgeAttack($this->situation)); + $specialMoves->add(new CornerAttack($this->situation)); + + try { + $this->addAttack($specialMoves->getMove()); + + } catch (MoveNotFoundException $e) { + + } + + if ($this->situation->isCornerAttackNeeded()) { + $corners = $this->situation->addShuffledCorners(); + $this->addAttacks($corners); + } + + Utils::log('Attack strategy (attack)' . var_export($this->getAttack(), true)); + + return $this->situation->getPosssibleMove($this->getAttack(), 'I attack!'); + } +} diff --git a/src/XO/Player/Dardar/Strategy/DefenceStrategy.php b/src/XO/Player/Dardar/Strategy/DefenceStrategy.php new file mode 100644 index 0000000..d2a935a --- /dev/null +++ b/src/XO/Player/Dardar/Strategy/DefenceStrategy.php @@ -0,0 +1,49 @@ +situation); + $specialMoves->add(new CenterMove()); + $specialMoves->add(new CrossDefence()); + + try { + $this->addAttack($specialMoves->getMove()); + + } catch (MoveNotFoundException $e) { + + } + + if ($this->isOponentPandoraMove()) { + $this->addAttack($this->getPandoraDefenceMove()); + } elseif ($this->situation->isCornerAttackNeeded()) { + $corners = $this->situation->addShuffledCorners(); + $this->addAttacks($corners); + } + + return $this->situation->getPosssibleMove($this->getAttack(), 'I attack!'); + } + + + public function isOponentPandoraMove() + { + return $this->situation->isPandoraMove($this->he(), $this->me()); + } + + public function getPandoraDefenceMove() + { + $move = $this->situation->getPandoraEnemyMove(); + Utils::log('Pandora defence - enemy on: ' . var_export($move, true)); + return $this->situation->findEmptySpaceNear($move); + } + +} diff --git a/src/XO/Player/Dardar/Strategy/SpecialMoveFinder.php b/src/XO/Player/Dardar/Strategy/SpecialMoveFinder.php new file mode 100644 index 0000000..221255e --- /dev/null +++ b/src/XO/Player/Dardar/Strategy/SpecialMoveFinder.php @@ -0,0 +1,53 @@ +situation = $situation; + } + + /** + * @param AbstractSpecial $attack + * @param $skippPercent int how much times in percent we will skip it? + */ + public function add(AbstractSpecial $attack, $skippPercent = 0) + { + $this->specials[] = $attack + ->setSituation($this->situation) + ->setSkipped($skippPercent); + } + + public function getMove() + { + foreach ($this->specials as $specialMove) { + try { + + /** @var SpecialsInterface $specialMove */ + if ($specialMove->isPossible()) { + return $specialMove->findMove(); + } + + } catch (MoveNotFoundException $e) { + continue; + } + } + + throw new MoveNotFoundException('Special attack not found'); + } + +} diff --git a/src/XO/Player/Dardar/Strategy/StrategyInterface.php b/src/XO/Player/Dardar/Strategy/StrategyInterface.php new file mode 100644 index 0000000..6d0ef34 --- /dev/null +++ b/src/XO/Player/Dardar/Strategy/StrategyInterface.php @@ -0,0 +1,7 @@ +situation = $situation; + } + + /** + * Let's start the AI magic + * Pick the best strategy by anallizing enemy source code + * and get his next move + * + * @return aa + */ + public function getStrategy() + { + //AI stuff will be added + //$ai = new AI(); + if ($this->iAmAttacker()) { + $this->situation->setSymbols(PlayerInterface::SYMBOL_X); + return new AttackStrategy($this->situation); + } else { + $this->situation->setSymbols(PlayerInterface::SYMBOL_O); + return new DefenceStrategy($this->situation); + } + } + + protected function iAmAttacker() + { + return $this->situation->countMoves() % 2 == 0; + } +} diff --git a/src/XO/Player/Dardar/Utils.php b/src/XO/Player/Dardar/Utils.php new file mode 100644 index 0000000..269c1a6 --- /dev/null +++ b/src/XO/Player/Dardar/Utils.php @@ -0,0 +1,13 @@ +getStrategy()->getTurn(); + Utils::log("I will move $x, $y"); + return [$x, $y]; + } +} diff --git a/src/XO/Service/PlayerRegistry.php b/src/XO/Service/PlayerRegistry.php index bb66c28..ab8c4ac 100644 --- a/src/XO/Service/PlayerRegistry.php +++ b/src/XO/Service/PlayerRegistry.php @@ -65,6 +65,7 @@ public static function getDefaultPlayers() $instance->setPlayer('drunk', new DrunkPlayer()); $instance->setPlayer('alf', new AlfPlayer()); $instance->setPlayer('evilmage', new Player\EvilMagePlayer()); + $instance->setPlayer('dardar', new Player\DardarPlayer()); return $instance; } diff --git a/tests/XO/Player/Dardar/Specials/CrossAttackTest.php b/tests/XO/Player/Dardar/Specials/CrossAttackTest.php new file mode 100644 index 0000000..c590eb6 --- /dev/null +++ b/tests/XO/Player/Dardar/Specials/CrossAttackTest.php @@ -0,0 +1,99 @@ +setSituation($situation); + $actual = $attack->isPossible(); + $this->assertSame($expected, $actual); + } + + /** + * @return array + */ + public function testFindMoveProvider() + { + $table = [ + [null, null, 'O'], + [null, 'X', null], + ['O', null, null], + ]; + $out[] = [[0,1], $table]; + + + $table = [ + ['O', null, null], + [null, 'X', null], + [null, null, 'O'], + ]; + $out[] = [[0,1], $table]; + + return $out; + } + /** + * @dataProvider testFindMoveProvider + * @param $expected + * @param $table + */ + public function testFindMove($expected, $table) + { + $situation = new Situation($table, PlayerInterface::SYMBOL_X); + $attack = new CrossDefence(); + $attack->setSituation($situation); + $actual = $attack->findMove()->getNaturalMove(); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/XO/Player/Dardar/Specials/EdgeAttackTest.php b/tests/XO/Player/Dardar/Specials/EdgeAttackTest.php new file mode 100644 index 0000000..bf61914 --- /dev/null +++ b/tests/XO/Player/Dardar/Specials/EdgeAttackTest.php @@ -0,0 +1,48 @@ +setSituation($situation); + $actual = $pandoraAttack->hasOpponnetMovedEdge(); + $this->assertEquals($expected, $actual); + } +} diff --git a/tests/XO/Player/Dardar/Specials/PandoraAttackTest.php b/tests/XO/Player/Dardar/Specials/PandoraAttackTest.php new file mode 100644 index 0000000..4df6909 --- /dev/null +++ b/tests/XO/Player/Dardar/Specials/PandoraAttackTest.php @@ -0,0 +1,55 @@ +setSituation($situation); + $actual = $attack->isPossible(); + $this->assertSame($expected, $actual); + } +} diff --git a/tests/XO/Player/Dardar/Strategy/SituationTest.php b/tests/XO/Player/Dardar/Strategy/SituationTest.php new file mode 100644 index 0000000..12fec0b --- /dev/null +++ b/tests/XO/Player/Dardar/Strategy/SituationTest.php @@ -0,0 +1,456 @@ +isMove($situation->createMove([null, null])); + + $actual = $situation->isMove($situation->createMove(['x', 'x'])); + + $actual = $situation->isMove($situation->createMove([false, true])); + + $actual = $situation->isMove($situation->createMove([1, null])); + + } + + public function testIsTurn() + { + $table = new TableHelper(); + $situation = new Situation($table, PlayerInterface::SYMBOL_X); + + + + $actual = $situation->isMove($situation->createMove([1, 1])); + $this->assertEquals(true, $actual); + } + + public function testCountBy() + { + $table = new TableHelper(); + $situation = new Situation($table, PlayerInterface::SYMBOL_X); + + $actual = $situation->countBy(['X', 'O', 'X'], 'my'); + $this->assertEquals(2, $actual); + + $actual = $situation->countBy([null, 'O', 'X'], 'enemy'); + $this->assertEquals(1, $actual); + } + + /** + * @return array + */ + public function testFindDefendCrossMoveProvider() + { + $table = [ + [null, null, 'O'], + [null, 'O', null], + [null, null, null], + ]; + $out[] = [[0,2], $table]; + + $table = [ + [null, null, null], + [null, 'O', null], + ['O', null, null], + ]; + $out[] = [[2, 0], $table]; + + $table = [ + [null, null, null], + [null, 'O', null], + [null, null, 'O'], + ]; + $out[] = [[0, 0], $table]; + + $table = [ + ['O', null, null], + [null, 'O', null], + [null, null, null], + ]; + $out[] = [[2, 2], $table]; + + $table = [ + [null, 'O', null], + [null, 'O', null], + [null, null, null], + ]; + $out[] = [null, $table]; + + return $out; + } + + /** + * @dataProvider testFindDefendCrossMoveProvider + * @param $expected + * @param $table + * @group this + */ + public function testFindDefendCrossMove($expected, $table) + { + $situation = new Situation($table, PlayerInterface::SYMBOL_X); + + try { + $actual = $situation->findDefendCrossMove()->getNaturalMove(); + } catch (MoveNotFoundException $e) { + $actual = null; + } + + $this->assertEquals($expected, $actual); + } + + /** + * @return array + */ + public function testIsCrossEdgeMoveProvider() + { + $table = [ + ['O', null, null], + [null, 'X', 'O'], + [null, null, 'O'], + ]; + $out[] = [false, $table]; + + $table = [ + ['X', null, null], + [null, 'X', 'O'], + [null, null, 'O'], + ]; + $out[] = [true, $table]; + + $table = [ + ['X', null, 'O'], + [null, 'X', null], + [null, null, 'O'], + ]; + $out[] = [false, $table]; + + $table = [ + ['X', null, null], + [null, 'X', null], + [null, null, 'O'], + ]; + $out[] = [false, $table]; + + return $out; + } + + /** + * @dataProvider testIsCrossEdgeMoveProvider + * @param $expected + * @param $table + * @group this + */ + public function IsCrossEdgeMove($expected, $table) + { + $situation = new Situation($table, PlayerInterface::SYMBOL_X); + $actual = $situation->IsCrossEdgeMove(); + $this->assertEquals($expected, $actual); + } + + /** + * @return array + */ + public function isCoordsNeighboursData() + { + $table = [ + ['O', 'O', null], + [null, null, null], + [null, null, null], + ]; + $out[] = [true, [0, 0], [1, 0]]; + + $out[] = [true, [0, 1], [0, 2]]; + + $out[] = [true, [0, 0], [0, 1]]; + + $table = [ + [null, null, null], + [null, null, 'O'], + [null, null, 'O'], + ]; + $out[] = [true, [2, 1], [2, 2]]; + + $table = [ + [null, null, null], + [null, null, null], + [null, 'O', 'O'], + ]; + $out[] = [true, [1, 2], [2, 2]]; + + $table = [ + [null, null, 'O'], + [null, null, null], + [null, null, 'O'], + ]; + $out[] = [false, [2, 0], [2, 2]]; + + $table = [ + [null, null, null], + [null, 'O', null], + [null, null, 'O'], + ]; + $out[] = [false, [1, 1], [2, 2]]; + + return $out; + } + /** + * @dataProvider isCoordsNeighboursData + * @param $expected + * @param $move1 + * @param $move2 + */ + public function testisCoordsNeighbours($expected, $move1, $move2) + { + $table = new TableHelper(); + $situation = new Situation($table, PlayerInterface::SYMBOL_X); + $actual = $situation->isCoordsNeighbours($move1, $move2); + $this->assertEquals($expected, $actual); + } + + /** + * @return array + */ + public function testCountMoveProvider() + { + $table = [ + [null, null, null], + ['X', 'O', null], + [null, null, null], + ]; + $out[] = [2, $table]; + + $table = [ + [null, null, null], + ['X', 'O', null], + [null, 'X', null], + ]; + $out[] = [3, $table]; + + return $out; + } + /** + * @dataProvider testCountMoveProvider + * @param $expected + * @param $table + */ + public function testCountMove($expected, $table) + { + $situation = new Situation($table, PlayerInterface::SYMBOL_X); + $actual = $situation->countMoves(); + $this->assertEquals($expected, $actual); + } + + /** + * @return array + */ + public function isPandoraMoveProvider() + { + + $table = [ + [null, 'O', null], + ['O', 'X', null], + [null, null, null], + ]; + $out[] = [true, $table]; + + $table = [ + [null, null, 'X'], + ['O', 'X', null], + [null, 'O', null], + ]; + $out[] = [false, $table]; + + $table = [ + [null, null, null], + [null, 'X', null], + ['O', null, null], + ]; + $out[] = [false, $table]; + + $table = [ + [null, null, 'O'], + [null, 'X', null], + ['O', 'O', null], + ]; + $out[] = [false, $table]; + + return $out; + } + /** + * @dataProvider isPandoraMoveProvider + * @param $expected + * @param $table + * @group patterns + */ + public function testIsPandoraMoveProvider($expected, $table) + { + $situation = new Situation($table, PlayerInterface::SYMBOL_X); + $actual = $situation->isPandoraMove(PlayerInterface::SYMBOL_O, PlayerInterface::SYMBOL_X); + $this->assertEquals($expected, $actual); + } + + /** + * @return array + */ + public function hasPatternProvider() + { + $pattern = ['O', 'X', 'X']; + $line = ['O', 'X', 'X']; + $out[] = [true, $pattern, $line]; + + $line = ['O', 'O', 'X']; + $out[] = [false, $pattern, $line]; + + $line = ['O', null, 'X']; + $out[] = [false, $pattern, $line]; + + return $out; + } + /** + * @dataProvider hasPatternProvider + * @param $expected + * @param $pattern + * @param $line + * @group patterns + */ + public function testHasPattern($expected, $pattern, $line) + { + $table = new TableHelper(); + $situation = new Situation($table, PlayerInterface::SYMBOL_X); + $actual = $situation->hasPattern($line, $pattern); + $this->assertEquals($expected, $actual); + } + + /** + * @return array + */ + public function countWithFilterProvider() + { + $line = ['O', 'X', 'X']; + $out[] = [2, SituationCounter::ME, $line]; + + $line = ['O', 'O', 'X']; + $out[] = [2, SituationCounter::HE, $line]; + + $line = ['O', null, 'X']; + $out[] = [1, SituationCounter::EMPTIES, $line]; + + $line = ['O', null, 'X']; + $out[] = [1, SituationCounter::ME, $line]; + + $line = ['O', null, 'X']; + $out[] = [1, SituationCounter::HE, $line]; + + $line = ['O', null, 'X']; + $out[] = [1, SituationCounter::ME, $line]; + + $line = ['O', null, null]; + $out[] = [0, SituationCounter::ME, $line]; + + $line = ['X', null, null]; + $out[] = [0, SituationCounter::HE, $line]; + + return $out; + } + /** + * @dataProvider countWithFilterProvider + * @param $expected + * @param $type + * @param $line + * @group filters + */ + public function testCountWithFilter($expected, $type, $line) + { + $table = new TableHelper(); + $situation = new Situation($table, PlayerInterface::SYMBOL_X); + $actual = $situation->countBy($line, $type); + $this->assertEquals($expected, $actual); + } + + + /** + * @return array + */ + public function testCountTableMovesByProvider() + { + $table = [ + ['O', null, null], + [null, 'X', 'O'], + [null, null, 'O'], + ]; + $out[] = [5, null, $table]; + + $table = [ + ['X', null, null], + [null, 'X', 'O'], + [null, null, 'O'], + ]; + $out[] = [2, 'X', $table]; + + $table = [ + ['X', null, 'O'], + [null, 'X', null], + [null, null, 'O'], + ]; + $out[] = [2, 'X', $table]; + + $table = [ + ['X', null, null], + [null, 'X', null], + [null, null, 'O'], + ]; + $out[] = [1, 'O', $table]; + + return $out; + } + /** + * @dataProvider testCountTableMovesByProvider + * @param $expected + * @param $symbol + * @param $table + * @group counters + */ + public function testCountTableMovesBy($expected, $symbol, $table) + { + $situation = new Situation($table, PlayerInterface::SYMBOL_X); + $actual = $situation->countTableMovesBy($symbol); + $this->assertEquals($expected, $actual); + } + + + + /** + * @group counters + */ + public function testareValuesNear() + { + $table = new TableHelper(); + $situation = new Situation($table, PlayerInterface::SYMBOL_X); + $actual = $situation->areValuesNear(0, 1); + $this->assertEquals(true, $actual); + + $actual = $situation->areValuesNear(1, 2); + $this->assertEquals(true, $actual); + + $actual = $situation->areValuesNear(2, 1); + $this->assertEquals(true, $actual); + + $actual = $situation->areValuesNear(2, 2); + $this->assertEquals(false, $actual); + } +} diff --git a/tests/XO/Player/DardarPlayerTest.php b/tests/XO/Player/DardarPlayerTest.php new file mode 100644 index 0000000..73e1943 --- /dev/null +++ b/tests/XO/Player/DardarPlayerTest.php @@ -0,0 +1,30 @@ +assertEquals(1, 1); + } + + protected function defenceStrategyProvider() + { + $out = []; + $table = [ + ['X', 'O', 'X'], + [null, 'X', 'O'], + ['O', 'X', null], + ]; + + $out[] = [[2,2], $table]; + return $out; + } + +} From ed6d49b34ae03d0cfcc14a4ea2d0fcba866c6d1d Mon Sep 17 00:00:00 2001 From: Darius Kasperavicius Date: Fri, 6 Jun 2014 13:27:50 +0300 Subject: [PATCH 2/2] Fix player symbols by adding setSymbolsIfMissing --- src/XO/Player/Dardar/SituationCounter.php | 7 +++++++ src/XO/Player/Dardar/Strategy/StrategySelector.php | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/XO/Player/Dardar/SituationCounter.php b/src/XO/Player/Dardar/SituationCounter.php index 1605ba6..4b6b093 100644 --- a/src/XO/Player/Dardar/SituationCounter.php +++ b/src/XO/Player/Dardar/SituationCounter.php @@ -42,6 +42,13 @@ public function setSymbols($symbol) $this->setEnemySymbol($this->invertSymbol($symbol)); } + public function setSymbolsIfMissing($symbol) + { + if (null === $this->me() && null === $this->he()) { + $this->setSymbols($symbol); + } + } + protected function invertSymbol($symbol) { return $symbol == PlayerInterface::SYMBOL_X diff --git a/src/XO/Player/Dardar/Strategy/StrategySelector.php b/src/XO/Player/Dardar/Strategy/StrategySelector.php index d14195e..f987f7a 100644 --- a/src/XO/Player/Dardar/Strategy/StrategySelector.php +++ b/src/XO/Player/Dardar/Strategy/StrategySelector.php @@ -21,7 +21,7 @@ public function __construct(Situation $situation) * Pick the best strategy by anallizing enemy source code * and get his next move * - * @return aa + * @return Move */ public function getStrategy() { @@ -31,7 +31,7 @@ public function getStrategy() $this->situation->setSymbols(PlayerInterface::SYMBOL_X); return new AttackStrategy($this->situation); } else { - $this->situation->setSymbols(PlayerInterface::SYMBOL_O); + $this->situation->setSymbolsIfMissing(PlayerInterface::SYMBOL_O); return new DefenceStrategy($this->situation); } }