199 lines
7.3 KiB
TypeScript
199 lines
7.3 KiB
TypeScript
import { GameField, BoardData } from '../../Domain/Game/GameAggregate';
|
||
import { logOther, logError } from '../Services/Logger';
|
||
|
||
interface SpecialFieldInfo {
|
||
position: number;
|
||
type: 'positive' | 'negative' | 'luck';
|
||
}
|
||
|
||
export class BoardGenerationService {
|
||
async generateBoard(
|
||
positiveFieldCount: number,
|
||
negativeFieldCount: number,
|
||
luckFieldCount: number
|
||
): Promise<BoardData> {
|
||
// Pattern-based approach has 100% success rate, no retry needed
|
||
const result = this.generateSingleAttempt(positiveFieldCount, negativeFieldCount, luckFieldCount);
|
||
|
||
logOther('Pattern-based board generation completed', {
|
||
totalFields: result.fields.length,
|
||
specialFields: result.fields.filter((f: GameField) => f.type !== 'regular').length,
|
||
positiveFields: result.fields.filter((f: GameField) => f.type === 'positive').length,
|
||
negativeFields: result.fields.filter((f: GameField) => f.type === 'negative').length,
|
||
luckFields: result.fields.filter((f: GameField) => f.type === 'luck').length
|
||
});
|
||
|
||
return result;
|
||
}
|
||
|
||
private generateSingleAttempt(
|
||
positiveFieldCount: number,
|
||
negativeFieldCount: number,
|
||
luckFieldCount: number
|
||
): BoardData {
|
||
// Step 1: Choose special field positions
|
||
const specialFieldPositions = this.chooseSpecialFieldPositions(
|
||
positiveFieldCount,
|
||
negativeFieldCount,
|
||
luckFieldCount
|
||
);
|
||
|
||
// Step 2: Calculate step values using pattern-based approach
|
||
const fields = this.calculatePatternBasedStepValues(specialFieldPositions);
|
||
|
||
return {
|
||
fields
|
||
};
|
||
}
|
||
|
||
private chooseSpecialFieldPositions(
|
||
positiveFieldCount: number,
|
||
negativeFieldCount: number,
|
||
luckFieldCount: number
|
||
): SpecialFieldInfo[] {
|
||
const totalSpecial = positiveFieldCount + negativeFieldCount + luckFieldCount;
|
||
const specialFields: SpecialFieldInfo[] = [];
|
||
|
||
// Generate unique random positions
|
||
const positions = new Set<number>();
|
||
while (positions.size < totalSpecial) {
|
||
const position = Math.floor(Math.random() * 100) + 1; // 1-100
|
||
positions.add(position);
|
||
}
|
||
|
||
// Convert to sorted array
|
||
const sortedPositions = Array.from(positions).sort((a, b) => a - b);
|
||
|
||
// Distribute types randomly
|
||
const types: ('positive' | 'negative' | 'luck')[] = [
|
||
...Array(positiveFieldCount).fill('positive'),
|
||
...Array(negativeFieldCount).fill('negative'),
|
||
...Array(luckFieldCount).fill('luck')
|
||
];
|
||
|
||
// Shuffle types
|
||
for (let i = types.length - 1; i > 0; i--) {
|
||
const j = Math.floor(Math.random() * (i + 1));
|
||
[types[i], types[j]] = [types[j], types[i]];
|
||
}
|
||
|
||
sortedPositions.forEach((position, index) => {
|
||
specialFields.push({
|
||
position,
|
||
type: types[index] || 'positive'
|
||
});
|
||
});
|
||
|
||
return specialFields;
|
||
}
|
||
|
||
private calculatePatternBasedStepValues(specialFields: SpecialFieldInfo[]): GameField[] {
|
||
// Initialize all fields as regular
|
||
const fields: GameField[] = Array.from({ length: 100 }, (_, i) => ({
|
||
position: i + 1,
|
||
type: 'regular' as const
|
||
}));
|
||
|
||
// Update special fields with pattern-based step values
|
||
specialFields.forEach(specialField => {
|
||
const fieldIndex = specialField.position - 1; // Convert to 0-based index
|
||
fields[fieldIndex].type = specialField.type;
|
||
|
||
if (specialField.type === 'luck') {
|
||
// Luck fields don't need step values
|
||
return;
|
||
}
|
||
|
||
// Calculate step values based on position rules
|
||
let maxStepValue: number;
|
||
let minStepValue: number;
|
||
|
||
if (specialField.position <= 80) {
|
||
// Positions 1-80: step values can be ±20
|
||
maxStepValue = 20;
|
||
minStepValue = -20;
|
||
} else {
|
||
// Positions 81-100: step values can be -30 to +10
|
||
maxStepValue = 10;
|
||
minStepValue = -30;
|
||
}
|
||
|
||
// 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
|
||
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
|
||
fields[fieldIndex].stepValue = Math.max(stepValue, minStepValue);
|
||
}
|
||
});
|
||
|
||
return fields;
|
||
}
|
||
|
||
// This method can be used by FieldEffectService for movement calculations
|
||
public calculatePatternBasedMovement(
|
||
currentPosition: number,
|
||
stepValue: number,
|
||
diceValue: number
|
||
): number {
|
||
// Calculate pattern modifier based on current position
|
||
const patternModifier = this.getPatternModifier(currentPosition);
|
||
|
||
// Calculate final position: currentPosition + (stepValue × dice) + patternModifier
|
||
const movement = stepValue * diceValue;
|
||
let finalPosition = currentPosition + movement + patternModifier;
|
||
|
||
// Ensure position stays within board bounds (1-100)
|
||
if (finalPosition < 1) {
|
||
finalPosition = 1;
|
||
} else if (finalPosition > 100) {
|
||
finalPosition = 100;
|
||
}
|
||
|
||
return finalPosition;
|
||
}
|
||
|
||
private getPatternModifier(position: number): number {
|
||
// Pattern modifiers 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
|
||
|
||
if (position % 10 === 0) {
|
||
return 0; // Positions ending in 0
|
||
} else if (position % 10 === 5) {
|
||
return Math.random() < 0.5 ? 3 : -3; // Positions ending in 5
|
||
} else if (position % 3 === 0) {
|
||
return Math.random() < 0.5 ? 2 : -2; // Divisible by 3
|
||
} else if (position % 2 === 1) {
|
||
return Math.random() < 0.5 ? 1 : -1; // Odd positions
|
||
} else {
|
||
return 0; // Other even positions
|
||
}
|
||
}
|
||
|
||
private validate20_30Rule(currentPosition: number, targetPosition: number, distance: number): boolean {
|
||
// Fields 1-85: max 20 fields in any direction
|
||
if (currentPosition <= 85) {
|
||
return distance <= 20;
|
||
}
|
||
|
||
// Fields 86-100: max 30 fields backward, max 20 fields forward
|
||
if (currentPosition > 85) {
|
||
if (targetPosition > currentPosition) {
|
||
// Moving forward: max 20 fields
|
||
return distance <= 20;
|
||
} else {
|
||
// Moving backward: max 30 fields
|
||
return distance <= 30;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
} |