Completed
Push — master ( 156670...59b4fe )
by Jarrett
06:23
created

Game::determineOutcome()   D

Complexity

Conditions 14
Paths 13

Size

Total Lines 92
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 40
CRAP Score 14.4345

Importance

Changes 3
Bugs 0 Features 1
Metric Value
cc 14
eloc 46
c 3
b 0
f 1
nc 13
nop 0
dl 0
loc 92
ccs 40
cts 46
cp 0.8696
crap 14.4345
rs 4.9516

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php namespace Jarrett\RockPaperScissorsSpockLizard;
2
3
use Jarrett\RockPaperScissorsSpockLizardException;
4
5
/**
6
 * Class RockPaperScissorsSpockLizard
7
 *
8
 * @author Jarrett Barnett <[email protected]
9
 * @see http://www.samkass.com/theories/RPSSL.html
10
 */
11
class Game {
12
13
    const ROCK = 0;
14
    const PAPER = 1;
15
    const SCISSORS = 2;
16
    const SPOCK = 3;
17
    const LIZARD = 4;
18
    
19
    const DEFAULT_NUM_ROUNDS = 1;
20
    const DEFAULT_PLAYER_NAME_PREFIX = 'Player ';
21
22
    /**
23
     * Move labels
24
     * @var array
25
     */
26
    private $labels = [
27
        self::ROCK      => 'rock',
28
        self::PAPER     => 'paper',
29
        self::SCISSORS  => 'scissors',
30
        self::SPOCK     => 'spock',
31
        self::LIZARD    => 'lizard',
32
    ];
33
34
    /**
35
     * Outcomes
36
     * @var array
37
     */
38
    private $move_outcomes = [
39
        self::ROCK => [
40
            self::SCISSORS => 'crushes',
41
            self::LIZARD => 'crushes'
42
        ],
43
        self::PAPER => [
44
            self::ROCK => 'covers',
45
            self::SPOCK => 'disproves'
46
        ],
47
        self::SCISSORS => [
48
            self::PAPER => 'cuts',
49
            self::LIZARD => 'decapitates'
50
        ],
51
        self::SPOCK => [
52
            self::SCISSORS => 'smashes',
53
            self::ROCK => 'vaporizes'
54
        ],
55
        self::LIZARD => [
56
            self::SPOCK => 'poisons',
57
            self::PAPER => 'eats'
58
        ]
59
    ];
60
    
61
    /**
62
     * Game Outcome
63
     * @var $outcomes
64
     */
0 ignored issues
show
Documentation Bug introduced by
The doc comment $outcomes at position 0 could not be parsed: Unknown type name '$outcomes' at position 0 in $outcomes.
Loading history...
65
    protected $outcomes;
66
    
67
    /**
68
     * The number of rounds to play
69
     * @var $rounds
70
     */
0 ignored issues
show
Documentation Bug introduced by
The doc comment $rounds at position 0 could not be parsed: Unknown type name '$rounds' at position 0 in $rounds.
Loading history...
71
    private $rounds = false;
72
    
73
    /**
74
     * Lock rounds from changing?
75
     * @var $rounds_lock
76
     */
0 ignored issues
show
Documentation Bug introduced by
The doc comment $rounds_lock at position 0 could not be parsed: Unknown type name '$rounds_lock' at position 0 in $rounds_lock.
Loading history...
77
    private $rounds_lock;
78
    
79
    /**
80
     * @var $last_outcome
81
     */
0 ignored issues
show
Documentation Bug introduced by
The doc comment $last_outcome at position 0 could not be parsed: Unknown type name '$last_outcome' at position 0 in $last_outcome.
Loading history...
82
    private $last_outcome = false;
83
    
84
    /**
85
     * End of index for move outcomes
86
     * @var bool|int
87
     */
88
    private $moves_index_end = false;
89
    
90
    /**
91
     * @var $players - collection of players
92
     */
0 ignored issues
show
Documentation Bug introduced by
The doc comment $players at position 0 could not be parsed: Unknown type name '$players' at position 0 in $players.
Loading history...
93
    protected $players = false;
94
    
95
    /**
96
     * RockPaperScissorsSpockLizard constructor.
97
     */
98 22
    public function __construct()
99
    {
100 22
        $this->setRounds(self::DEFAULT_NUM_ROUNDS);
101 22
        $this->moves_index_end = count(array_keys($this->move_outcomes)) - 1;
102
        
103 22
        return $this;
104
    }
105
    
106
    /**
107
     * Set Rounds
108
     *
109
     * @param $rounds
110
     * @param bool $lock
111
     * @return $this
112
     * @throws \Jarrett\RockPaperScissorsSpockLizardException
113
     */
114 22
    public function setRounds($rounds, $lock = false)
115
    {
116 22
        if ($this->rounds_lock === true)
117
        {
118 1
            throw new RockPaperScissorsSpockLizardException('The ability to set rounds has been locked for this game');
119
        }
120
        
121 22
        if (!is_numeric($rounds)) {
122 2
            throw new RockPaperScissorsSpockLizardException('Invalid value supplied for setRounds().');
123
        }
124
        
125 22
        if (!is_bool($lock)) {
126 1
            throw new RockPaperScissorsSpockLizardException('Lock parameter must be a boolean.');
127
        }
128
        
129 22
        $this->rounds_lock = (bool) $lock;
130 22
        $this->rounds = (int) $rounds;
131
132 22
        return $this;
133
    }
134
135
    /**
136
     * Get Rounds
137
     * @return mixed
138
     */
139 3
    public function getRounds()
140
    {
141 3
        return $this->rounds;
142
    }
143
144
    /**
145
     * Restart Game
146
     */
147 7
    public function restart()
148
    {
149 7
        $this->setRounds(self::DEFAULT_NUM_ROUNDS);
150 7
        $this->players = false;
151
152 7
        return $this;
153
    }
154
    
155
    /**
156
     * Play Move
157
     * @return $this
158
     * @throws \Jarrett\RockPaperScissorsSpockLizardException
159
     */
160 8
    public function play()
161
    {
162 8
        if ($this->getOutcomes() > 0 && $this->getRounds() >= count($this->getOutcomes())) {
163 1
            throw new RockPaperScissorsSpockLizardException('The game has already been played. Use getOutcomes() to see the game results!');
164
        }
165
        
166 8
        if (empty($this->getTotalPlayers())) {
167 1
            throw new RockPaperScissorsSpockLizardException('No players have been added to this game');
168
        }
169
        
170
        // if only 1 player has been added, add a computer player
171 7
        if ($this->getTotalPlayers() < 2) {
172 1
            throw new RockPaperScissorsSpockLizardException('This game requires at least 2 players');
173
        }
174
        
175 6
        $this->generateMovesForBots();
176 6
        $this->determineOutcome();
177
        
178 5
        return $this;
179
    }
180
    
181
    /**
182
     * Get Move's Index Value
183
     * @param $move
184
     * @return array|bool
185
     */
186 5
    protected function getMoveIndex($move)
187
    {
188 5
        return array_flip($this->labels)[key($move)];
189
    }
190
    
191
    /**
192
     * Get Random Move Using Mersenne Twister for even distribution
193
     * @return int
194
     */
195 1
    private function generateMove()
196
    {
197 1
        return $this->labels[mt_rand(0, $this->moves_index_end)];
198
    }
199
    
200
    /**
201
     * Generate Moves For Bots
202
     * @return $this
203
     */
204 6
    private function generateMovesForBots()
205
    {
206 6
        foreach ($this->getPlayers() as &$player) {
0 ignored issues
show
Bug introduced by
The expression $this->getPlayers() cannot be used as a reference.

Let?s assume that you have the following foreach statement:

foreach ($array as &$itemValue) { }

$itemValue is assigned by reference. This is possible because the expression (in the example $array) can be used as a reference target.

However, if we were to replace $array with something different like the result of a function call as in

foreach (getArray() as &$itemValue) { }

then assigning by reference is not possible anymore as there is no target that could be modified.

Available Fixes

1. Do not assign by reference
foreach (getArray() as $itemValue) { }
2. Assign to a local variable first
$array = getArray();
foreach ($array as &$itemValue) {}
3. Return a reference
function &getArray() { $array = array(); return $array; }

foreach (getArray() as &$itemValue) { }
Loading history...
Bug introduced by
The expression $this->getPlayers() of type boolean is not traversable.
Loading history...
207
208 6
            $last_move = $player->getLastMoveIndex();
209
210
            // generate move for bots
211 6
            if ($player->isBot() && empty($last_move)) {
212 1
                $move = $this->generateMove();
213 1
                $player->move($move);
214
            }
215
        }
216
        
217 6
        return $this;
218
    }
219
220
    /**
221
     * Determine Outcome
222
     * @return mixed - array on success or false on failure
223
     * @throws RockPaperScissorsSpockLizardException
224
     */
225 6
    private function determineOutcome()
226
    {
227 6
        $outcome = [];
228
        
229 6
        $players = $this->getPlayers();
230 6
        $opponents = $players;
231
232 6
        if ($players === false) {
233
            return false;
234
        }
235
236 6
        foreach ($players as $player) {
0 ignored issues
show
Bug introduced by
The expression $players of type true is not traversable.
Loading history...
237
        
238 6
            foreach ($opponents as $opponent) {
0 ignored issues
show
Bug introduced by
The expression $opponents of type boolean is not traversable.
Loading history...
239
    
240
                // dont play the player against themself
241 6
                if ($player->getId() === $opponent->getId())
242
                {
243 6
                    continue;
244
                }
245
246
                // move collection
247 6
                $player_move = $player->getLastMoveIndex();
248 6
                $opponent_move = $opponent->getLastMoveIndex();
249
250
                // verify moves have been set
251 6
                if (!is_array($player_move)) {
252 1
                    throw new RockPaperScissorsSpockLizardException($player->getName() . ' has not set a move!');
253
                }
254
255 5
                if (!is_array($opponent_move)) {
256
                    throw new RockPaperScissorsSpockLizardException($opponent->getName() . ' has not set a move!');
257
                }
258
259
                // move labels
260 5
                $player_move_label = ucfirst(key($player_move));
261 5
                $opponent_move_label = ucfirst(key($opponent_move));
262
263
                // map moves to an index
264 5
                $player_move_index = $this->getMoveIndex($player_move);
265 5
                $opponent_move_index = $this->getMoveIndex($opponent_move);
266
267
                // Exceptions
268 5
                if (!is_numeric($player_move_index)) {
269
                    throw new RockPaperScissorsSpockLizardException($player->getName() . ' made an illegal move!');
270
                }
271
272 5
                if (!is_numeric($opponent_move_index)) {
273
                    throw new RockPaperScissorsSpockLizardException($opponent->getName() . ' made an illegal move!');
274
                }
275
276 5
                if (current($player_move) === true) {
277
                    throw new RockPaperScissorsSpockLizardException($player->getName() . ' has already made this move!');
278
                }
279
280 5
                if (current($opponent_move) === true) {
281
                    throw new RockPaperScissorsSpockLizardException($opponent->getName() . ' has already made this move!');
282
                }
283
284
                // compare player with opponent
285 5
                if (isset($this->move_outcomes[$player_move_index][$opponent_move_index])) {
286 4
                    $outcome['winners'][] = [
287 4
                        'player' => $player,
288 4
                        'opponent' => $opponent,
289 4
                        'description' => $player_move_label . ' ' . $this->move_outcomes[$player_move_index][$opponent_move_index] . ' ' . $opponent_move_label
290
                    ];
291 4
                    $outcome['losers'][] = [
292 4
                        'player' => $opponent,
293 4
                        'opponent' => $player,
294 4
                        'description' => $player_move_label . ' ' . $this->move_outcomes[$player_move_index][$opponent_move_index] . ' ' . $opponent_move_label
295
                    ];
296 5
                } else if (isset($this->move_outcomes[$opponent_move_index][$player_move_index])) {
297
                    // dont do anything -- we just need to check this in order to determine whether a tie needs to be calculated
298
                } else {
299
                    // we just add the tie for the player since the opponent will be added to the ties on later iteration
300 1
                    $outcome['ties'][] = [
301 1
                        'player' => $player,
302 1
                        'opponent' => $opponent,
303 5
                        'description' => 'Both played ' . $player_move_label
304
                    ];
305
                }
306
            }
307
        }
308
309
        // mark last moves as played
310 5
        foreach ($players as $player) {
0 ignored issues
show
Bug introduced by
The expression $players of type true is not traversable.
Loading history...
311 5
            $player->lastMoveIsPlayed();
312
        }
313
        
314 5
        $this->setOutcome($outcome);
315
        
316 5
        return $this->getRoundOutcome();
317
    }
318
    
319
    /**
320
     * Set Outcome
321
     * @param $outcome
322
     * @return $this
323
     */
324 5
    public function setOutcome($outcome)
325
    {
326 5
        $this->outcomes[] = $outcome;
327 5
        $this->last_outcome = $outcome;
328
        
329 5
        return $this;
330
    }
331
    
332
    /**
333
     * Get Round Outcome
334
     * @return mixed string on success, bool if no last round outcome
335
     */
336 5
    public function getRoundOutcome()
337
    {
338 5
        return $this->last_outcome;
339
    }
340
    
341
    /**
342
     * Add Player
343
     * @param Player $player
344
     * @return $this
345
     */
346 13
    public function addPlayer(Player $player)
347
    {
348 13
        $count = $this->getTotalPlayers();
349
        
350
        // give player a name if they dont have one
351 13
        if (empty($player->getName())) {
352 13
            $player->setName(self::DEFAULT_PLAYER_NAME_PREFIX . ($count + 1)); // we add 1 since $count is an array pointer
353
        }
354
        
355
        // we set an id to make it easier to generate results later
356 13
        $player->setId($count + 1);
357
        
358 13
        $this->players[$count] = $player;
359
        
360 13
        return $this;
361
    }
362
    
363
    /**
364
     * Add Players
365
     *
366
     * @return $this
367
     * @throws RockPaperScissorsSpockLizardException
368
     */
369 10
    public function addPlayers()
370
    {
371 10
        if (func_num_args() < 1) {
372 1
            throw new RockPaperScissorsSpockLizardException('No player objects supplied to addPlayers()');
373
        }
374
        
375 9
        $count = 0;
376 9
        foreach (func_get_args() as $player) {
377
            
378 9
            $count++;
379
            // error if not Player objects
380 9
            if (!$player instanceof Player) {
381 1
                throw new RockPaperScissorsSpockLizardException('Parameter # ' . $count . ' is not an instance of Player()');
382
            }
383
            
384 9
            $this->addPlayer($player);
385
        }
386
        
387 8
        return $this;
388
    }
389
390
    /**
391
     * Get Round Winners
392
     * @return mixed
393
     */
394 1
    public function getRoundWinners()
395
    {
396 1
        $last_round = $this->getOutcomes();
397 1
        $outcome = end($last_round);
398 1
        return $outcome['winners'];
399
    }
400
401
    /**
402
     * Get Game Winners
403
     * @return array
404
     */
405 1
    public function getWinners()
406
    {
407 1
        $outcomes = $this->getOutcomes();
408
409 1
        foreach ($outcomes as $outcome)
410
        {
411 1
            $winners[] = $outcome['winners'];
412
        }
413
414 1
        return $winners;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $winners seems to be defined by a foreach iteration on line 409. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
415
    }
416
    
417
    /**
418
     * Get Outcomes
419
     * @return mixed
420
     */
421 8
    public function getOutcomes()
422
    {
423 8
        return $this->outcomes;
424
    }
425
    
426
    /**
427
     * @return mixed - array of players, false if not set
428
     */
429 15
    public function getPlayers()
430
    {
431 15
        return $this->players;
432
    }
433
    
434
    /**
435
     * Get Total Player Count
436
     * @return int
437
     */
438 14
    public function getTotalPlayers()
439
    {
440 14
        $player_count = $this->getPlayers();
441
    
442 14
        return $player_count === false ? 0 : count($player_count);
443
    }
444
}
445