Значительно изменил бд и генерацию так же добавил роли
This commit is contained in:
71
app/services/card_roles_repository.rb
Normal file
71
app/services/card_roles_repository.rb
Normal file
@@ -0,0 +1,71 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "json"
|
||||
|
||||
module ClashDeckGenerator2
|
||||
module Services
|
||||
class CardRolesRepository
|
||||
ROLES_PATH = File.expand_path("../../config/cards/roles.json", __dir__)
|
||||
|
||||
def all
|
||||
@all ||= load_roles
|
||||
end
|
||||
|
||||
def cards
|
||||
all.fetch("cards", {})
|
||||
end
|
||||
|
||||
def roles_for(card_name)
|
||||
cards.fetch(card_name, [])
|
||||
end
|
||||
|
||||
def has_role?(card_name, role)
|
||||
roles_for(card_name).include?(role)
|
||||
end
|
||||
|
||||
def meta
|
||||
all.fetch("_meta", {})
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_roles
|
||||
raise "roles.json not found: #{ROLES_PATH}" unless File.exist?(ROLES_PATH)
|
||||
|
||||
parsed = JSON.parse(File.read(ROLES_PATH))
|
||||
|
||||
unless parsed.is_a?(Hash)
|
||||
raise "roles.json must contain a JSON object at root"
|
||||
end
|
||||
|
||||
unless parsed.key?("cards")
|
||||
raise 'roles.json must contain "cards" key'
|
||||
end
|
||||
|
||||
cards = parsed["cards"]
|
||||
|
||||
unless cards.is_a?(Hash)
|
||||
raise '"cards" must be a JSON object'
|
||||
end
|
||||
|
||||
cards.each do |card_name, roles|
|
||||
unless card_name.is_a?(String) && !card_name.strip.empty?
|
||||
raise "Invalid card name in roles.json: #{card_name.inspect}"
|
||||
end
|
||||
|
||||
unless roles.is_a?(Array)
|
||||
raise "Roles for #{card_name} must be an array"
|
||||
end
|
||||
|
||||
roles.each do |role|
|
||||
unless role.is_a?(String) && !role.strip.empty?
|
||||
raise "Invalid role for #{card_name}: #{role.inspect}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
parsed
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -3,33 +3,124 @@
|
||||
module ClashDeckGenerator2
|
||||
module Services
|
||||
class DeckGenerator
|
||||
DECK_SIZE = 8
|
||||
DEFAULT_RULES = {
|
||||
deck_size: 8,
|
||||
min_spells: 1,
|
||||
min_troops: 1,
|
||||
max_buildings: 2,
|
||||
min_avg_elixir: 2.5,
|
||||
max_avg_elixir: 5.0
|
||||
}.freeze
|
||||
|
||||
def initialize(cards_repo:)
|
||||
SPECIAL_COMBINATIONS = [
|
||||
{ evolutions: 1, specials: 2 },
|
||||
{ evolutions: 2, specials: 1 }
|
||||
].freeze
|
||||
|
||||
def initialize(cards_repo: ClashDeckGenerator2::Repos::CardsRepo.new, rules: {})
|
||||
@cards_repo = cards_repo
|
||||
@rules = DEFAULT_RULES.merge(rules)
|
||||
end
|
||||
|
||||
def call(only_meta: true)
|
||||
pool = only_meta ? @cards_repo.meta_cards : @cards_repo.all_cards
|
||||
deck = sample_unique(pool, DECK_SIZE)
|
||||
def call
|
||||
cards = unwrap_cards(@cards_repo.all)
|
||||
|
||||
{
|
||||
cards: deck,
|
||||
avg_elixir: average_elixir(deck)
|
||||
}
|
||||
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| ["evolution", "hero", "champion"].include?(card.rarity) }
|
||||
|
||||
def enough_win_conditions?(deck)
|
||||
deck.count { |card| has_role?(card, "win_condition") } >= 1
|
||||
end
|
||||
|
||||
def enough_anti_air?(deck)
|
||||
deck.count { |card| has_role?(card, "anti_air") } >= 1
|
||||
end
|
||||
|
||||
def has_role?(card, role)
|
||||
@roles_repo.has_role?(card.name, role)
|
||||
end
|
||||
|
||||
attempts = 1000
|
||||
|
||||
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
|
||||
|
||||
chosen_regulars = remaining_pool.sample(remaining_needed)
|
||||
|
||||
deck = chosen_evolutions + chosen_specials + chosen_regulars
|
||||
next unless valid_deck?(deck)
|
||||
|
||||
return deck.shuffle
|
||||
end
|
||||
|
||||
raise "Failed to generate a valid deck after #{attempts} attempts"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def sample_unique(pool, n)
|
||||
raise "Not enough cards in pool (need #{n}, have #{pool.size})" if pool.size < n
|
||||
|
||||
pool.sample(n)
|
||||
def unwrap_cards(result)
|
||||
if result.is_a?(Array) && result.first == :cards
|
||||
result.last
|
||||
else
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
def average_elixir(deck)
|
||||
sum = deck.sum { |c| c[:elixir_cost].to_i }
|
||||
sum.fdiv(deck.size).round(2)
|
||||
def valid_deck?(deck)
|
||||
return false unless deck.size == @rules[:deck_size]
|
||||
return false unless unique_cards?(deck)
|
||||
return false unless enough_spells?(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)
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def unique_cards?(deck)
|
||||
deck.map(&:name).uniq.size == deck.size
|
||||
end
|
||||
|
||||
def enough_spells?(deck)
|
||||
deck.count { |card| card.type == "spell" } >= @rules[:min_spells]
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user