Como se resolve os tipos de baixo nível dependendo dos tipos de alto nível?

5

Eu freqüentemente me deparo com esse problema ao desenvolver jogos e simulações. Por exemplo, atualmente estou escrevendo uma implementação de xadrez. Eu estou usando isso como um veículo para experimentar sistemas de componentes de entidade e cadeias de responsabilidade, inspirado na palestra de Brian Bucklew sobre a arquitetura Roguelike ( link ). Em última análise, gostaria de poder expressar cada componente comportamental do Xadrez como um componente pequeno e encapsulado de uma peça de xadrez e trocá-los por um capricho para criar rapidamente peças com novos comportamentos.

Portanto, eu tenho um Game que tem um Board e o Board pode ter um número Piece s, cada um dos quais pode ter um número Component s. Eu não tenho noção de que tipo a Piece é, em vez disso eu codifico o comportamento deles através de Component s; você pode enviar uma mensagem "IsOwnedByPlayerOne" para determinar de que lado o Piece está, por exemplo:

IComponent.cs :

public interface IComponent
{
    void Handle(ref Message message);
}

Piece.cs :

public bool IsOwnedByPlayerOne()
{
    var isOwnedByPlayerOneMessage = new Message(MessageType.IsOwnedByPlayerOne);
    Handle(ref isOwnedByPlayerOneMessage);

    return isOwnedByPlayerOneMessage.GetParameter<bool>(MessageParameter.IsOwnedByPlayerOne);
}

A questão a que cheguei é a seguinte: um Component é um tipo de baixo nível, mas eles precisam de consciência do mundo ao seu redor, o que só pode ser alcançado através do acesso a tipos de alto nível. Por exemplo, digamos que eu envie uma mensagem "IsLegalMove" para uma Rook Piece , perguntando se ela pode passar legalmente de a1 para a6. Ele precisaria verificar se existem outros Piece s entre a1 e a6, o que requer acesso ao Board . Mas o Board contém o Piece , que por sua vez contém o Component , então, conceitualmente, o design está ficando um pouco maluco.

Eu poderia fornecer Piece ou Component acesso a Board como um campo. Ou eu poderia adicionar Board como um parâmetro ao método I Component '% s Handle() . Não me sinto muito bem em nenhuma dessas soluções, mas não consigo explicar por quê. Qual deles seria melhor alinhado com os princípios do software de qualidade? Ou há outros que você recomendaria?

    
por Jansky 26.05.2018 / 14:50
fonte

5 respostas

3

inspired by Brian Bucklew's talk on Roguelike architecture

O que o Bucklew fez é pegar todos os parâmetros e métodos que normalmente colocamos em uma interface (definindo assim o mini linguagem que os objetos do cliente usam para se comunicar com seus objetos de serviço) e ele os colocou em um evento. Então, agora, a única coisa nas interfaces entre objetos é o único método que aceita eventos.

AindaaquivocêestácomIsOwnedByPlayerOne().Issonãoéumevento.Masvocênãoprecisaseguirométododele.

UltimatelyI'dliketobeabletoexpresseachbehaviouralcomponentofChessasasmall,encapsulatedcomponentofachesspieceandswaptheseoutatawhimtoquicklycreatepieceswithnewbehaviours.

VocêpodefazerissomesmonosidiomasOOPdavelhaescola,sem fazer construções em xml ou crevendo seu próprio carregador de classes . Em vez disso, procure a herança toda vez, em vez disso, aprenda a prefira composição e delegação .

Deixe-me contar como eu criei minha rainha no meu torneio de xadrez . Ela era apenas uma peça como qualquer outra. Mas quando chegou a hora de gerar seus movimentos, ela não podia se incomodar com isso. Ela apenas pediu à torre e ao bispo o que eles poderiam fazer se estivessem no lugar dela.

Eu não fiz isso herdando as classes Rook ou Bishop. Eu compus minha rainha com eles. Eles foram injetados nela quando ela nasceu. Seu conhecimento deles era controlado. Ela nem sabia qual era qual. Ela só sabia que tinha uma equipe de peças que poderia decidir sua lista de mudanças. Ela delegou o trabalho para eles. Tudo o que ela fez foi transformar suas listas em uma lista.

Isso significava que as peças de xadrez de Sambawan de Elefante e Gavião podiam ser adicionadas ao jogo apenas com a injeção do cavaleiro.

The issue I've come to is this: a Component is very much a low-level type, but they need awareness of the world around them which can only be achieved through access to higher-level types.

Este é um problema diferente. Conceitualmente é um pouco difícil. Isso me lembra da relatividade. Deixe-me perguntar-lhe isto: você sabe onde você está? Como você sabe? Se eu te drogasse e levasse você e sua cama e flutuasse em um lago, como você saberia onde você está? Algo tem que te dizer que você está flutuando em um lago.

Foi assim que resolvi esse problema. Eu disse às peças onde elas estavam. Dessa forma, eles não precisam se lembrar e não precisam perguntar.

Para exibir uma peça:

board[rank, file].display(rank, file);

Para gerar movimentos legais de peças:

moves.addAll( board[rank, file].listMoves(board, rank, file) );

Isto segue um princípio que passa por alguns nomes, mas eu gosto de chamar isso de "Diga, não pergunte" .

O design permitiu que o código de uso lidasse com peças polimorficamente (a parte boa de OOP) e funcionalmente. O que significava que poderia apenas dar um loop na placa, que era uma simples matriz 2D de referências de peças e algum estado de jogo. Precisava adicionar um objeto nulo peça que não exibia nada, não oferecia movimentos, que qualquer um pudesse capturar, e isso não bloqueava movimento.

For example, say I send a "IsLegalMove" message to a Rook Piece, asking whether it can legally move from a1 to a6. It would need to check whether there are any other Pieces between a1 and a6, which requires access to the Board. But the Board itself contains the Piece which in turn contains the Component, so conceptually the design is getting a bit loopy.

Não envie mensagens "IsLegalMove". Enviar

newMoveList = board("a2").filterMoves(board, rooksMoveList)

E deixe a2 podar os lances ilegais para você com sua incrível habilidade de saber se é amigo, inimigo ou em branco. Você fará isso não apenas com o quadrado a2, mas com todos os quadrados entre a1 e a6. Você faz isso para cada movimento que a torre pode fazer. Além de facilitar a programação, isso garante que uma torre cercada de peças amistosas ou bordas da prancha não esteja perdendo tempo.

I could give either Piece or Component access to Board as a field. Or I could add Board as a parameter to IComponent'''s Handle() method. I don't feel great about either of these solutions but can't put my finger on why. Which one would be better aligned with quality software principles? Or are there others you'd recommend?

Sentir-se bem com uma abordagem só vem com o tempo. Não significa que está certo. Só que você se sentiu confortável com isso. Depois de décadas nisso, estou muito à vontade para minimizar o que sabe sobre o quê. Se uma peça não precisa saber onde está quando não estou usando, por que lembrar? Deixe o conselho fazer isso.

    
por 26.05.2018 / 21:18
fonte
3

say I send a "IsLegalMove" message to a Rook Piece, asking whether it can legally move from a1 to a6

Como uma peça não pode saber por si própria se uma jogada é legal, eu recomendaria contra esse design. Um pedaço (sabendo sobre seu tipo, cor e posição) pode conhecer as regras onde ele pode ser movido de sua posição atual em um tabuleiro vazio, então você pode dar a ele um método virtual GetListOfPotentialMoves para retornar a lista de lances legais em um tabuleiro vazio (os únicos parâmetros que este método requer são a largura e a altura do tabuleiro). Ele não deve apenas retornar o movimento como uma coordenada final, mas também as coordenadas intermediárias para cada movimento (aquelas que devem estar vazias para uma jogada legal).

O objeto Board , no entanto, pode gerar a lista de lances legais, chamando Piece.GetListOfPotentialMoves e filtrando os resultados de acordo com as regras (que as coordenadas intermediárias devem estar vazias e a coordenada de destino deve estar vazia ou conter um pedaço de cor diferente).

Observe que neste design, a peça não precisa saber o Board e o Board não precisa saber com qual tipo de peça ele trata.

    
por 26.05.2018 / 15:48
fonte
2

Não tenho certeza se esse é um bom design, mas construí uma implementação de 'aprendizado' muito semelhante.

Minha solução foi passar a placa como um parâmetro para IsLegalMove , e a peça chama a diretoria para verificar se ela é legal. A combinação dos dois tem o conhecimento necessário; por exemplo, uma Torre sabe que pode ir ao longo de linhas e colunas (primeira verificação, na classe Rook que é derivada de Peça), e então a Diretoria sabe (segundo nível de verificação) que peças não podem se mover fora da placa, não pode saltar sobre outra peças, e só pode pegar peças do adversário.

Eu estendi isso para que o mesmo mecanismo de jogo possa lidar com muitos jogos; por exemplo, Camelot, Halma e Shogi - que mudanças são apenas as regras que o conselho aplica ao que é permitido (então eu deduzo tabuleiros específicos do jogo da classe de tabuleiro geral).

Eu acho que isso é chamado Dependency Injection (porque você está 'injetando' na Piece o objeto necessário (Board) para lidar com sua dependência do ambiente), mas não tenho certeza se esse caso qualifica-se para o termo DC.

    
por 26.05.2018 / 15:28
fonte
0

Em geral, você precisa especificar o WorldState e o Entity que você deseja manipular para decidir o que fazer.

Como o Entity não atua sem solicitar que alguém saiba o GameState adequado, adicionar um ponteiro a este último é um desperdício, além de inibir a cópia simples.

Você não deve salvar o Entity para atuar no GameState , pois ele muda dependendo do método ativo, e é para isso que os parâmetros de método servem.

Se você chamar um método de comutação no tipo de componente ou chamar uma função de membro virtual do componente, é uma decisão muito sua.

    
por 26.05.2018 / 16:18
fonte
0

Se houver um jogo, tabuleiro e peça, então:

  • Uma diretoria tem campos, que tem as peças.
  • Peças têm atributos (cor, forma, tamanho, podem pular peças)
  • As peças podem ser verificadas por meio de deltas (horizontal, vertical, diagonal) se um movimento for válido.
  • o jogo tem regras do jogo
  • o jogo pergunta / verifica (executa) as regras se um movimento por peça for válido
por 26.05.2018 / 17:04
fonte