Je pense que le problème ici est que vous n'avez pas donné une description claire de quelles tâches doivent être gérées par quelles classes. Je décrirai ce que je pense être une bonne description de ce que chaque classe devrait faire, puis je donnerai un exemple de code générique qui illustre les idées. Nous verrons que le code est moins couplé, et donc il n'a pas vraiment de références circulaires.
Commençons par décrire ce que fait chaque classe.
La GameStateclasse ne doit contenir que des informations sur l'état actuel du jeu. Il ne doit contenir aucune information sur ce que les états passés du jeu ou quels mouvements futurs sont possibles. Il ne doit contenir que des informations sur les pièces sur les cases des échecs ou sur le nombre et le type de pions sur les points du backgammon. leGameState devra contenir des informations supplémentaires, comme des informations sur le roque aux échecs ou sur le cube doublant au backgammon.
La Moveclasse est un peu délicate. Je dirais que je peux spécifier un coup à jouer en spécifiant celui GameStatequi résulte de la lecture du coup. Vous pouvez donc imaginer qu'un mouvement peut simplement être implémenté en tant que GameState. Cependant, dans go (par exemple), vous pourriez imaginer qu'il est beaucoup plus facile de spécifier un mouvement en spécifiant un seul point sur la carte. Nous voulons que notre Moveclasse soit suffisamment flexible pour gérer l'un ou l'autre de ces cas. Par conséquent, la Moveclasse va en fait être une interface avec une méthode qui prend un pré-mouvement GameStateet retourne un nouveau post-mouvementGameState .
Maintenant, la RuleBookclasse est responsable de tout savoir sur les règles. Cela peut être décomposé en trois choses. Il doit savoir quelle est l'initiale GameState, il doit savoir quels mouvements sont légaux, et il doit pouvoir savoir si l'un des joueurs a gagné.
Vous pouvez également créer un GameHistorycours pour garder une trace de tous les mouvements qui ont été effectués et de tous ceux GameStatesqui se sont produits. Une nouvelle classe est nécessaire parce que nous avons décidé qu'un seul GameStatene devrait pas être responsable de connaître tous les GameStates qui l'ont précédé.
Ceci conclut les classes / interfaces dont je parlerai. Vous avez également unBoard classe. Mais je pense que les planches des différents jeux sont suffisamment différentes pour qu'il soit difficile de voir ce qui pourrait être fait génériquement avec les planches. Je vais maintenant donner des interfaces génériques et implémenter des classes génériques.
Le premier est GameState. Puisque cette classe dépend complètement du jeu particulier, il n'y a pas d' Gamestateinterface ou de classe générique .
Le suivant est Move. Comme je l'ai dit, cela peut être représenté par une interface qui a une seule méthode qui prend un état pré-mouvement et produit un état post-mouvement. Voici le code de cette interface:
package boardgame;
/**
*
* @param <T> The type of GameState
*/
public interface Move<T> {
T makeResultingState(T preMoveState) throws IllegalArgumentException;
}
Notez qu'il existe un paramètre de type. En effet, par exemple, ChessMoveil faudra connaître les détails du pré-déménagement ChessGameState. Ainsi, par exemple, la déclaration de classe de ChessMoveserait
class ChessMove extends Move<ChessGameState>,
où vous auriez déjà défini une ChessGameStateclasse.
Ensuite, je vais discuter de la RuleBookclasse générique . Voici le code:
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);
}
Encore une fois, il existe un paramètre de type pour la GameStateclasse. Puisque le RuleBookest supposé savoir quel est l'état initial, nous avons mis une méthode pour donner l'état initial. Puisque le RuleBookest censé savoir quels mouvements sont légaux, nous avons des méthodes pour tester si un mouvement est légal dans un état donné et pour donner une liste des mouvements légaux pour un état donné. Enfin, il existe une méthode pour évaluer le GameState. Remarquez que le RuleBookdevrait seulement être responsable de décrire si l'un ou l'autre des joueurs a déjà gagné, mais pas qui est mieux placé au milieu d'une partie. Décider qui est dans une meilleure position est une chose compliquée qui devrait être déplacée dans sa propre classe. Par conséquent, la StateEvaluationclasse n'est en fait qu'une simple énumération donnée comme suit:
package boardgame;
/**
*
*/
public enum StateEvaluation {
UNFINISHED,
PLAYER_ONE_WINS,
PLAYER_TWO_WINS,
DRAW,
ILLEGAL_STATE
}
Enfin, décrivons la GameHistoryclasse. Cette classe est chargée de se souvenir de toutes les positions qui ont été atteintes dans le jeu ainsi que des mouvements qui ont été joués. La principale chose qu'il devrait pouvoir faire est d'enregistrer un Movetel que joué. Vous pouvez également ajouter des fonctionnalités pour annuler les Moves. J'ai une implémentation ci-dessous.
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();
}
}
Enfin, nous pourrions imaginer faire un Gamecours pour tout lier ensemble. Cette Gameclasse est censée exposer des méthodes qui permettent aux gens de voir quel est le courant GameState, de voir qui, si quelqu'un en a un, de voir quels coups peuvent être joués et de jouer un coup. J'ai une implémentation ci-dessous
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());
}
}
Notez dans cette classe que le RuleBookn'est pas responsable de savoir quel est le courant GameState. C'est ça le GameHistoryboulot. Donc, le Gamedemande l' GameHistoryétat actuel et donne ces informations au RuleBookmoment où il Gamefaut dire quels sont les mouvements légaux ou si quelqu'un a gagné.
Quoi qu'il en soit, le point de cette réponse est qu'une fois que vous avez déterminé de manière raisonnable les responsabilités de chaque classe et que vous concentrez chaque classe sur un petit nombre de responsabilités, et que vous attribuez chaque responsabilité à une classe unique, puis les classes ont tendance à être découplés, et tout devient facile à coder. J'espère que cela ressort des exemples de code que j'ai donnés.
RuleBookprend par exemple leStatecomme argument et retourne le valideMoveList, c'est-à-dire "voici où nous en sommes maintenant, que peut-on faire ensuite?"