diff --git a/README.md b/README.md old mode 100644 new mode 100755 index f5a87cb..b61c48c --- a/README.md +++ b/README.md @@ -4,3 +4,11 @@ XO [![Build Status](https://travis-ci.org/saimaz/XO.svg?branch=master)](https://travis-ci.org/saimaz/XO) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/saimaz/XO/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/saimaz/XO/?branch=master) [![Code Coverage](https://scrutinizer-ci.com/g/saimaz/XO/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/saimaz/XO/?branch=master) + + + +Kaip paleisti žaisti vieną prieš kitą: + app/console xo:fight 'player1' 'Drunk Player' -g 1000 + + ./console xo:fight -g 10000 'Drunk player' 'Expert player' + ./console xo:fight -g 10000 'Expert player' 'Drunk player' 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..4b6b093 --- /dev/null +++ b/src/XO/Player/Dardar/SituationCounter.php @@ -0,0 +1,395 @@ +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)); + } + + public function setSymbolsIfMissing($symbol) + { + if (null === $this->me() && null === $this->he()) { + $this->setSymbols($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 Move + */ + 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->setSymbolsIfMissing(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/Player/ExpertPlayer.php b/src/XO/Player/ExpertPlayer.php new file mode 100755 index 0000000..cd77a71 --- /dev/null +++ b/src/XO/Player/ExpertPlayer.php @@ -0,0 +1,139 @@ +isLegalMove($table, $move[0], $move[1])) { + return $move; + } + + if ($move = $this->getWinMove($table, $symbol)) { + return $move; + } + + if ($move = $this->getCounterAttackMove($table, $symbol)) { + return $move; + } + + $move = $this->getPerfectMove($table, $symbol); +// $move = $this->getRandomMove($table); + + return $move; + } + + /** + * @param $table + * @return array + */ + protected function getRandomMove($table) + { + $playerService = new BinaryExpertPlayerService(); + + $state = $playerService->getState($table, PlayerInterface::SYMBOL_X, PlayerInterface::SYMBOL_O); + $legalMoves = $playerService->getLegalMoves($state); + $moveIndex = $playerService->getRandomMove($legalMoves); + return $playerService->getMoveXY($moveIndex); + } + + protected function getPerfectMove($table, $symbol = 'X') + { + $playerService = new BinaryExpertPlayerService(); + + $state = $playerService->getState($table, PlayerInterface::SYMBOL_X, PlayerInterface::SYMBOL_O); + $turn = -1; + if ($symbol === PlayerInterface::SYMBOL_X) { + $turn = 1; + } + + $legalMoves = $playerService->getPerfectMove($state, $turn); + $moveXY = $playerService->getMoveXY($legalMoves); + return $moveXY; + } + + /** + * @param $table + * @param $moveRow + * @param $moveColumn + * @return bool + */ + protected function isLegalMove($table, $moveRow, $moveColumn) + { + return null === $table[$moveRow][$moveColumn]; + } + + protected function getWinMove($table, $symbol) + { + for ($rowNr = 0; $rowNr <= 2; $rowNr++) { + if ($move = $this->getRowWinMove($table[$rowNr], $symbol, $rowNr)) { + return $move; + } + } + + if ($move = $this->getCrossWinMove($table, $symbol)) { + return $move; + } + + return null; + } + + protected function getCounterAttackMove($table, $symbol) + { + $enemySymbol = PlayerInterface::SYMBOL_X; + if ($symbol === PlayerInterface::SYMBOL_X) { + $enemySymbol = PlayerInterface::SYMBOL_O; + } + + return $this->getWinMove($table, $enemySymbol); + } + + protected function getRowWinMove($row, $symbol, $rowNr) + { + if ($row[0] === $symbol && $row[1] === $symbol && $row[2] === null) { + return [$rowNr, 2]; + } + if ($row[0] === $symbol && $row[1] === null && $row[2] === $symbol) { + return [$rowNr, 1]; + } + if ($row[0] === null && $row[1] === $symbol && $row[2] === $symbol) { + return [$rowNr, 0]; + } + + return null; + } + + protected function getCrossWinMove($table, $symbol) + { + if ($table[0][0] === $symbol && $table[1][1] === $symbol && $table[2][2] === null) { + return [2, 2]; + } + if ($table[0][0] === $symbol && $table[1][1] === null && $table[2][2] === $symbol) { + return [1, 1]; + } + if ($table[0][0] === null && $table[1][1] === $symbol && $table[2][2] === $symbol) { + return [0, 0]; + } + + if ($table[0][2] === $symbol && $table[1][1] === $symbol && $table[2][0] === null) { + return [2, 0]; + } + if ($table[0][2] === $symbol && $table[1][1] === null && $table[2][0] === $symbol) { + return [1, 1]; + } + if ($table[0][2] === null && $table[1][1] === $symbol && $table[2][0] === $symbol) { + return [0, 2]; + } + } +} diff --git a/src/XO/Service/BinaryExpertPlayerService.php b/src/XO/Service/BinaryExpertPlayerService.php new file mode 100755 index 0000000..5bfac48 --- /dev/null +++ b/src/XO/Service/BinaryExpertPlayerService.php @@ -0,0 +1,284 @@ + 0) { + $moveNum = rand(1, $numMoves); + $numMoves = 0; + for ($j = 0; $j < 9; $j++) { + if (($legalMoves & (1 << $j)) != 0) { + $numMoves++; + } + if ($numMoves == $moveNum) { + return $j; + } + } + } + } + + public function getMoveXY($moveIndex) + { + if (0 === $moveIndex) { + return [0, 0]; + } + if (1 === $moveIndex) { + return [0, 1]; + } + if (2 === $moveIndex) { + return [0, 2]; + } + if (3 === $moveIndex) { + return [1, 0]; + } + if (4 === $moveIndex) { + return [1, 1]; + } + if (5 === $moveIndex) { + return [1, 2]; + } + if (6 === $moveIndex) { + return [2, 0]; + } + if (7 === $moveIndex) { + return [2, 1]; + } + if (8 === $moveIndex) { + return [2, 2]; + } + } + + /** + * Add move to the cell and checks if new state has win + * + * @param $state + * @param $cellNum + * @param $nextTurn + * @return int + */ + public function detectWinMove($state, $cellNum, $nextTurn) + { + $value = 0b11; + if ($nextTurn == -1) { + $value = 0b10; + } + $newState = $state | ($value << $cellNum*2); + return $this->detectWin($newState); + } + + /** + * Check if this state is already won + * + * @param $state + * @return int + */ + public function detectWin($state) + { + if (($state & 0b111111000000000000) == 0b111111000000000000) { + return 0b0100111111000000000000; + } + if (($state & 0b111111000000000000) == 0b101010000000000000) { + return 0b1000101010000000000000; + } + if (($state & 0b000000111111000000) == 0b000000111111000000) { + return 0b0100000000111111000000; + } + if (($state & 0b000000111111000000) == 0b000000101010000000) { + return 0b1000000000101010000000; + } + if (($state & 0b000000000000111111) == 0b000000000000111111) { + return 0b0100000000000000111111; + } + if (($state & 0b000000000000111111) == 0b000000000000101010) { + return 0b1000000000000000101010; + } + if (($state & 0b000011000011000011) == 0b000011000011000011) { + return 0b0100000011000011000011; + } + if (($state & 0b000011000011000011) == 0b000010000010000010) { + return 0b1000000010000010000010; + } + if (($state & 0b001100001100001100) == 0b001100001100001100) { + return 0b0100001100001100001100; + } + if (($state & 0b001100001100001100) == 0b001000001000001000) { + return 0b1000001000001000001000; + } + if (($state & 0b110000110000110000) == 0b110000110000110000) { + return 0b0100110000110000110000; + } + if (($state & 0b110000110000110000) == 0b100000100000100000) { + return 0b1000100000100000100000; + } + if (($state & 0b000011001100110000) == 0b000011001100110000) { + return 0b0100000011001100110000; + } + if (($state & 0b000011001100110000) == 0b000010001000100000) { + return 0b1000000010001000100000; + } + if (($state & 0b110000001100000011) == 0b110000001100000011) { + return 0b0100110000001100000011; + } + if (($state & 0b110000001100000011) == 0b100000001000000010) { + return 0b1000100000001000000010; + } + if (($state & 0b101010101010101010) == 0b101010101010101010) { + return 0b1100000000000000000000; + } + return 0; + } + + public function openingBook($state) + { + $mask = $state & 0b101010101010101010; +// if ($mask == 0x000000000000000000) return 0b111111111; // empty table, go to any cell + if ($mask == 0x000000000000000000) return 0b101010101; // empty table, go to any cell except middle border + if ($mask == 0b000000001000000000) return 0b101000101; // used center, go to corner + if ($mask == 0b000000000000000010 || + $mask == 0b000000000000100000 || + $mask == 0b000010000000000000 || + $mask == 0b100000000000000000) return 0b000010000; // any corner is used, go to center + if ($mask == 0b000000000000001000) return 0b010010101; // top middle used, go to good places + if ($mask == 0b000000000010000000) return 0b001110001; // left middle used, go to good places + if ($mask == 0b000000100000000000) return 0b100011100; // right middle used, go to good places + if ($mask == 0b001000000000000000) return 0b101010010; // bottom middle used, go to good places + return 0; + } + + public function getPerfectMove($state, $turn) + { + $moves = $this->getLegalMoves($state); + $hope = -999; + $goodMoves = $this->openingBook($state); + if ($goodMoves == 0) { + for ($i=0; $i<9; $i++) { + if (($moves & (1<<$i)) != 0) { + $value = $this->moveValue($state, $i, $turn, $turn, 15, 1); + if ($value > $hope) { + $hope = $value; + $goodMoves = 0; + } + if ($hope == $value) { + $goodMoves |= (1 << $i); + } + } + } + } + return $this->getRandomMove($goodMoves); + } + + public function moveValue($istate, $move, $moveFor, $nextTurn, $limit, $depth) + { + $state = $this->stateMove($istate, $move, $nextTurn); + $winner = $this->detectWin($state); + if (($winner & 0x300000) == 0x300000) { + return 0; + } else { + if ($winner != 0) { + if ($moveFor == $nextTurn) { + return 10 - $depth; + } else { + return $depth - 10; + } + } + } + $hope = 999; + if ($moveFor != $nextTurn) { + $hope = -999; + } + if ($depth == $limit) { + return $hope; + } + $moves = $this->getLegalMoves($state); + for ($i = 0; $i < 9; $i++) { + if (($moves & (1 << $i)) != 0) { + $value = $this->moveValue($state, $i, $moveFor, -$nextTurn, 10 - abs($hope), $depth + 1); + if (abs($value) != 999) { + if ($moveFor == $nextTurn && $value < $hope) { + $hope = $value; + } else { + if ($moveFor != $nextTurn && $value > $hope) { + $hope = $value; + } + } + } + } + } + return $hope; + } + + public function stateMove($state, $move, $nextTurn) + { + $value = 0x3; + if ($nextTurn == -1) { + $value = 0x2; + } + return ($state | ($value << ($move*2))); + } +} diff --git a/src/XO/Service/PlayerRegistry.php b/src/XO/Service/PlayerRegistry.php old mode 100644 new mode 100755 index bb66c28..e392ac0 --- a/src/XO/Service/PlayerRegistry.php +++ b/src/XO/Service/PlayerRegistry.php @@ -2,11 +2,10 @@ namespace XO\Service; -use XO\Player\DariusPlayer; -use XO\Player\DrunkPlayer; use XO\Player\AlfPlayer; +use XO\Player\DrunkPlayer; +use XO\Player\ExpertPlayer; use XO\Player\PlayerInterface; -use XO\Player\WanisPlayer; use XO\Player as Player; /** @@ -62,9 +61,11 @@ public static function getDefaultPlayers() { $instance = new self; - $instance->setPlayer('drunk', new DrunkPlayer()); - $instance->setPlayer('alf', new AlfPlayer()); + $instance->setPlayer('Drunk player', new DrunkPlayer()); + $instance->setPlayer('Alf player', new AlfPlayer()); + $instance->setPlayer('Expert player', new ExpertPlayer()); $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; + } + +} diff --git a/tests/XO/Player/ExpertPlayerTest.php b/tests/XO/Player/ExpertPlayerTest.php new file mode 100755 index 0000000..c67669e --- /dev/null +++ b/tests/XO/Player/ExpertPlayerTest.php @@ -0,0 +1,125 @@ +getWinXPossibilityTables() as $tableMove) { + $table = $tableMove[0]; + $expectedMove = $tableMove[1]; + $this->assertSame($expectedMove, $expertPlayer->turn($table, PlayerInterface::SYMBOL_X)); + } + } + + /** + * @test + */ + public function testUseCounterAttackPossibility() + { + $expertPlayer = new ExpertPlayer(); + foreach ($this->getCounterAttackOPossibilityTables() as $tableMove) { + $table = $tableMove[0]; + $expectedMove = $tableMove[1]; + $this->assertSame($expectedMove, $expertPlayer->turn($table, PlayerInterface::SYMBOL_O)); + } + } + + private function getWinXPossibilityTables() + { + $tables = array( + array( + [ + ['O', 'O', null], + [null, 'X', null], + ['X', null, null], + ], + [0, 2] + ), + array( + [ + ['O', 'O', null], + ['X', 'X', null], + [null, null, null], + ], + [1, 2] + ), + array( + [ + ['O', 'O', null], + [null, 'X', null], + ['X', 'O', 'X'], + ], + [0, 2] + ), + array( + [ + ['O', null, null], + [null, 'O', null], + ['X', null, 'X'], + ], + [2, 1] + ), + ); + + return $tables; + } + + private function getCounterAttackOPossibilityTables() + { + $tables = array( + array( + [ + ['O', null, null], + [null, 'X', null], + ['X', 'O', null], + ], + [0, 2] + ), + array( + [ + ['O', null, null], + ['X', 'X', null], + [null, 'O', null], + ], + [1, 2] + ), + array( + [ + ['O', null, null], + ['O', 'X', null], + ['X', 'O', 'X'], + ], + [0, 2] + ), + array( + [ + ['O', null, null], + [null, 'O', null], + ['X', null, 'X'], + ], + [2, 1] + ), + ); + + return $tables; + } +} diff --git a/tests/XO/Service/BinaryExpertPlayerServiceTest.php b/tests/XO/Service/BinaryExpertPlayerServiceTest.php new file mode 100755 index 0000000..0f95781 --- /dev/null +++ b/tests/XO/Service/BinaryExpertPlayerServiceTest.php @@ -0,0 +1,221 @@ +getStateTestsData() as $expectedState => $table) { + $actualState = $service->getState($table, PlayerInterface::SYMBOL_X, PlayerInterface::SYMBOL_O); + $this->assertSame($expectedState, $actualState); + } + } + + /** + * @test + */ + public function testGetLegalMoves() + { + $service = new BinaryExpertPlayerService(); + + $legalMoves = 0b111111111; + $state = 0b000000000000000000; + $this->assertSame($legalMoves, $service->getLegalMoves($state)); + $this->assertSame(0b111111111, $service->getLegalMoves(0b000000000000000000)); + $this->assertSame(0b111111110, $service->getLegalMoves(0b000000000000000011)); + $this->assertSame(0b111111101, $service->getLegalMoves(0b000000000000001100)); + $this->assertSame(0b111111011, $service->getLegalMoves(0b000000000000110000)); + $this->assertSame(0b111111000, $service->getLegalMoves(0b000000000000111111)); + $this->assertSame(0b111111110, $service->getLegalMoves(0b000000000000000010)); + $this->assertSame(0b111111101, $service->getLegalMoves(0b000000000000001000)); + $this->assertSame(0b111111011, $service->getLegalMoves(0b000000000000100000)); + $this->assertSame(0b111111000, $service->getLegalMoves(0b000000000000101010)); + $this->assertSame(0b010111000, $service->getLegalMoves(0b110011000000101010)); + } + + /** + * @test + */ + public function testMoveRandom() + { + $service = new BinaryExpertPlayerService(); + $legalMoves = 0b100000001; + $this->assertContains($service->getRandomMove($legalMoves), array(0, 8)); + $this->assertContains($service->getRandomMove(0b000000011), array(0, 1)); + $this->assertContains($service->getRandomMove(0b100000111), array(0, 1, 2, 8)); + $this->assertContains($service->getRandomMove(0b100111001), array(0, 3, 4, 5, 8)); + } + + /** + * @test + */ + public function testGetMoveXY() + { + $service = new BinaryExpertPlayerService(); + $this->assertSame([0, 0], $service->getMoveXY(0)); + $this->assertSame([0, 1], $service->getMoveXY(1)); + $this->assertSame([0, 2], $service->getMoveXY(2)); + $this->assertSame([1, 0], $service->getMoveXY(3)); + $this->assertSame([1, 1], $service->getMoveXY(4)); + $this->assertSame([1, 2], $service->getMoveXY(5)); + $this->assertSame([2, 0], $service->getMoveXY(6)); + $this->assertSame([2, 1], $service->getMoveXY(7)); + $this->assertSame([2, 2], $service->getMoveXY(8)); + } + + /** + * @test + */ + public function testDetectWin() + { + $service = new BinaryExpertPlayerService(); + $state = 0b000000000000000000; + $this->assertSame(0b000000000000000000, $service->detectWin($state)); + + $this->assertSame(0b0100111111000000000000, $service->detectWin(0b111111000000000000)); + $this->assertSame(0b1000101010000000000000, $service->detectWin(0b101010000000000000)); + $this->assertSame(0b0100000000111111000000, $service->detectWin(0b000000111111000000)); + $this->assertSame(0b1000000000101010000000, $service->detectWin(0b000000101010000000)); + $this->assertSame(0b0100000000000000111111, $service->detectWin(0b000000000000111111)); + $this->assertSame(0b1000000000000000101010, $service->detectWin(0b000000000000101010)); + $this->assertSame(0b0100000011000011000011, $service->detectWin(0b000011000011000011)); + $this->assertSame(0b1000000010000010000010, $service->detectWin(0b000010000010000010)); + $this->assertSame(0b0100001100001100001100, $service->detectWin(0b001100001100001100)); + $this->assertSame(0b1000001000001000001000, $service->detectWin(0b001000001000001000)); + $this->assertSame(0b0100110000110000110000, $service->detectWin(0b110000110000110000)); + $this->assertSame(0b1000100000100000100000, $service->detectWin(0b100000100000100000)); + $this->assertSame(0b0100000011001100110000, $service->detectWin(0b000011001100110000)); + $this->assertSame(0b1000000010001000100000, $service->detectWin(0b000010001000100000)); + $this->assertSame(0b0100110000001100000011, $service->detectWin(0b110000001100000011)); + $this->assertSame(0b1000100000001000000010, $service->detectWin(0b100000001000000010)); + // $this->assertSame(0b1100000000000000000000, $service->detectWin(0b101010101010101010)); + } + + /** + * @test + */ + public function testDetectWinMove() + { + $serviceMock = $this->getMock( + '\XO\Service\BinaryExpertPlayerService', + array('detectWin') + ); + $serviceMock->expects($this->any())->method('detectWin')->willReturnArgument(0); + + $state = 0b000000000000000000; + $moveIndex = 0; // 0 .. 8 + $playerTurn = 1; // -1 (0) or 1 (X) + $this->assertSame(0b000000000000000011, $serviceMock->detectWinMove($state, $moveIndex, $playerTurn)); + + $playerTurn = -1; // -1 (0) or 1 (X) + $this->assertSame(0b000000000000000010, $serviceMock->detectWinMove($state, $moveIndex, $playerTurn)); + + + $state = 0b111000000000000000; + $moveIndex = 0; // 0 .. 8 + $playerTurn = 1; // -1 (0) or 1 (X) + $this->assertSame(0b111000000000000011, $serviceMock->detectWinMove($state, $moveIndex, $playerTurn)); + } + + /** + * @test + */ + public function testOpeningBook() + { + $service = new BinaryExpertPlayerService(); + $state = 0b000000000000000000; +// $this->assertSame(0b111111111, $service->openingBook($state)); + $this->assertSame(0b101010101, $service->openingBook($state)); + $this->assertSame(0b101000101, $service->openingBook(0b000000001000000000)); + $this->assertSame(0b000010000, $service->openingBook(0b000000000000000010)); + $this->assertSame(0b000010000, $service->openingBook(0b000000000000100000)); + $this->assertSame(0b000010000, $service->openingBook(0b000010000000000000)); + $this->assertSame(0b000010000, $service->openingBook(0b100000000000000000)); + $this->assertSame(0b010010101, $service->openingBook(0b000000000000001000)); + $this->assertSame(0b001110001, $service->openingBook(0b000000000010000000)); + $this->assertSame(0b100011100, $service->openingBook(0b000000100000000000)); + $this->assertSame(0b101010010, $service->openingBook(0b001000000000000000)); + + $this->assertSame(0b101000101, $service->openingBook(0b000000001100000000)); + $this->assertSame(0b000010000, $service->openingBook(0b000000000000000011)); + $this->assertSame(0b000010000, $service->openingBook(0b000000000000110000)); + $this->assertSame(0b000010000, $service->openingBook(0b000011000000000000)); + $this->assertSame(0b000010000, $service->openingBook(0b110000000000000000)); + $this->assertSame(0b010010101, $service->openingBook(0b000000000000001100)); + $this->assertSame(0b001110001, $service->openingBook(0b000000000011000000)); + $this->assertSame(0b100011100, $service->openingBook(0b000000110000000000)); + $this->assertSame(0b101010010, $service->openingBook(0b001100000000000000)); + } + + private function getStateTestsData() + { + $x = PlayerInterface::SYMBOL_X; + $o = PlayerInterface::SYMBOL_O; + + $tableStates = array( + 0b000000000000000000 => [ + [null, null, null], + [null, null, null], + [null, null, null] + ], + 0b000000000000000011 => [ + [$x, null, null], + [null, null, null], + [null, null, null] + ], + 0b000000000000001100 => [ + [null, $x, null], + [null, null, null], + [null, null, null] + ], + 0b000000000000110000 => [ + [null, null, $x], + [null, null, null], + [null, null, null] + ], + 0b000000000000111111 => [ + [$x, $x, $x], + [null, null, null], + [null, null, null] + ], + 0b000000000000000010 => [ + [$o, null, null], + [null, null, null], + [null, null, null] + ], + 0b000000000000001000 => [ + [null, $o, null], + [null, null, null], + [null, null, null] + ], + 0b000000000000100000 => [ + [null, null, $o], + [null, null, null], + [null, null, null] + ], + 0b000000000000101010 => [ + [$o, $o, $o], + [null, null, null], + [null, null, null] + ], + 0b110011000000101010 => [ + [$o, $o, $o], + [null, null, null], + [$x, null, $x] + ], + ); + + return $tableStates; + } +}