I. Présentation du jeu▲
I-A. Les règles▲
Le TicTacToe est un jeu à deux joueurs, et qui se joue avec une grille de 3x3 cases (qui est vide au départ du jeu). Les joueurs inscrivent des symboles, croix et ronds, à tour de rôle dans les différentes cases restées vides. Le premier joueur qui parvient à aligner trois de ses symboles en horizontal, vertical ou en diagonale a gagné.
I-B. L'architecture de notre application▲
C'est l'architecture de notre application illustrée avec un diagramme de classes UML.
Nous avons donc :
- une énumération CellValue, listant les différentes valeurs possibles pour les cases ;
- une classe métier BoardLogic, dont la responsabilité sera de gérer le cœur des fonctionnalités du plateau ;
- une classe métier BoardView, s'appuyant sur le framework TornadoFx pour définir l'apparence du plateau ;
- une classe d'entrée TicTacToeApp, s'appuyant également sur le framework TornafoFx, pour définir le point d'entrée de l'application.
La classe BoardLogic dispose de trois méthodes :
- reset() afin de réinitialiser l'état du plateau ;
- play() afin de permettre à un joueur de placer son pion sur une case, si toutefois la case est libre ;
- getPietAt() afin de récupérer le statut d'une case du plateau.
II. Prérequis et présentation de l’environnement▲
II-A. Prérequis▲
Afin de suivre ce tutoriel, il est recommandé d’avoir des notions :
- dans le langage Kotlin ;
- dans la bibliothèque graphique JavaFX ;
- de préférence, comprendre comment fonctionne un outil tel que Maven, voire Gradle qui sera utilisé dans ce tutoriel.
II-B. L’environnement▲
Notre environnement nous permettra de gérer le projet, en ne partant de quasiment rien. De plus, grâce au script Gradle, le projet pourra être recréé simplement, sans même avoir à télécharger manuellement les bibliothèques. Donc il suffira de transmettre le script à une personne tierce pour lui permettre de reconstruire le projet.
II-B-1. Kit Java SDK et Kit JavaFX▲
Vous aurez besoin d’un kit Java SDKSoftware Development Kit (Kit de développement logiciel) : mon choix s’est porté sur la version 8, mais rien ne vous empêche d’utiliser une version ultérieure. Par contre, ne serait-ce que pour que l’installation de JavaFX soit plus aisée, je vous recommande d’utiliser OpenJDK. À ce moment-là le kit JavaFX correspondant est tout simplement OpenJFX : OpenJFX 14 devrait fonctionner avec le OpenJDK 8.
II-B-2. Gradle▲
L’outil Gradle (qui est similaire à Maven pour ceux qui connaissent) permet d’importer simplement toutes les dépendances du projet grâce à un script.
Ces dépendances peuvent être le langage à utiliser (il doit pouvoir être exécuté sur la machine virtuelle de Java), mais aussi l’ensemble des bibliothèques extérieures.
Il n’est pas nécessaire d’installer Gradle, car cela sera géré par l’EDIEnvironement de développement intégré Intellij Idea.
II-B-3. Intellij Idea▲
La version Community (gratuite) est largement suffisante dans le cadre de ce tutoriel. Lors de la configuration initiale, je vous recommande de laisser les options par défaut (hormis le thème, où vous pouvez choisir celui qui vous plait bien évidemment) : ainsi le plugin Gradle et le langage Kotlin seront intégrés. Toutefois, dans le cas contraire, vous pouvez installer Gradle et Kotlin en allant dans le menu Configure?>Plugins depuis la page d’accueil. (Si vous n’êtes pas dans la page d’accueil, il est possible d’y revenir en lançant le menu File->Close Project).
Dans la barre de recherche, tapez Gradle (ou Kotlin pour le plugin Kotlin), puis cliquez sur le bouton Installer dans la section détails dans la zone droite de la fenêtre des plugins. Veuillez noter que l’installation ne sera effective qu’après avoir redémarré Intellij Idea.
II-B-4. Kotlin/TornadoFX▲
Ces deux dernières dépendances seront gérées par le script Gradle, une fois le projet créé, dans la section suivante.
III. Mise en place du projet▲
III-A. Création▲
Veuillez démarrer un nouveau projet depuis la page d’accueil.
Choisissez le type de projet Gradle dans la zone gauche, et sélectionnez l’option Kotlin/JVMJava Virtual Machine (Machine virtuelle Java) dans la zone détails. Veuillez aussi à ce que l’option Kotlin DSL for build script soit cochée.
Dans l’écran suivant, entrez le nom du projet : TicTacToeFx, vous pourrez alors valider la création du projet.
III-B. Gestion des dépendances▲
C’est au niveau du fichier build.gradle.kts que nous définissons les dépendances. Le fichier a déjà été créé en même temps que le projet.
C’est aussi ici que nous allons décrire, le moment venu, quelle est la classe principale pour le JARJava Archive (Archive Java) autoexécutable.
III-B-1. Explication du script généré▲
Tout d'abord, le voici :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
plugins {
java
kotlin("jvm"
) version "1.4.0"
}
group = "org.example"
version = "1.0-SNAPSHOT"
repositories
{
mavenCentral()
}
dependencies
{
implementation
(kotlin("stdlib"
))
testCompile("junit"
, "junit"
, "4.12"
)
}
- Lignes 1-4 : définitions des plugins utilisés. Ici, les plugins pour Java et Kotlin semblent donc logiques.
- Lignes 6 et 7 : ces informations sont davantage utiles dans le cadre de la création d’une bibliothèque. Ce qui ne nous empêche pas d’adapter au moins le groupe.
- Lignes 9-11 : où Gradle doit rechercher et télécharger les dépendances. La configuration mavenCentral() nous sera amplement suffisante ici. Mais nous aurions pu ajouter une ligne avec jCenter() afin de disposer d’un éventail de bibliothèques plus large.
- Lignes 13-16 : cette section est capitale. C’est ici que nous allons déclarer les bibliothèques à télécharger. Pour l’instant nous disposons de la bibliothèque standard de Kotlin et du framework de test Junit (dont nous n’aurons pas besoin dans ce tutoriel). En principe, les dépendances essentielles sont déclarées avec implementation suivi de l’identifiant de la dépendance.
III-B-2. Premières adaptations du script▲
- Remplacez le groupe par com.developpez et la version par 1.0.
- Dans la section des dépendances, vous pouvez retirer Junit.
- Il nous faut maintenant ajouter la dépendance vers TornadoFX. Sur la page de ![TornadoFX] https://tornadofx.io/, il y a notamment un badge mavenCental, avec le dernier numéro de version. En cliquant dessus, on arrive sur la page Maven Central liée aux artéfacts de TornadoFX. Celui qui nous intéresse est simplement tornadofx. Donc nous avons pour id no.tornado, pour nom tornadofx et pour numéro de version 1.7.20 (lors de la rédaction). Il nous faut donc ajouter la dépendance implementation " no.tornado:tornadofx:1.7.20 ".
- Enfin, ajouter le code suivant en fin de fichier, afin que le code soit entièrement interprété pour la version 8 de Java (ou la version que vous utilisez) :
2.
3.
tasks.withType<KotlinCompile> {
kotlinOptions.jvmTarget = "1.8"
}
Et l’importation suivante en début de fichier :
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
III-B-3. Mise en application du script▲
Il faut maintenant dire à Gradle de prendre en compte les modifications. Pour ce faire, il suffit de cliquer sur le bouton Local changes, dans la zone éditeur.
IV. Codage du TicTacToe▲
IV-A. Ressources▲
Tout d’abord, nous allons simplement utiliser des images pour les pièces des deux joueurs. Le site [FlatIcon] https://www.flaticon.com/ propose gratuitement de nombreuses images vectorielles, pour peu que l’on crédite les auteurs.
Nous allons utiliser [une image pour la croix] https://www.flaticon.com/free-icon/close_1828665?term=cross&page=1&position=2, et [une image pour le cercle] https://www.flaticon.com/free-icon/rec_808569?term=circle&page=1&position=5 (n’oubliez pas de changer la couleur par défaut, à l’aide du bouton Edit Color). Nous prendrons la version png, taille 24px. Sauvegardez-les dans le dossier src/main/resources du projet.
Prenez également soin de les renommer cross.png et circle.png, respectivement.
IV-B. Le code▲
Voici le code, que nous allons analyser. Mise en garde toutefois, l'application ne vérifie pas s'il y a un gagnant.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
package
com.developpez.tictactoe
import
javafx.geometry.Insets
import
javafx.scene.control.Button
import
javafx.scene.image.Image
import
javafx.scene.image.ImageView
import
tornadofx.*
import
java.lang.IllegalStateException
class
TicTacToeApp : App(BoardView::class
)
sealed
class
Piece {
}
enum
class
CellValue {
Empty, Cross, Circle
}
class
BoardLogic {
// Les valeurs des différentes cases : 1re dimension = rangée.
var
cellsValues: Array<Array<CellValue>> = Array(3
) { Array(3
) { CellValue.Empty } }
// Efface le plateau
fun
reset() {
cellsValues = Array(3
) { Array(3
) { CellValue.Empty } };
}
// Joue la pièce demandée à l'endroit demandé, si la case est vide.
// Retourne true si la pièce a pu être jouée, false sinon.
fun
play(row: Int
, col: Int
, value: CellValue): Boolean {
if
(cellsValues[row][col] == CellValue.Empty) {
cellsValues[row][col] = value;
return
true
}
return
false
}
// Simple getter.
fun
getPieceAt(row: Int
, col: Int
): CellValue = cellsValues[row][col]
}
data
class
CellClickedEvent(val
row: Int
, val
col: Int
) : FXEvent()
class
BoardView : View("Tic Tac Toe"
) {
var
currentPlayer: CellValue = CellValue.Cross
val
boardLogic = BoardLogic()
// Attention ! Cette ligne doit figurer avant le block init !
override
val
root = gridpane {
repeat(3
) { rowIndex ->
row {
repeat(3
) { colIndex ->
button {
gridpaneConstraints {
margin = Insets(2.0
)
setPrefSize(100.0
, 100.0
)
}
action {
fire(CellClickedEvent(rowIndex, colIndex))
}
}
}
}
}
}
init
{
boardLogic.reset()
// Réaction à l'évènement click sur le plateau
subscribe<CellClickedEvent> {
if
(boardLogic.getPieceAt(it
.row, it
.col) == CellValue.Empty) {
boardLogic.play(it
.row, it
.col, currentPlayer)
refreshContent()
toggleCurrentPlayer()
}
}
refreshContent()
}
// Change le joueur au trait
private
fun
toggleCurrentPlayer() {
currentPlayer = when
(currentPlayer) {
CellValue.Cross -> CellValue.Circle
CellValue.Circle -> CellValue.Cross
else
-> throw
IllegalStateException("Current player is not defined."
)
}
}
// Rafraichit l'affichage du plateau
private
fun
refreshContent() {
repeat(3
) { row ->
repeat(3
) { col ->
val
source = root.children[3
* row + col] as
Button
when
(boardLogic.getPieceAt(row, col)) {
CellValue.Cross -> source.graphic = ImageView(Image("/cross.png"
))
CellValue.Circle -> source.graphic = ImageView(Image("/circle.png"
))
else
-> {
}
}
}
}
}
}
Veuillez copier le code dans un fichier Launcher.kt, dans le dossier src/main/kotlin.
IV-B-1. Les importations▲
Lignes 3-8 : vous pouvez constater que la plupart des importations sont des importations JavaFX standards. TornadoFX ne remplace pas JavaFX : TornadoFX tend simplement à en simplifier l'utilisation dans le langage Kotlin. Il faut également importer différents éléments du paquetage tornadofx.
IV-B-2. L'entrée de l'application▲
Ligne 10. Nous définissons une sous-classe de la classe tornadofx.App. Par conséquent, il n'y a pas obligation de créer une fonction main.
Remarquez aussi que le constructeur de la classe App prend en paramètre la référence class de la classe Board : classe personnelle qui définit la vue de notre application. Nous allons l'analyser plus loin.
IV-B-3. Classe logique du plateau▲
Lignes 20 à 41, nous définissons la classe BoardLogic qui permet de gérer les fonctionnalités du plateau. Cette classe ne présente pas de difficulté particulière.
IV-B-4. Définition graphique du plateau▲
Ligne 43 nous définissons un évènement personnalisé : il hérite de la classe tornadofx.FXEvent.
Lignes 45 à 104, nous avons la définition graphique du plateau. Elle utilise différentes fonctions de TorndadoFX : gridpane, row, button, gridpaneConstraints, action. Et la syntaxe paraît assez intuitive. Évidemment, il ne faut pas hésiter à se rendre sur la documentation officielle de TornadoFX afin de connaître les différents composants et layouts disponibles.
Pour définir l'apparence graphique du composant, il faut redéfinir la valeur root (lignes 50 à 66). Notez que cette définition doit être située avant tout bloc init qui doit faire référence à la propriété root.children. En effet, on se retrouverait avec une valeur root nulle sinon.
Remarquez comment j'ai pu tout simplement insérer du code dans la définition : notamment la fonction repeat de Kotlin pour éviter de dupliquer bêtement les mêmes éléments.
L'élément action (lignes 59 à 61) permet ici de définir le gestionnaire d'évènement click pour le bouton. Il émet (par le biais de la fonction tornadofx.Component.fire) un objet de type CellClickedEvent, que nous avons nous-mêmes défini (ligne 43).
Lignes 68 à 80, nous définissons un bloc init, afin de définir les actions à entreprendre dès que l'évènement CellClickedEvent est émis. Cela est fait par l'intermédiaire de la fonction tornadofx.Component.subscribe.
Ce bloc appelle notamment notre méthode personnelle refreshContent, définie en lignes 92 à 104. Le tableau root.children étant à une seule dimension, il faut effectuer un petit calcul (ligne 95) pour retrouver les différents boutons. Pour changer l'image d'un bouton, il suffit d'attribuer une nouvelle instance de javafx.scene.image.ImageView à sa propriété graphic (lignes 97 et 98). Remarquez également qu'il est facile d'y faire référence (et cela fonctionnera même une fois que l'application sera empaquetée en JARJava Archive (Archive Java)) grâce au simple fait d'avoir mis les images dans le dossier resources (toujours lignes 97 et 98).
V. Empaquetage de l'application en JAR et exécution du programme▲
Nous voilà bientôt prêts à créer le fichier JARJava Archive (Archive Java) autoexécutable. Mais avant, il faut que le script Gradle indique que l'on souhaite disposer de cette possibilité.
V-A. Mise à jour du fichier build.gradle.kts▲
- Ajoutez l'import suivant au début du script :
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
- Dans la section plugins, ajoutez la ligne id("com.github.johnrengelman.shadow") version "6.0.0".
- Dans la section repositories, ajoutez le dépôt jcenter().
- Ajoutez également la configuration du plugin ShadowJar.
2.
3.
4.
5.
6.
7.
8.
9.
tasks {
named<ShadowJar>("shadowJar"
) {
archiveBaseName.set("TicTacToe"
)
mergeServiceFiles()
manifest {
attributes(mapOf("Main-Class"
to "com.developpez.tictactoe.TicTacToeApp"
))
}
}
}
Remarquez d'ailleurs la définition de la classe principale.
- Enfin, nous spécifions que la tâche build doit également construire le JAR :
2.
3.
4.
5.
tasks {
build {
dependsOn(shadowJar)
}
}
Vous devriez donc avoir un script similaire à celui-ci :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
plugins {
java
kotlin("jvm"
) version "1.4.0"
id("com.github.johnrengelman.shadow"
) version "6.0.0"
}
group = "com.developpez"
version = "1.0"
repositories
{
mavenCentral()
jcenter()
}
dependencies
{
implementation
(kotlin("stdlib"
))
implementation
("no.tornado:tornadofx:1.7.20"
)
}
tasks.withType<KotlinCompile> {
kotlinOptions.jvmTarget = "1.8"
}
tasks {
named<ShadowJar>("shadowJar"
) {
archiveBaseName.set("TicTacToe"
)
mergeServiceFiles()
manifest {
attributes(mapOf("Main-Class"
to "com.developpez.tictactoe.TicTacToeApp"
))
}
}
}
tasks {
build {
dependsOn(shadowJar)
}
}
N'oubliez pas d'appliquer les changements à l'aide du bouton Local Gradle changes.
V-B. Génération et exécution▲
D'abord, nous allons lancer la tâche build de Gradle. (Notez également que vous pouvez nettoyer le projet auparavant avec la tâche clean.)
Pour cela, cliquez sur le bouton d'accès rapide à Gradle. Voici comment cela se présente dans ma configuration.
Les tâches qui nous intéressent (clean et build) se situent dans le nœud Tasks->build. Il suffit de double-cliquer sur la tâche souhaitée. Lancez donc la tâche build.
Si tout se passe bien, le JARJava Archive (Archive Java) se situe dans le dossier build/libs.
Vous pouvez alors démarrer le terminal de l'EDIEnvironement de développement intégré.
Saisissez alors la commande suivante :
java -jar build/libs/TicTacToe-1
.0
-all.jar
Cela devrait vous permettre de démarrer votre nouveau programme. Par contre, pour les utilisateurs Linux dont le bureau est GTK3 (en tous cas, c'est sur cette configuration que j'ai rencontré le souci) : il existe un bug qui fait crasher l'application directement. Après avoir un peu recherché, il me semble que cela soit surtout lié à OpenJDK 8.
Quoi qu'il en soit, voici la solution sur laquelle je suis arrivé. Dans la commande d'exécution, il suffirait d'ajouter l'option java jdk.gtk.version=2. Ce qui nous donne :
java -Djdk.gtk.version
=
2
-jar build/libs/TicTacToe-1
.0
-all.jar
VI. Aller plus loin▲
Le site officiel de TornadoFX, et le guide officiel – les deux étant en anglais – peuvent vous permettre d'en savoir davantage sur les fonctionnalités de TornadoFX. Il existe aussi une chaîne Slack permettant de demander de l'aide.
N'hésitez pas aussi à consulter les ressources JavaFX disponibles sur le portail developpez.
Enfin, ceux qui le souhaitent peuvent essayer de gérer la détection d'un gagnant en guise d'exercice (voire la mise en évidence de la ligne gagnante : par exemple avec des images supplémentaires).
VII. Conclusion et remerciements▲
J'espère que ce tutoriel vous a plu.
Je tiens à remercier Mickaël Baron, pour sa relecture technique.
Je tiens également à remercier ClaudeLELOUP, pour sa relecture orthographique.