I'm implementing a Yahtzee app and i have a class StrategicAdvisor
that is responsible for deciding which strategy should be followed. The StrategicAdvisor
has some tools: a RollAnalyzer
which provides information about the current roll and a BoardAnalyzer
which can tell us about the Fields vaccant (or occupied) in the game board (as well as the amount of eyes required for the bonus and other infos about the board).
The Strategy just tells the player (either AI player or human) which die should be best kept to maximize on the next roll - or if the result already is optimal to stop rolling and write immediately into the board.
Note:
The StrategicAdvisor
just gives Hints on which die should be kept; there is another Advisor, namely the WriteAdvisor
who decides which field is optimal at the end of rolling to be written at.
Update / Note 2
the Strategy represents the Strategy for the current Roll - it is not applicatable for the Strategy of the AI-Player for this i had the idea to implement different StrategyAdvisors, e.g. AggressiveStrategicAdvisor
or DefensiveStrategicAdvisor
... i'm not there yet so the current StrategicAdvisor
works a kind of BasicStrategyAdvisor
(right now i cannot yet make proper estimates onthe value of a Strategy)
Question here is:
Clean Code: My StrategicAdvisor
has a lot of complexity and i can't find a nice way to reduce complexity here...
public class StrategyAdviser {
private final YahtzeeGame yahtzeeGame;
public StrategyAdviser(YahtzeeGame yahtzeeGame) {
this.yahtzeeGame = yahtzeeGame;
}
public Strategy getAdvise() {
Roll roll = yahtzeeGame.getRoll();
Board board = yahtzeeGame.getBoard();
YahtzeePlayer currentPlayer = yahtzeeGame.getCurrentPlayer();
StrategyFactory factory = new StrategyFactory();
if (!roll.hasRoll()) {
return factory.createStartRollStrategy();
}
if (!yahtzeeGame.canRoll()) {
return factory.createWriteToBoardStrategy();
}
RollAnalyze rollAnalyze = new RollAnalyze(roll);
BoardAnalyze boardAnalyze = new BoardAnalyze(board, currentPlayer);
int amountOfIdenticals = rollAnalyze.getAmountOfIdenticals();
int highestEyeOfIdenticals = rollAnalyze.getHighestEyeOfIdenticals();
boolean has6 = rollAnalyze.hasSix();
boolean has5 = rollAnalyze.hasFive();
boolean is6Empty = boardAnalyze.isTopRowEmpty(6);
boolean is5Empty = boardAnalyze.isTopRowEmpty(5);
boolean has3InRow = rollAnalyze.hasThreeInRow();
boolean has4InRow = rollAnalyze.hasFourInRow();
boolean hasBottomVariables = boardAnalyze.hasBottomRowsWithVariableCounter();
if (amountOfIdenticals == 5) {
return factory.createWriteToBoardStrategy();
}
if ((amountOfIdenticals >= 3 && boardAnalyze.isTopRowEmpty(highestEyeOfIdenticals))) {
return factory.createStrategyRollForThreeOrMoreIdentical(highestEyeOfIdenticals, rollAnalyze);
}
if (amountOfIdenticals >= 2 && highestEyeOfIdenticals >= 4) {
return factory.createStrategyRollForThreeOrMoreIdentical(highestEyeOfIdenticals, rollAnalyze);
}
if (has4InRow) {
return factory.createStrategyRollForMajorStreet();
}
if (has6 && is6Empty) {
return factory.createStrategyRollForSix(rollAnalyze);
}
if (((!has6) && (has5 && is5Empty))) {
return factory.createStrategyRollForSix(rollAnalyze);
}
if (hasBottomVariables) {
if (has6) {
return factory.createStrategyRollForSix(rollAnalyze);
}
if (has5) {
return factory.createStrategyRollForFive(rollAnalyze);
}
}
if (has3InRow) {
if (has5) {
return factory.createStrategyRollForMinorStreet(3, 4, 5);
} else {
return factory.createStrategyRollForMinorStreet(2, 3, 4);
}
}
return factory.createStrategyReRollAll();
}
I could write some Methods to simplify the statements but i think that would only slighlty decrease complexity... (e.g. instead of amountOfIdenticals == 5
write a method boolean hasFiveIdenticals(rollAnalyze ra);
Update
here is the intercace Strategy
: There are two abstract implementations in the StrategyFactory
for easier creation of Strategy
s.
public interface Strategy {
boolean adviseToContinueRolling();
void apply(YahtzeeGame game);
}
and StrategyFactory
:
public class StrategyFactory {
private static final Logger LOGGER = LoggerFactory.getLogger(Strategy.class);
public Strategy createStrategyRollForThreeOrMoreIdentical(int highestEyeOfIdenticals, RollAnalyze rollAnalyze) {
return new ContinueStrategy() {
@Override
public void apply(YahtzeeGame game) {
LOGGER.debug("apply strategy: roll, try to get as much identicals of {} as possible", +rollAnalyze.getHighestEyeOfIdenticals());
LOGGER.debug("based on roll: {}", rollAnalyze.getRoll());
roll(game, rollAnalyze.getKeepingFor(highestEyeOfIdenticals));
}
@Override
public String toString() {
return "roll for identicals of " + highestEyeOfIdenticals;
}
};
}
public Strategy createStartRollStrategy() {
return new ContinueStrategy() {
@Override
public void apply(YahtzeeGame game) {
LOGGER.debug("apply strategy: roll for the first time...");
roll(game);
}
@Override
public String toString() {
return "roll for the first time...";
}
};
}
public Strategy createWriteToBoardStrategy() {
return new StopStrategy() {
@Override
public void apply(YahtzeeGame game) {
LOGGER.debug("apply strategy: write to board... do NOT roll anymore");
}
@Override
public String toString() {
return "do not roll - write it into the board!";
}
};
}
public Strategy createStrategyRollForMajorStreet() {
return new ContinueStrategy() {
@Override
public void apply(YahtzeeGame game) {
LOGGER.debug("apply strategy: roll, try to get as much different since we have alreadey minor straight");
roll(game, new Keeping(new HashSet<>(Arrays.asList(2, 3, 4, 5))));
}
@Override
public String toString() {
return "roll for a major street";
}
};
}
public Strategy createStrategyRollForSix(RollAnalyze rollAnalyze) {
return createStartRollForEye(6, rollAnalyze);
}
public Strategy createStrategyRollForFive(RollAnalyze rollAnalyze) {
return createStartRollForEye(5, rollAnalyze);
}
private Strategy createStartRollForEye(int eye, RollAnalyze rollAnalyze) {
return new ContinueStrategy() {
@Override
public void apply(YahtzeeGame game) {
Keeping keeping = rollAnalyze.getKeepingFor(eye);
LOGGER.debug("apply strategy: roll, try to get as much identicals of {} as possible", +eye);
LOGGER.debug("based on roll: {}", keeping);
roll(game, keeping);
}
@Override
public String toString() {
return "roll one certain eye: " + eye;
}
};
}
public Strategy createStrategyRollForMinorStreet(Integer... eyes) {
return new ContinueStrategy() {
@Override
public void apply(YahtzeeGame game) {
LOGGER.debug("apply strategy: roll, try to get as much different since we have alreadey 3/4 of minor straight");
roll(game, new Keeping(new HashSet<>(Arrays.asList(eyes))));
}
@Override
public String toString() {
return "roll for a minor street";
}
};
}
public Strategy createStrategyReRollAll() {
return new ContinueStrategy() {
@Override
public void apply(YahtzeeGame game) {
LOGGER.debug("apply strategy: reRoll all...");
roll(game);
}
@Override
public String toString() {
return "reRoll all... ";
}
};
}
private abstract class TemplateStrategy implements Strategy {
private final boolean adviseToContinue;
private TemplateStrategy(final boolean adviseToContinue) {
this.adviseToContinue = adviseToContinue;
}
@Override
public boolean adviseToContinueRolling() {
return adviseToContinue;
}
void roll(YahtzeeGame yahtzeeGame, Keeping keeping) {
LOGGER.debug("set keeping: {}", keeping);
yahtzeeGame.setKeepings(keeping);
yahtzeeGame.roll();
LOGGER.debug("...done with set Keeping and rolling...");
}
void roll(YahtzeeGame yahtzeeGame) {
yahtzeeGame.roll();
LOGGER.debug("...done, rolling...");
}
}
private abstract class StopStrategy extends TemplateStrategy {
private StopStrategy() {
super(false);
}
}
private abstract class ContinueStrategy extends TemplateStrategy {
private ContinueStrategy() {
super(true);
}
}
}
further note:
a Keeping
is a set of Numbers, which tells the YahtzeeGame
which die should be kept (so it's a Keeping
) and wich shall be re-rolled
if you want to dig deeper into the code, it can be found under https://github.com/martinFrank/yatzee but please don't look too close - i fear i might get kicked out of this board, hahaha =)
Update
in hindsight of the progress of the question i think i should as well add the AiPlayer's turn
public class YahtzeePlayer extends BasePlayer<YahtzeeGame> {
private static final Logger LOGGER = LoggerFactory.getLogger(YahtzeePlayer.class);
YahtzeePlayer(String name, int color, boolean isHuman) {
super(name, color, isHuman);
}
@Override
public void performAiTurn() {
YahtzeeGame yahtzeeGame = getBoardGame();//weg damit
StrategyAdviser strategyAdviser = new StrategyAdviser(yahtzeeGame);
Strategy currentStrategy = strategyAdviser.getAdvise();
while (currentStrategy.adviseToContinueRolling()) {
currentStrategy.apply(yahtzeeGame);
currentStrategy = strategyAdviser.getAdvise();
}
WriteAdviser writeAdviser = new WriteAdviser(
yahtzeeGame.getRoll(), yahtzeeGame.getBoard(), this, new RollAnalyze(yahtzeeGame.getRoll()));
RowType rowType = writeAdviser.getOptimalRow();
if (!yahtzeeGame.getBoard().getRow(rowType, this).isEmpty()) {
throw new IllegalStateException("error - writing into an already filled row");
}
LOGGER.debug("write roll {} into rowType {}", yahtzeeGame.getRoll(), rowType);
yahtzeeGame.write(rowType);
LOGGER.debug("done Ai turn");
yahtzeeGame.endPlayersTurn();
yahtzeeGame.startPlayersTurn();
}
}
Strategy
andStrategyFactory
classes as well, as they are likely part of the problem. \$\endgroup\$