/**
 * <p>
 * Copyright © 2009-2010, Bruce-Robert Pocock
 * </p>
 * <p>
 * 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 of the License, or (at
 * your option) any later version.
 * </p>
 * <p>
 * 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.
 * </p>
 * <p>
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 * </p>
 * 
 * @author brpocock
 */
package org.starhope.appius.game;

import java.math.BigDecimal;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentSkipListSet;

import org.json.JSONException;
import org.json.JSONObject;
import org.starhope.appius.except.UserDeadException;
import org.starhope.appius.sql.SQLPeerDatum;
import org.starhope.appius.types.AbstractZone;
import org.starhope.appius.types.RoomAndZone;
import org.starhope.appius.user.AbstractUser;
import org.starhope.appius.user.User;
import org.starhope.appius.util.AcceptsMetronomeTicks;
import org.starhope.appius.util.AppiusConfig;
import org.starhope.util.LibMisc;

/**
 * A GameEvent is a room-wide (or multi-room) game that occurs within
 * the larger context of the game. Think “arena” or similar.
 * 
 * @author brpocock
 */
public abstract class GameEvent extends SQLPeerDatum implements
AcceptsMetronomeTicks, RoomListener, Comparable <GameEvent> {

	/**
	 * Java serialization unique ID serialVersionUID (long)
	 */
	private static final long serialVersionUID = -6751701646654524655L;

	/**
	 * Queue for state changes in all GameEvents
	 */
	private static ConcurrentLinkedQueue <Entry <GameEvent, GameStateFlag>> stateChangeQueue = new ConcurrentLinkedQueue <Entry <GameEvent, GameStateFlag>> ();

	/**
	 * Propagate any waiting game state changes to listeners
	 */
	public static void propagateGameStateChange () {
		try {
			while (true) {
				final Entry <GameEvent, GameStateFlag> e = GameEvent.stateChangeQueue
				.remove ();
				final GameEvent ev = e.getKey ();
				final GameStateFlag state = e.getValue ();
				AppiusClaudiusCaecus
				.blather ("game state pump firing game "
						+ ev.toString () + " to "
						+ state.toString ());
				for (final AbstractUser who : ev.getEveryone ()) {
					who.acceptGameStateChange (ev, state);
				}
				ev.acceptGameStateChange (ev, state);
				ev.freezeTag = false;
			}
		} catch (final NoSuchElementException e) {
			// blather ("game state pump emptied");
			return;
		}
	}

	/**
	 * Shitty recursion guard endingSolo (GameEvent)
	 */
	private boolean endingSolo = false;

	/**
	 * freeze game actions
	 */
	protected boolean freezeTag = false;

	/**
	 * the one-char identifier for this game
	 */
	protected char gameCode;

	/**
	 * the current game state (mode)
	 */
	protected GameStateFlag gameState = GameStateFlag.GAME_SOLO;

	/**
	 * the events in the global events table for each player, used for
	 * recording scores
	 */
	private final ConcurrentHashMap <Integer, Integer> playerEvents = new ConcurrentHashMap <Integer, Integer> ();

	/**
	 * the players (in any room)
	 */
	protected ConcurrentSkipListSet <Integer> players = new ConcurrentSkipListSet <Integer> ();

	/**
	 * The rooms which this GameEvent controls
	 */
	protected final ConcurrentSkipListSet <Room> rooms = new ConcurrentSkipListSet <Room> ();

	/**
	 * the current scores for all players
	 */
	protected final ConcurrentHashMap <Integer, Integer> scores = new ConcurrentHashMap <Integer, Integer> ();

	/**
	 * The rooms which this GameEvent will report the score to (in
	 * addition to {@link #rooms}) scoreWatchRooms (GameEvent)
	 */
	protected final ConcurrentSkipListSet <Room> scoreWatchRooms = new ConcurrentSkipListSet <Room> ();

	/**
	 * The game (countdown/play time) timer
	 */
	private long timer;

	/**
	 * The zone zone (GameEvent)
	 */
	private final Zone zone;

	/**
	 * @param z the zone in which the game is being played
	 */
	protected GameEvent (final Zone z) {
		gameCode = 'ø';
		zone = z;
	}

	/**
	 * @param u The operator issuing the command
	 * @param arena The room in which the operator's command is being
	 *        executed
	 * @param command The command and parameters
	 */
	public void acceptCommand (final AbstractUser u,
			final AbstractRoom arena, final String [] command) {
		/* No op */
	}

	/**
	 * Accept a developer-level command and react to it.
	 * 
	 * @param zone2 The zone in which the game is attached (should be
	 *            "zone" usually)
	 * @param room The room in which the invoking user exists
	 * @param user The invoking user
	 * @param command A command string split on whitespace
	 */
	public void acceptCommand (final User user,
			final AbstractRoom room, final Zone zone2,
			final String [] command) {
		if ("#pumpstate".equals (command [0])) {
			GameEvent.propagateGameStateChange ();
			user.acceptAdminMessage ("pumpstate", "pumpstate", "DM");
			return;
		}
		if ("#changestate".equals (command [0])) {
			final GameStateFlag newState;

			if ("#countdown".equals (command [1])) {
				newState = GameStateFlag.GAME_COUNTDOWN;
			} else if ("#solo".equals (command [1])) {
				newState = GameStateFlag.GAME_SOLO;
			} else if ("#running".equals (command [1])) {
				newState = GameStateFlag.GAME_RUNNING;
			} else {
				user
				.acceptAdminMessage (
						"#changestate to [ #solo | #countdown | #running ]",
						"#changestate", "DM");
				return;
			}
			user.acceptAdminMessage ("change state to "
					+ newState.toString (), "#changestate", "DM");
			changeGameState (newState);
			return;
		}
		if ("#resetplayers".equals (command [0])) {
			resetPlayers ();
			user.acceptAdminMessage ("resetPlayers: OK",
					"resetPlayers " + getGameShortName (), "DM");
			return;
		}
		if ("#roster".equals (command [0])) {
			final StringBuilder msg = new StringBuilder ();
			msg.append ("Roster for GameEvent:\n");
			for (final Integer player : players) {
				final AbstractUser playerUser = User.getByID (player);
				if (null == playerUser) {
					msg.append (" <INVALID> UserID#" + player);
				} else {
					msg.append (playerUser.getAvatarLabel ());
					if (false == playerUser.isOnline ()) {
						msg.append ("<OFFLINE> ");
					}
				}
				msg.append (", ");
			}
			msg.append ("<END>");
			user.acceptAdminMessage (msg.toString (), "Roster for "
					+ getGameShortName (), "DM");
			return;
		}
		if ("#scores".equals (command [0])) {
			final StringBuilder msg = new StringBuilder ();
			msg.append ("Scores (");
			msg.append (toString ());
			msg.append (")\n");
			for (final Entry <Integer, Integer> score : scores
					.entrySet ()) {
				final AbstractUser u = User.getByID (score.getKey ());
				if (null != u) {
					msg.append (u.getAvatarLabel ());
					msg.append ('\t');
					msg.append (score.getValue ());
					msg.append ('\n');
				}
			}
			user.acceptAdminMessage (msg.toString (), "scores", "DM");
			return;
		}
		if ("#state".equals (command [0])) {
			user.acceptAdminMessage ("Game\t" + toString ()
					+ "\ncode\t" + gameCode + "\nstate\t"
					+ gameState.toString () + "\ntimer\t" + timer
					+ "\nendingSolo\t" + endingSolo + "\n#rooms\t"
					+ rooms.size () + "\n#scoreWatchRooms\t"
					+ scoreWatchRooms.size () + "\nfreezeTag\t"
					+ freezeTag + "\nzone\t" + zone.toString (),
					"Game State " + getGameShortName (), "DM");
			return;
		}
		if ("#purge".equals (command [0])) {
			int purgedNull = 0;
			int purgedOffline = 0;
			for (final Integer player : players) {
				final AbstractUser playerUser = User.getByID (player);
				if (null == playerUser) {
					players.remove (player);
					++purgedNull;
				} else if (false == playerUser.isOnline ()) {
					players.remove (player);
					++purgedOffline;
				}
			}
			user.acceptAdminMessage ("Purged " + purgedNull
					+ " nulls and " + purgedOffline
					+ " dead players from roster", "Purge", "DM");
			return;
		}
		zone2.sendOops (user);
		return;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.RoomListener#acceptGameStateChange(org.starhope.appius.game.GameEvent,
	 *      org.starhope.appius.game.GameStateFlag)
	 */
	public void acceptGameStateChange (final GameEvent game,
			final GameStateFlag newGameState) {
		assert game == this;
		gameState = newGameState;
		updateRoomVars ();
		sendTimers ();
		freezeTag = false;
	}

	/**
	 * @see org.starhope.appius.game.RoomListener#acceptObjectJoinRoom(AbstractRoom,
	 *      RoomListener)
	 */
	public void acceptObjectJoinRoom (final AbstractRoom room,
			final RoomListener newListener) {
		if (newListener instanceof AbstractUser) {
			final AbstractUser newUser = (AbstractUser) newListener;
			final String playerTag = newUser.hasVariable ("player") ? newUser
					.getVariable ("player") : "0";
					AppiusClaudiusCaecus.blather (newUser.getAvatarLabel (),
							room.getName (), "", " *** User entered game room",
							false);
					if (0 != Integer.parseInt (playerTag)) {
						if (players.size () == 0) {
							changeGameState (GameStateFlag.GAME_SOLO);
						}
						players.add (newUser.getUserID ());
						if (!scores.containsKey (newUser.getUserID ())) {
							scores.put (newUser.getUserID (), 0);
						}
						if (players.size () > 1
								&& GameStateFlag.GAME_SOLO == gameState) {
							// start game play when 2º player has just entered
							changeGameState (GameStateFlag.GAME_COUNTDOWN);
						}
					}
					updateRoomVars ();
					sendTimers ();
		}
	}

	/**
	 * @see org.starhope.appius.game.RoomListener#acceptObjectPartRoom(AbstractRoom,
	 *      RoomListener)
	 */
	public void acceptObjectPartRoom (final AbstractRoom room,
			final RoomListener object) {
		if (object instanceof AbstractUser
				&& players.contains ( ((AbstractUser) object)
						.getUserID ())) {
			if (GameStateFlag.GAME_SOLO == gameState) {
				if ( !endingSolo) {
					endingSolo = true;
					sendEndEvents (getGameEventPrefix () + ".solo");
					endingSolo = false;
				}
				changeGameState (GameStateFlag.GAME_SOLO);
			}
			updateRoomVars ();
		}
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.RoomListener#acceptPublicMessage(AbstractUser,
	 *      AbstractRoom, String)
	 */
	public void acceptPublicMessage (final AbstractUser sender,
			final AbstractRoom room, final String message) {
		this.acceptPublicMessage (sender, message);
	}

	/**
	 * @param newState the new state of the game
	 */
	protected void changeGameState (final GameStateFlag newState) {
		freezeTag = true;
		if (GameStateFlag.GAME_COUNTDOWN == newState) {
			timer = getCountdownDuration ();
		} else if (GameStateFlag.GAME_RUNNING == newState) {
			timer = getGameDuration ();
		}

		final HashMap <GameEvent, GameStateFlag> trickery = new HashMap <GameEvent, GameStateFlag> ();
		trickery.put (this, newState);

		GameEvent.stateChangeQueue.addAll (trickery.entrySet ());
	}

	/**
	 * This is an overriding method.
	 * 
	 * @param other the other game event
	 * @return the ordering between the two based upon game code
	 * @see java.lang.Comparable#compareTo(java.lang.Object)
	 */
	public int compareTo (final GameEvent other) {
		return other.getGameCode () > gameCode ? 1 : -1;
	}

	/**
	 * decrease a user's score, but do not allow it to drop below 0.
	 * 
	 * @param userID who lost points
	 * @param howMuch the number of points to lose
	 */
	protected void decrementScore (final int userID, final int howMuch) {
		if (scores.containsKey (userID)) {
			final Integer score = scores.get (userID);
			if (score == 0) return;
			if (score < howMuch) {
				incrementScore (userID, -score);
				return;
			}
		}
		incrementScore (userID, -howMuch);
	}

	/**
	 * Stupid case of equals override.
	 * 
	 * @param other other game event
	 * @return true if they're the same
	 */
	public boolean equals (final GameEvent other) {
		return this == other;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals (final Object other) {
		if (other instanceof GameEvent)
			return this.equals ((GameEvent) other);
		return false;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.sql.SQLPeerDatum#getCacheUniqueID()
	 */
	@Override
	protected String getCacheUniqueID () {
		return toString ();
	}

	/**
	 * Time (in ms) for the game countdown period timer. Can be
	 * overridden by derived classes.
	 * 
	 * @return The game countdown timer in ms
	 */
	protected long getCountdownDuration () {
		return AppiusConfig.getIntOrDefault (
				"org.starhope.appius.games.countdownTime", 30000);
	}

	/**
	 * @return all players and spectators
	 */
	public Set <AbstractUser> getEveryone () {
		final Set <AbstractUser> everyone = getPlayers ();
		everyone.addAll (getSpectators ());
		return everyone;
	}

	/**
	 * @return the gameCode
	 */
	public char getGameCode () {
		return gameCode;
	}

	/**
	 * Time (in ms) for the game play period timer. Can be overridden by
	 * derived classes.
	 * 
	 * @return The game play timer in ms
	 */
	protected long getGameDuration () {
		return timer = AppiusConfig.getIntOrDefault (
				"org.starhope.appius.games.playTime", 120000);
	}

	/**
	 * Get the prefix to be applied to event types for this game
	 * 
	 * @return the string prefix used to find event types for this game
	 */
	public abstract String getGameEventPrefix ();

	/**
	 * @return the short name of the game: the class name without its
	 *         package (canonical) prefix
	 */
	public String getGameShortName () {
		final String [] canonicalName = this.getClass ()
		.getCanonicalName ().split ("\\.");
		return canonicalName [canonicalName.length - 1];
	}

	/**
	 * @return bonus points awarded to the leader, or else anyone tied
	 *         for max score
	 */
	protected int getLeaderBonus () {
		return 0;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.RoomListener#getLocation()
	 */
	public RoomAndZone getLocation () {
		return new RoomAndZone (rooms.first (), zone);
	}

	/**
	 * @return all players
	 */
	public Set <AbstractUser> getPlayers () {
		final HashSet <AbstractUser> ret = new HashSet <AbstractUser> ();
		for (final int playerID : players) {
			ret.add (User.getByID (playerID));
		}
		return ret;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.RoomListener#getRoom()
	 */
	public AbstractRoom getRoom () {
		return rooms.first ();
	}

	/**
	 * Get all of the rooms participating in this GameEvent
	 * 
	 * @return the set of all rooms participating in this GameEvent
	 */
	public Set <Room> getRooms () {
		final HashSet <Room> myRooms = new HashSet <Room> ();
		myRooms.addAll (rooms);
		return myRooms;
	}

	/**
	 * Get all of the rooms which are either participating in this
	 * GameEvent, or monitoring its scores
	 * 
	 * @return all rooms participating in, or watching the score from,
	 *         this GameEvent
	 */
	public Set <Room> getScoreWatchRooms () {
		final HashSet <Room> watchingRooms = new HashSet <Room> ();
		watchingRooms.addAll (rooms);
		watchingRooms.addAll (scoreWatchRooms);
		return watchingRooms;
	}

	/**
	 * @return all spectators
	 */
	public Set <AbstractUser> getSpectators () {
		final HashSet <AbstractUser> ret = new HashSet <AbstractUser> ();
		for (final AbstractRoom r : getRooms ()) {
			for (final AbstractUser guy : r.getAllUsers ()) {
				if ( !players.contains (guy.getUserID ())) {
					ret.add (guy);
				}
			}
		}
		return ret;
	}

	/**
	 * @return the timer
	 */
	public long getTimer () {
		return timer;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.game.RoomListener#getZone()
	 */
	public AbstractZone getZone () {
		return zone;
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode () {
		return LibMisc.makeHashCode (getCacheUniqueID ());
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see java.lang.Object#hashCode()
	 */
	// @Override
	// public int hashCode () {
	// return HashCodeBuilder.reflectionHashCode (277, 13, this);
	// }
	/**
	 * @param who user ID of the player to have score increased
	 * @param howMuch increment amount, can be negative
	 */
	public void incrementScore (final int who, final int howMuch) {
		final int score;
		if (scores.containsKey (who)) {
			score = scores.get (who) + howMuch;
		} else {
			AppiusClaudiusCaecus
			.blather (
					"#" + who,
					toString (),
					"",
					"Increment score for someone with no score present",
					false);
			score = howMuch;
		}
		AppiusClaudiusCaecus.blather ("#" + who, toString (), "",
				" Score  = " + score, false);
		scores.put (who, score);
		updateRoomVars ();
		final AbstractUser player = User.getByID (who);
		if (null == player) return;
		final AppiusClaudiusCaecus playerThread = player
		.getServerThread ();
		if (null == playerThread) return;

		sendScoreUpdate (playerThread, score, player);
	}

	/**
	 * Clear all players; clear all scores; put all users into player or
	 * spectator queues
	 */
	protected void resetPlayers () {
		AppiusClaudiusCaecus.blather ("", toString (), "",
				"Resetting players", false);
		playerEvents.clear ();
		players.clear ();
		scores.clear ();
		for (final AbstractRoom room : rooms) {
			for (final AbstractUser user : room.getAllUsers ()) {
				final String isPlayer = user.getVariable ("player");
				if (null != isPlayer && Integer.parseInt (isPlayer) > 0) {
					players.add (user.getUserID ());
					scores.put (user.getUserID (), 0);
				}
			}
		}
		updateScores ();
	}

	/**
	 * @param gameMoniker The unique event moniker for this game
	 */
	protected void sendEndEvents (final String gameMoniker) {
		AppiusClaudiusCaecus.blather ("", toString (), "",
				"sending end events", false);
		int maxScore = 0;
		for (final int id : players) {
			if (scores.containsKey (id)) {
				if (scores.get (id) > maxScore) {
					maxScore = scores.get (id);
				}
			}
		}

		for (final int id : players) {
			final AbstractUser user = User.getByID (id);
			if (null != user) {
				final int userID = user.getUserID ();
				if (playerEvents.containsKey (userID)) {
					AppiusClaudiusCaecus.blather (user
							.getAvatarLabel (), toString (), user
							.getIPAddress (), "sending end event",
							false);
					/*
					 * Check all parameters, very paranoid
					 */
					final Integer eventID = playerEvents.get (userID);
					if (null == eventID)
						throw AppiusClaudiusCaecus
						.fatalBug ("null eventID");
					if (eventID <= 0)
						throw AppiusClaudiusCaecus.fatalBug ("zero");
					Integer playerScore = scores.get (userID);
					if (null == playerScore) {
						playerScore = 0;
					}
					if (maxScore == playerScore) {
						playerScore += getLeaderBonus ();
					}
					final BigDecimal playerScoreDecimal = new BigDecimal (
							playerScore);

					/*
					 * Copy the scores table and sort it
					 */
					final ConcurrentHashMap <Integer, Integer> scoresCopy = new ConcurrentHashMap <Integer, Integer> ();
					scoresCopy.putAll (scores);
					final LinkedHashMap <Integer, Integer> sortedScores = LibMisc
					.sortHashMapByValues (scoresCopy);

					/*
					 * Call the end event for the user and notify them
					 * of the outcome
					 */
					try {
						user.acceptSuccessReply ("endEvent", user
								.endMultiplayerEvent (eventID,
										gameMoniker, "" + gameCode,
										playerScoreDecimal,
										sortedScores), user.getRoom ());
					} catch (final JSONException e) {
						AppiusClaudiusCaecus
						.reportBug (
								"Caught a JSONException in sendEndEvents",
								e);
					}
				} else {
					AppiusClaudiusCaecus.blather (user
							.getAvatarLabel (), toString (), "",
							"user never started", false);
				}
			}
		}
		resetPlayers ();
	}

	/**
	 * Send an update on the score of the game to a player
	 * 
	 * @param playerThread The player's AppiusClaudiusCaecus server
	 *            thread
	 * @param score the player's score
	 * @param player the player him/her-self
	 */
	private void sendScoreUpdate (
			final AppiusClaudiusCaecus playerThread, final int score,
			final AbstractUser player) {
		int place = 1;
		for (final int i : scores.values ()) {
			if (i > score) {
				++place;
			}
		}

		final JSONObject msg = new JSONObject ();
		try {
			msg.put ("from", "scoreUpdate");
			msg.put ("status", "true");
			msg.put ("score", score);
			msg.put ("place", place);
			playerThread.sendResponse (msg, getRoom ().getID ());
		} catch (final JSONException e) {
			AppiusClaudiusCaecus.reportBug (e);
		} catch (final UserDeadException e) {
			// ignore it, they're still “in the game” for now
		}

	}

	/**
	 * Send the start of events to all players
	 * 
	 * @param gameMoniker The game's unique event moniker
	 */
	protected void sendStartEvents (final String gameMoniker) {
		AppiusClaudiusCaecus.blather ("", toString (), "",
				"sending start events", false);
		for (final int id : players) {
			final AbstractUser user = User.getByID (id);
			if (null != user) {
				if (playerEvents.containsKey (user.getUserID ())) {
					AppiusClaudiusCaecus.blather (user
							.getAvatarLabel (), toString (), user
							.getIPAddress (), "already started", false);
				} else {
					AppiusClaudiusCaecus.blather (user
							.getAvatarLabel (), toString (), user
							.getIPAddress (), "starting user", false);
					try {
						final JSONObject result = user
						.startEvent (gameMoniker);

						zone.sendSuccessReply ("startEvent", result,
								user, user.getRoomNumber ());

						playerEvents.put (user.getUserID (), result
								.getInt ("eventID"));
					} catch (final JSONException e) {
						AppiusClaudiusCaecus.reportBug (e);
					}
				}
			}
		}
	}

	/**
	 * Send the game timers out to players and spectators. Uses the
	 * server time broadcast.
	 */
	private void sendTimers () {
		for (final int id : players) {
			final AbstractUser guy = User.getByID (id);
			if (null != guy) {
				try {
					Commands.do_getServerTime (null, guy, guy
							.getRoom ());
				} catch (final JSONException e) {
					AppiusClaudiusCaecus.reportBug (e);
				}
			}
		}
		for (final AbstractUser guy : getSpectators ()) {
			if (guy instanceof User) {
				try {
					Commands.do_getServerTime (null, guy,
							((User) guy).getRoom ());
				} catch (final JSONException e) {
					AppiusClaudiusCaecus.reportBug (e);
				}
			}
		}
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see org.starhope.appius.sql.SQLPeerDatum#set(java.sql.ResultSet)
	 */
	@Override
	protected void set (final ResultSet rs) throws SQLException {
		AppiusClaudiusCaecus.reportBug ("loading GameEvent?");
	}

	/**
	 * @see org.starhope.appius.util.AcceptsMetronomeTicks#tick(long,
	 *      long)
	 */
	public void tick (final long currentTime, final long deltaTime)
	throws UserDeadException {
		if (GameStateFlag.GAME_COUNTDOWN == gameState) {
			timer -= deltaTime;
			if (players.size () < 2) {
				changeGameState (GameStateFlag.GAME_SOLO);
			} else if (timer <= 0) {
				changeGameState (GameStateFlag.GAME_RUNNING);
			}
		} else if (GameStateFlag.GAME_RUNNING == gameState) {
			timer -= deltaTime;
			if (timer < 0) {
				if (players.size () < 2) {
					changeGameState (GameStateFlag.GAME_SOLO);
				} else {
					changeGameState (GameStateFlag.GAME_COUNTDOWN);
				}
			}
		}
	}

	/**
	 * This is an overriding method.
	 * 
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString () {
		return getGameShortName () + "(" + getGameCode () + "@"
		+ getZone ().getName () + ")";

	}

	/**
	 * Send updated room variables with the game score and status.
	 */
	private void updateRoomVars () {
		final HashMap <String, String> vars = new HashMap <String, String> ();
		vars.put ("numPlayers", String.valueOf (players.size ()));
		vars.put ("gameCode", "" + getGameCode ());
		switch (gameState) {
		case GAME_COUNTDOWN:
			vars.put ("leader", "");
			vars.put ("gameState", "countdown");
			break;
		case GAME_RUNNING:
			String leaderName = "";
			int leaderScore = 0;
			for (final Entry <Integer, Integer> who : scores
					.entrySet ()) {
				if (who.getValue () > leaderScore) {
					leaderName = User.getUserNameForID (who
							.getKey ());
					leaderScore = who.getValue ();
				}
			}
			vars.put ("leader", leaderScore + "~" + leaderName);
			vars.put ("gameState", "combat");
			break;
		case GAME_SOLO:
		default:
			vars.put ("leader", "");
			vars.put ("gameState", "practice");
			break;
		}
		for (final Room room : getScoreWatchRooms ()) {
			for (final Entry <String, String> var : vars.entrySet ()) {
				room.setVariable (var);
			}
		}
	}

	/**
	 * notify a player of their score
	 * 
	 * @param who the player whose score is being sent
	 */
	protected void updateScore (final AbstractUser who) {
		if (null != who && who instanceof User) {
			final AppiusClaudiusCaecus playerThread = ((User) who)
			.getServerThread ();
			if (null != playerThread) {
				if (scores.containsKey (who.getUserID ())) {
					final int score = scores.get (who.getUserID ());
					AppiusClaudiusCaecus.blather (
							who.getAvatarLabel (), toString (), "",
							"Score update = " + score, false);
					sendScoreUpdate (playerThread, score, who);
				}
			}
		}
	}

	/**
	 * update all players of their scores and update room vars to boot
	 */
	protected void updateScores () {
		for (final int id : players) {
			final AbstractUser player = User.getByID (id);
			updateScore (player);
		}
		updateRoomVars ();
	}

}
