I think the problem here is that you have not given a clear description of what tasks are to be handled by which classes. I will describe what I think is a good description of what each class should do, then I will give an example of generic code that illustrates the ideas. We will see that the code is less coupled, and so it doesn't really have circular references.
Let's start with describing what each class does.
The GameState
class should only contain information about the current state of the game. It should not contain any information about what the past states of the game or what future moves are possible. It should only contain information about what pieces are on what squares in chess, or how many and what type of checkers are on what points in backgammon. The GameState
will have to contain some extra information, like information about castling in chess or about the doubling cube in backgammon.
The Move
class is a little tricky. I would say that I can specify a move to play by specifying the GameState
that results from playing the move. So you could imagine that a move can just be implemented as a GameState
. However, in go (for example) you could imagine that is a lot easier to specify a move by specifying a single point on the board. We want our Move
class to be flexible enough to handle either of these to cases. Therefore the Move
class is actually going to be an interface with a method that takes a pre-move GameState
and returns a new post-move GameState
.
Now the RuleBook
class is responsible for knowing everything about the rules. This can be broken down into three things. It needs to know what the initial GameState
is, it needs to know what moves are legal, and it needs to be able to tell if one of the players has won.
You could also make a GameHistory
class to keep track of all the moves that have been made and all the GameStates
that have happened. A new class is necessary because we decided that a single GameState
should not be responsible for knowing all the GameState
s that came before it.
This concludes the classes/interfaces I will discuss. You also have a Board
class. But I think boards in different games are different enough that it is hard to see what could generically be done with boards. Now I will go on to give generic interfaces and implement generic classes.
First is GameState
. Since this class is completely dependent on the particular game, there is no generic Gamestate
interface or class.
Next is Move
. As I said, this can be represented with a interface that has a single method that takes a pre-move state and produces a post-move state. Here is the code for this interface:
package boardgame;
/**
*
* @param <T> The type of GameState
*/
public interface Move<T> {
T makeResultingState(T preMoveState) throws IllegalArgumentException;
}
Notice that there is a type parameter. This is because, for example, a ChessMove
will need to know about the particulars of the pre-move ChessGameState
. So, for example, the class declaration of ChessMove
would be
class ChessMove extends Move<ChessGameState>
,
where you would have already defined a ChessGameState
class.
Next I will discuss the generic RuleBook
class. Here is the code:
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);
}
Again there is a type parameter for the GameState
class. Since the RuleBook
is supposed to know what the initial state is, we have put a method to give the initial state. Since the RuleBook
is supposed to know what moves are legal, we have methods to test if a move is legal in a given state and to give a list of legal moves for a given state. Finally, there is a method to evaluate the GameState
. Notice the RuleBook
should only be responsible for describing if one or the other players has already won, but not who is in a better position in middle of a game. Deciding who is in a better position is a complicated thing that should be moved into its own class. Therefore the StateEvaluation
class is actually just a simple enum given as follows:
package boardgame;
/**
*
*/
public enum StateEvaluation {
UNFINISHED,
PLAYER_ONE_WINS,
PLAYER_TWO_WINS,
DRAW,
ILLEGAL_STATE
}
Lastly, let's describe the GameHistory
class. This class is responsible for remembering all positions that were reached in the game as well as the moves that were played. The main thing it should be able to do is record a Move
as played. You can also add functionality for undoing Move
s. I have an implementation below.
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();
}
}
Finally, we could imagine making a Game
class to tie everything together. This Game
class is supposed to expose methods that make it possible for people to see what the current GameState
is, see who, if anyone has one, see what moves can be played, and play a move. I have an implementation below
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());
}
}
Notice in this class that the RuleBook
is not responsible for knowing what the current GameState
is. That is the GameHistory
's job. So the Game
asks the GameHistory
what the current state is and gives this information to the RuleBook
when the Game
needs to say what the legal moves are or if anyone has won.
Anyway, the point of this answer is that once you have made a reasonable determination of what each class is responsible for, and you make each class focused on a small number of responsibilities, and you assign each responsibility to a unique class, then the classes tend to be decoupled, and everything gets easy to code. Hopefully that is apparent from the code examples I gave.
RuleBook
took e.g. theState
as an argument, and returned the validMoveList
, i.e. "here's where we are now, what can be done next?"