Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Ticket to Ride

title

Value of route length

import pandas as pd
import seaborn as sns
pd.set_option('display.float_format', '{:.2f}'.format)

CARS_PER_TURN = 2 # number of cards a player can pick up per turn
STARTING_CARS = 3 # Number of cards each player starts with
TOTAL_TRAIN_CARS = 45 # Number of train cars each player starts with
CARS_REMAINING_TO_END_GAME = 2 # When a player has equal to or less than this number of cars, each player has one more turn remaining before the game ends

ROUTE_LENGTHS = pd.DataFrame({
    "Cars": [1,2,3,4,5,6],
    "Points": [1,2,4,7,10,15]
},
index=[1,2,3,4,5,6]);
ROUTE_LENGTHS["Total Turns to Claim"] = 1+ROUTE_LENGTHS.Cars/CARS_PER_TURN
ROUTE_LENGTHS["Points per Turn"] = ROUTE_LENGTHS.Points/(1+ROUTE_LENGTHS.Cars/CARS_PER_TURN)
ROUTE_LENGTHS["Cars per Turn"] = ROUTE_LENGTHS.Cars/(1+ROUTE_LENGTHS.Cars/CARS_PER_TURN)

def set_style(styler):
    styler.background_gradient(cmap=sns.light_palette("seagreen", as_cmap=True), subset=["Points per Turn", "Cars per Turn"])
    styler.hide(axis=0) # Hides index column
    styler.format(precision=2)
    return styler

ROUTE_LENGTHS.style.pipe(set_style)
Loading...
ROUTE_LENGTHS.plot(x="Cars", y=["Points per Turn", "Cars per Turn"])
<Axes: xlabel='Cars'>
<Figure size 640x480 with 1 Axes>

Routes Matrix

from collections import defaultdict
ROUTES_MATRIX = pd.read_csv("america_routes.csv").fillna(value=0)
def set_style(styler):
    styler.background_gradient(axis=None,cmap=sns.dark_palette("seagreen", as_cmap=True))
    styler.hide(axis=0) # Hides index column
    styler.format(precision=0)
    styler.set_table_styles(
    [dict(selector="th",props=[('max-width', '80px')]),
        dict(selector="th.col_heading",
                 props=[("writing-mode", "vertical-rl"), 
                        ('vertical-align', 'text-top'), ('transform', 'rotateZ(180deg)'),
                        ])])
    return styler
ROUTES_MATRIX.style.pipe(set_style)
Loading...

Visualizing the routes graph

import networkx as nx
ROUTES_GRAPH_BY_TURNS = nx.Graph()
ROUTES_GRAPH_BY_TURNS_PER_POINT = nx.Graph()
for i, row in ROUTES_MATRIX.iterrows():
    for j in range(1, len(row)):
        route_length = row[ROUTES_MATRIX.columns[j]]
        if route_length > 0:
            turns = ROUTE_LENGTHS.loc[ROUTE_LENGTHS["Cars"] == route_length, "Total Turns to Claim"].iloc[0] # TODO: also make a graph that weights by turns per point
            ROUTES_GRAPH_BY_TURNS.add_edge(i,j-1,weight=turns)
            
            turns_per_point = 1 / ROUTE_LENGTHS.loc[ROUTE_LENGTHS["Cars"] == route_length, "Points per Turn"].iloc[0]
            ROUTES_GRAPH_BY_TURNS_PER_POINT.add_edge(i,j-1,weight=turns_per_point)
nx.draw(
    ROUTES_GRAPH_BY_TURNS, 
    with_labels=True, 
    labels={i-1: ROUTES_MATRIX.columns[i] for i in range(1, len(ROUTES_MATRIX.columns))},
)
<Figure size 640x480 with 1 Axes>

Solving for the best paths

Using the graph with edges weighted by the total number of turns to claim each route, we can calculate the steiner tree for a given set of cities we want to connect. This steiner tree will give us the set of routes to claim such that we complete these connections as fast as possible, lowering the risk of taking a ticket.

from networkx.algorithms.approximation import steiner_tree
from typing import List

def get_city_index(city_name: str) ->  int:
    return ROUTES_MATRIX.columns.get_loc(city_name)-1

def get_city_name(city_index: int) -> str:
    return ROUTES_MATRIX.columns[city_index+1]

def get_route_length_for_route(origin: str, destination: str) -> int:
    route_length = ROUTES_MATRIX.loc[ROUTES_MATRIX["City"] == origin, destination].iloc[0]
    if route_length == 0:
        raise f"Cities {origin} {destination} not connected!"
    return route_length

def get_turns_for_route_length(route_length: int) -> int:
    return ROUTE_LENGTHS.loc[ROUTE_LENGTHS["Cars"] == route_length, "Total Turns to Claim"].iloc[0]

def get_routeset_turns(routes: List[tuple[str,str]]) -> int:
    return sum(
        [get_turns_for_route_length(get_route_length_for_route(origin, destination))
            for origin, destination in routes]
    )

def get_points_for_route_length(route_length: int) -> int:
    return ROUTE_LENGTHS.loc[ROUTE_LENGTHS["Cars"] == route_length, "Points"].iloc[0]

def get_routeset_points(routes: List[tuple[str,str]]) -> int:
    return sum(
        [get_points_for_route_length(get_route_length_for_route(origin, destination))
            for origin, destination in routes]
    )

def get_routeset_cars(routes: List[tuple[str,str]]) -> int:
    return sum(
        [get_route_length_for_route(origin, destination)
            for origin, destination in routes]
    )

def get_fastest_tree(cities: List[str]) -> tuple[List[tuple[str,str]],float]:
    """
    returns the total number of turns for the routeset and the list of routes to claim
    """
    cities_indices = [get_city_index(city) for city in cities]
    tree = steiner_tree(ROUTES_GRAPH_BY_TURNS, cities_indices, weight='weight') # NOTE: is an approximation
    return ([(get_city_name(edge[0]), get_city_name(edge[1])) for edge in tree.edges], tree.size(weight='weight'))

def get_highest_point_per_turn_tree(cities: List[str]) -> tuple[List[tuple[str,str]],float]:
    """
    returns the routeset with the highest base points per turn.  
    Note this is not necessarily the highest points per turn for a ticket - the bonus points must be taken into account within the steiner tree algorithm, which will be more effort to implement.
    """
    cities_indices = [get_city_index(city) for city in cities]
    tree = steiner_tree(ROUTES_GRAPH_BY_TURNS_PER_POINT, cities_indices, weight='weight') # NOTE: is an approximation
    return ([(get_city_name(edge[0]), get_city_name(edge[1])) for edge in tree.edges], tree.size(weight='weight'))

# TODO: Highest cars per turn tree? Seems extremely situational

Value of Tickets

CITY_ABBREVIATIONS = pd.read_csv("america_city_abbreviations.csv")

def get_city_abbreviation(city: str) -> str:
    return CITY_ABBREVIATIONS.loc[CITY_ABBREVIATIONS["City"] == city, "Abbreviation"].iloc[0]
# Build ticket data frame
TICKETS = pd.read_csv("america_tickets.csv")
TICKETS["Ticket"] = TICKETS.apply(
    lambda x: ">".join([get_city_abbreviation(x["Origin"]), get_city_abbreviation(x["Destination"])]), axis=1)
TICKETS[["Fastest Routeset", "Fastest Routeset Turns"]] = TICKETS.apply(lambda x: pd.Series(get_fastest_tree([x["Origin"], x["Destination"]])), axis=1)
TICKETS["Fastest Routeset Cars"] = TICKETS.apply(lambda x: get_routeset_cars(x["Fastest Routeset"]), axis=1)
TICKETS["Fastest Routeset Points"] = TICKETS.apply(lambda x: get_routeset_points(x["Fastest Routeset"]), axis=1)
TICKETS["Fastest Routeset Points per Turn"] = TICKETS.apply(lambda x: x["Fastest Routeset Points"]/x["Fastest Routeset Turns"], axis=1)
TICKETS["Fastest Routeset Cars per Turn"] = TICKETS.apply(lambda x: x["Fastest Routeset Cars"]/x["Fastest Routeset Turns"], axis=1)
TICKETS["Fastest Routeset Total Points per Turn"] = TICKETS.apply(lambda x: (x["Bonus Points"]+x["Fastest Routeset Points"])/x["Fastest Routeset Turns"], axis=1)

TICKETS["Highest PpT Routeset"] = TICKETS.apply(lambda x: get_highest_point_per_turn_tree([x["Origin"], x["Destination"]])[0], axis=1)
TICKETS["Highest PpT Routeset Turns"] = TICKETS.apply(lambda x: get_routeset_turns(x["Highest PpT Routeset"]), axis=1)
TICKETS["Highest PpT Routeset Cars"] = TICKETS.apply(lambda x: get_routeset_cars(x["Highest PpT Routeset"]), axis=1)
TICKETS["Highest PpT Routeset Points"] = TICKETS.apply(lambda x: get_routeset_points(x["Highest PpT Routeset"]), axis=1)
TICKETS["Highest PpT Routeset Points per Turn"] = TICKETS.apply(lambda x: x["Highest PpT Routeset Points"]/x["Highest PpT Routeset Turns"], axis=1)
TICKETS["Highest PpT Routeset Cars per Turn"] = TICKETS.apply(lambda x: x["Highest PpT Routeset Cars"]/x["Highest PpT Routeset Turns"], axis=1)
TICKETS["Highest PpT Routeset Total Points per Turn"] = TICKETS.apply(lambda x: (x["Bonus Points"]+x["Highest PpT Routeset Points"])/x["Highest PpT Routeset Turns"], axis=1)

def set_style(styler):
    fastest_route_color = "green"
    styler.background_gradient(axis=None,cmap=sns.dark_palette("yellow", as_cmap=True), subset=["Bonus Points"])
    styler.background_gradient(axis=None,cmap=sns.dark_palette(fastest_route_color, as_cmap=True), subset=["Fastest Routeset Cars"])
    styler.background_gradient(axis=None,cmap=sns.dark_palette(fastest_route_color, as_cmap=True), subset=["Fastest Routeset Points"])
    styler.background_gradient(axis=None,cmap=sns.dark_palette(fastest_route_color, as_cmap=True), subset=["Fastest Routeset Points per Turn"])
    styler.background_gradient(axis=None,cmap=sns.dark_palette(fastest_route_color, as_cmap=True), subset=["Fastest Routeset Cars per Turn"])
    styler.background_gradient(axis=None,cmap=sns.dark_palette(fastest_route_color, as_cmap=True), subset=["Fastest Routeset Total Points per Turn"])
    styler.background_gradient(axis=None,cmap=sns.dark_palette(fastest_route_color, as_cmap=True), subset=["Fastest Routeset Total Points per Turn"])
    styler.background_gradient(axis=None,cmap=sns.dark_palette(fastest_route_color, as_cmap=True).reversed(), subset=["Fastest Routeset Turns"])
    highest_ppt_route_color = "blue"
    styler.background_gradient(axis=None,cmap=sns.dark_palette(highest_ppt_route_color, as_cmap=True), subset=["Highest PpT Routeset Cars"])
    styler.background_gradient(axis=None,cmap=sns.dark_palette(highest_ppt_route_color, as_cmap=True), subset=["Highest PpT Routeset Points"])
    styler.background_gradient(axis=None,cmap=sns.dark_palette(highest_ppt_route_color, as_cmap=True), subset=["Highest PpT Routeset Points per Turn"])
    styler.background_gradient(axis=None,cmap=sns.dark_palette(highest_ppt_route_color, as_cmap=True), subset=["Highest PpT Routeset Cars per Turn"])
    styler.background_gradient(axis=None,cmap=sns.dark_palette(highest_ppt_route_color, as_cmap=True), subset=["Highest PpT Routeset Total Points per Turn"])
    styler.background_gradient(axis=None,cmap=sns.dark_palette(highest_ppt_route_color, as_cmap=True), subset=["Highest PpT Routeset Total Points per Turn"])
    styler.background_gradient(axis=None,cmap=sns.dark_palette(highest_ppt_route_color, as_cmap=True).reversed(), subset=["Highest PpT Routeset Turns"])
    styler.hide(axis=0) # Hides index column
    styler.set_tooltips(
        pd.DataFrame({
            # "Ticket": TICKETS.apply(lambda x: ">".join([x["Origin"], x["Destination"]])),
            "Fastest Routeset Turns": TICKETS["Fastest Routeset"].apply(lambda x: "\n".join(["-".join(y) for y in x])),
            "Highest PpT Routeset Turns": TICKETS["Highest PpT Routeset"].apply(lambda x: "\n".join(["-".join(y) for y in x])),
        }), as_title_attribute=True)
    styler.hide(axis=1, subset=["Origin", "Destination", "Fastest Routeset", "Highest PpT Routeset"]) # TODO: find a better way to display?
    styler.format(precision=0, subset=["Bonus Points", "Fastest Routeset Cars", "Highest PpT Routeset Cars"])
    styler.format(precision=2)
    return styler
TICKETS.sort_values("Highest PpT Routeset Total Points per Turn", ascending=False).style.pipe(set_style)
Loading...

Steiner Forests for combinations of tickets

There are cases where we do not need to connect all of our cities to complete our tickets. Example: Denver-El Paso & New York-Atlanta. In this case, we need to compare the sums of the steiner trees for each ticket destination pair to the steiner tree of the combined ticket destinations.

from more_itertools import set_partitions
import math

def _get_steiner_forest(tickets: list[tuple[str, str]], get_tree_function: callable) -> tuple[list[tuple[str,str]], float]:
    def flatten(l: list[list]) -> list:
        return [e for m in l for e in m]
        
    min_weight = math.inf
    min_routeset = None
    for ticket_partitions in set_partitions(tickets):
        total_routeset = set()
        total_weight = 0
        for partition in ticket_partitions:
            cities = flatten(partition)
            partition_routeset, partition_weight = get_tree_function(cities)
            total_weight += partition_weight
            for route in partition_routeset:
                sorted_route = list(route)
                sorted_route.sort()  # Sort so we don't double count e.g. (New York, Atlanta) and (Atlanta, New York)
                total_routeset.add(tuple(sorted_route))
        if total_weight < min_weight:
            min_weight = total_weight
            min_routeset = total_routeset
    return list(min_routeset), min_weight

def get_fastest_forest(tickets: list[tuple[str, str]]) -> tuple[list[tuple[str,str]], float]:
    return _get_steiner_forest(tickets, get_fastest_tree)

def get_highest_point_per_turn_forest(tickets: list[tuple[str, str]]) -> tuple[list[tuple[str,str]], float]:
    return _get_steiner_forest(tickets, get_highest_point_per_turn_tree)

Ticket Synergies

Which tickets have the highest potential points per turn when combined?

Ticket Pairs

TICKET_PAIRS = TICKETS.filter(["Origin", "Destination", "Bonus Points"], axis=1)
TICKET_PAIRS = TICKET_PAIRS.merge(TICKET_PAIRS, how='cross')
TICKET_PAIRS.drop(
    TICKET_PAIRS[(TICKET_PAIRS.Origin_x == TICKET_PAIRS.Origin_y) & (TICKET_PAIRS.Destination_x == TICKET_PAIRS.Destination_y)].index, 
    inplace=True)
TICKET_PAIRS.drop(
    TICKET_PAIRS[TICKET_PAIRS.Origin_x + "|" + TICKET_PAIRS.Destination_x < TICKET_PAIRS.Origin_y + "|" + TICKET_PAIRS.Destination_y].index,
    inplace=True
)


TICKET_PAIRS["Tickets"] = TICKET_PAIRS.apply(
    lambda x: "\n".join([">".join([get_city_abbreviation(x[f"Origin_{c}"]), get_city_abbreviation(x[f"Destination_{c}"])]) for c in ["x", "y"]]), 
    axis=1)
TICKET_PAIRS["Total Bonus Points"] = TICKET_PAIRS.apply(
    lambda x: x["Bonus Points_x"]+x["Bonus Points_y"], axis=1)

TICKET_PAIRS[["Fastest Routeset", "Fastest Routeset Turns"]] = TICKET_PAIRS.apply(
    lambda x: pd.Series(get_fastest_forest([(x["Origin_x"], x["Destination_x"]), (x["Origin_y"], x["Destination_y"])])), axis=1)
TICKET_PAIRS["Fastest Routeset Cars"] = TICKET_PAIRS.apply(
    lambda x: get_routeset_cars(x["Fastest Routeset"]), axis=1)
TICKET_PAIRS["Fastest Routeset Points"] = TICKET_PAIRS.apply(
    lambda x: get_routeset_points(x["Fastest Routeset"]), axis=1)
TICKET_PAIRS["Fastest Routeset Points per Turn"] = TICKET_PAIRS.apply(
    lambda x: x["Fastest Routeset Points"]/x["Fastest Routeset Turns"], axis=1)
TICKET_PAIRS["Fastest Routeset Cars per Turn"] = TICKET_PAIRS.apply(
    lambda x: x["Fastest Routeset Cars"]/x["Fastest Routeset Turns"], axis=1)
TICKET_PAIRS["Fastest Routeset Total Points per Turn"] = TICKET_PAIRS.apply(
    lambda x: (x["Total Bonus Points"]+x["Fastest Routeset Points"])/x["Fastest Routeset Turns"], axis=1)

TICKET_PAIRS["Highest PpT Routeset"] = TICKET_PAIRS.apply(
    lambda x: get_highest_point_per_turn_forest([(x["Origin_x"], x["Destination_x"]), (x["Origin_y"], x["Destination_y"])])[0], axis=1)
TICKET_PAIRS["Highest PpT Routeset Turns"] = TICKET_PAIRS.apply(
    lambda x: get_routeset_turns(x["Highest PpT Routeset"]), axis=1)
TICKET_PAIRS["Highest PpT Routeset Cars"] = TICKET_PAIRS.apply(
    lambda x: get_routeset_cars(x["Highest PpT Routeset"]), axis=1)
TICKET_PAIRS["Highest PpT Routeset Points"] = TICKET_PAIRS.apply(
    lambda x: get_routeset_points(x["Highest PpT Routeset"]), axis=1)
TICKET_PAIRS["Highest PpT Routeset Points per Turn"] = TICKET_PAIRS.apply(
    lambda x: x["Highest PpT Routeset Points"]/x["Highest PpT Routeset Turns"], axis=1)
TICKET_PAIRS["Highest PpT Routeset Cars per Turn"] = TICKET_PAIRS.apply(
    lambda x: x["Highest PpT Routeset Cars"]/x["Highest PpT Routeset Turns"], axis=1)
TICKET_PAIRS["Highest PpT Routeset Total Points per Turn"] = TICKET_PAIRS.apply(
    lambda x: (x["Total Bonus Points"]+x["Highest PpT Routeset Points"])/x["Highest PpT Routeset Turns"], axis=1)

def set_style(styler):
    fastest_route_color = "green"
    styler.background_gradient(axis=None,cmap=sns.dark_palette("yellow", as_cmap=True), subset=["Total Bonus Points"])
    styler.background_gradient(axis=None,cmap=sns.dark_palette(fastest_route_color, as_cmap=True), subset=["Fastest Routeset Cars"])
    styler.background_gradient(axis=None,cmap=sns.dark_palette(fastest_route_color, as_cmap=True), subset=["Fastest Routeset Points"])
    styler.background_gradient(axis=None,cmap=sns.dark_palette(fastest_route_color, as_cmap=True), subset=["Fastest Routeset Points per Turn"])
    styler.background_gradient(axis=None,cmap=sns.dark_palette(fastest_route_color, as_cmap=True), subset=["Fastest Routeset Cars per Turn"])
    styler.background_gradient(axis=None,cmap=sns.dark_palette(fastest_route_color, as_cmap=True), subset=["Fastest Routeset Total Points per Turn"])
    styler.background_gradient(axis=None,cmap=sns.dark_palette(fastest_route_color, as_cmap=True), subset=["Fastest Routeset Total Points per Turn"])
    styler.background_gradient(axis=None,cmap=sns.dark_palette(fastest_route_color, as_cmap=True).reversed(), subset=["Fastest Routeset Turns"])
    highest_ppt_route_color = "blue"
    styler.background_gradient(axis=None,cmap=sns.dark_palette(highest_ppt_route_color, as_cmap=True), subset=["Highest PpT Routeset Cars"])
    styler.background_gradient(axis=None,cmap=sns.dark_palette(highest_ppt_route_color, as_cmap=True), subset=["Highest PpT Routeset Points"])
    styler.background_gradient(axis=None,cmap=sns.dark_palette(highest_ppt_route_color, as_cmap=True), subset=["Highest PpT Routeset Points per Turn"])
    styler.background_gradient(axis=None,cmap=sns.dark_palette(highest_ppt_route_color, as_cmap=True), subset=["Highest PpT Routeset Cars per Turn"])
    styler.background_gradient(axis=None,cmap=sns.dark_palette(highest_ppt_route_color, as_cmap=True), subset=["Highest PpT Routeset Total Points per Turn"])
    styler.background_gradient(axis=None,cmap=sns.dark_palette(highest_ppt_route_color, as_cmap=True), subset=["Highest PpT Routeset Total Points per Turn"])
    styler.background_gradient(axis=None,cmap=sns.dark_palette(highest_ppt_route_color, as_cmap=True).reversed(), subset=["Highest PpT Routeset Turns"])
    styler.hide(axis=0) # Hides index column
    styler.set_tooltips(
        pd.DataFrame({
            "Fastest Routeset Turns": TICKET_PAIRS["Fastest Routeset"].apply(lambda x: "\n".join(["-".join(y) for y in x])),
            "Highest PpT Routeset Turns": TICKET_PAIRS["Highest PpT Routeset"].apply(lambda x: "\n".join(["-".join(y) for y in x])),
        }), as_title_attribute=True)
    styler.hide(axis=1, subset=["Origin_x", "Origin_y", "Bonus Points_x", "Bonus Points_y", "Destination_x", "Destination_y", "Fastest Routeset", "Highest PpT Routeset"]) # TODO: find a better way to display?
    styler.format(precision=0, subset=["Total Bonus Points", "Fastest Routeset Cars", "Highest PpT Routeset Cars"])
    styler.format(precision=2)
    return styler
TICKET_PAIRS.sort_values("Highest PpT Routeset Total Points per Turn", ascending=False).style.pipe(set_style)
Loading...

Ticket Trios

TICKET_TRIOS = TICKET_PAIRS.filter(["Origin_x", "Destination_x", "Bonus Points_x", "Origin_y", "Destination_y", "Bonus Points_y"], axis=1).merge(
    TICKETS.filter(["Origin", "Destination", "Bonus Points"], axis=1), how='cross')
TICKET_TRIOS.rename(columns={"Origin": "Origin_z", "Destination": "Destination_z", "Bonus Points": "Bonus Points_z"}, inplace=True)
TICKET_TRIOS.drop(
    TICKET_TRIOS[(TICKET_TRIOS.Origin_z == TICKET_TRIOS.Origin_x) & (TICKET_TRIOS.Destination_z == TICKET_TRIOS.Destination_x)].index, 
    inplace=True)
TICKET_TRIOS.drop(
    TICKET_TRIOS[(TICKET_TRIOS.Origin_z == TICKET_TRIOS.Origin_y) & (TICKET_TRIOS.Destination_z == TICKET_TRIOS.Destination_y)].index, 
    inplace=True)
TICKET_TRIOS.drop(
    TICKET_TRIOS[(TICKET_TRIOS.Origin_x + "|" + TICKET_TRIOS.Destination_x + "|" < TICKET_TRIOS.Origin_y + "|" + TICKET_TRIOS.Destination_y) | (TICKET_TRIOS.Origin_y + "|" + TICKET_TRIOS.Destination_y < TICKET_TRIOS.Origin_z + "|" + TICKET_TRIOS.Destination_z)].index,
    inplace=True
)

TICKET_TRIOS["Tickets"] = TICKET_TRIOS.apply(
    lambda x: "\n".join([">".join([get_city_abbreviation(x[f"Origin_{c}"]), get_city_abbreviation(x[f"Destination_{c}"])]) for c in ["x", "y", "z"]]), 
    axis=1)
TICKET_TRIOS["Total Bonus Points"] = TICKET_TRIOS.apply(
    lambda x: x["Bonus Points_x"]+x["Bonus Points_y"]+x["Bonus Points_z"], axis=1)

TICKET_TRIOS[["Fastest Routeset", "Fastest Routeset Turns"]] = TICKET_TRIOS.apply(
    lambda x: pd.Series(get_fastest_forest([(x["Origin_x"], x["Destination_x"]), (x["Origin_y"], x["Destination_y"]), (x["Origin_z"], x["Destination_z"])])), axis=1)
TICKET_TRIOS["Fastest Routeset Cars"] = TICKET_TRIOS.apply(
    lambda x: get_routeset_cars(x["Fastest Routeset"]), axis=1)
TICKET_TRIOS["Fastest Routeset Points"] = TICKET_TRIOS.apply(
    lambda x: get_routeset_points(x["Fastest Routeset"]), axis=1)
TICKET_TRIOS["Fastest Routeset Points per Turn"] = TICKET_TRIOS.apply(
    lambda x: x["Fastest Routeset Points"]/x["Fastest Routeset Turns"], axis=1)
TICKET_TRIOS["Fastest Routeset Cars per Turn"] = TICKET_TRIOS.apply(
    lambda x: x["Fastest Routeset Cars"]/x["Fastest Routeset Turns"], axis=1)
TICKET_TRIOS["Fastest Routeset Total Points per Turn"] = TICKET_TRIOS.apply(
    lambda x: (x["Total Bonus Points"]+x["Fastest Routeset Points"])/x["Fastest Routeset Turns"], axis=1)

TICKET_TRIOS["Highest PpT Routeset"] = TICKET_TRIOS.apply(
    lambda x: get_highest_point_per_turn_forest([(x["Origin_x"], x["Destination_x"]), (x["Origin_y"], x["Destination_y"]), (x["Origin_z"], x["Destination_z"])])[0], axis=1)
TICKET_TRIOS["Highest PpT Routeset Turns"] = TICKET_TRIOS.apply(
    lambda x: get_routeset_turns(x["Highest PpT Routeset"]), axis=1)
TICKET_TRIOS["Highest PpT Routeset Cars"] = TICKET_TRIOS.apply(
    lambda x: get_routeset_cars(x["Highest PpT Routeset"]), axis=1)
TICKET_TRIOS["Highest PpT Routeset Points"] = TICKET_TRIOS.apply(
    lambda x: get_routeset_points(x["Highest PpT Routeset"]), axis=1)
TICKET_TRIOS["Highest PpT Routeset Points per Turn"] = TICKET_TRIOS.apply(
    lambda x: x["Highest PpT Routeset Points"]/x["Highest PpT Routeset Turns"], axis=1)
TICKET_TRIOS["Highest PpT Routeset Cars per Turn"] = TICKET_TRIOS.apply(
    lambda x: x["Highest PpT Routeset Cars"]/x["Highest PpT Routeset Turns"], axis=1)
TICKET_TRIOS["Highest PpT Routeset Total Points per Turn"] = TICKET_TRIOS.apply(
    lambda x: (x["Total Bonus Points"]+x["Highest PpT Routeset Points"])/x["Highest PpT Routeset Turns"], axis=1)

def set_style(styler):
    fastest_route_color = "green"
    styler.background_gradient(axis=None,cmap=sns.dark_palette("yellow", as_cmap=True), subset=["Total Bonus Points"])
    styler.background_gradient(axis=None,cmap=sns.dark_palette(fastest_route_color, as_cmap=True), subset=["Fastest Routeset Cars"])
    styler.background_gradient(axis=None,cmap=sns.dark_palette(fastest_route_color, as_cmap=True), subset=["Fastest Routeset Points"])
    styler.background_gradient(axis=None,cmap=sns.dark_palette(fastest_route_color, as_cmap=True), subset=["Fastest Routeset Points per Turn"])
    styler.background_gradient(axis=None,cmap=sns.dark_palette(fastest_route_color, as_cmap=True), subset=["Fastest Routeset Cars per Turn"])
    styler.background_gradient(axis=None,cmap=sns.dark_palette(fastest_route_color, as_cmap=True), subset=["Fastest Routeset Total Points per Turn"])
    styler.background_gradient(axis=None,cmap=sns.dark_palette(fastest_route_color, as_cmap=True), subset=["Fastest Routeset Total Points per Turn"])
    styler.background_gradient(axis=None,cmap=sns.dark_palette(fastest_route_color, as_cmap=True).reversed(), subset=["Fastest Routeset Turns"])
    highest_ppt_route_color = "blue"
    styler.background_gradient(axis=None,cmap=sns.dark_palette(highest_ppt_route_color, as_cmap=True), subset=["Highest PpT Routeset Cars"])
    styler.background_gradient(axis=None,cmap=sns.dark_palette(highest_ppt_route_color, as_cmap=True), subset=["Highest PpT Routeset Points"])
    styler.background_gradient(axis=None,cmap=sns.dark_palette(highest_ppt_route_color, as_cmap=True), subset=["Highest PpT Routeset Points per Turn"])
    styler.background_gradient(axis=None,cmap=sns.dark_palette(highest_ppt_route_color, as_cmap=True), subset=["Highest PpT Routeset Cars per Turn"])
    styler.background_gradient(axis=None,cmap=sns.dark_palette(highest_ppt_route_color, as_cmap=True), subset=["Highest PpT Routeset Total Points per Turn"])
    styler.background_gradient(axis=None,cmap=sns.dark_palette(highest_ppt_route_color, as_cmap=True), subset=["Highest PpT Routeset Total Points per Turn"])
    styler.background_gradient(axis=None,cmap=sns.dark_palette(highest_ppt_route_color, as_cmap=True).reversed(), subset=["Highest PpT Routeset Turns"])
    styler.hide(axis=0) # Hides index column
    styler.set_tooltips(
        pd.DataFrame({
            "Fastest Routeset Turns": TICKET_TRIOS["Fastest Routeset"].apply(lambda x: "\n".join(["-".join(y) for y in x])),
            "Highest PpT Routeset Turns": TICKET_TRIOS["Highest PpT Routeset"].apply(lambda x: "\n".join(["-".join(y) for y in x])),
        }), as_title_attribute=True)
    styler.hide(axis=1, subset=["Origin_x", "Origin_y", "Origin_z", "Bonus Points_x", "Bonus Points_y", "Bonus Points_z", "Destination_x", "Destination_y", "Destination_z", "Fastest Routeset", "Highest PpT Routeset"]) # TODO: find a better way to display?
    styler.format(precision=0, subset=["Total Bonus Points", "Fastest Routeset Cars", "Highest PpT Routeset Cars"])
    styler.format(precision=2)
    return styler
TICKET_TRIOS.sort_values("Highest PpT Routeset Total Points per Turn", ascending=False).style.pipe(set_style)
Loading...

Rush Strategy

This is a good benchmark to use to decide whether or not a set of tickets is worth pursuing, since this strategy is effectively always available to you

  • Ignore tickets (take the minimum starting tickets, and only take the lowest valued ones)

  • Claim the longest routes possible

  • End the game as fast as possible

from collections import defaultdict

routes_claimed_sets = []
for number_of_sixroutes_allowed in reversed(range(1,8)):
    routes_claimed_set = {i:0 for i in range(1,7)}
    train_cars_remaining = TOTAL_TRAIN_CARS
    sixroutes_remaining = number_of_sixroutes_allowed
    while train_cars_remaining > 0:
        if train_cars_remaining <= 5:
            routes_claimed_set[train_cars_remaining] += 1
            train_cars_remaining = 0
            continue
        if sixroutes_remaining > 0:
            routes_claimed_set[6] += 1
            sixroutes_remaining -= 1
            train_cars_remaining -= 6
            continue
        routes_claimed_set[5] += 1
        train_cars_remaining -= 5
    routes_claimed_sets.append(routes_claimed_set)

RUSH_MOVESETS = pd.DataFrame(routes_claimed_sets)
RUSH_MOVESETS["Turns"] = sum(
        [RUSH_MOVESETS[rte_length] * ROUTE_LENGTHS.loc[ROUTE_LENGTHS["Cars"] == rte_length, "Total Turns to Claim"].iloc[0] for rte_length in range(1,7)]
    ) - STARTING_CARS/CARS_PER_TURN
RUSH_MOVESETS["Points"] = sum(
        [RUSH_MOVESETS[rte_length] * ROUTE_LENGTHS.loc[ROUTE_LENGTHS["Cars"] == rte_length, "Points"].iloc[0] for rte_length in range(1,7)]
    )
RUSH_MOVESETS["Points per Turn"] = RUSH_MOVESETS["Points"] / RUSH_MOVESETS["Turns"]
RUSH_MOVESETS["Cars per Turn"] = TOTAL_TRAIN_CARS / RUSH_MOVESETS["Turns"]
# TODO: Add average ticket loss/min ticket loss/max ticket loss

def set_style(styler):
    styler.background_gradient(cmap=sns.light_palette("seagreen", as_cmap=True).reversed(), subset=["Turns"])
    styler.background_gradient(cmap=sns.light_palette("seagreen", as_cmap=True), subset=["Points", "Points per Turn", "Cars per Turn"])
    styler.hide(axis=0) # Hides index column
    styler.format(precision=2)
    styler.format(precision=0, subset=["Turns"])
    return styler

RUSH_MOVESETS.style.pipe(set_style)
Loading...