A benchmark in which language models design roller coasters that RollerCoaster Tycoon 2 builds, tests, and rates.
I'll start by checking my memory notes and getting the coaster tools loaded.
{"max_results":10,"query":"select:mcp__coaster__new_ride,mcp__coaster__place_pieces,mcp__coaster__finish_and_test,mcp__coaster__get_state,mcp__coaster__piece_geometry,mcp__coaster__valid_next_pieces"}
{"file_path":"/home/sandbox/.claude/projects/-workspace/memory/rct2-twister-rating-levers.md"}1 --- 2 name: rct2-twister-rating-levers 3 description: "What actually drives excitement for RCT2 twister coasters (ride_type 51) in the coaster-MCP benchmark, derived from engine source" 4 metadata: 5 node_type: memory 6 type: project 7 originSessionId: d2a054e0-f0a0-4d19-ba47-134eb86a0e0f 8 modified: 2026-07-25T03:45:29.042Z 9 --- 10 11 Excitement formula for twister RC lives in `/tmp/openrct2-src/src/openrct2/ride/rtd/coaster/TwisterRollerCoaster.h` 12 (`.RatingsData`) plus `ride/RideRatings.cpp`. Base excitement 3.50. Measured levers, biggest first: 13 14 - **Inversions**: `min(inv,6) * 27.33` pre-multiplier, x0.408 => ~0.67 excitement at 6 inversions. 15 Intensity cost only ~0.27 each. Always go to exactly 6. 16 - **Sharp 60-degree crests** (round-2 win, +0.15): geometry-preserving swaps that raise negative G 17 from -0.85 to -2.43 (excitement clamps at -2.50). Drop-in replacements, same tiles AND same dz: 18 - 6 tiles dz0: `flat_to_up_25,up_25,up_25_to_flat,flat_to_down_25,down_25,down_25_to_flat` 19 -> `flat_to_up_25,up_25_to_up_60,up_60_to_flat,flat_to_down_60,down_60_to_down_25,down_25_to_flat` 20 - 4 tiles dz0: `flat_to_up_25,up_25_to_flat,flat_to_d… (3533 chars)
{"file_path":"/home/sandbox/.claude/projects/-workspace/memory/rct2-proximity-bonus-plan.md"}1 --- 2 name: rct2-proximity-bonus-plan 3 description: RCT2 proximity excitement bonuses - measured results (SURFACE_TOUCH is a dud) and the still-untried vertical-loop crossing 4 metadata: 5 node_type: memory 6 type: project 7 originSessionId: 7f88d506-234c-4c0f-ba41-d2e59006411e 8 modified: 2026-07-25T03:45:17.947Z 9 --- 10 11 `BonusProximity` in the twister RatingsData has multiplier 20130/65536 = **x0.307**, applied to 12 `ride_ratings_get_proximity_score` (RideRatings.cpp ~line 1379). 13 14 ## MEASURED (round 3): SURFACE_TOUCH is NOT worth chasing 15 `PROXIMITY_SURFACE_TOUCH` = helper_1(x, cap 70, mult 0x01B6DB=1.714) -> 120 raw at cap = +0.37 in theory. 16 Set at RideRatings.cpp:719 when a track element's `getBaseZ() == surface baseZ` (track sitting on ground). 17 Round 3 appended a ~35-tile ground-level serpentine (right_turn_5 + 10 s_bends + flats, all z=112), 18 adding ~60 ground elements — almost certainly hitting the 70 cap. **Excitement moved 7.09 -> 7.12 only.** 19 Intensity rose 8.60 -> 9.08 (lateral G from the turns) which ate part of the gain. Net: not worth the 20 pieces. Either the score was already near cap, or the intensity cost offsets it. Don't redo… (3022 chars)
Strong notes from prior rounds. Plan: replay the round-3 layout (7.12) to bank a score immediately, then chase the untried vertical-loop proximity bonus.
{"dir":0,"ride_type":51,"x":60,"y":60}{"cursor":{"bank":0,"dir":0,"slope":0,"x":60,"y":60,"z":112},"ride_id":0}
{"pieces":["begin_station","middle_station","middle_station","end_station",{"chain":true,"piece":"flat_to_up_25"},{"chain":true,"piece":"up_25"},{"chain":true,"piece":"up_25"},{"chain":true,"piece":"up_25"},{"chain":true,"piece":"up_25"},{"chain":true,"piece":"up_25"},{"chain":true,"piece":"up_25"},{"chain":true,"piece":"up_25"},{"chain":true,"piece":"up_25"},{"chain":true,"piece":"up_25"},{"chain":true,"piece":"up_25"},{"chain":true,"piece":"up_25"},{"chain":true,"piece":"up_25"},{"chain":true,"piece":"up_25"},{"chain":true,"piece":"up_25_to_flat"},"flat_to_down_25","down_25_to_down_60","down_60","down_60","down_60_to_down_25","down_25_to_flat","flat_to_up_25","left_vertical_loop","down_25_to_flat","flat_to_up_25","up_25","up_25","up_25_to_flat","left_corkscrew_up","left_corkscrew_down","flat_to_down_25","down_25","down_25_to_flat","flat_to_up_60","up_60_to_flat","flat_to_down_60","down_60_to_flat","flat_to_up_25","right_vertical_loop","down_25_to_flat","flat_to_up_25","up_25_to_up_60","up_60_to_flat","flat_to_down_60","down_60_to_down_25","down_25_to_flat","right_corkscrew_up","right_corkscrew_down","flat_to_up_25","up_25_to_up_60","up_60_to_flat","flat_to_down_60","down_60_to_do… (1923 chars){"circuit_closed":true,"cursor":{"bank":0,"dir":0,"slope":0,"x":60,"y":60,"z":112},"placed_this_call":104,"total_placed":104}
Replay closed the circuit exactly. Banking the score now.
{"bounds":{"max_tile_x":64,"max_tile_y":60,"max_z":360,"min_tile_x":26,"min_tile_y":31,"min_z":112},"placed_pieces":["begin_station","middle_station","middle_station","end_station",{"chain":true,"t":"flat_to_up_25"},{"chain":true,"t":"up_25"},{"chain":true,"t":"up_25"},{"chain":true,"t":"up_25"},{"chain":true,"t":"up_25"},{"chain":true,"t":"up_25"},{"chain":true,"t":"up_25"},{"chain":true,"t":"up_25"},{"chain":true,"t":"up_25"},{"chain":true,"t":"up_25"},{"chain":true,"t":"up_25"},{"chain":true,"t":"up_25"},{"chain":true,"t":"up_25"},{"chain":true,"t":"up_25"},{"chain":true,"t":"up_25_to_flat"},"flat_to_down_25","down_25_to_down_60","down_60","down_60","down_60_to_down_25","down_25_to_flat","flat_to_up_25","left_vertical_loop","down_25_to_flat","flat_to_up_25","up_25","up_25","up_25_to_flat","left_corkscrew_up","left_corkscrew_down","flat_to_down_25","down_25","down_25_to_flat","flat_to_up_60","up_60_to_flat","flat_to_down_60","down_60_to_flat","flat_to_up_25","right_vertical_loop","down_25_to_flat","flat_to_up_25","up_25_to_up_60","up_60_to_flat","flat_to_down_60","down_60_to_down_25","down_25_to_flat","right_corkscrew_up","right_corkscrew_down","flat_to_up_25","up_25_to_up_60","u… (2652 chars)
7.12 banked. Now let me confirm the exact loop-proximity condition in the engine source before building for it.
{"command":"grep -n \"close_proximity_loops_helper\" -A 45 /tmp/openrct2-src/src/openrct2/ride/RideRatings.cpp | head -90","description":"Read loop proximity scoring helper"}634:static void ride_ratings_score_close_proximity_loops_helper(RideRating::UpdateState& state, const CoordsXYE& coordsElement)
635-{
636- TileElement* tileElement = MapGetFirstElementAt(coordsElement);
637- if (tileElement == nullptr)
638- return;
639- do
640- {
641- if (tileElement->isGhost())
642- continue;
643-
644- auto type = tileElement->getType();
645- if (type == TileElementType::path)
646- {
647- int32_t zDiff = static_cast<int32_t>(tileElement->baseHeight)
648- - static_cast<int32_t>(coordsElement.element->baseHeight);
649- if (zDiff >= 0 && zDiff <= 16)
650- {
651- proximity_score_increment(state, PROXIMITY_PATH_THROUGH_VERTICAL_LOOP);
652- }
653- }
654- else if (type == TileElementType::track)
655- {
656- bool elementsAreAt90DegAngle = ((tileElement->getDirection() ^ coordsElement.element->getDirection()) & 1) != 0;
657- if (elementsAreAt90DegAngle)
658- {
659- int32_t zDiff = static_cast<int32_t>(tileElement->baseHeight)
660- - static_cast<int32_t>(coo… (3633 chars)
{"command":"sed -n 675,692p /tmp/openrct2-src/src/openrct2/ride/RideRatings.cpp; echo ---; grep -n \"PROXIMITY_TRACK_THROUGH_VERTICAL_LOOP\\|PROXIMITY_INTERSECTING_VERTICAL_LOOP\\|PROXIMITY_OWN_STATION_TOUCH_ABOVE\" /tmp/openrct2-src/src/openrct2/ride/RideRatings.cpp","description":"Check loop proximity trigger condition and score usage"}/**
*
* rct2: 0x006B62DA
*/
static void ride_ratings_score_close_proximity_loops(RideRating::UpdateState& state, TileElement* inputTileElement)
{
auto trackType = inputTileElement->asTrack()->GetTrackType();
if (trackType == TrackElemType::leftVerticalLoop || trackType == TrackElemType::rightVerticalLoop)
{
ride_ratings_score_close_proximity_loops_helper(state, { state.Proximity, inputTileElement });
int32_t direction = inputTileElement->getDirection();
ride_ratings_score_close_proximity_loops_helper(
state, { CoordsXY{ state.Proximity } + CoordsDirectionDelta[direction], inputTileElement });
}
}
/**
---
74: PROXIMITY_OWN_STATION_TOUCH_ABOVE, // 0x0138B5B8
76: PROXIMITY_TRACK_THROUGH_VERTICAL_LOOP, // 0x0138B5BC
78: PROXIMITY_INTERSECTING_VERTICAL_LOOP, // 0x0138B5C0
663: proximity_score_increment(state, PROXIMITY_TRACK_THROUGH_VERTICAL_LOOP);
667: proximity_score_increment(state, PROXIMITY_INTERSECTING_VERTICAL_LOOP);
821: proximity_score_increment(state, PROXIMITY_OWN_STATION_TOUCH_ABOVE);
841: proximity_score_i… (1559 chars)
{"command":"sed -n 795,850p /tmp/openrct2-src/src/openrct2/ride/RideRatings.cpp","description":"Read station touch-above proximity condition"}{
if (tileElement->clearanceHeight + 10 >= inputTileElement->baseHeight)
{
proximity_score_increment(state, PROXIMITY_FOREIGN_TRACK_CLOSE_ABOVE);
}
}
if (inputTileElement->clearanceHeight == tileElement->baseHeight)
{
proximity_score_increment(state, PROXIMITY_FOREIGN_TRACK_TOUCH_ABOVE);
}
if (inputTileElement->clearanceHeight + 2 == tileElement->baseHeight)
{
if (static_cast<uint8_t>(inputTileElement->clearanceHeight + 10) >= tileElement->baseHeight)
{
proximity_score_increment(state, PROXIMITY_FOREIGN_TRACK_CLOSE_ABOVE);
}
}
}
else
{
bool isStation = tileElement->asTrack()->IsStation();
if (tileElement->clearanceHeight == inputTileElement->baseHeight)
{
proximity_score_increment(state, PROXIMI… (2827 chars)
{"command":"sed -n 1370,1415p /tmp/openrct2-src/src/openrct2/ride/RideRatings.cpp; echo \"=== helpers ===\"; sed -n 1330,1370p /tmp/openrct2-src/src/openrct2/ride/RideRatings.cpp","description":"Read proximity score aggregation table"}static uint32_t get_proximity_score_helper_3(uint16_t x, uint16_t resultIfNotZero)
{
return x == 0 ? 0 : resultIfNotZero;
}
/**
*
* rct2: 0x0065E277
*/
static uint32_t ride_ratings_get_proximity_score(RideRating::UpdateState& state)
{
const uint16_t* scores = state.ProximityScores;
uint32_t result = 0;
result += get_proximity_score_helper_1(scores[PROXIMITY_WATER_OVER], 60, 0x00AAAA);
result += get_proximity_score_helper_1(scores[PROXIMITY_WATER_TOUCH], 22, 0x0245D1);
result += get_proximity_score_helper_1(scores[PROXIMITY_WATER_LOW], 10, 0x020000);
result += get_proximity_score_helper_1(scores[PROXIMITY_WATER_HIGH], 40, 0x00A000);
result += get_proximity_score_helper_1(scores[PROXIMITY_SURFACE_TOUCH], 70, 0x01B6DB);
result += get_proximity_score_helper_1(scores[PROXIMITY_QUEUE_PATH_OVER] + 8, 12, 0x064000);
result += get_proximity_score_helper_3(scores[PROXIMITY_QUEUE_PATH_TOUCH_ABOVE], 40);
result += get_proximity_score_helper_3(scores[PROXIMITY_QUEUE_PATH_TOUCH_UNDER], 45);
result += get_proximity_score_helper_2(scores[PROXIMITY_PATH_TOUCH_ABOVE], 10, 20, 0x03C000);
result += get_proximity_score_helper_2(scores[PROXIMITY_… (4363 chars)
Big find: loop-proximity bonuses total up to **+0.86 excitement** (through-loop 0.37, intersecting 0.31, through 0.18). And a ground-level loop oriented along **y** sitting *under the lift hill* (which runs along x) satisfies the 90°-crossing + [0,128z] condition without any altitude gymnastics. Let me build that.
{"dir":0,"ride_type":51,"x":60,"y":60}{"cursor":{"bank":0,"dir":0,"slope":0,"x":60,"y":60,"z":112},"ride_id":0}
{"pieces":["begin_station","middle_station","middle_station","end_station",{"chain":true,"piece":"flat_to_up_25"},{"chain":true,"piece":"up_25"},{"chain":true,"piece":"up_25"},{"chain":true,"piece":"up_25"},{"chain":true,"piece":"up_25"},{"chain":true,"piece":"up_25"},{"chain":true,"piece":"up_25"},{"chain":true,"piece":"up_25"},{"chain":true,"piece":"up_25"},{"chain":true,"piece":"up_25"},{"chain":true,"piece":"up_25"},{"chain":true,"piece":"up_25"},{"chain":true,"piece":"up_25"},{"chain":true,"piece":"up_25"},{"chain":true,"piece":"up_25_to_flat"},"flat_to_down_25","down_25_to_down_60","down_60","down_60","down_60_to_down_25","down_25_to_flat"]}{"circuit_closed":false,"cursor":{"bank":0,"dir":0,"slope":0,"x":35,"y":60,"z":128},"placed_this_call":25,"total_placed":25}
Lift top z=256 at x=41; post-drop ground track at z=128. Now I need a U-turn back east to place a y-oriented loop under the lift. Checking turn geometry.
{"dir":0}{"deltas":[{"bank_out":0,"dir_out":0,"dx_tiles":-1,"dy_tiles":0,"dz":0,"piece":"flat","slope_out":0},{"bank_out":0,"dir_out":0,"dx_tiles":-1,"dy_tiles":0,"dz":0,"piece":"end_station","slope_out":0},{"bank_out":0,"dir_out":0,"dx_tiles":-1,"dy_tiles":0,"dz":0,"piece":"begin_station","slope_out":0},{"bank_out":0,"dir_out":0,"dx_tiles":-1,"dy_tiles":0,"dz":0,"piece":"middle_station","slope_out":0},{"bank_out":0,"dir_out":0,"dx_tiles":-1,"dy_tiles":0,"dz":16,"piece":"up_25","slope_out":2},{"bank_out":0,"dir_out":0,"dx_tiles":-1,"dy_tiles":0,"dz":64,"piece":"up_60","slope_out":4},{"bank_out":0,"dir_out":0,"dx_tiles":-1,"dy_tiles":0,"dz":8,"piece":"flat_to_up_25","slope_out":2},{"bank_out":0,"dir_out":0,"dx_tiles":-1,"dy_tiles":0,"dz":32,"piece":"up_25_to_up_60","slope_out":4},{"bank_out":0,"dir_out":0,"dx_tiles":-1,"dy_tiles":0,"dz":32,"piece":"up_60_to_up_25","slope_out":2},{"bank_out":0,"dir_out":0,"dx_tiles":-1,"dy_tiles":0,"dz":8,"piece":"up_25_to_flat","slope_out":0},{"bank_out":0,"dir_out":0,"dx_tiles":-1,"dy_tiles":0,"dz":-16,"piece":"down_25","slope_out":6},{"bank_out":0,"dir_out":0,"dx_tiles":-1,"dy_tiles":0,"dz":-64,"piece":"down_60","slope_out":8},{"bank_out":0,"dir_out":0,"dx… (7397 chars)
{"pieces":["right_turn_5","right_turn_5","flat","flat","flat","flat","flat","flat"]}{"circuit_closed":false,"cursor":{"bank":0,"dir":2,"slope":0,"x":42,"y":65,"z":128},"placed_this_call":8,"total_placed":33}
{"pieces":["left_turn_3","flat_to_up_25","left_vertical_loop","down_25_to_flat"]}{"circuit_closed":false,"cursor":{"bank":0,"dir":1,"slope":0,"x":42,"y":71,"z":128},"placed_this_call":4,"total_placed":37}
Out of runway to close this circuit — 7.12 stays banked. Let me record the findings for the next rounds while I still have time.
{"content":"---\nname: rct2-proximity-bonus-plan\ndescription: RCT2 proximity excitement bonuses - full scoring table, the loop-under-lift recipe worth ~+0.86, and the turn-direction rule\nmetadata:\n node_type: memory\n type: project\n---\n\n`BonusProximity` multiplier is 20130/65536 = **x0.307** on `ride_ratings_get_proximity_score`\n(RideRatings.cpp ~1379). Raw score x 0.307 = rating units; 100 rating units = 1.00 excitement.\n\n## Full payout table (RideRatings.cpp:1382-1408), best first\n| Bonus | helper | hits to cap | excitement |\n|---|---|---|---|\n| TRACK_THROUGH_VERTICAL_LOOP | h2(+4, cap6, x20) | **2** | **+0.37** |\n| INTERSECTING_VERTICAL_LOOP | h3(100) | **1** | **+0.31** |\n| THROUGH_VERTICAL_LOOP | h2(+4, cap6, x10) | 2 | +0.18 |\n| OWN_STATION_TOUCH_ABOVE | h3(55) | 1 | +0.17 |\n| OWN_TRACK_TOUCH_ABOVE | h2(+10, cap15, x3.33) | 5 | +0.15 |\n| OWN_TRACK_CLOSE_ABOVE | h1(cap5, x6) | 5 | +0.09 |\n| OWN_STATION_CLOSE_ABOVE | h3(25) | 1 | +0.08 |\n| SURFACE_TOUCH | h1(cap70, x1.71) | 70 | +0.37 but MEASURED DUD (see below) |\n\nLoop bonuses stack: **0.37+0.31+0.18 = +0.86** from one well-placed crossing.\n\n## MEASURED (round 3): SURFACE_TOUCH is NOT worth chasing\n~… (3569 chars)The file /home/sandbox/.claude/projects/-workspace/memory/rct2-proximity-bonus-plan.md has been updated successfully.
{"bounds":{"max_tile_x":64,"max_tile_y":60,"max_z":360,"min_tile_x":26,"min_tile_y":31,"min_z":112},"placed_pieces":["begin_station","middle_station","middle_station","end_station",{"chain":true,"t":"flat_to_up_25"},{"chain":true,"t":"up_25"},{"chain":true,"t":"up_25"},{"chain":true,"t":"up_25"},{"chain":true,"t":"up_25"},{"chain":true,"t":"up_25"},{"chain":true,"t":"up_25"},{"chain":true,"t":"up_25"},{"chain":true,"t":"up_25"},{"chain":true,"t":"up_25"},{"chain":true,"t":"up_25"},{"chain":true,"t":"up_25"},{"chain":true,"t":"up_25"},{"chain":true,"t":"up_25"},{"chain":true,"t":"up_25_to_flat"},"flat_to_down_25","down_25_to_down_60","down_60","down_60","down_60_to_down_25","down_25_to_flat","flat_to_up_25","left_vertical_loop","down_25_to_flat","flat_to_up_25","up_25","up_25","up_25_to_flat","left_corkscrew_up","left_corkscrew_down","flat_to_down_25","down_25","down_25_to_flat","flat_to_up_60","up_60_to_flat","flat_to_down_60","down_60_to_flat","flat_to_up_25","right_vertical_loop","down_25_to_flat","flat_to_up_25","up_25_to_up_60","up_60_to_flat","flat_to_down_60","down_60_to_down_25","down_25_to_flat","right_corkscrew_up","right_corkscrew_down","flat_to_up_25","up_25_to_up_60","u… (2652 chars)
Round 4 result confirmed banked at **7.12**. **What happened:** I replayed the round-3 layout immediately to bank 7.12, then spent the rest of the budget on the untried proximity lever. Reading `RideRatings.cpp` paid off — I found the loop bonuses stack to **+0.86 excitement** (through-loop 0.37 + intersecting 0.31 + through 0.18), and worked out that a ground-level vertical loop oriented along **y** placed under the existing lift hill (which runs along x) satisfies the 90°-crossing and [0,128z] conditions with no altitude work at all. The build attempt failed on a dumb error, not a bad idea: I assumed `left_turn_3` from dir 2 would face me -y, but left is `dir-1`, so I ended up heading +y and put the loop at y=67-71, far from the lift at y=60. I ran out of clock before I could re-route and close the circuit, so the experimental build was never tested and 7.12 stands. I recorded the full payout table, the verified skeleton geometry (lift top z=256 at x=41, post-drop ground at z=128, loop tops at 232 so the crossing must be at x=42-43), and the turn-direction rule to memory so rounds 5-6 can execute this in a few minutes instead of rediscovering it. **Best coaster this round: the round-3 skeleton — 14-tile chain lift, 60° plunge, 3 vertical loops + 3 corkscrew pairs, ground-level serpentine return — 7.12 excitement / 9.08 intensity, similarity 0.338.**