I recently made a new project for Chutes and Ladders (the Hasbro version of Snakes and ladders) and it is located here: https://github.com/vchittar/ChutesAndLadders
BoardLogic (implements an interface):
@Service
public class BoardLogic implements IBoardLogic {
private static final Logger LOG = LoggerFactory.getLogger(BoardLogic.class);
@Autowired
GameSpinner gameSpinner;
@Autowired
PlayerLogic playerLogic;
@Autowired
PlayerUtils playerUtils;
/**
* This method deals with updating the player positions that are on the board
*
* @param playerList - The list of players that are currently in play and that need to be updated
* @throws CLException - Custom Chutes and Ladder Exception
*/
@Override
public void runGame(List<Player> playerList) throws CLException {
boolean isWinner = false;
GameBoard gb = new GameBoard();
try {
while (!isWinner) {
for (Player player : playerList) {
// Positional Logic
int position = player.getPosition() + gameSpinner.spinner();
int prevPosition = player.getPosition();
if (position > 100) {
System.out.println(player.getName() + " got " + position + " " +
"which is more than 100, so they will stay put!");
continue;
}
// Move Logic for Players
playerLogic.nextTurn(player, position, gb);
//Print and Cleanup Logic
System.out.println(playerUtils.printBoard(player, prevPosition, position, gb));
resetPlayerStatus(player);
//Declare winner
if (player.getPosition() == 100) {
isWinner = true;
System.out.println("The winner is: " + player.getName());
break;
}
}
}
} catch (Exception e) {
LOG.error("Run Game has thrown an error! Please check your values", e);
throw new CLException("Run game has thrown an exception. Please check the logs", e);
}
}
/**
* Reset the Player Direction status so their correct statuses are printed
*
* @param p1 - Player object whose status is reset
*/
private void resetPlayerStatus(Player p1) {
p1.setPlayerDirection(PlayerDirection.EMPTY);
}
}
PlayerLogic:
@Service
public class PlayerLogic implements IPlayerLogic {
private static final Map<Integer, Integer> ladderMap = new HashMap<>();
private static final Map<Integer, Integer> chuteMap = new HashMap<>();
private static final Logger LOG = LoggerFactory.getLogger(ApplicationDriver.class);
/**
* Method that initializes the ladder and chute map,
* to set up the logic of the game
*/
private void initializeBoard() {
ladderMap.put(1, 38);
ladderMap.put(4, 14);
ladderMap.put(9, 31);
ladderMap.put(21, 42);
ladderMap.put(28, 84);
ladderMap.put(36, 44);
ladderMap.put(51, 67);
ladderMap.put(71, 91);
ladderMap.put(80, 100);
chuteMap.put(16, 6);
chuteMap.put(47, 26);
chuteMap.put(49, 11);
chuteMap.put(56, 53);
chuteMap.put(62, 19);
chuteMap.put(64, 60);
chuteMap.put(87, 24);
chuteMap.put(93, 73);
chuteMap.put(95, 75);
chuteMap.put(98, 78);
}
/**
* This method deals with updating the chutes and ladder logic for our game
* and updates the player object with either chutes or ladder or
* returns the player object if no valid key is found in the map
*
* @param p1 - Player object
* @param position - integer value that holds the current value of the dice roll
* @param gb - GameBoard object that holds values on the board
* @throws CLException - Custom Exception for Chutes and Ladders
* @return Player object that has been updated for either ladder or chute or neither/
*/
@Override
public Player nextTurn(Player p1, int position, GameBoard gb) throws CLException {
try {
// invalid input check
if (p1.getPosition() == null || p1.getPosition() < 0 || gb == null) {
LOG.error("Invalid position! Please check your values!");
throw new CLException("Invalid position! Please check your values");
}
//initialize
initializeBoard();
p1.setPosition(position);
if (ladderMap.containsKey(p1.getPosition())) {
return goUpLadder(p1, gb);
} else if (chuteMap.containsKey(p1.getPosition())) {
return goDownChute(p1, gb);
}
} catch (Exception e) {
LOG.error("There was an error generating the next turn correctly. Please check!", e);
throw new CLException("Error getting the next turn for the player: " + p1.getName(), e);
}
return p1;
}
/**
* This method deals with updating the ladder logic
* that takes place within the game
*
* @param p1 - Player object that gets updated
* @param gb - GameBoard object that holds values on the board
* @return Player object that has been updated with ladder logic
*/
private Player goUpLadder(Player p1, GameBoard gb) {
gb.setLadderDown(p1.getPosition());
p1.setPosition(ladderMap.get(p1.getPosition()));
p1.setPlayerDirection(PlayerDirection.LADDER);
gb.setLadderTop(p1.getPosition());
return p1;
}
/**
* This method deals with updating the chute logic
* that takes place within the game
*
* @param p1 - Player object that gets updated
* @param gb - GameBoard object that holds values on the board
* @return Player object that has been updated with chute logic
*
*/
private Player goDownChute(Player p1, GameBoard gb) {
gb.setChuteDown(chuteMap.get(p1.getPosition()));
gb.setChuteTop(p1.getPosition());
p1.setPosition(chuteMap.get(p1.getPosition()));
p1.setPlayerDirection(PlayerDirection.CHUTE);
return p1;
}
}
Utils classes:
PlayerUtils:
@Service
public class PlayerUtils {
private static final Logger LOG = LoggerFactory.getLogger(PlayerUtils.class);
private static int turnCounter = 1;
@Autowired
GameSpinner gameSpinner;
/**
* This method deals with deciding the turn order amongst the users that are playing
*
* @param playerList - The List of players that want to play
* @return List of players that have been organized into their proper turn order
*/
public List<Player> deriveTurnOrder(List<Player> playerList) throws CLException {
try {
LOG.info("Time to get the player order!");
for (Player player : playerList) {
player.setInitialRoll(gameSpinner.spinner());
System.out.println(player.getName() + " rolled a " + player.getInitialRoll());
}
// calculation that checks if any players need re-rolls
setRerollCalcs(playerList);
// get the highest index of the roll in the player list
// made it into a variable here for additional clarity
int maxIndex = getMaxRollIndex(playerList);
// this calc basically gets all the elements from left side of the highest element, as stated in the reqs
// for ex: {3 2 5 4} -> {5 2 3 4}
Collections.reverse(playerList);
Collections.rotate(playerList, maxIndex + 1);
} catch (Exception e) {
LOG.error("There was a problem deriving the turn order! Please check the exception");
throw new CLException("Error in the deriving turn order method", e);
}
return playerList.stream().map(i -> {
i.setTurnOrder(turnCounter);
turnCounter++;
System.out.println(i.getName() + " will go " + i.getTurnOrder() + " and they rolled a " + i.getInitialRoll());
return i;
}).sorted(Comparator.comparingInt(Player::getTurnOrder))
.collect(Collectors.toList());
}
/**
* This method handles the reroll calculations, if there are any to be
* @param playerList - List of players that need to be rerolled
*/
public void setRerollCalcs(List<Player> playerList) {
List<Player> reRollListPlayers = new ArrayList<>();
int max = 0;
for (Player player : playerList) {
if (player.getInitialRoll() > max) {
max = player.getInitialRoll();
reRollListPlayers.clear();
reRollListPlayers.add(player);
} else if (max == player.getInitialRoll()) {
reRollListPlayers.add(player);
}
}
if (reRollListPlayers.size() > 1) {
reRollPlayers(reRollListPlayers, max);
reRollListPlayers.clear();
}
}
/**
* This method works to get the maximum index of whoever got the highest roll
* @param playerList - The list that we will traverse to get the highest index
* @return Return an integer that holds the highest value for the index
*/
private int getMaxRollIndex(List<Player> playerList) {
AtomicInteger randomInt = new AtomicInteger();
IntStream.range(0, playerList.size()).reduce((a, b) ->
playerList.get(a).getInitialRoll() < playerList.get(b).getInitialRoll() ? b : a)
.ifPresent(randomInt::set);
return randomInt.get();
}
/**
* Method that basically rerolls till there are no two highest rolls
* @param players - List of players that are to be rerolled
* @param maxRoll - Max Roll contains the highest roll value in the player list
*/
private void reRollPlayers(List<Player> players, int maxRoll) {
players.stream().collect(Collectors.groupingBy(Player::getInitialRoll)).forEach((initialRoll, initialRollList) -> {
if (initialRoll >= maxRoll) {
initialRollList.forEach(player -> {
System.out.println("Will need to reroll for: " + player.getName());
player.setInitialRoll(gameSpinner.spinner());
System.out.println(player.getName() + " rerolled: " + player.getInitialRoll());
});
}
});
setRerollCalcs(players);
}
/**
* Method that creates the players based on the arg names that were given
*
* @param names - List of names that are given in the CLI
* @return List of player objects that contain the same names provided in the CLI
*/
public List<Player> createPlayers(List<String> names) {
return names.stream().map(Player::new)
.collect(Collectors.toCollection(ArrayList<Player>::new));
}
/**
* This method deals with printing out the output of the game to standard output
*
* @param player - Player object
* @param prevPosition - Previous position that the player was on
* @param position - The current position that the player is on
* @param gb - The gameboard object that keeps track of the game in progress
*/
public String printBoard(Player player, int prevPosition, int position, GameBoard gb) throws CLException {
if(player.getPlayerDirection() == null) {
throw new CLException("Player direction is null, please double check your values");
}
switch (player.getPlayerDirection()) {
case LADDER:
return String.format("%s: %d --> %d -- %s --> %d", player.getName(), prevPosition, position,
player.getPlayerDirection(), gb.getLadderTop());
case CHUTE:
return String.format("%s: %d --> %d -- %s --> %d", player.getName(), prevPosition, position,
player.getPlayerDirection(), gb.getChuteDown());
case EMPTY:
return String.format("%s: %d --> %d", player.getName(), prevPosition, position);
default:
LOG.error("A valid path wasn't found!");
return "A valid path wasn't found";
}
}
}
I would love it if someone was able to give me any recommendations on what I could do to improve or what I have done well, basically a code review.
I have about 3.5 years or so of exp. So please let me know how I am progressing based on that.
I just want to understand if this is the best way to tackle the reroll logic and if the board logic was set up properly and if I set up my classes, logic correctly according to OOP standards and what not.