Handle disconnects, add restart flow and UI fixes

Server: Make DisconnectedPlayer async, await hub notifications, add logging, and delay scheduled deletion (2s). Harden ScheduleGameDeletion with try/catch and only destroy/send GameDestroyed when game is not running or has no players.

Client: Add restart-game flow — expose restartGame on OnlineGame, propagate event from GameEndedMenu (adds local restarted state and disables button), and hook restart handling + player disconnect on unmount in OnlineMode. Also conditionally show bot switch in GameCreationMenu and include replayGameCode in GameEnded interface. Remove automatic reconnect on SignalR connection.

Overall: Improves robustness around player disconnects and adds a UI/logic path for restarting games.
This commit is contained in:
Jonas
2026-03-12 23:18:58 +01:00
parent 0e4fb5d828
commit 7a073b6fff
6 changed files with 61 additions and 18 deletions
+25 -13
View File
@@ -131,27 +131,28 @@ public class GameManager(IGameRepository gameRepository, IHubContext<GameHubSock
}
}
public Task DisconnectedPlayer(string playerConnectionId)
public async Task DisconnectedPlayer(string playerConnectionId)
{
var game = gameRepository.GetOneByConnectionId(playerConnectionId);
if (game == null || game.State == GameState.Ended)
return Task.CompletedTask;
return;
var player = game.GetPlayerByConnectionId(playerConnectionId);
if (player == null)
return Task.CompletedTask;
return;
game.RemovePlayer(playerConnectionId);
game.EndGame();
hubContext.Clients.Group(game.Id).SendAsync("GameEnded", new
Console.WriteLine("Play has Disconnected");
await hubContext.Clients.Group(game.Id).SendAsync("GameEnded", new
{
Method = "PlayerDisconnected",
Player = player
});
ScheduleGameDeletion(game.Id, TimeSpan.FromSeconds(0));
return Task.CompletedTask;
ScheduleGameDeletion(game.Id, TimeSpan.FromSeconds(2));
}
private static int[][] ConvertField(int[,]? field)
@@ -180,14 +181,25 @@ public class GameManager(IGameRepository gameRepository, IHubContext<GameHubSock
{
_ = Task.Run(async () =>
{
await Task.Delay(delay);
var g = gameRepository.GetOne(gameId);
if (g != null && g.State != GameState.Running)
try
{
gameRepository.Destroy(gameId);
await Task.Delay(delay);
await hubContext.Clients.Group(gameId).SendAsync("GameDestroyed");
var g = gameRepository.GetOne(gameId);
if (g == null)
return;
if (g.State != GameState.Running || g.Players.Count == 0)
{
gameRepository.Destroy(gameId);
await hubContext.Clients.Group(gameId).SendAsync("GameDestroyed");
}
Console.WriteLine($"Scheduled deletion of game {gameId} executed. {gameRepository.GetAll().Count}");
}
catch (Exception ex)
{
Console.WriteLine($"Error deleting game {gameId}: {ex}");
}
});
}
+1
View File
@@ -34,6 +34,7 @@ function gameFieldPreset(x: number, y: number) {
required
></v-text-field>
<v-switch
v-if="settingsProp.botPlayer2 != null"
v-model="settingsProp.botPlayer2"
:label="`${settingsProp.playerName2} ist ein Bot`"
color="primary"
+13 -3
View File
@@ -1,12 +1,16 @@
<script setup lang="ts">
import { computed } from 'vue';
import { computed, ref } from 'vue';
import type { GameEnded } from '@/scripts/logic/signalR/GameConnection';
defineEmits(['restartGame']);
const emit = defineEmits(['restartGame']);
const props = defineProps<{ gameEndedInformation: GameEnded | null }>();
const restarted = ref<boolean>(false);
const message = computed(() => {
if(restarted.value) {
return 'Das Spiel wird neugestartet...';
}
switch (props.gameEndedInformation?.method) {
case 'PlayerDisconnected':
return `Bei dem Spieler ${props.gameEndedInformation.player?.name} ist die Verbindung abgebrochen :(`;
@@ -18,6 +22,12 @@ const message = computed(() => {
return '';
}
});
function restartClicked() {
emit('restartGame');
restarted.value = true;
}
</script>
<template>
@@ -28,7 +38,7 @@ const message = computed(() => {
<v-divider class="mb-5 mt-5"></v-divider>
<div class="d-flex align-center justify-space-evenly ma-4 w-100">
<v-btn color="red" @click="$router.push('/')" rounded="xl"> Abbrechen </v-btn>
<v-btn color="primary" @click="$emit('restartGame')" rounded="xl"> Spiel Neustarten </v-btn>
<v-btn color="primary" @click="restartClicked()" rounded="xl" :disabled="restarted"> Spiel Neustarten </v-btn>
</div>
</v-sheet>
</v-container>
+11 -1
View File
@@ -9,7 +9,7 @@ import type { GameSettings } from '@/scripts/interfaces/GameSettings';
import type JoinGameObject from '@/scripts/interfaces/JoinGameObject';
import OnlineGame from '@/scripts/logic/onlineMode/OnlineGame';
import type { GameEnded } from '@/scripts/logic/signalR/GameConnection';
import { ref } from 'vue';
import { onUnmounted, ref } from 'vue';
enum CurrentState {
CreateOrJoinSelection,
@@ -63,6 +63,15 @@ async function createGame() {
async function tryToJoin() {
await game.value.joinGame(joiningModel.value);
}
async function restartGame() {
if(!gameEndedInformation.value?.replayGameCode) return;
await game.value.restartGame(gameEndedInformation.value.replayGameCode);
}
onUnmounted(async () => {
await game.value.player.disconnect();
});
</script>
<template>
@@ -89,6 +98,7 @@ async function tryToJoin() {
<GameEndedMenu
:game-ended-information="gameEndedInformation"
v-else-if="currentState === CurrentState.EndScreen"
@restart-game="restartGame()"
></GameEndedMenu>
<div id="game" v-else>
@@ -92,6 +92,16 @@ class OnlineGame {
this.gameState?.currentTurn == 1 ? this.gameState.players[0]?.name : this.gameState?.players[1]?.name;
this.currentDescription = getRandomMovePhrase(playerName ?? "Spieler");
}
async restartGame(replayGameCode: any) {
const code = Number(replayGameCode.value); // Proxy → primitive number
await this.player.disconnect();
await this.joinGame({
gameCode: code.toString(),
playerName: this.player.playerName ?? 'Spieler',
failed: false,
});
}
}
export default OnlineGame;
@@ -18,6 +18,7 @@ export interface Player {
export interface GameEnded {
method: string;
player?: Player | null;
replayGameCode?: number | null;
}
export interface GameIdentifier {
@@ -41,7 +42,6 @@ class GameConnection {
constructor() {
this.connection = new signalR.HubConnectionBuilder()
.withUrl('/api/gamehub')
.withAutomaticReconnect()
.build();
this.connection.on('GameCreated', (payload: GameIdentifier) => {