Master Gollum

Programando un MUD (VI)

Publicado hace un año, 300 días

Por fin ha llegado el momento de crear las primeras estadísticas de los personajes y permitir guardarlos para no perder los valores y el equipo conseguido cada vez que un jugador se reconecta. Lógicamente crearemos también un pequeño proceso de creación y un sistema de login.

Lección anterior: Scripting.

Persistencia de los personajes

Hay muchas formas de atacar esta parte del código. Podría, por ejemplo, usarse directamente la Serialización, un proceso extremadamente sencillo y rápido, pero tiene el problema que los ficheros están en binario y si no se gestionan bien los cambios de versión de las clases podemos encontrarnos que perdemos todos los jugadores serializados entre una release y otra. Otras estrategias pasarían por usar librerías de serialización, por ejemplo, transformando los objetos de forma automática en XML o JSON. Pero como en este tutorial estamos chapados a la antigua, nos hacemos nosotros mismos la rutina de escritura y lectura.

Nuestra estructura será extremadamente simple. Consistiría en un prefijo en cada línea que nos indicará que dato contiene.

Name Tor
Room 2029
Pass 115031
Culture prax
Occupation bison
Chars 12 12 14 14 11 12 11 13 13 24 24 11 11 3
S spear 50
S search 50
O 1001 1
End

Así que vamos a ello.

org.mg.mud.core.Core

  public void save(File file,Player player) throws IOException{
    PrintWriter out=new PrintWriter(new FileWriter(file));
    out.println("Name "+player.getProto().getName());
    out.println("Room "+player.getRoom());
    out.println("Pass "+player.getPassword());
    out.println("Culture "+player.getCulture());
    out.println("Occupation "+player.getOccupation());
    out.print("Chars");
    for(short s:player.getChars()){
      out.print(' ');
      out.print(s);
    }
    out.println();
    for(Skill s:player.getSkills()){
      out.println("S "+s.getName()+" "+s.getBase());
    }
    for(Item i:player.getInventory()){
      out.println("O "+i.getProto().getId()+" "+i.getQuantity());
      switch(i.getProto().getType()){
        case BAG:
          for(Item c:i.getInventory()){
            out.println("C "+c.getProto().getId()+" "+c.getQuantity());
          }
          break;
      }
    }
    out.println("End");
    out.close();
  }

La lectura del fichero tampoco entraña excesivas dificultades.

  public void load(File file,Context context) throws IOException{
    Player player=context.getPlayer();
    BufferedReader buf=new BufferedReader(new FileReader(file));
    String line;
    Item item=null;
    while((line=buf.readLine())!=null){
      int i=line.indexOf(' ');
      if(i==-1)continue;
      String key=line.substring(0,i);
      String value=line.substring(i+1);
      switch(key){
        case "Name":
          player.getProto().setName(value);
          player.getProto().setPreview(context.getCore()
                .i18n(player.getLocale(),"player.preview",value));
          break;
        case "Pass":
          player.setPassword(Long.parseLong(value));
          break;
        [..]
        case "Chars":
          String[] tmp=value.split("\\s");
          short[] c=player.getChars();
          for(int j=0;j<c.length;j++)
            c[j]=Short.parseShort(tmp[j]);
          break;
        case "S":
          tmp=value.split("\\s");
          player.getSkills().add(new Skill(tmp[0],Integer.parseInt(tmp[1])));
          break;
        case "O":
          item=parseItem(value);
          player.getInventory().add(item);
          break;
        case "C":
          item.getInventory().add(parseItem(value));
          break;
      }
    }
    buf.close();
  }

Proceso de bienvenida

El primer paso será modificar la clase MUDInput para incluir la gestión del proceso de bienvenida, así que modificaremos el método run(). La idea es que mientras el usuario sigue en el gestionando la entrada en el juego, no ejecutamos ninguna de las cosas que envíe al servidor como comando, sino que las delegamos a la clase Welcome.

org.mg.mud.net.MUDInput

  public void run(){
    String line;
    try{
      {
        Welcome welcome=new Welcome(session);
        welcome:
        while((line=in.readLine())!=null){
          switch(welcome.execute(line)){
            case WELCOME:break;
            case BYE:return;
            case LOGGED:
              //Colocar al personaje en la sala inicial
              Room room=session.getCore().getAreaLoader()
                  .findRoom(session.getContext().getPlayer().getRoom());
              session.getContext().setRoom(room);
              room.join(session,Direction.L);
              break welcome;
          }
        }
      }//para liberar de la memora el objeto welcome
      while((line=in.readLine())!=null){
        session.getCore().getCommander().execute(line,session);
      }
    }catch(IOException io){
      io.printStackTrace();
    }
  }

La clase Welcome no tiene mucho misterio, es una máquina de estados que va permitiendo al usuario elegir entre varios menús que tipo de personaje quiere y retroceder en el caso que quiera cambiar la opción elegida. He dividido la elección en tres bloques: Cultura, Profesión, Rasgos y por último el nombre del personaje. Por supuesto, se podría ampliar la clase y añadir otras elecciones, como el género o la religión del personaje.

En primer lugar definimos una enumeración con los tres estados de salida posibles: WELCOME, que indica que el usuario permanece realizando el proceso, LOGGED que ha logrado entrar en el sistema y ya podemos empezar a interpretar comandos y BYE que indica que se a abortado el proceso de Login y hay que desconectarlo del servidor.

org.mg.mud.net.Welcome

public class Welcome{
  public enum Out{
    WELCOME,
    LOGGED,
    BYE
  }
}

Añadimos a continuación una enumeración con los estados internos del proces de bienvenida y nos definimos una variable de clase para guardar el estado.

public class Welcome{
  [..]
  private enum State{
    INIT,
    CULTURE,
    CULTURE_CHOOSE,
    OCCUPATION,
    OCCUPATION_CHOOSE,
    TRAIT,
    NAME,
    PASSWORD,
    LOGGING
  }

  private State state=State.INIT;
}

A continuación implementaremos el execute() con la máquina de estados.

public class Welcome{
  [..]
  public Out execute(String line){
      switch(state){
      case INIT:
        if(line.equals(i18n("new"))){
          state=State.CULTURE;
          showCultures();
          return Out.WELCOME;
        }
        if(!session.getCore().isValidPlayerName(line)){
          session.getOutput().i18n("commander.what",TextType.ALERT);
          return Out.WELCOME;
        }
        file=session.getCore().getPlayerFile(line);
        if(!file.exists()){
          session.getOutput().i18n("welcome.player.wrong",TextType.ALERT);
          return Out.BYE;
        }
        showLoggin();
        try{
          session.getCore().load(file,session.getContext());
        }catch(IOException io){
          session.getOutput().i18n("welcome.file.io",TextType.ALERT);
          return Out.BYE;
        }
        state=State.LOGGING;
        return Out.WELCOME;
        [..]
     }
  }
}

La idea es reaccionar a lo que escriba el usuario en función del estado en el que nos encontremos sabiendo que el execute() se ejecutará por cada línea que escriba. Así por ejemplo, sabiendo que en el screenshot le hemos pedido que escriba "nuevo" o su nombre de jugador. Si recibimos juego, mostraremos la lista de culturas entre las que puede escoger y si escribe cualquier otra cosa valoraremos si parece un nombre válido, en cuyo caso procederemos a cargar el fichero de personaje (si es que existe) o daremos un error si no somos capaces.

Guardar el personaje

Como nuestra intención es guardar la situación en la que se encuentra el jugador cuando abandona el juego, modificaremos el comando Quick añadiendo la persistencia.

org.mg.mud.comm.Quit

public class Quit extends BaseCommand{
  public void execute(MUDSession session){
    Player player=session.getContext().getPlayer();
    Check c=session.getContext().getRoom().depart(session);
    if(c.isSuccess()){
      try{
        File file=session.getCore().getPlayerFile(player.getProto().getName());
        session.getCore().save(file,player);
      }catch(Exception e){
        session.getOutput().i18n("quit.save.error",TextType.ALERT);
        e.printStackTrace();
      }
      session.getOutput().i18n("quit.bye",TextType.SYSTEM);
      session.getOutput().close();
    }
  }
}

Nuevos comandos

Ya que ahora tenemos ficha, crearemos un comando para poder consultarla en todo momento.

org.mg.mud.comm.Stats

public class Stats extends BaseCommand{

  public void execute(MUDSession session){
    Mob m=session.getContext().getActor();
    MUDOutput out=session.getOutput();
    out.desc("stats.1",format(m.getSTR()),format(m.getHPMax()),format(m.getHP()));
    out.desc("stats.2",format(m.getCON()),format(m.getFPMax()),format(m.getFP()));
    out.desc("stats.3",format(m.getSIZ()),format(m.getMPMax()),format(m.getMP()));
    out.desc("stats.4",format(m.getINT()),Short.toString(m.getMov()));
    out.desc("stats.5",format(m.getPOW()));
    out.desc("stats.6",format(m.getDEX()));
    out.desc("stats.7",format(m.getAPP()));
  }

  private String format(short s){
    return s<10?" "+Short.toString(s):Short.toString(s);
  }
}

Lo registramos en el fichero commands.dat y hemos terminado.

system/commands.dat

Code stats
Name ficha
End

¡Y ya está! Arrancamos el servidor, nos conectamos y ya podemos probarlo.

Próxima lección

Nos queda todavía mucho trabajo por delante. Nos falta por definir nada más y menos que el sistema de combate. Pero antes de ver como podemos implementarlo, propondré detenernos en la economía y dotar a nuestro MUD de tiendas donde comprar y vender equipo.

Descarga el código: mud06.zip

[Programando un MUD: Anterior | Siguiente]