Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/engine/game/g_1862_usa_canada/entities.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ module Entities
type: 'token',
hexes: [],
discount: 80,
owner_type: 'corporation',
owner_type: 'player',
},
],
color: nil,
Expand Down
48 changes: 45 additions & 3 deletions lib/engine/game/g_1862_usa_canada/game.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -337,7 +379,7 @@ def stock_round
Engine::Step::DiscardTrain,
Engine::Step::Exchange,
Engine::Step::SpecialTrack,
Engine::Step::BuySellParShares,
G1862UsaCanada::Step::BuySellParShares,
])
end

Expand All @@ -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)
Expand Down
27 changes: 27 additions & 0 deletions lib/engine/game/g_1862_usa_canada/step/buy_sell_par_shares.rb
Original file line number Diff line number Diff line change
@@ -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
20 changes: 20 additions & 0 deletions lib/engine/game/g_1862_usa_canada/step/dividend.rb
Original file line number Diff line number Diff line change
@@ -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
35 changes: 35 additions & 0 deletions lib/engine/game/g_1862_usa_canada/step/token.rb
Original file line number Diff line number Diff line change
@@ -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
80 changes: 80 additions & 0 deletions spec/lib/engine/game/g_1862_usa_canada/game_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading