|
require "yaml" |
|
|
|
module Puzzdra |
|
Drop = Struct.new(:color, :reinforced) |
|
Vanish = Struct.new(:color, :drops_count) |
|
Attack = Struct.new(:color, :type, :damage) |
|
DropString = %w/火 水 木 光 闇 回 * 毒/ |
|
|
|
class Field |
|
attr_reader :drops |
|
|
|
def initialize |
|
@drops = 30.times.map { Puzzdra::Drop.new(rand(6), 0) } |
|
end |
|
|
|
def inspect |
|
_color_counts.reject { |_, count| count == 0 }.map do |color, count| |
|
"#{DropString[color]}: #{count}" |
|
end.join(", ") |
|
end |
|
|
|
def skill(name) |
|
case name |
|
when "北方七星陣" |
|
colors = [2, 3, 4] |
|
@drops = 30.times.map { Puzzdra::Drop.new(colors.sample, 0) } |
|
|
|
when "2色陣・木闇" || "継界召龍陣・木闇" |
|
colors = [2, 4] |
|
@drops = 30.times.map { Puzzdra::Drop.new(colors.sample, 0) } |
|
|
|
when "ドロップ強化・光闇" || "神魔の息吹" |
|
@drops.each do |d| |
|
next unless [3, 4].member? d.color |
|
d.reinforced = 1 |
|
end |
|
|
|
when "ダブル攻撃態勢・木" |
|
@drops.each do |d| |
|
next unless [0, 5].member? d.color |
|
d.color = 2 |
|
end |
|
|
|
when "防御態勢・木" |
|
@drops.each do |d| |
|
next unless d.color == 1 |
|
d.color = 5 |
|
end |
|
|
|
when "ドロップ変化・光" |
|
@drops.each do |d| |
|
next unless d.color == 4 |
|
d.color = 3 |
|
end |
|
|
|
else |
|
raise |
|
end |
|
end |
|
|
|
# 盤面、落ちコンを考慮しない理論最大コンボ |
|
# returns array of Puzzdra::Vanish |
|
def max_combo |
|
if elmore? |
|
_max_combo_elmore _color_counts |
|
else |
|
_max_combo_normal _color_counts |
|
end |
|
end |
|
|
|
def elmore? |
|
# max_color_countが16, 17の場合はミルフィーユ積みを採用する |
|
if _color_counts.values.max > 17 |
|
true |
|
else |
|
false |
|
end |
|
end |
|
|
|
private |
|
|
|
# 色数の大きい順に並んでいることを保証する |
|
def _color_counts |
|
colors = (0..7).map do |color| |
|
{ |
|
color: color, |
|
count: @drops.count { |d| d.color == color } |
|
} |
|
end |
|
|
|
colors.sort_by { |c| c[:count] }.reverse.each_with_object({}) do |c, result| |
|
result[c[:color]] = c[:count] |
|
end |
|
end |
|
|
|
def _max_combo_normal(color_counts) |
|
max_color_count = color_counts.values.max |
|
color_counts.each_with_object([]) do |(color, count), combo| |
|
loop do |
|
if count >= 6 |
|
combo.push Vanish.new(color, 3) |
|
elsif count >= 3 |
|
if max_color_count > 15 |
|
combo.push Vanish.new(color, 3) |
|
else |
|
combo.push Vanish.new(color, count) |
|
end |
|
break |
|
else |
|
break |
|
end |
|
count -= 3 |
|
end |
|
end |
|
end |
|
|
|
# http://livedoor.blogimg.jp/myhrtks/imgs/1/2/128525a9.jpg |
|
# てきとう |
|
ELMORE_COMBO = { |
|
1 => [], |
|
2 => [], |
|
3 => [3], |
|
4 => [3], |
|
5 => [5], |
|
6 => [3,3], |
|
7 => [5], |
|
8 => [4,3], |
|
9 => [3,3,3], |
|
10 => [3,3,3], |
|
11 => [3,3,3], |
|
12 => [3,3,3], |
|
13 => [3,3,3,3], |
|
14 => [3,3,3,3], |
|
15 => [3,3,3,3,3], |
|
16 => [3,3,3,3,4], |
|
17 => [3,3,3,3,4], |
|
18 => [3,3,3,3,3,3], |
|
19 => [3,3,3,3,3,3], |
|
20 => [8,3,3,3,3], |
|
21 => [8,3,3,3,3], |
|
22 => [9,3,3,3,3], |
|
23 => [14,3,3,3], |
|
24 => [17,3,3], |
|
25 => [18,3,3], |
|
26 => [23,3], |
|
27 => [27], |
|
28 => [28], |
|
29 => [29], |
|
30 => [30], |
|
} |
|
def _max_combo_elmore(color_counts) |
|
max_color_count = color_counts.values.max |
|
remains = 30 |
|
color_counts.each_with_object([]) do |(color, count), combo| |
|
next if count == 0 |
|
|
|
# XXX: [18, 9, 3], [18, 6, 6] のケースへの暫定対応 |
|
if max_color_count == 18 |
|
if count == 3 && remains == 3 |
|
next |
|
elsif count == 6 && remains == 6 |
|
combo.push Vanish.new(color, 3) |
|
next |
|
end |
|
end |
|
|
|
ELMORE_COMBO[count].each do |c| |
|
combo.push Vanish.new(color, c) |
|
end |
|
|
|
remains -= count |
|
end |
|
end |
|
end |
|
|
|
class Monster |
|
Data = YAML.load File.read("monsters.yml") |
|
attr_reader :data |
|
|
|
def initialize(name, plus) |
|
@data = Data.find { |d| d["name"] == name } |
|
@plus = plus || [0, 0, 0] |
|
end |
|
|
|
def atk |
|
@data["atk"] + @plus[1] * 5 |
|
end |
|
|
|
# ダメージ計算 |
|
# 攻撃力(atk) * 属性倍率(color_power) * コンボ倍率(combo_power) |
|
def attack(combo) |
|
attacks = [] |
|
|
|
main_color = @data["element"][0] |
|
sub_color = @data["element"][1] |
|
|
|
attacks.push Attack.new( |
|
main_color, |
|
@data["type"], |
|
atk * _color_power(main_color, combo) * _combo_power(combo) |
|
) |
|
|
|
if sub_color |
|
sub_power = main_color == sub_color ? 0.1 : 0.3 |
|
|
|
attacks.push Attack.new( |
|
sub_color, |
|
@data["type"], |
|
atk * _color_power(sub_color, combo) * _combo_power(combo) * sub_power |
|
) |
|
end |
|
|
|
attacks |
|
end |
|
|
|
private |
|
|
|
def _color_power(color, combo) |
|
combo.select { |v| v.color == color }.inject(0) do |power, v| |
|
power += 1 + 0.25 * (v.drops_count - 3) |
|
end |
|
end |
|
|
|
def _combo_power(combo) |
|
return 0 if combo.empty? |
|
1 + 0.25 * (combo.size - 1) |
|
end |
|
end |
|
|
|
class DamageCalc |
|
def initialize(field, party) |
|
@field = field |
|
@party = party |
|
@leader_skills = [] |
|
@skills = [] |
|
end |
|
|
|
def total_damage |
|
combo = @field.max_combo |
|
|
|
attacks = @party.map { |monster| monster.attack combo }.flatten |
|
|
|
# apply skill |
|
@skills.each do |s| |
|
s.call(attacks, combo) |
|
end |
|
|
|
# apply leader_skill |
|
@leader_skills.each do |ls| |
|
ls.call(attacks, combo) |
|
end |
|
|
|
attacks.inject(0) { |total, attack| total += attack.damage }.to_i |
|
end |
|
|
|
def leader_skill(name) |
|
case name |
|
when "クシナダ" || "撫子の想い" |
|
@leader_skills.push( |
|
lambda do |attacks, combo| |
|
return attacks if combo.size < 3 |
|
power = [combo.size / 2, 10].min |
|
attacks.each { |a| a.damage = a.damage * power } |
|
end |
|
) |
|
|
|
else |
|
raise |
|
end |
|
end |
|
|
|
def skill(name) |
|
case name |
|
when "神エンハンス" || "創造の息吹" |
|
@skills.push( |
|
lambda do |attacks, combo| |
|
attacks.each do |a| |
|
next unless a.type.member?("神") |
|
a.damage = a.damage * 2 |
|
end |
|
end |
|
) |
|
|
|
when "バランスエンハンス" |
|
@skills.push( |
|
lambda do |attacks, combo| |
|
attacks.each do |a| |
|
next unless a.type.member?("バランス") |
|
a.damage = a.damage * 3 |
|
end |
|
end |
|
) |
|
|
|
else |
|
raise |
|
end |
|
end |
|
end |
|
|
|
# 既に計算した結果があればキャッシュしてくれるやつ |
|
class DamageCalcManager |
|
attr_reader :stats |
|
|
|
def initialize(party: [], leader: "", friend: "", skills: [], field_skills: [], field_skill_selectable: false, cache_by_original_field: false) |
|
@party = party |
|
@leader = leader |
|
@friend = friend |
|
@skills = skills |
|
@field_skills = field_skills |
|
|
|
@result_cache_by_field = {} |
|
@result_cache_by_original_field = {} |
|
|
|
@stats = { |
|
total: 0, |
|
normal: 0, |
|
elmore: 0, |
|
} |
|
|
|
# field_skillを使う前の盤面で結果をキャッシュするか否か |
|
@cache_by_original_field = cache_by_original_field |
|
|
|
# field_skillを使用するか否かと、使用する順番を任意に選択可能か |
|
@field_skill_selectable = field_skill_selectable |
|
end |
|
|
|
def calc(original_field) |
|
original_field_str = original_field.inspect |
|
|
|
# get cache by original field |
|
if @cache_by_original_field && @result_cache_by_original_field[original_field_str] |
|
return _update_stats @result_cache_by_original_field[original_field_str] |
|
end |
|
|
|
if @field_skill_selectable |
|
result_candidates = _field_skill_permutation.map do |field_skills| |
|
_calc_sub(original_field, field_skills) |
|
end |
|
|
|
result = result_candidates.sort_by { |rc| [rc[:damage], rc[:field_skills].size * -1 ] }.last |
|
else |
|
result = _calc_sub(original_field) |
|
end |
|
|
|
# set cache |
|
@result_cache_by_original_field[original_field_str] = result if @cache_by_original_field |
|
|
|
return _update_stats result |
|
end |
|
|
|
private |
|
|
|
def _update_stats(result) |
|
@stats[:total] += 1 |
|
if result[:field].elmore? |
|
@stats[:elmore] += 1 |
|
else |
|
@stats[:normal] += 1 |
|
end |
|
result |
|
end |
|
|
|
# スキルの使用順を考慮した組み合わせ |
|
# [:a, :b] => [[], [:a], [:b], [:a, :b], [:b, :a]] |
|
def _field_skill_permutation |
|
@field_skill_permutation ||= (0..@field_skills.size).map do |skill_count| |
|
@field_skills.permutation(skill_count).to_a |
|
end.flatten(1) |
|
end |
|
|
|
def _calc_sub(original_field, field_skills = @field_skills) |
|
# deep copy |
|
field = Marshal.load(Marshal.dump(original_field)) |
|
|
|
field_skills.each do |fs| |
|
field.skill fs |
|
end |
|
field_str = field.inspect |
|
|
|
# get cache by field |
|
if @result_cache_by_field[field_str] |
|
return @result_cache_by_field[field_str] |
|
end |
|
|
|
damage_calc = Puzzdra::DamageCalc.new(field, @party) |
|
damage_calc.leader_skill(@leader) |
|
damage_calc.leader_skill(@friend) |
|
|
|
@skills.each do |s| |
|
damage_calc.skill s |
|
end |
|
|
|
# set cache |
|
@result_cache_by_field[field_str] = { |
|
damage: damage_calc.total_damage, |
|
field: field, |
|
original_field: original_field, |
|
combo: field.max_combo, |
|
field_skills: field_skills, |
|
} |
|
end |
|
end |
|
end |
|
|
|
def result_format(result, cache_by_original_field) |
|
if cache_by_original_field |
|
"#{result[:damage]} #{result[:combo].size}コンボ (#{result[:original_field].inspect}) => (#{result[:field].inspect}) #{result[:field_skills]}" |
|
else |
|
"#{result[:damage]} #{result[:combo].size}コンボ (#{result[:field].inspect})" |
|
end |
|
end |
|
|
|
YAML.load(File.read("teams.yml")).each do |team| |
|
p "--" |
|
p team["members"] |
|
|
|
party = team["members"].each_with_index.map do |name, i| |
|
Puzzdra::Monster.new(name, team["pluses"][i]) |
|
end |
|
|
|
result = [] |
|
|
|
damage_calc_manager = Puzzdra::DamageCalcManager.new( |
|
party: party, |
|
leader: team["members"].first, |
|
friend: team["members"].last, |
|
skills: team["skills"], |
|
field_skills: team["field_skills"], |
|
field_skill_selectable: team["field_skill_selectable"], |
|
cache_by_original_field: team["cache_by_original_field"], |
|
) |
|
|
|
100000.times do |i| |
|
#p i if (i % 10000 == 0 && i != 0) |
|
|
|
field = Puzzdra::Field.new |
|
|
|
result.push damage_calc_manager.calc(field) |
|
end |
|
|
|
damage_avg = result.inject(0) { |sum, r| sum += r[:damage] }.to_f / result.size |
|
min_result = result.min_by { |r| r[:damage] } |
|
max_result = result.max_by { |r| r[:damage] } |
|
variance = result.inject(0) { |sum, r| sum += (r[:damage] - damage_avg) ** 2 } / result.size |
|
|
|
puts "平均ダメージ: #{damage_avg}" |
|
puts "最小ダメージ: #{result_format(min_result, team["cache_by_original_field"])}" |
|
puts "最大ダメージ: #{result_format(max_result, team["cache_by_original_field"])}" |
|
puts "標準偏差 : #{Math.sqrt(variance).to_i}" |
|
puts "エルモア積み: #{sprintf("%.2f", damage_calc_manager.stats[:elmore].to_f * 100 / damage_calc_manager.stats[:total])}%" |
|
end |