-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathServer.cs
670 lines (592 loc) · 34.5 KB
/
Server.cs
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
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
using System.Net;
using System.Net.WebSockets;
using System.Text;
using Newtonsoft.Json;
namespace liveorlive_server {
public class Server {
readonly WebApplication app;
readonly List<Client> connectedClients = [];
GameData gameData = new();
readonly GameLog gameLog = new();
readonly Chat chat = new();
public Server() {
this.app = WebApplication.CreateBuilder().Build();
this.app.UseWebSockets();
// app.MapGet("/", () => new Microsoft.AspNetCore.Mvc.JsonResult("Test complete, can connect"));
this.app.MapGet("/", async context => {
// Make sure all incoming requests are WebSocket requests, otherwise send 400
if (context.WebSockets.IsWebSocketRequest) {
// Get the request and pass it off to our handler
using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
await this.ClientConnection(webSocket, context.Connection.Id);
} else {
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
}
});
}
public async Task Start(string url, int port) {
await this.app.RunAsync($"http://{url}:{port}");
}
// All clients are held in here, and this function only exits when they disconnect
private async Task ClientConnection(WebSocket webSocket, string ID) {
// Sometimes, clients can get stuck in a bugged closed state. This will attempt to purge them.
// Two phase to avoid looping over while removing it
List<Client> clientsToRemove = [];
foreach (Client c in this.connectedClients) {
if (c.webSocket.State == WebSocketState.Closed) {
clientsToRemove.Add(c);
}
}
foreach (Client c in clientsToRemove) {
this.connectedClients.Remove(c);
}
Client client = new(webSocket, this, ID);
this.connectedClients.Add(client);
// Constantly check for messages
var buffer = new byte[1024 * 4];
try {
while (true) {
WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
// Server only supports stringified JSON
if (result.MessageType == WebSocketMessageType.Text) {
// Decode it to an object and pass it off
string message = Encoding.UTF8.GetString(buffer, 0, result.Count);
try {
IClientPacket packet = JsonConvert.DeserializeObject<IClientPacket>(message, new PacketJSONConverter())!;
await this.PacketReceived(client, packet);
} catch (JsonSerializationException e) {
await Console.Out.WriteLineAsync($"ERROR: Got malformed packet! \n{e.Message}\n");
}
} else if (result.MessageType == WebSocketMessageType.Close || webSocket.State == WebSocketState.Aborted) {
await webSocket.CloseAsync(
result.CloseStatus ?? WebSocketCloseStatus.InternalServerError,
result.CloseStatusDescription, CancellationToken.None);
break;
}
}
} catch (WebSocketException) {
// Abormal disconnection, finally block has us covered
} finally {
this.connectedClients.Remove(client);
await this.HandlePlayerDisconnect(client);
client.OnDisconnect();
}
}
private async Task HandlePlayerDisconnect(Client client) {
if (client.player == null) {
return;
}
// If they're the host, try to pass that status on to someone else
// If they don't have a player assigned, don't bother
if (client.player.username == this.gameData.host) {
if (this.connectedClients.Any(client => client.player != null)) {
// Guarunteed to exist due to above condition, safe to use !
Player newHost = this.connectedClients[0].player!;
this.gameData.host = newHost.username;
await this.Broadcast(new HostSetPacket { username = this.gameData.host });
} else {
this.gameData.host = null;
}
}
// Tell everyone they left for UI updating purposes
await this.Broadcast(new PlayerLeftPacket { username = client.player.username });
client.player.inGame = false;
// If the game hasn't started, just remove them entirely
if (!this.gameData.gameStarted) {
Player? playerToRemove = this.gameData.GetPlayerByUsername(client.player.username);
if (playerToRemove != null) {
this.gameData.players.Remove(playerToRemove);
}
} else {
// If there is only one actively connected player and the game is in progress, end it
if (this.connectedClients.Where(client => client.player != null).Count() <= 1) {
await Console.Out.WriteLineAsync("Everyone has left the game. Ending with no winner.");
await this.EndGame();
// Otherwise, if the current turn left, make them forfeit their turn
} else if (client.player != null && client.player.username == this.gameData.CurrentTurn) {
await this.ForfeitTurn(client.player);
}
}
}
private async Task PacketReceived(Client sender, IClientPacket packet) {
await Console.Out.WriteLineAsync($"Received packet from {sender}: {packet}");
// Before the player is in the game but after they're connected
if (sender.player == null) {
switch (packet) {
case JoinGamePacket joinGamePacket:
// Check max length
if (joinGamePacket.username.Length > 20) {
await sender.SendMessage( new PlayerJoinRejectedPacket { reason = "That username is too long. Please choose another." });
return;
} else if (joinGamePacket.username.Length < 3) {
await sender.SendMessage(new PlayerJoinRejectedPacket { reason = "That username is too short. Please choose another." });
return;
}
// Check if the username was available
bool usernameTaken = this.gameData.players.Any(player => player.username == joinGamePacket.username);
if (!usernameTaken) {
sender.player = new Player(joinGamePacket.username, false, this.gameData.gameStarted);
this.gameData.players.Add(sender.player);
// If it's not, check if the username is still logged in. If so, error, if not, assume it's the player logging back in
} else {
Player takenPlayer = this.gameData.players.First(player => player.username == joinGamePacket.username);
if (takenPlayer.inGame) {
await sender.SendMessage(new PlayerJoinRejectedPacket { reason = "That username is already taken. Please choose another." });
return;
} else {
sender.player = takenPlayer;
}
}
// Either the client is new, or they're taking an existing player object
// Either way, we're good to go at this point
sender.player.inGame = true;
await this.Broadcast(new PlayerJoinedPacket { player = sender.player });
// If they're the first player, mark them as the host
if (this.gameData.GetActivePlayers().Count == 1) {
// TODO make SetHostPacket send a chat message
await this.Broadcast(new HostSetPacket { username = sender.player.username });
this.gameData.host = sender.player.username;
}
break;
default:
throw new Exception($"Invalid packet type (without player instance) of \"{packet.packetType}\". Did you forget to implement this?");
}
} else {
switch (packet) {
case SendNewChatMessagePacket sendNewChatMessagePacket:
ChatMessage message = this.chat.AddMessage(sender.player, sendNewChatMessagePacket.content);
await this.Broadcast(new NewChatMessageSentPacket { message = message });
break;
case ChatMessagesRequestPacket getChatMessagesPacket:
await sender.SendMessage(new ChatMessagesSyncPacket { messages = this.chat.GetMessages() });
break;
case GameLogMessagesRequestPacket getGameLogMessagesPacket:
await sender.SendMessage(new GameLogMessagesSyncPacket { messages = this.gameLog.GetMessages() });
break;
case GameDataRequestPacket gameInfoRequestPacket:
await sender.SendMessage(new GameDataSyncPacket { gameData = this.gameData });
break;
case StartGamePacket startGamePacket:
if (sender.player.username == this.gameData.host) {
if (this.gameData.gameStarted) {
await sender.SendMessage(new ActionFailedPacket { reason = "The game is already started. How did you even manage that?" });
} else if (this.connectedClients.Count >= 2 && this.gameData.GetActivePlayers().Count >= 2) {
await this.StartGame();
} else {
await sender.SendMessage(new ActionFailedPacket { reason = "There needs to be at least 2 players to start a game" });
}
}
break;
case KickPlayerPacket kickPlayerPacket:
if (sender.player.username == this.gameData.host) {
if (sender.player.username == kickPlayerPacket.username) {
await sender.SendMessage(new ActionFailedPacket { reason = "Why are you trying to kick yourself? Sounds painful." });
return;
}
// Search for the correct client and kick them
Client? clientToKick = this.connectedClients.Find(client => client.player != null && client.player.username == kickPlayerPacket.username);
// Ignore so we don't crash trying to kick a ghost player
if (clientToKick == null || clientToKick.player == null) {
return;
}
Player target = clientToKick.player;
// End this players turn (checks for game end)
await this.PostActionTransition(true);
// Eliminate them to handle adjusting turn order (have to do this after otherwise we skip two players)
this.gameData.EliminatePlayer(target);
// Discard the player entirely since they're likely not welcome back
this.gameData.players.Remove(target);
// Send currentTurn to avoid a game data sync (otherwise UI doesn't work properly)
await this.Broadcast(new PlayerKickedPacket { username = target.username, currentTurn = this.gameData.CurrentTurn });
await this.SendGameLogMessage($"{target.username} has been kicked.");
// Actually disconnected them, which runs handleClientDisconnect
// This removes the client from connectedClients, and checks for game end or host transference (though host transfer should never occur on kick since the host cannot kick themselves)
await clientToKick.webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "playerKicked", new CancellationToken());
}
break;
case ShootPlayerPacket shootPlayerPacket:
// Make sure it's the players turn
if (sender.player == this.gameData.GetCurrentPlayerForTurn()) {
Player? target = this.gameData.GetPlayerByUsername(shootPlayerPacket.target);
if (target == null) {
await sender.SendMessage(new ActionFailedPacket { reason = "Invalid player for steal item target" });
} else {
bool isTurnEndingMove = await this.ShootPlayer(sender.player, target);
await this.PostActionTransition(isTurnEndingMove);
}
}
break;
case UseSkipItemPacket useSkipItemPacket:
if (sender.player == this.gameData.GetCurrentPlayerForTurn()) {
Player? target = this.gameData.GetPlayerByUsername(useSkipItemPacket.target);
await this.UseSkipItem(sender, target);
}
break;
case UseDoubleDamageItemPacket useDoubleDamageItemPacket:
if (sender.player == this.gameData.GetCurrentPlayerForTurn()) {
await this.UseDoubleDamageItem(sender);
}
break;
case UseCheckBulletItemPacket useCheckBulletItemPacket:
if (sender.player == this.gameData.GetCurrentPlayerForTurn()) {
await this.UseCheckBulletItem(sender);
}
break;
case UseRebalancerItemPacket useRebalancerItemPacket:
if (sender.player == this.gameData.GetCurrentPlayerForTurn()) {
await this.UseRebalancerItem(sender, useRebalancerItemPacket.ammoType);
}
break;
case UseAdrenalineItemPacket useAdrenalineItemPacket:
if (sender.player == this.gameData.GetCurrentPlayerForTurn()) {
await this.UseAdrenalineItem(sender);
}
break;
case UseAddLifeItemPacket useAddLifeItemPacket:
if (sender.player == this.gameData.GetCurrentPlayerForTurn()) {
await this.UseAddLifeItem(sender);
}
break;
case UseQuickshotItemPacket useQuickshotItemPacket:
if (sender.player == this.gameData.GetCurrentPlayerForTurn()) {
await this.UseQuickshotItem(sender);
}
break;
case UseStealItemPacket useStealItemPacket:
if (sender.player == this.gameData.GetCurrentPlayerForTurn()) {
Player? target = this.gameData.GetPlayerByUsername(useStealItemPacket.target);
await this.UseStealItem(sender, target, useStealItemPacket.item, useStealItemPacket.ammoType, useStealItemPacket.skipTarget);
}
break;
default:
throw new Exception($"Invalid packet type (with player instance) of \"{packet.packetType}\". Did you forget to implement this?");
}
}
}
private async Task<bool> UseSkipItem(Client sender, Player? target, bool checkForItem = true) {
if (sender.player == null) {
await sender.SendMessage(new ActionFailedPacket { reason = "You... don't exist?" });
return false;
}
if (target == null) {
await sender.SendMessage(new ActionFailedPacket { reason = "Invalid player for steal item target" });
return false;
}
if (target.isSkipped) {
await sender.SendMessage(new ActionFailedPacket { reason = $"{target.username} has already been skipped!" });
return false;
}
if (!checkForItem || sender.player.items.Remove(Item.SkipPlayerTurn)) {
await this.Broadcast(new SkipItemUsedPacket { target = target.username });
await this.SendGameLogMessage($"{target.username} has been skipped by {sender.player.username}");
target.isSkipped = true;
return true;
} else {
await sender.SendMessage(new ActionFailedPacket { reason = "You don't have a Skip item!" });
return false;
}
}
private async Task<bool> UseDoubleDamageItem(Client sender, bool checkForItem = true) {
if (sender.player == null) {
await sender.SendMessage(new ActionFailedPacket { reason = "You... don't exist?" });
return false;
}
if (this.gameData.damageForShot != 1) {
await sender.SendMessage(new ActionFailedPacket { reason = "You've already used a Double Damage item for this shot!" });
return false;
}
if (!checkForItem || sender.player.items.Remove(Item.DoubleDamage)) {
await this.Broadcast(new DoubleDamageItemUsedPacket());
await this.SendGameLogMessage($"{sender.player.username} has used a Double Damage item");
this.gameData.damageForShot = 2;
return true;
} else {
await sender.SendMessage(new ActionFailedPacket { reason = "You don't have a Double Damage item!" });
return false;
}
}
private async Task<bool> UseCheckBulletItem(Client sender, bool checkForItem = true) {
if (sender.player == null) {
await sender.SendMessage(new ActionFailedPacket { reason = "You... don't exist?" });
return false;
}
if (!checkForItem || sender.player.items.Remove(Item.CheckBullet)) {
AmmoType peekResult = this.gameData.PeekAmmoFromChamber();
await sender.SendMessage(new CheckBulletItemResultPacket { result = peekResult });
await this.Broadcast(new CheckBulletItemUsedPacket());
await this.SendGameLogMessage($"{sender.player.username} has used a Chamber Check item");
return true;
} else {
await sender.SendMessage(new ActionFailedPacket { reason = "You don't have a Chamber Check item!" });
return false;
}
}
private async Task<bool> UseRebalancerItem(Client sender, AmmoType ammoType, bool checkForItem = true) {
if (sender.player == null) {
await sender.SendMessage(new ActionFailedPacket { reason = "You... don't exist?" });
return false;
}
if (!checkForItem || sender.player.items.Remove(Item.Rebalancer)) {
int count = this.gameData.AddAmmoToChamberAndShuffle(ammoType);
await this.Broadcast(new RebalancerItemUsedPacket { ammoType = ammoType, count = count });
await this.SendGameLogMessage($"{sender.player.username} has used a Rebalancer item and added {count} {ammoType.ToString().ToLower()} rounds");
return true;
} else {
await sender.SendMessage(new ActionFailedPacket { reason = "You don't have a Rebalancer item!" });
return false;
}
}
private async Task<bool> UseAdrenalineItem(Client sender, bool checkForItem = true) {
if (sender.player == null) {
await sender.SendMessage(new ActionFailedPacket { reason = "You... don't exist?" });
return false;
}
if (!checkForItem || sender.player.items.Remove(Item.Adrenaline)) {
int result = new Random().Next(2) * 2 - 1; // Coin flip
await this.Broadcast(new AdrenalineItemUsedPacket { result = result });
await this.SendGameLogMessage($"{sender.player.username} has used an Adrenaline item and {(result > 0 ? "gained" : "lost")} a life");
sender.player.lives += result;
await this.CheckAndEliminatePlayer(sender.player);
// This is only here to handle game end (no shot was taken so a round won't be started, and it won't move to the next turn)
await this.PostActionTransition(false);
return true;
} else {
await sender.SendMessage(new ActionFailedPacket { reason = "You don't have an Adrenaline item!" });
return false;
}
}
// Can't be stolen, don't need to worry about not checking
private async Task<bool> UseAddLifeItem(Client sender) {
if (sender.player == null) {
await sender.SendMessage(new ActionFailedPacket { reason = "You... don't exist?" });
return false;
}
if (sender.player.items.Remove(Item.AddLife)) {
await this.Broadcast(new AddLifeItemUsedPacket());
await this.SendGameLogMessage($"{sender.player.username} has used a +1 Life item");
sender.player.lives++;
return true;
} else {
await sender.SendMessage(new ActionFailedPacket { reason = "You don't have a +1 Life item!" });
return false;
}
}
private async Task<bool> UseQuickshotItem(Client sender, bool checkForItem = true) {
if (sender.player == null) {
await sender.SendMessage(new ActionFailedPacket { reason = "You... don't exist?" });
return false;
}
if (sender.player == null) {
await sender.SendMessage(new ActionFailedPacket { reason = "You... don't exist?" });
return false;
}
if (this.gameData.quickshotEnabled) {
await sender.SendMessage(new ActionFailedPacket { reason = "You've already used a Quickshot item for this turn!" });
return true;
}
if (!checkForItem || sender.player.items.Remove(Item.Quickshot)) {
await this.Broadcast(new QuickshotItemUsedPacket());
await this.SendGameLogMessage($"{sender.player.username} has used a Quickshot item");
this.gameData.quickshotEnabled = true;
return true;
} else {
await sender.SendMessage(new ActionFailedPacket { reason = "You don't have a Quickshot item!" });
return false;
}
}
// Can't be stolen, don't need to worry about not checking
private async Task<bool> UseStealItem(Client sender, Player? target, Item item, AmmoType? ammoType, string? skipTargetUsername) {
if (sender.player == null) {
await sender.SendMessage(new ActionFailedPacket { reason = "You... don't exist?" });
return false;
}
if (target == null) {
await sender.SendMessage(new ActionFailedPacket { reason = "Invalid player for steal item target" });
return false;
}
// Ensure nullable parameters aren't null when they shouldn't be
if (item == Item.Rebalancer && ammoType == null) {
await sender.SendMessage(new ActionFailedPacket { reason = "ERROR: ammoType was null when stealing Rebalancer. Please raise a GitHub issue." });
return false;
} else if (item == Item.SkipPlayerTurn && skipTargetUsername == null) {
await sender.SendMessage(new ActionFailedPacket { reason = "ERROR: skipTarget was null when stealing Skip. Please raise a GitHub issue." });
}
Player? skipTarget = this.gameData.GetPlayerByUsername(skipTargetUsername);
if (item == Item.SkipPlayerTurn && skipTarget == null) {
await sender.SendMessage(new ActionFailedPacket { reason = "Invalid player for skip target" });
return false;
}
// Not Remove since we only remove if it the child item was a success
if (sender.player.items.Contains(Item.StealItem)) {
bool useSuccess = item switch {
// skipTarget/ammoType is definitely not null (checks above), is safe
Item.SkipPlayerTurn => await this.UseSkipItem(sender, skipTarget!, false),
Item.DoubleDamage => await this.UseDoubleDamageItem(sender, false),
Item.CheckBullet => await this.UseCheckBulletItem(sender, false),
Item.Rebalancer => await this.UseRebalancerItem(sender, (AmmoType)ammoType!, false),
Item.Adrenaline => await this.UseAdrenalineItem(sender, false),
Item.Quickshot => await this.UseQuickshotItem(sender, false),
_ => false
};
if (useSuccess) {
sender.player.items.Remove(Item.StealItem);
target.items.Remove(item);
await this.Broadcast(new StealItemUsedPacket { target = target.username, item = item });
// Let each item function handle printing logs
// await this.sendGameLogMessage($"{sender.player.username} has used a Pickpocket item and stole {(item == Item.Adrenaline ? "an" : "a")} {item.ToString().ToLower()} item from {target.username}");
return true;
}
} else {
await sender.SendMessage(new ActionFailedPacket { reason = "You don't have a Pickpocket item!" });
}
return false;
}
private async Task<bool> ShootPlayer(Player shooter, Player target) {
AmmoType shot = this.gameData.PullAmmoFromChamber();
await this.Broadcast(new PlayerShotAtPacket { target = target.username, ammoType = shot, damage = this.gameData.damageForShot });
if (shooter != target) {
await this.SendGameLogMessage($"{shooter.username} shot {target.username} with a {shot.ToString().ToLower()} round.");
} else {
await this.SendGameLogMessage($"{shooter.username} shot themselves with a {shot.ToString().ToLower()} round.");
}
bool isTurnEndingMove = true;
// If it's a live round, regardless of who, the turn is over
if (shot == AmmoType.Live) {
target.lives -= this.gameData.damageForShot;
bool wasEliminated = await this.CheckAndEliminatePlayer(target);
if (wasEliminated && GameData.LOOTING && shooter != target) {
shooter.items.AddRange(target.items);
await this.SyncGameData(); // TODO do this proper... somehow
} else if (wasEliminated && GameData.VENGEANCE && shooter != target) {
if (target.isSkipped) {
// Should probably be a different packet, but this is fine
await this.Broadcast(new SkipItemUsedPacket { target = shooter.username });
shooter.isSkipped = true;
}
}
} else if (target == shooter) { // Implied it was a blank round
isTurnEndingMove = false;
}
this.gameData.damageForShot = 1;
// If they had a quickshot item, don't end their turn no matter what
if (this.gameData.quickshotEnabled) {
isTurnEndingMove = false;
this.gameData.quickshotEnabled = false;
}
// In case this triggers a round end, we pass along whether or not it was a turn ending shot, so that when a new round starts, it's still that players turn
return isTurnEndingMove;
}
private async Task<bool> CheckAndEliminatePlayer(Player player) {
if (player.lives <= 0) {
this.gameData.EliminatePlayer(player);
await this.SendGameLogMessage($"{player.username} has been eliminated.");
return true;
}
return false;
}
private async Task PostActionTransition(bool isTurnEndingMove) {
// Check for game end (if there's one player left standing)
// Make sure the game is still going in case this triggers twice
if (this.gameData.players.Count(player => player.isSpectator == false) <= 1 && this.gameData.gameStarted) {
await this.EndGame();
return;
}
// Check for round end
if (this.gameData.GetAmmoInChamberCount() <= 0) {
await this.StartNewRound();
}
if (isTurnEndingMove) {
await this.NextTurn();
}
}
// Player shoots themselves once and does not get to go again if it was blank
private async Task ForfeitTurn(Player player) {
await this.ShootPlayer(player, player);
await this.PostActionTransition(true);
}
private async Task StartGame() {
// Gives items, trigger refresh
this.gameData.StartGame();
await this.Broadcast(new GameStartedPacket());
this.gameLog.Clear();
await this.Broadcast(new GameLogMessagesSyncPacket { messages = this.gameLog.GetMessages() });
await this.StartNewRound();
await this.NextTurn();
}
private async Task NextTurn() {
// Send the turn end packet for the previous player (if there was one) automaticaly
if (this.gameData.CurrentTurn != null) {
await this.Broadcast(new TurnEndedPacket { username = this.gameData.CurrentTurn });
}
Player playerForTurn = this.gameData.StartNewTurn();
await this.Broadcast(new TurnStartedPacket { username = playerForTurn.username });
if (playerForTurn.isSkipped) {
await this.SendGameLogMessage($"{playerForTurn.username} has been skipped.");
playerForTurn.isSkipped = false;
await this.NextTurn();
}
// If the player left the game, have them shoot themselves and move on
if (!playerForTurn.inGame) {
await this.ForfeitTurn(playerForTurn);
}
}
private async Task StartNewRound() {
// Items are dealt out here
List<int> ammoCounts = this.gameData.NewRound();
await this.SendGameLogMessage($"A new round has started. The chamber has been loaded with {ammoCounts[0]} live round{(ammoCounts[0] != 1 ? "s" : "")} and {ammoCounts[1]} blank{(ammoCounts[1] != 1 ? "s" : "")}.");
// Auto-apply +1 Life items
foreach (Player player in this.gameData.players) {
int extraLives = player.items.RemoveAll(item => item == Item.AddLife);
player.lives += extraLives;
if (extraLives > 0) {
await this.SendGameLogMessage($"{player.username} has been given {extraLives} {(extraLives > 1 ? "lives" : "life")} from items");
}
}
await this.Broadcast(new NewRoundStartedPacket {
players = this.gameData.players,
liveCount = ammoCounts[0],
blankCount = ammoCounts[1]
});
}
private async Task EndGame() {
// There's almost certainly at least one player left when this runs (used to just wipe if there was nobody left, but that situation requires 2 people to DC at once which I don't care enough to account for)
Player? possibleWinner = this.gameData.players.Find(player => player.lives >= 1);
string winner = possibleWinner != null ? possibleWinner.username : "nobody";
// Copy any data we may need (like players)
GameData newGameData = new() {
players = this.gameData.players
.Where(player => player.inGame)
.Select(player => {
player.isSpectator = false;
player.isSkipped = false;
player.items.Clear();
player.lives = Player.DEFAULT_LIVES;
return player;
})
.ToList(),
host = this.gameData.host
};
this.gameData = newGameData;
await this.SyncGameData();
await this.SendGameLogMessage($"The game has ended. The winner is {winner}.");
}
private async Task SyncGameData() {
await this.Broadcast(new GameDataSyncPacket { gameData = this.gameData });
}
private async Task SendGameLogMessage(string content) {
GameLogMessage message = new(content);
this.gameLog.AddMessage(message);
await this.Broadcast(new NewGameLogMessageSentPacket {
message = message
});
}
private async Task Broadcast(IServerPacket packet) {
await Console.Out.WriteLineAsync($"Broadcasting {JsonConvert.SerializeObject(packet)}");
foreach (Client client in this.connectedClients) {
if (client.player?.inGame ?? false) {
await client.SendMessage(packet, false);
}
}
}
}
}