Game::detach()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
ccs 1
cts 1
cp 1
crap 1
1
<?php
2
3
namespace App\Model;
4
5
use App\Model\DeckOfCards;
6
use App\Model\FrenchSuitedDeck;
7
use App\Model\DeckShuffler;
8
use SplSubject;
9
use Random\Randomizer;
0 ignored issues
show
Bug introduced by
The type Random\Randomizer was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
10
use SplObjectStorage;
11
use Exception;
12
use App\Model\BetManager;
13
use SplObserver;
14
15
class Game implements SplSubject
16
{
17
    private ?DeckOfCards $deck;
18
    /**
19
     * @var array<?Player> The players of the game.
20
     */
21
    private array $players;
22
    private string $currentPlayer;
23
    private string $gameStatus;
24
    public BetManager $betManager;
25
    private DetermineWinner $determineWinner;
26
27
    protected SplObjectStorage $observers;
28
    /**
29
     * Constructor for the Game class.
30
     * @param array<?Player> $players The players of the game.
31
     * @param DeckOfCards|null $deck The deck of cards to be used in the game.
32
     * @param array<SplObserver> $observers The observers to be attached to the game.
33
     * @param string $gameStatus The status of the game.
34
     */
35
    public function __construct(
36 9
        array $players = [],
37
        DeckOfCards $deck = null,
38
        array $observers = [],
39
        string $gameStatus = 'ongoing'
40
    ) {
41
42
        $this->observers = new SplObjectStorage();
43 9
44
        foreach ($observers as $observer) {
45 9
            $this->attach($observer);
46
        }
47
        $this->betManager = new BetManager();
48 9
        $this->deck = $deck;
49 9
        $this->players = $players;
50 9
51
        if (count($players) > 0) {
52 9
            $this->currentPlayer = $players[0]->getName();
53 7
        }
54
        $this->setGameStatus($gameStatus);
55 9
        $this->determineWinner = new DetermineWinner();
56 9
57
58
        $this->notify();
59 9
    }
60
61
    public function getObservers(): SplObjectStorage
62 1
    {
63
        return $this->observers;
64 1
    }
65
66
    public function attach(SplObserver $observer): void
67 1
    {
68
        $this->observers->attach($observer);
69 1
    }
70
71
    public function detach(SplObserver $observer): void
72 1
    {
73
        $this->observers->detach($observer);
74 1
    }
75
76
    public function notify(): void
77 9
    {
78
        foreach ($this->observers as $observer) {
79 9
            $observer->update($this->getGameState());
80
        }
81
    }
82
83
    /**
84
     * Creates a new Game object from a saved game state.
85
     *
86
     * @param array<string, mixed> $gameState The saved game state.
87
     * @return Game The newly created Game object.
88
     */
89
    public static function createFromSavedState(array $gameState): Game
90 3
    {
91
        $game = new Game();
92 3
        $game->deck = FrenchSuitedDeck::createFromSession($gameState['deck']);
93 3
        foreach ($gameState['players'] as $player) {
94 3
            $game->players[$player->getName()] = $player;
95 3
        }
96
        $game->betManager = new BetManager($gameState['pot']);
97 3
        $game->currentPlayer = $gameState['currentPlayer'];
98 3
        $game->setGameStatus($gameState['gameStatus']);
99 3
        return $game;
100 3
    }
101
102
    /**
103
     * Get the current state of the game.
104
     *
105
     * @return array<string, mixed> The game state, including the deck, players, current player, pot, and game status.
106
     */
107
    public function getGameState(): array
108 3
    {
109
        return [
110 3
            'deck' => $this->deck ? $this->deck->getDeck() : null,
111 3
            'players' => $this->players,
112 3
            'currentPlayer' => $this->currentPlayer,
113 3
            'pot' => $this->betManager->getPot(),
114 3
            'gameStatus' => $this->getGameStatus()
115 3
        ];
116 3
    }
117
118
    public function getGameStatus(): string
119 3
    {
120
        return $this->gameStatus;
121 3
    }
122
123
    private function setGameStatus(string $gameStatus): void
124 9
    {
125
        $this->gameStatus = $gameStatus;
126 9
        $this->notify();
127 9
    }
128
129
    /**
130
     * Plays a round of the game based on the provided form data.
131
     *
132
     * @param array<string, string> $formData The form data containing the player's move and action.
133
     * @return void
134
     *
135
     * @throws Exception If the form data is invalid.
136 3
     */
137
    public function playRound(array $formData): void
138
    {
139 3
        $formValidator = new GameFormValidator();
140
        $formValidator->isValidForm($formData);
141 2
142 1
        if ($formData['action'] !== 'restart') {
143
            $this->processMove($formData);
144 1
145
            $winner = $this->determineWinner->getWinner($this->players, $this->currentPlayer);
146 1
147 1
            if ($winner !== null) {
148 1
                $this->betManager->payOut($this->players[$winner]);
149 1
                $this->setGameStatus('ended');
150 1
                $this->notify();
151
                return;
152
            }
153
        }
154 2
155 1
        if ($formData['action'] == 'restart') {
156 1
            $this->reset();
157
            return;
158
        }
159
    }
160 1
161
    public function getWinnerBasedOnHand(): string
162 1
    {
163
        return $this->determineWinner->getWinnerBasedOnHand($this->players);
164
    }
165
166
    /**
167
     * Process the move based on the given form data.
168
     *
169
     * @param array<string, mixed> $formData The form data containing the action and bet (if applicable).
170
     * @return void
171
     * @throws Exception If the action is invalid.
172 1
     */
173
    private function processMove(array $formData): void
174 1
    {
175 1
        $action = $this->players[$this->currentPlayer] instanceof HumanPlayer ?
176 1
            $formData['action'] :
177 1
                ($this->players[$this->currentPlayer] instanceof AiPlayerInterface ?
178 1
                    $this->players[$this->currentPlayer]->makeMove() :
179
                    throw new Exception('Invalid player type.'));
180 1
        switch ($action) {
181 1
            case 'hit':
182 1
                $this->dealCard();
183 1
                break;
184 1
            case 'stand':
185 1
                $this->players[$this->currentPlayer]->stand();
186 1
                $this->notify();
187 1
                if ($this->hasNextPlayer()) {
188
                    $this->nextPlayer();
189 1
                }
190
                break;
191
            case 'bet':
192
                $this->betManager->setPot($formData['bet'], $this->players);
193
                $this->notify();
194
                break;
195
            default:
196
                throw new Exception('Invalid action.');
197
        }
198
    }
199 1
200
    private function reset(): void
201 1
    {
202
        if (!$this->isRestartable()) {
203
            throw new Exception('Cannot restart when there are money in the pot.');
204 1
        }
205 1
        $this->setGameStatus('ongoing');
206 1
        $this->deck = FrenchSuitedDeck::create();
207 1
        $deckShuffler = new DeckShuffler(new Randomizer());
208 1
        $deckShuffler->shuffle($this->deck);
0 ignored issues
show
Bug introduced by
It seems like $this->deck can also be of type null; however, parameter $deck of App\Model\DeckShuffler::shuffle() does only seem to accept App\Model\DeckOfCards, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

208
        $deckShuffler->shuffle(/** @scrutinizer ignore-type */ $this->deck);
Loading history...
209 1
        foreach ($this->players as $player) {
210
            $player->resetHand();
211 1
            $player->resetStanding();
212 1
        }
213
        $this->currentPlayer = 'human';
214
        $this->notify();
215 1
    }
216
217 1
    public function isRestartable(): bool
218
    {
219
        return $this->betManager->getPot() === null;
220
    }
221
222
    /**
223
     * Get the players of the game.
224
     *
225 1
     * @return array<Player> The array of players.
226
     */
227 1
    public function getPlayers(): array
228
    {
229
        return $this->players;
230
    }
231
232
    // /**
233
    //  * Validates the form data.
234
    //  *
235
    //  * @param array<string, string> $formData The form data to validate.
236
    //  *
237 3
    //  * @throws Exception If an invalid form name is found.
238
    //  */
239 3
    // private function isValidForm(array $formData): void
240 3
    // {
241 3
    //     $formKeys = array_keys($formData);
242 1
    //     foreach ($formKeys as $key) {
243
    //         if (!in_array($key, self::VALID_FORM_NAMES)) {
244
    //             throw new Exception('Invalid form name.');
245
    //         }
246
247
    //         // ignorantly accepting all values...
248
    //     }
249 1
    // }
250
251 1
    public function dealCard(): void
252
    {
253
        if ($this->deck === null) {
254 1
            throw new Exception('Cannot deal cards from a non existing deck.');
255 1
        }
256
        $this->players[$this->currentPlayer]->addCardsToHand($this->deck->cardCollection->drawCards(1));
257
        $this->notify();
258 1
    }
259
260 1
    public function getCurrentPlayer(): string
261
    {
262
        return $this->currentPlayer;
263 1
    }
264
265 1
    public function nextPlayer(): void
266
    {
267
        if (!$this->hasNextPlayer()) {
268 1
            throw new Exception('No next player.');
269 1
        }
270 1
        $playerIndex = array_search($this->currentPlayer, array_keys($this->players));
271 1
        $nextPlayerIndex = $playerIndex + 1;
272
        $this->currentPlayer = array_keys($this->players)[$nextPlayerIndex];
273
        $this->notify();
274 1
    }
275
276 1
    private function hasNextPlayer(): bool
277 1
    {
278
        $playerIndex = array_search($this->currentPlayer, array_keys($this->players));
279
        return $playerIndex < count($this->players) - 1;
280
    }
281
}
282