Completed
Push — master ( e88ec9...da59d3 )
by Rémi
02:58
created

Hangman::isGameReady()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 0
crap 1
1
<?php
2
3
namespace Hangman\Entity;
4
5
use Broadway\EventSourcing\EventSourcedAggregateRoot;
6
use Hangman\Event\HangmanGameCreatedEvent;
7
use Hangman\Event\HangmanGameFailedStartingEvent;
8
use Hangman\Event\HangmanGameLostEvent;
9
use Hangman\Event\HangmanGameStartedEvent;
10
use Hangman\Event\HangmanPlayerCreatedEvent;
11
use Hangman\Event\HangmanPlayerFailedCreatingEvent;
12
use Hangman\Event\HangmanPlayerLostEvent;
13
use Hangman\Event\HangmanPlayerProposedInvalidAnswerEvent;
14
use Hangman\Event\HangmanPlayerTriedPlayingDuringAnotherPlayerTurnEvent;
15
use Hangman\Event\HangmanPlayerTriedPlayingInactiveGameEvent;
16
use Hangman\Event\HangmanPlayerTurnEvent;
17
use Hangman\Exception\HangmanException;
18
use Hangman\Exception\HangmanPlayerOptionsException;
19
use Hangman\Move\Answer;
20
use Hangman\Move\Proposition;
21
use Hangman\Options\HangmanPlayerOptions;
22
use Hangman\PlayersCollection;
23
use Hangman\Result\HangmanBadProposition;
24
use Hangman\Result\HangmanGoodProposition;
25
use Hangman\Result\HangmanLost;
26
use Hangman\Result\HangmanWon;
27
use Hangman\Word;
28
use MiniGame\Entity\MiniGame;
29
use MiniGame\Entity\MiniGameId;
30
use MiniGame\Entity\Player;
31
use MiniGame\Entity\PlayerId;
32
use MiniGame\Entity\PlayTrait;
33
use MiniGame\GameResult;
34
use MiniGame\PlayerOptions;
35
36
class Hangman extends EventSourcedAggregateRoot implements MiniGame
37
{
38
    use PlayTrait;
39
40
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
41
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
42
    //////////////////////////////////////////////   CONSTANTS   ///////////////////////////////////////////////////
43
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
44
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
45
46
    const STATE_UNINITIALIZED = 'uninitialized';
47
    const STATE_READY = 'ready';
48
    const STATE_STARTED = 'started';
49
    const STATE_OVER = 'over';
50
51
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
52
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
53
    /////////////////////////////////////////////   PROPERTIES   ///////////////////////////////////////////////////
54
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
55
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
56
57
    /**
58
     * @var MiniGameId
59
     */
60
    private $id;
61
62
    /**
63
     * @var Word
64
     */
65
    private $word;
66
67
    /**
68
     * @var PlayersCollection
69
     **/
70
    private $players;
71
72
    /**
73
     * @var string
74
     */
75
    private $state;
76
77
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
78
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
79
    /////////////////////////////////////////   PRIVATE CONSTRUCTOR   //////////////////////////////////////////////
80
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
81
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
82
83
    /**
84
     * Constructor
85
     */
86 111
    private function __construct()
87
    {
88 111
        $this->state = self::STATE_UNINITIALIZED;
89 111
    }
90
91
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
92
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
93
    //////////////////////////////////////////////   ACCESSORS   ///////////////////////////////////////////////////
94
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
95
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
96
97
    /**
98
     * Returns the id of the game
99
     *
100
     * @return MiniGameId
101
     */
102 87
    public function getId()
103
    {
104 87
        return $this->id;
105
    }
106
107
    /**
108
     * Returns the aggregate id
109
     *
110
     * @return MiniGameId
111
     */
112 111
    public function getAggregateRootId()
113
    {
114 111
        return $this->id;
115 32
    }
116
117
    /**
118
     * Returns the name of the mini-game
119
     *
120
     * @return string
121
     */
122 3
    public static function getName()
123
    {
124 3
        return 'HANGMAN';
125
    }
126
127
    /**
128
     * Get the player identified by PlayerId
129
     *
130
     * @param PlayerId $playerId
131
     *
132
     * @return HangmanPlayer
133
     */
134 12
    public function getPlayer(PlayerId $playerId = null)
135
    {
136 12
        if ($playerId === null) {
137 3
            return null;
138
        }
139
140 9
        return $this->players->get((string)$playerId);
141
    }
142
143
    /**
144
     * Returns the player who can play
145
     *
146
     * @return Player
147
     */
148 3
    public function getCurrentPlayer()
149
    {
150 3
        return $this->players->getCurrentPlayer();
151
    }
152
153
    /**
154
     * Get the players
155
     *
156
     * @return Player[]
157
     */
158 111
    public function getPlayers()
159
    {
160 111
        return $this->players->toArray();
161
    }
162
163
    /**
164
     * Is game started?
165
     *
166
     * @return bool
167
     */
168 36
    public function isGameStarted()
169
    {
170 36
        return $this->state === self::STATE_STARTED;
171
    }
172
173
    /**
174
     * Is it the player's turn?
175
     *
176
     * @param PlayerId $playerId
177
     *
178
     * @return bool
179
     */
180 36
    public function canPlayerPlay(PlayerId $playerId)
181
    {
182 36
        return $this->players->canPlayerPlay($playerId);
183
    }
184
185
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
186
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
187
    ///////////////////////////////////////////   DOMAIN METHODS   /////////////////////////////////////////////////
188
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
189
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
190
191
    /**
192
     * Starts the game
193
     *
194
     * @param PlayerId $playerId
195
     *
196
     * @return GameResult
197
     */
198 51
    public function startGame(PlayerId $playerId)
199
    {
200 51
        if (! $this->isGameReady()) {
201 3
            return $this->failStarting($playerId, HangmanGameFailedStartingEvent::BAD_STATE);
202
        }
203
204 51
        if (! $this->players->hasPlayers()) {
205 3
            return $this->failStarting($playerId, HangmanGameFailedStartingEvent::NO_PLAYER);
206
        }
207
208 48
        $event = new HangmanGameStartedEvent($this->id, $playerId);
209 48
        $this->apply($event);
210
211 48
        $this->setNextPlayer($playerId);
212
213 48
        return $event;
214
    }
215
216
    /**
217
     * Adds a player to the game
218
     *
219
     * @param PlayerOptions $playerOptions
220
     *
221
     * @throws HangmanPlayerOptionsException
222
     * @throws HangmanException
223
     *
224
     * @return GameResult
225
     */
226 66
    public function addPlayerToGame(PlayerOptions $playerOptions)
227
    {
228 66
        if (! $playerOptions instanceof HangmanPlayerOptions) {
229 3
            throw new HangmanPlayerOptionsException(
230 3
                $playerOptions->getPlayerId(),
231 3
                $this->getId(),
232 1
                'Options are not recognized'
233 2
            );
234
        }
235
236 63
        if (! $this->isGameReady()) {
237 3
            $event = new HangmanPlayerFailedCreatingEvent(
238 3
                $this->id,
239 3
                $playerOptions->getPlayerId(),
240 3
                $playerOptions->getExternalReference()
241 2
            );
242 3
            $this->apply($event);
243 3
            return $event;
244
        }
245
246 63
        $event = new HangmanPlayerCreatedEvent(
247 63
            $this->id,
248 63
            $playerOptions->getPlayerId(),
249 63
            $playerOptions->getName(),
250 63
            $playerOptions->getLives(),
251 63
            $playerOptions->getExternalReference()
252 42
        );
253 63
        $this->apply($event);
254 63
        return $event;
255
    }
256
257
    /**
258
     * A player leaves the game
259
     *
260
     * @param PlayerId $playerId
261
     *
262
     * @return GameResult
263
     */
264 9
    public function leaveGame(PlayerId $playerId)
265
    {
266 9
        switch ($this->state) {
267 9
            case self::STATE_STARTED:
268 3
                $player = $this->getPlayer($playerId);
269 3
                return $player ? $this->playerLoses($player) : null;
270 6
            case self::STATE_OVER:
271 3
                break;
272 2
            default:
273 3
                $this->players->remove((string) $playerId);
274 3
                break;
275 4
        }
276 6
        return null;
277 4
    }
278
279
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
280
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
281
    //////////////////////////////////////////   PRIVATE METHODS   /////////////////////////////////////////////////
282
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
283
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
284
285
    /**
286
     * Initialize the game
287
     *
288
     * @param MiniGameId             $id
289
     * @param string                 $word
290
     */
291 111
    private function initialize(MiniGameId $id, $word)
292
    {
293 111
        $this->apply(new HangmanGameCreatedEvent($id, $word));
294 111
    }
295
296
    /**
297
     * @return bool
298
     */
299 66
    private function isGameReady()
300
    {
301 66
        return $this->state === self::STATE_READY;
302
    }
303
304
    /**
305
     * @param PlayerId $playerId
306
     * @param string   $reason
307
     *
308
     * @return HangmanGameFailedStartingEvent
309
     */
310 6
    private function failStarting(PlayerId $playerId, $reason)
311
    {
312 6
        $event = new HangmanGameFailedStartingEvent(
313 6
            $this->id,
314 4
            $playerId,
315
            $reason
316 4
        );
317 6
        $this->apply($event);
318 6
        return $event;
319
    }
320
321
    /**
322
     * Player proposes a letter
323
     *
324
     * @param PlayerId $playerId
325
     * @param Proposition $move
326
     *
327
     * @return GameResult
328
     */
329 21
    private function playProposition(PlayerId $playerId, Proposition $move)
330
    {
331 21
        if ($errorEvent = $this->ensurePlayerCanPlay($playerId)) {
332 6
            $this->apply($errorEvent);
333 6
            return $errorEvent;
334
        }
335
336 15
        return $this->currentPlayerProposeLetter($move->getText());
337
    }
338
339
    /**
340
     * Player tries an answer
341
     *
342
     * @param PlayerId $playerId
343
     * @param Answer $move
344
     *
345
     * @return GameResult
346
     */
347 15
    private function playAnswer(PlayerId $playerId, Answer $move)
348
    {
349 15
        if ($errorEvent = $this->ensurePlayerCanPlay($playerId)) {
350 3
            $this->apply($errorEvent);
351 3
            return $errorEvent;
352
        }
353
354
        try {
355 12
            return $this->currentPlayerProposeAnswer($move->getText());
356 3
        } catch (HangmanException $e) {
357 3
            $event = new HangmanPlayerProposedInvalidAnswerEvent(
358 3
                $this->getId(),
359 2
                $playerId,
360
                $move
361 2
            );
362 3
            $this->apply($event);
363 3
            return $event;
364
        }
365
    }
366
367
    /**
368
     * Returns an error event if player cannot play
369
     *
370
     * @param PlayerId $playerId
371
     *
372
     * @return GameResult
373
     */
374 36
    private function ensurePlayerCanPlay(PlayerId $playerId)
375
    {
376 36
        if (!$this->isGameStarted()) {
377 3
            $event = new HangmanPlayerTriedPlayingInactiveGameEvent(
378 3
                $this->getId(),
379
                $playerId
380 2
            );
381 3
            return $event;
382
        }
383
384 33
        if (!$this->canPlayerPlay($playerId)) {
385 6
            $event = new HangmanPlayerTriedPlayingDuringAnotherPlayerTurnEvent(
386 6
                $this->getId(),
387
                $playerId
388 4
            );
389 6
            return $event;
390
        }
391
392 27
        return null;
393
    }
394
395
    /**
396
     * Propose a letter
397
     *
398
     * @param string $letter
399
     *
400
     * @return HangmanBadProposition | HangmanGoodProposition
401
     */
402 15
    private function currentPlayerProposeLetter($letter)
403
    {
404 15
        $result =  (!$this->word->contains($letter))
405 13
                   ? $this->currentPlayerBadProposition($letter) // remove a life
406 15
                   : $this->currentPlayerGoodProposition($letter); // yay!
407
408 15
        return $result;
409
    }
410
411
    /**
412
     * Propose an answer
413
     *
414
     * @param string $answer
415
     *
416
     * @return HangmanLost | HangmanWon
417
     */
418 12
    private function currentPlayerProposeAnswer($answer)
419
    {
420 12
        $this->checkAnswerIsValid($answer);
421
422 9
        if (! $this->word->equals($answer)) {
423 6
            return $this->playerLoses($this->players->getCurrentPlayer()); // you lose
424
        }
425
426 3
        return $this->playerWins($this->players->getCurrentPlayer()); // you win
427
    }
428
429
    /**
430
     * Function to call when a bad proposition has been made
431
     *
432
     * @param string $letter
433
     *
434
     * @return HangmanBadProposition | HangmanLost
435
     */
436 9
    private function currentPlayerBadProposition($letter)
437
    {
438 9
        $player = $this->players->getCurrentPlayer();
439
440 9
        $event = $player->playBadLetter($letter, 1);
441
442 9
        if ($event->getRemainingLives() === 0) {
443 6
            return $this->playerLoses($player);
444
        }
445
446 3
        $this->setNextPlayer($this->players->getNextPlayerId());
447
448 3
        return $event;
449
    }
450
451
    /**
452
     * Function to call after a good proposition of letter has been made
453
     *
454
     * @param string $letter
455
     *
456
     * @return HangmanGoodProposition | HangmanWon
457
     */
458 6
    private function currentPlayerGoodProposition($letter)
459
    {
460 6
        $player = $this->players->getCurrentPlayer();
461
462 6
        $event = $player->playGoodLetter($letter);
463
464 6
        if ($this->isAllLettersFoundForPlayer($player)) {
465 3
            return $this->playerWins($player);
466
        }
467
468 3
        $this->setNextPlayer($this->players->getNextPlayerId());
469
470 3
        return $event;
471
    }
472
473
    /**
474
     * Function to call when game is won by a player
475
     *
476
     * @param HangmanPlayer $player
477
     *
478
     * @return HangmanWon
479
     */
480 6
    private function playerWins(HangmanPlayer $player)
481
    {
482 6
        $event = $player->win($this->word);
483
484 6
        foreach ($this->players as $otherPlayer) {
485 6
            if ($otherPlayer->equals($player) || $otherPlayer->hasLost()) {
486 6
                continue;
487
            }
488 6
            $otherPlayer->lose($this->word);
489 4
        }
490
491 6
        return $event;
492
    }
493
494
    /**
495
     * Function to call when game is lost by a player
496
     *
497
     * @param HangmanPlayer $player
498
     *
499
     * @return hangmanLost | HangmanGameLostEvent
500
     */
501 15
    private function playerLoses(HangmanPlayer $player)
502
    {
503 15
        $event = $player->lose($this->word);
504
505 15
        if ($this->players->thereIsAtLeastOneActivePlayer() &&
506 13
            $this->players->isCurrentPlayer($player->getId())
507 10
        ) {
508 12
            $this->setNextPlayer($this->players->getNextPlayerId());
509 12
            return $event;
510
        }
511
512 3
        $event = new HangmanGameLostEvent(
513 3
            $this->id,
514 3
            $player->getId(),
515 3
            (string) $this->word
516 2
        );
517 3
        $this->apply($event);
518
519 3
        return $event;
520
    }
521
522
    /**
523
     * Sets the next player
524
     *
525
     * @param PlayerId $id
526
     */
527 48
    private function setNextPlayer(PlayerId $id = null)
528
    {
529 48
        if ($id === null || $this->players->isCurrentPlayer($id)) {
530
            return;
531
        }
532
533 48
        $this->apply(
534 48
            new HangmanPlayerTurnEvent($this->getId(), $id)
535 32
        );
536 48
    }
537
538
    /**
539
     * Build the word from played letters
540
     *
541
     * @param string[] $playedLetters
542
     *
543
     * @return string
544
     */
545 42
    public function buildWord($playedLetters)
546
    {
547 42
        return $this->word->buildWord($playedLetters);
548
    }
549
550
    /**
551
     * Checks if all letters for the word have been found
552
     *
553
     * @param HangmanPlayer $player
554
     *
555
     * @return bool
556
     */
557 6
    private function isAllLettersFoundForPlayer(HangmanPlayer $player)
558
    {
559 6
        $wordLetters = $this->word->getLetters();
560 6
        $playerLetters = $player->getPlayedLetters();
561 6
        return count(array_intersect($wordLetters, $playerLetters)) == count($wordLetters);
562
    }
563
564
    /**
565
     * Checks if the answer is valid
566
     * If it's not, ends player turn and throws an HangmanException
567
     *
568
     * @param string $answer
569
     *
570
     * @throws HangmanException
571
     */
572 12
    private function checkAnswerIsValid($answer)
573
    {
574 12
        if (! $this->word->isValid($answer)) {
575 3
            throw new HangmanException(sprintf('"%s" is not a valid answer!', $answer));
576
        }
577 9
    }
578
579
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
580
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
581
    ////////////////////////////////////////////   APPLY EVENTS   //////////////////////////////////////////////////
582
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
583
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
584
585
    /**
586
     * Apply the game created event
587
     *
588
     * @param HangmanGameCreatedEvent $event
589
     *
590
     * @return void
591
     */
592 111
    protected function applyHangmanGameCreatedEvent(HangmanGameCreatedEvent $event)
593
    {
594 111
        $this->id = $event->getGameId();
595 111
        $this->word = new Word($event->getWord());
596 111
        $this->players = new PlayersCollection();
597 111
        $this->state = self::STATE_READY;
598 111
    }
599
600
    /**
601
     * Apply the player created event
602
     *
603
     * @param HangmanPlayerCreatedEvent $event
604
     *
605
     * @return void
606
     */
607 63
    protected function applyHangmanPlayerCreatedEvent(HangmanPlayerCreatedEvent $event)
608
    {
609 63
        $this->players->add(
610 63
            new HangmanPlayer(
611 63
                $event->getPlayerId(),
612 63
                $event->getPlayerName(),
613 63
                $event->getLives(),
614 42
                $this,
615 63
                $event->getExternalReference()
616 42
            )
617 42
        );
618 63
    }
619
620
    /**
621
     * Apply the game created event
622
     */
623 48
    protected function applyHangmanGameStartedEvent()
624
    {
625 48
        $this->state = self::STATE_STARTED;
626 48
    }
627
628
    /**
629
     * Apply the player turn event
630
     *
631
     * @param HangmanPlayerTurnEvent $event
632
     */
633 48
    protected function applyHangmanPlayerTurnEvent(HangmanPlayerTurnEvent $event)
634
    {
635 48
        $this->players->setCurrentPlayer($event->getPlayerId());
636 48
    }
637
638
    /**
639
     * Apply the hangman player lost event
640
     *
641
     * @param HangmanPlayerLostEvent $event
642
     */
643 21
    protected function applyHangmanPlayerLostEvent(HangmanPlayerLostEvent $event)
0 ignored issues
show
Unused Code introduced by
The parameter $event is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
644
    {
645 21
        $this->state = self::STATE_OVER;
646 21
    }
647
648
    /**
649
     * Apply the hangman player win event
650
     *
651
     * @return void
652
     */
653 6
    protected function applyHangmanPlayerWinEvent()
654
    {
655 6
        $this->players->setCurrentPlayer(null);
656 6
        $this->state = self::STATE_OVER;
657 6
    }
658
659
    /**
660
     * Apply the hangman lost by all event
661
     *
662
     * @return void
663
     */
664 3
    protected function applyHangmanGameLostEvent()
665
    {
666 3
        $this->players->setCurrentPlayer(null);
667 3
        $this->state = self::STATE_OVER;
668 3
    }
669
670
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
671
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
672
    ////////////////////////////////////////////   EVENT SOURCED   /////////////////////////////////////////////////
673
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
674
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
675
676
    /**
677
     * @return Player[]
678
     */
679 111
    protected function getChildEntities()
680
    {
681 111
        return $this->getPlayers();
682
    }
683
684
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
685
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
686
    /////////////////////////////////////////   STATIC CONSTRUCTOR   ///////////////////////////////////////////////
687
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
688
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
689
690
    /**
691
     * Create a new instance
692
     *
693
     * @param MiniGameId $id
694
     * @param string     $word
695
     *
696
     * @return Hangman
697
     */
698 111
    public static function createGame(MiniGameId $id, $word)
699
    {
700 111
        $hangman = new self();
701 111
        $hangman->initialize($id, $word);
702
703 111
        return $hangman;
704
    }
705
706
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
707
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
708
    ///////////////////////////////////////////   RECONSTITUTION   /////////////////////////////////////////////////
709
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
710
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
711
712
    /**
713
     * Static construction method for reconstitution
714
     *
715
     * @return Hangman
716
     */
717 3
    public static function instantiateForReconstitution()
718
    {
719 3
        return new self();
720
    }
721
722
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
723
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
724
    /////////////////////////////////////////   APPLY RESTRICTIONS   ///////////////////////////////////////////////
725
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
726
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
727
728
    /**
729
     * @param mixed $event
730
     */
731 111
    public function apply($event)
732
    {
733 111
        if (! $this->isSupportedEvent($event)) {
734
            return;
735
        }
736
737 111
        parent::apply($event);
738 111
    }
739
740
    /**
741
     * @param mixed $event
742
     *
743
     * @return bool
744
     */
745 111
    private function isSupportedEvent($event)
746
    {
747
        return (
748 111
            $event instanceof GameResult &&
749 111
            ($this->id === null || $this->id == $event->getGameId())
750 74
        );
751
    }
752
}
753