Estou criando uma simples implementação MiniMax na linguagem de programação funcional Elixir. Como existem muitos jogos de conhecimento perfeito (jogo da velha, jogo quatro, damas, xadrez, etc), essa implementação pode ser uma estrutura para criar AIs de jogos para qualquer um desses jogos.
Um problema que estou enfrentando, no entanto, é como armazenar adequadamente um estado de jogo em uma linguagem funcional. Esses jogos lidam principalmente com tabuleiros de jogo bidimensionais, onde as seguintes operações são frequentes:
- Leia o conteúdo de um local específico do quadro
- Atualize o conteúdo de um local específico do quadro (ao retornar uma nova possibilidade de movimento)
- Considerando o conteúdo de um ou mais locais conectados ao local atual (ou seja, o próximo ou anterior local horizontal, vertical ou diagonal)
- Considerando o conteúdo de vários locais conectados em qualquer direção.
- Considerando o conteúdo de arquivos inteiros, classificações e diagonais.
- Girar ou espelhar a placa (para verificar simetrias que fornecem o mesmo resultado que algo já calculado).
A maioria das linguagens funcionais usa listas e tuplas vinculadas como blocos básicos de construção de estruturas de dados com vários elementos. No entanto, estes parecem muito mal feitos para o trabalho:
- As listas vinculadas têm tempo de pesquisa O (n) (linear). Além disso, como não podemos 'digitalizar e atualizar o quadro' em uma única varredura, usar listas parece muito impraticável.
- As tuplas têm tempo de pesquisa O (1) (constante). No entanto, representar o quadro como uma tupla de tamanho fixo dificulta a iteração sobre classificações, arquivos, diagonais ou outros tipos de quadrados consecutivos. Além disso, tanto Elixir, e Haskell (que são as duas línguas funcionais que eu conheço) falta sintaxe para ler o n º elemento de uma tupla. Isso tornaria impossível escrever uma solução dinâmica que funcionasse para placas de tamanho arbitrário.
O Elixir possui uma estrutura de dados de mapa incorporada (que a Haskell possui Data.Map
) que permite acesso a O (log n) (logarítmico) aos elementos. Agora eu uso um mapa, com x, y
tuplas que representam a posição como chaves.
Isso 'funciona', mas parece errado abusar dos mapas dessa maneira, embora eu não saiba exatamente o porquê. Estou procurando uma maneira melhor de armazenar um tabuleiro de jogo bidimensional em uma linguagem de programação funcional.
Respostas:
A
Map
é precisamente a estrutura de dados de base correta aqui. Não sei por que isso a deixaria desconfortável. Tem bons tempos de pesquisa e atualização, é dinâmico em tamanho e é muito fácil criar estruturas de dados derivadas. Por exemplo (em haskell):A outra coisa que é frequentemente difícil para os programadores entenderem quando começam a programar com imutabilidade total é que você não precisa se ater a apenas uma estrutura de dados. Você geralmente escolhe uma estrutura de dados como sua "fonte da verdade", mas pode criar quantos derivados quiser, até derivados de derivados, e sabe que eles permanecerão sincronizados pelo tempo que você precisar.
Isso significa que você pode usar um
Map
no nível mais alto, alternar paraLists
ouArrays
para análise de linha,Arrays
indexar o outro caminho para análise de colunas, máscaras de bits para análise de padrões e, em seguida,Strings
para exibição. Programas funcionais bem projetados não passam uma única estrutura de dados. Eles são uma série de etapas que incorporam uma estrutura de dados e emitem uma nova adequada para a próxima etapa.Desde que você possa sair do outro lado com uma mudança em um formato que o nível superior possa entender, não precisa se preocupar com a reestruturação dos dados. É imutável, por isso é possível traçar um caminho de volta à fonte da verdade no nível superior.
fonte
Fiz isso recentemente em F # e acabei usando uma lista unidimensional (em F #, essa é uma lista de link único). Na prática, a velocidade do indexador da lista O (n) não é um gargalo para tamanhos de placas utilizáveis por humanos. Eu experimentei outros tipos, como matriz 2D, mas no final, foi o compromisso de escrever meu próprio código de verificação de igualdade de valor ou uma tradução de classificação e arquivo para indexar e voltar. O último foi mais simples. Eu diria que funcione primeiro e, em seguida, otimize seu tipo de dados mais tarde, se necessário. Não é provável que faça uma diferença suficientemente grande para importar.
No final, sua implementação não deve importar tanto, desde que as operações da sua diretoria sejam devidamente encapsuladas por tipo e operações da diretoria. Por exemplo, aqui está como alguns dos meus testes podem parecer para montar uma placa:
Para esse código (ou qualquer código de chamada), não importa como a placa foi representada. O conselho é representado pelas operações nele mais do que sua estrutura subjacente.
fonte