From 5570d40dbe8aab53107b87585ccfc05e48c798b8 Mon Sep 17 00:00:00 2001 From: Patrik Olesen Date: Thu, 9 Jan 2025 22:31:36 +0100 Subject: [PATCH 01/21] [18Uruguay] Interest should not be paid for penalty loans --- lib/engine/game/g_18_uruguay/loans.rb | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/lib/engine/game/g_18_uruguay/loans.rb b/lib/engine/game/g_18_uruguay/loans.rb index 69ea47a173..ed513e8bf6 100644 --- a/lib/engine/game/g_18_uruguay/loans.rb +++ b/lib/engine/game/g_18_uruguay/loans.rb @@ -65,12 +65,23 @@ def adjust_stock_market_loan_penalty(entity) end end - def take_loan_if_needed_for_interest!(entity) + def pay_interest!(entity) owed = interest_owed(entity) - return if owed.zero? + interest_paid[entity] = owed - remaining = owed - entity.cash - perform_ebuy_loans(entity, remaining + 10) if remaining.positive? + while owed > entity.cash && + (loan = loans[0]) + take_loan(entity, loan, ebuy: true) + end + + if owed <= entity.cash + if owed.positive? + log_interest_payment(entity, owed) + entity.spend(owed, bank) + end + return + end + owed end def corps_pay_interest @@ -78,7 +89,6 @@ def corps_pay_interest corps.each do |corp| next if corp.closed? - take_loan_if_needed_for_interest!(corp) pay_interest!(corp) end end From 8456d8b2a822b87f32883c5198a919be7fabe4f7 Mon Sep 17 00:00:00 2001 From: Patrik Olesen Date: Sun, 19 Jan 2025 07:23:36 +0100 Subject: [PATCH 02/21] [18Uruguay] Corrected subsidy payment after core update --- lib/engine/game/g_18_uruguay/game.rb | 12 +-- lib/engine/game/g_18_uruguay/step/dividend.rb | 94 ++----------------- 2 files changed, 14 insertions(+), 92 deletions(-) diff --git a/lib/engine/game/g_18_uruguay/game.rb b/lib/engine/game/g_18_uruguay/game.rb index 86165b09a9..0e1e8330ad 100644 --- a/lib/engine/game/g_18_uruguay/game.rb +++ b/lib/engine/game/g_18_uruguay/game.rb @@ -403,16 +403,12 @@ def revenue_str(route) str end - def rptla_revenue(corporation) - return 0 if @rptla != corporation - - (corporation.loans.size.to_f / 2).floor * 10 + def rptla_revenue + (@rptla.loans.size.to_f / 2).floor * 10 end - def rptla_subsidy(corporation) - return 0 if @rptla != corporation - - (corporation.loans.size.to_f / 2).ceil * 10 + def rptla_subsidy + (@rptla.loans.size.to_f / 2).ceil * 10 end def revenue_for(route, stops) diff --git a/lib/engine/game/g_18_uruguay/step/dividend.rb b/lib/engine/game/g_18_uruguay/step/dividend.rb index a8b7e3192d..cdb82bc61e 100644 --- a/lib/engine/game/g_18_uruguay/step/dividend.rb +++ b/lib/engine/game/g_18_uruguay/step/dividend.rb @@ -2,7 +2,6 @@ require_relative '../../../step/dividend' require_relative '../../../step/half_pay' - module Engine module Game module G18Uruguay @@ -18,24 +17,20 @@ def actions(entity) ACTIONS end - def routes_revenue(routes, entity) + def total_revenue revenue = @game.routes_revenue(routes) - revenue += @game.rptla_revenue(entity) if entity == @game.rptla + revenue += @game.rptla_revenue if current_entity == @game.rptla revenue end - def routes_subsidy(routes, entity) + def total_subsidy revenue = @game.routes_subsidy(routes) - revenue += @game.rptla_subsidy(entity) if entity == @game.rptla + revenue += @game.rptla_subsidy if current_entity == @game.rptla revenue end - def missing_revenue(entity) - (routes_revenue(routes, entity).zero? && routes_subsidy(routes, entity).zero?) - end - - def description - 'Pay or Withhold Dividends' + def missing_revenue(_entity) + (total_revenue.zero? && total_subsidy.zero?) end def auto_actions(entity) @@ -46,86 +41,17 @@ def auto_actions(entity) [Engine::Action::Dividend.new(current_entity, kind: 'withhold')] end - def dividend_options(entity) - total_revenue = routes_revenue(routes, entity) - revenue = total_revenue - - dividend_types.to_h do |type| - payout = send(type, entity, revenue) - payout[:divs_to_corporation] = corporation_dividends(entity, payout[:per_share]) - [type, payout.merge(share_price_change(entity, total_revenue - payout[:corporation]))] - end - end - - def holder_for_corporation(_entity) - @game.share_pool - end - - def payout(entity, revenue) - if @game.nationalized? && entity.loans.size.positive? - return { - corporation: payout_per_share(entity, revenue) * 10, - per_share: 0, - } - end - - { corporation: 0, per_share: payout_per_share(entity, revenue) } - end - - def withhold(_entity, revenue) - { corporation: revenue, per_share: 0 } - end - - def process_dividend_rptla(action) - entity = action.entity - revenue = routes_revenue(routes, entity) - subsidy = routes_subsidy(routes, entity) - kind = action.kind.to_sym - payout = dividend_options(entity)[kind] - entity.operating_history[[@game.turn, @round.round_num]] = OperatingInfo.new( - routes, - action, - revenue, - @round.laid_hexes - ) - - entity.trains.each { |train| train.operated = true } - - @round.routes = [] - log_run_payout_sub(entity, kind, revenue, subsidy, action, payout) - @game.bank.spend(payout[:corporation] + subsidy, entity) if payout[:corporation].positive? - payout_shares(entity, revenue - payout[:corporation]) if payout[:per_share].positive? - change_share_price(entity, payout) - - pass! - end - - def process_dividend(action) - return process_dividend_rptla(action) if action.entity == @game.rptla - - super - loans_to_pay_off = [(current_entity.cash / 100).floor, current_entity&.loans&.size].min - return if !loans_to_pay_off.positive? || !@game.nationalized? + def corporation_dividends(entity, per_share) + return 0 if entity.minor? + return 0 if entity == @game.rptla - @game.payoff_loan(current_entity, loans_to_pay_off, current_entity) + dividends_for_entity(entity, holder_for_corporation(entity), per_share) end def log_run_payout(entity, kind, revenue, subisdy, action, payout) super unless entity.minor? end - def log_run_payout_sub(entity, kind, revenue, _subsidy, _action, payout) - unless Dividend::DIVIDEND_TYPES.include?(kind) - @log << "#{entity.name} runs for #{@game.format_currency(revenue)} and pays #{action.kind}" - end - - if payout[:corporation].positive? - @log << "#{entity.name} withholds #{@game.format_currency(payout[:corporation])}" - elsif payout[:per_share].zero? - @log << "#{entity.name} does not run" unless entity.minor? - end - end - def rptla_share_price_change(entity, revenue) return {} if entity == @game.rptla && @game.phase.current[:name] == '2' From 83e67ea13d1ca537b0b1baeb58cdc4ac0040ac93 Mon Sep 17 00:00:00 2001 From: Chris Rericha Date: Sun, 12 Jan 2025 14:17:48 -0500 Subject: [PATCH 03/21] Map hex market movement up to diagonally up-right and down to diagonally down-left --- lib/engine/stock_movement.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/engine/stock_movement.rb b/lib/engine/stock_movement.rb index 0c627d689f..db8dbff089 100644 --- a/lib/engine/stock_movement.rb +++ b/lib/engine/stock_movement.rb @@ -115,6 +115,14 @@ def right(corporation, coordinates) @market.diagonally_up_right(corporation, coordinates) end + def up(corporation, coordinates) + diagonally_up_right(corporation, coordinates) + end + + def down(corporation, coordinates) + diagonally_down_left(corporation, coordinates) + end + def diagonally_down_left(_corporation, coordinates) r, c = coordinates new_coords = [r + 1, c] From ba21ad975244fc42288074f19f7452862f1e1586 Mon Sep 17 00:00:00 2001 From: Chris Rericha Date: Sun, 12 Jan 2025 14:27:40 -0500 Subject: [PATCH 04/21] [1837] Show corporation stock cards in sorted order --- lib/engine/game/g_1837/step/buy_sell_par_shares.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/engine/game/g_1837/step/buy_sell_par_shares.rb b/lib/engine/game/g_1837/step/buy_sell_par_shares.rb index 23d888241e..751e482ba3 100644 --- a/lib/engine/game/g_1837/step/buy_sell_par_shares.rb +++ b/lib/engine/game/g_1837/step/buy_sell_par_shares.rb @@ -20,7 +20,7 @@ def actions(entity) end def visible_corporations - @game.corporations.reject { |c| c.type == :minor } + @game.sorted_corporations.reject { |c| c.type == :minor } end def hide_corporations? From f0830cae22003eb831493354c04f4acb969a423e Mon Sep 17 00:00:00 2001 From: Chris Rericha Date: Sun, 12 Jan 2025 14:42:14 -0500 Subject: [PATCH 05/21] [1837] Coal and minor exchanges --- lib/engine/game/g_1837/game.rb | 128 +++++++++++++++--- lib/engine/game/g_1837/round/exchange.rb | 48 +++++++ lib/engine/game/g_1837/round/stock.rb | 10 -- lib/engine/game/g_1837/step/coal_exchange.rb | 64 +++++++++ lib/engine/game/g_1837/step/minor_exchange.rb | 119 ++++++++++++++++ 5 files changed, 341 insertions(+), 28 deletions(-) create mode 100644 lib/engine/game/g_1837/round/exchange.rb create mode 100644 lib/engine/game/g_1837/step/coal_exchange.rb create mode 100644 lib/engine/game/g_1837/step/minor_exchange.rb diff --git a/lib/engine/game/g_1837/game.rb b/lib/engine/game/g_1837/game.rb index dde26f0e56..8feabb0de3 100644 --- a/lib/engine/game/g_1837/game.rb +++ b/lib/engine/game/g_1837/game.rb @@ -121,7 +121,7 @@ class Game < Game::Base num: 4, distance: 4, price: 470, - events: [{ 'type' => 'sd_formation' }, { 'type' => 'remove_italy' }], + events: [{ 'type' => 'sd_formation' }, { 'type' => 'kk_can_form' }, { 'type' => 'remove_italy' }], }, { name: '4E', @@ -131,6 +131,7 @@ class Game < Game::Base { 'nodes' => %w[town], 'pay' => 0, 'visit' => 99 }, ], price: 500, + events: [{ 'type' => 'ug_can_form' }], }, { name: '4+1', @@ -243,7 +244,9 @@ class Game < Game::Base 'buy_across' => ['Buy Across', 'Trains can be bought between companies'], 'sd_formation' => ['SD Formation', 'SD forms immediately'], 'remove_italy' => ['Remove Italy', 'Remove tiles in Italy. Italy no longer in play.'], + 'kk_can_form' => ['Optional KK Formation', 'KK can choose to form at beginning of SR/OR'], 'kk_formation' => ['KK Formation', 'KK forms immediately'], + 'ug_can_form' => ['Optional UG Formation', 'UG can choose to form at beginning of SR/OR'], 'ug_formation' => ['UG Formation', 'UG forms immediately'], 'exchange_coal_companies' => ['Exchange Coal Companies', 'All remaining coal companies are exchanged'], 'close_mountain_railways' => ['Mountain Railways Close', 'All Mountain Railways close'], @@ -327,6 +330,18 @@ def setup_nationals end end + def sd_minors + @sd_minors ||= %w[SD1 SD2 SD3 SD4 SD5].map { |id| corporation_by_id(id) } + end + + def kk_minors + @kk_minors ||= %w[KK1 KK2 KK3].map { |id| corporation_by_id(id) } + end + + def ug_minors + @ug_minors ||= %w[UG1 UG2 UG3].map { |id| corporation_by_id(id) } + end + def event_buy_across! @log << "-- Event: #{EVENTS_TEXT['buy_across'][1]} --" end @@ -334,8 +349,7 @@ def event_buy_across! def event_sd_formation! @log << "-- Event: #{EVENTS_TEXT['sd_formation'][1]} --" national = corporation_by_id('SD') - minors = %w[SD1 SD2 SD3 SD4 SD5].map { |id| corporation_by_id(id) } - form_national_railway!(national, minors) + form_national_railway!(national, sd_minors) end def event_remove_italy! @@ -351,8 +365,13 @@ def event_remove_italy! @graph.clear_graph_for_all end + def event_kk_can_form! + @log << "-- Event: #{EVENTS_TEXT['kk_can_form'][1]} --" + @kk_can_form = true + end + def event_kk_formation! - open_minors = %w[KK1 KK2 KK3].map { |id| corporation_by_id(id) }.reject(&:closed?) + open_minors = kk_minors.reject(&:closed?) return if open_minors.empty? @log << "-- Event: #{EVENTS_TEXT['kk_formation'][1]} --" @@ -362,12 +381,16 @@ def event_kk_formation! else @log << "#{national.name} already formed. Remaining minors must fold in." open_minors.each { |m| merge_minor!(m, national) } - set_national_president!(national) end end + def event_ug_can_form! + @log << "-- Event: #{EVENTS_TEXT['ug_can_form'][1]} --" + @ug_can_form = true + end + def event_ug_formation! - open_minors = %w[UG1 UG2 UG3].map { |id| corporation_by_id(id) }.reject(&:closed?) + open_minors = ug_minors.reject(&:closed?) return if open_minors.empty? national = corporation_by_id('UG') @@ -377,7 +400,6 @@ def event_ug_formation! else @log << "#{national.name} already formed. Remaining minors must fold in." open_minors.each { |m| merge_minor!(m, national) } - set_national_president!(national) end end @@ -391,23 +413,49 @@ def operating_order @minors.select(&:floated?) + minors + majors.sort end - def coal_company_exchange_order + def exchange_order + order = coal_company_exchange_order + order.concat(kk_minors.reject(&:closed?)) if @kk_can_form + order.concat(ug_minors.reject(&:closed?)) if @ug_can_form + order + end + + def exchange_target(entity) + if entity.company? + target_id = abilities(entity, :exchange, time: 'any')&.corporations&.first + corporation_by_id(target_id) + elsif kk_minors.include?(entity) + corporation_by_id('KK') + elsif ug_minors.include?(entity) + corporation_by_id('UG') + end + end + + def coal_company_exchange_order(mandatory = false) exchangeable_companies = Hash.new { |h, k| h[k] = [] } @companies.each do |c| next if c.closed? || !c.owner&.player? - next unless (ability = abilities(c, :exchange, time: 'any')) + next unless (target = exchange_target(c)) - exchangeable_companies[ability.corporations.first] << c + exchangeable_companies[target] << c end - major_order = (operating_order + @corporations.sort).uniq.select { |e| e.corporation? && e.type == :major } - major_order.flat_map do |major| - player_order = major.owner&.player? ? @players.rotate!(@players.index(major.owner)) : @players - exchangeable_companies[major.id].sort_by { |c| player_order.index(c.owner) } + order = operating_order + order = order.concat(@corporations).uniq if mandatory + order.select { |c| c.corporation? && c.type == :major }.flat_map do |major| + player_order = major.owner&.player? ? @players.rotate(@players.index(major.owner)) : @players + exchangeable_companies[major].sort_by { |c| player_order.index(c.owner) } end.compact end + def mandatory_coal_company_exchange?(entity) + return false if !entity.company? || entity.closed? || !entity.owner&.player? + + exchange_target(entity).percent_ipo_buyable.zero? + end + def exchange_coal_company(company) + @log << "#{company.sym} exchanged for a share of #{exchange_target(company).id}" major = corporation_by_id(abilities(company, :exchange, time: 'any').corporations.first) minor = minor_by_id(company.sym) merge_minor!(minor, major) @@ -420,6 +468,7 @@ def event_close_mountain_railways! end def form_national_railway!(national, merging_minors) + @log << "#{national.id} forms" national.floatable = true national.floated = true ipo_cash = (10 - national.num_ipo_reserved_shares) * national.par_price.price @@ -429,20 +478,20 @@ def form_national_railway!(national, merging_minors) tie_breaker_order = [] merging_minors.sort_by(&:name).each do |minor| tie_breaker_order << minor.owner - merge_minor!(minor, national) + merge_minor!(minor, national, allow_president_change: false) end set_national_president!(national, tie_breaker_order.uniq) graph.clear_graph_for(national) end - def merge_minor!(minor, corporation) + def merge_minor!(minor, corporation, allow_president_change: true) coal_company_exchange = minor.type == :coal @log << "#{minor.name} merges into #{corporation.name}" @log << "#{minor.owner.name} receives 1 share of #{corporation.name}" share = corporation.reserved_shares[0] share.buyable = true - @share_pool.transfer_shares(ShareBundle.new(share), minor.owner, allow_president_change: coal_company_exchange) + @share_pool.transfer_shares(ShareBundle.new(share), minor.owner, allow_president_change: allow_president_change) # TODO: cannot receive dividends if minor already operated this OR if minor.cash.positive? @@ -463,7 +512,7 @@ def merge_minor!(minor, corporation) new_token = Token.new(corporation) corporation.tokens << new_token if %w[L2 L8].include?(token.hex.id) - token.price = 20 + new_token.price = 20 else token.swap!(new_token, check_tokenable: false) end @@ -494,6 +543,36 @@ def set_national_president!(national, tie_breaker = []) national.owner = president end + def next_round! + @round = + case @round + when Round::Stock + @operating_rounds = @phase.operating_rounds + reorder_players + new_exchange_round(Round::Operating) + when Round::Exchange + if @round_after_exchange == Round::Stock + new_stock_round + else + new_operating_round(@round.round_num) + end + when Round::Operating + if @round.round_num < @operating_rounds + or_round_finished + new_exchange_round(Round::Operating, @round.round_num + 1) + else + @turn += 1 + or_round_finished + or_set_finished + new_exchange_round(Round::Stock) + end + when init_round.class + init_round_finished + reorder_players + new_stock_round + end + end + def new_auction_round Engine::Round::Auction.new(self, [ G1837::Step::SelectionAuction, @@ -521,6 +600,19 @@ def operating_round(round_num) ], round_num: round_num) end + def new_exchange_round(next_round, round_num = 1) + @round_after_exchange = next_round + exchange_round(round_num) + end + + def exchange_round(round_num) + G1837::Round::Exchange.new(self, [ + G1837::Step::DiscardTrain, + G1837::Step::CoalExchange, + G1837::Step::MinorExchange, + ], round_num: round_num) + end + def corporation_show_individual_reserved_shares? false end diff --git a/lib/engine/game/g_1837/round/exchange.rb b/lib/engine/game/g_1837/round/exchange.rb new file mode 100644 index 0000000000..6441711c45 --- /dev/null +++ b/lib/engine/game/g_1837/round/exchange.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require_relative '../../../round/choices' + +module Engine + module Game + module G1837 + module Round + class Exchange < Engine::Round::Choices + def name + 'Exchange Round' + end + + def self.short_name + 'ER' + end + + def select_entities + @game.exchange_order + end + + def setup + super + skip_steps + next_entity! if finished? + end + + def after_process(_action) + return if active_step + + next_entity! + end + + def next_entity! + next_entity_index! unless @entities.empty? + return if @entity_index.zero? + + @steps.each(&:unpass!) + @steps.each(&:setup) + + skip_steps + next_entity! if finished? + end + end + end + end + end +end diff --git a/lib/engine/game/g_1837/round/stock.rb b/lib/engine/game/g_1837/round/stock.rb index 7040d3be6b..4d7911ec7b 100644 --- a/lib/engine/game/g_1837/round/stock.rb +++ b/lib/engine/game/g_1837/round/stock.rb @@ -10,16 +10,6 @@ class Stock < Engine::Round::Stock def sold_out?(corporation) corporation.percent_ipo_buyable.zero? && corporation.num_market_shares.zero? end - - def sold_out_stock_movement(corp) - if corporation.owner.percent_of(corporation) <= 40 - @game.stock_market.move_up(corp) - else - original_share_price = corporation.share_price - @game.stock_market.move_left(corp) - @game.stock_market.move_up(corp) if original_share_price != corporation.share_price - end - end end end end diff --git a/lib/engine/game/g_1837/step/coal_exchange.rb b/lib/engine/game/g_1837/step/coal_exchange.rb new file mode 100644 index 0000000000..ce8e21c783 --- /dev/null +++ b/lib/engine/game/g_1837/step/coal_exchange.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require_relative '../../../step/base' + +module Engine + module Game + module G1837 + module Step + class CoalExchange < Engine::Step::Base + ACTIONS = %w[choose].freeze + CHOICES = { :exchange => 'Exchange', :pass => 'Decline' }.freeze + + def actions(entity) + return [] unless entity == current_entity + return [] unless can_exchange?(entity) + + ACTIONS + end + + def auto_actions(entity) + return [] unless @game.mandatory_coal_company_exchange?(entity) + + [Action::Choose.new(entity, choice: CHOICES[:exchange])] + end + + def description + 'Exchange' + end + + def can_exchange?(entity) + entity.company? && !entity.closed? + end + + def choice_name + "Exchange for #{exchange_target(current_entity).id} share" + end + + def choices + CHOICES.values + end + + def exchange_target(entity) + @game.exchange_target(entity) + end + + def process_choose(action) + entity = action.entity + if action.choice == CHOICES[:exchange] + @log << "#{entity.sym} must be exchanged" if @game.mandatory_coal_company_exchange?(entity) + @game.exchange_coal_company(entity) + else + @log << "#{entity.sym} declines exchange" + end + pass! + end + + def log_skip(entity) + @log << "#{entity.sym} cannot exchange" if entity.company? + end + end + end + end + end +end diff --git a/lib/engine/game/g_1837/step/minor_exchange.rb b/lib/engine/game/g_1837/step/minor_exchange.rb new file mode 100644 index 0000000000..2bdf611df9 --- /dev/null +++ b/lib/engine/game/g_1837/step/minor_exchange.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +require_relative '../../../step/base' + +module Engine + module Game + module G1837 + module Step + class MinorExchange < Engine::Step::Base + ACTIONS = %w[choose].freeze + CHOICES = { :form => 'form', :fold_in => 'fold_in', :decline => 'decline' }.freeze + + def actions(entity) + return [] unless entity == current_entity + return [] unless can_exchange?(entity) + + ACTIONS + end + + def auto_actions(_entity) + [] + end + + def description + 'National Railways' + end + + def can_exchange?(entity) + return false if entity.closed? || !entity.corporation? || entity.type != :minor + return true if forming_choice_minors.include?(entity) + + target = exchange_target(entity) + target_forming?(target) || exchange_target(entity).floated? + end + + def choice_name + if forming_choice_minors.include?(current_entity) + form_choice_label(exchange_target) + else + fold_in_choice_label(exchange_target) + end + end + + def form_choice_label(target) + "Form #{target.id}" + end + + def fold_in_choice_label(target) + "Fold into #{target.id}" + end + + def choices + if forming_choice_minors.include?(current_entity) + { + CHOICES[:form] => form_choice_label(exchange_target), + CHOICES[:decline] => CHOICES[:decline], + }.freeze + else + { + CHOICES[:fold_in] => fold_in_choice_label(exchange_target), + CHOICES[:decline] => CHOICES[:decline], + }.freeze + end + end + + def exchange_target(entity = current_entity) + @game.exchange_target(entity) + end + + def forming_choice_minors + @forming_choice_minors ||= %w[KK1 UG1].map { |id| @game.corporation_by_id(id) } + end + + def target_forming?(target) + !@round.forming_minors[target].empty? + end + + def process_choose(action) + entity = action.entity + target = exchange_target(entity) + choice = action.choice + if CHOICES[:form] == choice + @log << "#{entity.id} opts to form #{target.id}" + @round.forming_minors[target] << entity + elsif CHOICES[:fold_in] == choice + @log << "#{entity.id} opts to fold into #{target.id}" + if target_forming?(target) + @round.forming_minors[target] << entity + else + @game.merge_minor!(target, entity) + end + else + @log << if forming_choice_minors.include?(current_entity) + "#{entity.id} declines to form #{target.id}" + else + "#{entity.id} declines to fold into #{target.id}" + end + end + + pass! + return if !target_forming?(target) || (@game.kk_minors.last != entity && @game.ug_minors.last != entity) + + @game.form_national_railway!(target, @round.forming_minors[target]) + end + + def log_skip(entity) + @log << "#{entity.id} has no action" if entity.corporation? && entity.type == :minor + end + + def round_state + { + forming_minors: Hash.new { |h, k| h[k] = [] }, + } + end + end + end + end + end +end From c7ac1745fe81ab33854aaa06b8c218470316ba4a Mon Sep 17 00:00:00 2001 From: Chris Rericha Date: Sun, 12 Jan 2025 17:08:30 -0500 Subject: [PATCH 06/21] [1837] Shares exchanged after operation don't pay out --- lib/engine/game/g_1837/game.rb | 8 +++++++- lib/engine/game/g_1837/step/dividend.rb | 16 +++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/lib/engine/game/g_1837/game.rb b/lib/engine/game/g_1837/game.rb index 8feabb0de3..ba08cdfd5a 100644 --- a/lib/engine/game/g_1837/game.rb +++ b/lib/engine/game/g_1837/game.rb @@ -492,7 +492,9 @@ def merge_minor!(minor, corporation, allow_president_change: true) share = corporation.reserved_shares[0] share.buyable = true @share_pool.transfer_shares(ShareBundle.new(share), minor.owner, allow_president_change: allow_president_change) - # TODO: cannot receive dividends if minor already operated this OR + if @round.respond_to?(:non_paying_shares) && operated_this_round?(minor) + @round.non_paying_shares[minor.owner][corporation] += 1 + end if minor.cash.positive? @log << "#{corporation.name} receives #{format_currency(minor.cash)}" @@ -760,6 +762,10 @@ def sold_out_stock_movement(corp) @stock_market.move_diagonally_up_left(corp) end end + + def operated_this_round?(entity) + entity.operating_history.include?([@turn, @round.round_num]) + end end end end diff --git a/lib/engine/game/g_1837/step/dividend.rb b/lib/engine/game/g_1837/step/dividend.rb index 9fdf900b81..ef691b2dfa 100644 --- a/lib/engine/game/g_1837/step/dividend.rb +++ b/lib/engine/game/g_1837/step/dividend.rb @@ -33,7 +33,7 @@ def half(entity, revenue) end def dividends_for_entity(entity, holder, per_share) - (holder.num_shares_of(entity, ceil: false) * per_share).floor + (num_paying_shares(entity, holder) * per_share).floor end def share_price_change(_entity, revenue) @@ -45,6 +45,20 @@ def share_price_change(_entity, revenue) { share_direction: :diagonally_down_right, share_times: 1 } end end + + def round_state + super.merge( + { + non_paying_shares: Hash.new { |h, k| h[k] = Hash.new(0) }, + } + ) + end + + private + + def num_paying_shares(entity, holder) + holder.num_shares_of(entity) - @round.non_paying_shares[holder][entity] + end end end end From 804ca712818df329dda4055fb1c70a2aacb4f90e Mon Sep 17 00:00:00 2001 From: Chris Rericha Date: Sun, 12 Jan 2025 17:40:14 -0500 Subject: [PATCH 07/21] [Common] Allow game class to define sold_out? for corporation --- lib/engine/game/base.rb | 8 ++++++-- lib/engine/round/stock.rb | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/engine/game/base.rb b/lib/engine/game/base.rb index 3854692342..c0ef175013 100644 --- a/lib/engine/game/base.rb +++ b/lib/engine/game/base.rb @@ -1274,8 +1274,12 @@ def sold_out_increase?(_corporation) self.class::SOLD_OUT_INCREASE end - def sold_out_stock_movement(corp) - @stock_market.move_up(corp) + def sold_out?(corporation) + corporation.player_share_holders.values.sum == 100 + end + + def sold_out_stock_movement(corporation) + @stock_market.move_up(corporation) end def log_share_price(entity, from, steps = nil, log_steps: false) diff --git a/lib/engine/round/stock.rb b/lib/engine/round/stock.rb index d4c17ce97c..52da5e40ab 100644 --- a/lib/engine/round/stock.rb +++ b/lib/engine/round/stock.rb @@ -100,7 +100,7 @@ def sold_out_stock_movement(corp) end def sold_out?(corporation) - corporation.player_share_holders.values.sum == 100 + @game.sold_out?(corporation) end def inspect From 93b4b676cfabc2c7eef18e7b8c6e5ce57c5a6b4c Mon Sep 17 00:00:00 2001 From: Chris Rericha Date: Sun, 12 Jan 2025 18:17:32 -0500 Subject: [PATCH 08/21] [1837] Use new sold_out? method in game class --- lib/engine/game/g_1837/game.rb | 18 +++++++++++------- lib/engine/game/g_1837/round/stock.rb | 17 ----------------- 2 files changed, 11 insertions(+), 24 deletions(-) delete mode 100644 lib/engine/game/g_1837/round/stock.rb diff --git a/lib/engine/game/g_1837/game.rb b/lib/engine/game/g_1837/game.rb index ba08cdfd5a..d7b19e813c 100644 --- a/lib/engine/game/g_1837/game.rb +++ b/lib/engine/game/g_1837/game.rb @@ -548,12 +548,12 @@ def set_national_president!(national, tie_breaker = []) def next_round! @round = case @round - when Round::Stock + when Engine::Round::Stock @operating_rounds = @phase.operating_rounds reorder_players new_exchange_round(Round::Operating) when Round::Exchange - if @round_after_exchange == Round::Stock + if @round_after_exchange == Engine::Round::Stock new_stock_round else new_operating_round(@round.round_num) @@ -566,7 +566,7 @@ def next_round! @turn += 1 or_round_finished or_set_finished - new_exchange_round(Round::Stock) + new_exchange_round(Engine::Round::Stock) end when init_round.class init_round_finished @@ -582,7 +582,7 @@ def new_auction_round end def stock_round - G1837::Round::Stock.new(self, [ + Engine::Round::Stock.new(self, [ G1837::Step::DiscardTrain, G1837::Step::BuySellParShares, ]) @@ -755,9 +755,13 @@ def legal_tile_rotation?(entity, hex, tile) super end - def sold_out_stock_movement(corp) - if corp.owner.percent_of(corp) <= 40 - @stock_market.move_up(corp) + def sold_out?(corporation) + corporation.percent_ipo_buyable.zero? && corporation.num_market_shares.zero? + end + + def sold_out_stock_movement(corporation) + if corp.owner.percent_of(corporation) <= 40 + @stock_market.move_up(corporation) else @stock_market.move_diagonally_up_left(corp) end diff --git a/lib/engine/game/g_1837/round/stock.rb b/lib/engine/game/g_1837/round/stock.rb deleted file mode 100644 index 4d7911ec7b..0000000000 --- a/lib/engine/game/g_1837/round/stock.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -require_relative '../../../round/stock' - -module Engine - module Game - module G1837 - module Round - class Stock < Engine::Round::Stock - def sold_out?(corporation) - corporation.percent_ipo_buyable.zero? && corporation.num_market_shares.zero? - end - end - end - end - end -end From c489bac059009035e80bced9bb9dada24b058638 Mon Sep 17 00:00:00 2001 From: Chris Rericha Date: Sun, 12 Jan 2025 19:48:06 -0500 Subject: [PATCH 09/21] [Common] Fix typos in stock_movement methods --- lib/engine/stock_movement.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/engine/stock_movement.rb b/lib/engine/stock_movement.rb index db8dbff089..f7ddfb8563 100644 --- a/lib/engine/stock_movement.rb +++ b/lib/engine/stock_movement.rb @@ -39,7 +39,7 @@ def diagonally_down_right(_corporation, coordinates) raise NotImplementedError end - def diagnonally_up_right(_corporation, coordinates) + def diagonally_up_right(_corporation, coordinates) raise NotImplementedError end end @@ -148,7 +148,7 @@ def diagonally_down_right(_corporation, coordinates) coordinates end - def diagnonally_up_right(_corporation, coordinates) + def diagonally_up_right(_corporation, coordinates) r, c = coordinates new_coords = [r - 1, c] return new_coords if share_price(new_coords) From eb2b2d2715575e75bd5781b94027f1182a1a30b8 Mon Sep 17 00:00:00 2001 From: Chris Rericha Date: Sun, 12 Jan 2025 20:41:36 -0500 Subject: [PATCH 10/21] [1837] Misc bug fixes --- lib/engine/game/g_1837/game.rb | 10 +++++----- lib/engine/game/g_1837/step/minor_exchange.rb | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/engine/game/g_1837/game.rb b/lib/engine/game/g_1837/game.rb index d7b19e813c..0a062492f6 100644 --- a/lib/engine/game/g_1837/game.rb +++ b/lib/engine/game/g_1837/game.rb @@ -531,11 +531,11 @@ def close_minor!(minor) def set_national_president!(national, tie_breaker = []) tie_breaker = tie_breaker.reverse - current_president = national.presidents_share.owner + current_president = national.owner # president determined by most shares, then tie breaker, then current president - president_factors = national.player_share_holders.to_h do |player, shares| - [[shares.size, tie_breaker.index(player) || -1, player == current_president ? 1 : 0], player] + president_factors = national.player_share_holders.to_h do |player, percent| + [[percent, tie_breaker.index(player) || -1, player == current_president ? 1 : 0], player] end president = president_factors[president_factors.keys.max] return unless current_president != president @@ -760,10 +760,10 @@ def sold_out?(corporation) end def sold_out_stock_movement(corporation) - if corp.owner.percent_of(corporation) <= 40 + if corporation.owner.percent_of(corporation) <= 40 @stock_market.move_up(corporation) else - @stock_market.move_diagonally_up_left(corp) + @stock_market.move_diagonally_up_left(corporation) end end diff --git a/lib/engine/game/g_1837/step/minor_exchange.rb b/lib/engine/game/g_1837/step/minor_exchange.rb index 2bdf611df9..92b9bb0dfc 100644 --- a/lib/engine/game/g_1837/step/minor_exchange.rb +++ b/lib/engine/game/g_1837/step/minor_exchange.rb @@ -87,7 +87,7 @@ def process_choose(action) if target_forming?(target) @round.forming_minors[target] << entity else - @game.merge_minor!(target, entity) + @game.merge_minor!(entity, target) end else @log << if forming_choice_minors.include?(current_entity) From 835e61c862e01c423498266efeef110d656004ba Mon Sep 17 00:00:00 2001 From: Chris Rericha Date: Tue, 14 Jan 2025 01:19:04 -0500 Subject: [PATCH 11/21] [1837] Exchange bug fixes --- lib/engine/game/g_1837/game.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/engine/game/g_1837/game.rb b/lib/engine/game/g_1837/game.rb index 0a062492f6..7da2a78269 100644 --- a/lib/engine/game/g_1837/game.rb +++ b/lib/engine/game/g_1837/game.rb @@ -405,7 +405,7 @@ def event_ug_formation! def event_exchange_coal_companies! @log << "-- Event: #{EVENTS_TEXT['exchange_coal_companies'][1]} --" - coal_company_exchange_order.each { |c| exchange_coal_company(c) } + coal_company_exchange_order(mandatory: true).each { |c| exchange_coal_company(c) } end def operating_order @@ -431,7 +431,7 @@ def exchange_target(entity) end end - def coal_company_exchange_order(mandatory = false) + def coal_company_exchange_order(mandatory: false) exchangeable_companies = Hash.new { |h, k| h[k] = [] } @companies.each do |c| next if c.closed? || !c.owner&.player? @@ -481,7 +481,6 @@ def form_national_railway!(national, merging_minors) merge_minor!(minor, national, allow_president_change: false) end set_national_president!(national, tie_breaker_order.uniq) - graph.clear_graph_for(national) end def merge_minor!(minor, corporation, allow_president_change: true) @@ -522,6 +521,7 @@ def merge_minor!(minor, corporation, allow_president_change: true) end close_minor!(minor) + graph.clear_graph_for(corporation) end def close_minor!(minor) @@ -531,7 +531,7 @@ def close_minor!(minor) def set_national_president!(national, tie_breaker = []) tie_breaker = tie_breaker.reverse - current_president = national.owner + current_president = national.owner || national # president determined by most shares, then tie breaker, then current president president_factors = national.player_share_holders.to_h do |player, percent| From 9cdd7da0a23b44ccfb076ad0d843d8eea33a2785 Mon Sep 17 00:00:00 2001 From: Chris Rericha Date: Tue, 14 Jan 2025 01:37:20 -0500 Subject: [PATCH 12/21] [1837] EMR rules --- lib/engine/game/g_1837/game.rb | 3 +++ lib/engine/game/g_1837/step/buy_train.rb | 4 +--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/engine/game/g_1837/game.rb b/lib/engine/game/g_1837/game.rb index 7da2a78269..947e0ede2b 100644 --- a/lib/engine/game/g_1837/game.rb +++ b/lib/engine/game/g_1837/game.rb @@ -32,6 +32,9 @@ class Game < Game::Base HOME_TOKEN_TIMING = :float EBUY_DEPOT_TRAIN_MUST_BE_CHEAPEST = false + EBUY_FROM_OTHERS = :always + EBUY_SELL_MORE_THAN_NEEDED = true + EBUY_SELL_MORE_THAN_NEEDED_SETS_PURCHASE_MIN = true MUST_BUY_TRAIN = :always BANKRUPTCY_ENDS_GAME_AFTER = :all_but_one diff --git a/lib/engine/game/g_1837/step/buy_train.rb b/lib/engine/game/g_1837/step/buy_train.rb index c77fdf42d1..fe0954a500 100644 --- a/lib/engine/game/g_1837/step/buy_train.rb +++ b/lib/engine/game/g_1837/step/buy_train.rb @@ -8,10 +8,8 @@ module G1837 module Step class BuyTrain < Engine::Step::BuyTrain def actions(entity) - return [] if entity != current_entity - actions = super.clone - unless scrappable_trains(entity).empty? + if entity.operator? && !scrappable_trains(entity).empty? actions << 'pass' if actions.empty? actions << 'scrap_train' end From 99a8f2a43c9dfe40ed0cd6a80949e08a0a6bad57 Mon Sep 17 00:00:00 2001 From: Chris Rericha Date: Wed, 15 Jan 2025 02:12:22 -0500 Subject: [PATCH 13/21] [1837] Move corporation minor half pay logic to minor_half_pay module --- lib/engine/game/g_1837/step/dividend.rb | 16 ---------------- lib/engine/game/g_1837/step/minor_half_pay.rb | 19 ++++++++++++------- 2 files changed, 12 insertions(+), 23 deletions(-) diff --git a/lib/engine/game/g_1837/step/dividend.rb b/lib/engine/game/g_1837/step/dividend.rb index ef691b2dfa..b68abf8566 100644 --- a/lib/engine/game/g_1837/step/dividend.rb +++ b/lib/engine/game/g_1837/step/dividend.rb @@ -11,22 +11,6 @@ class Dividend < Engine::Step::Dividend DIVIDEND_TYPES = %i[payout half withhold].freeze include G1837::Step::MinorHalfPay - def actions(entity) - return super if !entity.corporation? || entity.type != :minor - - [] - end - - def skip! - return super if !current_entity.corporation? || current_entity.type != :minor - - revenue = @game.routes_revenue(routes) - process_dividend(Action::Dividend.new( - current_entity, - kind: revenue.positive? ? 'half' : 'withhold', - )) - end - def half(entity, revenue) amount = revenue / 2 { corporation: amount, per_share: payout_per_share(entity, amount) } diff --git a/lib/engine/game/g_1837/step/minor_half_pay.rb b/lib/engine/game/g_1837/step/minor_half_pay.rb index 4b139659f5..a06714303f 100644 --- a/lib/engine/game/g_1837/step/minor_half_pay.rb +++ b/lib/engine/game/g_1837/step/minor_half_pay.rb @@ -6,19 +6,24 @@ module G1837 module Step module MinorHalfPay def actions(entity) - return super unless entity.minor? + return [] if entity.minor? + return [] if entity.corporation? && entity.type == :minor - [] + super end def skip! - return super unless current_entity.minor? + return super if current_entity.corporation? && current_entity.type != :minor revenue = @game.routes_revenue(routes) - process_dividend(Action::Dividend.new( - current_entity, - kind: revenue.positive? ? 'payout' : 'withhold', - )) + kind = if revenue.zero? + 'withhold' + elsif current_entity.minor? + 'payout' + else + 'half' + end + process_dividend(Action::Dividend.new(current_entity, kind: kind)) end def share_price_change(entity, revenue = 0) From d08cc8435dc72e0c0fc70ffc018d46852c2a0683 Mon Sep 17 00:00:00 2001 From: Chris Rericha Date: Sun, 19 Jan 2025 22:43:14 -0500 Subject: [PATCH 14/21] [1837] Bankruptcy and financed trains --- assets/app/view/game/buy_trains.rb | 5 ++ lib/engine/game/g_1837/game.rb | 2 +- lib/engine/game/g_1837/step/bankrupt.rb | 73 +++++++++++++++++++ lib/engine/game/g_1837/step/buy_train.rb | 24 +++++- lib/engine/game/g_1837/step/dividend.rb | 6 ++ lib/engine/game/g_1837/step/minor_half_pay.rb | 12 +-- lib/engine/step/train.rb | 13 ++++ 7 files changed, 127 insertions(+), 8 deletions(-) create mode 100644 lib/engine/game/g_1837/step/bankrupt.rb diff --git a/assets/app/view/game/buy_trains.rb b/assets/app/view/game/buy_trains.rb index 5addba5f15..ebff69d71d 100644 --- a/assets/app/view/game/buy_trains.rb +++ b/assets/app/view/game/buy_trains.rb @@ -300,6 +300,11 @@ def render children << h(:div, issue_str) end + if @step.can_finance?(@corporation) + text = "#{@game.bank.name} will provide financing for the amount the corporation cannot pay." + children << h(:div, text) + end + if (@must_buy_train && @step.ebuy_president_can_contribute?(@corporation)) || @step.president_may_contribute?(@corporation, @active_shell) children.concat(render_president_contributions) diff --git a/lib/engine/game/g_1837/game.rb b/lib/engine/game/g_1837/game.rb index 947e0ede2b..ae79835ccb 100644 --- a/lib/engine/game/g_1837/game.rb +++ b/lib/engine/game/g_1837/game.rb @@ -593,7 +593,7 @@ def stock_round def operating_round(round_num) G1837::Round::Operating.new(self, [ - Engine::Step::Bankrupt, + G1837::Step::Bankrupt, G1837::Step::HomeToken, G1837::Step::DiscardTrain, G1837::Step::SpecialTrack, diff --git a/lib/engine/game/g_1837/step/bankrupt.rb b/lib/engine/game/g_1837/step/bankrupt.rb new file mode 100644 index 0000000000..cd843c2c03 --- /dev/null +++ b/lib/engine/game/g_1837/step/bankrupt.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require_relative '../../../step/bankrupt' + +module Engine + module Game + module G1837 + module Step + class Bankrupt < Engine::Step::Bankrupt + def process_bankrupt(action) + entity = action.entity + player = entity.corporation? ? entity.owner : entity + + @log << "-- #{player.name} goes bankrupt and remaining shares placed in the bank pool --" + + unless player.companies.empty? + @log << "#{player.name}'s companies close: #{player.companies.map(&:sym).join(', ')}" + player.companies.dup.each(&:close!) + end + + # President sells all normally allowed shares + player.shares_by_corporation(sorted: true).each do |corporation, _| + next unless corporation.share_price # if a corporation has not parred + next unless (bundle = @game.sellable_bundles(player, corporation).max_by(&:price)) + + @game.sell_shares_and_change_price(bundle) + end + + # Remaining shares are placed in the bank pool. Minors merge immediately, unless + # the presidency is transferred (UG1 and UG3). + player.shares_by_corporation(sorted: true).each do |corporation, shares| + next if shares.empty? + + bundle = ShareBundle.new(shares) + @game.share_pool.transfer_shares(bundle, @game.share_pool, allow_president_change: true) + next if corporation.owner != player + + corporation.owner = @game.share_pool + if corporation.type == :minor + @log << "#{corporation.name} is forced to merge immediately" + @game.merge_minor!(corporation, @game.exchange_target(corporation)) + else + @log << "-- #{corporation.name} is directorless --" + end + end + + # Coal companies merge immediately + @game.minors.select { |m| m.owner == player }.each do |minor| + minor.owner = @game.share_pool + @log << "#{minor.name} is forced to merge immediately" + @game.merge_minor!(minor, @game.exchange_target(minor)) + end + + if entity.corporation? && player.cash.positive? + @log << "#{player.name}'s remaining cash (#{@game.format_currency(player.cash)}) is "\ + "transferred to #{entity.name}" + player.spend(player.cash, entity) + end + + @game.declare_bankrupt(player) + player.cash = 0 + + @round.bankrupting_corporations << entity if entity.corporation? + end + + def round_state + super.merge(bankrupting_corporations: []) + end + end + end + end + end +end diff --git a/lib/engine/game/g_1837/step/buy_train.rb b/lib/engine/game/g_1837/step/buy_train.rb index fe0954a500..ccea9e1db6 100644 --- a/lib/engine/game/g_1837/step/buy_train.rb +++ b/lib/engine/game/g_1837/step/buy_train.rb @@ -13,9 +13,18 @@ def actions(entity) actions << 'pass' if actions.empty? actions << 'scrap_train' end + actions.delete('pass') if must_buy_train?(entity) actions end + def ebuy_president_can_contribute?(corporation) + super && president_may_contribute?(corporation) + end + + def president_may_contribute?(entity) + !can_finance?(entity) && super + end + def buyable_train_variants(train, entity) variants = super variants.select! { |t| @game.goods_train?(t[:name]) } if entity.type == :coal @@ -23,7 +32,9 @@ def buyable_train_variants(train, entity) end def other_trains(entity) - trains = super + return [] if can_finance?(entity) + + trains = super.reject { |t| t.owner.cash.negative? } trains.select! { |t| @game.goods_train?(t.name) } if entity.type == :coal trains end @@ -54,6 +65,12 @@ def scrap_header_text 'Trains to Surrender' end + def can_finance?(entity) + entity.trains.empty? && + needed_cash(entity) > buying_power(entity) && + (entity.owner == @game.share_pool || @round.bankrupting_corporations.include?(entity)) + end + def process_scrap_train(action) entity = action.entity train = action.train @@ -64,6 +81,11 @@ def process_scrap_train(action) entity.spend(surrender_cost(train), @game.bank) @game.depot.reclaim_train(train) end + + def process_buy_train(action) + super + action.train.operated = true + end end end end diff --git a/lib/engine/game/g_1837/step/dividend.rb b/lib/engine/game/g_1837/step/dividend.rb index b68abf8566..0371cc09ce 100644 --- a/lib/engine/game/g_1837/step/dividend.rb +++ b/lib/engine/game/g_1837/step/dividend.rb @@ -11,6 +11,12 @@ class Dividend < Engine::Step::Dividend DIVIDEND_TYPES = %i[payout half withhold].freeze include G1837::Step::MinorHalfPay + def auto_actions(entity) + return if !entity.corporation? || !entity.cash.negative? + + [Action::Dividend.new(entity, kind: 'withhold')] + end + def half(entity, revenue) amount = revenue / 2 { corporation: amount, per_share: payout_per_share(entity, amount) } diff --git a/lib/engine/game/g_1837/step/minor_half_pay.rb b/lib/engine/game/g_1837/step/minor_half_pay.rb index a06714303f..32b9a3ada1 100644 --- a/lib/engine/game/g_1837/step/minor_half_pay.rb +++ b/lib/engine/game/g_1837/step/minor_half_pay.rb @@ -17,12 +17,12 @@ def skip! revenue = @game.routes_revenue(routes) kind = if revenue.zero? - 'withhold' - elsif current_entity.minor? - 'payout' - else - 'half' - end + 'withhold' + elsif current_entity.minor? + 'payout' + else + 'half' + end process_dividend(Action::Dividend.new(current_entity, kind: kind)) end diff --git a/lib/engine/step/train.rb b/lib/engine/step/train.rb index 57030af297..b83b1f8ea7 100644 --- a/lib/engine/step/train.rb +++ b/lib/engine/step/train.rb @@ -65,6 +65,13 @@ def buy_train_action(action, entity = nil, borrow_from: nil) raise GameError, 'An entity cannot buy a train from itself' if train.owner == entity remaining = price - buying_power(entity) + if remaining.positive? && can_finance?(entity) + financed_cash = remaining + entity.cash += financed_cash + @log << "#{@game.bank.name} finances #{@game.format_currency(financed_cash)}" + remaining = 0 + end + if remaining.positive? && president_may_contribute?(entity, action.shell) check_for_cheapest_train(train) @@ -105,6 +112,8 @@ def buy_train_action(action, entity = nil, borrow_from: nil) @game.buy_train(entity, train, price) @game.phase.buying_train!(entity, train, source) + + action.entity.cash -= financed_cash if financed_cash pass! if !can_buy_train?(entity) && pass_if_cannot_buy_train?(entity) end @@ -247,6 +256,10 @@ def spend_minmax(entity, train) end end + def can_finance?(_entity) + false + end + private def face_value_ability?(entity) From 5e2c6f09dd7319dd13259df16b830a4ff9d8a6b3 Mon Sep 17 00:00:00 2001 From: Chris Rericha Date: Sun, 19 Jan 2025 23:08:17 -0500 Subject: [PATCH 15/21] [1837] Operation while in receivership --- lib/engine/game/g_1837/game.rb | 8 +++++++ lib/engine/game/g_1837/step/buy_train.rb | 2 +- .../game/g_1837/step/skip_receivership.rb | 21 +++++++++++++++++++ lib/engine/game/g_1837/step/token.rb | 2 ++ lib/engine/game/g_1837/step/track.rb | 2 ++ 5 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 lib/engine/game/g_1837/step/skip_receivership.rb diff --git a/lib/engine/game/g_1837/game.rb b/lib/engine/game/g_1837/game.rb index ae79835ccb..56309d1f3a 100644 --- a/lib/engine/game/g_1837/game.rb +++ b/lib/engine/game/g_1837/game.rb @@ -773,6 +773,14 @@ def sold_out_stock_movement(corporation) def operated_this_round?(entity) entity.operating_history.include?([@turn, @round.round_num]) end + + def acting_for_entity(entity) + if entity.corporation? && entity.type != :minor && entity.receivership? + return @players.find { |p| p.num_shares_of(entity).positive? } || @players.first + end + + super + end end end end diff --git a/lib/engine/game/g_1837/step/buy_train.rb b/lib/engine/game/g_1837/step/buy_train.rb index ccea9e1db6..76b4fde69d 100644 --- a/lib/engine/game/g_1837/step/buy_train.rb +++ b/lib/engine/game/g_1837/step/buy_train.rb @@ -68,7 +68,7 @@ def scrap_header_text def can_finance?(entity) entity.trains.empty? && needed_cash(entity) > buying_power(entity) && - (entity.owner == @game.share_pool || @round.bankrupting_corporations.include?(entity)) + (entity.receivership? || @round.bankrupting_corporations.include?(entity)) end def process_scrap_train(action) diff --git a/lib/engine/game/g_1837/step/skip_receivership.rb b/lib/engine/game/g_1837/step/skip_receivership.rb new file mode 100644 index 0000000000..4f17954428 --- /dev/null +++ b/lib/engine/game/g_1837/step/skip_receivership.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Engine + module Game + module G1837 + module Step + module SkipReceivership + def actions(entity) + return [] if entity.receivership? + + super + end + + def skip! + super unless current_entity.receivership? + end + end + end + end + end +end diff --git a/lib/engine/game/g_1837/step/token.rb b/lib/engine/game/g_1837/step/token.rb index 756c0d0fb8..a8459b9349 100644 --- a/lib/engine/game/g_1837/step/token.rb +++ b/lib/engine/game/g_1837/step/token.rb @@ -8,6 +8,8 @@ module Game module G1837 module Step class Token < Engine::Step::Token + include SkipReceivership + def actions(entity) return [] unless entity == current_entity return [] unless multiple_tokens?(entity) diff --git a/lib/engine/game/g_1837/step/track.rb b/lib/engine/game/g_1837/step/track.rb index 1f9bb9f5f0..777d8851c9 100644 --- a/lib/engine/game/g_1837/step/track.rb +++ b/lib/engine/game/g_1837/step/track.rb @@ -7,6 +7,8 @@ module Game module G1837 module Step class Track < Engine::Step::Track + include SkipReceivership + def lay_tile(action, extra_cost: 0, entity: nil, spender: nil) tile = action.tile case tile.name From 120090fd2eac4f09d2cfe6dffc0318137f383767 Mon Sep 17 00:00:00 2001 From: Chris Rericha Date: Sun, 19 Jan 2025 23:45:16 -0500 Subject: [PATCH 16/21] [1837] Fix method signature --- lib/engine/game/g_1837/game.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/engine/game/g_1837/game.rb b/lib/engine/game/g_1837/game.rb index 56309d1f3a..16048fc695 100644 --- a/lib/engine/game/g_1837/game.rb +++ b/lib/engine/game/g_1837/game.rb @@ -748,7 +748,7 @@ def blocking_token Token.new(@blocker) end - def token_graph_for_entity + def token_graph_for_entity(_entity) @token_graph ||= Graph.new(self, backtracking: true) end From 2cd5674db7ed5bec1196056ecf719b2e97d3778a Mon Sep 17 00:00:00 2001 From: Chris Rericha Date: Sat, 25 Jan 2025 00:28:25 -0500 Subject: [PATCH 17/21] [1837] Non-auctioned starting packet items stay intact until bought --- lib/engine/game/g_1837/entities.rb | 2 +- lib/engine/game/g_1837/game.rb | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/engine/game/g_1837/entities.rb b/lib/engine/game/g_1837/entities.rb index 5e3905b273..8654333ea9 100644 --- a/lib/engine/game/g_1837/entities.rb +++ b/lib/engine/game/g_1837/entities.rb @@ -323,7 +323,7 @@ module Entities sym: 'SD2', value: 120, desc: "Director's certificate of the minor company Kärntner Railway (SD2). The company " \ - "starts in Marburg (J16) with 90K starting capital.\n\n =Comes with the Mountain Railway " \ + "starts in Marburg (J16) with 90K starting capital.\n\nComes with the Mountain Railway " \ 'Karawanken Railway (J12) that has a value of 120K and a revenue of 25K.', color: :orange, meta: { start_packet: true }, diff --git a/lib/engine/game/g_1837/game.rb b/lib/engine/game/g_1837/game.rb index 16048fc695..5d5421634e 100644 --- a/lib/engine/game/g_1837/game.rb +++ b/lib/engine/game/g_1837/game.rb @@ -286,7 +286,7 @@ def initial_auction_companies def setup non_purchasable = @companies.flat_map do |c| - Array(c.meta['additional_companies']) + [c.meta['hidden'] ? c.id : nil] + [abilities(c, :acquire_company, time: 'any')&.company, c.meta['hidden'] ? c.id : nil] end.compact @companies.each { |company| company.owner = @bank unless non_purchasable.include?(company.id) } setup_mines @@ -623,6 +623,7 @@ def corporation_show_individual_reserved_shares? end def unowned_purchasable_companies(_entity) + @companies.select { |company| company.meta[:start_packet] } @companies.select { |c| c.owner == @bank } end From 5f5d9431f587313c6a75952c925bbdc060e7de6e Mon Sep 17 00:00:00 2001 From: Chris Rericha Date: Sat, 25 Jan 2025 00:56:59 -0500 Subject: [PATCH 18/21] [1837] Coal companies have no value at end game --- lib/engine/game/g_1837/game.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/engine/game/g_1837/game.rb b/lib/engine/game/g_1837/game.rb index 5d5421634e..614cbcbe98 100644 --- a/lib/engine/game/g_1837/game.rb +++ b/lib/engine/game/g_1837/game.rb @@ -641,6 +641,7 @@ def after_buy_company(player, company, _price) minor = minor_by_id(company.id) minor.owner = player float_minor!(minor) + company.value = 0 end abilities(company, :acquire_company) do |ability| From 5aac45a453cf9ec4515692683264e119a510354c Mon Sep 17 00:00:00 2001 From: Chris Rericha Date: Sat, 25 Jan 2025 19:17:37 -0500 Subject: [PATCH 19/21] [1837] Town upgrades --- lib/engine/game/g_1837/game.rb | 18 ++++++++++++++++++ lib/engine/game/g_1837/map.rb | 3 +++ lib/engine/game/g_1837/step/track.rb | 6 ++++++ 3 files changed, 27 insertions(+) diff --git a/lib/engine/game/g_1837/game.rb b/lib/engine/game/g_1837/game.rb index 614cbcbe98..ff4bb83bec 100644 --- a/lib/engine/game/g_1837/game.rb +++ b/lib/engine/game/g_1837/game.rb @@ -754,8 +754,26 @@ def token_graph_for_entity(_entity) @token_graph ||= Graph.new(self, backtracking: true) end + def upgrades_to?(from, to, special = false, selected_company: nil) + return yellow_town_tile_upgrades_to?(from, to) if from.color == :yellow && !from.towns.empty? + + super + end + + def yellow_town_tile_upgrades_to?(from, to) + # honors pre-existing track? + return false unless from.paths_are_subset_of?(to.paths) + + if from.towns.one? + self.class::YELLOW_SINGLE_TOWN_UPGRADES.include?(to.name) + else + self.class::YELLOW_DOUBLE_TOWN_UPGRADES.include?(to.name) + end + end + def legal_tile_rotation?(entity, hex, tile) return tile.rotation == 5 if tile.name == '436' + return false if !hex.tile.towns.empty? && !(hex.tile.exits - tile.towns.first.exits).empty? super end diff --git a/lib/engine/game/g_1837/map.rb b/lib/engine/game/g_1837/map.rb index 423837e0d6..8da753756e 100644 --- a/lib/engine/game/g_1837/map.rb +++ b/lib/engine/game/g_1837/map.rb @@ -227,6 +227,9 @@ module Map }, }.freeze + YELLOW_SINGLE_TOWN_UPGRADES = %w[410 411 412 413 414 415 416 417 418 419 420 421 422 423 424].freeze + YELLOW_DOUBLE_TOWN_UPGRADES = %w[87 88 204].freeze + LOCATION_NAMES = { 'C11' => 'Prague', 'C23' => 'Krakow', diff --git a/lib/engine/game/g_1837/step/track.rb b/lib/engine/game/g_1837/step/track.rb index 777d8851c9..f36370a7ef 100644 --- a/lib/engine/game/g_1837/step/track.rb +++ b/lib/engine/game/g_1837/step/track.rb @@ -26,6 +26,12 @@ def lay_tile(action, extra_cost: 0, entity: nil, spender: nil) end super end + + def check_track_restrictions!(entity, old_tile, new_tile) + return if @game.class::YELLOW_DOUBLE_TOWN_UPGRADES.include?(new_tile.name) + + super + end end end end From f8a9380518d959b0452453cbe2cb8202e0ff0a43 Mon Sep 17 00:00:00 2001 From: Chris Rericha Date: Sat, 25 Jan 2025 21:10:26 -0500 Subject: [PATCH 20/21] [Core] Placing tokens can affect both graphs, so simplify code by always clearing both --- lib/engine/game/base.rb | 3 --- lib/engine/game/g_1832/step/track.rb | 4 ++-- lib/engine/game/g_1841/game.rb | 4 ---- lib/engine/game/g_1850/step/track.rb | 4 ++-- lib/engine/game/g_1858/game.rb | 4 ---- .../game/g_system18/map_twisting_tracks_customization.rb | 2 +- lib/engine/step/tokener.rb | 2 +- 7 files changed, 6 insertions(+), 17 deletions(-) diff --git a/lib/engine/game/base.rb b/lib/engine/game/base.rb index c0ef175013..c0ad45966c 100644 --- a/lib/engine/game/base.rb +++ b/lib/engine/game/base.rb @@ -1667,9 +1667,6 @@ def clear_graph def clear_graph_for_entity(entity) graph_for_entity(entity).clear - end - - def clear_token_graph_for_entity(entity) token_graph_for_entity(entity).clear end diff --git a/lib/engine/game/g_1832/step/track.rb b/lib/engine/game/g_1832/step/track.rb index cd63d34512..30da09871b 100644 --- a/lib/engine/game/g_1832/step/track.rb +++ b/lib/engine/game/g_1832/step/track.rb @@ -59,12 +59,12 @@ def buy_coal_token(corporation) log_message += "#{@game.coal_token_counter} Coal tokens left in the game" @log << log_message corporation.coal_token = true - @game.clear_token_graph_for_entity(corporation) + @game.clear_graph_for_entity(corporation) end def hex_neighbors(entity, hex) connected = super - @game.clear_token_graph_for_entity(entity) if entity.tokens.none?(&:city) + @game.clear_graph_for_entity(entity) if entity.tokens.none?(&:city) connected end diff --git a/lib/engine/game/g_1841/game.rb b/lib/engine/game/g_1841/game.rb index a78075e06f..c53c8a3bf6 100644 --- a/lib/engine/game/g_1841/game.rb +++ b/lib/engine/game/g_1841/game.rb @@ -380,10 +380,6 @@ def clear_graph_for_entity(_entity) @border_paths = nil end - def clear_token_graph_for_entity(entity) - clear_graph_for_entity(entity) - end - def event_phase4_regions! modify_regions(2, false) modify_regions(4, true) diff --git a/lib/engine/game/g_1850/step/track.rb b/lib/engine/game/g_1850/step/track.rb index 8e1cb2bfbc..a208b0b685 100644 --- a/lib/engine/game/g_1850/step/track.rb +++ b/lib/engine/game/g_1850/step/track.rb @@ -70,12 +70,12 @@ def buy_mesabi_token(corporation) log_message += "#{@game.mesabi_token_counter} Mesabi tokens left in the game" @log << log_message corporation.mesabi_token = true - @game.clear_token_graph_for_entity(corporation) + @game.clear_graph_for_entity(corporation) end def hex_neighbors(entity, hex) connected = super - @game.clear_token_graph_for_entity(entity) if entity.tokens.none?(&:city) + @game.clear_graph_for_entity(entity) if entity.tokens.none?(&:city) connected end diff --git a/lib/engine/game/g_1858/game.rb b/lib/engine/game/g_1858/game.rb index c7181dee9a..b98fa5c053 100644 --- a/lib/engine/game/g_1858/game.rb +++ b/lib/engine/game/g_1858/game.rb @@ -165,10 +165,6 @@ def clear_graph_for_entity(_entity) @graph_metre.clear end - def clear_token_graph_for_entity(entity) - clear_graph_for_entity(entity) - end - def init_round if option_quick_start? quick_start diff --git a/lib/engine/game/g_system18/map_twisting_tracks_customization.rb b/lib/engine/game/g_system18/map_twisting_tracks_customization.rb index 634a88f6b1..82e8df4fe0 100644 --- a/lib/engine/game/g_system18/map_twisting_tracks_customization.rb +++ b/lib/engine/game/g_system18/map_twisting_tracks_customization.rb @@ -333,7 +333,7 @@ def map_twisting_tracks_post_lay_tile(entity, tile) tile.cities.first.place_token(entity, tc, check_tokenable: false) @log << "#{entity.name} places a Ticket Counter on tile" - clear_token_graph_for_entity(entity) + clear_graph_for_entity(entity) end # allow 2nd token on hex if it's a different type diff --git a/lib/engine/step/tokener.rb b/lib/engine/step/tokener.rb index b2280f03fb..9f97980d4f 100644 --- a/lib/engine/step/tokener.rb +++ b/lib/engine/step/tokener.rb @@ -97,7 +97,7 @@ def place_token(entity, city, token, connected: true, extra_action: false, end @round.tokened = true unless extra_action - @game.clear_token_graph_for_entity(entity) + @game.clear_graph_for_entity(entity) end def pay_token_cost(entity, cost, _city) From ea28db06c10dcd34907f3b2a1121a389d0b37613 Mon Sep 17 00:00:00 2001 From: Chris Rericha Date: Sun, 26 Jan 2025 16:30:43 -0500 Subject: [PATCH 21/21] [1837] To alpha --- assets/app/view/welcome.rb | 4 +--- lib/engine/game/g_1837/meta.rb | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/assets/app/view/welcome.rb b/assets/app/view/welcome.rb index 0e9e967e4b..457b9f2404 100644 --- a/assets/app/view/welcome.rb +++ b/assets/app/view/welcome.rb @@ -16,9 +16,7 @@ def render def render_notification message = <<~MESSAGE -

18RoyalGorge is now in beta.

- -

1858 Switzerland is in alpha.

+

1837 is now in alpha.

Report bugs and make feature requests on GitHub.

MESSAGE diff --git a/lib/engine/game/g_1837/meta.rb b/lib/engine/game/g_1837/meta.rb index 60b5f2cd6e..02ee4d9cd2 100644 --- a/lib/engine/game/g_1837/meta.rb +++ b/lib/engine/game/g_1837/meta.rb @@ -8,10 +8,10 @@ module G1837 module Meta include Game::Meta - DEV_STAGE = :prealpha + DEV_STAGE = :alpha GAME_DESIGNER = 'Leonhard Orgler' - GAME_LOCATION = 'Austria-Hungary' + GAME_LOCATION = 'Eastern Europe' GAME_PUBLISHER = :all_aboard_games GAME_RULES_URL = 'https://boardgamegeek.com/filepage/238953' GAME_INFO_URL = 'https://github.com/tobymao/18xx/wiki/1837'