Add bot play, replay creation, and move phrases

Create replay games when a match ends (win or draw) and include ReplayGameCode in the GameEnded payload; adjust deletion timing to keep the replay longer and clean up original games sooner. Update game deletion logic to only destroy non-running games and schedule immediate deletion for quick timeouts. Add optional local bot support (botPlayer2 flag) with automated random-delayed moves and a UI switch in the game creation menu; expose botPlayer2 in GameSettings and LocalMode defaults. Introduce RandomPlaceMessage utility to generate varied turn descriptions and integrate it into LocalGame and OnlineGame. Also include a minor whitespace fix in Game.cs.
This commit is contained in:
Jonas
2026-03-12 22:15:12 +01:00
parent 4826760d73
commit 0e4fb5d828
8 changed files with 151 additions and 12 deletions
+13 -6
View File
@@ -100,27 +100,34 @@ public class GameManager(IGameRepository gameRepository, IHubContext<GameHubSock
var winResult = game.Field.CheckForWin(); var winResult = game.Field.CheckForWin();
if (winResult != 0) if (winResult != 0)
{ {
var newGame = gameRepository.Create(game.Field.GFs);
var winPlayer = game.GetPlayerByTag(winResult); var winPlayer = game.GetPlayerByTag(winResult);
game.EndGame(); game.EndGame();
await hubContext.Clients.Group(game.Id).SendAsync("GameEnded", new await hubContext.Clients.Group(game.Id).SendAsync("GameEnded", new
{ {
Method = "Win", Method = "Win",
Player = winPlayer Player = winPlayer,
ReplayGameCode = newGame.GameCode
}); });
ScheduleGameDeletion(game.Id, TimeSpan.FromSeconds(5)); ScheduleGameDeletion(newGame.Id, TimeSpan.FromSeconds(20));
ScheduleGameDeletion(game.Id, TimeSpan.FromSeconds(10));
} }
if (game.Field.IsFull()) if (game.Field.IsFull())
{ {
var newGame = gameRepository.Create(game.Field.GFs);
game.EndGame(); game.EndGame();
await hubContext.Clients.Group(game.Id).SendAsync("GameEnded", new await hubContext.Clients.Group(game.Id).SendAsync("GameEnded", new
{ {
Method = "Draw" Method = "Draw",
ReplayGameCode = newGame.GameCode
}); });
ScheduleGameDeletion(game.Id, TimeSpan.FromSeconds(5)); ScheduleGameDeletion(newGame.Id, TimeSpan.FromSeconds(15));
ScheduleGameDeletion(game.Id, TimeSpan.FromSeconds(10));
} }
} }
@@ -142,7 +149,7 @@ public class GameManager(IGameRepository gameRepository, IHubContext<GameHubSock
Player = player Player = player
}); });
ScheduleGameDeletion(game.Id, TimeSpan.FromSeconds(5)); ScheduleGameDeletion(game.Id, TimeSpan.FromSeconds(0));
return Task.CompletedTask; return Task.CompletedTask;
} }
@@ -176,7 +183,7 @@ public class GameManager(IGameRepository gameRepository, IHubContext<GameHubSock
await Task.Delay(delay); await Task.Delay(delay);
var g = gameRepository.GetOne(gameId); var g = gameRepository.GetOne(gameId);
if (g != null && g.State == GameState.Ended) if (g != null && g.State != GameState.Running)
{ {
gameRepository.Destroy(gameId); gameRepository.Destroy(gameId);
+5
View File
@@ -33,6 +33,11 @@ function gameFieldPreset(x: number, y: number) {
label="Name vom Spieler 2" label="Name vom Spieler 2"
required required
></v-text-field> ></v-text-field>
<v-switch
v-model="settingsProp.botPlayer2"
:label="`${settingsProp.playerName2} ist ein Bot`"
color="primary"
></v-switch>
</v-col> </v-col>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<h2 class="settingsCat">Spielfeld</h2> <h2 class="settingsCat">Spielfeld</h2>
+1
View File
@@ -17,6 +17,7 @@ enum CurrentState {
let settings = ref<GameSettings>({ let settings = ref<GameSettings>({
playerName1: 'Spieler 1', playerName1: 'Spieler 1',
playerName2: 'Spieler 2', playerName2: 'Spieler 2',
botPlayer2: false,
fieldSize: { x: 7, y: 6 }, fieldSize: { x: 7, y: 6 },
}); });
+1 -1
View File
@@ -4,6 +4,6 @@ export interface GameSettings {
fieldSize: FieldSize; fieldSize: FieldSize;
playerName1: string; playerName1: string;
playerName2?: string | null; playerName2?: string | null;
aiLevel?: number | null; botPlayer2?: boolean | null;
message?: string | null; message?: string | null;
} }
+15 -3
View File
@@ -4,12 +4,14 @@ import GameConnection, {
type GameInformationDto, type GameInformationDto,
type GameEnded, type GameEnded,
} from '../signalR/GameConnection'; } from '../signalR/GameConnection';
import { getRandomMovePhrase } from '@/scripts/utils/RandomPlaceMessage';
class LocalGame { class LocalGame {
public player1: GameConnection; public player1: GameConnection;
public player2: GameConnection; public player2: GameConnection;
public currentDescription: string = 'Warte auf Spielaufbau ...'; public currentDescription: string = 'Warte auf Spielaufbau ...';
private gameId: string = ''; private gameId: string = '';
private botGame = false;
public gameState: GameInformationDto | undefined; public gameState: GameInformationDto | undefined;
public onGameStateChanged?: (gameState: GameInformationDto | undefined) => void; public onGameStateChanged?: (gameState: GameInformationDto | undefined) => void;
@@ -50,6 +52,8 @@ class LocalGame {
await this.player1.connect(settings.playerName1); await this.player1.connect(settings.playerName1);
await this.player2.connect(settings.playerName2 ?? 'Spieler 2'); await this.player2.connect(settings.playerName2 ?? 'Spieler 2');
this.botGame = settings.botPlayer2 ?? false;
await this.player1.createGame(settings.fieldSize); await this.player1.createGame(settings.fieldSize);
} }
@@ -63,7 +67,15 @@ class LocalGame {
async drop(index: number) { async drop(index: number) {
if (this.gameState?.currentTurn == 1) { if (this.gameState?.currentTurn == 1) {
this.player1.drop(this.gameId, index); this.player1.drop(this.gameId, index);
} else {
if (this.botGame) {
const randomDelay = Math.random() * 2000 + 1000;
setTimeout(() => {
const botIndex = Math.floor(Math.random() * this.gameState!.currentField.length);
this.player2.drop(this.gameId, botIndex);
}, randomDelay);
}
} else if(!this.botGame) {
this.player2.drop(this.gameId, index); this.player2.drop(this.gameId, index);
} }
} }
@@ -71,10 +83,10 @@ class LocalGame {
changePlaceDescription() { changePlaceDescription() {
const playerName = const playerName =
this.gameState?.currentTurn == 1 ? this.player1.playerName : this.player2.playerName; this.gameState?.currentTurn == 1 ? this.player1.playerName : this.player2.playerName;
this.currentDescription = `${playerName} ist dran mit setzen!`; this.currentDescription = getRandomMovePhrase(playerName);
} }
async disconnectAll(){ async disconnectAll() {
await this.player1.disconnect(); await this.player1.disconnect();
await this.player2.disconnect(); await this.player2.disconnect();
} }
@@ -5,6 +5,7 @@ import GameConnection, {
type GameInformationDto, type GameInformationDto,
} from '../signalR/GameConnection'; } from '../signalR/GameConnection';
import type JoinGameObject from '@/scripts/interfaces/JoinGameObject'; import type JoinGameObject from '@/scripts/interfaces/JoinGameObject';
import { getRandomMovePhrase } from '@/scripts/utils/RandomPlaceMessage';
class OnlineGame { class OnlineGame {
public player: GameConnection; public player: GameConnection;
@@ -89,7 +90,7 @@ class OnlineGame {
changePlaceDescription() { changePlaceDescription() {
const playerName = const playerName =
this.gameState?.currentTurn == 1 ? this.gameState.players[0]?.name : this.gameState?.players[1]?.name; this.gameState?.currentTurn == 1 ? this.gameState.players[0]?.name : this.gameState?.players[1]?.name;
this.currentDescription = `${playerName} ist dran mit setzen!`; this.currentDescription = getRandomMovePhrase(playerName ?? "Spieler");
} }
} }
+113
View File
@@ -0,0 +1,113 @@
const movePhrases: string[] = [
"Jetzt ist {playerName} dran.",
"Die Bühne gehört jetzt {playerName}.",
"Ein Stein fliegt gleich gesetzt von {playerName}.",
"Die nächste Spalte wartet auf {playerName}.",
"Alle schauen gespannt zu, während {playerName} überlegt.",
"Mit großer Verantwortung: {playerName}.",
"Ein taktischer Moment für {playerName}.",
"Die Entscheidung liegt jetzt bei {playerName}.",
"Wer rettet die Situation? {playerName}.",
"Ein neuer Stein rollt gesteuert von {playerName}.",
"Jetzt darf {playerName} zeigen, was Strategie bedeutet.",
"Der nächste Zug gehört {playerName}.",
"Spannung im Raum {playerName} ist dran.",
"Die nächste Spalte gehört heute {playerName}.",
"Alle warten auf den genialen Zug von {playerName}.",
"Ein mutiger Schritt von {playerName} steht an.",
"Jetzt kommt der Moment von {playerName}.",
"Das Brett wartet geduldig auf {playerName}.",
"Ein Stein fällt gleich dank {playerName}.",
"Strategiezeit für {playerName}.",
"Ein kleiner Stein, gesetzt von {playerName}.",
"Jetzt wirds ernst für {playerName}.",
"Ein neuer Zug steht an ausgeführt von {playerName}.",
"Jetzt entscheidet {playerName} über das Schicksal der Spalte.",
"Der nächste Stein gehört {playerName}.",
"Jetzt wird das Spielfeld von {playerName} erweitert.",
"Ein kluger Zug könnte gleich von {playerName} kommen.",
"Jetzt darf {playerName} das Spiel voranbringen.",
"Alle Augen auf {playerName}.",
"Der nächste Spielzug gehört eindeutig {playerName}.",
"Die nächste Spalte hat Besuch von {playerName}.",
"Jetzt ist Zeit für eine Entscheidung von {playerName}.",
"Ein Stein macht sich bereit {playerName} ist schuld.",
"Jetzt bringt {playerName} Bewegung ins Spiel.",
"Der nächste Schritt kommt von {playerName}.",
"Ein strategischer Moment für {playerName}.",
"Das Brett wartet gespannt auf {playerName}.",
"Jetzt liegt alles in den Händen von {playerName}.",
"Eine Spalte wird gleich von {playerName} erobert.",
"Jetzt darf {playerName} zeigen, was geplant war.",
"Ein neuer Stein gehört {playerName}.",
"Die Spannung steigt {playerName} ist dran.",
"Ein weiterer Spielzug kommt von {playerName}.",
"Das Spielfeld ruft nach {playerName}.",
"Jetzt kommt ein Zug von {playerName}.",
"Eine Spalte wird gleich von {playerName} gefüllt.",
"Der nächste Moment gehört {playerName}.",
"Jetzt wirds strategisch dank {playerName}.",
"Ein kluger Stein könnte gleich von {playerName} kommen.",
"Alle warten auf den Zug von {playerName}.",
"Jetzt entscheidet {playerName}.",
"Ein Zug voller Hoffnung von {playerName}.",
"Jetzt wird das Brett von {playerName} beeinflusst.",
"Eine neue Position entsteht durch {playerName}.",
"Jetzt darf {playerName} glänzen.",
"Ein Stein nimmt Fahrt auf gesteuert von {playerName}.",
"Jetzt kommt der nächste Schritt von {playerName}.",
"Das Spiel bewegt sich weiter dank {playerName}.",
"Ein entscheidender Moment für {playerName}.",
"Jetzt darf {playerName} kreativ werden.",
"Ein weiterer Stein gehört {playerName}.",
"Die Entscheidung fällt bei {playerName}.",
"Jetzt darf {playerName} das Brett verändern.",
"Ein Spielzug kündigt sich an von {playerName}.",
"Jetzt rollt der nächste Stein für {playerName}.",
"Ein taktischer Augenblick für {playerName}.",
"Jetzt ist der Zug von {playerName} gefragt.",
"Die nächste Spalte wartet geduldig auf {playerName}.",
"Jetzt darf {playerName} das Spiel formen.",
"Ein neuer Zug entsteht durch {playerName}.",
"Jetzt bringt {playerName} Bewegung ins Brett.",
"Ein Stein fällt gleich geworfen von {playerName}.",
"Jetzt ist {playerName} wieder am Steuer.",
"Ein neuer Spielzug wird von {playerName} gestartet.",
"Jetzt darf {playerName} das Spiel drehen.",
"Ein Moment voller Strategie für {playerName}.",
"Jetzt kommt ein Stein von {playerName}.",
"Eine Spalte wird gleich von {playerName} gewählt.",
"Jetzt darf {playerName} die nächste Lücke füllen.",
"Ein weiterer Zug gehört {playerName}.",
"Jetzt entscheidet die Idee von {playerName}.",
"Ein Stein fällt {playerName} hat entschieden.",
"Jetzt bringt {playerName} den nächsten Zug.",
"Eine neue Möglichkeit entsteht durch {playerName}.",
"Jetzt steht der Zug von {playerName} an.",
"Ein weiteres Kapitel im Spiel geschrieben von {playerName}.",
"Jetzt darf {playerName} die nächste Spalte wählen.",
"Ein kluger Moment für {playerName}.",
"Jetzt wird das Brett von {playerName} erweitert.",
"Ein weiterer Spielzug gehört {playerName}.",
"Jetzt kommt die Entscheidung von {playerName}.",
"Ein Stein landet gleich gesetzt von {playerName}.",
"Jetzt darf {playerName} den nächsten Plan umsetzen.",
"Ein weiterer Moment für {playerName}.",
"Jetzt führt {playerName} das Spiel weiter.",
"Ein Zug mit Wirkung von {playerName}.",
"Jetzt darf {playerName} die Strategie zeigen.",
"Ein weiterer Stein gehört eindeutig {playerName}.",
"Jetzt ist der Moment von {playerName} gekommen.",
"Ein neuer Zug entsteht durch die Entscheidung von {playerName}.",
"Jetzt darf {playerName} die nächste Lücke schließen.",
"Ein Stein macht sich bereit für {playerName}.",
"Jetzt setzt {playerName} den nächsten Impuls.",
"Ein weiterer Schritt im Spiel kommt von {playerName}.",
"Jetzt darf {playerName} das Brett verändern.",
"Ein neuer Spielzug wartet auf {playerName}."
];
export function getRandomMovePhrase(playerName: string): string {
const phrase = movePhrases[Math.floor(Math.random() * movePhrases.length)];
return phrase?.replace("{playerName}", playerName) ?? "Spieler";
}