final POC
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user