From d16af289feeeede07f13f9a8a061535306487762 Mon Sep 17 00:00:00 2001 From: yummy Date: Wed, 25 Mar 2026 20:44:28 +0500 Subject: [PATCH] =?UTF-8?q?=D0=97=D0=BD=D0=B0=D1=87=D0=B8=D1=82=D0=B5?= =?UTF-8?q?=D0=BB=D1=8C=D0=BD=D0=BE=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=B1=D0=B4=20=D0=B8=20=D0=B3=D0=B5=D0=BD=D0=B5=D1=80?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8E=20=D1=82=D0=B0=D0=BA=20=D0=B6=D0=B5=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20=D1=80=D0=BE=D0=BB?= =?UTF-8?q?=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- app/services/card_roles_repository.rb | 71 ++++++++++ app/services/deck_generator.rb | 123 +++++++++++++++--- config/cards/roles.json | 21 +++ .../db/migrate/20260303180000_create_cards.rb | 9 ++ config/db/seeds.rb | 67 ++++------ config/db/seeds/champion.rb | 12 ++ config/db/seeds/common.rb | 41 ++++++ config/db/seeds/epic.rb | 39 ++++++ config/db/seeds/evo.rb | 43 ++++++ config/db/seeds/hero.rb | 14 ++ config/db/seeds/legendary.rb | 28 ++++ config/db/seeds/rare.rb | 38 ++++++ script/test_deck.rb | 44 ++++++- 14 files changed, 485 insertions(+), 67 deletions(-) create mode 100644 app/services/card_roles_repository.rb create mode 100644 config/cards/roles.json create mode 100644 config/db/seeds/champion.rb create mode 100644 config/db/seeds/common.rb create mode 100644 config/db/seeds/epic.rb create mode 100644 config/db/seeds/evo.rb create mode 100644 config/db/seeds/hero.rb create mode 100644 config/db/seeds/legendary.rb create mode 100644 config/db/seeds/rare.rb diff --git a/README.md b/README.md index 1b42733..ba7aba9 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/services/card_roles_repository.rb b/app/services/card_roles_repository.rb new file mode 100644 index 0000000..1ec0295 --- /dev/null +++ b/app/services/card_roles_repository.rb @@ -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 \ No newline at end of file diff --git a/app/services/deck_generator.rb b/app/services/deck_generator.rb index cba1c7b..4bb8732 100644 --- a/app/services/deck_generator.rb +++ b/app/services/deck_generator.rb @@ -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 diff --git a/config/cards/roles.json b/config/cards/roles.json new file mode 100644 index 0000000..7c1c40e --- /dev/null +++ b/config/cards/roles.json @@ -0,0 +1,21 @@ +{ + "_meta": { + "version": 1, + "description": "Card roles mapping for deck generation" + }, + "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"] + } +} \ No newline at end of file diff --git a/config/db/migrate/20260303180000_create_cards.rb b/config/db/migrate/20260303180000_create_cards.rb index 0eb69ae..c5f75ad 100644 --- a/config/db/migrate/20260303180000_create_cards.rb +++ b/config/db/migrate/20260303180000_create_cards.rb @@ -13,6 +13,14 @@ Sequel.migration do column :rarity, String column :type, String + # Уникальность имени карты + add_unique_constraint :name + + # Ограничения + constraint(:valid_rarity, Sequel.lit("rarity IN ('common','rare','epic','legendary','champion', 'evo', 'hero')")) + constraint(:valid_type, Sequel.lit("type IN ('troop','spell','building')")) + constraint(:positive_elixir, Sequel.lit("elixir_cost >= 0")) + # 0/1 флаг: карточка в актуальной мете или нет. # Можно позже заменить на true/false, если решим хранить boolean. column :is_meta, Integer, default: 0 @@ -20,6 +28,7 @@ Sequel.migration do # Технические таймстемпы для аудита и сортировки column :created_at, DateTime, null: false, default: Sequel::CURRENT_TIMESTAMP column :updated_at, DateTime, null: false, default: Sequel::CURRENT_TIMESTAMP + end end end diff --git a/config/db/seeds.rb b/config/db/seeds.rb index 91d48a8..f958b21 100644 --- a/config/db/seeds.rb +++ b/config/db/seeds.rb @@ -3,55 +3,29 @@ repo = ClashDeckGenerator2::Repos::CardsRepo.new CARD_TYPES = %w[troop spell building].freeze -RARITIES = %w[common rare epic legendary champion].freeze +RARITIES = %w[common rare epic legendary champion hero evolution].freeze -COMMON_CARDS = [ - # ======================== - # TROOPS - # ======================== - { name: "Skeletons", elixir_cost: 1, rarity: "common", type: "troop", is_meta: 1 }, - { name: "Electro Spirit", elixir_cost: 1, rarity: "common", type: "troop", is_meta: 0 }, - { name: "Fire Spirit", elixir_cost: 1, rarity: "common", type: "troop", is_meta: 0 }, - { name: "Ice Spirit", elixir_cost: 1, rarity: "common", type: "troop", is_meta: 0 }, +# Подключаем файлы с картами +require_relative "seeds/common" +require_relative "seeds/rare" +require_relative "seeds/epic" +require_relative "seeds/legendary" +require_relative "seeds/champion" +require_relative "seeds/hero" +require_relative "seeds/evo" - { name: "Goblins", elixir_cost: 2, rarity: "common", type: "troop", is_meta: 0 }, - { name: "Spear Goblins", elixir_cost: 2, rarity: "common", type: "troop", is_meta: 0 }, - { name: "Bomber", elixir_cost: 2, rarity: "common", type: "troop", is_meta: 0 }, - { name: "Bats", elixir_cost: 2, rarity: "common", type: "troop", is_meta: 0 }, - { name: "Berserker", elixir_cost: 2, rarity: "common", type: "troop", is_meta: 0 }, +# Объединяем все карты +CARDS = [] +CARDS.concat(COMMON_CARDS) +CARDS.concat(RARE_CARDS) +CARDS.concat(EPIC_CARDS) +CARDS.concat(LEGENDARY_CARDS) +CARDS.concat(CHAMPION_CARDS) +CARDS.concat(HERO_CARDS) +CARDS.concat(EVOLUTION_CARDS) - { name: "Archers", elixir_cost: 3, rarity: "common", type: "troop", is_meta: 1 }, - { name: "Knight", elixir_cost: 3, rarity: "common", type: "troop", is_meta: 1 }, - { name: "Minions", elixir_cost: 3, rarity: "common", type: "troop", is_meta: 0 }, - { name: "Goblin Gang", elixir_cost: 3, rarity: "common", type: "troop", is_meta: 0 }, - { name: "Skeleton Barrel", elixir_cost: 3, rarity: "common", type: "troop", is_meta: 0 }, - { name: "Firecracker", elixir_cost: 3, rarity: "common", type: "troop", is_meta: 1 }, - { name: "Skeleton Dragons", elixir_cost: 4, rarity: "common", type: "troop", is_meta: 0 }, - { name: "Barbarians", elixir_cost: 5, rarity: "common", type: "troop", is_meta: 0 }, - { name: "Minion Horde", elixir_cost: 5, rarity: "common", type: "troop", is_meta: 1 }, - { name: "Rascals", elixir_cost: 5, rarity: "common", type: "troop", is_meta: 1 }, - { name: "Royal Giant", elixir_cost: 6, rarity: "common", type: "troop", is_meta: 1 }, - { name: "Elite Barbarians", elixir_cost: 6, rarity: "common", type: "troop", is_meta: 0 }, - { name: "Royal Recruits", elixir_cost: 7, rarity: "common", type: "troop", is_meta: 0 }, - - # ======================== - # SPELLS - # ======================== - { name: "Zap", elixir_cost: 2, rarity: "common", type: "spell", is_meta: 1 }, - { name: "Giant Snowball", elixir_cost: 2, rarity: "common", type: "spell", is_meta: 0 }, - { name: "Arrows", elixir_cost: 3, rarity: "common", type: "spell", is_meta: 0 }, - { name: "Royal Delivery", elixir_cost: 3, rarity: "common", type: "spell", is_meta: 0 }, - - # ======================== - # BUILDINGS - # ======================== - { name: "Cannon", elixir_cost: 3, rarity: "common", type: "building", is_meta: 0 }, - { name: "Mortar", elixir_cost: 4, rarity: "common", type: "building", is_meta: 0 }, - { name: "Tesla", elixir_cost: 4, rarity: "common", type: "building", is_meta: 0 } -].freeze - -CARDS = COMMON_CARDS +# ВАЛИДАЦИЯ CARDS.each do |c| raise "Missing name" if c[:name].nil? || c[:name].strip.empty? raise "Missing elixir_cost for #{c[:name]}" if c[:elixir_cost].nil? @@ -62,10 +36,13 @@ CARDS.each do |c| raise "Unknown type: #{c[:type]} for #{c[:name]}" unless CARD_TYPES.include?(c[:type]) end +# Проверка на дубликаты names = CARDS.map { |c| c[:name] } duplicates = names.group_by { |name| name }.select { |_k, v| v.size > 1 }.keys raise "Duplicate cards found: #{duplicates.join(', ')}" if duplicates.any? + +# ЗАПИСЬ В БАЗУ CARDS.each do |card| repo.create(card) end \ No newline at end of file diff --git a/config/db/seeds/champion.rb b/config/db/seeds/champion.rb new file mode 100644 index 0000000..3cfea14 --- /dev/null +++ b/config/db/seeds/champion.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +CHAMPION_CARDS = [ + { name: "Little Prince", elixir_cost: 3, rarity: "champion", type: "troop", is_meta: 1 }, + { name: "Golden Knight", elixir_cost: 4, rarity: "champion", type: "troop", is_meta: 1 }, + { name: "Skeleton King", elixir_cost: 4, rarity: "champion", type: "troop", is_meta: 1 }, + { name: "Mighty Miner", elixir_cost: 4, rarity: "champion", type: "troop", is_meta: 1 }, + { name: "Archer Queen", elixir_cost: 5, rarity: "champion", type: "troop", is_meta: 1 }, + { name: "Goblinstein", elixir_cost: 5, rarity: "champion", type: "troop", is_meta: 0 }, + { name: "Monk", elixir_cost: 5, rarity: "champion", type: "troop", is_meta: 1 }, + { name: "Boss Bandit", elixir_cost: 6, rarity: "champion", type: "troop", is_meta: 0 } +].freeze \ No newline at end of file diff --git a/config/db/seeds/common.rb b/config/db/seeds/common.rb new file mode 100644 index 0000000..abfa1c1 --- /dev/null +++ b/config/db/seeds/common.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +COMMON_CARDS = [ + + #Юниты + { name: "Skeletons", elixir_cost: 1, rarity: "common", type: "troop", is_meta: 1 }, + { name: "Electro Spirit", elixir_cost: 1, rarity: "common", type: "troop", is_meta: 0 }, + { name: "Fire Spirit", elixir_cost: 1, rarity: "common", type: "troop", is_meta: 0 }, + { name: "Ice Spirit", elixir_cost: 1, rarity: "common", type: "troop", is_meta: 0 }, + + { name: "Goblins", elixir_cost: 2, rarity: "common", type: "troop", is_meta: 0 }, + { name: "Spear Goblins", elixir_cost: 2, rarity: "common", type: "troop", is_meta: 0 }, + { name: "Bomber", elixir_cost: 2, rarity: "common", type: "troop", is_meta: 0 }, + { name: "Bats", elixir_cost: 2, rarity: "common", type: "troop", is_meta: 0 }, + { name: "Berserker", elixir_cost: 2, rarity: "common", type: "troop", is_meta: 0 }, + + { name: "Archers", elixir_cost: 3, rarity: "common", type: "troop", is_meta: 1 }, + { name: "Knight", elixir_cost: 3, rarity: "common", type: "troop", is_meta: 1 }, + { name: "Minions", elixir_cost: 3, rarity: "common", type: "troop", is_meta: 0 }, + { name: "Goblin Gang", elixir_cost: 3, rarity: "common", type: "troop", is_meta: 0 }, + { name: "Skeleton Barrel", elixir_cost: 3, rarity: "common", type: "troop", is_meta: 0 }, + { name: "Firecracker", elixir_cost: 3, rarity: "common", type: "troop", is_meta: 1 }, + { name: "Skeleton Dragons", elixir_cost: 4, rarity: "common", type: "troop", is_meta: 0 }, + { name: "Barbarians", elixir_cost: 5, rarity: "common", type: "troop", is_meta: 0 }, + { name: "Minion Horde", elixir_cost: 5, rarity: "common", type: "troop", is_meta: 1 }, + { name: "Rascals", elixir_cost: 5, rarity: "common", type: "troop", is_meta: 1 }, + { name: "Royal Giant", elixir_cost: 6, rarity: "common", type: "troop", is_meta: 1 }, + { name: "Elite Barbarians", elixir_cost: 6, rarity: "common", type: "troop", is_meta: 0 }, + { name: "Royal Recruits", elixir_cost: 7, rarity: "common", type: "troop", is_meta: 0 }, + + #Заклинания + { name: "Zap", elixir_cost: 2, rarity: "common", type: "spell", is_meta: 1 }, + { name: "Giant Snowball", elixir_cost: 2, rarity: "common", type: "spell", is_meta: 0 }, + { name: "Arrows", elixir_cost: 3, rarity: "common", type: "spell", is_meta: 0 }, + { name: "Royal Delivery", elixir_cost: 3, rarity: "common", type: "spell", is_meta: 0 }, + + #Здания + { name: "Cannon", elixir_cost: 3, rarity: "common", type: "building", is_meta: 0 }, + { name: "Mortar", elixir_cost: 4, rarity: "common", type: "building", is_meta: 0 }, + { name: "Tesla", elixir_cost: 4, rarity: "common", type: "building", is_meta: 0 } +].freeze \ No newline at end of file diff --git a/config/db/seeds/epic.rb b/config/db/seeds/epic.rb new file mode 100644 index 0000000..475b03a --- /dev/null +++ b/config/db/seeds/epic.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +EPIC_CARDS = [ + # Юниты + { name: "Wall Breakers", elixir_cost: 2, rarity: "epic", type: "troop", is_meta: 0 }, + { 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: "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: "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: "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: "Executioner", elixir_cost: 5, rarity: "epic", type: "troop", is_meta: 0 }, + + # Заклинания + { name: "Mirror", elixir_cost: 0, rarity: "epic", type: "spell", is_meta: 0 }, + { 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: "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: "Tornado", elixir_cost: 3, rarity: "epic", type: "spell", is_meta: 1 }, + + # Здания + { name: "X-Bow", elixir_cost: 6, rarity: "epic", type: "building", is_meta: 0 } +].freeze \ No newline at end of file diff --git a/config/db/seeds/evo.rb b/config/db/seeds/evo.rb new file mode 100644 index 0000000..688d82b --- /dev/null +++ b/config/db/seeds/evo.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +EVOLUTION_CARDS = [ + { name: "Archers Evolution", elixir_cost: 3, rarity: "evolution", type: "troop", is_meta: 1 }, + { name: "Baby Dragon Evolution", elixir_cost: 4, rarity: "evolution", type: "troop", is_meta: 1 }, + { name: "Barbarians Evolution", elixir_cost: 5, rarity: "evolution", type: "troop", is_meta: 0 }, + { name: "Battle Ram Evolution", elixir_cost: 4, rarity: "evolution", type: "troop", is_meta: 0 }, + { name: "Bats Evolution", elixir_cost: 2, rarity: "evolution", type: "troop", is_meta: 0 }, + { name: "Bomber Evolution", elixir_cost: 2, rarity: "evolution", type: "troop", is_meta: 1 }, + { name: "Cannon Evolution", elixir_cost: 3, rarity: "evolution", type: "building", is_meta: 0 }, + { name: "Dart Goblin Evolution", elixir_cost: 3, rarity: "evolution", type: "troop", is_meta: 0 }, + { name: "Electro Dragon Evolution", elixir_cost: 5, rarity: "evolution", type: "troop", is_meta: 1 }, + { name: "Executioner Evolution", elixir_cost: 5, rarity: "evolution", type: "troop", is_meta: 0 }, + { name: "Firecracker Evolution", elixir_cost: 3, rarity: "evolution", type: "troop", is_meta: 1 }, + { name: "Furnace Evolution", elixir_cost: 4, rarity: "evolution", type: "building", is_meta: 0 }, + { name: "Giant Snowball Evolution", elixir_cost: 2, rarity: "evolution", type: "spell", is_meta: 0 }, + { name: "Goblin Barrel Evolution", elixir_cost: 3, rarity: "evolution", type: "spell", is_meta: 1 }, + { name: "Goblin Cage Evolution", elixir_cost: 4, rarity: "evolution", type: "building", is_meta: 0 }, + { name: "Goblin Drill Evolution", elixir_cost: 4, rarity: "evolution", type: "building", is_meta: 1 }, + { name: "Goblin Giant Evolution", elixir_cost: 6, rarity: "evolution", type: "troop", is_meta: 0 }, + { name: "Hunter Evolution", elixir_cost: 4, rarity: "evolution", type: "troop", is_meta: 0 }, + { name: "Ice Spirit Evolution", elixir_cost: 1, rarity: "evolution", type: "troop", is_meta: 1 }, + { name: "Inferno Dragon Evolution", elixir_cost: 4, rarity: "evolution", type: "troop", is_meta: 1 }, + { name: "Knight Evolution", elixir_cost: 3, rarity: "evolution", type: "troop", is_meta: 1 }, + { name: "Lumberjack Evolution", elixir_cost: 4, rarity: "evolution", type: "troop", is_meta: 0 }, + { name: "Mega Knight Evolution", elixir_cost: 7, rarity: "evolution", type: "troop", is_meta: 1 }, + { name: "Mortar Evolution", elixir_cost: 4, rarity: "evolution", type: "building", is_meta: 0 }, + { name: "Musketeer Evolution", elixir_cost: 4, rarity: "evolution", type: "troop", is_meta: 1 }, + { name: "P.E.K.K.A Evolution", elixir_cost: 7, rarity: "evolution", type: "troop", is_meta: 1 }, + { name: "Royal Ghost Evolution", elixir_cost: 3, rarity: "evolution", type: "troop", is_meta: 1 }, + { name: "Royal Giant Evolution", elixir_cost: 6, rarity: "evolution", type: "troop", is_meta: 1 }, + { name: "Royal Hogs Evolution", elixir_cost: 5, rarity: "evolution", type: "troop", is_meta: 0 }, + { name: "Royal Recruits Evolution", elixir_cost: 7, rarity: "evolution", type: "troop", is_meta: 0 }, + { name: "Skeleton Army Evolution", elixir_cost: 3, rarity: "evolution", type: "troop", is_meta: 1 }, + { name: "Skeleton Barrel Evolution", elixir_cost: 3, rarity: "evolution", type: "troop", is_meta: 0 }, + { name: "Skeletons Evolution", elixir_cost: 1, rarity: "evolution", type: "troop", is_meta: 1 }, + { name: "Tesla Evolution", elixir_cost: 4, rarity: "evolution", type: "building", is_meta: 1 }, + { name: "Valkyrie Evolution", elixir_cost: 4, rarity: "evolution", type: "troop", is_meta: 1 }, + { name: "Wall Breakers Evolution", elixir_cost: 2, rarity: "evolution", type: "troop", is_meta: 0 }, + { name: "Witch Evolution", elixir_cost: 5, rarity: "evolution", type: "troop", is_meta: 0 }, + { name: "Wizard Evolution", elixir_cost: 5, rarity: "evolution", type: "troop", is_meta: 0 }, + { name: "Zap Evolution", elixir_cost: 2, rarity: "evolution", type: "spell", is_meta: 1 } +].freeze \ No newline at end of file diff --git a/config/db/seeds/hero.rb b/config/db/seeds/hero.rb new file mode 100644 index 0000000..d416229 --- /dev/null +++ b/config/db/seeds/hero.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +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: "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: "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 } +].freeze \ No newline at end of file diff --git a/config/db/seeds/legendary.rb b/config/db/seeds/legendary.rb new file mode 100644 index 0000000..be2c479 --- /dev/null +++ b/config/db/seeds/legendary.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +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: "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: "Bandit", elixir_cost: 3, rarity: "legendary", type: "troop", is_meta: 1 }, + { name: "Fisherman", elixir_cost: 3, rarity: "legendary", type: "troop", is_meta: 0 }, + { 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: "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: "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: "The Log", elixir_cost: 2, rarity: "legendary", type: "spell", is_meta: 1 }, + { name: "Graveyard", elixir_cost: 5, rarity: "legendary", type: "spell", is_meta: 1 } +].freeze \ No newline at end of file diff --git a/config/db/seeds/rare.rb b/config/db/seeds/rare.rb new file mode 100644 index 0000000..25aed0d --- /dev/null +++ b/config/db/seeds/rare.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +RARE_CARDS = [ + # Юниты + { name: "Heal Spirit", elixir_cost: 1, rarity: "rare", type: "troop", is_meta: 0 }, + { name: "Ice Golem", elixir_cost: 2, rarity: "rare", type: "troop", is_meta: 0 }, + { name: "Suspicious Bush", elixir_cost: 2, rarity: "rare", type: "troop", is_meta: 0 }, + { name: "Mega Minion", elixir_cost: 3, rarity: "rare", type: "troop", is_meta: 0 }, + { name: "Dart Goblin", elixir_cost: 3, rarity: "rare", type: "troop", is_meta: 0 }, + { name: "Elixir Golem", elixir_cost: 3, rarity: "rare", type: "troop", is_meta: 0 }, + { name: "Mini P.E.K.K.A", elixir_cost: 4, rarity: "rare", type: "troop", is_meta: 1 }, + { name: "Musketeer", elixir_cost: 4, rarity: "rare", type: "troop", is_meta: 1 }, + { name: "Valkyrie", elixir_cost: 4, rarity: "rare", type: "troop", is_meta: 1 }, + { name: "Battle Ram", elixir_cost: 4, rarity: "rare", type: "troop", is_meta: 0 }, + { name: "Hog Rider", elixir_cost: 4, rarity: "rare", type: "troop", is_meta: 1 }, + { name: "Battle Healer", elixir_cost: 4, rarity: "rare", type: "troop", is_meta: 0 }, + { name: "Goblin Demolisher", elixir_cost: 4, rarity: "rare", type: "troop", is_meta: 0 }, + { name: "Giant", elixir_cost: 5, rarity: "rare", type: "troop", is_meta: 0 }, + { name: "Wizard", elixir_cost: 5, rarity: "rare", type: "troop", is_meta: 0 }, + { name: "Royal Hogs", elixir_cost: 5, rarity: "rare", type: "troop", is_meta: 0 }, + { name: "Three Musketeers", elixir_cost: 9, rarity: "rare", type: "troop", is_meta: 0 }, + { name: "Zappies", elixir_cost: 4, rarity: "rare", type: "troop", is_meta: 0 }, + { name: "Furnace", elixir_cost: 4, rarity: "rare", type: "troop", is_meta: 0 }, + + # Заклинания + { name: "Earthquake", elixir_cost: 3, rarity: "rare", type: "spell", is_meta: 1 }, + { name: "Fireball", elixir_cost: 4, rarity: "rare", type: "spell", is_meta: 1 }, + { name: "Rocket", elixir_cost: 6, rarity: "rare", type: "spell", is_meta: 0 }, + + # Здания + { name: "Tombstone", elixir_cost: 3, rarity: "rare", type: "building", is_meta: 0 }, + { name: "Goblin Cage", elixir_cost: 4, rarity: "rare", type: "building", is_meta: 0 }, + { name: "Goblin Hut", elixir_cost: 5, rarity: "rare", type: "building", is_meta: 0 }, + { name: "Bomb Tower", elixir_cost: 4, rarity: "rare", type: "building", is_meta: 0 }, + { name: "Inferno Tower", elixir_cost: 5, rarity: "rare", type: "building", is_meta: 1 }, + { name: "Barbarian Hut", elixir_cost: 6, rarity: "rare", type: "building", is_meta: 0 }, + { name: "Elixir Collector", elixir_cost: 6, rarity: "rare", type: "building", is_meta: 0 } +].freeze \ No newline at end of file diff --git a/script/test_deck.rb b/script/test_deck.rb index 1a06316..9036e38 100644 --- a/script/test_deck.rb +++ b/script/test_deck.rb @@ -4,13 +4,41 @@ require_relative "../config/app" Hanami.app.prepare -repo = ClashDeckGenerator2::Repos::CardsRepo.new -cards = repo.all.select { |card| card.rarity == "common" } +roles_repo = ClashDeckGenerator2::Services::CardRolesRepository.new -raise "No common cards found in DB." if cards.empty? -raise "Not enough common cards to build a deck. Need at least 8, got #{cards.size}." if cards.size < 8 +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 -deck = cards.sample(8) +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 "========================================" puts "DECK GENERATED SUCCESSFULLY" @@ -28,4 +56,10 @@ 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 "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 "========================================" \ No newline at end of file