Game Engine Reference

The 18xx engine is a collection of plain Ruby classes under lib/engine/. The same code runs server-side in MRI Ruby and client-side as JavaScript compiled by Opal. This page lists the major classes and their responsibilities.

Core Classes

Engine::Game::Base [lib/engine/game/base.rb:91]

The root game object. Holds every other engine object and drives the game flow. Key responsibilities:

Method / constantPurpose
initialize (:566)Builds all objects; replays saved Actions on startup
initialize_actions (:819)Loads persisted Actions and applies them in order
process_action (:838)Entry point for a new player move; delegates to the active Round
next_round! (:2921)Transitions to the next Round after the current one finishes
game_end_check (:2970)Tests all end conditions after every Action
new_stock_round (:3177)Factory method for a new Stock Round
new_operating_round (:3192)Factory method for a new Operating Round

Every game title overrides constants in this class: TRAINS, CORPORATIONS, PHASES, HEXES, COMPANIES, PLAYER_RANGE, and many others.

Engine::Round::Base [lib/engine/round/base.rb:12]

Container for a turn segment. Holds an ordered list of Steps and the Entity queue.

MethodPurpose
initialize (:22)Builds Steps; merges DEFAULT_STEPS with title-specific steps
process_action (:75)Finds the first active, blocking Step that handles the action and calls it
active_step (:136)Returns the first blocking, non-finished Step

Concrete subclasses: Round::Stock, Round::Operating, Round::Auction.

Engine::Step::Base [lib/engine/step/base.rb:8]

Atomic decision unit. Each Step declares which action types it handles and processes them.

MethodPurpose
actions(_entity) (:31)Returns the list of action-type strings this Step allows
blocking? (:84)Returns true if this Step prevents lower-priority Steps from acting
process_*Handler called by the Round — e.g. process_buy_shares, process_lay_tile

Engine::Phase [lib/engine/phase.rb:6]

Tracks the current game phase. Advances when a train of the triggering type is purchased [lib/engine/phase.rb:19]. Controls which track types are legal, the number of Operating Rounds per set, and which trains rust.

Engine::Bank [lib/engine/bank.rb:8]

Central cash reserve. broken? (:38) returns true when cash drops to zero or below; this may trigger the bank end condition depending on the title.

Engine::Depot [lib/engine/depot.rb:7]

Supply of available trains. available (:114) returns trains purchasable by a given Corporation. Handles train export and reclaiming.

Engine::StockMarket [lib/engine/stock_market.rb:7]

Two-dimensional price grid. Movement methods (move_right, move_up, move_down, move_left, diagonal variants) adjust a Corporation's price marker. Special cells (top row, right ledge) can trigger game-end or Corporation closure.

Engine::Graph [lib/engine/graph.rb:6]

Computes the routing graph from the current tile layout. connected_hexes (:98) and connected_nodes (:103) return the set of Hexes/nodes reachable from a Corporation's tokens. Used by route-validation Steps.

Engine::Action::Base [lib/engine/action/base.rb:1]

Base class for all persisted moves. Every concrete action (e.g. Action::BuyShares, Action::LayTile) stores an entity reference, an action type string, and type-specific parameters as JSON.

Key Supporting Classes

ClassFileRole
Playerlib/engine/player.rb:1Cash, certificates, name
Corporationlib/engine/corporation.rb:1Shares, tokens, trains, cash, president
Companylib/engine/company.rb:1Private company; grants special abilities
Minorlib/engine/minor.rb:1Subsidiary; owns shares, runs routes
SharePricelib/engine/share_price.rb:1A cell on the StockMarket; holds par and current price
Hexlib/engine/hex.rb:1One hexagon on the map; holds a Tile
Tilelib/engine/tile.rb:1Track layout, cities, towns
Tokenlib/engine/token.rb:1Corporation presence on a city
Trainlib/engine/train.rb:1Reach (distance), phase trigger

Title Registration

lib/engine.rb:17-24 builds GAME_META_BY_TITLE, a hash that maps title strings (e.g. "1830") to Meta modules. The API and frontend use this map to instantiate the correct game class.


Implementation Layer Taxonomy

When adding mechanics to a game title, use this four-layer model to identify where the code lives and how much effort is required.

Layer 1 — Pure Configuration (constants only)

No custom Ruby methods — only values in TRAINS, PHASES, COMPANIES, CORPORATIONS, or scalar game constants. g_1830 is the gold standard; 95% of its mechanics live in constants.

TRAINS entries capture:

FieldPurpose
name:, distance:, price:, count:Train identity and roster
rusts_on:, obsolete_on:Rust/obsolete triggers
available_on:Gate — train only appears after another is bought
discount:Hash of discounts, e.g. {'2' => 30}
variants:Sub-types selectable at buy time; each can have own rusts_on: and multiplier:
events:Phase-transition events dispatched to event_xxx!
distance: (array)Multi-node specs: [{'nodes'=>[...], 'pay'=>N, 'visit'=>N}]

PHASES entries capture: train limit (scalar or {minor: N, major: N} hash), available tile colours, operating rounds per phase, status flag strings, and phase-trigger events:.

Key scalar game constants:

ConstantControls
CAPITALIZATION:full, :incremental, :none
SELL_BUY_ORDER, SELL_AFTER, SELL_MOVEMENT, POOL_SHARE_DROPShare sale mechanics
MUST_BUY_TRAIN, EBUY_FROM_OTHERS, EBUY_PRES_SWAPEmergency buy rules
HOME_TOKEN_TIMINGWhen home tokens are placed
TILE_RESERVATION_BLOCKS_OTHERS, TRACK_RESTRICTIONTile lay restrictions
BANK_CASH, STARTING_CASH, CERT_LIMITFinancial limits
GAME_END_CHECKHash of reason → timing pairs
TILE_LAYSDefault lay-slot array for each entity type
STATUS_TEXT, EVENTS_TEXT, MARKET_TEXTUI text for phases and events

Layer 2 — Hook Overrides (named Game::Base method overrides)

Require Ruby code but follow a clear template: override a named method, replace its return value or behaviour.

Hook methodWhat it controls
tile_lays(entity)Lay budget per entity type and/or phase
revenue_for(route, stops)Route revenue bonus additions
float_corporation(corp)When/how a corporation receives IPO cash
must_buy_train?(entity)Whether a corporation is forced to buy a train
upgrades_to?(from, to, ...)Custom tile upgrade validation
check_distance(route, visits)Route validity rules
check_other(route)Extra route validity rules
operating_orderOrder entities operate in an OR
next_round!Inter-round sequencing (see Guideline 12)
event_xxx!Named phase/train event handler
num_trains(train)Override train count
buying_power(entity)Available spending budget
can_par?(corp, parrer)Whether a corporation can be parred
setupOne-time game initialisation
action_processed(action)Post-action side effects
sold_out_increase?Stock market UP movement on sold-out

Layer 3 — Custom Step and Round Classes

Require new Ruby files. Used when a mechanic changes which actions are available or the order in which entities take them.

Custom Step categories:

Category18xx use case
Waterfall auctionInitial private/minor allocation
Minor acquisitionOR step for a major to absorb a minor
Merger/conversionCorporation merges and conversion
Emergency issue/buyEmergency train purchase flow
Nationalisation (auto-step)National corporation auto-operates

Custom Round categories:

CategoryReplaces
Waterfall auction roundInitial auction round
Consolidation roundNew round triggered at a phase boundary
Custom Stock roundExtended SR with special movement tracking

Layer 4 — Structural Divergence

Mechanics that fundamentally rewire the engine. Rare; avoid unless no other approach is possible.


Event Handler Library

Event method names are dispatched from TRAINS or PHASES events: arrays via @game.send("event_#{type}!"). They must exist in game.rb or base.rb.

Event nameWhat it does
close_companiesCloses all companies with no owner or qualifying criteria
remove_reservationsRemoves all unsold home reservations
remove_bonusesRemoves east-west / bonus tokens
float_30 / float_40 / float_60Changes float threshold during the game
green_minors_availableAllows minors to buy green trains
majors_can_ipoOpens major corporations for IPO
minors_cannot_startBlocks minors from starting
minors_nationalizedAll remaining minors nationalise
trainless_nationalizationTrainless entities nationalise
nationalize_companiesPrivates are absorbed into a national
phase_revenueChanges private revenues by phase
full_capitalisationFull capitalisation event
trigger_endgameSignals game-end countdown

tile_lays Mechanics

tile_lays(entity) [lib/engine/step/tracker.rb] returns an ordered array of lay-slot hashes. Each slot is one available lay action, consumed in sequence as @round.num_laid_track is incremented.

# Lay-slot hash keys:
{
  lay: true,                    # true / false / :not_if_upgraded
  upgrade: true,                # true / false / :not_if_upgraded
  cost: 0,                      # extra cost for yellow lays
  upgrade_cost: 0,              # extra cost for upgrades (defaults to :cost)
  cannot_reuse_same_hex: false  # prevents laying on the same hex twice
}

The TILE_LAYS constant defines the default slots for the base entity type. Override tile_lays(entity) in game.rb to return a different array based on entity type or current phase.


Share Ownership and Float

Float condition: float_percent determines when float_corporation! is called. For most entities: when the combined sold shares reach the threshold, the treasury receives the capitalisation.

Stock market movement triggers:

TriggerDirection
Dividend ≥ share priceRIGHT (share_direction: :right)
Zero dividendLEFT (share_direction: :left)
Each share soldDOWN — handled by sell_shares_and_change_price
Sold out at SR endUP — triggered by sold_out_increase? + finish_roundsold_out_stock_movement

Override sold_out_increase? in game.rb to restrict the UP movement to specific entity types (e.g., majors and nationals only).


National Revenue — Virtual Tokens

Nationals in some titles have virtual tokens in every city/town in their home zone. The engine mechanism:

# Create a national-aware graph (no physical tokens needed):
Graph.new(game, home_as_token: true, no_blocking: true)

This treats each home zone hex as if the national has a token there. Revenue calculation then:

  1. Identifies all linked cities/towns in the zone via graph connectivity.
  2. Counts linked stops at face value.
  3. Fills remaining train capacity at flat rates for unlinked stops (game-specific).

Requires a NATIONAL_REGION_HEXES constant enumerating exactly which hexes belong to each national's zone.


What's next


Version: 2026-05-08 — derived from lib/engine/game/base.rb, lib/engine/round/base.rb, lib/engine/step/base.rb, lib/engine/step/tracker.rb, lib/engine/phase.rb, lib/engine/bank.rb, lib/engine/depot.rb, lib/engine/stock_market.rb, lib/engine/graph.rb.