web-dev-qa-db-ja.com

関数型言語の2次元ボードゲームのデータ構造

関数型プログラミング言語Elixirで簡単な MiniMax 実装を作成しています。多くの完全な知識のあるゲーム(三目並べ、コネクトフォー、チェッカー、チェスなど)があるため、この実装は、これらのゲームのゲームAIを作成するためのフレームワークになる可能性があります。

ただし、私が直面している問題の1つは、ゲームの状態を関数型言語で適切に保存する方法です。これらのゲームは、主に次の操作が頻繁に行われる2次元のゲームボードを扱います。

  • 特定のボードの場所の内容を読む
  • 特定のボードの場所の内容を更新する(新しい移動の可能性を返すとき)
  • 現在の場所に接続されている1つまたは複数の場所のコンテンツを検討する(つまり、次または前の水平、垂直、または対角の場所)
  • 複数の接続された場所のコンテンツを任意の方向で検討します。
  • ファイル全体、ランク、対角線の内容を考慮します。
  • ボードを回転またはミラーリングします(既に計算されたものと同じ結果が得られる対称性をチェックするため)。

ほとんどの関数型言語は、多要素データ構造の基本的なビルディングブロックとしてリンクリストとタプルを使用します。しかし、これらは仕事のために非常にひどく作られたようです:

  • リンクされたリストはO(n)(linear)lookup timeを持っています。また、ボード」をボード上で1回のスイープで実行し、リストを使用することは非常に非現実的です。
  • タプルはO(1)(定数)ルックアップ時間を持っています。ただし、ボードを固定サイズのタプルとして表すランク、ファイル、対角線、または他の種類の連続する四角形を反復処理するのが非常に困難になります。また、エリクサーとハスケル(どちらも知っている2つの関数型言語)には、nタプルの要素。これにより、任意のサイズのボードで機能する動的ソリューションを作成することができなくなります。

ElixirにはMapデータ構造が組み込まれています(そしてHaskellにはData.Map)要素へのO(log n)(対数)アクセスを許可します。現在、私はx, y位置をキーとして表すタプル。

これは「うまくいく」が、このようにマップを乱用するのは間違っていると感じます。理由は正確にはわかりません2次元ゲームボードを関数型プログラミング言語で格納するためのより良い方法を探しています。

9
Qqwy

ここでは、Mapがまさに正しい基本データ構造です。なぜそれがあなたを不安にするのか分かりません。ルックアップ時間と更新時間が適切で、サイズが動的であり、派生データ構造を簡単に作成できます。例(haskell内):

filterWithKey (\k _ -> (snd k) == column) -- All pieces in given column
filterWithKey (\k _ -> (fst k) == row)    -- All pieces in given row
mapKeys (\(x, y) -> (-x, y))              -- Mirror

プログラマが最初に完全な不変性でプログラミングを開始するときに把握することがしばしば難しいもう1つのことは、1つのデータ構造だけに固執する必要がないことです。通常、「真実のソース」として1つのデータ構造を選択しますが、デリバティブのデリバティブでさえ、必要な数だけデリバティブを作成でき、必要な限り同期が維持されることがわかっています。

つまり、最上位レベルでMapを使用し、次にListsまたはArraysに切り替えて行を分析し、次にArraysを使用して別の方法で列にインデックスを付けることができます分析、次にパターン分析用のビットマスク、表示用のStrings。適切に設計された関数型プログラムは、単一のデータ構造を渡しません。これらは一連のステップであり、1つのデータ構造を取り込み、次のステップに適した新しいデータ構造を生成します。

トップレベルが理解できる形式での移動で反対側に出られる限り、その間のデータをどれだけ再構築するかを気にする必要はありません。それは不変なので、トップレベルで真実のソースへのパスをたどることができます。

6
Karl Bielefeldt

私は最近F#でこれを行い、1次元のリストを使用することになりました(F#では、単一リンクリストです)。実際には、O(n)リストインデクサーの速度は、人間が使用できるボードサイズのボトルネックではありません。2D配列などの他のタイプを試してみましたが、最終的には-独自の値の等価性チェックコードを作成するか、ランクとファイルをインデックスに変換して変換します。後者の方が簡単でした。最初に機能させ、後で必要に応じてデータ型を最適化します。重要なほど大きな違いを生む可能性は低いです。

結局、ボードの操作がボードのタイプと操作によって適切にカプセル化されている限り、実装はそれほど重要ではありません。たとえば、ボードを設定するためのテストの例を次に示します。

let pos r f = {Rank = r; File = f} // immutable record type
// or
let pos r f = OnBoard (r, f) // algebraic type
...
let testBoard =
    Board.createEmpty ()
    |> Board.setPiece p (pos 1 2)
    |> ...

このコード(または呼び出しコード)では、ボードがどのように表されているかは問題ではありません。ボードは、その基盤となる構造よりも、その上の操作によって表されます。

4
Kasey Speakman