Генератор колоды Beta
This commit is contained in:
54
app/actions/decks/generate.rb
Normal file
54
app/actions/decks/generate.rb
Normal file
@@ -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
|
||||
12
app/actions/home/index.rb
Normal file
12
app/actions/home/index.rb
Normal file
@@ -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
|
||||
@@ -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
|
||||
88
app/templates/home/index.html.erb
Normal file
88
app/templates/home/index.html.erb
Normal file
@@ -0,0 +1,88 @@
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 960px;
|
||||
margin: 40px auto;
|
||||
padding: 0 16px;
|
||||
background: #111;
|
||||
color: #f5f5f5;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #ff6b00;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 18px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.stats, .cards {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 1px solid #333;
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
margin-bottom: 10px;
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
.meta {
|
||||
color: #4ade80;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.non-meta {
|
||||
color: #f87171;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: #aaa;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<h1>Clash Deck Generator</h1>
|
||||
<p>Generate a deck based on roles, meta cards, and special slot rules.</p>
|
||||
|
||||
<form action="/decks/generate" method="post">
|
||||
<button type="submit">Generate deck</button>
|
||||
</form>
|
||||
|
||||
<% if deck.any? %>
|
||||
<div class="stats">
|
||||
<h2>Deck stats</h2>
|
||||
<p><strong>Average elixir:</strong> <%= stats[:average_elixir] %></p>
|
||||
<p><strong>Meta cards:</strong> <%= stats[:meta_cards_count] %></p>
|
||||
<p><strong>Spells:</strong> <%= stats[:spells_count] %></p>
|
||||
<p><strong>Troops:</strong> <%= stats[:troops_count] %></p>
|
||||
<p><strong>Buildings:</strong> <%= stats[:buildings_count] %></p>
|
||||
<p><strong>Champions:</strong> <%= stats[:champions_count] %></p>
|
||||
<p><strong>Heroes:</strong> <%= stats[:heroes_count] %></p>
|
||||
<p><strong>Evolutions:</strong> <%= stats[:evolutions_count] %></p>
|
||||
</div>
|
||||
|
||||
<div class="cards">
|
||||
<h2>Deck cards</h2>
|
||||
|
||||
<% 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" %>
|
||||
|
||||
<div class="card">
|
||||
<div><strong><%= index + 1 %>. <%= card.name %></strong></div>
|
||||
<div class="muted"><%= card.rarity %> | <%= card.type %> | <%= card.elixir_cost %> elixir</div>
|
||||
<div class="<%= meta_class %>"><%= meta_text %></div>
|
||||
<div class="muted">Roles: <%= roles_repo.roles_for(card.name).join(", ") %></div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -4,11 +4,8 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Clash deck generator2</title>
|
||||
<%= favicon_tag %>
|
||||
<%= stylesheet_tag "app" %>
|
||||
</head>
|
||||
<body>
|
||||
<%= yield %>
|
||||
<%= javascript_tag "app" %>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
13
app/views/home/index.rb
Normal file
13
app/views/home/index.rb
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user