bd80c348eb
There is no more SafeInterval, instead Intervals can be constructed with a RunQueue to use for expiring. PresentsDObjectMgr implements RunQueue. Client has a getRunQueue() method to get the client side RunQueue. git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@3283 542714f4-19e9-0310-aa3c-eee0fc999fb1
1099 lines
36 KiB
Java
1099 lines
36 KiB
Java
//
|
|
// $Id$
|
|
//
|
|
// Narya library - tools for developing networked games
|
|
// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
|
|
// http://www.threerings.net/code/narya/
|
|
//
|
|
// This library is free software; you can redistribute it and/or modify it
|
|
// under the terms of the GNU Lesser General Public License as published
|
|
// by the Free Software Foundation; either version 2.1 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This library 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
|
|
// Lesser General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Lesser General Public
|
|
// License along with this library; if not, write to the Free Software
|
|
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
|
|
package com.threerings.parlor.game;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.Iterator;
|
|
|
|
import com.samskivert.util.Interval;
|
|
import com.samskivert.util.RepeatCallTracker;
|
|
import com.samskivert.util.StringUtil;
|
|
import com.threerings.util.Name;
|
|
|
|
import com.threerings.presents.data.ClientObject;
|
|
import com.threerings.presents.dobj.AttributeChangeListener;
|
|
import com.threerings.presents.dobj.AttributeChangedEvent;
|
|
import com.threerings.presents.dobj.DObject;
|
|
import com.threerings.presents.dobj.OidList;
|
|
|
|
import com.threerings.crowd.chat.server.SpeakProvider;
|
|
|
|
import com.threerings.crowd.data.BodyObject;
|
|
import com.threerings.crowd.server.CrowdServer;
|
|
import com.threerings.crowd.server.PlaceManager;
|
|
import com.threerings.crowd.server.PlaceManagerDelegate;
|
|
|
|
import com.threerings.parlor.Log;
|
|
import com.threerings.parlor.data.ParlorCodes;
|
|
import com.threerings.parlor.server.ParlorSender;
|
|
|
|
/**
|
|
* The game manager handles the server side management of a game. It
|
|
* manipulates the game state in accordance with the logic of the game
|
|
* flow and generally manages the whole game playing process.
|
|
*
|
|
* <p> The game manager extends the place manager because games are
|
|
* implicitly played in a location, the players of the game implicitly
|
|
* bodies in that location.
|
|
*/
|
|
public class GameManager extends PlaceManager
|
|
implements ParlorCodes, GameCodes, GameProvider, AttributeChangeListener
|
|
{
|
|
// documentation inherited
|
|
protected void didInit ()
|
|
{
|
|
super.didInit();
|
|
|
|
// save off a casted reference to our config
|
|
_gameconfig = (GameConfig)_config;
|
|
|
|
// register this game manager
|
|
_managers.add(this);
|
|
|
|
// and start up a tick interval if we've not already got one
|
|
if (_tickInterval == null) {
|
|
_tickInterval = new Interval(CrowdServer.omgr) {
|
|
public void expired () {
|
|
tickAllGames();
|
|
}
|
|
};
|
|
_tickInterval.schedule(TICK_DELAY, true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the configuration object for the game being managed by this
|
|
* manager.
|
|
*/
|
|
public GameConfig getGameConfig ()
|
|
{
|
|
return _gameconfig;
|
|
}
|
|
|
|
/**
|
|
* Adds the given player to the game at the first available player
|
|
* index. This should only be called before the game is started, and
|
|
* is most likely to be used to add players to party games.
|
|
*
|
|
* @param player the username of the player to add to this game.
|
|
* @return the player index at which the player was added, or
|
|
* <code>-1</code> if the player could not be added to the game.
|
|
*/
|
|
public int addPlayer (Name player)
|
|
{
|
|
// determine the first available player index
|
|
int pidx = -1;
|
|
for (int ii = 0; ii < getPlayerSlots(); ii++) {
|
|
if (!_gameobj.isOccupiedPlayer(ii)) {
|
|
pidx = ii;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// sanity-check the player index
|
|
if (pidx == -1) {
|
|
Log.warning("Couldn't find free player index for player " +
|
|
"[game=" + _gameobj.which() + ", player=" + player +
|
|
", players=" + StringUtil.toString(_gameobj.players) +
|
|
"].");
|
|
return -1;
|
|
}
|
|
|
|
// proceed with the rest of the adding business
|
|
return (!addPlayerAt(player, pidx)) ? -1 : pidx;
|
|
}
|
|
|
|
/**
|
|
* Adds the given player to the game at the specified player index.
|
|
* This should only be called before the game is started, and is most
|
|
* likely to be used to add players to party games.
|
|
*
|
|
* @param player the username of the player to add to this game.
|
|
* @param pidx the player index at which the player is to be added.
|
|
* @return true if the player was added successfully, false if not.
|
|
*/
|
|
public boolean addPlayerAt (Name player, int pidx)
|
|
{
|
|
// make sure the specified player index is valid
|
|
if (pidx < 0 || pidx >= getPlayerSlots()) {
|
|
Log.warning("Attempt to add player at an invalid index " +
|
|
"[game=" + _gameobj.which() + ", player=" + player +
|
|
", pidx=" + pidx + "].");
|
|
return false;
|
|
}
|
|
|
|
// make sure the player index is available
|
|
if (_gameobj.players[pidx] != null) {
|
|
Log.warning("Attempt to add player at occupied index " +
|
|
"[game=" + _gameobj.which() + ", player=" + player +
|
|
", pidx=" + pidx + "].");
|
|
return false;
|
|
}
|
|
|
|
// make sure the player isn't already somehow a part of the game
|
|
// to avoid any potential badness that might ensue if we added
|
|
// them more than once
|
|
if (_gameobj.getPlayerIndex(player) != -1) {
|
|
Log.warning("Attempt to add player to game that they're already " +
|
|
"playing [game=" + _gameobj.which() +
|
|
", player=" + player + "].");
|
|
return false;
|
|
}
|
|
|
|
// get the player's body object
|
|
BodyObject bobj = CrowdServer.lookupBody(player);
|
|
if (bobj == null) {
|
|
Log.warning("Unable to get body object while adding player " +
|
|
"[game=" + _gameobj.which() +
|
|
", player=" + player + "].");
|
|
return false;
|
|
}
|
|
|
|
// fill in the player's information
|
|
_gameobj.setPlayersAt(player, pidx);
|
|
|
|
// increment the number of players in the game
|
|
_playerCount++;
|
|
|
|
// save off their oid
|
|
_playerOids[pidx] = bobj.getOid();
|
|
|
|
// let derived classes do what they like
|
|
playerWasAdded(player, pidx);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Called when a player was added to the game. Derived classes may
|
|
* override this method to perform any game-specific actions they
|
|
* desire, but should be sure to call
|
|
* <code>super.playerWasAdded()</code>.
|
|
*
|
|
* @param player the username of the player added to the game.
|
|
* @param pidx the player index of the player added to the game.
|
|
*/
|
|
protected void playerWasAdded (Name player, int pidx)
|
|
{
|
|
}
|
|
|
|
/**
|
|
* Removes the given player from the game. This is most likely to be
|
|
* used to allow players involved in a party game to leave the game
|
|
* early-on if they realize they'd rather not play for some reason.
|
|
*
|
|
* @param player the username of the player to remove from this game.
|
|
* @return true if the player was successfully removed, false if not.
|
|
*/
|
|
public boolean removePlayer (Name player)
|
|
{
|
|
// get the player's index in the player list
|
|
int pidx = _gameobj.getPlayerIndex(player);
|
|
|
|
// sanity-check the player index
|
|
if (pidx == -1) {
|
|
Log.warning("Attempt to remove non-player from players list " +
|
|
"[game=" + _gameobj.which() +
|
|
", player=" + player +
|
|
", players=" + StringUtil.toString(_gameobj.players) +
|
|
"].");
|
|
return false;
|
|
}
|
|
|
|
// remove the player from the players list
|
|
_gameobj.setPlayersAt(null, pidx);
|
|
|
|
// clear out the player's entry in the player oid list
|
|
_playerOids[pidx] = 0;
|
|
|
|
if (_AIs != null) {
|
|
// clear out the player's entry in the AI list
|
|
_AIs[pidx] = null;
|
|
}
|
|
|
|
// decrement the number of players in the game
|
|
_playerCount--;
|
|
|
|
// let derived classes do what they like
|
|
playerWasRemoved(player, pidx);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Called when a player was removed from the game. Derived classes
|
|
* may override this method to perform any game-specific actions they
|
|
* desire, but should be sure to call
|
|
* <code>super.playerWasRemoved()</code>.
|
|
*
|
|
* @param player the username of the player removed from the game.
|
|
* @param pidx the player index of the player before they were removed
|
|
* from the game.
|
|
*/
|
|
protected void playerWasRemoved (Name player, int pidx)
|
|
{
|
|
}
|
|
|
|
/**
|
|
* Returns the user object for the player with the specified index or
|
|
* null if the player at that index is not online.
|
|
*/
|
|
public BodyObject getPlayer (int playerIdx)
|
|
{
|
|
// if we have their oid, use that
|
|
int ploid = _playerOids[playerIdx];
|
|
if (ploid > 0) {
|
|
return (BodyObject)CrowdServer.omgr.getObject(ploid);
|
|
}
|
|
// otherwise look them up by name
|
|
Name name = getPlayerName(playerIdx);
|
|
return (name == null) ? null : CrowdServer.lookupBody(name);
|
|
}
|
|
|
|
/**
|
|
* Sets the specified player as an AI with the specified skill. It is
|
|
* assumed that this will be set soon after the player names for all
|
|
* AIs present in the game. (It should be done before human players
|
|
* start trickling into the game.)
|
|
*
|
|
* @param pidx the player index of the AI.
|
|
* @param skill the skill level, from 0 to 100 inclusive.
|
|
*/
|
|
public void setAI (final int pidx, final byte skill, final byte personality)
|
|
{
|
|
if (_AIs == null) {
|
|
// create and initialize the AI skill level array
|
|
_AIs = new AI[getPlayerSlots()];
|
|
// set up a delegate op for AI ticking
|
|
_tickAIOp = new TickAIDelegateOp();
|
|
}
|
|
|
|
// save off the AI's skill level
|
|
_AIs[pidx] = new AI(skill, personality);
|
|
|
|
// let the delegates know that the player's been made an AI
|
|
applyToDelegates(new DelegateOp() {
|
|
public void apply (PlaceManagerDelegate delegate) {
|
|
((GameManagerDelegate)delegate).setAI(pidx, skill, personality);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Returns the name of the player with the specified index or null if
|
|
* no player exists at that index.
|
|
*/
|
|
public Name getPlayerName (int index)
|
|
{
|
|
return (_gameobj == null) ? null : _gameobj.players[index];
|
|
}
|
|
|
|
/**
|
|
* Returns the player index of the given user in the game, or
|
|
* <code>-1</code> if the player is not involved in the game.
|
|
*/
|
|
public int getPlayerIndex (Name username)
|
|
{
|
|
return (_gameobj == null) ? -1 : _gameobj.getPlayerIndex(username);
|
|
}
|
|
|
|
/**
|
|
* Returns the user object oid of the player with the specified index.
|
|
*/
|
|
public int getPlayerOid (int index)
|
|
{
|
|
return (_playerOids == null) ? -1 : _playerOids[index];
|
|
}
|
|
|
|
/**
|
|
* Returns the number of players in the game.
|
|
*/
|
|
public int getPlayerCount ()
|
|
{
|
|
return _playerCount;
|
|
}
|
|
|
|
/**
|
|
* Returns the number of players allowed in this game.
|
|
*/
|
|
public int getPlayerSlots ()
|
|
{
|
|
return _gameconfig.players.length;
|
|
}
|
|
|
|
/**
|
|
* Returns whether the player at the specified player index is an AI.
|
|
*/
|
|
public boolean isAI (int pidx)
|
|
{
|
|
return (_AIs != null && _AIs[pidx] != null);
|
|
}
|
|
|
|
/**
|
|
* Returns the unique round identifier for the current round.
|
|
*/
|
|
public int getRoundId ()
|
|
{
|
|
return _gameobj.roundId;
|
|
}
|
|
|
|
/**
|
|
* Sends a system message to the players in the game room.
|
|
*/
|
|
public void systemMessage (String msgbundle, String msg)
|
|
{
|
|
systemMessage(msgbundle, msg, false);
|
|
}
|
|
|
|
/**
|
|
* Sends a system message to the players in the game room.
|
|
*
|
|
* @param waitForStart if true, the message will not be sent until the
|
|
* game has started.
|
|
*/
|
|
public void systemMessage (
|
|
String msgbundle, String msg, boolean waitForStart)
|
|
{
|
|
if (waitForStart &&
|
|
((_gameobj == null) ||
|
|
(_gameobj.state == GameObject.AWAITING_PLAYERS))) {
|
|
// queue up the message.
|
|
if (_startmsgs == null) {
|
|
_startmsgs = new ArrayList();
|
|
}
|
|
_startmsgs.add(msgbundle);
|
|
_startmsgs.add(msg);
|
|
return;
|
|
}
|
|
|
|
// otherwise, just deliver the message
|
|
SpeakProvider.sendInfo(_gameobj, msgbundle, msg);
|
|
}
|
|
|
|
// documentation inherited
|
|
protected Class getPlaceObjectClass ()
|
|
{
|
|
return GameObject.class;
|
|
}
|
|
|
|
// documentation inherited
|
|
protected void didStartup ()
|
|
{
|
|
// obtain a casted reference to our game object
|
|
_gameobj = (GameObject)_plobj;
|
|
|
|
// stick the players into the game object
|
|
_gameobj.setPlayers(_gameconfig.players);
|
|
|
|
// save off the number of players so that we needn't repeatedly
|
|
// iterate through the player name array server-side unnecessarily
|
|
_playerCount = _gameobj.getPlayerCount();
|
|
|
|
// instantiate a player oid array which we'll fill in later
|
|
_playerOids = new int[getPlayerSlots()];
|
|
|
|
// create and fill in our game service object
|
|
GameMarshaller service = (GameMarshaller)
|
|
_invmgr.registerDispatcher(new GameDispatcher(this), false);
|
|
_gameobj.setGameService(service);
|
|
|
|
// give delegates a chance to do their thing
|
|
super.didStartup();
|
|
|
|
// let the players of this game know that we're ready to roll (if
|
|
// we have a specific set of players)
|
|
for (int ii = 0; ii < getPlayerSlots(); ii++) {
|
|
// skip non-existent players
|
|
if (!_gameobj.isOccupiedPlayer(ii)) {
|
|
continue;
|
|
}
|
|
|
|
BodyObject bobj = CrowdServer.lookupBody(_gameobj.players[ii]);
|
|
if (bobj == null) {
|
|
Log.warning("Unable to deliver game ready to non-existent " +
|
|
"player [game=" + _gameobj.which() +
|
|
", player=" + _gameobj.players[ii] + "].");
|
|
continue;
|
|
}
|
|
|
|
// deliver a game ready notification to the player
|
|
ParlorSender.gameIsReady(bobj, _gameobj.getOid());
|
|
}
|
|
|
|
// start up a no-show timer if needed
|
|
if (needsNoShowTimer()) {
|
|
new Interval(CrowdServer.omgr) {
|
|
public void expired () {
|
|
checkForNoShows();
|
|
}
|
|
}.schedule(NOSHOW_DELAY);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns true if this game requires a no-show timer. The default
|
|
* implementation returns true for non-party games and false for party
|
|
* games. Derived classes may wish to change or augment this behavior.
|
|
*/
|
|
protected boolean needsNoShowTimer ()
|
|
{
|
|
return !isPartyGame();
|
|
}
|
|
|
|
// documentation inherited
|
|
protected void didShutdown ()
|
|
{
|
|
super.didShutdown();
|
|
|
|
// unregister this game manager
|
|
_managers.remove(this);
|
|
|
|
// remove the tick interval if there are no remaining managers
|
|
if (_managers.size() == 0) {
|
|
_tickInterval.cancel();
|
|
_tickInterval = null;
|
|
}
|
|
|
|
// clear out our service registration
|
|
_invmgr.clearDispatcher(_gameobj.gameService);
|
|
}
|
|
|
|
// documentation inherited
|
|
protected void bodyLeft (int bodyOid)
|
|
{
|
|
super.bodyLeft(bodyOid);
|
|
|
|
// TODO: if this is a party game not yet in play and the creator
|
|
// left, choose a new creator if at least one player still remains
|
|
|
|
// deal with disappearing players
|
|
}
|
|
|
|
/**
|
|
* When a game room becomes empty, we cancel the game if it's still in
|
|
* progress and close down the game room.
|
|
*/
|
|
protected void placeBecameEmpty ()
|
|
{
|
|
// Log.info("Game room empty. Going away. " +
|
|
// "[game=" + _gameobj.which() + "].");
|
|
|
|
// cancel the game if it was not already over
|
|
if (_gameobj.state != GameObject.GAME_OVER &&
|
|
_gameobj.state != GameObject.CANCELLED) {
|
|
_gameobj.setState(GameObject.CANCELLED);
|
|
}
|
|
|
|
// shut down the place (which will destroy the game object and
|
|
// clean up after everything)
|
|
shutdown();
|
|
}
|
|
|
|
/**
|
|
* Called when all players have arrived in the game room. By default,
|
|
* this starts up the game, but a manager may wish to override this
|
|
* and start the game according to different criterion.
|
|
*/
|
|
protected void playersAllHere ()
|
|
{
|
|
// start up the game if we're not a party game and if we haven't
|
|
// already done so
|
|
if (!isPartyGame() &&
|
|
_gameobj.state == GameObject.AWAITING_PLAYERS) {
|
|
startGame();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called after the no-show delay has expired following the delivery
|
|
* of notifications to all players that the game is ready.
|
|
* <em>Note:</em> this is not called for party games. Those games have
|
|
* a human who decides when to start the game.
|
|
*/
|
|
protected void checkForNoShows ()
|
|
{
|
|
// nothing to worry about if we're already started
|
|
if (_gameobj.state != GameObject.AWAITING_PLAYERS) {
|
|
return;
|
|
}
|
|
|
|
// if there's no one in the room, go ahead and clear it out
|
|
if (_plobj.occupants.size() == 0) {
|
|
Log.info("Cancelling total no-show " +
|
|
"[game=" + _gameobj.which() +
|
|
", players=" + StringUtil.toString(_gameobj.players) +
|
|
", poids=" + StringUtil.toString(_playerOids) + "].");
|
|
placeBecameEmpty();
|
|
|
|
} else {
|
|
// do the right thing if we have any no-show players
|
|
for (int ii = 0; ii < getPlayerSlots(); ii++) {
|
|
if (!playerIsReady(ii)) {
|
|
handlePartialNoShow();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This is called when some, but not all, players failed to show up
|
|
* for a game. The default implementation simply cancels the game.
|
|
*/
|
|
protected void handlePartialNoShow ()
|
|
{
|
|
// cancel the game
|
|
if (_gameobj.state != GameObject.GAME_OVER &&
|
|
_gameobj.state != GameObject.CANCELLED) {
|
|
_gameobj.setState(GameObject.CANCELLED);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This is called when the game is ready to start (all players
|
|
* involved have delivered their "am ready" notifications). It calls
|
|
* {@link #gameWillStart}, sets the necessary wheels in motion and
|
|
* then calls {@link #gameDidStart}. Derived classes should override
|
|
* one or both of the calldown functions (rather than this function)
|
|
* if they need to do things before or after the game starts.
|
|
*
|
|
* @return true if the game was started, false if it could not be
|
|
* started because it was already in play or because all players have
|
|
* not yet reported in.
|
|
*/
|
|
public boolean startGame ()
|
|
{
|
|
// complain if we're already started
|
|
if (_gameobj.state == GameObject.IN_PLAY) {
|
|
Log.warning("Requested to start an already in-play game " +
|
|
"[game=" + _gameobj.which() + "].");
|
|
Thread.dumpStack();
|
|
return false;
|
|
}
|
|
|
|
// TEMP: clear out our game end tracker
|
|
_gameEndTracker.clear();
|
|
|
|
// make sure everyone has turned up
|
|
if (!allPlayersReady()) {
|
|
Log.warning("Requested to start a game that is still " +
|
|
"awaiting players [game=" + _gameobj.which() +
|
|
", pnames=" + StringUtil.toString(_gameobj.players) +
|
|
", poids=" + StringUtil.toString(_playerOids) + "].");
|
|
return false;
|
|
}
|
|
|
|
// if we're still waiting for a call to endGame() to propagate,
|
|
// queue up a runnable to start the game which will allow the
|
|
// endGame() to propagate before we start things up
|
|
if (_committedState == GameObject.IN_PLAY) {
|
|
Log.info("Postponing start of still-ending game " +
|
|
"[which=" + _gameobj.which() + "].");
|
|
CrowdServer.omgr.postRunnable(new Runnable() {
|
|
public void run () {
|
|
startGame();
|
|
}
|
|
});
|
|
return true;
|
|
}
|
|
|
|
// let the derived class do its pre-start stuff
|
|
gameWillStart();
|
|
|
|
// transition the game to started
|
|
_gameobj.setState(GameObject.IN_PLAY);
|
|
|
|
// when our events are applied, we'll call gameDidStart()
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Called when the game is about to start, but before the game start
|
|
* notification has been delivered to the players. Derived classes
|
|
* should override this if they need to perform some pre-start
|
|
* activities, but should be sure to call
|
|
* <code>super.gameWillStart()</code>.
|
|
*/
|
|
protected void gameWillStart ()
|
|
{
|
|
// increment the round identifier
|
|
_gameobj.setRoundId(_gameobj.roundId + 1);
|
|
|
|
// let our delegates do their business
|
|
applyToDelegates(new DelegateOp() {
|
|
public void apply (PlaceManagerDelegate delegate) {
|
|
((GameManagerDelegate)delegate).gameWillStart();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Called after the game start notification was dispatched. Derived
|
|
* classes can override this to put whatever wheels they might need
|
|
* into motion now that the game is started (if anything other than
|
|
* transitioning the game to {@link GameObject#IN_PLAY} is necessary),
|
|
* but should be sure to call <code>super.gameDidStart()</code>.
|
|
*/
|
|
protected void gameDidStart ()
|
|
{
|
|
// let our delegates do their business
|
|
applyToDelegates(new DelegateOp() {
|
|
public void apply (PlaceManagerDelegate delegate) {
|
|
((GameManagerDelegate)delegate).gameDidStart();
|
|
}
|
|
});
|
|
|
|
// inform the players of any pending messages.
|
|
if (_startmsgs != null) {
|
|
for (Iterator iter = _startmsgs.iterator(); iter.hasNext(); ) {
|
|
systemMessage((String) iter.next(), // bundle
|
|
(String) iter.next()); // message
|
|
}
|
|
_startmsgs = null;
|
|
}
|
|
|
|
// and register ourselves to receive AI ticks
|
|
if (_AIs != null) {
|
|
AIGameTicker.registerAIGame(this);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called by the {@link AIGameTicker} if we're registered as an AI
|
|
* game.
|
|
*/
|
|
protected void tickAIs ()
|
|
{
|
|
for (int ii = 0; ii < _AIs.length; ii++) {
|
|
if (_AIs[ii] != null) {
|
|
tickAI(ii, _AIs[ii]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called by tickAIs to tick each AI in the game.
|
|
*/
|
|
protected void tickAI (int pidx, AI ai)
|
|
{
|
|
_tickAIOp.setAI(pidx, ai);
|
|
applyToDelegates(_tickAIOp);
|
|
}
|
|
|
|
/**
|
|
* Called when the game is known to be over. This will call some
|
|
* calldown functions to determine the winner of the game and then
|
|
* transition the game to the {@link GameObject#GAME_OVER} state.
|
|
*/
|
|
public void endGame ()
|
|
{
|
|
// TEMP: debug pending rating repeat bug
|
|
if (_gameEndTracker.checkCall(
|
|
"Requested to end already ended game " +
|
|
"[game=" + _gameobj.which() + "].")) {
|
|
return;
|
|
}
|
|
// END TEMP
|
|
|
|
if (_gameobj.state != GameObject.IN_PLAY) {
|
|
Log.debug("Refusing to end game that was not in play " +
|
|
"[game=" + _gameobj.which() + "].");
|
|
return;
|
|
}
|
|
|
|
_gameobj.startTransaction();
|
|
try {
|
|
// let the derived class do its pre-end stuff
|
|
gameWillEnd();
|
|
|
|
// determine winners and set them in the game object
|
|
boolean[] winners = new boolean[getPlayerSlots()];
|
|
assignWinners(winners);
|
|
_gameobj.setWinners(winners);
|
|
|
|
// transition to the game over state
|
|
_gameobj.setState(GameObject.GAME_OVER);
|
|
|
|
} finally {
|
|
_gameobj.commitTransaction();
|
|
}
|
|
|
|
// wait until we hear the game state transition on the game object
|
|
// to invoke our game over code so that we can be sure that any
|
|
// final events dispatched on the game object prior to the call to
|
|
// endGame() have been dispatched
|
|
}
|
|
|
|
/**
|
|
* Assigns the final winning status for each player to their respect
|
|
* player index in the supplied array. This will be called by {@link
|
|
* #endGame} when the game is over. The default implementation marks
|
|
* no players as winners. Derived classes should override this method
|
|
* in order to customize the winning conditions.
|
|
*/
|
|
protected void assignWinners (boolean[] winners)
|
|
{
|
|
Arrays.fill(winners, false);
|
|
}
|
|
|
|
/**
|
|
* Returns whether game conclusion antics such as rating updates
|
|
* should be performed when an in-play game is ended. Derived classes
|
|
* may wish to override this method to customize the conditions under
|
|
* which the game is concluded.
|
|
*/
|
|
public boolean shouldConcludeGame ()
|
|
{
|
|
return (_gameobj.state == GameObject.GAME_OVER);
|
|
}
|
|
|
|
/**
|
|
* Called when the game is about to end, but before the game end
|
|
* notification has been delivered to the players. Derived classes
|
|
* should override this if they need to perform some pre-end
|
|
* activities, but should be sure to call
|
|
* <code>super.gameWillEnd()</code>.
|
|
*/
|
|
protected void gameWillEnd ()
|
|
{
|
|
// let our delegates do their business
|
|
applyToDelegates(new DelegateOp() {
|
|
public void apply (PlaceManagerDelegate delegate) {
|
|
((GameManagerDelegate)delegate).gameWillEnd();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Called after the game has transitioned to the {@link
|
|
* GameObject#GAME_OVER} state. Derived classes should override this
|
|
* to perform any post-game activities, but should be sure to call
|
|
* <code>super.gameDidEnd()</code>.
|
|
*/
|
|
protected void gameDidEnd ()
|
|
{
|
|
// remove ourselves from the AI ticker, if applicable
|
|
if (_AIs != null) {
|
|
AIGameTicker.unregisterAIGame(this);
|
|
}
|
|
|
|
// let our delegates do their business
|
|
applyToDelegates(new DelegateOp() {
|
|
public void apply (PlaceManagerDelegate delegate) {
|
|
((GameManagerDelegate)delegate).gameDidEnd();
|
|
}
|
|
});
|
|
|
|
// report the winners and losers if appropriate
|
|
int winnerCount = _gameobj.getWinnerCount();
|
|
if (shouldConcludeGame() && winnerCount > 0 && !_gameobj.isDraw()) {
|
|
reportWinnersAndLosers();
|
|
}
|
|
|
|
// calculate ratings and all that...
|
|
}
|
|
|
|
/**
|
|
* Report winner and loser oids to each room that any of the
|
|
* winners/losers is in.
|
|
*/
|
|
protected void reportWinnersAndLosers ()
|
|
{
|
|
OidList winners = new OidList();
|
|
OidList losers = new OidList();
|
|
OidList places = new OidList();
|
|
|
|
Object[] args = new Object[] { winners, losers };
|
|
|
|
for (int ii=0, nn=_playerOids.length; ii < nn; ii++) {
|
|
BodyObject user = getPlayer(ii);
|
|
if (user != null) {
|
|
places.add(user.location);
|
|
(_gameobj.isWinner(ii) ? winners : losers).add(user.getOid());
|
|
}
|
|
}
|
|
|
|
// now send a message event to each room
|
|
for (int ii=0, nn = places.size(); ii < nn; ii++) {
|
|
DObject place = CrowdServer.omgr.getObject(places.get(ii));
|
|
if (place != null) {
|
|
place.postMessage(WINNERS_AND_LOSERS, args);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called when the game is to be reset to its starting state in
|
|
* preparation for a new game without actually ending the current
|
|
* game. It calls {@link #gameWillReset} followed by the standard game
|
|
* start processing ({@link #gameWillStart} and {@link
|
|
* #gameDidStart}). Derived classes should override these calldown
|
|
* functions (rather than this function) if they need to do things
|
|
* before or after the game resets.
|
|
*/
|
|
public void resetGame ()
|
|
{
|
|
// let the derived class do its pre-reset stuff
|
|
gameWillReset();
|
|
// do the standard game start processing
|
|
gameWillStart();
|
|
// transition to in-play which will trigger a call to gameDidStart()
|
|
_gameobj.setState(GameObject.IN_PLAY);
|
|
}
|
|
|
|
/**
|
|
* Called when the game is about to reset, but before the board has
|
|
* been re-initialized or any other clearing out of game data has
|
|
* taken place. Derived classes should override this if they need to
|
|
* perform some pre-reset activities.
|
|
*/
|
|
protected void gameWillReset ()
|
|
{
|
|
// let our delegates do their business
|
|
applyToDelegates(new DelegateOp() {
|
|
public void apply (PlaceManagerDelegate delegate) {
|
|
((GameManagerDelegate)delegate).gameWillReset();
|
|
}
|
|
});
|
|
}
|
|
|
|
// documentation inherited from interface
|
|
public void playerReady (ClientObject caller)
|
|
{
|
|
BodyObject plobj = (BodyObject)caller;
|
|
|
|
// get the user's player index
|
|
int pidx = _gameobj.getPlayerIndex(plobj.username);
|
|
if (pidx == -1) {
|
|
// only complain if this is not a party game, since it's
|
|
// perfectly normal to receive a player ready notification
|
|
// from a user entering a party game in which they're not yet
|
|
// a participant
|
|
if (!isPartyGame()) {
|
|
Log.warning("Received playerReady() from non-player? " +
|
|
"[caller=" + caller + "].");
|
|
}
|
|
return;
|
|
}
|
|
|
|
// make a note of this player's oid
|
|
_playerOids[pidx] = plobj.getOid();
|
|
|
|
// if everyone is now ready to go, get things underway
|
|
if (allPlayersReady()) {
|
|
playersAllHere();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns true if all (non-AI) players have delivered their {@link
|
|
* #playerReady} notifications, false if they have not.
|
|
*/
|
|
public boolean allPlayersReady ()
|
|
{
|
|
for (int ii = 0; ii < getPlayerSlots(); ii++) {
|
|
if (!playerIsReady(ii)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Returns true if the player at the specified slot is ready (or if
|
|
* there is meant to be no player in that slot), false if there is
|
|
* meant to be a player in the specified slot and they have not yet
|
|
* reported that they are ready.
|
|
*/
|
|
public boolean playerIsReady (int pidx)
|
|
{
|
|
return (!_gameobj.isOccupiedPlayer(pidx) || // unoccupied slot
|
|
_playerOids[pidx] != 0 || // player is ready
|
|
isAI(pidx)); // player is AI
|
|
}
|
|
|
|
// documentation inherited from interface
|
|
public void startPartyGame (ClientObject caller)
|
|
{
|
|
// make sure this is a party game
|
|
if (!isPartyGame()) {
|
|
Log.warning("Attempt to player-start a non-party game " +
|
|
"[game=" + _gameobj.which() +
|
|
", caller=" + caller + "].");
|
|
return;
|
|
}
|
|
|
|
// make sure the caller is the creating player
|
|
BodyObject plobj = (BodyObject)caller;
|
|
int pidx = _gameobj.getPlayerIndex(plobj.username);
|
|
if (pidx != _gameobj.creator) {
|
|
Log.warning("Attempt to start party game by non-creating player " +
|
|
"[game=" + _gameobj.which() +
|
|
", creator=" + getPlayerName(_gameobj.creator) +
|
|
", caller=" + caller + "].");
|
|
return;
|
|
}
|
|
|
|
// make sure the game is ready to go
|
|
if (!canStartPartyGame()) {
|
|
Log.warning("Attempt to start party game that can't yet begin " +
|
|
"[game=" + _gameobj.which() +
|
|
", caller=" + caller + "].");
|
|
return;
|
|
}
|
|
|
|
// start things up
|
|
startGame();
|
|
}
|
|
|
|
/**
|
|
* Returns whether this party game is all set to begin. The default
|
|
* implementation returns true. Derived classes that implement a
|
|
* party game should override this method to enforce any prerequisites
|
|
* (such as a minimum number of players) as appropriate.
|
|
*/
|
|
protected boolean canStartPartyGame ()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Gives game managers an opportunity to perform periodic processing
|
|
* that is not driven by events generated by the player.
|
|
*/
|
|
protected void tick (long tickStamp)
|
|
{
|
|
// nothing for now
|
|
}
|
|
|
|
// documentation inherited
|
|
public void attributeChanged (AttributeChangedEvent event)
|
|
{
|
|
if (event.getName().equals(GameObject.STATE)) {
|
|
switch (_committedState = event.getIntValue()) {
|
|
case GameObject.IN_PLAY:
|
|
gameDidStart();
|
|
break;
|
|
|
|
case GameObject.CANCELLED:
|
|
// fall through to GAME_OVER case
|
|
case GameObject.GAME_OVER:
|
|
// now we do our end of game processing
|
|
gameDidEnd();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called periodically to call {@link #tick} on all registered game
|
|
* managers.
|
|
*/
|
|
protected static void tickAllGames ()
|
|
{
|
|
long now = System.currentTimeMillis();
|
|
int size = _managers.size();
|
|
for (int ii = 0; ii < size; ii++) {
|
|
GameManager gmgr = (GameManager)_managers.get(ii);
|
|
try {
|
|
gmgr.tick(now);
|
|
} catch (Exception e) {
|
|
Log.warning("Game manager choked during tick " +
|
|
"[gmgr=" + gmgr + "].");
|
|
Log.logStackTrace(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A helper operation to distribute AI ticks to our delegates.
|
|
*/
|
|
protected class TickAIDelegateOp implements DelegateOp
|
|
{
|
|
public void apply (PlaceManagerDelegate delegate) {
|
|
((GameManagerDelegate) delegate).tickAI(_pidx, _ai);
|
|
}
|
|
|
|
public void setAI (int pidx, AI ai)
|
|
{
|
|
_pidx = pidx;
|
|
_ai = ai;
|
|
}
|
|
|
|
protected int _pidx;
|
|
protected AI _ai;
|
|
}
|
|
|
|
/**
|
|
* Used to determine if this game is a party game.
|
|
*/
|
|
protected boolean isPartyGame ()
|
|
{
|
|
return ((_gameconfig instanceof PartyGameConfig) &&
|
|
((PartyGameConfig)_gameconfig).isPartyGame());
|
|
}
|
|
|
|
/** A reference to our game config. */
|
|
protected GameConfig _gameconfig;
|
|
|
|
/** A reference to our game object. */
|
|
protected GameObject _gameobj;
|
|
|
|
/** The number of players in the game. */
|
|
protected int _playerCount;
|
|
|
|
/** The oids of our player and AI body objects. */
|
|
protected int[] _playerOids;
|
|
|
|
/** If AIs are present, contains their skill levels, or -1 at human
|
|
* player indexes. */
|
|
protected AI[] _AIs;
|
|
|
|
/** If non-null, contains bundles and messages that should be sent as
|
|
* system messages once the game has started. */
|
|
protected ArrayList _startmsgs;
|
|
|
|
/** Our delegate operator to tick AIs. */
|
|
protected TickAIDelegateOp _tickAIOp;
|
|
|
|
/** The state of the game that has been propagated to our
|
|
* subscribers. */
|
|
protected int _committedState;
|
|
|
|
/** TEMP: debugging the pending rating double release bug. */
|
|
protected RepeatCallTracker _gameEndTracker = new RepeatCallTracker();
|
|
|
|
/** A list of all currently active game managers. */
|
|
protected static ArrayList _managers = new ArrayList();
|
|
|
|
/** The interval for the game manager tick. */
|
|
protected static Interval _tickInterval;
|
|
|
|
/** We give players 30 seconds to turn up in a puzzle and after that,
|
|
* they're considered a no show. */
|
|
protected static final long NOSHOW_DELAY = 30 * 1000L;
|
|
|
|
/** The delay in milliseconds between ticking of all game managers. */
|
|
protected static final long TICK_DELAY = 5L * 1000L;
|
|
}
|