diff --git a/lib/engine/game/g_18_fr.rb b/lib/engine/game/g_18_fr.rb new file mode 100644 index 0000000000..8fa370d50c --- /dev/null +++ b/lib/engine/game/g_18_fr.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Engine + module Game + module G18FR + end + end +end diff --git a/lib/engine/game/g_18_fr/entities.rb b/lib/engine/game/g_18_fr/entities.rb new file mode 100644 index 0000000000..7a99e6d21e --- /dev/null +++ b/lib/engine/game/g_18_fr/entities.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Engine + module Game + module G18FR + module Entities + COMPANIES = [].freeze + end + end + end +end diff --git a/lib/engine/game/g_18_fr/game.rb b/lib/engine/game/g_18_fr/game.rb new file mode 100644 index 0000000000..22458065e1 --- /dev/null +++ b/lib/engine/game/g_18_fr/game.rb @@ -0,0 +1,213 @@ +# frozen_string_literal: true + +require_relative '../g_1817/game' +require_relative 'meta' +require_relative 'entities' +require_relative 'map' + +module Engine + module Game + module G18FR + class Game < G1817::Game + include_meta(G18FR::Meta) + include G18FR::Entities + include G18FR::Map + + CURRENCY_FORMAT_STR = '%s F' + + BANK_CASH = 99_999 + + CERT_LIMIT = { 3 => 16, 4 => 12, 5 => 10, 6 => 8 }.freeze + + STARTING_CASH = { 3 => 380, 4 => 290, 5 => 220, 6 => 190 }.freeze + + NEXT_SR_PLAYER_ORDER = :first_to_pass + + CAPITALIZATION = :incremental + + MUST_SELL_IN_BLOCKS = false + + MARKET = [ + %w[0l + 0a + 0a + 0a + 40 + 45p + 50p + 55s + 60p + 65p + 70s + 80p + 90p + 100p + 110p + 120s + 135p + 150p + 165p + 180p + 200p + 220 + 245 + 270 + 300 + 330 + 360 + 400 + 440 + 490 + 540 + 600 + 660 + 720 + 780 + 840 + 900], + ].freeze + + PHASES = [ + { + name: 'Yellow', + train_limit: 4, + tiles: [:yellow], + operating_rounds: 2, + corporation_sizes: [2], + }, + { + name: 'Yellow+', + on: '2+', + train_limit: 4, + tiles: [:yellow], + operating_rounds: 2, + corporation_sizes: [2, 5], + }, + { + name: 'Green', + on: '3+', + train_limit: 4, + tiles: %i[yellow green], + operating_rounds: 2, + corporation_sizes: [2, 5], + status: ['two_tile_lays'], + }, + { + name: 'Blue', + on: '3P', + train_limit: 3, + tiles: %i[yellow green], + operating_rounds: 2, + corporation_sizes: [2, 5], + status: ['free_ports'], + }, + { + name: 'Brown', + on: '5', + train_limit: 3, + tiles: %i[yellow green brown], + operating_rounds: 2, + corporation_sizes: [2, 5], + }, + { + name: 'Gray', + on: '6*D', + train_limit: 3, + tiles: %i[yellow green brown], + operating_rounds: 2, + corporation_sizes: [2, 5], + }, + ].freeze + + TRAINS = [{ name: '2', distance: 2, price: 100, rusts_on: '3P', num: 40 }, + { name: '2+', distance: 2, price: 100, obsolete_on: '3P', num: 4 }, + { name: '3+', distance: 3, price: 300, obsolete_on: 'G*D', num: 12 }, + { name: '3P', distance: 3, price: 400, num: 1 }, + { name: '2P', distance: 2, price: 300, num: 5 }, + { name: '5', distance: 5, price: 600, num: 6 }, + { name: '6*D', distance: 6, price: 800, num: 30 }, + { name: '2P*', distance: 2, price: 200, num: 1 }].freeze + + STATUS_TEXT = Base::STATUS_TEXT.merge( + 'two_tile_lays' => ['Two tiles', 'Corporations may lay two tiles for 20 F. One may be upgrade.'\ + 'Can\'t upgrade the tile just laid'], + 'free_ports' => ['Free ports', 'Ports no longer count towards train length'] + ).freeze + + ONE_YELLOW_TILE_LAY = [{ lay: true, upgrade: false }].freeze + TWO_TILE_LAYS = [ + { lay: true, upgrade: true }, + { lay: true, upgrade: :not_if_upgraded, cost: 20, cannot_reuse_same_hex: true }, + ].freeze + + B_HEX_NAMES = %w[E8 H3].freeze + YELLOW_B_TILE_NAME = 'FRBY' + GREEN_B_TILE_NAME = 'FRBG' + + def setup + @extra_tile_lay = false + @free_ports = false + end + + def init_round + # skipping the initial auction for now + @log << "-- #{round_description('Stock', 1)} --" + @round_counter = 1 + stock_round + end + + def stock_round + close_bank_shorts + @interest_fixed = nil + + G1817::Round::Stock.new(self, [ + Engine::Step::DiscardTrain, + Engine::Step::HomeToken, + G18FR::Step::BuySellParShares, + ]) + end + + def operating_round(round_num) + @interest_fixed = interest_rate + + G1817::Round::Operating.new(self, [ + G1817::Step::Bankrupt, + G1817::Step::CashCrisis, + G1817::Step::Loan, + G1817::Step::SpecialTrack, + G1817::Step::Assign, + G18FR::Step::Track, + Engine::Step::Token, + Engine::Step::Route, + G1817::Step::Dividend, + Engine::Step::DiscardTrain, + G1817::Step::BuyTrain, + ], round_num: round_num) + end + + def after_phase_change(name) + case name + when 'Green' + @log << '-- From now on, corporations may lay two for 20 F. Only one may be upgrade --' + @extra_tile_lay = true + when 'Blue' + @log << '-- From now on, corporations may lay two for 20 F. Only one may be upgrade --' + @free_ports = true + end + end + + def tile_lays(_entity) + @extra_tile_lay ? TWO_TILE_LAYS : ONE_YELLOW_TILE_LAY + end + + def upgrades_to?(from, to, _special = false, selected_company: nil) + # This is needed because yellow B tile adds a town (and green tile removes it) + return YELLOW_B_TILE_NAME == to.name if B_HEX_NAMES.include?(from.hex&.name) && from.color == :white + return GREEN_B_TILE_NAME == to.name if from.name == YELLOW_B_TILE_NAME && from.color == :yellow + + super + end + end + end + end +end diff --git a/lib/engine/game/g_18_fr/map.rb b/lib/engine/game/g_18_fr/map.rb new file mode 100644 index 0000000000..04396a9366 --- /dev/null +++ b/lib/engine/game/g_18_fr/map.rb @@ -0,0 +1,209 @@ +# frozen_string_literal: true + +module Engine + module Game + module G18FR + module Map + LAYOUT = :pointy + TILE_TYPE = :lawson + + TILES = { + '5' => 3, + '6' => 3, + '57' => 3, + 'FRC1' => + { + 'count' => 1, + 'color' => 'yellow', + 'code' => 'city=revenue:20;path=a:1,b:_0;path=a:4,b:_0;label=C', + }, + 'FRC2' => + { + 'count' => 1, + 'color' => 'yellow', + 'code' => 'city=revenue:20;path=a:1,b:_0;path=a:3,b:_0;label=C', + }, + 'FRC3' => + { + 'count' => 1, + 'color' => 'yellow', + 'code' => 'city=revenue:20;path=a:1,b:_0;path=a:2,b:_0;label=C', + }, + 'FRBY' => + { + 'count' => 2, + 'color' => 'yellow', + 'code' => 'city=revenue:30;town=revenue:30;path=a:1,b:_0;path=a:3,b:_0;path=a:5,b:_1;'\ + 'upgrade=cost:20,terrain:water;label=B', + }, + 'FRVY' => + { + 'count' => 2, + 'color' => 'yellow', + 'code' => 'city=revenue:10;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;path=a:6,b:_0;'\ + 'label=V', + }, + 'FRX' => + { + 'count' => 3, + 'color' => 'green', + 'code' => 'city=revenue:green_30|brown_40,slots:2;path=a:1,b:_0;path=a:2,b:_0;path=a:4,b:_0;path=a:5,b:_0', + }, + 'FRY' => + { + 'count' => 3, + 'color' => 'green', + 'code' => 'city=revenue:green_30|brown_40,slots:2;path=a:1,b:_0;path=a:2,b:_0;path=a:4,b:_0;path=a:6,b:_0', + }, + 'FRK' => + { + 'count' => 3, + 'color' => 'green', + 'code' => 'city=revenue:green_30|brown_40,slots:2;path=a:1,b:_0;path=a:2,b:_0;path=a:3,b:_0;path=a:4,b:_0', + }, + 'FRCX' => + { + 'count' => 2, + 'color' => 'green', + 'code' => 'city=revenue:green_40|brown_50|gray_60,slots:2;path=a:1,b:_0;path=a:2,b:_0;path=a:4,b:_0;path=a:5,b:_0;'\ + 'label=C', + }, + 'FRCY' => + { + 'count' => 2, + 'color' => 'green', + 'code' => 'city=revenue:green_40|brown_50|gray_60,slots:2;path=a:1,b:_0;path=a:2,b:_0;path=a:4,b:_0;path=a:6,b:_0;'\ + 'label=C', + }, + 'FRCK' => + { + 'count' => 2, + 'color' => 'green', + 'code' => 'city=revenue:green_50|brown_50|gray_60,slots:2;path=a:1,b:_0;path=a:2,b:_0;path=a:3,b:_0;path=a:4,b:_0;'\ + 'label=C', + }, + 'FRBG' => + { + 'count' => 2, + 'color' => 'green', + 'code' => 'city=revenue:50,slots:2;path=a:1,b:_0;path=a:3,b:_0;path=a:5,b:_0;upgrade=cost:20,terrain:water;'\ + 'label=B', + }, + 'FRAG' => + { + 'count' => 1, + 'color' => 'green', + 'code' => 'city=revenue:60,loc:1.5;city=revenue:60,loc:4.5;path=a:1,b:_0;path=a:2,b:_0;path=a:4,b:_1;path=a:5,b:_1;'\ + 'upgrade=cost:20,terrain:water;label=A', + }, + 'FRVG' => + { + 'count' => 1, + 'color' => 'green', + 'code' => 'city=revenue:30,slots:2;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;'\ + 'path=a:6,b:_0;label=V', + }, + 'FRBB' => + { + 'count' => 2, + 'color' => 'brown', + 'code' => 'city=revenue:brown_60|gray_80,slots:2;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=B', + }, + 'FRAB' => + { + 'count' => 1, + 'color' => 'brown', + 'code' => 'city=revenue:brown_80|gray_100,slots:3;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=A', + }, + 'FRW' => + { + 'count' => 1, + 'color' => 'gray', + 'code' => 'town=revenue:yellow_20|green_30|brown_40;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=W', + }, + }.freeze + + LOCATION_NAMES = { + 'A10' => 'North Sea', + 'B5' => 'English Chanel', + 'B9' => 'Lille', + 'B11' => 'Belgium', + 'C6' => 'Le Havre', + 'C8' => 'Rouen', + 'C10' => 'Reims', + 'C12' => 'Luxembourg', + 'D1' => 'Atlantic Ocean', + 'D3' => 'Brest', + 'D5' => 'Rennes', + 'D7' => 'Le Mans', + 'D9' => 'Paris', + 'D13' => 'Strasbourg', + 'D15' => 'Germany', + 'E4' => 'Bay of Biscay', + 'E6' => 'Nantes', + 'E8' => 'Orléans', + 'E10' => 'Troyes', + 'E12' => 'Dijon', + 'E14' => 'Switzerland', + 'F7' => 'Poitiers', + 'F9' => 'Vichy', + 'F11' => 'Lyon', + 'F13' => 'Italy', + 'G6' => 'Bordeaux', + 'G12' => 'Nice', + 'H5' => 'Bayonne', + 'H7' => 'Toulouse', + 'H9' => 'Montpellier', + 'H11' => 'Marseille', + 'I6' => 'Spain', + 'I10' => 'Mediterranean', + }.freeze + + HEXES = { + blue: { + %w[C4 G4] => 'offboard=revenue:20;path=a:4,b:_0;path=a:5,b:_0;icon=image:18_fr/beach', + ['A10'] => 'offboard=revenue:30;path=a:6,b:_0;icon=image:port', + ['B5'] => 'offboard=revenue:20;path=a:5,b:_0;icon=image:port', + ['D1'] => 'offboard=revenue:40;path=a:4,b:_0;icon=image:port', + ['E4'] => 'offboard=revenue:20;path=a:4,b:_0;icon=image:port', + ['H13'] => 'offboard=revenue:20;path=a:1,b:_0;path=a:2,b:_0;icon=image:18_fr/beach', + ['I10'] => 'offboard=revenue:yellow_20|green_40;path=a:2,b:_0;path=a:3,b:_0;icon=image:port', + }, + red: { + ['B11'] => 'offboard=revenue:yellow_30|green_50|brown_60;path=a:1,b:_0;path=a:6,b:_0', + ['C12'] => 'offboard=revenue:yellow_10|green_20|brown_40|gray_100;path=a:1,b:_0', + ['D15'] => 'offboard=revenue:yellow_40|green_50|brown_60;path=a:1,b:_0', + ['E14'] => 'offboard=revenue:yellow_20|green_30|brown_40|gray_80;path=a:1,b:_0;path=a:2,b:_0', + ['F13'] => 'offboard=revenue:yellow_20|green_40|brown_50;path=a:1,b:_0;path=a:6,b:_0', + ['I6'] => 'offboard=revenue:yellow_30|green_40|brown_50|gray_60;path=a:1,b:_0;path=a:2,b:_0;path=a:3,b:_0', + }, + white: { + %w[C6 C8 D5 D13 E10 E12 G12 H5 H9] => 'city=revenue:0', + %w[B9 E6 F11] => 'city=revenue:0;label=C', + ['C10'] => 'city=revenue:0;upgrade=cost:20,terrain:water', + ['E8'] => 'city=revenue:0;upgrade=cost:40,terrain:water;label=B', + ['F9'] => 'city=revenue:0;label=V', + ['G6'] => 'city=revenue:0;upgrade=cost:20,terrain:water;label=C', + ['H7'] => 'city=revenue:0;upgrade=cost:20,terrain:water;label=B', + ['H11'] => 'city=revenue:0;upgrade=cost:10,terrain:water;label=C', + }, + gray: { + ['D3'] => 'town=revenue:20;path=a:1,b:_0;path=a:4,b:_0', + ['D7'] => 'town=revenue:20;path=a:1,b:_0;path=a:2,b:_0;path=a:3,b:_0;path=a:4,b:_0;path=a:6,b:_0', + ['D11'] => 'town=revenue:10;path=a:1,b:_0;path=a:4,b:_0;path=a:5,b:_0;path=a:6,b:_0;icon=image:tree', + ['F7'] => 'town=revenue:20;path=a:2,b:_0;path=a:3,b:_0;path=a:4,b:_0;path=a:6,b:_0', + ['G8'] => 'town=revenue:10;path=a:3,b:_0;path=a:5,b:_0;path=a:6,b:_0;icon=image:tree', + ['G10'] => 'town=revenue:10;path=a:2,b:_0;path=a:4,b:_0;path=a:6,b:_0;icon=image:tree', + ['I4'] => 'path=a:3,b:4', + }, + yellow: { + ['D9'] => 'city=revenue:40;city=revenue:40;path=a:0,b:_0;path=a:3,b:_1;upgrade=cost:20,terrain:water;label=A', + }, + }.freeze + end + end + end +end diff --git a/lib/engine/game/g_18_fr/meta.rb b/lib/engine/game/g_18_fr/meta.rb new file mode 100644 index 0000000000..af702bac32 --- /dev/null +++ b/lib/engine/game/g_18_fr/meta.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require_relative '../meta' + +module Engine + module Game + module G18FR + module Meta + include Game::Meta + + DEV_STAGE = :prealpha + DEPENDS_ON = '1817' + + GAME_DESIGNER = 'Alex Rockwell' + GAME_IMPLEMENTER = 'Jan Kłos' + GAME_INFO_URL = 'https://github.com/tobymao/18xx/wiki/18FR' + GAME_LOCATION = 'France' + GAME_RULES_URL = '' + + PLAYER_RANGE = [3, 6].freeze + end + end + end +end diff --git a/lib/engine/game/g_18_fr/step/buy_sell_par_shares.rb b/lib/engine/game/g_18_fr/step/buy_sell_par_shares.rb new file mode 100644 index 0000000000..3b4618465c --- /dev/null +++ b/lib/engine/game/g_18_fr/step/buy_sell_par_shares.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require_relative '../../g_1817/step/buy_sell_par_shares' + +module Engine + module Game + module G18FR + module Step + class BuySellParShares < G1817::Step::BuySellParShares + MIN_BID = 90 + end + end + end + end +end diff --git a/lib/engine/game/g_18_fr/step/track.rb b/lib/engine/game/g_18_fr/step/track.rb new file mode 100644 index 0000000000..f383c06ec1 --- /dev/null +++ b/lib/engine/game/g_18_fr/step/track.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require_relative '../../g_1817/step/track' +require_relative '../../../step/base' +require_relative 'tracker' + +module Engine + module Game + module G18FR + module Step + class Track < G1817::Step::Track + include G18FR::Tracker + end + end + end + end +end diff --git a/lib/engine/game/g_18_fr/step/tracker.rb b/lib/engine/game/g_18_fr/step/tracker.rb new file mode 100644 index 0000000000..af64b6595d --- /dev/null +++ b/lib/engine/game/g_18_fr/step/tracker.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require_relative '../../../step/tracker' + +module Engine + module Game + module G18FR + module Tracker + include Engine::Step::Tracker + def legal_tile_rotation?(entity_or_entities, hex, tile) + # The town is removed from the yllow B tile, so the normal path upgrade rules are not followed + return true if tile.name == @game.class::GREEN_B_TILE_NAME && tile.rotation == hex.tile.rotation + + super + end + end + end + end +end diff --git a/public/icons/18_fr/beach.svg b/public/icons/18_fr/beach.svg new file mode 100644 index 0000000000..59eba79e26 --- /dev/null +++ b/public/icons/18_fr/beach.svg @@ -0,0 +1 @@ + \ No newline at end of file