From 286a1b2d4e367be4976d8f214e7a56e847fa5456 Mon Sep 17 00:00:00 2001 From: Andrew Zwicky Date: Mon, 13 Nov 2023 17:18:16 -0600 Subject: [PATCH 001/167] Copy files from 1844, update map. Map is in a basic shape of the map, special features are missing currently. --- lib/engine/game/g_1854.rb | 8 + lib/engine/game/g_1854/entities.rb | 583 ++++++++++ lib/engine/game/g_1854/game.rb | 993 ++++++++++++++++++ lib/engine/game/g_1854/map.rb | 227 ++++ lib/engine/game/g_1854/meta.rb | 23 + lib/engine/game/g_1854/player.rb | 22 + lib/engine/game/g_1854/round/operating.rb | 23 + lib/engine/game/g_1854/round/sbb_formation.rb | 33 + lib/engine/game/g_1854/step/buy_company.rb | 19 + .../game/g_1854/step/buy_sell_par_shares.rb | 78 ++ lib/engine/game/g_1854/step/buy_train.rb | 37 + .../game/g_1854/step/company_pending_par.rb | 17 + lib/engine/game/g_1854/step/destination.rb | 36 + .../game/g_1854/step/destination_check.rb | 27 + lib/engine/game/g_1854/step/dividend.rb | 18 + .../g_1854/step/mountain_railway_track.rb | 65 ++ .../game/g_1854/step/remove_sbb_tokens.rb | 57 + lib/engine/game/g_1854/step/route.rb | 23 + lib/engine/game/g_1854/step/special_choose.rb | 30 + lib/engine/game/g_1854/step/special_track.rb | 56 + lib/engine/game/g_1854/stock_market.rb | 24 + public/icons/1854/B1.svg | 1 + public/icons/1854/B2.svg | 1 + public/icons/1854/B3.svg | 1 + public/icons/1854/B4.svg | 1 + public/icons/1854/B5.svg | 1 + public/icons/1854/T1.svg | 1 + public/icons/1854/T2.svg | 1 + public/icons/1854/T3.svg | 1 + public/icons/1854/T4.svg | 1 + public/icons/1854/T5.svg | 1 + public/icons/1854/bonus_30.svg | 1 + public/icons/1854/bonus_40.svg | 1 + public/icons/1854/bonus_50.svg | 1 + public/icons/1854/bonus_90.svg | 1 + public/icons/1854/mine.svg | 1 + 36 files changed, 2414 insertions(+) create mode 100644 lib/engine/game/g_1854.rb create mode 100644 lib/engine/game/g_1854/entities.rb create mode 100644 lib/engine/game/g_1854/game.rb create mode 100644 lib/engine/game/g_1854/map.rb create mode 100644 lib/engine/game/g_1854/meta.rb create mode 100644 lib/engine/game/g_1854/player.rb create mode 100644 lib/engine/game/g_1854/round/operating.rb create mode 100644 lib/engine/game/g_1854/round/sbb_formation.rb create mode 100644 lib/engine/game/g_1854/step/buy_company.rb create mode 100644 lib/engine/game/g_1854/step/buy_sell_par_shares.rb create mode 100644 lib/engine/game/g_1854/step/buy_train.rb create mode 100644 lib/engine/game/g_1854/step/company_pending_par.rb create mode 100644 lib/engine/game/g_1854/step/destination.rb create mode 100644 lib/engine/game/g_1854/step/destination_check.rb create mode 100644 lib/engine/game/g_1854/step/dividend.rb create mode 100644 lib/engine/game/g_1854/step/mountain_railway_track.rb create mode 100644 lib/engine/game/g_1854/step/remove_sbb_tokens.rb create mode 100644 lib/engine/game/g_1854/step/route.rb create mode 100644 lib/engine/game/g_1854/step/special_choose.rb create mode 100644 lib/engine/game/g_1854/step/special_track.rb create mode 100644 lib/engine/game/g_1854/stock_market.rb create mode 100644 public/icons/1854/B1.svg create mode 100644 public/icons/1854/B2.svg create mode 100644 public/icons/1854/B3.svg create mode 100644 public/icons/1854/B4.svg create mode 100644 public/icons/1854/B5.svg create mode 100644 public/icons/1854/T1.svg create mode 100644 public/icons/1854/T2.svg create mode 100644 public/icons/1854/T3.svg create mode 100644 public/icons/1854/T4.svg create mode 100644 public/icons/1854/T5.svg create mode 100644 public/icons/1854/bonus_30.svg create mode 100644 public/icons/1854/bonus_40.svg create mode 100644 public/icons/1854/bonus_50.svg create mode 100644 public/icons/1854/bonus_90.svg create mode 100644 public/icons/1854/mine.svg diff --git a/lib/engine/game/g_1854.rb b/lib/engine/game/g_1854.rb new file mode 100644 index 0000000000..8553738ac8 --- /dev/null +++ b/lib/engine/game/g_1854.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Engine + module Game + module G1854 + end + end +end diff --git a/lib/engine/game/g_1854/entities.rb b/lib/engine/game/g_1854/entities.rb new file mode 100644 index 0000000000..e72441df85 --- /dev/null +++ b/lib/engine/game/g_1854/entities.rb @@ -0,0 +1,583 @@ +# frozen_string_literal: true + +module Engine + module Game + module G1854 + module Entities + MOUNTAIN_HEXES = %w[H7 L13 I14 G14 F19 J29 L23].freeze + MOUNTAIN_TILES = %w[XM1 XM2 XM3].freeze + + TUNNEL_HEXES = %w[J9 I12 K14 I16 H17 H19 H21 H23 H27].freeze + TUNNEL_TILES = %w[X78 X79].freeze + + COMPANIES = [ + { + name: 'P1 - Brienzer-Rothorn-Bahn', + sym: 'P1', + value: 20, + revenue: 5, + desc: 'No special ability.', + }, + { + name: 'P2 - Bödelibahn', + sym: 'P2', + value: 50, + revenue: 10, + desc: 'Once in the game, a corporation may place an additional yellow track tile' \ + ' according to the rules. Either that corporation or its director must be' \ + ' the owner of the company.', + abilities: [ + { + type: 'tile_lay', + after_phase: '2', + when: %w[track owning_player_track], + count: 1, + reachable: true, + special: false, + tiles: %w[3 4 5 6 7 8 9 57 58], + hexes: [], + }, + ], + }, + { + name: 'P3 - Gotthard-Postkutsche', + sym: 'P3', + value: 80, + revenue: 15, + desc: 'Comes with a Tunnel company', + abilities: [{ type: 'acquire_company', company: 'T1' }], + }, + { + name: 'P4 - Furka-Oberalpbahn', + sym: 'P4', + value: 110, + revenue: 20, + desc: 'The owner of this company may at any time place the Furka-Oberalp special tile. This is an' \ + ' additional tile lay and free of cost. Doing this closes the company; its owner receives 80 SFR' \ + ' as a compensation. This happens at the latest by the sale of the first 5/5H train.' \ + " Its owner can't waive that build.", + abilities: [ + { + type: 'choose_ability', + after_phase: '2', + when: %w[track owning_player_track], + choices: ['Place tile'], + }, + ], + }, + { + name: 'P5 - Compagnie Montreaux-Montbovon', + sym: 'P5', + value: 140, + revenue: 25, + desc: "Comes with a 10% share of the MOB. This share can't be sold until MOB has been parred.", + abilities: [{ type: 'shares', shares: 'MOB_1' }], + }, + { + name: 'P6 - Societa anonima delle ferrovie', + sym: 'P6', + value: 180, + revenue: 30, + desc: "Comes with the Director's share of the FNM. When purchased, the owner sets the par price for the FNM" \ + ' and it immediately floats, with 3 shares going to the market. The company closes when the FNM runs' \ + ' a train for the first time.', + abilities: [{ type: 'close', when: 'ran_train', corporation: 'FNM' }, + { type: 'no_buy' }, + { type: 'shares', shares: 'FNM_0' }], + }, + { + name: 'P7 - Lokfabrik Oerlikon', + sym: 'P7', + value: 100, + revenue: 0, + desc: 'The company closes when the first 5 or 5H train is bought and the player receives the 5H train "EVA".' \ + ' They may assign it immediately or later to any corporation they are the director of. Train limits must'\ + ' be kept.', + abilities: [ + { type: 'close', on_phase: 'never' }, + { type: 'no_buy' }, + { + type: 'choose_ability', + after_phase: '4', + when: 'owning_player_or_turn', + choices: [], # Defined in special_choose step + }, + ], + }, + { + name: 'B1 - Mountain Railway', + sym: 'B1', + value: 150, + revenue: 0, + desc: 'Upon purchase, select an unused revenue tile and assign it to an unoccupied mountain. Available' \ + ' revenue tiles are (2) 10/20/50/80, (2) 10/40/50/60, and (2) 10/50/80/10. Once the selected' \ + " mountain is included in any corporation's route, this company pays revenue of 40 SFR at the" \ + ' beginning of each operating round and is worth 150 SFR at end game. Each player can only' \ + ' purchase one Mountain Railway per stock round. Does not close and does not count against' \ + ' certificate limit.', + color: 'brown', + abilities: [ + { type: 'close', on_phase: 'never' }, + { type: 'no_buy' }, + { + type: 'tile_lay', + when: 'stock_round', + owner_type: 'player', + blocks: true, + count: 1, + tiles: MOUNTAIN_TILES, + hexes: MOUNTAIN_HEXES, + }, + ], + }, + { + name: 'B2 - Mountain Railway', + sym: 'B2', + value: 150, + revenue: 0, + desc: 'Upon purchase, select an unused revenue tile and assign it to an unoccupied mountain. Available' \ + ' revenue tiles are (2) 10/20/50/80, (2) 10/40/50/60, and (2) 10/50/80/10. Once the selected' \ + " mountain is included in any corporation's route, this company pays revenue of 40 SFR at the" \ + ' beginning of each operating round and is worth 150 SFR at end game. Each player can only' \ + ' purchase one Mountain Railway per stock round. Does not close and does not count against' \ + ' certificate limit.', + color: 'brown', + abilities: [ + { type: 'close', on_phase: 'never' }, + { type: 'no_buy' }, + { + type: 'tile_lay', + when: 'stock_round', + blocks: true, + count: 1, + tiles: MOUNTAIN_TILES, + hexes: MOUNTAIN_HEXES, + }, + ], + }, + { + name: 'B3 - Mountain Railway', + sym: 'B3', + value: 150, + revenue: 0, + desc: 'Upon purchase, select an unused revenue tile and assign it to an unoccupied mountain. Available' \ + ' revenue tiles are (2) 10/20/50/80, (2) 10/40/50/60, and (2) 10/50/80/10. Once the selected' \ + " mountain is included in any corporation's route, this company pays revenue of 40 SFR at the" \ + ' beginning of each operating round and is worth 150 SFR at end game. Each player can only' \ + ' purchase one Mountain Railway per stock round. Does not close and does not count against' \ + ' certificate limit.', + color: 'brown', + abilities: [ + { type: 'close', on_phase: 'never' }, + { type: 'no_buy' }, + { + type: 'tile_lay', + when: 'stock_round', + blocks: true, + count: 1, + tiles: MOUNTAIN_TILES, + hexes: MOUNTAIN_HEXES, + }, + ], + }, + { + name: 'B4 - Mountain Railway', + sym: 'B4', + value: 150, + revenue: 0, + desc: 'Upon purchase, select an unused revenue tile and assign it to an unoccupied mountain. Available' \ + ' revenue tiles are (2) 10/20/50/80, (2) 10/40/50/60, and (2) 10/50/80/10. Once the selected' \ + " mountain is included in any corporation's route, this company pays revenue of 40 SFR at the" \ + ' beginning of each operating round and is worth 150 SFR at end game. Each player can only' \ + ' purchase one Mountain Railway per stock round. Does not close and does not count against' \ + ' certificate limit.', + color: 'brown', + abilities: [ + { type: 'close', on_phase: 'never' }, + { type: 'no_buy' }, + { + type: 'tile_lay', + when: 'stock_round', + blocks: true, + count: 1, + tiles: MOUNTAIN_TILES, + hexes: MOUNTAIN_HEXES, + }, + ], + }, + { + name: 'B5 - Mountain Railway', + sym: 'B5', + value: 150, + revenue: 0, + desc: 'Upon purchase, select an unused revenue tile and assign it to an unoccupied mountain. Available' \ + ' revenue tiles are (2) 10/20/50/80, (2) 10/40/50/60, and (2) 10/50/80/10. Once the selected' \ + " mountain is included in any corporation's route, this company pays revenue of 40 SFR at the" \ + ' beginning of each operating round and is worth 150 SFR at end game. Each player can only' \ + ' purchase one Mountain Railway per stock round. Does not close and does not count against' \ + ' certificate limit.', + color: 'brown', + abilities: [ + { type: 'close', on_phase: 'never' }, + { type: 'no_buy' }, + { + type: 'tile_lay', + when: 'stock_round', + blocks: true, + count: 1, + tiles: MOUNTAIN_TILES, + hexes: MOUNTAIN_HEXES, + }, + ], + }, + { + name: 'T1 - Tunnel Company', + sym: 'T1', + value: 50, + revenue: 0, + desc: "As an additional tile lay action, one of the owning player's corporations may place a tunnel" \ + ' on an unused tunnel hex that it can reach for 100 SFR. Once the tunnel is included in any' \ + " corporation's route, this company pays revenue of 10 SFR at the beginning of each operating" \ + ' round and is worth 50 SFR at end game. Each player can only purchase one Tunnel Company per' \ + ' stock round. Does not close and does not count against certificate limit.', + color: 'gray', + abilities: [ + { + type: 'tile_lay', + when: 'owning_player_track', + count: 1, + cost: 100, + reachable: true, + tiles: TUNNEL_TILES, + hexes: TUNNEL_HEXES, + }, + { type: 'close', on_phase: 'never' }, + { type: 'no_buy' }, + ], + }, + { + name: 'T2 - Tunnel Company', + sym: 'T2', + value: 50, + revenue: 0, + desc: "As an additional tile lay action, one of the owning player's corporations may place a tunnel" \ + ' on an unused tunnel hex that it can reach for 100 SFR. Once the tunnel is included in any' \ + " corporation's route, this company pays revenue of 10 SFR at the beginning of each operating" \ + ' round and is worth 50 SFR at end game. Each player can only purchase one Tunnel Company per' \ + ' stock round. Does not close and does not count against certificate limit.', + color: 'gray', + abilities: [ + { + type: 'tile_lay', + when: 'owning_player_track', + count: 1, + cost: 100, + reachable: true, + tiles: TUNNEL_TILES, + hexes: TUNNEL_HEXES, + }, + { type: 'close', on_phase: 'never' }, + { type: 'no_buy' }, + ], + }, + { + name: 'T3 - Tunnel Company', + sym: 'T3', + value: 50, + revenue: 0, + desc: "As an additional tile lay action, one of the owning player's corporations may place a tunnel" \ + ' on an unused tunnel hex that it can reach for 100 SFR. Once the tunnel is included in any' \ + " corporation's route, this company pays revenue of 10 SFR at the beginning of each operating" \ + ' round and is worth 50 SFR at end game. Each player can only purchase one Tunnel Company per' \ + ' stock round. Does not close and does not count against certificate limit.', + color: 'gray', + abilities: [ + { + type: 'tile_lay', + when: 'owning_player_track', + count: 1, + cost: 100, + reachable: true, + tiles: TUNNEL_TILES, + hexes: TUNNEL_HEXES, + }, + { type: 'close', on_phase: 'never' }, + { type: 'no_buy' }, + ], + }, + { + name: 'T4 - Tunnel Company', + sym: 'T4', + value: 50, + revenue: 0, + desc: "As an additional tile lay action, one of the owning player's corporations may place a tunnel" \ + ' on an unused tunnel hex that it can reach for 100 SFR. Once the tunnel is included in any' \ + " corporation's route, this company pays revenue of 10 SFR at the beginning of each operating" \ + ' round and is worth 50 SFR at end game. Each player can only purchase one Tunnel Company per' \ + ' stock round. Does not close and does not count against certificate limit.', + color: 'gray', + abilities: [ + { + type: 'tile_lay', + when: 'owning_player_track', + count: 1, + cost: 100, + reachable: true, + tiles: TUNNEL_TILES, + hexes: TUNNEL_HEXES, + }, + { type: 'close', on_phase: 'never' }, + { type: 'no_buy' }, + ], + }, + { + name: 'T5 - Tunnel Company', + sym: 'T5', + value: 50, + revenue: 0, + desc: "As an additional tile lay action, one of the owning player's corporations may place a tunnel" \ + ' on an unused tunnel hex that it can reach for 100 SFR. Once the tunnel is included in any' \ + " corporation's route, this company pays revenue of 10 SFR at the beginning of each operating" \ + ' round and is worth 50 SFR at end game. Each player can only purchase one Tunnel Company per' \ + ' stock round. Does not close and does not count against certificate limit.', + color: 'gray', + abilities: [ + { + type: 'tile_lay', + when: 'owning_player_track', + count: 1, + cost: 100, + reachable: true, + tiles: TUNNEL_TILES, + hexes: TUNNEL_HEXES, + }, + { type: 'close', on_phase: 'never' }, + { type: 'no_buy' }, + ], + }, + ].freeze + + CORPORATIONS = [ + { + float_percent: 50, + sym: 'NOB', + name: 'Schweizerische Nordostbahn (V1)', + logo: '1854/NOB.alt', + simple_logo: '1854/NOB.alt', + type: 'pre-sbb', + shares: [50, 25, 25], + tokens: [0, 40], + max_ownership_percent: 75, + coordinates: 'D19', + destination_coordinates: 'D15', + color: '#d8d2d3', + text_color: '#363552', + }, + { + float_percent: 50, + sym: 'SCB', + name: 'Schweizerische Centralbahn (V2)', + logo: '1854/SCB.alt', + simple_logo: '1854/SCB.alt', + type: 'pre-sbb', + shares: [50, 25, 25], + tokens: [0, 40], + max_ownership_percent: 75, + coordinates: 'C12', + destination_coordinates: 'F17', + color: '#583838', + text_color: '#a2452b', + }, + { + float_percent: 50, + sym: 'VSB', + name: 'Vereinigte Schweizer Bahnen (V3)', + logo: '1854/VSB.alt', + simple_logo: '1854/VSB.alt', + type: 'pre-sbb', + shares: [50, 25, 25], + tokens: [0, 40], + max_ownership_percent: 75, + coordinates: 'C24', + destination_coordinates: 'F25', + color: '#225252', + text_color: '#d8d2d3', + }, + { + float_percent: 50, + sym: 'JS', + name: 'Jura-Simplon (V4)', + logo: '1854/JS.alt', + simple_logo: '1854/JS.alt', + type: 'pre-sbb', + shares: [50, 25, 25], + tokens: [0, 40], + max_ownership_percent: 75, + coordinates: 'I4', + city: 0, + destination_coordinates: 'F7', + color: '#d8d2d3', + text_color: '#225252', + }, + { + float_percent: 50, + sym: 'GB', + name: 'Gotthardbahn (V5)', + logo: '1854/GB.alt', + simple_logo: '1854/GB.alt', + type: 'pre-sbb', + shares: [50, 25, 25], + tokens: [0, 40], + max_ownership_percent: 75, + coordinates: 'G18', + destination_coordinates: 'H19', + color: '#c1b22b', + text_color: 'black', + }, + { + float_percent: 60, + sym: 'JN', + name: 'Jura Neuchatelois (R1)', + logo: '1854/JN.alt', + simple_logo: '1854/JN.alt', + shares: [40, 20, 20, 20], + tokens: [0, 40, 100], + type: 'regional', + coordinates: 'F7', + color: '#3f963d', + }, + { + float_percent: 60, + sym: 'ChA', + name: 'Chur-Arosa (R2)', + logo: '1854/ChA.alt', + simple_logo: '1854/ChA.alt', + shares: [40, 20, 20, 20], + tokens: [0, 40, 100], + type: 'regional', + coordinates: 'G28', + color: '#242943', + text_color: '#bcba4c', + }, + { + float_percent: 60, + sym: 'VZ', + name: 'Visp-Zermatt (R3)', + logo: '1854/VZ.alt', + simple_logo: '1854/VZ.alt', + shares: [40, 20, 20, 20], + tokens: [0, 40, 100], + type: 'regional', + coordinates: 'K10', + color: '#b02c2d', + }, + { + float_percent: 50, + sym: 'FNM', + name: 'Ferrovie Nord Milano (H1)', + logo: '1854/FNM.alt', + simple_logo: '1854/FNM.alt', + shares: [20, 10, 10, 10, 10, 10, 10, 10, 10], + tokens: [0, 40, 100, 100, 100], + type: 'historical', + coordinates: 'L21', + destination_coordinates: 'G20', + color: '#2a5f3b', + }, + { + float_percent: 50, + sym: 'RhB', + name: 'Rhätische Bahn (H2)', + logo: '1854/RhB.alt', + simple_logo: '1854/RhB.alt', + shares: [20, 10, 10, 10, 10, 10, 10, 10, 10], + tokens: [0, 40, 100, 100, 100], + type: 'historical', + coordinates: 'G26', + destination_coordinates: 'J13', + color: '#cf3334', + }, + { + float_percent: 50, + sym: 'BLS', + name: 'Bern-Lötschberg-Simplon (H3)', + logo: '1854/BLS.alt', + simple_logo: '1854/BLS.alt', + shares: [20, 10, 10, 10, 10, 10, 10, 10, 10], + tokens: [0, 40, 100, 100, 100], + type: 'historical', + coordinates: 'F11', + destination_coordinates: 'J13', + abilities: [{ type: 'assign_hexes', hexes: ['J13'], count: 1 }], + color: '#c1b22b', + }, + { + float_percent: 50, + sym: 'STB', + name: 'Sensetalbahn (H4)', + logo: '1854/STB.alt', + simple_logo: '1854/STB.alt', + shares: [20, 10, 10, 10, 10, 10, 10, 10, 10], + tokens: [0, 40, 100, 100, 100], + type: 'historical', + coordinates: 'D15', + city: 0, + destination_coordinates: 'H13', + color: '#3e3d5e', + }, + { + float_percent: 50, + sym: 'AB', + name: 'Appenzeller Bahn (H5)', + logo: '1854/AB.alt', + simple_logo: '1854/AB.alt', + shares: [20, 10, 10, 10, 10, 10, 10, 10, 10], + tokens: [0, 40, 100, 100, 100], + type: 'historical', + coordinates: 'D25', + destination_coordinates: 'C20', + color: '#d8d2d3', + text_color: 'black', + }, + { + float_percent: 50, + sym: 'MOB', + name: 'Montreux-Oberland Bernois (H6)', + logo: '1854/MOB.alt', + simple_logo: '1854/MOB.alt', + shares: [20, 10, 10, 10, 10, 10, 10, 10, 10], + tokens: [0, 40, 100, 100, 100], + type: 'historical', + coordinates: 'I6', + destination_coordinates: 'H13', + color: '#be8c3a', + }, + { + float_percent: 20, + sym: 'SBB', + name: 'Schweizer Bundesbahnen', + logo: '1854/SBB.alt', + simple_logo: '1854/SBB.alt', + shares: [10, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 10, 10, 10, 10], + tokens: [], + type: 'historical', + floatable: false, + color: '#913e2e', + abilities: [ + { + type: 'train_buy', + description: 'Buys and sells trains at face value only', + face_value: true, + }, + { + type: 'train_limit', + increase: 2, + description: 'Train limit: 4', + }, + ], + }, + ].freeze + end + end + end +end diff --git a/lib/engine/game/g_1854/game.rb b/lib/engine/game/g_1854/game.rb new file mode 100644 index 0000000000..f8e2ac7988 --- /dev/null +++ b/lib/engine/game/g_1854/game.rb @@ -0,0 +1,993 @@ +# frozen_string_literal: true + +require_relative 'entities' +require_relative 'map' +require_relative 'meta' +require_relative 'player' +require_relative 'stock_market' +require_relative '../base' + +module Engine + module Game + module G1854 + class Game < Game::Base + include_meta(G1854::Meta) + include Entities + include Map + + PLAYER_CLASS = G1854::Player + + CURRENCY_FORMAT_STR = '%s SFR' + + BANK_CASH = 12_000 + + CERT_LIMIT = { 3 => 24, 4 => 18, 5 => 15, 6 => 13, 7 => 11 }.freeze + CERT_LIMIT_INCLUDES_PRIVATES = false + + STARTING_CASH = { 3 => 800, 4 => 620, 5 => 510, 6 => 440, 7 => 400 }.freeze + + SELL_BUY_ORDER = :sell_buy + SELL_MOVEMENT = :down_block + POOL_SHARE_DROP = :left_block + NEXT_SR_PLAYER_ORDER = :most_cash + EBUY_PRES_SWAP = false + EBUY_DEPOT_TRAIN_MUST_BE_CHEAPEST = false + DISCARDED_TRAINS = :remove + + TRACK_RESTRICTION = :permissive + TILE_RESERVATION_BLOCKS_OTHERS = :always + + ASSIGNMENT_TOKENS = { + 'B1' => '/icons/1854/B1.svg', + 'B2' => '/icons/1854/B2.svg', + 'B3' => '/icons/1854/B3.svg', + 'B4' => '/icons/1854/B4.svg', + 'B5' => '/icons/1854/B5.svg', + 'T1' => '/icons/1854/T1.svg', + 'T2' => '/icons/1854/T2.svg', + 'T3' => '/icons/1854/T3.svg', + 'T4' => '/icons/1854/T4.svg', + 'T5' => '/icons/1854/T5.svg', + }.freeze + + MARKET = [ + ['', + '', + '90', + '100', + '110', + '120', + '130', + '140', + '155', + '170', + '185', + '200', + '220t', + '240t', + '260t', + '290t', + '320t', + '350t'], + ['', + '70', + '80', + '90', + '100p', + '110', + '120', + '130', + '145', + '160', + '175', + '190', + '210t', + '230t', + '250t', + '280t', + '310t', + '340t'], + %w[55 60 70 80 90p 100 110 120 135 150 165 180 200t 220t 240t 270t 300t 330t], + %w[50 56 60 70 80p 90 100 110 125 140 155 170 190t 210t 230t], + %w[45 52 57 60 70p 80 90 100 115 130 145 160], + %w[40 50 54 58 60p 70 80 90 100x 120], + %w[35 45 52 56 59 64 70 80], + %w[30 40 48 54 58 60], + ].freeze + + MARKET_TEXT = Base::MARKET_TEXT.merge(par_1: 'SBB starting price', type_limited: 'Regionals cannot enter').freeze + + STOCKMARKET_COLORS = Base::STOCKMARKET_COLORS.merge(par_1: :blue, type_limited: :peach).freeze + + PHASES = [ + { + name: '1', + train_limit: { 'pre-sbb': 2, regional: 2, historical: 4 }, + tiles: [:yellow], + operating_rounds: 1, + }, + { + name: '2', + on: '2', + train_limit: { 'pre-sbb': 2, regional: 2, historical: 4 }, + tiles: [:yellow], + operating_rounds: 1, + }, + { + name: '3', + on: '3', + train_limit: { 'pre-sbb': 2, regional: 2, historical: 4 }, + tiles: %i[yellow green], + operating_rounds: 2, + status: %w[can_buy_companies], + }, + { + name: '4', + on: '4', + train_limit: { 'pre-sbb': 2, regional: 2, historical: 3 }, + tiles: %i[yellow green], + operating_rounds: 2, + status: %w[can_buy_companies], + }, + { + name: '5', + on: '5', + train_limit: 2, + tiles: %i[yellow green brown], + operating_rounds: 3, + }, + { + name: '6', + on: '6', + train_limit: 2, + tiles: %i[yellow green brown], + operating_rounds: 3, + }, + { + name: '7', + on: '8E', + train_limit: 2, + tiles: %i[yellow green brown gray], + operating_rounds: 3, + }, + ].freeze + + TRAINS = [ + { + name: '2', + num: 13, + distance: 2, + price: 90, + rusts_on: '4', + variants: [ + { + name: '2H', + distance: 2, + price: 70, + }, + ], + events: [{ 'type' => 'train_exports' }], + }, + { + name: '3', + num: 9, + distance: 3, + price: 180, + rusts_on: '6', + variants: [ + { + name: '3H', + distance: 3, + price: 150, + }, + ], + events: [{ 'type' => '2t_downgrade' }, { 'type' => 'company_abilities' }, { 'type' => 'buy_across' }], + }, + { + name: '4', + num: 6, + distance: 4, + price: 300, + rusts_on: '8E', + variants: [ + { + name: '4H', + distance: 4, + price: 260, + }, + ], + events: [{ 'type' => '3t_downgrade' }], + }, + { + name: '5', + num: 6, + distance: 5, + price: 450, + variants: [ + { + name: '5H', + distance: 5, + price: 400, + }, + ], + events: [{ 'type' => 'close_companies' }, { 'type' => 'sbb_formation' }], + }, + { + name: '6', + num: 4, + distance: 6, + price: 630, + variants: [ + { + name: '6H', + distance: 6, + price: 550, + }, + ], + events: [{ 'type' => '4t_downgrade' }, { 'type' => 'full_capitalization' }], + }, + { + name: '8E', + num: 20, + distance: [{ 'nodes' => %w[city offboard town], 'pay' => 8, 'visit' => 99 }], + price: 960, + variants: [ + { + name: '8H', + distance: 8, + price: 700, + }, + ], + events: [{ 'type' => '5t_downgrade' }], + }, + ].freeze + + EVENTS_TEXT = Base::EVENTS_TEXT.merge( + 'train_exports' => ['Train Exports', 'Next train exported at the end of each OR set'], + '2t_downgrade' => ['2 -> 2H', '2 trains downgraded to 2H trains'], + 'company_abilities' => ['Company Abilities', 'Company special abilities can be used'], + 'buy_across' => ['Buy Across', 'Trains can be bought between corporations'], + '3t_downgrade' => ['3 -> 3H', '3 trains downgraded to 3H trains'], + 'sbb_formation' => ['SBB Forms', 'SBB forms after the Operating Round'], + '4t_downgrade' => ['4 -> 4H', '4 trains downgraded to 4H trains'], + 'full_capitalization' => ['Full Capitalization', 'Newly formed corporations receive full capitalization'], + '5t_downgrade' => ['5 -> 5H', '5 trains downgraded to 5H trains'], + ).freeze + + P4_TILE_LAYS = { 'H17' => 'OP3', 'H19' => 'OP2', 'H21' => 'OP2', 'H23' => 'OP2', 'I16' => 'OP1' }.freeze + + def privates + @privates ||= @companies.select { |c| c.sym[0] == 'P' } + end + + def mountain_railways + @mountain_railways ||= @companies.select { |c| c.sym[0] == 'B' } + end + + def unactivated_mountain_railways + mountain_railways.select { |mr| mr.owner&.player? && mr.value.zero? } + end + + def tunnel_companies + @tunnel_companies ||= @companies.select { |c| c.sym[0] == 'T' } + end + + def unactivated_tunnel_companies + tunnel_companies.select { |tc| tc.owner&.player? && tc.value.zero? } + end + + def pre_sbb_corporations + # Priority ordered list of pre-sbb corporations + @pre_sbb_corps ||= %w[NOB SCB VSB JS GB].map { |id| corporation_by_id(id) } + end + + def fnm + @fnm ||= corporation_by_id('FNM') + end + + def sbb + @sbb ||= corporation_by_id('SBB') + end + + def gb + @gb ||= corporation_by_id('GB') + end + + def p4 + @p4 ||= company_by_id('P4') + end + + def p7 + @p7 ||= company_by_id('P7') + end + + # def setup + # setup_destinations + # setup_offboard_bonuses + # mountain_railways.each { |mr| mr.owner = @bank } + # tunnel_companies.each { |tc| tc.owner = @bank } + + # @eva = @depot.trains.find { |t| t.name == '5' && t.events.empty? } + # @depot.remove_train(@eva) + # @eva.reserved = true + # @eva.variant = '5H' + + # @sbb_train = @depot.trains.find { |t| t.name == '5' && t.events.empty? } + # @depot.forget_train(@sbb_train) + # @sbb_train.variant = '5H' + # @sbb_train.buyable = false + # sbb.shares.select { |s| s.percent == 10 && !s.president }.each { |s| s.double_cert = true } + + # @all_tiles.each { |t| t.ignore_gauge_walk = true } + # @_tiles.values.each { |t| t.ignore_gauge_walk = true } + # @hexes.each { |h| h.tile.ignore_gauge_walk = true } + # @graph.clear_graph_for_all + # end + + def setup_destinations + @corporations.each do |c| + next unless c.destination_coordinates + + dest_hex = hex_by_id(c.destination_coordinates) + ability = Ability::Base.new( + type: 'base', + description: "Destination: #{dest_hex.location_name} (#{dest_hex.name})", + ) + c.add_ability(ability) + + dest_hex.assign!(c) + end + end + + def setup_offboard_bonuses + hex_info = @hexes.map do |hex| + offboard = hex.tile.offboards.first + next if !offboard || hex.tile.color != :red + + icon = offboard.tile.icons.find { |i| i.name.start_with?('bonus_') } + [hex, offboard.groups.find { |g| g.size > 1 }, icon ? icon.name.split('_')[1].to_i : nil] + end.compact + group_bonus = hex_info.map { |_hex, group, bonus| group && bonus ? [group, bonus] : nil }.compact.to_h + + @hex_bonus_revenue = hex_info.map do |hex, group, bonus| + [hex, bonus || group_bonus[group]] + end.compact.to_h + end + + def event_train_exports! + @log << "-- Event: #{EVENTS_TEXT['train_exports'][1]} --" + end + + def event_company_abilities! + @log << "-- Event: #{EVENTS_TEXT['company_abilities'][1]} --" + end + + def event_buy_across! + @log << "-- Event: #{EVENTS_TEXT['buy_across'][1]} --" + end + + def event_2t_downgrade! + downgrade_train_type!('2', '2H') + end + + def event_3t_downgrade! + downgrade_train_type!('3', '3H') + end + + def event_close_companies! + lay_p4_overpass! unless p4.closed? + p7.revenue = 0 + super + end + + def event_sbb_formation! + @log << "-- Event: #{EVENTS_TEXT['sbb_formation'][1]} --" + @ready_to_form_sbb = true + end + + def event_4t_downgrade! + downgrade_train_type!('4', '4H') + end + + def event_full_capitalization! + @log << "-- Event: #{EVENTS_TEXT['full_capitalization'][1]} --" + @full_capitalization = true + @corporations.select { |corp| corp.type == :historical && !corp.floated }.each do |corp| + next unless corp.destination_coordinates + + hex_by_id(corp.destination_coordinates).remove_assignment!(corp) + corp.remove_ability(corp.abilities.find { |a| a.description.start_with?('Destination') }) + end + end + + def event_5t_downgrade! + downgrade_train_type!('5', '5H') + end + + def downgrade_train_type!(name, downgrade_name) + owners = Hash.new(0) + trains.select { |t| t.name == name }.each do |t| + t.variant = downgrade_name + owners[t.owner.name] += 1 if t.owner && t.owner != @depot + end + + @log << "-- Event: #{name} trains downgrade to #{downgrade_name} trains" \ + " (#{owners.map { |c, t| "#{c} x#{t}" }.join(', ')}) --" + end + + def form_sbb! + @log << '-- Event: SBB forms --' + + @stock_market.set_par(sbb, @stock_market.share_prices_with_types(%i[par_1]).first) + sbb.floatable = true + sbb.ipoed = true + sbb.floated = true + + @bank.spend(400, sbb) + @sbb_train.owner = sbb + sbb.trains << @sbb_train + @log << "#{sbb.name} starts with #{format_currency(400)} and a #{@sbb_train.name} train" + + previous_owners = [] + pre_sbb_corporations.each do |corp| + @log << "#{corp.name} merging into #{sbb.name}" + previous_owners << corp.owner + + @log << "#{sbb.name} receives #{format_currency(corp.cash)}" + corp.spend(corp.cash, sbb) if corp.cash.positive? + + place_sbb_tokens!(corp) + + num_trains = corp.trains.size + if num_trains.positive? + @log << "#{sbb.name} receives #{num_trains} train#{num_trains == 1 ? '' : 's'}:" \ + " #{corp.trains.map(&:name).join(', ')}" + transfer(:trains, corp, sbb) + end + + sbb_share_exchange!(corp) + + close_corporation(corp, quiet: true) + end + + sbb.tokens.sort_by! { |t| t.used ? 0 : 1 } + + determine_sbb_president!(previous_owners.uniq) + end + + def place_sbb_tokens!(corporation) + locations = corporation.tokens.map { |token| token.used ? token.hex.full_name : 'Unused' }.join(', ') + @log << "#{sbb.name} receives #{corporation.tokens.size} tokens: #{locations}" + corporation.tokens.each do |token| + sbb.tokens << Token.new(sbb, price: 100) + next unless token.used + + if token.city.tokened_by?(sbb) + @log << "#{sbb.name} already has a token on #{token.hex.full_name}, placing token on charter instead" + token.remove! + else + token.swap!(sbb.tokens.last, check_tokenable: false) + end + end + end + + def sbb_share_exchange!(corporation) + corporation.share_holders.keys.each do |share_holder| + sbb_shares = sbb.shares_of(sbb) + shares = share_holder.shares_of(corporation).map do |corp_share| + percent = corp_share.president ? 10 : 5 + share = sbb_shares.find { |sbb_share| sbb_share.percent == percent } + sbb_shares.delete(share) + share + end + next if shares.empty? + + share_holder = @share_pool if share_holder.corporation? + bundle = ShareBundle.new(shares) + @share_pool.transfer_shares(bundle, share_holder, allow_president_change: false) + + cash_per_share = corporation.par_price ? corporation.share_price.price - sbb.share_price.price : 0 + cash = cash_per_share * bundle.percent / 5 + msg = share_holder.name.to_s + if cash.zero? || share_holder == @share_pool + msg += ' receives' + elsif cash.positive? + msg += " receives #{format_currency(cash)} and" + @bank.spend(cash, share_holder) + else + msg += " pays #{format_currency(cash.abs)} and receives" + share_holder.spend(cash.abs, @bank, check_cash: false) + end + + msg += " #{bundle.percent}% of #{sbb.name}" + @log << msg + next if !share_holder.player? || !share_holder.cash.negative? + + debt = share_holder.cash.abs + share_holder.debt += debt + share_holder.cash += debt + @log << "#{share_holder.name} takes #{format_currency(debt)} of debt to complete payment" + end + end + + def determine_sbb_president!(president_priority_order) + player_share_percent = sbb.player_share_holders + max_percent = player_share_percent.values.max || 0 + return if max_percent < 10 + + # Determine president + candidates = player_share_percent.select { |_, percent| percent == max_percent }.keys + if candidates.size > 1 + candidates.sort_by! { |player| president_priority_order.index(player) || president_priority_order.size } + end + president = candidates.first + + # Make sure president has a 10% cert + if (president_shares = president.shares_of(sbb)).none? { |share| share.percent == 10 } + ten_percent_share = @share_pool.shares_of(sbb).find { |share| share.percent == 10 } || + president_priority_order[-1].shares_of(sbb).find { |share| share.percent == 10 } + @share_pool.transfer_shares(ShareBundle.new([ten_percent_share]), president, allow_president_change: false) + @share_pool.transfer_shares(ShareBundle.new(president_shares.take(2)), share.owner, allow_president_change: false) + end + + # Make sure president has the presidents cert + if (presidents_share_owner = sbb.presidents_share.owner) != president + @share_pool.transfer_shares(ShareBundle.new([sbb.presidents_share]), president) + @share_pool.transfer_shares( + ShareBundle.new([president_shares.find { |share| !share.president && share.percent == 10 }]), + presidents_share_owner + ) + end + + sbb.owner = president + @log << "#{president.name} becomes the president of #{sbb.name}" + end + + def player_value(player) + super - (player.companies & privates).sum(&:value) + end + + def player_debt(player) + player.debt + end + + def init_stock_market + G1854::StockMarket.new(game_market, self.class::CERT_LIMIT_TYPES, + multiple_buy_types: self.class::MULTIPLE_BUY_TYPES) + end + + def initial_auction_companies + privates + end + + def unowned_purchasable_companies(_entity) + @companies.select { |c| c.owner == @bank } + end + + def next_round! + @round = + case @round + when Engine::Round::Auction + init_round_finished + reorder_players(log_player_order: true) + new_stock_round + when Engine::Round::Stock + apply_interest_to_player_debt! + @operating_rounds = @phase.operating_rounds + reorder_players(log_player_order: true) + new_operating_round + when G1854::Round::Operating + next_round = + 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 + -> { new_stock_round } + end + if @ready_to_form_sbb + @post_sbb_formation_round = next_round + new_sbb_formation_round + else + next_round.call + end + when G1854::Round::SBBFormation + next_round = @post_sbb_formation_round + @ready_to_form_sbb = false + @post_sbb_formation_round = nil + next_round.call + end + end + + def or_set_finished + @depot.export! if @phase.name.to_i >= 2 + end + + def new_auction_round + Engine::Round::Auction.new(self, [ + G1854::Step::CompanyPendingPar, + Engine::Step::SelectionAuction, + ]) + end + + def stock_round + Engine::Round::Stock.new(self, [ + G1854::Step::MountainRailwayTrack, + G1854::Step::BuySellParShares, + ]) + end + + def operating_round(round_num) + G1854::Round::Operating.new(self, [ + Engine::Step::Bankrupt, + Engine::Step::DiscardTrain, + Engine::Step::Exchange, + G1854::Step::SpecialChoose, + G1854::Step::SpecialTrack, + G1854::Step::Destination, + G1854::Step::BuyCompany, + Engine::Step::HomeToken, + Engine::Step::Track, + G1854::Step::DestinationCheck, + Engine::Step::Token, + G1854::Step::DestinationCheck, + G1854::Step::Route, + G1854::Step::Dividend, + G1854::Step::BuyTrain, + [G1854::Step::BuyCompany, { blocks: true }], + ], round_num: round_num) + end + + def new_sbb_formation_round + @log << '-- SBB Formation Round --' + G1854::Round::SBBFormation.new(self, [ + Engine::Step::DiscardTrain, + G1854::Step::RemoveSBBTokens, + ]) + end + + def next_sr_player_order + @round.is_a?(Engine::Round::Auction) ? :least_cash : :most_cash + end + + def can_par?(corporation, _parrer) + return false if corporation == sbb + + super + end + + def after_par(corporation) + super + return unless corporation.type == :historical + + num_tokens = + case corporation.share_price.price + when 100 then 5 + when 90 then 4 + when 80 then 3 + when 70 then 2 + when 60 then 1 + else 0 + end + corporation.tokens.slice!(num_tokens..-1) + @log << "#{corporation.name} receives #{num_tokens} token#{num_tokens > 1 ? 's' : ''}" + return unless corporation == fnm + + @share_pool.transfer_shares(ShareBundle.new(corporation.shares_of(corporation).take(3)), @share_pool) + @log << "3 #{corporation.name} shares moved to the market" + float_corporation(corporation) + end + + def float_corporation(corporation) + return if corporation == sbb + + @log << "#{corporation.name} floats" + multiplier = + case corporation.type + when :'pre-sbb' then 2 + when :regional then 5 + when :historical then @full_capitalization ? 10 : 5 + end + @bank.spend(corporation.par_price.price * multiplier, corporation) + @log << "#{corporation.name} receives #{format_currency(corporation.cash)}" + end + + def can_hold_above_corp_limit?(_entity) + true + end + + def sellable_bundles(player, corporation) + bundles = super + return bundles if bundles.empty? || corporation.operated? + + bundles.each do |bundle| + bundle.share_price = @stock_market.find_share_price(corporation, Array.new(bundle.num_shares) { :down }).price + end + bundles + end + + def after_buy_company(player, company, _price) + super + return if !mountain_railways.include?(company) && !tunnel_companies.include?(company) + + company.revenue = 0 + company.value = 0 + end + + def lay_p4_overpass! + company = p4 + return if company.abilities.empty? + + owner = company.owner + compensation = 80 + + @log << "#{owner.name} must use #{company.name}" if @phase.name.to_i >= 5 + @log << "#{owner.name} (#{company.name}) lays Furka-Oberalp special tile" + @log << "#{owner.name} receives #{format_currency(compensation)}" + + @bank.spend(compensation, owner) + + P4_TILE_LAYS.each do |hex_id, tile_name| + hex = hex_by_id(hex_id) + tile = @tiles.find { |t| t.name == tile_name } + if (tunnel_path = hex.tile.paths.find { |path| path.track == :narrow }) + tile = replace_tile_code(tile, extend_tile_code(tile, narrow_track_code_for(tunnel_path.exits))) + @_tiles[tile.id] = tile + end + + update_tile_lists(tile, hex.tile) + hex.lay(tile) + end + + @log << "#{company.name} closes" + company.close! + end + + def assign_p7_train(corporation) + company = p7 + @log << "#{company.owner.name} (#{company.name}) assigns EVA #{@eva.name} train to #{corporation.name}" + buy_train(corporation, @eva, :free) + company.close! + end + + def all_potential_upgrades(tile, tile_manifest: false, selected_company: nil) + if self.class::MOUNTAIN_HEXES.include?(tile.hex.id) + return @all_tiles.select { |t| self.class::MOUNTAIN_TILES.include?(t.name) }.uniq(&:name) + end + + super + end + + def destinated?(entity) + home_node = entity.tokens.first&.city + destination_hex = hex_by_id(entity.destination_coordinates) + return false if !home_node || !destination_hex + return false unless destination_hex.assigned?(entity) + return hex_by_id('H19').tile.paths.any? { |path| path.track == :narrow } if entity == gb + + home_node.walk(corporation: entity) do |path, _| + return true if destination_hex == path.hex + end + + false + end + + def destinated!(corporation) + hex_by_id(corporation.destination_coordinates).remove_assignment!(corporation) + multiplier = corporation.type == :historical ? 5 : 2 + amount = corporation.par_price.price * multiplier + @bank.spend(amount, corporation) + @log << "#{corporation.name} has reached its destination and receives #{format_currency(amount)}" + end + + def must_buy_train?(entity) + super && entity.type != :'pre-sbb' + end + + def can_buy_train_from_others? + @phase.name.to_i >= 3 + end + + def hex_train?(train) + hex_train_name?(train.name) + end + + def hex_train_name?(name) + name[-1] == 'H' + end + + def express_train?(train) + train.name[-1] == 'E' + end + + def route_distance(route) + hex_train?(route.train) ? route_hex_distance(route) : super + end + + def route_hex_distance(route) + edges = route.chains.sum { |conn| conn[:paths].each_cons(2).sum { |a, b| a.hex == b.hex ? 0 : 1 } } + route.chains.empty? ? 0 : edges + 1 + end + + def route_distance_str(route) + hex_train?(route.train) ? "#{route_hex_distance(route)}H" : super + end + + def check_distance(route, visits) + hex_train?(route.train) ? check_hex_distance(route, visits) : super + end + + def check_hex_distance(route, _visits) + distance = route_hex_distance(route) + raise GameError, "#{distance} is too many hexes for #{route.train.name} train" if distance > route.train.distance + end + + def check_other(route) + if route.stops.any? { |stop| stop.route_revenue(route.phase, route.train).zero? } + raise GameError, 'No Mountain Railway to visit' + end + return unless hex_train?(route.train) + + raise GameError, 'Cannot visit offboard hexes' if route.stops.any? { |stop| stop.tile.color == :red } + end + + def revenue_stops(route) + stops = super + return stops unless express_train?(route.train) + + distance = route.train.distance.first['pay'] + return stops if stops.size <= distance + + # Prune the list of stops to improve performance + stops_by_revenue = stops.sort_by { |stop| -1 * stop.route_revenue(route.phase, route.train) } + stops = stops_by_revenue.slice!(0...distance) + unless stops.find { |stop| stop.tokened_by?(route.corporation) } + stops.pop + tokened_stop = stops_by_revenue.find { |stop| stop.tokened_by?(route.corporation) } + stops << tokened_stop if tokened_stop + end + stops.concat(stops_by_revenue.select { |stop| stop.tile.color == :red }) + end + + def revenue_for(route, stops) + revenue = super + revenue += 10 * stops.size if route.paths.any? { |path| path.track == :narrow } + revenue += east_west_bonus_revenue(stops) + revenue += north_south_bonus_revenue(stops) + revenue + end + + def revenue_str(route) + stops = route.stops + stop_hexes = stops.map(&:hex) + str = route.hexes.map { |h| stop_hexes.include?(h) ? h&.name : "(#{h&.name})" }.join('-') + str += ' + EW' if east_west_bonus?(route.stops) + str += ' + NS' if north_south_bonus?(route.stops) + str + end + + def hex_bonus_revenue(hex) + @hex_bonus_revenue[hex] || 0 + end + + def east_west_bonus?(stops) + (stops.flat_map(&:groups) & %w[E W]).size == 2 + end + + def east_west_bonus_revenue(stops) + east_west_bonus?(stops) ? stops.sum { |stop| hex_bonus_revenue(stop.hex) } : 0 + end + + def north_south_bonus?(stops) + (stops.flat_map(&:groups) & %w[N S]).size == 2 + end + + def north_south_bonus_revenue(stops) + north_south_bonus?(stops) ? stops.sum { |stop| hex_bonus_revenue(stop.hex) } : 0 + end + + def check_for_mountain_or_tunnel_activation(routes) + routes.each do |route| + route.hexes.select { |hex| self.class::MOUNTAIN_HEXES.include?(hex.id) }.each do |hex| + (unactivated_mountain_railways.map(&:id) & hex.assignments.keys).each do |id| + mountain_railway = company_by_id(id) + mountain_railway.value = 150 + mountain_railway.revenue = 40 + hex.remove_assignment!(id) + @log << "#{mountain_railway.name} has been activated" + end + end + + route.paths.select { |path| path.track == :narrow }.each do |path| + (unactivated_tunnel_companies.map(&:id) & path.hex.assignments.keys).each do |id| + tunnel_company = company_by_id(id) + tunnel_company.value = 50 + tunnel_company.revenue = 10 + path.hex.remove_assignment!(id) + @log << "#{tunnel_company.name} has been activated" + end + end + end + end + + def upgrades_to?(from, to, special = false, selected_company: nil) + return to.color == :purple && from.paths.none? { |p| p.track == :narrow } if from.color == :purple + return %w[14 15 619].include?(to.name) if from.hex.id == 'D15' && from.color == :yellow + + super + end + + def create_tunnel_tile(hex, tile) + replace_tile_code(tile, extend_tile_code(hex.tile, narrow_track_code_for(tile.exits))) + end + + def narrow_track_code_for(exits) + "path=a:#{exits[0]},b:#{exits[1]},track:narrow" + end + + def extend_tile_code(tile, additional_code) + code = tile.code + ';' + additional_code + code = code[1..-1] if code[0] == ';' + code + end + + def replace_tile_code(tile, new_code) + tile = Engine::Tile.new( + tile.name, + code: new_code, + color: tile.color, + parts: Engine::Tile.decode(new_code), + index: tile.index, + hidden: true, + ignore_gauge_walk: true, + ) + tile.ignore_gauge_walk = true + tile + end + + def graph_skip_paths(entity) + entity.type == :regional ? regional_skip_paths : super + end + + def regional_skip_paths + @regional_skip_paths ||= @hexes.select { |hex| hex.tile.color == :red }.flat_map do |hex| + hex.tile.paths.map { |path| [path, true] } + end.to_h + end + + def take_player_loan(player, loan) + player.cash += loan # debt does not come from the bank + interest = player_debt_interest(loan) + player.debt += loan + interest + + @log << "#{player.name} takes #{format_currency(loan)} in debt" + @log << "#{player.name} has 50% interest (#{format_currency(interest)}) applied to this debt" + end + + def apply_interest_to_player_debt! + @players.each do |player| + next if player.debt.zero? + + interest = player_debt_interest(player.debt) + player.debt += interest + @log << "#{player.name} has an additional 50% interest (#{format_currency(interest)}) applied to their debt" + end + end + + def payoff_player_loan(player) + payoff = player.cash >= player.debt ? player.debt : player.cash + verb = payoff == player.debt ? 'pays off' : 'decreases' + @log << "#{player.name} #{verb} their debt of #{format_currency(player.debt)}" + player.cash -= payoff + player.debt -= payoff + end + + def player_debt_interest(debt) + (debt * 0.5).ceil + end + end + end + end +end diff --git a/lib/engine/game/g_1854/map.rb b/lib/engine/game/g_1854/map.rb new file mode 100644 index 0000000000..7de354b19b --- /dev/null +++ b/lib/engine/game/g_1854/map.rb @@ -0,0 +1,227 @@ +# frozen_string_literal: true + +module Engine + module Game + module G1854 + module Map + TILES = { + '3' => 3, + '4' => 6, + '5' => 5, + '6' => 6, + '7' => 5, + '8' => 11, + '9' => 11, + '14' => 4, + '15' => 7, + '16' => 2, + '19' => 2, + '20' => 2, + '23' => 6, + '24' => 6, + '25' => 2, + '26' => 2, + '27' => 2, + '28' => 2, + '29' => 2, + '39' => 1, + '40' => 1, + '41' => 1, + '42' => 1, + '43' => 1, + '44' => 1, + '45' => 1, + '46' => 1, + '47' => 1, + '57' => 6, + '58' => 6, + '59' => 2, + '64' => 1, + '65' => 1, + '66' => 1, + '67' => 1, + '68' => 1, + '70' => 1, + '87' => 2, + '88' => 2, + '204' => 2, + '611' => 6, + '619' => 4, + '901' => { + 'count' => 1, + 'color' => 'green', + 'code' => 'city=revenue:40,loc:0.5;city=revenue:40,loc:2.5;path=a:0,b:_0;path=a:1,b:_0;path=a:2,b:_1;'\ + 'path=a:3,b:_1;label=L', + }, + '902' => { + 'count' => 1, + 'color' => 'brown', + 'code' => 'city=revenue:50,slots:2;path=a:0,b:_0;path=a:3,b:_0;path=a:4,b:_0;path=a:5,b:_0;label=L', + }, + '903' => { + 'count' => 1, + 'color' => 'gray', + 'code' => 'city=revenue:60,slots:3;path=a:0,b:_0;path=a:1,b:_0;path=a:2,b:_0;path=a:3,b:_0;label=L', + }, + '904' => { + 'count' => 1, + 'color' => 'green', + 'code' => 'city=revenue:40,slots:2;path=a:0,b:_0;path=a:1,b:_0;path=a:3,b:_0;path=a:4,b:_0;path=a:5,b:_0;label=B', + }, + '905' => { + 'count' => 1, + 'color' => 'brown', + '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;label=B', + }, + '906' => { + 'count' => 1, + 'color' => 'gray', + 'code' => 'city=revenue:60,slots:3;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=B', + }, + '907' => { + 'count' => 1, + 'color' => 'green', + 'code' => 'city=revenue:40,slots:2;path=a:0,b:_0;path=a:2,b:_0;path=a:3,b:_0;label=Z', + }, + '908' => { + 'count' => 1, + 'color' => 'green', + 'code' => 'city=revenue:40,slots:2;path=a:0,b:_0;path=a:3,b:_0;path=a:4,b:_0;label=Z', + }, + '909' => { + 'count' => 1, + 'color' => 'brown', + 'code' => 'city=revenue:50,slots:3;path=a:0,b:_0;path=a:3,b:_0;path=a:4,b:_0;path=a:5,b:_0;label=Z', + }, + '910' => { + 'count' => 1, + 'color' => 'gray', + 'code' => 'city=revenue:60,slots:4;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;'\ + 'label=Z', + }, + '911' => { + 'count' => 2, + 'color' => 'brown', + 'code' => 'town=revenue:10;path=a:0,b:_0;path=a:1,b:_0;path=a:3,b:_0;path=a:4,b:_0;path=a:5,b:_0', + }, + '915' => { + 'count' => 2, + 'color' => 'gray', + 'code' => 'city=revenue:50,slots:3;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', + }, + 'XM1' => { + 'count' => 2, + 'color' => 'gray', + 'code' => 'offboard=revenue:yellow_10|green_20|brown_50|gray_80', + }, + 'XM2' => { + 'count' => 2, + 'color' => 'gray', + 'code' => 'offboard=revenue:yellow_10|green_40|brown_50|gray_60', + }, + 'XM3' => { + 'count' => 2, + 'color' => 'gray', + 'code' => 'offboard=revenue:yellow_10|green_50|brown_80|gray_10', + }, + 'X78' => { + 'count' => 5, + 'color' => 'purple', + 'code' => 'path=a:0,b:2,track:narrow', + }, + 'X79' => { + 'count' => 5, + 'color' => 'purple', + 'code' => 'path=a:0,b:3,track:narrow', + }, + 'OP1' => { + 'count' => 1, + 'hidden' => true, + 'color' => 'purple', + 'code' => 'path=a:3,b:5', + }, + 'OP2' => { + 'count' => 3, + 'hidden' => true, + 'color' => 'purple', + 'code' => 'path=a:1,b:4', + }, + 'OP3' => { + 'count' => 1, + 'hidden' => true, + 'color' => 'purple', + 'code' => 'town=revenue:10,loc:4;path=a:0,b:_0;path=a:4,b:_0', + }, + }.freeze + + LOCATION_NAMES = { + 'A19' => 'Prag', + 'A25' => 'Brunn', + 'D28' => 'Budapest', + }.freeze + + HEXES = { + red: { + ['A19'] => 'offboard=revenue:yellow_20|green_30|brown_50|gray_50;path=a:5,b:_0', + ['A25'] => 'offboard=revenue:yellow_20|green_30|brown_40|gray_40;path=a:5,b:_0', + ['C27'] => 'offboard=revenue:yellow_20|green_30|brown_40|gray_50;path=a:1,b:_0', + ['D4'] => 'offboard=revenue:yellow_00|green_20|brown_30|gray_40;path=a:0,b:_0', + ['E1'] => 'offboard=revenue:yellow_20|green_30|brown_40|gray_50;path=a:4,b:_0;path=a:5,b:_0', + ['G1'] => 'offboard=revenue:yellow_20|green_20|brown_20|gray_20;path=a:3,b:_0', + # TODO: group + ['D28'] => 'offboard=revenue:yellow_30|green_40|brown_50|gray_60,groups:Budapest,hide:1;path=a:1,b:_0;border=edge:0', + ['E27'] => 'offboard=revenue:yellow_30|green_40|brown_50|gray_60,groups:Budapest;path=a:2,b:_0;border=edge:3', + ['H10'] => 'offboard=revenue:yellow_30|green_40|brown_50|gray_60;path=a:2,b:_0;path=a:3,b:_0', + ['I15'] => 'offboard=revenue:yellow_20|green_30|brown_40|gray_50;path=a:3,b:_0', + ['I19'] => 'offboard=revenue:yellow_20|green_30|brown_30|gray_40;path=a:2,b:_0', + }, + gray: { + %w[A21 I31 I37] => 'path=a:0,b:5', + %w[M31 M37] => 'town=revenue:10;path=a:2,b:_0;path=a:_0,b:3', + ['C13'] => 'town=revenue:10;path=a:4,b:_0;path=a:_0,b:5', + ['H12'] => 'town=revenue:10;path=a:2,b:_0;path=a:_0,b:4', + }, + white: { + %w[B24 B26 E25 F24 G23 G21 G19 G9 H20 K35 L34 J32] => 'blank', + %w[F20] => 'upgrade=cost:90,terrain:mountain', + %w[F10 C25] => 'upgrade=cost:50,terrain:water', + %w[B16 K31 K29] => 'upgrade=cost:60,terrain:mountain', + %w[D14 E7 E9 E23 L38] => 'upgrade=cost:70,terrain:mountain', + %w[G15 H14] => 'upgrade=cost:80,terrain:mountain', + %w[B20] => 'upgrade=cost:50,terrain:mountain', + %w[E19 E15] => 'upgrade=cost:90,terrain:mountain', + %w[E17 E5 F16] => 'upgrade=cost:100,terrain:mountain', + %w[B18 K33] => 'town=revenue:0;upgrade=cost:50,terrain:mountain', + %w[L30 L36 K39] => 'town=revenue:0;upgrade=cost:60,terrain:mountain', + %w[G3] => 'town=revenue:0;upgrade=cost:80,terrain:mountain', + %w[L32] => 'town=revenue:0;town=revenue:0;upgrade=cost:80,terrain:mountain', + %w[F2 J30 D24] => 'town=revenue:0;town=revenue:0', + %w[B22 J36 K37 D22] => 'town=revenue:0;town=revenue:0;upgrade=cost:50,terrain:mountain', + %w[H16] => 'town=revenue:0;town=revenue:0;upgrade=cost:70,terrain:mountain', + %w[E21] => 'town=revenue:0;town=revenue:0;upgrade=cost:120,terrain:mountain', + %w[F4] => 'upgrade=cost:120,terrain:mountain', + %w[C21 E11] => 'town=revenue:0;town=revenue:0;upgrade=cost:50,terrain:water', + %w[C19 F6] => 'town=revenue:0;upgrade=cost:50,terrain:water', + %w[C15 D16 D26] => 'town=revenue:0', + %w[C17 E3 E13 F22 F8 H18 J34] => 'city=revenue:0', + }, + yellow: { + ['C23'] => 'city=revenue:40,loc:0;city=revenue:40,loc:1;city=revenue:40,loc:2;path=a:0,b:_0;path=a:1,b:_1;path=a:2,b:_2', + ['J38'] => 'city=revenue:20,loc:1;city=revenue:20,loc:5;path=a:1,b:_0;path=a:5,b:_1', + }, + brown: { + %w[F12 F14 F18 G17 G13 G11 G7 G5] => 'icon=image:1854/mine', + }, + purple: { + }, + blue: { + }, + }.freeze + + LAYOUT = :pointy + end + end + end +end diff --git a/lib/engine/game/g_1854/meta.rb b/lib/engine/game/g_1854/meta.rb new file mode 100644 index 0000000000..393de63f49 --- /dev/null +++ b/lib/engine/game/g_1854/meta.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require_relative '../meta' + +module Engine + module Game + module G1854 + module Meta + include Game::Meta + + DEV_STAGE = :alpha + + GAME_DESIGNER = 'Leonhard "Lonny" Orgler' + GAME_LOCATION = 'Austria' + GAME_PUBLISHER = :lonny_games + GAME_RULES_URL = 'https://lookout-spiele.de/upload/en_1854_1854.html_Rules_1854_EN.pdf' + GAME_INFO_URL = 'https://github.com/tobymao/18xx/wiki/1854' + + PLAYER_RANGE = [3, 6].freeze + end + end + end +end diff --git a/lib/engine/game/g_1854/player.rb b/lib/engine/game/g_1854/player.rb new file mode 100644 index 0000000000..e7ee9dc902 --- /dev/null +++ b/lib/engine/game/g_1854/player.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require_relative '../../player' + +module Engine + module Game + module G1854 + class Player < Engine::Player + attr_accessor :debt + + def initialize(id, name) + super + @debt = 0 + end + + def value + super - @debt + end + end + end + end +end diff --git a/lib/engine/game/g_1854/round/operating.rb b/lib/engine/game/g_1854/round/operating.rb new file mode 100644 index 0000000000..449df56260 --- /dev/null +++ b/lib/engine/game/g_1854/round/operating.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require_relative '../../../round/operating' +require_relative '../step/destination' + +module Engine + module Game + module G1854 + module Round + class Operating < Engine::Round::Operating + def initialize(game, steps, **opts) + super + @destination_step = @steps.find { |step| step.is_a?(Step::Destination) } + end + + def auto_actions + @destination_step.auto_actions(current_entity).concat(Array(super)) + end + end + end + end + end +end diff --git a/lib/engine/game/g_1854/round/sbb_formation.rb b/lib/engine/game/g_1854/round/sbb_formation.rb new file mode 100644 index 0000000000..dae9c19640 --- /dev/null +++ b/lib/engine/game/g_1854/round/sbb_formation.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require_relative '../../../round/merger' + +module Engine + module Game + module G1854 + module Round + class SBBFormation < Engine::Round::Merger + def self.round_name + 'SBB Formation Round' + end + + def self.short_name + 'SBB' + end + + def setup + @game.form_sbb! + end + + def select_entities + [@game.sbb] + end + + def force_next_entity! + clear_cache! + end + end + end + end + end +end diff --git a/lib/engine/game/g_1854/step/buy_company.rb b/lib/engine/game/g_1854/step/buy_company.rb new file mode 100644 index 0000000000..961d51536c --- /dev/null +++ b/lib/engine/game/g_1854/step/buy_company.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require_relative '../../../step/buy_company' + +module Engine + module Game + module G1854 + module Step + class BuyCompany < Engine::Step::BuyCompany + def actions(entity) + return [] if entity.corporation? && entity.type == :'pre-sbb' + + super + end + end + end + end + end +end diff --git a/lib/engine/game/g_1854/step/buy_sell_par_shares.rb b/lib/engine/game/g_1854/step/buy_sell_par_shares.rb new file mode 100644 index 0000000000..5353e67b75 --- /dev/null +++ b/lib/engine/game/g_1854/step/buy_sell_par_shares.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require_relative '../../../step/buy_sell_par_shares' + +module Engine + module Game + module G1854 + module Step + class BuySellParShares < Engine::Step::BuySellParShares + def round_state + super.merge( + { + players_purchased_companies: Hash.new { |h, k| h[k] = [] }, + } + ) + end + + def actions(entity) + actions = super.dup + actions << 'payoff_player_debt' if can_payoff_debt?(entity) + actions + end + + def help + return super unless indebted?(current_entity) + + "#{current_entity.name} cannot buy until they payoff their #{@game.format_currency(current_entity.debt)} loan. "\ + "#{current_entity.name} has #{@game.format_currency(current_entity.cash)} in cash." + end + + def can_gain?(entity, bundle, exchange: false) + # Can buy above the share limit if from the share pool + return true if bundle.owner == @game.share_pool && @game.num_certs(entity) < @game.cert_limit + + super + end + + def get_par_prices(entity, _corp) + @game.stock_market.share_prices_with_types([:par]).select { |p| p.price * 2 <= available_cash(entity) } + end + + def can_buy_any?(entity) + !indebted?(entity) && super + end + + def can_buy_company?(player, company) + return false if indebted?(player) || !super + + companies_of_type = [] + if @game.mountain_railways.include?(company) + companies_of_type = @game.mountain_railways + elsif @game.tunnel_companies.include?(company) + companies_of_type = @game.tunnel_companies + end + @round.players_purchased_companies[player].none? { |c| companies_of_type.include?(c) } + end + + def can_payoff_debt?(entity) + indebted?(entity) && entity.cash.positive? + end + + def process_buy_company(action) + super + @round.players_purchased_companies[action.entity] << action.company + end + + def process_payoff_player_debt(action) + @game.payoff_player_loan(action.entity) + end + + def indebted?(entity) + entity.player? && entity.debt.positive? + end + end + end + end + end +end diff --git a/lib/engine/game/g_1854/step/buy_train.rb b/lib/engine/game/g_1854/step/buy_train.rb new file mode 100644 index 0000000000..89c04829dd --- /dev/null +++ b/lib/engine/game/g_1854/step/buy_train.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require_relative '../../../step/buy_train' +require_relative '../../../step/automatic_loan' + +module Engine + module Game + module G1854 + module Step + class BuyTrain < Engine::Step::BuyTrain + def buyable_train_variants(train, entity) + variants = super + variants.select! { |t| @game.hex_train_name?(t[:name]) } if entity.type == :regional + variants + end + + def spend_minmax(entity, train) + return [train.price, train.price] if [train.owner, entity].include?(@game.sbb) || + (train.owner&.corporation? && train.owner.owner != entity.owner) + + super + end + + def must_take_player_loan?(entity) + @game.depot.min_depot_price > (entity.cash + entity.owner.cash) + end + + def try_take_player_loan(entity, cost) + return unless cost > entity.cash + + @game.take_player_loan(entity, cost - entity.cash) + end + end + end + end + end +end diff --git a/lib/engine/game/g_1854/step/company_pending_par.rb b/lib/engine/game/g_1854/step/company_pending_par.rb new file mode 100644 index 0000000000..adbfc76cb0 --- /dev/null +++ b/lib/engine/game/g_1854/step/company_pending_par.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require_relative '../../../step/company_pending_par' + +module Engine + module Game + module G1854 + module Step + class CompanyPendingPar < Engine::Step::CompanyPendingPar + def get_par_prices(_entity, _corp) + @game.stock_market.share_prices_with_types(%i[par]) + end + end + end + end + end +end diff --git a/lib/engine/game/g_1854/step/destination.rb b/lib/engine/game/g_1854/step/destination.rb new file mode 100644 index 0000000000..5ec1286c39 --- /dev/null +++ b/lib/engine/game/g_1854/step/destination.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require_relative '../../../step/base' + +module Engine + module Game + module G1854 + module Step + class Destination < Engine::Step::Base + ACTIONS = %w[destination_connection].freeze + + def actions(entity) + return [] unless entity == current_entity + + ACTIONS + end + + def auto_actions(entity) + destinated = @game.corporations.select { |c| @game.destinated?(c) } + return [] if destinated.empty? + + [Engine::Action::DestinationConnection.new(entity, corporations: destinated)] + end + + def blocks? + false + end + + def process_destination_connection(action) + action.corporations.each { |c| @game.destinated!(c) } + end + end + end + end + end +end diff --git a/lib/engine/game/g_1854/step/destination_check.rb b/lib/engine/game/g_1854/step/destination_check.rb new file mode 100644 index 0000000000..fc295014f4 --- /dev/null +++ b/lib/engine/game/g_1854/step/destination_check.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require_relative '../../../step/base' + +module Engine + module Game + module G1854 + module Step + class DestinationCheck < Engine::Step::Base + ACTIONS = %w[pass].freeze + + def actions(entity) + return [] unless entity == current_entity + + ACTIONS + end + + def auto_actions(entity) + [Engine::Action::Pass.new(entity)] + end + + def log_pass(entity); end + end + end + end + end +end diff --git a/lib/engine/game/g_1854/step/dividend.rb b/lib/engine/game/g_1854/step/dividend.rb new file mode 100644 index 0000000000..fe52028d13 --- /dev/null +++ b/lib/engine/game/g_1854/step/dividend.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require_relative '../../../step/dividend' + +module Engine + module Game + module G1854 + module Step + class Dividend < Engine::Step::Dividend + def dividends_for_entity(entity, holder, per_share) + dividends = (holder.num_shares_of(entity, ceil: false) * per_share) + holder == @game.share_pool ? dividends.floor : dividends.ceil + end + end + end + end + end +end diff --git a/lib/engine/game/g_1854/step/mountain_railway_track.rb b/lib/engine/game/g_1854/step/mountain_railway_track.rb new file mode 100644 index 0000000000..2f14f00725 --- /dev/null +++ b/lib/engine/game/g_1854/step/mountain_railway_track.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require_relative '../../../step/special_track' + +module Engine + module Game + module G1854 + module Step + class MountainRailwayTrack < Engine::Step::SpecialTrack + def description + "Assign Revenue Marker for #{current_entity.name}" + end + + def active_entities + Array(@game.mountain_railways.find { |c| c.owner&.player? && c.abilities.find { |a| a.type == :tile_lay } }) + end + + def abilities(entity, **kwargs, &block) + @game.abilities(entity, :tile_lay, time: 'stock_round', **kwargs, &block) + end + + def blocks? + true + end + + def help + "Select mountain hex and revenue marker for #{current_entity.name}." + end + + def available_hex(_entity, hex) + abilities(current_entity).hexes.include?(hex.id) && + hex.tile.offboards.first.max_revenue.zero? + end + + def upgradeable_tiles(entity, _hex) + ability = abilities(entity) + @game.tiles.select { |t| ability.tiles.include?(t.name) }.uniq(&:name) + end + + def potential_tile_colors(_entity, _hex) + [:gray] + end + + def process_lay_tile(action) + tile = action.tile + hex = action.hex + company = action.entity + + ability = abilities(company) + raise GameError, "#{company.name} cannot lay on hex #{hex.name}" unless ability.hexes.include?(hex.id) + raise GameError, "#{company.name} cannot lay tile #{tile.name}" unless ability.tiles.include?(tile.name) + + revenue = tile.offboards.first.revenue + hex.tile.offboards.first.parse_revenue(revenue.map { |color, value| "#{color}_#{value}" }.join('|')) + hex.assign!(company.id) + @log << "#{company.name} places #{revenue.values.join('/')} revenue marker on #{hex.location_name} (#{hex.name})" + + @game.tiles.delete(tile) + company.remove_ability(ability) + end + end + end + end + end +end diff --git a/lib/engine/game/g_1854/step/remove_sbb_tokens.rb b/lib/engine/game/g_1854/step/remove_sbb_tokens.rb new file mode 100644 index 0000000000..73266a46d9 --- /dev/null +++ b/lib/engine/game/g_1854/step/remove_sbb_tokens.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require_relative '../../../step/base' + +module Engine + module Game + module G1854 + module Step + class RemoveSBBTokens < Engine::Step::Base + REMOVE_TOKEN_ACTIONS = %w[remove_token].freeze + + def actions(entity) + return [] if entity != current_entity || hexes_to_resolve(entity).empty? + + REMOVE_TOKEN_ACTIONS + end + + def description + 'Remove token' + end + + def help + "#{current_entity.name} cannot have two tokens in the same hex. "\ + "Select which token to remove from #{hexes_to_resolve(current_entity).map(&:id).join(',')}." + end + + def active_entities + [@game.sbb] + end + + def can_replace_token?(entity, token) + available_hex(entity, token.hex) + end + + def hexes_to_resolve(entity) + entity.tokens.select(&:used).map(&:hex).group_by(&:itself).select { |_k, v| v.size > 1 }.keys + end + + def available_hex(entity, hex) + hexes_to_resolve(entity).include?(hex) + end + + def process_remove_token(action) + entity = action.entity + token = action.city.tokens[action.slot] + hex = token.hex + + raise GameError, "Cannot remove #{token.corporation.name} token" if !@game.loading && !available_hex(entity, hex) + + @log << "#{entity.name} removes token from #{hex.full_name} and places it on its charter" + token.remove! + end + end + end + end + end +end diff --git a/lib/engine/game/g_1854/step/route.rb b/lib/engine/game/g_1854/step/route.rb new file mode 100644 index 0000000000..48f7a3eb67 --- /dev/null +++ b/lib/engine/game/g_1854/step/route.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require_relative '../../../step/route' + +module Engine + module Game + module G1854 + module Step + class Route < Engine::Step::Route + def help + 'Corporations must run for highest total revenue. It is illegal to run for less revenue' \ + ' in order to activate a Mountain Railway or Tunnel Company.' + end + + def process_run_routes(action) + super + @game.check_for_mountain_or_tunnel_activation(action.routes) + end + end + end + end + end +end diff --git a/lib/engine/game/g_1854/step/special_choose.rb b/lib/engine/game/g_1854/step/special_choose.rb new file mode 100644 index 0000000000..2751e97158 --- /dev/null +++ b/lib/engine/game/g_1854/step/special_choose.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require_relative '../../../step/special_choose' + +module Engine + module Game + module G1854 + module Step + class SpecialChoose < Engine::Step::SpecialChoose + def choices_ability(entity) + return { current_entity.id => "Assign EVA to #{current_entity.name}" } if entity == @game.p7 + + super + end + + def process_choose_ability(action) + entity = action.entity + if entity == @game.p4 + @game.lay_p4_overpass! + elsif entity == @game.p7 + @game.assign_p7_train(current_entity) + else + raise GameError, "#{entity.name} does not have a choice ability" + end + end + end + end + end + end +end diff --git a/lib/engine/game/g_1854/step/special_track.rb b/lib/engine/game/g_1854/step/special_track.rb new file mode 100644 index 0000000000..f90e8a6359 --- /dev/null +++ b/lib/engine/game/g_1854/step/special_track.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require_relative '../../../step/special_track' + +module Engine + module Game + module G1854 + module Step + class SpecialTrack < Engine::Step::SpecialTrack + def available_hex(entity, hex) + return super unless @game.tunnel_companies.include?(entity) + + super && hex.tile.paths.none? { |p| p.track == :narrow } + end + + def process_lay_tile(action) + if @game.tunnel_companies.include?(action.entity) + action.tile.rotate!(action.rotation) + tile = @game.create_tunnel_tile(action.hex, action.tile) + action = Engine::Action::LayTile.new(action.entity, tile: tile, hex: action.hex, rotation: 0) + end + + super(action) + return unless @game.tunnel_companies.include?(action.entity) + + action.tile.hex.assign!(action.entity.id) + end + + def potential_tile_colors(entity, _hex) + return super unless @game.tunnel_companies.include?(entity) + + [:purple] + end + + def legal_tile_rotations(entity_or_entities, hex, tile) + Engine::Tile::ALL_EDGES.select do |rotation| + tile.rotate!(rotation) + legal_tile_rotation?( + entity_or_entities, + hex, + @game.tunnel_companies.include?(entity_or_entities) ? @game.create_tunnel_tile(hex, tile) : tile + ) + end + end + + def legal_tile_rotation?(entity, hex, tile) + return super unless @game.tunnel_companies.include?(entity) + + tunnel_track_exits = tile.paths.flat_map { |p| p.track == :narrow ? p.exits : [] } + super && tunnel_track_exits.none? { |edge| hex.neighbors[edge]&.tile&.color == :purple } + end + end + end + end + end +end diff --git a/lib/engine/game/g_1854/stock_market.rb b/lib/engine/game/g_1854/stock_market.rb new file mode 100644 index 0000000000..95d0a65590 --- /dev/null +++ b/lib/engine/game/g_1854/stock_market.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Engine + module Game + module G1854 + class StockMarket < Engine::StockMarket + def regional_max_price?(coordinates) + row, col = coordinates + return true if @market[row][col]&.type != :type_limited && @market[row][col + 1]&.type == :type_limited + end + + def right_ledge?(coordinates) + regional_max_price?(coordinates) ? true : super + end + + def right(corporation, coordinates) + return up(corporation, coordinates) if corporation&.type == :regional && regional_max_price?(coordinates) + + super + end + end + end + end +end diff --git a/public/icons/1854/B1.svg b/public/icons/1854/B1.svg new file mode 100644 index 0000000000..211e738c81 --- /dev/null +++ b/public/icons/1854/B1.svg @@ -0,0 +1 @@ +B1 \ No newline at end of file diff --git a/public/icons/1854/B2.svg b/public/icons/1854/B2.svg new file mode 100644 index 0000000000..4fb0202381 --- /dev/null +++ b/public/icons/1854/B2.svg @@ -0,0 +1 @@ +B2 \ No newline at end of file diff --git a/public/icons/1854/B3.svg b/public/icons/1854/B3.svg new file mode 100644 index 0000000000..931e0ab714 --- /dev/null +++ b/public/icons/1854/B3.svg @@ -0,0 +1 @@ +B3 \ No newline at end of file diff --git a/public/icons/1854/B4.svg b/public/icons/1854/B4.svg new file mode 100644 index 0000000000..17edfe1ffa --- /dev/null +++ b/public/icons/1854/B4.svg @@ -0,0 +1 @@ +B4 \ No newline at end of file diff --git a/public/icons/1854/B5.svg b/public/icons/1854/B5.svg new file mode 100644 index 0000000000..a420ee8aed --- /dev/null +++ b/public/icons/1854/B5.svg @@ -0,0 +1 @@ +B5 \ No newline at end of file diff --git a/public/icons/1854/T1.svg b/public/icons/1854/T1.svg new file mode 100644 index 0000000000..57208dc3d4 --- /dev/null +++ b/public/icons/1854/T1.svg @@ -0,0 +1 @@ +T1 \ No newline at end of file diff --git a/public/icons/1854/T2.svg b/public/icons/1854/T2.svg new file mode 100644 index 0000000000..7a25ff689e --- /dev/null +++ b/public/icons/1854/T2.svg @@ -0,0 +1 @@ +T2 \ No newline at end of file diff --git a/public/icons/1854/T3.svg b/public/icons/1854/T3.svg new file mode 100644 index 0000000000..d761ffce82 --- /dev/null +++ b/public/icons/1854/T3.svg @@ -0,0 +1 @@ +T3 \ No newline at end of file diff --git a/public/icons/1854/T4.svg b/public/icons/1854/T4.svg new file mode 100644 index 0000000000..4ec2a06a92 --- /dev/null +++ b/public/icons/1854/T4.svg @@ -0,0 +1 @@ +T4 \ No newline at end of file diff --git a/public/icons/1854/T5.svg b/public/icons/1854/T5.svg new file mode 100644 index 0000000000..87f11a5c1d --- /dev/null +++ b/public/icons/1854/T5.svg @@ -0,0 +1 @@ +T5 \ No newline at end of file diff --git a/public/icons/1854/bonus_30.svg b/public/icons/1854/bonus_30.svg new file mode 100644 index 0000000000..b08e57ef31 --- /dev/null +++ b/public/icons/1854/bonus_30.svg @@ -0,0 +1 @@ ++30 diff --git a/public/icons/1854/bonus_40.svg b/public/icons/1854/bonus_40.svg new file mode 100644 index 0000000000..1eb998412b --- /dev/null +++ b/public/icons/1854/bonus_40.svg @@ -0,0 +1 @@ ++40 diff --git a/public/icons/1854/bonus_50.svg b/public/icons/1854/bonus_50.svg new file mode 100644 index 0000000000..6939c227b8 --- /dev/null +++ b/public/icons/1854/bonus_50.svg @@ -0,0 +1 @@ ++50 \ No newline at end of file diff --git a/public/icons/1854/bonus_90.svg b/public/icons/1854/bonus_90.svg new file mode 100644 index 0000000000..b2c5c3488d --- /dev/null +++ b/public/icons/1854/bonus_90.svg @@ -0,0 +1 @@ ++90 diff --git a/public/icons/1854/mine.svg b/public/icons/1854/mine.svg new file mode 100644 index 0000000000..4306cd28f4 --- /dev/null +++ b/public/icons/1854/mine.svg @@ -0,0 +1 @@ + \ No newline at end of file From 5db4affad16f2fa024106b45f50d0b67d0937941 Mon Sep 17 00:00:00 2001 From: Andrew Zwicky Date: Wed, 15 Nov 2023 18:18:34 -0600 Subject: [PATCH 002/167] Add icons/logos and more map,tile,corporation info added. --- lib/engine/game/g_1854/entities.rb | 672 +++------ lib/engine/game/g_1854/game.rb | 937 +------------ lib/engine/game/g_1854/map.rb | 291 ++-- lib/engine/game/g_1854/phases.rb | 53 + lib/engine/game/g_1854/player.rb | 22 - lib/engine/game/g_1854/tiles.rb | 72 + lib/engine/game/g_1854/trains.rb | 84 ++ public/icons/1854/1.svg | 1 + public/icons/1854/2.svg | 1 + public/icons/1854/3.svg | 1 + public/icons/1854/4.svg | 1 + public/icons/1854/5.svg | 1 + public/icons/1854/6.svg | 1 + public/icons/1854/eiffel.svg | 2028 ++++++++++++++++++++++++++++ public/icons/1854/minus_ten.svg | 1 + public/logos/1854/1.svg | 1 + public/logos/1854/2.svg | 1 + public/logos/1854/3.svg | 1 + public/logos/1854/4.svg | 1 + public/logos/1854/5.svg | 1 + public/logos/1854/6.svg | 1 + public/logos/1854/FJ.svg | 1 + public/logos/1854/KB.svg | 1 + public/logos/1854/KE.svg | 1 + public/logos/1854/KR.svg | 1 + public/logos/1854/KT.svg | 1 + public/logos/1854/NT.svg | 1 + public/logos/1854/SB.svg | 1 + public/logos/1854/SD.svg | 1 + public/logos/1854/VB.svg | 1 + 30 files changed, 2542 insertions(+), 1639 deletions(-) create mode 100644 lib/engine/game/g_1854/phases.rb delete mode 100644 lib/engine/game/g_1854/player.rb create mode 100644 lib/engine/game/g_1854/tiles.rb create mode 100644 lib/engine/game/g_1854/trains.rb create mode 100644 public/icons/1854/1.svg create mode 100644 public/icons/1854/2.svg create mode 100644 public/icons/1854/3.svg create mode 100644 public/icons/1854/4.svg create mode 100644 public/icons/1854/5.svg create mode 100644 public/icons/1854/6.svg create mode 100644 public/icons/1854/eiffel.svg create mode 100644 public/icons/1854/minus_ten.svg create mode 100644 public/logos/1854/1.svg create mode 100644 public/logos/1854/2.svg create mode 100644 public/logos/1854/3.svg create mode 100644 public/logos/1854/4.svg create mode 100644 public/logos/1854/5.svg create mode 100644 public/logos/1854/6.svg create mode 100644 public/logos/1854/FJ.svg create mode 100644 public/logos/1854/KB.svg create mode 100644 public/logos/1854/KE.svg create mode 100644 public/logos/1854/KR.svg create mode 100644 public/logos/1854/KT.svg create mode 100644 public/logos/1854/NT.svg create mode 100644 public/logos/1854/SB.svg create mode 100644 public/logos/1854/SD.svg create mode 100644 public/logos/1854/VB.svg diff --git a/lib/engine/game/g_1854/entities.rb b/lib/engine/game/g_1854/entities.rb index e72441df85..41ffad174d 100644 --- a/lib/engine/game/g_1854/entities.rb +++ b/lib/engine/game/g_1854/entities.rb @@ -4,579 +4,215 @@ module Engine module Game module G1854 module Entities - MOUNTAIN_HEXES = %w[H7 L13 I14 G14 F19 J29 L23].freeze - MOUNTAIN_TILES = %w[XM1 XM2 XM3].freeze + LOCAL_NAMES = [ + 'Mariazellerbahn', + 'Kernhofer Bahn', + 'Ybbstalbahn', + 'Steyrtalbahn', + 'Pyhrnbahn', + 'Salzkammergutbahn' + ] - TUNNEL_HEXES = %w[J9 I12 K14 I16 H17 H19 H21 H23 H27].freeze - TUNNEL_TILES = %w[X78 X79].freeze + LOCAL_COORDINATES =[ + 'J38', + 'J38', + 'M35', + 'J32', + 'J28', + 'L28' + ] + + LOCAL_CITIES = [ + 0, + 1, + 0, + 0, + 0, + 0, + ] + + LOCAL_COMPANIES = LOCAL_NAMES.zip(LOCAL_COORDINATES, LOCAL_CITIES).map.with_index do |vals, index| + name, coords, city = vals + sym = (index + 1).to_s + { + sym: sym, + name: "#{name} (#{sym})", + value: 150, + desc: '', + revenue: nil, + abilities: [{ type: 'close', when: 'par', corporation: sym }, + { type: 'shares', shares: "#{sym}_0" }], + color: nil, + } + end.freeze COMPANIES = [ { - name: 'P1 - Brienzer-Rothorn-Bahn', + name: 'Außerfernbahn', sym: 'P1', value: 20, revenue: 5, - desc: 'No special ability.', + desc: 'Building on one mountain is 20 G cheaper.', }, { - name: 'P2 - Bödelibahn', + name: 'Murtalbahn', sym: 'P2', value: 50, revenue: 10, - desc: 'Once in the game, a corporation may place an additional yellow track tile' \ - ' according to the rules. Either that corporation or its director must be' \ - ' the owner of the company.', - abilities: [ - { - type: 'tile_lay', - after_phase: '2', - when: %w[track owning_player_track], - count: 1, - reachable: true, - special: false, - tiles: %w[3 4 5 6 7 8 9 57 58], - hexes: [], - }, - ], + desc: 'Building one tunnel is 40 G cheaper.', }, { - name: 'P3 - Gotthard-Postkutsche', + name: 'Graz-Köflacher Bahn', sym: 'P3', - value: 80, + value: 70, revenue: 15, - desc: 'Comes with a Tunnel company', - abilities: [{ type: 'acquire_company', company: 'T1' }], + desc: 'Routes through Graz earn 10 G extra.', }, + *LOCAL_COMPANIES, { - name: 'P4 - Furka-Oberalpbahn', + name: 'Arlbergbahn', sym: 'P4', - value: 110, + value: 170, revenue: 20, - desc: 'The owner of this company may at any time place the Furka-Oberalp special tile. This is an' \ - ' additional tile lay and free of cost. Doing this closes the company; its owner receives 80 SFR' \ - ' as a compensation. This happens at the latest by the sale of the first 5/5H train.' \ - " Its owner can't waive that build.", - abilities: [ - { - type: 'choose_ability', - after_phase: '2', - when: %w[track owning_player_track], - choices: ['Place tile'], - }, - ], + desc: 'Receives a 20% VB share. Closes when the VB runs for the first time.', }, { - name: 'P5 - Compagnie Montreaux-Montbovon', + name: 'Semmeringbahn', sym: 'P5', - value: 140, + value: 190, revenue: 25, - desc: "Comes with a 10% share of the MOB. This share can't be sold until MOB has been parred.", - abilities: [{ type: 'shares', shares: 'MOB_1' }], - }, - { - name: 'P6 - Societa anonima delle ferrovie', - sym: 'P6', - value: 180, - revenue: 30, - desc: "Comes with the Director's share of the FNM. When purchased, the owner sets the par price for the FNM" \ - ' and it immediately floats, with 3 shares going to the market. The company closes when the FNM runs' \ - ' a train for the first time.', - abilities: [{ type: 'close', when: 'ran_train', corporation: 'FNM' }, - { type: 'no_buy' }, - { type: 'shares', shares: 'FNM_0' }], - }, - { - name: 'P7 - Lokfabrik Oerlikon', - sym: 'P7', - value: 100, - revenue: 0, - desc: 'The company closes when the first 5 or 5H train is bought and the player receives the 5H train "EVA".' \ - ' They may assign it immediately or later to any corporation they are the director of. Train limits must'\ - ' be kept.', - abilities: [ - { type: 'close', on_phase: 'never' }, - { type: 'no_buy' }, - { - type: 'choose_ability', - after_phase: '4', - when: 'owning_player_or_turn', - choices: [], # Defined in special_choose step - }, - ], - }, - { - name: 'B1 - Mountain Railway', - sym: 'B1', - value: 150, - revenue: 0, - desc: 'Upon purchase, select an unused revenue tile and assign it to an unoccupied mountain. Available' \ - ' revenue tiles are (2) 10/20/50/80, (2) 10/40/50/60, and (2) 10/50/80/10. Once the selected' \ - " mountain is included in any corporation's route, this company pays revenue of 40 SFR at the" \ - ' beginning of each operating round and is worth 150 SFR at end game. Each player can only' \ - ' purchase one Mountain Railway per stock round. Does not close and does not count against' \ - ' certificate limit.', - color: 'brown', - abilities: [ - { type: 'close', on_phase: 'never' }, - { type: 'no_buy' }, - { - type: 'tile_lay', - when: 'stock_round', - owner_type: 'player', - blocks: true, - count: 1, - tiles: MOUNTAIN_TILES, - hexes: MOUNTAIN_HEXES, - }, - ], - }, - { - name: 'B2 - Mountain Railway', - sym: 'B2', - value: 150, - revenue: 0, - desc: 'Upon purchase, select an unused revenue tile and assign it to an unoccupied mountain. Available' \ - ' revenue tiles are (2) 10/20/50/80, (2) 10/40/50/60, and (2) 10/50/80/10. Once the selected' \ - " mountain is included in any corporation's route, this company pays revenue of 40 SFR at the" \ - ' beginning of each operating round and is worth 150 SFR at end game. Each player can only' \ - ' purchase one Mountain Railway per stock round. Does not close and does not count against' \ - ' certificate limit.', - color: 'brown', - abilities: [ - { type: 'close', on_phase: 'never' }, - { type: 'no_buy' }, - { - type: 'tile_lay', - when: 'stock_round', - blocks: true, - count: 1, - tiles: MOUNTAIN_TILES, - hexes: MOUNTAIN_HEXES, - }, - ], - }, - { - name: 'B3 - Mountain Railway', - sym: 'B3', - value: 150, - revenue: 0, - desc: 'Upon purchase, select an unused revenue tile and assign it to an unoccupied mountain. Available' \ - ' revenue tiles are (2) 10/20/50/80, (2) 10/40/50/60, and (2) 10/50/80/10. Once the selected' \ - " mountain is included in any corporation's route, this company pays revenue of 40 SFR at the" \ - ' beginning of each operating round and is worth 150 SFR at end game. Each player can only' \ - ' purchase one Mountain Railway per stock round. Does not close and does not count against' \ - ' certificate limit.', - color: 'brown', - abilities: [ - { type: 'close', on_phase: 'never' }, - { type: 'no_buy' }, - { - type: 'tile_lay', - when: 'stock_round', - blocks: true, - count: 1, - tiles: MOUNTAIN_TILES, - hexes: MOUNTAIN_HEXES, - }, - ], - }, - { - name: 'B4 - Mountain Railway', - sym: 'B4', - value: 150, - revenue: 0, - desc: 'Upon purchase, select an unused revenue tile and assign it to an unoccupied mountain. Available' \ - ' revenue tiles are (2) 10/20/50/80, (2) 10/40/50/60, and (2) 10/50/80/10. Once the selected' \ - " mountain is included in any corporation's route, this company pays revenue of 40 SFR at the" \ - ' beginning of each operating round and is worth 150 SFR at end game. Each player can only' \ - ' purchase one Mountain Railway per stock round. Does not close and does not count against' \ - ' certificate limit.', - color: 'brown', - abilities: [ - { type: 'close', on_phase: 'never' }, - { type: 'no_buy' }, - { - type: 'tile_lay', - when: 'stock_round', - blocks: true, - count: 1, - tiles: MOUNTAIN_TILES, - hexes: MOUNTAIN_HEXES, - }, - ], - }, - { - name: 'B5 - Mountain Railway', - sym: 'B5', - value: 150, - revenue: 0, - desc: 'Upon purchase, select an unused revenue tile and assign it to an unoccupied mountain. Available' \ - ' revenue tiles are (2) 10/20/50/80, (2) 10/40/50/60, and (2) 10/50/80/10. Once the selected' \ - " mountain is included in any corporation's route, this company pays revenue of 40 SFR at the" \ - ' beginning of each operating round and is worth 150 SFR at end game. Each player can only' \ - ' purchase one Mountain Railway per stock round. Does not close and does not count against' \ - ' certificate limit.', - color: 'brown', - abilities: [ - { type: 'close', on_phase: 'never' }, - { type: 'no_buy' }, - { - type: 'tile_lay', - when: 'stock_round', - blocks: true, - count: 1, - tiles: MOUNTAIN_TILES, - hexes: MOUNTAIN_HEXES, - }, - ], - }, - { - name: 'T1 - Tunnel Company', - sym: 'T1', - value: 50, - revenue: 0, - desc: "As an additional tile lay action, one of the owning player's corporations may place a tunnel" \ - ' on an unused tunnel hex that it can reach for 100 SFR. Once the tunnel is included in any' \ - " corporation's route, this company pays revenue of 10 SFR at the beginning of each operating" \ - ' round and is worth 50 SFR at end game. Each player can only purchase one Tunnel Company per' \ - ' stock round. Does not close and does not count against certificate limit.', - color: 'gray', - abilities: [ - { - type: 'tile_lay', - when: 'owning_player_track', - count: 1, - cost: 100, - reachable: true, - tiles: TUNNEL_TILES, - hexes: TUNNEL_HEXES, - }, - { type: 'close', on_phase: 'never' }, - { type: 'no_buy' }, - ], - }, - { - name: 'T2 - Tunnel Company', - sym: 'T2', - value: 50, - revenue: 0, - desc: "As an additional tile lay action, one of the owning player's corporations may place a tunnel" \ - ' on an unused tunnel hex that it can reach for 100 SFR. Once the tunnel is included in any' \ - " corporation's route, this company pays revenue of 10 SFR at the beginning of each operating" \ - ' round and is worth 50 SFR at end game. Each player can only purchase one Tunnel Company per' \ - ' stock round. Does not close and does not count against certificate limit.', - color: 'gray', - abilities: [ - { - type: 'tile_lay', - when: 'owning_player_track', - count: 1, - cost: 100, - reachable: true, - tiles: TUNNEL_TILES, - hexes: TUNNEL_HEXES, - }, - { type: 'close', on_phase: 'never' }, - { type: 'no_buy' }, - ], - }, - { - name: 'T3 - Tunnel Company', - sym: 'T3', - value: 50, - revenue: 0, - desc: "As an additional tile lay action, one of the owning player's corporations may place a tunnel" \ - ' on an unused tunnel hex that it can reach for 100 SFR. Once the tunnel is included in any' \ - " corporation's route, this company pays revenue of 10 SFR at the beginning of each operating" \ - ' round and is worth 50 SFR at end game. Each player can only purchase one Tunnel Company per' \ - ' stock round. Does not close and does not count against certificate limit.', - color: 'gray', - abilities: [ - { - type: 'tile_lay', - when: 'owning_player_track', - count: 1, - cost: 100, - reachable: true, - tiles: TUNNEL_TILES, - hexes: TUNNEL_HEXES, - }, - { type: 'close', on_phase: 'never' }, - { type: 'no_buy' }, - ], - }, - { - name: 'T4 - Tunnel Company', - sym: 'T4', - value: 50, - revenue: 0, - desc: "As an additional tile lay action, one of the owning player's corporations may place a tunnel" \ - ' on an unused tunnel hex that it can reach for 100 SFR. Once the tunnel is included in any' \ - " corporation's route, this company pays revenue of 10 SFR at the beginning of each operating" \ - ' round and is worth 50 SFR at end game. Each player can only purchase one Tunnel Company per' \ - ' stock round. Does not close and does not count against certificate limit.', - color: 'gray', - abilities: [ - { - type: 'tile_lay', - when: 'owning_player_track', - count: 1, - cost: 100, - reachable: true, - tiles: TUNNEL_TILES, - hexes: TUNNEL_HEXES, - }, - { type: 'close', on_phase: 'never' }, - { type: 'no_buy' }, - ], - }, - { - name: 'T5 - Tunnel Company', - sym: 'T5', - value: 50, - revenue: 0, - desc: "As an additional tile lay action, one of the owning player's corporations may place a tunnel" \ - ' on an unused tunnel hex that it can reach for 100 SFR. Once the tunnel is included in any' \ - " corporation's route, this company pays revenue of 10 SFR at the beginning of each operating" \ - ' round and is worth 50 SFR at end game. Each player can only purchase one Tunnel Company per' \ - ' stock round. Does not close and does not count against certificate limit.', - color: 'gray', - abilities: [ - { - type: 'tile_lay', - when: 'owning_player_track', - count: 1, - cost: 100, - reachable: true, - tiles: TUNNEL_TILES, - hexes: TUNNEL_HEXES, - }, - { type: 'close', on_phase: 'never' }, - { type: 'no_buy' }, - ], + desc: 'Receives a 20% SD share. Closes when the SD runs for the first time.', }, ].freeze - CORPORATIONS = [ - { - float_percent: 50, - sym: 'NOB', - name: 'Schweizerische Nordostbahn (V1)', - logo: '1854/NOB.alt', - simple_logo: '1854/NOB.alt', - type: 'pre-sbb', - shares: [50, 25, 25], + LOCAL_CORPORATIONS = LOCAL_NAMES.zip(LOCAL_COORDINATES, LOCAL_CITIES).map.with_index do |vals, index| + name, coords, city = vals + sym = (index + 1).to_s + { + # sym: sym, + # name: name, + # logo: "1854/#{sym}", + # simple_logo: "1854/#{sym}", + # tokens: [0, 40], + # coordinates: coords, + # city: city, + # color: '#000000', + + # float_percent: 100, + sym: sym, + name: name, + coordinates: coords, + city: city, + logo: "1854/#{sym}", + simple_logo: "1854/#{sym}", tokens: [0, 40], - max_ownership_percent: 75, - coordinates: 'D19', - destination_coordinates: 'D15', - color: '#d8d2d3', - text_color: '#363552', - }, + color: '#000000', + # shares: [100], + # max_ownership_percent: 100, + # hide_shares: true, + # type: 'minor', + } + end.freeze + + MINORS = LOCAL_CORPORATIONS + + CORPORATIONS = [ { float_percent: 50, - sym: 'SCB', - name: 'Schweizerische Centralbahn (V2)', - logo: '1854/SCB.alt', - simple_logo: '1854/SCB.alt', - type: 'pre-sbb', - shares: [50, 25, 25], - tokens: [0, 40], - max_ownership_percent: 75, - coordinates: 'C12', - destination_coordinates: 'F17', - color: '#583838', - text_color: '#a2452b', + sym: 'KE', + name: 'Kaiserin Elisabeth-Westbahn', + logo: '1854/KE', + simple_logo: '1854/KE', + tokens: [0, 40, 100, 100], + type: 'major', + coordinates: 'C23', + city: 1, + color: '#F0AC9D', + text_color: 'black', }, { float_percent: 50, - sym: 'VSB', - name: 'Vereinigte Schweizer Bahnen (V3)', - logo: '1854/VSB.alt', - simple_logo: '1854/VSB.alt', - type: 'pre-sbb', - shares: [50, 25, 25], - tokens: [0, 40], - max_ownership_percent: 75, - coordinates: 'C24', - destination_coordinates: 'F25', - color: '#225252', - text_color: '#d8d2d3', + sym: 'FJ', + name: 'Kaiser Franz Joseph-Bahn', + logo: '1854/FJ', + simple_logo: '1854/FJ', + tokens: [0, 40, 100], + type: 'major', + coordinates: 'C23', + city: 2, + color: '#D4AB6F', }, { float_percent: 50, - sym: 'JS', - name: 'Jura-Simplon (V4)', - logo: '1854/JS.alt', - simple_logo: '1854/JS.alt', - type: 'pre-sbb', - shares: [50, 25, 25], - tokens: [0, 40], - max_ownership_percent: 75, - coordinates: 'I4', + sym: 'SD', + name: 'Südbahn', + logo: '1854/SD', + simple_logo: '1854/SD', + tokens: [0, 40, 100, 100], + type: 'major', + coordinates: 'C23', city: 0, - destination_coordinates: 'F7', - color: '#d8d2d3', - text_color: '#225252', + color: '#E5712F', }, { float_percent: 50, - sym: 'GB', - name: 'Gotthardbahn (V5)', - logo: '1854/GB.alt', - simple_logo: '1854/GB.alt', - type: 'pre-sbb', - shares: [50, 25, 25], - tokens: [0, 40], - max_ownership_percent: 75, - coordinates: 'G18', - destination_coordinates: 'H19', - color: '#c1b22b', + sym: 'KR', + name: 'Kronprinz Rudolf-Bahn', + logo: '1854/KR', + simple_logo: '1854/KR', + tokens: [0, 40, 100, 100], + type: 'major', + coordinates: 'C17', + color: '#82B642', text_color: 'black', }, - { - float_percent: 60, - sym: 'JN', - name: 'Jura Neuchatelois (R1)', - logo: '1854/JN.alt', - simple_logo: '1854/JN.alt', - shares: [40, 20, 20, 20], - tokens: [0, 40, 100], - type: 'regional', - coordinates: 'F7', - color: '#3f963d', - }, - { - float_percent: 60, - sym: 'ChA', - name: 'Chur-Arosa (R2)', - logo: '1854/ChA.alt', - simple_logo: '1854/ChA.alt', - shares: [40, 20, 20, 20], - tokens: [0, 40, 100], - type: 'regional', - coordinates: 'G28', - color: '#242943', - text_color: '#bcba4c', - }, - { - float_percent: 60, - sym: 'VZ', - name: 'Visp-Zermatt (R3)', - logo: '1854/VZ.alt', - simple_logo: '1854/VZ.alt', - shares: [40, 20, 20, 20], - tokens: [0, 40, 100], - type: 'regional', - coordinates: 'K10', - color: '#b02c2d', - }, { float_percent: 50, - sym: 'FNM', - name: 'Ferrovie Nord Milano (H1)', - logo: '1854/FNM.alt', - simple_logo: '1854/FNM.alt', - shares: [20, 10, 10, 10, 10, 10, 10, 10, 10], - tokens: [0, 40, 100, 100, 100], - type: 'historical', - coordinates: 'L21', - destination_coordinates: 'G20', - color: '#2a5f3b', - }, - { - float_percent: 50, - sym: 'RhB', - name: 'Rhätische Bahn (H2)', - logo: '1854/RhB.alt', - simple_logo: '1854/RhB.alt', - shares: [20, 10, 10, 10, 10, 10, 10, 10, 10], - tokens: [0, 40, 100, 100, 100], - type: 'historical', - coordinates: 'G26', - destination_coordinates: 'J13', - color: '#cf3334', + sym: 'KT', + name: 'Kärntner Bahn', + logo: '1854/KT', + simple_logo: '1854/KT', + tokens: [0, 40, 100], + type: 'major', + coordinates: 'H18', + color: '#FFFFFF', + text_color: 'black', }, { float_percent: 50, - sym: 'BLS', - name: 'Bern-Lötschberg-Simplon (H3)', - logo: '1854/BLS.alt', - simple_logo: '1854/BLS.alt', - shares: [20, 10, 10, 10, 10, 10, 10, 10, 10], - tokens: [0, 40, 100, 100, 100], - type: 'historical', - coordinates: 'F11', - destination_coordinates: 'J13', - abilities: [{ type: 'assign_hexes', hexes: ['J13'], count: 1 }], - color: '#c1b22b', + sym: 'SB', + name: 'Salzburger Bahn', + logo: '1854/SB', + simple_logo: '1854/SB', + tokens: [0, 40, 100], + type: 'major', + coordinates: 'E13', + color: '#FF3B1E', }, { float_percent: 50, - sym: 'STB', - name: 'Sensetalbahn (H4)', - logo: '1854/STB.alt', - simple_logo: '1854/STB.alt', - shares: [20, 10, 10, 10, 10, 10, 10, 10, 10], - tokens: [0, 40, 100, 100, 100], - type: 'historical', - coordinates: 'D15', - city: 0, - destination_coordinates: 'H13', - color: '#3e3d5e', + sym: 'NT', + name: 'Nordtiroler Staatsbahn', + logo: '1854/NT', + simple_logo: '1854/NT', + tokens: [0, 40, 100, 100], + type: 'major', + coordinates: 'F8', + color: '#7DC5E0', }, { float_percent: 50, - sym: 'AB', - name: 'Appenzeller Bahn (H5)', - logo: '1854/AB.alt', - simple_logo: '1854/AB.alt', - shares: [20, 10, 10, 10, 10, 10, 10, 10, 10], - tokens: [0, 40, 100, 100, 100], - type: 'historical', - coordinates: 'D25', - destination_coordinates: 'C20', - color: '#d8d2d3', + sym: 'VB', + name: 'Vorarlberger Bahn', + logo: '1854/VB', + simple_logo: '1854/VB', + tokens: [0, 40, 100], + type: 'major', + coordinates: 'E3', + color: '#ECE821', text_color: 'black', }, - { - float_percent: 50, - sym: 'MOB', - name: 'Montreux-Oberland Bernois (H6)', - logo: '1854/MOB.alt', - simple_logo: '1854/MOB.alt', - shares: [20, 10, 10, 10, 10, 10, 10, 10, 10], - tokens: [0, 40, 100, 100, 100], - type: 'historical', - coordinates: 'I6', - destination_coordinates: 'H13', - color: '#be8c3a', - }, - { - float_percent: 20, - sym: 'SBB', - name: 'Schweizer Bundesbahnen', - logo: '1854/SBB.alt', - simple_logo: '1854/SBB.alt', - shares: [10, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 10, 10, 10, 10], - tokens: [], - type: 'historical', - floatable: false, - color: '#913e2e', - abilities: [ - { - type: 'train_buy', - description: 'Buys and sells trains at face value only', - face_value: true, - }, - { - type: 'train_limit', - increase: 2, - description: 'Train limit: 4', - }, - ], - }, - ].freeze + ] end end end diff --git a/lib/engine/game/g_1854/game.rb b/lib/engine/game/g_1854/game.rb index f8e2ac7988..900ead6f0b 100644 --- a/lib/engine/game/g_1854/game.rb +++ b/lib/engine/game/g_1854/game.rb @@ -3,7 +3,9 @@ require_relative 'entities' require_relative 'map' require_relative 'meta' -require_relative 'player' +require_relative 'phases' +require_relative 'trains' +require_relative 'tiles' require_relative 'stock_market' require_relative '../base' @@ -14,42 +16,40 @@ class Game < Game::Base include_meta(G1854::Meta) include Entities include Map + include Phases + include Trains + include Tiles - PLAYER_CLASS = G1854::Player + CURRENCY_FORMAT_STR = '%s G' - CURRENCY_FORMAT_STR = '%s SFR' + BANK_CASH = 10_000 - BANK_CASH = 12_000 - - CERT_LIMIT = { 3 => 24, 4 => 18, 5 => 15, 6 => 13, 7 => 11 }.freeze + # TODO: cert limit changes with share split companies + CERT_LIMIT = { + 3 => 24, + 4 => 18, + 5 => 15, + 6 => 13, + 7 => 11 + }.freeze CERT_LIMIT_INCLUDES_PRIVATES = false - STARTING_CASH = { 3 => 800, 4 => 620, 5 => 510, 6 => 440, 7 => 400 }.freeze + STARTING_CASH = { 3 => 860, 4 => 650, 5 => 525, 6 => 450 }.freeze SELL_BUY_ORDER = :sell_buy + + # TODO: this is different for hex market SELL_MOVEMENT = :down_block POOL_SHARE_DROP = :left_block - NEXT_SR_PLAYER_ORDER = :most_cash + EBUY_PRES_SWAP = false + + # TODO: unsure EBUY_DEPOT_TRAIN_MUST_BE_CHEAPEST = false - DISCARDED_TRAINS = :remove TRACK_RESTRICTION = :permissive TILE_RESERVATION_BLOCKS_OTHERS = :always - ASSIGNMENT_TOKENS = { - 'B1' => '/icons/1854/B1.svg', - 'B2' => '/icons/1854/B2.svg', - 'B3' => '/icons/1854/B3.svg', - 'B4' => '/icons/1854/B4.svg', - 'B5' => '/icons/1854/B5.svg', - 'T1' => '/icons/1854/T1.svg', - 'T2' => '/icons/1854/T2.svg', - 'T3' => '/icons/1854/T3.svg', - 'T4' => '/icons/1854/T4.svg', - 'T5' => '/icons/1854/T5.svg', - }.freeze - MARKET = [ ['', '', @@ -99,893 +99,18 @@ class Game < Game::Base STOCKMARKET_COLORS = Base::STOCKMARKET_COLORS.merge(par_1: :blue, type_limited: :peach).freeze - PHASES = [ - { - name: '1', - train_limit: { 'pre-sbb': 2, regional: 2, historical: 4 }, - tiles: [:yellow], - operating_rounds: 1, - }, - { - name: '2', - on: '2', - train_limit: { 'pre-sbb': 2, regional: 2, historical: 4 }, - tiles: [:yellow], - operating_rounds: 1, - }, - { - name: '3', - on: '3', - train_limit: { 'pre-sbb': 2, regional: 2, historical: 4 }, - tiles: %i[yellow green], - operating_rounds: 2, - status: %w[can_buy_companies], - }, - { - name: '4', - on: '4', - train_limit: { 'pre-sbb': 2, regional: 2, historical: 3 }, - tiles: %i[yellow green], - operating_rounds: 2, - status: %w[can_buy_companies], - }, - { - name: '5', - on: '5', - train_limit: 2, - tiles: %i[yellow green brown], - operating_rounds: 3, - }, - { - name: '6', - on: '6', - train_limit: 2, - tiles: %i[yellow green brown], - operating_rounds: 3, - }, - { - name: '7', - on: '8E', - train_limit: 2, - tiles: %i[yellow green brown gray], - operating_rounds: 3, - }, - ].freeze - - TRAINS = [ - { - name: '2', - num: 13, - distance: 2, - price: 90, - rusts_on: '4', - variants: [ - { - name: '2H', - distance: 2, - price: 70, - }, - ], - events: [{ 'type' => 'train_exports' }], - }, - { - name: '3', - num: 9, - distance: 3, - price: 180, - rusts_on: '6', - variants: [ - { - name: '3H', - distance: 3, - price: 150, - }, - ], - events: [{ 'type' => '2t_downgrade' }, { 'type' => 'company_abilities' }, { 'type' => 'buy_across' }], - }, - { - name: '4', - num: 6, - distance: 4, - price: 300, - rusts_on: '8E', - variants: [ - { - name: '4H', - distance: 4, - price: 260, - }, - ], - events: [{ 'type' => '3t_downgrade' }], - }, - { - name: '5', - num: 6, - distance: 5, - price: 450, - variants: [ - { - name: '5H', - distance: 5, - price: 400, - }, - ], - events: [{ 'type' => 'close_companies' }, { 'type' => 'sbb_formation' }], - }, - { - name: '6', - num: 4, - distance: 6, - price: 630, - variants: [ - { - name: '6H', - distance: 6, - price: 550, - }, - ], - events: [{ 'type' => '4t_downgrade' }, { 'type' => 'full_capitalization' }], - }, - { - name: '8E', - num: 20, - distance: [{ 'nodes' => %w[city offboard town], 'pay' => 8, 'visit' => 99 }], - price: 960, - variants: [ - { - name: '8H', - distance: 8, - price: 700, - }, - ], - events: [{ 'type' => '5t_downgrade' }], - }, - ].freeze - - EVENTS_TEXT = Base::EVENTS_TEXT.merge( - 'train_exports' => ['Train Exports', 'Next train exported at the end of each OR set'], - '2t_downgrade' => ['2 -> 2H', '2 trains downgraded to 2H trains'], - 'company_abilities' => ['Company Abilities', 'Company special abilities can be used'], - 'buy_across' => ['Buy Across', 'Trains can be bought between corporations'], - '3t_downgrade' => ['3 -> 3H', '3 trains downgraded to 3H trains'], - 'sbb_formation' => ['SBB Forms', 'SBB forms after the Operating Round'], - '4t_downgrade' => ['4 -> 4H', '4 trains downgraded to 4H trains'], - 'full_capitalization' => ['Full Capitalization', 'Newly formed corporations receive full capitalization'], - '5t_downgrade' => ['5 -> 5H', '5 trains downgraded to 5H trains'], - ).freeze - - P4_TILE_LAYS = { 'H17' => 'OP3', 'H19' => 'OP2', 'H21' => 'OP2', 'H23' => 'OP2', 'I16' => 'OP1' }.freeze - - def privates - @privates ||= @companies.select { |c| c.sym[0] == 'P' } - end - - def mountain_railways - @mountain_railways ||= @companies.select { |c| c.sym[0] == 'B' } - end - - def unactivated_mountain_railways - mountain_railways.select { |mr| mr.owner&.player? && mr.value.zero? } - end - - def tunnel_companies - @tunnel_companies ||= @companies.select { |c| c.sym[0] == 'T' } - end - - def unactivated_tunnel_companies - tunnel_companies.select { |tc| tc.owner&.player? && tc.value.zero? } - end - - def pre_sbb_corporations - # Priority ordered list of pre-sbb corporations - @pre_sbb_corps ||= %w[NOB SCB VSB JS GB].map { |id| corporation_by_id(id) } - end - - def fnm - @fnm ||= corporation_by_id('FNM') - end - - def sbb - @sbb ||= corporation_by_id('SBB') - end - - def gb - @gb ||= corporation_by_id('GB') - end - - def p4 - @p4 ||= company_by_id('P4') - end - - def p7 - @p7 ||= company_by_id('P7') - end - - # def setup - # setup_destinations - # setup_offboard_bonuses - # mountain_railways.each { |mr| mr.owner = @bank } - # tunnel_companies.each { |tc| tc.owner = @bank } - - # @eva = @depot.trains.find { |t| t.name == '5' && t.events.empty? } - # @depot.remove_train(@eva) - # @eva.reserved = true - # @eva.variant = '5H' - - # @sbb_train = @depot.trains.find { |t| t.name == '5' && t.events.empty? } - # @depot.forget_train(@sbb_train) - # @sbb_train.variant = '5H' - # @sbb_train.buyable = false - # sbb.shares.select { |s| s.percent == 10 && !s.president }.each { |s| s.double_cert = true } - - # @all_tiles.each { |t| t.ignore_gauge_walk = true } - # @_tiles.values.each { |t| t.ignore_gauge_walk = true } - # @hexes.each { |h| h.tile.ignore_gauge_walk = true } - # @graph.clear_graph_for_all - # end - - def setup_destinations - @corporations.each do |c| - next unless c.destination_coordinates - - dest_hex = hex_by_id(c.destination_coordinates) - ability = Ability::Base.new( - type: 'base', - description: "Destination: #{dest_hex.location_name} (#{dest_hex.name})", - ) - c.add_ability(ability) - - dest_hex.assign!(c) - end - end - - def setup_offboard_bonuses - hex_info = @hexes.map do |hex| - offboard = hex.tile.offboards.first - next if !offboard || hex.tile.color != :red - - icon = offboard.tile.icons.find { |i| i.name.start_with?('bonus_') } - [hex, offboard.groups.find { |g| g.size > 1 }, icon ? icon.name.split('_')[1].to_i : nil] - end.compact - group_bonus = hex_info.map { |_hex, group, bonus| group && bonus ? [group, bonus] : nil }.compact.to_h - - @hex_bonus_revenue = hex_info.map do |hex, group, bonus| - [hex, bonus || group_bonus[group]] - end.compact.to_h - end - - def event_train_exports! - @log << "-- Event: #{EVENTS_TEXT['train_exports'][1]} --" - end - - def event_company_abilities! - @log << "-- Event: #{EVENTS_TEXT['company_abilities'][1]} --" - end - - def event_buy_across! - @log << "-- Event: #{EVENTS_TEXT['buy_across'][1]} --" - end - - def event_2t_downgrade! - downgrade_train_type!('2', '2H') - end - - def event_3t_downgrade! - downgrade_train_type!('3', '3H') - end - - def event_close_companies! - lay_p4_overpass! unless p4.closed? - p7.revenue = 0 - super - end - - def event_sbb_formation! - @log << "-- Event: #{EVENTS_TEXT['sbb_formation'][1]} --" - @ready_to_form_sbb = true - end - - def event_4t_downgrade! - downgrade_train_type!('4', '4H') - end - - def event_full_capitalization! - @log << "-- Event: #{EVENTS_TEXT['full_capitalization'][1]} --" - @full_capitalization = true - @corporations.select { |corp| corp.type == :historical && !corp.floated }.each do |corp| - next unless corp.destination_coordinates - - hex_by_id(corp.destination_coordinates).remove_assignment!(corp) - corp.remove_ability(corp.abilities.find { |a| a.description.start_with?('Destination') }) - end - end - - def event_5t_downgrade! - downgrade_train_type!('5', '5H') - end - - def downgrade_train_type!(name, downgrade_name) - owners = Hash.new(0) - trains.select { |t| t.name == name }.each do |t| - t.variant = downgrade_name - owners[t.owner.name] += 1 if t.owner && t.owner != @depot - end - - @log << "-- Event: #{name} trains downgrade to #{downgrade_name} trains" \ - " (#{owners.map { |c, t| "#{c} x#{t}" }.join(', ')}) --" - end - - def form_sbb! - @log << '-- Event: SBB forms --' - - @stock_market.set_par(sbb, @stock_market.share_prices_with_types(%i[par_1]).first) - sbb.floatable = true - sbb.ipoed = true - sbb.floated = true - - @bank.spend(400, sbb) - @sbb_train.owner = sbb - sbb.trains << @sbb_train - @log << "#{sbb.name} starts with #{format_currency(400)} and a #{@sbb_train.name} train" - - previous_owners = [] - pre_sbb_corporations.each do |corp| - @log << "#{corp.name} merging into #{sbb.name}" - previous_owners << corp.owner - - @log << "#{sbb.name} receives #{format_currency(corp.cash)}" - corp.spend(corp.cash, sbb) if corp.cash.positive? - - place_sbb_tokens!(corp) - - num_trains = corp.trains.size - if num_trains.positive? - @log << "#{sbb.name} receives #{num_trains} train#{num_trains == 1 ? '' : 's'}:" \ - " #{corp.trains.map(&:name).join(', ')}" - transfer(:trains, corp, sbb) - end - - sbb_share_exchange!(corp) - - close_corporation(corp, quiet: true) - end - - sbb.tokens.sort_by! { |t| t.used ? 0 : 1 } - - determine_sbb_president!(previous_owners.uniq) - end - - def place_sbb_tokens!(corporation) - locations = corporation.tokens.map { |token| token.used ? token.hex.full_name : 'Unused' }.join(', ') - @log << "#{sbb.name} receives #{corporation.tokens.size} tokens: #{locations}" - corporation.tokens.each do |token| - sbb.tokens << Token.new(sbb, price: 100) - next unless token.used - - if token.city.tokened_by?(sbb) - @log << "#{sbb.name} already has a token on #{token.hex.full_name}, placing token on charter instead" - token.remove! - else - token.swap!(sbb.tokens.last, check_tokenable: false) - end - end - end - - def sbb_share_exchange!(corporation) - corporation.share_holders.keys.each do |share_holder| - sbb_shares = sbb.shares_of(sbb) - shares = share_holder.shares_of(corporation).map do |corp_share| - percent = corp_share.president ? 10 : 5 - share = sbb_shares.find { |sbb_share| sbb_share.percent == percent } - sbb_shares.delete(share) - share - end - next if shares.empty? - - share_holder = @share_pool if share_holder.corporation? - bundle = ShareBundle.new(shares) - @share_pool.transfer_shares(bundle, share_holder, allow_president_change: false) - - cash_per_share = corporation.par_price ? corporation.share_price.price - sbb.share_price.price : 0 - cash = cash_per_share * bundle.percent / 5 - msg = share_holder.name.to_s - if cash.zero? || share_holder == @share_pool - msg += ' receives' - elsif cash.positive? - msg += " receives #{format_currency(cash)} and" - @bank.spend(cash, share_holder) - else - msg += " pays #{format_currency(cash.abs)} and receives" - share_holder.spend(cash.abs, @bank, check_cash: false) - end - - msg += " #{bundle.percent}% of #{sbb.name}" - @log << msg - next if !share_holder.player? || !share_holder.cash.negative? - - debt = share_holder.cash.abs - share_holder.debt += debt - share_holder.cash += debt - @log << "#{share_holder.name} takes #{format_currency(debt)} of debt to complete payment" - end - end - - def determine_sbb_president!(president_priority_order) - player_share_percent = sbb.player_share_holders - max_percent = player_share_percent.values.max || 0 - return if max_percent < 10 - - # Determine president - candidates = player_share_percent.select { |_, percent| percent == max_percent }.keys - if candidates.size > 1 - candidates.sort_by! { |player| president_priority_order.index(player) || president_priority_order.size } - end - president = candidates.first - - # Make sure president has a 10% cert - if (president_shares = president.shares_of(sbb)).none? { |share| share.percent == 10 } - ten_percent_share = @share_pool.shares_of(sbb).find { |share| share.percent == 10 } || - president_priority_order[-1].shares_of(sbb).find { |share| share.percent == 10 } - @share_pool.transfer_shares(ShareBundle.new([ten_percent_share]), president, allow_president_change: false) - @share_pool.transfer_shares(ShareBundle.new(president_shares.take(2)), share.owner, allow_president_change: false) - end - - # Make sure president has the presidents cert - if (presidents_share_owner = sbb.presidents_share.owner) != president - @share_pool.transfer_shares(ShareBundle.new([sbb.presidents_share]), president) - @share_pool.transfer_shares( - ShareBundle.new([president_shares.find { |share| !share.president && share.percent == 10 }]), - presidents_share_owner - ) - end - - sbb.owner = president - @log << "#{president.name} becomes the president of #{sbb.name}" - end - - def player_value(player) - super - (player.companies & privates).sum(&:value) - end - - def player_debt(player) - player.debt - end - - def init_stock_market - G1854::StockMarket.new(game_market, self.class::CERT_LIMIT_TYPES, - multiple_buy_types: self.class::MULTIPLE_BUY_TYPES) - end - - def initial_auction_companies - privates - end - - def unowned_purchasable_companies(_entity) - @companies.select { |c| c.owner == @bank } - end - - def next_round! - @round = - case @round - when Engine::Round::Auction - init_round_finished - reorder_players(log_player_order: true) - new_stock_round - when Engine::Round::Stock - apply_interest_to_player_debt! - @operating_rounds = @phase.operating_rounds - reorder_players(log_player_order: true) - new_operating_round - when G1854::Round::Operating - next_round = - 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 - -> { new_stock_round } - end - if @ready_to_form_sbb - @post_sbb_formation_round = next_round - new_sbb_formation_round - else - next_round.call - end - when G1854::Round::SBBFormation - next_round = @post_sbb_formation_round - @ready_to_form_sbb = false - @post_sbb_formation_round = nil - next_round.call - end - end - - def or_set_finished - @depot.export! if @phase.name.to_i >= 2 - end - - def new_auction_round - Engine::Round::Auction.new(self, [ - G1854::Step::CompanyPendingPar, - Engine::Step::SelectionAuction, - ]) - end - - def stock_round - Engine::Round::Stock.new(self, [ - G1854::Step::MountainRailwayTrack, - G1854::Step::BuySellParShares, - ]) - end - - def operating_round(round_num) - G1854::Round::Operating.new(self, [ - Engine::Step::Bankrupt, - Engine::Step::DiscardTrain, - Engine::Step::Exchange, - G1854::Step::SpecialChoose, - G1854::Step::SpecialTrack, - G1854::Step::Destination, - G1854::Step::BuyCompany, - Engine::Step::HomeToken, - Engine::Step::Track, - G1854::Step::DestinationCheck, - Engine::Step::Token, - G1854::Step::DestinationCheck, - G1854::Step::Route, - G1854::Step::Dividend, - G1854::Step::BuyTrain, - [G1854::Step::BuyCompany, { blocks: true }], - ], round_num: round_num) - end - - def new_sbb_formation_round - @log << '-- SBB Formation Round --' - G1854::Round::SBBFormation.new(self, [ - Engine::Step::DiscardTrain, - G1854::Step::RemoveSBBTokens, - ]) - end - - def next_sr_player_order - @round.is_a?(Engine::Round::Auction) ? :least_cash : :most_cash - end - - def can_par?(corporation, _parrer) - return false if corporation == sbb - - super - end - - def after_par(corporation) - super - return unless corporation.type == :historical - - num_tokens = - case corporation.share_price.price - when 100 then 5 - when 90 then 4 - when 80 then 3 - when 70 then 2 - when 60 then 1 - else 0 - end - corporation.tokens.slice!(num_tokens..-1) - @log << "#{corporation.name} receives #{num_tokens} token#{num_tokens > 1 ? 's' : ''}" - return unless corporation == fnm - - @share_pool.transfer_shares(ShareBundle.new(corporation.shares_of(corporation).take(3)), @share_pool) - @log << "3 #{corporation.name} shares moved to the market" - float_corporation(corporation) - end - - def float_corporation(corporation) - return if corporation == sbb - - @log << "#{corporation.name} floats" - multiplier = - case corporation.type - when :'pre-sbb' then 2 - when :regional then 5 - when :historical then @full_capitalization ? 10 : 5 - end - @bank.spend(corporation.par_price.price * multiplier, corporation) - @log << "#{corporation.name} receives #{format_currency(corporation.cash)}" - end - - def can_hold_above_corp_limit?(_entity) - true - end - - def sellable_bundles(player, corporation) - bundles = super - return bundles if bundles.empty? || corporation.operated? - - bundles.each do |bundle| - bundle.share_price = @stock_market.find_share_price(corporation, Array.new(bundle.num_shares) { :down }).price - end - bundles - end - - def after_buy_company(player, company, _price) - super - return if !mountain_railways.include?(company) && !tunnel_companies.include?(company) - - company.revenue = 0 - company.value = 0 - end - - def lay_p4_overpass! - company = p4 - return if company.abilities.empty? - - owner = company.owner - compensation = 80 - - @log << "#{owner.name} must use #{company.name}" if @phase.name.to_i >= 5 - @log << "#{owner.name} (#{company.name}) lays Furka-Oberalp special tile" - @log << "#{owner.name} receives #{format_currency(compensation)}" - - @bank.spend(compensation, owner) - - P4_TILE_LAYS.each do |hex_id, tile_name| - hex = hex_by_id(hex_id) - tile = @tiles.find { |t| t.name == tile_name } - if (tunnel_path = hex.tile.paths.find { |path| path.track == :narrow }) - tile = replace_tile_code(tile, extend_tile_code(tile, narrow_track_code_for(tunnel_path.exits))) - @_tiles[tile.id] = tile - end - - update_tile_lists(tile, hex.tile) - hex.lay(tile) - end - - @log << "#{company.name} closes" - company.close! - end - - def assign_p7_train(corporation) - company = p7 - @log << "#{company.owner.name} (#{company.name}) assigns EVA #{@eva.name} train to #{corporation.name}" - buy_train(corporation, @eva, :free) - company.close! - end - - def all_potential_upgrades(tile, tile_manifest: false, selected_company: nil) - if self.class::MOUNTAIN_HEXES.include?(tile.hex.id) - return @all_tiles.select { |t| self.class::MOUNTAIN_TILES.include?(t.name) }.uniq(&:name) - end - - super - end - - def destinated?(entity) - home_node = entity.tokens.first&.city - destination_hex = hex_by_id(entity.destination_coordinates) - return false if !home_node || !destination_hex - return false unless destination_hex.assigned?(entity) - return hex_by_id('H19').tile.paths.any? { |path| path.track == :narrow } if entity == gb - - home_node.walk(corporation: entity) do |path, _| - return true if destination_hex == path.hex - end - - false - end - - def destinated!(corporation) - hex_by_id(corporation.destination_coordinates).remove_assignment!(corporation) - multiplier = corporation.type == :historical ? 5 : 2 - amount = corporation.par_price.price * multiplier - @bank.spend(amount, corporation) - @log << "#{corporation.name} has reached its destination and receives #{format_currency(amount)}" - end - - def must_buy_train?(entity) - super && entity.type != :'pre-sbb' - end - - def can_buy_train_from_others? - @phase.name.to_i >= 3 - end - - def hex_train?(train) - hex_train_name?(train.name) - end - - def hex_train_name?(name) - name[-1] == 'H' - end - - def express_train?(train) - train.name[-1] == 'E' - end - - def route_distance(route) - hex_train?(route.train) ? route_hex_distance(route) : super - end - - def route_hex_distance(route) - edges = route.chains.sum { |conn| conn[:paths].each_cons(2).sum { |a, b| a.hex == b.hex ? 0 : 1 } } - route.chains.empty? ? 0 : edges + 1 - end - - def route_distance_str(route) - hex_train?(route.train) ? "#{route_hex_distance(route)}H" : super - end - - def check_distance(route, visits) - hex_train?(route.train) ? check_hex_distance(route, visits) : super - end - - def check_hex_distance(route, _visits) - distance = route_hex_distance(route) - raise GameError, "#{distance} is too many hexes for #{route.train.name} train" if distance > route.train.distance - end - - def check_other(route) - if route.stops.any? { |stop| stop.route_revenue(route.phase, route.train).zero? } - raise GameError, 'No Mountain Railway to visit' - end - return unless hex_train?(route.train) - - raise GameError, 'Cannot visit offboard hexes' if route.stops.any? { |stop| stop.tile.color == :red } - end - - def revenue_stops(route) - stops = super - return stops unless express_train?(route.train) - - distance = route.train.distance.first['pay'] - return stops if stops.size <= distance - - # Prune the list of stops to improve performance - stops_by_revenue = stops.sort_by { |stop| -1 * stop.route_revenue(route.phase, route.train) } - stops = stops_by_revenue.slice!(0...distance) - unless stops.find { |stop| stop.tokened_by?(route.corporation) } - stops.pop - tokened_stop = stops_by_revenue.find { |stop| stop.tokened_by?(route.corporation) } - stops << tokened_stop if tokened_stop - end - stops.concat(stops_by_revenue.select { |stop| stop.tile.color == :red }) - end - - def revenue_for(route, stops) - revenue = super - revenue += 10 * stops.size if route.paths.any? { |path| path.track == :narrow } - revenue += east_west_bonus_revenue(stops) - revenue += north_south_bonus_revenue(stops) - revenue - end - - def revenue_str(route) - stops = route.stops - stop_hexes = stops.map(&:hex) - str = route.hexes.map { |h| stop_hexes.include?(h) ? h&.name : "(#{h&.name})" }.join('-') - str += ' + EW' if east_west_bonus?(route.stops) - str += ' + NS' if north_south_bonus?(route.stops) - str - end - - def hex_bonus_revenue(hex) - @hex_bonus_revenue[hex] || 0 - end - - def east_west_bonus?(stops) - (stops.flat_map(&:groups) & %w[E W]).size == 2 - end - - def east_west_bonus_revenue(stops) - east_west_bonus?(stops) ? stops.sum { |stop| hex_bonus_revenue(stop.hex) } : 0 - end - - def north_south_bonus?(stops) - (stops.flat_map(&:groups) & %w[N S]).size == 2 - end - - def north_south_bonus_revenue(stops) - north_south_bonus?(stops) ? stops.sum { |stop| hex_bonus_revenue(stop.hex) } : 0 - end - - def check_for_mountain_or_tunnel_activation(routes) - routes.each do |route| - route.hexes.select { |hex| self.class::MOUNTAIN_HEXES.include?(hex.id) }.each do |hex| - (unactivated_mountain_railways.map(&:id) & hex.assignments.keys).each do |id| - mountain_railway = company_by_id(id) - mountain_railway.value = 150 - mountain_railway.revenue = 40 - hex.remove_assignment!(id) - @log << "#{mountain_railway.name} has been activated" - end - end - - route.paths.select { |path| path.track == :narrow }.each do |path| - (unactivated_tunnel_companies.map(&:id) & path.hex.assignments.keys).each do |id| - tunnel_company = company_by_id(id) - tunnel_company.value = 50 - tunnel_company.revenue = 10 - path.hex.remove_assignment!(id) - @log << "#{tunnel_company.name} has been activated" - end - end - end - end - - def upgrades_to?(from, to, special = false, selected_company: nil) - return to.color == :purple && from.paths.none? { |p| p.track == :narrow } if from.color == :purple - return %w[14 15 619].include?(to.name) if from.hex.id == 'D15' && from.color == :yellow - - super - end - - def create_tunnel_tile(hex, tile) - replace_tile_code(tile, extend_tile_code(hex.tile, narrow_track_code_for(tile.exits))) - end - - def narrow_track_code_for(exits) - "path=a:#{exits[0]},b:#{exits[1]},track:narrow" - end - - def extend_tile_code(tile, additional_code) - code = tile.code + ';' + additional_code - code = code[1..-1] if code[0] == ';' - code - end - - def replace_tile_code(tile, new_code) - tile = Engine::Tile.new( - tile.name, - code: new_code, - color: tile.color, - parts: Engine::Tile.decode(new_code), - index: tile.index, - hidden: true, - ignore_gauge_walk: true, - ) - tile.ignore_gauge_walk = true - tile - end - - def graph_skip_paths(entity) - entity.type == :regional ? regional_skip_paths : super - end - - def regional_skip_paths - @regional_skip_paths ||= @hexes.select { |hex| hex.tile.color == :red }.flat_map do |hex| - hex.tile.paths.map { |path| [path, true] } - end.to_h - end - - def take_player_loan(player, loan) - player.cash += loan # debt does not come from the bank - interest = player_debt_interest(loan) - player.debt += loan + interest - - @log << "#{player.name} takes #{format_currency(loan)} in debt" - @log << "#{player.name} has 50% interest (#{format_currency(interest)}) applied to this debt" - end - - def apply_interest_to_player_debt! - @players.each do |player| - next if player.debt.zero? - - interest = player_debt_interest(player.debt) - player.debt += interest - @log << "#{player.name} has an additional 50% interest (#{format_currency(interest)}) applied to their debt" + def setup + # each minor starts with 150G, regardless of price paid in + # initial auction. + @minors.each do |minor| + @bank.spend(150, minor) end end - def payoff_player_loan(player) - payoff = player.cash >= player.debt ? player.debt : player.cash - verb = payoff == player.debt ? 'pays off' : 'decreases' - @log << "#{player.name} #{verb} their debt of #{format_currency(player.debt)}" - player.cash -= payoff - player.debt -= payoff - end - - def player_debt_interest(debt) - (debt * 0.5).ceil + def reservation_corporations + # populate reserved spaces on starting map + # so locals starting spaces can be seen more easily + @corporations + @minors end end end diff --git a/lib/engine/game/g_1854/map.rb b/lib/engine/game/g_1854/map.rb index 7de354b19b..ab3cd81fd2 100644 --- a/lib/engine/game/g_1854/map.rb +++ b/lib/engine/game/g_1854/map.rb @@ -4,215 +4,120 @@ module Engine module Game module G1854 module Map - TILES = { - '3' => 3, - '4' => 6, - '5' => 5, - '6' => 6, - '7' => 5, - '8' => 11, - '9' => 11, - '14' => 4, - '15' => 7, - '16' => 2, - '19' => 2, - '20' => 2, - '23' => 6, - '24' => 6, - '25' => 2, - '26' => 2, - '27' => 2, - '28' => 2, - '29' => 2, - '39' => 1, - '40' => 1, - '41' => 1, - '42' => 1, - '43' => 1, - '44' => 1, - '45' => 1, - '46' => 1, - '47' => 1, - '57' => 6, - '58' => 6, - '59' => 2, - '64' => 1, - '65' => 1, - '66' => 1, - '67' => 1, - '68' => 1, - '70' => 1, - '87' => 2, - '88' => 2, - '204' => 2, - '611' => 6, - '619' => 4, - '901' => { - 'count' => 1, - 'color' => 'green', - 'code' => 'city=revenue:40,loc:0.5;city=revenue:40,loc:2.5;path=a:0,b:_0;path=a:1,b:_0;path=a:2,b:_1;'\ - 'path=a:3,b:_1;label=L', - }, - '902' => { - 'count' => 1, - 'color' => 'brown', - 'code' => 'city=revenue:50,slots:2;path=a:0,b:_0;path=a:3,b:_0;path=a:4,b:_0;path=a:5,b:_0;label=L', - }, - '903' => { - 'count' => 1, - 'color' => 'gray', - 'code' => 'city=revenue:60,slots:3;path=a:0,b:_0;path=a:1,b:_0;path=a:2,b:_0;path=a:3,b:_0;label=L', - }, - '904' => { - 'count' => 1, - 'color' => 'green', - 'code' => 'city=revenue:40,slots:2;path=a:0,b:_0;path=a:1,b:_0;path=a:3,b:_0;path=a:4,b:_0;path=a:5,b:_0;label=B', - }, - '905' => { - 'count' => 1, - 'color' => 'brown', - '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;label=B', - }, - '906' => { - 'count' => 1, - 'color' => 'gray', - 'code' => 'city=revenue:60,slots:3;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=B', - }, - '907' => { - 'count' => 1, - 'color' => 'green', - 'code' => 'city=revenue:40,slots:2;path=a:0,b:_0;path=a:2,b:_0;path=a:3,b:_0;label=Z', - }, - '908' => { - 'count' => 1, - 'color' => 'green', - 'code' => 'city=revenue:40,slots:2;path=a:0,b:_0;path=a:3,b:_0;path=a:4,b:_0;label=Z', - }, - '909' => { - 'count' => 1, - 'color' => 'brown', - 'code' => 'city=revenue:50,slots:3;path=a:0,b:_0;path=a:3,b:_0;path=a:4,b:_0;path=a:5,b:_0;label=Z', - }, - '910' => { - 'count' => 1, - 'color' => 'gray', - 'code' => 'city=revenue:60,slots:4;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;'\ - 'label=Z', - }, - '911' => { - 'count' => 2, - 'color' => 'brown', - 'code' => 'town=revenue:10;path=a:0,b:_0;path=a:1,b:_0;path=a:3,b:_0;path=a:4,b:_0;path=a:5,b:_0', - }, - '915' => { - 'count' => 2, - 'color' => 'gray', - 'code' => 'city=revenue:50,slots:3;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', - }, - 'XM1' => { - 'count' => 2, - 'color' => 'gray', - 'code' => 'offboard=revenue:yellow_10|green_20|brown_50|gray_80', - }, - 'XM2' => { - 'count' => 2, - 'color' => 'gray', - 'code' => 'offboard=revenue:yellow_10|green_40|brown_50|gray_60', - }, - 'XM3' => { - 'count' => 2, - 'color' => 'gray', - 'code' => 'offboard=revenue:yellow_10|green_50|brown_80|gray_10', - }, - 'X78' => { - 'count' => 5, - 'color' => 'purple', - 'code' => 'path=a:0,b:2,track:narrow', - }, - 'X79' => { - 'count' => 5, - 'color' => 'purple', - 'code' => 'path=a:0,b:3,track:narrow', - }, - 'OP1' => { - 'count' => 1, - 'hidden' => true, - 'color' => 'purple', - 'code' => 'path=a:3,b:5', - }, - 'OP2' => { - 'count' => 3, - 'hidden' => true, - 'color' => 'purple', - 'code' => 'path=a:1,b:4', - }, - 'OP3' => { - 'count' => 1, - 'hidden' => true, - 'color' => 'purple', - 'code' => 'town=revenue:10,loc:4;path=a:0,b:_0;path=a:4,b:_0', - }, - }.freeze - LOCATION_NAMES = { 'A19' => 'Prag', - 'A25' => 'Brunn', + 'A25' => 'Brünn', + 'B18' => 'Freistadt', + 'B22' => 'Stockerau & Klosterneuburg', + 'C13' => 'Braunau', + 'C15' => 'Ried', + 'C17' => 'Linz', + 'C19' => 'Ybbs', + 'C21' => 'Krems & Tulln', + 'C23' => 'Wien & Vienna', + 'C27' => 'Pressburg', #TODO: + 'D0' => 'Paris', + 'D12' => 'München', + 'D16' => 'Wels', + 'D18' => 'Steyr & Bad Ischl', + 'D20' => 'Amstetten & Sankt Pölten', + 'D22' => 'Modling & Baden', + 'D24' => 'Wiener Neustadt', + 'D26' => 'Eisenstadt', 'D28' => 'Budapest', + 'D4' => 'Augsburg', # TODO: + 'E1' => 'Zurich', + 'E11' => 'Kufstein & Wörgl', + 'E13' => 'Salzburg', + 'E21' => 'Leoben & Kapfenberg', + 'E23' => 'Semmeringbahn', + 'E3' => 'Bregenz', + 'E5' => 'Außerfernbahn', + 'F14' => 'Mauterndorf', + 'F16' => 'Murtalbahn', + 'F2' => 'Dornbirn & Feldkirch', + 'F20' => 'Graz-Köflacher Bahn', + 'F22' => 'Graz', + 'F24' => 'Oberwart', + 'F28' => 'Konstantinopel', + 'F4' => 'Arlbergbahn', + 'F6' => 'Landeck', + 'F8' => 'Innsbruck', + 'G1' => 'Vaduz', + 'G3' => 'Bludenz', + 'H10' => 'Brenner', + 'H12' => 'Lienz', + 'H16' => 'Spital & Villach', + 'H18' => 'Klagenfurt', + 'I15' => 'Venedig', + 'I19' => 'Laibach', + 'J28' => 'Kirchdorf', + 'J32' => 'Steyr', + 'J34' => 'Amstetten', + 'J38' => 'Sankt Pölten', + 'L28' => 'Bad Ischl', + 'M35' => 'Hieflau', }.freeze HEXES = { red: { - ['A19'] => 'offboard=revenue:yellow_20|green_30|brown_50|gray_50;path=a:5,b:_0', - ['A25'] => 'offboard=revenue:yellow_20|green_30|brown_40|gray_40;path=a:5,b:_0', - ['C27'] => 'offboard=revenue:yellow_20|green_30|brown_40|gray_50;path=a:1,b:_0', - ['D4'] => 'offboard=revenue:yellow_00|green_20|brown_30|gray_40;path=a:0,b:_0', - ['E1'] => 'offboard=revenue:yellow_20|green_30|brown_40|gray_50;path=a:4,b:_0;path=a:5,b:_0', - ['G1'] => 'offboard=revenue:yellow_20|green_20|brown_20|gray_20;path=a:3,b:_0', - # TODO: group - ['D28'] => 'offboard=revenue:yellow_30|green_40|brown_50|gray_60,groups:Budapest,hide:1;path=a:1,b:_0;border=edge:0', - ['E27'] => 'offboard=revenue:yellow_30|green_40|brown_50|gray_60,groups:Budapest;path=a:2,b:_0;border=edge:3', - ['H10'] => 'offboard=revenue:yellow_30|green_40|brown_50|gray_60;path=a:2,b:_0;path=a:3,b:_0', - ['I15'] => 'offboard=revenue:yellow_20|green_30|brown_40|gray_50;path=a:3,b:_0', - ['I19'] => 'offboard=revenue:yellow_20|green_30|brown_30|gray_40;path=a:2,b:_0', + ['D4'] => 'offboard=revenue:yellow_00|green_20|brown_30|gray_40;path=a:0,b:_0', + ['G1'] => 'offboard=revenue:yellow_20|green_20|brown_20|gray_20;path=a:3,b:_0', + ['I19'] => 'offboard=revenue:yellow_20|green_30|brown_30|gray_40;path=a:2,b:_0', + ['A25'] => 'offboard=revenue:yellow_20|green_30|brown_40|gray_40;path=a:5,b:_0', + ['C27'] => 'offboard=revenue:yellow_20|green_30|brown_40|gray_50;path=a:1,b:_0', + ['E1'] => 'offboard=revenue:yellow_20|green_30|brown_40|gray_50;path=a:4,b:_0;path=a:5,b:_0', + ['I15'] => 'offboard=revenue:yellow_20|green_30|brown_40|gray_50;path=a:3,b:_0', + ['A19'] => 'offboard=revenue:yellow_20|green_30|brown_50|gray_50;path=a:5,b:_0', + ['D12'] => 'offboard=revenue:yellow_20|green_30|brown_40|gray_50;path=a:5,b:_0;path=a:0,b:4;icon=image:1854/minus_ten,loc:0.5', + ['I29'] => 'offboard=revenue:yellow_10|green_20|brown_30;path=a:5,b:_0', + ['M39'] => 'offboard=revenue:yellow_10|green_20|brown_30;path=a:2,b:_0', + ['E27'] => 'offboard=revenue:yellow_30|green_40|brown_50|gray_60,groups:Budapest;path=a:2,b:_0;border=edge:3', + ['H10'] => 'offboard=revenue:yellow_30|green_40|brown_50|gray_60;path=a:2,b:_0;path=a:3,b:_0', + ['D28'] => 'offboard=revenue:yellow_30|green_40|brown_50|gray_60,groups:Budapest,hide:1;path=a:1,b:_0;border=edge:0', }, gray: { - %w[A21 I31 I37] => 'path=a:0,b:5', - %w[M31 M37] => 'town=revenue:10;path=a:2,b:_0;path=a:_0,b:3', - ['C13'] => 'town=revenue:10;path=a:4,b:_0;path=a:_0,b:5', - ['H12'] => 'town=revenue:10;path=a:2,b:_0;path=a:_0,b:4', + %w[A21 I31 I37] => 'path=a:0,b:5', + %w[M31 M37] => 'town=revenue:10;path=a:2,b:_0;path=a:_0,b:3', + %w[M35] => 'city=revenue:yellow_10|green_20|brown_30,loc:2.5;path=a:2,b:_0;path=a:_0,b:3', + %w[L28] => 'city=revenue:20,loc:3.5;path=a:3,b:_0;path=a:_0,b:4', + %w[J28] => 'city=revenue:yellow_10|green_20|brown_30,loc:4.5;path=a:4,b:_0;path=a:_0,b:5', + ['C13'] => 'town=revenue:10;path=a:4,b:_0;path=a:_0,b:5', + ['H12'] => 'town=revenue:10;path=a:2,b:_0;path=a:_0,b:4', + ['D0'] => 'icon=image:1854/eiffel', + ['F28'] => 'icon=image:1854/eiffel', }, white: { - %w[B24 B26 E25 F24 G23 G21 G19 G9 H20 K35 L34 J32] => 'blank', - %w[F20] => 'upgrade=cost:90,terrain:mountain', - %w[F10 C25] => 'upgrade=cost:50,terrain:water', - %w[B16 K31 K29] => 'upgrade=cost:60,terrain:mountain', - %w[D14 E7 E9 E23 L38] => 'upgrade=cost:70,terrain:mountain', - %w[G15 H14] => 'upgrade=cost:80,terrain:mountain', - %w[B20] => 'upgrade=cost:50,terrain:mountain', - %w[E19 E15] => 'upgrade=cost:90,terrain:mountain', - %w[E17 E5 F16] => 'upgrade=cost:100,terrain:mountain', - %w[B18 K33] => 'town=revenue:0;upgrade=cost:50,terrain:mountain', - %w[L30 L36 K39] => 'town=revenue:0;upgrade=cost:60,terrain:mountain', - %w[G3] => 'town=revenue:0;upgrade=cost:80,terrain:mountain', - %w[L32] => 'town=revenue:0;town=revenue:0;upgrade=cost:80,terrain:mountain', - %w[F2 J30 D24] => 'town=revenue:0;town=revenue:0', - %w[B22 J36 K37 D22] => 'town=revenue:0;town=revenue:0;upgrade=cost:50,terrain:mountain', - %w[H16] => 'town=revenue:0;town=revenue:0;upgrade=cost:70,terrain:mountain', - %w[E21] => 'town=revenue:0;town=revenue:0;upgrade=cost:120,terrain:mountain', - %w[F4] => 'upgrade=cost:120,terrain:mountain', - %w[C21 E11] => 'town=revenue:0;town=revenue:0;upgrade=cost:50,terrain:water', - %w[C19 F6] => 'town=revenue:0;upgrade=cost:50,terrain:water', - %w[C15 D16 D26] => 'town=revenue:0', - %w[C17 E3 E13 F22 F8 H18 J34] => 'city=revenue:0', + %w[B24 B26 E25 G23 G21 G19 G9 H20 K35 L34 C25] => 'blank', + %w[B20] => 'upgrade=cost:50,terrain:mountain', + %w[B16 K31 K29] => 'upgrade=cost:60,terrain:mountain', + %w[D14 E7 E9 E23 L38] => 'upgrade=cost:70,terrain:mountain', + %w[G15 H14] => 'upgrade=cost:80,terrain:mountain', + %w[E19 E15 F20] => 'upgrade=cost:90,terrain:mountain', + %w[E17 E5 F16] => 'upgrade=cost:100,terrain:mountain', + %w[F4] => 'upgrade=cost:120,terrain:mountain', + %w[F10] => 'upgrade=cost:50,terrain:water', + %w[C15 D16 D26 F24 D24] => 'town=revenue:0', + %w[C19 F6] => 'town=revenue:0;upgrade=cost:50,terrain:water', + %w[B18 K33] => 'town=revenue:0;upgrade=cost:50,terrain:mountain', + %w[L30 L36 K39] => 'town=revenue:0;upgrade=cost:60,terrain:mountain', + %w[G3] => 'town=revenue:0;upgrade=cost:80,terrain:mountain', + %w[F2 J30 ] => 'town=revenue:0;town=revenue:0', + %w[D18 D20] => 'town=revenue:0;town=revenue:0;frame=color:#BBB', + %w[B22 J36 K37 D22] => 'town=revenue:0;town=revenue:0;upgrade=cost:50,terrain:mountain', + %w[C21 E11] => 'town=revenue:0;town=revenue:0;upgrade=cost:50,terrain:water', + %w[H16] => 'town=revenue:0;town=revenue:0;upgrade=cost:70,terrain:mountain', + %w[L32] => 'town=revenue:0;town=revenue:0;upgrade=cost:80,terrain:mountain', + %w[E21] => 'town=revenue:0;town=revenue:0;upgrade=cost:120,terrain:mountain', + %w[C17 E3 E13 F22 F8 H18 J34 J32] => 'city=revenue:0', }, yellow: { - ['C23'] => 'city=revenue:40,loc:0;city=revenue:40,loc:1;city=revenue:40,loc:2;path=a:0,b:_0;path=a:1,b:_1;path=a:2,b:_2', + ['C23'] => 'city=revenue:40,loc:0;city=revenue:40,loc:1;city=revenue:40,loc:2;path=a:0,b:_0;path=a:1,b:_1;path=a:2,b:_2;label=W', ['J38'] => 'city=revenue:20,loc:1;city=revenue:20,loc:5;path=a:1,b:_0;path=a:5,b:_1', }, brown: { - %w[F12 F14 F18 G17 G13 G11 G7 G5] => 'icon=image:1854/mine', + %w[F12 F18 G17 G13 G11 G7 G5] => 'icon=image:1854/mine', + %w[F14] => 'town=revenue:10;path=a:4,b:_0;icon=image:1854/mine', }, purple: { }, diff --git a/lib/engine/game/g_1854/phases.rb b/lib/engine/game/g_1854/phases.rb new file mode 100644 index 0000000000..5f7e84a8ab --- /dev/null +++ b/lib/engine/game/g_1854/phases.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module Engine + module Game + module G1854 + module Phases + PHASES = [ + { + name: '2', + train_limit: {minor: 2, major: 4}, + tiles: [:yellow], + operating_rounds: 1, + }, + { + name: '3', + on: '3', + train_limit: {minor: 2, major: 4}, + tiles: %i[yellow green], + operating_rounds: 2, + }, + { + name: '4', + on: '4', + train_limit: {minor: 1, major: 3}, + tiles: %i[yellow green], + operating_rounds: 2, + }, + { + name: '5', + on: '5', + train_limit: 2, + tiles: %i[yellow green brown], + operating_rounds: 3, + }, + { + name: '6', + on: '6', + train_limit: 2, + tiles: %i[yellow green brown], + operating_rounds: 3, + }, + { + name: '7', + on: '8', + train_limit: 2, + tiles: %i[yellow green brown gray], + operating_rounds: 3, + }, + ].freeze + end + end + end +end diff --git a/lib/engine/game/g_1854/player.rb b/lib/engine/game/g_1854/player.rb deleted file mode 100644 index e7ee9dc902..0000000000 --- a/lib/engine/game/g_1854/player.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -require_relative '../../player' - -module Engine - module Game - module G1854 - class Player < Engine::Player - attr_accessor :debt - - def initialize(id, name) - super - @debt = 0 - end - - def value - super - @debt - end - end - end - end -end diff --git a/lib/engine/game/g_1854/tiles.rb b/lib/engine/game/g_1854/tiles.rb new file mode 100644 index 0000000000..9c26910395 --- /dev/null +++ b/lib/engine/game/g_1854/tiles.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +module Engine + module Game + module G1854 + module Tiles + TILES = { + '1' => 2, + '2' => 2, + '3' => 3, + '4' => 6, + '5' => 5, + '6' => 6, + '7' => 5, + '8' => 11, + '9' => 11, + '14' => 4, + '15' => 7, + '16' => 2, + '19' => 2, + '20' => 2, + '23' => 6, + '24' => 6, + '25' => 2, + '26' => 2, + '27' => 2, + '28' => 2, + '29' => 2, + '39' => 1, + '40' => 1, + '41' => 1, + '42' => 1, + '43' => 1, + '44' => 1, + '45' => 1, + '46' => 1, + '47' => 1, + '55' => 2, + '56' => 2, + '57' => 6, + '58' => 6, + '69' => 1, + '70' => 1, + '611' => 6, + '619' => 4, + '433' => { + 'count' => 1, + 'color' => 'green', + 'code' => 'city=revenue:50,loc:0;city=revenue:50,loc:1;city=revenue:50,loc:2;city=revenue:50,loc:3;city=revenue:50,loc:4;'\ + 'path=a:0,b:_0;path=a:1,b:_1;path=a:2,b:_2;path=a:3,b:_3;path=a:4,b:_4;label=W', + }, + '451' => { + 'count' => 1, + 'color' => 'brown', + 'code' => 'city=revenue:60;city=revenue:60,loc:1.5,slots:2;city=revenue:60,loc:4.5,slots:2;'\ + 'path=a:0,b:_0;path=a:_0,b:3;path=a:1,b:_1;path=a:_1,b:2;path=a:4,b:_2;path=a:_2,b:5;label=W', + }, + '456' => { + 'count' => 1, + 'color' => 'gray', + 'code' => 'city=revenue:70,slots:5;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=W', + }, + '915' => { + 'count' => 2, + 'color' => 'gray', + 'code' => 'city=revenue:50,slots:3;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', + }, + }.freeze + end + end + end +end diff --git a/lib/engine/game/g_1854/trains.rb b/lib/engine/game/g_1854/trains.rb new file mode 100644 index 0000000000..78fbf481da --- /dev/null +++ b/lib/engine/game/g_1854/trains.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +module Engine + module Game + module G1854 + module Trains + TRAINS = [ + { + name: '2', + distance: 2, + num: 6, + price: 100, + rusts_on: '4', + }, + { + name: '1+', + distance: [{ 'nodes' => %w[city offboard], 'pay' => 1, 'visit' => 1 }, + { 'nodes' => ['town'], 'pay' => 99, 'visit' => 99 }], + num: 6, + price: 100, + rusts_on: '4', + available_on: '2', + }, + { + name: '3', + distance: 3, + num: 5, + price: 200, + rusts_on: '6', + }, + { + name: '2+', + num: 4, + distance: [{ 'nodes' => %w[city offboard], 'pay' => 2, 'visit' => 2 }, + { 'nodes' => ['town'], 'pay' => 99, 'visit' => 99 }], + price: 120, + rusts_on: '6', + available_on: '3', + }, + { + name: '3+', + num: 3, + distance: [{ 'nodes' => %w[city offboard], 'pay' => 3, 'visit' => 3 }, + { 'nodes' => ['town'], 'pay' => 99, 'visit' => 99 }], + price: 160, + rusts_on: '6', + available_on: '3', + }, + { + name: '4', + distance: 4, + num: 4, + price: 320, + rusts_on: '4', + }, + { + name: '5', + distance: 5, + num: 3, + price: 530, + }, + { + name: '6', + distance: 6, + num: 2, + price: 670, + }, + { + name: '8', + distance: 8, + num: 6, + price: 900, + }, + { + name: '8Ox', + distance: 8, + num: 5, + price: 1200, + }, + ].freeze + end + end + end +end diff --git a/public/icons/1854/1.svg b/public/icons/1854/1.svg new file mode 100644 index 0000000000..4367117e8b --- /dev/null +++ b/public/icons/1854/1.svg @@ -0,0 +1 @@ +1 \ No newline at end of file diff --git a/public/icons/1854/2.svg b/public/icons/1854/2.svg new file mode 100644 index 0000000000..927c79c31f --- /dev/null +++ b/public/icons/1854/2.svg @@ -0,0 +1 @@ +2 \ No newline at end of file diff --git a/public/icons/1854/3.svg b/public/icons/1854/3.svg new file mode 100644 index 0000000000..062cf92459 --- /dev/null +++ b/public/icons/1854/3.svg @@ -0,0 +1 @@ +3 \ No newline at end of file diff --git a/public/icons/1854/4.svg b/public/icons/1854/4.svg new file mode 100644 index 0000000000..89d4e21592 --- /dev/null +++ b/public/icons/1854/4.svg @@ -0,0 +1 @@ +4 \ No newline at end of file diff --git a/public/icons/1854/5.svg b/public/icons/1854/5.svg new file mode 100644 index 0000000000..32f950239c --- /dev/null +++ b/public/icons/1854/5.svg @@ -0,0 +1 @@ +5 \ No newline at end of file diff --git a/public/icons/1854/6.svg b/public/icons/1854/6.svg new file mode 100644 index 0000000000..8d1e0469cf --- /dev/null +++ b/public/icons/1854/6.svg @@ -0,0 +1 @@ +6 \ No newline at end of file diff --git a/public/icons/1854/eiffel.svg b/public/icons/1854/eiffel.svg new file mode 100644 index 0000000000..4ebd4b53a1 --- /dev/null +++ b/public/icons/1854/eiffel.svg @@ -0,0 +1,2028 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +