class CwCardUtils::SynergyProbability

Calculates probability of drawing specific card combinations for synergy analysis

def approx_combo(target_names, draws)

Approximation for >3 cards (same as old method)
def approx_combo(target_names, draws)
  prob_missing = 0.0
  target_names.each do |name|
    copies = count_copies([name])
    miss_prob = hypergeometric(@deck_size - copies, draws).to_f / hypergeometric(@deck_size, draws)
    prob_missing += miss_prob
  end
  [1 - prob_missing, 0].max
end

def count_copies(names)

Utility: count how many copies of given cards are in the deck
def count_copies(names)
  @deck.main.sum { |card| names.include?(card.name) ? card.count : 0 }
end

def factorial(n)

def factorial(n)
  return 1 if n.zero?
  (1..n).reduce(1, :*)
end

def hypergeometric(n, k)

Hypergeometric combination helper
def hypergeometric(n, k)
  return 0 if k > n
  factorial(n) / (factorial(k) * factorial(n - k))
end

def initialize(deck, deck_size: 60)

def initialize(deck, deck_size: 60)
  @deck = deck
  @deck_size = deck_size
end

def prob_combo(target_names, draws)

Probability of drawing ALL cards in the targets list (synergy pair/trio)
def prob_combo(target_names, draws)
  case target_names.size
  when 1
    prob_single(target_names, draws)
  when 2
    prob_two_card_combo(target_names, draws)
  when 3
    prob_three_card_combo(target_names, draws)
  else
    # For >3 cards, fallback to approximation
    approx_combo(target_names, draws)
  end
end

def prob_single(target_names, draws)

Probability of drawing at least one of the target cards
def prob_single(target_names, draws)
  total_copies = count_copies(target_names)
  1 - hypergeometric(@deck_size - total_copies, draws).to_f / hypergeometric(@deck_size, draws)
end

def prob_three_card_combo(names, draws)

Exact for 3-card combos
def prob_three_card_combo(names, draws)
  copies_a = count_copies([names[0]])
  copies_b = count_copies([names[1]])
  copies_c = count_copies([names[2]])
  total = hypergeometric(@deck_size, draws).to_f
  miss_a = hypergeometric(@deck_size - copies_a, draws) / total
  miss_b = hypergeometric(@deck_size - copies_b, draws) / total
  miss_c = hypergeometric(@deck_size - copies_c, draws) / total
  miss_ab = hypergeometric(@deck_size - (copies_a + copies_b), draws) / total
  miss_ac = hypergeometric(@deck_size - (copies_a + copies_c), draws) / total
  miss_bc = hypergeometric(@deck_size - (copies_b + copies_c), draws) / total
  miss_abc = hypergeometric(@deck_size - (copies_a + copies_b + copies_c), draws) / total
  # Inclusion–exclusion for 3 sets
  [1 - (miss_a + miss_b + miss_c) +
    (miss_ab + miss_ac + miss_bc) -
    miss_abc, 0].max
end

def prob_two_card_combo(names, draws)

Exact for 2-card combos
def prob_two_card_combo(names, draws)
  copies_a = count_copies([names[0]])
  copies_b = count_copies([names[1]])
  total = hypergeometric(@deck_size, draws).to_f
  # Probability missing A
  miss_a = hypergeometric(@deck_size - copies_a, draws) / total
  # Probability missing B
  miss_b = hypergeometric(@deck_size - copies_b, draws) / total
  # Probability missing both
  miss_both = hypergeometric(@deck_size - (copies_a + copies_b), draws) / total
  # Inclusion–exclusion
  [1 - (miss_a + miss_b - miss_both), 0].max
end