diff --git a/assets/app/view/game/discard_trains.rb b/assets/app/view/game/discard_trains.rb index faea8fee0f..25f7e1b70f 100644 --- a/assets/app/view/game/discard_trains.rb +++ b/assets/app/view/game/discard_trains.rb @@ -35,7 +35,8 @@ def render h(:div, trains), ]) end - + overflow << h(ScrapTrains, corporation: @current_entity) if @game.round.actions_for(step.current_entity) + &.include?('scrap_train') overflow << h(Map, game: @game) if @game.round.is_a?(Engine::Round::Operating) h(:div, [ diff --git a/assets/app/view/game/round/merger.rb b/assets/app/view/game/round/merger.rb index 108d914855..61db9c469b 100644 --- a/assets/app/view/game/round/merger.rb +++ b/assets/app/view/game/round/merger.rb @@ -54,6 +54,7 @@ def render buttons << render_offer(entity, auctioning_corporation) if actions.include?('assign') buttons << render_merge(entity) if actions.include?('merge') + buttons << h(ScrapTrains, corporation: entity) if actions.include?('scrap_train') children << h(:div, buttons) if buttons.any? props = { diff --git a/assets/app/view/game/round/stock.rb b/assets/app/view/game/round/stock.rb index ed06a2a968..3dcd7db83c 100644 --- a/assets/app/view/game/round/stock.rb +++ b/assets/app/view/game/round/stock.rb @@ -170,6 +170,7 @@ def render_input(corporation) ] inputs << h(IssueShares, entity: corporation) unless (@step.actions(corporation) & %w[buy_shares sell_shares]).empty? inputs << h(BuyTrains, corporation: corporation) if @step.actions(corporation).include?('buy_train') + inputs << h(ScrapTrains, corporation: corporation) if @step.actions(corporation).include?('scrap_train') inputs << h(Choose, entity: corporation) if @current_actions.include?('choose') && @step.choice_available?(corporation) inputs = inputs.compact h('div.margined_bottom', { style: { width: '20rem' } }, inputs) if inputs.any? diff --git a/assets/app/view/game/scrap_trains.rb b/assets/app/view/game/scrap_trains.rb index c508a55341..faee20490c 100644 --- a/assets/app/view/game/scrap_trains.rb +++ b/assets/app/view/game/scrap_trains.rb @@ -6,14 +6,29 @@ module View module Game class ScrapTrains < Snabberb::Component include Actionable + needs :corporation, default: nil def render - @corporation = @game.round.active_step.current_entity - step = @game.round.active_step + @corporation ||= @game.round.active_step.current_entity + step = @game.round.step_for(@corporation, 'scrap_train') scrappable_trains = step.scrappable_trains(@corporation) return nil if scrappable_trains.empty? + if step.respond_to?(:scrap_trains_button_only?) && step.scrap_trains_button_only? + render_buttons(scrappable_trains) + else + render_section(scrappable_trains) + end + end + + def render_buttons(scrappable_trains) + h(:div, generate_scrap_train_actions(scrappable_trains) do |scrap, train, step| + h(:button, { on: { click: scrap } }, step.scrap_info(train)) + end) + end + + def render_section(scrappable_trains) div_props = { style: { display: 'grid', @@ -22,15 +37,22 @@ def render alignItems: 'center', }, } - - h(:div, [ - h(:h3, 'Trains to Scrap'), - h(:div, div_props, scrap_trains(scrappable_trains)), - ]) + h(:div, + [h(:h3, 'Trains to Scrap'), + h(:div, div_props, scrap_trains(scrappable_trains))]) end def scrap_trains(scrappable_trains) - step = @game.round.active_step + generate_scrap_train_actions(scrappable_trains) do |scrap, train, step| + [h(:div, train.name), + h('div.nowrap', train.owner.name), + h('div.right', step.scrap_info(train)), + h('button.no_margin', { on: { click: scrap } }, step.scrap_button_text(train))] + end + end + + def generate_scrap_train_actions(scrappable_trains) + step = @game.round.step_for(@corporation, 'scrap_train') scrappable_trains.flat_map do |train| scrap = lambda do process_action(Engine::Action::ScrapTrain.new( @@ -38,11 +60,7 @@ def scrap_trains(scrappable_trains) train: train, )) end - - [h(:div, train.name), - h('div.nowrap', train.owner.name), - h('div.right', step.scrap_info(train)), - h('button.no_margin', { on: { click: scrap } }, step.scrap_button_text(train))] + yield(scrap, train, step) end end end diff --git a/assets/app/view/game_page.rb b/assets/app/view/game_page.rb index 5ecfd5bc09..713a300a1b 100644 --- a/assets/app/view/game_page.rb +++ b/assets/app/view/game_page.rb @@ -418,7 +418,8 @@ def render_action Engine::Round::Draft h(Game::Round::Auction, game: @game, user: @user) when Engine::Round::Merger - if !(%w[buy_train scrap_train reassign_trains] & current_entity_actions).empty? + if !(%w[buy_train scrap_train reassign_trains] & current_entity_actions).empty? && + @game.train_actions_always_use_operating_round_view? h(Game::Round::Operating, game: @game) else h(Game::Round::Merger, game: @game) diff --git a/lib/engine/game/base.rb b/lib/engine/game/base.rb index 21f3d17ac9..d48015f3a3 100644 --- a/lib/engine/game/base.rb +++ b/lib/engine/game/base.rb @@ -2775,6 +2775,14 @@ def train_power? def show_map_legend? false end + + # If a game overrides this to true, then if the possible actions for the current entity include any of + # buy_train, scrap_train, or reassign_train then + # the Operating view will be used instead of the Merger round view for train actiosn in a merger round. + # See https://github.com/tobymao/18xx/issues/7169 + def train_actions_always_use_operating_round_view? + false + end end end end diff --git a/lib/engine/game/g_18_usa/game.rb b/lib/engine/game/g_18_usa/game.rb index a54b5322c2..3c59da3a3f 100644 --- a/lib/engine/game/g_18_usa/game.rb +++ b/lib/engine/game/g_18_usa/game.rb @@ -623,7 +623,7 @@ def stock_round @interest_fixed = nil G18USA::Round::Stock.new(self, [ - Engine::Step::DiscardTrain, + G18USA::Step::DiscardTrain, G18USA::Step::DenverTrack, G18USA::Step::HomeToken, G18USA::Step::BuySellParShares, @@ -653,6 +653,7 @@ def operating_round(round_num) G1817::Step::CashCrisis, G18USA::Step::ObsoleteTrain, G18USA::Step::Loan, + G18USA::Step::DiscardTrain, G18USA::Step::SpecialTrack, G18USA::Step::SpecialToken, G18USA::Step::SpecialBuyTrain, @@ -660,13 +661,17 @@ def operating_round(round_num) G18USA::Step::DenverTrack, G18USA::Step::Track, G18USA::Step::Token, + G18USA::Step::BuyPullman, G18USA::Step::Route, G18USA::Step::Dividend, - Engine::Step::DiscardTrain, G18USA::Step::BuyTrain, ], round_num: round_num) end + def crowded_corps + @crowded_corps ||= super | corporations.select { |c| c.trains.count { |t| pullman_train?(t) } > 1 } + end + def next_round! clear_interest_paid @round = @@ -682,10 +687,10 @@ def next_round! @log << "-- #{round_description('Merger and Conversion', @round.round_num)} --" G1817::Round::Merger.new(self, [ G18USA::Step::ReduceTokens, - Engine::Step::DiscardTrain, + G18USA::Step::DiscardTrain, G1817::Step::PostConversion, - G1817::Step::PostConversionLoans, - G1817::Step::Conversion, + G18USA::Step::PostConversionLoans, + G18USA::Step::Conversion, ], round_num: @round.round_num) when G1817::Round::Merger @log << "-- #{round_description('Acquisition', @round.round_num)} --" @@ -693,8 +698,8 @@ def next_round! G18USA::Step::ReduceTokens, G1817::Step::Bankrupt, G1817::Step::CashCrisis, - Engine::Step::DiscardTrain, - G1817::Step::Acquire, + G18USA::Step::DiscardTrain, + G18USA::Step::Acquire, ], round_num: @round.round_num) when G1817::Round::Acquisition if @round.round_num < @operating_rounds @@ -831,6 +836,11 @@ def tokened_out_stop(route) visits[1..-2].find { |node| node.city? && node.blocks?(corporation) } end + def pullmans_available? + # Pullmans are available in phase 5, using the availability of brown track as an easy signal of this + @phase.tiles.include?(:brown) + end + def route_trains(entity) entity.runnable_trains.reject { |t| pullman_train?(t) } end diff --git a/lib/engine/game/g_18_usa/round/operating.rb b/lib/engine/game/g_18_usa/round/operating.rb index d7e339bdf6..8f0f939cb5 100644 --- a/lib/engine/game/g_18_usa/round/operating.rb +++ b/lib/engine/game/g_18_usa/round/operating.rb @@ -22,6 +22,14 @@ def finished? true end + + def pay_interest!(entity) + # 1817's pay_interest! does a 'return unless step_passed?(Engine::Step::BuyTrain)' which unintentionally + # passes for the 18USA BuyPullmanStep - here we check that 18USA BuyTrain is passed before continuing + return unless step_passed?(G18USA::Step::BuyTrain) + + super + end end end end diff --git a/lib/engine/game/g_18_usa/step/acquire.rb b/lib/engine/game/g_18_usa/step/acquire.rb new file mode 100644 index 0000000000..9dd1d470da --- /dev/null +++ b/lib/engine/game/g_18_usa/step/acquire.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require_relative '../../g_1817/step/acquire' +require_relative 'scrap_train_module' +module Engine + module Game + module G18USA + module Step + class Acquire < G1817::Step::Acquire + include ScrapTrainModule + def actions(entity) + actions = super + if entity == @buyer && can_scrap_train?(entity) + actions = %w[pass] if actions.empty? + actions << 'scrap_train' + end + actions + end + + def pass_description + return 'Pass (Scrap Train)' if @buyer && !can_take_loan?(@buyer) && !can_payoff?(@buyer) + + super + end + + def process_pass(action) + if @buyer && !can_take_loan?(@buyer) && !can_payoff?(@buyer) + @passed_scrap_trains = true + @game.log << "#{@buyer.name} passes scrapping trains" + acquire_post_loan + else + super + end + end + + def acquire_post_loan + return if can_scrap_train?(@buyer) + + super + end + + # This version is needed to reference @passed_scrap_trains + def can_scrap_train?(entity) + return true if entity.corporation? && !@passed_scrap_trains && entity.trains.find { |t| @game.pullman_train?(t) } + end + end + end + end + end +end diff --git a/lib/engine/game/g_18_usa/step/buy_pullman.rb b/lib/engine/game/g_18_usa/step/buy_pullman.rb new file mode 100644 index 0000000000..cf33d4f76e --- /dev/null +++ b/lib/engine/game/g_18_usa/step/buy_pullman.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require_relative '../../../step/buy_train' +module Engine + module Game + module G18USA + module Step + class BuyPullman < Engine::Step::BuyTrain + def description + 'Buy Pullman' + end + + def pass_description + 'Skip (Pullman)' + end + + def must_buy_train?(_) + false + end + + def president_may_contribute?(_) + false + end + + def can_buy_train?(entity, _shell = nil) + @game.pullmans_available? && entity.runnable_trains.none? { |t| @game.pullman_train?(t) } + end + + def buyable_trains(entity) + # Can't buy a second pullman and can't buy a pullman if it's not legal to well, buy pullmans. + return [] unless can_buy_train?(entity) + + # Cannot buy a pullman if you have a pullman + (@depot.depot_trains & super).select { |t| @game.pullman_train?(t) } + end + end + end + end + end +end diff --git a/lib/engine/game/g_18_usa/step/buy_sell_par_shares.rb b/lib/engine/game/g_18_usa/step/buy_sell_par_shares.rb index f93a44db63..a2e53e11e0 100644 --- a/lib/engine/game/g_18_usa/step/buy_sell_par_shares.rb +++ b/lib/engine/game/g_18_usa/step/buy_sell_par_shares.rb @@ -1,16 +1,24 @@ # frozen_string_literal: true require_relative '../../g_1817/step/buy_sell_par_shares' +require_relative 'scrap_train_module' module Engine module Game module G18USA module Step class BuySellParShares < G1817::Step::BuySellParShares + include ScrapTrainModule MIN_BID = 100 MAX_BID = 100_000 MAX_PAR_PRICE = 200 + def corporate_actions(entity) + actions = super + actions << 'scrap_train' if !@winning_bid && @round.current_actions.none? && can_scrap_train?(entity) + actions + end + def auto_actions(entity) return [Engine::Action::Pass.new(entity)] if @auctioning && max_bid(entity, @auctioning) < min_bid(@auctioning) diff --git a/lib/engine/game/g_18_usa/step/buy_train.rb b/lib/engine/game/g_18_usa/step/buy_train.rb index 9c228eb631..5a116a22c6 100644 --- a/lib/engine/game/g_18_usa/step/buy_train.rb +++ b/lib/engine/game/g_18_usa/step/buy_train.rb @@ -14,7 +14,7 @@ def should_buy_train?(entity) def buyable_trains(entity) buyable_trains = super # Cannot buy a pullman if you have a pullman - buyable_trains.reject! { |t| @game.pullman_train?(t) } if entity.runnable_trains&.any? { |t| @game.pullman_train?(t) } + buyable_trains.reject! { |t| @game.pullman_train?(t) } if entity.trains&.any? { |t| @game.pullman_train?(t) } buyable_trains end end diff --git a/lib/engine/game/g_18_usa/step/conversion.rb b/lib/engine/game/g_18_usa/step/conversion.rb new file mode 100644 index 0000000000..0f090129a1 --- /dev/null +++ b/lib/engine/game/g_18_usa/step/conversion.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require_relative '../../g_1817/step/conversion' +require_relative 'scrap_train_module' +module Engine + module Game + module G18USA + module Step + class Conversion < G1817::Step::Conversion + include ScrapTrainModule + def actions(entity) + actions = super + actions << 'scrap_train' if can_scrap_train?(entity) + actions + end + end + end + end + end +end diff --git a/lib/engine/game/g_18_usa/step/discard_train.rb b/lib/engine/game/g_18_usa/step/discard_train.rb new file mode 100644 index 0000000000..98e58713ef --- /dev/null +++ b/lib/engine/game/g_18_usa/step/discard_train.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require_relative '../../../step/discard_train' +require_relative 'scrap_train_module' +module Engine + module Game + module G18USA + module Step + class DiscardTrain < Engine::Step::DiscardTrain + include ScrapTrainModule + def actions(entity) + actions = super + actions << 'scrap_train' if can_scrap_train?(entity) + actions + end + + def trains(corporation) + return super unless corporation.trains.count { |t| @game.pullman_train?(t) } > 1 + + corporation.trains.select { |t| @game.pullman_train?(t) } + end + end + end + end + end +end diff --git a/lib/engine/game/g_18_usa/step/loan.rb b/lib/engine/game/g_18_usa/step/loan.rb index 04a0beebe5..3bb49374fc 100644 --- a/lib/engine/game/g_18_usa/step/loan.rb +++ b/lib/engine/game/g_18_usa/step/loan.rb @@ -1,12 +1,20 @@ # frozen_string_literal: true require_relative '../../g_1817/step/loan' +require_relative 'scrap_train_module' module Engine module Game module G18USA module Step class Loan < G1817::Step::Loan + include ScrapTrainModule + def actions(entity) + actions = super + actions << 'scrap_train' if entity == current_entity && can_scrap_train?(current_entity) + actions + end + def can_payoff?(entity) super && !@loan_taken end diff --git a/lib/engine/game/g_18_usa/step/post_conversion_loans.rb b/lib/engine/game/g_18_usa/step/post_conversion_loans.rb new file mode 100644 index 0000000000..3801f5df51 --- /dev/null +++ b/lib/engine/game/g_18_usa/step/post_conversion_loans.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require_relative '../../g_1817/step/post_conversion_loans' +require_relative 'scrap_train_module' + +module Engine + module Game + module G18USA + module Step + class PostConversionLoans < G1817::Step::PostConversionLoans + include ScrapTrainModule + def actions(entity) + actions = super + actions << 'scrap_train' if can_scrap_train?(entity) + actions + end + end + end + end + end +end diff --git a/lib/engine/game/g_18_usa/step/scrap_train_module.rb b/lib/engine/game/g_18_usa/step/scrap_train_module.rb new file mode 100644 index 0000000000..3c5dd54681 --- /dev/null +++ b/lib/engine/game/g_18_usa/step/scrap_train_module.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module Engine + module Game + module G18USA + module Step + module ScrapTrainModule + def scrappable_trains(entity) + entity.trains.select { |t| @game.pullman_train?(t) } + end + + def scrap_info(_) + "Scrap Pullman for #{@game.format_currency(pullman_scrap_value)}" + end + + def scrap_button_text(_) + 'Scrap Pullman' + end + + def pullman_scrap_value + 50 + end + + def scrap_trains_button_only? + true + end + + # owner is the alleged owner of the company scrapping a pullman + def scrap_train_by_owner(action, _owner) + entity = action.entity + raise GameError, "#{entity.name} cannot scrap a train now" unless entity.owned_by?(current_entity) + + train = action.train + raise GameError, "#{entity.name} cannot scrap a #{train.name} train" unless @game.pullman_train?(train) + + scrap_train(train) + end + + # Do error checking before calling this. + def scrap_train(train) + @game.bank.spend(pullman_scrap_value, train.owner) + @game.log << "#{train.owner.name} scraps a pullman for #{@game.format_currency(pullman_scrap_value)}" + @game.depot.reclaim_train(train) + # @game.reset_crowded_corps + end + + def can_scrap_train?(entity) + return false unless entity&.corporation? + return false unless entity.owned_by?(current_entity) + + entity.trains.find { |t| @game.pullman_train?(t) } + end + + def process_scrap_train(action) + @corporate_action = action + scrap_train_by_owner(action, current_entity) + end + end + end + end + end +end