Dagaz:簡単から複雑へ

画像 かたつぶりそそろそそろ登れ富士の山



富士の坂道で静かにい、カタツムリ、

高さまで



小林一茶(小林一茶)



結果として得たいものについて多くのことを書きました彼はそれをどのように使うかを話しましたが、答えられていない1つの簡単な質問を残しました。 なぜ私はこのすべて(大丈夫、 ほとんどすべて)が機能すると確信しているのですか? 私には秘密兵器があります! そして今日、私は彼について話したいです。



私が書いているプロジェクトは複雑です。 ボードゲームの記述に適した可能性のあるユニバーサルモデルを開発しています。 このようなプロジェクトをゼロから開発する方法について考える必要はありません。開始して、動作するかどうかを確認してください。 さらに、実行するものはまだありません。 このモデルを起動できるコントローラーも圧倒的なビューもありません。 しかし、ここで記述されたコードをチェックしてデバッグする必要があります! その後、コントローラーとビューが表示されると、すべてをデバッグすることはまったく不可能になります!



私はそのような問題に遭遇する最初の人ではなく、それを解決する方法は長い間発明てきました 。 コードをテストするにはQUnitを使用しますが、もちろん、これがJavaScriptの世界で唯一のソリューションではありません。 コードの記述にテストを先行させることはしないという点で、 TDD方法論には固執していませんが、可能な限りテストでモデルコード全体をカバーしようとしています。 これにより、次の問題を解決できます。





このアプローチはすでに正当化されています
開発の最初の段階で、JavaScriptを使用してまだ「あなた」 にいとき 、私はJoclyコードを基盤としていました 。 今、私はその時に書かれたものの多くを取り除く必要がありますが、その瞬間、私はどこかから始めなければなりませんでした。 私はこのタスクをよく理解していました(時間が示すように、十分ではありませんでした)が、言語の知識は非常に乏しかったです。 これらの時代のコードサンプルの1つを次に示します。



配列内のアイテムを検索する
if ([].indexOf) { Model.find = function(array, value) { return array.indexOf(value); } } else { Model.find = function(array, value) { for (var i = 0; i < array.length; i++) { if (array[i] === value) return i; } return -1; } }
      
      





はい、時期尚早な最適化。 配列が「 indexOf 」をサポートしている場合はそれを使用し、そうでない場合はループ内で手動で検索します。 数値のみで動作するように最初からモデルを構築したため、しばらくしてから、他の何かを最適化することにしました。



整数値の配列
 if (typeof Int32Array !== "undefined") { Model.int32Array = function(array) { var a = new Int32Array(array.length); a.set(array); return a; } } else { Model.int32Array = function(array) { return array; } }
      
      





論理は同じです。 数値配列を使用できる人は、残りはできる限り使用します。 しばらくの間、これはすべてうまくいきました。 使用したブラウザで。 しかし、ある晴れた日、IE11でテストを実行しました。 そして、Microsoftの作成はストライキが遅くなかった。 テストは機能しませんでした。 すべてがこの修正につながりました。 このコードの方がはるかに優れているとは言いたくありません(現在は書き直されています)が、テストを定期的に異なるプラットフォームで実行していなければ、この問題について知らなかったでしょう! 単体テストは本当に機能します。



テストを開発することで、単純なコードからより複雑なコードに移行します。 移動の生成の複雑なロジックをチェックする前に(これがモデルが行う主なことです)、使用されるすべてのパーツが正しく機能することを確認する必要があります。 私のモデルで使用されるすべてのクラスは、「複雑さ」を増やすことでランク付けできます。





ZrfPieceクラス非常に単純であるため、テストでは完全なゲームデザインさえ必要ありません。 ただし、検証が必要な非自明な機能がいくつかあります。 たとえば、図のタイプ、所有者、または属性の一部を変更するときに新しいオブジェクトを作成するロジック。



これはすべて基本的なチェックです。
 QUnit.test( "Piece", function( assert ) { var design = Model.Game.getDesign(); design.addPlayer("White", []); design.addPlayer("Black", []); design.addPiece("Man", 0); design.addPiece("King", 1); var man = Model.Game.createPiece(0, 1); assert.equal( man.toString(), "White Man", "White Man"); var king = man.promote(1); assert.ok( king !== man, "Promoted Man"); assert.equal( king.toString(), "White King", "White King"); assert.equal( man.getValue(0), null, "Non existent value"); var piece = man.setValue(0, true); assert.ok( piece !== man, "Non mutable pieces"); assert.ok( piece.getValue(0) === true, "Existent value"); piece = piece.setValue(0, false); assert.ok( piece.getValue(0) === false, "Reset value"); var p = piece.setValue(0, false); assert.equal( piece, p, "Value not changed"); Model.Game.design = undefined; });
      
      





最小限の「デザイン」(2人のプレーヤー、2種類のピース、ボードのヒントなし)を手動で作成し、関心のあるすべてのチェックを手動で実行します。 その後、私たちはZrfPieceを静かに使用します。 後で何かを確認するのを忘れたことが判明した場合でも、さらにいくつかのチェックを追加します。 次に、より複雑なコードをテストします。



ゲームデザイン
 QUnit.test( "Design", function( assert ) { var design = Model.Game.getDesign(); design.addDirection("w"); design.addDirection("e"); design.addDirection("s"); design.addDirection("n"); assert.equal( design.dirs.length, 4, "Directions"); design.addPlayer("White", [1, 0, 3, 2]); design.addPlayer("Black", [0, 1, 3, 2]); assert.equal( design.players[0].length, 4, "Opposite"); assert.equal( design.players[2].length, 4, "Symmetry"); design.addPosition("a2", [ 0, 1, 2, 0]); design.addPosition("b2", [-1, 0, 2, 0]); design.addPosition("a1", [ 0, 1, 0, -2]); design.addPosition("b1", [-1, 0, 0, -2]); var pos = 2; assert.equal( design.positionNames.length,4, "Positions"); assert.equal( Model.Game.posToString(pos), "a1", "Start position"); pos = design.navigate(1, pos, 3); assert.equal( Model.Game.posToString(pos), "a2", "Player A moving"); pos = design.navigate(2, pos, 3); assert.equal( Model.Game.posToString(pos), "a1", "Player B moving"); ... Model.Game.design = undefined; });
      
      





ZrfDesignは、99%のゲームボードナビゲーションです。 確認します。 デザインを手動で作成し(現在は小さなボードを使用)、その後、最も一般的なテストケースを実行します。 最後に、作成したデザインをクリアすることを忘れないでください! 彼が他のテストを壊さないように。



ところで、今では判明しました
ゲームのデザインをシングルトンと考えたとき、私は非常に間違っていました! サーバーバージョンは言うまでもなく、複数の異なるゲームモデルを同時に操作できる必要があるだけでなく、別の興味深いケースがあります。 最も単純なゲームボットに取り組んで、 素晴らしいゲームを思い出しました。





地雷は地中に散らばっていますが、敵を知っていれば、どのように敵を引き寄せますか? 結局のところ、彼が単に「採掘された」フィールドに立って彼の姿を失う理由はまったくありません。 タスクは簡単に解決されます。 使用するボットは、わずかに異なるゲームデザインを取得できます。 ボード、ピースを移動するためのルール-すべては同じですが、1つの小さな例外があります。 彼は地雷について何も知りません。



実際、これは、コンピューターにすべての数字を見せたくないが、 KriegspielLuzhanqiなどの不完全な情報を持つゲームを実装する唯一の適切な方法です。 いずれにせよ、私は今取り組んでいます。 そして、ユニットテストはこれで再び助けになります! このような大規模なリファクタリングを実行する場合、何もバラバラになっていないことを知ることが重要です!



さらに、テストはますます高レベルになっています。 ZrfMoveGeneratorクラスを使用してテンプレートに従って単一の動きを生成し、その動きをゲーム状態に適用し、最後に特定の位置で一連の動きを生成します。



女性による数体の戦い
 QUnit.test( "King's capturing chain", function( assert ) { Model.Game.InitGame(); var design = Model.Game.getDesign(); var board = Model.Game.getInitBoard(); board.clear(); assert.equal( board.moves.length, 0, "No board moves"); design.setup("White", "King", Model.Game.stringToPos("d4")); design.setup("Black", "Man", Model.Game.stringToPos("c4")); design.setup("Black", "Man", Model.Game.stringToPos("a6")); design.setup("Black", "Man", Model.Game.stringToPos("f8")); board.generate(); assert.equal( board.moves.length, 2, "2 moves generated"); assert.equal( board.moves[0].toString(), "d4 - a4 - a8 - g8 x c4 x a6 x f8", "d4 - a4 - a8 - g8 x c4 x a6 x f8"); assert.equal( board.moves[1].toString(), "d4 - a4 - a8 - h8 x c4 x a6 x f8", "d4 - a4 - a8 - h8 x c4 x a6 x f8"); Model.Game.design = undefined; Model.Game.board = undefined; });
      
      





テストの簡潔さのために、これはほぼ完全なゲームです! いずれにせよ、そこから移動します。 ここでは、複合移動と、「移動」ピースと、ゲーム拡張として実装され、可能な限り最大数の敵ピースのキャプチャを必要とする多数決ルールでさえも優先されるムーブがテストされます! この小さなテストは、モデルのほぼすべての機能を対象としています。 そして、何かが壊れると、それを見てすぐに修正します。



単体テストが役立つもう1つのことは、リファクタリングです! ある時点で、プロジェクトでUnderscore 使用することを決定しました。 この素晴らしいライブラリは、 機能的なスタイルでコードを記述し、より簡潔で保守しやすくするのに役立ちます。 それを明確にするために、プロジェクトの生涯の一例を挙げます。



関数型プログラミングは、タスクが難しいほど有用です。 コードが完全に単純な場合、機能的なスタイルで書き直してもほとんど効果はありません。 ただし、タスクがもう少し複雑な場合は、機能的アプローチの利点がより明白になります。





このゲームを覚えていますか? 彼女には2つの面白いルールがあります。





キーワードをマークしました。 「プレイヤーが敵の石を捕まえることができる」とはどういう意味ですか? 対戦相手のボードにN個のピース​​がある場合、まさにこの回数、各ムーブを複製する必要があり、「行」の構築につながります。 これらの動きは、撮影した数字だけが異なります! Zillions of Gamesでは、まさにこれが行われています。 そして、これはゲームの実装を想像を絶するほど複雑にします! しかし、まだ「飛ぶ」石のルールがあります...



別の解決策があります。 私たちは1つの動きを形成することができ、そこに潜在的な捕獲のすべての位置をリストします。 もちろん、これは私たちがすべてではなく、すべての石を取ることを意味しません! リストされているものの1つだけが取得されます。 コースは非決定論的になります。 ムーブメントでも同じです。 「飛んでいる」石が「列」を作ることができる場合、結果は敵のフィギュアによって占められた位置のセットで結果として生じるすべての動きのデカルト積です。



ユーザーインターフェイスがこのような動きに対応できる良い方法を思いつきましたが、AIボットには適用できません! AIは厳密に決定論的な動きを受けなければなりません! これは、非決定論的な動きを決定論に変えるメカニズムがなければならないことを意味します。



これは私がかつて書いたものの最初のバージョンです
 var getIx = function(x, ix, mx) { if (ix > x.length) { x = []; return null; } if (ix == x.length) { c.push(0); return 0; } var r = x[ix]; if (r >= mx) { if (ix + 1 >= x.length) { x = []; return null; } for (var i = 0; i <= ix; i++) { x[ix] = 0; } x[ix + 1]++; } return r; } ZrfMove.prototype.determinate = function() { var r = []; for (var x = [0]; x.length > 0; x[0]++) { var m = Model.Game.createMove(); var ix = 0; for (var i in this.actions) { var k = 0; var fp = this.actions[i][0]; if (fp !== null) { k = getIx(x, ix++, fp.length); if (k === null) { break; } fp = [ fp[k] ]; } var tp = this.actions[i][1]; if (tp !== null) { k = getIx(x, ix++, tp.length); if (k === null) { break; } tp = [ tp[k] ]; } var pc = this.actions[i][2]; if (pc !== null) { k = getIx(x, ix++, pc.length); if (k === null) { break; } pc = [ pc[k] ]; } var pn = this.actions[i][3]; m.actions.push([fp, tp, pc, pn]); } r.push(m); } return r; }
      
      





完全に理解不能で絶対にサポートされていない60行のコード! おそらく動作しません! 私はそれをテストしていません。



代わりに、書き直しました
 ZrfMove.prototype.getControlList = function() { return _.chain(this.actions) .map(function (action) { return _.chain(_.range(3)) .map(function (ix) { if (action[ix] === null) { return 0; } else { return action[ix].length; } }) .filter(function (n) { return n > 1; }) .value(); }) .flatten() .map(function (n) { return _.range(n); }) .cartesian() .value(); } ZrfMove.prototype.determinate = function() { var c = this.getControlList(); if (c.length > 1) { return _.chain(c) .map(function (l) { var r = new ZrfMove(); var pos = 0; _.each(this.actions, function (action) { var x = []; _.each(_.range(3), function (ix) { pos = pushItem(this, action[ix], l, pos); }, x); x.push(action[3]); if (isValidAction(x)) { this.actions.push(x); } }, r); return r; }, this) .filter(isValidMove) .value(); } else { return [ this ]; } }
      
      





コードは長くなっており、一見すると、より理解しやすいようには見えません。 しかし、それを詳しく見てみましょう。 始めるために、問題を理解してみましょう。 コースの説明( ZrfMove )は一連のアクション( actions )で構成され、各アクションは4つの要素のタプルです:



  1. 開始位置( from
  2. 終了位置(
  3. 図( ピース
  4. 部分ストローク数( num


ミルでは図形の変換は行われず、複合移動は使用されないため、これらの値の最初の2つだけが重要です。 実行されたアクションを説明するには十分です。





しかし、これは戦いの半分に過ぎません! 実際、 fromtoの両方(そして、それについてではなく、 ピースでもあります)も配列です! 移動が決定的である場合、これらの各配列には要素が1つだけ含まれます。 それらのいずれかに、より多くの値が存在するということは、選択の可能性を意味します(これを処理する必要があります)。



非決定的ストローク
 var m = [ [ [0], [1, 2] ], [ [3, 4, 5], null ] ]; //   
      
      





位置0から2つの位置( 1または2 )のいずれかへのフィギュアの移動と、位置3、4または5からの1つの敵のフィギュアのキャプチャがあります。 まず、すべての「非決定的」位置(複数の要素を含む)のサイズを選択できます。



コード
 m = _.map(m, function(action) { return _.chain(_.range(2)) .map(function (ix) { if (action[ix] === null) { return 0; } else { return action[ix].length; } }) .filter(function (n) { return n > 1; }) .value(); });
      
      



結果
 m == [ [2], [3] ] //  "" 
      
      





この配列は「平滑化」された後、各数値を範囲に変換します。



コード


m = _.chain(m)

.flatten()

.map(関数(n){return _.range(n);})

.value();

結果
 m == [ [0, 1], [0, 1, 2] ]
      
      





ここで、基本的なUnderscore.js構成では提供されていない操作が必要です。 デカルト積のようなもの。 心配する必要はありません。



自分で書いて
 var cartesian = function(r, prefix, arr) { if (arr.length > 0) { _.each(_.first(arr), function (n) { var x = _.clone(prefix); x.push(n); cartesian(r, x, _.rest(arr)); }); } else { r.push(prefix); } }
      
      



Underscore.jsに埋め込みます
 _.mixin({ cartesian: function(x) { var r = []; cartesian(r, [], x); return r; } });
      
      





私の決定は完全に「コーシャ」ではないことを認めます。 誰かがより良い方法を知っているなら、コメントに書いてください。 適用してください:



コード
  _.chain(m) .map(function(action) { return _.chain(_.range(2)) .map(function (ix) { if (action[ix] === null) { return 0; } else { return action[ix].length; } }) .filter(function (n) { return n > 1; }) .value(); }) .flatten() .map(function (n) { return _.range(n); }) .cartesian() .value();
      
      



結果
 [ [0, 0], [0, 1], [0, 2], [1, 0], [1, 1], [1, 2] ]
      
      





残りのタスクは少し複雑です。 既存のベビーベッドに従って、ストロークの初期バージョンから「非決定的」位置を選択する必要があります。 私はこれで読者を悩ませません、タスクは純粋に技術的です。 最も重要なことは、機能的なアプローチを使用することで、かなり複雑なタスクを個別に解決およびデバッグできる部分に分割できることです。



もちろん、機能的なアプローチの使用は、そのようなパズルの解決策と常に関連付けられているわけではありません。 通常、すべてがいくぶん単純です。 典型的な例として、 maximal-capturesモジュールを挙げることができます。このモジュールは、Zillions of Gamesから継承されたオプションを実装し、ドラフトファミリのゲームで最大数のピースが取得されるようにします。



だった
 Model.Game.PostActions = function(board) { PostActions(board); if (mode !== 0) { var moves = []; var mx = 0; var mk = 0; for (var i in board.moves) { var vl = 0; var kv = 0; for (var j in board.moves[i].actions) { var fp = board.moves[i].actions[j][0]; var tp = board.moves[i].actions[j][1]; if (tp === null) { var piece = board.getPiece(fp[0]); if (piece !== null) { if (piece.type > 0) { kv++; } vl++; } } } if (vl > mx) { mx = vl; } if (kv > mk) { mk = kv; } } for (var i in board.moves) { var vl = 0; var kv = 0; for (var j in board.moves[i].actions) { var fp = board.moves[i].actions[j][0]; var tp = board.moves[i].actions[j][1]; if (tp === null) { var piece = board.getPiece(fp[0]); if (piece !== null) { if (piece.type > 0) { kv++; } vl++; } } } if ((mode === 2) && (mk > 0)) { if (kv == mk) { moves.push(board.moves[i]); } } else { if (vl == mx) { moves.push(board.moves[i]); } } } board.moves = moves; } }
      
      



...そしてそうなった
 Model.Game.PostActions = function(board) { PostActions(board); var captures = function(move) { return _.chain(move.actions) .filter(function(action) { return (action[0] !== null) && (action[1] === null); }) .map(function(action) { return board.getPiece(action[0]); }) .compact() .map(function(piece) { return piece.type; }) .countBy(function(type) { return (type === 0) ? "Mans" : "Kings"; }) .defaults({ Mans: 0, Kings: 0 }) .value(); }; if (mode !== 0) { var caps = _.map(board.moves, captures); var all = _.chain(caps) .map(function(captured) { return captured.Mans + captured.Kings; }) .max() .value(); var kings = _.chain(caps) .map(function(captured) { return captured.Kings; }) .max() .value(); board.moves = _.chain(board.moves) .filter(function(move) { var c = captures(move); if ((mode === 2) && (kings > 0)) { return c.Kings >= kings; } else { return c.Mans + c.Kings >= all; } }) .value(); } }
      
      





両方のオプションは非常にうまく機能します(コードはリファクタリング時にテストですでにカバーされていました)が、機能バージョンは短く、理解しやすく、統合ブロックから組み立てられます。 それを維持することは確かにはるかに簡単です。



記事の結論として、私の仕事で従おうとするいくつかの原則を表明したいと思います。 決してそれらから教義を作りたくはありませんが、彼らは私を助けます。



線のない日ではない
プロジェクトの作業を中断しないでください! いずれにせよ、どのくらいの間。 休憩が長ければ長いほど、仕事に戻るのが難しくなります。 毎日やれば、作業にかかる時間と労力ははるかに少なくなります。 少なくとも少し! これは、コードを「絞り込めない」ことを「私はできない」ということを意味しません(しばらくの間、燃え尽きます)。 プロジェクトが複雑で興味深い場合は、常に気分で仕事を見つけることができます。

朝のコード、夜のテスト
はい、はい、私は知っています、これはTDD方法論に完全に反します。 しかし、誰が私がそれに固執すると言ったのですか? ユニットテストは、最前線に置かなくても(非常に)便利です! 単純なコードでも複雑なコードでも、可能な限りテストでカバーする必要があります。 この場合、単純な機能から複雑な機能に移行し、それが構築されている機能の操作性に疑いの余地がない場合は、より複雑な機能をテストすることが望ましいです。 テストは、関連性が失われるまで削除しないでください! それどころか、さまざまな環境でできるだけ頻繁に実行する必要があります。 このようにして、いくつかの非常に重要な重大な間違いを見つけました!

害を及ぼさない
変更によって、すでにテストでカバーされているコードが壊れてはいけません! 壊れたテストでコードをコミットする必要はありません。 たとえこのコードに終日取り組んだとしても! ここでの問題に対処するのに非常に疲れていても! 動作しないコードはゴミです。 これはいつでも爆発する爆弾です! それに対処する強さがなければ、それを完全に削除し、再度書き直すことをお勧めします。

あきらめない
コードを何度も書き換えることを恐れないでください! 少しのリファクタリング、またはプロジェクトの完全な書き直しさえも-これはパニックの理由ではありません! これは問題をより良く解決する機会です。

正直に勝てないなら、ただ勝て
良いコードは問題を解決します。 優れたコードは理解可能であり、保守可能です。 それだけです! OOPFP、または他の何かのイデオロギーの下でそれを「合わせる」ために裏返す必要はありません! 言語またはその環境のいくつかの機能を使用して、ファッションについてではなく、プロジェクトに対するこれらの「機能」の有用性についてのみ考える必要があります。 あなたはまだファッションに追いつくことができません!



もちろん、私にはまだ成長する余地があります。 私はこれを問題とは思わない。 言語に対する私の理解は変わりつつあり(より良いものになることを願っています)、それに伴いコードも変わります。 単体テストはそれを助けてくれます。




All Articles