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.
GameState
Klasa 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. GameState
Będą musiały zawierać dodatkowe informacje, takie jak informacje o roszady w szachy lub o podwojenie sześcianu w backgammon.
Move
Klasa jest trochę trudne. Powiedziałbym, że mogę określić ruch, który chcesz zagrać, określając GameState
wynik 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 Move
klasa była wystarczająco elastyczna, aby poradzić sobie z jedną z tych spraw. Dlatego Move
klasa będzie interfejsem z metodą, która pobiera ruch przed GameState
i zwraca nowy ruch po GameState
.
Teraz RuleBook
klasa odpowiada za znajomość zasad. Można to podzielić na trzy rzeczy. Musi wiedzieć, co to GameState
jest inicjał , musi wiedzieć, jakie ruchy są legalne, i musi wiedzieć, czy któryś z graczy wygrał.
Możesz także stworzyć GameHistory
klasę, 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 GameState
nie powinien być odpowiedzialny za znajomość wszystkich GameState
poprzedniej.
To kończy klasy / interfejsy, które omówię. Ty też masz Board
klasę. 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 Gamestate
interfejsu 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 ChessMove
trzeba będzie wiedzieć o szczegółach wstępnego ruchu ChessGameState
. Na przykład deklaracja klasy ChessMove
będzie
class ChessMove extends Move<ChessGameState>
,
gdzie już zdefiniowałbyś ChessGameState
klasę.
Następnie omówię RuleBook
klasę 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 GameState
klasy. Ponieważ RuleBook
ma wiedzieć, jaki jest stan początkowy, umieściliśmy metodę nadania stanu początkowego. Ponieważ RuleBook
ma 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 RuleBook
powinien 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 StateEvaluation
klasa 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 GameHistory
klasę. 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ć Move
jako 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 Game
klasy, która łączy wszystko razem. Ta Game
klasa ma na celu ujawnienie metod, które pozwalają ludziom zobaczyć, co GameState
jest 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 RuleBook
nie jest odpowiedzialny za wiedzieć, co to GameState
jest prąd . To jest GameHistory
praca. Więc Game
prosi GameHistory
Jaki jest obecny stan i daje te informacje RuleBook
, gdy Game
potrzeby, 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.
RuleBook
wziął np.State
Jako argument i zwrócił prawidłowyMoveList
, tj. „Oto gdzie jesteśmy teraz, co można zrobić dalej?”