-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathTable.php
More file actions
293 lines (252 loc) · 8.61 KB
/
Copy pathTable.php
File metadata and controls
293 lines (252 loc) · 8.61 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
<?php
declare(strict_types=1);
namespace Likewinter\CardDeck;
/**
* The card table: owns the deck, named hands, and pile for a game session.
*
* Table is the single orchestration point for card movement. Games register
* named hands, draw cards into them, discard back to the pile, and reset
* for a new round — all through this interface. The underlying Stacks are
* private; games interact only through Table's behavioral methods.
*
* Hands are keyed by string identifiers chosen by the game:
* $table->addHand('dealer', new Stack());
* $table->addHand('north', new Stack(capacity: 13));
* $table->draw('north', 5);
*/
class Table
{
private Stack $deck;
private Stack $pile;
/** @var array<string, Stack> */
private array $hands = [];
/**
* @param Stack $deck The draw pile; the table owns it from here on.
* @param DrawMode $drawMode How draw()/drawAll() move cards from the deck into hands.
* @param bool $shuffle Shuffle the deck immediately.
*/
public function __construct(
Stack $deck,
private readonly DrawMode $drawMode = DrawMode::Sequential,
bool $shuffle = false,
) {
$this->deck = $deck;
$this->pile = new Stack();
if ($shuffle) {
$this->deck->shuffle();
}
}
// ── Hand registry ────────────────────────────────────────────────────
/**
* Register a named hand.
*
* @throws \InvalidArgumentException If a hand with this name already exists.
*/
public function addHand(string $name, Stack $hand): void
{
if (array_key_exists($name, $this->hands)) {
throw new \InvalidArgumentException("Hand '{$name}' already exists");
}
$this->hands[$name] = $hand;
}
/**
* Remove a named hand, moving its remaining cards to the pile.
*/
public function removeHand(string $name): void
{
$hand = $this->hand($name);
$hand->moveAllTo($this->pile);
unset($this->hands[$name]);
}
/**
* Returns the named hand.
*
* @throws \InvalidArgumentException If no hand with this name exists.
*/
public function hand(string $name): Stack
{
return $this->hands[$name] ?? throw new \InvalidArgumentException("Hand '{$name}' does not exist");
}
/**
* Whether a hand with this name has been registered.
*/
public function hasHand(string $name): bool
{
return array_key_exists($name, $this->hands);
}
/**
* Names of all registered hands, in registration order.
*
* @return list<string>
*/
public function handNames(): array
{
return array_keys($this->hands);
}
// ── Drawing ──────────────────────────────────────────────────────────
/**
* Draw cards from the deck into a named hand.
* Respects the table's DrawMode (Random draws random cards).
*
* @throws \InvalidArgumentException If the hand does not exist.
* @throws \LogicException If the deck has fewer than $num cards.
*/
public function draw(string $name, int $num = 1): void
{
$hand = $this->hand($name);
if ($num > $this->deck->count()) {
throw new \LogicException('Not enough cards in deck');
}
match ($this->drawMode) {
DrawMode::Random => $this->drawRandomToHand($hand, $num),
DrawMode::Sequential, DrawMode::OneByOne => $this->deck->moveTo($hand, $num),
};
}
/**
* Draw $num cards to every registered hand, respecting DrawMode.
*
* @throws \LogicException If no hands are registered or the deck cannot cover the full deal.
*/
public function drawAll(int $num = 1): void
{
if ($this->hands === []) {
throw new \LogicException('No hands registered');
}
if (($num * count($this->hands)) > $this->deck->count()) {
throw new \LogicException('Not enough cards in deck');
}
match ($this->drawMode) {
DrawMode::OneByOne => $this->drawOneByOne($num),
DrawMode::Sequential => $this->drawSequential($num),
DrawMode::Random => $this->drawRandomAll($num),
};
}
/**
* Draw cards into a named hand until it reaches $target cards,
* or the deck runs out — whichever comes first.
*
* Returns the number of cards actually drawn.
*
* @throws \InvalidArgumentException If the hand does not exist.
*/
#[\NoDiscard]
public function drawUpTo(string $name, int $target): int
{
$hand = $this->hand($name);
$needed = $target - $hand->count();
if ($needed <= 0) {
return 0;
}
$available = min($needed, $this->deck->count());
if ($available > 0) {
$this->draw($name, $available);
}
return $available;
}
// ── Discarding ───────────────────────────────────────────────────────
/**
* Discard specific cards from a named hand to the pile.
* With no cards argument, discards the entire hand.
*
* @throws \InvalidArgumentException If the hand does not exist or does not contain the given cards.
*/
public function discard(string $name, PlayableCard ...$cards): void
{
$hand = $this->hand($name);
if ($cards === []) {
$hand->moveAllTo($this->pile);
return;
}
$hand->moveCardsTo($this->pile, ...$cards);
}
/**
* Collect cards from outside the table (e.g., played trick cards)
* into the pile so reset() can recover them.
*/
public function collectToPile(PlayableCard ...$cards): void
{
$this->pile->addCards(...$cards);
}
// ── Lifecycle ────────────────────────────────────────────────────────
/**
* Return all cards from hands and the pile to the deck.
*/
public function reset(): void
{
foreach ($this->hands as $hand) {
$hand->moveAllTo($this->deck);
}
$this->pile->moveAllTo($this->deck);
}
/**
* Shuffle the deck. Does not affect hands or the pile.
*/
public function shuffle(): void
{
$this->deck->shuffle();
}
// ── Inspection ───────────────────────────────────────────────────────
/**
* Number of cards remaining in the deck.
*/
public function deckCount(): int
{
return $this->deck->count();
}
/**
* Number of cards currently in the pile.
*/
public function pileCount(): int
{
return $this->pile->count();
}
/**
* Peek at cards in the deck without removing them.
*
* @throws \InvalidArgumentException If the deck has fewer than $num cards.
*/
#[\NoDiscard]
public function peekDeck(int $num = 1): Stack
{
return $this->deck->peek($num);
}
/**
* Peek at cards from the bottom of the deck without removing them.
*
* @throws \InvalidArgumentException If the deck has fewer than $num cards.
*/
#[\NoDiscard]
public function peekDeckBottom(int $num = 1): Stack
{
return $this->deck->peekBottom($num);
}
// ── Private draw strategies ──────────────────────────────────────────
private function drawOneByOne(int $num): void
{
for ($i = 0; $i < $num; $i++) {
foreach ($this->hands as $hand) {
$this->deck->moveTo($hand, 1);
}
}
}
private function drawSequential(int $num): void
{
foreach ($this->hands as $hand) {
$this->deck->moveTo($hand, $num);
}
}
private function drawRandomAll(int $num): void
{
for ($i = 0; $i < $num; $i++) {
foreach ($this->hands as $hand) {
$this->drawRandomToHand($hand, 1);
}
}
}
private function drawRandomToHand(Stack $hand, int $num): void
{
for ($i = 0; $i < $num; $i++) {
$this->deck->moveCardsTo($hand, ...$this->deck->peekRandom());
}
}
}