Important note: BGA Type Safe Template is a project created by a developer that is not part of the BGA team. This is currently not officially supported by BGA.
This page was generated from BGA TS Template: Reversi.md. It is recommended to view this document from the source file. All links will redirect to source documentation, and images will only be included via link.
TS Template: Reversi Tutorial
Project Files: BGA-TS-Template-Reversi
This page is a beginner friendly tutorial which mimics the official Tutorial Reversi page, but is more approachable and uses the BGA Type Safe Template. If you are not a new Board Game Arena developer, you may want to follow along with the BGA Type Safe Template: Getting Started guide for a more concise overview of the template.
All steps are recorded as a git branch so you can track the exact changes that were made, and view the project files from any point in the tutorial.
Steps 0, 1, 2, 3
Go to TS Template: Tutorial for the first four steps of the tutorial. The first four steps for any project are the same. To avoid redundancy, they are not repeated here.
Step 3.5 - From here on out
At this point, your are ready to start developing your game. You can choose to follow any of the tutorials or guides on the BGA Studio Documentation to continue.
From this point in the tutorial and on:
- It will be assumed that files are built and synced before running the game in the BGA studio.
- It will be assumed that you do a hard refresh of the client page after making changes.
- It will be assumed that you are using all of the default settings for initializing the typescript template. Adjustments can be made as needed and all sections have links to original file documentation for reference.
- Most steps will have specific instructions, but you can always adjust at your own discretion.
Make sure the following game information is set for Reversi:
// gameinfos.jsonc
{
"players": [ 2 ],
"bgg_id": 2389,
"player_colors": [ "cbcbcb", "363636" ],
"favorite_colors_support": false
}
Step 4 - Game Board
- Download the following board image for Reversi, and place it in the
img
folder of your project. It should be namedboard.jpg
to match the SCSS background-image in the next step.
https://github.com/NevinAF/bga-ts-template/tree/main/docs/tutorials/img/reversi/assets/board.jpg
Add the board to the DOM by adding the following Smarty template code to the
yourgamename_yourgamename.tpl
file:<div id="board"> <!-- BEGIN square --> <div id="square_{X}_{Y}" class="square" style="left: {LEFT}px; top: {TOP}px;"></div> <!-- END square --> </div>
The
<!-- BEGIN <block> -->
and<!-- END <block> -->
are Smarty template tags that let you programmatically generate HTML as you will use in a following step. See X_X.tpl for more information about the template file for BGA games.Add the following SCSS to the
yourgamename.scss
file to style the board:#board { // Setup Image width: 536px; height: 528px; background-image: url('img/board.jpg'); // Center the board margin: auto; left: 0; top: 0; right: 0; bottom: 0; // Tokens and squares are absolutely placed relative to the board position: relative; } .square { // Size of a square width: 62px; height: 62px; // Position relative to the board, not affected by other elements. position: absolute; // Placeholder for validating the positions. background-color: red; }
See X.css for more information about the style sheet for BGA games.
https://github.com/NevinAF/bga-ts-template/tree/main/docs/tutorials/img/reversi/step4-3.png
Reloading the game (with a hard refresh).Add the following code to the
yourgamename.view.php
file.// function build_page( $viewArgs ) { // ... /*********** Place your code below: ************/ // States that we should start inserting at the 'square' block // <yourgamename>_<yourgamename> should be replaced with your game name: tstemplatereversi_tstemplatereversi $this->page->begin_block( "<yourgamename>_<yourgamename>", "square" ); $hor_scale = 64.8; // Constant for square width $ver_scale = 64.4; // Constant for square height for( $x=1; $x<=8; $x++ ) // Loop the 8 columns.. { for( $y=1; $y<=8; $y++ ) // Loop the 8 rows.. { // Inserts the code found at the square block based on the variables. $this->page->insert_block( "square", array( 'X' => $x, 'Y' => $y, 'LEFT' => round( ($x-1)*$hor_scale+10 ), 'TOP' => round( ($y-1)*$ver_scale+7 ) ) ); } }
See X.view.php for more information about the view file for BGA games.
https://github.com/NevinAF/bga-ts-template/tree/main/docs/tutorials/img/reversi/step4-4.png
Reloading the game (with a hard refresh).
Step 5 - The Tokens
Add the following token image to the
img
folder of your project. It should be namedtokens.png
to match the SCSS background-image in the next step.https://github.com/NevinAF/bga-ts-template/tree/main/docs/tutorials/img/reversi/assets/tokens.png
Add a template variable to the
yourgamename_yourgamename.tpl
file to represent the tokens:<div id="board"> ... </div> <script type="text/javascript"> var jstpl_token='<div class="token tokencolor_${color}" id="token_${x_y}"></div>'; </script>
Note that this does not have any information that needs to be populated by the server, so this could be directly defined in the TypeScript file.
Add the following SCSS to the
yourgamename.scss
file to style the tokens:// Same as square, but matching the token size/image. .token { width: 56px; height: 56px; position: absolute; background-image: url('img/tokens.png'); } // For clarity, but doesn't actually do anything. .tokencolor_cbcbcb { background-position: 0px 0px; } // Translate the background the exact size of the white token, so the black one shows instead. .tokencolor_363636 { background-position: -56px 0px; }
Add a TypeScript function to the
yourgamename.ts
file to place the tokens on the board. You can hover over any function/property in the TypeScript file to see the documentation and usage./////////////////////////////////////////////////// //// Utility methods /** Adds a token matching the given player to the board at the specified location. */ addTokenOnBoard( x: number, y: number, player_id: number ) { let player = this.gamedatas.players[ player_id ]; if (!player) throw new Error( 'Unknown player id: ' + player_id ); dojo.place( this.format_block( 'jstpl_token', { x_y: `${x}_${y}`, color: player.color } ) , 'board' ); this.placeOnObject( `token_${x}_${y}`, `overall_player_board_${player_id}` ); this.slideToObject( `token_${x}_${y}`, `square_${x}_${y}` ).play(); }
See X.js for more information about the script file for BGA games.
For a placeholder, test your tokens by manually placing some in the setup function. you can also remove the boilerplate code.:
/////////////////////////////////////////////////// //// Game setup setup(gamedatas: Gamedatas): void { console.log( "Starting game setup" ); this.addTokenOnBoard( 2, 2, this.player_id ); this.addTokenOnBoard( 6, 3, this.player_id ); this.setupNotifications(); // <-- Keep this line }
https://github.com/NevinAF/bga-ts-template/tree/main/docs/tutorials/img/reversi/step5-4.png
Reloading the game (with a hard refresh).
Step 6 - The Database and Initial Setup
Create a database for your board in the
dbmodel.sql
file:CREATE TABLE IF NOT EXISTS `board` ( `board_x` smallint(5) unsigned NOT NULL, `board_y` smallint(5) unsigned NOT NULL, `board_player` int(10) unsigned DEFAULT NULL, PRIMARY KEY (`board_x`,`board_y`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Pay special attention to the backtick
`
character vs. the single quote'
when working with SQL. You can quit and restart your game to see if the file is correct.See dbmodel.sql for more information about the database file for BGA games.
Setup the board in your
yourgamename.game.php
file. This is initializing the data in the database and setting the four discs in the center of the board:protected function setupNewGame( $players, $options = array() ) { //... $sql .= implode( ',', $values ); self::DbQuery( $sql ); // self::reattributeColorsBasedOnPreferences( $players, $gameinfos['player_colors'] ); self::reloadPlayersBasicInfos(); /************ Start the game initialization *****/ $sql = "INSERT INTO board (board_x,board_y,board_player) VALUES "; $sql_values = array(); list( $whiteplayer_id, $blackplayer_id ) = array_keys( $players ); for( $x=1; $x<=8; $x++ ) { for( $y=1; $y<=8; $y++ ) { // Initial positions of white player if( ($x==4 && $y==4) || ($x==5 && $y==5) ) $token_value = "'$whiteplayer_id'"; // Initial positions of black player else if( ($x==4 && $y==5) || ($x==5 && $y==4) ) $token_value = "'$blackplayer_id'"; // Not a starting position else $token_value = "NULL"; $sql_values[] = "('$x','$y',$token_value)"; } } $sql .= implode( ',', $sql_values ); self::DbQuery( $sql ); //... }
Now we need to send the board information to the client whenever the page is reloaded. In your
yourgamename.game.php
file, add the following code to thegetAllDatas
function:protected function getAllDatas() { //... $sql = "SELECT board_x x, board_y y, board_player player FROM board WHERE board_player IS NOT NULL"; $result['board'] = self::getObjectListFromDB( $sql ); //... }
Add the board data to the
Gamedatas
interface in theyourgamename.d.ts
file:interface Gamedatas { board: { x: number, y: number, player: number }[]; }
Add the following to the
yourgamename.ts
file place the discs at the start of a page load:setup(gamedatas: Gamedatas): void { console.log( "Starting game setup" ); // Place the tokens on the board for( let i in gamedatas.board ) { let square = gamedatas.board[i]; if( square?.player ) // If square is defined and has a player this.addTokenOnBoard( square.x, square.y, square.player ); } this.setupNotifications(); // <-- Keep this line }
https://github.com/NevinAF/bga-ts-template/tree/main/docs/tutorials/img/reversi/step6-5.png
Reloading the game (with a hard refresh).
Step 7 - Game States
Replace the gamestates.jsonc file with the following:
{ "$schema": "../../node_modules/bga-ts-template/schema/gamestates.schema.json", // The initial state. Please do not modify. "1": { "name": "gameSetup", "description": "", "type": "manager", "action": "stGameSetup", "transitions": { "": 10 } }, "10": { "name": "playerTurn", "description": "${actplayer} must play a disc", "descriptionmyturn": "${you} must play a disc", "type": "activeplayer", "args": "argPlayerTurn", "argsType": { "possibleMoves": "boolean[][]" }, "possibleactions": { "playDisc": [ { "name": "x", "type": "AT_int" }, { "name": "y", "type": "AT_int" } ] }, "transitions": { "playDisc": 11, "zombiePass": 11 } }, "11": { "name": "nextPlayer", "type": "game", "action": "stNextPlayer", "updateGameProgression": true, "transitions": { "nextTurn": 10, "cantPlay": 11, "endGame": 99 } }, // Final state. // Please do not modify (and do not overload action/args methods}. "99": { "name": "gameEnd", "description": "End of game", "type": "manager", "action": "stGameEnd", "args": "argGameEnd", "argsType": "object" } }
There is a lot of information in this file. Most information can be found in the hover-over tooltips, but the Game States page has more information about the concepts.
This will automatically be converted to
gamestates.inc.php
,yourgamename.action.php
, andbuild/gamestates.d.ts
when you runnpm run build
.Add placeholder functions to remove
Undefined method
errors. Add the following to youryourgamename.action.php
file. Note that this replaces the large section of comments in the file describing the player actions, state arguments, and state actions.// Player actions function playDisc( int $x, int $y ) { /* TODO */ } // Game state arguments function argPlayerTurn() { /* TODO */ } // Game state actions function stNextPlayer() { /* TODO */ }
Fix the
dummmy
state errors in youryourgamename.ts
file. These are added as a placeholder and now cause issues because there is no gamestate with the namedummmy
:onEnteringState(stateName: GameStateName, args: CurrentStateArgs): void { console.log( 'Entering state: ' + stateName ); } onLeavingState(stateName: GameStateName): void { console.log( 'Leaving state: ' + stateName ); } onUpdateActionButtons(stateName: GameStateName, args: AnyGameStateArgs | null): void { console.log( 'onUpdateActionButtons: ' + stateName, args ); }
You should no longer have any type issues in any of your files.
When you reload your game (this will cause current games to fail), your should see the banner change to {actplayer} must play a disc
or You must play a disc
. This will be the only visible change in the game at this point.
https://github.com/NevinAF/bga-ts-template/tree/main/docs/tutorials/img/reversi/step7-3.png
Reloading the game (with a hard refresh).
Step 8 - Game Rules and Possible Moves
There are a couple of variations on the rules for Reversi, but the most common is with outflanking rules
. You can see the official rules at World Othello. There rules have been implemented with the following utility functions.
Copy these functions into your
yourgamename.game.php
file:// Get the complete board with a double associative array function getBoard() { $sql = "SELECT board_x x, board_y y, board_player player FROM board"; return self::getDoubleKeyCollectionFromDB( $sql, true ); } // Get the list of possible moves (x => y => true) function getPossibleMoves( $player_id ) { $result = array(); $board = self::getBoard(); for( $x=1; $x<=8; $x++ ) { for( $y=1; $y<=8; $y++ ) { $returned = self::getTurnedOverDiscs( $x, $y, $player_id, $board ); if( count( $returned ) == 0 ) { // No discs returned => not a possible move } else { // Okay => set this coordinate to "true" if( ! isset( $result[$x] ) ) $result[$x] = array(); $result[$x][$y] = true; } } } return $result; } // Get the list of returned disc when "player" we play at this place ("x", "y"), // or a void array if no disc is returned (invalid move) function getTurnedOverDiscs( $x, $y, $player, $board ) { $turnedOverDiscs = array(); if( $board[ $x ][ $y ] === null ) // If there is already a disc on this place, this can't be a valid move { // For each directions... $directions = array( array( -1,-1 ), array( -1,0 ), array( -1, 1 ), array( 0, -1), array( 0,1 ), array( 1,-1), array( 1,0 ), array( 1, 1 ) ); foreach( $directions as $direction ) { // Starting from the square we want to place a disc... $current_x = $x; $current_y = $y; $bContinue = true; $mayBeTurnedOver = array(); while( $bContinue ) { // Go to the next square in this direction $current_x += $direction[0]; $current_y += $direction[1]; if( $current_x<1 || $current_x>8 || $current_y<1 || $current_y>8 ) $bContinue = false; // Out of the board => stop here for this direction else if( $board[ $current_x ][ $current_y ] === null ) $bContinue = false; // An empty square => stop here for this direction else if( $board[ $current_x ][ $current_y ] != $player ) { // There is a disc from our opponent on this square // => add it to the list of the "may be turned over", and continue on this direction $mayBeTurnedOver[] = array( 'x' => $current_x, 'y' => $current_y ); } else if( $board[ $current_x ][ $current_y ] == $player ) { // This is one of our disc if( count( $mayBeTurnedOver ) == 0 ) { // There is no disc to be turned over between our 2 discs => stop here for this direction $bContinue = false; } else { // We found some disc to be turned over between our 2 discs // => add them to the result and stop here for this direction $turnedOverDiscs = array_merge( $turnedOverDiscs, $mayBeTurnedOver ); $bContinue = false; } } } } } return $turnedOverDiscs; }
Add the list of possible moves to the
argPlayerTurn
function in youryourgamename.action.php
file:function argPlayerTurn() { $possibleMoves = self::getPossibleMoves( self::getActivePlayerId() ); return array( 'possibleMoves' => $possibleMoves ); }
Add a css class to represent the possible moves in your
yourgamename.scss
file:.possibleMove { background-color: rgba(255, 255, 255, 0.15); cursor: pointer; transition: 100ms; } .possibleMove:hover { background-color: rgba(255, 255, 255, 0.3); }
Add the possible moves to the board when entering a new state by adding the following to your
yourgamename.ts
file:// Utility methods /** Removes the 'possibleMove' class from all elements. */ clearPossibleMoves() { document.querySelectorAll('.possibleMove').forEach(element => { element.classList.remove('possibleMove'); }); } /** Updates the squares on the board matching the possible moves. */ updatePossibleMoves( possibleMoves: boolean[][] ) { this.clearPossibleMoves(); for( var x in possibleMoves ) { for( var y in possibleMoves[ x ] ) { let square = $(`square_${x}_${y}`); if( !square ) throw new Error( `Unknown square element: ${x}_${y}. Make sure the board grid was set up correctly in the tpl file.` ); square.classList.add('possibleMove'); } } this.addTooltipToClass( 'possibleMove', '', _('Place a disc here') ); } // Game & client states onEnteringState(stateName: GameStateName, args: CurrentStateArgs): void { console.log( 'Entering state: '+stateName ); switch( stateName ) { case 'playerTurn': this.updatePossibleMoves( args.args!.possibleMoves ); break; } }
https://github.com/NevinAF/bga-ts-template/tree/main/docs/tutorials/img/reversi/step8-4.png
Reloading the game (with a hard refresh).
The game is still not playable, but you can now see the possible moves for the first turn.
Step 9 - Player Actions
Fill in the
playDisc
function in youryourgamename.action.php
file:function playDisc( int $x, int $y ) { // Check that this player is active and that this action is possible at this moment self::checkAction( 'playDisc' ); // Now, check if this is a possible move $board = self::getBoard(); $player_id = self::getActivePlayerId(); $turnedOverDiscs = self::getTurnedOverDiscs( $x, $y, $player_id, $board ); if( count( $turnedOverDiscs ) <= 0 ) throw new BgaSystemException( "Impossible move" ); // This move is possible! // Let's place a disc at x,y and return all "$returned" discs to the active player $sql = "UPDATE board SET board_player='$player_id' WHERE ( board_x, board_y) IN ( "; foreach( $turnedOverDiscs as $turnedOver ) $sql .= "('".$turnedOver['x']."','".$turnedOver['y']."'),"; $sql .= "('$x','$y') ) "; self::DbQuery( $sql ); // Update scores according to the number of disc on board $sql = "UPDATE player SET player_score = ( SELECT COUNT( board_x ) FROM board WHERE board_player=player_id )"; self::DbQuery( $sql ); // Then, go to the next state $this->gamestate->nextState( 'playDisc' ); }
Fill in the
stNextPlayer
function in youryourgamename.action.php
file:function stNextPlayer() { // Active next player $player_id = self::activeNextPlayer(); // Check if both player has at least 1 discs, and if there are free squares to play $sql = "SELECT board_player, COUNT( board_x ) FROM board GROUP BY board_player"; $player_to_discs = self::getCollectionFromDb( $sql, true ); if( ! isset( $player_to_discs[ null ] ) ) { // Index 0 has not been set => there's no more free place on the board ! // => end of the game $this->gamestate->nextState( 'endGame' ); return ; } else if( ! isset( $player_to_discs[ $player_id ] ) ) { // Active player has no more disc on the board => he looses immediately $this->gamestate->nextState( 'endGame' ); return ; } // Can this player play? $possibleMoves = self::getPossibleMoves( $player_id ); if( count( $possibleMoves ) == 0 ) { // This player can't play // Can his opponent play ? $opponent_id = self::getUniqueValueFromDb( "SELECT player_id FROM player WHERE player_id!='$player_id' " ); if( count( self::getPossibleMoves( $opponent_id ) ) == 0 ) { // Nobody can move => end of the game $this->gamestate->nextState( 'endGame' ); } else { // => pass his turn $this->gamestate->nextState( 'cantPlay' ); } } else { // This player can play. Give him some extra time self::giveExtraTime( $player_id ); $this->gamestate->nextState( 'nextTurn' ); } }
Connect an
onclick
function to all of the squares on the board:```typescript setup(gamedatas: Gamedatas): void { // … dojo.query( ‘.square’ ).connect( ‘onclick’, this, ‘onPlayDisc’ ); // … }
Add the
onPlayDisc
function to youryourgamename.ts
file:// Player's action /** Called when a square is clicked, check if it is a possible move and send the action to the server. */ onPlayDisc( evt: Event ) { // Stop this event propagation evt.preventDefault(); if (!(evt.currentTarget instanceof HTMLElement)) throw new Error('evt.currentTarget is null! Make sure that this function is being connected to a DOM HTMLElement.'); // Check if this is a possible move if( !evt.currentTarget.classList.contains('possibleMove') ) return; // Check that this action is possible at this moment (shows error dialog if not possible) if( !this.checkAction( 'playDisc' ) ) return; // Get the clicked square x and y // Note: square id format is "square_X_Y" let [_square_, x, y] = evt.currentTarget.id.split('_'); this.ajaxcall( `/${this.game_name}/${this.game_name}/playDisc.html`, { x, y, lock: true }, this, function() {} ); }
Now you should be able to play the game! However, you will need to reload the game after every move to force the board to update.
Step 10 - Notifications
Notifications are used to inform players when something happens in the game. In our case, we only need to send one notification stating when the player has played a disc; however, it is often much more useful to send the notifications as a sequence of events so they can be automatically sequenced in the client. In this case, we will send three notifications:=, all of which can be sent from the player action function: - When a player plays a disc - When discs are converted because of a move - When the scores
Add the following to the
playDisc
function in youryourgamename.action.php
file:function playDisc( int $x, int $y ) { // ... // Notify self::notifyAllPlayers( "playDisc", clienttranslate( '${player_name} plays a disc and turns over ${returned_nbr} disc(s)' ), array( 'player_id' => $player_id, 'player_name' => self::getActivePlayerName(), 'returned_nbr' => count( $turnedOverDiscs ), 'x' => $x, 'y' => $y ) ); self::notifyAllPlayers( "turnOverDiscs", '', array( 'player_id' => $player_id, 'turnedOver' => $turnedOverDiscs ) ); $newScores = self::getCollectionFromDb( "SELECT player_id, player_score FROM player", true ); self::notifyAllPlayers( "newScores", "", array( "scores" => $newScores ) ); // Then, go to the next state $this->gamestate->nextState( 'playDisc' ); }
Add the notifications to your
NotifTypes
interface in theyourgamename.d.ts
file:interface NotifTypes { 'playDisc': { x: number, y: number, player_id: number }; 'turnOverDiscs': { player_id: number, turnedOver: { x: number, y: number }[] }, 'newScores': { scores: Record<number, number> }; }
Add the following to your
yourgamename.ts
file to listen and handle the notifications:setupNotifications() { console.log( 'notifications subscriptions setup' ); dojo.subscribe( 'playDisc', this, "notif_playDisc" ); this.notifqueue.setSynchronous( 'playDisc', 500 ); dojo.subscribe( 'turnOverDiscs', this, "notif_turnOverDiscs" ); this.notifqueue.setSynchronous( 'turnOverDiscs', 1000 ); dojo.subscribe( 'newScores', this, "notif_newScores" ); this.notifqueue.setSynchronous( 'newScores', 500 ); } notif_playDisc( notif: NotifAs<'playDisc'> ) { this.clearPossibleMoves(); this.addTokenOnBoard( notif.args.x, notif.args.y, notif.args.player_id ); } notif_turnOverDiscs( notif: NotifAs<'turnOverDiscs'> ) { // Change the color of the turned over discs for( var i in notif.args.turnedOver ) { let token_data = notif.args.turnedOver[ i ]!; let token = $<HTMLElement>( `token_${token_data.x}_${token_data.y}` ); if (!token) throw new Error( `Unknown token element: ${token_data.x}_${token_data.y}. Make sure the board grid was set up correctly in the tpl file.` ); token.classList.toggle('tokencolor_cbcbcb'); token.classList.toggle('tokencolor_363636'); } } notif_newScores( notif: NotifAs<'newScores'> ) { for( var player_id in notif.args.scores ) { let counter = this.scoreCtrl[ player_id ]; let newScore = notif.args.scores[ player_id ]; if (counter && newScore) counter.toValue( newScore ); } }
(optional) Add a flipping animation to the token when the classes:
yourgamename_yourgamename.tpl
html // Updated token template var jstpl_token = '<div class="token-container tokencolor_${color}" id="token_${x_y}"><div class="token-flip"><div class="token-white"></div><div class="token-black"></div></div></div>';
yourgamename.scss ```scss .token-container { background-color: transparent; width: 56px; height: 56px; perspective: 250px; position: absolute; }
.token-flip { position: relative; width: 100%; height: 100%; text-align: center; transition: transform 1.2s; transform-style: preserve-3d; border-radius:50%; box-shadow: 0 4px 8px 0 rgba(0,0,0,0.25); }
.tokencolor_363636 > .token-flip { transform: rotateY(180deg); }
.token-white, .token-black { position: absolute; width: 100%; height: 100%; -webkit-backface-visibility: hidden; backface-visibility: hidden; background-image: url(“img/tokens.png”); }
.token-white { background-position: 0px 0px; }
.token-black { background-position: -56px 0px; transform: rotateY(180deg); } ```
https://github.com/NevinAF/bga-ts-template/tree/main/docs/tutorials/img/reversi/step10-4.gif
At this point, you now have a fully functional game that can be played on BGA. The following steps are to add flurishes and additional features to the game.
(optional) Step 11 - Statistics
Statistics are mostly optional and are a fun way to compare player strategies or how much they have played the game.
Replace the contents of
shared/stats.jsonc
with the following:{ "$schema": "../../node_modules/bga-ts-template/schema/stats.schema.json", // All of these stats are tracked per player "player": { "discPlayedOnCorner": { "id": 10, "name": "Discs played on a corner", "type": "int" }, "discPlayedOnBorder": { "id": 11, "name": "Discs played on a border", "type": "int" }, "discPlayedOnCenter": { "id": 12, "name": "Discs played on board center part", "type": "int" }, "turnedOver": { "id": 13, "name": "Number of discs turned over", "type": "int" } } }
See stats.json for more information about the stats file for BGA games.
Add the following to the
playDisc
function in youryourgamename.action.php
file:function playDisc( int $x, int $y ) { //... // Update scores... // Statistics self::incStat( count( $turnedOverDiscs ), "turnedOver", $player_id ); if( ($x==1 && $y==1) || ($x==8 && $y==1) || ($x==1 && $y==8) || ($x==8 && $y==8) ) self::incStat( 1, 'discPlayedOnCorner', $player_id ); else if( $x==1 || $x==8 || $y==1 || $y==8 ) self::incStat( 1, 'discPlayedOnBorder', $player_id ); else if( $x>=3 && $x<=6 && $y>=3 && $y<=6 ) self::incStat( 1, 'discPlayedOnCenter', $player_id ); // Notify... }
Reload the statistics configuration on the manage game page. See Step 3.3 for more information.
Play a game and verify that your new statistics are visible at the end of the game:
https://github.com/NevinAF/bga-ts-template/tree/main/docs/tutorials/img/reversi/step11-3.png
(Optional) Step 12 - Game Options
Game options are a way for users to change game behavior or rules. As an example, we will be adding another rule variant called “Reversi Bombs” that flips all 8 adjacent discs when a disc is played.
Replace your gameoptions with the following in the
shared/gameoptions.jsonc
file:{ "$schema": "../../node_modules/bga-ts-template/schema/gameoptions.schema.json", "100": { "name": "Variant", "values": { // This is the current rule set we have implemented "1": { "name": "Standard", "description": "Flip all outflanked discs." }, "2": { "name": "Reversi Bombs", "description": "Flip all 8 adjacent discs." } } } }
See gameoptions.json for more information about the options file for BGA games.
Modify the
getTurnedOverDiscs
function in youryourgamename.game.php
file to handle the new game option:function getTurnedOverDiscs( $x, $y, $player, $board ) { //... foreach( $directions as $direction ) { // If game option '100' is set to 2 (reversi bombs) if ($this->gamestate->table_globals[100] == 2) { $current_x = $x + $direction[0]; $current_y = $y + $direction[1]; if( $current_x<1 || $current_x>8 || $current_y<1 || $current_y>8 ) continue; // Out of the board => stop here for this direction if ($board[ $current_x ][ $current_y ] !== null) // push the disc to be turned over $turnedOverDiscs[] = array( 'x' => $current_x, 'y' => $current_y ); continue; // Don't do standard game logic } // Standard... } //... }
Fix the
playDisc
function to flip all turned disc instead of setting them to the active player:// Let's place a disc at x,y and return all "$returned" discs to the active player $other_id = self::getUniqueValueFromDb( "SELECT player_id FROM player WHERE player_id!='$player_id'" ); $sql = "UPDATE board SET board_player = CASE WHEN board_player = '$player_id' THEN '$other_id' ELSE '$player_id' END WHERE (board_x, board_y) IN ("; foreach( $turnedOverDiscs as $turnedOver ) //...
Reload the game options from within the game manager page. See Step 3.3 for more information.
https://github.com/NevinAF/bga-ts-template/tree/main/docs/tutorials/img/reversi/step12-4.gif
User Preferences
User preferences is something cosmetic about the game interface which you want users to be able to controll. Reversi has a classic athstetic, but in this example, we will add a user preference to change the color of the board. This is not a great use case for user preferences, but it is a simple example.
Replace the contents of
shared/gamepreferences.jsonc
with the following:{ "$schema": "../../node_modules/bga-ts-template/schema/gamepreferences.schema.json", "101": { "name": "Game Style", "needReload": true, "values": { "1": { "name": "Classic", "cssPref": "game_style_classic" }, "2": { "name": "Wooden", // Then this is added to the page, we will override the image urls in the css. "cssPref": "game_style_wooden" } }, "default": 1 } }
See gamepreferences.json for more information about the options file for BGA games.
Download the following images and add them to your
img
folder. They should be named ‘wooden_board.jpg’ and ‘wooden_tokens.png’.Add the following to your
yourgamename.scss
file to change the board and token images:// This overrides the #board + .token- images when the .game_style_wooden is automatically added to the page (by the user preference). .game_style_wooden { #board { background-image: url('img/wooden_board.jpg'); } .token-white, .token-black { background-image: url('img/wooden_tokens.png'); } }
Refresh the game options in the game manager page. See Step 3.3 for more information.
Reload the game and change the user preference to see the new board and token images:
https://github.com/NevinAF/bga-ts-template/tree/main/docs/tutorials/img/reversi/step13-5.gif