283 lines
10 KiB
TypeScript
283 lines
10 KiB
TypeScript
import { Injectable } from '@angular/core';
|
|
import { BehaviorSubject } from 'rxjs';
|
|
|
|
export interface SelectionData {
|
|
label: string;
|
|
numbers: (number | string)[];
|
|
value: number;
|
|
total: number;
|
|
isBoxed?: boolean;
|
|
}
|
|
|
|
@Injectable({ providedIn: 'root' })
|
|
export class SelectionService {
|
|
private selections = new BehaviorSubject<SelectionData[]>([]);
|
|
selections$ = this.selections.asObservable();
|
|
|
|
private currentRow = new BehaviorSubject<SelectionData>({
|
|
label: '',
|
|
numbers: [],
|
|
value: 0,
|
|
total: 0,
|
|
isBoxed: false
|
|
});
|
|
currentRow$ = this.currentRow.asObservable();
|
|
|
|
updatePartial(update: Partial<SelectionData>) {
|
|
const current = this.currentRow.value;
|
|
const updated: SelectionData = { ...current, ...update };
|
|
|
|
const label = updated.label;
|
|
const value = updated.value;
|
|
const numbers = updated.numbers || [];
|
|
const isBoxed = updated.isBoxed ?? false;
|
|
|
|
updated.total = 0;
|
|
|
|
if ((numbers.length > 0 || numbers.includes('F')) && value > 0 && label) {
|
|
switch (label) {
|
|
case 'WIN':
|
|
case 'SHP':
|
|
case 'THP':
|
|
case 'PLC':
|
|
case 'SHW': {
|
|
if (numbers.includes('F')) {
|
|
updated.total = 12 * value * 10;
|
|
} else {
|
|
updated.total = numbers.filter(n => typeof n === 'number').length * value * 10;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case 'FOR': {
|
|
const dashIndex = numbers.indexOf('-');
|
|
const group1 = dashIndex === -1 ? numbers : numbers.slice(0, dashIndex);
|
|
const group2 = dashIndex === -1 ? [] : numbers.slice(dashIndex + 1);
|
|
const group1Count = group1.includes('F') ? 12 : group1.filter(n => typeof n === 'number').length;
|
|
const group2Count = group2.includes('F') ? 12 : group2.filter(n => typeof n === 'number').length;
|
|
|
|
let combinations = 0;
|
|
|
|
if (isBoxed) {
|
|
const allNums = [...group1, ...group2].filter(n => typeof n === 'number') as number[];
|
|
const uniqueCount = new Set(allNums).size + (group1.includes('F') || group2.includes('F') ? 12 : 0);
|
|
combinations = uniqueCount >= 2 ? this.calculatePermutations(uniqueCount) : 0;
|
|
updated.numbers = [...group1, '-', ...group2];
|
|
} else {
|
|
if (group1.includes('F') && group2.includes('F')) {
|
|
combinations = 12 * 11; // nP2 for 12 runners
|
|
} else if (group1.includes('F') || group2.includes('F')) {
|
|
const nonFieldGroup = group1.includes('F') ? group2 : group1;
|
|
const nonFieldNumbers = nonFieldGroup.filter(n => typeof n === 'number') as number[];
|
|
combinations = 12 * nonFieldNumbers.length;
|
|
if (nonFieldNumbers.length > 0) {
|
|
combinations -= nonFieldNumbers.length; // Subtract overlapping runners
|
|
}
|
|
} else {
|
|
const numGroup1 = group1.filter(n => typeof n === 'number') as number[];
|
|
const numGroup2 = group2.filter(n => typeof n === 'number') as number[];
|
|
for (const a of numGroup1) {
|
|
for (const b of numGroup2) {
|
|
if (a !== b) combinations++;
|
|
}
|
|
}
|
|
}
|
|
updated.numbers = [...group1, '-', ...group2];
|
|
}
|
|
|
|
updated.total = combinations * value * 10;
|
|
break;
|
|
}
|
|
|
|
case 'QUI': {
|
|
const dashIndex = numbers.indexOf('-');
|
|
const group1 = dashIndex === -1 ? numbers : numbers.slice(0, dashIndex);
|
|
const group2 = dashIndex === -1 ? [] : numbers.slice(dashIndex + 1);
|
|
const group1_is_field = group1.includes('F');
|
|
const group2_is_field = group2.includes('F');
|
|
const group1Numbers = group1.filter(n => typeof n === 'number') as number[];
|
|
const group2Numbers = group2.filter(n => typeof n === 'number') as number[];
|
|
|
|
let combinations = 0;
|
|
|
|
if (isBoxed) {
|
|
const allNums = [...group1, ...group2].filter(n => typeof n === 'number') as number[];
|
|
const uniqueCount = new Set(allNums).size + (group1_is_field || group2_is_field ? 12 : 0);
|
|
combinations = uniqueCount >= 2 ? (uniqueCount * (uniqueCount - 1)) / 2 : 0;
|
|
} else if (group1_is_field && group2_is_field) {
|
|
combinations = (12 * 11) / 2; // C(12,2)
|
|
} else if (group1_is_field || group2_is_field) {
|
|
const fieldSide = group1_is_field ? group1Numbers : group2Numbers;
|
|
const nonFieldNumbers = (group1_is_field ? group2 : group1).filter(n => typeof n === 'number') as number[];
|
|
const pairSet = new Set<string>();
|
|
const fieldNumbers = fieldSide.length > 0 ? fieldSide : Array.from({ length: 12 }, (_, i) => i + 1);
|
|
for (let j of nonFieldNumbers) {
|
|
for (let i of fieldNumbers) {
|
|
if (i !== j) {
|
|
let min = Math.min(i, j);
|
|
let max = Math.max(i, j);
|
|
pairSet.add(`${min},${max}`);
|
|
}
|
|
}
|
|
}
|
|
combinations = pairSet.size;
|
|
} else {
|
|
const pairSet = new Set<string>();
|
|
for (let a of group1Numbers) {
|
|
for (let b of group2Numbers) {
|
|
if (a === b) continue;
|
|
let key = a < b ? `${a},${b}` : `${b},${a}`;
|
|
pairSet.add(key);
|
|
}
|
|
}
|
|
combinations = pairSet.size;
|
|
}
|
|
|
|
updated.numbers = [...group1, '-', ...group2];
|
|
updated.total = combinations * value * 10;
|
|
break;
|
|
}
|
|
|
|
case 'TAN': {
|
|
const dashIndices = numbers
|
|
.map((n, idx) => (n === '-' ? idx : -1))
|
|
.filter(idx => idx !== -1);
|
|
|
|
if (dashIndices.length < 2 && !isBoxed) break;
|
|
|
|
const group1 = dashIndices.length > 0 ? numbers.slice(0, dashIndices[0]) : numbers;
|
|
const group2 = dashIndices.length > 0 ? numbers.slice(dashIndices[0] + 1, dashIndices[1] || numbers.length) : [];
|
|
const group3 = dashIndices.length > 1 ? numbers.slice(dashIndices[1] + 1) : [];
|
|
|
|
let combinations = 0;
|
|
|
|
if (isBoxed) {
|
|
if (numbers.includes('F')) {
|
|
combinations = this.calculatePermutationsN(12, 3);
|
|
} else {
|
|
const allNums = [...group1, ...group2, ...group3].filter(n => typeof n === 'number') as number[];
|
|
combinations = allNums.length >= 3 ? this.calculatePermutationsN(allNums.length, 3) : 0;
|
|
}
|
|
} else {
|
|
// Define possible values for each group: 'F' means all runners (1 to 12), else only selected numbers
|
|
const runners = Array.from({ length: 12 }, (_, i) => i + 1);
|
|
const group1Vals = group1.includes('F') ? runners : group1.filter(n => typeof n === 'number') as number[];
|
|
const group2Vals = group2.includes('F') ? runners : group2.filter(n => typeof n === 'number') as number[];
|
|
const group3Vals = group3.includes('F') ? runners : group3.filter(n => typeof n === 'number') as number[];
|
|
|
|
combinations = 0;
|
|
for (const i of group1Vals) {
|
|
for (const j of group2Vals) {
|
|
if (i === j) continue;
|
|
for (const k of group3Vals) {
|
|
if (i === k || j === k) continue;
|
|
combinations++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
updated.numbers = [...group1, ...(group2.length ? ['-', ...group2] : []), ...(group3.length ? ['-', ...group3] : [])];
|
|
updated.total = combinations * value * 10;
|
|
break;
|
|
}
|
|
|
|
case 'TRE':
|
|
case 'MJP':
|
|
case 'JKP': {
|
|
const legs = this.splitToLegs(numbers, this.getLegCount(label));
|
|
const requiredLegs = this.getLegCount(label);
|
|
|
|
const legCounts = legs.map(leg => leg.includes('F') ? 12 : leg.filter(n => typeof n === 'number').length);
|
|
const filledLegs = legs.filter(leg => leg.length > 0).length;
|
|
|
|
if (filledLegs >= requiredLegs - 1) {
|
|
const combinations = legCounts.reduce((acc, count) => acc * (count || 1), 1);
|
|
updated.total = combinations * value * 10;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// ⚪ BOX logic
|
|
default: {
|
|
const combCount = isBoxed
|
|
? this.calculatePermutations(numbers.filter(n => typeof n === 'number').length)
|
|
: this.calculateCombinations(numbers.filter(n => typeof n === 'number').length);
|
|
updated.total = combCount * value * 10;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
this.currentRow.next(updated);
|
|
}
|
|
|
|
finalizeCurrentRow() {
|
|
const completed = this.currentRow.value;
|
|
if (!completed.label || completed.numbers.length === 0 || completed.value <= 0) return;
|
|
this.selections.next([...this.selections.value, { ...completed }]);
|
|
this.currentRow.next({ label: '', numbers: [], value: 0, total: 0, isBoxed: false });
|
|
}
|
|
|
|
clearSelections() {
|
|
this.selections.next([]);
|
|
this.currentRow.next({ label: '', numbers: [], value: 0, total: 0, isBoxed: false });
|
|
}
|
|
|
|
private calculateCombinations(n: number): number {
|
|
return n >= 2 ? (n * (n - 1)) / 2 : 0;
|
|
}
|
|
|
|
private calculatePermutations(n: number): number {
|
|
return n >= 2 ? n * (n - 1) : 0;
|
|
}
|
|
|
|
private calculatePermutationsN(n: number, r: number): number {
|
|
if (n < r) return 0;
|
|
let result = 1;
|
|
for (let i = 0; i < r; i++) {
|
|
result *= (n - i);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private splitToLegs(numbers: (number | string)[], legCount: number): (number | string)[][] {
|
|
const result: (number | string)[][] = [];
|
|
let currentLeg: (number | string)[] = [];
|
|
let separatorCount = 0;
|
|
|
|
for (const item of numbers) {
|
|
if (item === '/') {
|
|
if (currentLeg.length > 0) {
|
|
result.push([...currentLeg]);
|
|
currentLeg = [];
|
|
separatorCount++;
|
|
}
|
|
if (separatorCount >= legCount - 1) break;
|
|
} else {
|
|
currentLeg.push(item);
|
|
}
|
|
}
|
|
|
|
// Push the last leg if it has numbers
|
|
if (currentLeg.length > 0) {
|
|
result.push([...currentLeg]);
|
|
}
|
|
|
|
// Fill remaining legs with empty arrays if needed
|
|
while (result.length < legCount) {
|
|
result.push([]);
|
|
}
|
|
|
|
return result.slice(0, legCount);
|
|
}
|
|
|
|
private getLegCount(label: string): number {
|
|
switch (label) {
|
|
case 'TRE': return 3;
|
|
case 'MJP': return 4;
|
|
case 'JKP': return 5;
|
|
default: return 3;
|
|
}
|
|
}
|
|
} |