08 · Rules & Flow

Scoring & Penalties

Fifteen penalty categories, forty-two bonus types, two difficulty profiles, and one 268-byte table that defines how harshly the game judges you. Every threshold below is read from the binary.

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
    }
  }
}
Exhibit: 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 functionAddressCondition (exact)
0judgeStopLine0x1c5fc8Stop position vs stop-line windows (below); severities 0–2 by overrun depth
1judgeLateTime0x1c6498arrival second vs schedule; tiered onsets
2judgeOverSpeed0x1c66e8Speed > comLim (+1 km/h tolerance), consecutive-frame counter; severity bands
3judgeAtcOverSpeed0x1c6820Speed > comAtc while ATC panel active
4judgeGSensor0x1c5ee0smoothed jerk > threshold, with cooldown
5judgeNoiseHorn0x1c6xxxhorn use in noise-restricted sections
6judgeGameManner (rough control)0x1c6b58notch/brake churn > rough_control_change_limit within window
7(G-sensor variant)—see BLOCK_F profile table
8judgeGameManner (rough brake)0x1c6b58brake-change limit while running
9judgeNoiseHorn—second horn path (just-pressed)
10door/judgment family—doors open while moving etc.
11judgeAtcOverSpeed (first hit)0x1c687asubLifePoint(0xb,0) once per event
12controlAts red-signal0x1bd584subLifePoint(0xc,0), atsHit=2
13/14event/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) */
Exhibit: the stop judgment. 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.
Stop position judgment windows
The stop windows. Everything within ±record[5] is a perfect stop (+4); bands 1–3 grade the approach; beyond the overrun thresholds penalties escalate; >10 m short is its own judgment.

The judgment, live

Captured from the original binary during this report's sessions; the train crossed the stop line and the judge fired:

Stop judgment popup: passing range, 229 cm
judgeStopLine verdict popup: 合格範囲!! (“within passing range”) with the distance readout (229 cm). The chain counter (top right) starts climbing: 500 points already banked.
Four stacked stop position GREAT popups
The chain mechanic live: four stacked 停止位置 GREAT!! popups (“stop position GREAT”), 1,000 points at chain 4. Each re-judgment as the train creeps forward re-fires the award and extends the chain.
Chain popup showing 4 chains text
The chain summary readout: “4 chains” over the final 98 cm distance, with the score 1,000 held at chain 4 while the conductor-door phase begins.
Observed live: parking short of the line A train stopped 13 m short of the stop line never reaches the judgment; the stop-line event hasn't crossed, so no doors, no verdict, no result screen (the game waits indefinitely; see Chapter 10). Creeping the last meters across the line is what fires 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:

TypeAward forWhere
0–3Stop bands (good/excellent…)judgeStopLine
4Perfect stopjudgeStopLine
39Coupling at 2–6 km/hjoinMapFunc
41Coupling distance 2.75–3.26 mjoinMapFunc
0x28Horn at the right momentdead 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).

Two profiles 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).
← 07 · Train Physics