Support player names and online join flow

Move SignalR group join into GameManager and add support for player names and improved online join flow.

- Moved Groups.AddToGroupAsync call out of GameHubSocket into GameManager (hubContext.Groups.AddToGroupAsync) so group membership is handled when adding players to a game.
- GUI: added player name input to GameJoinMenu and updated JoinGameObject to include playerName and use string gameCode.
- OnlineMode & LocalMode: constructors updated to instantiate games without passing settings; removed WaitingForOpponent state and cleaned up event bindings.
- OnlineGame: rewrote to handle onGameCreated/onGameJoined/onGameStarted/onFieldUpdated/onGameEnded/onError, added joinGame validation (expects 6-digit code), join flow (connect + join), state updates, drop forwarding to player.drop(gameId, index) and place description updates.
- LocalGame: constructor signature simplified to no-arg.

These changes centralize group management, add player identification, validate join codes, and improve game state/event handling for online play.
This commit is contained in:
2026-03-08 21:45:05 +01:00
committed by Jonas
parent acae815a2a
commit 4826760d73
8 changed files with 100 additions and 17 deletions
-2
View File
@@ -42,8 +42,6 @@ public class GameHubSocket(IGameManager gameManager) : Hub
return; return;
} }
await Groups.AddToGroupAsync(Context.ConnectionId, result);
await Clients.Caller.SendAsync("GameJoined", new await Clients.Caller.SendAsync("GameJoined", new
{ {
GameId = result, GameId = result,
+3
View File
@@ -3,6 +3,7 @@ using API.Models.DataClasses;
using API.Models.Game; using API.Models.Game;
using API.Repository.GameRepo; using API.Repository.GameRepo;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
using System.Text.RegularExpressions;
namespace API.Services.GameManager; namespace API.Services.GameManager;
@@ -27,6 +28,8 @@ public class GameManager(IGameRepository gameRepository, IHubContext<GameHubSock
return null; return null;
} }
await hubContext.Groups.AddToGroupAsync(player.ConnectionId, game.Id);
if (game.Players.Count == 2) if (game.Players.Count == 2)
{ {
game.StartGame(); game.StartGame();
+5
View File
@@ -17,6 +17,11 @@ let joiningModel = defineModel<JoinGameObject>('joinGameObject', {
dein Freund gegeben hat. dein Freund gegeben hat.
</h3> </h3>
<v-divider class="mb-5 mt-5"></v-divider> <v-divider class="mb-5 mt-5"></v-divider>
<v-text-field
v-model="joiningModel.playerName"
label="Name vom Spieler 1"
required
></v-text-field>
<v-otp-input <v-otp-input
length="6" length="6"
v-model="joiningModel.gameCode" v-model="joiningModel.gameCode"
+1 -1
View File
@@ -21,7 +21,7 @@ let settings = ref<GameSettings>({
}); });
var currentState = ref<CurrentState>(CurrentState.CreatingGame); var currentState = ref<CurrentState>(CurrentState.CreatingGame);
const game = ref<LocalGame>(new LocalGame(settings.value)); const game = ref<LocalGame>(new LocalGame());
const gameField = ref<number[][]>([]); const gameField = ref<number[][]>([]);
const currentSelectionIndex = ref<number | null>(null); const currentSelectionIndex = ref<number | null>(null);
const gameEndedInformation = ref<GameEnded | null>(null); const gameEndedInformation = ref<GameEnded | null>(null);
+20 -6
View File
@@ -1,5 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import CreateOrJoinMenu from '@/components/CreateOrJoinMenu.vue'; import CreateOrJoinMenu from '@/components/CreateOrJoinMenu.vue';
import Field from '@/components/game/Field.vue';
import InfoField from '@/components/game/InfoField.vue';
import GameCreationMenu from '@/components/GameCreationMenu.vue'; import GameCreationMenu from '@/components/GameCreationMenu.vue';
import GameEndedMenu from '@/components/GameEndedMenu.vue'; import GameEndedMenu from '@/components/GameEndedMenu.vue';
import GameJoinMenu from '@/components/GameJoinMenu.vue'; import GameJoinMenu from '@/components/GameJoinMenu.vue';
@@ -13,7 +15,6 @@ enum CurrentState {
CreateOrJoinSelection, CreateOrJoinSelection,
CreatingGame, CreatingGame,
JoiningGame, JoiningGame,
WaitingForOpponent,
Game, Game,
EndScreen, EndScreen,
} }
@@ -24,11 +25,12 @@ let settings = ref<GameSettings>({
}); });
let joiningModel = ref<JoinGameObject>({ let joiningModel = ref<JoinGameObject>({
playerName: 'Spieler 2',
failed: false, failed: false,
}); });
var currentState = ref<CurrentState>(CurrentState.CreateOrJoinSelection); var currentState = ref<CurrentState>(CurrentState.CreateOrJoinSelection);
const game = ref<OnlineGame>(new OnlineGame(settings.value)); const game = ref<OnlineGame>(new OnlineGame());
const gameField = ref<number[][]>([]); const gameField = ref<number[][]>([]);
const currentSelectionIndex = ref<number | null>(null); const currentSelectionIndex = ref<number | null>(null);
const gameEndedInformation = ref<GameEnded | null>(null); const gameEndedInformation = ref<GameEnded | null>(null);
@@ -46,11 +48,21 @@ game.value.onGameCreated = (gameCode: string) => {
settings.value.message = `Das Spiel ist erstellt, deine Freund kann über folgendem Code Beitreten: ${gameCode}`; settings.value.message = `Das Spiel ist erstellt, deine Freund kann über folgendem Code Beitreten: ${gameCode}`;
}; };
game.value.onGameStarted = () => {
currentState.value = CurrentState.Game;
};
game.value.onGameJoinedFailed = () => {
joiningModel.value.failed = true;
};
async function createGame() { async function createGame() {
await game.value.createGame(settings.value); await game.value.createGame(settings.value);
} }
async function tryToJoin() {} async function tryToJoin() {
await game.value.joinGame(joiningModel.value);
}
</script> </script>
<template> <template>
@@ -62,14 +74,16 @@ async function tryToJoin() {}
<GameCreationMenu <GameCreationMenu
v-model:settings="settings" v-model:settings="settings"
@create-game="createGame()" @create-game="createGame"
v-else-if="currentState === CurrentState.CreatingGame || currentState === CurrentState.WaitingForOpponent" v-else-if="
currentState === CurrentState.CreatingGame
"
/> />
<GameJoinMenu <GameJoinMenu
:joinGameObject="joiningModel" :joinGameObject="joiningModel"
v-else-if="currentState === CurrentState.JoiningGame" v-else-if="currentState === CurrentState.JoiningGame"
@join="tryToJoin()" @join="tryToJoin"
/> />
<GameEndedMenu <GameEndedMenu
+2 -1
View File
@@ -1,4 +1,5 @@
export default interface JoinGameObject { export default interface JoinGameObject {
failed: boolean; failed: boolean;
gameCode?: number; playerName: string;
gameCode?: string;
} }
+1 -1
View File
@@ -15,7 +15,7 @@ class LocalGame {
public onGameStateChanged?: (gameState: GameInformationDto | undefined) => void; public onGameStateChanged?: (gameState: GameInformationDto | undefined) => void;
public onGameEnded?: (gameEndedInfo: GameEnded) => void; public onGameEnded?: (gameEndedInfo: GameEnded) => void;
constructor(settings: GameSettings) { constructor() {
this.player1 = new GameConnection(); this.player1 = new GameConnection();
this.player2 = new GameConnection(); this.player2 = new GameConnection();
+66 -4
View File
@@ -1,5 +1,10 @@
import type { GameSettings } from '@/scripts/interfaces/GameSettings'; import type { GameSettings } from '@/scripts/interfaces/GameSettings';
import GameConnection, { type GameEnded, type GameInformationDto } from '../signalR/GameConnection'; import GameConnection, {
type GameEnded,
type GameIdentifier,
type GameInformationDto,
} from '../signalR/GameConnection';
import type JoinGameObject from '@/scripts/interfaces/JoinGameObject';
class OnlineGame { class OnlineGame {
public player: GameConnection; public player: GameConnection;
@@ -8,17 +13,51 @@ class OnlineGame {
public gameState: GameInformationDto | undefined; public gameState: GameInformationDto | undefined;
public onGameCreated?: (gameCode: string) => void; public onGameCreated?: (gameCode: string) => void;
public onGameJoinedFailed?: (gameCode: string) => void; public onGameJoinedFailed?: () => void;
public onGameStarted?: () => void;
public onGameStateChanged?: (gameState: GameInformationDto | undefined) => void; public onGameStateChanged?: (gameState: GameInformationDto | undefined) => void;
public onGameEnded?: (gameEndedInfo: GameEnded) => void; public onGameEnded?: (gameEndedInfo: GameEnded) => void;
constructor(settings: GameSettings) { constructor() {
this.player = new GameConnection(); this.player = new GameConnection();
this.player.onGameCreated = (gameIdentifier) => { this.player.onGameCreated = (gameIdentifier) => {
this.gameId = gameIdentifier.gameId; this.gameId = gameIdentifier.gameId;
this.onGameCreated?.(gameIdentifier.gameCode.toString()); this.onGameCreated?.(gameIdentifier.gameCode.toString());
};
this.player.onGameJoined = (gameIdentifier: GameIdentifier) => {
this.gameId = gameIdentifier.gameId;
};
this.player.onGameStarted = (gameInfo: GameInformationDto) => {
console.log("Game started!", gameInfo);
this.gameState = gameInfo;
this.onGameStarted?.();
this.onGameStateChanged?.(this.gameState);
this.changePlaceDescription();
};
this.player.onFieldUpdated = (currentState: GameInformationDto) => {
this.updateState(currentState);
this.changePlaceDescription();
};
this.player.onGameEnded = (gameEndedInfo: GameEnded) => {
this.onGameEnded?.(gameEndedInfo);
};
this.player.onError = (errorMessage: string) => {
switch (errorMessage) {
case 'Spiel Existiert nicht!':
this.onGameJoinedFailed?.();
this.player.disconnect();
break;
default:
console.error('Unhandled error:', errorMessage);
} }
};
} }
async createGame(settings: GameSettings) { async createGame(settings: GameSettings) {
@@ -26,8 +65,31 @@ class OnlineGame {
await this.player.createGame(settings.fieldSize); await this.player.createGame(settings.fieldSize);
} }
async drop(index: number){ async joinGame(joinObject: JoinGameObject) {
if (joinObject.gameCode?.toString().split('').length != 6 || parseInt(joinObject.gameCode) < 0) {
this.onGameJoinedFailed?.();
return;
}
await this.player.connect(joinObject.playerName);
await this.player.joinGame(parseInt(joinObject.gameCode));
}
async updateState(newState: GameInformationDto) {
if (this.gameState) {
this.gameState = newState;
this.onGameStateChanged?.(this.gameState);
}
}
async drop(index: number) {
await this.player.drop(this.gameId, index);
}
changePlaceDescription() {
const playerName =
this.gameState?.currentTurn == 1 ? this.gameState.players[0]?.name : this.gameState?.players[1]?.name;
this.currentDescription = `${playerName} ist dran mit setzen!`;
} }
} }