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 GameStateclasse 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 GameStatearquivo 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 GameStateresultados 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 Moveclasse seja flexível o suficiente para lidar com qualquer um desses casos. Portanto, a Moveclasse realmente será uma interface com um método que realiza uma pré-movimentação GameStatee retorna uma nova pós-movimentação GameState.
Agora a RuleBookturma é 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 GameStatesque aconteceu. Uma nova classe é necessária porque decidimos que uma única GameStatenão deveria ser responsável por conhecer todos os GameStates que vieram antes dela.
Isso conclui as classes / interfaces que discutirei. Você também tem uma Boardaula. 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á Gamestateinterface 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 ChessMoveprecisará saber sobre os detalhes do movimento anterior ChessGameState. Assim, por exemplo, a declaração de classe de ChessMoveseria
class ChessMove extends Move<ChessGameState>,
onde você já teria definido uma ChessGameStateclasse.
A seguir, discutirei a RuleBookclasse 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 GameStateclasse. 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 RuleBookque 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 StateEvaluationclasse é 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 GameHistoryclasse. 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 Movecomo tocado. Você também pode adicionar funcionalidade para desfazer Moves. 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 Gameaula para amarrar tudo. GameSupõ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 RuleBooknão é responsável por saber qual é a corrente GameState. Esse é o GameHistorytrabalho do. Então, Gamepergunta o GameHistoryque é o estado atual e fornece essas informações para RuleBookquando é Gamenecessá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.
RuleBookexemplo, por exemplo, pegouStatecomo argumento e retornou o válidoMoveList, ou seja, "aqui está onde estamos agora, o que pode ser feito a seguir?"