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 @@ + + +

Clash Deck Generator

+

Generate a deck based on roles, meta cards, and special slot rules.

+ +
+ +
+ +<% if deck.any? %> +
+

Deck stats

+

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] %>

+
+ +
+

Deck cards

+ + <% deck.each_with_index do |card, index| %> + <% meta_class = card.is_meta == 1 ? "meta" : "non-meta" %> + <% meta_text = card.is_meta == 1 ? "META" : "NON-META" %> + +
+
<%= index + 1 %>. <%= card.name %>
+
<%= card.rarity %> | <%= card.type %> | <%= card.elixir_cost %> elixir
+
<%= meta_text %>
+
Roles: <%= roles_repo.roles_for(card.name).join(", ") %>
+
+ <% end %> +
+<% end %> \ No newline at end of file diff --git a/app/templates/layouts/app.html.erb b/app/templates/layouts/app.html.erb index b217b7b..b27c83f 100644 --- a/app/templates/layouts/app.html.erb +++ b/app/templates/layouts/app.html.erb @@ -4,11 +4,8 @@ Clash deck generator2 - <%= favicon_tag %> - <%= stylesheet_tag "app" %> <%= yield %> - <%= javascript_tag "app" %> - + \ No newline at end of file diff --git a/app/views/home/index.rb b/app/views/home/index.rb new file mode 100644 index 0000000..417531f --- /dev/null +++ b/app/views/home/index.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module ClashDeckGenerator2 + module Views + module Home + class Index < ClashDeckGenerator2::View + expose :deck, default: [] + expose :roles_repo + expose :stats, default: {} + end + end + end +end \ No newline at end of file diff --git a/config.ru b/config.ru index 879c085..a98fc57 100644 --- a/config.ru +++ b/config.ru @@ -2,4 +2,4 @@ require "hanami/boot" -run Hanami.app +run Hanami.app \ No newline at end of file diff --git a/config/cards/roles.json b/config/cards/roles.json index 7c1c40e..dff059b 100644 --- a/config/cards/roles.json +++ b/config/cards/roles.json @@ -1,21 +1,765 @@ { "_meta": { "version": 1, - "description": "Card roles mapping for deck generation" + "description": "Card roles mapping for deck generation", + "generated_from_db": true, + "cards_count": 166, + "available_roles": [ + "anti_air", + "building", + "building_hate", + "control", + "cycle", + "defense", + "finisher", + "spell", + "splash", + "support", + "tank", + "win_condition" + ] }, "cards": { - "Knight": ["tank", "defense"], - "Archers": ["support", "anti_air", "cycle"], - "Musketeer": ["support", "anti_air"], - "Tesla": ["building", "defense", "anti_air"], - "Fireball": ["spell", "splash"], - "Zap": ["spell", "cycle"], - "Hog Rider": ["win_condition"], - "Royal Giant": ["win_condition", "tank"], - "Goblin Barrel": ["win_condition"], - "Balloon": ["win_condition"], - "Giant": ["win_condition", "tank"], - "P.E.K.K.A": ["tank", "defense"], - "Baby Dragon": ["support", "anti_air", "splash"] + "Archer Queen": [ + "support" + ], + "Archers": [ + "anti_air", + "cycle", + "support" + ], + "Archers Evolution": [ + "anti_air", + "cycle", + "support" + ], + "Arrows": [ + "spell", + "splash" + ], + "Baby Dragon": [ + "anti_air", + "splash", + "support" + ], + "Baby Dragon Evolution": [ + "anti_air", + "splash", + "support" + ], + "Balloon": [ + "support", + "win_condition" + ], + "Bandit": [ + "support" + ], + "Barbarian Barrel Hero": [ + "cycle", + "defense", + "spell" + ], + "Barbarian Hut": [ + "building", + "defense" + ], + "Barbarians": [ + "support" + ], + "Barbarians Evolution": [ + "support" + ], + "Bats": [ + "anti_air", + "cycle", + "support" + ], + "Bats Evolution": [ + "anti_air", + "cycle", + "support" + ], + "Battle Healer": [ + "support", + "tank" + ], + "Battle Ram": [ + "support", + "win_condition" + ], + "Battle Ram Evolution": [ + "support", + "win_condition" + ], + "Berserker": [ + "support" + ], + "Bomb Tower": [ + "building", + "defense", + "splash" + ], + "Bomber": [ + "cycle", + "splash", + "support" + ], + "Bomber Evolution": [ + "cycle", + "splash", + "support" + ], + "Boss Bandit": [ + "support" + ], + "Bowler": [ + "defense", + "splash", + "support" + ], + "Cannon": [ + "building", + "defense" + ], + "Cannon Cart": [ + "defense", + "support" + ], + "Cannon Evolution": [ + "building", + "defense" + ], + "Clone": [ + "spell", + "support" + ], + "Dark Prince": [ + "splash", + "support", + "tank" + ], + "Dart Goblin": [ + "anti_air", + "support" + ], + "Dart Goblin Evolution": [ + "anti_air", + "support" + ], + "Earthquake": [ + "building_hate", + "spell" + ], + "Electro Dragon": [ + "anti_air", + "splash", + "support" + ], + "Electro Dragon Evolution": [ + "anti_air", + "splash", + "support" + ], + "Electro Giant": [ + "support", + "tank", + "win_condition" + ], + "Electro Spirit": [ + "cycle", + "support" + ], + "Electro Wizard": [ + "anti_air", + "defense", + "support" + ], + "Elite Barbarians": [ + "support" + ], + "Elixir Collector": [ + "building", + "defense", + "support" + ], + "Elixir Golem": [ + "support", + "tank", + "win_condition" + ], + "Executioner": [ + "anti_air", + "splash", + "support" + ], + "Executioner Evolution": [ + "anti_air", + "splash", + "support" + ], + "Fire Spirit": [ + "cycle", + "splash", + "support" + ], + "Fireball": [ + "finisher", + "spell", + "splash" + ], + "Firecracker": [ + "anti_air", + "splash", + "support" + ], + "Firecracker Evolution": [ + "anti_air", + "splash", + "support" + ], + "Fisherman": [ + "control", + "defense", + "support" + ], + "Freeze": [ + "control", + "spell" + ], + "Furnace": [ + "building", + "defense", + "splash", + "support" + ], + "Furnace Evolution": [ + "building", + "defense", + "splash" + ], + "Giant": [ + "support", + "tank", + "win_condition" + ], + "Giant Hero": [ + "support", + "tank", + "win_condition" + ], + "Giant Skeleton": [ + "splash", + "support", + "tank" + ], + "Giant Snowball": [ + "cycle", + "spell" + ], + "Giant Snowball Evolution": [ + "cycle", + "spell" + ], + "Goblin Barrel": [ + "spell", + "win_condition" + ], + "Goblin Barrel Evolution": [ + "spell", + "win_condition" + ], + "Goblin Cage": [ + "building", + "defense" + ], + "Goblin Cage Evolution": [ + "building", + "defense" + ], + "Goblin Demolisher": [ + "splash", + "support" + ], + "Goblin Drill": [ + "building", + "support", + "win_condition" + ], + "Goblin Drill Evolution": [ + "building", + "defense", + "win_condition" + ], + "Goblin Gang": [ + "support" + ], + "Goblin Giant": [ + "support", + "tank", + "win_condition" + ], + "Goblin Giant Evolution": [ + "support", + "tank", + "win_condition" + ], + "Goblin Giantess": [ + "support", + "tank" + ], + "Goblin Hut": [ + "building", + "defense" + ], + "Goblin Machine": [ + "support" + ], + "Goblins": [ + "cycle", + "defense", + "support" + ], + "Goblins Hero": [ + "cycle", + "defense", + "support" + ], + "Goblinstein": [ + "support" + ], + "Golden Knight": [ + "support", + "tank" + ], + "Golem": [ + "support", + "tank", + "win_condition" + ], + "Graveyard": [ + "spell", + "win_condition" + ], + "Guards": [ + "defense", + "support" + ], + "Heal Spirit": [ + "cycle", + "support" + ], + "Hog Rider": [ + "support", + "win_condition" + ], + "Hunter": [ + "anti_air", + "defense", + "support" + ], + "Hunter Evolution": [ + "anti_air", + "defense", + "support" + ], + "Ice Golem": [ + "cycle", + "defense", + "support", + "tank" + ], + "Ice Golem Hero": [ + "cycle", + "defense", + "support", + "tank" + ], + "Ice Spirit": [ + "cycle", + "support" + ], + "Ice Spirit Evolution": [ + "cycle", + "support" + ], + "Ice Wizard": [ + "defense", + "splash", + "support" + ], + "Inferno Dragon": [ + "anti_air", + "defense", + "support" + ], + "Inferno Dragon Evolution": [ + "anti_air", + "defense", + "support" + ], + "Inferno Tower": [ + "building", + "defense" + ], + "Knight": [ + "defense", + "support", + "tank" + ], + "Knight Evolution": [ + "defense", + "support", + "tank" + ], + "Knight Hero": [ + "defense", + "support", + "tank" + ], + "Lava Hound": [ + "anti_air", + "support", + "tank", + "win_condition" + ], + "Lightning": [ + "finisher", + "spell" + ], + "Little Prince": [ + "anti_air", + "support" + ], + "Lumberjack": [ + "support", + "tank" + ], + "Lumberjack Evolution": [ + "support", + "tank" + ], + "Magic Archer": [ + "anti_air", + "support" + ], + "Magic Archer Hero": [ + "anti_air", + "support" + ], + "Mega Knight": [ + "defense", + "splash", + "support", + "tank" + ], + "Mega Knight Evolution": [ + "defense", + "splash", + "support", + "tank" + ], + "Mega Minion": [ + "anti_air", + "support" + ], + "Mega Minion Hero": [ + "anti_air", + "support" + ], + "Mighty Miner": [ + "defense", + "support", + "tank" + ], + "Miner": [ + "support", + "win_condition" + ], + "Mini P.E.K.K.A": [ + "defense", + "support", + "tank" + ], + "Mini P.E.K.K.A Hero": [ + "defense", + "support", + "tank" + ], + "Minion Horde": [ + "anti_air", + "support" + ], + "Minions": [ + "anti_air", + "support" + ], + "Mirror": [ + "spell", + "support" + ], + "Monk": [ + "defense", + "support", + "tank" + ], + "Mortar": [ + "building", + "defense", + "win_condition" + ], + "Mortar Evolution": [ + "building", + "defense", + "win_condition" + ], + "Mother Witch": [ + "anti_air", + "support" + ], + "Musketeer": [ + "anti_air", + "support" + ], + "Musketeer Evolution": [ + "anti_air", + "support" + ], + "Musketeer Hero": [ + "anti_air", + "support" + ], + "Night Witch": [ + "support" + ], + "P.E.K.K.A": [ + "defense", + "support", + "tank" + ], + "P.E.K.K.A Evolution": [ + "defense", + "support", + "tank" + ], + "Phoenix": [ + "anti_air", + "support" + ], + "Poison": [ + "finisher", + "spell", + "splash" + ], + "Prince": [ + "support", + "tank" + ], + "Princess": [ + "splash", + "support" + ], + "Rage": [ + "spell", + "support" + ], + "Ram Rider": [ + "support", + "win_condition" + ], + "Rascals": [ + "anti_air", + "defense", + "support" + ], + "Rocket": [ + "finisher", + "spell" + ], + "Royal Delivery": [ + "defense", + "spell", + "splash" + ], + "Royal Ghost": [ + "splash", + "support", + "tank" + ], + "Royal Ghost Evolution": [ + "splash", + "support", + "tank" + ], + "Royal Giant": [ + "support", + "tank", + "win_condition" + ], + "Royal Giant Evolution": [ + "support", + "tank", + "win_condition" + ], + "Royal Hogs": [ + "support", + "win_condition" + ], + "Royal Hogs Evolution": [ + "support", + "win_condition" + ], + "Royal Recruits": [ + "defense", + "support" + ], + "Royal Recruits Evolution": [ + "defense", + "support" + ], + "Skeleton Army": [ + "defense", + "support" + ], + "Skeleton Army Evolution": [ + "defense", + "support" + ], + "Skeleton Barrel": [ + "support", + "win_condition" + ], + "Skeleton Barrel Evolution": [ + "support", + "win_condition" + ], + "Skeleton Dragons": [ + "anti_air", + "splash", + "support" + ], + "Skeleton King": [ + "support", + "tank" + ], + "Skeletons": [ + "cycle", + "defense", + "support" + ], + "Skeletons Evolution": [ + "cycle", + "defense", + "support" + ], + "Sparky": [ + "defense", + "support" + ], + "Spear Goblins": [ + "cycle", + "support" + ], + "Spirit Empress": [ + "support" + ], + "Suspicious Bush": [ + "support" + ], + "Tesla": [ + "anti_air", + "building", + "defense" + ], + "Tesla Evolution": [ + "anti_air", + "building", + "defense" + ], + "The Log": [ + "cycle", + "defense", + "spell" + ], + "Three Musketeers": [ + "anti_air", + "support", + "win_condition" + ], + "Tombstone": [ + "building", + "defense" + ], + "Tornado": [ + "control", + "defense", + "spell" + ], + "Valkyrie": [ + "defense", + "splash", + "support", + "tank" + ], + "Valkyrie Evolution": [ + "defense", + "splash", + "support", + "tank" + ], + "Void": [ + "finisher", + "spell" + ], + "Wall Breakers": [ + "cycle", + "support", + "win_condition" + ], + "Wall Breakers Evolution": [ + "cycle", + "support", + "win_condition" + ], + "Witch": [ + "splash", + "support" + ], + "Witch Evolution": [ + "splash", + "support" + ], + "Wizard": [ + "anti_air", + "splash", + "support" + ], + "Wizard Evolution": [ + "anti_air", + "splash", + "support" + ], + "Wizard Hero": [ + "anti_air", + "splash", + "support" + ], + "X-Bow": [ + "building", + "defense", + "win_condition" + ], + "Zap": [ + "cycle", + "spell" + ], + "Zap Evolution": [ + "cycle", + "spell" + ], + "Zappies": [ + "anti_air", + "defense", + "support" + ] } } \ No newline at end of file diff --git a/config/db/seeds/epic.rb b/config/db/seeds/epic.rb index 475b03a..e6fb14e 100644 --- a/config/db/seeds/epic.rb +++ b/config/db/seeds/epic.rb @@ -6,21 +6,21 @@ EPIC_CARDS = [ { name: "Guards", elixir_cost: 3, rarity: "epic", type: "troop", is_meta: 0 }, { name: "Dark Prince", elixir_cost: 4, rarity: "epic", type: "troop", is_meta: 1 }, { name: "Hunter", elixir_cost: 4, rarity: "epic", type: "troop", is_meta: 1 }, - { name: "Baby Dragon", elixir_cost: 4, rarity: "epic", type: "troop", is_meta: 1 }, - { name: "Goblin Drill", elixir_cost: 4, rarity: "epic", type: "troop", is_meta: 1 }, - { name: "Prince", elixir_cost: 5, rarity: "epic", type: "troop", is_meta: 1 }, + { name: "Baby Dragon", elixir_cost: 4, rarity: "epic", type: "troop", is_meta: 0 }, + { name: "Goblin Drill", elixir_cost: 4, rarity: "epic", type: "troop", is_meta: 0 }, + { name: "Prince", elixir_cost: 5, rarity: "epic", type: "troop", is_meta: 0 }, { name: "Balloon", elixir_cost: 5, rarity: "epic", type: "troop", is_meta: 1 }, { name: "Witch", elixir_cost: 5, rarity: "epic", type: "troop", is_meta: 0 }, - { name: "Bowler", elixir_cost: 5, rarity: "epic", type: "troop", is_meta: 0 }, - { name: "Cannon Cart", elixir_cost: 5, rarity: "epic", type: "troop", is_meta: 0 }, + { name: "Bowler", elixir_cost: 5, rarity: "epic", type: "troop", is_meta: 1 }, + { name: "Cannon Cart", elixir_cost: 5, rarity: "epic", type: "troop", is_meta: 1 }, { name: "Electro Dragon", elixir_cost: 5, rarity: "epic", type: "troop", is_meta: 0 }, { name: "Giant Skeleton", elixir_cost: 6, rarity: "epic", type: "troop", is_meta: 1 }, { name: "Goblin Giant", elixir_cost: 6, rarity: "epic", type: "troop", is_meta: 0 }, { name: "P.E.K.K.A", elixir_cost: 7, rarity: "epic", type: "troop", is_meta: 1 }, - { name: "Golem", elixir_cost: 8, rarity: "epic", type: "troop", is_meta: 0 }, + { name: "Golem", elixir_cost: 8, rarity: "epic", type: "troop", is_meta: 1 }, { name: "Electro Giant", elixir_cost: 7, rarity: "epic", type: "troop", is_meta: 0 }, { name: "Goblin Giantess", elixir_cost: 6, rarity: "epic", type: "troop", is_meta: 0 }, - { name: "Skeleton Army", elixir_cost: 3, rarity: "epic", type: "troop", is_meta: 1 }, + { name: "Skeleton Army", elixir_cost: 3, rarity: "epic", type: "troop", is_meta: 0 }, { name: "Executioner", elixir_cost: 5, rarity: "epic", type: "troop", is_meta: 0 }, # Заклинания @@ -28,12 +28,12 @@ EPIC_CARDS = [ { name: "Rage", elixir_cost: 2, rarity: "epic", type: "spell", is_meta: 0 }, { name: "Clone", elixir_cost: 3, rarity: "epic", type: "spell", is_meta: 0 }, { name: "Freeze", elixir_cost: 4, rarity: "epic", type: "spell", is_meta: 1 }, - { name: "Poison", elixir_cost: 4, rarity: "epic", type: "spell", is_meta: 1 }, - { name: "Lightning", elixir_cost: 6, rarity: "epic", type: "spell", is_meta: 0 }, + { name: "Poison", elixir_cost: 4, rarity: "epic", type: "spell", is_meta: 0 }, + { name: "Lightning", elixir_cost: 6, rarity: "epic", type: "spell", is_meta: 1 }, { name: "Void", elixir_cost: 3, rarity: "epic", type: "spell", is_meta: 0 }, - { name: "Goblin Barrel", elixir_cost: 3, rarity: "epic", type: "spell", is_meta: 1 }, + { name: "Goblin Barrel", elixir_cost: 3, rarity: "epic", type: "spell", is_meta: 0 }, { name: "Tornado", elixir_cost: 3, rarity: "epic", type: "spell", is_meta: 1 }, # Здания - { name: "X-Bow", elixir_cost: 6, rarity: "epic", type: "building", is_meta: 0 } + { name: "X-Bow", elixir_cost: 6, rarity: "epic", type: "building", is_meta: 1 } ].freeze \ No newline at end of file diff --git a/config/db/seeds/hero.rb b/config/db/seeds/hero.rb index d416229..5a1e00e 100644 --- a/config/db/seeds/hero.rb +++ b/config/db/seeds/hero.rb @@ -2,12 +2,12 @@ HERO_CARDS = [ { name: "Goblins Hero", elixir_cost: 2, rarity: "hero", type: "troop", is_meta: 1 }, - { name: "Knight Hero", elixir_cost: 3, rarity: "hero", type: "troop", is_meta: 1 }, + { name: "Knight Hero", elixir_cost: 3, rarity: "hero", type: "troop", is_meta: 0 }, { name: "Ice Golem Hero", elixir_cost: 2, rarity: "hero", type: "troop", is_meta: 0 }, - { name: "Mega Minion Hero", elixir_cost: 3, rarity: "hero", type: "troop", is_meta: 0 }, - { name: "Mini P.E.K.K.A Hero", elixir_cost: 4, rarity: "hero", type: "troop", is_meta: 1 }, - { name: "Musketeer Hero", elixir_cost: 4, rarity: "hero", type: "troop", is_meta: 1 }, - { name: "Giant Hero", elixir_cost: 5, rarity: "hero", type: "troop", is_meta: 0 }, + { name: "Mega Minion Hero", elixir_cost: 3, rarity: "hero", type: "troop", is_meta: 1 }, + { name: "Mini P.E.K.K.A Hero", elixir_cost: 4, rarity: "hero", type: "troop", is_meta: 0 }, + { name: "Musketeer Hero", elixir_cost: 4, rarity: "hero", type: "troop", is_meta: 0 }, + { name: "Giant Hero", elixir_cost: 5, rarity: "hero", type: "troop", is_meta: 1 }, { name: "Wizard Hero", elixir_cost: 5, rarity: "hero", type: "troop", is_meta: 0 }, { name: "Barbarian Barrel Hero", elixir_cost: 2, rarity: "hero", type: "spell", is_meta: 1 }, { name: "Magic Archer Hero", elixir_cost: 4, rarity: "hero", type: "troop", is_meta: 1 } diff --git a/config/db/seeds/legendary.rb b/config/db/seeds/legendary.rb index be2c479..f034734 100644 --- a/config/db/seeds/legendary.rb +++ b/config/db/seeds/legendary.rb @@ -2,27 +2,27 @@ LEGENDARY_CARDS = [ # Юниты - { name: "Miner", elixir_cost: 3, rarity: "legendary", type: "troop", is_meta: 1 }, - { name: "Princess", elixir_cost: 3, rarity: "legendary", type: "troop", is_meta: 1 }, + { name: "Miner", elixir_cost: 3, rarity: "legendary", type: "troop", is_meta: 0 }, + { name: "Princess", elixir_cost: 3, rarity: "legendary", type: "troop", is_meta: 0 }, { name: "Ice Wizard", elixir_cost: 3, rarity: "legendary", type: "troop", is_meta: 0 }, - { name: "Royal Ghost", elixir_cost: 3, rarity: "legendary", type: "troop", is_meta: 1 }, + { name: "Royal Ghost", elixir_cost: 3, rarity: "legendary", type: "troop", is_meta: 0 }, { name: "Bandit", elixir_cost: 3, rarity: "legendary", type: "troop", is_meta: 1 }, - { name: "Fisherman", elixir_cost: 3, rarity: "legendary", type: "troop", is_meta: 0 }, + { name: "Fisherman", elixir_cost: 3, rarity: "legendary", type: "troop", is_meta: 1 }, { name: "Electro Wizard", elixir_cost: 4, rarity: "legendary", type: "troop", is_meta: 1 }, - { name: "Inferno Dragon", elixir_cost: 4, rarity: "legendary", type: "troop", is_meta: 1 }, + { name: "Inferno Dragon", elixir_cost: 4, rarity: "legendary", type: "troop", is_meta: 0 }, { name: "Phoenix", elixir_cost: 4, rarity: "legendary", type: "troop", is_meta: 1 }, - { name: "Magic Archer", elixir_cost: 4, rarity: "legendary", type: "troop", is_meta: 1 }, - { name: "Lumberjack", elixir_cost: 4, rarity: "legendary", type: "troop", is_meta: 1 }, - { name: "Night Witch", elixir_cost: 4, rarity: "legendary", type: "troop", is_meta: 0 }, - { name: "Mother Witch", elixir_cost: 4, rarity: "legendary", type: "troop", is_meta: 0 }, + { name: "Magic Archer", elixir_cost: 4, rarity: "legendary", type: "troop", is_meta: 0 }, + { name: "Lumberjack", elixir_cost: 4, rarity: "legendary", type: "troop", is_meta: 0 }, + { name: "Night Witch", elixir_cost: 4, rarity: "legendary", type: "troop", is_meta: 1 }, + { name: "Mother Witch", elixir_cost: 4, rarity: "legendary", type: "troop", is_meta: 1 }, { name: "Ram Rider", elixir_cost: 5, rarity: "legendary", type: "troop", is_meta: 0 }, { name: "Goblin Machine", elixir_cost: 5, rarity: "legendary", type: "troop", is_meta: 0 }, { name: "Sparky", elixir_cost: 6, rarity: "legendary", type: "troop", is_meta: 0 }, - { name: "Spirit Empress", elixir_cost: 6, rarity: "legendary", type: "troop", is_meta: 0 }, - { name: "Mega Knight", elixir_cost: 7, rarity: "legendary", type: "troop", is_meta: 1 }, - { name: "Lava Hound", elixir_cost: 7, rarity: "legendary", type: "troop", is_meta: 0 }, + { name: "Spirit Empress", elixir_cost: 6, rarity: "legendary", type: "troop", is_meta: 1 }, + { name: "Mega Knight", elixir_cost: 7, rarity: "legendary", type: "troop", is_meta: 0 }, + { name: "Lava Hound", elixir_cost: 7, rarity: "legendary", type: "troop", is_meta: 1 }, # Заклинания - { name: "The Log", elixir_cost: 2, rarity: "legendary", type: "spell", is_meta: 1 }, + { name: "The Log", elixir_cost: 2, rarity: "legendary", type: "spell", is_meta: 0 }, { name: "Graveyard", elixir_cost: 5, rarity: "legendary", type: "spell", is_meta: 1 } ].freeze \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index a845b81..505a76a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,6 +2,7 @@ module ClashDeckGenerator2 class Routes < Hanami::Routes - # Add your routes here. See https://guides.hanamirb.org/routing/overview/ for details. + root to: "home.index" + post "/decks/generate", to: "decks.generate" end -end +end \ No newline at end of file diff --git a/script/build_roles_json.rb b/script/build_roles_json.rb new file mode 100644 index 0000000..2d1d042 --- /dev/null +++ b/script/build_roles_json.rb @@ -0,0 +1,257 @@ +# frozen_string_literal: true + +require "json" +require_relative "../config/app" + +Hanami.app.prepare + +module ClashDeckGenerator2 + module Scripts + class BuildRolesJson + OUTPUT_PATH = File.expand_path("../config/cards/roles.json", __dir__) + + BASE_ROLES_BY_TYPE = { + "spell" => %w[spell], + "building" => %w[building defense], + "troop" => %w[support] + }.freeze + + ROLE_OVERRIDES = { + # cycle + "Skeletons" => %w[cycle defense], + "Ice Spirit" => %w[cycle support], + "Electro Spirit" => %w[cycle support], + "Fire Spirit" => %w[cycle support splash], + "Heal Spirit" => %w[cycle support], + "Goblins" => %w[cycle defense], + "Spear Goblins" => %w[cycle support], + "Bats" => %w[cycle anti_air], + "Wall Breakers" => %w[cycle win_condition], + "Bomber" => %w[cycle splash support], + + # win conditions + "Hog Rider" => %w[win_condition], + "Royal Giant" => %w[win_condition tank], + "Goblin Barrel" => %w[win_condition], + "Balloon" => %w[win_condition], + "Giant" => %w[win_condition tank], + "Golem" => %w[win_condition tank], + "Electro Giant" => %w[win_condition tank], + "Goblin Drill" => %w[win_condition], + "X-Bow" => %w[win_condition building defense], + "Mortar" => %w[win_condition building defense], + "Ram Rider" => %w[win_condition support], + "Graveyard" => %w[win_condition spell], + "Royal Hogs" => %w[win_condition], + "Battle Ram" => %w[win_condition], + "Elixir Golem" => %w[win_condition tank], + "Lava Hound" => %w[win_condition tank anti_air], + "Goblin Barrel" => %w[win_condition], + "Goblin Drill" => %w[win_condition building], + "Miner" => %w[win_condition support], + "Goblin Giant" => %w[win_condition tank], + "Three Musketeers" => %w[win_condition support anti_air], + "Skeleton Barrel" => %w[win_condition], + "Rocket" => %w[spell finisher], + "Fireball" => %w[spell finisher splash], + "Poison" => %w[spell finisher splash], + "Lightning" => %w[spell finisher], + "Void" => %w[spell finisher], + + # anti air + "Archers" => %w[anti_air support cycle], + "Musketeer" => %w[anti_air support], + "Wizard" => %w[anti_air support splash], + "Baby Dragon" => %w[anti_air support splash], + "Electro Dragon" => %w[anti_air support splash], + "Inferno Dragon" => %w[anti_air defense], + "Mega Minion" => %w[anti_air support], + "Flying Machine" => %w[anti_air support], + "Hunter" => %w[anti_air defense], + "Firecracker" => %w[anti_air support splash], + "Minions" => %w[anti_air support], + "Minion Horde" => %w[anti_air support], + "Skeleton Dragons" => %w[anti_air support splash], + "Phoenix" => %w[anti_air support], + "Executioner" => %w[anti_air support splash], + "Tesla" => %w[anti_air building defense], + "Inferno Tower" => %w[building defense], + "Magic Archer" => %w[support anti_air], + "Zappies" => %w[support anti_air defense], + "Dart Goblin" => %w[support anti_air], + "Electro Wizard" => %w[support anti_air defense], + "Mother Witch" => %w[support anti_air], + "Little Prince" => %w[support anti_air], + + # tanks / mini tanks + "Knight" => %w[tank defense], + "Valkyrie" => %w[tank defense splash], + "Ice Golem" => %w[tank cycle defense], + "P.E.K.K.A" => %w[tank defense], + "Mini P.E.K.K.A" => %w[defense tank], + "Mega Knight" => %w[tank defense splash], + "Royal Ghost" => %w[tank support splash], + "Dark Prince" => %w[tank support splash], + "Prince" => %w[tank support], + "Skeleton King" => %w[tank support], + "Golden Knight" => %w[tank support], + "Monk" => %w[tank defense], + "Mighty Miner" => %w[defense tank], + "Giant Skeleton" => %w[tank splash], + "Lumberjack" => %w[support tank], + "Bandit" => %w[support], + "Battle Healer" => %w[support tank], + "Goblin Giantess" => %w[tank support], + + # buildings / defense + "Cannon" => %w[building defense], + "Tesla" => %w[building defense anti_air], + "Bomb Tower" => %w[building defense splash], + "Tombstone" => %w[building defense], + "Goblin Cage" => %w[building defense], + "Goblin Hut" => %w[building defense], + "Barbarian Hut" => %w[building defense], + "Furnace" => %w[building defense splash], + "Elixir Collector" => %w[building support], + "Goblin Drill" => %w[building win_condition], + "X-Bow" => %w[building win_condition defense], + "Mortar" => %w[building win_condition defense], + + # splash / control + "Bowler" => %w[splash support defense], + "Witch" => %w[support splash], + "Bomber" => %w[support splash cycle], + "Arrows" => %w[spell splash], + "Zap" => %w[spell cycle], + "Giant Snowball" => %w[spell cycle], + "Royal Delivery" => %w[spell defense splash], + "Tornado" => %w[spell control defense], + "Rage" => %w[spell support], + "Freeze" => %w[spell control], + "Clone" => %w[spell support], + "Mirror" => %w[spell support], + "Earthquake" => %w[spell building_hate], + "The Log" => %w[spell cycle defense], + "Barbarian Barrel" => %w[spell cycle defense], + + # support / utility + "Skeleton Army" => %w[defense], + "Guards" => %w[defense], + "Royal Recruits" => %w[defense], + "Rascals" => %w[defense support anti_air], + "Cannon Cart" => %w[support defense], + "Sparky" => %w[support defense], + "Night Witch" => %w[support], + "Princess" => %w[support splash], + "Ice Wizard" => %w[support defense splash], + "Fisherman" => %w[defense control], + "Goblin Demolisher" => %w[support splash], + "Suspicious Bush" => %w[support], + "Battle Ram" => %w[win_condition], + "Royal Hogs" => %w[win_condition], + "Royal Recruits" => %w[defense], + "Electro Giant" => %w[win_condition tank], + "Electro Dragon" => %w[anti_air support splash], + "Goblin Machine" => %w[support], + "Spirit Empress" => %w[support], + "Boss Bandit" => %w[support], + "Goblins" => %w[cycle defense] + }.freeze + + def call + repo = ClashDeckGenerator2::Repos::CardsRepo.new + cards = unwrap(repo.all) + + cards_by_name = {} + cards.each do |card| + cards_by_name[card.name] ||= card + end + + result = { + "_meta" => { + "version" => 1, + "description" => "Card roles mapping for deck generation", + "generated_from_db" => true, + "cards_count" => cards_by_name.size, + "available_roles" => available_roles + }, + "cards" => {} + } + + cards_by_name.keys.sort.each do |card_name| + card = cards_by_name[card_name] + roles = roles_for(card) + result["cards"][card_name] = roles + end + + FileUtils.mkdir_p(File.dirname(OUTPUT_PATH)) + File.write(OUTPUT_PATH, JSON.pretty_generate(result)) + + puts "roles.json generated: #{OUTPUT_PATH}" + puts "Unique cards exported: #{cards_by_name.size}" + end + + private + + def unwrap(result) + if result.is_a?(Array) && result.first == :cards + result.last + else + result + end + end + + def roles_for(card) + roles = [] + roles.concat(BASE_ROLES_BY_TYPE.fetch(card.type, [])) + + base_name = base_card_name(card.name) + override_roles = ROLE_OVERRIDES[card.name] || ROLE_OVERRIDES[base_name] || [] + roles.concat(override_roles) + + roles = roles.map(&:to_s).map(&:strip).reject(&:empty?).uniq.sort + + roles = fallback_roles(card) if roles.empty? + roles + end + + def base_card_name(name) + name + .sub(/ Evolution\z/, "") + .sub(/ Hero\z/, "") + end + + def fallback_roles(card) + case card.type + when "spell" + ["spell"] + when "building" + %w[building defense] + when "troop" + ["support"] + else + ["support"] + end + end + + def available_roles + %w[ + anti_air + building + building_hate + control + cycle + defense + finisher + spell + splash + support + tank + win_condition + ] + end + end + end +end + +ClashDeckGenerator2::Scripts::BuildRolesJson.new.call \ No newline at end of file diff --git a/script/test_deck.rb b/script/test_deck.rb index 9036e38..2098d10 100644 --- a/script/test_deck.rb +++ b/script/test_deck.rb @@ -4,40 +4,14 @@ require_relative "../config/app" Hanami.app.prepare +repo = ClashDeckGenerator2::Repos::CardsRepo.new roles_repo = ClashDeckGenerator2::Services::CardRolesRepository.new +generator = ClashDeckGenerator2::Services::DeckGenerator.new( + cards_repo: repo, + roles_repo: roles_repo +) -puts roles_repo.roles_for("Knight").inspect -puts roles_repo.roles_for("Hog Rider").inspect -puts roles_repo.has_role?("Hog Rider", "win_condition") -puts roles_repo.has_role?("Tesla", "win_condition") -puts roles_repo.meta.inspect - -cards = repo.all -cards = cards.last if cards.is_a?(Array) && cards.first == :cards - -rarity_counts = cards.group_by(&:rarity).transform_values(&:size) - -puts "RARITY COUNTS:" -rarity_counts.each do |rarity, count| - puts "#{rarity}: #{count}" -end - -puts "========================================" -puts "POOL DIAGNOSTICS" -puts "========================================" -puts "Total cards: #{cards.size}" -puts "Spells: #{cards.count { |c| c.type == 'spell' }}" -puts "Troops: #{cards.count { |c| c.type == 'troop' }}" -puts "Buildings: #{cards.count { |c| c.type == 'building' }}" -puts "Champions: #{cards.count { |c| c.rarity == 'champion' }}" -puts "Heroes: #{cards.count { |c| c.rarity == 'hero' }}" -puts "Evolutions: #{cards.count { |c| c.rarity == 'evolution' }}" -puts "Avg elixir all cards: #{(cards.sum(&:elixir_cost).to_f / cards.size).round(2)}" -puts "========================================" - -generator = ClashDeckGenerator2::Services::DeckGenerator.new(cards_repo: repo) deck = generator.call - deck = deck.last if deck.is_a?(Array) && deck.first == :cards puts "========================================" @@ -45,7 +19,11 @@ puts "DECK GENERATED SUCCESSFULLY" puts "========================================" deck.each_with_index do |card, index| - puts "#{index + 1}. #{card.name} | #{card.type} | #{card.rarity} | #{card.elixir_cost}" + roles = roles_repo.roles_for(card.name) + meta_flag = card.is_meta == 1 ? "META" : "NON-META" + + puts "#{index + 1}. #{card.name} | #{card.type} | #{card.rarity} | #{card.elixir_cost} | #{meta_flag}" + puts " roles: #{roles.join(', ')}" end puts "----------------------------------------" @@ -56,10 +34,18 @@ puts "Unique cards count: #{names.uniq.size}" avg_elixir = deck.sum(&:elixir_cost).to_f / deck.size puts "Average elixir: #{avg_elixir.round(2)}" -puts "Spells count: #{deck.count { |c| c.type == 'spell' }}" +puts "Spells by type: #{deck.count { |c| c.type == 'spell' }}" puts "Troops count: #{deck.count { |c| c.type == 'troop' }}" puts "Buildings count: #{deck.count { |c| c.type == 'building' }}" puts "Champions count: #{deck.count { |c| c.rarity == 'champion' }}" puts "Heroes count: #{deck.count { |c| c.rarity == 'hero' }}" puts "Evolutions count: #{deck.count { |c| c.rarity == 'evolution' }}" +puts "Meta cards count: #{deck.count { |c| c.is_meta == 1 }}" + +all_roles = deck.flat_map { |card| roles_repo.roles_for(card.name) }.uniq.sort +puts "Deck roles coverage: #{all_roles.join(', ')}" + +puts "Win conditions count: #{deck.count { |c| roles_repo.has_role?(c.name, 'win_condition') }}" +puts "Anti-air count: #{deck.count { |c| roles_repo.has_role?(c.name, 'anti_air') }}" +puts "Spell-role count: #{deck.count { |c| roles_repo.has_role?(c.name, 'spell') }}" puts "========================================" \ No newline at end of file