227 lines
6.9 KiB
Ruby
227 lines
6.9 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module ClashDeckGenerator2
|
|
module Services
|
|
class DeckGenerator
|
|
DEFAULT_RULES = {
|
|
deck_size: 8,
|
|
min_troops: 1,
|
|
max_buildings: 2,
|
|
min_avg_elixir: 2.5,
|
|
max_avg_elixir: 5.0,
|
|
min_meta_cards: 4,
|
|
max_meta_cards: 8,
|
|
min_win_conditions: 1,
|
|
min_anti_air: 1,
|
|
min_spell_roles: 1
|
|
}.freeze
|
|
|
|
SPECIAL_COMBINATIONS = [
|
|
{ evolutions: 1, specials: 2 },
|
|
{ evolutions: 2, specials: 1 }
|
|
].freeze
|
|
|
|
def initialize(
|
|
cards_repo: ClashDeckGenerator2::Repos::CardsRepo.new,
|
|
roles_repo: ClashDeckGenerator2::Services::CardRolesRepository.new,
|
|
rules: {}
|
|
)
|
|
@cards_repo = cards_repo
|
|
@roles_repo = roles_repo
|
|
@rules = DEFAULT_RULES.merge(rules)
|
|
end
|
|
|
|
def call
|
|
cards = unwrap_cards(@cards_repo.all)
|
|
|
|
raise "No cards found in DB" if cards.empty?
|
|
raise "Not enough cards to build a deck" if cards.size < @rules[:deck_size]
|
|
|
|
evolution_cards = cards.select { |card| card.rarity == "evolution" }
|
|
special_cards = cards.select { |card| %w[hero champion].include?(card.rarity) }
|
|
regular_cards = cards.reject { |card| %w[evolution hero champion].include?(card.rarity) }
|
|
|
|
attempts = 5000
|
|
|
|
attempts.times do
|
|
combo = SPECIAL_COMBINATIONS.sample
|
|
|
|
next if evolution_cards.size < combo[:evolutions]
|
|
next if special_cards.size < combo[:specials]
|
|
|
|
chosen_evolutions = evolution_cards.sample(combo[:evolutions])
|
|
chosen_specials = special_cards.sample(combo[:specials])
|
|
|
|
used_names = (chosen_evolutions + chosen_specials).map(&:name)
|
|
remaining_pool = regular_cards.reject { |card| used_names.include?(card.name) }
|
|
remaining_needed = @rules[:deck_size] - chosen_evolutions.size - chosen_specials.size
|
|
|
|
next if remaining_pool.size < remaining_needed
|
|
|
|
deck = build_deck(
|
|
chosen_evolutions: chosen_evolutions,
|
|
chosen_specials: chosen_specials,
|
|
remaining_pool: remaining_pool
|
|
)
|
|
|
|
next if deck.nil?
|
|
next unless valid_deck?(deck)
|
|
|
|
return deck.shuffle
|
|
end
|
|
|
|
raise "Failed to generate a valid deck after #{attempts} attempts"
|
|
end
|
|
|
|
private
|
|
|
|
def unwrap_cards(result)
|
|
if result.is_a?(Array) && result.first == :cards
|
|
result.last
|
|
else
|
|
result
|
|
end
|
|
end
|
|
|
|
def build_deck(chosen_evolutions:, chosen_specials:, remaining_pool:)
|
|
deck = chosen_evolutions + chosen_specials
|
|
used_names = deck.map(&:name)
|
|
|
|
deck = add_required_role_card(deck, remaining_pool, used_names, "spell")
|
|
return nil if deck.nil?
|
|
used_names = deck.map(&:name)
|
|
|
|
deck = add_required_type_card(deck, remaining_pool, used_names, "troop")
|
|
return nil if deck.nil?
|
|
used_names = deck.map(&:name)
|
|
|
|
deck = add_required_role_card(deck, remaining_pool, used_names, "win_condition")
|
|
return nil if deck.nil?
|
|
used_names = deck.map(&:name)
|
|
|
|
deck = add_required_role_card(deck, remaining_pool, used_names, "anti_air")
|
|
return nil if deck.nil?
|
|
used_names = deck.map(&:name)
|
|
|
|
deck = add_required_meta_cards(deck, remaining_pool, used_names)
|
|
return nil if deck.nil?
|
|
used_names = deck.map(&:name)
|
|
|
|
cards_left = @rules[:deck_size] - deck.size
|
|
return nil if cards_left.negative?
|
|
|
|
filler_pool = remaining_pool.reject { |card| used_names.include?(card.name) }
|
|
return nil if filler_pool.size < cards_left
|
|
|
|
deck + filler_pool.sample(cards_left)
|
|
end
|
|
|
|
def add_required_role_card(deck, pool, used_names, role)
|
|
return deck if deck.any? { |card| has_role?(card, role) }
|
|
|
|
candidates = pool.select do |card|
|
|
!used_names.include?(card.name) && has_role?(card, role)
|
|
end
|
|
|
|
return nil if candidates.empty?
|
|
|
|
deck + [candidates.sample]
|
|
end
|
|
|
|
def add_required_type_card(deck, pool, used_names, type)
|
|
return deck if deck.any? { |card| card.type == type }
|
|
|
|
candidates = pool.select do |card|
|
|
!used_names.include?(card.name) && card.type == type
|
|
end
|
|
|
|
return nil if candidates.empty?
|
|
|
|
deck + [candidates.sample]
|
|
end
|
|
|
|
def add_required_meta_cards(deck, pool, used_names)
|
|
current_meta = meta_cards_count(deck)
|
|
needed_meta = @rules[:min_meta_cards] - current_meta
|
|
|
|
return deck if needed_meta <= 0
|
|
|
|
meta_candidates = pool.select do |card|
|
|
!used_names.include?(card.name) && meta_card?(card)
|
|
end
|
|
|
|
return nil if meta_candidates.size < needed_meta
|
|
|
|
deck + meta_candidates.sample(needed_meta)
|
|
end
|
|
|
|
def valid_deck?(deck)
|
|
return false unless deck.size == @rules[:deck_size]
|
|
return false unless unique_cards?(deck)
|
|
return false unless enough_troops?(deck)
|
|
return false unless buildings_limit_ok?(deck)
|
|
return false unless avg_elixir_ok?(deck)
|
|
return false unless special_slots_ok?(deck)
|
|
return false unless enough_win_conditions?(deck)
|
|
return false unless enough_anti_air?(deck)
|
|
return false unless enough_spell_roles?(deck)
|
|
return false unless meta_cards_ok?(deck)
|
|
|
|
true
|
|
end
|
|
|
|
def unique_cards?(deck)
|
|
deck.map(&:name).uniq.size == deck.size
|
|
end
|
|
|
|
def enough_troops?(deck)
|
|
deck.count { |card| card.type == "troop" } >= @rules[:min_troops]
|
|
end
|
|
|
|
def buildings_limit_ok?(deck)
|
|
deck.count { |card| card.type == "building" } <= @rules[:max_buildings]
|
|
end
|
|
|
|
def avg_elixir_ok?(deck)
|
|
avg = deck.sum(&:elixir_cost).to_f / deck.size
|
|
avg >= @rules[:min_avg_elixir] && avg <= @rules[:max_avg_elixir]
|
|
end
|
|
|
|
def special_slots_ok?(deck)
|
|
evolutions_count = deck.count { |card| card.rarity == "evolution" }
|
|
special_count = deck.count { |card| %w[hero champion].include?(card.rarity) }
|
|
|
|
[[1, 2], [2, 1]].include?([evolutions_count, special_count])
|
|
end
|
|
|
|
def enough_win_conditions?(deck)
|
|
deck.count { |card| has_role?(card, "win_condition") } >= @rules[:min_win_conditions]
|
|
end
|
|
|
|
def enough_anti_air?(deck)
|
|
deck.count { |card| has_role?(card, "anti_air") } >= @rules[:min_anti_air]
|
|
end
|
|
|
|
def enough_spell_roles?(deck)
|
|
deck.count { |card| has_role?(card, "spell") } >= @rules[:min_spell_roles]
|
|
end
|
|
|
|
def meta_cards_ok?(deck)
|
|
count = meta_cards_count(deck)
|
|
count >= @rules[:min_meta_cards] && count <= @rules[:max_meta_cards]
|
|
end
|
|
|
|
def meta_cards_count(deck)
|
|
deck.count { |card| meta_card?(card) }
|
|
end
|
|
|
|
def meta_card?(card)
|
|
card.is_meta == 1
|
|
end
|
|
|
|
def has_role?(card, role)
|
|
@roles_repo.has_role?(card.name, role)
|
|
end
|
|
end
|
|
end
|
|
end |