Symfony for Quarto in Allegro! My third integration week at Marmelab.
For my previous integration sprint, I made a Quarto game playable in a terminal, in Python, and connected it to a basic AI server, in GoLang.
My new goal is to implement a web interface for that game using Symfony. It should handle both the single player and the two players modes by re-using the existing AI API. Added to this a new constraint: games now have to be stored in a database.
(Re)discovering PHP
I've been using C# for the past 3 years. A long time ago, I used to work with PHP, but it was called PHP3 at that time. I realized that I that learned about PHP seems so archaic today. PHP has evolved a lot, taking advantage of classes, types, a package system, etc. There is a big community around PHP today, and you can find librairies to fulfill all your needs, and help for all your purposes.
Here are the ingredients I discovered for modern PHP development.
Symfony Framework
Symfony is the reference framework when you want to begin a PHP project with multiple facets. It gives a basic structure for your project and code organization. You just have to choose the modules and combine everything.
The quickstart documentation is clear and straightforward.
Composer
An important element is composer. This tool is a dependency manager for PHP. It allows you to automatically install needed packages by declaring them in a json file. For instance:
{
"type": "project",
"license": "proprietary",
"require": {
"php": "^7.1.3",
"ext-iconv": "*",
"components/jquery": "^3.3",
"components/jqueryui": "^1.12",
"symfony/asset": "^4.1",
"symfony/expression-language": "^4.1",
...
"symfony/webpack-encore-pack": "*",
"twbs/bootstrap": "^4.1"
},
"require-dev": {
"symfony/debug-pack": "*",
"symfony/dotenv": "^4.1",
"symfony/maker-bundle": "^1.0",
},
"config": {
"preferred-install": {
"*": "dist"
},
"sort-packages": true
},
"scripts": {
"auto-scripts": {
"cache:clear": "symfony-cmd",
"assets:install %PUBLIC_DIR%": "symfony-cmd"
}
}
}
It's a bit like npm for node.js, with the same kind of features.
With that ally, it's easy to create new PHP projects.
Twig templating
For people used to old school PHP, like me, its worst shortcoming was the ugly mix between HTML code
and PHP code
.
But now, we have good templating engines to do that correctly.
The one I used, Twig, is the most famous engine for Symfony. It has its own language, different from PHP code, but it's well documented and learning it is fast.
HTML views are saved in templates files named like my_file.html.twig
. And you just have to mix HTML code
and Twig code
in these files to build your page.
Here is an example Twig template for my game:
{% extends "base.html.twig" %}
{% block refresh %}
{% if (canPlay == false) %}
<meta http-equiv="Refresh" content="2">
{% endif %}
{% endblock %}
{% block header %}
<div class="section">
<h4>Welcome to Quarto online !</h4>
</div>
{% endblock %}
{% block content %}
<div class="board">
<div>
{% include 'grid.html.twig' with {
'gameId': game.getIdGame(),
'grid': game.getGrid(),
'pieceSelected' : game.getSelectedPiece(),
'winningLine' : game.getWinningLine(),
'canPlay' : canPlay} only %}
</div>
<div>
{% include 'pieces.html.twig' with {
'gameId': game.getIdGame(),
'pieces': pieces,
'pieceSelected' : game.getSelectedPiece(),
'winningLine' : game.getWinningLine(),
'canPlay' : canPlay} only %}
</div>
<div>
{% include 'players.html.twig' with {
'playerOneTurn': game.getIsPlayerOneTurn(),
'pieceSelected' : game.getSelectedPiece(),
'player1Name' : 'Player 1',
'player2Name' : 'Player 2',
'winningLine' : game.getWinningLine(),
'canPlay' : canPlay,
'playerId' : playerId} only %}
</div>
</div>
{% endblock %}
The {% ... %}
tags delimit a Twig code portion.
Files can include themselves easily to compose complex pages. For instance:
{% include 'grid.html.twig' with {
'gameId': game.getIdGame(),
'grid': game.getGrid(),
'pieceSelected' : game.getSelectedPiece(),
'winningLine' : game.getWinningLine(),
'canPlay' : canPlay} only %}
Handling Data Storage: Doctrine
For the first time of my integration, I had to store data. To this end, I chose a PostgreSQL database.
And to make things simple, I used Doctrine. It's a complete ORM for Symfony that should help me access and change my data without worrying about the SQL syntax.
I just have to describe objects corresponding to my data (the Entities), and Doctrine does all the SQL tasks, from database creation to data updating passing by migrations and querying.
Here is a sample for my Game
entity:
/**
* @ORM\Entity(repositoryClass="App\Repository\GameRepository")
* @ORM\Table
*/
class Game
{
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id_game;
/** @ORM\Column(type="json_array") */
private $grid;
public function __construct(
int $id_game,
array $grid
) {
$this->id_game = $id_game;
$this->grid = $grid;
}
public function getIdGame() : int
{
return $this->id_game;
}
public function getGrid() : Array
{
return $this->grid;
}
public function setIdGame(int $id_game)
{
$this->id_game = $id_game;
}
public function setGrid(array $grid) : Game
{
$this->grid = $grid;
return $this;
}
Each Entity starts with something as simple as a getter/setter class decorated with Doctrine instructions.
Game architecture
Structuring Data
With Doctrine and Postgresql, the structure of my data can be very simple. And mostly, I can keep the structure I used in previous iterations.
Postgres is able to manage json
types directly as a field.
I used and abused this possibility. Everything can be stored in a single Game
table, where one row represents one played game. There is no interaction between the different Games.
The game grid is simply a json_array
field stored inside a Game
record. By doing that, all the progress of the game is managed by the PHP code. There is no specific constraint in the database. No numerous tables to handle each part of the game. The integrity of the data during a game is the responsibility of the code which manipulates this json_array before storing it.. The database has only a basic storage role.
- Why this structure choice?
This structure is what I already used in previous sprints. So it was the fastest way to evolve Quarto in PHP without wasting time to reconsider all data. In addition to that, manipulating a JSON structure locally allows to limit database interactions, and makes the game faster, too.
- Why not use a Document-oriented database in this case?
The fact is I am very familiar with SQL databases. I chose PostgreSQL spontaneously at the beginning of the sprint. When it has been decided that I should be using only one table with json inside, I really hesitated to change for a Mongodb database. It seemed more appropriate.
Finally, I just kept PostgreSQL because the sprint is short, and refactoring a database at this time would not have given more value to the result.
Code Organization
A player can create a game by clicking on
.
To join an existing game, a second player has to enter the URL of this game in his browser. something like http://my-server/game/12
.
This implies that player 1 has to communicate manually his own game URL to player 2. This 'user un-friendly' method was accepted from the start and can be improved during future sprints.
All the low game methods are defined in the game Entity Game.php
public function changeTurn() : Game
{
$this->setIsPlayerOneTurn(!$this->getIsPlayerOneTurn());
return $this;
}
public function selectNextPiece(int $id_piece) : Game
{
$this->setSelectedPiece($id_piece);
return $this;
}
public function placePiece(int $x, int $y) : Game
...
public function getAllPieces() : array
...
The GameApiController.php
is a PHP Controller class managing all routes.
The GameManager.php
contains the high level methods and is the gate between Entity and Controller.
And I have some minor other managers for various behavior like cookies (CookieManager.php
) and tokens(TokenManager.php
)
The rendering is made with Twig and distributed into .twig files in the templates directory.
Designing Realistic Pieces
To be nice and easily playable in a browser, the game had to use a friendly design. The constraint of the previous console versions no longer exists. So it was worth spending a little time to draw beautiful assets based on the real game pieces.
For this, I used Tinkercad. It's a free and online 3D model designer.
It's fast to learn. It allows to make moderately complex objects, and drawing 16 simple geometric pieces was very easy.
Rendering The Game Differently For Two Players
To allow online multiplayer, I had to use cookies.
When the first player creates a new game, a first player token is created and added to their own cookies.
Then, when a second player joins the game, a second player token is created and added to their own cookies, too.
All the future calls to the game will check the token sent by the players to determine if they are authorized to act and to decide which facet of the game they have to render.
If a third person joins the game, the server doesn't create a new token because the first player token and second player token already exist. This person is considered as a spectator. They just can watch the party and are not allowed to play in the game.
public function securiseGameBeforeReturn(string $token, int $register = 0) : Game
{
//If we ask for player one game but we have token of player two (or the opposite)
if (($this->getTokenPlayerOne() != $token && $this->getIsPlayerOneTurn())
|| ($this->getTokenPlayerTwo() != $token && !$this->getIsPlayerOneTurn())) {
$this->locked = true;
//If we are nor player one neither player two token
if ((!$token || ($this->getTokenPlayerOne() != $token && $this->getTokenPlayerTwo() != $token))
&& $register != 1) {
$this->watch_only = true;
}
} else {
$this->locked = false;
}
return $this;
}
With this solution, your opponent can't usurp your own identity without stealing your token. (And if he does, that means he hacked more than just this game and you have to worry about your security 🙄)
Moreover, a different cookie is created for each different game. So a player can start many games at the same time against different opponents or AI.
Giving Tips To Players
Trying to make this online game more interesting than the real world one, I wanted to add visual pieces of information for the players.
Each time a piece is placed, the game has to check if it's a winning move. Then I modify this check to return the positions of the 4 winning pieces.
$winningLine = $this->getWinningPosition($x, $y, $this->getSelectedPiece());
These 4 winning pieces can be in a row, in a column, or a diagonal ( / or **** )
public function getWinningPosition(int $x, int $y, int $piece) : array
{
$testGame = clone $this;
$grid = $testGame->getGrid();
$grid[$y][$x] = $piece;
$testGame->setGrid($grid);
$piecesLine = $testGame->getPiecesRow($x, $y);
if (Piece::isWinningLine($piecesLine)) {
return $piecesLine;
}
$piecesLine = $testGame->getPiecesColumn($x, $y);
if (Piece::isWinningLine($piecesLine)) {
return $piecesLine;
}
$piecesLine = $testGame->getPiecesSlashDiag($x, $y);
if (Piece::isWinningLine($piecesLine)) {
return $piecesLine;
}
$piecesLine = $testGame->getPiecesBackSlashDiag($x, $y);
if (Piece::isWinningLine($piecesLine)) {
return $piecesLine;
}
return [];
}
The winning line is stored in the game table an can be displayed to every person connected to the party.
Some other display tips would have been nice, like which move to avoid to lose at the next turn. Or, at the opposite, which move can make you win at the next turn.
But I was short on time during this sprint. You can count on me for the next sprint.
Let's Check The Result
A First Online Playing Game
At the end of this one week iteration, I have an online game. It's playable in solo or two players. Its design is not so ugly and pleasant to play (it's even beautiful when compared to the previous version).
I can say I'm quite proud of the result regarding the 5 days allowed to produce it.
Limits of the twig templating
Twig is nice to use, but not the best way to build a game. Elements are rendered server-side, and there is no easy way to refresh the page without:
- a client call to the server
- a total re-rendering of the page
I had to use a HTTP Refresh to make the game updating screen itself.
<meta http-equiv="Refresh" content="2">
For the next sprint, a better method is required, using JavaScript.
Ups and downs
The choice of an API for the AI part was a very good choice because the implementation in a new application was extremely fast and easy.
The technical choices (PHP, Twig, PostgreSQL) were not the best for this kind of project, but it wasn't really a hurdle.
All the technologies were almost new to me. And as usual, peoples at Marmelab were here to make this challenge easier and pleasant.
Better is coming
For the next sprint, I'll have to bring Quarto to mobile devices using the amazing React Native framework. Don't miss it!
The current project is, as always, available on github.
Older project repositories are here: