Tutorial gomoku
Este tutorial te guiar谩 a trav茅s de los fundamentos de la creaci贸n de un juego sencillo en BGA Studio, a trav茅s del ejemplo de Gomoku (tambi茅n conocido como Gobang o Five in a Row).
Empezar谩s con nuestra plantilla de "juego vac铆o"
Este es el aspecto por defecto de tus juegos cuando acaban de ser creados:
Prepara el tablero
Re煤ne las im谩genes 煤tiles para el juego y ed铆talas como sea necesario. S煤belas en la carpeta 'img' de tu acceso SFTP.
Edita el .tpl para a帽adir algunos divs para el tablero en el HTML. Por ejemplo:
<div id="gmk_game_area"> <div id="gmk_background"> <div id="gmk_goban"> </div> </div> </div>
Editar el .css para establecer los tama帽os y posiciones de los div y mostrar la imagen del tablero como fondo.
#gmk_game_area { text-align: center; position: relative; } #gmk_background { ancho: 620px; altura: 620px; position: relative; display: inline-block; } #gmk_goban { background-image: url( 'img/goban.jpg'); anchura: 620px; altura: 620px; position: absolute; }
Configura la columna vertebral de tu juego
Edita dbmodel.sql para crear una tabla para las intersecciones. Necesitamos coordenadas para cada intersecci贸n y un campo para almacenar el color de la piedra en esta intersecci贸n (si la hay).
CREATE TABLE IF NOT EXISTS `intersection` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `coord_x` tinyint(2) unsigned NOT NULL, `coord_y` tinyint(2) unsigned NOT NULL, `color_piedra` varchar(8) NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
Edita .game.php->setupNewGame() para insertar las intersecciones vac铆as (19x19) con coordenadas en la base de datos.
// Insertar las intersecciones (vac铆as) en la base de datos $sql = "INSERT INTO intersection (coord_x, coord_y) VALUES "; $valores = array(); for ($x = 0; $x < 19; $x++) { for ($y = 0; $y < 19; $y++) { $valores[] = "($x, $y)"; } } $sql .= implode( $valores, ',' ); self::DbQuery( $sql );
Edita .game.php->getAllDatas() para recuperar el estado de las intersecciones de la base de datos.
// Intersecciones $sql = "SELECT id, coord_x, coord_y, stone_color FROM intersection "; $resultado['intersecciones'] = self::getCollectionFromDb( $sql );
Edita el .tpl para crear una plantilla para las intersecciones.
var jstpl_intersection='<div class="gmk_intersection ${stone_type}" id="intersection_${x}_${y}"></div>';
Definir los estilos para los divs de intersecci贸n.
.gmk_intersection { width: 30px; height: 30px; position: relative; }
Edita .js->setup() para configurar la capa de intersecciones que se utilizar谩 para obtener los eventos de clic y para mostrar las piedras. Los datos que devolviste en $result['intersections'] en .game.php->getAllDatas() est谩n ahora disponibles en tu .js->setup() en gamedatas.intersections.
// Configurar intersecciones for( var id in gamedatas.intersections ) { var intersection = gamedatas.intersections[id]; dojo.place( this.format_block('jstpl_intersection', { x:intersection.coord_x, y:intersection.coord_y, tipo_piedra:(intersection.stone_color == null ? "no_stone" : 'stone_' + intersection.stone_color) } ), $ ('gmk_background' ); var x_pix = this.getXPixelCoordinates(intersection.coord_x); var y_pix = this.getYPixelCoordinates(intersection.coord_y); this.slideToObjectPos( $('intersection_'+intersection.coord_x+'_'+intersection.coord_y), $('gmk_background'), x_pix, y_pix, 10 ).play(); if (intersection.stone_color != null) { // Esta intersecci贸n est谩 tomada, ya no deber铆a aparecer como clicable dojo.removeClass( 'intersection_' + intersection.coord_x + '_' + intersection.coord_y, 'clickable' ); } }
Utiliza un poco de css temporal border-color o background-color y opacidad para ver los divs y asegurarte de que los tienes bien posicionados.
.gmk_intersection { width: 30px; height: 30px; posici贸n: relative; background-color: blue; opacity: 0.3; }
Puedes declarar algunas constantes en material.inc.php y pasarlas a tu .js para facilitar el reposicionamiento (modificar la constante, refrescar). Esto es especialmente 煤til si las mismas constantes tienen que ser utilizadas en el servidor y en el cliente.
- Declare sus constantes en material.inc.php (esto se incluir谩 autom谩ticamente en su .game.php)
$this->gameConstants = array( "INTERSECTION_WIDTH" => 30, "INTERSECTION_HEIGHT" => 30, "INTERSECTION_X_SPACER" => 2.8, // Float "INTERSECTION_Y_SPACER" => 2.8, // Float "X_ORIGIN" => 0 "Y_ORIGIN" => 0, );
- En .game.php->getAllDatas(), a帽ada las constantes a la matriz de resultados
// Constantes $resultado['constantes'] = $this->gameConstants;
- En el constructor .js, definir una variable de clase para las constantes
// Constantes del juego this.gameConstants = null;
- En .js->setup() asigna las constantes a esta variable
this.gameConstants = gamedatas.constants;
- Luego 煤salo en tus funciones getXPixelCoordinates y getYPixelCoordinates
getXPixelCoordinates: function( intersection_x ) { return this.gameConstants['X_ORIGIN'] + intersection_x * (this.gameConstants['INTERSECTION_WIDTH'] + this.gameConstants['INTERSECTION_X_SPACER']); }, getYPixelCoordinates: function( intersection_y ) { return this.gameConstants['ORIGEN_Y'] + intersection_y * (this.gameConstants['ALTURA_INTERSECCI脫N'] + this.gameConstants['ESPACIADOR_Y']); },
Esto es lo que deber铆as obtener:
Gesti贸n de estados y eventos
Define los estados de tu juego en states.inc.php. Para gomoku usaremos 3 estados adem谩s de los estados predefinidos 1 (gameSetup) y 99 (gameEnd). Uno para jugar, otro para comprobar la condici贸n de fin de juego, otro para ceder su turno al otro jugador si la partida no ha terminado.
El primer estado requiere una acci贸n del jugador, por lo que su tipo es "jugador activo".
Los otros dos son acciones autom谩ticas para el juego, por lo que su tipo es 'juego'.
Actualizaremos la progresi贸n mientras comprobamos el final de la partida, por lo que para este estado ponemos el flag 'updateGameProgression' a true.
2 => array( "name" => "playerTurn", "description" => clienttranslate('${actplayer} debe jugar una piedra'), "descriptionmyturn" => clienttranslate('${you} must play a stone'), "type" => "activeplayer", "possibleactions" => array( "playStone" ), "transitions" => array( "stonePlayed" => 3, "zombiePass" => 3 ) ), 3 => array( "name" => "checkEndOfGame", "description" => '', "type" => "game", "action" => "stCheckEndOfGame", "updateGameProgression" => true, "transitions" => array( "gameEnded" => 99, "notEndedYet" => 4 ) ), 4 => array( "name" => "nextPlayer", "description" => '', "type" => "game", "action" => "stNextPlayer", "transitions" => array( "" => 2 ) ),
Implementa la funci贸n 'stNextPlayer()' en .game.php para gestionar la rotaci贸n de los turnos. Excepto si hay reglas especiales para el turno de juego dependiendo del contexto, esto es realmente f谩cil:
funci贸n stNextPlayer() { self::trace( "stNextPlayer" ); // Pasar al siguiente jugador $jugador_activo = self::activeNextPlayer(); self::giveExtraTime( $jugador_activo ); $this->gamestate->nextState(); }
A帽adir eventos onclick en las intersecciones en .js->setup()
// A帽adir eventos en elementos activos (el tercer par谩metro es el m茅todo que se llamar谩 cuando ocurra el evento definido por el segundo par谩metro - este m茅todo debe ser declarado previamente) this.addEventToClass( "gmk_intersection", "onclick", "onClickIntersection");
Declarar la funci贸n correspondiente.js->onClickIntersection(), que llama a una funci贸n de acci贸n en el servidor con los par谩metros adecuados. En la ruta de ajaxcall, sustituye "gomoku" por tu propio nombre de proyecto.
onClickIntersection: function( evt ) { console.log( '$$$$ Evento : onClickIntersection' ); dojo.stopEvent( evt ); if( ! this.checkAction( 'playStone' ) ) { return; } var node = evt.currentTarget.id; var coord_x = node.split('_')[1]; var coord_y = node.split('_')[2]; console.log( '$$$$ Intersecci贸n seleccionada : (' + coord_x + ', ' + coord_y + ')' ); if ( this.isCurrentPlayerActive() ) { this.ajaxcall( "/gomoku/gomoku/playStone.html", { lock: true, coord_x: coord_x, coord_y: coord_y }, this, function( result ) {}, function( is_error ) {} ); } },
A帽ade esta funci贸n de acci贸n en .action.php, recuperando los par谩metros y llamando a la acci贸n del juego correspondiente
public function jugarPiedra() { self::setAjaxMode(); // Recuperar los argumentos // Nota: estos argumentos corresponden a lo que se ha enviado a trav茅s del m茅todo "ajaxcall" de javascript $coord_x = self::getArg( "coord_x", AT_posint, true ); $coord_y = self::getArg( "coord_y", AT_posint, true ); // Luego, llama al m茅todo apropiado en tu l贸gica de juego, como "playCard" o "myAction" $this->game->playStone( $coord_x, $coord_y ); self::ajaxResponse( ); }
A帽adir la acci贸n del juego en .game.php para actualizar la base de datos, enviar una notificaci贸n al cliente proporcionando el evento notificado ('stonePlayed') y sus par谩metros, y proceder al siguiente estado.
function playStone( $coord_x, $coord_y ) { // Comprueba que es el turno del jugador y que es una "acci贸n posible" en este estado del juego (ver states.inc.php) self::checkAction( 'playStone' ); $player_id = self::getActivePlayerId(); // Comprobar que esta intersecci贸n est谩 libre $sql = "SELECT id, coord_x, coord_y, stone_color FROM intersecci贸n WHERE coord_x = $coord_x AND coord_y = $coord_y AND stone_color is null "; $intersecci贸n = self::getObjectFromDb( $sql ); if ($intersection == null) { throw new BgaUserException( self::_("Ya hay una piedra en esta intersecci贸n, no puedes jugar ah铆") ); } // Obtener el color del jugador $sql = "SELECT player_id, player_color FROM jugador WHERE player_id = $player_id "; $jugador = self::getNonEmptyObjectFromDb( $sql ); $color = ($jugador['color_jugador'] == 'ffffff' ? 'blanco' : 'negro'); // Actualizar la intersecci贸n con una piedra del color apropiado $intersection_id = $intersection['id']; $sql = "UPDATE intersecci贸n SET color_piedra = '$color' WHERE id = $intersection_id "; self::DbQuery($sql); // Notificar a todos los jugadores self::notifyAllPlayers( "stonePlayed", clienttranslate( '${player_name} dropped a stone on ${coord_x},${coord_y}' ), array( 'player_id' => $player_id, 'nombre_jugador' => self::getActivePlayerName(), 'coord_x' => $coord_x, 'coord_y' => $coord_y, 'color' => $color ) ); // Pasar al siguiente estado del juego $this->gamestate->nextState( "stonePlayed" ); }
Captura la notificaci贸n en .js->setupNotifications() y enl谩zala a una funci贸n javascript para que se ejecute cuando se reciba la notificaci贸n.
setupNotifications: function() { console.log( 'setup notifications subscriptions' ); dojo.subscribe( 'stonePlayed', this, "notif_stonePlayed" ); }
Implementar esta funci贸n en javascript para actualizar la intersecci贸n para mostrar la piedra, y registrarla dentro de la funci贸n setNotifications.
notif_stonePlayed: function( notif ) { console.log( '**** Notificaci贸n : stonePlayed' ); console.log( notif ); // Crear una piedra dojo.place( this.format_block('jstpl_stone', { stone_type:'stone_' + notif.args.color, x:notif.args.coord_x, y:notif.args.coord_y } ), $( 'intersection_' + notif.args.coord_x + '_' + notif.args.coord_y ); // Col贸calo en el panel del reproductor this.placeOnObject( $( 'piedra_' + notif.args.coord_x + '_' + notif.args.coord_y ), $( 'tablero_deljugador_' + notif.args.player_id ); // Animar un deslizamiento desde el panel del reproductor hasta la intersecci贸n dojo.style( 'piedra_' + notif.args.coord_x + '_' + notif.args.coord_y, 'zIndex', 1 ); var slide = this.slideToObject( $( 'stone_' + notif.args.coord_x + '_' + notif.args.coord_y ), $( 'intersection_' + notif.args.coord_x + '_' + notif.args.coord_y ), 1000 ); dojo.connect( slide, 'onEnd', this, dojo.hitch( this, function() { // Al final de la diapositiva, actualiza la intersecci贸n dojo.removeClass( 'intersection_' + notif.args.coord_x + '_' + notif.args.coord_y, 'no_stone' ); dojo.addClass( 'intersection_' + notif.args.coord_x + '_' + notif.args.coord_y, 'stone_' + notif.args.color ); dojo.removeClass( 'intersection_' + notif.args.coord_x + '_' + notif.args.coord_y, 'clickable' ); // Ahora podemos destruir la piedra ya que ahora es visible por el cambio de estilo de la intersecci贸n dojo.destroy( 'piedra_' + notif.args.coord_x + '_' + notif.args.coord_y ); })); slide.play(); },
Para que esta funci贸n funcione correctamente, tambi茅n es necesario:
- declarar una plantilla javascript de piedra en su archivo .tpl.
var jstpl_stone='<div class="gmk_stone ${stone_type}" id="stone_${x}_${y}"></div>';
- para definir los estilos css de las piedras
.gmk_intersection { width: 30px; height: 30px; posici贸n: relativa; background-image: url( 'img/stones.png' ); } .gmk_stone { anchura: 30px; altura: 30px; position: absolute; background-image: url( 'img/stones.png' ); } .no_stone { background-position: -60px 0px; } .stone_black { background-position: 0px 0px; } .stone_white { background-position: -30px 0px; }
Estos estilos se basan en una imagen PNG (con fondo transparente) tanto de las piedras blancas como de las negras, y posiciona el fondo adecuadamente para mostrar s贸lo la parte de la imagen de fondo que coincide con la piedra correspondiente (o el espacio transparente si no hay piedra). Este es el aspecto de la imagen:
El c铆rculo rojo se utiliza para resaltar las intersecciones en las que se puede dejar caer una piedra cuando el cursor del jugador pasa por encima de ellas (tambi茅n cambiamos el cursor por una mano). Para ello:
- definimos en el archivo css la clase css 'clickable'
.clickable { cursor: pointer; } .clickable:hover { background-position: -90px 0px; }
- En el .js, cuando entramos en el estado 'playerTurn', a帽adimos el estilo 'clickable' a las intersecciones donde no hay piedra
onEnteringState: function( stateName, args ) { console.log( 'Entrando en el estado: '+nombredelestado ); switch( stateName ) { case 'playerTurn': if( this.isCurrentPlayerActive() ) { var queueEntries = dojo.query( '.no_stone' ); for(var i=0; i<queueEntries.length; i++) { dojo.addClass( queueEntries[i], 'clickable' ); } } } },
Por 煤ltimo, aseg煤rate de modificar los colores por defecto de los jugadores a blanco y negro
$default_colors = array( "000000", "ffffff", );
El turno de juego b谩sico est谩 implementado: 隆ya puedes soltar algunas piedras!
Limpiar los estilos
Eliminar los ayudantes de visualizaci贸n css temporales: 隆se ve bien!
A帽adir algunos contadores en los paneles de los jugadores
Edita el .tpl para crear una plantilla para tus contadores.
var jstpl_player_board = '<div class="cp_board">\N-. <div id="stoneicon_p${id}" class="gmk_stoneicon gmk_stoneicon_${color}"></div><span id="stonecount_p${id}">0</span>\. </div>';
Edita .js->setup() para configurar los paneles de los jugadores con esta informaci贸n extra
// Configurar los tableros de los jugadores for( var player_id in gamedatas.players ) { var player = gamedatas.players[player_id]; // Configurar los tableros de los jugadores si es necesario var player_board_div = $('player_board_'+player_id); dojo.place( this.format_block('jstpl_player_board', player ), player_board_div ); }
A帽ade algunos estilos en tu .css
.gmk_stoneicon { width: 14px; height: 14px; display: inline-block; position: relative; background-repeat: no-repeat; background-image: url( 'img/stone_icons.png'); background-position: -28px 0px; margin-top: 4px; margin-right: 3px; } .gmk_stoneicon_000000 { background-position:0px 0px } .gmk_stoneicon_ffffff { background-position:-14px 0px } .cp_board { clear: both; }
En tu .game.php, crea una funci贸n que devuelva los contadores del juego
/* getGameCounters: Re煤ne todos los contadores relevantes sobre la situaci贸n actual del juego (visibles por el jugador actual). */ function getGameCounters($player_id) { $sql = " SELECT concat('stonecount_p', cast(p.player_id as char)) counter_name, case when p.player_color = 'white' then 180 - count(id) else 181 - count(id) end counter_value FROM (select player_id, case when player_color = 'ffffff' then 'white' else 'black' end player_color FROM player) p LEFT JOIN intersection i on i.stone_color = p.player_color GROUP BY p.player_color, p.player_id "; if ($player_id != null) { // Contadores privados de jugadores: concatenar la petici贸n SQL extra con UNION usando el par谩metro $player_id } return self::getNonEmptyCollectionFromDB( $sql ); }
Devuelve los contadores de tu juego en tu.game.php->getAllDatas()
// Contadores $resultado['contadores'] = $this->getGameCounters($current_player_id);
Y pasarlos en cualquier notificaci贸n que necesite actualizarlos
// Notificar a todos los jugadores self::notifyAllPlayers( "stonePlayed", clienttranslate( '${player_name} dropped a stone ${coordinates}' ), array( 'player_id' => $player_id, 'player_name' => self::getActivePlayerName(), 'coord_x' => $coord_x, 'coord_y' => $coord_y, 'color' => $color, 'counters' => $this->getGameCounters(self::getCurrentPlayerId()) ) );
Finalmente, en su llamada a la funci贸n .js->setup() y al manejador de notificaciones respectivamente
this.updateCounters(gamedatas.counters);
y
this.updateCounters(notif.args.counters);
隆Ahora tienes un contador que funciona!
Implementar reglas y condiciones de fin de partida
Implementa reglas espec铆ficas para el juego. Por ejemplo en Gomoku, el negro juega primero. As铆 que en .game.php->setupNewGame(), al final de la configuraci贸n haz que el jugador negro est茅 activo:
// El negro juega primero $sql = "SELECT player_id, player_name FROM player WHERE player_color = '000000' "; $jugador_negro = self::getNonEmptyObjectFromDb( $sql ); $this->gamestate->changeActivePlayer( $black_player['player_id'] );
Implementar la regla para calcular la progresi贸n del juego en .game.php->getGameProgression(). Para Gomoku usaremos la tasa de intersecciones ocupadas sobre el n煤mero total de intersecciones. Esto a menudo ser谩 salvajemente inexacto ya que el juego puede terminar bastante r谩pido, pero es lo mejor que podemos hacer (el juego puede arrastrarse hasta un punto muerto con todas las intersecciones ocupadas y sin ganador).
function getGameProgression() { // Calcula y devuelve la progresi贸n del juego // N煤mero de piedras colocadas en el goban sobre el n煤mero total de intersecciones * 100 $sql = " SELECT round(100 * count(id) / (19*19) ) as value from intersection WHERE stone_color is not null "; $counter = self::getNonEmptyObjectFromDB( $sql ); return $counter['valor']; }
Implementar la detecci贸n del final de la partida y actualizar la puntuaci贸n seg煤n qui茅n sea el ganador. Es m谩s f谩cil comprobar una victoria directamente despu茅s de establecer la piedra, as铆 que:
- declara una variable global 'end_of_game' en .game.php->__construct()
self::initGameStateLabels( array( "end_of_game" => 10, ) );
- init esa variable global a 0 en .game.php->setupNewGame()
self::setGameStateInitialValue( 'end_of_game', 0 );
- a帽adir el c贸digo apropiado en .game.php antes de pasar al siguiente estado, utilizando una funci贸n checkForWin() implementada por separado para mayor claridad. Si el juego ha sido ganado, establecemos la puntuaci贸n, enviamos una notificaci贸n de actualizaci贸n de la puntuaci贸n al lado del cliente, y establecemos la variable global 'end_of_game' a 1 como una bandera que se帽ala que el juego ha terminado.
// Comprueba si se ha cumplido el final de la partida if ($this->checkForWin( $coord_x, $coord_y, $color )) { // Establece la puntuaci贸n del jugador activo en 1 (es el ganador) $sql = "UPDATE player SET player_score = 1 WHERE player_id = $player_id"; self::DbQuery($sql); // Notificar la puntuaci贸n final $this->notifyAllPlayers( "finalScore", clienttranslate( '隆${nombre_del_jugador} gana la partida!' ), array( "nombre_jugador" => self::getActivePlayerName(), "player_id" => $player_id, "score_delta" => 1, ) ); // Establecer la bandera de la variable global para pasar la informaci贸n de que el juego ha terminado self::setGameStateValue('end_of_game', 1); // Mensaje de fin de partida $this->notifyAllPlayers( "mensaje", clienttranslate('隆Gracias por jugar!'), array( ) ); }
- Luego en la funci贸n gomoku->stCheckEndOfGame() que se llama cuando tu m谩quina de estados pasa al estado 'checkEndOfGame', comprueba esta variable y otras posibles condiciones de 'fin de juego' (sorteo).
funci贸n stCheckEndOfGame() { self::trace( "stCheckEndOfGame" ); $transition = "notEndedYet"; // Si no hay m谩s intersecciones libres, el juego termina $sql = "SELECT id, coord_x, coord_y, stone_color FROM intersection WHERE stone_color is null"; $free = self::getCollectionFromDb( $sql ); if (count($free) == 0) { $transition = "gameEnded"; } // Si se ha establecido la bandera de "fin de juego", termina el juego if (self::getGameStateValue('end_of_game') == 1) { $transition = "gameEnded"; } $this->gamestate->nextState( $transition ); }
- Captura la notificaci贸n de puntuaci贸n en el lado del cliente en .js->setupNotifications(). Se aconseja establecer un peque帽o retardo despu茅s de eso para que el popup de fin de partida no se muestre demasiado r谩pido.
dojo.subscribe( 'finalScore', this, "notif_finalScore" ); this.notifqueue.setSynchronous( 'finalScore', 1500 );
- Implementar la funci贸n declarada para manejar la notificaci贸n.
notif_finalScore: function( notif ) { console.log( '**** Notificaci贸n : finalScore' ); console.log( notif ); // Actualizar la puntuaci贸n this.scoreCtrl[ notif.args.player_id ].incValue( notif.args.score_delta ); },
Prueba todo a fondo... 隆ya has terminado!