package jrummikub.model; import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import jrummikub.util.Pair; import static jrummikub.model.StoneSet.Type.*; /** Class managing {@link Stone}s joined together to form sets */ public class StoneSet implements Iterable, Sizeable, Serializable { private static final long serialVersionUID = -3852631195648599398L; static final float VERTICAL_BORDER = 0.5f; static final float HORIZONTAL_BORDER = 0.125f; private ArrayList stones; /** * Create a new single stone stone set * * @param stone * single stone of the set */ public StoneSet(Stone stone) { stones = new ArrayList(Collections.singletonList(stone)); } /** * Create a stone set from a list of stones * * @param stones * list of stones to build a set of */ public StoneSet(List stones) { this.stones = new ArrayList(stones); } /** Validity type of the set */ public enum Type { /** Set is a valid group */ GROUP, /** Set is a valid run */ RUN, /** Set is invalid */ INVALID } /** * Test for rule conflict within the StoneSet * * @param settings * GameSettings * * @return true when the set is valid according to the rules */ public boolean isValid(GameSettings settings) { return classify(settings).getFirst() != INVALID; } /** * Test for rule conflict within the StoneSet and determine whether the set * is a group or a run * * @param settings * GameSettings * * @return GROUP or RUN for valid sets, INVALID otherwise */ public Pair classify(GameSettings settings) { if (stones.size() < 3) { return new Pair(INVALID, 0); } int nonJoker = -1; for (int i = 0; i < stones.size(); i++) { if (stones.get(i).isJoker()) { continue; } nonJoker = i; } if (nonJoker == -1) { return classifyJokersOnly(settings, nonJoker); } int runScore = isValidRun(nonJoker, settings); int groupScore = isValidGroup(stones.get(nonJoker).getValue(), settings); if (runScore > groupScore) { return new Pair(RUN, runScore); } else if (groupScore == 0) { return new Pair(INVALID, 0); } else { return new Pair(GROUP, groupScore); } } private Pair classifyJokersOnly(GameSettings settings, int nonJoker) { if (stones.size() > settings.getHighestValue() && stones.size() > settings.getStoneColors().size() && !settings.isNoLimits()) { return new Pair(INVALID, 0); } else if (stones.size() > settings.getStoneColors().size()) { int value = 0; int stoneValue = settings.getHighestValue(); for (int i = 0; i < stones.size(); i++) { value += stoneValue; stoneValue--; if (stoneValue == 0) { stoneValue = settings.getHighestValue(); } } return new Pair(RUN, value); } else { return new Pair(GROUP, stones.size() * settings.getHighestValue()); } } /** * Test for rule conflict within the StoneSet, assuming we have a run * * @param referencePosition * position of stone used as reference (any non-joker stone) * @param settings */ private int isValidRun(int referencePosition, GameSettings settings) { StoneColor runColor = stones.get(referencePosition).getColor(); int startValue = stones.get(referencePosition).getValue() - referencePosition; int endValue = startValue + stones.size() - 1; if (!settings.isNoLimits()) { if (startValue < 1 || endValue > settings.getHighestValue()) { return 0; } } int value = 0; for (int i = 0; i < stones.size(); i++) { int expectedValue = i + startValue; while (expectedValue < 1) { expectedValue += settings.getHighestValue(); } while (expectedValue > settings.getHighestValue()) { expectedValue -= settings.getHighestValue(); } value += expectedValue; if (stones.get(i).isJoker()) { continue; } if (stones.get(i).getColor() != runColor) { return 0; } if (stones.get(i).getValue() != expectedValue) { return 0; } } return value; } /** * Test for rule conflict within the StoneSet, assuming we have a group * * @param settings */ private int isValidGroup(int value, GameSettings settings) { if (stones.size() > settings.getStoneColors().size()) { return 0; } Set seenColors = new HashSet(); for (Stone i : stones) { if (i.isJoker()) { continue; } if (i.getValue() != value) { return 0; } if (seenColors.contains(i.getColor())) { return 0; } else { seenColors.add(i.getColor()); } } return value * stones.size(); } /** * Splits the StoneSet at a specified {@link Position} and returns two new * Stone Sets * * @param position * Splitting {@link Position} * @return A pair of StoneSets, one for each split part */ public Pair splitAt(int position) { if (position == 0) { return new Pair(null, this); } else if (position == stones.size()) { return new Pair(this, null); } StoneSet firstSet = new StoneSet(stones.subList(0, position)); StoneSet secondSet = new StoneSet(stones.subList(position, stones.size())); return new Pair(firstSet, secondSet); } /** * Joins StoneSet to another StoneSet and returns the resulting new StoneSet * * @param other * StoneSet to be joined to active StoneSet * @return the combined StoneSet */ public StoneSet join(StoneSet other) { List joinedList = new ArrayList(); joinedList.addAll(stones); joinedList.addAll(other.stones); return new StoneSet(joinedList); } /** * Returns the number of stones in the set. * * @return number of stones */ public int getSize() { return stones.size(); } /** * Returns the i-th stone of the set (starting with 0) * * @param i * number of the stone to return * @return the i-th stone */ public Stone get(int i) { return stones.get(i); } @Override public Iterator iterator() { final Iterator it = stones.iterator(); return new Iterator() { @Override public boolean hasNext() { return it.hasNext(); } @Override public Stone next() { return it.next(); } @Override public void remove() { // removing stones is impossible throw new UnsupportedOperationException(); } }; } @Override public float getWidth() { return stones.size() + 2 * VERTICAL_BORDER; } @Override public float getHeight() { return 1 + 2 * HORIZONTAL_BORDER; } @Override public String toString() { String ret = "StoneSet["; boolean first = true; for (Stone stone : stones) { if (!first) ret += ","; ret += stone.toString(); first = false; } return ret + "]"; } }