package jrummikub.control; import static jrummikub.model.PlayerSettings.Type.*; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import jrummikub.control.turn.AIControl; import jrummikub.control.turn.ITurnControl; import jrummikub.control.turn.TurnControlFactory; import jrummikub.control.turn.TurnMode; import jrummikub.model.IHand; import jrummikub.model.IPlayer; import jrummikub.model.IRoundState; import jrummikub.model.ITable; import jrummikub.model.PlayerSettings.Type; import jrummikub.model.Position; import jrummikub.model.Score; import jrummikub.model.Stone; import jrummikub.model.StoneSet; import jrummikub.util.Connection; import jrummikub.util.Event; import jrummikub.util.Event1; import jrummikub.util.IEvent; import jrummikub.util.IEvent1; import jrummikub.util.IListener; import jrummikub.util.IListener2; import jrummikub.util.Pair; import jrummikub.view.IView; import jrummikub.view.IView.BottomPanelType; /** * Controller that manages a single round of rummikub */ public class RoundControl { /** * Enum summarizing the different types of invalid turns to set the correct * panel message */ public enum InvalidTurnType { /** There are invalid set(s) on the table */ INVALID_SETS, /** * The player tried to modify the table without providing the initial meld * threshold first */ INITIAL_MELD_ERROR, /** * The player didn't provide enough points for the initial meld threshold */ NOT_ENOUGH_POINTS } /** * Table, stone sets and type of an invalid turn to allow a user to track his * own errors */ public static class InvalidTurnInfo implements Serializable { private static final long serialVersionUID = -3591000741414366776L; private ITable table; private InvalidTurnType type; private ArrayList invalidSets; /** * Creates new InvalidTurnInfo * * @param table * the table after the turn * @param type * the type of the invalid turn * @param invalidSets * the sets causing the turn to be invalid */ public InvalidTurnInfo(ITable table, InvalidTurnType type, Collection invalidSets) { this.table = (ITable) table.clone(); this.type = type; this.invalidSets = new ArrayList(invalidSets); } /** * Getter for table * * @return the table */ public ITable getTable() { return table; } /** * Getter for invalid turn type * * @return the type */ public InvalidTurnType getType() { return type; } /** * Getter for the invalid sets * * @return invalid sets */ public List getInvalidSets() { return invalidSets; } } private ITurnControl turnControl; protected IRoundState roundState; private IView view; private Event restartRoundEvent = new Event(); private Event1 roundStateUpdateEvent = new Event1(); private Event1 endOfRoundEvent = new Event1(); protected List connections = new ArrayList(); private boolean mayPause; public RoundControl(IRoundState roundState, IView view) { this(roundState, view, true); } /** * Create a new RoundControl using the given gameState and view * * @param roundState * initial round state * @param view * view used for user interaction */ protected RoundControl(IRoundState roundState, IView view, boolean mayPause) { this.roundState = roundState; this.view = view; this.mayPause = mayPause; } /** * Is emitted in network when a new turn starts * * @return the event */ public IEvent1 getRoundStateUpdateEvent() { return roundStateUpdateEvent; } /** * End the round * * @return endOfRoundEvent */ public IEvent1 getEndOfRoundEvent() { return endOfRoundEvent; } /** * Begin the round */ public void startRound() { if (roundState != null) { deal(); } continueRound(); } /** * Continue a saved round after loading */ public void continueRound() { connections.add(view.getStartTurnEvent().add(new IListener() { @Override public void handle() { startTurn(); } })); connections.add(view.getAcknowledgeInvalidEvent().add(new IListener() { @Override public void handle() { nextPlayer(); } })); if (roundState != null) { prepareTurn(); } } /** * Abort round if a new one is started or a saved one is loaded */ public void abortRound() { removeListeners(); if (turnControl != null) { turnControl.abortTurn(); turnControl = null; } } /** * Sets the current round state * * @param state * to be set */ protected void setRoundState(IRoundState state) { roundState = state; roundStateUpdateEvent.emit(state); } /** * Prepare a player's turn by checking the player types and setting the * correct turn control */ protected void prepareTurn() { doPrepareTurn(); boolean isHuman = roundState.getActivePlayer().getPlayerSettings() .getType() == HUMAN; boolean oneHuman = roundState.getGameSettings().oneHuman(); boolean isAI = (turnControl instanceof AIControl); if (isAI || (isHuman && oneHuman)) { startTurn(); } } /** * Prepare turn by setting the view components */ protected void doPrepareTurn() { updateSidePanel(); boolean isHuman = roundState.getActivePlayer().getPlayerSettings() .getType() == HUMAN; boolean oneHuman = roundState.getGameSettings().oneHuman(); if (isHuman && !oneHuman) { view.setBottomPanel(BottomPanelType.START_TURN_PANEL); } view.getTablePanel().setStoneSets(roundState.getTable().clone()); view.setCurrentPlayerName(roundState.getActivePlayer().getPlayerSettings() .getName()); view.setCurrentPlayerColor(roundState.getActivePlayer().getPlayerSettings() .getColor()); view.setCurrentPlayerHasLaidOut(roundState.getActivePlayer().getLaidOut()); turnControl = createTurnControl(roundState.getActivePlayer() .getPlayerSettings().getType()); } /** * Start a players turn with the correct turn control */ protected void startTurn() { if (turnControl == null) { doPrepareTurn(); } boolean isHuman = roundState.getActivePlayer().getPlayerSettings() .getType() == HUMAN; view.setBottomPanel(isHuman ? BottomPanelType.HUMAN_HAND_PANEL : BottomPanelType.NONHUMAN_HAND_PANEL); TurnMode turnMode = TurnMode.NORMAL_TURN; view.getTablePanel().setStoneSets(roundState.getTable().clone()); if (roundState.getTurnNumber() < 1) { view.setStoneCollectionHidden(true); turnMode = TurnMode.INSPECT_ONLY; if (roundState.getActivePlayer().getHand().getIdenticalStoneCount() >= 3) { turnMode = TurnMode.MAY_REDEAL; } } if (isHuman) { view.getPlayerPanel().setEndTurnMode(turnMode); } turnControl.setup( new ITurnControl.TurnInfo(roundState, turnMode, mayPause), roundState.getGameSettings(), view); turnControl.getEndOfTurnEvent().add( new IListener2() { @Override public void handle(IRoundState newState, InvalidTurnInfo invalidTurnInfo) { setRoundState(newState); endOfTurn(invalidTurnInfo); } }); turnControl.getRedealEvent().add(new IListener() { @Override public void handle() { redeal(); } }); addTurnControlListeners(turnControl); turnControl.startTurn(); } /** * Update the side panel to show correct player order and heap size */ private void updateSidePanel() { view.showSidePanel(true); view.getSidePanel().setGameSettings(roundState.getGameSettings()); List players = new ArrayList(); for (int i = 1; i < roundState.getPlayerCount(); i++) { players.add(roundState.getNthNextPlayer(-i)); } view.getSidePanel().setPlayers(players); view.getSidePanel().setHeapCapacity( roundState.getGameSettings().getTotalStones()); view.getSidePanel().setHeapSize(roundState.getGameHeap().getSize()); } /** * Override this * * @param turnControl * current turn control */ protected void addTurnControlListeners(ITurnControl turnControl) { } /** * Creates new turn control of the specified type * * @param type * of the new turn control * @return the new turn control */ protected ITurnControl createTurnControl(Type type) { return TurnControlFactory.getFactory(type).create(); } /** * Deal each player the number of stones specified in the game settings * (numberOfStonesDealt) */ protected void deal() { for (int i = 0; i < roundState.getPlayerCount(); i++) { IHand hand = roundState.getNthNextPlayer(i).getHand(); for (int j = 0; j < roundState.getGameSettings().getNumberOfStonesDealt(); j++) { hand.drop(roundState.getGameHeap().drawStone(), new Position(0, 0)); } } view.getSidePanel().setHeapSize(roundState.getGameHeap().getSize()); } /** * End the players turn * * @param invalidTurnInfo * info about the player's last turn */ protected void endOfTurn(InvalidTurnInfo invalidTurnInfo) { boolean isHuman = roundState.getActivePlayer().getPlayerSettings() .getType() == Type.HUMAN; turnControl = null; view.getTablePanel().setStoneSets(invalidTurnInfo.getTable()); if (invalidTurnInfo.getType() != null) { if (isHuman) { view.setBottomPanel(BottomPanelType.INVALID_TURN_PANEL); } view.setInvalidStoneSets(invalidTurnInfo.getInvalidSets()); switch (invalidTurnInfo.getType()) { case INITIAL_MELD_ERROR: view.setInitialMeldFirstError(); break; case INVALID_SETS: view.setStoneCollectionHidden(true); break; case NOT_ENOUGH_POINTS: view.setInitialMeldError(roundState.getGameSettings() .getInitialMeldThreshold()); break; } if (!isHuman) { nextPlayer(); } return; } view.setBottomPanel(BottomPanelType.NONHUMAN_HAND_PANEL); view.getPlayerPanel().setTime(roundState.getGameSettings().getTotalTime(), roundState.getGameSettings().getTotalTime()); nextPlayer(); } /** * Set the next player as active player if the round is not finished */ protected void nextPlayer() { view.setSelectedStones(Collections. emptyList()); view.setInvalidStoneSets(Collections. emptyList()); view.setStoneCollectionHidden(false); if (roundState.getActivePlayer().getHand().getSize() == 0) { endOfRound(); return; } if (roundState.getLastPlayer() == null) { if (roundState.getGameHeap().getSize() == 0) { roundState.setLastPlayer(roundState.getActivePlayer()); roundState.nextPlayer(); roundState.nextTurn(); } else { roundState.nextPlayer(); roundState.nextTurn(); } } else { if (roundState.getActivePlayer() == roundState.getLastPlayer()) { endOfRound(); return; } else { roundState.nextPlayer(); roundState.nextTurn(); } } prepareTurn(); } /** * Ends the current round and emits an event setting the score */ void endOfRound() { removeListeners(); Score roundScore = score(); endOfRoundEvent.emit(roundScore); } /** * Removes all listeners form the connections */ private void removeListeners() { for (Connection c : connections) { c.remove(); } } /** * Calculate the score for the current round and the total game score * * @return the new score */ private Score score() { List winners = new ArrayList(); List points = new ArrayList(); boolean foundRegularWinner = false; int winnerPlayerNumber = 0; Pair bestScore = new Pair( Integer.MIN_VALUE, Integer.MAX_VALUE); int pointSum = 0; for (int i = 0; i < roundState.getPlayerCount(); i++) { IPlayer player = roundState.getNthPlayer(i); IHand playerHand = player.getHand(); boolean isWinner = playerHand.getSize() == 0; winners.add(isWinner); if (isWinner) { foundRegularWinner = true; winnerPlayerNumber = i; } int stonePoints = 0; if (!player.getLaidOut()) { stonePoints = playerHand.isInitialMeldPossible(roundState .getGameSettings()) ? 200 : 100; } else { stonePoints = playerHand.getStonePoints(roundState.getGameSettings()); } bestScore = updateBestScore(bestScore, -stonePoints, playerHand.getSize()); points.add(-stonePoints); pointSum += stonePoints; } if (foundRegularWinner) { points.set(winnerPlayerNumber, pointSum); } else { for (int i = 0; i < roundState.getPlayerCount(); i++) { if (bestScore.equals(new Pair(points.get(i), roundState.getNthPlayer(i).getHand().getSize()))) { winners.set(i, true); } } } return new Score(winners, points); } /** * Update the best score to find the winner in case of special game end * (everybody still has stones on hand) * * @param bestScore * of previous rounds * @param stonePoints * sum of points still left on hands * @param size * number of players in game (= size of score list in columns) * @return Pair of maximum points and hand size */ private static Pair updateBestScore( Pair bestScore, int stonePoints, int size) { if (bestScore.getFirst() == stonePoints) { return new Pair(stonePoints, Math.min( bestScore.getSecond(), size)); } else if (bestScore.getFirst() < stonePoints) { return new Pair(stonePoints, size); } return bestScore; } /** * Emitted when the round is aborted and needs to be restarted * * @return the event */ public IEvent getRestartRoundEvent() { return restartRoundEvent; } /** * Redeal stones and restart round if a player was allowed to redeal and chose * to do so */ private void redeal() { turnControl = null; for (Connection c : new ArrayList(connections)) { c.remove(); } restartRoundEvent.emit(); } }