final POC

This commit is contained in:
magdo
2025-11-24 23:28:57 +01:00
parent ce02f55a99
commit 6b3446e9b6
49 changed files with 4634 additions and 4620 deletions
@@ -120,12 +120,14 @@ export class BoardGenerationService {
// Generate appropriate step value for field type
if (specialField.type === 'positive') {
// Positive fields: use positive step values (3-8 range for good gameplay)
const stepValue = Math.floor(Math.random() * 6) + 3; // 3-8
// Positive fields: use positive step values (1-3 range for balanced gameplay)
// Max movement: 3 × 6 (dice) = 18 steps
const stepValue = Math.floor(Math.random() * 3) + 1; // 1-3
fields[fieldIndex].stepValue = Math.min(stepValue, maxStepValue);
} else {
// Negative fields: use negative step values (-3 to -8 range)
const stepValue = -(Math.floor(Math.random() * 6) + 3); // -3 to -8
// Negative fields: use negative step values (-1 to -3 range)
// Max backward: -3 × 6 (dice) = -18 steps
const stepValue = -(Math.floor(Math.random() * 3) + 1); // -1 to -3
fields[fieldIndex].stepValue = Math.max(stepValue, minStepValue);
}
});
@@ -156,25 +158,33 @@ export class BoardGenerationService {
return finalPosition;
}
private getPatternModifier(position: number, positiveField: boolean): number {
// Pattern modifiers for strategic complexity:
public getPatternModifier(position: number, positiveField: boolean): number {
// Pattern modifiers STACK for strategic complexity:
// - Positions ending in 0 (10, 20, 30...): No modifier
// - Positions ending in 5 (15, 25, 35...): ±3 modifier
// - Positions divisible by 3 (9, 12, 21...): ±2 modifier
// - Odd positions (1, 7, 11...): ±1 modifier
// - Other even positions: No modifier
// Multiple conditions can apply and stack
if (position % 10 === 0) {
return 0; // Positions ending in 0
} else if (position % 10 === 5) {
return positiveField ? 3 : -3; // Positions ending in 5
} else if (position % 3 === 0) {
return positiveField ? 2 : -2; // Divisible by 3
} else if (position % 2 === 1) {
return positiveField ? 1 : -1; // Odd positions
} else {
return 0; // Other even positions
return 0; // Positions ending in 0 - no modifier
}
let modifier = 0;
const direction = positiveField ? 1 : -1;
// Check each condition and stack modifiers
if (position % 10 === 5) {
modifier += 3 * direction; // Positions ending in 5
}
if (position % 3 === 0) {
modifier += 2 * direction; // Divisible by 3
}
if (position % 2 === 1) {
modifier += 1 * direction; // Odd positions
}
return modifier;
}
private validate20_30Rule(currentPosition: number, targetPosition: number, distance: number): boolean {
@@ -49,7 +49,8 @@ export class JoinGameCommandHandler {
}
// Generate player ID for public games or use provided one
const actualPlayerId = command.playerId || uuidv4();
// For anonymous players (no playerId), use playerName as the identifier to allow rejoining
const actualPlayerId = command.playerId || `guest_${command.playerName}`;
// Validate game joinability (authentication/org checks done in router)
this.validateGameJoinability(game, actualPlayerId, command);
@@ -122,7 +123,7 @@ export class JoinGameCommandHandler {
private async updateGameInRedis(game: GameAggregate, command: JoinGameCommand & { playerId: string }): Promise<void> {
try {
const redisKey = `game:${game.id}`;
const redisKey = `game:${game.gamecode}`;
// Get existing game data from Redis or create new
let gameData: ActiveGameData;
@@ -189,9 +190,9 @@ export class JoinGameCommandHandler {
}
}
async getGameFromRedis(gameId: string): Promise<ActiveGameData | null> {
async getGameFromRedis(gameCode: string): Promise<ActiveGameData | null> {
try {
const redisKey = `game:${gameId}`;
const redisKey = `game:${gameCode}`;
const data = await this.redisService.get(redisKey);
return data ? JSON.parse(data) as ActiveGameData : null;
} catch (error) {
@@ -200,9 +201,9 @@ export class JoinGameCommandHandler {
}
}
async removePlayerFromRedis(gameId: string, playerId: string): Promise<void> {
async removePlayerFromRedis(gameCode: string, playerId: string): Promise<void> {
try {
const redisKey = `game:${gameId}`;
const redisKey = `game:${gameCode}`;
const existingData = await this.redisService.get(redisKey);
if (existingData) {
@@ -163,6 +163,7 @@ export class StartGameCommandHandler {
cardid: this.generateCardId(),
question: card.text,
answer: card.answer || undefined,
type: card.type, // Include card type for proper processing
consequence: card.consequence || null,
played: false,
playerid: undefined
@@ -25,6 +25,7 @@ export interface ActiveGamePlayData {
createdAt: Date;
startedAt: Date;
currentTurn: number; // Index of current player in turn order
currentPlayer: string; // ID of the player whose turn it is
turnSequence: string[]; // Ordered array of player IDs based on turnOrder
websocketRoom: string;
gamePhase: 'starting' | 'playing' | 'paused' | 'finished';
@@ -131,10 +132,13 @@ export class StartGamePlayCommandHandler {
private async initializeGamePlayInRedis(game: GameAggregate, boardData: BoardData): Promise<void> {
try {
const redisKey = `gameplay:${game.id}`;
const redisKey = `gameplay:${game.gamecode}`;
// Get connected player names from Redis (stored by WebSocket)
const playerNamesMap = await this.getPlayerNames(game.gamecode);
// Generate random turn orders for all players
const playersWithPositions = this.initializePlayerPositions(game.players);
const playersWithPositions = this.initializePlayerPositions(game.players, playerNamesMap);
// Sort by turn order to create turn sequence
const turnSequence = [...playersWithPositions]
@@ -151,6 +155,7 @@ export class StartGamePlayCommandHandler {
createdAt: game.createdate,
startedAt: new Date(),
currentTurn: 0, // Start with first player in sequence
currentPlayer: turnSequence[0], // First player in turn sequence
turnSequence,
websocketRoom: `game_${game.gamecode}`,
gamePhase: 'starting',
@@ -160,13 +165,6 @@ export class StartGamePlayCommandHandler {
// Store game play data in Redis with TTL (24 hours)
await this.redisService.setWithExpiry(redisKey, JSON.stringify(gamePlayData), 24 * 60 * 60);
// Create turn sequence mapping for quick lookups
await this.redisService.setWithExpiry(
`game_turns:${game.id}`,
JSON.stringify(turnSequence),
24 * 60 * 60
);
logOther('Game play initialized in Redis', {
gameId: game.id,
gameCode: game.gamecode,
@@ -182,7 +180,7 @@ export class StartGamePlayCommandHandler {
}
}
private initializePlayerPositions(playerIds: string[]): GamePlayerPosition[] {
private initializePlayerPositions(playerIds: string[], playerNamesMap: Map<string, string>): GamePlayerPosition[] {
const players: GamePlayerPosition[] = [];
// Generate random turn orders (1 to playerCount)
@@ -191,6 +189,7 @@ export class StartGamePlayCommandHandler {
playerIds.forEach((playerId, index) => {
players.push({
playerId,
playerName: playerNamesMap.get(playerId) || playerId, // Use mapped name or fallback to ID
position: 0, // All players start at position 0
turnOrder: turnOrders[index],
isOnline: true, // Assume online when game starts
@@ -203,6 +202,7 @@ export class StartGamePlayCommandHandler {
turnOrders: turnOrders,
playersData: players.map(p => ({
playerId: p.playerId,
playerName: p.playerName,
position: p.position,
turnOrder: p.turnOrder
}))
@@ -226,23 +226,18 @@ export class StartGamePlayCommandHandler {
private async notifyGameStart(game: GameAggregate): Promise<void> {
try {
// Get board data from Redis
const redisKey = `game_board_${game.id}`;
const boardDataStr = await this.redisService.get(redisKey);
if (!boardDataStr) {
logError('Board data not found in Redis during game start notification', new Error('Missing board data'));
return;
}
const boardData: BoardData = JSON.parse(boardDataStr);
// Get turn sequence from Redis
const gamePlayData = await this.getGamePlayFromRedis(game.id);
// Get game play data from Redis (contains board data)
const gamePlayData = await this.getGamePlayFromRedis(game.gamecode);
if (!gamePlayData) {
logError('Game play data not found in Redis', new Error('Missing game play data'));
return;
}
const boardData = gamePlayData.boardData;
if (!boardData) {
logError('Board data not found in game play data', new Error('Missing board data'));
return;
}
// Get WebSocket service from DIContainer and broadcast game start
const gameWebSocketService = DIContainer.getInstance().gameWebSocketService;
@@ -267,9 +262,9 @@ export class StartGamePlayCommandHandler {
}
}
async getGamePlayFromRedis(gameId: string): Promise<ActiveGamePlayData | null> {
async getGamePlayFromRedis(gameCode: string): Promise<ActiveGamePlayData | null> {
try {
const redisKey = `gameplay:${gameId}`;
const redisKey = `gameplay:${gameCode}`;
const data = await this.redisService.get(redisKey);
return data ? JSON.parse(data) as ActiveGamePlayData : null;
} catch (error) {
@@ -278,9 +273,9 @@ export class StartGamePlayCommandHandler {
}
}
async updatePlayerPosition(gameId: string, playerId: string, newPosition: number): Promise<void> {
async updatePlayerPosition(gameCode: string, playerId: string, newPosition: number): Promise<void> {
try {
const gameData = await this.getGamePlayFromRedis(gameId);
const gameData = await this.getGamePlayFromRedis(gameCode);
if (!gameData) {
throw new Error('Game session not found');
}
@@ -291,11 +286,11 @@ export class StartGamePlayCommandHandler {
player.position = newPosition;
// Save back to Redis
const redisKey = `gameplay:${gameId}`;
const redisKey = `gameplay:${gameCode}`;
await this.redisService.setWithExpiry(redisKey, JSON.stringify(gameData), 24 * 60 * 60);
logOther('Player position updated', {
gameId,
gameCode,
playerId,
newPosition
});
@@ -306,9 +301,9 @@ export class StartGamePlayCommandHandler {
}
}
async getNextPlayer(gameId: string): Promise<string | null> {
async getNextPlayer(gameCode: string): Promise<string | null> {
try {
const gameData = await this.getGamePlayFromRedis(gameId);
const gameData = await this.getGamePlayFromRedis(gameCode);
if (!gameData) {
return null;
}
@@ -321,6 +316,39 @@ export class StartGamePlayCommandHandler {
}
}
private async getPlayerNames(gameCode: string): Promise<Map<string, string>> {
try {
// Get active game data from Redis which contains player names
const activeGameKey = `game:${gameCode}`;
const activeGameStr = await this.redisService.get(activeGameKey);
const playerNamesMap = new Map<string, string>();
if (activeGameStr) {
const activeGame = JSON.parse(activeGameStr);
if (activeGame.currentPlayers && Array.isArray(activeGame.currentPlayers)) {
// Map playerIds to playerNames from active game data
activeGame.currentPlayers.forEach((player: any) => {
if (player.playerId && player.playerName) {
playerNamesMap.set(player.playerId, player.playerName);
}
});
}
}
logOther('Retrieved player names map', {
gameCode,
playerCount: playerNamesMap.size,
players: Array.from(playerNamesMap.entries()).map(([id, name]) => ({ id, name }))
});
return playerNamesMap;
} catch (error) {
logError('Failed to get player names', error instanceof Error ? error : new Error(String(error)));
return new Map();
}
}
async advanceTurn(gameId: string): Promise<string | null> {
try {
const gameData = await this.getGamePlayFromRedis(gameId);