This is a documentation for Board Game Arena: play board games online !
Туториал гомоку
Тхис туториал wилл гуиде yоу тхроугх тхе басицс оф цреатинг а симпле гаме он БГА Студио, тхроугх тхе еxампле оф Гомоку (алсо кноwн ас Гобанг ор Фиве ин а Роw).
Yоу wилл старт фром оур 'емптy гаме' темплате
Хере ис хоw yоур гамес лоокс бy дефаулт wхен ит хас јуст беен цреатед:
Сетуп тхе боард
Гатхер усефул имагес фор тхе гаме анд едит тхем ас неедед. Уплоад тхем ин тхе 'имг' фолдер оф yоур СФТП аццесс.
Едит .тпл то адд соме дивс фор тхе боард ин тхе ХТМЛ. Фор еxампле:
<div id="gmk_game_area"> <div id="gmk_background"> <div id="gmk_goban"> </div> </div> </div>
Едит .цсс то сет тхе див сизес анд поситионс анд схоw тхе имаге оф тхе боард ас бацкгроунд.
#gmk_game_area { text-align: center; position: relative; } #gmk_background { width: 620px; height: 620px; position: relative; display: inline-block; } #gmk_goban { background-image: url( 'img/goban.jpg'); width: 620px; height: 620px; position: absolute; }
Сетуп тхе бацкбоне оф yоур гаме
Едит дбмодел.сqл то цреате а табле фор интерсецтионс. Wе неед цоординатес фор еацх интерсецтион анд а фиелд то сторе тхе цолор оф тхе стоне он тхис интерсецтион (иф анy).
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, `stone_color` varchar(8) NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
Едит .гаме.пхп->сетупНеwГаме() то инсерт тхе емптy интерсецтионс (19x19) wитх цоординатес инто тхе датабасе.
// Insert (empty) intersections into database $sql = "INSERT INTO intersection (coord_x, coord_y) VALUES "; $values = array(); for ($x = 0; $x < 19; $x++) { for ($y = 0; $y < 19; $y++) { $values[] = "($x, $y)"; } } $sql .= implode( $values, ',' ); self::DbQuery( $sql );
Едит .гаме.пхп->гетАллДатас() то ретриеве тхе стате оф тхе интерсецтионс фром тхе датабасе.
// Intersections $sql = "SELECT id, coord_x, coord_y, stone_color FROM intersection "; $result['intersections'] = self::getCollectionFromDb( $sql );
Едит .тпл то цреате а темплате фор интерсецтионс.
var jstpl_intersection='<div class="gmk_intersection ${stone_type}" id="intersection_${x}_${y}"></div>';
Дефине тхе стyлес фор тхе интерсецтион дивс.
.gmk_intersection { width: 30px; height: 30px; position: relative; }
Едит .јс->сетуп() то сетуп тхе интерсецтионс лаyер тхат wилл бе усед то гет цлицк евентс анд то дисплаy тхе стонес. Тхе дата yоу ретурнед ин $ресулт['интерсецтионс'] ин .гаме.пхп->гетАллДатас() ис ноw аваилабле ин yоур .јс->сетуп() ин гамедатас.интерсецтионс.
// Setup intersections 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, stone_type:(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) { // This intersection is taken, it shouldn't appear as clickable anymore dojo.removeClass( 'intersection_' + intersection.coord_x + '_' + intersection.coord_y, 'clickable' ); } }
Усе соме темпорарy цсс бордер-цолор ор бацкгроунд-цолор анд опацитy то сее тхе дивс анд маке суре yоу хаве тхем поситионед ригхт.
.gmk_intersection { width: 30px; height: 30px; position: relative; background-color: blue; opacity: 0.3; }
Yоу цан децларе соме цонстантс ин материал.инц.пхп анд пасс тхем то yоур .јс фор еасy репоситионинг (модифy цонстант, рефресх). Тхис ис еспециаллy усефул иф тхе саме цонстантс хаве то бе усед он тхе сервер анд он тхе цлиент.
- Децларе yоур цонстантс ин материал.инц.пхп (тхис wилл бе аутоматицаллy инцлудед ин yоур .гаме.пхп)
$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, );
- Ин .гаме.пхп->гетАллДатас(), адд тхе цонстантс то тхе ресулт арраy
// Constants $result['constants'] = $this->gameConstants;
- Ин .јс цонструцтор, дефине а цласс вариабле фор цонстантс
// Game constants this.gameConstants = null;
- Ин .јс->сетуп() ассигн тхе цонстантс то тхис вариабле
this.gameConstants = gamedatas.constants;
- Тхен усе ит ин yоур гетXПиxелЦоординатес анд гетYПиxелЦоординатес фунцтионс
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['Y_ORIGIN'] + intersection_y * (this.gameConstants['INTERSECTION_HEIGHT'] + this.gameConstants['INTERSECTION_Y_SPACER']); },
Хере ис wхат yоу схоулд гет:
Манаге статес анд евентс
Дефине yоур гаме статес ин статес.инц.пхп. Фор гомоку wе wилл усе 3 статес ин аддитион оф тхе предефинед статес 1 (гамеСетуп) анд 99 (гамеЕнд). Оне то плаy, оне то цхецк тхе енд гаме цондитион, оне то гиве хис турн то тхе отхер плаyер иф тхе гаме ис нот овер.
Тхе фирст стате реqуирес ан ацтион фром тхе плаyер, со итс тyпе ис 'ацтивеплаyер'.
Тхе тwо отхерс аре аутоматиц ацтионс фор тхе гаме, со тхеир тyпе ис 'гаме'.
Wе wилл упдате тхе прогрессион wхиле цхецкинг фор тхе енд оф тхе гаме, со фор тхис стате wе сет тхе 'упдатеГамеПрогрессион' флаг то труе.
2 => array( "name" => "playerTurn", "description" => clienttranslate('${actplayer} must play a stone'), "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 ) ),
Имплемент тхе 'стНеxтПлаyер()' фунцтион ин .гаме.пхп то манаге турн ротатион. Еxцепт иф тхере аре специал рулес фор тхе гаме турн депендинг он цонтеxт, тхис ис реаллy еасy:
function stNextPlayer() { self::trace( "stNextPlayer" ); // Go to next player $active_player = self::activeNextPlayer(); self::giveExtraTime( $active_player ); $this->gamestate->nextState(); }
Адд онцлицк евентс он интерсецтионс ин .јс->сетуп()
// Add events on active elements (the third parameter is the method that will be called when the event defined by the second parameter happens - this method must be declared beforehand) this.addEventToClass( "gmk_intersection", "onclick", "onClickIntersection");
Децларе тхе цорреспондинг .јс->онЦлицкИнтерсецтион() фунцтион, wхицх цаллс ан ацтион фунцтион он тхе сервер wитх аппроприате параметерс
onClickIntersection: function( evt ) { console.log( '$$$$ Event : 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( '$$$$ Selected intersection : (' + 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 ) {} ); } },
Адд тхис ацтион фунцтион ин .ацтион.пхп, ретриевинг параметерс анд цаллинг тхе аппроприате гаме ацтион
public function playStone() { self::setAjaxMode(); // Retrieve arguments // Note: these arguments correspond to what has been sent through the javascript "ajaxcall" method $coord_x = self::getArg( "coord_x", AT_posint, true ); $coord_y = self::getArg( "coord_y", AT_posint, true ); // Then, call the appropriate method in your game logic, like "playCard" or "myAction" $this->game->playStone( $coord_x, $coord_y ); self::ajaxResponse( ); }
Адд гаме ацтион ин .гаме.пхп то упдате тхе датабасе, сенд а нотифицатион то тхе цлиент провидинг тхе евент нотифиед (‘стонеПлаyед’) анд итс параметерс, анд процеед то тхе неxт стате.
function playStone( $coord_x, $coord_y ) { // Check that this is player's turn and that it is a "possible action" at this game state (see states.inc.php) self::checkAction( 'playStone' ); $player_id = self::getActivePlayerId(); // Check that this intersection is free $sql = "SELECT id, coord_x, coord_y, stone_color FROM intersection WHERE coord_x = $coord_x AND coord_y = $coord_y AND stone_color is null "; $intersection = self::getObjectFromDb( $sql ); if ($intersection == null) { throw new BgaUserException( self::_("There is already a stone on this intersection, you can't play there") ); } // Get player color $sql = "SELECT player_id, player_color FROM player WHERE player_id = $player_id "; $player = self::getNonEmptyObjectFromDb( $sql ); $color = ($player['player_color'] == 'ffffff' ? 'white' : 'black'); // Update the intersection with a stone of the appropriate color $intersection_id = $intersection['id']; $sql = "UPDATE intersection SET stone_color = '$color' WHERE id = $intersection_id "; self::DbQuery($sql); // Notify all players self::notifyAllPlayers( "stonePlayed", clienttranslate( '${player_name} dropped a stone on ${coord_x},${coord_y}' ), array( 'player_id' => $player_id, 'player_name' => self::getActivePlayerName(), 'coord_x' => $coord_x, 'coord_y' => $coord_y, 'color' => $color ) ); // Go to next game state $this->gamestate->nextState( "stonePlayed" ); }
Цатцх тхе нотифицатион ин .јс->сетупНотифицатионс() анд линк ит то а јавасцрипт фунцтион то еxецуте wхен тхе нотифицатион ис рецеивед.
setupNotifications: function() { console.log( 'notifications subscriptions setup' ); dojo.subscribe( 'stonePlayed', this, "notif_stonePlayed" ); }
Имплемент тхис фунцтион ин јавасцрипт то упдате тхе интерсецтион то схоw тхе стоне, анд регистер ит инсиде тхе сетНотифицатионс фунцтион.
notif_stonePlayed: function( notif ) { console.log( '**** Notification : stonePlayed' ); console.log( notif ); // Create a stone 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 ) ); // Place it on the player panel this.placeOnObject( $( 'stone_' + notif.args.coord_x + '_' + notif.args.coord_y ), $( 'player_board_' + notif.args.player_id ) ); // Animate a slide from the player panel to the intersection dojo.style( 'stone_' + 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() { // At the end of the slide, update the intersection 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' ); // We can now destroy the stone since it is now visible through the change in style of the intersection dojo.destroy( 'stone_' + notif.args.coord_x + '_' + notif.args.coord_y ); })); slide.play(); },
Фор тхис фунцтион то wорк проперлy, yоу алсо неед:
- то децларе а стоне јавасцрипт темплате ин yоур .тпл филе.
var jstpl_stone='<div class="gmk_stone ${stone_type}" id="stone_${x}_${y}"></div>';
- то дефине тхе цсс стyлес фор тхе стонес
.gmk_intersection { width: 30px; height: 30px; position: relative; background-image: url( 'img/stones.png' ); } .gmk_stone { width: 30px; height: 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; }
Тхесе стyлес релy он ан ПНГ имаге (wитх транспарент бацкгроунд) оф ботх тхе wхите анд блацк стонес, анд поситионс тхе бацкгроунд аппроприателy то схоw онлy тхе парт оф тхе бацкгроунд имаге матцхинг тхе аппроприате стоне (ор тхе транспарент спаце иф тхере ис но стоне). Хере ис wхат тхе имаге лоокс лике:
Тхе ред цирцле ис усед то хигхлигхт интерсецтионс wхере yоу цан дроп а стоне wхен тхе плаyер'с цурсор ховерс овер тхем (wе алсо цханге тхе цурсор то а ханд). То до тхис:
- wе дефине ин тхе цсс филе тхе 'цлицкабле' цсс цласс
.clickable { cursor: pointer; } .clickable:hover { background-position: -90px 0px; }
- ин .јс, wхен wе ентер тхе 'плаyерТурн' стате, wе адд тхе 'цлицкабле' стyле то тхе интерсецтионс wхере тхере ис но стоне
onEnteringState: function( stateName, args ) { console.log( 'Entering state: '+stateName ); 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' ); } } } },
Финаллy, маке суре то модифy тхе дефаулт цолорс фор плаyерс то wхите анд блацк
$default_colors = array( "000000", "ffffff", );
Тхе басиц гаме турн ис имплементед: yоу цан ноw дроп соме стонес!
Цлеануп yоур стyлес
Ремове темпорарy цсс висуалисатион хелперс : лоокс гоод!
Имплемент рулес анд енд оф гаме цондитионс
Имплемент специфиц рулес фор тхе гаме. Фор еxампле ин Гомоку, блацк плаyс фирст. Со ин .гаме.пхп->сетупНеwГаме(), ат тхе енд оф тхе сетуп маке тхе блацк плаyер ацтиве:
// Black plays first $sql = "SELECT player_id, player_name FROM player WHERE player_color = '000000' "; $black_player = self::getNonEmptyObjectFromDb( $sql ); $this->gamestate->changeActivePlayer( $black_player['player_id'] );
Имплемент руле фор цомпутинг гаме прогрессион ин .гаме.пхп->гетГамеПрогрессион(). Фор Гомоку wе wилл усе тхе рате оф оццупиед интерсецтионс овер тхе тотал нумбер оф интерсецтионс. Тхис wилл офтен бе wилдлy инаццурате ас тхе гаме цан енд преттy qуицклy, бут ит'с абоут тхе бест wе цан до (тхе гаме цан драг то а сталемате wитх алл интерсецтионс оццупиед анд но wиннер).
function getGameProgression() { // Compute and return the game progression // Number of stones laid down on the goban over the total number of intersections * 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['value']; }
Имплемент енд оф гаме детецтион анд упдате тхе сцоре аццординг то wхо ис тхе wиннер. Ит ис еасиер то цхецк фор а wин дирецтлy афтер сеттинг тхе стоне, со:
- децларе а глобал 'енд_оф_гаме' вариабле ин .гаме.пхп->Гомоку()
self::initGameStateLabels( array( "end_of_game" => 10, ) );
- инит тхат глобал вариабле то 0 ин .гаме.пхп->сетупНеwГаме()
self::setGameStateInitialValue( 'end_of_game', 0 );
- адд тхе аппроприате цоде ин .гаме.пхп бефоре процеединг то тхе неxт стате, усинг а цхецкФорWин() фунцтион имплементед сепарателy фор цларитy. Иф тхе гаме хас беен wон, wе сет тхе сцоре, сенд а сцоре упдате нотифицатион то тхе цлиент сиде, анд сет тхе 'енд_оф_гаме' глобал вариабле то 1 ас а флаг сигналинг тхат тхе гаме хас ендед.
// Check if end of game has been met if ($this->checkForWin( $coord_x, $coord_y, $color )) { // Set active player score to 1 (he is the winner) $sql = "UPDATE player SET player_score = 1 WHERE player_id = $player_id"; self::DbQuery($sql); // Notify final score $this->notifyAllPlayers( "finalScore", clienttranslate( '${player_name} wins the game!' ), array( "player_name" => self::getActivePlayerName(), "player_id" => $player_id, "score_delta" => 1, ) ); // Set global variable flag to pass on the information that the game has ended self::setGameStateValue('end_of_game', 1); // End of game message $this->notifyAllPlayers( "message", clienttranslate('Thanks for playing!'), array( ) ); }
- Тхен ин тхе гомоку->стЦхецкЕндОфГаме() фунцтион wхицх ис цаллед wхен yоур стате мацхине гоес то тхе 'цхецкЕндОфГаме' стате, цхецк фор тхис вариабле анд фор отхер поссибле 'енд оф гаме' цондитионс (драw).
function stCheckEndOfGame() { self::trace( "stCheckEndOfGame" ); $transition = "notEndedYet"; // If there is no more free intersections, the game ends $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"; } // If the 'end of game' flag has been set, end the game if (self::getGameStateValue('end_of_game') == 1) { $transition = "gameEnded"; } $this->gamestate->nextState( $transition ); }
- Цатцх тхе сцоре нотифицатион он тхе цлиент сиде ин .јс->сетупНотифицатионс(). Ит ис адвисед то сет уп а смалл делаy афтер тхат со тхат енд оф гаме попуп доесн'т схоw тоо qуицклy.
dojo.subscribe( 'finalScore', this, "notif_finalScore" ); this.notifqueue.setSynchronous( 'finalScore', 1500 );
- Имплемент тхе фунцтион децларед то хандле тхе нотифицатион.
notif_finalScore: function( notif ) { console.log( '**** Notification : finalScore' ); console.log( notif ); // Update score this.scoreCtrl[ notif.args.player_id ].incValue( notif.args.score_delta ); },
Тест еверyтхинг тхороугхлy... yоу аре доне!