AOYAMA Koji's プログラミングブログ - プログラミングを楽しく体験

【技術解説】複雑なロジックこそテストを:詰将棋開発で学ぶテスト駆動パフォーマンス向上【自動生成☆詰将棋】プログラマー向け実装解説

2025/08/30
【技術解説】複雑なロジックこそテストを:詰将棋開発で学ぶテスト駆動パフォーマンス向上【自動生成☆詰将棋】

 自動生成☆詰将棋Webアプリでは、テストを導入したことで、パフォーマンス向上ができました。 本記事ではその有効性および、JavaScriptの特徴に合わせて工夫したポイントなどを解説します。
 なお自動生成☆詰将棋Webアプリは、この記事などでお楽しみください。

プログラミングブログ記事一覧



[PR]

複雑なロジックでの開発課題


 自動生成☆詰将棋Webアプリの中で、詰手順探索は、複雑なロジックのプログラムになっています。 そのため、さまざまな課題があります。

結果が簡単に変わる


 詰手順探索処理は、わずかな変更で、今まで解けていた詰将棋が解けなくなってしまうことがあり、大きな開発課題となります。
 また、解けたとしても、複数の詰み手順がある場合に、変更で正解が簡単に変わってしまうのも問題です。 これは、各要素にスコアを付けてミニマックス評価を行うため、先手は最高スコアを選択、後手は最低スコアを選択するという処理になり、 少々のスコア計算方法変更で結果が変わる可能性があるためです。

不具合箇所の特定が困難


 詰手順探索処理は、プログラムの規模が大きく、ループ回数も多いため、不具合箇所やタイミングの特定が難しいのも課題です。

テストの価値:安心してパフォーマンス向上に専念


 前述の問題を解決するため、テストを導入することにしました。

導入タイミング


 テストを導入したタイミングは、開発後半です。 自動生成と詰手順探索がある程度完成して、これからパフォーマンス向上に取り組もうとしたところでした。

テスト導入の効果


 テストを導入したことにより、特にパフォーマンス向上に専念できたのが、大きな効果でした。
 パフォーマンス向上のためには、例えば無駄な処理の省略や、処理順序の変更を行います。 慎重に行っていても、結果が変わってしまう心配はどうしてもあります。
 その際、迅速に「結果が変わっていない」ことを確認でき、安心して次に進めたのが、テスト導入の効果になります。
[PR]

テストプログラミング


 テストのプログラミングについて解説します。

あらかじめ用意した結果と差分取得


 テストの基本的な動作は、対象の関数等を実行して、あらかじめ用意しておいた結果と比べます。 差分が無ければ OK 、有れば ERROR です。

テストの単位


 基本的には、各最小単位の関数に対してテストを行います。 これはユニットテストと呼ばれます。
 当ブログでは、共通ライブラリを自作しており、その各関数に対してユニットテストを実施しています。
 一方、ある程度大きな単位で呼び出して、結果を確認することもテストです。 関数の入出力だけでなく、例えばゲーム開発では生成された画像を比較するテスト方法もあります。
 自動生成☆詰将棋では、主に大きな単位でテストを行います。 自動生成された詰将棋の問題や、指定の詰将棋問題に対する詰み手順を、あらかじめ用意していたものと比較します。

テストモード


 当ブログではテストモードを用意して、その状態にあるときは特殊な処理にて、結果の差分が取れるようにしています。
 例えば、 alert など画面に表示する組み込み関数についても lo.alert という専用の関数を自作して、 通常モードでは alert の動作、テストモード時は画面に表示した文字列を保存しておき、後で差分が取れるようにしています。

実装


 テストはプログラミング手法としては一般的なため、ライブラリも存在しますが、当ブログでは自作しています。
 理由は、細かいところに手が届くというのもありますが、一番は実装が楽しいからです。 特にすべての型のデータの差分を取る実装が楽しかったです。
 ただし、いわゆる車輪の再発明になり、時間は必要になるため、 スケジュールが厳しい開発環境では、既存ライブラリを活用すべきだと思います。

ランダム要素がテストに与える課題


 テストは、同じ条件で関数を呼び出すと、同じ結果になる前提で作られます。
 そのため、結果にランダムの要素があると、テストできません。 対応方法を含めて詳細に解説します。

ランダム要素で結果が変わる問題


 自動生成☆詰将棋のように、ランダム要素である乱数を用いたアプリケーションは、乱数として取得した値に応じて結果が変わります。
 完成する詰将棋問題も変わりますし、パフォーマンスにも差分が生じます。

一般的な解決方法


 乱数は、乱数を生成する関数を呼び出し、返り値を使用します。 つまり求めるアルゴリズムがあります。
 一般的には、シード値と呼ばれる値を引数に取る、乱数を初期化する関数が用意されています。 シード値が同じなら、乱数を生成する関数は、必ず同じ値を、順次返します。
 そのため、テストモード時はシード値を固定して実行することで、毎回同じ乱数値が順次得られることになり、最終的にも同じ結果が得られます。

JavaScriptにシード値無し


 JavaScriptには Math.random() という乱数の関数が用意されています。 しかしこれには、前述のシード値による初期化機能がありません。
 そのため、一般的な解決方法は使用できません。

独自乱数の実装


 そのため当ブログでは、乱数の関数自体を、自作することにしました。
 実装として、関数 getValue() が呼ばれると、通常時は Math.random() を呼び出します。
 一方テストモード時は、あらかじめ用意しておいた乱数配列を順番に取り出し、現値に加算するという、単純な方式を取っています。 これにより、テストモード時の乱数の結果がわかりやすいという利点があります。
 実装コードは以下です。参考までに。
//**********************************************************************
// モジュールグローバル変数
//**********************************************************************
let g = {
  // 前回の乱数値
  fLastValue: 0.0,
  // 前回のパターン配列インデックス
  iLastIndex: 0,
  // パターン配列による決定的乱数(テスト用固定値)
  afPattern: [
    0.5922684335117193, 0.8078439726764971, 0.3641185390801466, 0.8834599110167176,
    0.5747007177845497, 0.5585639457862531, 0.0574007786448971, 0.5125442903385252,
    0.8351250345281758, 0.1990975662729854, 0.9587076855269266, 0.1946109341468509,
    0.2144854269351175, 0.8550531849249028, 0.5478502150066047, 0.7079574620691914
    ],
};
//**********************************************************************
// 初期化
//**********************************************************************
export function init( fInitValue = 0.0 ) {
  g.fLastValue = ( 0 < fInitValue && fInitValue < 1.0 )? fInitValue : 0.0;
  g.iLastIndex = 0;
}
//**********************************************************************
// 乱数取得
//**********************************************************************
export function getValue() {
  // モードによる分岐
  if ( lo.isTestMode() ) {
    // テストモードの処理
    // 現在の値に加算
    g.fLastValue = g.fLastValue + g.afPattern[ g.iLastIndex ];
    // 0以上1未満に
    if ( 1 <= g.fLastValue ) g.fLastValue -= 1.0;
    // インデックスを次へ
    g.iLastIndex += 1;
    if ( g.afPattern.length <= g.iLastIndex ) g.iLastIndex = 0;
    // テストモード用の値を返す
    return g.fLastValue;
  }
  // 通常モードの処理
  return Math.random();
}

自動生成☆詰将棋でのテスト例


 自動生成☆詰将棋では、以下のようにテストを実行し、効果を得ています。

詰み手順探索処理のテスト例


 詰み手順探索処理では、以下の配列データを用いて、詰将棋の問題と解答、表示される文言等が一致するかを確認しています。 詰む例に注意が行きがちですが、詰まない例も重要です。

詰む例

[ '3手詰▽1一玉▽1四歩▽3一銀▲2三金▲香' , 
  '(1/3) ▲1三香; (2/3) ▽2一玉[合い利かず]; (3/3) ▲1二香成' , 
  '詰みました' ]

詰まない例

[ '1手詰▽1一玉▽1三歩▽3一香▲2二香▲2三金▲歩' ,
  '(1/1) ▲1二歩' ,
  '打ち歩詰め(反則)です' ],

詰将棋問題の自動生成のテスト例


 詰将棋問題の自動生成部分のテストは、乱数を初期化してから、詰将棋問題を1つ生成し、文字列として結果を比較しています。 このような形です。
// 1手詰 設定0
[ 1 ,  0 , '1手詰▽2一玉▽3一金▲1三金▲2三金' ],
// 3手詰 設定5
[ 3 ,  5 , '3手詰▽1二玉▲1一銀▲3一銀▲3四銀▲金' ],

自動生成☆詰将棋における効果


【技術解説】複雑なロジックこそテストを:詰将棋開発で学ぶテスト駆動パフォーマンス向上【自動生成☆詰将棋】 テスト駆動パフォーマンス向上
 効果としては、テストのおかげで、結果が変わっていないことを確認でき、安心して改修できる部分が大きいです。

パフォーマンス向上への安心感


 パフォーマンス向上を目的とした場合、無駄な処理の削除などを行います。 考えた上で無駄だと判断した処理を削除しているので、この改修が不具合に繋がることはほぼ無いのですが、 テストを実施することで、変わっていないという安心感を持てるのが大きいです。

特定条件下での不具合発見


 実際に不具合を発見した事例もあります。
 この記事で解説した、 ハッシュテーブル追加時に発生した不具合は、テストを実施したことによりで発見できました。
 少ない駒数の詰将棋では問題が発生しないため、自分で簡単に確認した範囲では、不具合に気づきませんでした。 しかし駒が多くなると結果が変わるため、一部テストが失敗し、発見できました。
 このように意図せぬ変更になってしまったことを早期に発見できるという、明確な効果があります。

パフォーマンス測定


 テストに要する時間を測定することで、実際のパフォーマンスを測定することができました。 例えば以下のように高速化ができていることが、テストをすることでわかります。
設定
最適化前
最適化後
1手詰0
325ms
258ms
1手詰1
331ms
258ms
1手詰2
333ms
267ms
1手詰3
1133ms
1068ms

テスト導入のデメリット


 テストを導入するデメリットについても記載します。

実装工数


 テストを実装する工数はどうしても必要です。
 関数を作る度にテストを書く場合、ある意味で倍の作業が必要です。
 対策としては、気づかず不具合になりそうなところなど、効果的だと考えられる関数に絞ってテストを書くのも一案です。
 もちろん、チームのルール上あるいは顧客納品条件としてテストが必要であれば、時間をしっかりかけてすべてに対してテスト記載をするべきでしょう。

正解の変更が必要な場合


 自動生成☆詰将棋では、テストを記載後に、パフォーマンス向上のため、玉の移動において、相手が王手した駒を取れるならその手を最優先とすることにしました。
 この変更により、詰まない手順をより早く発見できるケースが増えました。 詰手順探索処理は、詰まないパターンを発見すると、そこで処理を打ち切りますので、 この変更によりパフォーマンスが向上します。
 このとき、詰まないという結果は変わりませんが、王手解除の具体的な方法は変わる場合があります。
 つまりテストで差分が生じます。
 しかしこの例では、最新の方を採用すべきですので、 あらかじめ用意しておいた「正解」の方を変更する必要が生じます。
 この手間は、頻度が多いとかなり厄介です。 そのため、しっかりと関数仕様を固めてから、テストを導入できるとベターです。
[PR]

まとめ


 本記事では、テスト駆動によるパフォーマンス向上について解説しました。 また、JavaScript特有の工夫点も解説しました。
 デメリットでも書いたように、テストを記述するには時間が必要です。 それを無駄に考える方もいらっしゃるかもしれませんが、 不具合を早期発見できますし、安心して改修ができるところもありますので、結果として効率的になることは少なくありません。
 特に詰将棋のような複雑なロジックではテストの価値が高く、 本記事で紹介したようにテストする箇所を限定すれば、最小限の手間で、大きな効果を得られます。
 これらの紹介が、少しでも、皆さまの開発保守効率の向上に繋がりましたら幸いです。

補足

  • 記事の校正/添削に生成AIの Anthropic Claude を利用しております。
  • 記事内の画像の作成に生成AIの OpenAI ChatGPT を利用しております。
  • 画像内のラスタライズ文字フォントにOpen Font LicenseNoto Sans Japaneseを使用しております。

カテゴリー:自動生成☆詰将棋,プログラミング解説
[PR]