/*
(C) 2007-2013 yura.net
This file is part of Lobby.
Lobby 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
Lobby 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, see <http://www.gnu.org/licenses/>.
*/
package net.yura.lobby.server;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import net.yura.util.PausableScheduledThreadPoolExecutor;
/**
* @author Yura
*/
public abstract class TurnBasedGame implements ServerGame {
public static final int MAX_SKIPS = 5;
public static final PausableScheduledThreadPoolExecutor scheduler = new PausableScheduledThreadPoolExecutor(1);
static {
// JAVA 7 ONLY
scheduler.setRemoveOnCancelPolicy(true);
}
private Future future;
private ServerGameListener listoner;
private Collection<LobbySession> spectators = new ConcurrentSkipListSet();
protected boolean finished,startGameCalled;
String whoiwantinputfrom;
int timeout;
Map<String,Integer> skips = new HashMap();
// ################################### abstract methods ####################################
// life cycle of a game
@Override public abstract void startGame(String startGameOptions, String[] players);
@Override public abstract void loadGame(byte[] gameData);
public abstract void destroyGame();
public abstract void playerJoins(String username);
public abstract void playerResigns(String username);
public abstract void renamePlayer(String oldser,String newuser);
//comunication
public abstract void clientHasJoined(String username);
public abstract void stringFromPlayer(String username, String message);
// called by this if the player has left or they have timmed out
public abstract void doBasicGo(String username);
// ################################### methods callable by the game ####################################
// called by the game when it comes to an end
// set things up for another game
public final void gameFinished(String winner) {
killFuture();
whoiwantinputfrom = null;
finished=true;
sendStringToAllClient("LOBBY_GAMEOVER"); // stops the timeout clock on the client, IF it has been implemented
listoner.sendChatroomMessage("Game over! "+winner+" has won!");
listoner.gameFinished();
}
public final void getInputFromClient(String username) {
String oldPlayer = whoiwantinputfrom;
whoiwantinputfrom = username;
if (!LobbyServer.equals(oldPlayer, whoiwantinputfrom)) {
// the first time we need input from a user we know the game has finished setup
// is now started/ready to be opened
if (whoiwantinputfrom!=null && !startGameCalled) {
listoner.gameStarted();
startGameCalled = true;
}
// if whoiwantinputfrom is null we send it to tell the client
// that we do not need input from any human player
listoner.needInputFrom(whoiwantinputfrom);
}
killFuture();
if (whoiwantinputfrom!=null) {
future = scheduler.schedule(new Runnable() {
@Override
public void run() {
final String username = whoiwantinputfrom;
try {
if (username==null) {
throw new IllegalStateException("username is null");
}
// if 100 turns have been skipped, this game is dead
int skip = skips.get(username)==null?1:skips.get(username)+1;
skips.put(username, skip);
if (skip >= MAX_SKIPS) {
listoner.sendChatroomMessage(username+" has timed out "+MAX_SKIPS+" times and has been resigned from the game.");
listoner.resignPlayer(username);
}
else {
listoner.sendChatroomMessage(username+" has timed out on their turn");
doBasicGo(username);
}
}
catch(Throwable th) {
LobbyServer.logger.log(Level.WARNING, "error in timeout for game: "+id+" for user "+username, th);
}
}
}, timeout + 10, TimeUnit.SECONDS); // add 10 seconds for bad ping
}
}
private Collection<LobbySession> usernameToLobbySessionArray(String username) {
List<LobbySession> sessions = new ArrayList();
for (LobbySession session:spectators) {
if (session.getUsername().equals( username )) {
sessions.add(session);
}
}
return sessions;
}
public final void sendStringToClient(String a, String username) {
listoner.messageFromGame(a, usernameToLobbySessionArray(username) );
}
public final void sendStringToAllClient(String a) {
listoner.messageFromGame(a, spectators );
}
public final void sendObjectToClient(Serializable a,String username) {
listoner.messageFromGame( serializableToByteArray(a) , usernameToLobbySessionArray(username) );
}
public final void sendObjectToAllClient(Serializable a) {
listoner.messageFromGame( serializableToByteArray(a), spectators );
}
private static byte[] serializableToByteArray(Serializable a) {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream oout = new ObjectOutputStream(out);
oout.writeObject(a);
oout.flush();
return out.toByteArray();
}
catch (Exception ex) {
throw new RuntimeException(ex);
}
}
// ################################### implementation of ServerGame ####################################
int id;
@Override
public void setId(int id) {
this.id=id;
}
@Override
public int getId() {
return id;
}
@Override
public void setTimeout(int timeout) {
this.timeout = timeout;
if (this.timeout <= 0) {
this.timeout = 60; // a turn based game MUST have a timeout to give players a time to take there turn
}
}
@Override
public final void addServerGameListener(ServerGameListener l) {
listoner = l;
}
@Override
public final void removeServerGameListener(ServerGameListener l) {
if( listoner.equals(l)) {
listoner = null;
}
}
@Override
public Collection<LobbySession> getAllClients() {
return spectators;
}
@Override
public final void clientEntered(final LobbySession session) {
// we tell everyone currently in the game this person is joining
listoner.sendChatroomMessage(session.getUsername()+" has entered the game");
// we add this new player as a spectator to make sure we do not miss any messages
// from the game that can happen between clientHasJoined() and spectators.add()
spectators.add(session);
// this is here so if the clientHasJoined sends anything to the client
// the game object is already created on the client and ready
// new Thread() {
//
// public void run() {
//
// try { Thread.sleep(1000); }
// catch(InterruptedException e){}
//
// TODO we send game object to every session with this username
// would be better if just sent it to the session we needed to
clientHasJoined( session.getUsername() );
//
// }
//
// }.start();
}
// game will finish if someone leaves
@Override
public final void clientLeaves(LobbySession username) {
spectators.remove(username);
listoner.sendChatroomMessage(username.getUsername()+" has left the game");
}
@Override
public void playerResigned(String player) {
// TODO what if player clicks resign twice, there is not check if we are already resigned here
if (player.equals(whoiwantinputfrom)) {
killFuture();
whoiwantinputfrom = null;
}
listoner.sendChatroomMessage( player+" has resigned from the game");
// even though a error may happen in the resign, that would lead to us not being resigned
// we still have to call the sendChatroomMessage method first, as it will make more sense
// in the log (player X resigned, game over) and the game may get deleted by the resign
// command, and calling sendChatroomMessage on a deleted game will throw a nullpointer
playerResigns( player ); // this may lead to the game being deleted (gameFinished called)
}
@Override
public void playerJoined(String player) {
playerJoins( player );
listoner.sendChatroomMessage( player+" has joined the game");
}
@Override
public final void messageFromUser(String username, Object object) {
skips.put(username, 0); // if someone does something reset skips counter
if (username.equals(whoiwantinputfrom)) {
killFuture();
}
stringFromPlayer(username, (String)object );
}
// TODO should be synchronized with something?
@Override
public final void midgameLogin(String oldser,String newuser) {
renamePlayer( oldser , newuser );
Integer skip = skips.remove(oldser);
if (skip!=null) {
skips.put(newuser, skip);
}
if (oldser.equals(whoiwantinputfrom)) {
whoiwantinputfrom = newuser;
}
listoner.sendChatroomMessage(oldser+" has logged in as "+newuser);
}
@Override
public boolean isFinished() {
return finished;
}
@Override
public String getWhosTurn() {
return whoiwantinputfrom;
}
@Override
public void destroyServerGame() {
killFuture();
destroyGame();
}
void killFuture() {
Future f = future;
if (f!=null && !f.isDone() && !f.isCancelled()) {
f.cancel(true);
}
}
}