目指せパチプロ パチ夫くん (FC) 攻略/解析

パスワード

本作は 16 文字のパスワードを用いてゲーム状態のセーブ/ロードを行う。ゲーム内で生成される正規のパスワードの場合、64 種の文字が使われるため、1 文字あたりの情報量は 6 ビットで、全体の情報量は 96 ビットとなる。ただし、正規でない文字も考慮するとパスワードの情報量は 96 ビットよりわずかに多くなる(後述)。

パスワードは RAM $56-$65 に置かれる (この領域はセーブ/ロード時の中間バッファや生年月日バッファとしても使われる)。ゲーム状態とパスワードの相互変換は以下のルーチンで行われる:

アドレス内容
$EECCゲーム状態をパスワードにセーブする。
$F028ゲーム状態をパスワードからロードすることを試みる。

なお、一部のパスワードはデバッグメニューを起動する裏技パスワードとして扱われる(後述)。

筆者作のパスワードライブラリ pachio1_password も適宜参照されたい (パスワードのセーブ/ロードおよび裏技パスワード/最短パスワードの探索を実装している)。

ゲーム状態

パスワードには以下の情報が記録される ("bits" はパスワード内での情報量):

識別子アドレスbits内容
fortune $4A 6 運命数 (通常は 1〜6)。最後の店 (じょい) をクリアした際の性格分析メッセージに影響する。
score $4B-$4D 24 得点 (6 桁 packed BCD, リトルエンディアン)。エクステンドや成績評価に影響する。
total_win $4E 8 総打ち止め回数。成績評価に影響する。
stage_win $4F 6 現在の面における打ち止め回数。規定回数に達すると面クリア可能となる。面クリア時に総打ち止め回数に加算される。
gameover $50 7 ゲームオーバーになってコンティニューした回数。成績評価に影響する。
stage $51 3 現在の面 (通常は 1〜7)。
hand_ball $52-$54 24 持ち玉 (6 桁 packed BCD, リトルエンディアン)。
life $55 3 残機 (プレイ中の自機を含まない。通常は 0〜5)。
key 暗号鍵。セーブ時は 0〜0x3F の乱数となるが、ロード時は正規でない文字を入力することでそれ以外の値にもできる。よって、情報量は 6 ビットよりわずかに多くなる。
check 9 チェックコード。これが正しくないとロードが失敗する。

パスワード文字

パスワードとして入力可能な文字は全部で 90 種あるが、正規のパスワード内に現れる文字は 64 種のみである。これらを「正規文字」、残りの 26 種を「代替文字」と呼ぶ。代替文字は基本的には対応する正規文字と同等に扱われるが、パスワードの末尾に現れた場合のみ扱いが異なる(後述)。

パスワード文字コード表 (RAM $56-$65 上の値) を以下に示す。代替文字については斜体で表示し、対応する正規文字を付記している。なお、"SP" は空白を意味する:

x0x1x2x3x4x5x6x7x8x9xAxBxCxDxExF
2xSP(わ)
3x(ぶ)(べ)(ぼ)(ぱ)(ぴ)(ぷ)(ぺ)(ぽ)(あ)(い)
4x
5x
6x(で)(ど)
7x
8x
9x
Ax(ろ)(わ)(が)(ぎ)(げ)(ぞ)(だ)(で)(ど)
Bx(か)
Cx(の)(は)
Dx(が)

パスワードへのセーブ処理

パスワードに記録すべき情報を格納したバイト列を「セーブデータ」と呼ぶ。セーブデータは形式によらず 16 バイトで、RAM $56-$65 上で in-place に読み書きされる。

ゲーム状態は以下のステップを経てパスワードに変換される:

ゲーム状態から 8-bit 形式セーブデータへの変換

まずゼロクリアされた 16 バイトの配列 bytes を用意する。今後、これをセーブデータ(形式不問)操作用バッファとして用いる。

事前に乱数を用いて 6 ビットの暗号鍵を生成しておく。そして、bytes 内にゲーム状態および暗号鍵を以下のように詰め込む (ビット列は MSB-first で各バイトに詰め込まれる。たとえば、fortunebytes[1] の bit2-7 に入る。なお、空欄は全て 0 である)。ここで、check_lo はチェックコード bit0-7, h はチェックコード bit8 の格納予定地だが、この時点ではまだ 0 で埋められている:

 0                   1                   2
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   check_lo    |  fortune  |   score[0]    |   score[1]    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

 3                   4                   5                   6
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   score[2]    |   total_win   | stage_win |  gameover   |stage|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

 6               7                   8                   9
 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| hand_ball[0]  | hand_ball[1]  | hand_ball[2]  |life |h|           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

         1                   1                   1
 9       0                   1                   2
 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                   |    key    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

次に 9 ビットのチェックコードを求め、結果を check_lo および h の部分に格納する。チェックコード計算処理はたとえば Rust では以下のように書ける (暗号鍵に依存することに注意。なお、過程全体を通じて check の下位 9 ビット以外は無視してよい):

fn calc_check(bytes: [u8; 16]) -> u16 {
    let key = bytes[15];

    let mut check: u16 = 0;

    for b in bytes.into_iter().rev() {
        check = check.wrapping_add(u16::from(key));
        check ^= u16::from(b);
        check <<= 1;
        if check & (1 << 8) == 0 {
            check ^= 0x79;
        }
    }

    check & 0x1FF
}

最後に bytes に対して簡単なスクランブル処理を施し、その結果が 8-bit 形式セーブデータとなる。スクランブル処理は bytes[1..=10] の各バイトに bytes[0] (check_lo) を加算するものである (間接的に暗号鍵に依存していることに注意)。たとえば Rust では以下のように書ける:

fn scramble8(bytes: &mut [u8; 16]) {
    let check_lo = bytes[0];

    for b in bytes[1..=10].iter_mut() {
        *b = b.wrapping_add(check_lo);
    }
}

8-bit 形式セーブデータから 6-bit 形式セーブデータへの変換

8-bit 形式セーブデータにおいて、bytes[0..=11] の各バイトは上位 2 ビットに何らかの情報を持っている。これらを以下のように bytes[11..=14] へ移動し、bytes 内の全てのバイトの情報量を 6 ビットにする。ここで、hi2(x)x >> 6 を意味する:

次に、bytes に対して暗号鍵に依存するスクランブル処理を施し、その結果が 6-bit 形式セーブデータとなる。スクランブル処理は bytes[0..=14] の各バイトに対して暗号鍵の下位 2 ビットに依存するバイト列を XOR するものである。たとえば Rust では以下のように書ける:

fn scramble6(bytes: &mut [u8; 16]) {
    const TABLE: [[u8; 15]; 4] = [
        [0x16, 0x03, 0x1A, 0x17, 0x09, 0x12, 0x1A, 0x17, 0x10, 0x03, 0x1A, 0x04, 0x1A, 0x01, 0x09],
        [0x32, 0x1B, 0x2D, 0x12, 0x32, 0x1B, 0x2D, 0x12, 0x31, 0x1B, 0x2D, 0x22, 0x00, 0x2B, 0x2D],
        [0x0D, 0x12, 0x1B, 0x1B, 0x31, 0x2D, 0x19, 0x01, 0x15, 0x08, 0x1F, 0x15, 0x28, 0x32, 0x2E],
        [0x07, 0x09, 0x07, 0x0A, 0x0D, 0x34, 0x0D, 0x14, 0x27, 0x28, 0x2C, 0x2F, 0x3E, 0x36, 0x1F],
    ];

    let key = bytes[15];
    let xor_array = &TABLE[usize::from(key & 3)];

    for (b, &xor) in bytes[..15].iter_mut().zip(xor_array) {
        *b = (*b ^ xor) & 0x3F;
    }
}

なお、このスクランブル処理は対称な操作 (2 回行うと元に戻る) なので、逆変換時も全く同じことを行えばよい。

6-bit 形式セーブデータからパスワードへのエンコード

6-bit 形式セーブデータ内の各バイト (上位 2 ビットは 0 とする) を以下の規則で正規文字に変換したものがパスワードとなる:

0x00..=0x03 => 0xB1..=0xB4 ('あ'..='え')
0x04..=0x14 => 0xB6..=0xC6 ('か'..='に')
0x15..=0x28 => 0xC9..=0xDC ('の'..='わ')
0x29..=0x33 => 0x61..=0x6B ('が'..='だ')
0x34..=0x3F => 0x6E..=0x79 ('で'..='ぽ')

パスワードからのロード処理

ロード処理は基本的にはセーブ処理の逆を行うだけである。つまり、パスワードは以下のステップを経てゲーム状態に変換される:

パスワードから 6-bit 形式セーブデータへのデコード

基本的にはセーブ時の処理の逆を行うだけだが、ロード時は代替文字が現れうることに注意を要する。

代替文字はパスワードの末尾以外では対応する正規文字と等価に扱われ(文字コード表を参照)、パスワードの末尾においては以下の規則でデコードされる (表中にないものは対応する正規文字と等価に扱う):

文字バイト
0x67
0x68
0x69
0x6A
0x6C
0x72
0x73
0x74
0x75
(空白)0xE8
0xF8
0xF9
0xFA
0xFB
0xFC
0xFD
0xFE
0xFF

セーブデータの末尾バイトは暗号鍵なので、代替文字を用いると 0〜0x3F 以外の暗号鍵も指定可能ということになる。そのため、パスワード全体の情報量は 96 ビットよりわずかに多くなる。

6-bit 形式セーブデータから 8-bit 形式セーブデータへの変換

セーブ時の処理の逆を行うだけである。

8-bit 形式セーブデータからゲーム状態への変換

基本的にはセーブ時の処理の逆を行うだけである。チェックコードが正しくない場合、不正なパスワードとみなされてロードは失敗する (チェックコードは暗号鍵に依存することに注意)。

なお、ゲーム状態に関する値域チェックは行われないので、不正な値をロードさせることもできる。たとえば以下のような現象が確認されている:

裏技パスワード

パスワードのロードに成功し、かつその結果のゲーム状態 (暗号鍵とチェックコードを除く) が全てゼロクリアされている場合、それは裏技パスワードとして扱われる。

裏技パスワードをロードすると以下のようなデバッグメニューが起動し、ゲーム状態を編集した上でプレイを開始できる:

デバッグメニュー

デバッグメニュー内の「れべる」は現在の面、「パチ夫」は残機、「すっちゃった」はゲームオーバー回数を意味する。なお、デバッグメニューにおいては運命数/現在の面/残機に対する値域チェックが行われており、値が不正だとプレイを開始できない。

裏技パスワードは正規文字の範囲では 64 種存在する(暗号鍵に依存)が、このうち「かまととぶりつこみいはあぷろぐれ」(カマトト ぶりっ子 ミーハー プログレ) が開発者の意図したパスワードと思われる。代替文字も許した裏技パスワードは 23504 種存在するが、上記以外に意味ありげな文字列は見当たらなかった。

ちなみに、続編の『パチ夫くん2』には上記の裏技パスワードを喋るNPCが存在する (どりーむ店などに出現):

パチ夫くん2で前作の裏技パスワードを喋るNPC in ドリーム店

最短パスワード

ロード可能なパスワードは最短で 2 文字 (3 文字目以降は空白) であり、全部で 30 種存在する。一覧を以下に示す。なお、暗号鍵は全て 0xE8 である (デコード処理を参照):

パスワード fortune score total_win stage_win gameover stage hand_ball life
3う370xBD642151204440x81CFA23
5ふ440x3DE521179526050x0150237
7ら450xBE65E252207650x82D0A33
ごが430x39E1DD175516010x3D4C1F7
ご」430x39E1DD175516010x3D4C1F7
ごん430x39E1DD175516010x3D4C1F7
だと430xBF6623532010860x83D1A43
ぱう370xBD642151204440x81CFA23
ぷふ440x3DE521179526050x0150237
ぽら450xBE65E252207650x82D0A33
。4510xBC63A050201230x80CEA13
。ぴ510xBC63A050201230x80CEA13
ゅと430xBF6623532010860x83D1A43
けづ570x846B6858221330x88D6A93
けど570x846B6858221330x88D6A93
けっ570x846B6858221330x88D6A93
さた430x00E8A4182532900x0453267
ちる510x82696656217710x86D4A73
てれ550x06EEAA188549360x0A592C7
ぬぢ610x07EF2B1895412570x0B5A2D7
ぬで610x07EF2B1895412570x0B5A2D7
ぬょ610x07EF2B1895412570x0B5A2D7
のぢ610x07EF2B1895412570x0B5A2D7
ので610x07EF2B1895412570x0B5A2D7
のょ610x07EF2B1895412570x0B5A2D7
むが550x05EDE9187546150x09582B7
む」550x05EDE9187546150x09582B7
むん550x05EDE9187546150x09582B7
ろ4510xBC63A050201230x80CEA13
ろぴ510xBC63A050201230x80CEA13