Skip to content

Commit

Permalink
Merge pull request #11297 from ollybh/1858switzerland-robot
Browse files Browse the repository at this point in the history
[1858Switzerland] Robot variant for two-players
  • Loading branch information
ollybh authored Nov 15, 2024
2 parents 550aff3 + 1f2ae5b commit 86d8794
Show file tree
Hide file tree
Showing 12 changed files with 502 additions and 4 deletions.
12 changes: 12 additions & 0 deletions lib/engine/game/g_1858_switzerland/entities.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,18 @@ module Entities
capitalization: :incremental,
tokens: [0, 20, 20],
},
{
sym: 'SBB',
name: 'Schweizerische Bundesbahnen',
logo: '1858_switzerland/SBB',
color: 'red',
text_color: 'black',
floatable: false,
type: 'national',
hide_shares: true,
capitalization: :none,
tokens: [0, 0, 0, 0, 0],
},
].freeze

COMPANIES = [
Expand Down
208 changes: 204 additions & 4 deletions lib/engine/game/g_1858_switzerland/game.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ class Game < G1858::Game
include Map
include Tiles

attr_reader :robot

GRAPH_CLASS = G1858Switzerland::Graph
CURRENCY_FORMAT_STR = '%ssfr'

Expand All @@ -39,6 +41,10 @@ class Game < G1858::Game
],
).freeze
EVENTS_TEXT = G1858::Trains::EVENTS_TEXT.merge(
'sbb_starts' => [
'SBB starts',
'The SBB starts operating',
],
'blue_privates_available' => [
'Blue privates can start',
'The third set of private companies becomes available',
Expand All @@ -55,6 +61,9 @@ class Game < G1858::Game
],
).freeze

ROBOT_MINOR_TILE_LAYS = [{ lay: true, upgrade: false }].freeze
ROBOT_MAJOR_TILE_LAYS = [{ lay: true, upgrade: true }].freeze

def game_phases
phases = super
_phase2, _phase3, phase4, phase5, phase6 = phases
Expand All @@ -71,6 +80,14 @@ def timeline
'6H/3M trains rust when the fourth 6E/5M/5D train is bought.']
end

def event_sbb_starts!
@log << '-- The SBB starts operating --'
sbb.owner = @robot
sbb.floatable = true
sbb.floated = true
@round.entities << sbb
end

def event_blue_privates_available!
@log << '-- Event: Blue private companies can be started --'
# Don't need to change anything, the check in buyable_bank_owned_companies
Expand Down Expand Up @@ -102,7 +119,8 @@ def event_privates_close2!

def game_trains
trains = super
_train_2h, _train_4h, train_6h, _train_5e, train_6e, train_5d = trains
train_2h, _train_4h, train_6h, _train_5e, train_6e, train_5d = trains
train_2h[:events] = [{ 'type' => 'sbb_starts' }] if robot?
train_6h.delete(:obsolete_on) # Wounded on second grey train, handled in code
train_6h[:events] = [{ 'type' => 'blue_privates_available' }]
train_6e[:events] = [{ 'type' => 'privates_close2' }]
Expand All @@ -125,6 +143,32 @@ def setup
@phase4_train_trigger = PHASE4_TRAINS_RUST
end

def init_starting_cash(players, bank)
return super unless robot?

# This method is called before the robot player is added to `players`.
# The robot does not receive any cash, but the amount received by the
# human players is reduced to the starting cash for a three-player
# game.
cash = self.class::STARTING_CASH[players.size + 1]
players.each do |player|
bank.spend(cash, player)
end
end

def game_cert_limit
return super unless robot?

# This method is called before the robot player is added to `players`.
# The certificate limit is reduced to account for the extra player.
self.class::CERT_LIMIT.transform_keys { |player_count| player_count - 1 }
end

def game_corporations
excluded = robot? ? 'RhB' : 'SBB'
super.reject { |corp| corp[:sym] == excluded }
end

def maybe_rust_wounded_trains!(grey_trains_bought, purchased_train)
obsolete_trains!(%w[6H 3M], purchased_train) if grey_trains_bought == PHASE4_TRAINS_OBSOLETE
rust_wounded_trains!(%w[4H 2M], purchased_train) if grey_trains_bought == PHASE3_TRAINS_RUST
Expand All @@ -137,6 +181,28 @@ def obsolete_trains!(train_names, purchased_train)
rust_trains!(purchased_train, purchased_train.owner)
end

def stock_round
Engine::Round::Stock.new(self, [
G1858::Step::Exchange,
G1858::Step::ExchangeApproval,
G1858::Step::HomeToken,
G1858Switzerland::Step::BuySellParShares,
])
end

def operating_round(round_num = 1)
@round_num = round_num
Engine::Round::Operating.new(self, [
G1858Switzerland::Step::Track,
G1858Switzerland::Step::Token,
G1858Switzerland::Step::Route,
G1858Switzerland::Step::Dividend,
G1858Switzerland::Step::DiscardTrain,
G1858Switzerland::Step::BuyTrain,
G1858Switzerland::Step::IssueShares,
], round_num: round_num)
end

def closure_round(round_num)
G1858Switzerland::Round::Closure.new(self, [
G1858::Step::ExchangeApproval,
Expand All @@ -145,6 +211,29 @@ def closure_round(round_num)
], round_num: round_num)
end

def reorder_players(order = nil, log_player_order: false, silent: false)
super
return unless robot?

# The robot player is always last in priority order.
@players.delete(@robot)
@players << @robot
end

def operating_order
return super unless robot?

super.sort_by { |entity| entity == sbb ? 1 : 0 }
end

def companies_to_payout(ignore: [])
@companies.select do |company|
company.owner &&
company.owner != @robot &&
!ignore.include?(company.id)
end
end

BONUS_HEXES = {
north: %w[C4 D3 E2 H1 I2],
south: %w[H15 I16],
Expand Down Expand Up @@ -214,13 +303,23 @@ def upgrades_to?(from, to, special, selected_company: nil)
end

def after_lay_tile(_hex, tile, entity)
return unless tile.name == MOUNTAIN_RAILWAY_TILE
if tile.name == MOUNTAIN_RAILWAY_TILE
entity.assign!(MOUNTAIN_RAILWAY_ASSIGNMENT)
elsif robot_owner?(entity) && home_route_complete?(entity)
private_nationalised(entity)
end
end

def setup_preround
super
return unless robot?

entity.assign!(MOUNTAIN_RAILWAY_ASSIGNMENT)
@robot = Player.new(-1, 'Robot')
@players << @robot
end

# This method is called to remove some private railways from 1858 when
# there are two players. This does not happen in 18CH.
# there are two players. This does not happen in 1858 Switzerland.
def setup_unbuyable_privates; end

def gotthard
Expand Down Expand Up @@ -248,8 +347,92 @@ def home_hex?(operator, hex, gauge = nil)
(operator == gb_minor && gauge == :broad)
end

def corporation_private_connected?(corporation, minor)
# Private railway companies cannot be exchanged for shares of SBB.
return false if corporation.type == :national

super
end

def robot_owner?(entity)
return false unless robot?
return false if !entity.corporation? && !entity.minor?

entity.owner == @robot
end

def acting_for_player(player)
return player unless player == @robot

acting_for_robot(current_entity)
end

# Finds the player who should take track actions for robot-owned
# private railways and public companies.
def acting_for_robot(operator)
player_index =
if operator.corporation?
# SBB is operated by the priority holder in the first round of an
# OR set, and the other player in the second.
@round.round_num - 1
else
# The players take turns operate the robot's private railway
# companies, starting with the priority deal holder.
minor_index = @round.entities
.select { |e| e.minor? && e.owner == @robot }
.index(operator)
minor_index % human_players.size
end
human_players[player_index]
end

def tile_lays(entity)
return super unless robot_owner?(entity)

entity.corporation? ? ROBOT_MAJOR_TILE_LAYS : ROBOT_MINOR_TILE_LAYS
end

def can_par?(corporation, parrer)
return false if corporation == sbb

super
end

def close_company(company)
# Bit of a hack to avoid rewriting the method in G1858::Game.
# This avoids the SBB being paid for any of their companies.
company.owner = @bank if company.owner == sbb

super
end

def buy_train(entity, train, price)
super(entity, train, price.zero? ? :free : price)
end

# Checks whether tiles have been laid in all the hexes of a private
# railway company.
def home_route_complete?(entity)
return false unless entity.minor?

entity.coordinates.none? { |coord| hex_by_id(coord).tile.color == :white }
end

private

def sbb
@sbb ||= corporation_by_id('SBB')
end

# Is this game using the rules for the two-player robot variant?
def robot?
@optional_rules.include?(:robot)
end

def human_players
@players.reject { |player| player == @robot }
end

def hexes_by_id(coordinates)
coordinates.map { |coord| hex_by_id(coord) }
end
Expand Down Expand Up @@ -288,6 +471,23 @@ def mountain_railway_built?(entity)

entity.assignments.key?(MOUNTAIN_RAILWAY_ASSIGNMENT)
end

# Called when a private railway company owned by the robot has finished
# laying track in all its home hexes. This closes the private railway
# and, if possible, places a SBB token in one of its home cities.
def private_nationalised(minor)
@log << "#{minor.id} has built all its reserved hexes and is " \
"acquired by #{sbb.id}."
company = private_company(minor)
@robot.companies.delete(company)
company.owner = sbb
city = @cities.find { |c| c.reserved_by?(company) }
if city&.tokenable?(sbb, free: true)
@log << "#{sbb.id} places a token in #{city.tile.hex.location_name}."
city.place_token(sbb, sbb.next_token, free: true)
end
close_private(minor)
end
end
end
end
Expand Down
9 changes: 9 additions & 0 deletions lib/engine/game/g_1858_switzerland/meta.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ module Meta
GAME_ISSUE_LABEL = '1858Switzerland'

PLAYER_RANGE = [2, 4].freeze

OPTIONAL_RULES = [
{
sym: :robot,
short_name: 'Robot',
desc: 'Adds a robot player and the SBB national company.',
players: [2],
},
].freeze
end
end
end
Expand Down
55 changes: 55 additions & 0 deletions lib/engine/game/g_1858_switzerland/step/buy_sell_par_shares.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# frozen_string_literal: true

require_relative '../../g_1858/step/buy_sell_par_shares'

module Engine
module Game
module G1858Switzerland
module Step
class BuySellParShares < G1858::Step::BuySellParShares
ROBOT_ACTIONS = %w[buy_company].freeze

def actions(entity)
return super unless entity == @game.robot
return [] if @acted
return [] unless priciest_company

ROBOT_ACTIONS
end

def auto_actions(entity)
return super unless entity == @game.robot
return [] unless (company = priciest_company)

[Engine::Action::BuyCompany.new(entity, company: company, price: 0)]
end

def process_buy_company(action)
player = action.entity
company = action.company
owner = company.owner

raise GameError, "Cannot buy #{company.name} from #{owner.name}" unless owner == @game.bank
raise GameError, "Only #{@game.robot.name} can buy a private railway company." unless player == @game.robot

@log << "#{player.name} acquires #{company.name} from #{owner.name}."
@game.purchase_company(player, company, action.price)
track_action(action, company)
end

private

# Finds the most expensive private railway company currently
# available. If there are two at the same price then the first in
# company order is returned. Nil is returned if there are no
# companies available.
def priciest_company
companies = @game.buyable_bank_owned_companies
max_value = companies.map(&:value).max
companies.find { |c| c.value == max_value }
end
end
end
end
end
end
Loading

0 comments on commit 86d8794

Please sign in to comment.