Acho que o problema aqui é que você não forneceu uma descrição clara de quais tarefas devem ser tratadas por quais classes. Descreverei o que considero uma boa descrição do que cada classe deve fazer; depois, darei um exemplo de código genérico que ilustra as idéias. Veremos que o código é menos acoplado e, portanto, não possui referências circulares.
Vamos começar descrevendo o que cada classe faz.
A GameState
classe deve conter apenas informações sobre o estado atual do jogo. Não deve conter nenhuma informação sobre o que os estados passados do jogo ou quais movimentos futuros são possíveis. Ele deve conter apenas informações sobre quais peças estão em quais quadrados no xadrez, ou quantas e que tipo de damas existem em quais pontos do gamão. O GameState
arquivo deverá conter algumas informações extras, como informações sobre o jogo de xadrez no xadrez ou sobre o cubo dobrado no gamão.
o Move
aula é um pouco complicada. Eu diria que posso especificar uma jogada para jogar especificando os GameState
resultados da jogada. Então você pode imaginar que uma mudança pode ser implementada como uma GameState
. No entanto, em go (por exemplo), você pode imaginar que é muito mais fácil especificar uma jogada especificando um único ponto no tabuleiro. Queremos que nossa Move
classe seja flexível o suficiente para lidar com qualquer um desses casos. Portanto, a Move
classe realmente será uma interface com um método que realiza uma pré-movimentação GameState
e retorna uma nova pós-movimentação GameState
.
Agora a RuleBook
turma é responsável por saber tudo sobre as regras. Isso pode ser dividido em três coisas. Ele precisa saber qual é a inicial GameState
, precisa saber quais jogadas são legais e precisa saber se um dos jogadores venceu.
Você também pode fazer uma GameHistory
aula para acompanhar todos os movimentos que foram feitos e tudo o GameStates
que aconteceu. Uma nova classe é necessária porque decidimos que uma única GameState
não deveria ser responsável por conhecer todos os GameState
s que vieram antes dela.
Isso conclui as classes / interfaces que discutirei. Você também tem uma Board
aula. Mas acho que tabuleiros em jogos diferentes são diferentes o suficiente para que seja difícil ver o que genericamente poderia ser feito com tabuleiros. Agora vou dar interfaces genéricas e implementar classes genéricas.
Primeiro é GameState
. Como essa classe depende completamente do jogo em particular, não há Gamestate
interface ou classe genérica .
O próximo é Move
. Como eu disse, isso pode ser representado com uma interface que possui um único método que assume um estado de pré-movimentação e produz um estado pós-movimentação. Aqui está o código para esta interface:
package boardgame;
/**
*
* @param <T> The type of GameState
*/
public interface Move<T> {
T makeResultingState(T preMoveState) throws IllegalArgumentException;
}
Observe que existe um parâmetro de tipo. Isso ocorre porque, por exemplo, um ChessMove
precisará saber sobre os detalhes do movimento anterior ChessGameState
. Assim, por exemplo, a declaração de classe de ChessMove
seria
class ChessMove extends Move<ChessGameState>
,
onde você já teria definido uma ChessGameState
classe.
A seguir, discutirei a RuleBook
classe genérica . Aqui está o código:
package boardgame;
import java.util.List;
/**
*
* @param <T> The type of GameState
*/
public interface RuleBook<T> {
T makeInitialState();
List<Move<T>> makeMoveList(T gameState);
StateEvaluation evaluateState(T gameState);
boolean isMoveLegal(Move<T> move, T currentState);
}
Novamente, há um parâmetro de tipo para a GameState
classe. Como RuleBook
é suposto saber qual é o estado inicial, criamos um método para fornecer o estado inicial. Como RuleBook
é suposto que sabemos que movimentos são legais, temos métodos para testar se um movimento é legal em um determinado estado e fornecer uma lista de movimentos legais para um determinado estado. Finalmente, existe um método para avaliar o GameState
. Observe RuleBook
que só deve ser responsável por descrever se um ou os outros jogadores já venceram, mas não quem está em uma posição melhor no meio do jogo. Decidir quem está em uma posição melhor é algo complicado que deve ser transferido para sua própria classe. Portanto, a StateEvaluation
classe é na verdade apenas uma enumeração simples, da seguinte maneira:
package boardgame;
/**
*
*/
public enum StateEvaluation {
UNFINISHED,
PLAYER_ONE_WINS,
PLAYER_TWO_WINS,
DRAW,
ILLEGAL_STATE
}
Por fim, vamos descrever a GameHistory
classe. Esta classe é responsável por lembrar todas as posições alcançadas no jogo, bem como os movimentos que foram jogados. A principal coisa que ele deve ser capaz de fazer é gravar um Move
como tocado. Você também pode adicionar funcionalidade para desfazer Move
s. Eu tenho uma implementação abaixo.
package boardgame;
import java.util.ArrayList;
import java.util.List;
/**
*
* @param <T> The type of GameState
*/
public class GameHistory<T> {
private List<T> states;
private List<Move<T>> moves;
public GameHistory(T initialState) {
states = new ArrayList<>();
states.add(initialState);
moves = new ArrayList<>();
}
void recordMove(Move<T> move) throws IllegalArgumentException {
moves.add(move);
states.add(move.makeResultingState(getMostRecentState()));
}
void resetToNthState(int n) {
states = states.subList(0, n + 1);
moves = moves.subList(0, n);
}
void undoLastMove() {
resetToNthState(getNumberOfMoves() - 1);
}
T getMostRecentState() {
return states.get(getNumberOfMoves());
}
T getStateAfterNthMove(int n) {
return states.get(n + 1);
}
Move<T> getNthMove(int n) {
return moves.get(n);
}
int getNumberOfMoves() {
return moves.size();
}
}
Finalmente, poderíamos imaginar fazer uma Game
aula para amarrar tudo. Game
Supõe-se que esta classe exponha métodos que possibilitem às pessoas ver qual é a corrente GameState
, ver quem, se alguém tiver uma, ver quais movimentos podem ser executados e executar uma jogada. Eu tenho uma implementação abaixo
package boardgame;
import java.util.List;
/**
*
* @author brian
* @param <T> The type of GameState
*/
public class Game<T> {
GameHistory<T> gameHistory;
RuleBook<T> ruleBook;
public Game(RuleBook<T> ruleBook) {
this.ruleBook = ruleBook;
final T initialState = ruleBook.makeInitialState();
gameHistory = new GameHistory<>(initialState);
}
T getCurrentState() {
return gameHistory.getMostRecentState();
}
List<Move<T>> getLegalMoves() {
return ruleBook.makeMoveList(getCurrentState());
}
void doMove(Move<T> move) throws IllegalArgumentException {
if (!ruleBook.isMoveLegal(move, getCurrentState())) {
throw new IllegalArgumentException("Move is not legal in this position");
}
gameHistory.recordMove(move);
}
void undoMove() {
gameHistory.undoLastMove();
}
StateEvaluation evaluateState() {
return ruleBook.evaluateState(getCurrentState());
}
}
Observe nesta classe que RuleBook
não é responsável por saber qual é a corrente GameState
. Esse é o GameHistory
trabalho do. Então, Game
pergunta o GameHistory
que é o estado atual e fornece essas informações para RuleBook
quando é Game
necessário dizer quais são os movimentos legais ou se alguém ganhou.
De qualquer forma, o objetivo desta resposta é que, depois de determinar com precisão o que cada classe é responsável e concentrar cada classe em um pequeno número de responsabilidades, e atribuir cada responsabilidade a uma classe única, as classes tendem a ser dissociados e tudo fica fácil de codificar. Espero que isso seja aparente nos exemplos de código que eu dei.
RuleBook
exemplo, por exemplo, pegouState
como argumento e retornou o válidoMoveList
, ou seja, "aqui está onde estamos agora, o que pode ser feito a seguir?"