Penso che il problema qui sia che non hai fornito una descrizione chiara di quali compiti devono essere gestiti da quali classi. Descriverò ciò che penso sia una buona descrizione di ciò che ogni classe dovrebbe fare, quindi fornirò un esempio di codice generico che illustra le idee. Vedremo che il codice è meno accoppiato e quindi non ha riferimenti circolari.
Cominciamo descrivendo cosa fa ogni classe.
La GameStateclasse dovrebbe contenere solo informazioni sullo stato attuale del gioco. Non dovrebbe contenere alcuna informazione su ciò che gli stati passati del gioco o quali mosse future sono possibili. Dovrebbe contenere solo informazioni su quali pezzi si trovano su quali quadrati negli scacchi, o quanti e che tipo di pedine sono su quali punti nel backgammon. L' GameStatedovrà contenere alcune informazioni aggiuntive, come le informazioni relative arrocco negli scacchi o circa il cubo del raddoppio in backgammon.
La Movelezione è un po 'complicata. Direi che posso specificare una mossa da giocare specificando GameStateche risulta dalla riproduzione della mossa. Quindi puoi immaginare che una mossa possa essere implementata come a GameState. Tuttavia, in go (per esempio) puoi immaginare che è molto più facile specificare una mossa specificando un singolo punto sulla scacchiera. Vogliamo che la nostra Moveclasse sia abbastanza flessibile da gestire uno di questi casi. Pertanto, la Moveclasse sarà effettivamente un'interfaccia con un metodo che accetta un pre-spostamento GameStatee restituisce un nuovo post-spostamento GameState.
Ora la RuleBookclasse è responsabile di sapere tutto sulle regole. Questo può essere suddiviso in tre cose. Deve sapere qual è l'iniziale GameState, deve sapere quali mosse sono legali e deve essere in grado di dire se uno dei giocatori ha vinto.
Potresti anche fare una GameHistorylezione per tenere traccia di tutte le mosse che sono state fatte e di tutto GameStatesciò che è accaduto. È necessaria una nuova classe perché abbiamo deciso che un singolo GameStatenon dovrebbe essere responsabile della conoscenza di tutti GameStatei precedenti.
Questo conclude le classi / interfacce che discuterò. Hai anche una Boardlezione. Ma penso che le schede in diversi giochi siano abbastanza diverse da rendere difficile capire cosa si potrebbe fare genericamente con le schede. Ora continuerò a fornire interfacce generiche e implementare classi generiche.
Il primo è GameState. Poiché questa classe dipende completamente dal gioco specifico, non esiste Gamestateun'interfaccia o classe generica .
Il prossimo è Move. Come ho detto, questo può essere rappresentato con un'interfaccia che ha un singolo metodo che prende uno stato pre-spostamento e produce uno stato post-spostamento. Ecco il codice per questa interfaccia:
package boardgame;
/**
*
* @param <T> The type of GameState
*/
public interface Move<T> {
T makeResultingState(T preMoveState) throws IllegalArgumentException;
}
Si noti che esiste un parametro di tipo. Questo perché, ad esempio, è ChessMovenecessario conoscere i dettagli della pre-mossa ChessGameState. Quindi, per esempio, la dichiarazione di classe di ChessMovesarebbe
class ChessMove extends Move<ChessGameState>,
dove avresti già definito una ChessGameStateclasse.
Successivamente parlerò della RuleBookclasse generica . Ecco il codice:
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);
}
Di nuovo c'è un parametro di tipo per la GameStateclasse. Poiché RuleBooksi suppone che sappia qual è lo stato iniziale, abbiamo messo un metodo per dare lo stato iniziale. Poiché RuleBooksi suppone che sappiano quali mosse sono legali, disponiamo di metodi per verificare se una mossa è legale in un determinato stato e per fornire un elenco di mosse legali per un determinato stato. Infine, esiste un metodo per valutare GameState. Si noti che RuleBookdovrebbe essere responsabile solo di descrivere se uno o gli altri giocatori hanno già vinto, ma non chi si trova in una posizione migliore nel mezzo di una partita. Decidere chi è in una posizione migliore è una cosa complicata che dovrebbe essere spostata nella sua classe. Pertanto la StateEvaluationclasse è in realtà solo un semplice enum dato come segue:
package boardgame;
/**
*
*/
public enum StateEvaluation {
UNFINISHED,
PLAYER_ONE_WINS,
PLAYER_TWO_WINS,
DRAW,
ILLEGAL_STATE
}
Infine, descriviamo la GameHistoryclasse. Questa classe è responsabile per ricordare tutte le posizioni raggiunte nel gioco e le mosse giocate. La cosa principale che dovrebbe essere in grado di fare è registrare un Movecome giocato. Puoi anche aggiungere funzionalità per annullare Movei messaggi di posta elettronica . Ho un'implementazione di seguito.
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, potremmo immaginare di creare una Gameclasse per legare tutto insieme. Questa Gameclasse dovrebbe esporre metodi che consentano alle persone di vedere qual è la corrente GameState, vedere chi, se qualcuno ne ha uno, vedere quali mosse possono essere giocate e giocare una mossa. Ho un'implementazione di seguito
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());
}
}
Si noti in questa classe che RuleBooknon è responsabile della conoscenza della corrente GameState. Questo è il GameHistorylavoro di. Quindi Gamechiede GameHistoryquale sia lo stato attuale e fornisce queste informazioni a RuleBookquando è Gamenecessario dire quali sono le mosse legali o se qualcuno ha vinto.
Comunque, il punto di questa risposta è che una volta che hai preso una ragionevole determinazione di ciò che ogni classe è responsabile, e fai ogni classe focalizzata su un piccolo numero di responsabilità, e assegni ogni responsabilità a una classe unica, quindi le classi tendono ad essere disaccoppiati e tutto diventa facile da codificare. Spero che ciò sia evidente dagli esempi di codice che ho dato.
RuleBookprendesse ad esempio ilStatecome argomento e restituisse il validoMoveList, ovvero "ecco dove siamo ora, cosa si può fare dopo?"