/*
* (c) 2000-2009 Carlos G�mez Rodr�guez, todos los derechos reservados / all rights reserved.
* Licencia en license/bsd.txt / License in license/bsd.txt
*/
package eu.irreality.age;
//package AetheriaAWT;
import java.util.*;
import java.io.*; //logs
import javax.swing.SwingUtilities;
import bsh.TargetError;
import eu.irreality.age.debug.Debug;
import eu.irreality.age.debug.ExceptionPrinter;
import eu.irreality.age.scripting.ScriptException;
import eu.irreality.age.util.VersionComparator;
/**
* Clase del personaje, jugador.
*
*/
public class Player extends Mobile implements Informador
{
//INSTANCE VARIABLES
/**�Estamos cargando de un log?*/
protected boolean from_log;
protected Vector logfile; //log de ENTRADA
BufferedReader logReader;
public void setPlayerName( String nombre )
{
//Funci�n de conveniencia para poner nombre a un jugador, que sirve como
//title, reference name, etc.
title = nombre;
properName = true;
//max priority as reference
respondToSing.add(0,nombre);
respondToPlur.add(0,nombre);
//respondToSing = nombre+"$"+respondToSing;
//respondToPlur = nombre+"$"+respondToPlur;
singNames = new Description[1];
singNames[0] = new Description(nombre,0,0);
plurNames = new Description[1];
plurNames[0] = new Description(nombre,0,0);
}
void initDefaultProperties( World mundo )
{
//set multiple args matches which was default behaviour in versions [1.0,1.1.7]
if ( new VersionComparator().compare(mundo.getParserVersion(),"1.0") >= 0 && new VersionComparator().compare(mundo.getParserVersion(),"1.1.7") <= 0 )
{
if ( this.getPropertyValueAsObject("multipleArgsMatches") == null )
setProperty("multipleArgsMatches",true);
}
}
/**Lista din�mica de objetos.*/
//protected Inventory inventory; -> heredado de Mobile
public Player ( World mundo , InputOutputClient io , org.w3c.dom.Node n ) throws XMLtoWorldException
{
super ( mundo , n );
//this.mundo = mundo;
this.io = io;
//lenguaje = mundo.getLang();
//setRoom ( mundo.getRoom(1) );
//setRoom ( mundo.getRoom(1) );
setNewState(1,1);
//mundo.setPlayer(this);
initDefaultProperties(mundo);
}
//TEMPORAL CONSTRUCTOR
public Player ( World mundo , InputOutputClient io ) throws java.io.IOException
{
super ( mundo, Utility.playerFile(mundo) );
//this.mundo = mundo;
this.io = io;
//Debug.println("Player's I/O: " + io);
//lenguaje = mundo.getLang(); //toma las herramientas de lenguaje del mundo
//name = "Desarrollador";
//habitacionActual = mundo.getRoom(1);
//habitacionAnterior = mundo.getRoom(1);
//substituted by:
setRoom(mundo.getRoom(1));
setRoom(mundo.getRoom(1)); //quedan seteadas actual y anterior
//state = 1; //001 IDLE
//timeUnitsLeft = 1; //estado inicial.
setNewState(1,1);
//mundo.setPlayer(this);
initDefaultProperties(mundo);
}
//METHODS
/*Para salvados: en vez de coger comandos de teclado, se ejecuta un fichero de log
hasta que termine. Esto abre el fichero, lo dem�s ya lo har� el thread del engine
(es autom�tico) -> antes de hacer el start al thread, poner prepareLog si es un
salvado.*/
//TRIED TO RENDER IT UNUSEFUL AS OF 04.03.29
/*
public void prepareLog(String s) throws java.io.FileNotFoundException
{
FileInputStream logInput = new FileInputStream(s);
logReader = new BufferedReader ( Utility.getBestInputStreamReader ( logInput ) );
try
{
logReader.readLine(); //la primera linea no contiene un comando
logReader.readLine(); //la segunda tampoco
}
catch ( java.io.IOException exc )
{
escribir(io.getColorCode("error") + "Excepci�n I/O al leer el log" + io.getColorCode("reset"));
}
from_log = true;
}
*/
public void prepareLog ( BufferedReader br )
{
Debug.println("Preparing log input on " + this);
this.logReader = br;
from_log = true;
}
public void endOfLog()
{
from_log = false;
if ( this.getClient() instanceof MultimediaInputOutputClient )
{
MultimediaInputOutputClient mioc = (MultimediaInputOutputClient) this.getClient();
SoundClient sc = mioc.getSoundClient();
if ( sc instanceof AGESoundClient )
{
AGESoundClient asc = (AGESoundClient) sc;
asc.activate();
}
}
}
synchronized public void resumeExecution ( )
{
notify();
}
//from Entity.
//excludes property "state". <- ? Not sure about if this comment is actually true.
public boolean update ( PropertyEntry pe , World mundo )
{
//Debug.println("[PROPERTY UPDATE: " + pe.getName() + "]" + new Vector(propertiesList).toString());
String theProp = pe.getName();
if ( theProp.equals("custom_parsing") )
{
boolean value = pe.getValueAsBoolean();
if ( value )
{
try
{
return customParse();
}
catch ( IOException ioe )
{
write( io.getColorCode("error") + "Excepci�n E/S en update() para propiedad custom_parsing" + io.getColorCode("reset") );
return false;
}
}
else return true;
}
else
{
//player's state must always be updated, can't just go and fall into negative numbers and be ignored
if ( theProp.equals("state") && pe.getTimeLeft() < 0 )
pe.setTime(0);
return super.update(pe,mundo);
}
}
public void setParseRoutine ( Entity holdingTheRoutine , String routineName )
{
setProperty("custom_parsing",true,0);
pushPropertyToFront("custom_parsing"); //para que se actualice antes que state
setRelationshipProperty(holdingTheRoutine,"custom_parser",routineName);
}
//preCD:
//property "custom_parsing" of this player is true and just expired,
//player has a relationship "custom_parser" with a single, coded Entity. Relationship value is the name of the (bsh) parse routine.
synchronized public boolean customParse ( ) throws java.io.IOException
{
String theCommand;
if ( from_log )
{
String newCommand = logReader.readLine();
if ( newCommand == null )
{
//se acab� el log
from_log = false;
mundo.endOfLog();
return customParse();
}
else
{
//que aparezca el comando como si lo hubieramos introducido
io.forceInput ( newCommand , true );
theCommand = newCommand;
}
}
else
{
//hacemos lo mismo que en execCommand() [el parser por defecto]
GameEngineThread gte = (GameEngineThread)Thread.currentThread();
if ( gte.isRealTimeEnabled() )
{
theCommand = io.getRealTimeInput(this);
if ( theCommand == null )
{
//seguir esperando
setPropertyTimeLeft ( "custom_parsing" , 1 );
return false;
}
}
else
{
theCommand = io.getInput(this);
if ( theCommand == null )
{
if ( io.isDisconnected() ) //remote player disconnected
{
disconnect();
return true;
}
}
}
}
//Ya tenemos lo que el t�o escribi�, en theCommand.
List ofEntities = getRelatedEntities ( "custom_parser" );
//s�lo consideramos la primera no nula, porque s�lo puede haber una.
if ( ofEntities.size() < 1 )
{
write(io.getColorCode("error") + "Error: llamada a customParse() sin entidades con parsers activos para �sta." + io.getColorCode("reset"));
return false;
}
else
{
Entity ourEntity=null;
String routineName=null;
for ( int i = 0 ; i < ofEntities.size() ; i++ )
{
ourEntity = (Entity) ofEntities.get(i);
routineName = getRelationshipPropertyValueAsString ( ourEntity , "custom_parser" );
if ( routineName != null ) break;
}
if ( ourEntity instanceof SupportingCode )
{
ReturnValue retVal = new ReturnValue(null);
try
{
((SupportingCode)ourEntity).execCode ( routineName , new Object[] { this , theCommand } , retVal );
//removed: may have to parse moar
//setProperty("custom_parsing",false,0);
//setRelationshipProperty(ourEntity,"custom_parser",null);
}
catch ( ScriptException bshte )
{
write( io.getColorCode("error") + "bsh.TargetError found at customParse(), execcing from ID " + getID() + " the routine " + routineName + " of " + ourEntity.getID() );
bshte.printStackTrace();
writeError(ExceptionPrinter.getExceptionReport(bshte));
}
if ( retVal.getRetVal() == null || ! ( retVal.getRetVal() instanceof Boolean ) )
{
setProperty("custom_parsing",false,0);
setRelationshipProperty(ourEntity,"custom_parser",null);
return false;
}
else
{
Boolean ret = (Boolean) retVal.getRetVal();
if ( ret.booleanValue() == false )
return customParse(); //parse until it's true! DO IT!
else
{
//don't paser more
setProperty("custom_parsing",false,0);
setRelationshipProperty(ourEntity,"custom_parser",null);
return ret.booleanValue();
}
}
}
else
{
write(io.getColorCode("error") + "Error: llamada a customParse() para clase que no lo soporta: " + ourEntity.getClass() );
return false;
}
}
//getObject(getTarget())
}
/**
* Obtains a command from the client linked to a Player, putting it into the commandstring attribute.
* Returns false if no command was obtained due to client having disconnected.
* @return
*/
private boolean obtainCommandFromClient()
{
GameEngineThread gte = (GameEngineThread)Thread.currentThread();
if ( gte.isRealTimeEnabled() )
{
commandstring = io.getRealTimeInput(this);
if ( commandstring == null )
{
commandstring = "";
if ( io.isDisconnected() )
{
disconnect();
return false;
}
}
}
else
{
commandstring = io.getInput(this);
if ( commandstring == null && io.isDisconnected() )
{
disconnect();
return false;
}
}
return true;
}
/**
* Obtains an executes a command.
* The command can be obtained:
* - From the command queue,
* - From the "forced command" string,
* - From a log if we're executing a log,
* - From client input if none of the above apply.
*/
synchronized public boolean obtainAndExecCommand ( World mundo ) throws java.io.IOException
{
/*
* This loop is so that we can have metacommands that don't consume time.
* In normal cases, we execute a command and return from the function so only one iteration is ran.
* But when a metacommand is used, we do 'continue' so the function doesn't return (and no game time is consumed).
*/
for(;;)
{
/*Pueden darse dos casos:
- Que hayan quedado comandos pendientes de ejecuci�n de otra vez: estar�n
en la cola de comandos pendientes.
- Que no: esperamos una entrada por la editbox, que cambiar� la variable
"commandstring" poniendo el nuevo comando y despertar� el thread del wait.
*/
secondChance = false; //luego se pone a true en obtainCommandFromQueue() si hace falta
//mirar si cola de comandos vacia
//el !forced es porque si hemos forzado un comando, pasa por delante de la cola
if ( !commandQueue.isEmpty() && !forced ) //obtain enqueued piece of command - this was not a directly input command so it is not subject to preprocessCommand and eval
{
if ( !obtainCommandFromQueue() ) return false;
}
else
{
if ( forced )
{
forced = false;
io.forceInput ( force_string , false );
commandstring = force_string;
}
else if ( from_log )
{
String newCommand = logReader.readLine();
if ( newCommand == null )
{
from_log = false;
mundo.endOfLog();
return obtainAndExecCommand(mundo);
}
else
{
io.forceInput ( newCommand , true );
commandstring = newCommand;
}
}
else
{
//obtain command from player input
//in asynchronous mode, we get it via a nonblocking call
//in synchronous mode, we get it via a blocking call (we wait for input)
if ( !obtainCommandFromClient() ) return true; //true bc. if player disconnected, we return true
}
//Process raw command:
/*Preparaci�n del comando:*/
if ( commandstring != null ) commandstring = commandstring.trim();
/*Llamada a preprocessCommand() configurable*/
if ( runPreprocessCommand() ) return true;
//comando nulo
if ( commandstring == null || commandstring.equals("") ) return false;
/*Comandos eval - continue porque no se ejecuta un comando normal, es un metacomando de fuera del mundo*/
if ( runEvalIfApplicable() ) continue;
/*Separate commands composed of several sentences. The sentences are placed in the queue. False is returned only if the command is actually
* empty (e.g. a command consisting only of commands)*/
if ( !separateSentences() ) return false;
}
//modular execCommand()
if ( commandstring.isEmpty() ) return false; //empty strings can result if, for example, input was ",something", etc.
//TODO:
//The idea was that execCommand returns false on denials and "I don't understand".
//If this worked well, we could always use the return value for the loop (or loop outside, when we call this function).
//But this has to be done when there is some time, as it needs some care...
return execCommand ( commandstring );
}
}
public void changeState ( World mundo )
{
Debug.println("Player state " + getState() + ", target " + getTarget() + ", tu's " + getPropertyTimeLeft("state") );
//Debug.println("[PROPERTY UPDATE: " + "state" + "]" + new Vector(propertiesList).toString());
//Debug.println("changeState()");
try
{
characterChangeState( mundo );
}
catch ( java.io.IOException nopuidorl )
{
write( io.getColorCode("error") + "Excepci�n E/S en characterChangeState()" + io.getColorCode("reset") );
}
}
public void characterChangeState( World mundo ) throws java.io.IOException
{
//Debug.println("State: (" + getState() + "," + getPropertyTimeLeft("state") + ")");
switch ( getState() )
{
case 1: //IDLE
//if ( ! execCommand ( mundo ) )
// setNewState ( 1 /*IDLE*/, 1 /*penalizacion*/ );
break;
case 2: //GO
//escribir exit-description
if ( movingState_Path != null )
write( io.getColorCode("action") + movingState_Path.getDescription(this) + "\n" + io.getColorCode("reset") );
//has dejado la habitacion en que estabas (habitacionActual)
//-> ejecutar eventos onExitRoom
try
{
habitacionActual.execCode("event_exitroom","this: " + habitacionActual.getID() + "\n" + "player: " + getID() + "\n" + "dest: " + getTarget() );
habitacionActual.execCode("onExitRoom" , new Object[] {this} );
}
catch ( EVASemanticException exc )
{
write( io.getColorCode("error") + "EVASemanticException found at event_exitroom , room number " + habitacionActual.getID() + io.getColorCode("reset") );
}
catch ( ScriptException bshte )
{
write( io.getColorCode("error") + "bsh.TargetError found onExitRoom , room number " + habitacionActual.getID() + ": " + bshte + io.getColorCode("reset") );
writeError(ExceptionPrinter.getExceptionReport(bshte));
}
habitacionActual.reportAction(this,null,"$1 se va hacia " + exitname + ".\n" , null , null , false );
setRoom ( mundo.getRoom(getTarget()) );
habitacionActual.reportAction(this,null,"$1 llega desde " + Path.invert(exitname) + ".\n" , null , null , false );
//has entrado en nueva habitacion (habitacionActual)
//-> ejecutar eventos onEnterRoom
try
{
habitacionActual.execCode("event_enterroom","this: " + habitacionActual.getID() + "\n" + "player: " + getID() + "\n" + "orig: " + habitacionAnterior );
habitacionActual.execCode("onEnterRoom" , new Object[] {this} );
}
catch ( EVASemanticException exc )
{
write( io.getColorCode("error") + "EVASemanticException found at event_enterroom , room number " + habitacionActual.getID() + io.getColorCode("reset") );
}
catch ( ScriptException bshte )
{
write( io.getColorCode("error") + "bsh.TargetError found onEnterRoom , room number " + habitacionActual.getID() + ": " + bshte + io.getColorCode("reset") );
writeError(ExceptionPrinter.getExceptionReport(bshte));
}
//-> si hay Mobiles, pueden reaccionar tambi�n a que entres (onEnterRoom de Mobile)
MobileList ml = habitacionActual.getMobiles();
if ( ml != null )
{
for ( int i = 0 ; i < ml.size() ; i++ )
{
Mobile bichoActual = ml.elementAt(i);
try
{
//bichoActual.execCode("event_enterroom","this: " + habitacionActual.getID() + "\n" + "player: " + getID() + "\n" + "orig: " + habitacionAnterior );
bichoActual.execCode("onEnterRoom" , new Object[] {this} );
}
catch ( ScriptException bshte )
{
write( io.getColorCode("error") + "bsh.TargetError found onEnterRoom , mobile number " + bichoActual.getID() + ": " + bshte + "\n" + bshte.getMessage() + io.getColorCode("reset") );
writeError(ExceptionPrinter.getExceptionReport(bshte));
bshte.printStackTrace();
}
}
}
//mostrar sala si esto no est� desactivado
if ( getPropertyValueAsBoolean("describeRoomsOnArrival") )
{
show_room ( mundo );
}
setNewState ( 1 /*IDLE*/, 0 );
break;
case ATTACKING:
manageEndOfAttackState(); //de Mobile
//escribir("Attack done.");
return;
case CASTING:
manageEndOfCastState(); //de Mobile
return;
case ATTACK_RECOVER:
write("Te recuperas de tu movimiento de ataque.\n");
setNewState ( IDLE , 0 );
showCombatReport();
break;
case DAMAGE_RECOVER:
write("Te recuperas del golpe recibido.\n");
setNewState ( IDLE , 0 );
showCombatReport();
break;
case BLOCK_RECOVER:
write("Te recuperas de tu movimiento defensivo.\n");
setNewState ( IDLE , 0 );
showCombatReport();
break;
case SURPRISE_RECOVER: //para usar en c�digo bsh (encuentros por sorpresa)
//escribir("Te haces cargo de la situaci�n.\n");
setNewState ( IDLE , 0 );
showCombatReport();
break;
case BLOCKING:
write("Est�s preparado para bloquear...\n");
setNewState ( READY_TO_BLOCK , 0 );
return;
case DODGING:
setNewState ( READY_TO_DODGE , 0 );
return;
case READY_TO_BLOCK:
//hold on that state
setNewState ( READY_TO_BLOCK , 0 );
return;
case READY_TO_DODGE:
//hold on to that state
setNewState ( READY_TO_DODGE , 0 );
return;
case DYING:
//die
die();
return;
case DEAD:
//don't hold on that state itc, you're in Limbo, enjoy yourself
setNewState ( IDLE , 1 );
//escribir("\nEst�s muerto.");
return;
case DISABLED: //disconnected, etc. Don't do anything.
setNewState ( DISABLED , 1 );
return;
}
//listen 4 events?
if ( ! obtainAndExecCommand ( mundo ) )
setNewState ( 1 /*IDLE*/, 1 /*penalizacion*/ );
}
/*
public boolean resolveParseCommandOnContentsForOneEntity ( EntityList posiblesObjectivos , String arguments , String fullArguments )
{
}
*/
//nos dice "tal est� a punto de atacarte, podr�as bloquearlo... la espada de tal
//ya casi te ha pillado..."
public void showCombatReport ( )
{
if ( getEnemies() == null ) return;
for ( int i = 0 ; i < getEnemies().size() ; i++ )
{
Mobile enemigo = getEnemies().elementAt(i);
if ( enemigo.getState() == ATTACKING && enemigo.getTarget() == getID() )
{
long tiempo = enemigo.getPropertyTimeLeft ( "state" );
int nSimulations = (int) getStat("INT");
wieldedWeapons = getWieldedWeapons();
for ( int j = 0 ; j < wieldedWeapons.size() ; j++ )
{
Weapon w = (Weapon) wieldedWeapons.elementAt(j);
if ( w != null )
{
//ver si tiempo llegar�a para bloquear
int blocksInTime = 0;
for ( int s = 0 ; s < nSimulations ; s++ )
{
int t = generateBlockTime ( w );
if ( t <= tiempo )
blocksInTime++;
}
double inTimeProb = (double)blocksInTime / (double)nSimulations;
String toInform;
if ( inTimeProb >= 0.9 )
{
toInform = "Seguramente te dar�a tiempo a bloquear el golpe de $2 con " + w.constructName2OneItem(this) + ".";
}
if ( inTimeProb >= 0.7 )
{
toInform = "Crees que te dar�a tiempo a bloquear el golpe de $2 con " + w.constructName2OneItem(this) + ".";
}
else if ( inTimeProb >= 0.5 )
{
toInform = "Es posible que te d� tiempo a bloquear el golpe de $2 con " + w.constructName2OneItem(this) + ".";
}
else if ( inTimeProb >= 0.3 )
{
toInform = "Ser� bastante dif�cil bloquear a tiempo el golpe de $2 con " + w.constructName2OneItem(this) + ".";
}
else if ( inTimeProb >= 0.1 )
{
toInform = "Ser� muy dif�cil bloquear a tiempo el golpe de $2 con " + w.constructName2OneItem(this) + ".";
}
else
{
toInform = "No crees que puedas bloquear a tiempo el golpe de $2 con " + w.constructName2OneItem(this) + ".";
}
habitacionActual.reportAction ( this , enemigo , null , null , toInform + "\n" , true );
} //end if weapon not null
} //end for each weapon
//ver si tiempo llegar�a para esquivar
int dodgesInTime = 0;
for ( int s = 0 ; s < nSimulations ; s++ )
{
int t = generateDodgeTime ( );
if ( t <= tiempo )
dodgesInTime++;
}
double dodgeInTimeProb = (double)dodgesInTime / (double)nSimulations;
String toInform;
if ( dodgeInTimeProb >= 0.8 )
{
toInform = "Seguramente podr�as esquivar el ataque de $2 a tiempo.";
}
else if ( dodgeInTimeProb >= 0.6 )
{
toInform = "Probablemente podr�as esquivar a tiempo el ataque de $2.";
}
else if ( dodgeInTimeProb >= 0.4 )
{
toInform = enemigo.getCurrentWeapon().constructName2OneItem(this) + " de $2 est� cerca, ser� dif�cil esquivar su ataque.";
}
else if ( dodgeInTimeProb >= 0.2 )
{
toInform = enemigo.getCurrentWeapon().constructName2OneItem(this) + " de $2 est� casi encima , ser� muy dif�cil esquivar su ataque.";
}
else
{
toInform = enemigo.getCurrentWeapon().constructName2OneItem(this) + " de $2 est� encima, no crees que puedas esquivar el ataque.";
}
habitacionActual.reportAction ( this , enemigo , null , null , toInform + "\n" , true );
} //end if enemy attacking
} //end for each enemy
}
//pone el estado "press any key" de la entrada...
public synchronized void waitKeyPress ( )
{
if ( !from_log ) //si estamos ejecutando un log (salvado) es absurdo
{ //vernos interrumpidos por un "press any key"
//try
//{
//io.setWaitingPlayer(this);
//io.activatePressAnyKeyState();
//wait();
Thread th = Thread.currentThread();
//Debug.println("WKP Thread: " + th + "( " + SwingUtilities.isEventDispatchThread() + " )");
if ( th instanceof GameEngineThread )
{
GameEngineThread gte = (GameEngineThread)th;
if ( gte.isRealTimeEnabled() )
{
if ( mundo.getPlayerList().size() > 1 )
{
io.write("--\n"); //comportamiento alternativo
return; //no podemos dejar a todo el motor esperando si estamos en modo as�ncrono y con m�s jugadores.
}
}
}
Debug.println("WKP Call: " + io.getClass());
io.waitKeyPress();
//}
//catch ( InterruptedException intex )
//{
// ;
//}
}
}
/**
* @deprecated Use {@link #clearScreen()} instead
*/
public void borrarPantalla()
{
clearScreen();
}
public void clearScreen()
{
io.clearScreen();
}
//de informador
/**
* @deprecated Use {@link #write(String)} instead
*/
public void escribir ( String s )
{
write(s);
}
//de informador
public void write ( String s )
{
io.write(s);
}
public void setIO ( InputOutputClient es )
{
io = es;
}
//otra para plurales, y ZR's para plurales.
public Vector getFinalCommandLog() //para estad�sticas
{
return finalExecutedCommandLog;
}
//Funci�n que es llamada si el jugador es remoto (est� asociado a un cliente remoto)
//y quien lo maneja se desconecta. Hace todo lo necesario para manejar la desconexi�n
//(id est, que la cosa d� el pego en el mundo)
public void disconnect()
{
//mark player as disconnected
setProperty ( "disconnected" , true );
//set disabled state so the Player object won't try to get input on changestates
setNewState ( DISABLED , 1 );
//store current room ID as property in order to be able to return player to room later
setProperty ( "room" , habitacionActual.getID() );
//remove player from its room for others not to interact with it
habitacionActual.removeMob(this);
//inform that the player's leaving. Auto, for only 3rd person will be shown.
habitacionActual.reportActionAuto ( this , null , "$1 desaparece en un mar de irrealidad.\n" , true );
}
//inversa de disconnect()
public void reconnect( InputOutputClient io )
{
if ( getPropertyValueAsBoolean("disconnected") == false )
{
write("Hmm. �No est�s conectado ya?\n");
return;
}
setProperty ( "disconnected" , false );
setNewState ( IDLE , 1 );
Room enQueEstaba = mundo.getRoom ( getPropertyValueAsInteger("room") );
enQueEstaba.addMob(this);
getClient().write("Has sido a�adido al mundo.\n");
getRoom().reportActionAuto(this,null,"De repente, $1 aparece de la nada.\n",false);
write("Old player rejoined the game.\n");
setIO ( io );
}
}