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


9

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

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

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

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

  • リンクされたリストには、O(n)(線形)ルックアップ時間があります。また、ボードを1回スイープして「ボードをスキャンして更新」することはできないため、リストを使用することは非常に非現実的です。
  • タプルのルックアップ時間はO(1)(一定)です。ただし、ボードを固定サイズのタプルとして表すと、ランク、ファイル、対角線、または他の種類の連続する正方形を反復処理することが非常に難しくなります。また、ElixirとHaskell(どちらも知っている2つの関数型言語)には、タプルのn番目の要素を読み取るための構文がありません。これは、任意のサイズのボードで機能する動的ソリューションを作成することを不可能にします。

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

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


1
私はプラクシスについて話すことはできませんが、Haskellから2つのことが頭に浮かびます:ジッパー、データ構造に対する一定時間の「ステップ」を可能にします。 )
phipsgabler 2016年

この再生盤の大きさは?Big Oは、アルゴリズムのスケーリングを特徴付けるものであり、アルゴリズムの速度ではありません。小さなボード(たとえば、各方向で100未満)では、O(1)とO(n)は、各正方形に1回だけ触れる場合、それほど重要ではありません。
Robert Harvey

@RobertHarveyそれは異なります。しかし例を挙げましょう:チェスには64x64のボードがありますが、可能な動きを確認し、現在の位置のヒューリスティック値(マテリアルの違い、チェックのキングの有無、ポーンのパスなど)を決定するすべての計算すべてがボードの正方形にアクセスする必要があります。
Qqwy

1
チェスの8x8ボードがあります。Cのようなメモリマップ言語では、セルの正確なアドレスを取得するために数学的な計算を行うことができますが、これはメモリ管理言語では当てはまりません(順序アドレッシングは実装の詳細です)。14ノード(最大)をジャンプするのに、メモリ管理された言語で配列要素をアドレス指定するのとほぼ同じ時間がかかるとしても、私は驚くことではありません。
ロバートハーベイ

回答:


6

Mapここで、A はまさに正しい基本データ構造です。なぜそれがあなたを不安にさせるのか分かりません。ルックアップと更新の時間が適切で、サイズが動的で、派生データ構造を簡単に作成できます。例(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つのデータ構造を取り込み、次のステップに適した新しいデータ構造を生成します。

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


4

私は最近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)
    |> ...

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


こんにちは、どこかにコードが公開されていますか?私は現在、F#(楽しいプロジェクト)でチェスのようなゲームに取り組んでおり、ボードを表すためにMap <Square、Piece>を使用している場合でも、ボードにカプセル化する方法を確認したいですタイプとモジュール。
asibahi

いいえ、どこにも公開されていません。
Kasey Speakman、2016年

私の現在の実装を見て、どうすればそれを改善できるかアドバイスしていただけませんか。
asibahi

タイプをちらっと見て、Positionタイプに到達するまで、非常によく似た実装に落ち着きました。後でコードレビューの詳細な分析を行います。
Kasey Speakman、2016年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.