Struktur data untuk permainan papan dua dimensi dalam bahasa Fungsional

9

Saya membuat implementasi MiniMax sederhana dalam bahasa pemrograman fungsional Elixir. Karena ada banyak game pengetahuan sempurna (tic tac toe, connect-four, checker, catur, dll), implementasi ini bisa menjadi kerangka kerja untuk membuat AI game untuk game-game ini.

Namun, satu masalah yang saya hadapi adalah bagaimana cara menyimpan kondisi permainan dengan benar dalam bahasa fungsional. Game-game ini sebagian besar berurusan dengan papan permainan dua dimensi, di mana operasi berikut sering dilakukan:

  • Baca isi lokasi papan tertentu
  • Perbarui konten lokasi papan tertentu (saat mengembalikan kemungkinan langkah baru)
  • Mempertimbangkan isi dari satu atau lebih lokasi yang terhubung ke lokasi saat ini (yaitu lokasi horisontal, vertikal atau diagonal berikutnya atau sebelumnya)
  • Mempertimbangkan isi dari beberapa lokasi yang terhubung ke segala arah.
  • Mempertimbangkan isi dari seluruh file, pangkat dan diagonal.
  • Memutar atau menyalin papan (untuk memeriksa simetri yang memberikan hasil yang sama dengan sesuatu yang sudah dihitung).

Sebagian besar bahasa fungsional menggunakan Linked Lists dan Tuples sebagai blok bangunan dasar dari struktur data multi-elemen. Namun, ini tampaknya dibuat sangat buruk untuk pekerjaan itu:

  • Daftar tertaut memiliki waktu pencarian O (n) (linier). Juga, karena kita tidak dapat 'memindai dan memperbarui papan' dalam sekali sapuan di papan, menggunakan daftar tampaknya sangat tidak praktis.
  • Tuples memiliki waktu pencarian O (1) (konstan). Namun, merepresentasikan papan sebagai tuple ukuran tetap membuatnya sangat sulit untuk beralih ke peringkat, file, diagonal, atau jenis kotak lainnya secara berurutan. Juga, baik Elixir, dan Haskell (yang merupakan dua bahasa fungsional yang saya tahu) tidak memiliki sintaks untuk membaca elemen ke- n sebuah tuple. Ini akan membuat mustahil untuk menulis solusi dinamis yang akan bekerja untuk dewan dengan ukuran sewenang-wenang.

Elixir memiliki struktur data Peta bawaan (dan Haskell memiliki Data.Map) yang memungkinkan O (log n) (logaritmik) akses ke elemen. Saat ini saya menggunakan peta, dengan x, ytupel yang mewakili posisi sebagai kunci.

Ini 'berhasil' tapi rasanya salah dengan penyalahgunaan peta dengan cara ini, meskipun saya tidak tahu persis mengapa. Saya mencari cara yang lebih baik untuk menyimpan papan permainan dua dimensi dalam bahasa pemrograman fungsional.

Qqwy
sumber
1
Saya tidak dapat berbicara tentang praksis, tetapi ada dua hal yang muncul dalam pikiran saya dari Haskell: resleting , memungkinkan "langkah" waktu-konstan atas struktur data, dan comonad, yang terkait dengan resleting oleh beberapa teori yang saya tidak ingat atau tidak mengerti dengan baik; )
phipsgabler
Seberapa besar papan bermain ini? Big O mencirikan bagaimana suatu skala algoritma, bukan seberapa cepat itu. Pada papan kecil (katakanlah, kurang dari 100 di setiap arah), O (1) vs O (n) tidak akan terlalu berarti, jika Anda hanya menyentuh setiap kotak satu kali.
Robert Harvey
@RobertHarvey Ini akan bervariasi. Tetapi untuk memberikan contoh: Dalam Catur, kami memiliki papan 64x64, tetapi semua perhitungan untuk memeriksa gerakan apa yang mungkin dilakukan, dan untuk menentukan nilai heuristik posisi saat ini (perbedaan bahan, raja dalam periksa atau tidak, bidak yang disahkan, dll) semua perlu mengakses kotak papan.
Qqwy
1
Anda memiliki papan 8x8 dalam catur. Dalam bahasa yang dipetakan dengan memori seperti C, Anda bisa membuat perhitungan matematis untuk mendapatkan alamat sel yang tepat, tetapi itu tidak benar dalam bahasa yang dikelola memori (di mana pengalamatan ordinal adalah detail implementasi). Itu tidak akan mengejutkan saya jika melompat melintasi (maksimal) 14 node membutuhkan waktu yang kira-kira sama dengan menangani elemen array dalam bahasa yang dikelola memori.
Robert Harvey

Jawaban:

6

A Mapadalah struktur data dasar yang tepat di sini. Saya tidak yakin mengapa itu akan membuat Anda gelisah. Ini memiliki waktu pencarian dan pembaruan yang baik, ukurannya dinamis, dan sangat mudah untuk membuat struktur data turunan. Misalnya (dalam 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

Hal lain yang sering sulit dipahami oleh para programmer ketika mereka pertama kali memulai pemrograman dengan kekekalan penuh adalah Anda tidak perlu hanya berpegang pada satu struktur data saja. Anda biasanya memilih satu struktur data sebagai "sumber kebenaran", tetapi Anda dapat membuat turunan sebanyak yang Anda inginkan, bahkan turunan dari turunan, dan Anda tahu mereka akan tetap disinkronkan selama Anda membutuhkannya.

Itu berarti Anda dapat menggunakan a Mapdi tingkat paling atas, lalu beralih ke Listsatau Arraysuntuk analisis baris, lalu Arraysdiindeks dengan cara lain untuk analisis kolom, kemudian bitmask untuk analisis pola, lalu Stringsuntuk tampilan. Program fungsional yang dirancang dengan baik tidak melewatkan struktur data tunggal. Mereka adalah serangkaian langkah yang mengambil satu struktur data dan memancarkan yang baru yang cocok untuk langkah selanjutnya.

Selama Anda bisa keluar dari sisi lain dengan gerakan dalam format yang bisa dipahami oleh tingkat atas, Anda tidak perlu khawatir seberapa besar Anda merestrukturisasi data di antaranya. Itu tidak berubah, jadi mungkin untuk melacak jalan kembali ke sumber kebenaran di tingkat atas.

Karl Bielefeldt
sumber
4

Saya telah melakukan ini baru-baru ini di F # dan saya akhirnya menggunakan daftar satu dimensi (dalam F #, itu adalah daftar yang terhubung tunggal). Dalam praktiknya, kecepatan pengindeks daftar O (n) bukanlah hambatan untuk ukuran papan yang dapat digunakan manusia. Saya bereksperimen dengan tipe lain seperti array 2d, tetapi pada akhirnya, itu merupakan trade-off dari penulisan kode pengecekan nilai saya sendiri atau terjemahan dari peringkat dan file untuk diindeks dan dikembalikan. Yang terakhir lebih sederhana. Saya akan mengatakan membuatnya bekerja lebih dulu, dan kemudian mengoptimalkan tipe data Anda nanti jika diperlukan. Sepertinya tidak akan membuat perbedaan yang cukup besar menjadi masalah.

Pada akhirnya implementasi Anda tidak akan menjadi masalah selama operasi dewan Anda dienkapsulasi dengan benar berdasarkan jenis dan operasi Dewan. Sebagai contoh, berikut adalah bagaimana beberapa tes saya terlihat mengatur papan:

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)
    |> ...

Untuk kode ini (atau kode panggilan apa pun), tidak masalah bagaimana dewan direpresentasikan. Dewan direpresentasikan oleh operasi di atasnya lebih dari struktur yang mendasarinya.

Kasey Speakman
sumber
Halo, apakah Anda memiliki kode yang diterbitkan di suatu tempat? Saat ini saya sedang mengerjakan permainan seperti catur di F # (proyek untuk bersenang-senang), dan bahkan saya menggunakan Peta <Square, Piece> untuk mewakili papan, saya ingin melihat bagaimana Anda merangkumnya ke dalam Papan jenis dan modul.
asibahi
Tidak, ini tidak diterbitkan di mana pun.
Kasey Speakman
jadi maukah Anda melihat implementasi saya saat ini dan menyarankan bagaimana saya bisa memperbaikinya?
asibahi
Melirik jenis-jenisnya, saya memutuskan implementasi yang sangat mirip sampai saya mendapatkan jenis Posisi. Saya akan melakukan analisis yang lebih menyeluruh tentang Tinjauan Kode nanti.
Kasey Speakman