Я думаю, что проблема здесь в том, что вы не дали четкого описания того, какие задачи должны решаться какими классами. Я опишу то, что я считаю хорошим описанием того, что должен делать каждый класс, а затем приведу пример общего кода, который иллюстрирует идеи. Мы увидим, что код менее связан, и поэтому он не имеет циклических ссылок.
Давайте начнем с описания того, что делает каждый класс.
GameStateКласс должен содержать только информацию о текущем состоянии игры. Он не должен содержать какую-либо информацию о том, какие состояния игры в прошлом или какие ходы возможны в будущем. Он должен содержать только информацию о том, какие фигуры находятся на каких клетках в шахматах, и сколько и какие типы шашек на каких точках в нардах. Они GameStateдолжны будут содержать некоторую дополнительную информацию, например, информацию о рокировке в шахматах или о кубе удвоения в нардах.
MoveКласс немного сложнее. Я бы сказал, что могу указать ход для игры, указав GameStateрезультат, полученный в результате хода. Таким образом, вы можете представить, что движение может быть реализовано как GameState. Тем не менее, в go (например) вы можете представить, что намного проще указать ход, указав одну точку на доске. Мы хотим, чтобы наш Moveкласс был достаточно гибким, чтобы справиться с любым из этих случаев. Следовательно, Moveкласс на самом деле будет интерфейсом с методом, который выполняет предварительное перемещение GameStateи возвращает новое после перемещения GameState.
Теперь RuleBookкласс отвечает за знание всего о правилах. Это можно разбить на три вещи. Он должен знать, что такое начальный GameState, он должен знать, какие ходы законны, и он должен уметь определить, выиграл ли один из игроков.
Вы также можете создать GameHistoryкласс, чтобы отслеживать все шаги, которые были сделаны, и все, GameStatesчто произошло. Новый класс необходим, потому что мы решили, что один GameStateне должен нести ответственность за знание всего, GameStateчто было до него.
Это завершает классы / интерфейсы, которые я буду обсуждать. У вас также есть Boardкласс. Но я думаю, что доски в разных играх достаточно разные, поэтому трудно понять, что в общем можно сделать с досками. Теперь я продолжу давать универсальные интерфейсы и реализовывать универсальные классы.
Первый есть GameState. Поскольку этот класс полностью зависит от конкретной игры, нет универсального Gamestateинтерфейса или класса.
Дальше есть Move. Как я уже сказал, это может быть представлено интерфейсом, который имеет единственный метод, который принимает состояние перед перемещением и создает состояние после перемещения. Вот код для этого интерфейса:
package boardgame;
/**
*
* @param <T> The type of GameState
*/
public interface Move<T> {
T makeResultingState(T preMoveState) throws IllegalArgumentException;
}
Обратите внимание, что есть параметр типа. Это связано с тем, что, например, ChessMoveнеобходимо знать подробности предварительного хода ChessGameState. Так, например, объявление класса ChessMoveбудет
class ChessMove extends Move<ChessGameState>,
где вы уже определили ChessGameStateкласс.
Далее я буду обсуждать общий RuleBookкласс. Вот код:
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);
}
Опять же, есть параметр типа для GameStateкласса. Поскольку RuleBookпредполагается, что оно знает начальное состояние, мы создали метод для определения начального состояния. Так как RuleBookпредполагается, что ходы являются законными, у нас есть методы, чтобы проверить, является ли ход законным в данном состоянии, и дать список законных ходов для данного состояния. Наконец, есть метод оценки GameState. Обратите внимание, что RuleBookответственность должна быть только за описание того, выиграл ли тот или иной игрок, но не за то, кто находится в лучшем положении в середине игры. Решение, кто находится в лучшем положении, является сложной вещью, которую следует перевести в свой класс. Следовательно, StateEvaluationкласс на самом деле представляет собой простое перечисление, заданное следующим образом:
package boardgame;
/**
*
*/
public enum StateEvaluation {
UNFINISHED,
PLAYER_ONE_WINS,
PLAYER_TWO_WINS,
DRAW,
ILLEGAL_STATE
}
Наконец, давайте опишем GameHistoryкласс. Этот класс отвечает за запоминание всех позиций, которые были достигнуты в игре, а также ходов, которые были сыграны. Главное, что он должен уметь делать - это записывать Moveкак проигранные. Вы также можете добавить функциональность для удаления Moves. У меня есть реализация ниже.
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();
}
}
Наконец, мы могли бы представить себе Gameкласс, чтобы связать все вместе. GameПредполагается, что этот класс предоставляет методы, позволяющие людям увидеть, что это за ток GameState, увидеть, кто, если есть, посмотреть, какие ходы можно сыграть, и сыграть ход. У меня есть реализация ниже
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());
}
}
Обратите внимание на этот класс, что RuleBookне несет ответственности за знание того, что ток GameState. Это GameHistoryработа. Таким образом, он Gameспрашивает, GameHistoryкаково текущее состояние, и дает эту информацию, RuleBookкогда Gameнужно сказать, каковы законные шаги или если кто-то победил.
В любом случае, смысл этого ответа заключается в том, что, как только вы сделали разумное определение того, за что отвечает каждый класс, и вы сделали каждый класс сосредоточенным на небольшом числе обязанностей, и вы назначаете каждую ответственность уникальному классу, то классы имеют тенденцию быть отделенными, и все становится легко закодировать. Надеюсь, это видно из приведенных мною примеров кода.
RuleBookнапример , взятьStateв качестве аргумента аргумент и вернуть действительное значениеMoveList, то есть «вот где мы сейчас находимся, что можно сделать дальше?»