40K, ce jeu où il y a trop de règles
Revenant enfin sur le métier, dans le calcul (projet calcul, il était temps), j'ai essayé de décomposer les étapes (d'abord déterminer le jet pour toucher, pour blesser ...).
Mais je suis arrivé à un problème: il faut vérifier que celui qui utilise l'API ne donne pas en entrée de la merde.
Par exemple, que l'on ne simule pas un phase de tir en mêlée alors que l'on est avec une arme à tir rapide (encore un problème, le Dark Angel peut en mêlée avec une arme à tir rapide ou une arme d'assaut en doctrine tactique)
Et mine de rien, il y a plein de choses à valider

.
Du coup, j'ai re-décomposé et je me suis occupé que de la validation.
Pour commencé, comme Java est un bon langage, j'ai commencé par définir de annotations pour savoir quoi valider lors des différents calculs:
Code:
package com.calculateur.warhammer.calcul.mort.validation.annotations;
/**
* Validation pour un corps à corps
* @author phili
*
*/
public @interface ValidationCorpsACorps {
}
Code:
package com.calculateur.warhammer.calcul.mort.validation.annotations;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Si validation à la phase de tir
* @author phili
*
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidationPhaseTir {
}
Code:
package com.calculateur.warhammer.calcul.mort.validation.annotations;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Valmidation pour un tir de contre charge
* @author phili
*
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidationTirContreCharge {
}
Ce qui permettra de fabriquer une factory (Design Pattern du GOFF).
Du coup, il n'y a plus qu'à valider:
Code:
package com.calculateur.warhammer.calcul.mort.validation;
import com.calculateur.warhammer.calcul.mort.validation.exception.ValidationCalculException;
import com.calculateur.warhammer.data.action.IContexteAction;
import com.calculateur.warhammer.data.identifiable.IArme;
import com.calculateur.warhammer.data.unite.IConstituantAttaquant;
import com.calculateur.warhammer.data.unite.IConstituantDefenseur;
/**
* Validateur pour savoir si l'action calculé ou simulée est possible
* @author phili
*
* @param <A> Arme utilisée
*/
public interface IValidationCalcul <A extends IArme>{
/**
* Valide l'action simulée ou calculée
* @param attaquant Attaquant effectuant l'attaque
* @param defenseur Défenseur effectuant l'attaque
* @param contexteAction Contexte de l'action
* @throws ValidationCalculException Si l'action n'est pas possible
*/
void valideCalcul(IConstituantAttaquant<A> attaquant,IConstituantDefenseur defenseur,IContexteAction<A> contexteAction)throws ValidationCalculException;
}
Avec un utilisateur qui verra un truc traduit:
Code:
package com.calculateur.warhammer.calcul.mort.validation.exception;
import java.text.MessageFormat;
import java.util.Locale;
import java.util.ResourceBundle;
import com.calculateur.warhammer.base.traduction.ITraduction;
/**
* Exception lancée lorsque le calcul n'est pas possible
* @author phili
*
*/
@SuppressWarnings("all")
public class ValidationCalculException extends Exception implements ITraduction{
/**
*
*/
private static final long serialVersionUID = 1L;
private final String resources;
private final Object[] objets;
private final String key;
private String message;
public ValidationCalculException(String resources, Object[] objets, String key) {
this.resources = resources;
this.objets = objets;
this.key = key;
}
@Override
public ResourceBundle getResourceBundle(Locale locale) {
return ResourceBundle.getBundle(resources, locale);
}
@Override
public void traduireLibelles(Locale locale) {
String messageToFormat = getResourceBundle(locale).getString(key);
message = (objets != null)? MessageFormat.format(messageToFormat, objets):messageToFormat;
}
@Override
public String getMessage() {
return message != null?message:key;
}
public Object[] getObjets() {
return objets;
}
}
Et un test de base (toujours faire un TU):
Code:
package com.calculateur.warhammer.calcul.mort.validation;
import java.text.MessageFormat;
import java.util.Locale;
import java.util.ResourceBundle;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import com.calculateur.warhammer.calcul.mort.validation.exception.ValidationCalculException;
import com.calculateur.warhammer.data.action.IContexteAction;
import com.calculateur.warhammer.data.identifiable.IArme;
import com.calculateur.warhammer.data.identifiable.IProfil;
import com.calculateur.warhammer.data.regles.IRegleAttaque;
import com.calculateur.warhammer.data.unite.IConstituantAttaquant;
import com.calculateur.warhammer.data.unite.IConstituantDefenseur;
import com.calculateur.warhammer.data.unite.implementation.BasicConstituantUniteAttaquant;
@Execution(ExecutionMode.SAME_THREAD)
@ExtendWith(MockitoExtension.class)
public abstract class AbstractValidationTest <A extends IArme>{
private Locale[] LOCALES = {Locale.FRANCE,Locale.ENGLISH};
private IConstituantAttaquant<A> attaquant;
@Mock
protected IProfil profilAttaquant;
@Mock
protected IRegleAttaque regleAttaquant;
@Mock
protected IContexteAction<A> contextAction;
private final IValidationCalcul<A> validator;
protected AbstractValidationTest(IValidationCalcul<A> validator) {
this.validator = validator;
}
@BeforeEach
public void beforeEach() {
attaquant = new BasicConstituantUniteAttaquant<A>(profilAttaquant, null, null, getArme(), regleAttaquant);
}
protected abstract A getArme();
protected void testValidationStandard(boolean isExceptionAttendue) {
testValidationStandard(isExceptionAttendue, attaquant, contextAction);
}
protected void testValidationStandard(boolean isExceptionAttendue,IConstituantAttaquant<A> aAttaquant,IContexteAction<A> aContext) {
testValidationStandard(isExceptionAttendue, aAttaquant, null,contextAction);
}
protected void testValidationStandard(boolean isExceptionAttendue,IConstituantAttaquant<A> aAttaquant,IConstituantDefenseur defenseur,IContexteAction<A> aContext) {
try {
validator.valideCalcul(aAttaquant,defenseur, aContext);
Assertions.assertThat(isExceptionAttendue).isFalse();
}catch(ValidationCalculException ex) {
Assertions.assertThat(isExceptionAttendue).isTrue();
valideException(ex);
}
}
private void valideException(ValidationCalculException ex) {
String key = ex.getMessage();
Object[] params = ex.getObjets();
Assertions.assertThat(key).isNotNull();
ResourceBundle res;
for(Locale locale:LOCALES) {
res = ex.getResourceBundle(locale);
ex.traduireLibelles(locale);
Assertions.assertThat(ex.getMessage()).isEqualTo(MessageFormat.format(res.getString(key), params));
}
}
}
Soit pour savoir si on tir en mêlée:
Code:
package com.calculateur.warhammer.calcul.mort.validation;
import java.util.Set;
import com.calculateur.warhammer.calcul.mort.validation.annotations.ValidationPhaseTir;
import com.calculateur.warhammer.calcul.mort.validation.exception.ValidationCalculException;
import com.calculateur.warhammer.data.action.IContexteAction;
import com.calculateur.warhammer.data.enumeration.EProfil;
import com.calculateur.warhammer.data.identifiable.IArmeDeTir;
import com.calculateur.warhammer.data.unite.IConstituantAttaquant;
import com.calculateur.warhammer.data.unite.IConstituantDefenseur;
/**
* Validation si on peut tirer en mêlée
* @author phili
*
*/
@ValidationPhaseTir
public class ValidationPhaseTirMelee implements IValidationCalcul<IArmeDeTir>{
@Override
public void valideCalcul(IConstituantAttaquant<IArmeDeTir> attaquant,IConstituantDefenseur defenseur, IContexteAction<IArmeDeTir> contexteAction)
throws ValidationCalculException {
if(contexteAction.isZoneEngagement() &&
!isVehiculeOuMonstre(attaquant.getProfil().getTypesProfils()) &&
!attaquant.getRegles().isPeutUtiliserArmePorteeEngagement(attaquant.getArme().getType())) {
Object[] params = {attaquant.getArme().getType()};
throw new ValidationCalculException(ValidationCalculUtil.RESOURCE_ERREUR_PHASE_TIR,params, "erreur.tir.melee");
}
}
private boolean isVehiculeOuMonstre(Set<EProfil> typesProfils) {
return typesProfils.contains(EProfil.VEHICULE) || typesProfils.contains(EProfil.MONSTRE);
}
}
Ou si on peut charger après une retraite:
Code:
package com.calculateur.warhammer.calcul.mort.validation;
import com.calculateur.warhammer.calcul.mort.validation.annotations.ValidationCorpsACorps;
import com.calculateur.warhammer.calcul.mort.validation.exception.ValidationCalculException;
import com.calculateur.warhammer.data.action.IContexteAction;
import com.calculateur.warhammer.data.enumeration.ESimule;
import com.calculateur.warhammer.data.identifiable.IArme;
import com.calculateur.warhammer.data.unite.IConstituantAttaquant;
import com.calculateur.warhammer.data.unite.IConstituantDefenseur;
/**
* Valida si la charge est possible
* @author phili
*
*/
@ValidationCorpsACorps
public class ValidationChargePossible implements IValidationCalcul<IArme>{
@Override
public void valideCalcul(IConstituantAttaquant<IArme> attaquant,IConstituantDefenseur defenseur, IContexteAction<IArme> contexteAction)
throws ValidationCalculException {
if(contexteAction.getSimule() == ESimule.CORPS_A_CORPS_APRES_CHARGE && !attaquant.getRegles().isChargeApresTypeMouvement(contexteAction.getMouvementAttaquant())) {
Object[] param = {contexteAction.getMouvementAttaquant()};
throw new ValidationCalculException(ValidationCalculUtil.RESOURCE_ERREUR_CORPS_A_CORPS, param, "erreur.charge.impossible");
}
}
}
Evidement, tout ça est testé (avec d'autres validateurs) et les TU ont été écrit avant selon le principe du TDD.