ファミコン時代のセーブ用のパスワード作ってみる

ayumegu(プログラマー)
よろしくお願いします。

ファミコン時代のセーブってパスワードのノート用意してひたすらメモをとっていたじゃないですか・・・。
一文字かえただけだとちゃんとはじかれるんですよね・・・。
それがちょっと気になったので自分でもにたようなことをやってみました。
シフト演算とか基礎的なことをろくすっぽ学ばずに育ったのでいろいろソースはご愛嬌ですが・・・。

ちなみに・・・

レトロゲーム研究室
こちらの記事によるとファミコン時代の最長の保存用の文字列は119文字!
携帯もデジカメもないような時代にこれはきつい!

私は桃太郎伝説がすきだったので38 文字でしたが、毎回2回パスワードを記述していた幼稚園児代。
最終的にクリアできないままだったけれど・・・。
あのパスワード入力画面の音楽が小さい頃はすごく怖かった覚えがあります。

あ、最近?ではないですがポケモンダンジョンでふしぎなメールというのでこのようなパスワードつかわれていましたね〜。
まぁこういった系は解析されてしまうのは仕方ないですが・・・。でもわくわくするよね!

保存するデータ

保存するデータは下記のようにしました。

名前は5文字 1文字8bit * 5 30bit
経験値 0~9999     14bit
所持金 0~9999     14bit
アイテム 0~9 4bit * 4 16bit   下記4種類
薬草
毒消し草
しびれ草
雑草
チェック用 4bit
合計 78bit 文字数13文字

名前はドラクエの方式のようなのにするので
あゆめぐだとぐが2もじになるので5文字にしましたw
6ビットで1文字になるので78bit 13文字のパスワードを作ります。
使用する文字などは下記

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   // 保存用のセーブ文字に使用する文字 64文字
  public static readonly string[] wordList = new string[]{
      "あ","い","う","え","お",
      "か","き","く","け","こ",
      "さ","し","す","せ","そ",
      "た","ち","つ","て","と",
      "な","に","ぬ","ね","の",
      "は","ひ","ふ","へ","ほ",
      "ま","み","む","め","も",
      "や","ゆ","よ",
      "ら","り","る","れ","ろ","わ",
      "が","ぎ","ぐ","げ","ご",
      "ざ","じ","ず","ぜ","ぞ",
      "ば","び","ぶ","べ","ぼ",
      "ぱ","ぴ","ぷ","ぺ","ぽ"};
  
  // プレイヤーの名前に使える文字
  public static readonly string[] inputWordList = new string[]{
      "あ","い","う","え","お",
      "か","き","く","け","こ",
      "さ","し","す","せ","そ",
      "た","ち","つ","て","と",
      "な","に","ぬ","ね","の",
      "は","ひ","ふ","へ","ほ",
      "ま","み","む","め","も",
      "や","ゆ","よ",
      "ら","り","る","れ","ろ",
      "わ","を","ん",
      "っ","ゃ","ゅ","ょ",
      "゛","゜"," "};

とりあえずPlayerData

とりあえずPlayerのデータを保持するクラス

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
using UnityEngine;
using System.Collections;

public class PlayerData {
  
  private string playerName;
  private int exp;
  private int money;
  private int[] itemCountList;

  public PlayerData()
  {
  }

  public void CreateRandomData()
  {
      playerName = "あゆめく゛";
      exp = Random.Range(0, 10000);
      money = Random.Range(0, 10000);
      itemCountList = new int[4];
      for(int i = 0; i < itemCountList.Length; i++)
      {
          itemCountList[i] = Random.Range(0, 10);
      }
  }

  public string PlayerName
  {
      get { return playerName; }
      set { playerName = value; }
  }

  public int Exp
  {
      get { return exp; }
      set { exp = value; }
  }

  public int Money
  {
      get { return money; }
      set { money = value; }
  }

  public int[] ItemCountList
  {
      get { return itemCountList; }
      set { itemCountList = value; }
  }
}

ま、簡単にこんな感じで・・・。
では実装の方を・・・。 手順としては、 情報を10進数から2進数にする
6ビットごとに10進数に戻して、保存用の文字にする
本来は10進数にする前に不正防止用のチェック用の文字を適当に入れるようなのですが、
いったんそれ抜きでそれ以外のところを実装

パスワードを作成してみる

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System;
public class CommonFunc : MonoBehaviour {

  public static PlayerData playerData;
  public static int itemCount = 4;
  public const int nameSize = 5;

  // 保存用のセーブ文字に使用する文字 64文字
  public static readonly string[] wordList = new string[]{
      "あ","い","う","え","お",
      "か","き","く","け","こ",
      "さ","し","す","せ","そ",
      "た","ち","つ","て","と",
      "な","に","ぬ","ね","の",
      "は","ひ","ふ","へ","ほ",
      "ま","み","む","め","も",
      "や","ゆ","よ",
      "ら","り","る","れ","ろ","わ",
      "が","ぎ","ぐ","げ","ご",
      "ざ","じ","ず","ぜ","ぞ",
      "ば","び","ぶ","べ","ぼ",
      "ぱ","ぴ","ぷ","ぺ","ぽ"};

  // プレイヤーの名前に使える文字
  public static readonly string[] inputWordList = new string[]{
      "あ","い","う","え","お",
      "か","き","く","け","こ",
      "さ","し","す","せ","そ",
      "た","ち","つ","て","と",
      "な","に","ぬ","ね","の",
      "は","ひ","ふ","へ","ほ",
      "ま","み","む","め","も",
      "や","ゆ","よ",
      "ら","り","る","れ","ろ",
      "わ","を","ん",
      "っ","ゃ","ゅ","ょ",
      "゛","゜"," "};
  
  // 保存文字作成メソッド
  public static string CreateSaveWord()
  {
      string resultNo = CreateSaveWordNo();
      Debug.Log(resultNo + " : " + resultNo.Length);
      return CreateSaveWordStr(resultNo);
  }

  // 保存用の数字の文字列をセーブデータ用の文字に置き換えるメソッド
  private static string CreateSaveWordStr(string resultNo)
  {
      string str = "";
      int passwordCount = resultNo.Length / 6;
      for(int i = 0; i < passwordCount; i++)
      {
          int num = Convert.ToInt32(resultNo.Substring(i * 6, 6), 2);
          str += wordList[num];
      }
      return str;
  }

  // 保存データを2ビットのデータにするメソッド
  private static string CreateSaveWordNo()
  {
      string resultNo = "";
      for(int i = 0; i < nameSize; i++)
      {
          int no = GetInputWordNo(playerData.PlayerName.Substring(i, 1));
          resultNo += ChangeString(Convert.ToString(no, 2), 6);
      }

      resultNo += ChangeString(Convert.ToString(playerData.Exp, 2), 14);
      resultNo += ChangeString(Convert.ToString(playerData.Money, 2), 14);
      for(int i = 0; i < itemCount; i++)
      {
          resultNo += ChangeString(Convert.ToString(playerData.ItemCountList[i], 2), 4);
      }

      resultNo += "0000"; // チェック用の文字。とりあえず今回は使用しないので最後に追加
      return resultNo;
  }

  // 0埋め
  private static string ChangeString(string str, int count)
  {
      if(str.Length != count)
      {
          while(str.Length < count)
          {
              str = "0" + str;
          }
      }
      return str;
  }

  // パスワード文字の番号取得
  private static int GetWordNo(string str)
  {
      for(int i = 0; i < wordList.Length; i++)
      {
          if(str == wordList[i])
              return i;
      }
      return -1;
  }

  // 入力文字の番号取得
  private static int GetInputWordNo(string str)
  {
      for(int i = 0; i < inputWordList.Length; i++)
      {
          if(str == inputWordList[i])
              return i;
      }
      return -1;
  }
  
}

ロード用のメソッド

ロードはこんな感じ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
   public static void LoadSaveWord(string result)
  {
      playerData = new PlayerData();
      string no = "";
      for(int i = 0; i < result.Length; i++)
      {
          int wordNo = GetWordNo(result.Substring(i, 1));
          no += ChangeString(Convert.ToString(wordNo, 2), 6);
      }
      Debug.Log ("wordNo : " + no.Length);
      string loadName = "";
      for(int i = 0; i < nameSize; i++)
      {
          int num = Convert.ToInt32(no.Substring(i * 6, 6), 2);
          loadName += inputWordList[num];
      }

      int loadExp = Convert.ToInt32(no.Substring(6 * nameSize, 14), 2);
      int loadMoney = Convert.ToInt32(no.Substring(44, 14), 2);
      int[] loadItemCountList = new int[itemCount];

      playerData.PlayerName = loadName;
      playerData.Exp = loadExp;
      playerData.Money = loadMoney;
      int[] itemCountList = new int[itemCount];
      for(int i = 0; i < itemCount; i++)
      {
          itemCountList[i] = Convert.ToInt32(no.Substring(58 + i * 4, 4), 2);
      }
      playerData.ItemCountList = itemCountList;
  }

ここまでの完成品

あとはUIを簡単に用意して・・・。

こんな感じになりました。
このままだと名前が最初なのでパスワードの最初が名前なのがバレバレなのと、
現在のままだと1文字かえても普通に通ってしまいます。 この対策にチェック用の4bitを使用します。
また本来のファミコンでは同じ状態のデータでも保存パスワード毎回かわっていましたよね?

残り4bitでチェックようの値を埋め込む

のこりの4bitを好きな位置に好きに埋め込みます。
4bitだけだとたかがしれていますが・・・。
たとえば〜、全体の1の数が奇数だったら1にしたり、
4つとも同じ数字で埋め込んだり・・・。
ここでランダム値を使わないと同じデータでも違うパスワードにならないので・・・
今回はこんなのにしました。

最初の2文字がランダム 0~3 (00 01 10 11の4パターンです)
3文字目が文字数引く上記のランダム数-7のbitと同じもの
4文字目がこれ以外のビットの1の数が奇数だったら1

これによって少ないが何パターンか生成可能。
4カ所しかかわるところないけれど・・・。orz
かえても合う確率はちょっとだけさがるかなと

あとはロードにチェックをすれば完了!


ちょっと画像だとわからないかもですが、無事ロード完了です
同じパラメータでも 数種類くらいは違うパスワードができます・・・。
もっとチェック用のbitをふやせば複雑にできるかと思います。(パスワード長くなるけど・・・。)

最後に、疲れちゃったのでこのままのコードを張っておくことにします・・・。
WebPlayerでビルドしたのですが、うまくビルドできなかったのでソースだけ・・・。
文字入力がきかなかったんだよね・・・。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System;
public class CommonFunc : MonoBehaviour {

/*
名前 5文字  1文字6bit * 5 30bit
経験値    0~9999       14bit
所持金    0~9999       14bit
アイテム   0~9   4bit * 4 16bit
薬草
毒消し草
しびれ草
雑草
チェック用      0~15        4bit
合計 78bit 文字数13文字
*/


  public static PlayerData playerData;
  public static int itemCount = 4;
  private static int checkNo = 7;
  public const int nameSize = 5;
  public static int[] checkNoPos = new int[]{2, 12, 22, 55};  // チェック用の文字を入れる場所

  // 保存用のセーブ文字に使用する文字 64文字
  public static readonly string[] wordList = new string[]{
      "あ","い","う","え","お",
      "か","き","く","け","こ",
      "さ","し","す","せ","そ",
      "た","ち","つ","て","と",
      "な","に","ぬ","ね","の",
      "は","ひ","ふ","へ","ほ",
      "ま","み","む","め","も",
      "や","ゆ","よ",
      "ら","り","る","れ","ろ","わ",
      "が","ぎ","ぐ","げ","ご",
      "ざ","じ","ず","ぜ","ぞ",
      "ば","び","ぶ","べ","ぼ",
      "ぱ","ぴ","ぷ","ぺ","ぽ"};

  // プレイヤーの名前に使える文字
  public static readonly string[] inputWordList = new string[]{
      "あ","い","う","え","お",
      "か","き","く","け","こ",
      "さ","し","す","せ","そ",
      "た","ち","つ","て","と",
      "な","に","ぬ","ね","の",
      "は","ひ","ふ","へ","ほ",
      "ま","み","む","め","も",
      "や","ゆ","よ",
      "ら","り","る","れ","ろ",
      "わ","を","ん",
      "っ","ゃ","ゅ","ょ",
      "゛","゜"," "};
  
  // 保存文字作成メソッド
  public static string CreateSaveWord()
  {
      string resultNo = CreateSaveWordNo();
      Debug.LogWarning(resultNo + " : " + resultNo.Length);
      resultNo = AddCheckNo(resultNo);
      Debug.Log (resultNo);
      return CreateSaveWordStr(resultNo);
  }

  // チェックようの番号を追加する
  private static string AddCheckNo(string resultNo)
  {
/*
最初の2文字がランダム 0~3
3文字目が文字数引く上記のランダム数-7のbitと同じもの
4文字目がこれ以外のビットの1の数が奇数だったら1

これによって少ないが何パターンか生成可能。
かえても合う確率はちょっとだけさがるかなと
*/
      int rand = UnityEngine.Random.Range(0, 4);
      string str1 = ChangeString(Convert.ToString(rand, 2), 2);
      string str2 = resultNo.Substring(resultNo.Length - rand - 7, 1);
      string str3 = "0";
      int count = 0;
      for(int i = 0; i < resultNo.Length; i++)
      {
          if(resultNo.Substring(i, 1) == "1") count++;
      }
      if(str1.Substring(0, 1) == "1") count++;
      if(str1.Substring(0, 1) == "1") count++;
      if(str2 == "1") count++;
      if(count % 2 == 1) str3 = "1";
      Debug.Log (str1 + ":" + str2 + ":"+ str3);
      string password = resultNo.Substring(0, checkNoPos[0]) + str1.Substring(0, 1) +
          resultNo.Substring(checkNoPos[0], checkNoPos[1] - checkNoPos[0]) + str1.Substring(1, 1) +
              resultNo.Substring(checkNoPos[1], checkNoPos[2] - checkNoPos[1]) + str2 +
              resultNo.Substring(checkNoPos[2], checkNoPos[3] - checkNoPos[2]) + str3 +
              resultNo.Substring(checkNoPos[3], resultNo.Length - checkNoPos[3]);
      return password;
  }

  // ロード用メソッド
  public static bool LoadSaveWord(string result)
  {
      playerData = new PlayerData();
      string no = "";
      for(int i = 0; i < result.Length; i++)
      {
          int wordNo = GetWordNo(result.Substring(i, 1));
          no += ChangeString(Convert.ToString(wordNo, 2), 6);
      }

      string password = "";
      string str1 = no.Substring(checkNoPos[0], 1) + no.Substring(checkNoPos[1] + 1, 1);
      string str2 = no.Substring(checkNoPos[2] + 2, 1);
      string str3 = no.Substring(checkNoPos[3] + 3, 1);
      Debug.Log (str1 + ":" + str2 + ":"+ str3);
      
      for(int i = 0; i < no.Length; i++)
      {
          if(i != checkNoPos[0] && i != checkNoPos[1] + 1 && i != checkNoPos[2] + 2 && i != checkNoPos[3] + 3 )
              password += no.Substring(i, 1);
      }
      Debug.LogWarning (password);

      int randomNo = Convert.ToInt32(str1, 2);
      if(str2 != password.Substring(password.Length - randomNo - 7, 1))
      {
          return false;
      }

      int count = 0;
      for(int i = 0; i < password.Length; i++)
      {
          if(password.Substring(i, 1) == "1") count++;
      }
      if(str1.Substring(0, 1) == "1") count++;
      if(str1.Substring(0, 1) == "1") count++;
      if(str2 == "1") count++;
      if(count % 2 != int.Parse(str3))
      {
          return false;
      }

      Debug.Log ("wordNo : " + password.Length);
      string loadName = "";
      for(int i = 0; i < nameSize; i++)
      {
          int num = Convert.ToInt32(password.Substring(i * 6, 6), 2);
          loadName += inputWordList[num];
      }

      int loadExp = Convert.ToInt32(password.Substring(6 * nameSize, 14), 2);
      if(9999 < loadExp)
      {
          return false;
      }
      int loadMoney = Convert.ToInt32(password.Substring(44, 14), 2);
      if(9999 < loadMoney)
      {
          return false;
      }
      int[] loadItemCountList = new int[itemCount];

      playerData.PlayerName = loadName;
      playerData.Exp = loadExp;
      playerData.Money = loadMoney;
      int[] itemCountList = new int[itemCount];
      for(int i = 0; i < itemCount; i++)
      {
          itemCountList[i] = Convert.ToInt32(password.Substring(58 + i * 4, 4), 2);
          if(9 < itemCountList[i])
          {
              return false;
          }
      }
      playerData.ItemCountList = itemCountList;
      return true;
  }

  private static string CreateSaveWordStr(string resultNo)
  {
      string str = "";
      int passwordCount = resultNo.Length / 6;
      for(int i = 0; i < passwordCount; i++)
      {
          int num = Convert.ToInt32(resultNo.Substring(i * 6, 6), 2);
          str += wordList[num];
      }
      return str;
  }

  private static string CreateSaveWordNo()
  {
      string resultNo = "";
      for(int i = 0; i < nameSize; i++)
      {
          int no = GetInputWordNo(playerData.PlayerName.Substring(i, 1));
          resultNo += ChangeString(Convert.ToString(no, 2), 6);
      }

      resultNo += ChangeString(Convert.ToString(playerData.Exp, 2), 14);
      resultNo += ChangeString(Convert.ToString(playerData.Money, 2), 14);
      for(int i = 0; i < itemCount; i++)
      {
          resultNo += ChangeString(Convert.ToString(playerData.ItemCountList[i], 2), 4);
      }


      return resultNo;
  }

  // 0埋め
  private static string ChangeString(string str, int count)
  {
      if(str.Length != count)
      {
          while(str.Length < count)
          {
              str = "0" + str;
          }
      }
      return str;
  }

  // パスワード文字の番号取得
  private static int GetWordNo(string str)
  {
      for(int i = 0; i < wordList.Length; i++)
      {
          if(str == wordList[i])
              return i;
      }
      return -1;
  }

  // 入力文字の番号取得
  private static int GetInputWordNo(string str)
  {
      for(int i = 0; i < inputWordList.Length; i++)
      {
          if(str == inputWordList[i])
              return i;
      }
      return -1;
  }
  
}

今回の感想

仕組み的には結構シンプルにできることがわかりました。
今は遺伝のアルゴリズムに興味が出てきている私であります。
脳みそが足りないかもですが・・・何かつくってみよう。
タワーディフェンス・・・orz
それではまた!