-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathGame.java
More file actions
540 lines (472 loc) · 19.7 KB
/
Game.java
File metadata and controls
540 lines (472 loc) · 19.7 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
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
package logic;
import logic.utils.Card;
import logic.utils.Deck;
import logic.utils.players.Player;
import networking.protocol.Command;
import networking.protocol.Error;
import networking.server.ClientHandler;
import networking.server.ServerGame;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;
public class Game {
// Constants
public static final int DEFAULT_HAND_SIZE = 7;
public static final int DEFUSES_COUNT = 6;
public static final int NOPE_DELAY = 10;
public static final ScheduledExecutorService DELAYED_EXECUTOR = Executors.newSingleThreadScheduledExecutor();
// Game data
protected final List<ClientHandler> clientHandlers;
protected final Deck deck;
// Players for turn logic
protected ClientHandler currentClient;
private ClientHandler previousClient;
// Game turns utils
private final Stack<Card> actionStack;
private final Stack<Card> skippedStack;
private Card nopedCard;
private Card lastCard;
private ClientHandler favorTarget;
private ScheduledFuture<?> delayedAction;
private Thread delayedThread;
private boolean awaitUserInteraction;
private boolean canNope;
public Game(List<ClientHandler> clientHandlers) {
this.clientHandlers = clientHandlers;
this.deck = new Deck();
this.actionStack = new Stack<>();
this.skippedStack = new Stack<>();
this.awaitUserInteraction = false;
}
/**
* Sends a command to all connected client handlers.
*
* @param command The command to be sent.
* @param args Additional arguments for the command, if any.
*/
private void broadcast(Command command, String... args) {
clientHandlers.forEach((client) -> client.sendCommand(command, args));
}
/**
* Starts the game.
*
*/
public void startGame() {
dealInitialCards();
deck.insertExplosionsAndDefuses(clientHandlers.size());
pickStartingPlayer();
broadcast(Command.NOTIFY, "The game has been started!");
nextTurn();
System.out.println("A game has started!");
}
/**
* Deals the initial cards to each client handler's player.
* Adds cards from the deck to the player's hand, including a DEFUSE card.
* Sends a command to each client handler to update their player's cards and the number of cards left in the deck.
*/
private void dealInitialCards() {
for (ClientHandler clientHandler : clientHandlers) {
Player player = clientHandler.getPlayer();
for (int i = 1; i < DEFAULT_HAND_SIZE; i++) {
Card card = deck.drawCard();
player.addCard(card);
}
player.addCard(Card.DEFUSE);
}
}
/**
* Randomly select the starting player
*/
private void pickStartingPlayer() {
Random random = new Random();
int startingPlayerIndex = random.nextInt(clientHandlers.size());
currentClient = clientHandlers.get(startingPlayerIndex);
}
/**
* Handles the move made by a client in the game.
*
* @param clientHandler The client handler making the move.
* @param card The card played by the client.
*/
public void doMove(ClientHandler clientHandler, Card card) {
// Handle unknown card and when the user has to send a specific action or if it doesn't have a card
if (card == null || awaitUserInteraction || !clientHandler.getPlayer().hasCard(card)) {
clientHandler.sendError(Error.E3);
return;
}
if (card.equals(Card.NOPE)) { // let player play nope card in any circumstances
playNope(clientHandler);
return;
}
if (clientHandler != currentClient) { // handle not player turn
clientHandler.sendError(Error.E6);
return;
}
playCard(clientHandler, card);
}
/**
* Sends updates to all connected client handlers.
* Updates include sending the player's cards and the number of cards left in the deck.
*/
private void sendAllUpdates() {
clientHandlers.forEach(this::sendPlayerUpdate);
}
/**
* Sends a command to a specific client handler to update the player's cards and the number of cards left in the deck.
*
* @param clientHandler The client handler to send the command to.
*/
private void sendPlayerUpdate(ClientHandler clientHandler) {
clientHandler.sendCommand(
Command.PLAYERS,
clientHandler.getPlayer().getCards(),
lastCard != null ? lastCard.name(): "",
deck.size() + ""
);
}
/**
* Updates the game state after playing the "NOPE" card.
*
* @param clientHandler The client handler of the player who played the "NOPE" card.
*/
private void playNope(ClientHandler clientHandler) {
clientHandler.getPlayer().playCard(Card.NOPE);
// prevent current player receiving double updates
if (!clientHandler.equals(currentClient) || !canNope) clientHandler.sendCommand(Command.EXECUTEDMOVE);
if (!canNope) return;
if (nopedCard != null) {
actionStack.push(nopedCard);
lastCard = nopedCard;
nopedCard = null;
} else if (!actionStack.isEmpty() && !actionStack.peek().equals(Card.DEFUSE)) {
// nope previous person turn
if (actionStack.peek().equals(Card.DRAW)) {
actionStack.pop();
broadcast(Command.NEXT, previousClient.getPlayer().getName(), previousClient.getPlayer().getName());
currentClient.sendCommand(Command.EXECUTEDMOVE);
currentClient = previousClient;
previousClient = null;
// reverse first attack action
if (!actionStack.isEmpty() && actionStack.peek().equals(Card.ATTACK) && actionStack.size() == 1) {
actionStack.pop();
actionStack.push(Card.DRAW);
actionStack.push(Card.ATTACK);
}
}
if (!skippedStack.isEmpty() && Card.SKIP.equals(lastCard)) {
actionStack.push(skippedStack.pop());
actionStack.push(Card.SKIP);
}
nopedCard = actionStack.pop();
lastCard = Card.NOPE;
}
doEffects();
}
/**
* Discards the current "NOPE" card, if any.
*/
private void discardNoped() {
if (nopedCard == null) return;
nopedCard = null;
}
/**
* Cancels the delayed task if it exists.
*
* @param doCard Flag indicating whether to execute the effects of the top card.
*/
private void cancelDelayedTask(boolean doCard) {
if (delayedAction != null) {
// wait for termination if its running
if (delayedAction.getDelay(TimeUnit.MILLISECONDS) <= 0) {
broadcast(Command.NOTIFY, "Too late...");
try {
if (!Thread.currentThread().equals(delayedThread)) delayedAction.get();
} catch (InterruptedException | ExecutionException ignore) {}
delayedAction = null;
return;
}
delayedAction.cancel(false);
if (doCard) doEffects();
else broadcast(Command.NOTIFY, "The card has been cancelled");
delayedAction = null;
}
}
/**
* Plays a card in the game.
*
* @param clientHandler The client handler making the move.
* @param card The card to be played.
*/
private void playCard(ClientHandler clientHandler, Card card) {
if ( // make sure the player doesn't try to play a card when the last card ends their turn
delayedAction != null
&& !actionStack.isEmpty()
&& (actionStack.peek().equals(Card.ATTACK)
|| (actionStack.isEmpty()
&& !skippedStack.isEmpty()
)
)
) clientHandler.sendError(Error.E3);
cancelDelayedTask(true);
lastCard = card;
actionStack.push(card);
clientHandler.getPlayer().playCard(card);
canNope = true;
discardNoped();
doEffects();
}
/**
* Executes the effects of the top card on the action stack.
* This method is private and should only be called from within the {@code Game} class.
*/
private void doEffects() {
if (actionStack.isEmpty()) return;
Card topCard = actionStack.peek();
ClientHandler playingClient = currentClient;
// creates a delayed task to let users nope it if they want
if (delayedAction == null && Card.DELAYED_CARD.contains(topCard)) {
delayedAction = DELAYED_EXECUTOR
.schedule(() -> {
delayedThread = Thread.currentThread();
canNope = false;
this.doEffects();
}, NOPE_DELAY, TimeUnit.SECONDS);
broadcast(
Command.NOTIFY,
playingClient.getPlayer().getName()
+ " is placing the card "
+ topCard.name()
+ " hurry if you want to nope it!"
);
return;
}
cancelDelayedTask(false); // cancel card delay as it necessarily has been played
switch (topCard) {
case SKIP: // skip
actionStack.pop();
if (!actionStack.isEmpty()) skippedStack.push(actionStack.pop());
// process separately from empty stack because it needs confirmation
if (!actionStack.isEmpty()) {
doEffects();
return;
}
break;
case DEFUSE: // Defuse exploding kitten or pre-defuse in case an exploding kitten is drawn
actionStack.pop();
if (!actionStack.peek().equals(Card.EXPLODING_KITTEN)) {
actionStack.push(Card.DEFUSE); // Pre-Defuse
break;
}
actionStack.pop();
playingClient.getPlayer().addCard(Card.EXPLODING_KITTEN);
playingClient.sendCommand(Command.EXPLODINGKITTEN);
return;
case SHUFFLE: // shuffle the deck
deck.shuffle();
actionStack.pop();
break;
case SEE_THE_FUTURE: // send the 3 three cards of the deck to the current player
actionStack.pop();
playingClient.sendCommand(
Command.NOTIFY,
"Here are the 3 top cards on the deck: \\n"
+ deck.peekTopCards(3)
.stream()
.map(Card::name)
.collect(Collectors.joining(", "))
);
break;
case FAVOR: // make another player give the current player a card
if (favorTarget != null) break;
String name = playingClient.getPlayer().getName();
playingClient.sendCommand( // ask player for a target
Command.HAND,
clientHandlers
.stream()
.filter(clientHandler -> !clientHandler.getPlayer().getName().equals(name))
.map(clientHandler -> clientHandler.getPlayer().getName())
.collect(Collectors.joining(", "))
);
awaitUserInteraction = true;
break;
case ATTACK: // make the next player draw two time per attack card on the pile/actionStack
// remove draw card if it's the first attack card in the strike
if (actionStack.size() == 2 && actionStack.get(0).equals(Card.DRAW)) actionStack.remove(Card.DRAW);
// only move to the next player if the attack was done by the current player
if (lastCard.equals(Card.ATTACK)) nextTurn();
break;
case EXPLODING_KITTEN: // Kill the player if no defuse
if (!actionStack.contains(Card.DEFUSE)) {
if (currentClient.getPlayer().hasCard(Card.DEFUSE)) {
actionStack.push(Card.DEFUSE);
playingClient.getPlayer().playCard(Card.DEFUSE);
sendPlayerUpdate(playingClient);
doEffects();
return;
} else {
playingClient.sendCommand(Command.NOTIFY, "You drew an EXPLODING_KITTEN but you don't have a DEFUSE!");
gameOver(playingClient);
actionStack.pop();
}
} else {
broadcast(Command.NOTIFY, playingClient.getPlayer().getName() + " has preemptively defused a kitten that was just drawn");
actionStack.remove(Card.DEFUSE);
actionStack.push(Card.DEFUSE);
doEffects();
return;
}
break;
}
if (actionStack.isEmpty()) nextTurn();
else sendAllUpdates();
if (!List.of(Card.FAVOR, Card.EXPLODING_KITTEN).contains(topCard)) {
if (previousClient == null) {
previousClient = currentClient;
return;
}
playingClient.sendCommand(Command.EXECUTEDMOVE); // confirm move
}
}
/**
* Method for handling the game over event.
*
* @param clientHandler The client handler that triggered the game over event.
*/
public void gameOver(ClientHandler clientHandler) {
broadcast(Command.NOTIFY, clientHandler.getPlayer().getName() + " has lost!");
clientHandlers.remove(clientHandler);
clientHandler.sendCommand(Command.GAMEOVER);
clientHandler.getPlayer().reset();
if (clientHandlers.size() == 1) {
ClientHandler winner = clientHandlers.get(0);
winner.sendCommand(Command.NOTIFY, "You won the game well done");
winner.sendCommand(Command.GAMEOVER);
ServerGame.EndGame();
}
}
/**
* Allows a client to choose a target player for the current action.
*
* @param clientHandler The client handler making the choice.
* @param target The target player's name.
*/
public void chooseTarget(ClientHandler clientHandler, String target) {
if ( // check for target empty, correct action, and different player
favorTarget != null
|| !actionStack.peek().equals(Card.FAVOR)
|| !clientHandler.equals(currentClient)
|| target.equals(currentClient.getPlayer().getName())
) {
clientHandler.sendError(Error.E3);
return;
}
// get targeted client if any
ClientHandler targetedClient = clientHandlers
.stream()
.filter(client -> client.getPlayer().getName().equals(target))
.findFirst()
.orElse(null);
if (targetedClient == null) { // no client targeted
clientHandler.sendError(Error.E4);
return;
}
if (targetedClient.getPlayer().getHand().isEmpty()) { // when the target is out of cards
clientHandler.sendCommand(Command.NOTIFY, target + " is out of cards");
clientHandler.sendCommand(Command.EXECUTEDMOVE);
actionStack.pop();
awaitUserInteraction = false;
return;
}
clientHandler.sendCommand(Command.NOTIFY, "Waiting for " + target + " to choose a card");
favorTarget = targetedClient;
favorTarget.sendCommand(Command.DEMAND, clientHandler.getPlayer().getName()); // send demand to other player
}
/**
* Gives a card from one player's hand to another player's hand.
*
* @param clientHandler The client handler initiating the card transfer.
* @param card The card to be given.
*/
public void giveCard(ClientHandler clientHandler, Card card) {
if ( // check for target empty, correct action, and if he has card
(!actionStack.isEmpty() && !actionStack.peek().equals(Card.FAVOR))
|| !clientHandler.equals(favorTarget)
|| !clientHandler.getPlayer().hasCard(card)
) {
clientHandler.sendError(Error.E3);
return;
}
favorTarget.getPlayer().getHand().remove(card);
currentClient.getPlayer().getHand().add(card);
broadcast(
Command.NOTIFY,
favorTarget.getPlayer().getName()
+ " has given a card to "
+ currentClient.getPlayer().getName()
);
sendPlayerUpdate(favorTarget);
sendPlayerUpdate(currentClient);
clientHandler.sendCommand(Command.EXECUTEDMOVE);
currentClient.sendCommand(Command.EXECUTEDMOVE);
actionStack.pop();
awaitUserInteraction = false;
favorTarget = null;
}
/**
* Updates the game state to the next turn.
*/
public void nextTurn() {
previousClient = currentClient;
currentClient = clientHandlers.get((clientHandlers.indexOf(currentClient) + 1) % clientHandlers.size());
actionStack.push(Card.DRAW);
sendAllUpdates();
broadcast(Command.NEXT, previousClient.getPlayer().getName(), currentClient.getPlayer().getName());
}
/**
* Draws a card for the specified client handler.
*
* @param clientHandler The client handler for whom to draw the card.
*/
public void drawCard(ClientHandler clientHandler) {
if (!currentClient.equals(clientHandler)) {
clientHandler.sendError(Error.E6);
}
discardNoped();
// Draw a card
Card card = deck.drawCard();
if (card.equals(Card.EXPLODING_KITTEN)) { // kills the player if no defuse
actionStack.push(Card.EXPLODING_KITTEN);
doEffects();
} else {
clientHandler.getPlayer().addCard(card);
}
// Do that after drawing a card to make sure preemptive defuses work
actionStack.removeAll(Collections.singleton(Card.DEFUSE)); // remove all preemptive defuses
if (!actionStack.isEmpty()) actionStack.pop(); // consume action shouldn't have anything else than attacks or draw cards on the top
sendPlayerUpdate(clientHandler);
if (card.equals(Card.EXPLODING_KITTEN)) return;
if (actionStack.empty()) nextTurn();
clientHandler.sendCommand(Command.EXECUTEDMOVE); // give client feedback
}
/**
* Places a card in the game deck at the specified index.
*
* @param clientHandler The client handler making the move.
* @param card The card to be placed.
* @param index The index at which to place the card in the deck.
*/
public void place(ClientHandler clientHandler, Card card, int index) {
if (!currentClient.equals(clientHandler)
|| !clientHandler.getPlayer().hasCard(card)
|| index < 0
|| index >= deck.size()
) {
clientHandler.sendError(Error.E3);
}
clientHandler.getPlayer().playCard(card);
deck.insertCardAtPosition(card, index);
sendAllUpdates();
if (actionStack.isEmpty()) nextTurn();
clientHandler.sendCommand(Command.EXECUTEDMOVE); // give client feedback
}
}