├── README.md ├── composer.json ├── docs ├── bowling-game.md ├── fizz-buzz.md ├── gilded-rose.md ├── katas.md ├── prime-factors.md ├── roman-numerals.md ├── string-calculator.md └── tennis-game.md ├── resources └── data │ └── players.json └── src └── Katas ├── BowlingGame.php ├── Contracts └── Executable.php ├── FizzBuzz.php ├── GildedRose ├── BackstagePasses.php ├── Brie.php ├── Conjured.php ├── GildedRose.php ├── Item.php ├── Normal.php └── Sulfurus.php ├── Kata.php ├── PrimeFactors.php ├── RomanNumerals.php ├── StringCalculator.php ├── Support └── Arr.php ├── TeamMaker.php └── Tennis ├── Game.php └── Player.php /README.md: -------------------------------------------------------------------------------- 1 | # Code Katas with PHPUnit 2 | 3 | ## Introduction 4 | 5 | If martial artists use kata as a method for exercise and practice, what might be the equivalent for coders like us? Coding katas are short, repeatable programming challenges which are meant to exercise everything from your focus, to your workflow. 6 | 7 | ## Katas 8 | 9 | - Prime Factors 10 | - Roman Numerals 11 | - Bowling Game 12 | - String Calculator 13 | - Tennis Match 14 | - FizzBuzz 15 | - The Gilded Rose 16 | 17 | ## Installation 18 | 19 | ### Prerequisites 20 | 21 | To run this project, you must have PHP 7.3 or higher installed. 22 | 23 | Begin by cloning this repository to your machine, and installing all Composer dependencies. 24 | 25 | ### Get Started 26 | 27 | ```bash 28 | git clone git@github.com:Thavarshan/phpunit-code-katas.git katas 29 | cd katas && composer install 30 | ``` 31 | 32 | ## Testing 33 | 34 | Just run PHPUnit in the project root. 35 | 36 | ```bash 37 | ./vendor/bin/phpunit 38 | ``` 39 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "thavarshan/code-katas", 3 | "description": "Code Katas with PHPUnit", 4 | "type": "package", 5 | "require": { 6 | "php": "^8.1", 7 | "ext-json": "*", 8 | "symfony/var-dumper": "^6.0", 9 | "phpunit/phpunit": "^9.5", 10 | "ext-dom": "*" 11 | }, 12 | "autoload": { 13 | "psr-4": { 14 | "Katas\\": "src/Katas" 15 | } 16 | }, 17 | "autoload-dev": { 18 | "psr-4": { 19 | "Katas\\Tests\\": "tests" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /docs/bowling-game.md: -------------------------------------------------------------------------------- 1 | # Bowling Game Kata 2 | 3 | ## Definition 4 | 5 | 6 | -------------------------------------------------------------------------------- /docs/fizz-buzz.md: -------------------------------------------------------------------------------- 1 | # Fizz Buzz Kata 2 | 3 | ## Definition 4 | 5 | Write a program that prints the numbers from 1 to 100. But for multiples of three print “Fizz” instead of the number and for the multiples of five print “Buzz”. For numbers which are multiples of both three and five print “FizzBuzz”. 6 | 7 | ## Rules 8 | 9 | 1. For multiples of three print "Fizz" instead of the number 10 | 2. For multiples of five print "Buzz" 11 | 3. For number which are multiples of both three and five print "FizzBuzz". 12 | 13 | ## Process 14 | 15 | 1. Check if given number is divisible by 3. 16 | 2. If so print out "Fizz". 17 | 2. If not check if number is divisible by 5. 18 | 3. If so print out "Buzz". 19 | 4. If number is not divisible by either 3 or 5 print out the given number. 20 | 5. If number is divisible by both 3 and 5 print out "FizzBuzz". 21 | 22 | -------------------------------------------------------------------------------- /docs/gilded-rose.md: -------------------------------------------------------------------------------- 1 | # Gilded Rose Refactoring Kata 2 | 3 | Hi and welcome to team Gilded Rose. As you know, we are a small inn with a 4 | prime location in a prominent city ran by a friendly innkeeper named 5 | Allison. We also buy and sell only the finest goods. Unfortunately, our 6 | goods are constantly degrading in quality as they approach their sell by 7 | date. We have a system in place that updates our inventory for us. It was 8 | developed by a no-nonsense type named Leeroy, who has moved on to new 9 | adventures. Your task is to add the new feature to our system so that we 10 | can begin selling a new category of items. First an introduction to our 11 | system: 12 | 13 | - All items have a SellIn value which denotes the number of days we have 14 | to sell the item 15 | - All items have a Quality value which denotes how valuable the item is 16 | - At the end of each day our system lowers both values for every item 17 | 18 | Pretty simple, right? Well this is where it gets interesting: 19 | 20 | - Once the sell by date has passed, Quality degrades twice as fast 21 | - The Quality of an item is never negative 22 | - "Aged Brie" actually increases in Quality the older it gets 23 | - The Quality of an item is never more than 50 24 | - "Sulfuras", being a legendary item, never has to be sold or decreases 25 | in Quality 26 | - "Backstage passes", like aged brie, increases in Quality as it's SellIn 27 | value approaches; Quality increases by 2 when there are 10 days or less 28 | and by 3 when there are 5 days or less but Quality drops to 0 after the 29 | concert 30 | 31 | We have recently signed a supplier of conjured items. This requires an 32 | update to our system: 33 | 34 | - "Conjured" items degrade in Quality twice as fast as normal items 35 | 36 | Feel free to make any changes to the UpdateQuality method and add any 37 | new code as long as everything still works correctly. However, do not 38 | alter the Item class or Items property as those belong to the goblin 39 | in the corner who will insta-rage and one-shot you as he doesn't 40 | believe in shared code ownership (you can make the UpdateQuality 41 | method and Items property static if you like, we'll cover for you). 42 | 43 | Just for clarification, an item can never have its Quality increase 44 | above 50, however "Sulfuras" is a legendary item and as such its 45 | Quality is 80 and it never alters. 46 | 47 | ## Attribution 48 | 49 | This work is by [@TerryHughes](https://twitter.com/TerryHughes), [@NotMyself](https://twitter.com/NotMyself) 50 | 51 | The repository can be found at [https://github.com/NotMyself/GildedRose](https://github.com/NotMyself/GildedRose) 52 | -------------------------------------------------------------------------------- /docs/katas.md: -------------------------------------------------------------------------------- 1 | # Katas 2 | 3 | Kata is a martial arts term from Japan that refers to a choreographed sequence of movements. The goal is to perfect and internalize these movements through repetition and memorization. Once mastered, these sequences serve as a reference guide that you can instantly reach for and adapt without thought. 4 | 5 | Outside of martial arts, this word, kata, is used more generally to refer to any set of steps or patterns that is repeated until mastery. Think of it as a way to harness your form and routine. 6 | -------------------------------------------------------------------------------- /docs/prime-factors.md: -------------------------------------------------------------------------------- 1 | # Prime Factors Kata 2 | 3 | ## Definition 4 | 5 | A prime number is a number that is only divisible by itself and one, example: (2, 3, 5, 7, 11...). Further, for any given number, we can generate its prime factors by reducing it down to exclusively prime numbers that, when multiplied, equal the number in question. 6 | 7 | ## Process 8 | 9 | 1. Is the number divisible by 2. 10 | 2. If true then divide by 2. 11 | 3. If false, increase candidate and try again. 12 | 4. Repeat. 13 | -------------------------------------------------------------------------------- /docs/roman-numerals.md: -------------------------------------------------------------------------------- 1 | # Roman Numerals Kata 2 | 3 | ## Definition 4 | 5 | Roman numerals are a system of numbers where numbers are represented by a combination of letters. 6 | 7 | ## Process 8 | 9 | Create a class using TDD that can generate the corresponding Roman numeral for a provided number. 10 | 11 | 1. Is the number equal to special characters. 12 | 2. If true then generate equivalent special character. 13 | 3. If false, generate non special character. 14 | 4. Repeat. 15 | -------------------------------------------------------------------------------- /docs/string-calculator.md: -------------------------------------------------------------------------------- 1 | # String Calculator Kata 2 | 3 | ## Definition 4 | 5 | A string calculator breaks down a given string in to characters, evaluates them to determine if they are numbers and performs calculations on them, finally returning the result as the output. 6 | 7 | ## Process 8 | 9 | 1. Create a simple string calculator with a method signature: `add(string $number)` 10 | - The method can take up to two numbers, seperated by commas, and will return their sum. 11 | - For example "" or "1" or "1, 2" as inputs. (For an empty string it will return 0) 12 | 2. Allow the add method to handle an unknown amount of numbers 13 | 3. Allow the add method to handle new lines between numbers (instead of commas). 14 | - The following input is ok: "1\n2, 3" (will equal 6) 15 | - The following input is NOT ok: "1, \n" (no need to prove it - just clarifying) 16 | 4. Support different delimeters 17 | - To change delimeters, the beginning os the string will contain a seperate line that looks like the first line is optional. All existing scenarios should still be supported. 18 | 5. Calling add with a negative number will throw an exception "negative not allowed" - and the negative that was passed. 19 | 6. If there are multiple negatives, show all of them in the exception message. 20 | 7. Using TDD, Add a method to `StringCalculator` called `public int GetCalledCount()` that returns how many times `add()` was invoked. Remember-Start with a failing (or even non compiling) test. 21 | 22 | 23 | -------------------------------------------------------------------------------- /docs/tennis-game.md: -------------------------------------------------------------------------------- 1 | # Tennis Game Kata 2 | 3 | ## Definition 4 | 5 | The object of the game is to maneuver the ball in such a way that the opponent is not able to play a valid return. The player who is unable to return the ball will not gain a point, while the opposite player will. 6 | 7 | ## Rules 8 | 9 | 1. A game is won by the first player to have won at least four points in total and at least two points more than the opponent. 10 | 2. The running score of each game is described in a manner peculiar to tennis: 11 | - Scores from zero to three points are described as "love", "fifteen", "thirty", and "forty" respectively. 12 | 3. If at least three points have been scored by each player, and the scores are equal, the score is "deuce". 13 | 4. If at least three points have been scored by each side and a player has one more point that his opponent, the score of the game is "advantage" for the player in the lead. 14 | 15 | ## Process 16 | 17 | 1. Check if either player has a score enough to be declared a winner. 18 | 2. If so get the name of the player and pass on as winner of the match. 19 | 3. If not continue with the game by checking to see if either player has an advantage over the other. 20 | 4. If so determine which player has an advantage over the other and pass on as player with advantage. 21 | 5. Check if the score between the two players are deuce. 22 | 6. If so show the score as "deuce". 23 | 7. If not show the score according to respective terms of the points. 24 | -------------------------------------------------------------------------------- /resources/data/players.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Chris R", 4 | "score": 0.58 5 | }, 6 | { 7 | "name": "Joe B", 8 | "score": 0.57 9 | }, 10 | { 11 | "name": "Martin T", 12 | "score": 0.57 13 | }, 14 | { 15 | "name": "Jasmine A", 16 | "score": 0.55 17 | }, 18 | { 19 | "name": "Luke F", 20 | "score": 0.54 21 | }, 22 | { 23 | "name": "Tom L", 24 | "score": 0.53 25 | }, 26 | { 27 | "name": "Dale J", 28 | "score": 0.53 29 | }, 30 | { 31 | "name": "Lucy H", 32 | "score": 0.53 33 | }, 34 | { 35 | "name": "Robert B", 36 | "score": 0.53 37 | }, 38 | { 39 | "name": "Terry V", 40 | "score": 0.48 41 | } 42 | ] 43 | -------------------------------------------------------------------------------- /src/Katas/BowlingGame.php: -------------------------------------------------------------------------------- 1 | roll($arguments[0]); 27 | 28 | return $this->score(); 29 | } 30 | 31 | /** 32 | * Roll the ball. 33 | * 34 | * @param int $pins 35 | * 36 | * @return void 37 | */ 38 | public function roll(int $pins): void 39 | { 40 | $this->rolls[] = $pins; 41 | } 42 | 43 | /** 44 | * Calculate the final score. 45 | * 46 | * @return int 47 | */ 48 | public function score() 49 | { 50 | $score = 0; 51 | $roll = 0; 52 | 53 | foreach (range(1, self::FRAMES_PER_GAME) as $frame) { 54 | if ($this->isStrike($roll)) { 55 | $score += $this->pinCount($roll) + $this->strikeBonus($roll); 56 | 57 | ++$roll; 58 | 59 | continue; 60 | } 61 | 62 | $score += $this->defaultFrameScore($roll); 63 | 64 | if ($this->isSpare($roll)) { 65 | $score += $this->spareBonus($roll); 66 | } 67 | 68 | $roll += 2; 69 | } 70 | 71 | return $score; 72 | } 73 | 74 | /** 75 | * Determine if the current roll was a strike. 76 | * 77 | * @param int $roll 78 | * 79 | * @return bool 80 | */ 81 | protected function isStrike(int $roll): bool 82 | { 83 | return 10 === $this->pinCount($roll); 84 | } 85 | 86 | /** 87 | * Determine if the current frame was a spare. 88 | * 89 | * @param int $roll 90 | * 91 | * @return bool 92 | */ 93 | protected function isSpare(int $roll): bool 94 | { 95 | return 10 === $this->defaultFrameScore($roll); 96 | } 97 | 98 | /** 99 | * Calculate the score for the frame. 100 | * 101 | * @param int $roll 102 | * 103 | * @return int 104 | */ 105 | protected function defaultFrameScore(int $roll): int 106 | { 107 | return $this->pinCount($roll) + $this->pinCount($roll + 1); 108 | } 109 | 110 | /** 111 | * Get the bonus for a strike. 112 | * 113 | * @param int $roll 114 | * 115 | * @return int 116 | */ 117 | protected function strikeBonus(int $roll): int 118 | { 119 | return $this->pinCount($roll + 1) + $this->pinCount($roll + 2); 120 | } 121 | 122 | /** 123 | * Get the bonus for a spare. 124 | * 125 | * @param int $roll 126 | * 127 | * @return int 128 | */ 129 | protected function spareBonus(int $roll): int 130 | { 131 | return $this->pinCount($roll + 2); 132 | } 133 | 134 | /** 135 | * Get the number of pins knocked down for the given roll. 136 | * 137 | * @param int $roll 138 | * 139 | * @return int 140 | */ 141 | protected function pinCount(int $roll): int 142 | { 143 | return $this->rolls[$roll]; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/Katas/Contracts/Executable.php: -------------------------------------------------------------------------------- 1 | convert($arguments[0]); 15 | } 16 | 17 | /** 18 | * Give appropriate term of number being checked. 19 | * 20 | * @param int $number 21 | * 22 | * @return string 23 | */ 24 | public function convert(int $number): string 25 | { 26 | $result = ''; 27 | 28 | if (0 === $number % 3) { 29 | $result .= 'Fizz'; 30 | } 31 | 32 | if (0 === $number % 5) { 33 | $result .= 'Buzz'; 34 | } 35 | 36 | return $result ?: $number; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Katas/GildedRose/BackstagePasses.php: -------------------------------------------------------------------------------- 1 | quality += 1; 13 | 14 | if ($this->sellIn <= 10) { 15 | $this->quality += 1; 16 | } 17 | 18 | if ($this->sellIn <= 5) { 19 | $this->quality += 1; 20 | } 21 | 22 | if ($this->sellIn <= 0) { 23 | $this->quality = 0; 24 | } 25 | 26 | if ($this->quality > 50) { 27 | $this->quality = 50; 28 | } 29 | 30 | $this->sellIn -= 1; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Katas/GildedRose/Brie.php: -------------------------------------------------------------------------------- 1 | sellIn -= 1; 13 | $this->quality += 1; 14 | 15 | if ($this->sellIn <= 0) { 16 | $this->quality += 1; 17 | } 18 | 19 | if ($this->quality > 50) { 20 | $this->quality = 50; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Katas/GildedRose/Conjured.php: -------------------------------------------------------------------------------- 1 | sellIn -= 1; 13 | $this->quality -= 2; 14 | 15 | if ($this->sellIn <= 0) { 16 | $this->quality -= 2; 17 | } 18 | 19 | if ($this->quality < 0) { 20 | $this->quality = 0; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Katas/GildedRose/GildedRose.php: -------------------------------------------------------------------------------- 1 | Normal::class, 18 | 'Aged Brie' => Brie::class, 19 | 'Sulfuras, Hand of Ragnaros' => Sulfurus::class, 20 | 'Backstage passes to a TAFKAL80ETC concert' => BackstagePasses::class, 21 | 'Conjured Mana Cake' => Conjured::class, 22 | ]; 23 | 24 | /** 25 | * {@inheritDoc} 26 | */ 27 | public function execute(...$arguments) 28 | { 29 | $arguments = $arguments[0]; 30 | 31 | $item = self::of($arguments[0], $arguments[1], $arguments[2]); 32 | 33 | $item->tick(); 34 | 35 | return [ 36 | 'Quality' => $item->quality, 37 | 'Sell in' => $item->sellIn, 38 | ]; 39 | } 40 | 41 | /** 42 | * Update item status. 43 | * 44 | * @param string $name 45 | * @param int $quality 46 | * @param int $sellIn 47 | * @return \Katas\GildedRose\Item 48 | * 49 | * @throws \InvalidArgumentException 50 | */ 51 | public static function of($name, $quality, $sellIn) 52 | { 53 | if (array_key_exists($name, self::$items)) { 54 | return new self::$items[$name]($quality, $sellIn); 55 | } 56 | 57 | throw new InvalidArgumentException('Item type does not exist.'); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Katas/GildedRose/Item.php: -------------------------------------------------------------------------------- 1 | quality = $quality; 30 | $this->sellIn = $sellIn; 31 | } 32 | 33 | /** 34 | * Update item quality and sell by date. 35 | * 36 | * @return void 37 | */ 38 | abstract public function tick(); 39 | } 40 | -------------------------------------------------------------------------------- /src/Katas/GildedRose/Normal.php: -------------------------------------------------------------------------------- 1 | sellIn -= 1; 13 | $this->quality -= 1; 14 | 15 | if ($this->sellIn <= 0) { 16 | $this->quality -= 1; 17 | } 18 | 19 | if ($this->quality < 0) { 20 | $this->quality = 0; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Katas/GildedRose/Sulfurus.php: -------------------------------------------------------------------------------- 1 | generate($arguments[0]); 29 | } 30 | 31 | /** 32 | * Generate prime factors of given numbers. 33 | * 34 | * @param int $number 35 | * 36 | * @return array 37 | */ 38 | public function generate(int $number): array 39 | { 40 | for ($this->divisor = 2; $number > 1; ++$this->divisor) { 41 | for (null; 0 === $number % $this->divisor; $number /= $this->divisor) { 42 | $this->factors[] = (int) $this->divisor; 43 | } 44 | } 45 | 46 | return $this->factors; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Katas/RomanNumerals.php: -------------------------------------------------------------------------------- 1 | 1000, 14 | 'CM' => 900, 15 | 'D' => 500, 16 | 'CD' => 400, 17 | 'C' => 100, 18 | 'XC' => 90, 19 | 'L' => 50, 20 | 'XL' => 40, 21 | 'X' => 10, 22 | 'IX' => 9, 23 | 'V' => 5, 24 | 'IV' => 4, 25 | 'I' => 1, 26 | ]; 27 | 28 | /** 29 | * {@inheritDoc} 30 | */ 31 | public function execute(...$arguments) 32 | { 33 | return $this->generate($arguments[0]); 34 | } 35 | 36 | /** 37 | * Generate respective roman numeral representation of given number. 38 | * 39 | * @param int $number 40 | * 41 | * @return string 42 | */ 43 | public function generate(int $number) 44 | { 45 | if ($number <= 0 || $number >= 4000) { 46 | return false; 47 | } 48 | 49 | $result = ''; 50 | 51 | foreach (static::NUMERALS as $numeral => $arabic) { 52 | for (; $number >= $arabic; $number -= $arabic) { 53 | $result .= $numeral; 54 | } 55 | } 56 | 57 | return $result; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Katas/StringCalculator.php: -------------------------------------------------------------------------------- 1 | add($arguments[0]); 28 | } 29 | 30 | /** 31 | * Sum up the given string as integers. 32 | * 33 | * @param string $numbers 34 | * 35 | * @return int 36 | * 37 | * @throws \InvalidArgumentException 38 | */ 39 | public function add(string $numbers): int 40 | { 41 | $numbers = $this->parseString($numbers); 42 | 43 | $numbers = $this->disallowNegativeNumbers($numbers) 44 | ->ignoreGreateThanOneThousand($numbers); 45 | 46 | return (int) array_sum($numbers); 47 | } 48 | 49 | /** 50 | * Identify and parse given parameter. 51 | * 52 | * @param string $numbers 53 | * 54 | * @return array 55 | */ 56 | protected function parseString(string $numbers): array 57 | { 58 | // Define a custom delimeter of our own example. 59 | // NOTE: just for placeholder sake. 60 | $customDelimter = "\/\/(.)\n"; 61 | 62 | // Identify custom delimeter 63 | if (preg_match("/{$customDelimter}/", $numbers, $matches)) { 64 | $this->delimeter = $matches[1]; 65 | // Replace identified custom delimeter and replace 66 | // it with an empty string 67 | $numbers = str_replace($matches[0], '', $numbers); 68 | } 69 | 70 | // If a new line exists within the given parameter 71 | // remove it and given only characters. 72 | return preg_split("/{$this->delimeter}/", $numbers); 73 | } 74 | 75 | /** 76 | * Determine if geven array of numbers are positive integers. 77 | * 78 | * @param array $numbers 79 | * 80 | * @return \Katas\StringCalculator 81 | */ 82 | protected function disallowNegativeNumbers(array $numbers): StringCalculator 83 | { 84 | // Determine if each character is not negative 85 | // if negative throw an exception 86 | foreach ($numbers as $number) { 87 | if (intval($number) < 0) { 88 | throw new InvalidArgumentException('Negative numbers are disallowed.'); 89 | } 90 | } 91 | 92 | return $this; 93 | } 94 | 95 | /** 96 | * Determine if each integer in the given array is below 1000. 97 | * 98 | * @param array $numbers 99 | * 100 | * @return array 101 | */ 102 | protected function ignoreGreateThanOneThousand(array $numbers): array 103 | { 104 | // Filter out the characters that are less than 1001 105 | return array_filter( 106 | $numbers, 107 | fn ($number) => $number <= self::MAX_NUMBER_ALLOWED 108 | ); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Katas/Support/Arr.php: -------------------------------------------------------------------------------- 1 | $key : $v[$key]; 19 | }, $array); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Katas/TeamMaker.php: -------------------------------------------------------------------------------- 1 | makeTeam($arguments[0]); 28 | 29 | $teamScore1 = round($this->getTeamRanking($team1)); 30 | $teamScore2 = round($this->getTeamRanking($team2)); 31 | } while ($teamScore1 !== $teamScore2); 32 | 33 | return [$team1, $team2]; 34 | } 35 | 36 | /** 37 | * Generate two teams. 38 | * 39 | * @param array $players 40 | * @param int $size 41 | * 42 | * @return array 43 | */ 44 | public function makeTeam($players, $size = 5) 45 | { 46 | shuffle($players); 47 | 48 | return array_chunk($players, $size); 49 | } 50 | 51 | /** 52 | * Extract the scores of each player and sum it up 53 | * resulting in the total score for the team. 54 | * 55 | * @param array $team 56 | * 57 | * @return float 58 | */ 59 | public function getTeamRanking($team) 60 | { 61 | return array_sum(Arr::pluck($team, 'score')); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Katas/Tennis/Game.php: -------------------------------------------------------------------------------- 1 | playerOne = $playerOne ?? new Player('John'); 33 | $this->playerTwo = $playerTwo ?? new Player('Jane'); 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function execute(...$arguments) 40 | { 41 | $arguments = $arguments[0]; 42 | 43 | for ($i = 0; $i < $arguments[0]; $i++) { 44 | $this->pointTo($this->playerOne->getName()); 45 | } 46 | 47 | for ($i = 0; $i < $arguments[1]; $i++) { 48 | $this->pointTo($this->playerTwo->getName()); 49 | } 50 | 51 | return $this->score(); 52 | } 53 | 54 | /** 55 | * Set score for player one. 56 | * 57 | * @param string $playerName 58 | * @return void 59 | */ 60 | public function pointTo(string $playerName): void 61 | { 62 | if ($this->playerOne->getName() === $playerName) { 63 | $this->playerOne->score(); 64 | } 65 | 66 | if ($this->playerTwo->getName() === $playerName) { 67 | $this->playerTwo->score(); 68 | } 69 | } 70 | 71 | /** 72 | * Show score of current match. 73 | * 74 | * @return string 75 | */ 76 | public function score(): string 77 | { 78 | if ($this->hasWinner()) { 79 | return 'Winner: ' . $this->leader(); 80 | } 81 | 82 | if ($this->hasAdvantage()) { 83 | return 'Advantage: ' . $this->leader(); 84 | } 85 | 86 | if ($this->isDeuce()) { 87 | return 'deuce'; 88 | } 89 | 90 | return sprintf( 91 | '%s-%s', 92 | $this->pointsToTerm($this->playerOne->getPoints()), 93 | $this->pointsToTerm($this->playerTwo->getPoints()), 94 | ); 95 | } 96 | 97 | /** 98 | * Determine which player is in the lead regarding score. 99 | * 100 | * @return string 101 | */ 102 | protected function leader(): string 103 | { 104 | return $this->playerOne->getPoints() > $this->playerTwo->getPoints() 105 | ? $this->playerOne->getName() 106 | : $this->playerTwo->getName(); 107 | } 108 | 109 | /** 110 | * Determine if either player has enough score to win a macth. 111 | * 112 | * @return bool 113 | */ 114 | protected function hasWinner(): bool 115 | { 116 | if ($this->playerOne->getPoints() < 4 && $this->playerTwo->getPoints() < 4) { 117 | return false; 118 | } 119 | 120 | return abs($this->playerOne->getPoints() - $this->playerTwo->getPoints()) >= 2; 121 | } 122 | 123 | /** 124 | * Determine if the either player has an advantage over the other. 125 | * 126 | * @return bool 127 | */ 128 | protected function hasAdvantage(): bool 129 | { 130 | if (! $this->hasReachedDeuceThreshold()) { 131 | return false; 132 | } 133 | 134 | return ! $this->isDeuce(); 135 | } 136 | 137 | /** 138 | * Determine if the score is "deuce/tied". 139 | * 140 | * @return bool 141 | */ 142 | protected function isDeuce(): bool 143 | { 144 | if (! $this->hasReachedDeuceThreshold()) { 145 | return false; 146 | } 147 | 148 | return $this->playerOne->getPoints() === $this->playerTwo->getPoints(); 149 | } 150 | 151 | /** 152 | * Determine if a match can be won. 153 | * 154 | * @param string $value 155 | * @return bool 156 | */ 157 | protected function hasReachedDeuceThreshold(): bool 158 | { 159 | return $this->playerOne->getPoints() >= 3 && 160 | $this->playerTwo->getPoints() >= 3; 161 | } 162 | 163 | /** 164 | * Get respective term for score. 165 | * 166 | * @param int $points 167 | * @return string 168 | */ 169 | protected function pointsToTerm(int $points): string 170 | { 171 | switch ($points) { 172 | case 0: 173 | return 'love'; 174 | 175 | case 1: 176 | return 'fifteen'; 177 | 178 | case 2: 179 | return 'thirty'; 180 | 181 | case 3: 182 | return 'forty'; 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/Katas/Tennis/Player.php: -------------------------------------------------------------------------------- 1 | name = $name; 29 | } 30 | 31 | /** 32 | * Update player points. 33 | * 34 | * @return void 35 | */ 36 | public function score(): void 37 | { 38 | $this->points++; 39 | } 40 | 41 | /** 42 | * Get player points. 43 | * 44 | * @return int 45 | */ 46 | public function getPoints(): int 47 | { 48 | return $this->points; 49 | } 50 | 51 | /** 52 | * Get name of player. 53 | * 54 | * @return string 55 | */ 56 | public function getName(): string 57 | { 58 | return $this->name; 59 | } 60 | } 61 | --------------------------------------------------------------------------------