Java tutorial
/* * Clock.java * * Copyright (c) 2010, 2011 Roberto Corradini. All rights reserved. * * This file is part of the reversi program * http://github.com/rcrr/reversi * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 3, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA * or visit the site <http://www.gnu.org/licenses/>. */ package rcrr.reversi; import java.util.Map; import java.util.EnumMap; import java.util.Collections; import java.text.DecimalFormat; import java.text.NumberFormat; import org.joda.time.DateTimeConstants; import org.joda.time.Duration; import org.joda.time.Period; import rcrr.reversi.board.Player; /** * The {@code Clock} class defines the different clocks used in the Reversi game. * It has two properties, the black's remaining time, and the white's one. * The remaining times are received and returned by clock's methods by * means of duration objects, as defined by the Joda-Time {@code Duration} class. * <p> * {@code Clock} is immutable. * <p> * Three static factories are available In order to create a new clock instance. * The first is: * <pre> * {@code * Clock c = Clock.valueOf(blackDuration, whiteDuration); * } * </pre> * where the parameters {@code blackDuration} and {@code whiteDuration} are of type {@code Duration}. * The second factory is: * <pre> * {@code * Clock c = Clock.valueOf(durations); * } * </pre> * where the {@code durations} parameter is a map defined having objects of the {@code Player} class as keys * and objects of the {@code Duration} class as values. * And the third factory is: * <pre> * {@code * Clock c = Clock.initialClock(gameDuration); * } * </pre> * where the {@code gameDuration} parameter is the time assigned equally to both players. * <p> * Another way to create a new clock instance is by calling the {@code set} * method. It returns a new clock subtracting the provided delta time * to the specified player. For instance let say that we have already a {@code Clock c} and that we want to * subtract one full second from the black player: * <pre> * {@code * Clock c; * ... * Duration amount = Seconds.ONE.toStandardDuration(); * Clock updated = c.set(Player.BLACK, amount); * } * </pre> * where {@code Seconds.ONE.toStandardDuration()} parameter is * a one second duration object belonging to the {@code Duration} class. */ public final class Clock { /** * A clock builder is a facility to generate clock instances for testing. * <p> * {@code ClockBuilder} is mutable, and it is thread-safe. * The object status is guarded by a lock on {@code this}. */ /** * An instance of this class encapsulates the information needed to instantiate * and initialize a clock object. That process is triggered when the {@code build()} * method is called. * <p> * The builder has one property, the {@code durations} map. It is initialized as follow: * <ul> * <li>{@code Duration A_DURATION = Period.minutes(1).toStandardDuration();}</li> * <li>{@code durations.put(Player.BLACK, A_DURATION);}</li> * <li>{@code durations.put(Player.WHITE, A_DURATION);}</li> * </ul> * <p> * The {@code Builder} class is mutable, and it is thread-safe. * The object status is guarded by a lock on {@code this}. */ public static final class Builder { /** A generic duration. */ private static final Duration A_DURATION = Period.minutes(1).toStandardDuration(); /** The durations field. */ private Map<Player, Duration> durations; /** * Construct a new builder. */ public Builder() { this.durations = new EnumMap<Player, Duration>(Player.class); this.durations.put(Player.BLACK, A_DURATION); this.durations.put(Player.WHITE, A_DURATION); } /** * Returns a new instance of a clock object. * * @return the clock instance as prepared by the current clock's builder */ public synchronized Clock build() { return Clock.valueOf(durations); } /** * The method returns the player's duration. * * @param player the player to whom get the remaining duration * @return the remaning duration for the given player */ private synchronized Duration get(final Player player) { return this.durations.get(player); } /** * The method assigns a duration to the given player. * * @param player the player to whom set the remaning duration * @param duration the assigning duration */ private synchronized void put(final Player player, final Duration duration) { this.durations.put(player, duration); } /** * The setter method for the durations field. * * @param durations the update for the durations field */ private synchronized void setDurations(final Map<Player, Duration> durations) { this.durations = durations; } /** * Returns the {@code this} reference after setting the new {@code duration} * value to the {@code player} entry. * * @param player the player to whom assign the remaining duration * @param duration the assigning duration * @return the {@code this} reference */ public Clock.Builder withDuration(final Player player, final Duration duration) { put(player, duration); return this; } /** * Returns the {@code this} reference after setting the new {@code durations} * field value. * * @param durations the field hosting the two players remaning durations * @return the {@code this} reference */ public Clock.Builder withDurations(final Map<Player, Duration> durations) { setDurations(durations); return this; } } /** It is really needed? */ private static final NumberFormat TIME_FORMATTER = new DecimalFormat("##00"); /** Prime number 17. */ private static final int PRIME_NUMBER_17 = 17; /** Prime number 37. */ private static final int PRIME_NUMBER_37 = 37; /** * Class static factory that returns a new clock object with the given initial value * assigned equally to both players. * <p> * Parameter {@code initialDuration} must be not {@code null}, and must be not negative. * * @param initialDuration the game's duration assigned to the two players * @return a new clock having black's and white's time duration * set to the same given value * @throws NullPointerException if parameter {@code initialDuration} is {@code null} * @throws IllegalArgumentException if parameter {@code initialDuration} is shorter than a zero duration */ public static Clock initialClock(final Duration initialDuration) { if (initialDuration == null) { throw new NullPointerException("Parameter initialDuration cannot be null."); } if (initialDuration.isShorterThan(Duration.ZERO)) { throw new IllegalArgumentException("Parameter initialDuration cannot be negative."); } return valueOf(transientDurations(initialDuration, initialDuration)); } /** * Class static factory that returns a new clock object constructed using the given map. * <p> * Parameter {@code durations} must satisfy the following conditions: * <ul> * <li>Must be not {@code null}</li> * <li>Must have a number of entries equal to the players' count</li> * <li>Must not have {@code null} keys</li> * <li>Must not have {@code null} values</li> * <li>Must not have negative duration values</li> * </ul> * * @param durations the game's time assigned to the players * @return a new clock having black's and white's time set to the * given durations * @throws NullPointerException when parameter {@code durations} is {@code null}, * or it has a {@code null} key, * or it has a {@code null} value * @throws IllegalArgumentException when parameter {@code durations} size is different from the number * of players, or duration values are negative */ public static Clock valueOf(final Map<Player, Duration> durations) { if (durations == null) { throw new NullPointerException("Parameter durations cannot be null."); } if (durations.size() != Player.values().length) { throw new IllegalArgumentException("Parameter durations size is not consistent." + " durations.size()=" + durations.size() + " expected value: " + Player.values().length); } if (durations.containsKey(null)) { throw new NullPointerException("Parameter durations cannot have null keys."); } if (durations.containsValue(null)) { throw new NullPointerException("Parameter durations cannot have null values."); } for (Duration duration : durations.values()) { if (duration.isShorterThan(Duration.ZERO)) { throw new IllegalArgumentException( "Parameter durations cannot have negative values." + " durations=" + durations); } } return new Clock(durations); } /** * Returns a String representing the clock. * The format is mm:ss corresponding to the given time in milliseconds, where: * - mm is the amount of minutes * - ss is the amount of seconds * * @param duration time in milliseconds * @return a formatted {@code String} with minutes and seconds */ private static String timeString(final Duration duration) { final long durationAsMilliseconds = duration.getMillis(); final long durationAsSeconds = durationAsMilliseconds / DateTimeConstants.MILLIS_PER_SECOND; final long minutes = durationAsSeconds / DateTimeConstants.SECONDS_PER_MINUTE; final long seconds = durationAsSeconds - (minutes * DateTimeConstants.SECONDS_PER_MINUTE); return TIME_FORMATTER.format(minutes) + ":" + TIME_FORMATTER.format(seconds); } /** * Returns a new map object with the given duaration values * assigned to each individual player. * * @param blackDuration the game's time assigned to the Black player * @param whiteDuration the game's time assigned to the White player * @return a new map having black's and white's time set to the * given parameters */ private static Map<Player, Duration> transientDurations(final Duration blackDuration, final Duration whiteDuration) { Map<Player, Duration> transientDurations = new EnumMap<Player, Duration>(Player.class); transientDurations.put(Player.BLACK, blackDuration); transientDurations.put(Player.WHITE, whiteDuration); return transientDurations; } /** * The durations field. * It stores each players' clock time as a Joda-Time Duration. * Internally Durations store the time in milliseconds. */ private final Map<Player, Duration> durations; /** Lazily initialized, cached hashCode. */ private volatile int hashCode = 0; /** * Class constructor. * <p> * This constructor creates a Clock object given the Black's time and the White's * one. * * @param durations a map having the remaining time duration assigned to each player. */ private Clock(final Map<Player, Duration> durations) { assert (durations != null) : "Parameter durations cannot be null."; assert (durations.size() == Player.values().length) : "Parameter durations size is not consistent." + " durations.size()=" + durations.size() + ", expected value: " + Player.values().length; assert (!durations.containsKey(null)) : "Parameter durations cannot contains null keys."; assert (!durations.containsValue(null)) : "Parameter durations cannot contains null values."; this.durations = Collections.unmodifiableMap(new EnumMap<Player, Duration>(durations)); } /** * Returns a new clock object generated subtracting the delta value from * the specified player remaining clock time. * <p> * When the delta parameter is greather than the player's actual time * the updated value is set to zero. * <p> * Parameter {@code player} must be not {@code null}. * Parameter {@code delta} must be not {@code null}, and must be not negative. * * @param player the player from which to take away the specified time * @param delta the amount of time in milliseconds to subtract from the * player's clock time * @return a new updated clock * @throws NullPointerException if the player or delta parameter is null * @throws IllegalArgumentException if the delta parameter is negative. */ public Clock decrement(final Player player, final Duration delta) { if (player == null) { throw new NullPointerException("Parameter player connot be null"); } if (delta == null) { throw new NullPointerException("Parameter delta connot be null."); } if (delta.isShorterThan(Duration.ZERO)) { throw new IllegalArgumentException("Parameter delta cannot be negative."); } final Duration actual = this.durations.get(player); final Duration updated = (actual.isLongerThan(delta)) ? actual.minus(delta) : Duration.ZERO; return valueOf(set(player, updated)); } /** * Returns true if the specified object is equal to this clock. * Two clockss are equal when they have the same remaining times. * * @param object the object to compare to * @return {@code true} when the {@code object} parameter is an instance of * the {@code Clock} class and when the time remaining to both * players is equal. */ @Override public boolean equals(final Object object) { if (object == this) { return true; } if (!(object instanceof Clock)) { return false; } Clock clock = (Clock) object; for (Player player : Player.values()) { if (!get(player).equals(clock.get(player))) { return false; } } return true; } /** * Returns a {@code Duration} value that represents the player's remaining time * as registered by the clock instance. * <p> * Parameter {@code player} must be not {@code null}. * * @param player the player for which the remaining time is queried * @return the player remaining duration * @throws NullPointerException if the player parameter is null */ public Duration get(final Player player) { if (player == null) { throw new NullPointerException("Parameter player connot be null."); } return this.durations.get(player); } /** * Returns a hash code value for the clock object. * * @return a hash code value for this clock */ @Override public int hashCode() { if (hashCode == 0) { int result = PRIME_NUMBER_17; for (Duration duration : this.durations.values()) { int k = duration.hashCode(); result = PRIME_NUMBER_37 * result + k; } hashCode = result; } return hashCode; } /** * Returns a {@code String} representing the {@code Clock} object. * * @return a {@code String} representing the clock */ @Override public String toString() { return "[" + Player.BLACK + "=" + timeString(get(Player.BLACK)) + ", " + Player.WHITE + "=" + timeString(get(Player.WHITE)) + "]"; } /** * Returns a formatted string, showing the two player clocks. * * @return a string showing the two player's clocks */ public String printClock() { return "[" + Player.BLACK.symbol() + "=" + timeString(get(Player.BLACK)) + ", " + Player.WHITE.symbol() + "=" + timeString(get(Player.WHITE)) + "]"; } /** * Returns a new durations map, having the duration associated to player updated * with the given value. * * @param player the player to apply the update * @param updated the new duration value * @return a new durations map */ private Map<Player, Duration> set(final Player player, final Duration updated) { assert (player != null) : "Parameter player cannot be null."; assert (updated != null) : "Parameter updated cannot be null."; final Map<Player, Duration> transientDurations = new EnumMap<Player, Duration>(this.durations); transientDurations.put(player, updated); return transientDurations; } }