Я думаю, что проблема здесь в том, что вы не дали четкого описания того, какие задачи должны решаться какими классами. Я опишу то, что я считаю хорошим описанием того, что должен делать каждый класс, а затем приведу пример общего кода, который иллюстрирует идеи. Мы увидим, что код менее связан, и поэтому он не имеет циклических ссылок.
Давайте начнем с описания того, что делает каждый класс.
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
как проигранные. Вы также можете добавить функциональность для удаления Move
s. У меня есть реализация ниже.
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
, то есть «вот где мы сейчас находимся, что можно сделать дальше?»