¿Estás interesado en una herramienta que te ayude a jugar a Guerras Tribales?, dame tu opinión y veré si merece la pena. Opinar AQUI.
En ocasiones disponemos de fuentes de información que nos gustaría poder procesar de acuerdo a nuestras propias necesidades. Puede tratarse de las cotizaciones de bolsa, de las coordenadas de las ciudades del mundo o, como en este caso, de información global de un juego de estrategia.
Las fuentes externas pueden estar disponibles en una amplia variedad de formas pero, exceptuando que el orígen disponga de algún tipo de servicio (por ejemplo OLAP o lógicamente SQL) no podremos realizar consultas eficientes de forma remota. Es por eso que se hace necesario descargar y guardar la información de forma adecuada.
Sin entrar en detalle (ni porqué ni para qué) disponemos de la siguiente información:
Existen otros archivos disponibles, pero éstos forman un conjunto cerrado (no hay códigos cuyas tablas no estén definidas) y son un buen ejemplo.
Sin lugar a dudas, la mejor forma de poder procesar adecuadamente los datos es usar un buen motor de bases de datos. Siento decir (lo siento) que MySql no es (con mucho) mi base de datos preferida (ya lo he comentado en algún otro artículo) y sitiendolo mucho (sitiendolo por los detractores) mi preferido (motor de base de datos) es sin duda Microsoft SQL Server. Pero, aquí sólo dispongo de MySql, así que tendrá que bastar.
Bueno, además de recuperar la información, querremos un histórico del estado en el que se encuentra todo, con ésto podremos identificar los cambios producidos y su evolución en el tiempo (algo muy importante en la estrategia si queremos saber qué tan rápidos responden o evolucionan otros jugadores o tribus). Así, las tablas que haremos serán:
La única diferencia entre las tablas gt_[ALIAS] y las tablas gt_[ALIAS]_HIS es que las primeras almacenan el estado actual, mientras que las segundas guardan todas y cada una de las variaciones producidas. Podrías pensar (y con razón) que sólo hacen falta tres tablas (que con las _HIS podemos tener todos los datos) pero es mejor hacerlo así, ya que nos facilitará (en consultas y en rendimiento) el trabajo.
Bueno, las tablas no tienen ningún misterio, casi son una traslación directa de lo ya escrito.
/* Tribus */ CREATE TABLE gt_ALLY ( ally_pk INT NOT NULL, ally_name VARCHAR(50) NOT NULL, tag VARCHAR(15) NOT NULL, PRIMARY KEY ( ally_pk ) ); /* Si pudiéramos asegurar que son únicas, crearíamos el índice UNIQUE: */ CREATE INDEX idx_gt_ALLY_ally_name ON gt_ALLY ( ally_name ASC ); /* Si pudiéramos asegurar que son únicas, crearíamos el índice UNIQUE: */ CREATE INDEX idx_gt_ALLY_tag ON gt_ALLY ( tag ASC ); /* Jugadores */ CREATE TABLE gt_PLAYER ( player_pk INT NOT NULL, player_name VARCHAR(30) NOT NULL, ally_pk INT NOT NULL, PRIMARY KEY ( player_pk ), FOREIGN KEY ( ally_pk ) REFERENCES gt_ALLY ( ally_pk ) ); /* Si pudiéramos asegurar que son únicas, crearíamos el índice UNIQUE: /* CREATE INDEX idx_gt_PLAYER_player_name ON gt_PLAYER ( player_name ASC ); /* Nos vendrá bien para buscar los miembros de una tribu: */ CREATE INDEX idx_gt_PLAYER_ally_pk ON gt_PLAYER ( ally_pk ASC ); /* Ciudades */ CREATE TABLE gt_VILLAGE ( village_pk INT NOT NULL, village_name VARCHAR(130) NOT NULL, player_pk INT NOT NULL, x INT NOT NULL, y INT NOT NULL, points INT NOT NULL, rank INT NOT NULL, PRIMARY KEY ( village_pk ), FOREIGN KEY ( player_pk ) REFERENCES gt_PLAYER ( player_pk ) ); /* Si pudiéramos asegurar que son únicas, crearíamos el índice UNIQUE: */ CREATE INDEX idx_gt_VILLAGE_village_name ON gt_VILLAGE ( village_name ASC ); /* Nos vendrá bien para buscar las ciudades de un jugador: */ CREATE INDEX idx_gt_VILLAGE_ally_pk ON gt_VILLAGE ( player_pk ASC ); /* Tribus */ CREATE TABLE gt_ALLY_HIS ( ally_pk INT NOT NULL, rtime DATETIME NOT NULL, ally_name VARCHAR(50) NOT NULL, tag VARCHAR(15) NOT NULL, PRIMARY KEY ( rtime, ally_pk ) ); CREATE INDEX idx_gt_ALLY_HIS_ally_name ON gt_ALLY_HIS ( ally_name ASC ); /* Si pudiéramos asegurar que son únicas, crearíamos el índice UNIQUE: */ CREATE INDEX idx_gt_ALLY_HIS_tag ON gt_ALLY_HIS ( tag ASC ); /* Jugadores */ CREATE TABLE gt_PLAYER_HIS ( player_pk INT NOT NULL, rtime DATETIME NOT NULL, player_name VARCHAR(30) NOT NULL, ally_pk INT NOT NULL, PRIMARY KEY ( rtime, player_pk ), FOREIGN KEY ( ally_pk ) REFERENCES gt_ALLY_HIS ( ally_pk ) ); CREATE INDEX idx_gt_PLAYER_HIS_player_name ON gt_PLAYER_HIS ( player_name ASC ); /* Nos vendrá bien para buscar los miembros de una tribu: */ CREATE INDEX idx_gt_PLAYER_HIS_ally_pk ON gt_PLAYER_HIS ( ally_pk ASC ); /* Ciudades */ CREATE TABLE gt_VILLAGE_HIS ( village_pk INT NOT NULL, rtime DATETIME NOT NULL, village_name VARCHAR(130) NOT NULL, player_pk INT NOT NULL, x INT NOT NULL, y INT NOT NULL, points INT NOT NULL, rank INT NOT NULL, PRIMARY KEY ( rtime, village_pk ), FOREIGN KEY ( player_pk ) REFERENCES gt_PLAYER_HIS ( player_pk ) ); CREATE INDEX idx_gt_VILLAGE_HIS_village_name ON gt_VILLAGE_HIS ( village_name ASC ); /* Nos vendrá bien para buscar las ciudades de un jugador: */ CREATE INDEX idx_gt_VILLAGE_HIS_ally_pk ON gt_VILLAGE_HIS ( player_pk ASC );
Sin duda, el uso de procedmientos almacenados es vital en la gestión eficiente (en muchos sentidos) de bases de datos, e aquí uno de los 3 (los otros dos son casi idénticos) que nos permitirán cargar la información de forma cómoda y sencilla:
gtp_ally_update
DELIMITER // CREATE PROCEDURE gtp_ally_update( _ally_pk INT, _ally_name VARCHAR(50), _tag VARCHAR(15) ) BEGIN DECLARE _prev_ally_pk INT; # ¿Existe uno exactamente con esos datos? SELECT ally_pk FROM gt_ALLY WHERE ally_pk = _ally_pk AND ally_name = _ally_name AND tag = _tag LIMIT 1 INTO _prev_ally_pk; # Sólo si no existe con esos datos debemos insertar o actualizar: IF _prev_ally_pk IS NULL THEN # Veamos si el registro existe: SELECT ally_pk FROM gt_ALLY WHERE ally_pk = _ally_pk LIMIT 1 INTO _prev_ally_pk; # ¿Insertar? IF _prev_ally_pk IS NULL THEN INSERT INTO gt_ALLY ( ally_pk, ally_name, tag ) SELECT _ally_pk as ally_pk, _ally_name as ally_name, _tag as tag; ELSE # ¿Actualizar? UPDATE gt_ALLY SET _ally_name = ally_name, _tag = tag WHERE ally_pk = _ally_pk; END IF; # En cualquier caso, guardamos en el histórico INSERT INTO gt_ALLY_HIS ( ally_pk, rtime, ally_name, tag ) SELECT _ally_pk as ally_pk, NOW() as rtime, _ally_name as ally_name, _tag as tag; END IF; END // DELIMITER ;
Supuesto que tenemos los archivos localmente y descomprimidos (ver más adelante el script), una buena forma de importalos es mediante Perl, sí, se que podía usar el interprete de PHP de la misma forma que el de Perl pero es que Perl me gusta mucho más (por ahí explico porqué).
Bueno, el código es muy sencillo, me ahorraré explicaciones (y como antes, añadir la parte que lee de los otros dos archivos es idéntica a la mostrada).
GT_Up_DataFile.pl
#!/usr/bin/perl use strict; use DBI(); my $dbh = DBI->connect( 'DBI:mysql:database=BASE_DE_DATOS;host=SERVIDOR_MYSQL', 'USUARIO', 'PASSWORD', {'RaiseError' => 1} ); open ALLY, "ally.txt" or die("Imposible abrir el archivo 'ally.txt'.\n"); while( <ALLY> ) { if( /^([^,]*),([^,]*),([^,]*),/ ) { my $id = $1; my $nm = URLDecode($2); my $tg = URLDecode($3); $id =~ s/'\''/'\'\''/g; $nm =~ s/'\''/'\'\''/g; $tg =~ s/'\''/'\'\''/g; print "$id: \"$nm\", \"$tg\".\n"; my $sth = $dbh->prepare("call gtp_ally_update( '$id', '$nm', '$tg' )"); $sth->execute(); $sth->finish(); } } close ALLY; $dbh->disconnect(); sub URLDecode { my $theURL = $_[0]; $theURL =~ tr/+/ /; $theURL =~ s/%([a-fA-F0-9]{2,2})/chr(hex($1))/eg; $theURL =~ s/<!-(.|\n)*->//g; return $theURL; }
Bajar los archivos, descomprimirlos y lanzar el script en Perl que carga los datos en la base de datos de MySql es también muy sencillo. Dependiendo de si usas Windows o usas Linux (o Mac u otros) emplearás un intérprete de comandos u otro. La alternativa que yo he usado es la siguiente:
GT_Check_Changes.Bat
wget -O village_txt.gz http://es2.guerrastribales.es/map/village.txt.gz wget -O player_txt.gz http://es2.guerrastribales.es/map/player.txt.gz wget -O ally_txt.gz http://es2.guerrastribales.es/map/ally.txt.gz winrar.exe x village_txt.gz winrar.exe x player_txt.gz winrar.exe x ally_txt.gz GT_Up_DataFile.pl
Bastante sencillo ¿no?. Lanzando periodicamente este script tendremos actualizada nuestra base de datos.
Con todos los datos normalizados y metidos en una base de datos son obvias las aplicaciones que dichos datos tienen. Quizás el más útil sea saber qué pueblos bárbaros y de bonificación (u otras granjas si somos agresivos) tenemos en un radio determinado con un nivel de puntos dentro de un rango (ni muy pequeños que no nos darán recursos, ni muy grandes que nos producirán bajas) y quizás con otros criterios más refinados como aquellos que, además, estén más alejados de otros posibles granjeadores, ir almacenando el resultado de nuestros ataques, etc... Pero hay muchas otras aplicaciones, como saber qué progresión tecnológica (indicada de forma aproximada por los puntos) poseen los contrincantes cercanos, conocer cuales están más o menos activos (para atacar o no), saber qué miembros de la tribu evolucionan con mayor rapidez, saber quienes conquistan a quienes, etc...
Los 30 pueblos abandonados más cercanos a la posición (222|333) con puntos entre 100 y 200 .
SET @x = 222; SET @y = 333; SET @A = 100; SET @B = 200; SELECT CONCAT( x , '|' , y ) as Posicion, SQRT( (x-@x) * (x-@x) + (y-@y) * (y-@y) ) as Distancia, g.* FROM gt_VILLAGE g WHERE player_pk = 0 AND points BETWEEN @A AND @B ORDER BY SQRT( (x-@x) * (x-@x) + (y-@y) * (y-@y) ) LIMIT 30;
Todos los jugadores con más de 4000 puntos con alguna ciudad a menos de 10 cuadros de alguna de las ciudades del jugador 666.
SET @x = 0; SET @y = 0; SELECT x, y FROM gt_VILLAGE WHERE player_pk = 149051 INTO @x, @y; SELECT player_pk FROM gt_VILLAGE WHERE x = @x AND y = @y;