艦これ:独自編成IDをつくろう
2015/7/17のメンテで敵の編成IDが消えちゃいました。Σ(゚д゚lll)ガーン
先読みはまだしも、編成IDないとデータベースを作る際に結構困ったりします。例えば、
・敵の編成を記録しておいて、ドロップとの関係を調べる
例:6-1の空母棲姫マスみたいに特定の編成でないと401がドロップしない
・敵の編成を記録して、Wiki方式に編成一覧を出力する
・敵の編成IDを表示して、どの敵と戦っているか一目瞭然にする
・編成IDを記録して、戦闘履歴を記録する……
先読み厨がいっぱいいますが、編成IDは解析面からでも常識的に考えて結構使います。さすがに羅針盤の段階で表示しないようにするだけで、戦闘中になったらいつもどおりIDが返ってくるのかと思ったら、それすらなくて「そのたった十数字節約するとこなの!? ネジでボロ儲けしてるんだからケチケチせずに増設しなさい」ってマジギレしてました。今でも許してません。冗長性って大事だよね。
編成IDがないの?じゃあ作ってしまえばいいじゃないというわけで、苦し紛れに編成IDを作ってみます。これはもちろん、サーバー側に用意されている(7/17のメンテ前までは普通に取得できた)固有の値ではなくて、ローカルに勝手に作る識別子のようなものです。運営が用意した編成IDも結構行き当たりばったりにつけられていて、1-1から1番を割り当てて5-4まで行くものの、1-5行ったら5-4の続きで500番台が出てくるし、イベントマップに行ったら急に2000番代になるし……。お世辞にもいいIDとはいえません。
そこで今回作る識別子、これをローカルIDと呼ぶことにします。仕様は3分ぐらいで思いついて、実装しながら調整したやつなんで相当ガバガバかと思いますが一案として。まずこのローカルIDは次の要件を満たさなければいけません。
ローカルIDが満たすべき要件
・ローカルIDはできれば数値でほしい
・海域番号、マップ番号、(セル番号)、敵のマスターIDの配列(api_ship_ke)、陣形 が与えられれば、ローカルIDは一意に決まらなければならない
・ローカルIDから上記の5要素を復元できなくてもよい(不可逆変換でもいい)
・既存のこれらのデータから、ローカルIDを計算して、ローカルIDによる運用にスライドできるようにしなければならない
※最初にちょっろっと公開したときはGetHashCode()で実装してしまいましたが、GetHashCode()の値に一意性の保証がないため、MD5でちゃんとハッシュを取るように変更しました。
ぱっと思いつく解決法が、ハッシュ値を取る方法。具体的にはファイルの検証等で使われるMD5の値を取ります。ローカルID→海域…陣形データは復元できないものの、一意の変換はオッケーです。ハッシュ値は衝突することがありますが、普通の32ビットintの間で取る限り、衝突する確率はめちゃくちゃ低いです。
MD5を計算するためにはまずバイナリに変換してあげなければいけません。インスタンスをそのままシリアル化してバイナリにぽいーでもいいのですが、バイナリを文字列に変換するとまずバイナリがぱっと見て何かわからないのでバグが出たときに面倒になりそう。
そこでおすすめなのは、艦これのゲームと同じく、このクラスのオブジェクトをJSONでシリアル化して、そのJSON文字列のハッシュ値を取る方法。JSONにしてしまえばぱっと見なにかわかるし、クラスの変数名や算出方法を統一すれば誰がやっても同じ値が取れます。
例えばこのような実装をしてみます。
ここでMakeLongHashCode()とMakeShortHashCode(int longhash)の2つが出てきますが、MakeLongHashCode()がローカルIDにあたるもので、内部のデータ処理はこの値を擬似的な編成IDとして処理をします。LongHashCodeは32ビットintの値の範囲内(-2,147,483,648 ~ 2,147,483,647)の数値が頻繁に出るので表示用には適さないので、表示用に変換したMakeShortHashCode()というのもあわせて実装します
解説
LongHashCodeの部分(MD5を取る部分)
・JSON文字列を作る。シリアル化のライブラリ使ってないけど、この程度のJSONだったらStringBuilderで手動でやったほうが早いし面倒じゃない気がする。DynamicJSONで、手動でシリアル化した結果と、ライブラリつかってシリアル化した結果が一致するのを確認。
・JSON文字列→バイナリ。エンコードは何でもいいけどとりあえずUTF-8で。
・バイナリ→MD5。ここらへんはテンプレどおり。MD5は通常16進数の羅列で表記することが多いけど、結局は128ビット変数なんでBigIntegerで一時的に受け止める。
・MD5(128ビット)→int(32ビット)。今まで編成IDはintで定義してたので、ローカルIDもintに切り落とす必要がある(ぶっちゃけ16ビットでも十分すぎるぐらい)。これはint.MaxValueでBigIntegerの剰余を取ることで、範囲を切り詰めることができる。
LongHashCodeからShortHashCodeへの変換定義は次のとおりです。
LongHashCodeからShortHashCodeへの変換定義
・LongHashCodeが与えられているものとする
・ShortHashCodeは5桁~6桁(程度)の数字とする(何桁でもいいけど見づらくならずに、同一マップで編成IDがダブらない程度に)
・以下のように変換する
ShortHashCode = AABCCC
※A:海域番号(5-4だったら5) 海域番号が1桁の場合Aは1桁
※B:マップ番号(5-4だったら4)
※CCC:LongHashCode下3桁 (longHashCode % 1000(注) の絶対値 )
(注)C#では、マイナスの数字の剰余を求めると答えがマイナスになるので絶対値処理をかぶせました(-2mod5 = -2になる)。ハッシュ関数自体が一様な変換なので、mod5が2だろうが3だろうがあんまり関係がないはず。
例1:5-4のセル1でapi_ship_keが[0,1,2,3,4,5]、陣形が1の場合
JSON:{"api_maparea_id":5,"api_mapinfo_no":4,"api_cell_id":1,"api_ship_ke":[0,1,2
,3,4,5],"api_formation_enemy":1}
MD5:-7287270422730532209274110936903801675
LongHashCode:-1264373903
ShortHashCode:54903
ShortHashCodeのほうが見やすいことがわかったでしょうか?
例2:例1と全く同じ値の別インスタンスを作成した場合(そのままインスタンス.GetHashCode()では異なる例)
JSON:{"api_maparea_id":5,"api_mapinfo_no":4,"api_cell_id":1,"api_ship_ke":[0,1,2
,3,4,5],"api_formation_enemy":1}
MD5:-7287270422730532209274110936903801675
LongHashCode:-1264373903
ShortHashCode:54903
インスタンスが(参照が)いくら変わろうが、値が同じならHashCodeも同じという点が確認できます。実質的な値比較です。
例3:例1のセル番号を1→2に変えた場合
JSON:{"api_maparea_id":5,"api_mapinfo_no":4,"api_cell_id":2,"api_ship_ke":[0,1,2
,3,4,5],"api_formation_enemy":1}
MD5:-22229292957627216802792691185990212046
LongHashCode:-1936480517
ShortHashCode:54517
当然、値が変わればHashCodeも変わります
一意性を保証しつつ、値のみに着目したローカルIDを作ることが出来ました!内部処理用のローカルID(LongHashCode)と、表示用のローカルID(ShortHashCode)をわけることで、表示用のローカルIDからはどのマップが特定でき(下3桁の数字は飛び飛びになりますが)、なおかつ内部処理用のローカルIDは十分な桁数を確保することで衝突を出来る限りおこらなくできます。
なので、ほっぽで今後、以前編成IDのあった場所に変な5,6桁の数字が出てたら、こしあんさんが勝手に作った表示用のローカルID(ShortHashCode)だと思ってください(これを伝えたかった)。
このローカルIDの計算方法は、後ほど、DataLibraryをコミットしたらgithubで公開する予定です。→公開しました。リンクはこ↑こ↓
https://github.com/koshian2/HoppoAlpha.DataLibrary/blob/master/HoppoAlpha.DataLibrary/DataObject/UserEnemyID.cs
5-4南の1戦目のnext はい、編成ID死んだー! http://t.co/GuEqlf0S5P
— おまんこしあんのおぱんつ (@koshian2) 2015, 7月 17
先読みはまだしも、編成IDないとデータベースを作る際に結構困ったりします。例えば、
・敵の編成を記録しておいて、ドロップとの関係を調べる
例:6-1の空母棲姫マスみたいに特定の編成でないと401がドロップしない
・敵の編成を記録して、Wiki方式に編成一覧を出力する
・敵の編成IDを表示して、どの敵と戦っているか一目瞭然にする
・編成IDを記録して、戦闘履歴を記録する……
先読み厨がいっぱいいますが、編成IDは解析面からでも常識的に考えて結構使います。さすがに羅針盤の段階で表示しないようにするだけで、戦闘中になったらいつもどおりIDが返ってくるのかと思ったら、それすらなくて「そのたった十数字節約するとこなの!? ネジでボロ儲けしてるんだからケチケチせずに増設しなさい」ってマジギレしてました。今でも許してません。冗長性って大事だよね。
編成IDがないの?じゃあ作ってしまえばいいじゃないというわけで、苦し紛れに編成IDを作ってみます。これはもちろん、サーバー側に用意されている(7/17のメンテ前までは普通に取得できた)固有の値ではなくて、ローカルに勝手に作る識別子のようなものです。運営が用意した編成IDも結構行き当たりばったりにつけられていて、1-1から1番を割り当てて5-4まで行くものの、1-5行ったら5-4の続きで500番台が出てくるし、イベントマップに行ったら急に2000番代になるし……。お世辞にもいいIDとはいえません。
そこで今回作る識別子、これをローカルIDと呼ぶことにします。仕様は3分ぐらいで思いついて、実装しながら調整したやつなんで相当ガバガバかと思いますが一案として。まずこのローカルIDは次の要件を満たさなければいけません。
ローカルIDが満たすべき要件
・ローカルIDはできれば数値でほしい
・海域番号、マップ番号、(セル番号)、敵のマスターIDの配列(api_ship_ke)、陣形 が与えられれば、ローカルIDは一意に決まらなければならない
・ローカルIDから上記の5要素を復元できなくてもよい(不可逆変換でもいい)
・既存のこれらのデータから、ローカルIDを計算して、ローカルIDによる運用にスライドできるようにしなければならない
※最初にちょっろっと公開したときはGetHashCode()で実装してしまいましたが、GetHashCode()の値に一意性の保証がないため、MD5でちゃんとハッシュを取るように変更しました。
ぱっと思いつく解決法が、ハッシュ値を取る方法。具体的にはファイルの検証等で使われるMD5の値を取ります。ローカルID→海域…陣形データは復元できないものの、一意の変換はオッケーです。ハッシュ値は衝突することがありますが、普通の32ビットintの間で取る限り、衝突する確率はめちゃくちゃ低いです。
MD5を計算するためにはまずバイナリに変換してあげなければいけません。インスタンスをそのままシリアル化してバイナリにぽいーでもいいのですが、バイナリを文字列に変換するとまずバイナリがぱっと見て何かわからないのでバグが出たときに面倒になりそう。
そこでおすすめなのは、艦これのゲームと同じく、このクラスのオブジェクトをJSONでシリアル化して、そのJSON文字列のハッシュ値を取る方法。JSONにしてしまえばぱっと見なにかわかるし、クラスの変数名や算出方法を統一すれば誰がやっても同じ値が取れます。
例えばこのような実装をしてみます。
using System.Security.Cryptography;
using System.Numerics;
///
/// ローカルな編成IDを計算するためのクラス
///
public class UserEnemyID
{
///
/// 海域番号(ex.5-4-1の5)
///
public int api_maparea_id { get; set; }
///
/// マップ番号(ex.5-4-1の4)
///
public int api_mapinfo_no { get; set; }
///
/// セル番号(ex.5-4-1の1)
///
public int api_cell_id { get; set; }
///
/// 敵編成の型。戦闘APIのapi_ship_ke
///
public Listapi_ship_ke { get; set; }
///
/// 敵の陣形。api_formationから敵の部分だけ抽出
///
public int api_formation_enemy { get; set; }
///
/// ハッシュコードをフルで返します。シリアル化してから比較するため値比較されます。
///
///ハッシュコード(フル)
public int MakeLongHashCode()
{
//JSONでシリアル化 手動実装(キリッ)
StringBuilder sb = new StringBuilder();
sb.Append("{\"api_maparea_id\":");
sb.Append(api_maparea_id);
sb.Append(",\"api_mapinfo_no\":");
sb.Append(api_mapinfo_no);
sb.Append(",\"api_cell_id\":");
sb.Append(api_cell_id);
sb.Append(",\"api_ship_ke\":[");
sb.Append(string.Join(",", api_ship_ke));
sb.AppendFormat("],\"api_formation_enemy\":");
sb.Append(api_formation_enemy);
sb.Append("}");
string json = sb.ToString();
//UTF8でバイナリに変換
byte[] binary = Encoding.UTF8.GetBytes(json);
//MD5の計算
MD5 cropto = new MD5CryptoServiceProvider();
byte[] hashByte = cropto.ComputeHash(binary);
//MD5を数値に変換
BigInteger hashValue = new BigInteger(hashByte);
//MD5をintに収まるようにMODを取る
int longHashCode = (int)(hashValue % int.MaxValue);
Console.WriteLine("JSON:{0}", json);
Console.WriteLine("MD5:{0}", hashValue);
Console.WriteLine("LongHashCode:{0}", longHashCode);
return longHashCode;
}
///
/// 海域番号+マップ番号+ハッシュコードの数値を返します。シリアル化してから比較するため値比較されます。
///
/// ハッシュコード(フル)
///ハッシュコード(短縮版)
public int MakeShortHashCode(int longhash)
{
int shorthash = Math.Abs(longhash % 1000);//LongHashの下3桁(マイナスなら絶対値で反転)
shorthash += api_mapinfo_no * 1000;
shorthash += api_maparea_id * 10000;
Console.WriteLine("ShortHashCode:{0}", shorthash);
return shorthash;
}
}
ここでMakeLongHashCode()とMakeShortHashCode(int longhash)の2つが出てきますが、MakeLongHashCode()がローカルIDにあたるもので、内部のデータ処理はこの値を擬似的な編成IDとして処理をします。LongHashCodeは32ビットintの値の範囲内(-2,147,483,648 ~ 2,147,483,647)の数値が頻繁に出るので表示用には適さないので、表示用に変換したMakeShortHashCode()というのもあわせて実装します
解説
LongHashCodeの部分(MD5を取る部分)
・JSON文字列を作る。シリアル化のライブラリ使ってないけど、この程度のJSONだったらStringBuilderで手動でやったほうが早いし面倒じゃない気がする。DynamicJSONで、手動でシリアル化した結果と、ライブラリつかってシリアル化した結果が一致するのを確認。
・JSON文字列→バイナリ。エンコードは何でもいいけどとりあえずUTF-8で。
・バイナリ→MD5。ここらへんはテンプレどおり。MD5は通常16進数の羅列で表記することが多いけど、結局は128ビット変数なんでBigIntegerで一時的に受け止める。
・MD5(128ビット)→int(32ビット)。今まで編成IDはintで定義してたので、ローカルIDもintに切り落とす必要がある(ぶっちゃけ16ビットでも十分すぎるぐらい)。これはint.MaxValueでBigIntegerの剰余を取ることで、範囲を切り詰めることができる。
LongHashCodeからShortHashCodeへの変換定義は次のとおりです。
LongHashCodeからShortHashCodeへの変換定義
・LongHashCodeが与えられているものとする
・ShortHashCodeは5桁~6桁(程度)の数字とする(何桁でもいいけど見づらくならずに、同一マップで編成IDがダブらない程度に)
・以下のように変換する
ShortHashCode = AABCCC
※A:海域番号(5-4だったら5) 海域番号が1桁の場合Aは1桁
※B:マップ番号(5-4だったら4)
※CCC:LongHashCode下3桁 (longHashCode % 1000(注) の絶対値 )
(注)C#では、マイナスの数字の剰余を求めると答えがマイナスになるので絶対値処理をかぶせました(-2mod5 = -2になる)。ハッシュ関数自体が一様な変換なので、mod5が2だろうが3だろうがあんまり関係がないはず。
例1:5-4のセル1でapi_ship_keが[0,1,2,3,4,5]、陣形が1の場合
JSON:{"api_maparea_id":5,"api_mapinfo_no":4,"api_cell_id":1,"api_ship_ke":[0,1,2
,3,4,5],"api_formation_enemy":1}
MD5:-7287270422730532209274110936903801675
LongHashCode:-1264373903
ShortHashCode:54903
ShortHashCodeのほうが見やすいことがわかったでしょうか?
例2:例1と全く同じ値の別インスタンスを作成した場合(そのままインスタンス.GetHashCode()では異なる例)
JSON:{"api_maparea_id":5,"api_mapinfo_no":4,"api_cell_id":1,"api_ship_ke":[0,1,2
,3,4,5],"api_formation_enemy":1}
MD5:-7287270422730532209274110936903801675
LongHashCode:-1264373903
ShortHashCode:54903
インスタンスが(参照が)いくら変わろうが、値が同じならHashCodeも同じという点が確認できます。実質的な値比較です。
例3:例1のセル番号を1→2に変えた場合
JSON:{"api_maparea_id":5,"api_mapinfo_no":4,"api_cell_id":2,"api_ship_ke":[0,1,2
,3,4,5],"api_formation_enemy":1}
MD5:-22229292957627216802792691185990212046
LongHashCode:-1936480517
ShortHashCode:54517
当然、値が変わればHashCodeも変わります
一意性を保証しつつ、値のみに着目したローカルIDを作ることが出来ました!内部処理用のローカルID(LongHashCode)と、表示用のローカルID(ShortHashCode)をわけることで、表示用のローカルIDからはどのマップが特定でき(下3桁の数字は飛び飛びになりますが)、なおかつ内部処理用のローカルIDは十分な桁数を確保することで衝突を出来る限りおこらなくできます。
なので、ほっぽで今後、以前編成IDのあった場所に変な5,6桁の数字が出てたら、こしあんさんが勝手に作った表示用のローカルID(ShortHashCode)だと思ってください(これを伝えたかった)。
このローカルIDの計算方法は、後ほど、DataLibraryをコミットしたらgithubで公開する予定です。→公開しました。リンクはこ↑こ↓
https://github.com/koshian2/HoppoAlpha.DataLibrary/blob/master/HoppoAlpha.DataLibrary/DataObject/UserEnemyID.cs
スポンサーサイト