Play 2 et Rest : colonies


Bientôt, une nouvelle mission à faire du Play 2 et du Rest, donc on se retrousse les manches et on fait un projet perso là dessus !

Le Projet

Le projet est le suivant : vous connaissez Dwarf Fortress ? Bon ben c’est un jeu en ascii moche où on essaye de faire une forteresse avec des nains et où on meurt tout le temps. J’ai des qualifications très fortes pour faire des trucs moches (cf mes projets précédents) donc faisons notre version web dont je traine plusieurs ancêtres dans mes différentes clés USB, et qui s’appellent tous Astrum Incognitum : Colonies. C’est long mais personnellement je trouve cela classieux, et qui ici doute de mon bon goût supérieur ? Bien. Donc :

  1. un monde ("astre") fait en génération pseudo-procédurale (ça veut dire "au hasard" mais en plus joli, et pseudo parce que pas complètement au hasard)
  2. Des joueurs représentés dans le jeu par une boite qui donne des ordres aux…
  3. Colons qui obéissent ou pas aux ordres et acquièrent des compétences
  4. Des objets à amasser ramasser transformer détruire construire

J’ai longuement hésité entre du Sql et du NoSql… Puis finalement bon, la future mission sera avec BDD en Sql donc il faut s’y plier.

Architecture

Play 2 et Rest, donc Rest servira à CRUDer les infos du joueur, le monde persistant, les colons, les ressources, les ennemis, etc, autant pour l’administration que pour les clients et les pages web et les outils d’administration seront en play 2 classique.

Gwt et Appfuse

Nouveau framework : GWT, nouvelle couche de persistence en NoSql : le Datastore de google et le PaaS (Plateform as a Service, ou "filez vos programmes on gère le bousin sur nos serveurs"). Le but sera de développer une appli de gestion de partie de jeu de société. Définition du projet L’application Réservajeux (merci j’ai trouvé le nom tout seul, en effet) permet à un utilisateur de :

  • créer un compte avec login et mot de passe
  • renseigner les jeux qu’il possède
  • créer ou rejoindre un groupe de joueurs
  • créer un lieu de jeu
  • proposer afficher ou s’inscrire un créneau de jeu avec jeu de société

Le début est très simple  : installation du plugin pour éclipse (si vous voulez donner des sous pour l’achat de idea, n’hésitez pas à me contacter) et une fois installé, la belle icône G nous permet de créer une web application en un clic.

Créons là après avoir suivi les instructions du post sur git (création d’un reservajeux dans le dépot, clonage dans un répertoire de projets), et créons le dans le répertoire reservajeux du répertoire de projets, pour pouvoir commiter le projet vide et tout propre. Par contre, pas de maven, et il faut garder les fichiers de conf eclipse. La création du compte appengine suit, il suffit de se connecter avec un compte gmail pour  avoir son compte lié au compte appengine, c’est vraiment simple.

Déployons notre application par la même occasion, cela se fait en 3-4 minutes le temps de compiler et d’uploader les fichiers. Une fois l’appli déployée, on peut tester dans le domaine reservajeux.appspot.com notre application. Le premier appel prend quelques secondes, le second est quasi immédiat. Par défaut et pour les comptes gratos, Google ne laisse pas les instances tourner mais les ferme après un peu d’inactivité donc tout est logique. On peut aussi le lancer en local, par contre l’url proposée ressemble à http://127.0.0.1:8888/Reservajeux.html?gwt.codesvr=127.0.0.1:9997 et propose le téléchargement d’un plugin pour chrome ou firefox. N

‘ayant pas réussi à l’utiliser (il est bien présent dans chrome mais même après redémarrage l’application demande à le télécharger), j’ai du chercher un mode non-développement ; il suffit simplement de virer l’option gwt.codesvr : http://127.0.0.1:8888/Reservajeux.html

Cela reste du mode developpement pour le serveur, on perd juste le mode développement côté client. Mais quand on n’arrive pas à dompter la machine pour lui installer un plugin… Bref. J’utiliserai le tuto officiel et un autre tuto que j’ai trouvé pour avancer rapidement, mais ça peut varier… Nous allons commencer par le mode simple : CRUD d’un jeu et CRUD de parties : date, heure, lieu et listes de noms de joueurs pour y participer. Créons nos entités :

@PersistenceCapable
public class Jeu {
@PrimaryKey
@Persistent(valueStrategy=IdGeneratorStrategy.IDENTITY)
private Key key;

@Persistent
private String nom;

@Persistent
private String urlFiche;
// setters et getters
}

@PersistenceCapable
public class Partie {
  @Persistent
  private Date date;

  @Persistent
  private List joueurs;

  @PrimaryKey
  @Persistent(valueStrategy=IdGeneratorStrategy.IDENTITY)
  private Key key;

  @Persistent
  private Key keyJeu;

  @Persistent
  private String lieu;

  // Setters et getters
}

Et créons notre service de Crud et son AsynVersion :

@RemoteServiceRelativePath("jeu")
public interface JeuxService extends RemoteService {
  void creerJeu(Jeu jeu) throws IllegalArgumentException;
  void supprimerJeu(Jeu jeu) throws IllegalArgumentException;
  List listeJeu();
}
public interface JeuxServiceAsync {
  void creerJeu(Jeu jeu, AsyncCallback callback) throws IllegalArgumentException;
  void supprimerJeu(Jeu jeu, AsyncCallback callback) throws IllegalArgumentException;
  void listeJeu(AsyncCallback<List> callback);
}

Ensuite côté serveur nous allons créer le singleton qui nous fournira notre PersistenceManagerFactory. Point de spring pour ce petit projet, donc pas d’injection, on le récupère via le singleton.

public final class PMF {
  private static final PersistenceManagerFactory pmfInstance = JDOHelper
      .getPersistenceManagerFactory("transactions-optional");

  private PMF() {
  }

  public static PersistenceManagerFactory get() {
    return pmfInstance;
  }
}

Et enfin le service web qui fait tout cela ; on pourrait ajouter une couche entre le ServiceServlet qui ferait le traitement plutôt que le laisser ici (et les 3 fonctions ne feraient qu’appeler les méthodes de cette couche), mais c’est un projet simple donc faisons un peu moche et rapide :

@SuppressWarnings("serial")
public class JeuxServiceImpl extends RemoteServiceServlet implements
    JeuxService {

  private PersistenceManager getPersistenceManager() {
    return PMF.get().getPersistenceManager();
  }

  @Override
  public void creerJeu(Jeu jeu) throws IllegalArgumentException {
    PersistenceManager persistenceManager = getPersistenceManager();
if (jeu.getNom().length() < 4 || jeu.getNom().length() > 30) {
 throw new IllegalArgumentException("Erreur sur le nom du jeu : " + jeu.getNom());
 }
    try {
      persistenceManager.makePersistent(jeu);
    } finally {
      persistenceManager.close();
    }
  }

  @Override
  public void supprimerJeu(Jeu jeu) {
    PersistenceManager persistenceManager = getPersistenceManager();
    try {
      persistenceManager.deletePersistent(jeu);
    } finally {
      persistenceManager.close();
    }
  }

  @Override
  public List listeJeu() {
    PersistenceManager persistenceManager = getPersistenceManager();
    List listeJeux = new ArrayList();
    try {

      Extent e = persistenceManager.getExtent(Jeu.class, true);
      Iterator iter=e.iterator();
      while (iter.hasNext())
      {
          Jeu jeu=(Jeu)iter.next();
          listeJeux.add(jeu);
      }
    } finally {
      persistenceManager.close();
    }
    return listeJeu();
  }
}

Enfin on code le EntryPoint, qui correspond à la page.


public class Reservajeux implements EntryPoint {
  private final JeuxServiceAsync jeuxService = GWT.create(JeuxService.class);

  private final FlexTable jeuxFlextable = new FlexTable();
  private final Button creeJeuButton = new Button("Créer le jeu");
  private final TextBox nomJeuField = new TextBox();
  private final TextBox urlJeuField = new TextBox();
  private final Label errorLabel = new Label();
  
  private ArrayList<String> jeux = new ArrayList<String>();

  public void onModuleLoad() {
    RootPanel.get("creeJeuButtonContainer").add(creeJeuButton);
    RootPanel.get("listeJeuxContainer").add(jeuxFlextable);
    RootPanel.get("nomJeuContainer").add(nomJeuField);
    RootPanel.get("nomJeuContainer").add(urlJeuField);
    creeJeuButton.addStyleName("sendButton");

    nomJeuField.setText("Nom du jeu");

    jeuxFlextable.setText(0, 0, "Nom");
    jeuxFlextable.setText(0, 1, "Fiche");
    jeuxFlextable.setText(0, 2, "");
    
    jeuxFlextable.setCellSpacing(5);
    jeuxFlextable.setCellPadding(5);
    jeuxFlextable.setWidth("100%");

    creeJeuButton.addClickHandler(new ClickHandler() {
      public void onClick(ClickEvent event) {
        ajouterJeu();
      }
    });

    RootPanel.get("errorLabelContainer").add(errorLabel);

    jeuxService.listeJeu(new AsyncCallback<List<Jeu>>(){
      @Override
      public void onFailure(Throwable caught) {
        Window.alert("Erreur récupération des jeux");
      }

      @Override
      public void onSuccess(List<Jeu> result) {
        for (Jeu jeu : result) {
          addLigne(jeu);
        }
      }
    });
  }

  private void ajouterJeu() {
    final String texteJeu = nomJeuField.getText().toUpperCase().trim();
    nomJeuField.setFocus(true);
    nomJeuField.setText("");
    urlJeuField.setText("");
    final Jeu jeu = new Jeu();
    jeu.setNom(texteJeu);
    jeu.setUrlFiche(urlJeuField.getText().trim());
    
    jeuxService.creerJeu(jeu, new AsyncCallback<Void>() {
      @Override
      public void onFailure(Throwable caught) {
        errorLabel.setText("Erreur lors de la création " + caught.getMessage());
      }

      @Override
      public void onSuccess(Void result) {
        addLigne(jeu);
      }
    });
  }

  private void addLigne(final Jeu jeu){
    int row = jeuxFlextable.getRowCount();
    jeuxFlextable.setText(row, 0, jeu.getNom());
    jeuxFlextable.setWidget(row, 1, new Hyperlink("fiche", jeu.getUrlFiche()));
    
    Button removeJeu = new Button("x");
    removeJeu.addClickHandler(new ClickHandler() {
      public void onClick(ClickEvent event) {
        jeuxService.supprimerJeu(jeu, new AsyncCallback<Void>() {
          @Override
          public void onFailure(Throwable caught) {
            errorLabel.setText("Erreur lors de la suppression " + caught.getMessage());
          }
          
          @Override
          public void onSuccess(Void result) {
            int removedIndex = jeux.indexOf(jeu.getNom());
            jeux.remove(removedIndex);
            jeuxFlextable.removeRow(removedIndex + 1);
          }
        });
      }
    });
    jeuxFlextable.setWidget(row, 3, removeJeu);
  }
  
}

La grosse difficulté aura été de comprendre une erreur de StackOverFlowException, qui nécessitait la suppression du répertoire gwt-unitCache, et surtout une erreur de Invalid Type sur l’objet Jeu.

Dans Gwt, les objets qui sont communs doivent hériter de l’interface "IsSerializable" et non pas Serializable comme dans mon souvenir douteux d’un ancien essai. Il fallait également avoir une clé de type Long et non pas String ou Key :

@PersistenceCapable
public class Jeu implements IsSerializable {
  @PrimaryKey
  @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
  @Extension(vendorName = "datanucleus", key = "gae.encoded-pk", value = "true")
  Long id;
  
  @Persistent
  private String nom;

  @Persistent
  private String urlFiche;
}

Ceci nous fait un beau tableau qui peut être mis à jour mais pour l’instant je reste un peu refroidi par les exceptions qui ne proposent pas de solutions probables au problème. Un ptit message "Vos objets partagés sont-ils IsSerializable ?" n’aurait pas été de refus sur l’exception "IncompatibleRemoteServiceException".

gwt

C’est encore loin du compte (remarquez l’attention donnée au choix des données de test), mais je dois repasser sur un autre projet, donc on s’arrête pour le moment sur GWT. Et en plus ça ne supporte pas les sources en Cp1251, donc problèmes de caractères sur les accents. Tsss…

Revenir sur sevenwonders

J’ai du mettre en pause le blog pendant 3 mois pour une mission qui demandait pas mal de temps de cerveau disponible et qui m’a beaucoup appris. Si j’avais donc à refaire sevenwonders, voilà ce qui changerait en gros:

  • utilisation de Spring Roo pour créer rapidement la partie web
  • faire plus attention aux modifieurs d’accès java (par défaut j’oublie de mettre des private partout où je peux) ou static ou final
  • utilisation de Guava pour faire des fonctions de filtrage avec les prédicats sur les cartes et utiliser les fonctions helpers sur les collections, la possibilité de marquer des champs en nullable, etc
  • Utilisation de testNg et son dataprovider fort utile qui permet de tester plein de cas de tests en mettant les données et résultats attendus dans des tableaux :
@Resource(name="listeMerveilles")
List<Merveille> listeMerveilles;

@DataProvider
  private Object[][] merveillesProvider() {
    Object[][] testMerveille = new Object[7][3];
    return new Object[][] {
        {listeMerveilles.get(0), 1, "The Colossus of Rhodes", Lists<Cout>.asList(new Cout(0, Ressource.Brique, Ressource.Brique), new Cout(0, Ressource.Minerai), new Cout(0, Ressource.Pierre), null)},
        {listeMerveilles.get(1), 2, "The Pyramids Face B", Lists<Cout>.asList(new Cout(0, Ressource.Brique, Ressource.Brique), new Cout(0, Ressource.Minerai), null, null)},
        // etc...
    };
  }
@Test(dataProvider = "merveillesProvider")
public void testCoutEtagesMerveilles(Merveille merveille, Integer id, List<Cout> couts) {
     assertEquals(merveille.getId(), id);
     for (int i = 0; i < couts.size()) {
          assertEquals(merveille.getCoutAge(i), couts.get(i));
     }
}

Ainsi on peut tester tous les coûts de toutes les merveilles plus efficacement et en séparant le test des données à tester.

Bref, à 4 mois d’écarts le projet aurait été différent… Je le reprendrai dans un futur indéterminé pour tester le dépot sur un cloudbees ou autre PaaS.

Seven Wonders 5 – MVC (Spring Me VoiCi) et Jetty

L’appli web sera écrite en MVC. Plusieurs possibilités : l’écrire from scratch, ou en utilisant  le wizard de SpringSource, ou l’archetype appfuse spring mvc, ou en partant de tatami sur github…

Pour aller vite, je reprend un projet de test Spring MVC créé avec SpringSource que je transfère dans mon module sevenwonders-web. Celui-ci gagne un HomeControlleur.java et sa home.jsp, un web.xml, un root-context.xml et un servlet-context.xml ainsi qu’un log4j.xml.

Pour faire du beau site (disons "pas moche") rapidement, récupérons Twitter Bootstrap pour le css et la mise en forme du site.

Nous allons enfin utiliser jetty pour lancer rapidement l’application. Grâce aux conventions maven, le plugin jetty sait directement où chercher le war (dans target) et utilise un tas de paramètres par défaut, dont le port à 8080.

Ajoutons simplement le plugin Jetty à notre pom :


<plugin>
 <groupId>org.mortbay.jetty</groupId>
 <artifactId>jetty-maven-plugin</artifactId>
 <version>${maven.jetty.version}</version>
 <configuration>
 <scanIntervalSeconds>${jetty.scanIntervalSeconds}</scanIntervalSeconds>
 <stopKey>stop-jetty</stopKey>
 <stopPort>9999</stopPort>
 <systemProperties>
 <systemProperty>
 <name>jetty.port</name>
 <value>${jetty.port}</value>
 </systemProperty>
 </systemProperties>
 </configuration>
 <executions>
 <execution>
 <id>start-jetty</id>
 <phase>pre-integration-test</phase>
 <goals>
 <!-- <goal>run-exploded</goal> -->
 <goal>run</goal>
 </goals>
 <configuration>
 <scanIntervalSeconds>0</scanIntervalSeconds>
 <daemon>true</daemon>
 </configuration>
 </execution>
 <execution>
 <id>stop-jetty</id>
 <phase>post-integration-test</phase>
 <goals>
 <goal>stop</goal>
 </goals>
 </execution>
 </executions>
 </plugin>

Il suffit de spécifier maintenant la version utilisée, en l’occurence la dernière : <maven.jetty.version>8.1.5.v20120716</maven.jetty.version>

Enfin, lançons le mvn jetty:run et allons voir en localhost:8080 le beau site web un peu flou qui nous attend, la simplicité de cette étape me mettant la larme à l’oeil.

Bon, essuyons vite la larme, la page est vide, ou quasiment, vu que je suis parti d’un contrôleur dont la simplicité est un hommage aux jardins de pierre japonais et qui se contente d’afficher l’heure qu’il est.

@Controller
public class HomeController {

private static final Logger logger = LoggerFactory.getLogger(HomeController.class);

/**
* Simply selects the home view to render by returning its name.
*/
@RequestMapping(value = "/", method = RequestMethod.GET)
public String home(Locale locale, Model model) {
logger.info("Welcome home! the client locale is "+ locale.toString());

Date date = new Date();
DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale);

String formattedDate = dateFormat.format(date);

model.addAttribute("serverTime", formattedDate);

return "home";

}

home.jsp :

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form"%>
<%@ page session="false"%>
<html>
<head>
<title>Home</title>
</head>
<body>
	<h1>Hello world!</h1>

	<P>The time on the server is ${serverTime}.</P>

</body>
</html>

Nous pourrions nous arrêter sur ce bel écran et atteindre l’illumination, mais continuons d’avancer vers le but que nous nous sommes fixés et ajoutons un formulaire pour créer une partie. Ce formulaire sera très simple et n’aura comme choix que le nombre de bots de la partie.

Initialisation du formulaire :

@ModelAttribute("partieForm")
  public PartieForm initPartieForm(){
	  PartieForm partieForm = new PartieForm();
	  partieForm.setNombreBots(4);
	  return partieForm;
  }

@RequestMapping(method=RequestMethod.POST)
  public ModelAndView submitForm(@ModelAttribute("partieForm") PartieForm partieForm, Model model) {
    Partie partie = new Partie();
    partieManager.initPartie(partie, partieForm.getNombreBots());
    model.addAttribute("partie", partie);
    for (Joueur joueur : partie.getListeJoueurs()) {
      System.out.println("joueur " + joueur.getMerveille().getNomEn());
    }
    return new ModelAndView("partie", "partie", partie);
  }

On fait un tas de belles jsp avec des beaux css pour faire du bel affichage, et voilà ce qui arrive quand on n’est pas une brute en design et qu’on en fait quand même :

Bon, c’est pas encore bon, mais intéressons nous plutôt à l’erreur "fichier ayant une section mappée utilisateur , elle provient de l’utilisation de jetty et empêche la modification de fichiers utilisés par jetty. La solution a été trouvée ici : http://blog.xebia.fr/2009/06/10/maven-jetty-plugin-et-section-mappee/ et en rajoutant la ligne du nouveau connector, le problème disparait.

Ensuite on code un paquet de trucs. Oui, en langage littéraire ça s’appelle une ellipse, mais c’est ce qui arrive quand on code plus vite qu’on n’écrit ses articles sur le blog. Bref :

Création du service de bot qui sert à choisir une action pour les places gérées par l’ordinateur

Du javascript jsp et css pour mettre en forme l’interface ; sur ce point je rame un peu, difficile de faire tenir 7 merveilles + une dizaine de cartes sur un écran (de surcroît quand c’est un portable 12 pouces) ainsi que le formulaire pour jouer une action. Je devrais probablement n’afficher qu’un résumé des infos des joueurs qui ne sont pas soi / voisin droite / voisin gauche, avec un popup pour leur jeu entier, mais ça nécessite pas mal de temps à creuser les css div html toussa toussa… Bref pour l’instant tout est en une page, et passablement encore hideux. Et voici :

Ah suite : un bon tuto pour débugguer du jetty : http://karamimed.developpez.com/tutoriels/java/web/eclipse/maven-jetty/debug/

Seven Wonders 5 – Springons tout ça

Nous allons springifier tout ça : le PartieManager va devenir un service qui va se faire injecter pour le test.

On ajoute spring dans le pom du sevenwonders-business :

<properties>
		<java-version>1.6</java-version>
		<org.springframework-version>3.0.6.RELEASE</org.springframework-version>
		<org.aspectj-version>1.6.9</org.aspectj-version>
		<org.slf4j-version>1.5.10</org.slf4j-version>
	</properties>

	<dependencies>

		<!-- Spring -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-context</artifactId>
			<version>${org.springframework-version}</version>
			<exclusions>
				<!-- Exclude Commons Logging in favor of SLF4j -->
				<exclusion>
					<groupId>commons-logging</groupId>
					<artifactId>commons-logging</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-webmvc</artifactId>
			<version>${org.springframework-version}</version>
		</dependency>

	</dependencies>

Et on passe partieManagerImpl en service :

@Service("partieManager")
public class PartieManagerImpl implements PartieManager {
//...
}

Et dans notre test où nous avions

@Before
  public void setUp() {
    partieManager = new PartieManagerImpl();
//...
}

Nous allons laisser faire ça à Spring :

Créons un conf-context.xml dans src-main-resources avec :

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"
	default-autowire="byType">

	<bean class="org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor" />

	<bean id="partieManager" class="fr.mathieujjava.sevenwonders.PartieManagerImpl" />
</beans>

et modifions notre classe de test :

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"/conf-context.xml"})
public class PartieManagerImplTest {

  @Autowired
  private PartieManagerImpl partieManager;
  //...
}

Lançons le test : ça couine sur le log4j.

log4j:WARN No appenders could be found for logger (org.springframework.test.context.junit4.SpringJUnit4ClassRunner).
log4j:WARN Please initialize the log4j system properly.

Pas de problème créons un log4j.properties au même endroit que le root-context et mettons-y une config de base :

# Set root logger level to DEBUG and its only appender to A1.
log4j.rootLogger=DEBUG, A1

# A1 is set to be a ConsoleAppender.
log4j.appender.A1=org.apache.log4j.ConsoleAppender

# A1 uses PatternLayout.
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%-4r [%t] %-5p %c %x – %m%n

Nous pouvons créer nos premiers beans de merveilles dans le merveilles-beans.xml qu’on met dans src/main/resources (je ne suis pas très certain que ça soit le bon endroit, si qqn a une meilleure suggestion je suis preneur). Voilà la première merveille :

<bean id="merveille1" class="fr.mathieujjava.sevenwonders.Merveille">
		<constructor-arg name="nomEn" value="The Colossus of Rhodes" />
		<constructor-arg name="nomFr" value="Le Colosse de Rhodes" />
		<constructor-arg name="face" value="1" />
		<constructor-arg name="texte" value="1er étage : 3 points de victoire; 2d étage : Fournit 2 puissance militaire; 3ème étage : 7 points de victoire" />
		<constructor-arg name="ressource" value="Minerai" />
		<constructor-arg name="cout" value="B,BB,BBB" />
	</bean>

Nous avons un

Error creating bean with name ‘merveille1′ defined in class path resource [merveilles-beans.xml]: Could not resolve matching constructor (hint: specify index/type/name arguments for simple parameters to avoid type ambiguities)

Cela vient de la transformation du cout : il nous faut un transformateur de String en Cout :

public class CoutEditor extends PropertyEditorSupport {
  public void setAsText(String text) throws IllegalArgumentException {
    Cout cout = new Cout(0);

    for (char c : text.toCharArray()) {
      switch (c) {
      case 'B':
        cout.ajoute(Ressource.Bois);
        break;
      case 'P':
        cout.ajoute(Ressource.Pierre);
        break;
      case 'R':
        cout.ajoute(Ressource.Brique);
        break;
      case 'M':
        cout.ajoute(Ressource.Minerai);
        break;
      case 'T':
        cout.ajoute(Ressource.Tissu);
        break;
      case 'V':
        cout.ajoute(Ressource.Verre);
        break;
      case 'Y':
        cout.ajoute(Ressource.Papyrus);
        break;
      }
    }
    setValue(cout);
  }
}

et on déclare ce constructeur de Cout dans le xml :

<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
		<property name="customEditors">
			<map>
				<entry key="fr.mathieujjava.sevenwonders.Cout" value="fr.mathieujjava.sevenwonders.propertyeditor.CoutEditor" />
			</map>
		</property>
	</bean>

Une fois ceci fait, nous pouvons effectivement créer en beans les 7 merveilles et de la même façon les cartes dans cartes-context.xml.
J’ai collé le coutEditor dans conf-context général vu qu’il sert pour les merveilles et les cartes.

Avec les cartes et les merveilles, nous pouvons développer les fonctions qui utilisent les vraies cartes, ainsi quand un joueur joue une carte de Commerce il s’attend à recevoir des sous :

public static void effectueCarte(Partie partie, Joueur joueur, Carte carte) {
    if ("Tavern".equals(carte.getNomEn())) {
      joueur.modifieNombrePieces(5);
    } else if ("Vineyard".equals(carte.getNomEn())) {
      joueur.modifieNombrePieces(partie.compteNombreCartes(joueur, TypeCarte.MatierePremiere, true, true, true));
    } else if ("Bazar".equals(carte.getNomEn())) {
      joueur.modifieNombrePieces(partie.compteNombreCartes(joueur, TypeCarte.Manufacture, true, true, true));
    } else if ("Chamber of Commerce".equals(carte.getNomEn())) {
      joueur.modifieNombrePieces(2 * partie.compteNombreCartes(joueur, TypeCarte.Manufacture, false, true, false));
    } else if ("Lighthouse".equals(carte.getNomEn())) {
      joueur.modifieNombrePieces(partie.compteNombreCartes(joueur, TypeCarte.Commerce, false, true, false));
    } else if ("Haven".equals(carte.getNomEn())) {
      joueur.modifieNombrePieces(partie.compteNombreCartes(joueur, TypeCarte.MatierePremiere, false, true, false));
    }
  }

Là dessus j’ai quelques réserves : utiliser le nom anglais comme identifiant de la carte ne me semble pas très propre, mais il existe des cartes avec le même nom (donc fonctionnellement la même carte) à différents âges, donc utiliser un vrai identifiant générerait des identifiants identiques pour la carte Bazar de l’age 1 et de l’age 2…

De même, on va pouvoir coder les autres actions que la défausse, dont la construction de la merveille :

case Merveille:
      joueur.getMain().remove(action.getCarte());
      if (joueur.getEtageMerveille() == 3) throw new Exception("Le joueur a déjà tout construit");
      switch (joueur.getEtageMerveille()) {
      case 0 :
      }
      paieCout(partie, joueur, joueur.merveille....);
      break;

On le voit rapidement, faire un switch sur l’étage pour savoir quel coût payer dans la merveille n’est pas très propre, il faudrait transformer les 4 coûts en tableau, ce qui permet en plus de savoir combien il y a d’étages.

Refactorons Merveille ainsi :

public class Merveille {
  Cout[] coutsAge;
    //...

  public Merveille(String nomEn, String nomFr, Integer face, String texte, Ressource ressource, Cout... cout) {
    //...
    coutsAge = cout;
  }

  public Cout getCoutAge(Integer age) {
    return coutsAge[age-1];
  }
}

Après une modif rapide des codes unitaires pour utiliser le bon getter plutôt que les 4 anciens getters (un par age), tout fonctionne bien.

Maintenant que le coeur de l’appli fonctionne, faisons une petite webapp pour montrer tout ça au reste de l’humanité.

Gestion des projets – Git

Développer sur plusieurs pc à la fois c’est sympa mais on perd du temps en winmerges fastidieux lors des synchronisations. Arrive Git qui nous aide à gérer tous nos projets éparpillés partout en nous filant des dépôts où seront stockés notre code.

Pour la doc : http://git-scm.com/book/fr

Un bon tutorial : http://www.siteduzero.com/tutoriel-3-254198-gerez-vos-codes-source-avec-git.html

Créons donc notre dépôt sur clé Usb qui sera ensuite répliqué sur les autres environnements de dev (pc fixe chez moi, portable en déplacement, game boy color aux toilettes)

Ne faites pas mon erreur qui était de mettre tous les projets dans un seul repository : quand vous voudrez les récupérer vous ne pourrez pas en récupérer qu’une partie. Avec Git, un projet = un repository. Dans la clé usb il y a donc un répertoire "Depots" qui contient un repository par projet à stocker.

On veut un dépot "bare" pour avoir un dépôt "officiel" donc j’ai du renommer mes projets, les cloner en mode bare avec le bon nom, et utiliser ce bon dépôt "bare" comme base. Ne pas oublier de créer un fichier .gitignore à la racine (et de le commiter) pour tous les fichiers et répertoires de build, eclipse etc.

Pour résumer et avoir un dépot propre :

  • dans votre répertoire de dépot, créez un répertoire du nom de votre projet.
  • Dedans ce répertoire, faites git init –bare
  • Dans un répertoire de projets courants, faites git clone <répertoire de dépot du projet>
  • copiez-y tous les fichiers de votre projet courant
  • n’oubliez pas d’y mettre un .gitignore
  • ajoutez et commitez tous les fichiers, puis enfin
  • faites un pull vers le répertoire de dépot propre.

Pour commiter, il y a plus rapide que l’utilisation de vi, faites directement :
git commit -m "MESSAGE DE COMMIT"

Pour uploader un projet perso sur github :
– l’initialiser avec git init
– préciser le repository sur lequel uploader :
git remote add LABEL_REPO git@github.com:LOGIN/NOM_REPO
D’habitude on utilise "origine" comme Label du repository. Si on utilise plusieurs githubs, par ex. celui de github.com et celui de heroku, il est ainsi possible de les différencier.
– si vous vous trompez, supprimez le avec git remote rm LABEL_REPO
– uploader :
git push LABEL_REPO master

Seven Wonders 4 – la couche de service

Passons au module business. Nous allons y créer notre premier service. Pour Partie je vais faire une interface PartieManager et une implémentation PartieManagerImpl. Notre partie doit pouvoir faire les 4 actions possibles :

  1. construire un étage avec une carte en payant le coût
  2. jouer une carte puis effectuer l’effet
  3. exécuter l’action "défausse une carte pour 3 pièces"
  4. faire une "action spéciale" relative à une merveille.

Pour l’instant, faisons comme si les faces B des merveilles n’existaient pas. Ces faces B offrent des pouvoirs spéciaux en delà de ces 3 actions. Elles seront l’occasion de faire un peu de refactoring… Commençons par définir une action : d’abord l’enum spécifiant quelle action est faite :


public enum TypeAction {
	Joue, Defausse, Merveille, Special
}

Puis l’action qui est donc définie par le joueur qui la fait, le type d’action et d’autres paramètres que nous ajouterons au fur et à mesure, mais le plus évident et commun au cas 1, 2 et 3, c’est que l’action portera toujours sur un carte (en pratique, les actions spéciales également).

public class Action {
	Joueur joueur;

	TypeAction typeAction;

	Carte carte;
}

Ces 2 classes vont dans sevenwonders-common. Reprenons notre PartieManager : Il faut qu’il puisse effectuer une action. Repartons sur du TDD mais plus précisément que pour les modèles (il y avait des approximation et l’oubli de la phase de refactoring); D’abord le code (ici l’interface et l’implémentation) :

public interface PartieManager {
  public void effectueAction(Partie partie, Action action);
}

public class PartieManagerImpl implements PartieManager {
  @Override
  public void effectueAction(Partie partie, Action action) {
  }
}

Et nous allons faire pour débuter les tests sur la défausse : 1 réussie qui donne 3 pièces et met la carte à la défausse, et une ratée qui doit lever une exception :

public class PartieManagerImplTest {

  private Partie partie;

  private Joueur joueurA;

  private Carte carteScierie, carteUniversite;

  private PartieManagerImpl partieManager;

  @Before
  public void setUp() {
    partieManager = new PartieManagerImpl();
    Merveille merveilleA = new Merveille("A", "A", 1, "", null, new Cout[0]);
    partie = new Partie();
    partie.addJoueur(joueurA = new Joueur(merveilleA));
    joueurA.getMain().add(carteScierie = new Carte(TypeCarte.MatierePremiere, "Sawmill", "Scierie", new Cout(1), null, "Fournit 2 bois"));
    joueurA.getMain().add(new Carte(TypeCarte.Science, "Library", "Bibliothèque", new Cout(0, Ressource.Pierre, Ressource.Pierre, Ressource.Tissu), null, "Ecriture"));
    carteUniversite = new Carte(TypeCarte.Science, "University", "Université", new Cout(0, Ressource.Bois, Ressource.Bois, Ressource.Papyrus, Ressource.Verre), null, "Ecriture");
  }

  @Test
  public void testDefausseReussie() throws Exception {
    Action action = new Action(joueurA, TypeAction.Defausse, carteScierie);

    // on vérifie la configuration de départ
    assertEquals(0, partie.getDefausse().size());
    assertEquals(3, joueurA.getNombrePieces().intValue());
    assertEquals(2, joueurA.getMain().size());
    partieManager.effectueAction(partie, action);
    // le joueur gagne 3 pièces, et met une carte en défausse
    assertEquals(6, joueurA.getNombrePieces().intValue());
    assertEquals(1, partie.getDefausse().size());
    assertEquals(carteScierie, partie.getDefausse().get(0));
    assertEquals(1, joueurA.getMain().size());
  }

  @Test(expected=Exception.class)
  public void testDefausseImpossible() throws Exception {
    Action action = new Action(joueurA, TypeAction.Defausse, carteUniversite);
    partieManager.effectueAction(partie, action);
  }
}

Le test plante, la fonction est vide, tout est normal. Ecrivons donc la fonction :

  @Override
  public void effectueAction(Partie partie, Action action) throws Exception {

      if (action.getJoueur().getMain().contains(action.getCarte())) {
        action.getJoueur().getMain().remove(action.getCarte());
        partie.getDefausse().add(action.getCarte());
        action.getJoueur().modifieNombrePieces(3);
      } else {
        throw new Exception("Le joueur ne possede pas la carte " + action.getCarte());
      }
 }

Notez qu’on ne teste pas le type de l’action, et c’est voulu ; ceci viendra avec les autres tests ; pour l’instant, le TDD nous fait écrire la fonction minimale. Celle-ci passe le test : un peu de refactoring, on externalise le joueur et la carte pour plus de lisibilité et on vérifie que tout est vert.

@Override
  public void effectueAction(Partie partie, Action action) throws Exception {
    Joueur joueur = action.getJoueur();
    Carte carte = action.getCarte();

    if (joueur.getMain().contains(carte)) {
      joueur.getMain().remove(carte);
      partie.getDefausse().add(carte);
      joueur.modifieNombrePieces(3);
    } else {
      throw new Exception("Le joueur ne possede pas la carte " + carte);
    }
  }

Pour accélérer un peu nous allons juste décrire la suite sans coller tout le code et les tests correspondants : Pour les autres action et payer son cout, un joueur peut utiliser ses pièces à lui, ses ressources, et donner des pièces à ses voisins de droite ou gauche pour leur acheter des ressources :

public class ChoixAchatRessources {
	Integer piecesPourBanque = 0;

	List ressourcesDroite = new ArrayList();
	List ressourcesGauche = new ArrayList();
	List ressourcesSoi = new ArrayList();

	Integer piecesPourGauche = 0;
	Integer piecesPourDroite = 0;
}

On écrit ensuite la fonction qui fait payer un cout à un joueur et les tests correspondants.

public enum Place {
  Gauche, Droite, Soi
}

@Override
  public void paieCout(Partie partie, Joueur joueur, ChoixAchatRessources cout) {

  }

Bon j’accélère un poil sinon nous y serons encore dans 6 mois : je développe les autres fonctions utiles. A un moment j’ai besoin d’avoir spécifiquement la liste des cartes, pour coder les tests du type "telle carte donne 2 pts de victoire" ou "telle carte donne 3 en puissance militaire". Nous allons développer les cartes et les merveilles mais en spring beans, pas dans une base de données ; plusieurs raisons : les cartes et merveilles sont statiques et on peut ainsi voir la gestion des beans spring.

Un dernier exemple avec mockito cette fois : nous allons tester la carte qui calcule la puissance militaire d’un joueur. Facile : il faut faire la somme de ses médailles… sauf s’il a des cartes militaires en jeu, ou s’il a construit le 2ème étage de la merveille Colosse de Rhodes. On peut donc créer une partie avec ces joueurs, ces merveilles, ces cartes et tester tout ce bousin, ou se contenter de mocker la partie pour qu’elle réponde "oui" à "est ce que le joueur a construit une caserne" et à "est ce que le joueur a le colosse de Rhodes et a construit le 2eme étage.

Donc : de base, la puissance est de 0, -1 par médaille perdante, +1 par médaille de victoire de l’age 1, +3 par médaille de victoire de l’age 3, et +5 par médaille de victoire de l’age 3:

@Test
  public void testCalculPuissanceMilitaire1() {
    assertEquals(0, partieManager.calculePuissanceMilitaire(partie, joueurA));
  }
  
  @Test
  public void testCalculPuissanceMilitaire2() {
    joueurA.ajouteMedaille(Medaille.Defaite);
    assertEquals(-1, partieManager.calculePuissanceMilitaire(partie, joueurA));
  }

  @Test
  public void testCalculPuissanceMilitaire3() {
    joueurA.ajouteMedaille(Medaille.VictoireII);
    joueurA.ajouteMedaille(Medaille.VictoireIII);
    assertEquals(8, partieManager.calculePuissanceMilitaire(partie, joueurA));
  }

  @Test
  public void testCalculPuissanceMilitaire() {
    Joueur mockedJoueur = Mockito.mock(Joueur.class);
    Merveille mockedMerveille = Mockito.mock(Merveille.class);
    mockedJoueur.ajouteMedaille(Medaille.Defaite);
    Mockito.when(mockedMerveille.getNomEn()).thenReturn("Rhodes Colossus");
    Mockito.when(mockedJoueur.getMerveille()).thenReturn(mockedMerveille);
    Mockito.when(mockedJoueur.getEtageMerveille()).thenReturn(2);
    assertEquals(2, partieManager.calculePuissanceMilitaire(partie, mockedJoueur));
  }

Plus besoin de créer le joueur et la merveille, les mocks simples suffisent pour ce test.