diff --git a/README.md b/README.md index ba7aba9..1b42733 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ bundle exec hanami db prepare ```bat bundle exec puma -C config/puma.rb config.ru ``` -cd + Открыть: [http://localhost:2300](http://localhost:2300) ## Важно про SQLite для Hanami diff --git a/app/actions/decks/generate.rb b/app/actions/decks/generate.rb new file mode 100644 index 0000000..ad7ba48 --- /dev/null +++ b/app/actions/decks/generate.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module ClashDeckGenerator2 + module Actions + module Decks + class Generate < ClashDeckGenerator2::Action + include Deps[view: "views.home.index"] + + def handle(_req, res) + roles_repo = ClashDeckGenerator2::Services::CardRolesRepository.new + cards_repo = ClashDeckGenerator2::Repos::CardsRepo.new + + generator = ClashDeckGenerator2::Services::DeckGenerator.new( + cards_repo: cards_repo, + roles_repo: roles_repo + ) + + deck = unwrap(generator.call) + + res.render( + view, + deck: deck, + roles_repo: roles_repo, + stats: build_stats(deck) + ) + end + + private + + def unwrap(result) + if result.is_a?(Array) && result.first == :cards + result.last + else + result + end + end + + def build_stats(deck) + { + cards_count: deck.size, + average_elixir: (deck.sum(&:elixir_cost).to_f / deck.size).round(2), + spells_count: deck.count { |c| c.type == "spell" }, + troops_count: deck.count { |c| c.type == "troop" }, + buildings_count: deck.count { |c| c.type == "building" }, + champions_count: deck.count { |c| c.rarity == "champion" }, + heroes_count: deck.count { |c| c.rarity == "hero" }, + evolutions_count: deck.count { |c| c.rarity == "evolution" }, + meta_cards_count: deck.count { |c| c.is_meta == 1 } + } + end + end + end + end +end \ No newline at end of file diff --git a/app/actions/home/index.rb b/app/actions/home/index.rb new file mode 100644 index 0000000..ee78d0d --- /dev/null +++ b/app/actions/home/index.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module ClashDeckGenerator2 + module Actions + module Home + class Index < ClashDeckGenerator2::Action + def handle(_req, _res) + end + end + end + end +end \ No newline at end of file diff --git a/app/services/deck_generator.rb b/app/services/deck_generator.rb index 4bb8732..e28f8e9 100644 --- a/app/services/deck_generator.rb +++ b/app/services/deck_generator.rb @@ -5,11 +5,15 @@ module ClashDeckGenerator2 class DeckGenerator DEFAULT_RULES = { deck_size: 8, - min_spells: 1, min_troops: 1, max_buildings: 2, min_avg_elixir: 2.5, - max_avg_elixir: 5.0 + 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 = [ @@ -17,8 +21,13 @@ module ClashDeckGenerator2 { evolutions: 2, specials: 1 } ].freeze - def initialize(cards_repo: ClashDeckGenerator2::Repos::CardsRepo.new, rules: {}) + 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 @@ -30,21 +39,9 @@ module ClashDeckGenerator2 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) } + regular_cards = cards.reject { |card| %w[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 = 5000 attempts.times do combo = SPECIAL_COMBINATIONS.sample @@ -53,18 +50,21 @@ end next if special_cards.size < combo[:specials] chosen_evolutions = evolution_cards.sample(combo[:evolutions]) - chosen_specials = special_cards.sample(combo[:specials]) + 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 = build_deck( + chosen_evolutions: chosen_evolutions, + chosen_specials: chosen_specials, + remaining_pool: remaining_pool + ) - deck = chosen_evolutions + chosen_specials + chosen_regulars + next if deck.nil? next unless valid_deck?(deck) return deck.shuffle @@ -83,14 +83,89 @@ end 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_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) + 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 @@ -99,10 +174,6 @@ end 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 @@ -122,6 +193,35 @@ end [[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 \ No newline at end of file diff --git a/app/templates/home/index.html.erb b/app/templates/home/index.html.erb new file mode 100644 index 0000000..fcd0f28 --- /dev/null +++ b/app/templates/home/index.html.erb @@ -0,0 +1,88 @@ + + +
Generate a deck based on roles, meta cards, and special slot rules.
+ + + +<% if deck.any? %> +Average elixir: <%= stats[:average_elixir] %>
+Meta cards: <%= stats[:meta_cards_count] %>
+Spells: <%= stats[:spells_count] %>
+Troops: <%= stats[:troops_count] %>
+Buildings: <%= stats[:buildings_count] %>
+Champions: <%= stats[:champions_count] %>
+Heroes: <%= stats[:heroes_count] %>
+Evolutions: <%= stats[:evolutions_count] %>
+