Copy Link
Add to Bookmark
Report
SET 034 0x0C
-[ 0x0C ]--------------------------------------------------------------------
-[ Unete al Software libre ]-------------------------------------------------
-[ by FCA00000 ]-----------------------------------------------------SET-34--
Este artículo describe los pasos que he seguido para mejorar un programa de
código libre. El nivel es bajo, puesto que el objetivo es demostrar que es
fácil subirse al carro del software libre; no sólo como pasajero, sino como
tripulación.
Este texto quizás es muy obvio para algunos; pero espero que también sirva para
aquellos que quieren un empujoncito para meterse en el mundo de la programación.
Sin más preámbulo, empezaré recordando mi situación hace algunos años:
deambulaba yo por la sala de ordenadores de la universidad, y vi que un tipo
perdía el tiempo con un juego de mover fichas por un mapa. En esa época estaba
yo aficionado a los juegos de tablero (risk, Squad Leader, ...) así que
despertó mi interés.
Al día siguiente me acerqué a la sala de nuevo y el chico estaba jugando de
nuevo. Lo mismo sucedió al otro día, y me dí cuenta de que el juego parecía
ser bastante adictivo. Eso, o el individuo era un ludópata.
Me acerqué para preguntarle, y me dijo que el juego se llamaba ... Civilization:
uno de los mejores juegos de estrategia de la historia.
Lo copié y lo instalé en casa. Al principio parecía complicado porque había
muchos tipos de unidades, pero la verdad es que requería esfuerzo dejar de
jugar. Desde ese momento, yo calculo que he desperdiciado unas 2.000 horas a lo
largo de 5 años.
Luego publicaron la típica secuela llamada Colonization, pero no me atrajo lo
suficiente.
El tiempo ha pasado, ahora me gano el sueldo haciendo programas, incluidos un
par de lenguajes que casi nadie conoce, y solo de vez en cuando vuelvo a echar
una partida.
Con el auge de Internet, era inevitable que surgieran versiones de Civilization,
la más avanzada se llama FreeCiv, y es bastante fiel a la original, excepto que
no me enganchó.
Y también ha salido una adaptación de Colonization, llamada FreeCol (estos
tipos no tienen imaginación para los nombres).
Ya que no me atrapó en sus inicios, decidí darle una segunda oportunidad a esta
nueva versión.
La bajo, la instalo, leo la documentación inicial, y me pongo a jugar. El juego
en sí es fácil de aprender, y lamentablemente los enemigos no son lo bastante
inteligentes como para ponerme en aprietos. Si aumentas el nivel de dificultad,
lo único que pasa es que es más gravoso conseguir los recursos y el avance es
más lento.
Lo que me sorprendió es que el juego parece bastante maduro. No se cuelga, la
parte gráfica es aceptablemente rápida, y no tarda una eternidad en mover las
fichas enemigas.
Al ser un programa de software libre, el código fuente está disponible, así que
lo bajé por curiosidad. Y la primera sorpresa es que está escrito en Java !
Eso explica porqué tarda 20 segundos en arrancar, y usa 100 Mg de memoria.
También destroza mi creencia de que Java es lento.
Una primera ojeada muestra que está dividido en módulos:
-el interface gráfico
-el modelo de control de unidades, colonias, y terreno
-el servidor, usado en modo multijugador
-la inteligencia artificial
En total, unas 300 clases, aunque sólo la mitad son interesantes: el resto son
interfaces, pequeñas variaciones (override) de otras clases, y clases con
constantes y excepciones.
Después de jugar un par de partidas en 10 horas, me hago una idea de los
conceptos del juego. Entiendo que hay:
-jugadores
-colonias
-unidades
-edificios
-productos
-consumos (impuestos, comida, cruces)
-padres fundadores
-mapa
-terreno
-turnos
Por tanto, espero encontrar clases de estos tipos. Pero no adelantemos
acontecimientos.
El programa ejecutable viene dentro del fichero FreeCol.jar que ocupa 3 Mb en
el disco, y se puede iniciar con
java -Xmx128M -jar FreeCol.jar
El siguiente paso es obtener el código fuente freecol-0.6.1-src.tar.gz y
descomprimirlo en algún sitio.
Ahora necesito un programa para navegar por los fuentes, y compilar los cambios.
Yo hace bastantes años que no programo en Java, así que no estoy muy puesto al
día de los compiladores, entornos de desarrollo, y librerías necesarias.
Visitando las webs habituales, decido instalar
java 1.6.0 como runtime
NetBeans 5.5 como IDE (Integrated Development Enviroment)
Así, de paso, aprendo este entorno. No digo que sea el mejor, sino que es el
primero que apareció en mi búsqueda.
Genero un nuevo proyecto, tomando como base el directorio donde he
descomprimido las fuentes.
Empiezo a navegar por el código fuente y me familiarizo con el sistema de
manejar ficheros.
Para ello uso la ventana "Archivos" que los muestra según la estructura de
directorios.
Otra cosa que necesito es poder seleccionar una clase, y ver su código. Esto se
hace con el botón derecho, y:
Go To -> Source
Go To -> Declaration
También es útil el menú Find Usages, y el Edit->FindInProjects
Obviamente tengo que ser capaz de re-compilar mis cambios. Esto se consigue con
el menú Build, o también con Run. Incluso es mejor el Debug, que permite poner
breakpoints.
Intento la primera compilación, y se queja de no encontrar la clase en la línea
import cz.autel.dmi.HIGLayout;
Esta no me suena que sea una clase normal de java, ni tampoco parece ser
perteneciente a este proyecto.
Busco en el disco una librería cz.* pero no encuentro nada.
Después busco HIGLayout.*
y encuentro
freecol/jars/higlayout.jar
La descomprimo y veo que efectivamente contiene dicha clase.
Ahora hay que decirle a NetBeans que use el directorio freecol/jars , lo cual
se hace en
File->Project->Properties->Libraries->Compile-time
Lo intento de nuevo, y ahora compila sin problemas.
Magnífico: realmente el código fuente está preparado para los que quieren
modificarlo.
El siguiente paso es ejecutar el programa. También se queja de que no encuentra
las librerías, pero se arregla de manera similar, en el sub-menú
Libraries->Run-time
Eso sí: el entorno usa 100 Mg de memoria, y el juego otros 100 Mg. El
Civilization original funcionaba en MS-DOS con 500 Kb. de memoria !
Vamos a probar a hacer algún cambio.
Lo más visible del juego es obviamente la parte gráfica. Este es el punto de
inicio para ir tirando del ovillo.
Por ejemplo, el informe del consejero de asuntos externos (Report->Foreign
Affairs Advisor) nos muestra la actitud diplomática que tenemos con otros
jugadores, en una lista que incluye la frase:
Stance: Peace
Para saber dónde se define dicha variable, buscamos la palabra "Stance" y la
encontramos en el fichero
FreeColMessages.properties
en la línea
report.stance=Stance
esto hace extremadamente fácil la tarea de los traductores, pues las frases que
se muestran al usuario están almacenadas en un fichero, que se puede traducir
sin necesidad de recompilar el programa.
Ahora busco la palabra "report.stance" y la encuentro, entre otros, en el
fichero
ReportForeignAffairPanel.java
en la línea
enemyPanel.add(new JLabel(Messages.message("report.stance")), higConst.rc(row,
labelColumn));
int stance = Player.getStance();
enemyPanel.add(new JLabel(Player.getStanceAsString(stance)), higConst.rc(row,
valueColumn));
La primera línea lo que hace es añadir una etiqueta con el texto obtenido de
Messages.message("report.stance")
Está claro que esta clase se encarga de leer el texto adecuado en función del
idioma del usuario.
Por ejemplo, el fichero
FreeColMessages_it_IT.properties
contiene la traducción a italiano, que contiene
report.stance=Linea Diplom.
Volviendo al programa, la siguiente línea
int stance = Player.getStance();
nos lleva (GoTo Source) hasta la clase Player.java y hace
public int getStance(Player player) {
return getStance(player.getNation());
}
O sea, que es una función de salto a:
public int getStance(int nation) {
if (nation == NO_NATION) {
return 0;
} else {
return stance[nation];
}
}
Que a su vez nos referencia al array
stance[];
que está definido (GoTo Declaration) al principio de esta clase, como
private int[] stance = new int[NUMBER_OF_NATIONS];
y se usa (Find Usages) en
public void setStance(Player player, int newStance)
Ahí encontramos que los posibles valores son:
public static final int WAR = -2, CEASE_FIRE = -1, PEACE = 0, ALLIANCE = 1;
A su vez, setStance se invoca desde las funciones:
Player.declareIndependence -> WAR contra la madre patria
Player.giveIndependence ->PEACE con la madre patria
Monarch.declareWar -> WAR contra otro jugador
AIPlayer.determineStances -> un jugador no-humano tiene ganas de guerra
ahora supongamos que queremos que el juego sea pacífico, y que no queremos
líos con los jugadores robots. Entonces antes de
stance[player.getNation()] = newStance;
incluimos
if(newStance==WAR && player.isAI()) newStance=CEASE_FIRE;
Obviamente no todos los jugadores estarán de acuerdo con esta regla, así que se
podría incluir como una opción.
También se podría alterar en función de:
-el año: al principo del juego sería calmado, y el final sería más violento
-la nacionalidad: los holandeses son más favorables a la paz; los españoles más
belicosos
-el poder militar: no es bueno enfadar a una nación poderosa
-la riqueza: la tentación de un buen tesoro es difícil de resistir
-los padres fundadores: Benjamín Franklin es sosegado; Hernán Cortés irascible
Pero en vez de alterar el juego, vamos a mejorarlo.
En esta rutina setStance hay un error: si declaras la guerra a otro jugador, lo opuesto no sucede.
Esto es malo porque el estado de guerra proporciona algunas ventajas en la lucha, y no parece justo que el agraviado ofrezca la otra mejilla.
Notar que la función (pseudo-código) es
Atacante.setStance(Victima, WAR);
por lo que es necesario incluir una nueva línea
Victima.setStance(Atacante, WAR);
lo cual se hace antes del final:
if(newStance==WAR && player.getStance(this) !=WAR) player.setStance(this,WAR);
puesto que
this es Atacante
player es Victima
Es decir, intercambiamos los papeles.
Una vez que hemos solucionado el bug y lo hemos probado, lo normal es
publicarlo.
Dependiendo del tipo de control que los autores del programa quieren tener,
será necesario subcribirse a algún tipo de repositorio colaborativo,
normalmente en un sitio controlado por CVS o SVN, tal como sourceforge.
En ese caso, el sitio es http://www.freecol.org
que apunta a
freecol.sourceforge.net
y la lista de correo es
freecol-users@lists.sourceforge.net
Al principio es una buena net-etiqueta el presentarse, y ofrecer tu cooperación.
Los cambios pequeños, y los que haces en los primeros dias tras unirte al grupo,
se pueden mandar en un correo diciendo algo así como:
Estimado señor programador,
en el fichero
src/net/sf/freecol/common/model/Player.java
cerca de la línea 2073
dice
...
if (oldStance == PEACE && newStance == WAR) {
...
y yo creo que se podría mejorar, provocando la guerra complementaria.
Esto es, debería decir
...
if(newStance==WAR && player.getStance(this) !=WAR)
player.setStance(this,WAR);
if (oldStance == PEACE && newStance == WAR) {
...
Atentamente,
otro programador.
En este caso particular, una revisión de la lógica del juego demostró que este
error no era un error: es intencionado. Esto demuestra que todos cometemos
errores, sobre todos los novatos. Antes de abrir la boca, hay que saber de lo
que se habla.
Al cabo del tiempo, cuando los otros programadores confían en tí, puedes crear
un usuario en sourceforge y trabajar directamente sobre el repositorio.
Para ello tienes que instalar CVS/SVN y luego:
-transferir la versión más reciente a tu ordenador
-averiguar qué fichero quieres modificar
-checkout
-hacer los cambios
-probarlos
-checkin
En algunos grupos de colaboración sólo el administrador puede hacer cambios. En
este caso debes mandarle los cambios para que los pueda revisar e instalar.
Esto se hace en forma de parches:
Primero debes tener la versión más reciente del código fuente.
Luego haces los cambios en tu ordenador, probando que todo funciona bien.
Comparas los ficheros:
diff -U 3 -H -d -p -r -N original/src/..../Player.java FCA00000/src/..
../Player.java
que genera un fichero parecido a esto:
diff -U 3 -H -d -p -r -N original/src/net/sf/freecol/common/model/Player.java
FCA00000/src/net/sf/freecol/common/model/Player.java
--- original/src/net/sf/freecol/common/model/Player.java
2007-04-03 19:48:42.000000000 +0200
+++ FCA00000/src/net/sf/freecol/common/model/Player.java
2007-05-07 15:19:58.000000000 +0200
@@ -2073,33 +2073,34 @@ public void setStance(Player player, int newStance) {
+if(newStance==WAR && player.getStance(this) !=WAR)
player.setStance(this,WAR);
if (oldStance == PEACE && newStance == WAR) {
como puedes ver, incluye el nombre del fichero modificado, la función donde
está en cambio, la línea original, el código original, y el código añadido.
Esto se guarda en un fichero
FCA00000.2007.05.07.diff
y se le manda al administrador del proyecto, que lo incluirá si le parece bien.
Si tu nuevo código es útil y está bien escrito, lo incluirán. También es
posible que lo cambien para adecuarse a las normas de escritura (mayúsculas,
indentado, formateado, ...) y no debes ofenderte si recibes alguna crítica. Al
fin y al cabo, ellos estaban antes, así que eres tú el que debe adecuarse a sus
reglas.
Siguiendo con las modificaciones, voy a dar otro ejemplo de un cambio que yo he
hecho a este programa.
El objetivo del juego es declarar la independencia de la madre patria. Para eso
fundas colonias y construyes edificios. Una vez que has completado uno puedes
empezar con otro, pero solo sucede automáticamente en unos pocos casos: si
acabas de construir un almacén, inmediatamente empiezas a trabajar en uno más
grande. Cuando lo finalizas, no hay otra ampliación disponible.
Esto provoca que algunas de tus colonias no construyen nada, lo cual es una
pérdida de recursos.
Es posible hacer aparecer un menú con la lista de todas tus colonias, y te
muestra los edificios ya construidos, además del que se está construyendo (en
color gris). Lamentablemente no es evidente que no se está construyendo nada.
¿Dónde se programa esta lista?
Bueno, el título es "Colony Advisor" que está definido como
menuBar.report.colony=Colony Advisor
que, entre otros, aparece en
ReportProductionPanel.java
que hace
...
add(new JLabel(Messages.message("Colony")), higConst.rc(1, colonyColumn));
...
for (int colonyIndex = 0; colonyIndex < colonies.size(); colonyIndex++) {
...
Building building = colony.getBuildingForProducing(goodsType);
}
Así que pongo un breakpoint en
getBuildingForProducing
y arranco el programa.
Hago aparecer el panel "Colony Advisor" y efectivamente acaba en el breakpoint. Lo que me sorprende es que mirando el stack (pila de llamadas) descubro que viene desde
ReportColonyPanel.java
Bueno, me he equivocado de panel, pero he llegado al mismo punto.
Analizo la rutina
private JPanel createBuildingPanel(Colony colony) {
...
for (int buildingType = 0; buildingType < Building.NUMBER_OF_TYPES;
buildingType++) {
buildingPanel.add(new JLabel(building.getName()));
if (buildingType == colony.getCurrentlyBuilding()) {
buildingLabel.setForeground(Color.GRAY);
}
}
Lo cual quiere decir:
-para esta colonia:
-para cada edificio:
-muéstralo
-si se está construyendo, ponlo de color gris
Lo que quiero hacer es que si no se está construyendo nada, mostrar una línea
en rojo que lo diga.
Algo así como:
if(colony.getCurrentlyBuilding()==NULL)
buildingPanel.add(new JLabel("No se está construyendo nada"),
Colour.RED);
Hay varios pequeños detalles:
-primero, getCurrentlyBuilding() devuelve -1 , no NULL
-segundo, que JLabel no admite un color como segundo argumento.
-por último, que queremos que aparezca en todos los idiomas
Todos estos ajustes son detectados por el compilador, por lo que no es
necesario probar el programa para darse cuenta de que no funciona.
De hecho, la manera correcta es
if (colony.getCurrentlyBuilding() == -1) {
JLabel unitLabel = new JLabel(Messages.message("nothing"));
unitLabel.setForeground(Color.RED);
buildingPanel.add(unitLabel);
}
Es más, el entorno NetBeans permite modificar el código en tiempo de ejecución.
Usando el menú Run->ApplyCodeChanges los cambios quedan reflejados
inmedatamente. Por supuesto que no todas las clases permiten esto, pero si
funciona, es un esfuerzo que te ahorras.
Probarlo es fácil: hago que una colonia no construya nada, produzco el informe,
y efectivamente se muestra una línea "Nothing" en color rojo chillón.
En los primeros dias de jueguetear con el código fuente es habitual estar
perdido y navegar desde una clase a otra sin encontrar lo que buscas, Aparte de
poner breakpoints y examinar el stack, también es útil tracear el programa.
En este caso, los autores han previsto que necesitan saber lo que va haciendo
el programa. No sólo para ellos mismos, sino para que otro usuario que tenga un
error pueda mandar un informe detallado de la situación.
Eso se consigue a través de una clase
logger
que escribe en un fichero dependiendo del nivel de detalle que necesitas.
Incluye métodos
severe, warning, info, config, fine, finer, finest
Cuando quiero saber cuáles han sido las rutinas y clases ejecutadas más
recientemente, sólo tengo que mirar las últimas líneas de este fichero.
Esto es un método realmente cómodo. Es sorprendente que haya muchos proyectos
que no incluyen una traza de ejecución, sobre todo cuando disponen de capacidad
de hacerlo. Me pregunto cómo hacen para debuggear errores en el entorno de
producción.
También he encontrado útil el uso del parámetro
java -verbose
cuando se ejecuta el programa. Esto muestra todas las clases que se van
cargando, por lo que resulta más eficiente para saber dónde buscar.
Bueno, eso es todo. Ahora, ve y busca un proyecto en sorceforge. Seguro que
encuentras alguno en el que cooperar.
Yo, voy a ver si acabo con los Franceses de Louis XIV en Martinique.
Este artículo ha sido escrito en 5 horas, sin contar la investigación inicial
sobre los fuentes de Colonization.
Durante su redacción, ningún vegetal fue sometido a daños innecesarios.
*EOF*