diff --git a/lib/engine/game/g_1862_usa_canada/entities.rb b/lib/engine/game/g_1862_usa_canada/entities.rb index baa548fd75..477ec54ffc 100644 --- a/lib/engine/game/g_1862_usa_canada/entities.rb +++ b/lib/engine/game/g_1862_usa_canada/entities.rb @@ -52,7 +52,7 @@ module Entities type: 'token', hexes: [], discount: 80, - owner_type: 'corporation', + owner_type: 'player', }, ], color: nil, diff --git a/lib/engine/game/g_1862_usa_canada/game.rb b/lib/engine/game/g_1862_usa_canada/game.rb index f4d6d5cf17..3aa43eee38 100644 --- a/lib/engine/game/g_1862_usa_canada/game.rb +++ b/lib/engine/game/g_1862_usa_canada/game.rb @@ -6,6 +6,9 @@ require_relative 'entities' require_relative 'map' require_relative '../base' +require_relative 'step/buy_sell_par_shares' +require_relative 'step/dividend' +require_relative 'step/token' module Engine module Game @@ -280,6 +283,45 @@ class Game < Game::Base GAME_END_CHECK = { bank: :full_or, stock_market: :full_or }.freeze + # --------------------------------------------------------------------------- + # Private company close triggers. + # SOC closes when CPR or UP floats. + # NHSC closes when NYH floats. + # PSC closes when WP pays its first dividend (via on_first_payout!). + # FNY closes when NYC pays its first dividend (via on_first_payout!). + # TOR closes automatically via closed_when_used_up on its tile_lay ability. + # --------------------------------------------------------------------------- + def float_corporation(corporation) + super + on_corporation_floated!(corporation) + end + + def on_corporation_floated!(corporation) + case corporation.id + when 'CPR', 'UP' + close_private_if_open!('SOC', "#{corporation.name} floats") + when 'NYH' + close_private_if_open!('NHSC', "#{corporation.name} floats") + end + end + + def on_first_payout!(corporation) + case corporation.id + when 'WP' + close_private_if_open!('PSC', "#{corporation.name} pays first dividend") + when 'NYC' + close_private_if_open!('FNY', "#{corporation.name} pays first dividend") + end + end + + def close_private_if_open!(sym, reason) + company = companies.find { |c| c.sym == sym && !c.closed? } + return unless company + + company.close! + @log << "#{company.name} closes (#{reason})" + end + # --------------------------------------------------------------------------- # Tile-lay budget override. # --------------------------------------------------------------------------- @@ -337,7 +379,7 @@ def stock_round Engine::Step::DiscardTrain, Engine::Step::Exchange, Engine::Step::SpecialTrack, - Engine::Step::BuySellParShares, + G1862UsaCanada::Step::BuySellParShares, ]) end @@ -348,9 +390,9 @@ def operating_round(round_num) Engine::Step::SpecialTrack, Engine::Step::HomeToken, Engine::Step::Track, - Engine::Step::Token, + G1862UsaCanada::Step::Token, Engine::Step::Route, - Engine::Step::Dividend, + G1862UsaCanada::Step::Dividend, Engine::Step::DiscardTrain, Engine::Step::BuyTrain, ], round_num: round_num) diff --git a/lib/engine/game/g_1862_usa_canada/step/buy_sell_par_shares.rb b/lib/engine/game/g_1862_usa_canada/step/buy_sell_par_shares.rb new file mode 100644 index 0000000000..6a10ce6e2d --- /dev/null +++ b/lib/engine/game/g_1862_usa_canada/step/buy_sell_par_shares.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require_relative '../../../step/buy_sell_par_shares' + +module Engine + module Game + module G1862UsaCanada + module Step + class BuySellParShares < Engine::Step::BuySellParShares + # NHSC gives the buyer NYH's director cert; NYH must be parred at + # exactly $100. Restrict available par prices to that single value. + def get_par_prices(entity, corporation) + return nyh_par_prices if corporation.id == 'NYH' + + super + end + + private + + def nyh_par_prices + @game.stock_market.par_prices.select { |p| p.price == 100 } + end + end + end + end + end +end diff --git a/lib/engine/game/g_1862_usa_canada/step/dividend.rb b/lib/engine/game/g_1862_usa_canada/step/dividend.rb new file mode 100644 index 0000000000..2f6e0a45c3 --- /dev/null +++ b/lib/engine/game/g_1862_usa_canada/step/dividend.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require_relative '../../../step/dividend' + +module Engine + module Game + module G1862UsaCanada + module Step + class Dividend < Engine::Step::Dividend + def process_dividend(action) + entity = action.entity + first_time = entity.operating_history.none? { |_, info| info.dividend.kind.to_sym == :payout } + super + @game.on_first_payout!(entity) if first_time && action.kind.to_sym == :payout + end + end + end + end + end +end diff --git a/lib/engine/game/g_1862_usa_canada/step/token.rb b/lib/engine/game/g_1862_usa_canada/step/token.rb new file mode 100644 index 0000000000..db2e52fa2c --- /dev/null +++ b/lib/engine/game/g_1862_usa_canada/step/token.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require_relative '../../../step/token' + +module Engine + module Game + module G1862UsaCanada + module Step + class Token < Engine::Step::Token + # GHU (Bahnhoflizenz) gives the director's corporation an $80 discount + # on station token placement (minimum $0). The discount is auto-applied + # whenever the operating corporation's director holds GHU. + def process_place_token(action) + apply_ghu_discount!(action.entity, action.token) + super + end + + private + + def ghu_company + @game.companies.find { |c| c.sym == 'GHU' && !c.closed? } + end + + def apply_ghu_discount!(corporation, token) + ghu = ghu_company + return unless ghu&.owner == corporation.owner + + discount = ghu.abilities.find { |a| a.type == :token }&.discount.to_i + token.price = [token.price - discount, 0].max + end + end + end + end + end +end diff --git a/spec/lib/engine/game/g_1862_usa_canada/game_spec.rb b/spec/lib/engine/game/g_1862_usa_canada/game_spec.rb index a21ce3713b..fe77644e07 100644 --- a/spec/lib/engine/game/g_1862_usa_canada/game_spec.rb +++ b/spec/lib/engine/game/g_1862_usa_canada/game_spec.rb @@ -31,6 +31,86 @@ module Engine expect(described_class::SELL_BUY_ORDER).to eq(:sell_buy) end + describe 'private close triggers' do + let(:soc) { game.companies.find { |c| c.sym == 'SOC' } } + let(:nhsc) { game.companies.find { |c| c.sym == 'NHSC' } } + let(:psc) { game.companies.find { |c| c.sym == 'PSC' } } + let(:fny) { game.companies.find { |c| c.sym == 'FNY' } } + let(:cpr) { game.corporation_by_id('CPR') } + let(:up) { game.corporation_by_id('UP') } + let(:nyh) { game.corporation_by_id('NYH') } + let(:wp) { game.corporation_by_id('WP') } + let(:nyc) { game.corporation_by_id('NYC') } + + it 'SOC closes when CPR floats' do + game.on_corporation_floated!(cpr) + expect(soc.closed?).to be true + end + + it 'SOC closes when UP floats' do + game.on_corporation_floated!(up) + expect(soc.closed?).to be true + end + + it 'NHSC closes when NYH floats' do + game.on_corporation_floated!(nyh) + expect(nhsc.closed?).to be true + end + + it 'SOC does not close when NYH floats' do + game.on_corporation_floated!(nyh) + expect(soc.closed?).to be false + end + + it 'PSC closes on first WP payout' do + game.on_first_payout!(wp) + expect(psc.closed?).to be true + end + + it 'FNY closes on first NYC payout' do + game.on_first_payout!(nyc) + expect(fny.closed?).to be true + end + + it 'FNY does not close when WP pays first dividend' do + game.on_first_payout!(wp) + expect(fny.closed?).to be false + end + end + + describe 'GHU token discount' do + let(:ghu) { game.companies.find { |c| c.sym == 'GHU' } } + + it 'GHU ability has player owner_type' do + ability = ghu.abilities.find { |a| a.type == :token } + expect(ability.owner_type).to eq(:player) + end + + it 'GHU discount is $80' do + ability = ghu.abilities.find { |a| a.type == :token } + expect(ability.discount).to eq(80) + end + end + + describe 'NYH par price restriction' do + let(:nyh) { game.corporation_by_id('NYH') } + let(:step) { game.stock_round.active_step } + + it 'NYH par is restricted to $100' do + game.companies.each { |c| c.owner = game.players.first } + prices = step.get_par_prices(game.players.first, nyh).map(&:price) + expect(prices).to eq([100]) + end + + it 'other corps have multiple par prices available' do + game.companies.each { |c| c.owner = game.players.first } + [game.corporation_by_id('NYC'), game.corporation_by_id('CP')].each do |corp| + prices = step.get_par_prices(game.players.first, corp) + expect(prices.size).to be > 1 + end + end + end + describe 'tile-lay budget' do it 'phase 2: single yellow tile only' do lays = game.tile_lays(game.corporations.first)