Game Logic in Frontend

This commit is contained in:
jhim
2026-03-07 14:05:28 +01:00
committed by Jonas
parent 34f683854b
commit 919bb71f19
8 changed files with 148 additions and 54 deletions
-10
View File
@@ -4,17 +4,7 @@ namespace API.Models.Game;
public readonly struct Coordinates public readonly struct Coordinates
{ {
[JsonConstructor]
public Coordinates(int x, int y)
{
X = x;
Y = y;
}
[JsonPropertyName("x")]
public int X { get; init; } public int X { get; init; }
[JsonPropertyName("y")]
public int Y { get; init; } public int Y { get; init; }
} }
+2
View File
@@ -69,6 +69,8 @@ public class GameManager(IGameRepository gameRepository, IHubContext<GameHubSock
if(game.CurrentTurn != player.PlayerTag) if(game.CurrentTurn != player.PlayerTag)
return; return;
if(game.State != GameState.Running) return;
var result = game.Field.Drop(column, player.PlayerTag); var result = game.Field.Drop(column, player.PlayerTag);
if (result != DropResult.Placed) if (result != DropResult.Placed)
+41
View File
@@ -0,0 +1,41 @@
<script setup lang="ts">
import { computed } from 'vue'
import type { GameEnded } from '@/scripts/logic/signalR/GameConnection'
const props = defineProps<{ gameEndedInformation: GameEnded | null }>()
const message = computed(() => {
switch (props.gameEndedInformation?.method) {
case "PlayerDisconnected":
return `Bei Spieler ${props.gameEndedInformation.player?.name} ist die Verbindung abgebrochen`
case "Draw":
return "Das Spielfeld ist voll und es ist ein Unentschieden"
case "Win":
return `Spieler ${props.gameEndedInformation.player?.name} hat gewonnen!`
default:
return ""
}
})
</script>
<template>
<v-sheet width="100%" class="text-centered pa-2 w-75" rounded>
<h1 class="text-center">Spiel Beendet</h1>
<h3 class="test-center">{{ message }}</h3>
<v-divider class="mb-4"></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('createGame')" rounded="xl">
Spiel Starten
</v-btn>
</div>
</v-sheet>
</template>
<style scoped>
.settingsCat {
margin-top: 5px;
}
</style>
+5 -3
View File
@@ -76,8 +76,8 @@ function selectionUpdate(index: number, active: boolean) {
<style scoped> <style scoped>
.game-container { .game-container {
height: calc(100dvh - var(--v-layout-top));
width: 100%; width: 100%;
height: 100%;
overflow: hidden; overflow: hidden;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -87,8 +87,10 @@ function selectionUpdate(index: number, active: boolean) {
.game-cell { .game-cell {
--cols: v-bind('gameFieldSize.x || 7'); --cols: v-bind('gameFieldSize.x || 7');
--rows: v-bind('gameFieldSize.y || 6'); --rows: v-bind('gameFieldSize.y || 6');
width: min(calc(85vw / var(--cols)), calc((100dvh - var(--v-layout-top)) * 0.85 / var(--rows)));
height: min(calc(85vw / var(--cols)), calc((100dvh - var(--v-layout-top)) * 0.85 / var(--rows))); width: min(calc(80vw / var(--cols)), calc(75dvh / (var(--rows) + 1)));
height: min(calc(80vw / var(--cols)), calc(75dvh / (var(--rows) + 1)));
flex: 0 0 auto; flex: 0 0 auto;
user-select: none; user-select: none;
} }
+14
View File
@@ -0,0 +1,14 @@
<script setup lang="ts">
const props = defineProps<{ msg: string, currenlyWaiting?: boolean }>()
</script>
<template>
<h1 v-if="!currenlyWaiting || currenlyWaiting == null">{{msg}}</h1>
</template>
<style scoped>
#infoField {
width: min(50vw, 700px);
text-align: center;
}
</style>
+44 -12
View File
@@ -4,10 +4,14 @@ import GameCreationMenu from '@/components/GameCreationMenu.vue';
import type { GameSettings } from '@/scripts/interfaces/GameSettings'; import type { GameSettings } from '@/scripts/interfaces/GameSettings';
import LocalGame from '@/scripts/logic/localMode/LocalGame'; import LocalGame from '@/scripts/logic/localMode/LocalGame';
import Field from '@/components/game/Field.vue'; import Field from '@/components/game/Field.vue';
import InfoField from '@/components/game/InfoField.vue';
import type { GameEnded } from '@/scripts/logic/signalR/GameConnection';
import GameEndedMenu from '@/components/GameEndedMenu.vue';
enum CurrentState { enum CurrentState {
CreatingGame, CreatingGame,
Game, Game,
EndScreen,
} }
let settings = ref<GameSettings>({ let settings = ref<GameSettings>({
@@ -20,32 +24,60 @@ var currentState = ref<CurrentState>(CurrentState.CreatingGame);
const game = ref<LocalGame>(new LocalGame(settings.value)); const game = ref<LocalGame>(new LocalGame(settings.value));
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);
game.value.onGameStateChanged = (gameState) => { game.value.onGameStateChanged = (gameState) => {
gameField.value = gameState?.currentField ?? []; gameField.value = gameState?.currentField ?? [];
}; };
game.value.onGameEnded = (gameEndedInfo) => {
gameEndedInformation.value = gameEndedInfo;
currentState.value = CurrentState.EndScreen;
}
async function startGame() { async function startGame() {
await game.value.start(); await game.value.start(settings.value);
currentState.value = CurrentState.Game; currentState.value = CurrentState.Game;
} }
</script> </script>
<template> <template>
<v-container <v-container class="d-flex align-center justify-center fill-height" v-if="currentState === CurrentState.CreatingGame">
class="d-flex align-center justify-center fill-height"
v-if="currentState === CurrentState.CreatingGame"
>
<GameCreationMenu v-model:settings="settings" @create-game="startGame()" /> <GameCreationMenu v-model:settings="settings" @create-game="startGame()" />
</v-container> </v-container>
<Field <v-container class="d-flex align-center justify-center fill-height"
v-else v-else-if="currentState === CurrentState.EndScreen">
:game-state="gameField" <GameEndedMenu :game-ended-information="gameEndedInformation"></GameEndedMenu>
v-model:current-selection-index="currentSelectionIndex" </v-container>
@click-on-game-field="game.drop(currentSelectionIndex ?? 1)"
/> <div id="game" v-else>
<div class="game-content">
<Field :game-state="gameField" v-model:current-selection-index="currentSelectionIndex"
@click-on-game-field="game.drop(currentSelectionIndex ?? 1)" />
<InfoField :msg="game.currentDescription"></InfoField>
</div>
</div>
</template> </template>
<style scoped></style> <style scoped>
#game {
width: 100%;
min-height: calc(100dvh - var(--v-layout-top));
display: flex;
justify-content: center;
align-items: center;
padding: 16px;
box-sizing: border-box;
}
.game-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
width: 100%;
}
</style>
+30 -17
View File
@@ -2,33 +2,34 @@ import type { GameSettings } from '@/scripts/interfaces/GameSettings';
import GameConnection, { import GameConnection, {
type GameIdentifier, type GameIdentifier,
type GameInformationDto, type GameInformationDto,
type GameEnded,
} from '../signalR/GameConnection'; } from '../signalR/GameConnection';
class LocalGame { class LocalGame {
public _settings: GameSettings;
public player1: GameConnection; public player1: GameConnection;
public player2: GameConnection; public player2: GameConnection;
public currentDescription: string = 'Warte auf Spielaufbau...';
private gameId: string = ''; private gameId: string = '';
public gameState: GameInformationDto | undefined; public gameState: GameInformationDto | undefined;
public onGameStateChanged?: (gameState: GameInformationDto | undefined) => void; public onGameStateChanged?: (gameState: GameInformationDto | undefined) => void;
public onGameEnded?: (gameEndedInfo: GameEnded) => void;
constructor(settings: GameSettings) { constructor(settings: GameSettings) {
this._settings = settings; this.player1 = new GameConnection();
this.player2 = new GameConnection();
this.player1 = new GameConnection(settings.playerName1);
this.player2 = new GameConnection(settings.playerName2 ?? 'Player 2');
this.player1.onGameStarted = (gameInfo: GameInformationDto) => { this.player1.onGameStarted = (gameInfo: GameInformationDto) => {
console.log('Game started for player 1', gameInfo); console.log('Game started for player 1', gameInfo);
this.updateState(gameInfo); this.gameState = gameInfo;
this.onGameStateChanged?.(this.gameState);
this.changePlaceDescription();
}; };
this.player1.onFieldUpdated = (currentState: GameInformationDto) => { this.player1.onFieldUpdated = (currentState: GameInformationDto) => {
if (this.gameState) { this.updateState(currentState);
this.gameState = currentState; this.changePlaceDescription();
this.onGameStateChanged?.(this.gameState);
}
}; };
this.player1.onGameCreated = (gameIdentifier: GameIdentifier) => { this.player1.onGameCreated = (gameIdentifier: GameIdentifier) => {
@@ -39,18 +40,24 @@ class LocalGame {
this.player2.onGameJoined = (gameIdentifier: GameIdentifier) => { this.player2.onGameJoined = (gameIdentifier: GameIdentifier) => {
this.gameId = gameIdentifier.gameId; this.gameId = gameIdentifier.gameId;
}; };
this.player1.onGameEnded = (gameEndedInfo: GameEnded) => {
this.onGameEnded?.(gameEndedInfo);
};
} }
async start() { async start(settings: GameSettings) {
await this.player1.connect(); await this.player1.connect(settings.playerName1);
await this.player2.connect(); await this.player2.connect(settings.playerName2 ?? 'Spieler 2');
await this.player1.createGame(this._settings.fieldSize); await this.player1.createGame(settings.fieldSize);
} }
async updateState(gameInfo: GameInformationDto) { async updateState(newState: GameInformationDto) {
this.gameState = gameInfo; if (this.gameState) {
this.onGameStateChanged?.(this.gameState); this.gameState = newState;
this.onGameStateChanged?.(this.gameState);
}
} }
async drop(index: number) { async drop(index: number) {
@@ -60,6 +67,12 @@ class LocalGame {
this.player2.drop(this.gameId, index); this.player2.drop(this.gameId, index);
} }
} }
changePlaceDescription() {
const playerName =
this.gameState?.currentTurn == 1 ? this.player1.playerName : this.player2.playerName;
this.currentDescription = `${playerName} ist dran mit setzen!`;
}
} }
export default LocalGame; export default LocalGame;
+10 -10
View File
@@ -15,31 +15,30 @@ export interface Player {
playerTag: number; playerTag: number;
} }
export interface GameEndedDto { export interface GameEnded {
method: string, method: string;
player?: Player | null; player?: Player | null;
} }
export interface GameIdentifier{ export interface GameIdentifier {
gameId: string; gameId: string;
gameCode: number; gameCode: number;
} }
class GameConnection { class GameConnection {
private connection: signalR.HubConnection; private connection: signalR.HubConnection;
public playerName: string; public playerName: string = 'Spieler';
public onGameCreated?: (gameIdentifier: GameIdentifier) => void; public onGameCreated?: (gameIdentifier: GameIdentifier) => void;
public onGameJoined?: (gameIdentifier: GameIdentifier) => void; public onGameJoined?: (gameIdentifier: GameIdentifier) => void;
public onGameStarted?: (gameInfo: GameInformationDto) => void; public onGameStarted?: (gameInfo: GameInformationDto) => void;
public onGameInformation?: (gameInfo: GameInformationDto) => void; public onGameInformation?: (gameInfo: GameInformationDto) => void;
public onFieldUpdated?: (currentField: GameInformationDto) => void; public onFieldUpdated?: (currentField: GameInformationDto) => void;
public onGameEnded?: (gameEndedInfo: GameEndedDto) => void; public onGameEnded?: (gameEndedInfo: GameEnded) => void;
public onError?: (error: string) => void; public onError?: (error: string) => void;
public onGameDestroyed?: () => void; public onGameDestroyed?: () => void;
constructor(playerName: string) { constructor() {
this.playerName = playerName;
this.connection = new signalR.HubConnectionBuilder() this.connection = new signalR.HubConnectionBuilder()
.withUrl('/api/gamehub') .withUrl('/api/gamehub')
.withAutomaticReconnect() .withAutomaticReconnect()
@@ -65,7 +64,7 @@ class GameConnection {
this.onFieldUpdated?.(payload); this.onFieldUpdated?.(payload);
}); });
this.connection.on('GameEnded', (payload: GameEndedDto) => { this.connection.on('GameEnded', (payload: GameEnded) => {
this.onGameEnded?.(payload); this.onGameEnded?.(payload);
}); });
@@ -78,7 +77,8 @@ class GameConnection {
}); });
} }
async connect() { async connect(playerName: string) {
this.playerName = playerName;
await this.connection.start(); await this.connection.start();
console.log('Connected'); console.log('Connected');
} }
@@ -95,7 +95,7 @@ class GameConnection {
await this.connection.invoke('RequestGameInformation', gameId); await this.connection.invoke('RequestGameInformation', gameId);
} }
async drop(gameId: string, column: number){ async drop(gameId: string, column: number) {
await this.connection.invoke('Drop', gameId, column); await this.connection.invoke('Drop', gameId, column);
} }
} }