diff --git a/API/Controllers/GameHubSocket.cs b/API/Controllers/GameHubSocket.cs index 2857139..d284cea 100644 --- a/API/Controllers/GameHubSocket.cs +++ b/API/Controllers/GameHubSocket.cs @@ -11,6 +11,12 @@ public class GameHubSocket(IGameManager gameManager) : Hub public async Task CreateGame(string playerName, Coordinates gFs) { + if (gFs.X <= 0 || gFs.Y <= 0) + { + await Clients.Caller.SendAsync("Error", "Ungültige Spielfeldgröße."); + return; + } + var player = new Player(playerName, Context.ConnectionId); var result = _gameManager.CreateGame(gFs, player); @@ -24,7 +30,7 @@ public class GameHubSocket(IGameManager gameManager) : Hub }); } - public async Task JoinGame(int gameCode, string playerName) + public async Task JoinGame(string playerName, int gameCode) { var player = new Player(playerName, Context.ConnectionId); @@ -36,7 +42,7 @@ public class GameHubSocket(IGameManager gameManager) : Hub return; } - await Groups.AddToGroupAsync(Context.ConnectionId, gameCode.ToString()); + await Groups.AddToGroupAsync(Context.ConnectionId, result); await Clients.Caller.SendAsync("GameJoined", new { @@ -50,7 +56,7 @@ public class GameHubSocket(IGameManager gameManager) : Hub await _gameManager.RequestGameInformation(gameId, Context.ConnectionId); } - public async Task Place(string gameId, int column) + public async Task Drop(string gameId, int column) { await _gameManager.Drop(gameId, column, Context.ConnectionId); } @@ -61,4 +67,4 @@ public class GameHubSocket(IGameManager gameManager) : Hub await base.OnDisconnectedAsync(exception); } -} \ No newline at end of file +} diff --git a/API/Models/Game/GameField.cs b/API/Models/Game/GameField.cs index e4c332b..93be1a7 100644 --- a/API/Models/Game/GameField.cs +++ b/API/Models/Game/GameField.cs @@ -1,9 +1,21 @@ -namespace API.Models.Game; +using System.Text.Json.Serialization; -public readonly struct Coordinates(int x, int y) +namespace API.Models.Game; + +public readonly struct Coordinates { - public readonly int X = x; - public readonly int Y = y; + [JsonConstructor] + public Coordinates(int x, int y) + { + X = x; + Y = y; + } + + [JsonPropertyName("x")] + public int X { get; init; } + + [JsonPropertyName("y")] + public int Y { get; init; } } public enum DropResult @@ -146,4 +158,4 @@ public class GameField(Coordinates gFs) { Array.Copy(CurrentField, BackupField, CurrentField.Length); } -} \ No newline at end of file +} diff --git a/API/Models/Game/GameInformationDto.cs b/API/Models/Game/GameInformationDto.cs index 108a2f3..6e8deea 100644 --- a/API/Models/Game/GameInformationDto.cs +++ b/API/Models/Game/GameInformationDto.cs @@ -5,7 +5,7 @@ public string Id { get; set; } public List Players { get; set; } public GameState? State { get; set; } - public int[,] CurrentField { get; set; } + public int[][] CurrentField { get; set; } public int CurrentTurn { get; set; } } } diff --git a/API/Services/GameManager/GameManager.cs b/API/Services/GameManager/GameManager.cs index 446ed13..472209d 100644 --- a/API/Services/GameManager/GameManager.cs +++ b/API/Services/GameManager/GameManager.cs @@ -27,7 +27,15 @@ public class GameManager(IGameRepository gameRepository, IHubContext(); + + var rows = field.GetLength(0); + var cols = field.GetLength(1); + var convertedField = new int[rows][]; + + for (var y = 0; y < rows; y++) + { + convertedField[y] = new int[cols]; + + for (var x = 0; x < cols; x++) + { + convertedField[y][x] = field[y, x]; + } + } + + return convertedField; + } + private void ScheduleGameDeletion(string gameId, TimeSpan delay) { _ = Task.Run(async () => @@ -138,4 +168,4 @@ public class GameManager(IGameRepository gameRepository, IHubContext +import type { GameSettings } from '@/scripts/interfaces/GameSettings'; +import Slider from './Slider.vue'; + +let settingsProp = defineModel('settings', { + required: true, +}); + +defineEmits(['createGame']); + +function gameFieldPreset(x: number, y: number) { + settingsProp.value.fieldSize.x = x; + settingsProp.value.fieldSize.y = y; +} + + + + + diff --git a/GUI/src/components/Slider.vue b/GUI/src/components/Slider.vue new file mode 100644 index 0000000..39aa63b --- /dev/null +++ b/GUI/src/components/Slider.vue @@ -0,0 +1,55 @@ + + + \ No newline at end of file diff --git a/GUI/src/routes/LocalMode.vue b/GUI/src/routes/LocalMode.vue index 7fac94b..817406c 100644 --- a/GUI/src/routes/LocalMode.vue +++ b/GUI/src/routes/LocalMode.vue @@ -1,24 +1,51 @@ - \ No newline at end of file + diff --git a/GUI/src/scripts/interfaces/GameSettings.ts b/GUI/src/scripts/interfaces/GameSettings.ts new file mode 100644 index 0000000..46b6898 --- /dev/null +++ b/GUI/src/scripts/interfaces/GameSettings.ts @@ -0,0 +1,8 @@ +import type { FieldSize } from './FieldSize'; + +export interface GameSettings { + fieldSize: FieldSize; + playerName1: string; + playerName2?: string | null; + aiLevel?: number | null; +} diff --git a/GUI/src/scripts/logic/localMode/LocalGame.ts b/GUI/src/scripts/logic/localMode/LocalGame.ts index f45fcb2..fde452e 100644 --- a/GUI/src/scripts/logic/localMode/LocalGame.ts +++ b/GUI/src/scripts/logic/localMode/LocalGame.ts @@ -1,12 +1,65 @@ -import type { FieldSize } from '@/scripts/interfaces/FieldSize.ts' -import buildFieldArray from '@/scripts/utils/BuildFieldArray' +import type { GameSettings } from '@/scripts/interfaces/GameSettings'; +import GameConnection, { + type GameIdentifier, + type GameInformationDto, +} from '../signalR/GameConnection'; class LocalGame { - public field: number[][] + public _settings: GameSettings; + public player1: GameConnection; + public player2: GameConnection; + private gameId: string = ''; - constructor(fieldSize: FieldSize) { - this.field = buildFieldArray(fieldSize); + public gameState: GameInformationDto | undefined; + public onGameStateChanged?: (gameState: GameInformationDto | undefined) => void; + + constructor(settings: GameSettings) { + this._settings = settings; + + this.player1 = new GameConnection(settings.playerName1); + this.player2 = new GameConnection(settings.playerName2 ?? 'Player 2'); + + this.player1.onGameStarted = (gameInfo: GameInformationDto) => { + console.log('Game started for player 1', gameInfo); + this.gameStarted(gameInfo); + }; + + this.player1.onFieldUpdated = (currentField: number[][]) => { + if (this.gameState) { + this.gameState.currentField = currentField; + this.onGameStateChanged?.(this.gameState); + } + }; + + this.player1.onGameCreated = (gameIdentifier: GameIdentifier) => { + this.gameId = gameIdentifier.gameId; + this.player2.joinGame(gameIdentifier.gameCode); + }; + + this.player2.onGameJoined = (gameIdentifier: GameIdentifier) => { + this.gameId = gameIdentifier.gameId; + }; + } + + async start() { + await this.player1.connect(); + await this.player2.connect(); + + await this.player1.createGame(this._settings.fieldSize); + } + + async gameStarted(gameInfo: GameInformationDto) { + this.gameState = gameInfo; + this.onGameStateChanged?.(this.gameState); + } + + async drop(index: number) { + if (this.gameState?.currentTurn == 1) { + this.player1.drop(this.gameId, index); + } else { + this.player2.drop(this.gameId, index); + } } } -export default LocalGame \ No newline at end of file +export default LocalGame; diff --git a/GUI/src/scripts/logic/signalR/GameConnection.ts b/GUI/src/scripts/logic/signalR/GameConnection.ts index 98a6d2a..221e6a2 100644 --- a/GUI/src/scripts/logic/signalR/GameConnection.ts +++ b/GUI/src/scripts/logic/signalR/GameConnection.ts @@ -1,13 +1,103 @@ +import type { FieldSize } from '@/scripts/interfaces/FieldSize'; import * as signalR from '@microsoft/signalr'; -class GameConnection{ - public connection: signalR.HubConnection - constructor(playerName: string, joinCode: number | undefined) { +export interface GameInformationDto { + id: string | null; + players: Player[]; + state: number; + currentField: number[][]; + currentTurn: number; +} + +export interface Player { + name: string; + connectionId: string; + playerTag: number; +} + +export interface GameEndedDto { + method: string, + player?: Player | null; +} + +export interface GameIdentifier{ + gameId: string; + gameCode: number; +} + +class GameConnection { + private connection: signalR.HubConnection; + public playerName: string; + + public onGameCreated?: (gameIdentifier: GameIdentifier) => void; + public onGameJoined?: (gameIdentifier: GameIdentifier) => void; + public onGameStarted?: (gameInfo: GameInformationDto) => void; + public onGameInformation?: (gameInfo: GameInformationDto) => void; + public onFieldUpdated?: (currentField: number[][]) => void; + public onGameEnded?: (gameEndedInfo: GameEndedDto) => void; + public onError?: (error: string) => void; + public onGameDestroyed?: () => void; + + constructor(playerName: string) { + this.playerName = playerName; this.connection = new signalR.HubConnectionBuilder() - .withUrl("/api/gamehub") + .withUrl('/api/gamehub') .withAutomaticReconnect() .build(); + + this.connection.on('GameCreated', (payload: GameIdentifier) => { + this.onGameCreated?.(payload); + }); + + this.connection.on('GameJoined', (payload: GameIdentifier) => { + this.onGameJoined?.(payload); + }); + + this.connection.on('GameStarted', (payload: GameInformationDto) => { + this.onGameStarted?.(payload); + }); + + this.connection.on('GameInformation', (payload: GameInformationDto) => { + this.onGameInformation?.(payload); + }); + + this.connection.on('FieldUpdated', (currentField: number[][]) => { + this.onFieldUpdated?.(currentField); + }); + + this.connection.on('GameEnded', (payload: GameEndedDto) => { + this.onGameEnded?.(payload); + }); + + this.connection.on('Error', (error: string) => { + this.onError?.(error); + }); + + this.connection.on('GameDestroyed', () => { + this.onGameDestroyed?.(); + }); + } + + async connect() { + await this.connection.start(); + console.log('Connected'); + } + + async createGame(gFs: FieldSize) { + await this.connection.invoke('CreateGame', this.playerName, gFs); + } + + async joinGame(gameCode: number) { + await this.connection.invoke('JoinGame', this.playerName, gameCode); + } + + async requestGameInformation(gameId: string) { + await this.connection.invoke('RequestGameInformation', gameId); + } + + async drop(gameId: string, column: number){ + await this.connection.invoke('Drop', gameId, column); } } -export default GameConnection; \ No newline at end of file +export default GameConnection;