DominoGame::reset()   A
last analyzed

Complexity

Conditions 4
Paths 3

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 9
c 1
b 0
f 0
dl 0
loc 17
rs 9.9666
cc 4
nc 3
nop 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Arp\DominoGame;
6
7
use Arp\DominoGame\Exception\DominoGameException;
8
use Arp\DominoGame\Exception\InvalidArgumentException;
9
use Arp\DominoGame\Value\Board;
10
use Arp\DominoGame\Value\Domino;
11
use Arp\DominoGame\Value\DominoCollection;
12
use Arp\DominoGame\Value\Player;
13
use Arp\DominoGame\Value\PlayerCollection;
14
use Psr\Log\LoggerInterface;
15
16
/**
17
 * @author  Alex Patterson <[email protected]>
18
 * @package Arp\DominoGame
19
 */
20
final class DominoGame
21
{
22
    /**
23
     * @var Board
24
     */
25
    private Board $board;
26
27
    /**
28
     * Collection of all the players playing the game.
29
     *
30
     * @var PlayerCollection|Player[]
31
     */
32
    private PlayerCollection $players;
33
34
    /**
35
     * A collection of Dominoes that are available to be picked by players.
36
     *
37
     * @var DominoCollection
38
     */
39
    private DominoCollection $deck;
40
41
    /**
42
     * A PSR logger that will record the game actions
43
     *
44
     * @var LoggerInterface
45
     */
46
    private LoggerInterface $logger;
47
48
    /**
49
     * @param PlayerCollection $players
50
     * @param LoggerInterface  $logger
51
     * @param int              $maxTileSize
52
     *
53
     * @throws DominoGameException If the game cannot be created
54
     */
55
    public function __construct(PlayerCollection $players, LoggerInterface $logger, int $maxTileSize)
56
    {
57
        $this->logger = $logger;
58
        $this->board = new Board();
59
60
        $this->reset($players, $maxTileSize);
61
    }
62
63
    /**
64
     * Deal a new set of dominoes to the players. This will remove any existing dominoes from the players.
65
     *
66
     * @param int $handSize The number of dominoes to deal to each player.
67
     *
68
     * @throws DominoGameException
69
     */
70
    public function deal(int $handSize): void
71
    {
72
        if ($handSize < 1) {
73
            throw new DominoGameException('Hand sizes must be a minimum of 1');
74
        }
75
76
        $deckCount = $this->deck->count();
77
        $playerCount = $this->players->count();
78
79
        if ($deckCount < ($handSize * $playerCount)) {
80
            throw new DominoGameException(
81
                sprintf(
82
                    'The hand size %d exceeds the maximum permissible for current deck size of %d',
83
                    $handSize,
84
                    $deckCount
85
                )
86
            );
87
        }
88
89
        // Ensure that we randomise the deck before dealing a domino to each player in turn
90
        $this->deck->shuffle();
91
92
        $this->logger->info(sprintf('Deck shuffled: %s', (string)$this->deck));
93
        $this->logger->info(
94
            sprintf('Dealing a hand size of %d dominoes to %d players', $handSize, $playerCount)
95
        );
96
97
        foreach ($this->deck as $domino) {
98
            foreach ($this->players->getOrderedByLowestCount() as $player) {
99
                if ($player->getHandCount() < $handSize) {
100
                    $player->addToHand($domino);
101
                    $this->deck->removeElement($domino);
102
103
                    $this->logger->info(
104
                        sprintf('\'%s\' was dealt domino \'%s\'', (string)$player, (string)$domino)
105
                    );
106
107
                    // Move on to next domino
108
                    continue 2;
109
                }
110
            }
111
        }
112
    }
113
114
    /**
115
     * Run the game and return the winner.
116
     *
117
     * @param int $handSize
118
     *
119
     * @return Player
120
     *
121
     * @throws DominoGameException
122
     * @throws InvalidArgumentException
123
     */
124
    public function run(int $handSize): ?Player
125
    {
126
        $this->deal($handSize);
127
        $this->logSummary();
128
129
        do {
130
            $winner = $this->takeTurns();
131
        } while (null === $winner);
132
133
        return $winner;
134
    }
135
136
    /**
137
     * Reset the game scores to zero and prepare a new boneyard so a new game can be played.
138
     *
139
     * @param PlayerCollection|Player[] $players
140
     * @param int                       $maxTileSize
141
     *
142
     * @throws DominoGameException If the game cannot be reset
143
     */
144
    public function reset(PlayerCollection $players, int $maxTileSize): void
145
    {
146
        $playerCount = $players->count();
147
148
        if ($playerCount < 2 || $playerCount > 4) {
149
            throw new DominoGameException(
150
                sprintf('There must be a minimum of 1 and a maximum of 4 players; %d provided', $playerCount)
151
            );
152
        }
153
154
        $this->logger->info('Resetting game');
155
        foreach ($players as $player) {
156
            $player->getHand()->removeElements(null);
157
        }
158
159
        $this->deck = $this->createDominoCollection($maxTileSize);
160
        $this->players = $players;
161
    }
162
163
    /**
164
     * @return PlayerCollection
165
     */
166
    public function getPlayers(): PlayerCollection
167
    {
168
        return $this->players;
169
    }
170
171
    /**
172
     * @return DominoCollection
173
     */
174
    public function getDeck(): DominoCollection
175
    {
176
        return $this->deck;
177
    }
178
179
    /**
180
     * Each player executes a single turn. The first placement on the board will require the
181
     * player with the highest double to place a single piece.
182
     *
183
     * @return Player|null
184
     *
185
     * @throws InvalidArgumentException
186
     * @throws DominoGameException
187
     */
188
    private function takeTurns(): ?Player
189
    {
190
        $players = $this->board->isEmpty()
191
            ? $this->players->getOrderedByHighestDouble()
192
            : $this->players;
193
194
        foreach ($players as $player) {
195
            $winner = $this->takeTurn($player);
196
197
            if (null !== $winner) {
198
                $this->logger->info(
199
                    sprintf(
200
                        '\'%s\' is the winner with the lowest hand value of \'%d\'',
201
                        (string)$winner,
202
                        $winner->getHandValue()
203
                    )
204
                );
205
                return $winner;
206
            }
207
208
            if (null === $winner && 0 === $player->getHandCount()) {
209
                $this->logger->info(
210
                    sprintf(
211
                        '\'%s\' is the winner with 0 dominoes left to play',
212
                        (string)$player
213
                    )
214
                );
215
                return $player;
216
            }
217
        }
218
219
        $this->logSummary();
220
        return null;
221
    }
222
223
    /**
224
     * Perform a single turn for the provided player.
225
     *
226
     * @param Player $player
227
     *
228
     * @return Player|null
229
     *
230
     * @throws DominoGameException
231
     * @throws InvalidArgumentException
232
     */
233
    private function takeTurn(Player $player): ?Player
234
    {
235
        $domino = $player->getDominoWithMatchingTile($this->board);
236
        if (null === $domino) {
237
            $this->logger->info(sprintf('\'%s\' was unable to find a matching domino', (string)$player));
238
239
            // If there are no more dominoes to pick, we need to resolve the winner
240
            if ($this->deck->isEmpty()) {
241
                $this->logger->info(
242
                    sprintf(
243
                        '\'%s\' has no more dominoes available to pick from the deck. '
244
                        . 'Determining the winner from the lowest total score from each players hand',
245
                        (string)$player
246
                    )
247
                );
248
                return $this->players->getWithLowestHandValue();
249
            }
250
251
            // We could not find a matching tile to place so we have to pick up another
252
            $randomPick = $this->deck->pickRandom();
253
            $player->addToHand($randomPick);
254
255
            $this->logger->info(
256
                sprintf(
257
                    '\'%s\' has picked domino %s from the deck',
258
                    (string)$player,
259
                    (string)$randomPick
260
                )
261
            );
262
            return null;
263
        }
264
265
        $this->logger->info(sprintf('\'%s\' has placed domino %s', (string)$player, (string)$domino));
266
267
        $this->board->place($domino);
268
        $player->removeFromHand($domino);
269
270
        return null;
271
    }
272
273
    /**
274
     * Generate the Domino collection (the 'boneyard') that players will pick from.
275
     *
276
     * @param int $maxTileSize
277
     *
278
     * @return DominoCollection
279
     */
280
    private function createDominoCollection(int $maxTileSize): DominoCollection
281
    {
282
        $collection = new DominoCollection();
283
284
        for ($x = 0; $x <= $maxTileSize; $x++) {
285
            for ($y = $x; $y <= $maxTileSize; $y++) {
286
                $collection->add(new Domino($x, $y));
287
            }
288
        }
289
290
        return $collection;
291
    }
292
293
    /**
294
     * Log a summary of the current state of the board and players hands
295
     */
296
    private function logSummary(): void
297
    {
298
        if (!$this->deck->isEmpty()) {
299
            $this->logger->info('Remaining tiles: ' . (string)$this->deck);
300
        }
301
302
        foreach ($this->players as $player) {
303
            $this->logger->info(
304
                sprintf('\'%s\' hand: %s', (string)$player, (string)$player->getHand())
305
            );
306
        }
307
    }
308
}
309