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 GameState
classe 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 Move
classe est un peu délicate. Je dirais que je peux spécifier un coup à jouer en spécifiant celui GameState
qui 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 Move
classe soit suffisamment flexible pour gérer l'un ou l'autre de ces cas. Par conséquent, la Move
classe va en fait être une interface avec une méthode qui prend un pré-mouvement GameState
et retourne un nouveau post-mouvementGameState
.
Maintenant, la RuleBook
classe 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 GameHistory
cours pour garder une trace de tous les mouvements qui ont été effectués et de tous ceux GameStates
qui se sont produits. Une nouvelle classe est nécessaire parce que nous avons décidé qu'un seul GameState
ne devrait pas être responsable de connaître tous les GameState
s 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' Gamestate
interface 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, ChessMove
il faudra connaître les détails du pré-déménagement ChessGameState
. Ainsi, par exemple, la déclaration de classe de ChessMove
serait
class ChessMove extends Move<ChessGameState>
,
où vous auriez déjà défini une ChessGameState
classe.
Ensuite, je vais discuter de la RuleBook
classe 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 GameState
classe. Puisque le RuleBook
est supposé savoir quel est l'état initial, nous avons mis une méthode pour donner l'état initial. Puisque le RuleBook
est 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 RuleBook
devrait 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 StateEvaluation
classe 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 GameHistory
classe. 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 Move
tel que joué. Vous pouvez également ajouter des fonctionnalités pour annuler les Move
s. 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 Game
cours pour tout lier ensemble. Cette Game
classe 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 RuleBook
n'est pas responsable de savoir quel est le courant GameState
. C'est ça le GameHistory
boulot. Donc, le Game
demande l' GameHistory
état actuel et donne ces informations au RuleBook
moment où il Game
faut 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.
RuleBook
prend par exemple leState
comme argument et retourne le valideMoveList
, c'est-à-dire "voici où nous en sommes maintenant, que peut-on faire ensuite?"