diff --git a/Makefile b/Makefile index 669275f34d..934aab75b1 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ CONTAINER_COMPOSE ?= $(CONTAINER_ENGINE)-compose CONTAINER_ENGINE ?= docker clean: - sudo rm -rfv build/ public/assets/*.js public/assets/*.js.gz public/assets/version.json + sudo rm -rfv build/ public/assets/*.js public/assets/*.js.map public/assets/*.js.gz public/assets/version.json cleandeps: sudo rm -rfv public/assets/deps.js diff --git a/api.rb b/api.rb index aaff94a426..296193e886 100644 --- a/api.rb +++ b/api.rb @@ -77,7 +77,7 @@ class Api < Roda } end - ASSETS = Assets.new(precompiled: PRODUCTION) + ASSETS = Assets.new(precompiled: PRODUCTION, source_maps: !PRODUCTION) Bus.configure diff --git a/assets/app/view/game/dividend.rb b/assets/app/view/game/dividend.rb index 03adb31595..eaee593722 100644 --- a/assets/app/view/game/dividend.rb +++ b/assets/app/view/game/dividend.rb @@ -35,7 +35,7 @@ def render @step.respond_to?(:dividend_name) ? @step.dividend_name(type) : type end - corp_income = option[:corporation] + option[:divs_to_corporation] + corp_income = option[:corporation] + option[:divs_to_corporation] + @step.total_subsidy direction = if (new_share_price = entity.share_price) && option[:share_direction] diff --git a/lib/assets.rb b/lib/assets.rb index d00da96158..d85fbbff6c 100644 --- a/lib/assets.rb +++ b/lib/assets.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require 'json' require 'opal' require 'snabberb' require 'tempfile' @@ -12,7 +13,40 @@ class Assets OUTPUT_BASE = 'public' PIN_DIR = '/pinned/' - def initialize(compress: false, gzip: false, cache: true, precompiled: false) + class SourceMap + def initialize + @source_map = { + 'version' => 3, + 'sections' => [], + } + @current_line = 0 + end + + def append(file_contents, source_map_file) + @source_map['sections'].append({ + 'offset' => { 'line' => @current_line, 'column' => 0 }, + 'map' => JSON.parse(File.read(source_map_file)), + }) + @current_line += file_contents.lines.count + 1 + end + + def extend(file_contents, source_map_file) + file_source_map = JSON.parse(File.read(source_map_file)) + file_source_map['sections'].each do |section| + @source_map['sections'].append({ + 'offset' => { 'line' => @current_line + section['offset']['line'], 'column' => 0 }, + 'map' => section['map'], + }) + end + @current_line += file_contents.lines.count + 1 + end + + def to_json(*args) + @source_map.to_json(*args) + end + end + + def initialize(compress: false, gzip: false, cache: true, precompiled: false, source_maps: false) @build_path = 'build' @out_path = OUTPUT_BASE + '/assets' @root_path = '/assets' @@ -25,6 +59,7 @@ def initialize(compress: false, gzip: false, cache: true, precompiled: false) @compress = compress @gzip = gzip @precompiled = precompiled + @source_maps = source_maps end def context(titles) @@ -97,8 +132,8 @@ def builds(titles = []) **game_builds(:all), } else - @_opal ||= compile_lib('opal') - @_deps ||= compile_lib('deps', 'assets') + @_opal ||= compile_lib('opal', 'opal') + @_deps ||= compile_lib('deps_only', 'deps', 'assets') @_engine ||= compile('engine', 'lib', 'engine') @_app ||= compile('app', 'assets/app', '') @@ -163,9 +198,17 @@ def combine(titles = []) @_combined.include?(key) ? next : @_combined.add(key) - source = build['files'].map { |file| File.read(file).to_s }.join("\n") + source_map = SourceMap.new + source = build['files'].map do |filepath| + file = File.read(filepath).to_s + source_map.extend(file, filepath + '.map') if @source_maps + file + end.join("\n") + source += "\n//# sourceMappingURL=#{build['path'].delete_prefix('public')}.map\n" if @source_maps + source = compress(key, source) if @compress File.write(build['path'], source) + File.write(build['path'] + '.map', source_map.to_json) if @source_maps next if !@gzip || build['path'] == @server_path @@ -182,13 +225,14 @@ def combine(titles = []) [@deps_path, @main_path, *game_paths] end - def compile_lib(name, *append_paths) + def compile_lib(output_name, name, *append_paths) builder = Opal::Builder.new append_paths.each { |ap| builder.append_paths(ap) } - path = "#{@out_path}/#{name}.js" - if !@cache || !File.exist?(path) || path == @deps_path + path = "#{@out_path}/#{output_name}.js" + if !@cache || !File.exist?(path) time = Time.now File.write(path, builder.build(name)) + File.write("#{path}.map", builder.source_map.map.to_json) if @source_maps puts "Compiling #{name} - #{Time.now - time}" end path @@ -214,17 +258,22 @@ def compile(name, lib_path, ns = nil, game: nil) time = Time.now File.write(opts[:js_path], compiler.compile) + File.write(opts[:js_path] + '.map', compiler.source_map.map.to_json) if @source_maps puts "Compiling #{file} - #{Time.now - time}" end + source_map = SourceMap.new source = metadata.map do |_file, opts| - File.read(opts[:js_path]).to_s + file = File.read(opts[:js_path]) + source_map.append(file, opts[:js_path] + '.map') if @source_maps + file end.join("\n") opal_load = game ? "engine/game/#{game}" : name source += "\nOpal.load('#{opal_load}')" File.write(output, source) + File.write(output + '.map', source_map.to_json) if @source_maps output end diff --git a/lib/engine/action/run_routes.rb b/lib/engine/action/run_routes.rb index b7d7fcc268..e4bb9a5d36 100644 --- a/lib/engine/action/run_routes.rb +++ b/lib/engine/action/run_routes.rb @@ -6,12 +6,13 @@ module Engine module Action class RunRoutes < Base - attr_reader :routes, :extra_revenue + attr_reader :routes, :extra_revenue, :subsidy - def initialize(entity, routes:, extra_revenue: 0) + def initialize(entity, routes:, extra_revenue: 0, subsidy: 0) super(entity) @routes = routes @extra_revenue = extra_revenue + @subsidy = subsidy end def self.h_to_args(h, game) @@ -41,6 +42,7 @@ def self.h_to_args(h, game) { routes: routes, extra_revenue: h['extra_revenue'], + subsidy: h['subsidy'], } end @@ -62,6 +64,7 @@ def args_to_h { 'routes' => routes, 'extra_revenue' => extra_revenue, + 'subsidy' => subsidy, } end end diff --git a/lib/engine/game/base.rb b/lib/engine/game/base.rb index 0ba169774b..ccf8bf9ac5 100644 --- a/lib/engine/game/base.rb +++ b/lib/engine/game/base.rb @@ -810,9 +810,9 @@ def process_action(action, add_auto_actions: false, validate_auto_actions: false @last_processed_action = action.id self - rescue StandardError => e - rescue_exception(e, action) - self + # rescue StandardError => e + # rescue_exception(e, action) + # self end def process_single_action(action) @@ -838,8 +838,8 @@ def process_single_action(action) transition_to_next_round! end end - rescue Engine::GameError => e - rescue_exception(e, action) + # rescue Engine::GameError => e + # rescue_exception(e, action) end def rescue_exception(e, action) diff --git a/lib/engine/game/g_1822/step/dividend.rb b/lib/engine/game/g_1822/step/dividend.rb index ded272902b..1a1fdbe000 100644 --- a/lib/engine/game/g_1822/step/dividend.rb +++ b/lib/engine/game/g_1822/step/dividend.rb @@ -41,15 +41,13 @@ def dividend_options(entity) if extra_train train = find_extra_train(entity) extra_train_revenue = routes.find { |r| r.train == train }.revenue - extra_train_payout = send(@extra_train_choice, entity, extra_train_revenue, 0) + extra_train_payout = send(@extra_train_choice, entity, extra_train_revenue) revenue = total_revenue - extra_train_revenue else revenue = total_revenue end - subsidy = @game.routes_subsidy(routes) - total_revenue += subsidy dividend_types.to_h do |type| - payout = send(type, entity, revenue, subsidy) + payout = send(type, entity, revenue) if extra_train payout[:corporation] += extra_train_payout[:corporation] payout[:per_share] += extra_train_payout[:per_share] @@ -67,9 +65,9 @@ def find_extra_train(entity) revenue.positive? && revenue != @game.routes_revenue(routes) ? train : nil end - def half(entity, revenue, subsidy) + def half(entity, revenue) withheld = half_pay_withhold_amount(entity, revenue) - { corporation: withheld + subsidy, per_share: payout_per_share(entity, revenue - withheld) } + { corporation: withheld, per_share: payout_per_share(entity, revenue - withheld) } end def half_pay_withhold_amount(entity, revenue) @@ -83,7 +81,7 @@ def holder_for_corporation(entity) def log_run_payout(entity, kind, revenue, subsidy, _action, payout) @log << "#{entity.name} runs for #{@game.format_currency(revenue)} and pays half" if kind == 'half' - withhold = payout[:corporation] - subsidy + withhold = payout[:corporation] if withhold.positive? @log << "#{entity.name} withholds #{@game.format_currency(withhold)}" elsif payout[:per_share].zero? @@ -92,8 +90,8 @@ def log_run_payout(entity, kind, revenue, subsidy, _action, payout) @log << "#{entity.name} earns subsidy of #{@game.format_currency(subsidy)}" if subsidy.positive? end - def payout(entity, revenue, subsidy) - { corporation: subsidy, per_share: payout_per_share(entity, revenue) } + def payout(entity, revenue) + { corporation: 0, per_share: payout_per_share(entity, revenue) } end def payout_shares(entity, revenue) @@ -138,8 +136,8 @@ def process_dividend(action) @round.routes = [] log_run_payout(entity, kind, revenue, subsidy, action, payout) - @game.bank.spend(payout[:corporation], entity) if payout[:corporation].positive? - payout_shares(entity, revenue + subsidy - payout[:corporation]) if payout[:per_share].positive? + payout_corporation(payout[:corporation] + subsidy, entity) + payout_shares(entity, revenue - payout[:corporation]) if payout[:per_share].positive? change_share_price(entity, payout) pass! @@ -171,8 +169,8 @@ def share_price_change(entity, revenue = 0) end end - def withhold(_entity, revenue, subsidy) - { corporation: revenue + subsidy, per_share: 0 } + def withhold(_entity, revenue) + { corporation: revenue, per_share: 0 } end end end diff --git a/lib/engine/game/g_1822_nrs/map.rb b/lib/engine/game/g_1822_nrs/map.rb index 17f01cbfdd..5473aa0890 100644 --- a/lib/engine/game/g_1822_nrs/map.rb +++ b/lib/engine/game/g_1822_nrs/map.rb @@ -145,7 +145,7 @@ module Map 'G20' => 'Blackpool', 'G22' => 'Liverpool', 'G24' => 'Chester', - 'G28' => 'Shrewbury', + 'G28' => 'Shrewsbury', 'H1' => 'Aberdeen', 'H3' => 'Dunfermline', 'H5' => 'Edinburgh', diff --git a/lib/engine/game/g_1825/step/dividend.rb b/lib/engine/game/g_1825/step/dividend.rb index 8090dd2722..4f9f7f54b3 100644 --- a/lib/engine/game/g_1825/step/dividend.rb +++ b/lib/engine/game/g_1825/step/dividend.rb @@ -57,7 +57,7 @@ def process_dividend(action) @round.receivership_loan = 0 @round.routes = [] - log_run_payout(entity, kind, revenue, action, payout) + log_run_payout(entity, kind, revenue, 0, action, payout) payout_corporation(payout[:corporation], entity) diff --git a/lib/engine/game/g_1840/step/dividend.rb b/lib/engine/game/g_1840/step/dividend.rb index 65abc0d444..e17e6e2c33 100644 --- a/lib/engine/game/g_1840/step/dividend.rb +++ b/lib/engine/game/g_1840/step/dividend.rb @@ -95,7 +95,7 @@ def payout_corporation(amount, entity) major.spend(to_pay, @game.bank) end - def log_run_payout(entity, kind, revenue, action, payout) + def log_run_payout(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 diff --git a/lib/engine/game/g_1847_ae/entities.rb b/lib/engine/game/g_1847_ae/entities.rb index 670708cdc4..e271b70bab 100644 --- a/lib/engine/game/g_1847_ae/entities.rb +++ b/lib/engine/game/g_1847_ae/entities.rb @@ -42,7 +42,7 @@ module Entities revenue: 30, min_price: 100, max_price: 200, - desc: 'Revenue increases to 50M when a tile is laid in E9. '\ + desc: 'Revenue increases to 50M when a tile is laid in E9 (the tile may be laid only in Phase 5 or later). '\ 'May be sold to a corporation for 100 to 200M. '\ 'Never closes.', sym: 'R', diff --git a/lib/engine/game/g_1847_ae/step/buy_single_train_of_type.rb b/lib/engine/game/g_1847_ae/step/buy_single_train_of_type.rb index f6c7601fdd..bf5bf853d7 100644 --- a/lib/engine/game/g_1847_ae/step/buy_single_train_of_type.rb +++ b/lib/engine/game/g_1847_ae/step/buy_single_train_of_type.rb @@ -16,11 +16,11 @@ def buyable_trains(entity) end def process_buy_train(action) - from_depot = action.train.from_depot? + from_discard = @depot.discarded.include?(action.train) super lfk = @game.lfk - return if @game.train_bought_this_round || !lfk.floated? || !from_depot + return if @game.train_bought_this_round || !lfk.floated? || from_discard lfk_owner = lfk.owner if lfk_owner.player? diff --git a/lib/engine/game/g_1850_jr/meta.rb b/lib/engine/game/g_1850_jr/meta.rb index 77657bfad5..2fd2ab955a 100644 --- a/lib/engine/game/g_1850_jr/meta.rb +++ b/lib/engine/game/g_1850_jr/meta.rb @@ -8,7 +8,7 @@ module G1850Jr module Meta include Game::Meta - DEV_STAGE = :alpha + DEV_STAGE = :beta GAME_DESIGNER = 'Gabriele Callari, Fabio Pellegrino' GAME_LOCATION = 'Sicily' diff --git a/lib/engine/game/g_1856/step/dividend.rb b/lib/engine/game/g_1856/step/dividend.rb index 36a194add1..b3a7255482 100644 --- a/lib/engine/game/g_1856/step/dividend.rb +++ b/lib/engine/game/g_1856/step/dividend.rb @@ -12,13 +12,13 @@ class Dividend < Engine::Step::Dividend def actions(entity) # National must withhold if it never owned a permanent return [] if entity.corporation? && entity == @game.national && !@game.national_ever_owned_permanent + return [] if entity.company? || routes.empty? - super + Engine::Step::Dividend::ACTIONS end def dividend_options(entity) - penalty = @round.interest_penalty[entity] || 0 - revenue = @game.routes_revenue(routes) - penalty + revenue = total_revenue dividend_types.to_h do |type| payout = send(type, entity, revenue) @@ -32,37 +32,6 @@ def payout_per_share(entity, revenue) (revenue / entity.total_shares.to_f) end - def process_dividend(action) - entity = action.entity - penalty = @round.interest_penalty[entity] || 0 - revenue = @game.routes_revenue(routes) - penalty - kind = action.kind.to_sym - payout = dividend_options(entity)[kind] - - rust_obsolete_trains!(entity) - - 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(entity, kind, revenue, action, payout) - - @game.bank.spend(payout[:corporation], entity) if payout[:corporation].positive? - - payout_shares(entity, revenue - payout[:corporation]) if payout[:per_share].positive? - - change_share_price(entity, payout) - - pass! - end - def share_price_change(entity, _revenue) # Share price of national does not change until it owns a permanent return {} if entity == @game.national && !@game.national_ever_owned_permanent @@ -75,7 +44,11 @@ def holder_for_corporation(_entity) @game.share_pool end - def log_run_payout(entity, kind, revenue, action, payout) + def total_revenue + super - (@round.interest_penalty[@round.current_operator] || 0) + end + + def log_run_payout(entity, kind, revenue, _subsidy, action, payout) if (@round.interest_penalty[entity] || 0).positive? @log << "#{entity.name} deducts #{@game.format_currency(@round.interest_penalty[entity])}"\ ' for unpaid interest' diff --git a/lib/engine/game/g_1858_switzerland/game.rb b/lib/engine/game/g_1858_switzerland/game.rb index 528d775967..6916f64d3c 100644 --- a/lib/engine/game/g_1858_switzerland/game.rb +++ b/lib/engine/game/g_1858_switzerland/game.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require_relative 'entities' +require_relative 'graph' require_relative 'map' require_relative 'meta' require_relative 'tiles' @@ -15,6 +16,7 @@ class Game < G1858::Game include Map include Tiles + GRAPH_CLASS = G1858Switzerland::Graph CURRENCY_FORMAT_STR = '%ssfr' BANK_CASH = 8_000 @@ -122,6 +124,170 @@ def setup super @phase4_train_trigger = PHASE4_TRAINS_RUST end + + def maybe_rust_wounded_trains!(grey_trains_bought, purchased_train) + obsolete_trains!(%w[6H 3M], purchased_train) if grey_trains_bought == PHASE4_TRAINS_OBSOLETE + rust_wounded_trains!(%w[4H 2M], purchased_train) if grey_trains_bought == PHASE3_TRAINS_RUST + rust_wounded_trains!(%w[6H 3M], purchased_train) if grey_trains_bought == PHASE4_TRAINS_RUST + end + + def obsolete_trains!(train_names, purchased_train) + trains.select { |train| train_names.include?(train.name) } + .each { |train| train.obsolete_on = purchased_train.sym } + rust_trains!(purchased_train, purchased_train.owner) + end + + def closure_round(round_num) + G1858Switzerland::Round::Closure.new(self, [ + G1858::Step::ExchangeApproval, + G1858::Step::HomeToken, + G1858::Step::PrivateClosure, + ], round_num: round_num) + end + + BONUS_HEXES = { + north: %w[C4 D3 E2 H1 I2], + south: %w[H15 I16], + east: %w[L5 L7], + west: %w[A14 B7], + revenue_ns: 'B16', + revenue_ew: 'L16', + }.freeze + + def revenue_for(route, stops) + super + north_south_bonus(route, stops) + east_west_bonus(route, stops) + end + + def private_colors_available(phase) + if phase.status.include?('yellow_privates') + %i[yellow] + elsif phase.status.include?('green_privates') + %i[yellow green] + elsif phase.status.include?('all_privates') + %i[yellow green lightblue] + elsif phase.status.include?('blue_privates') + %i[lightblue] + else + [] + end + end + + MOUNTAIN_RAILWAY_TILE = 'X28' + MOUNTAIN_RAILWAY_ASSIGNMENT = '+40' + MOUNTAIN_RAILWAY_BONUS = 40 + ASSIGNMENT_TOKENS = { + MOUNTAIN_RAILWAY_ASSIGNMENT => '/icons/1858_switzerland/mountain.svg', + }.freeze + + def submit_revenue_str(routes, _show_subsidy) + mountain_revenue = mountain_bonus(current_entity, routes) + return super if mountain_revenue.zero? + + train_revenue = routes_revenue(routes) + "#{format_revenue_currency(train_revenue)} + " \ + "#{format_revenue_currency(mountain_revenue)} mountain railway bonus" + end + + def extra_revenue(entity, routes) + mountain_bonus(entity, routes) + end + + def mountain_bonus(entity, routes) + return 0 if routes.empty? + + mountain_railway_built?(entity) ? MOUNTAIN_RAILWAY_BONUS : 0 + end + + def all_potential_upgrades(tile, tile_manifest: false, selected_company: nil) + return super unless mountain_hex?(tile) + + super + Array(@tiles.find { |t| t.name == MOUNTAIN_RAILWAY_TILE }) + end + + def upgrades_to?(from, to, special, selected_company: nil) + valid = super + return valid unless current_entity.corporation? + return valid if mountain_railway_built?(current_entity) + return valid unless mountain_hex?(from) + + valid || to.name == MOUNTAIN_RAILWAY_TILE + end + + def after_lay_tile(_hex, tile, entity) + return unless tile.name == MOUNTAIN_RAILWAY_TILE + + entity.assign!(MOUNTAIN_RAILWAY_ASSIGNMENT) + end + + # This method is called to remove some private railways from 1858 when + # there are two players. This does not happen in 18CH. + def setup_unbuyable_privates; end + + def gotthard + @gotthard ||= hex_by_id('H11') + end + + def fob_minor + @fob_minor ||= minor_by_id('FOB') + end + + def gb_minor + @gb_minor ||= minor_by_id('GB') + end + + def home_hex?(operator, hex, gauge = nil) + home_hex = super + return home_hex if !home_hex || hex != gotthard + + # Gotthard (H11) is different from other hexes. A public company + # doesn't just to have to have a connection to anywhere on the hex to + # be able to absorb a private railway. To absorb the GB private it + # needs to have a connection to the broad gauge track, to absorb the + # FOB it needs to have a connection to the metre (narrow) gauge track. + (operator == fob_minor && gauge == :narrow) || + (operator == gb_minor && gauge == :broad) + end + + private + + def hexes_by_id(coordinates) + coordinates.map { |coord| hex_by_id(coord) } + end + + def r2r_bonus(route, stops, zone1, zone2, bonus) + @bonus_nodes ||= { + north: hexes_by_id(BONUS_HEXES[:north]).map(&:tile).flat_map(&:offboards), + south: hexes_by_id(BONUS_HEXES[:south]).map(&:tile).flat_map(&:offboards), + east: hexes_by_id(BONUS_HEXES[:east]).map(&:tile).flat_map(&:offboards), + west: hexes_by_id(BONUS_HEXES[:west]).map(&:tile).flat_map(&:offboards), + } + @bonus_revenue ||= { + north_south: hex_by_id(BONUS_HEXES[:revenue_ns]).tile.offboards.first, + east_west: hex_by_id(BONUS_HEXES[:revenue_ew]).tile.offboards.first, + } + return 0 unless stops.intersect?(@bonus_nodes[zone1]) + return 0 unless stops.intersect?(@bonus_nodes[zone2]) + + @bonus_revenue[bonus].route_revenue(@phase, route.train) + end + + def north_south_bonus(route, stops) + r2r_bonus(route, stops, :north, :south, :north_south) + end + + def east_west_bonus(route, stops) + r2r_bonus(route, stops, :east, :west, :east_west) + end + + def mountain_hex?(tile) + tile.color == :white && tile.upgrades.any? { |u| u.cost == 120 } + end + + def mountain_railway_built?(entity) + return false unless entity.corporation? + + entity.assignments.key?(MOUNTAIN_RAILWAY_ASSIGNMENT) + end end end end diff --git a/lib/engine/game/g_1858_switzerland/graph.rb b/lib/engine/game/g_1858_switzerland/graph.rb new file mode 100644 index 0000000000..431e0c23e8 --- /dev/null +++ b/lib/engine/game/g_1858_switzerland/graph.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require_relative '../g_1858/graph' + +module Engine + module Game + module G1858Switzerland + class Graph < G1858::Graph + private + + def path_node(path, entity) + node = G1858::Part::PathNode.new(path) + return node unless path.hex == @game.gotthard + + # The pre-printed Gotthard hex is a special case. It is a home hex + # for two private railways (FOB and GB) and it has two paths, one + # broad gauge and one metre (narrow) gauge. FOB is only allowed to + # trace routes from this hex using the metre gauge path, and GB can + # only use the broad gauge path. + return node if (path.track == :broad && entity == @game.gb_minor) || + (path.track == :narrow && entity == @game.fob_minor) + end + end + end + end +end diff --git a/lib/engine/game/g_1858_switzerland/round/closure.rb b/lib/engine/game/g_1858_switzerland/round/closure.rb new file mode 100644 index 0000000000..5cc2dde1c4 --- /dev/null +++ b/lib/engine/game/g_1858_switzerland/round/closure.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require_relative '../../g_1858/round/closure' + +module Engine + module Game + module G1858Switzerland + module Round + class Closure < G1858::Round::Closure + private + + # Is this the final private closure round? + def last_pcr? + @game.phase.name != '5' + end + + def companies + companies = @game.companies.reject(&:closed?) + return companies if last_pcr? + + companies.reject { |company| company.color == :lightblue } + end + + def minors + minors = @game.minors.reject(&:closed?) + return minors if last_pcr? + + minors.reject { |minor| minor.color == :lightblue } + end + end + end + end + end +end diff --git a/lib/engine/game/g_1860/step/dividend.rb b/lib/engine/game/g_1860/step/dividend.rb index 90ab75be56..b2f2385e65 100644 --- a/lib/engine/game/g_1860/step/dividend.rb +++ b/lib/engine/game/g_1860/step/dividend.rb @@ -11,52 +11,17 @@ module Step class Dividend < Engine::Step::Dividend def dividend_options(entity) revenue = @game.routes_revenue(routes) - subsidy = @game.routes_subsidy(routes) dividend_types.to_h do |type| - payout = send(type, entity, revenue, subsidy) + payout = send(type, entity, revenue) payout[:divs_to_corporation] = 0 [type, payout.merge(share_price_change(entity, payout[:per_share].positive? ? revenue : 0))] end end def process_dividend(action) - entity = action.entity - revenue = @game.routes_revenue(routes) - subsidy = @game.routes_subsidy(routes) - kind = action.kind.to_sym - payout = dividend_options(entity)[kind] - - entity.operating_history[[@game.turn, @round.round_num]] = OperatingInfo.new( - routes, - (@game.insolvent?(entity) ? @game.actions.last : action), - revenue, - @round.laid_hexes - ) - - entity.trains.each { |train| train.operated = true } unless @game.insolvent?(entity) - - @round.routes = [] - - log_run_payout(entity, kind, revenue, subsidy, action, payout) - @game.bank.spend(payout[:corporation], entity) if payout[:corporation].positive? - payout_shares(entity, revenue) if payout[:per_share].positive? - change_share_price(entity, payout) + super @game.check_bank_broken! - pass! - @game.check_bankruptcy!(entity) - end - - def log_run_payout(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[:per_share].zero? && payout[:corporation].zero? - @log << "#{entity.name} does not run" - elsif payout[:per_share].zero? - @log << "#{entity.name} withholds #{@game.format_currency(revenue)}" - end - @log << "#{entity.name} earns subsidy of #{@game.format_currency(subsidy)}" if subsidy.positive? + @game.check_bankruptcy!(action.entity) end def share_price_change(entity, revenue) @@ -78,12 +43,12 @@ def share_price_change(entity, revenue) end end - def withhold(_entity, revenue, subsidy) - { corporation: revenue + subsidy, per_share: 0 } + def withhold(_entity, revenue) + { corporation: revenue, per_share: 0 } end - def payout(entity, revenue, subsidy) - { corporation: subsidy, per_share: payout_per_share(entity, revenue) } + def payout(entity, revenue) + { corporation: 0, per_share: payout_per_share(entity, revenue) } end def payout_shares(entity, revenue) diff --git a/lib/engine/game/g_1861/map.rb b/lib/engine/game/g_1861/map.rb index 46b4bffd17..9323ac9953 100644 --- a/lib/engine/game/g_1861/map.rb +++ b/lib/engine/game/g_1861/map.rb @@ -138,7 +138,7 @@ module Map 'H8' => 'Moscow', 'H10' => 'Tula', 'H18' => 'Yuzovka', - 'I5' => 'Yaroslav', + 'I5' => 'Yaroslavl', 'I13' => 'Voronezh', 'I17' => 'Lugansk', 'I19' => 'Rostov', diff --git a/lib/engine/game/g_1862/step/dividend.rb b/lib/engine/game/g_1862/step/dividend.rb index 0eb9c98afc..92eee1c33e 100644 --- a/lib/engine/game/g_1862/step/dividend.rb +++ b/lib/engine/game/g_1862/step/dividend.rb @@ -65,7 +65,7 @@ def dividend_options(entity) revenue = @game.routes_revenue(routes) subsidy = @game.routes_subsidy(routes) actual_dividend_types(entity, revenue, subsidy).to_h do |type| - payout = send(type, entity, revenue, subsidy) + payout = send(type, entity, revenue) payout[:divs_to_corporation] = corporation_dividends(entity, payout[:per_share]) net_revenue = revenue net_revenue += hudson_delta(entity, revenue) if type == :hudson @@ -95,10 +95,11 @@ def process_dividend(action) @round.routes = [] log_run_payout(entity, kind, revenue, subsidy, action, payout) - if kind == :hudson && payout[:corporation].negative? - entity.spend(-payout[:corporation], @game.bank) - elsif payout[:corporation].positive? - @game.bank.spend(payout[:corporation], entity) + corporation_total_payout = payout[:corporation] + subsidy + if kind == :hudson && corporation_total_payout.negative? + entity.spend(-corporation_total_payout, @game.bank) + elsif corporation_total_payout.positive? + @game.bank.spend(corporation_total_payout, entity) end payout_shares(entity, revenue) if payout[:per_share].positive? change_share_price(entity, payout) @@ -151,17 +152,17 @@ def share_price_change(entity, revenue) end end - def withhold(_entity, revenue, subsidy) - { corporation: revenue + subsidy, per_share: 0 } + def withhold(_entity, revenue) + { corporation: revenue, per_share: 0 } end - def payout(entity, revenue, subsidy) - { corporation: subsidy, per_share: payout_per_share(entity, revenue) } + def payout(entity, revenue) + { corporation: 0, per_share: payout_per_share(entity, revenue) } end - def hudson(entity, revenue, subsidy) + def hudson(entity, revenue) diff = hudson_delta(entity, revenue) - { corporation: subsidy - diff, per_share: payout_per_share(entity, revenue + diff) } + { corporation: -diff, per_share: payout_per_share(entity, revenue + diff) } end def handle_warranties!(entity) diff --git a/lib/engine/game/g_1866/step/dividend.rb b/lib/engine/game/g_1866/step/dividend.rb index 9d8d1ae76b..797d18013e 100644 --- a/lib/engine/game/g_1866/step/dividend.rb +++ b/lib/engine/game/g_1866/step/dividend.rb @@ -17,18 +17,16 @@ def actions(entity) def dividend_options(entity) revenue = @game.routes_revenue(routes) - subsidy = @game.routes_subsidy(routes) - total_revenue = revenue + subsidy dividend_types.to_h do |type| - payout = send(type, entity, revenue, subsidy) + 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 half(entity, revenue, subsidy) + def half(entity, revenue) withheld = half_pay_withhold_amount(entity, revenue) - { corporation: withheld + subsidy, per_share: payout_per_share(entity, revenue - withheld) } + { corporation: withheld, per_share: payout_per_share(entity, revenue - withheld) } end def half_pay_withhold_amount(entity, revenue) @@ -44,47 +42,8 @@ def log_payout_shares(entity, revenue, per_share, receivers) "#{@game.format_currency(per_share)} per share (#{receivers})" end - def log_run_payout(entity, kind, revenue, subsidy, _action, payout) - @log << "#{entity.name} runs for #{@game.format_currency(revenue)} and pays half" if kind == 'half' - - withhold = payout[:corporation] - subsidy - if withhold.positive? && !@game.national_corporation?(entity) - @log << "#{entity.name} withholds #{@game.format_currency(withhold)}" - elsif payout[:per_share].zero? - @log << "#{entity.name} does not run" - end - @log << "#{entity.name} earns subsidy of #{@game.format_currency(subsidy)}" if subsidy.positive? - end - - def process_dividend(action) - entity = action.entity - revenue = @game.routes_revenue(routes) - subsidy = @game.routes_subsidy(routes) - kind = action.kind.to_sym - payout = dividend_options(entity)[kind] - - rust_obsolete_trains!(entity) - - 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(entity, kind, revenue, subsidy, action, payout) - payout_corporation(payout[:corporation], entity) unless @game.national_corporation?(entity) - payout_shares(entity, revenue + subsidy - payout[:corporation]) if payout[:per_share].positive? - change_share_price(entity, payout) - - pass! - end - - def payout(entity, revenue, subsidy) - { corporation: subsidy, per_share: payout_per_share(entity, revenue) } + def payout(entity, revenue) + { corporation: 0, per_share: payout_per_share(entity, revenue) } end def payout_shares(entity, revenue) @@ -128,8 +87,8 @@ def share_price_change(entity, revenue = 0) end end - def withhold(_entity, revenue, subsidy) - { corporation: revenue + subsidy, per_share: 0 } + def withhold(_entity, revenue) + { corporation: revenue, per_share: 0 } end end end diff --git a/lib/engine/game/g_1868_wy/step/dividend.rb b/lib/engine/game/g_1868_wy/step/dividend.rb index 741e5170dd..f7c7341b51 100644 --- a/lib/engine/game/g_1868_wy/step/dividend.rb +++ b/lib/engine/game/g_1868_wy/step/dividend.rb @@ -21,7 +21,7 @@ def share_price_change(entity, revenue = 0) end end - def log_run_payout(entity, kind, revenue, action, payout) + def log_run_payout(entity, kind, revenue, subsidy, action, payout) super unless entity.minor? end diff --git a/lib/engine/game/g_1870/step/connection_dividend.rb b/lib/engine/game/g_1870/step/connection_dividend.rb index 5e2987f0fc..067833895a 100644 --- a/lib/engine/game/g_1870/step/connection_dividend.rb +++ b/lib/engine/game/g_1870/step/connection_dividend.rb @@ -38,7 +38,7 @@ def process_dividend(action) @round.routes = [] - log_run_payout(entity, kind, revenue, action, payout) + log_run_payout(entity, kind, revenue, 0, action, payout) @game.bank.spend(payout[:corporation], entity) if payout[:corporation].positive? diff --git a/lib/engine/game/g_1873/step/dividend.rb b/lib/engine/game/g_1873/step/dividend.rb index 913247bd03..b8b475d517 100644 --- a/lib/engine/game/g_1873/step/dividend.rb +++ b/lib/engine/game/g_1873/step/dividend.rb @@ -96,7 +96,7 @@ def process_dividend(action) entity.trains.each { |train| train.operated = true } @round.routes = [] - log_run_payout(entity, kind, revenue, action, payout) + log_run_payout(entity, kind, revenue, 0, action, payout) @game.bank.spend(payout[:corporation], entity) if payout[:corporation].positive? @@ -110,7 +110,7 @@ def process_dividend(action) pass! end - def log_run_payout(entity, kind, revenue, action, payout) + def log_run_payout(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 diff --git a/lib/engine/game/g_1888/step/dividend.rb b/lib/engine/game/g_1888/step/dividend.rb index 248f2e0e29..4a81f75056 100644 --- a/lib/engine/game/g_1888/step/dividend.rb +++ b/lib/engine/game/g_1888/step/dividend.rb @@ -9,65 +9,19 @@ module Step class Dividend < Engine::Step::Dividend def dividend_options(entity) revenue = @game.routes_revenue(routes) - subsidy = @game.routes_subsidy(routes) dividend_types.to_h do |type| - payout = send(type, entity, revenue, subsidy) + payout = send(type, entity, revenue) payout[:divs_to_corporation] = corporation_dividends(entity, payout[:per_share]) - [type, payout.merge(share_price_change(entity, revenue - payout[:corporation] + subsidy))] + [type, payout.merge(share_price_change(entity, revenue - payout[:corporation]))] end end - def process_dividend(action) - entity = action.entity - revenue = @game.routes_revenue(routes) - subsidy = @game.routes_subsidy(routes) - kind = action.kind.to_sym - payout = dividend_options(entity)[kind] - - rust_obsolete_trains!(entity) - - 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(entity, kind, revenue, subsidy, action, payout) - - payout_corporation(payout[:corporation], entity) - - payout_shares(entity, revenue - payout[:corporation] + subsidy) if payout[:per_share].positive? - - change_share_price(entity, payout) - - pass! - end - - def log_run_payout(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] - subsidy).positive? - @log << "#{entity.name} withholds #{@game.format_currency(payout[:corporation])}" - elsif subsidy.positive? - @log << "#{entity.name} retains a subsidy of #{@game.format_currency(subsidy)}" - elsif payout[:per_share].zero? - @log << "#{entity.name} does not run" - end - end - - def withhold(_entity, revenue, subsidy) - { corporation: revenue + subsidy, per_share: 0 } + def withhold(_entity, revenue) + { corporation: revenue, per_share: 0 } end - def payout(entity, revenue, subsidy) - { corporation: subsidy, per_share: payout_per_share(entity, revenue) } + def payout(entity, revenue) + { corporation: 0, per_share: payout_per_share(entity, revenue) } end end end diff --git a/lib/engine/game/g_18_ardennes/game.rb b/lib/engine/game/g_18_ardennes/game.rb index 57ceee3c26..c881781164 100644 --- a/lib/engine/game/g_18_ardennes/game.rb +++ b/lib/engine/game/g_18_ardennes/game.rb @@ -208,12 +208,12 @@ def can_go_bankrupt?(player, _corporation) bankrupt?(player) end - def declare_bankrupt(player, cash_target = @bank) + def bankrupt!(player, cash_target) @log << "-- #{player.name} goes bankrupt and sells remaining shares --" bankrupt_sell_shares(player) bankrupt_sell_companies(player) bankrupt_transfer_cash(player, cash_target) - super + declare_bankrupt(player) end # If a player has gone above 60% holding in a major (by exchanging diff --git a/lib/engine/game/g_18_ardennes/step/bankrupt.rb b/lib/engine/game/g_18_ardennes/step/bankrupt.rb index b99fcef979..0f8edf2a0c 100644 --- a/lib/engine/game/g_18_ardennes/step/bankrupt.rb +++ b/lib/engine/game/g_18_ardennes/step/bankrupt.rb @@ -13,7 +13,7 @@ class Bankrupt < Engine::Step::Bankrupt def process_bankrupt(action) corporation = action.entity player = corporation.player - @game.declare_bankrupt(player, corporation) + @game.bankrupt!(player, corporation) receivership_buy_train(corporation) end diff --git a/lib/engine/game/g_18_ardennes/step/buy_sell_par_shares_companies.rb b/lib/engine/game/g_18_ardennes/step/buy_sell_par_shares_companies.rb index 1de9c2556e..53a13e3d2c 100644 --- a/lib/engine/game/g_18_ardennes/step/buy_sell_par_shares_companies.rb +++ b/lib/engine/game/g_18_ardennes/step/buy_sell_par_shares_companies.rb @@ -193,7 +193,7 @@ def process_par(action) end def process_bankrupt(action) - @game.declare_bankrupt(action.entity) + @game.bankrupt!(action.entity, @game.bank) end def log_skip(entity) diff --git a/lib/engine/game/g_18_esp/game.rb b/lib/engine/game/g_18_esp/game.rb index 0f5e706b6e..8d650c2504 100644 --- a/lib/engine/game/g_18_esp/game.rb +++ b/lib/engine/game/g_18_esp/game.rb @@ -359,7 +359,7 @@ def mza end def madrid_hex - @madrid_hex ||= @game.hex_by_id(MADRID_HEX) + @madrid_hex ||= hex_by_id(MADRID_HEX) end def setup diff --git a/lib/engine/game/g_18_esp/step/dividend.rb b/lib/engine/game/g_18_esp/step/dividend.rb index 1b401257dc..4fb32ac731 100644 --- a/lib/engine/game/g_18_esp/step/dividend.rb +++ b/lib/engine/game/g_18_esp/step/dividend.rb @@ -19,10 +19,8 @@ def actions(entity) def dividend_options(entity) revenue = @game.routes_revenue(routes) - subsidy = @game.routes_subsidy(routes) - total_revenue = revenue + subsidy dividend_types.to_h do |type| - payout = send(type, entity, revenue, subsidy) + 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 @@ -44,66 +42,21 @@ def movement_str(times, dir) "#{times} #{dir}" end - def process_dividend(action) - entity = action.entity - revenue = @game.routes_revenue(routes) - subsidy = @game.routes_subsidy(routes) - kind = action.kind.to_sym - payout = dividend_options(entity)[kind] - - rust_obsolete_trains!(entity) - - 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(entity, kind, revenue, subsidy, action, payout) - - payout_corporation(payout[:corporation], entity) - - payout_shares(entity, revenue - payout[:corporation] + subsidy) if payout[:per_share].positive? - - change_share_price(entity, payout) - - pass! - end - - def log_run_payout(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] - subsidy).positive? - @log << "#{entity.name} withholds #{@game.format_currency(payout[:corporation])}" - elsif subsidy.positive? - @log << "#{entity.name} retains a subsidy of #{@game.format_currency(subsidy)}" - elsif payout[:per_share].zero? - @log << "#{entity.name} does not run" - end - end - def payout_per_share(entity, revenue) revenue * 1.0 / entity.total_shares end - def payout(entity, revenue, subsidy) + def payout(entity, revenue) if entity.corporation? && entity.type != :minor - { corporation: subsidy, per_share: payout_per_share(entity, revenue) } + { corporation: 0, per_share: payout_per_share(entity, revenue) } else amount = revenue / 2 - { corporation: amount + subsidy, per_share: amount } + { corporation: amount, per_share: amount } end end - def withhold(_entity, revenue, subsidy) - { corporation: revenue + subsidy, per_share: 0 } + def withhold(_entity, revenue) + { corporation: revenue, per_share: 0 } end end end diff --git a/lib/engine/game/g_18_hiawatha/meta.rb b/lib/engine/game/g_18_hiawatha/meta.rb index 4337c447af..36a642a950 100644 --- a/lib/engine/game/g_18_hiawatha/meta.rb +++ b/lib/engine/game/g_18_hiawatha/meta.rb @@ -8,14 +8,14 @@ module G18Hiawatha module Meta include Game::Meta - DEV_STAGE = :alpha + DEV_STAGE = :beta DEPENDS_ON = '1817' GAME_DESIGNER = 'Michael Carter, Anthony Fryer, & Nick Neylon' GAME_INFO_URL = 'https://github.com/tobymao/18xx/wiki/18Hiawatha' GAME_LOCATION = 'Midwest USA' GAME_TITLE = '18 Hiawatha' - GAME_RULES_URL = 'https://boardgamegeek.com/filepage/279555/18-hiawatha-official-game-rules' + GAME_RULES_URL = 'https://boardgamegeek.com/filepage/279677/18-hiawatha-rules-from-mainline-issue-1' PLAYER_RANGE = [3, 6].freeze diff --git a/lib/engine/game/g_18_mag/game.rb b/lib/engine/game/g_18_mag/game.rb index ee9a66e512..1de40ce286 100644 --- a/lib/engine/game/g_18_mag/game.rb +++ b/lib/engine/game/g_18_mag/game.rb @@ -727,7 +727,7 @@ def ciwl_delta end def routes_subsidy(routes) - routes.sum(&:subsidy) + routes.first&.train&.owner&.minor? ? routes.sum(&:subsidy) : 0 end # see if minor bought unused rail-cars diff --git a/lib/engine/game/g_18_mag/step/dividend.rb b/lib/engine/game/g_18_mag/step/dividend.rb index 1636a28c43..480901d83d 100644 --- a/lib/engine/game/g_18_mag/step/dividend.rb +++ b/lib/engine/game/g_18_mag/step/dividend.rb @@ -47,14 +47,7 @@ def skip! end def process_dividend(action) - if action.entity.minor? - subsidy = @game.routes_subsidy(routes) - if subsidy.positive? - @game.bank.spend(subsidy, action.entity) - @log << "#{action.entity.name} retains a subsidy of #{@game.format_currency(subsidy)}" - end - return super - end + return super if action.entity.minor? entity = action.entity action_kind = action.kind diff --git a/lib/engine/game/g_18_norway/game.rb b/lib/engine/game/g_18_norway/game.rb index ab9d9842c7..0249c679fc 100644 --- a/lib/engine/game/g_18_norway/game.rb +++ b/lib/engine/game/g_18_norway/game.rb @@ -106,6 +106,14 @@ def nsb @nsb ||= corporation_by_id('NSB') end + def harbor_city_coordinates(hex_id) + CITY_HARBOR_MAP.key(hex_id) + end + + def harbor_hex?(hex) + HARBOR_HEXES.include?(hex.id) + end + def price_movement_chart [ ['Action', 'Share Price Change'], diff --git a/lib/engine/game/g_18_norway/steps/token.rb b/lib/engine/game/g_18_norway/steps/token.rb index 58d4fdaa29..bceeafeeed 100644 --- a/lib/engine/game/g_18_norway/steps/token.rb +++ b/lib/engine/game/g_18_norway/steps/token.rb @@ -8,10 +8,15 @@ module G18Norway module Step class Token < Engine::Step::Token def available_hex(entity, hex) - return true if super(entity, hex) - return true if @game.ferry_graph.reachable_hexes(entity)[hex] + super(entity, hex) || harbor_available?(entity, hex) + end + + def harbor_available?(entity, hex) + other_hex = @game.hex_by_id(@game.harbor_city_coordinates(hex.id)) + return false unless other_hex - false + @game.ferry_graph.reachable_hexes(entity)[other_hex] || + @game.token_graph_for_entity(entity).reachable_hexes(entity)[other_hex] end def can_place_token?(_entity) @@ -21,25 +26,21 @@ def can_place_token?(_entity) def process_place_token(action) entity = action.entity city = action.city - connected_city = @game.loading || @game.token_graph_for_entity(entity).connected_nodes(entity)[city] - place_token(entity, action.city, action.token) if connected_city - - unless connected_city + connected_city = @game.token_graph_for_entity(entity).connected_nodes(entity)[city] + if connected_city + place_token(entity, action.city, action.token) + elsif harbor_available?(entity, city.hex) @game.abilities(entity, :token) do |ability| place_token(entity, action.city, action.token, connected: false, special_ability: ability) + entity.add_ability(Ability::Token.new(type: 'token', hexes: Engine::Game::G18Norway::Game::HARBOR_HEXES, + from_owner: true, discount: 0, connected: true)) end + else + raise GameError, "Cannot place token on #{city.hex.name} city is not connected" end @game.clear_graph pass! end - - def check_connected(entity, city, hex) - return if @game.loading || @game.token_graph_for_entity(entity).connected_nodes(entity)[city] - return if @game.loading || @game.ferry_graph.connected_nodes(entity)[city] - - city_string = hex.tile.cities.size > 1 ? " city #{city.index}" : '' - raise GameError, "Cannot place token on #{hex.name}#{city_string} because it is not connected" - end end end end diff --git a/lib/engine/game/g_18_tn/step/dividend.rb b/lib/engine/game/g_18_tn/step/dividend.rb index 14b67936f5..933ac05747 100644 --- a/lib/engine/game/g_18_tn/step/dividend.rb +++ b/lib/engine/game/g_18_tn/step/dividend.rb @@ -28,7 +28,7 @@ def dividend_options(entity) { withhold: { corporation: 0, per_share: 0, divs_to_corporation: 0 } } end - def log_run_payout(entity, kind, revenue, action, payout) + def log_run_payout(entity, kind, revenue, subsidy, action, payout) return super unless civil_war_effect_with_single_train?(entity) @log << "#{entity.name}'s run is ignored due to Civil War" diff --git a/lib/engine/game/g_18_uruguay/game.rb b/lib/engine/game/g_18_uruguay/game.rb index cecd5ef96f..063826bca3 100644 --- a/lib/engine/game/g_18_uruguay/game.rb +++ b/lib/engine/game/g_18_uruguay/game.rb @@ -32,6 +32,16 @@ class Game < Game::Base include Goods include Nationalization + SHIP_CAPACITY = + { + 'Ship 1' => 1, + 'Ship 2' => 1, + 'Ship 3' => 2, + 'Ship 4' => 2, + 'Ship 5' => 3, + 'Ship 6' => 3, + }.freeze + EBUY_SELL_MORE_THAN_NEEDED = true EBUY_DEPOT_TRAIN_MUST_BE_CHEAPEST = false @@ -283,6 +293,7 @@ def after_buy_company(player, company, _price) def operating_round(round_num) Round::Operating.new(self, [ + G18Uruguay::Step::DestinationBonus, Engine::Step::Bankrupt, Engine::Step::Exchange, G18Uruguay::Step::Farm, @@ -339,6 +350,7 @@ def float_corporation(corporation) float_capitalization = nationalized? ? 10 : 5 amount = corporation.par_price.price * float_capitalization + abilities(corporation, :destination_bonus).use! if nationalized? @bank.spend(amount, corporation) @log << "#{corporation.name} receives #{format_currency(corporation.cash)}" take_loan(corporation, @loans[0]) if @loans.size.positive? && !nationalized? @@ -535,6 +547,13 @@ def operating_order super.reject { |c| c == @rptla }.append(@rptla) end + + def ship_capacity(train) + val = SHIP_CAPACITY[train.name] + return 0 if val.nil? + + val + end end end end diff --git a/lib/engine/game/g_18_uruguay/nationalization.rb b/lib/engine/game/g_18_uruguay/nationalization.rb index a4b56e1e26..ad6caa663e 100644 --- a/lib/engine/game/g_18_uruguay/nationalization.rb +++ b/lib/engine/game/g_18_uruguay/nationalization.rb @@ -160,8 +160,10 @@ def start_merge(originatior) def nationalization_final_export! return if number_of_goods_at_harbor.zero? - @log << "Nationalization: Final Export #{number_of_goods_at_harbor} goods for #{format_currency(50)} each" - amount_per_share = 5 * number_of_goods_at_harbor + nr_goods = [number_of_goods_at_harbor, @rptla.trains.sum { |train| ship_capacity(train) }].min + @log << "Nationalization: Final Export #{nr_goods} good for #{format_currency(50)}" if nr_goods == 1 + @log << "Nationalization: Final Export #{nr_goods} goods for #{format_currency(50)} each" if nr_goods > 1 + amount_per_share = 5 * nr_goods players.each do |holder| amount = holder.num_shares_of(@rptla) * amount_per_share next unless amount.positive? diff --git a/lib/engine/game/g_18_uruguay/step/buy_sell_par_shares.rb b/lib/engine/game/g_18_uruguay/step/buy_sell_par_shares.rb index fdf32f3205..e3eb27f06f 100644 --- a/lib/engine/game/g_18_uruguay/step/buy_sell_par_shares.rb +++ b/lib/engine/game/g_18_uruguay/step/buy_sell_par_shares.rb @@ -7,10 +7,18 @@ module Game module G18Uruguay module Step class BuySellParShares < Engine::Step::BuySellParShares - def can_buy?(entity, bundle) + def can_gain?(entity, bundle, exchange: false) return false if bundle&.corporation == @game.rptla && !@game.phase.status.include?('rptla_available') + return true if excess_loans?(entity, bundle) - super(entity, bundle) + super + end + + def excess_loans?(_entity, bundle) + return false if bundle.nil? + return false if bundle.owner.corporation? + + bundle.corporation.loans.size > @game.maximum_loans(bundle.corporation) end end end diff --git a/lib/engine/game/g_18_uruguay/step/destination_bonus.rb b/lib/engine/game/g_18_uruguay/step/destination_bonus.rb index 584c32f54b..b7849e80ce 100644 --- a/lib/engine/game/g_18_uruguay/step/destination_bonus.rb +++ b/lib/engine/game/g_18_uruguay/step/destination_bonus.rb @@ -12,8 +12,9 @@ def description end def log_skip(entity) - return if entity.minor? + return unless entity.corporation? return if entity == @game.rptla + return unless destination_node_check?(entity) super end @@ -21,23 +22,24 @@ def log_skip(entity) def actions(entity) return [] if entity.minor? return [] if entity == @game.rptla + return [] if @game.abilities(entity, :destination_bonus).nil? self.class::ACTIONS end def auto_actions(entity) - corporations = @round.entities.select { |c| destination_node_check?(c) } - return [Engine::Action::Pass.new(entity)] if corporations.empty? + return [] unless entity.corporation? + return [Engine::Action::Pass.new(entity)] unless destination_node_check?(entity) - [Engine::Action::DestinationConnection.new(entity, corporations: corporations)] + [Engine::Action::DestinationConnection.new(entity, corporations: [entity])] end def destination_node_check?(corporation) - return if corporation.closed? - return if corporation.destination_coordinates.nil? + return false if corporation.closed? + return false if corporation.destination_coordinates.nil? ability = @game.abilities(corporation, :destination_bonus) - return if ability.nil? + return false if ability.nil? destination_hex = @game.hex_by_id(corporation.destination_coordinates) home_node = corporation.tokens.first.city diff --git a/lib/engine/game/g_18_uruguay/step/dividend.rb b/lib/engine/game/g_18_uruguay/step/dividend.rb index 1b31da150d..a8b7e3192d 100644 --- a/lib/engine/game/g_18_uruguay/step/dividend.rb +++ b/lib/engine/game/g_18_uruguay/step/dividend.rb @@ -39,6 +39,7 @@ def description end def auto_actions(entity) + return [] unless entity.corporation? return [] unless @game.nationalized? return [] if entity.loans.empty? @@ -49,10 +50,8 @@ def dividend_options(entity) total_revenue = routes_revenue(routes, entity) revenue = total_revenue - subsidy = routes_subsidy(routes, entity) - total_revenue += subsidy dividend_types.to_h do |type| - payout = send(type, entity, revenue, subsidy) + 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 @@ -62,19 +61,19 @@ def holder_for_corporation(_entity) @game.share_pool end - def payout(entity, revenue, subsidy) + def payout(entity, revenue) if @game.nationalized? && entity.loans.size.positive? return { - corporation: subsidy + (payout_per_share(entity, revenue) * 10), + corporation: payout_per_share(entity, revenue) * 10, per_share: 0, } end - { corporation: subsidy, per_share: payout_per_share(entity, revenue) } + { corporation: 0, per_share: payout_per_share(entity, revenue) } end - def withhold(_entity, revenue, subsidy) - { corporation: revenue + subsidy, per_share: 0 } + def withhold(_entity, revenue) + { corporation: revenue, per_share: 0 } end def process_dividend_rptla(action) @@ -94,8 +93,8 @@ def process_dividend_rptla(action) @round.routes = [] log_run_payout_sub(entity, kind, revenue, subsidy, action, payout) - @game.bank.spend(payout[:corporation], entity) if payout[:corporation].positive? - payout_shares(entity, revenue + subsidy - payout[:corporation]) if payout[:per_share].positive? + @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! @@ -111,7 +110,7 @@ def process_dividend(action) @game.payoff_loan(current_entity, loans_to_pay_off, current_entity) end - def log_run_payout(entity, kind, revenue, action, payout) + def log_run_payout(entity, kind, revenue, subisdy, action, payout) super unless entity.minor? end diff --git a/lib/engine/game/g_18_uruguay/step/route_rptla.rb b/lib/engine/game/g_18_uruguay/step/route_rptla.rb index db0b619d80..e0a1b5fdda 100644 --- a/lib/engine/game/g_18_uruguay/step/route_rptla.rb +++ b/lib/engine/game/g_18_uruguay/step/route_rptla.rb @@ -7,16 +7,6 @@ module Game module G18Uruguay module Step class RouteRptla < Engine::Step::Route - SHIP_CAPACITY = - { - 'Ship 1' => 1, - 'Ship 2' => 1, - 'Ship 3' => 2, - 'Ship 4' => 2, - 'Ship 5' => 3, - 'Ship 6' => 3, - }.freeze - def description 'Ship to England' end @@ -55,13 +45,9 @@ def choices choices end - def ship_capacity(train) - SHIP_CAPACITY[train.name.partition('+')[0]] - end - def total_ship_capacity?(entity) trains = @game.route_trains(entity) - trains.sum { |train| ship_capacity(train) } + trains.sum { |train| @game.ship_capacity(train) } end def process_choose(action) @@ -74,7 +60,7 @@ def process_choose(action) remaining = goods_to_deliver ships.each do |ship| ship.name = ship.name.partition('+')[0] unless ship.nil? - capacity = ship_capacity(ship) + capacity = @game.ship_capacity(ship) goods_count = [remaining, capacity].min remaining -= goods_count @game.add_goods_to_ship(ship, goods_count) diff --git a/lib/engine/game/g_18_va/step/dividend.rb b/lib/engine/game/g_18_va/step/dividend.rb index c91781247c..f344d24625 100644 --- a/lib/engine/game/g_18_va/step/dividend.rb +++ b/lib/engine/game/g_18_va/step/dividend.rb @@ -8,69 +8,12 @@ module Game module G18VA module Step class Dividend < Engine::Step::Dividend - def withhold(_entity, revenue, subsidy) - { corporation: revenue + subsidy, per_share: 0 } + def withhold(_entity, revenue) + { corporation: revenue, per_share: 0 } end - def payout(entity, revenue, subsidy) - { corporation: subsidy, per_share: payout_per_share(entity, revenue) } - end - - def dividend_options(entity) - revenue = @game.routes_revenue(routes) - subsidy = @game.routes_subsidy(routes) - dividend_types.to_h do |type| - payout = send(type, entity, revenue, subsidy) - payout[:divs_to_corporation] = corporation_dividends(entity, payout[:per_share]) - [type, payout.merge(share_price_change(entity, revenue + subsidy - payout[:corporation]))] - end - end - - def process_dividend(action) - entity = action.entity - revenue = @game.routes_revenue(routes) - subsidy = @game.routes_subsidy(routes) - kind = action.kind.to_sym - payout = dividend_options(entity)[kind] - - rust_obsolete_trains!(entity) - - 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(entity, kind, revenue, subsidy, action, payout) - - payout_corporation(payout[:corporation], entity) - - adjusted_revenue = subsidy ? revenue + subsidy : revenue - payout_shares(entity, adjusted_revenue - payout[:corporation]) if payout[:per_share].positive? - - change_share_price(entity, payout) - - pass! - end - - def log_run_payout(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 - - withheld_amount = payout[:corporation] - subsidy - if withheld_amount.positive? - @log << "#{entity.name} withholds #{@game.format_currency(withheld_amount)}" - elsif payout[:per_share].zero? - @log << "#{entity.name} does not run" - end - - @log << "#{entity.name} earns subsidy of #{@game.format_currency(subsidy)}" if subsidy.positive? + def payout(entity, revenue) + { corporation: 0, per_share: payout_per_share(entity, revenue) } end def share_price_change(entity, revenue = 0) diff --git a/lib/engine/game/g_18_zoo/step/dividend.rb b/lib/engine/game/g_18_zoo/step/dividend.rb index f5bd9fdf00..5a1e1d1c1b 100644 --- a/lib/engine/game/g_18_zoo/step/dividend.rb +++ b/lib/engine/game/g_18_zoo/step/dividend.rb @@ -11,10 +11,9 @@ class Dividend < Engine::Step::Dividend def dividend_options(entity) revenue = @game.routes_revenue(routes) - subsidy = @game.routes_subsidy(routes) dividend_types.to_h do |type| - [type, send(type, entity, revenue, subsidy)] + [type, send(type, entity, revenue)] end end @@ -22,9 +21,9 @@ def share_price_change(entity, revenue) :right if revenue >= @game.threshold(entity) end - def withhold(_entity, revenue, subsidy) + def withhold(_entity, revenue) { - corporation: (revenue / 25.0).ceil + subsidy, + corporation: (revenue / 25.0).ceil, per_share: 0, share_direction: :left, share_times: 1, @@ -32,9 +31,9 @@ def withhold(_entity, revenue, subsidy) } end - def payout(entity, revenue, subsidy) + def payout(entity, revenue) { - corporation: subsidy, + corporation: 0, per_share: payout_per_share(entity, revenue), share_direction: revenue >= @game.threshold(entity) ? :right : nil, share_times: 1, @@ -63,11 +62,8 @@ def payout_shares(entity, revenue) def process_dividend(action) @subsidy = @game.routes_subsidy(routes) - super - @subsidy = 0 - action.entity.remove_assignment!('BARREL') if @game.two_barrels_used_this_or?(action.entity) end diff --git a/lib/engine/game/g_22_mars/game.rb b/lib/engine/game/g_22_mars/game.rb index 9685cd9845..5a47fb3efa 100644 --- a/lib/engine/game/g_22_mars/game.rb +++ b/lib/engine/game/g_22_mars/game.rb @@ -168,18 +168,105 @@ class Game < Game::Base }, ].freeze + # Game specific constants LAST_OR = 11 + FIRST_REVOLT_CHECK_OR = 7 + LAST_REVOLT_CHECK_OR = 10 - @or = 0 + def setup + @or = 0 + @three_or_round = false + @revolt_round = determine_revolt_round + end def end_now?(_after) @or == LAST_OR end + def setup_preround + revolts, permits = @companies.partition(&:revolt?) + sorted_permits = permits.sort_by { rand } + sorted_revolts = revolts.sort_by { rand } + + @companies = sorted_permits.take(@players.size) + assigned_companies = sorted_permits.drop(@players.size) + sorted_revolts + + assigned_companies.zip(@corporations).each do |company, corporation| + if corporation + company.owner = corporation + corporation.companies << company + @log << "#{company.name} is assigned to #{corporation.name}" + else + company.close! + @log << "#{company.name} is discarded" + end + end + end + + def unowned_purchasable_companies + @companies + end + + def init_round + G22Mars::Round::PermitPurchase.new(self, [G22Mars::Step::SelectOrDiscard]) + end + def new_operating_round(round_num = 1) @or += 1 - super + if @or == 9 + @operating_rounds = 3 + @three_or_round = true + end + + round = super + + check_revolt! + + round + end + + def or_round_finished + # In case we get phase change during the last OR set we ensure we have 3 ORs + @operating_rounds = 3 if @three_or_round + end + + def revolt_happened? + @revolt_happened + end + + def determine_revolt_round + (FIRST_REVOLT_CHECK_OR..LAST_REVOLT_CHECK_OR).each do |round| + probability = LAST_REVOLT_CHECK_OR - round + 1 + + return round if (rand % probability).zero? + end + end + + def check_revolt! + return if @or < FIRST_REVOLT_CHECK_OR || revolt_happened? + + if @or == @revolt_round + revolt! + else + @log << 'Revolt! does not happen' + end + end + + def revolt! + @log << '-- Revolt!' + @log << '-- All permits were discarded. You can now use abilities of Revolt! cards.' + @log << '-- All Convict bases are now neutral and cities with them generate no revenue.' + + @revolt_happened = true + + @companies.each do |company| + company.close! unless abilities(company, :close, on_phase: 'never') + end + end + + def ipo_name(_entity = nil) + 'Treasury' end def timeline @@ -209,16 +296,26 @@ def progress_information { type: :OR, name: '5' }, { type: :OR, name: '6' }, { type: :SR, name: '4' }, - { type: :OR, name: '7', value: '✘/✔' }, - { type: :OR, name: '8', value: '✘/✔' }, + { type: :OR, name: '7', value: draw_revolt_marker(7) }, + { type: :OR, name: '8', value: draw_revolt_marker(8) }, { type: :SR, name: '5' }, - { type: :OR, name: '9', value: '✘/✔' }, - { type: :OR, name: '10', value: '✔' }, + { type: :OR, name: '9', value: draw_revolt_marker(9) }, + { type: :OR, name: '10', value: draw_revolt_marker(10) }, { type: :OR, name: '11' }, { type: :End }, ] end + def draw_revolt_marker(or_number) + if @revolt_happened + return '✔' if or_number == @revolt_round + + or_number < @revolt_round ? '✘' : '—' + else + or_number == LAST_REVOLT_CHECK_OR ? '✔' : '✘/✔' + end + end + def price_movement_chart [ ['Action', 'Share Price Change'], @@ -233,6 +330,12 @@ def price_movement_chart def company_header(company) company.revolt? ? 'REVOLT!' : 'PERMIT' end + + def round_description(name, round_number = nil) + return 'Permit Purchase Round' if name == 'PermitPurchase' + + super + end end end end diff --git a/lib/engine/game/g_22_mars/round/permit_purchase.rb b/lib/engine/game/g_22_mars/round/permit_purchase.rb new file mode 100644 index 0000000000..d57345c45e --- /dev/null +++ b/lib/engine/game/g_22_mars/round/permit_purchase.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Engine + module Game + module G22Mars + module Round + class PermitPurchase < Engine::Round::Draft + def self.short_name + 'PPR' + end + + def name + 'Permit Purchase Round' + end + end + end + end + end +end diff --git a/lib/engine/game/g_22_mars/step/select_or_discard.rb b/lib/engine/game/g_22_mars/step/select_or_discard.rb new file mode 100644 index 0000000000..7a04d17414 --- /dev/null +++ b/lib/engine/game/g_22_mars/step/select_or_discard.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module Engine + module Game + module G22Mars + module Step + class SelectOrDiscard < Engine::Step::SimpleDraft + ACTIONS = %w[bid pass].freeze + + def setup + @companies = @game.companies + end + + def available + [@companies.first] + end + + def finished? + @companies.empty? + end + + def actions(entity) + return [] if finished? + + entity == current_entity ? ACTIONS : [] + end + + def process_pass(action) + company = available.first + player = action.entity + + company.close! + @companies.delete(company) + + @log << "#{player.name} discards #{company.name} from the game" + + @round.next_entity_index! + action_finalized + end + + def description + 'Buy or Discard Permit' + end + + def pass_description + 'Discard Permit' + end + end + end + end + end +end diff --git a/lib/engine/game/g_system18/game.rb b/lib/engine/game/g_system18/game.rb index 2b082d4df3..38088d6bc9 100644 --- a/lib/engine/game/g_system18/game.rb +++ b/lib/engine/game/g_system18/game.rb @@ -12,6 +12,7 @@ require_relative 'map_uk_limited_customization' require_relative 'map_china_rapid_development_customization' require_relative 'map_poland_customization' +require_relative 'map_britain_customization' module Engine module Game @@ -28,6 +29,7 @@ class Game < Game::Base include MapUKLimitedCustomization include MapChinaRapidDevelopmentCustomization include MapPolandCustomization + include MapBritainCustomization register_colors(red: '#d1232a', orange: '#f58121', @@ -275,6 +277,7 @@ class Game < Game::Base MUST_EMERGENCY_ISSUE_BEFORE_EBUY = false BANKRUPTCY_ENDS_GAME_AFTER = :one STATUS_TEXT = {}.freeze + TILE_UPGRADES_MUST_USE_MAX_EXITS = [].freeze def find_map_name optional_rules&.find { |r| r.to_s.include?('map_') }&.to_s&.delete_prefix('map_')&.downcase @@ -359,9 +362,13 @@ def game_phases proto = send("map_#{map_name}_game_phases") proto.each { |pp| phases << pp.dup } - # change last phase based on train roster - phases[-1][:name] = game_trains.last[:name] - phases[-1][:on] = game_trains.last[:name] + if respond_to?("map_#{map_name}_post_game_phases") + phases = send("map_#{map_name}_post_game_phases", phases) + else + # change last phase based on train roster + phases.last[:name] = game_trains.last[:name] + phases.last[:on] = game_trains.last[:name] + end phases end @@ -616,6 +623,18 @@ def revenue_str(route) revenue_str + send("map_#{map_name}_extra_revenue_str", route) end + def extra_revenue(entity, routes) + return super unless respond_to?("map_#{map_name}_extra_revenue") + + send("map_#{map_name}_extra_revenue", entity, routes) + end + + def submit_revenue_str(routes, show_subsidy) + return super unless respond_to?("map_#{map_name}_submit_revenue_str") + + send("map_#{map_name}_submit_revenue_str", routes, show_subsidy) + end + def timeline return super unless respond_to?("map_#{map_name}_timeline") @@ -625,6 +644,54 @@ def timeline def ipo_name(_corp) game_capitalization == :incremental ? 'Treasury' : 'IPO' end + + def can_remove_icon?(entity) + return false unless respond_to?("map_#{map_name}_can_remove_icon?") + + send("map_#{map_name}_can_remove_icon?", entity) + end + + def icon_hexes(entity) + return [] unless respond_to?("map_#{map_name}_icon_hexes") + + send("map_#{map_name}_icon_hexes", entity) + end + + def remove_icon(entity, hex_id) + return unless respond_to?("map_#{map_name}_remove_icon") + + send("map_#{map_name}_remove_icon", entity, hex_id) + end + + def removable_icon_action_str + return unless respond_to?("map_#{map_name}_removable_icon_action_str") + + send("map_#{map_name}_removable_icon_action_str") + end + + def status_str(corporation) + return super unless respond_to?("map_#{map_name}_status_str") + + send("map_#{map_name}_status_str", corporation) + end + + def modify_tile_lay(entity, action) + return action unless respond_to?("map_#{map_name}_modify_tile_lay") + + send("map_#{map_name}_modify_tile_lay", entity, action) + end + + def pre_lay_tile_action(action, entity, tile_lay) + return unless respond_to?("map_#{map_name}_pre_lay_tile_action") + + send("map_#{map_name}_pre_lay_tile_action", action, entity, tile_lay) + end + + def place_home_token(corporation) + return super unless respond_to?("map_#{map_name}_place_home_token") + + send("map_#{map_name}_place_home_token", corporation) + end end end end diff --git a/lib/engine/game/g_system18/map_base_customization.rb b/lib/engine/game/g_system18/map_base_customization.rb index bb7c8eb8f4..b06048e83e 100644 --- a/lib/engine/game/g_system18/map_base_customization.rb +++ b/lib/engine/game/g_system18/map_base_customization.rb @@ -37,11 +37,11 @@ def map_base_game_corporations(corps) end def map_base_game_cash - { 2 => 800, 3 => 500, 4 => 400 } + { 2 => 800, 3 => 500, 4 => 400, 5 => 300 } end def map_base_game_cert_limit - { 2 => 20, 3 => 15, 4 => 10 } + { 2 => 20, 3 => 15, 4 => 10, 5 => 8 } end def map_base_game_capitalization diff --git a/lib/engine/game/g_system18/map_britain_customization.rb b/lib/engine/game/g_system18/map_britain_customization.rb new file mode 100644 index 0000000000..f7d89f9d7a --- /dev/null +++ b/lib/engine/game/g_system18/map_britain_customization.rb @@ -0,0 +1,517 @@ +# frozen_string_literal: true + +module Engine + module Game + module GSystem18 + module MapBritainCustomization + BRITAIN_REGION_HEXES = { + 'Scotland' => %w[A6 A8 A10 B3 B5 B7 B9 C4 C6], + 'Wales' => %w[G4 H3 I2 I4 J3], + }.freeze + + BRITAIN_MINE_HEXES = %w[B7 D9 G10 I4].freeze + BRITAIN_LONDON_HEX = 'K10' + + BRITAIN_SCOTLAND_BONUS_VAL_HEX = 'A4' + BRITAIN_WALES_BONUS_VAL_HEX = 'G2' + BRITAIN_LONDON_BONUS_VAL_HEX = 'L11' + BRITAIN_MINE_BONUS_VAL_HEX = 'G12' + + # rubocop:disable Layout/LineLength + def map_britain_game_tiles(tiles) + tiles['8'] = 6 + tiles['9'] = 6 + tiles['23'] = 2 + tiles['24'] = 2 + tiles['25'] = 2 + tiles['448'] = 3 + tiles.merge!({ + 'X1' => + { + 'count' => 3, + 'color' => 'gray', + 'code' => 'city=revenue:70,slots:2;path=a:0,b:_0;path=a:1,b:_0;path=a:2,b:_0;path=a:3,b:_0;path=a:4,b:_0;path=a:5,b:_0;label=OO', + }, + 'X2' => + { + 'count' => 1, + 'color' => 'gray', + 'code' => 'city=revenue:50,slots:2;path=a:0,b:_0;path=a:1,b:_0;path=a:2,b:_0;path=a:3,b:_0;path=a:4,b:_0;path=a:5,b:_0', + }, + }) + end + # rubocop:enable Layout/LineLength + + def map_britain_layout + :pointy + end + + def map_britain_game_location_names + { + 'A4' => 'Scotland Bonus', + 'A6' => 'Glasgow', + 'A8' => 'Edinburgh', + 'B3' => 'N. Ireland', + 'C4' => '(Scotland)', + 'C8' => 'Carlisle', + 'C10' => 'Newcastle', + 'D11' => 'Sunderland & Middlesbrough', + 'E6' => 'Preston', + 'E10' => 'York', + 'F5' => 'Liverpool', + 'F7' => 'Bolton & Manchester', + 'F9' => 'Leeds & Bradford', + 'F11' => 'Hull', + 'G2' => 'Wales Bonus', + 'G6' => 'Crewe & Stoke on Trent', + 'G8' => 'Derby', + 'G12' => 'Mine Bonus', + 'H3' => '(Wales)', + 'H7' => 'Birmingham & Wolverh\'ton', + 'H9' => 'Nottingham', + 'I2' => 'S. Ireland', + 'I8' => 'Coventry', + 'I12' => 'Norwich', + 'J1' => 'Swansea', + 'J3' => 'Cardiff', + 'J5' => 'Bristol', + 'J9' => 'Bedford & Luton', + 'J11' => 'Cambridge & Colchester', + 'J13' => 'Harwich', + 'K2' => 'Plymouth', + 'K8' => 'Reading & Guildford', + 'K10' => 'London', + 'L5' => 'Southampton', + 'L7' => 'Portsmouth', + 'L11' => 'London Port Bonus', + } + end + + # rubocop:disable Layout/LineLength + def map_britain_game_hexes + { + gray: { + %w[A4] => 'offboard=revenue:yellow_10|green_20|brown_30|gray_40', + %w[C12] => 'path=a:0,b:1', + %w[G2] => 'offboard=revenue:yellow_10|green_20|brown_20|gray_30', + %w[G12] => 'offboard=revenue:yellow_10|green_20|brown_30|gray_40', + %w[J1] => 'town=revenue:10;path=a:4,b:_0', + %w[L11] => 'offboard=revenue:yellow_10|green_20|brown_30|gray_40', + }, + + red: { + %w[B3] => 'offboard=revenue:yellow_20|green_20|brown_30|gray_30;path=a:4,b:_0;path=a:5,b:_0;icon=image:port', + %w[F5] => 'offboard=revenue:yellow_30|green_40|brown_50|gray_70;path=a:3,b:_0;path=a:4,b:_0;path=a:5,b:_0', + %w[I2] => 'offboard=revenue:yellow_20|green_20|brown_30|gray_30;path=a:3,b:_0;path=a:4,b:_0;path=a:5,b:_0;icon=image:port', + %w[J13] => 'offboard=revenue:yellow_10|green_20|brown_30|gray_40;path=a:0,b:_0;path=a:1,b:_0;path=a:2,b:_0;icon=image:port', + %w[K2] => 'offboard=revenue:yellow_20|green_30|brown_40|gray_40;path=a:4,b:_0;path=a:5,b:_0;icon=image:port', + %w[K10] => 'offboard=revenue:yellow_40|green_50|brown_70|gray_100;path=a:0,b:_0;path=a:1,b:_0;path=a:2,b:_0;path=a:3,b:_0;path=a:4,b:_0', + }, + green: { + %w[F7] => 'city=revenue:40,pos:0;city=revenue:40,pos:2;path=a:0,b:_0;path=a:2,b:_1;label=OO', + %w[H7] => 'city=revenue:40,pos:3;city=revenue:40,pos:5;path=a:3,b:_1;path=a:5,b:_0;label=OO', + }, + yellow: { + %w[A6] => 'city=revenue:30;path=a:0,b:_0;path=a:2,b:_0;path=a:4,b:_0;label=B', + %w[F9] => 'city=revenue:30,pos:0;city=revenue:30,pos:3;path=a:3,b:_1;label=OO', + }, + white: { + %w[B5 C4 H11 I10 J7 K6 K12 L3 L9] => '', + %w[A8] => 'city=revenue:0', + %w[A10] => 'border=type:province,color:brown,edge:5', + %w[B7] => 'upgrade=cost:40,terrain:mountain;icon=image:mine,sticky:1;border=type:province,color:brown,edge:5', + %w[B9] => 'upgrade=cost:40,terrain:mountain;border=type:province,color:brown,edge:0;border=type:province,color:brown,edge:4;border=type:province,color:brown,edge:5', + %w[B11] => 'border=type:province,color:brown,edge:1;border=type:province,color:brown,edge:2', + %w[C6] => 'border=type:province,color:brown,cost:80,edge:5', + %w[C8] => 'city=revenue:0;upgrade=cost:20,terrain:water;border=type:province,color:brown,edge:1;border=type:province,color:brown,edge:2;border=type:province,color:brown,edge:3', + %w[C10] => 'city=revenue:0;upgrade=cost:20,terrain:water;border=type:province,color:brown,edge:2', + %w[D7] => 'upgrade=cost:40,terrain:mountain;border=type:province,color:brown,cost:80,edge:2', + %w[D9] => 'upgrade=cost:40,terrain:mountain;icon=image:mine,sticky:1', + %w[D11] => 'town=revenue:0;town=revenue:0', + %w[E6 E10] => 'city=revenue:0', + %w[E8] => 'upgrade=cost:80,terrain:mountain', + %w[E12] => 'upgrade=cost:40,terrain:mountain', + %w[F11] => 'city=revenue:0;upgrade=cost:20,terrain:water', + %w[G4] => 'upgrade=cost:40,terrain:mountain;border=type:province,color:brown,edge:4;border=type:province,color:brown,edge:5', + %w[G6] => 'town=revenue:0;town=revenue:0;border=type:province,color:brown,edge:1', + %w[G8] => 'city=revenue:0', + %w[G10] => 'upgrade=cost:20,terrain:water;icon=image:mine,sticky:1', + %w[H3] => 'upgrade=cost:40,terrain:mountain;border=type:province,color:brown,edge:4', + %w[H5] => 'border=type:province,color:brown,edge:0;border=type:province,color:brown,edge:1;border=type:province,color:brown,edge:2', + %w[H9] => 'city=revenue:0', + %w[I4] => 'upgrade=cost:40,terrain:mountain;icon=image:mine,sticky:1;border=type:province,color:brown,edge:3;border=type:province,color:brown,edge:4;border=type:province,color:brown,edge:5', + %w[I6] => 'upgrade=cost:20,terrain:water;border=type:province,color:brown,edge:1', + %w[I8 I12] => 'city=revenue:0', + %w[J3] => 'city=revenue:0;border=type:province,color:brown,cost:80,edge:4;border=type:impassable,edge:5', + %w[J5] => 'city=revenue:0;border=type:province,color:brown,cost:80,edge:1;border=type:province,color:brown,edge:2', + %w[J9 J11] => 'town=revenue:0;town=revenue:0', + %w[K4] => 'border=type:impassable,edge:2', + %w[K8] => 'town=revenue:0;town=revenue:0;upgrade=cost:20,terrain:water', + %w[L5 L7] => 'city=revenue:0', + }, + } + end + # rubocop:enable Layout/LineLength + + def map_britain_game_companies + [ + { + name: 'DGN Charter', + sym: 'DGN', + value: 0, + revenue: 0, + desc: 'Allows opening DGN corporation', + color: '#50c878', + }, + { + name: 'GFN Charter', + sym: 'GFN', + value: 0, + revenue: 0, + desc: 'Allows opening GFN corporation', + color: '#999999', + }, + { + name: 'PHX Charter', + sym: 'PHX', + value: 0, + revenue: 0, + desc: 'Allows opening PHX corporation', + color: '#ff7518', + text_color: 'black', + }, + { + name: 'KKN Charter', + sym: 'KKN', + value: 0, + revenue: 0, + desc: 'Allows opening KKN corporation', + color: '#0096ff', + }, + { + name: 'SPX Charter', + sym: 'SPX', + value: 0, + revenue: 0, + desc: 'Allows opening SPX corporation', + color: '#fafa33', + text_color: 'black', + }, + { + name: 'PGS Charter', + sym: 'PGS', + value: 0, + revenue: 0, + desc: 'Allows opening PGS corporation', + color: '#cd79a7', + text_color: 'black', + }, + ] + end + + # DGN GFN PHX KKN SPX PGS + def map_britain_game_corporations(corps) + corps.append( + { + float_percent: 20, + sym: 'PGS', + name: 'Pegasus', + logo: 'System18/PGS', + tokens: [0, 40, 100], + coordinates: nil, + color: '#cd79a7', + reservation_color: nil, + max_ownership_percent: 60, + } + ) + corps.each_with_index do |c, idx| + c[:float_percent] = 20 + c[:always_market_price] = true + c[:tokens] = [40, 100, 100, 100] + c[:coordinates] = [%w[F7 I8], 'J5', %w[F9 G8], 'A6', 'I12', 'A8'][idx] + c[:city] = [1, nil, 1, nil, nil, nil][idx] + end + corps + end + + def map_britain_game_cash + { 3 => 420, 4 => 315, 5 => 250 } + end + + def map_britain_game_cert_limit + { 3 => 16, 4 => 12, 5 => 10 } + end + + def map_britain_game_capitalization + :incremental + end + + def map_britain_game_market + self.class::MARKET_1D + end + + def map_britain_game_trains(trains) + # don't use D trains + trains.delete(find_train(trains, 'D')) + find_train(trains, '4')[:rusts_on] = '8' + # udpate quantities + find_train(trains, '2')[:num] = 6 + find_train(trains, '3')[:num] = 4 + find_train(trains, '4')[:num] = 3 + find_train(trains, '5')[:num] = 3 + find_train(trains, '6')[:num] = 2 + find_train(trains, '8')[:num] = 99 + trains.append({ + name: '4D', + distance: [{ 'nodes' => %w[city offboard town], 'pay' => 4, 'visit' => 99, 'multiplier' => 2 }], + price: 1000, + num: 99, + available_on: '8', + }) + trains + end + + def map_britain_game_phases + self.class::S18_INCCAP_PHASES + end + + def map_britain_post_game_phases(phases) + phases + end + + def map_britain_constants + redef_const(:CURRENCY_FORMAT_STR, '£%s') + redef_const(:TILE_UPGRADES_MUST_USE_MAX_EXITS, %i[unlabeled_cities]) + redef_const(:TILE_LAYS, [{ lay: true, upgrade: true, cost: 0 }, { lay_replaced: :if_green_upgraded }]) + end + + def map_britain_setup + @britain_mines = BRITAIN_MINE_HEXES.map { |h| hex_by_id(h) } + @britain_corps_with_mines = {} + @scotland_bonus_val = hex_by_id(BRITAIN_SCOTLAND_BONUS_VAL_HEX).tile.offboards.first + @wales_bonus_val = hex_by_id(BRITAIN_WALES_BONUS_VAL_HEX).tile.offboards.first + @london_bonus_val = hex_by_id(BRITAIN_LONDON_BONUS_VAL_HEX).tile.offboards.first + @mine_bonus_val = hex_by_id(BRITAIN_MINE_BONUS_VAL_HEX).tile.offboards.first + end + + def map_britain_company_header(_company) + 'CHARTER' + end + + def map_britain_init_round + map_britain_new_parliament_round + end + + def map_britain_new_parliament_round + @log << "-- Parliament Round #{@turn} -- " + GSystem18::Round::Parliament.new(self, [ + GSystem18::Step::CharterAuction, + ]) + end + + def map_britain_next_round! + @round = + case @round + when Engine::Round::Stock + map_britain_stock_round_finished + @operating_rounds = @phase.operating_rounds + reorder_players + new_operating_round + when Round::Operating + if @round.round_num < @operating_rounds + or_round_finished + new_operating_round(@round.round_num + 1) + else + @turn += 1 + or_round_finished + or_set_finished + map_britain_new_parliament_round + end + else # Parliament Round + init_round_finished + new_stock_round + end + end + + # remove un-excersized charters from players + def map_britain_stock_round_finished + @players.each do |player| + player.companies.dup.each do |c| + @log << "Right to open #{c.sym} lapses for #{player.name}" + player.companies.delete(c) + c.owner = nil + end + end + end + + def map_britain_can_par?(corporation, entity) + !corporation.ipoed && entity.companies.find { |c| c.sym == corporation.name } + end + + def map_britain_after_par(corporation) + entity = corporation.owner + + company = entity.companies.find { |c| c.sym == corporation.name } + raise GameError, 'Logic error, no matching company found' unless company + + entity.companies.delete(company) + company.close! + end + + # can the corporation reach an icon? + def map_britain_can_remove_icon?(entity) + return false unless entity.corporation? + return false if @britain_corps_with_mines[entity.name] + + @britain_mines.any? { |m| @graph.reachable_hexes(entity).include?(m) } + end + + def map_britain_icon_hexes(entity) + return [] unless entity.corporation? + return [] if @britain_corps_with_mines[entity.name] + + @britain_mines.select { |m| @graph.reachable_hexes(entity).include?(m) }.map(&:id) + end + + def map_britain_remove_icon(entity, hex_id) + return unless entity.corporation? + return if @britain_corps_with_mines[entity.name] + + hex = hex_by_id(hex_id) + hex.tile.icons.clear # should be the only icon on the tile + @log << "#{entity.name} takes mine token from #{hex_id}" + @britain_corps_with_mines[entity.name] = hex_id + end + + def map_britain_removable_icon_action_str + 'Take a mine token' + end + + def map_britain_extra_revenue_for(route, stops) + map_britain_bonuses(route, stops)[:revenue] + end + + def map_britain_extra_revenue_str(route) + bonus = map_britain_bonuses(route, route.stops)[:description] + bonus ? " + #{bonus}" : '' + end + + def map_britain_bonuses(route, stops) + train = route.train + bonus = { revenue: 0 } + return bonus unless train + + desc = [] + + # Scotland (doubles with 4D) + scotland_city = stops.find do |stop| + BRITAIN_REGION_HEXES['Scotland'].include?(stop.hex.id) && (stop.city? || stop.offboard?) + end + non_scotland_city = stops.find { |stop| !BRITAIN_REGION_HEXES['Scotland'].include?(stop.hex.id) && stop.city? } + if scotland_city && non_scotland_city + bonus[:revenue] += @scotland_bonus_val.route_revenue(@phase, train) * (train.name == '4D' ? 2 : 1) + desc << 'Scotland' + end + + # Wales (doubles with 4D) + wales_city = stops.find { |stop| BRITAIN_REGION_HEXES['Wales'].include?(stop.hex.id) && (stop.city? || stop.offboard?) } + non_wales_city = stops.find { |stop| !BRITAIN_REGION_HEXES['Wales'].include?(stop.hex.id) && stop.city? } + if wales_city && non_wales_city + bonus[:revenue] += @wales_bonus_val.route_revenue(@phase, train) * (train.name == '4D' ? 2 : 1) + desc << 'Wales' + end + + # London-Port (doubles with 4D) + london = stops.find { |stop| stop.hex.id == BRITAIN_LONDON_HEX } + port = stops.find { |stop| stop.tile.icons.any? { |i| i.name == 'port' } } + if london && port + bonus[:revenue] += @london_bonus_val.route_revenue(@phase, train) * (train.name == '4D' ? 2 : 1) + desc << 'London-Port' + end + + bonus[:description] = "(#{desc.join(', ')})" unless desc.empty? + bonus + end + + def map_britain_mine_bonus(routes) + valid_route = routes.find { |r| !r.stops.empty? } + train = valid_route&.train + if train && @britain_corps_with_mines[train.owner.name] + @mine_bonus_val.route_revenue(@phase, train) + else + 0 + end + end + + def map_britain_extra_revenue(_entity, routes) + map_britain_mine_bonus(routes) + end + + def map_britain_submit_revenue_str(routes, _show_subsidy) + train_revenue = routes_revenue(routes) + mine_revenue = map_britain_mine_bonus(routes) + return format_revenue_currency(train_revenue) if mine_revenue.zero? + + "#{format_revenue_currency(train_revenue)} + #{format_revenue_currency(mine_revenue)} mine bonus" + end + + def map_britain_status_str(corporation) + return unless @britain_corps_with_mines[corporation.name] + + "Mine (from #{@britain_corps_with_mines[corporation.name]})" + end + + def map_britain_modify_tile_lay(_entity, action) + return unless action + + if action[:lay_replaced] && @round.upgraded_track && + (@round.laid_hexes.first.tile.color == :green) + action[:lay_replaced] = true + action[:lay] = true + else + action[:lay_replaced] = nil + end + + action + end + + def map_britain_pre_lay_tile_action(action, _entity, tile_lay) + tile = action.tile + hex = action.hex + old_tile = hex.tile + + if tile_lay[:lay_replaced] && ((@round.last_old_tile != tile) || + (@tiles.count { |t| t.name == tile.name } != 1) || + (tile.color != :yellow)) + raise GameError, 'Must lay yellow tile just replaced' + end + + @round.last_old_tile = old_tile + end + + def map_britain_place_home_token(corporation) + return if corporation.tokens.first&.used + + Array(corporation.coordinates).each do |coord| + hex = hex_by_id(coord) + tile = hex&.tile + cities = tile.cities + city = cities.find { |c| c.reserved_by?(corporation) } || cities.first + token = corporation.find_token_by_type + + @log << "#{corporation.name} places a token on #{hex.name}" + city.place_token(corporation, token) + end + end + + # FIXME: add reopen! method to Engine::Company + # + # open company associated with closed corporation + # def map_britain_close_corporation_extra(corporation) + # company = companies.find { |c| c.sym == corporation.name } + # company.reopen! + # end + end + end + end +end diff --git a/lib/engine/game/g_system18/map_poland_customization.rb b/lib/engine/game/g_system18/map_poland_customization.rb index 5759545aae..377778c601 100644 --- a/lib/engine/game/g_system18/map_poland_customization.rb +++ b/lib/engine/game/g_system18/map_poland_customization.rb @@ -257,8 +257,8 @@ def map_poland_init_round def map_poland_new_parliament_round @log << "-- Parliament Round #{@turn} -- " - GSystem18::Round::PolandParliament.new(self, [ - GSystem18::Step::PolandCharterAuction, + GSystem18::Round::Parliament.new(self, [ + GSystem18::Step::CharterAuction, ]) end diff --git a/lib/engine/game/g_system18/meta.rb b/lib/engine/game/g_system18/meta.rb index e7f3f7fb99..68e82f993d 100644 --- a/lib/engine/game/g_system18/meta.rb +++ b/lib/engine/game/g_system18/meta.rb @@ -17,7 +17,7 @@ module Meta GAME_INFO_URL = 'https://github.com/tobymao/18xx/wiki/System18' GAME_RULES_URL = 'https://github.com/tobymao/18xx/wiki/System18' - PLAYER_RANGE = [2, 4].freeze + PLAYER_RANGE = [2, 5].freeze OPTIONAL_RULES = [ { sym: :map_NEUS, @@ -56,10 +56,16 @@ module Meta players: [2, 3, 4], designer: 'Ian Wilson', }, + { + sym: :map_Britain, + short_name: 'Map: Britain', + players: [3, 4, 5], + designer: 'Ian Wilson', + }, ].freeze MUTEX_RULES = [ - %i[map_NEUS map_France map_Twisting_Tracks map_UK_Limited map_China_Rapid_Development map_Poland], + %i[map_NEUS map_France map_Twisting_Tracks map_UK_Limited map_China_Rapid_Development map_Poland map_Britain], ].freeze end end diff --git a/lib/engine/game/g_system18/round/poland_parliament.rb b/lib/engine/game/g_system18/round/parliament.rb similarity index 76% rename from lib/engine/game/g_system18/round/poland_parliament.rb rename to lib/engine/game/g_system18/round/parliament.rb index 2d70736840..877462fa59 100644 --- a/lib/engine/game/g_system18/round/poland_parliament.rb +++ b/lib/engine/game/g_system18/round/parliament.rb @@ -6,7 +6,7 @@ module Engine module Game module GSystem18 module Round - class PolandParliament < Engine::Round::Auction + class Parliament < Engine::Round::Auction end end end diff --git a/lib/engine/game/g_system18/step/poland_charter_auction.rb b/lib/engine/game/g_system18/step/charter_auction.rb similarity index 75% rename from lib/engine/game/g_system18/step/poland_charter_auction.rb rename to lib/engine/game/g_system18/step/charter_auction.rb index 57549b33b7..14e9e1a3ea 100644 --- a/lib/engine/game/g_system18/step/poland_charter_auction.rb +++ b/lib/engine/game/g_system18/step/charter_auction.rb @@ -7,7 +7,7 @@ module Engine module Game module GSystem18 module Step - class PolandCharterAuction < GSystem18::Step::UpwardsAuction + class CharterAuction < GSystem18::Step::UpwardsAuction end end end diff --git a/lib/engine/game/g_system18/step/token.rb b/lib/engine/game/g_system18/step/token.rb index b725d0d49d..66e08ff3f4 100644 --- a/lib/engine/game/g_system18/step/token.rb +++ b/lib/engine/game/g_system18/step/token.rb @@ -8,7 +8,43 @@ module GSystem18 module Step class Token < Engine::Step::Token def actions(entity) - entity.receivership? ? [] : super + return [] if entity.receivership? + return [] unless entity == current_entity + + actions = [] + actions << 'place_token' if can_place_token?(entity) + actions << 'choose' if can_remove_icon?(entity) + actions << 'pass' unless actions.empty? + + actions + end + + def description + if can_place_token?(current_entity) + return "#{@game.removable_icon_action_str} or Place a Token" if can_remove_icon?(current_entity) + elsif can_remove_icon?(current_entity) + return @game.removable_icon_action_str + end + 'Place a Token' + end + + def can_remove_icon?(entity) + @game.can_remove_icon?(entity) + end + + def choices + @game.icon_hexes(current_entity) + end + + def render_choices? + false + end + + def process_choose(action) + entity = action.entity + hex_id = action.choice + + @game.remove_icon(entity, hex_id) end def process_place_token(action) @@ -24,7 +60,8 @@ def check_connected(entity, city, hex) end def tokener_available_hex(entity, hex) - @game.tokener_available_hex(entity, hex) && super + @game.icon_hexes(entity).include?(hex.id) || + (@game.tokener_available_hex(entity, hex) && super) end end end diff --git a/lib/engine/game/g_system18/step/track.rb b/lib/engine/game/g_system18/step/track.rb index 71dfa3ba86..e5b5c90817 100644 --- a/lib/engine/game/g_system18/step/track.rb +++ b/lib/engine/game/g_system18/step/track.rb @@ -15,6 +15,30 @@ def lay_tile(action, extra_cost: 0, entity: nil, spender: nil) super @game.post_lay_tile(action.entity, action.tile) end + + def get_tile_lay(entity) + action = super + @game.modify_tile_lay(entity, action) + end + + def lay_tile_action(action, entity: nil, spender: nil) + @game.pre_lay_tile_action(action, entity, get_tile_lay(action.entity)) + + super + end + + def round_state + super.merge( + { + last_old_tile: nil, + } + ) + end + + def setup + super + @round.last_old_tile = nil + end end end end diff --git a/lib/engine/step/dividend.rb b/lib/engine/step/dividend.rb index 42a55aa8b5..215afe2581 100644 --- a/lib/engine/step/dividend.rb +++ b/lib/engine/step/dividend.rb @@ -67,6 +67,7 @@ def variable_max def process_dividend(action) entity = action.entity revenue = total_revenue + subsidy = total_subsidy kind = action.kind.to_sym payout = dividend_options(entity)[kind] @@ -85,9 +86,9 @@ def process_dividend(action) @round.routes = [] @round.extra_revenue = 0 - log_run_payout(entity, kind, revenue, action, payout) + log_run_payout(entity, kind, revenue, subsidy, action, payout) - payout_corporation(payout[:corporation], entity) + payout_corporation(payout[:corporation] + subsidy, entity) payout_shares(entity, revenue - payout[:corporation]) if payout[:per_share].positive? @@ -100,7 +101,7 @@ def payout_corporation(amount, entity) @game.bank.spend(amount, entity) if amount.positive? end - def log_run_payout(entity, kind, revenue, action, payout) + def log_run_payout(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 @@ -110,6 +111,7 @@ def log_run_payout(entity, kind, revenue, action, payout) elsif payout[:per_share].zero? @log << "#{entity.name} does not run" end + @log << "#{entity.name} earns subsidy of #{@game.format_currency(subsidy)}" if subsidy.positive? end def share_price_change(_entity, revenue) @@ -217,6 +219,10 @@ def total_revenue @game.routes_revenue(routes) + extra_revenue end + def total_subsidy + @game.routes_subsidy(routes) + end + def rust_obsolete_trains!(entity, log: true) rusted_trains = entity.trains.select(&:obsolete).each do |train| @game.rust(train) diff --git a/public/logos/System18/PGS.svg b/public/logos/System18/PGS.svg new file mode 100644 index 0000000000..b4bc28fc75 --- /dev/null +++ b/public/logos/System18/PGS.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/queue.rb b/queue.rb index 211cc1073d..33fe5ba4bf 100755 --- a/queue.rb +++ b/queue.rb @@ -20,7 +20,7 @@ Bus.configure -ASSETS = Assets.new(precompiled: PRODUCTION) +ASSETS = Assets.new(precompiled: PRODUCTION, source_maps: !PRODUCTION) scheduler = Rufus::Scheduler.new diff --git a/spec/game_state_spec.rb b/spec/game_state_spec.rb index e05e98a7d2..46e67af01d 100644 --- a/spec/game_state_spec.rb +++ b/spec/game_state_spec.rb @@ -355,7 +355,7 @@ module Engine }, } expect(game.exception).to be_nil - expect(game.process_action(action).exception).to be_a(GameError) + expect { game.process_action(action) }.to raise_error(GameError) end end @@ -391,7 +391,7 @@ module Engine 'tokener' => 'BB', } expect(game.exception).to be_nil - expect(game.process_action(action).exception).to be_a(GameError) + expect { game.process_action(action) }.to raise_error(GameError) end end end diff --git a/spec/lib/engine/games/g_18_zoo_spec.rb b/spec/lib/engine/games/g_18_zoo_spec.rb index 162073ffea..dd404fbcb0 100644 --- a/spec/lib/engine/games/g_18_zoo_spec.rb +++ b/spec/lib/engine/games/g_18_zoo_spec.rb @@ -457,7 +457,7 @@ def first_player_buy_power_on_isr(company) 'slot' => 1, } expect(game.exception).to be_nil - expect(game.process_action(action).exception).to be_a(GameError) + expect { game.process_action(action) }.to raise_error(GameError) end end @@ -474,7 +474,7 @@ def first_player_buy_power_on_isr(company) 'slot' => 0, } expect(game.exception).to be_nil - expect(game.process_action(action).exception).to be_a(GameError) + expect { game.process_action(action) }.to raise_error(GameError) end end @@ -491,7 +491,7 @@ def first_player_buy_power_on_isr(company) 'slot' => 0, } expect(game.exception).to be_nil - expect(game.process_action(action).exception).to be_a(GameError) + expect { game.process_action(action) }.to raise_error(GameError) end end @@ -508,7 +508,7 @@ def first_player_buy_power_on_isr(company) 'slot' => 0, } expect(game.exception).to be_nil - expect(game.process_action(action).exception).to be_a(GameError) + expect { game.process_action(action) }.to raise_error(GameError) end end @@ -525,7 +525,7 @@ def first_player_buy_power_on_isr(company) 'target_type' => 'hex', } expect(game.exception).to be_nil - expect(game.process_action(action).exception).to be_a(GameError) + expect { game.process_action(action) }.to raise_error(GameError) end end @@ -543,7 +543,7 @@ def first_player_buy_power_on_isr(company) 'tokener' => 'PB', } expect(game.exception).to be_nil - expect(game.process_action(action).exception).to be_a(GameError) + expect { game.process_action(action) }.to raise_error(GameError) end end end diff --git a/spec/lib/engine/games/g_18eu_spec.rb b/spec/lib/engine/games/g_18eu_spec.rb index 43d0a5595a..e75886d3c3 100644 --- a/spec/lib/engine/games/g_18eu_spec.rb +++ b/spec/lib/engine/games/g_18eu_spec.rb @@ -75,7 +75,7 @@ module Engine expect(game.current_entity.shares.length).to be 6 # Prevent player from attempting to purchase > 60% through normal stock buy. - expect(game.process_action(action).exception).to be_a(GameError) + expect { game.process_action(action) }.to raise_error(GameError) expect(game.current_entity.shares.length).to be 6 end end