Myślę, że problem polega na tym, że nie podałeś jasnego opisu zadań, które mają być obsługiwane przez poszczególne klasy. Opiszę, co uważam za dobry opis tego, co każda klasa powinna zrobić, a następnie podam przykład ogólnego kodu ilustrującego pomysły. Przekonamy się, że kod jest mniej sprzężony, a więc tak naprawdę nie ma odwołań cyklicznych.
Zacznijmy od opisania tego, co robi każda klasa.
GameStateKlasa powinna zawierać tylko informacje na temat aktualnego stanu gry. Nie powinien zawierać żadnych informacji o tym, jakie są wcześniejsze stany gry ani jakie ruchy będą możliwe w przyszłości. Powinien zawierać tylko informacje o tym, jakie elementy znajdują się na jakich kwadratach w szachach, lub ile i jakie są rodzaje szachów w jakich punktach w backgammon. GameStateBędą musiały zawierać dodatkowe informacje, takie jak informacje o roszady w szachy lub o podwojenie sześcianu w backgammon.
MoveKlasa jest trochę trudne. Powiedziałbym, że mogę określić ruch, który chcesz zagrać, określając GameStatewynik tego ruchu. Więc możesz sobie wyobrazić, że ruch można po prostu zaimplementować jako GameState. Jednak w go (na przykład) można sobie wyobrazić, że o wiele łatwiej jest określić ruch, określając pojedynczy punkt na planszy. Chcemy, aby nasza Moveklasa była wystarczająco elastyczna, aby poradzić sobie z jedną z tych spraw. Dlatego Moveklasa będzie interfejsem z metodą, która pobiera ruch przed GameStatei zwraca nowy ruch po GameState.
Teraz RuleBookklasa odpowiada za znajomość zasad. Można to podzielić na trzy rzeczy. Musi wiedzieć, co to GameStatejest inicjał , musi wiedzieć, jakie ruchy są legalne, i musi wiedzieć, czy któryś z graczy wygrał.
Możesz także stworzyć GameHistoryklasę, która będzie śledzić wszystkie ruchy, które zostały wykonane i wszystkie GameStates, które się wydarzyły. Nowa klasa jest konieczna, ponieważ zdecydowaliśmy, że jeden GameStatenie powinien być odpowiedzialny za znajomość wszystkich GameStatepoprzedniej.
To kończy klasy / interfejsy, które omówię. Ty też masz Boardklasę. Ale myślę, że plansze w różnych grach są na tyle różne, że trudno jest zobaczyć, co można ogólnie zrobić z planszami. Teraz przejdę do tworzenia ogólnych interfejsów i implementowania klas ogólnych.
Po pierwsze jest GameState. Ponieważ ta klasa jest całkowicie zależna od konkretnej gry, nie ma ogólnego Gamestateinterfejsu ani klasy.
Dalej jest Move. Jak powiedziałem, można to przedstawić za pomocą interfejsu, który ma jedną metodę, która przyjmuje stan przed ruchem i wytwarza stan po ruchu. Oto kod tego interfejsu:
package boardgame;
/**
*
* @param <T> The type of GameState
*/
public interface Move<T> {
T makeResultingState(T preMoveState) throws IllegalArgumentException;
}
Zauważ, że istnieje parametr typu. Jest tak, ponieważ na przykład ChessMovetrzeba będzie wiedzieć o szczegółach wstępnego ruchu ChessGameState. Na przykład deklaracja klasy ChessMovebędzie
class ChessMove extends Move<ChessGameState>,
gdzie już zdefiniowałbyś ChessGameStateklasę.
Następnie omówię RuleBookklasę ogólną . Oto kod:
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);
}
Ponownie istnieje parametr typu dla GameStateklasy. Ponieważ RuleBookma wiedzieć, jaki jest stan początkowy, umieściliśmy metodę nadania stanu początkowego. Ponieważ RuleBookma wiedzieć, które ruchy są legalne, dysponujemy metodami sprawdzania, czy ruch jest legalny w danym stanie, i podajemy listę legalnych ruchów dla danego stanu. Wreszcie istnieje metoda oceny GameState. Zauważ, że RuleBookpowinien być odpowiedzialny tylko za opisanie, czy jeden lub inni gracze już wygrywają, ale nie kto jest na lepszej pozycji w środku gry. Decyzja o tym, kto ma lepszą pozycję, jest skomplikowaną sprawą, którą należy przenieść do własnej klasy. Dlatego StateEvaluationklasa jest w rzeczywistości zwykłym wyliczeniem podanym w następujący sposób:
package boardgame;
/**
*
*/
public enum StateEvaluation {
UNFINISHED,
PLAYER_ONE_WINS,
PLAYER_TWO_WINS,
DRAW,
ILLEGAL_STATE
}
Na koniec opiszmy GameHistoryklasę. Ta klasa jest odpowiedzialna za zapamiętywanie wszystkich pozycji, które zostały osiągnięte w grze, a także wykonanych ruchów. Najważniejsze, co powinien być w stanie zrobić, to nagrać Movejako odtworzony. Możesz także dodać funkcję cofania Move. Mam implementację poniżej.
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();
}
}
Wreszcie możemy sobie wyobrazić tworzenie Gameklasy, która łączy wszystko razem. Ta Gameklasa ma na celu ujawnienie metod, które pozwalają ludziom zobaczyć, co GameStatejest prądem , zobaczyć, kto, jeśli ktoś ma, zobaczyć, jakie ruchy mogą być odtwarzane, i wykonać ruch. Mam implementację poniżej
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());
}
}
Zauważ w tej klasie, że RuleBooknie jest odpowiedzialny za wiedzieć, co to GameStatejest prąd . To jest GameHistorypraca. Więc Gameprosi GameHistoryJaki jest obecny stan i daje te informacje RuleBook, gdy Gamepotrzeby, aby powiedzieć, co porusza prawne lub jeśli ktoś wygrał.
W każdym razie, celem tej odpowiedzi jest to, że po dokonaniu rozsądnego ustalenia, za co każda klasa jest odpowiedzialna, i skupieniu każdej klasy na niewielkiej liczbie obowiązków, i przypisujesz każdą odpowiedzialność wyjątkowej klasie, a następnie klasom mają tendencję do rozłączania się i wszystko staje się łatwe do kodowania. Mam nadzieję, że wynika to z przykładów kodu, które podałem.
RuleBookwziął np.StateJako argument i zwrócił prawidłowyMoveList, tj. „Oto gdzie jesteśmy teraz, co można zrobić dalej?”