Scoring & Penalties
8.1The score pool
Driving well is a negative-space system: you start each station with a life/score
pool and lose points for mistakes. The pool lives at score_status+0x0 (the game
status word, gameStatus+8 @ 0x6d3300); per-category accumulators sit
at fixed offsets behind it. Penalties call one function, subLifePoint(category,
severity) @ 0x1cbb94:
/* subLifePoint @ 0x1cbb94 — the single penalty entry point */
void subLifePoint(int category, int severity)
{
if (GameCondition & 0x1000000) return; // suppression gate
int profile = *(int *)(getDiaStatus() + 0x20); // 0 = normal, 1 = lecture
int pts;
switch (category) {
case 0: pts = (severity==0) ? T[0x29] : (severity==1) ? T[0x2a] : T[0x2b]; break;
case 1: pts = T[0x2c]; break;
case 2: pts = T[0x2d + severity]; break; // 4 severity tiers
case 3: pts = T[0x31]; break; // 4: overshoot …
/* …cases 4..8 at T[0x32..0x36], 9/10 at T[0x3a/0x3b],
11 (ATC overspeed) T[0x3e], 12 (ATS red) T[0x3f], 13/14 T[0x40/0x41] */
}
if (pts != 0) {
inScoresubj(category + 0x2a); // popup UI
acc = &score_status[8 + (category + 0x2a) * 4];
*acc = max(*acc + pts, -999); // per-category accumulator, floor −999
if (getGameMode() != 1 && profile == 0) { // normal profile only:
SePlay(0x3b); // penalty buzzer
score_status->life = max(life + pts, 0); // deduct from the pool, floor 0
}
}
}
subLifePoint @ 0x1cbb94. T
is ScoreTbl @ 0x429760: two 0x10c-byte profile rows of 67 int32s;
penalty values begin at row+0xa4. In lecture mode (profile 1) categories accumulate but the
pool is never drained.8.2The fifteen penalty categories
| # | Trigger function | Address | Condition (exact) |
|---|---|---|---|
| 0 | judgeStopLine | 0x1c5fc8 | Stop position vs stop-line windows (below); severities 0–2 by overrun depth |
| 1 | judgeLateTime | 0x1c6498 | arrival second vs schedule; tiered onsets |
| 2 | judgeOverSpeed | 0x1c66e8 | Speed > comLim (+1 km/h tolerance), consecutive-frame counter; severity bands |
| 3 | judgeAtcOverSpeed | 0x1c6820 | Speed > comAtc while ATC panel active |
| 4 | judgeGSensor | 0x1c5ee0 | smoothed jerk > threshold, with cooldown |
| 5 | judgeNoiseHorn | 0x1c6xxx | horn use in noise-restricted sections |
| 6 | judgeGameManner (rough control) | 0x1c6b58 | notch/brake churn > rough_control_change_limit within window |
| 7 | (G-sensor variant) | — | see BLOCK_F profile table |
| 8 | judgeGameManner (rough brake) | 0x1c6b58 | brake-change limit while running |
| 9 | judgeNoiseHorn | — | second horn path (just-pressed) |
| 10 | door/judgment family | — | doors open while moving etc. |
| 11 | judgeAtcOverSpeed (first hit) | 0x1c687a | subLifePoint(0xb,0) once per event |
| 12 | controlAts red-signal | 0x1bd584 | subLifePoint(0xc,0), atsHit=2 |
| 13/14 | event/limit-stop family | — | deadline and limit-stop violations |
8.3Stop-position judgment: the windows
The signature judgment. judgeStopLine(distStop) @ 0x1c5fc8 reads a
six-float window record per (profile, tier) from judgeTbl @ 0x425f60
(two 0x1dc-byte profiles; tier = dia_status+0x14, the diagram rank):
/* judgeStopLine @ 0x1c5fc8 — condensed */
float e = DAT_001c6230; // epsilon
record = judgeTbl.profile[profile].stop_window[tier]; // 6 floats
if (distStop >= record[0] + e) { StopTrainLect_Check10m(); return; } // stopped >10 m short
if (GameCondition & 0x200) return; // game-over already: skip
if (getGameMode() == 2) return; // conductor mode: skip
if (GameCondition << 0x12 < 0) { subLifePoint(0, 2); return; } // severe-overrun override
if (distStop > overrun_threshold[tier][0] - e) { // inside the band:
if (score_status[0xe8] == 0) { // not already disqualified
if (|distStop| < record[5] + e) { // PERFECT band
outKiraStar(0); outKiraStar(1); // sparkle particles
inKiraStar(0,5,0x5a); inKiraStar(1,5,0x5a);
PcmPlay(0x8a4, 0); // "excellent" jingle
setBonusScore(4); // +4 bonus
entryStopLecture(0x66);
return;
}
/* bands record[1..4]: bonus 0..3 (「ようし/いいぞ/…] grades) */
setBonusScore(band - 1);
entryStopLecture(band + 0x61);
return;
}
subLifePoint(0, ?); entryStopLecture(0x6c); // disqualified path
return;
}
/* overrun: three depth bands → subLifePoint(0, 1..3) + entryStopLecture(0x69) */
overrun_threshold rows
(profile+0x78+tier×0xc): minor/medium/severe = (−5/−10/−20), (−5/−10/−20), (−4/−10/−20),
(−3/−10/−20), (−2/−10/−20) meters for tiers 0–4.The judgment, live
Captured from the original binary during this report's sessions; the train crossed the stop line and the judge fired:
judgeStopLine verdict popup: 合格範囲!! (“within
passing range”) with the distance readout (229 cm). The chain counter (top right) starts
climbing: 500 points already banked.
eventStopLine → judgeStopLine, exactly as the state machines dictate.8.4Bonuses
setBonusScore(type) awards from a 42-entry table (in ScoreTbl,
profile rows); each award can chain (chain popup), spawn the kira-star particles,
and is doubled while the tutorial score item is active
(checkItemWScore()). Notable awards:
| Type | Award for | Where |
|---|---|---|
| 0–3 | Stop bands (good/excellent…) | judgeStopLine |
| 4 | Perfect stop | judgeStopLine |
| 39 | Coupling at 2–6 km/h | joinMapFunc |
| 41 | Coupling distance 2.75–3.26 m | joinMapFunc |
| 0x28 | Horn at the right moment | dead code (unreachable, Chapter 6) |
8.5Grades and the results screen
At each station, map mode 3 (shortReplayMapFunc) shows the replay and the
grade. The final grade comes from procGradeStg (@ 0x1c2238 family):
it compares the station's remaining pool against profile thresholds, then
setClearData folds the weakest link across the route into the
diagram's clear rank (saved into the 76-byte DGF_PlayerDiagramRecord, Chapter 9).
Score < 1 at the station gate = failure → the continue flow (Chapter 10).
ScoreTbl row 0 = normal play; row 1 = lecture/tutorial (pool never drains; penalties accumulate per category for the instructor's feedback instead). The profile byte is
dia_status+0x20 and also gates the item system (only lecture profiles have items).