diff --git a/lib/engine/game/g_18_uruguay/game.rb b/lib/engine/game/g_18_uruguay/game.rb index 12f52c83a7..ef9310f3b8 100644 --- a/lib/engine/game/g_18_uruguay/game.rb +++ b/lib/engine/game/g_18_uruguay/game.rb @@ -10,6 +10,8 @@ require_relative 'loans' require_relative '../../loan' require_relative 'ability_ship' +require_relative 'goods' +require_relative 'step/route_rptla' module Engine module Game @@ -26,9 +28,9 @@ class Game < Game::Base include Phases include InterestOnLoans include Loans + include Goods EBUY_SELL_MORE_THAN_NEEDED = true - GOODS_TRAIN = 'Goods' register_colors(darkred: '#ff131a', red: '#d1232a', @@ -44,7 +46,6 @@ class Game < Game::Base SELL_BUY_ORDER = :sell_buy TILE_RESERVATION_BLOCKS_OTHERS = true CURRENCY_FORMAT_STR = '$U%d' - GOODS_DESCRIPTION_STR = 'Number of goods: ' MUST_BUY_TRAIN = :always @@ -173,6 +174,7 @@ def cattle_farm def setup super + goods_setup @rptla = @corporations.find { |c| c.id == 'RPTLA' } @fce = @corporations.find { |c| c.id == 'FCE' } @@ -274,7 +276,8 @@ def operating_round(round_num) Engine::Step::HomeToken, Engine::Step::Track, G18Uruguay::Step::Token, - Engine::Step::Route, + G18Uruguay::Step::Route, + G18Uruguay::Step::RouteRptla, G18Uruguay::Step::Dividend, G18Uruguay::Step::DiscardTrain, G18Uruguay::Step::BuyTrain, @@ -338,26 +341,6 @@ def perform_ebuy_loans(entity, remaining) end end - # Goods - def number_of_goods_at_harbor - ability = @rptla.abilities.find { |a| a.type == 'Goods' } - ability.description[/\d+/].to_i - end - - def add_good_to_rptla - ability = @rptla.abilities.find { |a| a.type == 'Goods' } - count = number_of_goods_at_harbor + 1 - ability.description = GOODS_DESCRIPTION_STR + count.to_s - end - - def remove_goods_from_rptla(goods_count) - return if number_of_goods_at_harbor < goods_count - - ability = @rptla.abilities.find { |a| a.type == 'Goods' } - count = number_of_goods_at_harbor - goods_count - ability.description = GOODS_DESCRIPTION_STR + count.to_s - end - def check_distance(route, visits, train = nil) @round.current_routes[route.train] = route if route.corporation != @rptla && !nationalized? @@ -372,10 +355,21 @@ def check_distance(route, visits, train = nil) end # Revenue + def revenue_str_rptla(route) + count = goods_on_ship(route.train) + str = 'No goods' + str = count.to_s + ' good' if count.positive? + str += 's' if count > 1 + str + end + def revenue_str(route) - return super unless route&.corporation == @rptla + return revenue_str_rptla(route) if route&.corporation == @rptla - 'Ship' + good_hex = good_pickup_hex(route.train) + str = super + str += '+Good(' + good_hex.id + ')' unless good_hex.nil? + str end def rptla_revenue(corporation) @@ -397,7 +391,7 @@ def revenue_for(route, stops) return revenue unless route&.corporation == @rptla train = route.train - revenue * goods_on_train(train) + revenue * goods_on_ship(train) end def or_round_finished diff --git a/lib/engine/game/g_18_uruguay/goods.rb b/lib/engine/game/g_18_uruguay/goods.rb new file mode 100644 index 0000000000..bb1dc1e2fd --- /dev/null +++ b/lib/engine/game/g_18_uruguay/goods.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +module Engine + module Game + module G18Uruguay + module Goods + GOODS_DESCRIPTION_STR = 'Number of goods: ' + + def goods_setup + @pickup_hex_for_train = {} + @goods_on_ship = {} + @number_of_goods_at_harbor = 0 + end + + # Train delivery + def train_with_goods?(train) + return unless train + + @pickup_hex_for_train.key?(train.id) + end + + def attach_good_to_train(train, hex) + @pickup_hex_for_train[train.id] = hex + end + + def good_pickup_hex(train) + @pickup_hex_for_train[train.id] + end + + def unload_good(train) + @pickup_hex_for_train.delete(train.id) if train_with_goods?(train) + end + + # Harbor + def visits_include_port?(visits) + visits.any? { |visit| self.class::PORTS.include?(visit.hex.id) } + end + + def route_include_port?(route) + route.hexes.any? { |hex| self.class::PORTS.include?(hex.id) } + end + + def check_for_goods_if_run_to_port(route, visits) + true if route.corporation == @rptla + visits_include_port?(visits) || !train_with_goods?(route.train) + end + + def check_for_port_if_goods_attached(route, visits) + true if route.corporation == @rptla + !visits_include_port?(visits) || train_with_goods?(route.train) + end + + def number_of_goods_at_harbor + @number_of_goods_at_harbor + end + + def add_good_to_rptla + ability = @rptla.abilities.find { |a| a.type == :Goods } + return if ability.nil? + + @number_of_goods_at_harbor += 1 + ability.description = GOODS_DESCRIPTION_STR + @number_of_goods_at_harbor.to_s + end + + def remove_goods_from_rptla(goods_count) + return if @number_of_goods_at_harbor < goods_count + + ability = @rptla.abilities.find { |a| a.type == :Goods } + return if ability.nil? + + @number_of_goods_at_harbor -= goods_count + ability.description = GOODS_DESCRIPTION_STR + count.to_s + end + + # Ship gooods + def add_goods_to_ship(ship, count) + @goods_on_ship[ship.id] = count + end + + def remove_goods_from_ship(ship) + return unless @goods_on_ship.key?(ship.id) + + @goods_on_ship[ship.id] = 0 + end + + def goods_on_ship(ship) + return 0 unless @goods_on_ship.key?(ship.id) + + @goods_on_ship[ship.id] + end + end + end + end +end diff --git a/lib/engine/game/g_18_uruguay/step/route.rb b/lib/engine/game/g_18_uruguay/step/route.rb new file mode 100644 index 0000000000..d82a07e80d --- /dev/null +++ b/lib/engine/game/g_18_uruguay/step/route.rb @@ -0,0 +1,130 @@ +# frozen_string_literal: true + +require_relative '../../../step/route' + +module Engine + module Game + module G18Uruguay + module Step + class Route < Engine::Step::Route + def setup + @round.current_routes = {} + end + + def actions(entity) + return %w[].freeze if entity.corporation == @game.rptla + return [] if !entity.operator? || @game.route_trains(entity).empty? || !@game.can_run_route?(entity) + return [] if entity.corporation? && entity.type == :minor + + actions = ACTIONS.dup + actions << 'choose' if choosing?(entity) + actions + end + + def choosing?(_entity) + true + end + + def choice_name + return 'Attach goods to ships' if current_entity == @game.rptla + + 'Attach good to a train' + end + + def goods_hexes + @game.hexes.select do |hex| + hex.assignments.keys.find { |a| a.include? 'GOODS' } + end + end + + def choices + choices = {} + goods_train_choices(current_entity).each_with_index do |train, _index| + hex = train['hex'] + index_str = "train\##{train['train_index']}" + index_str += "_#{train['hex'].id}" unless hex.nil? + choices[index_str] = "#{train['train'].name} train\##{train['train_index']} (#{train['hex'].id})" unless hex.nil? + choices[index_str] = "#{train['train'].name} train\##{train['train_index']} unload" if hex.nil? + end + choices + end + + def route_for_train(train) + @round.current_routes[train] unless train.nil? + end + + def get_train_goods_combo(name) + str_split = name.split('_') + train_index = str_split[0].split('#')[1] + train = @game.route_trains(current_entity)[train_index.to_i - 1] + hex = @game.hex_by_id(str_split[1]) if str_split.size > 1 + [train, hex] + end + + def goods_train_choices(entity) + choices_array = [] + @game.route_trains(entity).each_with_index do |train, index| + route = route_for_train(train) + if @game.train_with_goods?(train) + choices_array.push({ train: train, train_index: index + 1, hex: nil, loaded: true }) + else + goods_hexes.each do |hex| + if route + val = { train: train, train_index: index + 1, hex: hex, loaded: false } + choices_array.push(val) if route.hexes.include?(hex) + end + end + end + end + choices_array + end + + def process_choose(action) + entity = action.entity + + train, hex = get_train_goods_combo(action.choice) + + if hex + @log << "#{entity.id} attaches good from #{hex.id} to a #{train.name} train" + + @game.attach_good_to_train(train, hex) + else + @log << "#{entity.id} remove good from #{train.name} train" + @game.unload_good(train) + end + end + + def detach_goods(routes) + routes.each do |route| + train = route.train + next unless @game.train_with_goods?(train) + + hex = @game.good_pickup_hex(train) + good = hex.assignments.keys.find { |a| a.include? 'GOODS' } + @game.unload_good(train) + raise NoToken, "No good token found at Hex #{hex&.id}" if good.nil? + raise NoToken, "Hex #{hex&.id} is not included in route for train #{train.name}" unless route.hexes.include?(hex) + + hex.remove_assignment!(good) + @log << "#{current_entity.id} moves a good to the harbor" + @game.add_good_to_rptla unless good.nil? + end + end + + def process_run_routes(action) + super + entity = action.entity + detach_goods(action.routes) unless action.entity == @game.rptla + @game.route_trains(entity)&.each do |train| + @game.unload_good(train) + end + end + + def round_state + super.merge({ current_routes: {} }) + end + end + end + end + end +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 new file mode 100644 index 0000000000..bfae160dcf --- /dev/null +++ b/lib/engine/game/g_18_uruguay/step/route_rptla.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +require_relative '../../../step/route' + +module Engine + 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 setup + @goods_shipped = 0 + end + + def actions(entity) + return [] unless entity.corporation == @game.rptla + + %w[run_routes choose].freeze + end + + def choosing?(_entity) + true + end + + def choice_name + 'Attach goods to ships' + end + + def choices + choices = {} + number_of_goods = [@game.number_of_goods_at_harbor, total_ship_capacity?(current_entity)].min + number_of_goods.times do |count| + choices[count] = '1 Good' if count.zero? + choices[count] = (count + 1).to_s + ' Goods' if count.positive? + end + 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) } + end + + def process_choose(action) + entity = action.entity + goods_to_deliver = action.choice.to_i + 1 + ships = @game.route_trains(entity) + return unless ships + return unless ships.length.positive? + + remaining = goods_to_deliver + ships.each do |ship| + ship.name = ship.name.partition('+')[0] unless ship.nil? + capacity = ship_capacity(ship) + goods_count = [remaining, capacity].min + remaining -= goods_count + @game.add_goods_to_ship(ship, goods_count) + end + @goods_shipped = goods_to_deliver - remaining + end + + def process_run_routes(action) + super + entity = action.entity + @game.remove_goods_from_rptla(@goods_shipped) if @goods_shipped.positive? && entity == @game.rptla + @log << "#{entity.id} ships #{@goods_shipped} good to England" if @goods_shipped == 1 && entity == @game.rptla + @log << "#{entity.id} ships #{@goods_shipped} goods to England" if @goods_shipped > 1 && entity == @game.rptla + @game.route_trains(entity)&.each do |ship| + @game.remove_goods_from_ship(ship) + end + end + end + end + end + end +end