enjine 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (269) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +28 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +201 -0
  5. data/SUPPORTED.md +387 -0
  6. data/docs/ARCHITECTURE.md +141 -0
  7. data/docs/DESIGN.md +139 -0
  8. data/docs/MECHANICS_COVERAGE.md +670 -0
  9. data/docs/guides/adding-a-mechanic.md +60 -0
  10. data/docs/guides/api.md +614 -0
  11. data/docs/guides/card-data-format.md +1472 -0
  12. data/docs/guides/deck-builder.md +157 -0
  13. data/docs/plans/standard.md +371 -0
  14. data/docs/reference/abilities.md +1680 -0
  15. data/docs/reference/costs.md +339 -0
  16. data/docs/reference/counters.md +121 -0
  17. data/docs/reference/decisions.md +102 -0
  18. data/docs/reference/effects.md +2340 -0
  19. data/docs/reference/filters.md +172 -0
  20. data/docs/reference/keywords.md +647 -0
  21. data/docs/reference/subtypes.md +169 -0
  22. data/docs/reference/supertypes.md +31 -0
  23. data/docs/reference/triggers.md +797 -0
  24. data/docs/reference/types.md +216 -0
  25. data/exe/enjine +7 -0
  26. data/lib/enjine/actions/activate_mana_ability.rb +24 -0
  27. data/lib/enjine/actions/cast.rb +57 -0
  28. data/lib/enjine/actions/pass.rb +18 -0
  29. data/lib/enjine/actions/play_land.rb +52 -0
  30. data/lib/enjine/actions/special_action.rb +37 -0
  31. data/lib/enjine/actions.rb +20 -0
  32. data/lib/enjine/agents/busy.rb +27 -0
  33. data/lib/enjine/agents/random.rb +28 -0
  34. data/lib/enjine/card_definition.rb +433 -0
  35. data/lib/enjine/card_faces.rb +116 -0
  36. data/lib/enjine/catalog.rb +56 -0
  37. data/lib/enjine/characteristics.rb +39 -0
  38. data/lib/enjine/cli/text.rb +295 -0
  39. data/lib/enjine/cli.rb +312 -0
  40. data/lib/enjine/decisions/bottom.rb +29 -0
  41. data/lib/enjine/decisions/casting.rb +188 -0
  42. data/lib/enjine/decisions/combat.rb +75 -0
  43. data/lib/enjine/decisions/discard.rb +29 -0
  44. data/lib/enjine/decisions/effect_choice.rb +21 -0
  45. data/lib/enjine/decisions/enter_choice.rb +26 -0
  46. data/lib/enjine/decisions/legend_rule.rb +21 -0
  47. data/lib/enjine/decisions/mulligan.rb +26 -0
  48. data/lib/enjine/decisions/order_triggers.rb +21 -0
  49. data/lib/enjine/decisions/play_or_draw.rb +19 -0
  50. data/lib/enjine/decisions/priority.rb +22 -0
  51. data/lib/enjine/decisions/untap_choice.rb +35 -0
  52. data/lib/enjine/decisions.rb +22 -0
  53. data/lib/enjine/deck.rb +151 -0
  54. data/lib/enjine/deck_builder.rb +454 -0
  55. data/lib/enjine/docs.rb +90 -0
  56. data/lib/enjine/engine/ability_layers.rb +118 -0
  57. data/lib/enjine/engine/attachments.rb +86 -0
  58. data/lib/enjine/engine/casting/alt_casting.rb +254 -0
  59. data/lib/enjine/engine/casting/cast_choices.rb +292 -0
  60. data/lib/enjine/engine/casting/casting.rb +326 -0
  61. data/lib/enjine/engine/casting/cost_options.rb +99 -0
  62. data/lib/enjine/engine/casting/costs.rb +117 -0
  63. data/lib/enjine/engine/casting/payment.rb +353 -0
  64. data/lib/enjine/engine/casting/permissions.rb +412 -0
  65. data/lib/enjine/engine/casting/target_counts.rb +113 -0
  66. data/lib/enjine/engine/casting/waterbend.rb +90 -0
  67. data/lib/enjine/engine/characteristics.rb +379 -0
  68. data/lib/enjine/engine/class_levels.rb +25 -0
  69. data/lib/enjine/engine/combat.rb +315 -0
  70. data/lib/enjine/engine/copies.rb +208 -0
  71. data/lib/enjine/engine/damage.rb +196 -0
  72. data/lib/enjine/engine/designations.rb +46 -0
  73. data/lib/enjine/engine/emblems.rb +37 -0
  74. data/lib/enjine/engine/entering.rb +156 -0
  75. data/lib/enjine/engine/face_down.rb +30 -0
  76. data/lib/enjine/engine/faces.rb +222 -0
  77. data/lib/enjine/engine/follow_ups.rb +27 -0
  78. data/lib/enjine/engine/mana_sources.rb +230 -0
  79. data/lib/enjine/engine/phasing.rb +74 -0
  80. data/lib/enjine/engine/pregame.rb +95 -0
  81. data/lib/enjine/engine/priority.rb +47 -0
  82. data/lib/enjine/engine/resolution.rb +168 -0
  83. data/lib/enjine/engine/rules.rb +287 -0
  84. data/lib/enjine/engine/sagas.rb +41 -0
  85. data/lib/enjine/engine/speed.rb +104 -0
  86. data/lib/enjine/engine/spell_grants.rb +46 -0
  87. data/lib/enjine/engine/state_based_actions.rb +157 -0
  88. data/lib/enjine/engine/triggers.rb +364 -0
  89. data/lib/enjine/engine/turn_history.rb +240 -0
  90. data/lib/enjine/engine/turn_structure.rb +224 -0
  91. data/lib/enjine/engine/zones.rb +278 -0
  92. data/lib/enjine/engine.rb +55 -0
  93. data/lib/enjine/errors.rb +62 -0
  94. data/lib/enjine/events.rb +161 -0
  95. data/lib/enjine/frames.rb +84 -0
  96. data/lib/enjine/fuzz.rb +112 -0
  97. data/lib/enjine/game.rb +204 -0
  98. data/lib/enjine/history.rb +26 -0
  99. data/lib/enjine/mana.rb +142 -0
  100. data/lib/enjine/mechanics/abilities/additional_triggers.rb +86 -0
  101. data/lib/enjine/mechanics/abilities/chosen_values.rb +213 -0
  102. data/lib/enjine/mechanics/abilities/cost_changes.rb +154 -0
  103. data/lib/enjine/mechanics/abilities/damage_replacements.rb +569 -0
  104. data/lib/enjine/mechanics/abilities/double_counters.rb +71 -0
  105. data/lib/enjine/mechanics/abilities/entering_choices.rb +91 -0
  106. data/lib/enjine/mechanics/abilities/entering_copy.rb +69 -0
  107. data/lib/enjine/mechanics/abilities/enters_tapped.rb +159 -0
  108. data/lib/enjine/mechanics/abilities/enters_with_counters.rb +116 -0
  109. data/lib/enjine/mechanics/abilities/mana_ability.rb +209 -0
  110. data/lib/enjine/mechanics/abilities/play_permissions.rb +527 -0
  111. data/lib/enjine/mechanics/abilities/player_rules.rb +169 -0
  112. data/lib/enjine/mechanics/abilities/replacement_effects.rb +368 -0
  113. data/lib/enjine/mechanics/abilities/restrictions.rb +144 -0
  114. data/lib/enjine/mechanics/abilities/spell_keywords.rb +55 -0
  115. data/lib/enjine/mechanics/abilities/stack_abilities.rb +542 -0
  116. data/lib/enjine/mechanics/abilities/static.rb +365 -0
  117. data/lib/enjine/mechanics/abilities/tapping_power.rb +71 -0
  118. data/lib/enjine/mechanics/abilities/turn_rules.rb +49 -0
  119. data/lib/enjine/mechanics/card_spec.rb +177 -0
  120. data/lib/enjine/mechanics/copy_except.rb +73 -0
  121. data/lib/enjine/mechanics/costs/base.rb +115 -0
  122. data/lib/enjine/mechanics/costs/behold.rb +76 -0
  123. data/lib/enjine/mechanics/costs/discard.rb +65 -0
  124. data/lib/enjine/mechanics/costs/exert.rb +29 -0
  125. data/lib/enjine/mechanics/costs/exile_cards.rb +39 -0
  126. data/lib/enjine/mechanics/costs/from_graveyard.rb +45 -0
  127. data/lib/enjine/mechanics/costs/get_counters.rb +39 -0
  128. data/lib/enjine/mechanics/costs/mana.rb +37 -0
  129. data/lib/enjine/mechanics/costs/one_of.rb +49 -0
  130. data/lib/enjine/mechanics/costs/pay_life.rb +50 -0
  131. data/lib/enjine/mechanics/costs/remove_counters.rb +51 -0
  132. data/lib/enjine/mechanics/costs/sacrifice.rb +59 -0
  133. data/lib/enjine/mechanics/costs/tap.rb +56 -0
  134. data/lib/enjine/mechanics/costs/waterbend.rb +29 -0
  135. data/lib/enjine/mechanics/counters/kinds.rb +190 -0
  136. data/lib/enjine/mechanics/effects/add_subtypes.rb +30 -0
  137. data/lib/enjine/mechanics/effects/airbend.rb +41 -0
  138. data/lib/enjine/mechanics/effects/amass.rb +124 -0
  139. data/lib/enjine/mechanics/effects/assimilate.rb +72 -0
  140. data/lib/enjine/mechanics/effects/base.rb +148 -0
  141. data/lib/enjine/mechanics/effects/become.rb +182 -0
  142. data/lib/enjine/mechanics/effects/behold.rb +48 -0
  143. data/lib/enjine/mechanics/effects/blight.rb +94 -0
  144. data/lib/enjine/mechanics/effects/choose_cards.rb +168 -0
  145. data/lib/enjine/mechanics/effects/copy.rb +370 -0
  146. data/lib/enjine/mechanics/effects/counter_spell.rb +21 -0
  147. data/lib/enjine/mechanics/effects/counters.rb +161 -0
  148. data/lib/enjine/mechanics/effects/damage.rb +72 -0
  149. data/lib/enjine/mechanics/effects/delayed_triggers.rb +112 -0
  150. data/lib/enjine/mechanics/effects/destroy.rb +22 -0
  151. data/lib/enjine/mechanics/effects/discard.rb +86 -0
  152. data/lib/enjine/mechanics/effects/draw.rb +16 -0
  153. data/lib/enjine/mechanics/effects/earthbend.rb +70 -0
  154. data/lib/enjine/mechanics/effects/emblem.rb +52 -0
  155. data/lib/enjine/mechanics/effects/endure.rb +63 -0
  156. data/lib/enjine/mechanics/effects/exile.rb +49 -0
  157. data/lib/enjine/mechanics/effects/extra_turn.rb +122 -0
  158. data/lib/enjine/mechanics/effects/faces.rb +115 -0
  159. data/lib/enjine/mechanics/effects/fight.rb +76 -0
  160. data/lib/enjine/mechanics/effects/flip_coins.rb +97 -0
  161. data/lib/enjine/mechanics/effects/gain_control.rb +94 -0
  162. data/lib/enjine/mechanics/effects/game_end.rb +42 -0
  163. data/lib/enjine/mechanics/effects/library.rb +327 -0
  164. data/lib/enjine/mechanics/effects/life.rb +83 -0
  165. data/lib/enjine/mechanics/effects/look_at_hand.rb +17 -0
  166. data/lib/enjine/mechanics/effects/loot.rb +118 -0
  167. data/lib/enjine/mechanics/effects/mana.rb +96 -0
  168. data/lib/enjine/mechanics/effects/may.rb +82 -0
  169. data/lib/enjine/mechanics/effects/meld.rb +77 -0
  170. data/lib/enjine/mechanics/effects/mill.rb +22 -0
  171. data/lib/enjine/mechanics/effects/pay.rb +231 -0
  172. data/lib/enjine/mechanics/effects/phasing.rb +51 -0
  173. data/lib/enjine/mechanics/effects/proliferate.rb +72 -0
  174. data/lib/enjine/mechanics/effects/pump.rb +60 -0
  175. data/lib/enjine/mechanics/effects/put_in_library.rb +98 -0
  176. data/lib/enjine/mechanics/effects/return_choice.rb +29 -0
  177. data/lib/enjine/mechanics/effects/return_to_battlefield.rb +80 -0
  178. data/lib/enjine/mechanics/effects/return_to_hand.rb +20 -0
  179. data/lib/enjine/mechanics/effects/roll_dice.rb +45 -0
  180. data/lib/enjine/mechanics/effects/sacrifice.rb +118 -0
  181. data/lib/enjine/mechanics/effects/scry_and_surveil.rb +74 -0
  182. data/lib/enjine/mechanics/effects/search.rb +156 -0
  183. data/lib/enjine/mechanics/effects/tap_untap.rb +23 -0
  184. data/lib/enjine/mechanics/effects/tokens.rb +76 -0
  185. data/lib/enjine/mechanics/filters/any_target.rb +15 -0
  186. data/lib/enjine/mechanics/filters/base.rb +386 -0
  187. data/lib/enjine/mechanics/filters/graveyard.rb +23 -0
  188. data/lib/enjine/mechanics/filters/one_of.rb +55 -0
  189. data/lib/enjine/mechanics/filters/permanents.rb +52 -0
  190. data/lib/enjine/mechanics/filters/players.rb +20 -0
  191. data/lib/enjine/mechanics/filters/stack.rb +97 -0
  192. data/lib/enjine/mechanics/follow_up.rb +46 -0
  193. data/lib/enjine/mechanics/keywords/alternative_costs.rb +361 -0
  194. data/lib/enjine/mechanics/keywords/beam_me_up.rb +78 -0
  195. data/lib/enjine/mechanics/keywords/changeling.rb +58 -0
  196. data/lib/enjine/mechanics/keywords/convoke.rb +77 -0
  197. data/lib/enjine/mechanics/keywords/crew.rb +155 -0
  198. data/lib/enjine/mechanics/keywords/cycling.rb +58 -0
  199. data/lib/enjine/mechanics/keywords/decayed.rb +25 -0
  200. data/lib/enjine/mechanics/keywords/devour.rb +61 -0
  201. data/lib/enjine/mechanics/keywords/evergreen.rb +54 -0
  202. data/lib/enjine/mechanics/keywords/extort.rb +26 -0
  203. data/lib/enjine/mechanics/keywords/flags.rb +46 -0
  204. data/lib/enjine/mechanics/keywords/foretell.rb +45 -0
  205. data/lib/enjine/mechanics/keywords/gift.rb +50 -0
  206. data/lib/enjine/mechanics/keywords/hideaway.rb +28 -0
  207. data/lib/enjine/mechanics/keywords/increment.rb +31 -0
  208. data/lib/enjine/mechanics/keywords/job_select.rb +29 -0
  209. data/lib/enjine/mechanics/keywords/kicker.rb +92 -0
  210. data/lib/enjine/mechanics/keywords/mobilize_and_firebending.rb +84 -0
  211. data/lib/enjine/mechanics/keywords/paradigm.rb +91 -0
  212. data/lib/enjine/mechanics/keywords/power_up.rb +48 -0
  213. data/lib/enjine/mechanics/keywords/protection.rb +111 -0
  214. data/lib/enjine/mechanics/keywords/prowess.rb +30 -0
  215. data/lib/enjine/mechanics/keywords/riot.rb +35 -0
  216. data/lib/enjine/mechanics/keywords/saddle.rb +115 -0
  217. data/lib/enjine/mechanics/keywords/speed.rb +89 -0
  218. data/lib/enjine/mechanics/keywords/station.rb +163 -0
  219. data/lib/enjine/mechanics/keywords/storied.rb +41 -0
  220. data/lib/enjine/mechanics/keywords/storm_and_cascade.rb +86 -0
  221. data/lib/enjine/mechanics/keywords/sunburst.rb +29 -0
  222. data/lib/enjine/mechanics/keywords/suspend.rb +191 -0
  223. data/lib/enjine/mechanics/keywords/teamwork.rb +45 -0
  224. data/lib/enjine/mechanics/keywords/ward.rb +67 -0
  225. data/lib/enjine/mechanics/linked.rb +67 -0
  226. data/lib/enjine/mechanics/mana_spent.rb +99 -0
  227. data/lib/enjine/mechanics/selector.rb +207 -0
  228. data/lib/enjine/mechanics/subtypes/auras_and_equipment.rb +176 -0
  229. data/lib/enjine/mechanics/subtypes/basic_land_types.rb +36 -0
  230. data/lib/enjine/mechanics/subtypes/class_levels.rb +102 -0
  231. data/lib/enjine/mechanics/subtypes/saga.rb +89 -0
  232. data/lib/enjine/mechanics/supertypes/basic.rb +17 -0
  233. data/lib/enjine/mechanics/supertypes/legendary.rb +18 -0
  234. data/lib/enjine/mechanics/triggers/ability_triggers.rb +51 -0
  235. data/lib/enjine/mechanics/triggers/any_of.rb +43 -0
  236. data/lib/enjine/mechanics/triggers/base.rb +228 -0
  237. data/lib/enjine/mechanics/triggers/casting.rb +219 -0
  238. data/lib/enjine/mechanics/triggers/combat.rb +123 -0
  239. data/lib/enjine/mechanics/triggers/damage.rb +130 -0
  240. data/lib/enjine/mechanics/triggers/dilemma.rb +37 -0
  241. data/lib/enjine/mechanics/triggers/draw_and_discard.rb +85 -0
  242. data/lib/enjine/mechanics/triggers/keyword_actions.rb +55 -0
  243. data/lib/enjine/mechanics/triggers/life.rb +35 -0
  244. data/lib/enjine/mechanics/triggers/steps.rb +39 -0
  245. data/lib/enjine/mechanics/triggers/tap_and_counters.rb +111 -0
  246. data/lib/enjine/mechanics/triggers/zone_changes.rb +249 -0
  247. data/lib/enjine/mechanics/types/kindred.rb +30 -0
  248. data/lib/enjine/mechanics/types/land.rb +24 -0
  249. data/lib/enjine/mechanics/types/permanent_types.rb +67 -0
  250. data/lib/enjine/mechanics/types/spell_types.rb +38 -0
  251. data/lib/enjine/mechanics/values.rb +675 -0
  252. data/lib/enjine/mechanics.rb +74 -0
  253. data/lib/enjine/player.rb +30 -0
  254. data/lib/enjine/primitive.rb +61 -0
  255. data/lib/enjine/replacements.rb +191 -0
  256. data/lib/enjine/rng.rb +57 -0
  257. data/lib/enjine/rules_monitor.rb +404 -0
  258. data/lib/enjine/setup.rb +45 -0
  259. data/lib/enjine/snapshot/player_checks.rb +86 -0
  260. data/lib/enjine/snapshot/turn_checks.rb +91 -0
  261. data/lib/enjine/snapshot.rb +307 -0
  262. data/lib/enjine/state.rb +173 -0
  263. data/lib/enjine/steps.rb +117 -0
  264. data/lib/enjine/tokens.rb +25 -0
  265. data/lib/enjine/verify.rb +233 -0
  266. data/lib/enjine/version.rb +7 -0
  267. data/lib/enjine/view.rb +162 -0
  268. data/lib/enjine.rb +46 -0
  269. metadata +315 -0
@@ -0,0 +1,326 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enjine
4
+ class Engine
5
+ # Casting spells (CR 601) and activating abilities (CR 602), one atomic decision
6
+ # at a time: announce -> choose targets -> pay mana (tapping sources manually)
7
+ # -> cast. Until every cost is paid the whole process can be cancelled and is
8
+ # rewound (CR 734, "Handling Illegal Actions"). The in-progress context lives in
9
+ # casting so a game can be exported mid-cast.
10
+ module Casting
11
+ # ---- what can be started ---------------------------------------------------
12
+
13
+ def castable?(index, id)
14
+ obj = object(id)
15
+ defn = definition(obj)
16
+ return false unless defn.castable?
17
+ return false unless defn.instant_speed? || sorcery_timing?(index)
18
+ return false if cast_forbidden?(index, obj) # CR 101.2 (Engine::Rules)
19
+ return false unless additional_costs_payable?(obj) # CR 601.2f: e.g. "sacrifice a creature"
20
+
21
+ if ability_targetable?(defn.spell_ability, index, id) && # CR 601.2b: some way of paying its costs (Engine::CostOptions)
22
+ cost_variant_affordable?(index, spell_cost_list(obj, false), obj, defn.mana_cost, nil, { "spell" => id }) do
23
+ total_cost(index, it, spell: obj)
24
+ end
25
+ return true
26
+ end
27
+
28
+ kicked_castable?(index, obj) # CR 702.33g: only the kicked spell has legal targets
29
+ end
30
+
31
+ def activatable?(index, id, ability_ref)
32
+ obj = object(id)
33
+ ability = chars(obj).ability(ability_ref)
34
+ return false unless ability && %i[activated loyalty].include?(ability.kind)
35
+ return false unless obj.zone == ability.zone # CR 113.6: e.g. cycling only from the hand (702.29a)
36
+ return false if ability.sorcery_speed? && !sorcery_timing?(index)
37
+ return false if activation_forbidden?(index, obj) # CR 602.5, 101.2 (Engine::Rules)
38
+ return false unless activation_allowed?(index, obj, ability_ref, ability) # CR 602.5b
39
+
40
+ if (ability.kind == :loyalty) && (obj.loyalty_turn == state.turn || (loyalty(obj) + ability.loyalty_cost).negative?)
41
+ # CR 606.3: once per turn. CR 118.3: a cost can't remove more counters than there are.
42
+ return false
43
+ end
44
+
45
+ # CR 118.3, 601.2b: some way of paying its costs (Engine::CostOptions); waterbend: CR 701.67a
46
+ ability_targetable?(ability, index, nil, id, ref: ability_ref) &&
47
+ cost_variant_affordable?(index, ability.costs, obj, nil, needs_tap?(ability) ? id : nil, { "ability" => id }) do
48
+ total_cost(index, it, ability: ability, source: id)
49
+ end
50
+ end
51
+
52
+ # Whether the ability can get legal targets: for modal abilities, enough modes must (CR 700.2),
53
+ # not counting modes already chosen for the source of a "choose one that hasn't been chosen".
54
+ def ability_targetable?(ability, index, exclude, source = nil, ref: nil)
55
+ return true if ability.nil?
56
+ return targets_available?(ability.targets, index, exclude, source) unless ability.modal?
57
+
58
+ (available_modes(ability, index, exclude, source) - used_modes(ability, source, ref)).size >= ability.choose
59
+ end
60
+
61
+ def available_modes(ability, index, exclude, source = nil)
62
+ ability.modes.each_index.select { targets_available?(ability.modes[it].targets, index, exclude, source) }
63
+ end
64
+
65
+ # "Up to one target" needs no candidate (CR 601.2c).
66
+ def targets_available?(specs, index, exclude, source = nil)
67
+ specs.all? do |spec|
68
+ need = target_minimum(spec, index, source) # Engine::TargetCounts
69
+ need.zero? || spec.candidates_for(self, index, source).count do
70
+ it != exclude && targetable?(it, index, source, targeter: source || exclude) # a spell's card is `exclude`
71
+ end >= need
72
+ end
73
+ end
74
+
75
+ def optional_target?(spec) = spec.respond_to?(:optional?) && spec.optional?
76
+
77
+ # CR 702.11b: hexproof permanents can't be targeted by their controller's opponents. `source`: the
78
+ # permanent whose ability targets (nil for a spell); `targeter`: the spell, or the ability's source (protection,
79
+ # CR 702.16b); see Engine::Rules#targeting_forbidden?.
80
+ def targetable?(ref, index, source = nil, targeter: source)
81
+ return false if targeting_forbidden?(ref, index, source, targeter: targeter) # CR 702.11c, 115.4
82
+
83
+ obj = state.objects[ref]
84
+ return true unless obj && obj.zone == :battlefield && obj.controller != index
85
+
86
+ !chars(obj).keyword?(:hexproof)
87
+ end
88
+
89
+ # The ability an ability object on the stack represents. It keeps existing even if its
90
+ # source has left the battlefield (CR 113.7a), so it's looked up on the source's card.
91
+ def stack_ability(obj) = definition(obj).ability(obj.ability)
92
+
93
+ def needs_tap?(ability) = ability.costs.any?(Mechanics::Costs::Tap)
94
+
95
+ # The spell ability of a spell (printed, CR 113.3a).
96
+ def spell_ability_of(obj) = definition(obj).spell_ability
97
+
98
+ # ---- starting ----------------------------------------------------------------
99
+
100
+ # CR 601.2a: the card moves from where it is to the stack. `via` names how a card outside the
101
+ # hand is cast (see Engine::Permissions); "free" casts it without paying its mana cost during
102
+ # the resolution of a spell or ability (CR 608.2g, 118.9), after which nobody gets priority.
103
+ # `face`: cast another face of the card (a modal card's back, an Adventure, an Omen; Engine::Faces).
104
+ # `riders`: how an effect casting it while resolving changes the cast ("exile_instead", "any_mana_type").
105
+ def begin_cast!(index, card_id, via: nil, face: nil, riders: {})
106
+ return begin_outside_cast!(index, card_id, via) if outside_key?(card_id) # CR 400.11b
107
+
108
+ card = object(card_id)
109
+ method = via && play_method(index, card_id, via).merge(riders)
110
+ origin = { "id" => card_id, "zone" => card.zone.to_s,
111
+ "index" => state.zone_list(card.zone, card.owner).index(card_id) }
112
+ origin["memory"] = card.memory.slice("face_down", "may_look") if face_down?(card) # turned face up (CR 406.3a)
113
+ origin["memory"] = card.memory.slice("prepared_for") if card.memory["prepared_for"] # a prepare spell (CR 723.3c)
114
+ obj = move!(card_id, :stack, controller: index, reason: "cast")
115
+ turn_spell_face!(obj, face) # CR 712.8b, 716.3b, 721.3b
116
+ extra = method ? { "method" => method } : {}
117
+ extra["no_priority"] = true if %w[free resolving].include?(via) # cast while resolving (CR 608.2g)
118
+ start_casting!(index, obj.id, "spell", origin: origin, extra: extra)
119
+ end
120
+
121
+ # CR 602.2a: the ability is created on the stack.
122
+ def begin_activation!(index, source_id, ability_ref)
123
+ source = object(source_id)
124
+ # CR 602.2a: activating from a hidden zone reveals the card.
125
+ emit Events::CardRevealed.new(player: name(index), object: source_id, card: source.card) if source.zone == :hand
126
+ card, ref, granter = AbilityLayers.granted_parts(ability_ref) # a granted ability lives in the granting card's data
127
+ obj = create_object(card || source.card, index, :stack)
128
+ obj.kind = "ability"
129
+ obj.source = source_id
130
+ obj.ability = ref || ability_ref
131
+ obj.memory["granter"] = granter if granter # CR 607.1a: "Return Trusty Boomerang" (Selector "granter")
132
+ state.stack.unshift(obj.id)
133
+ start_casting!(index, obj.id, "ability", source: source_id, ability: ability_ref)
134
+ end
135
+
136
+ # `x` stays nil until announced (CR 601.2b); `remaining` stays nil until the total cost is
137
+ # determined and locked in after targets are chosen (CR 601.2f).
138
+ def start_casting!(index, object_id, kind, origin: nil, source: nil, ability: nil, extra: {})
139
+ state.frames << { "type" => "cast", "player" => index, "object" => object_id, "kind" => kind,
140
+ "origin" => origin, "source" => source, "ability" => ability, "targets" => [], "modes" => [],
141
+ "x" => nil, "remaining" => nil,
142
+ "pool" => player(index).mana_pool.transform_keys(&:to_s), "tapped" => [],
143
+ "restricted" => Snapshot.deep_copy(player(index).restricted_mana),
144
+ "pool_sources" => Snapshot.deep_copy(player(index).pool_sources),
145
+ "triggers_before" => state.pending_triggers.map { it["id"] } }.merge(extra)
146
+ ask!(nil) # advance! now steps the cast frame
147
+ end
148
+
149
+ # The cast/activation in progress (the top frame while a cast decision is pending).
150
+ def casting
151
+ frame = state.frames.last
152
+ frame if frame && frame["type"] == "cast"
153
+ end
154
+
155
+ def casting_ability
156
+ ctx = casting
157
+ obj = object(ctx["object"])
158
+ ctx["kind"] == "spell" ? definition(obj).spell_ability : stack_ability(obj)
159
+ end
160
+
161
+ # ---- the in-progress steps --------------------------------------------------
162
+
163
+ # Steps the cast frame: modes and X (601.2b), targets (601.2c), total cost (601.2f), payment.
164
+ def continue_casting!
165
+ ctx = casting
166
+ ability = casting_ability
167
+ return :wait if ask_kicker!(ctx)
168
+
169
+ if ability&.modal? && modes_pending?(ctx, ability)
170
+ min, max = mode_range(ctx, ability)
171
+ ask!({ "kind" => "choose_mode", "player" => ctx["player"], "choose" => max, "chosen" => ctx["modes"].size }
172
+ .merge(min == max ? {} : { "min" => min }))
173
+ return :wait
174
+ end
175
+ return :wait if ask_cost_option!(ctx) # CR 601.2b: which of two costs (Engine::CostOptions)
176
+ return :wait if ask_x!(ctx)
177
+
178
+ specs = casting_targets
179
+ return :wait if ask_targets!(ctx, specs) # Engine::TargetCounts
180
+ return :wait if ask_distribution!(ctx, ability)
181
+
182
+ return ask_harmonize!(ctx) if harmonize_pending?(ctx) # CR 702.180b (Engine::AltCasting)
183
+
184
+ lock_total_cost!(ctx) unless ctx["remaining"]
185
+ if beam_choice_pending?(ctx) then ask_beam_choice!(ctx) # CR 702.196a (Engine::Permissions)
186
+ elsif (cost = next_cost_choice(ctx)) then ask_cost_choice!(ctx, casting_costs(ctx)[cost])
187
+ elsif remaining_left?(ctx["remaining"])
188
+ ask!("kind" => "pay_cost", "player" => ctx["player"], "remaining" => remaining_text)
189
+ :wait
190
+ else
191
+ finish_casting!
192
+ :done
193
+ end
194
+ end
195
+
196
+ def casting_targets = casting_ability&.targets_for(casting["modes"], kicked: casting["kicked"]) || []
197
+
198
+ # CR 601.2c: targets, in order; a spell or ability can't target itself (CR 115.5), and the same
199
+ # object can't be chosen twice for one instance of the word "target" (`distinct_from`).
200
+ def target_candidates
201
+ ctx = casting
202
+ spec = casting_targets[target_slot(ctx)] # Engine::TargetCounts: a counted slot's list may be open
203
+ taken = spec.respond_to?(:distinct_from) ? spec.distinct_from.flat_map { Array(ctx["targets"][it]) } : []
204
+ taken += open_target_group(ctx)
205
+ candidates = spec.candidates_for(self, ctx["player"], ctx["source"]).reject do
206
+ it == ctx["object"] || taken.include?(it) || !targetable?(it, ctx["player"], ctx["source"],
207
+ targeter: ctx["source"] || ctx["object"])
208
+ end
209
+ sharing_type(candidates, spec, ctx)
210
+ end
211
+
212
+ # "Target permanents that share a card type" (`shares_type_with`, CR 205.2a): only candidates sharing one
213
+ # with the earlier target.
214
+ def sharing_type(candidates, spec, ctx)
215
+ index = spec.respond_to?(:shares_type_with) && spec.shares_type_with
216
+ other = index && state.objects[Array(ctx["targets"][index]).first] or return candidates
217
+
218
+ candidates.select { state.objects.key?(it) && chars(it).types.intersect?(chars(other).types) }
219
+ end
220
+
221
+ # The target options; an "up to one" target may be left unchosen (`done`).
222
+ def target_options
223
+ options = target_candidates.map { { type: "target", target: it } }
224
+ optional_target?(casting_targets[casting["targets"].size]) && !casting["counting"] ? options + [{ type: "done" }] : options
225
+ end
226
+
227
+ # `ref` nil: no target chosen for an optional ("up to one") target. Abilities that trigger when
228
+ # something becomes a target (ward, CR 702.21a) learn which spell or ability targeted it.
229
+ def choose_target!(ref)
230
+ ctx = casting
231
+ add_chosen_target!(ctx, ref)
232
+ if ref
233
+ queued = state.pending_triggers.size
234
+ emit Events::TargetChosen.new(player: name(ctx["player"]), object: ctx["object"], target: ref)
235
+ state.pending_triggers.drop(queued).each { it["memory"] = it["memory"].merge("targeted_by" => ctx["object"]) }
236
+ end
237
+ ask!(nil)
238
+ end
239
+
240
+ # CR 601.2h-i / 602.2b: remaining costs are paid, the spell becomes cast.
241
+ def finish_casting!
242
+ ctx = casting
243
+ index = ctx["player"]
244
+ obj = object(ctx["object"])
245
+ obj.targets = ctx["targets"].dup
246
+ obj.modes = ctx["modes"].dup unless ctx["modes"].empty?
247
+ obj.mana_spent = ctx["spent"].dup if ctx["spent"]
248
+ obj.memory["mana_from"] = Snapshot.deep_copy(ctx["spent_from"]) if ctx["spent_from"] # the sources (ManaSources)
249
+ record_announcements!(obj, ctx) # kicked (CR 702.33d), division (CR 601.2d), modes chosen before
250
+ record_cast_choices!(obj, ctx) # cost choices; facing a dilemma (CR 700.17)
251
+ waterbent!(ctx) # CR 701.67c (Engine::Waterbending)
252
+ if ctx["kind"] == "trigger"
253
+ emit Events::TriggerPutOnStack.new(player: name(index), object: obj.id, source: ctx["source"], card: obj.card,
254
+ ability: ctx["ability"], targets: obj.targets.flatten)
255
+ return # placing continues in the put_triggers frame; no priority yet
256
+ elsif ctx["kind"] == "ability"
257
+ pay_ability_costs!(ctx, obj)
258
+ record_activation!(ctx) # CR 602.5b
259
+ emit Events::AbilityActivated.new(player: name(index), object: obj.id, card: obj.card, source: ctx["source"],
260
+ ability: ctx["ability"], targets: obj.targets.flatten)
261
+ else
262
+ finish_spell_cast!(ctx, obj)
263
+ end
264
+ return if ctx["no_priority"] # CR 608.2g: no priority; the resolving spell or ability continues
265
+
266
+ action_taken!(index) # CR 117.3c (the frame is popped by advance!)
267
+ end
268
+
269
+ # A spell becomes cast (CR 601.2i) once its other costs are paid.
270
+ def finish_spell_cast!(ctx, obj)
271
+ casting_costs(ctx).each { it.pay!(self, obj) unless it.respond_to?(:choices_needed) } # pay life (CR 119.4)
272
+ pay_chosen_costs!(ctx) # e.g. the creature sacrificed for an additional cost (CR 601.2h)
273
+ finish_cast_method!(obj, ctx) # Engine::Permissions
274
+ cast_prepared_copy!(ctx.dig("origin", "id")) # CR 723.3c (Engine::Faces)
275
+ emit Events::SpellCast.new(player: name(ctx["player"]), object: obj.id, card: obj.card,
276
+ targets: obj.targets.flatten, during_resolution: ctx["no_priority"] == true)
277
+ end
278
+
279
+ # The non-mana costs (CR 602.2b): its own ({T}, sacrifice it, exile it from the graveyard), then
280
+ # the chosen ones (sacrifice another creature, remove counters). A source sacrificed as a cost is
281
+ # remembered as it last existed (its counters, CR 608.2h).
282
+ def pay_ability_costs!(ctx, obj)
283
+ source = object(ctx["source"])
284
+ ability = chars(source).ability(ctx["ability"])
285
+ obj.memory["source_counters"] = source.counters.dup if ability.costs.any?(Mechanics::Costs::SacrificeSelf)
286
+ frame_costs(ctx).reject(&:mana?).each { it.pay!(self, source) } # the chosen option of a one_of (CR 601.2b)
287
+ pay_chosen_costs!(ctx)
288
+ return unless ability.kind == :loyalty
289
+
290
+ source.loyalty_turn = state.turn
291
+ cost = ability.loyalty_cost
292
+ cost.negative? ? remove_counters!(source, "loyalty", -cost) : add_counters!(source, "loyalty", cost)
293
+ end
294
+
295
+ # Rewinds an unfinished cast or activation (CR 734.1). Offered only while nothing irreversible
296
+ # has been paid (see Engine::Payment#cancellable?): tapping is undone, mana abilities' mana
297
+ # leaves the pool, and abilities that triggered meanwhile are dropped.
298
+ def cancel_casting!
299
+ ctx = casting
300
+ index = ctx["player"]
301
+ rewind_payment!(ctx)
302
+ obj = object(ctx["object"])
303
+ state.stack.delete(obj.id)
304
+ state.objects.delete(obj.id)
305
+ restore_to_origin!(obj, ctx["origin"]) if ctx["kind"] == "spell"
306
+ emit Events::CastCancelled.new(player: name(index), object: obj.id, card: obj.card)
307
+ state.frames.pop
308
+ return ask!(nil) if ctx["no_priority"] # cast during resolution: the resolution goes on (CR 608.2g)
309
+
310
+ give_priority!(index)
311
+ end
312
+
313
+ # (Costs that need a choice, e.g. tapping another creature: see Engine::CastChoices.)
314
+
315
+ # As a spell or ability becomes cast, activated or put on the stack: it keeps the cost choices
316
+ # (CR 702.184a uses the tapped creature's power), and chosen modes mean its controller faced a
317
+ # dilemma (CR 700.17). Recording it here, not as modes are chosen, keeps a cancelled cast from counting.
318
+ def record_cast_choices!(obj, ctx)
319
+ obj.paid = ctx["paid"].map(&:dup) if ctx["paid"]
320
+ emit Events::DilemmaFaced.new(player: name(ctx["player"]), object: obj.id, modes: obj.modes.dup) if obj.modes
321
+ end
322
+ end
323
+
324
+ include Casting
325
+ end
326
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enjine
4
+ class Engine
5
+ # Choosing between costs (Costs::OneOf, CR 601.2b): which of the listed costs will be paid is announced
6
+ # while casting or activating, before X and the total cost (CR 601.2f, 602.2b). The cast frame keeps the
7
+ # option chosen for each one_of, in order ("cost_options"); from then on the chosen cost stands in its place.
8
+ # Before the cast begins, every combination is tried (`cost_variants`).
9
+ module CostOptions
10
+ ONE_OF = Mechanics::Costs::OneOf
11
+
12
+ # Every way to pay `costs`: each one_of replaced by one of its options.
13
+ def cost_variants(costs)
14
+ costs.reduce([[]]) do |variants, cost|
15
+ cost.is_a?(ONE_OF) ? variants.product(cost.options).map { |v, o| v + [o] } : variants.map { it + [cost] }
16
+ end
17
+ end
18
+
19
+ # `costs` with the n-th one_of replaced by its option chosen[n]; one_ofs not announced yet are left out.
20
+ def with_cost_options(costs, chosen)
21
+ n = -1
22
+ costs.filter_map do |cost|
23
+ next cost unless cost.is_a?(ONE_OF)
24
+
25
+ option = chosen[n += 1]
26
+ option && cost.options[option]
27
+ end
28
+ end
29
+
30
+ # The mana parts of a cost list, as one mana cost.
31
+ def costs_mana(costs) = Mana.sum_costs(costs.select(&:mana?).map(&:mana_cost))
32
+
33
+ # Whether some way of paying `costs` (on top of the mana cost `base`) is open to `index`: its other parts
34
+ # payable for `payer` (CR 118.3) and the total cost affordable (CR 601.2f). The block turns the mana to pay
35
+ # into the total cost.
36
+ def cost_variant_affordable?(index, costs, payer, base, excluded, purpose)
37
+ variants = costs.any?(ONE_OF) ? cost_variants(costs) : [costs]
38
+ variants.any? do |variant|
39
+ variant.all? { it.mana? || it.payable?(self, payer) } &&
40
+ affordable?(index, yield(variant_mana(base, variant)), excluded, { **purpose, **waterbend_purpose(variant) })
41
+ end
42
+ end
43
+
44
+ # The mana cost `base` plus the mana parts of a cost list.
45
+ def variant_mana(base, costs)
46
+ return base if base && costs.none?(&:mana?)
47
+
48
+ Mana.sum_costs([base, costs_mana(costs)].compact)
49
+ end
50
+
51
+ # The cost list of a cast frame, before the announced options are applied: an ability's costs, a spell's
52
+ # additional costs (and kicker's when kicked).
53
+ def frame_cost_list(ctx)
54
+ case ctx["kind"]
55
+ when "ability" then stack_ability(object(ctx["object"])).costs
56
+ when "spell" then spell_cost_list(object(ctx["object"]), ctx["kicked"])
57
+ else []
58
+ end
59
+ end
60
+
61
+ def frame_costs(ctx) = with_cost_options(frame_cost_list(ctx), ctx["cost_options"] || [])
62
+
63
+ # CR 601.2b: true while waiting for the choice of the next one_of.
64
+ def ask_cost_option!(ctx)
65
+ one_of = pending_one_of(ctx) or return false
66
+ ask!("kind" => "choose_cost_option", "player" => ctx["player"], "costs" => one_of.option_texts)
67
+ true
68
+ end
69
+
70
+ def pending_one_of(ctx) = frame_cost_list(ctx).grep(ONE_OF)[ctx.fetch("cost_options", []).size]
71
+
72
+ # `{type: "cost_option", option: i}` for each option that could be paid: its own part payable (CR 118.3)
73
+ # and the total cost with it affordable (CR 601.2f).
74
+ def cost_option_options
75
+ ctx = casting
76
+ chosen = ctx.fetch("cost_options", [])
77
+ pending_one_of(ctx).options.each_with_index.filter_map do |cost, i|
78
+ { type: "cost_option", option: i } if cost_option_payable?(ctx.merge("cost_options" => chosen + [i]), cost)
79
+ end
80
+ end
81
+
82
+ def cost_option_payable?(trial, cost)
83
+ return false unless cost.mana? || cost.payable?(self, cost_payer(trial))
84
+
85
+ bend = waterbend_generic(frame_costs(trial))
86
+ purpose = payment_purpose(bend.positive? ? trial.merge("waterbend" => bend) : trial)
87
+ affordable?(trial["player"], frame_total_cost(trial, x_value: 0, targets: nil), payment_excluded_source(trial), purpose)
88
+ end
89
+
90
+ def choose_cost_option!(option)
91
+ ctx = casting
92
+ (ctx["cost_options"] ||= []) << option
93
+ ask!(nil)
94
+ end
95
+ end
96
+
97
+ include CostOptions
98
+ end
99
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Enjine
4
+ class Engine
5
+ # The total cost of a spell or ability (CR 601.2f, 602.2b): the mana cost with X announced
6
+ # (CR 107.3a, 601.2b), plus cost increases, minus cost reductions. It is determined after
7
+ # targets are chosen and then locked in for payment.
8
+ module Costs
9
+ # The mana cost with X announced (CR 107.3a), plus cost increases, minus cost reductions.
10
+ # Changes affect only the generic part (CR 118.7a) and can't take it below {0}. `targets`
11
+ # nil means "not chosen yet": conditions about targets then count as met, so the result
12
+ # is a lower bound on what casting can cost (an upper bound on affordability).
13
+ def total_cost(index, cost, spell: nil, ability: nil, source: nil, x_value: 0, targets: nil)
14
+ return nil if cost.nil?
15
+
16
+ cost = Mechanics::Keywords::PowerUp.reduced(self, cost.with_x(x_value), ability, source) # CR 702.193a
17
+ change = cost_change(index, spell, ability, source, targets)
18
+ change.zero? ? cost : cost.with(generic: [cost.generic + change, 0].max)
19
+ end
20
+
21
+ # Net generic change: + increases - reductions, from the spell's own abilities ("this spell
22
+ # costs {1} less"), the activated ability's own reduction, and static abilities of
23
+ # permanents ("spells you cast cost {1} less").
24
+ def cost_change(index, spell, ability, source, targets)
25
+ if spell
26
+ ctx = Mechanics::EffectContext.new(index, spell.id, spell.id, targets)
27
+ own = chars(spell).abilities.sum do |inst|
28
+ inst.ability.kind == :cost_reduction ? -inst.ability.reduction(self, ctx) : 0
29
+ end
30
+ own + cost_modifiers.sum { |perm, modifier| modifier.change_for(self, perm, spell, index) }
31
+ elsif ability.respond_to?(:cost_reduction) && ability.cost_reduction
32
+ -ability.cost_reduction.reduction(self, Mechanics::EffectContext.new(index, source, nil, targets))
33
+ else
34
+ 0
35
+ end
36
+ end
37
+
38
+ # [[permanent, CostModifier], ...] on the battlefield.
39
+ def cost_modifiers
40
+ state.battlefield.flat_map do |id|
41
+ obj = object(id)
42
+ chars(obj).abilities.filter_map { [obj, it.ability] if it.ability.kind == :cost_modifier }
43
+ end
44
+ end
45
+
46
+ # The mana cost being paid, before X and cost changes (nil for a triggered ability).
47
+ def base_cost(ctx)
48
+ obj = object(ctx["object"])
49
+ case ctx["kind"]
50
+ when "spell" # plus additional costs, kicker and chosen tiered modes (CR 601.2f, 702.183a); alternative
51
+ # costs (CR 118.9, Permissions)
52
+ cost = ctx["method"] ? alternative_base_cost(ctx) : chars(obj).mana_cost
53
+ modes = spell_ability_of(obj)&.mode_costs(ctx["modes"] || []) || []
54
+ cost && Mana.sum_costs([cost, costs_mana(frame_costs(ctx)), *modes]) # Engine::CostOptions
55
+ when "ability" then costs_mana(frame_costs(ctx))
56
+ end
57
+ end
58
+
59
+ def frame_total_cost(ctx, x_value: ctx["x"].to_i, targets: ctx["targets"])
60
+ obj = object(ctx["object"])
61
+ spell = ctx["kind"] == "spell" ? obj : nil
62
+ ability = ctx["kind"] == "ability" ? stack_ability(obj) : nil
63
+ total = total_cost(ctx["player"], base_cost(ctx), spell: spell, ability: ability, source: ctx["source"],
64
+ x_value: x_value, targets: targets)
65
+ harmonize_reduced(total, frame_harmonize_power(ctx)) # CR 702.180a (Engine::AltCasting)
66
+ end
67
+
68
+ # CR 601.2f: the total cost is determined once and locked in.
69
+ def lock_total_cost!(ctx)
70
+ ctx["remaining"] = remaining_hash(frame_total_cost(ctx))
71
+ lock_waterbend!(ctx) # CR 701.67b (Engine::Waterbending)
72
+ end
73
+
74
+ # CR 601.2b: X is announced (once) before targets are chosen. True while waiting for it. Without {X} in
75
+ # the mana cost or a cost such as "pay X life" or "exile X cards", X is 0.
76
+ def ask_x!(ctx)
77
+ return false unless ctx["x"].nil?
78
+ return (ctx["x"] = 0) && false unless base_cost(ctx)&.x_symbols.to_i.positive? || x_costs(ctx).any?
79
+
80
+ ask!("kind" => "choose_x", "player" => ctx["player"], "max" => x_limit(ctx))
81
+ true
82
+ end
83
+
84
+ # The costs paid while casting that define X: "pay X life" (Costs::PayLife), "exile X cards from your
85
+ # graveyard" (Costs::CardsCost).
86
+ def x_costs(ctx) = casting_costs(ctx).select { it.respond_to?(:x?) && it.x? }
87
+
88
+ # A {T} ability's own source can't pay for it.
89
+ def payment_excluded_source(ctx)
90
+ ctx["type"] == "cast" && ctx["kind"] == "ability" && needs_tap?(casting_ability) ? ctx["source"] : nil
91
+ end
92
+
93
+ # The largest X the player could pay for: the mana available beyond the rest of the cost, and for
94
+ # "pay X life" no more than their life total (CR 119.4), for "exile X cards" no more than there are (CR 118.3).
95
+ def x_limit(ctx)
96
+ limits = x_costs(ctx).map { it.x_limit(self, cost_payer(ctx)) }
97
+ symbols = base_cost(ctx)&.x_symbols.to_i
98
+ return limits.min if symbols.zero?
99
+
100
+ cost = frame_total_cost(ctx, x_value: 0, targets: nil)
101
+ spare = mana_units(ctx["player"], payment_excluded_source(ctx), payment_purpose(ctx)).size - cost.mana_value
102
+ (limits << ([spare, 0].max / symbols)).min
103
+ end
104
+
105
+ # CR 601.2b / 107.3a: the value of X is announced; the spell or ability keeps it on the stack.
106
+ def announce_x!(value)
107
+ ctx = casting
108
+ ctx["x"] = value
109
+ object(ctx["object"]).x = value
110
+ emit Events::XAnnounced.new(player: name(ctx["player"]), object: ctx["object"], value: value)
111
+ ask!(nil)
112
+ end
113
+ end
114
+
115
+ include Costs
116
+ end
117
+ end