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 GameState
classe 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' GameState
dovrà contenere alcune informazioni aggiuntive, come le informazioni relative arrocco negli scacchi o circa il cubo del raddoppio in backgammon.
La Move
lezione è un po 'complicata. Direi che posso specificare una mossa da giocare specificando GameState
che 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 Move
classe sia abbastanza flessibile da gestire uno di questi casi. Pertanto, la Move
classe sarà effettivamente un'interfaccia con un metodo che accetta un pre-spostamento GameState
e restituisce un nuovo post-spostamento GameState
.
Ora la RuleBook
classe è 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 GameHistory
lezione per tenere traccia di tutte le mosse che sono state fatte e di tutto GameStates
ciò che è accaduto. È necessaria una nuova classe perché abbiamo deciso che un singolo GameState
non dovrebbe essere responsabile della conoscenza di tutti GameState
i precedenti.
Questo conclude le classi / interfacce che discuterò. Hai anche una Board
lezione. 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 Gamestate
un'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, è ChessMove
necessario conoscere i dettagli della pre-mossa ChessGameState
. Quindi, per esempio, la dichiarazione di classe di ChessMove
sarebbe
class ChessMove extends Move<ChessGameState>
,
dove avresti già definito una ChessGameState
classe.
Successivamente parlerò della RuleBook
classe 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 GameState
classe. Poiché RuleBook
si suppone che sappia qual è lo stato iniziale, abbiamo messo un metodo per dare lo stato iniziale. Poiché RuleBook
si 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 RuleBook
dovrebbe 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 StateEvaluation
classe è 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 GameHistory
classe. 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 Move
come giocato. Puoi anche aggiungere funzionalità per annullare Move
i 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 Game
classe per legare tutto insieme. Questa Game
classe 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 RuleBook
non è responsabile della conoscenza della corrente GameState
. Questo è il GameHistory
lavoro di. Quindi Game
chiede GameHistory
quale sia lo stato attuale e fornisce queste informazioni a RuleBook
quando è Game
necessario 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.
RuleBook
prendesse ad esempio ilState
come argomento e restituisse il validoMoveList
, ovvero "ecco dove siamo ora, cosa si può fare dopo?"