Présentation d’un puzzle Codingame
L’interface
Lorsqu’on se lance dans la résolution d’un puzzle sur Codingame, l’interface principale se décompose en quatre parties :
-
En haut à gauche, la description du puzzle (format attendu des entrées et des sorties, règles à implémenter), avec des illustrations et des exemples.
-
En bas à gauche, la sortie console.
-
En haut à droite, l’éditeur de code, dans lequel on développe sa solution (pré-configuré avec le code de lecture des entrées du puzzle).
-
En bas à droite, une liste de jeux de test préconfigurés, permettant de valider sa solution avant de la soumettre.
J’aime beaucoup l’interface de Codingame. Elle est bien pensée, et met en avant l’importance des tests dans le développement, avec une approche TDD (les tests étant en place avant même le début du développement). |
Fonctionnement du puzzle
Tous les puzzles (et ) Codingame fonctionnent de la même façon :
-
Les jeux de données d’entrée sont écrites sur l’entrée standard.
-
Le programme écrit la solution sur la sortie standard.
-
La sortie d’erreur est ignorée par les programmes de vérification du code, mais affichée dans la zone "Console" : elle est donc destinée au débogage.
Utiliser IntelliJ IDEA
Problématique
Malgré ses efforts (autocomplétion rudimentaire, etc.), l’interface de développement Codingame est assez vite limitée. Pour résoudre les puzzles un peu complexes, je préfère donc développer dans IntelliJ IDEA.
Codingame fournit un plugin de synchronisation pour Chrome, mais le fonctionnement de celui-ci ne me convenait pas. Il m’oblige en particulier à utiliser le même fichier pour tous les puzzles (une classe "Solution" dans le package par défaut). |
Je suis donc parti sur un simple projet Maven, donc je copie-colle le code vers Codingame une fois le développement terminé.
Mise en place du projet avec Maven
Je suis parti sur la solution suivante :
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.courtine</groupId>
<artifactId>codingame</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Solutions des puzzles Codingame</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<!-- TestNG peut être remplacé par JUnit, en fonction des préférences. -->
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>6.9.10</version>
<scope>test</scope>
</dependency>
<!-- Dépendances facultatives, mais simplifiant l'écriture des tests. -->
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.3.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>19.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Les tests
Lancement manuel
Pour lancer les tests, plusieurs solutions :
-
Copier-coller le code vers Codingame et les lancer depuis l’interface du site.
-
Lancer la méthode
main()
du puzzle, et copier-coller le jeu de test de Codingame vers la console IntelliJ.
Ces deux solutions "manuelles" sont assez fastidieuses, et obligent à faire des aller-retour entre l’IDE et le site. Afin de pouvoir les tests directement depuis IntelliJ, nous allons recopier les jeux de test fournis sous la forme de tests unitaires.
Rendre le code testable
import java.util.*;
import java.io.*;
import java.math.*;
class Solution {
public static void main(String args[]) {
Scanner in = new Scanner(System.in);
int n = in.nextInt(); // the number of temperatures to analyse
in.nextLine();
String temps = in.nextLine(); // the n temperatures expressed as integers ranging from -273 to 5526
System.out.println("result");
}
}
Ce code initial utilisant l’entrée et la sortie standard, il est assez difficilement testable en l’état. Nous allons donc le décomposer, en séparant la partie I/O, le parsing de l’énoncé, et le cœur du calcul.
import java.util.*;
class Solution {
public static void main(String args[]) {
List<Integer> temps = parseInput(System.in);
int result = nearestFromZero(temps);
System.out.println(result);
}
public static List<Integer> parseInput(InputStream is) {
Scanner in = new Scanner(is);
int n = in.nextInt(); // the number of temperatures to analyse
in.nextLine();
List<Integer> temps = new ArrayList<Integer>();
for (int i = 0; i < n; i++) {
temps.add(in.nextInt());
}
return temps;
}
public static int nearestFromZero(List<Integer> temps) {
// TODO
return Integer.MAX_VALUE;
}
}
Avec ce code décomposé, on peut donc vérifier unitairement que la méthode de parsing fonctionne, mais surtout vérifier le comportement du code "métier" répondant aux différents cas de test du puzzle.
Recopie des tests issus de Codingame
import com.google.common.collect.Lists;
import org.testng.annotations.Test;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
public class SolutionTest {
/** Vérification du comportement de la méthode de parsing du flux d'entrée. */
@Test
public void test_technique_de_la_methode_de_parsing() {
String input = "5\n1 2 3 4 5";
InputStream is = new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8));
List<Integer> temps = Solution.parseInput(is);
assertThat(temps).containsExactly(1, 2, 3, 4, 5);
}
/**
* Dans le cas où le jeu de données est volumineux, on peut préférer le stocker dans un
* fichier dédié plutôt que dans une chaîne de caractères. On peut alors écrire le test
* de cette façon en plaçant le fichier dans le répertoire "src/test/resources".
*/
@Test
public void test_de_parsing_en_utilisant_un_fichier_en_entree() throws IOException {
try (InputStream is = getClass().getResourceAsStream("/jeu_de_test.txt")) {
List<Integer> temps = Solution.parseInput(is);
assertThat(temps).containsExactly(1, 2, 3, 4, 5);
}
}
@Test
public void test_codingame_donnees_simples() {
List<Integer> temps = Lists.newArrayList(1, -2, -8, 4, 5);
assertThat(Solution.nearestFromZero(temps)).isEqualTo(1);
}
/**
* Si le jeu de données d'entrée est complexe, on peut enchaîner le parsing depuis le
* fichier d'entrée (le fichier de données et le code de parsing étant directement
* fournis par Codingame) avec le test de la méthode de calcul principale.
*/
@Test
public void test_codingame_donnees_simples_dans_un_fichier() {
try (InputStream is = getClass().getResourceAsStream("/donnees_simples.txt")) {
List<Integer> temps = Solution.parseInput(is);
assertThat(Solution.nearestFromZero(temps)).isEqualTo(1);
}
}
// Recopier l'ensemble des tests en provenance de Codingame… en rajoutant des tests
// supplémentaires en cas de besoin.
}
Les classes d’exemple ci-dessus n’ont pas de package. En réalité, je développe chaque solution de puzzle dans
un package dédié (org.courtine.codingame.easy.temperatures par exemple, dans le cas qui nous intéresse).
|
Conclusion
Ce projet mis en place, nous pouvons exécuter les tests de validation depuis notre IDE. Pour ce puzzle d’exemple "Températures" qui se résout en quelques lignes de code, c’est un peu extrême, mais pour les puzzles plus complexes, décomposer le code en de petites méthodes que l’on peut tester unitairement s’avère indispensable.