Issues (13)

src/Game.php (11 issues)

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
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...
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
The expression $players of type true is not traversable.
Loading history...
237
        
238 6
            foreach ($opponents as $opponent) {
0 ignored issues
show
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
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