421 lines
20 KiB
JavaScript
421 lines
20 KiB
JavaScript
// TicketToRideBoard.js — pure data + geometry for Ticket to Ride (USA edition).
|
||
// No Phaser imports, no game state. Everything here is computed once at module
|
||
// load and shared by Logic, AI, and the scene so they agree on one model.
|
||
//
|
||
// Coordinate space is the 1920×1080 canvas. The map occupies the left/centre
|
||
// (x < ~1500, y < ~850); the right column is reserved for the card market and
|
||
// piles, and the bottom strip for the human's hand.
|
||
|
||
// ── Cities ──────────────────────────────────────────────────────────────────
|
||
// 36 cities, positioned to mirror the real US/Canada geography of the board.
|
||
export const CITIES = [
|
||
{ id: 0, name: 'Vancouver', x: 160, y: 170 },
|
||
{ id: 1, name: 'Seattle', x: 180, y: 260 },
|
||
{ id: 2, name: 'Portland', x: 150, y: 355 },
|
||
{ id: 3, name: 'San Francisco', x: 120, y: 520 },
|
||
{ id: 4, name: 'Los Angeles', x: 200, y: 665 },
|
||
{ id: 5, name: 'Calgary', x: 340, y: 150 },
|
||
{ id: 6, name: 'Winnipeg', x: 640, y: 160 },
|
||
{ id: 7, name: 'Helena', x: 450, y: 330 },
|
||
{ id: 8, name: 'Duluth', x: 740, y: 300 },
|
||
{ id: 9, name: 'Salt Lake City', x: 340, y: 475 },
|
||
{ id: 10, name: 'Las Vegas', x: 290, y: 590 },
|
||
{ id: 11, name: 'Phoenix', x: 380, y: 675 },
|
||
{ id: 12, name: 'Santa Fe', x: 490, y: 585 },
|
||
{ id: 13, name: 'Denver', x: 510, y: 485 },
|
||
{ id: 14, name: 'El Paso', x: 540, y: 705 },
|
||
{ id: 15, name: 'Omaha', x: 740, y: 430 },
|
||
{ id: 16, name: 'Kansas City', x: 775, y: 505 },
|
||
{ id: 17, name: 'Oklahoma City', x: 730, y: 625 },
|
||
{ id: 18, name: 'Dallas', x: 740, y: 720 },
|
||
{ id: 19, name: 'Houston', x: 800, y: 800 },
|
||
{ id: 20, name: 'Little Rock', x: 845, y: 645 },
|
||
{ id: 21, name: 'Chicago', x: 915, y: 400 },
|
||
{ id: 22, name: 'Saint Louis', x: 880, y: 545 },
|
||
{ id: 23, name: 'Sault Ste Marie', x: 940, y: 235 },
|
||
{ id: 24, name: 'Nashville', x: 975, y: 595 },
|
||
{ id: 25, name: 'New Orleans', x: 915, y: 765 },
|
||
{ id: 26, name: 'Atlanta', x: 1045, y: 645 },
|
||
{ id: 27, name: 'Toronto', x: 1090, y: 300 },
|
||
{ id: 28, name: 'Montreal', x: 1230, y: 215 },
|
||
{ id: 29, name: 'Boston', x: 1360, y: 290 },
|
||
{ id: 30, name: 'New York', x: 1300, y: 365 },
|
||
{ id: 31, name: 'Pittsburgh', x: 1110, y: 425 },
|
||
{ id: 32, name: 'Washington', x: 1280, y: 455 },
|
||
{ id: 33, name: 'Raleigh', x: 1170, y: 560 },
|
||
{ id: 34, name: 'Charleston', x: 1230, y: 660 },
|
||
{ id: 35, name: 'Miami', x: 1235, y: 825 },
|
||
];
|
||
|
||
export function cityAt(id) { return CITIES[id]; }
|
||
export function cityId(name) { return CITIES.find((c) => c.name === name)?.id ?? -1; }
|
||
|
||
// ── Train-card colours ────────────────────────────────────────────────────────
|
||
// The 8 train colours (TTR's "pink" is rendered here as purple), plus 'gray'
|
||
// for wild routes (claimable with any single colour) and 'locomotive' wilds.
|
||
export const TRAIN_COLORS = ['red', 'orange', 'yellow', 'green', 'blue', 'purple', 'black', 'white'];
|
||
|
||
export const CARD_COLOR_HEX = {
|
||
red: 0xd23b3b,
|
||
orange: 0xe08a1e,
|
||
yellow: 0xe0b000,
|
||
green: 0x2e7d32,
|
||
blue: 0x2d6cdf,
|
||
purple: 0x8e44ad,
|
||
black: 0x2b2b2b,
|
||
white: 0xe8e4d8,
|
||
gray: 0x9a8f7d, // neutral route colour
|
||
locomotive: 0xdda0dd, // wild — rendered with a rainbow accent in the scene
|
||
};
|
||
|
||
export const CARD_LABEL = {
|
||
red: 'Red', orange: 'Orange', yellow: 'Yellow', green: 'Green',
|
||
blue: 'Blue', purple: 'Purple', black: 'Black', white: 'White',
|
||
locomotive: 'Locomotive',
|
||
};
|
||
|
||
// 110-card train deck: 12 of each of the 8 colours + 14 locomotive wilds.
|
||
export const TRAIN_DECK = [
|
||
...TRAIN_COLORS.flatMap((c) => Array(12).fill(c)),
|
||
...Array(14).fill('locomotive'),
|
||
];
|
||
|
||
// ── Player colours ─────────────────────────────────────────────────────────────
|
||
export const PLAYER_COLORS = [
|
||
{ key: 'blue', hex: 0x2d6cdf, hexDark: 0x1c4490, name: 'Blue' },
|
||
{ key: 'red', hex: 0xd23b3b, hexDark: 0x8f2424, name: 'Red' },
|
||
{ key: 'green', hex: 0x2e9e4f, hexDark: 0x1d6633, name: 'Green' },
|
||
{ key: 'yellow', hex: 0xe0b000, hexDark: 0x9c7a00, name: 'Yellow' },
|
||
{ key: 'black', hex: 0x3a3a3a, hexDark: 0x161616, name: 'Black' },
|
||
];
|
||
|
||
// ── Scoring + counts ────────────────────────────────────────────────────────────
|
||
export const ROUTE_SCORE = { 1: 1, 2: 2, 3: 4, 4: 7, 5: 10, 6: 15 };
|
||
export const TRAINS_PER_PLAYER = 45;
|
||
export const LONGEST_PATH_BONUS = 10;
|
||
export const FACE_UP_COUNT = 5;
|
||
export const ENDGAME_TRAIN_THRESHOLD = 2; // a turn ending with <= this triggers the last round
|
||
|
||
// ── Routes ───────────────────────────────────────────────────────────────────
|
||
// Authored compactly as [cityA, cityB, length, colour]. A double route between
|
||
// the same two cities uses [cityA, cityB, length, [colour1, colour2]] and is
|
||
// expanded into two parallel ROUTE entries sharing a doubleGroup id.
|
||
// 'pink' in the physical game maps to 'purple' here.
|
||
const ROUTE_DEFS = [
|
||
[0, 5, 3, 'gray'], // Vancouver – Calgary
|
||
[0, 1, 1, ['gray', 'gray']], // Vancouver – Seattle
|
||
[1, 5, 4, 'gray'], // Seattle – Calgary
|
||
[1, 2, 1, ['gray', 'gray']], // Seattle – Portland
|
||
[2, 3, 5, ['green', 'purple']], // Portland – San Francisco
|
||
[2, 9, 6, 'blue'], // Portland – Salt Lake City
|
||
[5, 6, 6, 'white'], // Calgary – Winnipeg
|
||
[5, 7, 4, 'gray'], // Calgary – Helena
|
||
[1, 7, 6, 'yellow'], // Seattle – Helena
|
||
[3, 9, 5, ['orange', 'white']], // San Francisco – Salt Lake City
|
||
[3, 4, 3, ['yellow', 'purple']], // San Francisco – Los Angeles
|
||
[9, 7, 3, 'purple'], // Salt Lake City – Helena
|
||
[9, 10, 3, 'orange'], // Salt Lake City – Las Vegas
|
||
[9, 13, 3, ['red', 'yellow']], // Salt Lake City – Denver
|
||
[4, 10, 2, 'gray'], // Los Angeles – Las Vegas
|
||
[4, 11, 3, 'gray'], // Los Angeles – Phoenix
|
||
[4, 14, 6, 'black'], // Los Angeles – El Paso
|
||
[7, 6, 4, 'blue'], // Helena – Winnipeg
|
||
[7, 8, 6, 'orange'], // Helena – Duluth
|
||
[7, 13, 4, 'green'], // Helena – Denver
|
||
[11, 13, 5, 'white'], // Phoenix – Denver
|
||
[11, 12, 3, 'gray'], // Phoenix – Santa Fe
|
||
[11, 14, 3, 'gray'], // Phoenix – El Paso
|
||
[13, 12, 2, 'gray'], // Denver – Santa Fe
|
||
[12, 14, 2, 'gray'], // Santa Fe – El Paso
|
||
[13, 15, 4, 'purple'], // Denver – Omaha
|
||
[13, 16, 4, ['black', 'orange']], // Denver – Kansas City
|
||
[13, 17, 4, 'red'], // Denver – Oklahoma City
|
||
[12, 17, 3, 'blue'], // Santa Fe – Oklahoma City
|
||
[14, 17, 5, 'yellow'], // El Paso – Oklahoma City
|
||
[14, 18, 4, 'red'], // El Paso – Dallas
|
||
[14, 19, 6, 'green'], // El Paso – Houston
|
||
[6, 23, 6, 'gray'], // Winnipeg – Sault Ste Marie
|
||
[6, 8, 4, 'black'], // Winnipeg – Duluth
|
||
[8, 23, 3, 'gray'], // Duluth – Sault Ste Marie
|
||
[8, 15, 2, ['gray', 'gray']], // Duluth – Omaha
|
||
[8, 21, 3, 'red'], // Duluth – Chicago
|
||
[8, 27, 6, 'purple'], // Duluth – Toronto
|
||
[15, 16, 1, ['gray', 'gray']], // Omaha – Kansas City
|
||
[15, 21, 4, 'blue'], // Omaha – Chicago
|
||
[16, 17, 2, ['gray', 'gray']], // Kansas City – Oklahoma City
|
||
[16, 22, 2, ['blue', 'purple']], // Kansas City – Saint Louis
|
||
[17, 20, 2, 'gray'], // Oklahoma City – Little Rock
|
||
[17, 18, 2, ['gray', 'gray']], // Oklahoma City – Dallas
|
||
[18, 19, 1, ['gray', 'gray']], // Dallas – Houston
|
||
[19, 25, 2, 'gray'], // Houston – New Orleans
|
||
[20, 22, 2, 'gray'], // Little Rock – Saint Louis
|
||
[20, 24, 3, 'white'], // Little Rock – Nashville
|
||
[20, 25, 3, 'green'], // Little Rock – New Orleans
|
||
[22, 21, 2, ['green', 'white']], // Saint Louis – Chicago
|
||
[22, 31, 5, 'green'], // Saint Louis – Pittsburgh
|
||
[22, 24, 2, 'gray'], // Saint Louis – Nashville
|
||
[21, 31, 3, ['orange', 'black']], // Chicago – Pittsburgh
|
||
[21, 27, 4, 'white'], // Chicago – Toronto
|
||
[23, 27, 2, 'gray'], // Sault Ste Marie – Toronto
|
||
[23, 28, 5, 'black'], // Sault Ste Marie – Montreal
|
||
[27, 28, 3, 'gray'], // Toronto – Montreal
|
||
[27, 31, 2, 'gray'], // Toronto – Pittsburgh
|
||
[28, 29, 2, ['gray', 'gray']], // Montreal – Boston
|
||
[28, 30, 3, 'blue'], // Montreal – New York
|
||
[29, 30, 2, ['gray', 'gray']], // Boston – New York
|
||
[30, 31, 2, ['white', 'green']], // New York – Pittsburgh
|
||
[30, 32, 2, ['orange', 'black']], // New York – Washington
|
||
[31, 32, 2, 'gray'], // Pittsburgh – Washington
|
||
[31, 33, 2, 'gray'], // Pittsburgh – Raleigh
|
||
[31, 24, 4, 'yellow'], // Pittsburgh – Nashville
|
||
[24, 33, 3, 'black'], // Nashville – Raleigh
|
||
[24, 26, 1, 'gray'], // Nashville – Atlanta
|
||
[32, 33, 2, ['gray', 'gray']], // Washington – Raleigh
|
||
[33, 26, 2, ['gray', 'gray']], // Raleigh – Atlanta
|
||
[33, 34, 2, 'gray'], // Raleigh – Charleston
|
||
[26, 34, 2, 'gray'], // Atlanta – Charleston
|
||
[26, 35, 5, 'blue'], // Atlanta – Miami
|
||
[26, 25, 4, ['yellow', 'orange']], // Atlanta – New Orleans
|
||
[34, 35, 4, 'purple'], // Charleston – Miami
|
||
[25, 35, 6, 'red'], // New Orleans – Miami
|
||
];
|
||
|
||
// Optional curve overrides for routes that should arc rather than run straight.
|
||
// Key is 'minCityId-maxCityId'. dir is a cardinal screen direction; pct is the
|
||
// control-point displacement as a fraction of the straight-line length.
|
||
const ROUTE_CURVES = {
|
||
'4-14': { dir: 'down', pct: 0.35 }, // Los Angeles – El Paso
|
||
'5-6': { dir: 'up', pct: 0.25 }, // Calgary – Winnipeg
|
||
'3-4': { dir: 'left', pct: 0.20 }, // San Francisco – Los Angeles
|
||
'2-3': { dir: 'left', pct: 0.20 }, // Portland – San Francisco
|
||
'11-13': { dir: 'left', pct: 0.20 }, // Phoenix – Denver
|
||
'9-10': { dir: 'right', pct: 0.20 }, // Salt Lake City – Las Vegas
|
||
'13-17': { dir: 'down', pct: 0.25 }, // Denver – Oklahoma City
|
||
'13-16': { dir: 'down', pct: 0.10 }, // Denver – Kansas City
|
||
'13-15': { dir: 'up', pct: 0.10 }, // Denver – Omaha
|
||
'21-31': { dir: 'up', pct: 0.10 }, // Chicago – Pittsburgh
|
||
'25-35': { dir: 'up', pct: 0.35 }, // New Orleans – Miami
|
||
'26-35': { dir: 'right', pct: 0.20 }, // Atlanta – Miami
|
||
'14-19': { dir: 'down', pct: 0.25 }, // El Paso – Houston
|
||
'12-17': { dir: 'down', pct: 0.20 }, // Santa Fe – Oklahoma City
|
||
'1-5': { dir: 'down', pct: 0.30 }, // Seattle – Calgary
|
||
'23-28': { dir: 'up', pct: 0.25 }, // Sault Ste Marie – Montreal
|
||
'27-28': { dir: 'up', pct: 0.20 }, // Toronto – Montreal
|
||
'28-30': { dir: 'left', pct: 0.25 }, // Montreal – New York
|
||
};
|
||
|
||
// Expand ROUTE_DEFS into the flat ROUTES array, generating ids, double-route
|
||
// grouping, and parallelSide so the two strips of a double render side-by-side.
|
||
export const ROUTES = (() => {
|
||
const out = [];
|
||
for (const [a, b, length, colour] of ROUTE_DEFS) {
|
||
const curveKey = `${Math.min(a, b)}-${Math.max(a, b)}`;
|
||
const curve = ROUTE_CURVES[curveKey] ?? null;
|
||
if (Array.isArray(colour)) {
|
||
const group = `${a}-${b}`;
|
||
colour.forEach((c, i) => {
|
||
out.push({ id: out.length, a, b, length, color: c, doubleGroup: group, parallelSide: i, curve });
|
||
});
|
||
} else {
|
||
out.push({ id: out.length, a, b, length, color: colour, doubleGroup: null, parallelSide: 0, curve });
|
||
}
|
||
}
|
||
return out;
|
||
})();
|
||
|
||
// ── Destination tickets ──────────────────────────────────────────────────────
|
||
// The 30 USA destination tickets [cityNameA, cityNameB, points].
|
||
const TICKET_DEFS = [
|
||
['Los Angeles', 'New York', 21],
|
||
['Duluth', 'Houston', 8],
|
||
['Sault Ste Marie', 'Nashville', 8],
|
||
['New York', 'Atlanta', 6],
|
||
['Portland', 'Nashville', 17],
|
||
['Vancouver', 'Montreal', 20],
|
||
['Duluth', 'El Paso', 10],
|
||
['Toronto', 'Miami', 10],
|
||
['Portland', 'Phoenix', 11],
|
||
['Dallas', 'New York', 11],
|
||
['Calgary', 'Salt Lake City', 7],
|
||
['Calgary', 'Phoenix', 13],
|
||
['Los Angeles', 'Miami', 20],
|
||
['Winnipeg', 'Little Rock', 11],
|
||
['San Francisco', 'Atlanta', 17],
|
||
['Kansas City', 'Houston', 5],
|
||
['Los Angeles', 'Chicago', 16],
|
||
['Denver', 'Pittsburgh', 11],
|
||
['Chicago', 'Santa Fe', 9],
|
||
['Vancouver', 'Santa Fe', 13],
|
||
['Boston', 'Miami', 12],
|
||
['Chicago', 'New Orleans', 7],
|
||
['Montreal', 'Atlanta', 9],
|
||
['Seattle', 'New York', 22],
|
||
['Denver', 'El Paso', 4],
|
||
['Helena', 'Los Angeles', 8],
|
||
['Winnipeg', 'Houston', 12],
|
||
['Montreal', 'New Orleans', 13],
|
||
['Sault Ste Marie', 'Oklahoma City', 9],
|
||
['Seattle', 'Los Angeles', 9],
|
||
];
|
||
|
||
export const TICKETS = TICKET_DEFS.map(([nameA, nameB, points], id) => ({
|
||
id, a: cityId(nameA), b: cityId(nameB), points,
|
||
}));
|
||
|
||
// ── Adjacency index (for pathfinding / connectivity) ───────────────────────────
|
||
// cityId -> [{ routeId, other, length, color, doubleGroup }]
|
||
function buildAdj(cities, routes) {
|
||
const adj = new Map();
|
||
for (const c of cities) adj.set(c.id, []);
|
||
for (const r of routes) {
|
||
adj.get(r.a).push({ routeId: r.id, other: r.b, length: r.length, color: r.color, doubleGroup: r.doubleGroup });
|
||
adj.get(r.b).push({ routeId: r.id, other: r.a, length: r.length, color: r.color, doubleGroup: r.doubleGroup });
|
||
}
|
||
return adj;
|
||
}
|
||
export const ROUTE_ADJ = buildAdj(CITIES, ROUTES);
|
||
|
||
// ── Route segment geometry ──────────────────────────────────────────────────────
|
||
const CITY_MARGIN = 30; // keep car slots clear of the city dots
|
||
const CAR_GAP = 6; // pixel gap between adjacent cars
|
||
const CAR_WIDTH = 16; // perpendicular thickness of a car
|
||
const DOUBLE_OFFSET = 12; // perpendicular shift for each strip of a double route
|
||
const CURVE_SAMPLES = 10; // total bezier sample points (including endpoints)
|
||
|
||
// Samples a quadratic bezier through a displaced midpoint control, returning
|
||
// CURVE_SAMPLES {x,y} points that form a smooth polyline.
|
||
function bezierPolyline(A, B, curve) {
|
||
const shift = curve.pct * Math.hypot(B.x - A.x, B.y - A.y);
|
||
const C = {
|
||
x: (A.x + B.x) / 2 + (curve.dir === 'right' ? shift : curve.dir === 'left' ? -shift : 0),
|
||
y: (A.y + B.y) / 2 + (curve.dir === 'down' ? shift : curve.dir === 'up' ? -shift : 0),
|
||
};
|
||
const pts = [];
|
||
for (let i = 0; i < CURVE_SAMPLES; i++) {
|
||
const t = i / (CURVE_SAMPLES - 1);
|
||
const mt = 1 - t;
|
||
pts.push({ x: mt*mt*A.x + 2*mt*t*C.x + t*t*B.x, y: mt*mt*A.y + 2*mt*t*C.y + t*t*B.y });
|
||
}
|
||
return pts;
|
||
}
|
||
|
||
function polylineLength(pts) {
|
||
let len = 0;
|
||
for (let i = 1; i < pts.length; i++) len += Math.hypot(pts[i].x - pts[i-1].x, pts[i].y - pts[i-1].y);
|
||
return len;
|
||
}
|
||
|
||
// Returns {x, y, angle} at distance d along pts, clamped to the final segment.
|
||
function pointAtDist(pts, d) {
|
||
let acc = 0;
|
||
for (let i = 1; i < pts.length; i++) {
|
||
const segLen = Math.hypot(pts[i].x - pts[i-1].x, pts[i].y - pts[i-1].y);
|
||
if (acc + segLen >= d || i === pts.length - 1) {
|
||
const t = segLen > 0 ? Math.min((d - acc) / segLen, 1) : 0;
|
||
return {
|
||
x: pts[i-1].x + t * (pts[i].x - pts[i-1].x),
|
||
y: pts[i-1].y + t * (pts[i].y - pts[i-1].y),
|
||
angle: Math.atan2(pts[i].y - pts[i-1].y, pts[i].x - pts[i-1].x),
|
||
};
|
||
}
|
||
acc += segLen;
|
||
}
|
||
}
|
||
|
||
// Returns one slot per train-length: { cx, cy, angle, w, h }.
|
||
// Curved routes distribute cars along a bezier polyline; each car is still a
|
||
// straight rectangle aligned to its local polyline segment.
|
||
export function routeSegments(route) {
|
||
const A = CITIES[route.a];
|
||
const B = CITIES[route.b];
|
||
const off = route.doubleGroup ? (route.parallelSide === 0 ? -DOUBLE_OFFSET : DOUBLE_OFFSET) : 0;
|
||
const pts = route.curve ? bezierPolyline(A, B, route.curve) : [A, B];
|
||
const totalLen = polylineLength(pts);
|
||
const n = route.length;
|
||
const carLen = (totalLen - CITY_MARGIN * 2 - CAR_GAP * (n - 1)) / n;
|
||
const segs = [];
|
||
for (let i = 0; i < n; i++) {
|
||
const pos = pointAtDist(pts, CITY_MARGIN + carLen / 2 + i * (carLen + CAR_GAP));
|
||
segs.push({
|
||
cx: pos.x - Math.sin(pos.angle) * off,
|
||
cy: pos.y + Math.cos(pos.angle) * off,
|
||
angle: pos.angle,
|
||
w: carLen,
|
||
h: CAR_WIDTH,
|
||
});
|
||
}
|
||
return segs;
|
||
}
|
||
|
||
// Midpoint of a route's strip (used for the rotated hit-area rectangle).
|
||
export function routeMidpoint(route) {
|
||
const A = CITIES[route.a];
|
||
const B = CITIES[route.b];
|
||
const off = route.doubleGroup ? (route.parallelSide === 0 ? -DOUBLE_OFFSET : DOUBLE_OFFSET) : 0;
|
||
const pts = route.curve ? bezierPolyline(A, B, route.curve) : [A, B];
|
||
const totalLen = polylineLength(pts);
|
||
const pos = pointAtDist(pts, totalLen / 2);
|
||
return {
|
||
x: pos.x - Math.sin(pos.angle) * off,
|
||
y: pos.y + Math.cos(pos.angle) * off,
|
||
angle: pos.angle,
|
||
length: totalLen - CITY_MARGIN * 2,
|
||
width: CAR_WIDTH + 8,
|
||
};
|
||
}
|
||
|
||
// ── Land silhouette (decorative backdrop) ──────────────────────────────────────
|
||
// A simplified US lower-48 + southern Canada outline, hand-traced clockwise in
|
||
// canvas space. Drawn as a filled polygon behind the routes and cities.
|
||
export const LAND_OUTLINE = [
|
||
// Pacific NW / BC coast
|
||
[108, 218], [148, 142], [295, 118],
|
||
// Northern border east across Canada
|
||
[500, 115], [640, 128], [790, 148], [908, 170],
|
||
// Northern Ontario above Great Lakes
|
||
[1015, 160], [1095, 165], [1205, 160],
|
||
// Quebec / Northeast approach
|
||
[1315, 192], [1408, 250],
|
||
// New England coast south
|
||
[1418, 288], [1372, 328], [1342, 365],
|
||
// Mid-Atlantic (NJ bulges right, then Chesapeake indent)
|
||
[1358, 402], [1322, 450], [1312, 482],
|
||
// Cape Hatteras juts right
|
||
[1342, 520],
|
||
// SE coast toward Florida
|
||
[1298, 568], [1258, 622], [1250, 662],
|
||
// Florida east coast (peninsula runs south)
|
||
[1265, 700], [1292, 755], [1298, 800],
|
||
// Florida tip — south of Miami (1235, 825)
|
||
[1278, 845], [1238, 868],
|
||
// Florida Gulf coast back north
|
||
[1195, 858], [1155, 842],
|
||
// Gulf Coast — panhandle, Alabama, Louisiana, Texas
|
||
[1082, 828], [1010, 818], [920, 796],
|
||
[832, 826], [768, 820], [685, 825],
|
||
// Texas coast south to Rio Grande delta
|
||
[600, 832], [565, 778],
|
||
// Mexico border west — dips below the LA–El Paso route, passes through El Paso (540, 705)
|
||
[540, 720], [488, 738], [418, 755], [340, 768], [250, 748],
|
||
// Pacific coast north — San Diego → LA → SF → Oregon → WA
|
||
[192, 718], [182, 665], [158, 618],
|
||
[108, 535], [90, 478], [112, 410], [128, 372],
|
||
[148, 332], [154, 282], [148, 240],
|
||
];
|
||
|
||
// Great Lakes region: Superior (Duluth→Sault Ste Marie), Michigan, Huron, Erie, Ontario (Toronto).
|
||
export const GREAT_LAKES = [
|
||
[728, 288], // west Superior near Duluth
|
||
[775, 232], // north shore Superior west
|
||
[868, 210], // north shore Superior central
|
||
[950, 228], // northeast Superior / Sault Ste Marie
|
||
[1010, 250], // Georgian Bay / Lake Huron NE
|
||
[1088, 278], // Lake Ontario west near Toronto
|
||
[1112, 332], // Lake Ontario south shore
|
||
[1048, 362], // Lake Erie / Niagara
|
||
[968, 378], // south lakes boundary
|
||
[918, 365], // south Michigan
|
||
[888, 320], // central Lake Michigan
|
||
[862, 282], // back toward Superior
|
||
];
|